diff --git a/CHANGELOG.md b/CHANGELOG.md
index dcd6e56b75..95dea03b33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,19 @@ Fork releases of SourceGit-SK. Upstream changes arrive through upstream release
## [Unreleased]
+### Added
+
+- The stacked multi-file diff now covers every change view: commit details (History), compare, revision compare, submodule compare, and stashes all show the whole change set as one scrollable diff when no file is selected, stack a multi-selection, and show a single file when one is selected ([#8](https://github.com/skoonin/sourcegit-sk/pull/8))
+- "Open Another Repository..." entry at the bottom of the repository switcher (Ctrl/Cmd+P) opens a folder picker to open a repo not yet in the list ([#8](https://github.com/skoonin/sourcegit-sk/pull/8))
+
### Changed
- Local-changes diff view is now selection-driven: selecting a single file shows just that file, selecting several stacks those files, and the continuous stacked view of all changes appears when nothing is selected. The continuous-diff toggle has been removed.
+### Fixed
+
+- Clicking blank space below a file list clears the selection (returning the diff panel to the whole-changeset view) ([#8](https://github.com/skoonin/sourcegit-sk/pull/8))
+
## [2026.15-sk] - 2026-07-11
First fork release, based on upstream [v2026.14](https://github.com/sourcegit-scm/sourcegit/releases/tag/v2026.14).
diff --git a/TRANSLATION.md b/TRANSLATION.md
index ea71c2b346..495092dc97 100644
--- a/TRANSLATION.md
+++ b/TRANSLATION.md
@@ -6,7 +6,7 @@ This document shows the translation status of each locale file in the repository
### 
-### 
+### 
Missing keys in de_DE.axaml
@@ -88,6 +88,7 @@ This document shows the translation status of each locale file in the repository
- Text.Init.CommandTip
- Text.Init.ErrorMessageTip
- Text.InteractiveRebase.NoVerify
+- Text.Launcher.OpenOtherRepository
- Text.Merge.Test
- Text.Merge.Test.NoConflicts
- Text.Merge.Test.UnknownError
@@ -124,7 +125,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in el_GR.axaml
@@ -162,6 +163,7 @@ This document shows the translation status of each locale file in the repository
- Text.GitFlow.FinishWithRebase
- Text.GitFlow.StartAt
- Text.GitFlow.StartName
+- Text.Launcher.OpenOtherRepository
- Text.Repository.Commits
- Text.TagCM.Checkout
- Text.TagCM.Merge
@@ -170,7 +172,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in es_ES.axaml
@@ -193,12 +195,13 @@ This document shows the translation status of each locale file in the repository
- Text.FileModeChange.Submodule
- Text.FileModeChange.Symlink
- Text.FileModeChange.Unknown
+- Text.Launcher.OpenOtherRepository
- Text.Repository.Commits
- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder
-### 
+### 
Missing keys in fr_FR.axaml
@@ -238,6 +241,7 @@ This document shows the translation status of each locale file in the repository
- Text.GitFlow.FinishWithRebase
- Text.GitFlow.StartAt
- Text.GitFlow.StartName
+- Text.Launcher.OpenOtherRepository
- Text.Merge.Test
- Text.Merge.Test.NoConflicts
- Text.Merge.Test.UnknownError
@@ -255,7 +259,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in he_IL.axaml
@@ -295,6 +299,7 @@ This document shows the translation status of each locale file in the repository
- Text.GitFlow.FinishWithRebase
- Text.GitFlow.StartAt
- Text.GitFlow.StartName
+- Text.Launcher.OpenOtherRepository
- Text.Merge.Test
- Text.Merge.Test.NoConflicts
- Text.Merge.Test.UnknownError
@@ -312,7 +317,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in id_ID.axaml
@@ -425,6 +430,7 @@ This document shows the translation status of each locale file in the repository
- Text.Init.ErrorMessageTip
- Text.InteractiveRebase.NoVerify
- Text.Launcher.Commands
+- Text.Launcher.OpenOtherRepository
- Text.Launcher.OpenRepository
- Text.Merge.Test
- Text.Merge.Test.NoConflicts
@@ -502,7 +508,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in it_IT.axaml
@@ -587,6 +593,7 @@ This document shows the translation status of each locale file in the repository
- Text.Init.CommandTip
- Text.Init.ErrorMessageTip
- Text.InteractiveRebase.NoVerify
+- Text.Launcher.OpenOtherRepository
- Text.Merge.Test
- Text.Merge.Test.NoConflicts
- Text.Merge.Test.UnknownError
@@ -625,7 +632,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in ja_JP.axaml
@@ -637,11 +644,12 @@ This document shows the translation status of each locale file in the repository
- Text.Diff.LastFile
- Text.Diff.NextFile
- Text.Diff.PrevFile
+- Text.Launcher.OpenOtherRepository
- Text.Repository.Commits
-### 
+### 
Missing keys in ko_KR.axaml
@@ -675,6 +683,7 @@ This document shows the translation status of each locale file in the repository
- Text.GitFlow.FinishWithRebase
- Text.GitFlow.StartAt
- Text.GitFlow.StartName
+- Text.Launcher.OpenOtherRepository
- Text.Repository.Commits
- Text.TagCM.Checkout
- Text.TagCM.Merge
@@ -683,7 +692,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in pt_BR.axaml
@@ -889,6 +898,7 @@ This document shows the translation status of each locale file in the repository
- Text.InteractiveRebase.NoVerify
- Text.InteractiveRebase.ReorderTip
- Text.Launcher.Commands
+- Text.Launcher.OpenOtherRepository
- Text.Launcher.OpenRepository
- Text.Launcher.Pages
- Text.Launcher.Workspaces
@@ -1076,7 +1086,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in ru_RU.axaml
@@ -1099,12 +1109,13 @@ This document shows the translation status of each locale file in the repository
- Text.FileModeChange.Submodule
- Text.FileModeChange.Symlink
- Text.FileModeChange.Unknown
+- Text.Launcher.OpenOtherRepository
- Text.Repository.Commits
- Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder
-### 
+### 
Missing keys in ta_IN.axaml
@@ -1319,6 +1330,7 @@ This document shows the translation status of each locale file in the repository
- Text.InteractiveRebase.NoVerify
- Text.InteractiveRebase.ReorderTip
- Text.Launcher.Commands
+- Text.Launcher.OpenOtherRepository
- Text.Launcher.OpenRepository
- Text.Launcher.Pages
- Text.Launcher.Workspaces
@@ -1478,7 +1490,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in uk_UA.axaml
@@ -1689,6 +1701,7 @@ This document shows the translation status of each locale file in the repository
- Text.InteractiveRebase.NoVerify
- Text.InteractiveRebase.ReorderTip
- Text.Launcher.Commands
+- Text.Launcher.OpenOtherRepository
- Text.Launcher.OpenRepository
- Text.Launcher.Pages
- Text.Launcher.Workspaces
@@ -1844,7 +1857,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in zh_CN.axaml
@@ -1857,11 +1870,12 @@ This document shows the translation status of each locale file in the repository
- Text.Diff.LastFile
- Text.Diff.NextFile
- Text.Diff.PrevFile
+- Text.Launcher.OpenOtherRepository
- Text.Repository.Commits
-### 
+### 
Missing keys in zh_TW.axaml
@@ -1874,6 +1888,7 @@ This document shows the translation status of each locale file in the repository
- Text.Diff.LastFile
- Text.Diff.NextFile
- Text.Diff.PrevFile
+- Text.Launcher.OpenOtherRepository
- Text.Repository.Commits
\ No newline at end of file
diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh
index 262f141ae8..3c923a94ca 100755
--- a/build/scripts/package.linux.sh
+++ b/build/scripts/package.linux.sh
@@ -82,5 +82,7 @@ sed -i -e "s/^Version:.*/Version: $VERSION/" \
# Build deb package with gzip compression
dpkg-deb -Zgzip --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb"
-rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION"
-mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./
+# RPM forbids '-' in Version (reserved for the Release separator); the fork's -sk/-sk-dev suffix must use dots.
+rpm_version=${VERSION//-/.}
+rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $rpm_version"
+mv "resources/rpm/RPMS/$target/sourcegit-$rpm_version-1.$target.rpm" ./
diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml
index be16767c2d..ef68a67095 100644
--- a/src/Resources/Locales/en_US.axaml
+++ b/src/Resources/Locales/en_US.axaml
@@ -587,6 +587,7 @@
CommandsERRORNOTICE
+ Open Another Repository...Open RepositoriesTabsWorkspaces
diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs
index 5d543b65c2..65c3059b2f 100644
--- a/src/ViewModels/CommitDetail.cs
+++ b/src/ViewModels/CommitDetail.cs
@@ -41,8 +41,8 @@ public int ActiveTabIndex
{
_sharedData.ActiveTabIndex = value;
- if (value == 1 && DiffContext == null && _selectedChanges is { Count: 1 })
- DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, _selectedChanges[0]));
+ if (value == 1 && DetailContext == null)
+ UpdateDetail();
}
}
}
@@ -102,19 +102,14 @@ public List SelectedChanges
set
{
if (SetProperty(ref _selectedChanges, value))
- {
- if (ActiveTabIndex != 1 || value is not { Count: 1 })
- DiffContext = null;
- else
- DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, value[0]), _diffContext);
- }
+ UpdateDetail();
}
}
- public DiffContext DiffContext
+ public object DetailContext
{
- get => _diffContext;
- private set => SetProperty(ref _diffContext, value);
+ get => _detailContext;
+ private set => SetProperty(ref _detailContext, value);
}
public string SearchChangeFilter
@@ -479,7 +474,7 @@ private void Refresh()
{
Changes = [];
VisibleChanges = [];
- SelectedChanges = null;
+ ResetSelection();
return;
}
@@ -551,10 +546,8 @@ private void Refresh()
Changes = changes;
VisibleChanges = visible;
- if (visible.Count == 0)
- SelectedChanges = null;
- else
- SelectedChanges = [VisibleChanges[0]];
+ // No auto-selected file: an empty selection shows the whole commit as a stacked diff.
+ ResetSelection();
});
}
}, token);
@@ -628,6 +621,42 @@ private void RefreshVisibleChanges()
VisibleChanges = visible;
}
+
+ // The whole-set stack tracks the filtered list, but rebuilding it per keystroke would spawn
+ // a git diff per visible file; coalesce until typing pauses.
+ if (_selectedChanges is not { Count: > 0 })
+ {
+ _filterDebounce ??= new Debouncer(TimeSpan.FromMilliseconds(300), () =>
+ {
+ if (_selectedChanges is not { Count: > 0 })
+ UpdateDetail();
+ });
+ _filterDebounce.Trigger();
+ }
+ }
+
+ private void ResetSelection()
+ {
+ // Setting null on an already-null selection raises no change event, so force the rebuild
+ // that keys the detail to the new commit.
+ var hadSelection = _selectedChanges is { Count: > 0 };
+ SelectedChanges = null;
+ if (!hadSelection)
+ UpdateDetail();
+ }
+
+ // Detail is lazy until the Changes tab (index 1) is visible.
+ private void UpdateDetail()
+ {
+ if (ActiveTabIndex != 1 || _commit == null)
+ DetailContext = null;
+ else
+ DetailContext = MultipleDiffContext.BuildDetail(_repo.FullPath, MakeDiffOption, _selectedChanges, _visibleChanges, _detailContext);
+ }
+
+ private Models.DiffOption MakeDiffOption(Models.Change change)
+ {
+ return new Models.DiffOption(_commit, change);
}
private void RefreshRevisionSearchSuggestion()
@@ -768,7 +797,8 @@ private async Task SetViewingCommitAsync(Models.Object file)
private List _visibleChanges = [];
private List _selectedChanges = null;
private string _searchChangeFilter = string.Empty;
- private DiffContext _diffContext = null;
+ private object _detailContext = null;
+ private Debouncer _filterDebounce = null;
private string _viewRevisionFilePath = string.Empty;
private object _viewRevisionFileContent = null;
private CancellationTokenSource _cancellationSource = null;
diff --git a/src/ViewModels/Compare.cs b/src/ViewModels/Compare.cs
index 9a3f4f4906..a1278c2d0f 100644
--- a/src/ViewModels/Compare.cs
+++ b/src/ViewModels/Compare.cs
@@ -74,12 +74,7 @@ public List SelectedChanges
set
{
if (SetProperty(ref _selectedChanges, value))
- {
- if (value is { Count: 1 })
- DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_based, _to, value[0]), _diffContext);
- else
- DiffContext = null;
- }
+ UpdateDetail();
}
}
@@ -93,10 +88,10 @@ public string SearchFilter
}
}
- public DiffContext DiffContext
+ public object DetailContext
{
- get => _diffContext;
- private set => SetProperty(ref _diffContext, value);
+ get => _detailContext;
+ private set => SetProperty(ref _detailContext, value);
}
public List LeftOnlyCommits
@@ -334,10 +329,8 @@ private void UpdateChanges()
VisibleChanges = visible;
IsLoadingChanges = false;
- if (VisibleChanges.Count > 0)
- SelectedChanges = [VisibleChanges[0]];
- else
- SelectedChanges = [];
+ // No auto-selected file: an empty selection shows the whole comparison as a stacked diff.
+ SelectedChanges = [];
});
});
}
@@ -362,6 +355,28 @@ private void RefreshVisibleChanges()
VisibleChanges = visible;
}
+
+ // The whole-set stack tracks the filtered list, but rebuilding it per keystroke would spawn
+ // a git diff per visible file; coalesce until typing pauses.
+ if (_selectedChanges is not { Count: > 0 })
+ {
+ _filterDebounce ??= new Debouncer(TimeSpan.FromMilliseconds(300), () =>
+ {
+ if (_selectedChanges is not { Count: > 0 })
+ UpdateDetail();
+ });
+ _filterDebounce.Trigger();
+ }
+ }
+
+ private void UpdateDetail()
+ {
+ DetailContext = MultipleDiffContext.BuildDetail(_repo.FullPath, MakeDiffOption, _selectedChanges, _visibleChanges, _detailContext);
+ }
+
+ private Models.DiffOption MakeDiffOption(Models.Change change)
+ {
+ return new Models.DiffOption(_based, _to, change);
}
private string GetName(object obj)
@@ -404,6 +419,7 @@ private string GetSHA(object obj)
private List _leftOnlyCommits = [];
private List _rightOnlyCommits = [];
private string _searchFilter = string.Empty;
- private DiffContext _diffContext = null;
+ private object _detailContext = null;
+ private Debouncer _filterDebounce = null;
}
}
diff --git a/src/ViewModels/Debouncer.cs b/src/ViewModels/Debouncer.cs
new file mode 100644
index 0000000000..95dd3162c6
--- /dev/null
+++ b/src/ViewModels/Debouncer.cs
@@ -0,0 +1,30 @@
+using System;
+
+using Avalonia.Threading;
+
+namespace SourceGit.ViewModels
+{
+ // Coalesces rapid triggers (e.g. per-keystroke filter changes) into one action after a quiet
+ // period, so expensive rebuilds like the stacked diff do not run for every character typed.
+ // UI-thread only: both Trigger() and the action run on the Avalonia dispatcher.
+ public class Debouncer
+ {
+ public Debouncer(TimeSpan delay, Action action)
+ {
+ _timer = new DispatcherTimer { Interval = delay };
+ _timer.Tick += (_, _) =>
+ {
+ _timer.Stop();
+ action();
+ };
+ }
+
+ public void Trigger()
+ {
+ _timer.Stop();
+ _timer.Start();
+ }
+
+ private readonly DispatcherTimer _timer;
+ }
+}
diff --git a/src/ViewModels/LauncherPagesCommandPalette.cs b/src/ViewModels/LauncherPagesCommandPalette.cs
index 265aa07040..bffdde74ce 100644
--- a/src/ViewModels/LauncherPagesCommandPalette.cs
+++ b/src/ViewModels/LauncherPagesCommandPalette.cs
@@ -3,6 +3,9 @@
namespace SourceGit.ViewModels
{
+ // Sentinel row rendered at the end of the repository list; activating it browses for a repo not yet known.
+ public sealed class OpenOtherRepositoryAction { }
+
public class LauncherPagesCommandPalette : ICommandPalette
{
public List VisiblePages
@@ -11,7 +14,8 @@ public List VisiblePages
private set => SetProperty(ref _visiblePages, value);
}
- public List VisibleRepos
+ // Repository nodes plus a trailing OpenOtherRepositoryAction sentinel.
+ public List