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)