diff --git a/CHANGELOG.md b/CHANGELOG.md index a4aa49a..b813951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +## [0.2.1] - 2026-08-05 + +### Added + +- A native watched-folder manager with a macOS folder chooser, current-folder list, and per-folder remove buttons + +### Changed + +- The navigator settings button now opens the watched-folder manager while preserving the raw machine-overridable setting for portability + ## [0.2.0] - 2026-08-05 ### Added @@ -50,6 +60,7 @@ The first Git Fleet release, forked from Neo Git Graph 0.5.0. Git Fleet began from Neo Git Graph 0.5.0. Its earlier changelog remains available in the [upstream repository](https://github.com/asispts/neo-git-graph/blob/main/CHANGELOG.md). -[Unreleased]: https://github.com/wrgrant/git-fleet/compare/v0.2.0...HEAD +[Unreleased]: https://github.com/wrgrant/git-fleet/compare/v0.2.1...HEAD +[0.2.1]: https://github.com/wrgrant/git-fleet/releases/tag/v0.2.1 [0.2.0]: https://github.com/wrgrant/git-fleet/releases/tag/v0.2.0 [0.1.0]: https://github.com/wrgrant/git-fleet/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 6497ff9..ec729ea 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ code --install-extension git-fleet-v0.2.0.vsix Or choose **Extensions: Install from VSIX...** from the VS Code Command Palette. -After installation, select the Git Fleet icon in the Activity Bar. The folder-plus button adds a repository search root that should be visible from every VS Code window. +After installation, select the Git Fleet icon in the Activity Bar. The folder-plus button opens the native folder chooser. The adjacent settings button opens **Git Fleet: Watched Folders**, where extra folders can be added or removed without editing JSON. ## Configuration diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 54748df..a59642a 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -18,6 +18,14 @@ "Unable to load Git Fleet": "Unable to load Git Fleet", "Either the current workspace does not contain a Git repository, or the Git repository is not configured correctly.": "Either the current workspace does not contain a Git repository, or the Git repository is not configured correctly.", "If you are using a portable Git installation, make sure you have set the Visual Studio Code Setting \"git.path\" to the path of your portable installation (e.g. \"C:\\Program Files\\Git\\bin\\git.exe\" on Windows).": "If you are using a portable Git installation, make sure you have set the Visual Studio Code Setting \"git.path\" to the path of your portable installation (e.g. \"C:\\Program Files\\Git\\bin\\git.exe\" on Windows).", + "Watch for Git repositories": "Watch for Git repositories", + "Add folders to Git Fleet": "Add folders to Git Fleet", + "Add watched folders": "Add watched folders", + "Edit paths in Settings": "Edit paths in Settings", + "Stop watching this folder": "Stop watching this folder", + "Use + to add folders or the trash button to stop watching one": "Use + to add folders or the trash button to stop watching one", + "No extra folders yet — use + to choose one": "No extra folders yet — use + to choose one", + "Git Fleet: Watched Folders": "Git Fleet: Watched Folders", "Added in {0}": "Added in {0}", "Deleted in {0}": "Deleted in {0}", "Working Tree": "Working Tree", diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index f6439a8..c23a03e 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -28,6 +28,14 @@ "Unable to load Git Fleet": "无法加载 Git Fleet", "Either the current workspace does not contain a Git repository, or the Git repository is not configured correctly.": "当前工作区不包含 Git 仓库,或者 Git 仓库配置不正确。", "If you are using a portable Git installation, make sure you have set the Visual Studio Code Setting \"git.path\" to the path of your portable installation (e.g. \"C:\\Program Files\\Git\\bin\\git.exe\" on Windows).": "如果您使用的是便携版 Git,请确保已将 Visual Studio Code 的设置 “git.path” 设置为便携版安装的路径(例如 Windows 上的 “C:\\Program Files\\Git\\bin\\git.exe”)。", + "Watch for Git repositories": "监视 Git 仓库", + "Add folders to Git Fleet": "向 Git Fleet 添加文件夹", + "Add watched folders": "添加监视的文件夹", + "Edit paths in Settings": "在设置中编辑路径", + "Stop watching this folder": "停止监视此文件夹", + "Use + to add folders or the trash button to stop watching one": "使用 + 添加文件夹,或使用垃圾桶按钮停止监视", + "No extra folders yet — use + to choose one": "尚无额外文件夹 — 使用 + 选择一个", + "Git Fleet: Watched Folders": "Git Fleet:监视的文件夹", "Working Tree": "工作目录", "Hover to connect HEAD and its inferred base to history.": "悬停以将 HEAD 及其推断的基点连接到历史记录。", "current": "当前", diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index 5433849..ba7b525 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -28,6 +28,14 @@ "Unable to load Git Fleet": "無法載入 Git Fleet", "Either the current workspace does not contain a Git repository, or the Git repository is not configured correctly.": "目前工作區不包含 Git 存放庫,或者 Git 存放庫設定不正確。", "If you are using a portable Git installation, make sure you have set the Visual Studio Code Setting \"git.path\" to the path of your portable installation (e.g. \"C:\\Program Files\\Git\\bin\\git.exe\" on Windows).": "如果您使用的是可攜式 Git 安裝程式,請確保已將 Visual Studio Code 的設定 “git.path” 設定為該可攜式安裝程式的內容路徑(例如 Windows 上的 “C:\\Program Files\\Git\\bin\\git.exe”)。", + "Watch for Git repositories": "監看 Git 儲存庫", + "Add folders to Git Fleet": "將資料夾新增至 Git Fleet", + "Add watched folders": "新增監看的資料夾", + "Edit paths in Settings": "在設定中編輯路徑", + "Stop watching this folder": "停止監看此資料夾", + "Use + to add folders or the trash button to stop watching one": "使用 + 新增資料夾,或使用垃圾桶按鈕停止監看", + "No extra folders yet — use + to choose one": "尚無其他資料夾 — 使用 + 選擇一個", + "Git Fleet: Watched Folders": "Git Fleet:監看的資料夾", "Working Tree": "工作目錄", "Hover to connect HEAD and its inferred base to history.": "暫留以將 HEAD 及其推斷的基點連接到歷史記錄。", "current": "目前", diff --git a/package.json b/package.json index 5eb42cb..d347c86 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "git-fleet", "displayName": "Git Fleet", - "version": "0.2.0", + "version": "0.2.1", "description": "%description%", "categories": [ "SCM Providers", diff --git a/package.nls.json b/package.nls.json index 54545a9..17a7dd0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -12,7 +12,7 @@ "command.fetchRepository": "Fetch Repository", "command.showAllRepositories": "Show Clean Repositories", "command.addRepositorySearchRoot": "Add Repository Search Root", - "command.openRepositorySearchRootsSettings": "Manage Repository Search Roots", + "command.openRepositorySearchRootsSettings": "Manage Watched Folders", "viewContainer.gitFleet": "Git Fleet", "view.repositoryNavigator": "Repositories", "config.autoCenterCommitDetailsView": "Automatically center the commit details view when it is opened.", @@ -32,7 +32,7 @@ "config.initialLoadCommits": "Specifies the number of commits to initially load.", "config.loadMoreCommits": "Specifies the number of commits to load when the \"Load More Commits\" button is pressed (only shown when more commits are available).", "config.maxDepthOfRepoSearch": "Specifies the maximum depth of subfolders to search when discovering repositories in the workspace. Defaults to 4 so a parent code folder can discover nested projects.", - "config.repositorySearchRoots": "Additional folders Git Fleet searches for repositories in every VS Code window. Use this for repositories outside the current workspace.", + "config.repositorySearchRoots": "Additional folders Git Fleet watches for repositories in every VS Code window. Use Git Fleet: Manage Watched Folders for a native folder chooser, or edit these paths directly.", "config.showCurrentBranchByDefault": "Show the current branch by default when the graph is opened. Default: false (show all branches)", "config.showStatusBarItem": "Show a Status Bar item which opens Git Fleet when clicked.", "config.showUncommittedChanges": "Show uncommitted changes (set to false to decrease load time on large repositories).", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 0d17695..2e6e8ee 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -12,7 +12,7 @@ "command.hideCleanRepositories": "隐藏无更改的仓库", "command.showAllRepositories": "显示无更改的仓库", "command.addRepositorySearchRoot": "添加仓库搜索根目录", - "command.openRepositorySearchRootsSettings": "管理仓库搜索根目录", + "command.openRepositorySearchRootsSettings": "管理监视的文件夹", "viewContainer.gitFleet": "Git 仓库总览", "view.repositoryNavigator": "仓库", "config.autoCenterCommitDetailsView": "打开提交详情视图时自动居中显示。", @@ -32,7 +32,7 @@ "config.initialLoadCommits": "指定初始加载的提交数量。", "config.loadMoreCommits": "指定按下“加载更多提交”按钮时要加载的提交数量(仅在有更多提交可用时显示)。", "config.maxDepthOfRepoSearch": "指定在工作区中发现仓库时搜索的子文件夹最大深度。", - "config.repositorySearchRoots": "Git Fleet 在每个 VS Code 窗口中搜索仓库的其他文件夹。适用于当前工作区之外的仓库。", + "config.repositorySearchRoots": "Git Fleet 在每个 VS Code 窗口中监视仓库的其他文件夹。使用 Git Fleet:管理监视的文件夹打开原生文件夹选择器,或直接编辑这些路径。", "config.showCurrentBranchByDefault": "打开 Git 图形时默认显示当前分支。默认值:false(显示所有分支)", "config.showStatusBarItem": "显示状态栏项,单击时打开 Git 图形。", "config.showUncommittedChanges": "显示未提交的更改(在大型仓库上设置为 false 以减少加载时间)。", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 37a1e04..dc0080b 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -12,7 +12,7 @@ "command.hideCleanRepositories": "隱藏無變更的儲存庫", "command.showAllRepositories": "顯示無變更的儲存庫", "command.addRepositorySearchRoot": "新增儲存庫搜尋根目錄", - "command.openRepositorySearchRootsSettings": "管理儲存庫搜尋根目錄", + "command.openRepositorySearchRootsSettings": "管理監看的資料夾", "viewContainer.gitFleet": "Git 儲存庫總覽", "view.repositoryNavigator": "儲存庫", "config.autoCenterCommitDetailsView": "開啟提交詳情檢視時自動置中顯示。", @@ -32,7 +32,7 @@ "config.initialLoadCommits": "指定初始載入的提交數量。", "config.loadMoreCommits": "指定按下「載入更多提交」按鈕時要載入的提交數量(僅在有更多提交可用時顯示)。", "config.maxDepthOfRepoSearch": "指定在工作區中發現存放庫時搜尋的子資料夾最大深度。", - "config.repositorySearchRoots": "Git Fleet 在每個 VS Code 視窗中搜尋儲存庫的其他資料夾。適用於目前工作區之外的儲存庫。", + "config.repositorySearchRoots": "Git Fleet 在每個 VS Code 視窗中監看儲存庫的其他資料夾。使用 Git Fleet:管理監看的資料夾開啟原生資料夾選擇器,或直接編輯這些路徑。", "config.showCurrentBranchByDefault": "開啟 Git 圖形時預設顯示目前分支。預設值:false(顯示所有分支)", "config.showStatusBarItem": "顯示狀態列項目,按一下時開啟 Git 圖形。", "config.showUncommittedChanges": "顯示未提交的變更(在大型存放庫上設定為 false 以減少載入時間)。", diff --git a/plans/content-discovery-queue.md b/plans/content-discovery-queue.md index ed9df2f..601c494 100644 --- a/plans/content-discovery-queue.md +++ b/plans/content-discovery-queue.md @@ -1,5 +1,13 @@ # Content discovery queue +## 2026-08-05 — Keep portable settings, add a humane management surface + +- Status: idea +- Why it matters: VS Code array settings are portable and syncable, but raw path editors are a poor way to choose folders on a desktop. +- Evidence: Git Fleet keeps `repositorySearchRoots` as a machine-overridable string array while adding a native folder chooser and per-folder remove controls through a Quick Pick manager. +- Files changed: `src/extension/repositorySearchRoots.ts`, `package.json`, `README.md` +- Suggested content angle: Good extension UX often composes native VS Code controls around a durable setting instead of inventing a custom settings format. + ## 2026-08-05 — Separate repository structure from urgency - Why it matters: Folder hierarchy and operational priority answer different questions. A fleet view should let someone keep spatial context while still sorting repositories by recent activity or uncommitted work. diff --git a/src/extension/repositorySearchRoots.ts b/src/extension/repositorySearchRoots.ts index 87ed30f..ad7ddc1 100644 --- a/src/extension/repositorySearchRoots.ts +++ b/src/extension/repositorySearchRoots.ts @@ -7,6 +7,8 @@ import { config } from "@/config"; const SETTINGS_QUERY = "@ext:wgrant-dev.git-fleet repositorySearchRoots"; +type SearchRootItem = vscode.QuickPickItem & { root: string }; + function normalizeRoot(root: string): string { const expanded = root === "~" @@ -24,33 +26,114 @@ export function getRepositorySearchRoots(): string[] { return [...new Set([...workspaceRoots, ...config.repositorySearchRoots()].map(normalizeRoot))]; } +function getConfiguredRepositorySearchRoots(): string[] { + return [...new Set(config.repositorySearchRoots().map(normalizeRoot))]; +} + +async function saveRepositorySearchRoots(roots: string[]): Promise { + await vscode.workspace + .getConfiguration("git-fleet") + .update("repositorySearchRoots", roots, vscode.ConfigurationTarget.Global); +} + +async function chooseRepositorySearchRoots(): Promise { + const selected = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: true, + openLabel: vscode.l10n.t("Watch for Git repositories"), + title: vscode.l10n.t("Add folders to Git Fleet") + }); + if (!selected?.length) { + return false; + } + + await saveRepositorySearchRoots([ + ...new Set([ + ...getConfiguredRepositorySearchRoots(), + ...selected.map((uri) => normalizeRoot(uri.fsPath)) + ]) + ]); + return true; +} + +async function showRepositorySearchRootManager(): Promise { + const picker = vscode.window.createQuickPick(); + const addButton: vscode.QuickInputButton = { + iconPath: new vscode.ThemeIcon("folder-add"), + tooltip: vscode.l10n.t("Add watched folders") + }; + const editButton: vscode.QuickInputButton = { + iconPath: new vscode.ThemeIcon("settings-gear"), + tooltip: vscode.l10n.t("Edit paths in Settings") + }; + const removeButton: vscode.QuickInputButton = { + iconPath: new vscode.ThemeIcon("trash"), + tooltip: vscode.l10n.t("Stop watching this folder") + }; + + const refreshItems = () => { + const roots = getConfiguredRepositorySearchRoots(); + picker.items = roots.map((root) => ({ + alwaysShow: true, + buttons: [removeButton], + description: path.dirname(root), + detail: root, + iconPath: new vscode.ThemeIcon("folder"), + label: path.basename(root), + root + })); + picker.placeholder = roots.length + ? vscode.l10n.t("Use + to add folders or the trash button to stop watching one") + : vscode.l10n.t("No extra folders yet — use + to choose one"); + }; + + picker.title = vscode.l10n.t("Git Fleet: Watched Folders"); + picker.matchOnDescription = true; + picker.matchOnDetail = true; + picker.buttons = [addButton, editButton]; + refreshItems(); + + const disposables = [ + picker.onDidTriggerButton(async (button) => { + if (button === addButton) { + if (await chooseRepositorySearchRoots()) { + refreshItems(); + } + } else if (button === editButton) { + picker.hide(); + await vscode.commands.executeCommand("workbench.action.openSettings", SETTINGS_QUERY); + } + }), + picker.onDidTriggerItemButton(async ({ item }) => { + await saveRepositorySearchRoots( + getConfiguredRepositorySearchRoots().filter((root) => root !== item.root) + ); + refreshItems(); + }) + ]; + + await new Promise((resolve) => { + disposables.push( + picker.onDidHide(() => { + for (const disposable of disposables) { + disposable.dispose(); + } + picker.dispose(); + resolve(); + }) + ); + picker.show(); + }); +} + export function registerRepositorySearchRootCommands(ctx: vscode.ExtensionContext): void { ctx.subscriptions.push( vscode.commands.registerCommand("git-fleet.addRepositorySearchRoot", async () => { - const selected = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: true, - openLabel: "Watch for Git repositories", - title: "Add Git Fleet Search Root" - }); - if (!selected?.length) { - return; - } - - const roots = [ - ...new Set( - [...config.repositorySearchRoots(), ...selected.map((uri) => uri.fsPath)].map( - normalizeRoot - ) - ) - ]; - await vscode.workspace - .getConfiguration("git-fleet") - .update("repositorySearchRoots", roots, vscode.ConfigurationTarget.Global); + await chooseRepositorySearchRoots(); }), vscode.commands.registerCommand("git-fleet.openRepositorySearchRootsSettings", () => - vscode.commands.executeCommand("workbench.action.openSettings", SETTINGS_QUERY) + showRepositorySearchRootManager() ) ); } diff --git a/tests/extension/repositorySearchRoots.test.ts b/tests/extension/repositorySearchRoots.test.ts index 03d52a7..4c1eb21 100644 --- a/tests/extension/repositorySearchRoots.test.ts +++ b/tests/extension/repositorySearchRoots.test.ts @@ -11,10 +11,42 @@ const mock = vi.hoisted(() => { let configuredRoots: string[] = []; let workspaceRoots: string[] = []; const commands = new Map unknown>(); - const update = vi.fn(); + const update = vi.fn((_key: string, value: string[]) => { + configuredRoots = value; + }); + const topButtonListeners: Array<(button: unknown) => unknown> = []; + const itemButtonListeners: Array<(event: { item: unknown }) => unknown> = []; + const hideListeners: Array<() => unknown> = []; + const quickPick = { + buttons: [] as unknown[], + items: [] as unknown[], + placeholder: "", + title: "", + matchOnDescription: false, + matchOnDetail: false, + dispose: vi.fn(), + hide: vi.fn(() => hideListeners.forEach((listener) => listener())), + show: vi.fn(), + onDidHide: (listener: () => unknown) => { + hideListeners.push(listener); + return { dispose: vi.fn() }; + }, + onDidTriggerButton: (listener: (button: unknown) => unknown) => { + topButtonListeners.push(listener); + return { dispose: vi.fn() }; + }, + onDidTriggerItemButton: (listener: (event: { item: unknown }) => unknown) => { + itemButtonListeners.push(listener); + return { dispose: vi.fn() }; + } + }; return { ConfigurationTarget: { Global: 1 }, + ThemeIcon: class ThemeIcon { + constructor(public readonly id: string) {} + }, + l10n: { t: (value: string) => value }, Uri: { file: (fsPath: string) => ({ fsPath }) }, commands: { executeCommand: vi.fn(), @@ -33,7 +65,7 @@ const mock = vi.hoisted(() => { update }) }, - window: { showOpenDialog: vi.fn() }, + window: { createQuickPick: vi.fn(() => quickPick), showOpenDialog: vi.fn() }, setConfiguredRoots(roots: string[]) { configuredRoots = roots; }, @@ -43,6 +75,13 @@ const mock = vi.hoisted(() => { invoke(id: string) { return commands.get(id)?.(); }, + quickPick, + async triggerTopButton(index: number) { + await topButtonListeners[0]?.(quickPick.buttons[index]); + }, + async triggerItemButton(index: number) { + await itemButtonListeners[0]?.({ item: quickPick.items[index] }); + }, update }; }); @@ -53,6 +92,7 @@ beforeEach(() => { vi.clearAllMocks(); mock.setConfiguredRoots([]); mock.setWorkspaceRoots([]); + mock.quickPick.items = []; }); describe("repository search roots", () => { @@ -78,4 +118,33 @@ describe("repository search roots", () => { mock.ConfigurationTarget.Global ); }); + + it("manages watched folders with native add and remove controls", async () => { + const ctx = { subscriptions: [] } as unknown as import("vscode").ExtensionContext; + mock.setConfiguredRoots(["/repos"]); + mock.window.showOpenDialog.mockResolvedValue([{ fsPath: "/archive" }]); + registerRepositorySearchRootCommands(ctx); + + const manager = mock.invoke("git-fleet.openRepositorySearchRootsSettings"); + expect(mock.quickPick.title).toBe("Git Fleet: Watched Folders"); + expect(mock.quickPick.items).toHaveLength(1); + + await mock.triggerTopButton(0); + expect(mock.update).toHaveBeenCalledWith( + "repositorySearchRoots", + [path.resolve("/repos"), path.resolve("/archive")], + mock.ConfigurationTarget.Global + ); + expect(mock.quickPick.items).toHaveLength(2); + + await mock.triggerItemButton(0); + expect(mock.update).toHaveBeenLastCalledWith( + "repositorySearchRoots", + [path.resolve("/archive")], + mock.ConfigurationTarget.Global + ); + + mock.quickPick.hide(); + await manager; + }); });