From e74fd74a43cab50173399431e17937a9d703ae11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:24:13 +0000 Subject: [PATCH 01/13] Initial plan From 484df711e5f5b7163eb1bc68c2aeab68216c1ae9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:30:20 +0000 Subject: [PATCH 02/13] Implement checkbox reset when file status changes Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 814a6bd..7372a8d 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -503,6 +503,23 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos await this.updateRefs(); } + // Build a map of old file statuses to detect changes + const oldFileStatuses = new Map(); + if (this.filesInsideTreeRoot) { + for (const files of this.filesInsideTreeRoot.values()) { + for (const file of files) { + oldFileStatuses.set(file.dstAbsPath, file.status); + } + } + } + if (this.filesOutsideTreeRoot) { + for (const files of this.filesOutsideTreeRoot.values()) { + for (const file of files) { + oldFileStatuses.set(file.dstAbsPath, file.status); + } + } + } + const filesInsideTreeRoot = new Map(); const filesOutsideTreeRoot = new Map(); @@ -534,6 +551,23 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos entries.push(entry); } + // Reset checkbox state for files whose status has changed + for (const entry of diff) { + const oldStatus = oldFileStatuses.get(entry.dstAbsPath); + if (oldStatus !== undefined && oldStatus !== entry.status) { + // File status changed, reset checkbox state + this.checkboxStates.delete(entry.dstAbsPath); + } + } + + // Also clear checkbox state for files that no longer exist in the diff + const newFilePaths = new Set(diff.map(f => f.dstAbsPath)); + for (const [filePath] of this.checkboxStates) { + if (!newFilePaths.has(filePath)) { + this.checkboxStates.delete(filePath); + } + } + let treeHasChanged = false; if (fireChangeEvents) { const hasChanged = (folderPath: string, insideTreeRoot: boolean) => { From e641d1d4a3d6462cf43dfa12181cb78b85b7fee1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:31:36 +0000 Subject: [PATCH 03/13] Optimize checkbox reset logic to avoid extra iteration Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 7372a8d..e6c4406 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -527,6 +527,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos const untrackedCount = diff.reduce((prev, cur, _) => prev + (cur.status === 'U' ? 1 : 0), 0); this.log(`${diff.length} diff entries (${untrackedCount} untracked)`); + const newFilePaths = new Set(); for (const entry of diff) { const folder = path.dirname(entry.dstAbsPath); @@ -549,10 +550,9 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos const entries = files.get(folder)!; entries.push(entry); - } - // Reset checkbox state for files whose status has changed - for (const entry of diff) { + // Track new file paths and reset checkbox if status changed + newFilePaths.add(entry.dstAbsPath); const oldStatus = oldFileStatuses.get(entry.dstAbsPath); if (oldStatus !== undefined && oldStatus !== entry.status) { // File status changed, reset checkbox state @@ -560,8 +560,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos } } - // Also clear checkbox state for files that no longer exist in the diff - const newFilePaths = new Set(diff.map(f => f.dstAbsPath)); + // Clear checkbox state for files that no longer exist in the diff for (const [filePath] of this.checkboxStates) { if (!newFilePaths.has(filePath)) { this.checkboxStates.delete(filePath); From df7cb21eef8a1b70ab94cc46a236d6e0ecc0f8f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:32:25 +0000 Subject: [PATCH 04/13] Fix Map iteration safety issue Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index e6c4406..79c1016 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -561,11 +561,15 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos } // Clear checkbox state for files that no longer exist in the diff + const pathsToDelete: string[] = []; for (const [filePath] of this.checkboxStates) { if (!newFilePaths.has(filePath)) { - this.checkboxStates.delete(filePath); + pathsToDelete.push(filePath); } } + for (const filePath of pathsToDelete) { + this.checkboxStates.delete(filePath); + } let treeHasChanged = false; if (fireChangeEvents) { From 994b18c0d9127a4f26e84b1e72c3ac22a20bb769 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:48:05 +0000 Subject: [PATCH 05/13] Implement checkbox reset based on file modification time Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- package.json | 5 ++++ src/treeProvider.ts | 71 +++++++++++++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index ac16d97..a352ab1 100644 --- a/package.json +++ b/package.json @@ -415,6 +415,11 @@ "type": "boolean", "description": "Whether to show checkboxes such that files or folders can be ticked off, for example when reviewing.", "default": false + }, + "gitTreeCompare.resetCheckboxOnFileChange": { + "type": "boolean", + "description": "When enabled, automatically resets checkboxes when a file is modified after being checked. This ensures that checked files reflect their reviewed state, and any subsequent modifications require re-review.", + "default": false } } } diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 79c1016..9b822ab 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -17,6 +17,11 @@ import { debounce, throttle } from './git/decorators' import { normalizePath } from './fsUtils'; import { API as GitAPI, Repository as GitAPIRepository } from './typings/git'; +interface CheckboxStateInfo { + state: TreeItemCheckboxState; + timestamp: number; // When the checkbox was checked +} + class FileElement implements IDiffStatus { constructor( public srcAbsPath: string, @@ -104,6 +109,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos private showCollapsed: boolean; private compactFolders: boolean; private showCheckboxes: boolean; + private resetCheckboxOnFileChange: boolean; // Dynamic options private repository: Repository | undefined; @@ -133,7 +139,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos // UI state private treeView: TreeView; private isPaused: boolean; - private checkboxStates: Map = new Map(); + private checkboxStates: Map = new Map(); // Other private readonly disposables: Disposable[] = []; @@ -307,7 +313,10 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos private async handleChangeCheckboxState(e: TreeCheckboxChangeEvent) { for (let [element, state] of e.items) { if (element instanceof FileElement || element instanceof FolderElement) { - this.checkboxStates.set(element.dstAbsPath, state); + this.checkboxStates.set(element.dstAbsPath, { + state: state, + timestamp: Date.now() + }); } } } @@ -344,6 +353,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.showCollapsed = config.get('collapsed', false); this.compactFolders = config.get('compactFolders', false); this.showCheckboxes = config.get('showCheckboxes', false); + this.resetCheckboxOnFileChange = config.get('resetCheckboxOnFileChange', false); } private async getStoredBaseRef(): Promise { @@ -382,7 +392,8 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos let checkboxState: TreeItemCheckboxState | undefined; if (this.showCheckboxes) { if (element instanceof FileElement || element instanceof FolderElement) { - checkboxState = this.checkboxStates.get(element.dstAbsPath) ?? TreeItemCheckboxState.Unchecked; + const stateInfo = this.checkboxStates.get(element.dstAbsPath); + checkboxState = stateInfo?.state ?? TreeItemCheckboxState.Unchecked; } } return toTreeItem(element, this.openChangesOnSelect, this.iconsMinimal, this.showCollapsed, this.viewAsList, checkboxState, this.asAbsolutePath); @@ -503,19 +514,21 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos await this.updateRefs(); } - // Build a map of old file statuses to detect changes + // Build a map of old file statuses to detect changes (for backwards compatibility when feature is disabled) const oldFileStatuses = new Map(); - if (this.filesInsideTreeRoot) { - for (const files of this.filesInsideTreeRoot.values()) { - for (const file of files) { - oldFileStatuses.set(file.dstAbsPath, file.status); + if (!this.resetCheckboxOnFileChange) { + if (this.filesInsideTreeRoot) { + for (const files of this.filesInsideTreeRoot.values()) { + for (const file of files) { + oldFileStatuses.set(file.dstAbsPath, file.status); + } } } - } - if (this.filesOutsideTreeRoot) { - for (const files of this.filesOutsideTreeRoot.values()) { - for (const file of files) { - oldFileStatuses.set(file.dstAbsPath, file.status); + if (this.filesOutsideTreeRoot) { + for (const files of this.filesOutsideTreeRoot.values()) { + for (const file of files) { + oldFileStatuses.set(file.dstAbsPath, file.status); + } } } } @@ -551,12 +564,34 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos const entries = files.get(folder)!; entries.push(entry); - // Track new file paths and reset checkbox if status changed + // Track new file paths newFilePaths.add(entry.dstAbsPath); - const oldStatus = oldFileStatuses.get(entry.dstAbsPath); - if (oldStatus !== undefined && oldStatus !== entry.status) { - // File status changed, reset checkbox state - this.checkboxStates.delete(entry.dstAbsPath); + + // Reset checkbox based on selected mode + if (this.resetCheckboxOnFileChange) { + // New behavior: Reset checkbox if file was modified after checkbox was checked + const stateInfo = this.checkboxStates.get(entry.dstAbsPath); + if (stateInfo && stateInfo.state === TreeItemCheckboxState.Checked) { + try { + // Get file modification time + const stats = fs.statSync(entry.dstAbsPath); + const fileMtime = stats.mtimeMs; + + // If file was modified after checkbox was checked, reset it + if (fileMtime > stateInfo.timestamp) { + this.checkboxStates.delete(entry.dstAbsPath); + } + } catch (error) { + // If we can't read the file, just ignore (might be deleted) + } + } + } else { + // Old behavior: Reset checkbox if git status changed + const oldStatus = oldFileStatuses.get(entry.dstAbsPath); + if (oldStatus !== undefined && oldStatus !== entry.status) { + // File status changed, reset checkbox state + this.checkboxStates.delete(entry.dstAbsPath); + } } } From eba430da52dac601636db496a8a9ab011331b922 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:49:24 +0000 Subject: [PATCH 06/13] Optimize file stat calls to use async promises Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 9b822ab..6ac3b6c 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -541,6 +541,9 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.log(`${diff.length} diff entries (${untrackedCount} untracked)`); const newFilePaths = new Set(); + // Collect files that need mtime checking for async batch processing + const filesToCheckMtime: Array<{path: string, stateInfo: CheckboxStateInfo}> = []; + for (const entry of diff) { const folder = path.dirname(entry.dstAbsPath); @@ -572,18 +575,8 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos // New behavior: Reset checkbox if file was modified after checkbox was checked const stateInfo = this.checkboxStates.get(entry.dstAbsPath); if (stateInfo && stateInfo.state === TreeItemCheckboxState.Checked) { - try { - // Get file modification time - const stats = fs.statSync(entry.dstAbsPath); - const fileMtime = stats.mtimeMs; - - // If file was modified after checkbox was checked, reset it - if (fileMtime > stateInfo.timestamp) { - this.checkboxStates.delete(entry.dstAbsPath); - } - } catch (error) { - // If we can't read the file, just ignore (might be deleted) - } + // Collect files to check asynchronously + filesToCheckMtime.push({path: entry.dstAbsPath, stateInfo}); } } else { // Old behavior: Reset checkbox if git status changed @@ -595,6 +588,31 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos } } + // Check file modification times asynchronously in parallel + if (this.resetCheckboxOnFileChange && filesToCheckMtime.length > 0) { + const statPromises = filesToCheckMtime.map(async ({path, stateInfo}) => { + try { + const stats = await fs.promises.stat(path); + const fileMtime = stats.mtimeMs; + + // If file was modified after checkbox was checked, reset it + if (fileMtime > stateInfo.timestamp) { + return path; + } + } catch (error) { + // If we can't read the file, just ignore (might be deleted) + } + return null; + }); + + const pathsToReset = await Promise.all(statPromises); + for (const path of pathsToReset) { + if (path !== null) { + this.checkboxStates.delete(path); + } + } + } + // Clear checkbox state for files that no longer exist in the diff const pathsToDelete: string[] = []; for (const [filePath] of this.checkboxStates) { From 52697f2e39a87dfb478005e223a9169aff3392f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:50:25 +0000 Subject: [PATCH 07/13] Add error logging and improve filtering efficiency Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 6ac3b6c..e81fcbe 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -599,18 +599,17 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos if (fileMtime > stateInfo.timestamp) { return path; } - } catch (error) { - // If we can't read the file, just ignore (might be deleted) + } catch (error: any) { + // File might be deleted or inaccessible - this is expected in some cases + this.log(`Could not stat file for checkbox reset check: ${path}: ${error.message}`); } return null; }); const pathsToReset = await Promise.all(statPromises); - for (const path of pathsToReset) { - if (path !== null) { - this.checkboxStates.delete(path); - } - } + pathsToReset + .filter((path): path is string => path !== null) + .forEach(path => this.checkboxStates.delete(path)); } // Clear checkbox state for files that no longer exist in the diff From cd5bf2702e047a4ad8db3dfb40cd9e9b6354e54e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:51:21 +0000 Subject: [PATCH 08/13] Use proper TypeScript error type Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index e81fcbe..8c19723 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -599,9 +599,10 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos if (fileMtime > stateInfo.timestamp) { return path; } - } catch (error: any) { + } catch (error: unknown) { // File might be deleted or inaccessible - this is expected in some cases - this.log(`Could not stat file for checkbox reset check: ${path}: ${error.message}`); + const errorMessage = error instanceof Error ? error.message : String(error); + this.log(`Could not stat file for checkbox reset check: ${path}: ${errorMessage}`); } return null; }); From f2749e7a0c907adee28574d5fc22b08b812a2207 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:52:49 +0000 Subject: [PATCH 09/13] Fix variable shadowing of path module Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 8c19723..60478ca 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -542,7 +542,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos const newFilePaths = new Set(); // Collect files that need mtime checking for async batch processing - const filesToCheckMtime: Array<{path: string, stateInfo: CheckboxStateInfo}> = []; + const filesToCheckMtime: Array<{filePath: string, stateInfo: CheckboxStateInfo}> = []; for (const entry of diff) { const folder = path.dirname(entry.dstAbsPath); @@ -576,7 +576,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos const stateInfo = this.checkboxStates.get(entry.dstAbsPath); if (stateInfo && stateInfo.state === TreeItemCheckboxState.Checked) { // Collect files to check asynchronously - filesToCheckMtime.push({path: entry.dstAbsPath, stateInfo}); + filesToCheckMtime.push({filePath: entry.dstAbsPath, stateInfo}); } } else { // Old behavior: Reset checkbox if git status changed @@ -590,27 +590,27 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos // Check file modification times asynchronously in parallel if (this.resetCheckboxOnFileChange && filesToCheckMtime.length > 0) { - const statPromises = filesToCheckMtime.map(async ({path, stateInfo}) => { + const statPromises = filesToCheckMtime.map(async ({filePath, stateInfo}) => { try { - const stats = await fs.promises.stat(path); + const stats = await fs.promises.stat(filePath); const fileMtime = stats.mtimeMs; // If file was modified after checkbox was checked, reset it if (fileMtime > stateInfo.timestamp) { - return path; + return filePath; } } catch (error: unknown) { // File might be deleted or inaccessible - this is expected in some cases const errorMessage = error instanceof Error ? error.message : String(error); - this.log(`Could not stat file for checkbox reset check: ${path}: ${errorMessage}`); + this.log(`Could not stat file for checkbox reset check: ${filePath}: ${errorMessage}`); } return null; }); const pathsToReset = await Promise.all(statPromises); pathsToReset - .filter((path): path is string => path !== null) - .forEach(path => this.checkboxStates.delete(path)); + .filter((filePath): filePath is string => filePath !== null) + .forEach(filePath => this.checkboxStates.delete(filePath)); } // Clear checkbox state for files that no longer exist in the diff From cac7629bfe73565953f13403aacea6303b2c3eca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:02:40 +0000 Subject: [PATCH 10/13] Remove old status-based checkbox reset behavior Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 60478ca..1ae5127 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -514,25 +514,6 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos await this.updateRefs(); } - // Build a map of old file statuses to detect changes (for backwards compatibility when feature is disabled) - const oldFileStatuses = new Map(); - if (!this.resetCheckboxOnFileChange) { - if (this.filesInsideTreeRoot) { - for (const files of this.filesInsideTreeRoot.values()) { - for (const file of files) { - oldFileStatuses.set(file.dstAbsPath, file.status); - } - } - } - if (this.filesOutsideTreeRoot) { - for (const files of this.filesOutsideTreeRoot.values()) { - for (const file of files) { - oldFileStatuses.set(file.dstAbsPath, file.status); - } - } - } - } - const filesInsideTreeRoot = new Map(); const filesOutsideTreeRoot = new Map(); @@ -570,21 +551,13 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos // Track new file paths newFilePaths.add(entry.dstAbsPath); - // Reset checkbox based on selected mode + // Collect files that need mtime checking when feature is enabled if (this.resetCheckboxOnFileChange) { - // New behavior: Reset checkbox if file was modified after checkbox was checked const stateInfo = this.checkboxStates.get(entry.dstAbsPath); if (stateInfo && stateInfo.state === TreeItemCheckboxState.Checked) { // Collect files to check asynchronously filesToCheckMtime.push({filePath: entry.dstAbsPath, stateInfo}); } - } else { - // Old behavior: Reset checkbox if git status changed - const oldStatus = oldFileStatuses.get(entry.dstAbsPath); - if (oldStatus !== undefined && oldStatus !== entry.status) { - // File status changed, reset checkbox state - this.checkboxStates.delete(entry.dstAbsPath); - } } } From 2b83fbed13edbc3ed7cce5d7274491cc1bff8472 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:03:37 +0000 Subject: [PATCH 11/13] Improve comment clarity for checkbox reset logic Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- src/treeProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 1ae5127..ee29dff 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -551,7 +551,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos // Track new file paths newFilePaths.add(entry.dstAbsPath); - // Collect files that need mtime checking when feature is enabled + // Collect checked files for mtime checking to reset if modified after being checked if (this.resetCheckboxOnFileChange) { const stateInfo = this.checkboxStates.get(entry.dstAbsPath); if (stateInfo && stateInfo.state === TreeItemCheckboxState.Checked) { From c1ce15e4c230639ecd5b2e0b2db321c5eeea28f4 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Sat, 24 Jan 2026 16:39:29 +0000 Subject: [PATCH 12/13] fix implementation --- README.md | 4 ++++ src/treeProvider.ts | 46 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2c95cf1..c191358 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,7 @@ By default, the tree view is located in its own container accessible from the ac `gitTreeCompare.collapsed` When enabled, shows folders collapsed instead of expanded. NOTE: Changing this option requires restarting VS Code. `gitTreeCompare.compactFolders` When enabled, compacts (flattens) single-child folders into a single tree element. Useful for Java package structures, for example. May have a performance impact for large diff trees. + +`gitTreeCompare.showCheckboxes` When enabled, shows checkboxes next to files and folders, allowing you to tick off items, for example when reviewing changes. + +`gitTreeCompare.resetCheckboxOnFileChange` When enabled, automatically resets a file's checkbox when the file is modified after being checked. This ensures that checked files reflect their reviewed state, and any subsequent modifications require re-review. Only effective when `showCheckboxes` is enabled. diff --git a/src/treeProvider.ts b/src/treeProvider.ts index ee29dff..0994ace 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -391,14 +391,47 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos getTreeItem(element: Element): TreeItem { let checkboxState: TreeItemCheckboxState | undefined; if (this.showCheckboxes) { - if (element instanceof FileElement || element instanceof FolderElement) { + if (element instanceof FileElement) { const stateInfo = this.checkboxStates.get(element.dstAbsPath); checkboxState = stateInfo?.state ?? TreeItemCheckboxState.Unchecked; + } else if (element instanceof FolderElement) { + // Compute folder state from children: checked if all children are checked + checkboxState = this.computeFolderCheckboxState(element); } } return toTreeItem(element, this.openChangesOnSelect, this.iconsMinimal, this.showCollapsed, this.viewAsList, checkboxState, this.asAbsolutePath); } + private computeFolderCheckboxState(folder: FolderElement): TreeItemCheckboxState { + // Check if user explicitly set state on this folder + const explicitState = this.checkboxStates.get(folder.dstAbsPath); + if (explicitState) { + return explicitState.state; + } + + // Otherwise derive from files: folder is checked only if ALL files under it are checked + const files = folder.useFilesOutsideTreeRoot ? this.filesOutsideTreeRoot : this.filesInsideTreeRoot; + let hasFiles = false; + let allChecked = true; + + for (const [folderPath, fileEntries] of files.entries()) { + // Check if this folder is under the target folder + if (folderPath === folder.dstAbsPath || folderPath.startsWith(folder.dstAbsPath + path.sep)) { + for (const file of fileEntries) { + hasFiles = true; + const stateInfo = this.checkboxStates.get(file.dstAbsPath); + if (!stateInfo || stateInfo.state !== TreeItemCheckboxState.Checked) { + allChecked = false; + break; + } + } + if (!allChecked) break; + } + } + + return (hasFiles && allChecked) ? TreeItemCheckboxState.Checked : TreeItemCheckboxState.Unchecked; + } + async getChildren(element?: Element): Promise { if (!element) { if (!this.repository) { @@ -555,7 +588,6 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos if (this.resetCheckboxOnFileChange) { const stateInfo = this.checkboxStates.get(entry.dstAbsPath); if (stateInfo && stateInfo.state === TreeItemCheckboxState.Checked) { - // Collect files to check asynchronously filesToCheckMtime.push({filePath: entry.dstAbsPath, stateInfo}); } } @@ -581,9 +613,13 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos }); const pathsToReset = await Promise.all(statPromises); - pathsToReset - .filter((filePath): filePath is string => filePath !== null) - .forEach(filePath => this.checkboxStates.delete(filePath)); + const actualPathsToReset = pathsToReset.filter((filePath): filePath is string => filePath !== null); + actualPathsToReset.forEach(filePath => this.checkboxStates.delete(filePath)); + + // Fire tree refresh to update checkbox UI + if (actualPathsToReset.length > 0) { + this._onDidChangeTreeData.fire(); + } } // Clear checkbox state for files that no longer exist in the diff From b695ab8b3ae8fd763adec65b3cc8bba8d0bbfb79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:52:09 +0000 Subject: [PATCH 13/13] Merge master branch and resolve conflicts Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- README.md | 2 ++ package.json | 9 +++++++++ src/treeProvider.ts | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c191358..ef51dfd 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,5 @@ By default, the tree view is located in its own container accessible from the ac `gitTreeCompare.showCheckboxes` When enabled, shows checkboxes next to files and folders, allowing you to tick off items, for example when reviewing changes. `gitTreeCompare.resetCheckboxOnFileChange` When enabled, automatically resets a file's checkbox when the file is modified after being checked. This ensures that checked files reflect their reviewed state, and any subsequent modifications require re-review. Only effective when `showCheckboxes` is enabled. + +`gitTreeCompare.refSortOrder` Determines how refs (branches, tags) are sorted when changing the comparison base. Default is `committerdate` which sorts by most recently committed first, making it easy to find recently-used branches. Can be set to `alphabetically` for alphabetical sorting. diff --git a/package.json b/package.json index a352ab1..c4687ad 100644 --- a/package.json +++ b/package.json @@ -420,6 +420,15 @@ "type": "boolean", "description": "When enabled, automatically resets checkboxes when a file is modified after being checked. This ensures that checked files reflect their reviewed state, and any subsequent modifications require re-review.", "default": false + }, + "gitTreeCompare.refSortOrder": { + "type": "string", + "enum": [ + "alphabetically", + "committerdate" + ], + "description": "How to sort refs (branches, tags) when changing the comparison base. 'committerdate' sorts by most recently committed first.", + "default": "committerdate" } } } diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 0994ace..76ee2d7 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -1106,7 +1106,8 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos return; } const commit = new ChangeBaseCommitItem(); - const refs = (await this.repository.getRefs()).filter(ref => ref.name); + const sortOrder = workspace.getConfiguration(NAMESPACE).get<'alphabetically' | 'committerdate'>('refSortOrder', 'committerdate'); + const refs = (await this.repository.getRefs({ sort: sortOrder })).filter(ref => ref.name); const heads = refs.filter(ref => ref.type === RefType.Head).map(ref => new ChangeBaseRefItem(ref)); const tags = refs.filter(ref => ref.type === RefType.Tag).map(ref => new ChangeBaseTagItem(ref)); const remoteHeads = refs.filter(ref => ref.type === RefType.RemoteHead).map(ref => new ChangeBaseRemoteHeadItem(ref));