Skip to content

Commit d468451

Browse files
Add Extensions, starting with Git Repositories (#5620)
* Add an engine to keep main git clones current and their worktrees tidy First phase of the experimental Git Repositories page: a UI-free UniGetUI.GitRepositories project that the page will build on. - Check fetches a clone's remote once, with prune, and reports whether it is up to date, behind, ahead, diverged, has changed tracked files, is on another branch or detached, or has no remote. - The default branch is the remote's HEAD, else main, else master. - Update only ever fast-forwards (merge --ff-only) a clean clone that is on its default branch; anything else is refused and reported. - Worktrees are listed with what makes each safe to remove: local changes, commits not on the remote default branch, a deleted upstream, locks and missing folders. Removal and prune refuse the main and locked worktrees, and need force for unsaved work. - Git never prompts on a terminal, both pipes are drained, and every command has a time limit. Tests run against real temporary repositories with an isolated git configuration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add Extensions, starting with Git Repositories Settings gains an Extensions section for optional tools that go beyond package management. Each extension is off by default, turns on and off without a restart, and is described once in AppExtensions, from which the Extensions list and each extension's settings page are built. Git Repositories is the first extension. It keeps main git clones on their default branch up to date, with the engine added in the previous commit: - A sidebar page lists the clones with their state, checks and fast-forwards them one by one or all at once, and can check on a schedule and update after checking. A badge counts the clones behind. - One Add repository button takes a clone, a worktree (its main clone is added) or a folder whose clones are found two levels down. - A resizable panel under the clones shows the selected clone's worktrees or branches. Worktrees can be opened, removed (with their branch) and pruned; branches can be deleted one by one, and those merged into the default branch all at once. - The worktree list shows before git status has run in each worktree, and branch details come from one for-each-ref call, so clones with dozens of agent worktrees load in about a second. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name the git engine project after the Extensions it belongs to Rename UniGetUI.GitRepositories and its tests to UniGetUI.Extensions.GitRepositories(.Tests), with matching namespaces and an UniGetUI.Extensions solution folder, the way package managers live in UniGetUI.PackageEngine.Managers.*. Later extensions get their own UniGetUI.Extensions.* projects next to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent fea4ca7 commit d468451

47 files changed

Lines changed: 4227 additions & 15 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/CLI.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ other hosts; this does not substitute for their native lock-contention tests.
168168
| --- | --- | --- | --- |
169169
| `app status` | None | None | Returns app/session state such as headless mode, page, and supported UI actions. |
170170
| `app show` | None | None | Shows and focuses the window when a GUI session exists. |
171-
| `app navigate` | `--page <page>` | `--manager <id>`, `--help-attachment <path>` | Valid pages include `discover`, `updates`, `installed`, `bundles`, `catalog`, `catalog-editor`, `settings`, `managers`, `own-log`, `manager-log`, `operation-history`, `help`, `release-notes`, and `about`. `catalog` and `catalog-editor` require Enable Software Catalog in Experimental settings and developer options, followed by an application restart. |
171+
| `app navigate` | `--page <page>` | `--manager <id>`, `--help-attachment <path>` | Valid pages include `discover`, `updates`, `installed`, `bundles`, `catalog`, `catalog-editor`, `git-repositories`, `settings`, `managers`, `own-log`, `manager-log`, `operation-history`, `help`, `release-notes`, and `about`. `catalog` and `catalog-editor` require Enable Software Catalog in Experimental settings and developer options, followed by an application restart. `git-repositories` requires the Git Repositories extension to be turned on in Settings, under Extensions; no restart is needed. |
172172
| `app quit` | None | None | Gracefully shuts down the selected session, including headless daemons. |
173173

174174
### Operations

‎src/Languages/lang_en.json‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,143 @@
111111
"Requires a machine-wide SoftwareCatalog.json file and an application restart.": "Requires a machine-wide SoftwareCatalog.json file and an application restart.",
112112
"Software Catalog is disabled": "Software Catalog is disabled",
113113
"Enable Software Catalog in Experimental settings and developer options, then restart UniGetUI.": "Enable Software Catalog in Experimental settings and developer options, then restart UniGetUI.",
114+
"Git Repositories": "Git Repositories",
115+
"Worktrees": "Worktrees",
116+
"Only when asked": "Only when asked",
117+
"Every 15 minutes": "Every 15 minutes",
118+
"Every 30 minutes": "Every 30 minutes",
119+
"Every hour": "Every hour",
120+
"Every 4 hours": "Every 4 hours",
121+
"Git was not found": "Git was not found",
122+
"Install Git and make sure it is on the PATH, then restart UniGetUI.": "Install Git and make sure it is on the PATH, then restart UniGetUI.",
123+
"{0} git clones were added": "{0} git clones were added",
124+
"Choose git clones, or folders that contain them": "Choose git clones, or folders that contain them",
125+
"No git clones were found in {0}": "No git clones were found in {0}",
126+
"No git clones were found in the chosen folders": "No git clones were found in the chosen folders",
127+
"Clones are looked for in the chosen folders and two levels of subfolders.": "Clones are looked for in the chosen folders and two levels of subfolders.",
128+
"Every git clone found is already in the list": "Every git clone found is already in the list",
129+
"1 git clone was added": "1 git clone was added",
130+
"Pick a clone to keep up to date, or a folder such as your dev folder to add every clone in it": "Pick a clone to keep up to date, or a folder such as your dev folder to add every clone in it",
131+
"{0} is not a git repository": "{0} is not a git repository",
132+
"{0} is already in the list": "{0} is already in the list",
133+
"Added the main clone of this worktree": "Added the main clone of this worktree",
134+
"{0} is a worktree of {1}, so {1} was added instead.": "{0} is a worktree of {1}, so {1} was added instead.",
135+
"UniGetUI keeps this clone current on the branch below. Leave it empty to follow the remote's default branch.": "UniGetUI keeps this clone current on the branch below. Leave it empty to follow the remote's default branch.",
136+
"Branch to keep current": "Branch to keep current",
137+
"{0} repositories could not be updated": "{0} repositories could not be updated",
138+
"{0} repositories updated": "{0} repositories updated",
139+
"{0} were left alone: see their status for why.": "{0} were left alone: see their status for why.",
140+
"Every repository is up to date": "Every repository is up to date",
141+
"Could not list the worktrees of {0}": "Could not list the worktrees of {0}",
142+
"Could not prune the worktrees of {0}": "Could not prune the worktrees of {0}",
143+
"The folder {0} will be deleted.": "The folder {0} will be deleted.",
144+
"It has changed or untracked files. They will be lost.": "It has changed or untracked files. They will be lost.",
145+
"Its branch has commits that are not on the default branch. Keep the branch to keep them.": "Its branch has commits that are not on the default branch. Keep the branch to keep them.",
146+
"Also delete the branch {0}": "Also delete the branch {0}",
147+
"Remove this worktree?": "Remove this worktree?",
148+
"Could not remove the worktree {0}": "Could not remove the worktree {0}",
149+
"Not checked yet": "Not checked yet",
150+
"1 commit behind": "1 commit behind",
151+
"{0} commits behind": "{0} commits behind",
152+
"1 local commit not on the remote": "1 local commit not on the remote",
153+
"{0} local commits not on the remote": "{0} local commits not on the remote",
154+
"Diverged from {0}": "Diverged from {0}",
155+
"Has local changes": "Has local changes",
156+
"On {0} instead of {1}": "On {0} instead of {1}",
157+
"No branch checked out": "No branch checked out",
158+
"No remote branch to follow": "No remote branch to follow",
159+
"Could not be checked": "Could not be checked",
160+
"Check this repository to compare it with its remote.": "Check this repository to compare it with its remote.",
161+
"Level with {0}.": "Level with {0}.",
162+
"Updating fast-forwards it to {0}.": "Updating fast-forwards it to {0}.",
163+
"It has commits that are not on {0}, so it is not updated: that would need a merge.": "It has commits that are not on {0}, so it is not updated: that would need a merge.",
164+
"Tracked files have been changed, so it is not updated. Commit, stash or discard them first.": "Tracked files have been changed, so it is not updated. Commit, stash or discard them first.",
165+
"Only a clone checked out on {0} is updated.": "Only a clone checked out on {0} is updated.",
166+
"It has no remote, or its remote has no main or master branch. You can choose the branch to follow.": "It has no remote, or its remote has no main or master branch. You can choose the branch to follow.",
167+
"detached": "detached",
168+
"Checked at {0}": "Checked at {0}",
169+
"1 worktree": "1 worktree",
170+
"{0} worktrees": "{0} worktrees",
171+
"detached at {0}": "detached at {0}",
172+
"Last commit {0}": "Last commit {0}",
173+
"1 commit not on the default branch": "1 commit not on the default branch",
174+
"{0} commits not on the default branch": "{0} commits not on the default branch",
175+
"Locked": "Locked",
176+
"Locked: {0}": "Locked: {0}",
177+
"Keep your main clones up to date on their default branch, and clean up their worktrees and branches.": "Keep your main clones up to date on their default branch, and clean up their worktrees and branches.",
178+
"Add repository": "Add repository",
179+
"Fetch every repository and compare it with its remote": "Fetch every repository and compare it with its remote",
180+
"Check all": "Check all",
181+
"Fetch every repository and fast-forward the ones that are only behind": "Fetch every repository and fast-forward the ones that are only behind",
182+
"Check automatically:": "Check automatically:",
183+
"Check automatically": "Check automatically",
184+
"Only clones on their default branch, with no local changes and no commits of their own, are fast-forwarded.": "Only clones on their default branch, with no local changes and no commits of their own, are fast-forwarded.",
185+
"Update after checking": "Update after checking",
186+
"Check": "Check",
187+
"Open folder": "Open folder",
188+
"Choose the branch to keep current": "Choose the branch to keep current",
189+
"Remove from the list": "Remove from the list",
190+
"No repositories yet": "No repositories yet",
191+
"Add the main clones you keep on main or master. UniGetUI fetches them and fast-forwards the ones that are only behind, never touching local work.": "Add the main clones you keep on main or master. UniGetUI fetches them and fast-forwards the ones that are only behind, never touching local work.",
192+
"Forget the worktrees whose folder was deleted": "Forget the worktrees whose folder was deleted",
193+
"Prune missing": "Prune missing",
194+
"Main clone": "Main clone",
195+
"Everything committed on it is on the default branch": "Everything committed on it is on the default branch",
196+
"Merged": "Merged",
197+
"Its remote branch was deleted, as after a merged pull request": "Its remote branch was deleted, as after a merged pull request",
198+
"Remote branch deleted": "Remote branch deleted",
199+
"Has changed or untracked files": "Has changed or untracked files",
200+
"Looking for changed files...": "Looking for changed files...",
201+
"1 clone": "1 clone",
202+
"Enable Git Repositories": "Enable Git Repositories",
203+
"Extensions": "Extensions",
204+
"Fetches your main clones, fast-forwards their default branch, and helps you remove leftover worktrees and branches.": "Fetches your main clones, fast-forwards their default branch, and helps you remove leftover worktrees and branches.",
205+
"Found {0}": "Found {0}",
206+
"Git": "Git",
207+
"Git was not found. Install it from Discover Packages, then come back to this page.": "Git was not found. Install it from Discover Packages, then come back to this page.",
208+
"Keep your main git clones up to date and clean up leftover worktrees and branches": "Keep your main git clones up to date and clean up leftover worktrees and branches",
209+
"Looking for Git...": "Looking for Git...",
210+
"No clones yet": "No clones yet",
211+
"Off": "Off",
212+
"On": "On",
213+
"Optional tools that go beyond package management": "Optional tools that go beyond package management",
214+
"Preferences": "Preferences",
215+
"Requirements": "Requirements",
216+
"Turn it on in Settings, under Extensions.": "Turn it on in Settings, under Extensions.",
217+
"{0} behind": "{0} behind",
218+
"{0} clones": "{0} clones",
219+
"Check for new commits automatically": "Check for new commits automatically",
220+
"Choose the clones to keep up to date": "Choose the clones to keep up to date",
221+
"Git Repositories is turned off": "Git Repositories is turned off",
222+
"Open Git Repositories": "Open Git Repositories",
223+
"Update clones after checking": "Update clones after checking",
224+
"Branches": "Branches",
225+
"Checked out in {0}": "Checked out in {0}",
226+
"Could not delete the branch {0}": "Could not delete the branch {0}",
227+
"Could not list the branches of {0}": "Could not list the branches of {0}",
228+
"Default branch": "Default branch",
229+
"Delete merged": "Delete merged",
230+
"Delete the branches whose commits are all on the default branch": "Delete the branches whose commits are all on the default branch",
231+
"Delete this branch?": "Delete this branch?",
232+
"Delete {0} merged branches?": "Delete {0} merged branches?",
233+
"Drag to resize": "Drag to resize",
234+
"Everything committed on these branches is already on the default branch, so nothing is lost:": "Everything committed on these branches is already on the default branch, so nothing is lost:",
235+
"Git repositories": "Git repositories",
236+
"In sync with {0}": "In sync with {0}",
237+
"It does not track a remote branch": "It does not track a remote branch",
238+
"It has commits that are not on the default branch. They will be lost unless they were pushed.": "It has commits that are not on the default branch. They will be lost unless they were pushed.",
239+
"It has commits that were never pushed to {0}.": "It has commits that were never pushed to {0}.",
240+
"Local only": "Local only",
241+
"Open the worktree it is checked out in": "Open the worktree it is checked out in",
242+
"Read them again from git": "Read them again from git",
243+
"Select a repository to see its worktrees and branches": "Select a repository to see its worktrees and branches",
244+
"Show": "Show",
245+
"Some branches could not be deleted": "Some branches could not be deleted",
246+
"The branch {0} will be deleted from {1}.": "The branch {0} will be deleted from {1}.",
247+
"{0} to pull from {1}": "{0} to pull from {1}",
248+
"{0} to push and {1} to pull from {2}": "{0} to push and {1} to pull from {2}",
249+
"{0} to push to {1}": "{0} to push to {1}",
250+
"Folder missing": "Folder missing",
114251
"The software catalog could not be loaded. Ask an administrator to check {0}, then try again.": "The software catalog could not be loaded. Ask an administrator to check {0}, then try again.",
115252
"Edit software catalog": "Edit software catalog",
116253
"Back to {0}": "Back to {0}",
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 7 additions & 0 deletions
Loading

‎src/UniGetUI.Avalonia.slnx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@
9191
<Platform Solution="*|x64" Project="x64" />
9292
</Project>
9393
</Folder>
94+
<Folder Name="/UniGetUI.Extensions/">
95+
<Project Path="UniGetUI.Extensions.GitRepositories/UniGetUI.Extensions.GitRepositories.csproj">
96+
<Platform Solution="*|arm64" Project="arm64" />
97+
<Platform Solution="*|x64" Project="x64" />
98+
</Project>
99+
<Project Path="UniGetUI.Extensions.GitRepositories.Tests/UniGetUI.Extensions.GitRepositories.Tests.csproj">
100+
<Platform Solution="*|arm64" Project="arm64" />
101+
<Platform Solution="*|x64" Project="x64" />
102+
</Project>
103+
</Folder>
94104
<Folder Name="/UniGetUI.Interface.Enums/">
95105
<Project Path="UniGetUI.Interface.Enums/UniGetUI.Interface.Enums.csproj">
96106
<Platform Solution="*|arm64" Project="arm64" />
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using Avalonia.Controls;
2+
using UniGetUI.Avalonia.Views;
3+
using UniGetUI.Avalonia.Views.Pages.SettingsPages;
4+
using UniGetUI.Core.SettingsEngine;
5+
using UniGetUI.Core.Tools;
6+
using UniGetUI.Interface.Enums;
7+
8+
namespace UniGetUI.Avalonia.Infrastructure;
9+
10+
/// <summary>
11+
/// A built-in, optional part of UniGetUI that goes beyond package management, such as keeping git clones
12+
/// current. Each is off until turned on from Settings &gt; Extensions, and turns on and off without a restart.
13+
/// </summary>
14+
internal sealed class AppExtension
15+
{
16+
/// <summary>Stable identifier, also the page name the CLI and IPC navigate to.</summary>
17+
public required string Id { get; init; }
18+
19+
/// <summary>The English name, translated where it is shown.</summary>
20+
public required string Name { get; init; }
21+
22+
/// <summary>One English sentence on what it does, translated where it is shown.</summary>
23+
public required string Description { get; init; }
24+
25+
public required IconType Icon { get; init; }
26+
public required Settings.K EnabledSetting { get; init; }
27+
public required PageType Page { get; init; }
28+
29+
/// <summary>Its page under Settings &gt; Extensions, and how to make one without reflection.</summary>
30+
public required Type SettingsPage { get; init; }
31+
public required Func<UserControl> CreateSettingsPage { get; init; }
32+
33+
/// <summary>Starts or stops what it runs in the app, once its setting has changed.</summary>
34+
public required Action<bool> ApplyEnabled { get; init; }
35+
36+
/// <summary>A few words on how it is doing while on, such as "2 behind"; empty when there is nothing to say.</summary>
37+
public Func<string> Status { get; init; } = () => "";
38+
39+
public bool IsEnabled => Settings.Get(EnabledSetting);
40+
41+
public void SetEnabled(bool enabled)
42+
{
43+
Settings.Set(EnabledSetting, enabled);
44+
ApplyEnabled(enabled);
45+
}
46+
}
47+
48+
internal static class AppExtensions
49+
{
50+
public static readonly AppExtension GitRepositories = new()
51+
{
52+
Id = "git-repositories",
53+
Name = "Git Repositories",
54+
Description = "Keep your main git clones up to date and clean up leftover worktrees and branches",
55+
Icon = IconType.GitBranch,
56+
EnabledSetting = Settings.K.EnableGitRepositories,
57+
Page = PageType.GitRepositories,
58+
SettingsPage = typeof(GitRepositoriesSettings),
59+
CreateSettingsPage = () => new GitRepositoriesSettings(),
60+
ApplyEnabled = enabled => MainWindow.Instance?.SetGitRepositoriesEnabled(enabled),
61+
Status = () => MainWindow.Instance?.GitRepositories is not { } git
62+
? ""
63+
: git.BehindCount > 0
64+
? CoreTools.Translate("{0} behind", git.BehindCount)
65+
: git.Repositories.Count switch
66+
{
67+
0 => CoreTools.Translate("No clones yet"),
68+
1 => CoreTools.Translate("1 clone"),
69+
int count => CoreTools.Translate("{0} clones", count),
70+
},
71+
};
72+
73+
/// <summary>Every extension, in the order Settings lists them.</summary>
74+
public static IReadOnlyList<AppExtension> All { get; } = [GitRepositories];
75+
}

‎src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ private static IpcAppInfo GetAppInfo()
260260
{
261261
"catalog" => window is { SoftwareCatalogEnabled: true },
262262
"catalog-editor" => window is { CatalogEditorEnabled: true },
263+
"git-repositories" => window is { GitRepositoriesEnabled: true },
263264
_ => true,
264265
})
265266
.ToArray(),
@@ -305,6 +306,11 @@ private static IpcCommandResult NavigateApp(IpcAppNavigateRequest request)
305306
throw new InvalidOperationException("Catalog Editor is disabled. Enable it in Experimental settings and developer options, then restart UniGetUI.");
306307
window.Navigate(page == "catalog-editor" ? PageType.CatalogEditor : PageType.Catalog);
307308
break;
309+
case "git-repositories":
310+
if (!window.GitRepositoriesEnabled)
311+
throw new InvalidOperationException("Git Repositories is turned off. Turn it on in Settings, under Extensions.");
312+
window.Navigate(PageType.GitRepositories);
313+
break;
308314
case "settings":
309315
window.Navigate(PageType.Settings);
310316
break;

0 commit comments

Comments
 (0)