feat: implement IPC for opening repositories in new tabs (#1185)

* refactor: improve diff handling for EOL changes and enhance text diff display

- Updated `Diff.cs` to streamline whitespace handling in diff arguments.
- Enhanced `DiffContext.cs` to check for EOL changes when old and new hashes differ, creating a text diff if necessary.
- Added support for showing end-of-line symbols in `TextDiffView.axaml.cs` options.

* localization: update translations to include EOF handling in ignore whitespace messages

- Modified the ignore whitespace text in multiple language files to specify that EOF changes are also ignored.
- Ensured consistency across all localization files for the patch application feature.

* revert: Typo in DiffResult comment

* revert: update diff arguments to ignore CR at EOL in whitespace handling (like before changes)

* revert: update translations to remove EOF references in Text.Apply.IgnoreWS and fixed typo in Text.Diff.IgnoreWhitespace (EOF => EOL)

* feat: add workspace-specific default clone directory functionality

- Implemented logic in Clone.cs to set ParentFolder based on the active workspace's DefaultCloneDir if available, falling back to the global GitDefaultCloneDir.
- Added DefaultCloneDir property to Workspace.cs to store the default clone directory for each workspace.
- Updated ConfigureWorkspace.axaml to include a TextBox and Button for setting the DefaultCloneDir in the UI.
- Implemented folder selection functionality in ConfigureWorkspace.axaml.cs to allow users to choose a directory for cloning.
- This closes issue #1145

* feat: implement IPC for opening repositories in new tabs

- Added functionality to send repository paths to an existing instance of the application using named pipes.
- Introduced a new preference option to open repositories in a new tab instead of a new window.
- Updated UI to include a checkbox for the new preference.
- Enhanced the handling of IPC server lifecycle based on the new preference setting.
- This closes issue #1184

---------

Co-authored-by: mpagani <massimo.pagani@unitec-group.com>
This commit is contained in:
Massimo 2025-04-14 13:16:15 +02:00 committed by GitHub
parent 558eb7c9ac
commit 09c0edef8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 157 additions and 3 deletions

View file

@ -2,12 +2,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Pipes;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@ -46,6 +48,8 @@ namespace SourceGit
Environment.Exit(exitTodo); Environment.Exit(exitTodo);
else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage)) else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage))
Environment.Exit(exitMessage); Environment.Exit(exitMessage);
else if (TrySendArgsToExistingInstance(args))
Environment.Exit(0);
else else
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
} }
@ -77,6 +81,44 @@ namespace SourceGit
return builder; return builder;
} }
private static bool TrySendArgsToExistingInstance(string[] args)
{
if (args == null || args.Length != 1 || !Directory.Exists(args[0]))
return false;
var pref = ViewModels.Preferences.Instance;
if (!pref.OpenReposInNewTab)
return false;
try
{
var processes = Process.GetProcessesByName("SourceGit");
if (processes.Length <= 1)
return false;
using var client = new NamedPipeClientStream(".", "SourceGitIPC", PipeDirection.Out);
client.Connect(1000);
if (client.IsConnected)
{
using var writer = new StreamWriter(client);
writer.WriteLine(args[0]);
writer.Flush();
return true;
}
}
catch (Exception)
{
}
return false;
}
private static void LogException(Exception ex) private static void LogException(Exception ex)
{ {
if (ex == null) if (ex == null)
@ -328,7 +370,13 @@ namespace SourceGit
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
var pref = ViewModels.Preferences.Instance; var pref = ViewModels.Preferences.Instance;
pref.PropertyChanged += (_, _) => pref.Save();
pref.PropertyChanged += (s, e) => {
pref.Save();
if (e.PropertyName.Equals(nameof(ViewModels.Preferences.OpenReposInNewTab)))
HandleOpenReposInNewTabChanged();
};
SetLocale(pref.Locale); SetLocale(pref.Locale);
SetTheme(pref.Theme, pref.ThemeOverrides); SetTheme(pref.Theme, pref.ThemeOverrides);
@ -488,13 +536,104 @@ namespace SourceGit
_launcher = new ViewModels.Launcher(startupRepo); _launcher = new ViewModels.Launcher(startupRepo);
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
#if !DISABLE_UPDATE_DETECTION
var pref = ViewModels.Preferences.Instance; var pref = ViewModels.Preferences.Instance;
HandleOpenReposInNewTabChanged();
#if !DISABLE_UPDATE_DETECTION
if (pref.ShouldCheck4UpdateOnStartup()) if (pref.ShouldCheck4UpdateOnStartup())
Check4Update(); Check4Update();
#endif #endif
} }
private void HandleOpenReposInNewTabChanged()
{
var pref = ViewModels.Preferences.Instance;
if (pref.OpenReposInNewTab)
{
if (_ipcServerTask == null || _ipcServerTask.IsCompleted)
{
// Start IPC server
_ipcServerCts = new CancellationTokenSource();
_ipcServerTask = Task.Run(() => StartIPCServer(_ipcServerCts.Token));
}
}
else
{
// Stop IPC server if running
if (_ipcServerCts != null && !_ipcServerCts.IsCancellationRequested)
{
_ipcServerCts.Cancel();
_ipcServerCts.Dispose();
_ipcServerCts = null;
}
_ipcServerTask = null;
}
}
private void StartIPCServer(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
using var server = new NamedPipeServerStream("SourceGitIPC", PipeDirection.In);
// Use WaitForConnectionAsync with cancellation token
try
{
Task connectionTask = server.WaitForConnectionAsync(cancellationToken);
connectionTask.Wait(cancellationToken);
}
catch (OperationCanceledException)
{
return;
}
catch (AggregateException ae) when (ae.InnerExceptions.Any(e => e is OperationCanceledException))
{
return;
}
// Process the connection
using var reader = new StreamReader(server);
var repoPath = reader.ReadLine();
if (!string.IsNullOrEmpty(repoPath) && Directory.Exists(repoPath))
{
Dispatcher.UIThread.Post(() =>
{
try
{
var test = new Commands.QueryRepositoryRootPath(repoPath).ReadToEnd();
if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut))
{
var repoRootPath = test.StdOut.Trim();
var pref = ViewModels.Preferences.Instance;
var node = pref.FindOrAddNodeByRepositoryPath(repoRootPath, null, false);
ViewModels.Welcome.Instance.Refresh();
_launcher?.OpenRepositoryInTab(node, null);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null)
desktop.MainWindow.Activate();
}
}
catch (Exception)
{
}
});
}
}
}
catch (Exception)
{
// Pipe server failed, we can just exit the thread
}
}
private void Check4Update(bool manually = false) private void Check4Update(bool manually = false)
{ {
Task.Run(async () => Task.Run(async () =>
@ -584,5 +723,7 @@ namespace SourceGit
private ResourceDictionary _activeLocale = null; private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null; private ResourceDictionary _themeOverrides = null;
private ResourceDictionary _fontsOverrides = null; private ResourceDictionary _fontsOverrides = null;
private Task _ipcServerTask = null;
private CancellationTokenSource _ipcServerCts = null;
} }
} }

View file

@ -752,4 +752,5 @@
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Lock</x:String> <x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Lock</x:String>
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String> <x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String> <x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
<x:String x:Key="Text.Preferences.General.OpenReposInNewTab" xml:space="preserve">Open repositories in new tab instead of new window</x:String>
</ResourceDictionary> </ResourceDictionary>

View file

@ -90,6 +90,12 @@ namespace SourceGit.ViewModels
} }
} }
public bool OpenReposInNewTab
{
get => _openReposInNewTab;
set => SetProperty(ref _openReposInNewTab, value);
}
public bool UseSystemWindowFrame public bool UseSystemWindowFrame
{ {
get => _useSystemWindowFrame; get => _useSystemWindowFrame;
@ -632,6 +638,7 @@ namespace SourceGit.ViewModels
private string _defaultFontFamily = string.Empty; private string _defaultFontFamily = string.Empty;
private string _monospaceFontFamily = string.Empty; private string _monospaceFontFamily = string.Empty;
private bool _onlyUseMonoFontInEditor = false; private bool _onlyUseMonoFontInEditor = false;
private bool _openReposInNewTab = false;
private bool _useSystemWindowFrame = false; private bool _useSystemWindowFrame = false;
private double _defaultFontSize = 13; private double _defaultFontSize = 13;
private double _editorFontSize = 13; private double _editorFontSize = 13;

View file

@ -46,7 +46,7 @@
<TabItem.Header> <TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.General}"/> <TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.General}"/>
</TabItem.Header> </TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*"> <Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0" <TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preferences.General.Locale}" Text="{DynamicResource Text.Preferences.General.Locale}"
HorizontalAlignment="Right" HorizontalAlignment="Right"
@ -142,6 +142,11 @@
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowChildren, Mode=TwoWay}"/> IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowChildren, Mode=TwoWay}"/>
<CheckBox Grid.Row="8" Grid.Column="1" <CheckBox Grid.Row="8" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.OpenReposInNewTab}"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=OpenReposInNewTab, Mode=TwoWay}"/>
<CheckBox Grid.Row="9" Grid.Column="1"
Height="32" Height="32"
Content="{DynamicResource Text.Preferences.General.Check4UpdatesOnStartup}" Content="{DynamicResource Text.Preferences.General.Check4UpdatesOnStartup}"
IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"