Skip to content

Commit e19ffab

Browse files
committed
Fix worktree deletion caused by stale-read race in removeFolder
1 parent f6893e4 commit e19ffab

File tree

3 files changed

+54
-31
lines changed

3 files changed

+54
-31
lines changed

apps/twig/src/main/services/folders/service.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,17 @@ export class FoldersService {
101101
}
102102

103103
async removeFolder(folderId: string): Promise<void> {
104-
const folders = foldersStore.get("folders", []);
105-
const folder = folders.find((f) => f.id === folderId);
106-
const associations = foldersStore.get("taskAssociations", []);
104+
const folder = foldersStore
105+
.get("folders", [])
106+
.find((f) => f.id === folderId);
107+
const worktreeAssocs = foldersStore
108+
.get("taskAssociations", [])
109+
.filter(
110+
(a) =>
111+
a.folderId === folderId && a.mode === "worktree" && "worktree" in a,
112+
);
107113

108-
const associationsToRemove = associations.filter(
109-
(a) => a.folderId === folderId,
110-
);
111-
for (const assoc of associationsToRemove) {
114+
for (const assoc of worktreeAssocs) {
112115
if (assoc.mode === "worktree" && folder) {
113116
const worktreeBasePath = getWorktreeLocation();
114117
const worktreePath = path.join(
@@ -128,13 +131,17 @@ export class FoldersService {
128131
}
129132
}
130133

131-
const filtered = folders.filter((f) => f.id !== folderId);
132-
const filteredAssociations = associations.filter(
133-
(a) => a.folderId !== folderId,
134-
);
134+
const currentFolders = foldersStore.get("folders", []);
135+
const currentAssociations = foldersStore.get("taskAssociations", []);
135136

136-
foldersStore.set("folders", filtered);
137-
foldersStore.set("taskAssociations", filteredAssociations);
137+
foldersStore.set(
138+
"folders",
139+
currentFolders.filter((f) => f.id !== folderId),
140+
);
141+
foldersStore.set(
142+
"taskAssociations",
143+
currentAssociations.filter((a) => a.folderId !== folderId),
144+
);
138145
log.debug(`Removed folder with ID: ${folderId}`);
139146
}
140147

apps/twig/src/main/utils/store.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -172,22 +172,32 @@ function getWorktreePath(folderPath: string, worktreeName: string): string {
172172

173173
export async function clearAllStoreData(): Promise<void> {
174174
const associations = foldersStore.get("taskAssociations", []);
175+
const worktreesToDelete: Array<{
176+
worktreePath: string;
177+
mainRepoPath: string;
178+
}> = [];
179+
175180
for (const assoc of associations) {
176181
if (assoc.mode === "worktree") {
177182
const folderPath = getFolderPath(assoc.folderId);
178183
if (!folderPath) continue;
184+
worktreesToDelete.push({
185+
worktreePath: getWorktreePath(folderPath, assoc.worktree),
186+
mainRepoPath: folderPath,
187+
});
188+
}
189+
}
179190

180-
const worktreePath = getWorktreePath(folderPath, assoc.worktree);
181-
try {
182-
const worktreeBasePath = getWorktreeLocation();
183-
const manager = new WorktreeManager({
184-
mainRepoPath: folderPath,
185-
worktreeBasePath,
186-
});
187-
await manager.deleteWorktree(worktreePath);
188-
} catch (error) {
189-
log.error(`Failed to delete worktree ${worktreePath}:`, error);
190-
}
191+
for (const { worktreePath, mainRepoPath } of worktreesToDelete) {
192+
try {
193+
const worktreeBasePath = getWorktreeLocation();
194+
const manager = new WorktreeManager({
195+
mainRepoPath,
196+
worktreeBasePath,
197+
});
198+
await manager.deleteWorktree(worktreePath);
199+
} catch (error) {
200+
log.error(`Failed to delete worktree ${worktreePath}:`, error);
191201
}
192202
}
193203

apps/twig/src/renderer/stores/registeredFoldersStore.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,20 @@ export const useRegisteredFoldersStore = create<RegisteredFoldersState>()(
4545
try {
4646
const loadedFolders = await loadFolders();
4747

48-
// Remove folders that no longer exist on disk
4948
const deletedFolders = loadedFolders.filter((f) => f.exists === false);
50-
for (const folder of deletedFolders) {
51-
trpcVanilla.folders.removeFolder
52-
.mutate({ folderId: folder.id })
53-
.catch((err) =>
54-
log.error(`Failed to remove deleted folder ${folder.path}:`, err),
55-
);
49+
if (deletedFolders.length > 0) {
50+
await Promise.all(
51+
deletedFolders.map((folder) =>
52+
trpcVanilla.folders.removeFolder
53+
.mutate({ folderId: folder.id })
54+
.catch((err) =>
55+
log.error(
56+
`Failed to remove deleted folder ${folder.path}:`,
57+
err,
58+
),
59+
),
60+
),
61+
);
5662
}
5763
const existingFolders = loadedFolders.filter((f) => f.exists !== false);
5864

0 commit comments

Comments
 (0)