Skip to content

Commit 5c43656

Browse files
committed
Refactor repo reload, diff generation, and branch update logic
1 parent 2364138 commit 5c43656

6 files changed

Lines changed: 377 additions & 226 deletions

File tree

src/extension.ts

Lines changed: 112 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
fetchDiff,
1010
isGitRepo,
1111
getCurrentBranch,
12+
hasGitChanges,
1213
} from "./git/git-logic";
1314
import { CstManager } from "./cst-manager";
1415

@@ -24,6 +25,7 @@ export function activate(context: vscode.ExtensionContext) {
2425
let repoWatcher: vscode.FileSystemWatcher | undefined;
2526
let nonRepoWatcher: vscode.FileSystemWatcher | undefined;
2627
let watchedRepoPath: string | undefined;
28+
let currentViewedBranch: string | undefined;
2729
let debounceTimer: NodeJS.Timeout | undefined;
2830
let nonRepoDebounceTimer: NodeJS.Timeout | undefined;
2931
let lastState:
@@ -44,15 +46,14 @@ export function activate(context: vscode.ExtensionContext) {
4446
if (e.exitCode === 0) {
4547
activePanel.webview.postMessage({
4648
command: "displayOutput",
47-
data: `Command finished successfully. Refreshing graph...`,
49+
data: `Command finished successfully.`,
4850
});
4951
} else {
5052
activePanel.webview.postMessage({
5153
command: "displayOutput",
5254
data: `Command failed with code ${e.exitCode}`,
5355
});
5456
}
55-
activePanel.webview.postMessage({ command: "refreshGraph" });
5657
activePanel.webview.postMessage({ command: "resetExecuting" });
5758
}
5859
}
@@ -274,11 +275,15 @@ export function activate(context: vscode.ExtensionContext) {
274275
repoPath: string,
275276
branch?: string,
276277
isManual = true,
278+
reloadReason?: "git" | "workdir",
277279
) {
278280
if (!repoPath) throw new Error("Please specify a directory path.");
279281

282+
const pathChanged = watchedRepoPath !== repoPath;
283+
currentViewedBranch = branch;
284+
280285
// Setup file system watchers if path changed
281-
if (watchedRepoPath !== repoPath) {
286+
if (pathChanged) {
282287
if (repoWatcher) {
283288
repoWatcher.dispose();
284289
repoWatcher = undefined;
@@ -295,94 +300,6 @@ export function activate(context: vscode.ExtensionContext) {
295300
clearTimeout(nonRepoDebounceTimer);
296301
nonRepoDebounceTimer = undefined;
297302
}
298-
299-
// Watch for any changes in the .git directory (to trigger an automatic refresh)
300-
const gitPattern = new vscode.RelativePattern(repoPath, ".git/**");
301-
repoWatcher = vscode.workspace.createFileSystemWatcher(gitPattern);
302-
303-
const refresh = () => {
304-
if (debounceTimer) {
305-
clearTimeout(debounceTimer);
306-
}
307-
debounceTimer = setTimeout(() => {
308-
if (activePanel) {
309-
activePanel.webview.postMessage({ command: "refreshGraph" });
310-
}
311-
}, 1000); // 1 second debounce
312-
};
313-
314-
repoWatcher.onDidChange(refresh);
315-
repoWatcher.onDidCreate(refresh);
316-
repoWatcher.onDidDelete(refresh);
317-
318-
// Watch for changes outside of .git and show a small 'reload' notification
319-
// Only setup if not ignored by user
320-
const ignoreNotifications = context.globalState.get<boolean>(
321-
"codestory.ignoreFileChangeNotifications",
322-
false,
323-
);
324-
325-
if (!ignoreNotifications) {
326-
const allPattern = new vscode.RelativePattern(repoPath, "**");
327-
nonRepoWatcher =
328-
vscode.workspace.createFileSystemWatcher(allPattern);
329-
330-
const nonRepoRefresh = (uri?: vscode.Uri) => {
331-
const fsPath = uri?.fsPath || repoPath;
332-
// If the change is inside .git, ignore it here
333-
const rel = path.relative(repoPath, fsPath);
334-
if (rel.split(path.sep)[0] === ".git") return;
335-
336-
if (nonRepoDebounceTimer) {
337-
clearTimeout(nonRepoDebounceTimer);
338-
}
339-
340-
nonRepoDebounceTimer = setTimeout(async () => {
341-
// Only show notification when the panel is open
342-
if (!activePanel) return;
343-
344-
// Re-check in case it was changed while debouncing
345-
if (
346-
context.globalState.get<boolean>(
347-
"codestory.ignoreFileChangeNotifications",
348-
false,
349-
)
350-
) {
351-
return;
352-
}
353-
354-
const selection = await vscode.window.showInformationMessage(
355-
"File changes detected outside .git. Reload?",
356-
"Reload",
357-
"Don't show again",
358-
);
359-
360-
if (selection === "Reload") {
361-
// Trigger a manual reload just like clicking the branch reload button
362-
// Use last known branch if available
363-
const branchToUse =
364-
lastState?.currentBranch || branch || "HEAD";
365-
await handleLoadRepo(panel, repoPath, branchToUse, true);
366-
} else if (selection === "Don't show again") {
367-
await context.globalState.update(
368-
"codestory.ignoreFileChangeNotifications",
369-
true,
370-
);
371-
// Dispose watcher since we won't need it anymore
372-
if (nonRepoWatcher) {
373-
nonRepoWatcher.dispose();
374-
nonRepoWatcher = undefined;
375-
}
376-
}
377-
}, 1000);
378-
};
379-
380-
nonRepoWatcher.onDidChange(nonRepoRefresh);
381-
nonRepoWatcher.onDidCreate(nonRepoRefresh);
382-
nonRepoWatcher.onDidDelete(nonRepoRefresh);
383-
}
384-
385-
watchedRepoPath = repoPath;
386303
lastState = undefined;
387304
}
388305

@@ -420,6 +337,16 @@ export function activate(context: vscode.ExtensionContext) {
420337

421338
lastState = newState;
422339

340+
if (reloadReason === "git") {
341+
vscode.window.showInformationMessage(
342+
"Repo reloaded because of a .git change",
343+
);
344+
} else if (reloadReason === "workdir") {
345+
vscode.window.showInformationMessage(
346+
"Change in working dir, reload.",
347+
);
348+
}
349+
423350
panel.webview.postMessage({
424351
command: "displayOutput",
425352
data: `Loading repository: ${repoPath} (branch: ${branch || "HEAD"})`,
@@ -443,6 +370,85 @@ export function activate(context: vscode.ExtensionContext) {
443370
command: "displayOutput",
444371
data: `Successfully loaded ${commits.length} commits.`,
445372
});
373+
374+
if (pathChanged) {
375+
// Watch for any changes in the .git directory (to trigger an automatic refresh)
376+
const gitPattern = new vscode.RelativePattern(repoPath, ".git/**");
377+
repoWatcher = vscode.workspace.createFileSystemWatcher(gitPattern);
378+
379+
const handleFileChange = async (isGitChange = false) => {
380+
if (!activePanel) return;
381+
382+
if (isGitChange) {
383+
// if git changes, always reload
384+
vscode.window.setStatusBarMessage(
385+
"Repo State Changed, Reloading...",
386+
3000,
387+
);
388+
const branchToUse =
389+
currentViewedBranch || lastState?.currentBranch || "HEAD";
390+
await handleLoadRepo(panel, repoPath, branchToUse, false, "git");
391+
} else {
392+
const currentHasChanges = await hasGitChanges(repoPath);
393+
const previousHasChanges =
394+
lastState?.commits.some((c) => c.id === "WORKING_DIR") || false;
395+
if (currentHasChanges !== previousHasChanges) {
396+
// Status-bar and an informational popup for working-directory changes
397+
vscode.window.setStatusBarMessage(
398+
"Working Directory State Changed, Reloading...",
399+
3000,
400+
);
401+
402+
const branchToUse =
403+
currentViewedBranch || lastState?.currentBranch || "HEAD";
404+
await handleLoadRepo(
405+
panel,
406+
repoPath,
407+
branchToUse,
408+
false,
409+
"workdir",
410+
);
411+
}
412+
}
413+
};
414+
415+
const refresh = () => {
416+
if (debounceTimer) {
417+
clearTimeout(debounceTimer);
418+
}
419+
debounceTimer = setTimeout(() => handleFileChange(true), 1000);
420+
};
421+
422+
repoWatcher.onDidChange(refresh);
423+
repoWatcher.onDidCreate(refresh);
424+
repoWatcher.onDidDelete(refresh);
425+
426+
// Watch for changes outside of .git
427+
const allPattern = new vscode.RelativePattern(repoPath, "**");
428+
nonRepoWatcher = vscode.workspace.createFileSystemWatcher(allPattern);
429+
430+
const nonRepoRefresh = (uri?: vscode.Uri) => {
431+
const fsPath = uri?.fsPath || repoPath;
432+
// If the change is inside .git, ignore it here
433+
const rel = path.relative(repoPath, fsPath);
434+
if (rel.split(path.sep)[0] === ".git") return;
435+
436+
if (nonRepoDebounceTimer) {
437+
clearTimeout(nonRepoDebounceTimer);
438+
}
439+
440+
nonRepoDebounceTimer = setTimeout(
441+
() => handleFileChange(false),
442+
1000,
443+
);
444+
};
445+
446+
nonRepoWatcher.onDidChange(nonRepoRefresh);
447+
nonRepoWatcher.onDidCreate(nonRepoRefresh);
448+
nonRepoWatcher.onDidDelete(nonRepoRefresh);
449+
450+
watchedRepoPath = repoPath;
451+
}
446452
}
447453

448454
panel.webview.onDidReceiveMessage(
@@ -563,22 +569,24 @@ export function activate(context: vscode.ExtensionContext) {
563569
console.error("Failed to parse global config", e);
564570
}
565571
}
566-
const ignoreBranchPrompt = context.globalState.get<boolean>(
567-
"codestory.ignoreBranchPrompt",
568-
false,
569-
);
572+
const branchUpdateStrategy = vscode.workspace
573+
.getConfiguration("codestoryView")
574+
.get<string>("branchUpdateStrategy", "prompt");
570575
panel.webview.postMessage({
571576
command: "globalConfig",
572577
config,
573-
ignoreBranchPrompt,
578+
branchUpdateStrategy,
574579
});
575580
return;
576581

577-
case "setIgnoreBranchPrompt":
578-
await context.globalState.update(
579-
"codestory.ignoreBranchPrompt",
580-
message.value,
581-
);
582+
case "setBranchUpdateStrategy":
583+
await vscode.workspace
584+
.getConfiguration("codestoryView")
585+
.update(
586+
"branchUpdateStrategy",
587+
message.value,
588+
vscode.ConfigurationTarget.Global,
589+
);
582590
return;
583591

584592
case "setGlobalConfig":
@@ -671,19 +679,9 @@ async function runCstTool(
671679
(err as any).code === "ENOENT" ||
672680
stderr.includes("not recognized")
673681
) {
674-
vscode.window
675-
.showErrorMessage(
676-
`Could not find 'cst' executable. Is it installed?`,
677-
"Open Settings",
678-
)
679-
.then((selection) => {
680-
if (selection === "Open Settings") {
681-
vscode.commands.executeCommand(
682-
"workbench.action.openSettings",
683-
"codestoryView.executablePath",
684-
);
685-
}
686-
});
682+
vscode.window.showErrorMessage(
683+
`Could not find 'cst' executable. Is it installed?`,
684+
);
687685
reject(new Error("Codestory not found"));
688686
return;
689687
}

src/git/git-logic.ts

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ export async function isGitRepo(repoPath: string): Promise<boolean> {
7373
}
7474
}
7575

76+
export async function hasGitChanges(repoPath: string): Promise<boolean> {
77+
try {
78+
const status = await runGit(repoPath, ["status", "--porcelain"], {
79+
allowErrors: true,
80+
});
81+
return status.trim().length > 0;
82+
} catch {
83+
return false;
84+
}
85+
}
86+
7687
export async function fetchCommits(
7788
repoPath: string,
7889
branch: string = "HEAD",
@@ -268,39 +279,26 @@ export async function fetchDiff(
268279
.filter((s) => s.length > 0);
269280

270281
if (files.length > 0) {
271-
// Create a temporary empty file
272-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codestory-"));
273-
const emptyPath = path.join(tmpDir, "empty");
274-
fs.writeFileSync(emptyPath, "");
275-
try {
276-
for (const f of files) {
277-
const perFile = await runGit(
278-
repoPath,
279-
[
280-
"diff",
281-
"--no-index",
282-
"--no-color",
283-
"--no-ext-diff",
284-
"--",
285-
emptyPath,
286-
f,
287-
],
288-
{ allowErrors: true },
289-
);
290-
if (perFile && perFile.trim().length > 0) {
291-
if (diffText && !diffText.endsWith("\n")) {
292-
diffText += "\n";
293-
}
294-
diffText += perFile;
282+
for (const f of files) {
283+
const perFile = await runGit(
284+
repoPath,
285+
[
286+
"diff",
287+
"--no-index",
288+
"--no-color",
289+
"--no-ext-diff",
290+
"--",
291+
"/dev/null",
292+
f,
293+
],
294+
{ allowErrors: true },
295+
);
296+
if (perFile && perFile.trim().length > 0) {
297+
if (diffText && !diffText.endsWith("\n")) {
298+
diffText += "\n";
295299
}
300+
diffText += perFile;
296301
}
297-
} finally {
298-
try {
299-
fs.unlinkSync(emptyPath);
300-
} catch {}
301-
try {
302-
fs.rmdirSync(tmpDir);
303-
} catch {}
304302
}
305303
}
306304
} else {

0 commit comments

Comments
 (0)