From a3d21440d126f9e94b89d2c4cc0303ef07b47c3e Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:55:06 +0800 Subject: [PATCH 1/5] feat: add "fetch all repositories" action in workspace menu --- src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/ViewModels/Launcher.cs | 22 ++++++++++++++++++++ src/ViewModels/Repository.cs | 34 +++++++++++++++++++++++++++++++ src/Views/Launcher.axaml.cs | 14 +++++++++++++ 6 files changed, 73 insertions(+) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index eb8ff1ca7e..d018731181 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1065,6 +1065,7 @@ Template: ${0}$ WORKSPACE: Configure Workspaces... + Fetch All Repositories WORKTREE BRANCH Copy Path diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 761cc724a2..f281e4cd53 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1069,6 +1069,7 @@ 模板:${0}$ 工作区: 配置工作区... + 拉取工作区所有仓库 本地工作树 連結分支 复制工作树路径 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 135596f6e9..06d24d4fef 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -1066,6 +1066,7 @@ 範本: ${0}$ 工作區: 設定工作區... + 提取工作區所有存放庫 本機工作樹 分支 複製工作樹路徑 diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 1cf00a848c..595c72f480 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Threading; @@ -122,6 +124,26 @@ public void CloseAll() _ignoreIndexChange = false; } + public async Task FetchAllRepositoriesAsync() + { + // avoid collection was modified while enumerating. + var pages = new List(); + pages.AddRange(Pages); + + var count = 0; + foreach (var page in pages) + { + if (page.Data is Repository repo) + { + if (await repo.FetchAllRemotesAsync()) + count++; + } + } + + if (count > 0) + Models.Notification.Send(null, $"Fetched {count} repositories"); + } + public void SwitchWorkspace(Workspace to) { if (to == null || to.IsActive) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 9957e860bd..5971c8bf85 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -650,6 +650,40 @@ public async Task FetchAsync(bool autoStart) ShowPopup(new Fetch(this)); } + public async Task FetchAllRemotesAsync() + { + if (IsAutoFetching) + return false; + + CommandLog log = null; + + try + { + var lockFile = Path.Combine(GitDir, "index.lock"); + if (File.Exists(lockFile)) + return false; + + if (_remotes.Count == 0) + return false; + + IsAutoFetching = true; + log = CreateLog("Fetch"); + + foreach (var remote in _remotes) + await new Commands.Fetch(FullPath, remote).Use(log).ExecAsync(); + + _lastFetchTime = DateTime.Now; + } + catch + { + // Ignore all exceptions. + } + + IsAutoFetching = false; + log?.Complete(); + return true; + } + public async Task PullAsync(bool autoStart) { if (IsBare || !CanCreatePopup()) diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index 0ac14e6bb3..68d466f6b6 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Avalonia; using Avalonia.Controls; @@ -405,6 +406,19 @@ private void OnOpenWorkspaceMenu(object sender, RoutedEventArgs e) menu.Items.Add(new MenuItem() { Header = "-" }); + var hasRepos = launcher.Pages.Any(p => p.Data is ViewModels.Repository); + + var fetchAll = new MenuItem(); + fetchAll.Header = App.Text("Workspace.FetchAllRepositories"); + fetchAll.Icon = this.CreateMenuIcon("Icons.Fetch"); + fetchAll.IsEnabled = hasRepos; + fetchAll.Click += async (_, ev) => + { + await launcher.FetchAllRepositoriesAsync(); + ev.Handled = true; + }; + menu.Items.Add(fetchAll); + var configure = new MenuItem(); configure.Header = App.Text("Workspace.Configure"); configure.Click += async (_, ev) => From 7a52803ede639065b0942411f957bce9b0348128 Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:08:56 +0800 Subject: [PATCH 2/5] update feature - `Repository.FetchAllRemotesAsync()` now return `bool` (success/failure) - `Launcher.FetchAllRepositoriesAsync()` now sends a summary notification (`Fetched X/Y repositories`) to the active page when done. --- src/ViewModels/Launcher.cs | 19 +++++++++++-------- src/ViewModels/Repository.cs | 10 ++++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 595c72f480..92b13dda25 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -130,18 +130,21 @@ public async Task FetchAllRepositoriesAsync() var pages = new List(); pages.AddRange(Pages); - var count = 0; + var total = 0; + var succeeded = 0; + foreach (var page in pages) { - if (page.Data is Repository repo) - { - if (await repo.FetchAllRemotesAsync()) - count++; - } + if (page.Data is not Repository repo) + continue; + + total++; + if (await repo.FetchAllRemotesAsync()) + succeeded++; } - if (count > 0) - Models.Notification.Send(null, $"Fetched {count} repositories"); + if (total > 0) + Models.Notification.Send(null, $"Fetched {succeeded}/{total} repositories", succeeded < total); } public void SwitchWorkspace(Workspace to) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 5971c8bf85..75d7207b30 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -656,6 +656,7 @@ public async Task FetchAllRemotesAsync() return false; CommandLog log = null; + var succeeded = true; try { @@ -670,18 +671,23 @@ public async Task FetchAllRemotesAsync() log = CreateLog("Fetch"); foreach (var remote in _remotes) - await new Commands.Fetch(FullPath, remote).Use(log).ExecAsync(); + { + var succ = await new Commands.Fetch(FullPath, remote).Use(log).ExecAsync(); + if (!succ) + succeeded = false; + } _lastFetchTime = DateTime.Now; } catch { // Ignore all exceptions. + succeeded = false; } IsAutoFetching = false; log?.Complete(); - return true; + return succeeded; } public async Task PullAsync(bool autoStart) From 2c1371f14c3dbc4c6e19944c26871d7c6429f4ac Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:52:36 +0800 Subject: [PATCH 3/5] show no remote repositories --- src/ViewModels/Launcher.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 92b13dda25..76530be723 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -132,19 +132,30 @@ public async Task FetchAllRepositoriesAsync() var total = 0; var succeeded = 0; + var noRemoteNames = new List(); foreach (var page in pages) { if (page.Data is not Repository repo) continue; + if (repo.Remotes.Count == 0) + { + noRemoteNames.Add(page.Node.Name); + continue; + } + total++; if (await repo.FetchAllRemotesAsync()) succeeded++; } - if (total > 0) - Models.Notification.Send(null, $"Fetched {succeeded}/{total} repositories", succeeded < total); + var message = $"Fetched {succeeded}/{total} repositories"; + + if (noRemoteNames.Count > 0) + message += $"\n{noRemoteNames.Count} repositories skipped (no remote):\n {string.Join("\n ", noRemoteNames)}"; + + Models.Notification.Send(null, message, succeeded < total); } public void SwitchWorkspace(Workspace to) From 27fc219836ee51736cf18aa871000c965815c0ab Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Fri, 18 Sep 2026 08:52:48 +0800 Subject: [PATCH 4/5] show failed repositories --- src/ViewModels/Launcher.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 76530be723..ccfcad5d18 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -133,6 +133,7 @@ public async Task FetchAllRepositoriesAsync() var total = 0; var succeeded = 0; var noRemoteNames = new List(); + var failedNames = new List(); foreach (var page in pages) { @@ -148,6 +149,8 @@ public async Task FetchAllRepositoriesAsync() total++; if (await repo.FetchAllRemotesAsync()) succeeded++; + else + failedNames.Add(page.Node.Name); } var message = $"Fetched {succeeded}/{total} repositories"; @@ -155,6 +158,9 @@ public async Task FetchAllRepositoriesAsync() if (noRemoteNames.Count > 0) message += $"\n{noRemoteNames.Count} repositories skipped (no remote):\n {string.Join("\n ", noRemoteNames)}"; + if (failedNames.Count > 0) + message += $"\n{failedNames.Count} repositories failed:\n {string.Join("\n ", failedNames)}"; + Models.Notification.Send(null, message, succeeded < total); } From 212a0dfa55a79d7616dfbbe54b197765f8d10464 Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:49:30 +0800 Subject: [PATCH 5/5] show skipped repositories --- src/ViewModels/Launcher.cs | 23 ++++++++++++++++++---- src/ViewModels/Repository.cs | 38 ++++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index ccfcad5d18..94b0e73eb1 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -133,6 +133,7 @@ public async Task FetchAllRepositoriesAsync() var total = 0; var succeeded = 0; var noRemoteNames = new List(); + var skippedNames = new List(); var failedNames = new List(); foreach (var page in pages) @@ -147,10 +148,21 @@ public async Task FetchAllRepositoriesAsync() } total++; - if (await repo.FetchAllRemotesAsync()) - succeeded++; - else - failedNames.Add(page.Node.Name); + var result = await repo.FetchAllRemotesAsync(); + switch (result.Status) + { + case Repository.FetchAllStatus.Succeeded: + succeeded++; + break; + case Repository.FetchAllStatus.Skipped: + skippedNames.Add(page.Node.Name); + total--; + break; + case Repository.FetchAllStatus.Failed: + var remotesInfo = string.Join(", ", result.FailedRemotes); + failedNames.Add($"{page.Node.Name} (remote: {remotesInfo})"); + break; + } } var message = $"Fetched {succeeded}/{total} repositories"; @@ -158,6 +170,9 @@ public async Task FetchAllRepositoriesAsync() if (noRemoteNames.Count > 0) message += $"\n{noRemoteNames.Count} repositories skipped (no remote):\n {string.Join("\n ", noRemoteNames)}"; + if (skippedNames.Count > 0) + message += $"\n{skippedNames.Count} repositories skipped:\n {string.Join("\n ", skippedNames)}"; + if (failedNames.Count > 0) message += $"\n{failedNames.Count} repositories failed:\n {string.Join("\n ", failedNames)}"; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 75d7207b30..c2c14e2b7e 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -650,23 +650,33 @@ public async Task FetchAsync(bool autoStart) ShowPopup(new Fetch(this)); } - public async Task FetchAllRemotesAsync() + public enum FetchAllStatus { + Succeeded, + Failed, + Skipped, + } + + public class FetchAllRemotesResult + { + public FetchAllStatus Status { get; set; } = FetchAllStatus.Succeeded; + public List FailedRemotes { get; } = new List(); + } + + public async Task FetchAllRemotesAsync() + { + var result = new FetchAllRemotesResult(); + if (IsAutoFetching) - return false; + { + result.Status = FetchAllStatus.Skipped; + return result; + } CommandLog log = null; - var succeeded = true; try { - var lockFile = Path.Combine(GitDir, "index.lock"); - if (File.Exists(lockFile)) - return false; - - if (_remotes.Count == 0) - return false; - IsAutoFetching = true; log = CreateLog("Fetch"); @@ -674,7 +684,7 @@ public async Task FetchAllRemotesAsync() { var succ = await new Commands.Fetch(FullPath, remote).Use(log).ExecAsync(); if (!succ) - succeeded = false; + result.FailedRemotes.Add(remote.Name); } _lastFetchTime = DateTime.Now; @@ -682,12 +692,14 @@ public async Task FetchAllRemotesAsync() catch { // Ignore all exceptions. - succeeded = false; + result.FailedRemotes.Add("*"); } IsAutoFetching = false; log?.Complete(); - return succeeded; + + result.Status = result.FailedRemotes.Count == 0 ? FetchAllStatus.Succeeded : FetchAllStatus.Failed; + return result; } public async Task PullAsync(bool autoStart)