diff --git a/README.md b/README.md index 4086a641..c932ec8a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ ## Translation Status -[![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-99.08%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-91.68%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-99.87%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-91.41%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)](TRANSLATION.md) +[![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-99.07%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-91.66%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-99.87%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-91.39%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)](TRANSLATION.md) > [!NOTE] > You can find the missing keys in [TRANSLATION.md](TRANSLATION.md) @@ -79,7 +79,7 @@ For **Windows** users: ``` > [!NOTE] > `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar. -* You can install the latest stable by `scoope` with follow commands: +* You can install the latest stable by `scoop` with follow commands: ```shell scoop bucket add extras scoop install sourcegit diff --git a/TRANSLATION.md b/TRANSLATION.md index a5abbbba..e3f9d2a1 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -1,4 +1,4 @@ -### de_DE.axaml: 99.08% +### de_DE.axaml: 99.07%
@@ -24,7 +24,7 @@
-### fr_FR.axaml: 91.68% +### fr_FR.axaml: 91.66%
@@ -106,7 +106,7 @@
-### pt_BR.axaml: 91.41% +### pt_BR.axaml: 91.39%
diff --git a/VERSION b/VERSION index a75bd422..23993bfb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.08 \ No newline at end of file +2025.09 \ No newline at end of file diff --git a/build/scripts/package.windows.sh b/build/scripts/package.windows.sh index 1a8f99c1..c22a9d35 100755 --- a/build/scripts/package.windows.sh +++ b/build/scripts/package.windows.sh @@ -10,7 +10,7 @@ cd build rm -rf SourceGit/*.pdb if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then - powershell -Command "Compress-Archive -Path SourceGit\\* -DestinationPath \"sourcegit_$VERSION.$RUNTIME.zip\" -Force" + powershell -Command "Compress-Archive -Path SourceGit -DestinationPath \"sourcegit_$VERSION.$RUNTIME.zip\" -Force" else - zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit + zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit fi diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 25e32323..f59d35db 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -77,6 +77,31 @@ namespace SourceGit Native.OS.SetupApp(builder); return builder; } + + private static void LogException(Exception ex) + { + if (ex == null) + return; + + var builder = new StringBuilder(); + builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n"); + builder.Append("----------------------------\n"); + builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n"); + builder.Append($"OS: {Environment.OSVersion}\n"); + builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n"); + builder.Append($"Source: {ex.Source}\n"); + builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n"); + builder.Append($"User: {Environment.UserName}\n"); + builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n"); + builder.Append($"Exception Time: {DateTime.Now}\n"); + builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n"); + builder.Append($"---------------------------\n\n"); + builder.Append(ex); + + var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); + File.WriteAllText(file, builder.ToString()); + } #endregion #region Utility Functions @@ -181,6 +206,9 @@ namespace SourceGit app._fontsOverrides = null; } + defaultFont = app.FixFontFamilyName(defaultFont); + monospaceFont = app.FixFontFamilyName(monospaceFont); + var resDic = new ResourceDictionary(); if (!string.IsNullOrEmpty(defaultFont)) resDic.Add("Fonts.Default", new FontFamily(defaultFont)); @@ -325,31 +353,6 @@ namespace SourceGit } #endregion - private static void LogException(Exception ex) - { - if (ex == null) - return; - - var builder = new StringBuilder(); - builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n"); - builder.Append("----------------------------\n"); - builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n"); - builder.Append($"OS: {Environment.OSVersion}\n"); - builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n"); - builder.Append($"Source: {ex.Source}\n"); - builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n"); - builder.Append($"User: {Environment.UserName}\n"); - builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n"); - builder.Append($"Exception Time: {DateTime.Now}\n"); - builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n"); - builder.Append($"---------------------------\n\n"); - builder.Append(ex); - - var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); - File.WriteAllText(file, builder.ToString()); - } - private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) { exitCode = -1; @@ -546,6 +549,24 @@ namespace SourceGit }); } + private string FixFontFamilyName(string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + var parts = input.Split(','); + var trimmed = new List(); + + foreach (var part in parts) + { + var t = part.Trim(); + if (!string.IsNullOrEmpty(t)) + trimmed.Add(t); + } + + return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; + } + private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; private ResourceDictionary _themeOverrides = null; diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 391aeeb2..2dc8a98d 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -54,21 +54,14 @@ public static bool DeleteRemote(string repo, string remote, string name) { + bool exists = new Remote(repo).HasBranch(remote, name); + if (exists) + return new Push(repo, remote, $"refs/heads/{name}", true).Exec(); + var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - - bool exists = new Remote(repo).HasBranch(remote, name); - if (exists) - { - cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - cmd.Args = $"push {remote} --delete {name}"; - } - else - { - cmd.Args = $"branch -D -r {remote}/{name}"; - } - + cmd.Args = $"branch -D -r {remote}/{name}"; return cmd.Exec(); } } diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 3f61de17..0fef1235 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using Avalonia.Threading; @@ -10,11 +11,6 @@ namespace SourceGit.Commands { public partial class Command { - public class CancelToken - { - public bool Requested { get; set; } = false; - } - public class ReadToEndResult { public bool IsSuccess { get; set; } = false; @@ -30,7 +26,7 @@ namespace SourceGit.Commands } public string Context { get; set; } = string.Empty; - public CancelToken Cancel { get; set; } = null; + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; public string WorkingDirectory { get; set; } = null; public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode public string SSHKey { get; set; } = string.Empty; @@ -43,36 +39,15 @@ namespace SourceGit.Commands var start = CreateGitStartInfo(); var errs = new List(); var proc = new Process() { StartInfo = start }; - var isCancelled = false; proc.OutputDataReceived += (_, e) => { - if (Cancel != null && Cancel.Requested) - { - isCancelled = true; - proc.CancelErrorRead(); - proc.CancelOutputRead(); - if (!proc.HasExited) - proc.Kill(true); - return; - } - if (e.Data != null) OnReadline(e.Data); }; proc.ErrorDataReceived += (_, e) => { - if (Cancel != null && Cancel.Requested) - { - isCancelled = true; - proc.CancelErrorRead(); - proc.CancelOutputRead(); - if (!proc.HasExited) - proc.Kill(true); - return; - } - if (string.IsNullOrEmpty(e.Data)) { errs.Add(string.Empty); @@ -97,9 +72,25 @@ namespace SourceGit.Commands errs.Add(e.Data); }; + var dummy = null as Process; + var dummyProcLock = new object(); try { proc.Start(); + + // It not safe, please only use `CancellationToken` in readonly commands. + if (CancellationToken.CanBeCanceled) + { + dummy = proc; + CancellationToken.Register(() => + { + lock (dummyProcLock) + { + if (dummy is { HasExited: false }) + dummy.Kill(); + } + }); + } } catch (Exception e) { @@ -113,10 +104,18 @@ namespace SourceGit.Commands proc.BeginErrorReadLine(); proc.WaitForExit(); + if (dummy != null) + { + lock (dummyProcLock) + { + dummy = null; + } + } + int exitCode = proc.ExitCode; proc.Close(); - if (!isCancelled && exitCode != 0) + if (!CancellationToken.IsCancellationRequested && exitCode != 0) { if (RaiseError) { @@ -192,13 +191,12 @@ namespace SourceGit.Commands if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey)) start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'"); - // Force using en_US.UTF-8 locale to avoid GCM crash + // Force using en_US.UTF-8 locale if (OperatingSystem.IsLinux()) - start.Environment.Add("LANG", "en_US.UTF-8"); - - // Fix macOS `PATH` env - if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv)) - start.Environment.Add("PATH", Native.OS.CustomPathEnv); + { + start.Environment.Add("LANG", "C"); + start.Environment.Add("LC_ALL", "C"); + } // Force using this app as git editor. switch (Editor) diff --git a/src/Commands/CountLocalChangesWithoutUntracked.cs b/src/Commands/CountLocalChangesWithoutUntracked.cs index afb62840..7ab9a54a 100644 --- a/src/Commands/CountLocalChangesWithoutUntracked.cs +++ b/src/Commands/CountLocalChangesWithoutUntracked.cs @@ -8,7 +8,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "status -uno --ignore-submodules=dirty --porcelain"; + Args = "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain"; } public int Result() diff --git a/src/Commands/ExecuteCustomAction.cs b/src/Commands/ExecuteCustomAction.cs index 7775da34..000c8fd1 100644 --- a/src/Commands/ExecuteCustomAction.cs +++ b/src/Commands/ExecuteCustomAction.cs @@ -17,14 +17,6 @@ namespace SourceGit.Commands start.CreateNoWindow = true; start.WorkingDirectory = repo; - // Force using en_US.UTF-8 locale to avoid GCM crash - if (OperatingSystem.IsLinux()) - start.Environment.Add("LANG", "en_US.UTF-8"); - - // Fix macOS `PATH` env - if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv)) - start.Environment.Add("PATH", Native.OS.CustomPathEnv); - try { Process.Start(start); @@ -48,14 +40,6 @@ namespace SourceGit.Commands start.StandardErrorEncoding = Encoding.UTF8; start.WorkingDirectory = repo; - // Force using en_US.UTF-8 locale to avoid GCM crash - if (OperatingSystem.IsLinux()) - start.Environment.Add("LANG", "en_US.UTF-8"); - - // Fix macOS `PATH` env - if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv)) - start.Environment.Add("PATH", Native.OS.CustomPathEnv); - var proc = new Process() { StartInfo = start }; var builder = new StringBuilder(); diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index 69b859ab..dc81f606 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -26,7 +26,7 @@ namespace SourceGit.Commands Args += $"{remote} {local}:{remoteBranch}"; } - public Push(string repo, string remote, string tag, bool isDelete) + public Push(string repo, string remote, string refname, bool isDelete) { WorkingDirectory = repo; Context = repo; @@ -36,7 +36,7 @@ namespace SourceGit.Commands if (isDelete) Args += "--delete "; - Args += $"{remote} refs/tags/{tag}"; + Args += $"{remote} {refname}"; } protected override void OnReadline(string line) diff --git a/src/Commands/QueryCommitChildren.cs b/src/Commands/QueryCommitChildren.cs index d1bced52..6a6ed909 100644 --- a/src/Commands/QueryCommitChildren.cs +++ b/src/Commands/QueryCommitChildren.cs @@ -9,7 +9,7 @@ namespace SourceGit.Commands WorkingDirectory = repo; Context = repo; _commit = commit; - Args = $"rev-list -{max} --parents --branches --remotes ^{commit}"; + Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path={commit} ^{commit}"; } public List Result() diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index ea422215..9458e5f5 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -13,7 +13,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain"; + Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain"; } public List Result() diff --git a/src/Commands/QueryRevisionFileNames.cs b/src/Commands/QueryRevisionFileNames.cs index d2d69614..c6fd7373 100644 --- a/src/Commands/QueryRevisionFileNames.cs +++ b/src/Commands/QueryRevisionFileNames.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Collections.Generic; + +namespace SourceGit.Commands { public class QueryRevisionFileNames : Command { @@ -9,13 +11,17 @@ Args = $"ls-tree -r -z --name-only {revision}"; } - public string[] Result() + public List Result() { var rs = ReadToEnd(); - if (rs.IsSuccess) - return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries); + if (!rs.IsSuccess) + return []; - return []; + var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries); + var outs = new List(); + foreach (var line in lines) + outs.Add(line); + return outs; } } } diff --git a/src/Commands/QuerySubmodules.cs b/src/Commands/QuerySubmodules.cs index 6ebfa8b1..1ceccf78 100644 --- a/src/Commands/QuerySubmodules.cs +++ b/src/Commands/QuerySubmodules.cs @@ -49,7 +49,7 @@ namespace SourceGit.Commands if (submodules.Count > 0) { - Args = $"status -uno --porcelain -- {builder}"; + Args = $"--no-optional-locks status -uno --porcelain -- {builder}"; rs = ReadToEnd(); if (!rs.IsSuccess) return submodules; diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs index fa11e366..23dbb11c 100644 --- a/src/Commands/Tag.cs +++ b/src/Commands/Tag.cs @@ -48,9 +48,7 @@ namespace SourceGit.Commands if (remotes != null) { foreach (var r in remotes) - { - new Push(repo, r.Name, name, true).Exec(); - } + new Push(repo, r.Name, $"refs/tags/{name}", true).Exec(); } return true; diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 5c48b0c0..0bad8376 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -8,6 +8,7 @@ namespace SourceGit.Models { public enum CommitSearchMethod { + BySHA = 0, ByAuthor, ByCommitter, ByMessage, @@ -35,6 +36,7 @@ namespace SourceGit.Models public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime); public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime); public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly); + public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly); public bool IsMerged { get; set; } = false; public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; diff --git a/src/Models/DealWithLocalChanges.cs b/src/Models/DealWithLocalChanges.cs deleted file mode 100644 index f308a90c..00000000 --- a/src/Models/DealWithLocalChanges.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SourceGit.Models -{ - public enum DealWithLocalChanges - { - DoNothing, - StashAndReaply, - Discard, - } -} diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 08bf48ca..76a7f160 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -471,11 +471,7 @@ namespace SourceGit.Models public CustomAction AddNewCustomAction() { - var act = new CustomAction() - { - Name = "Unnamed Custom Action", - }; - + var act = new CustomAction() { Name = "Unnamed Action" }; CustomActions.Add(act); return act; } diff --git a/src/Models/User.cs b/src/Models/User.cs index 850bcf2f..066ab747 100644 --- a/src/Models/User.cs +++ b/src/Models/User.cs @@ -43,6 +43,11 @@ namespace SourceGit.Models return _caches.GetOrAdd(data, key => new User(key)); } + public override string ToString() + { + return $"{Name} <{Email}>"; + } + private static ConcurrentDictionary _caches = new ConcurrentDictionary(); private readonly int _hash; } diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index 633ef5eb..123b160b 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -18,9 +18,22 @@ namespace SourceGit.Native DisableDefaultApplicationMenuItems = true, }); + // Fix `PATH` env on macOS. + var path = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrEmpty(path)) + path = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"; + else if (!path.Contains("/opt/homebrew/", StringComparison.Ordinal)) + path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path; + var customPathFile = Path.Combine(OS.DataDir, "PATH"); if (File.Exists(customPathFile)) - OS.CustomPathEnv = File.ReadAllText(customPathFile).Trim(); + { + var env = File.ReadAllText(customPathFile).Trim(); + if (!string.IsNullOrEmpty(env)) + path = env; + } + + Environment.SetEnvironmentVariable("PATH", path); } public string FindGitExecutable() diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 3a688654..f11d1e7f 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -31,12 +31,6 @@ namespace SourceGit.Native private set; } = string.Empty; - public static string CustomPathEnv - { - get; - set; - } = string.Empty; - public static string GitExecutable { get => _gitExecutable; diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index 11b6bd13..eb354f10 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -8,6 +8,7 @@ using System.Text; using Avalonia; using Avalonia.Controls; +using Avalonia.Threading; namespace SourceGit.Native { @@ -214,12 +215,17 @@ namespace SourceGit.Native private void FixWindowFrameOnWin10(Window w) { - var platformHandle = w.TryGetPlatformHandle(); - if (platformHandle == null) - return; + // Schedule the DWM frame extension to run in the next render frame + // to ensure proper timing with the window initialization sequence + Dispatcher.UIThread.InvokeAsync(() => + { + var platformHandle = w.TryGetPlatformHandle(); + if (platformHandle == null) + return; - var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 }; - DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins); + var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 }; + DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins); + }, DispatcherPriority.Render); } #region EXTERNAL_EDITOR_FINDER diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 1d0ca895..aff8ffc5 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -90,7 +90,6 @@ Branch: Lokale Änderungen: Verwerfen - Nichts tun Stashen & wieder anwenden Cherry Pick Quelle an Commit-Nachricht anhängen @@ -206,7 +205,6 @@ Erstellten Branch auschecken Lokale Änderungen: Verwerfen - Nichts tun Stashen & wieder anwenden Neuer Branch-Name: Branch-Namen eingeben. @@ -445,6 +443,7 @@ Einfügen Gerade eben Vor {0} Minuten + Vor 1 Stunde Vor {0} Stunden Gestern Vor {0} Tagen @@ -517,7 +516,6 @@ Lokaler Branch: Lokale Änderungen: Verwerfen - Nichts tun Stashen & wieder anwenden Ohne Tags fetchen Remote: diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 5df3ca71..818bd9bb 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -88,7 +88,6 @@ Branch: Local Changes: Discard - Do Nothing Stash & Reapply Cherry Pick Append source to commit message @@ -205,7 +204,6 @@ Check out the created branch Local Changes: Discard - Do Nothing Stash & Reapply New Branch Name: Enter branch name. @@ -446,6 +444,7 @@ Paste Just now {0} minutes ago + 1 hour ago {0} hours ago Yesterday {0} days ago @@ -520,7 +519,6 @@ Into: Local Changes: Discard - Do Nothing Stash & Reapply Fetch without tags Remote: diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 4073f512..e909a14e 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -91,7 +91,6 @@ Rama: Cambios Locales: Descartar - No Hacer Nada Stash & Reaplicar Cherry Pick Añadir fuente al mensaje de commit @@ -208,7 +207,6 @@ Checkout de la rama creada Cambios Locales: Descartar - No Hacer Nada Stash & Reaplicar Nombre de la Nueva Rama: Introduzca el nombre de la rama. @@ -449,6 +447,7 @@ Pegar Justo ahora Hace {0} minutos + Hace 1 hora Hace {0} horas Ayer Hace {0} días @@ -524,7 +523,6 @@ En: Cambios Locales: Descartar - No Hacer Nada Stash & Reaplicar Fetch sin etiquetas Remoto: diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 59fe00a8..aecea9ad 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -83,7 +83,6 @@ Branche : Changements locaux : Annuler - Ne rien faire Mettre en stash et réappliquer Cherry-Pick de ce commit Ajouter la source au message de commit @@ -198,7 +197,6 @@ Récupérer la branche créée Changements locaux : Rejeter - Ne rien faire Stash & Réappliquer Nom de la nouvelle branche : Entrez le nom de la branche. @@ -424,6 +422,7 @@ Coller A l'instant il y a {0} minutes + il y a 1 heure il y a {0} heures Hier il y a {0} jours @@ -492,7 +491,6 @@ Dans : Changements locaux : Rejeter - Ne rien faire Stash & Réappliquer Fetch sans les tags Dépôt distant : diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index 6e99decf..4dcc8771 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -91,7 +91,6 @@ Branch: Modifiche Locali: Scarta - Non fare nulla Stasha e Ripristina Cherry Pick Aggiungi sorgente al messaggio di commit @@ -208,7 +207,6 @@ Checkout del Branch Creato Modifiche Locali: Scarta - Non Fare Nulla Stasha e Ripristina Nome Nuovo Branch: Inserisci il nome del branch. @@ -450,6 +448,7 @@ Incolla Proprio ora {0} minuti fa + 1 ora fa {0} ore fa Ieri {0} giorni fa @@ -523,7 +522,6 @@ In: Modifiche Locali: Scarta - Non fare nulla Stasha e Riapplica Recupera senza tag Remoto: diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 78445bfb..b146bf0e 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -107,7 +107,6 @@ Branch: Alterações Locais: Descartar - Nada Stash & Reaplicar Cherry-Pick Adicionar origem à mensagem de commit @@ -215,7 +214,6 @@ Checar o branch criado Alterações Locais: Descartar - Não Fazer Nada Guardar & Reaplicar Nome do Novo Branch: Insira o nome do branch. @@ -437,6 +435,7 @@ Colar Agora mesmo {0} minutos atrás + 1 hora atrás {0} horas atrás Ontem {0} dias atrás @@ -506,7 +505,6 @@ Para: Alterações Locais: Descartar - Não Fazer Nada Guardar & Reaplicar Buscar sem tags Remoto: diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index b15a769b..07fb7c94 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -11,9 +11,9 @@ • Исходный код можно найти по адресу Бесплатный графический клиент Git с исходным кодом Добавить рабочий каталог - Что проверить: - Существующую ветку - Создать новую ветку + Переключиться на: + ветку из списка + создать новую ветку Расположение: Путь к рабочему каталогу (поддерживается относительный путь) Имя ветки: @@ -91,7 +91,6 @@ Ветка: Локальные изменения: Отклонить - Ничего не делать Отложить и примненить повторно Частичный выбор Добавить источник для ревизии сообщения @@ -126,7 +125,7 @@ Сбросить ${0}$ сюда Отменить ревизию Изменить комментарий - Сохранить как patch-файл... + Сохранить как заплатки... Объединить с предыдущей ревизией Объединить все следующие ревизии с этим ИЗМЕНЕНИЯ @@ -167,7 +166,7 @@ Адрес электронной почты Адрес электронной почты GIT - Автоматическая загрузка изменений + Автозагрузка изменений Минут(а/ы) Внешний репозиторий по умолчанию ОТСЛЕЖИВАНИЕ ПРОБЛЕМ @@ -209,7 +208,6 @@ Проверить созданную ветку Локальные изменения: Отклонить - Ничего не делать Отложить и применить повторно Имя новой ветки: Введите имя ветки. @@ -218,15 +216,15 @@ Создать метку... Новая метка у: GPG подпись - Сообщение с меткой: + Сообщение с меткой: Необязательно. Имя метки: Рекомендуемый формат: v1.0.0-alpha Выложить на все внешние репозитории после создания Создать новую метку Вид: - Аннотированный - Лёгкий + С примечаниями + Простой Удерживайте Ctrl, чтобы начать сразу Вырезать Удалить ветку @@ -237,7 +235,7 @@ Вы пытаетесь удалить несколько веток одновременно. Обязательно перепроверьте, прежде чем предпринимать какие-либо действия! Удалить внешний репозиторий Внешний репозиторий: - Path: + Путь: Цель: Все дочерние элементы будут удалены из списка. Подтвердите удаление группы @@ -262,7 +260,7 @@ НИКАКИХ ИЗМЕНЕНИЙ ИЛИ МЕНЯЕТСЯ ТОЛЬКО EOL Предыдущее различие Сохранить как заплатку - Различие бок о бок + Различие рядом ПОДМОДУЛЬ НОВЫЙ Обмен @@ -450,6 +448,7 @@ Вставить Сейчас {0} минут назад + 1 час назад {0} часов назад Вчера {0} дней назад @@ -524,7 +523,6 @@ В: Локальные изменения: Отклонить - Ничего не делать Отложить и применить повторно Забрать без меток Внешний репозиторий: @@ -553,7 +551,7 @@ Редактировать внешний репозиторий Имя: Имя внешнего репозитория - Адрес репозитория: + Адрес: Адрес внешнего репозитория git Копировать адрес Удалить... @@ -693,8 +691,8 @@ Копировать относительный путь Извлечение вложенных подмодулей Открыть подмодуль репозитория - Относительный путь: - Относительный каталог для хранения подмодуля. + Каталог: + Относительный путь для хранения подмодуля. Удалить подмодуль ОК Копировать имя метки diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 72b434f9..2d160ad2 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -91,7 +91,6 @@ 目标分支 : 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 挑选提交 提交信息中追加来源信息 @@ -208,7 +207,6 @@ 完成后切换到新分支 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 新分支名 : 填写分支名称。 @@ -449,6 +447,7 @@ 粘贴 刚刚 {0}分钟前 + 1小时前 {0}小时前 昨天 {0}天前 @@ -524,7 +523,6 @@ 本地分支 : 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 不拉取远程标签 远程 : diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index bc9991f6..e50a600d 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -91,7 +91,6 @@ 目標分支: 未提交變更: 捨棄變更 - 不做處理 擱置變更並自動復原 揀選提交 提交資訊中追加來源資訊 @@ -208,7 +207,6 @@ 完成後切換到新分支 未提交變更: 捨棄變更 - 不做處理 擱置變更並自動復原 新分支名稱: 輸入分支名稱。 @@ -449,6 +447,7 @@ 貼上 剛剛 {0} 分鐘前 + 1 小時前 {0} 小時前 昨天 {0} 天前 @@ -523,7 +522,6 @@ 本機分支: 未提交變更: 捨棄變更 - 不做處理 擱置變更並自動復原 不拉取遠端標籤 遠端: diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index 9376741d..3334eba4 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -9,16 +9,17 @@ namespace SourceGit.ViewModels get; } - public Models.DealWithLocalChanges PreAction + public bool DiscardLocalChanges { get; set; - } = Models.DealWithLocalChanges.DoNothing; + } public Checkout(Repository repo, string branch) { _repo = repo; Branch = branch; + DiscardLocalChanges = false; View = new Views.Checkout() { DataContext = this }; } @@ -33,7 +34,12 @@ namespace SourceGit.ViewModels var needPopStash = false; if (changes > 0) { - if (PreAction == Models.DealWithLocalChanges.StashAndReaply) + if (DiscardLocalChanges) + { + SetProgressDescription("Discard local changes ..."); + Commands.Discard.All(_repo.FullPath, false); + } + else { SetProgressDescription("Stash local changes ..."); var succ = new Commands.Stash(_repo.FullPath).Push("CHECKOUT_AUTO_STASH"); @@ -45,11 +51,6 @@ namespace SourceGit.ViewModels needPopStash = true; } - else if (PreAction == Models.DealWithLocalChanges.Discard) - { - SetProgressDescription("Discard local changes ..."); - Commands.Discard.All(_repo.FullPath, false); - } } SetProgressDescription("Checkout branch ..."); diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutCommit.cs index ddc0a0c6..1876a425 100644 --- a/src/ViewModels/CheckoutCommit.cs +++ b/src/ViewModels/CheckoutCommit.cs @@ -9,16 +9,17 @@ namespace SourceGit.ViewModels get; } - public bool AutoStash + public bool DiscardLocalChanges { - get => _autoStash; - set => SetProperty(ref _autoStash, value); + get; + set; } public CheckoutCommit(Repository repo, Models.Commit commit) { _repo = repo; Commit = commit; + DiscardLocalChanges = false; View = new Views.CheckoutCommit() { DataContext = this }; } @@ -33,7 +34,12 @@ namespace SourceGit.ViewModels var needPopStash = false; if (changes > 0) { - if (AutoStash) + if (DiscardLocalChanges) + { + SetProgressDescription("Discard local changes ..."); + Commands.Discard.All(_repo.FullPath, false); + } + else { SetProgressDescription("Stash local changes ..."); var succ = new Commands.Stash(_repo.FullPath).Push("CHECKOUT_AUTO_STASH"); @@ -45,11 +51,6 @@ namespace SourceGit.ViewModels needPopStash = true; } - else - { - SetProgressDescription("Discard local changes ..."); - Commands.Discard.All(_repo.FullPath, false); - } } SetProgressDescription("Checkout commit ..."); @@ -67,6 +68,5 @@ namespace SourceGit.ViewModels } private readonly Repository _repo = null; - private bool _autoStash = true; } } diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 456e99f8..34ac8308 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; @@ -171,7 +172,7 @@ namespace SourceGit.ViewModels _searchChangeFilter = null; _diffContext = null; _viewRevisionFileContent = null; - _cancelToken = null; + _cancellationSource = null; WebLinks.Clear(); _revisionFiles = null; _revisionFileSearchSuggestion = null; @@ -589,32 +590,36 @@ namespace SourceGit.ViewModels if (_commit == null) return; + if (_cancellationSource is { IsCancellationRequested: false }) + _cancellationSource.Cancel(); + + _cancellationSource = new CancellationTokenSource(); + var token = _cancellationSource.Token; + Task.Run(() => { var message = new Commands.QueryCommitFullMessage(_repo.FullPath, _commit.SHA).Result(); var links = ParseLinksInMessage(message); - Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Links = links }); + + if (!token.IsCancellationRequested) + Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Links = links }); }); Task.Run(() => { var signInfo = new Commands.QueryCommitSignInfo(_repo.FullPath, _commit.SHA, !_repo.HasAllowedSignersFile).Result(); - Dispatcher.UIThread.Invoke(() => SignInfo = signInfo); + if (!token.IsCancellationRequested) + Dispatcher.UIThread.Invoke(() => SignInfo = signInfo); }); - if (_cancelToken != null) - _cancelToken.Requested = true; - - _cancelToken = new Commands.Command.CancelToken(); - if (Preferences.Instance.ShowChildren) { Task.Run(() => { var max = Preferences.Instance.MaxHistoryCommits; - var cmdChildren = new Commands.QueryCommitChildren(_repo.FullPath, _commit.SHA, max) { Cancel = _cancelToken }; - var children = cmdChildren.Result(); - if (!cmdChildren.Cancel.Requested) + var cmd = new Commands.QueryCommitChildren(_repo.FullPath, _commit.SHA, max) { CancellationToken = token }; + var children = cmd.Result(); + if (!token.IsCancellationRequested) Dispatcher.UIThread.Post(() => Children = children); }); } @@ -622,8 +627,8 @@ namespace SourceGit.ViewModels Task.Run(() => { var parent = _commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : _commit.Parents[0]; - var cmdChanges = new Commands.CompareRevisions(_repo.FullPath, parent, _commit.SHA) { Cancel = _cancelToken }; - var changes = cmdChanges.Result(); + var cmd = new Commands.CompareRevisions(_repo.FullPath, parent, _commit.SHA) { CancellationToken = token }; + var changes = cmd.Result(); var visible = changes; if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) { @@ -635,7 +640,7 @@ namespace SourceGit.ViewModels } } - if (!cmdChanges.Cancel.Requested) + if (!token.IsCancellationRequested) { Dispatcher.UIThread.Post(() => { @@ -809,14 +814,11 @@ namespace SourceGit.ViewModels Task.Run(() => { var files = new Commands.QueryRevisionFileNames(_repo.FullPath, sha).Result(); - var filesList = new List(); - filesList.AddRange(files); - Dispatcher.UIThread.Invoke(() => { if (sha == Commit.SHA) { - _revisionFiles = filesList; + _revisionFiles = files; if (!string.IsNullOrEmpty(_revisionFileSearchFilter)) CalcRevisionFileSearchSuggestion(); } @@ -873,7 +875,7 @@ namespace SourceGit.ViewModels private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; private object _viewRevisionFileContent = null; - private Commands.Command.CancelToken _cancelToken = null; + private CancellationTokenSource _cancellationSource = null; private List _revisionFiles = null; private string _revisionFileSearchFilter = string.Empty; private List _revisionFileSearchSuggestion = null; diff --git a/src/ViewModels/Conflict.cs b/src/ViewModels/Conflict.cs index 03c09e8a..1ccf4a33 100644 --- a/src/ViewModels/Conflict.cs +++ b/src/ViewModels/Conflict.cs @@ -1,5 +1,26 @@ namespace SourceGit.ViewModels { + public class ConflictSourceBranch + { + public string Name { get; private set; } + public string Head { get; private set; } + public Models.Commit Revision { get; private set; } + + public ConflictSourceBranch(string name, string head, Models.Commit revision) + { + Name = name; + Head = head; + Revision = revision; + } + + public ConflictSourceBranch(Repository repo, Models.Branch branch) + { + Name = branch.Name; + Head = branch.Head; + Revision = new Commands.QuerySingleCommit(repo.FullPath, branch.Head).Result() ?? new Models.Commit() { SHA = branch.Head }; + } + } + public class Conflict { public object Theirs @@ -31,28 +52,32 @@ if (context is CherryPickInProgress cherryPick) { Theirs = cherryPick.Head; - Mine = repo.CurrentBranch; + Mine = new ConflictSourceBranch(repo, repo.CurrentBranch); } else if (context is RebaseInProgress rebase) { var b = repo.Branches.Find(x => x.IsLocal && x.Name == rebase.HeadName); - Theirs = (object)b ?? rebase.StoppedAt; + if (b != null) + Theirs = new ConflictSourceBranch(b.Name, b.Head, rebase.StoppedAt); + else + Theirs = new ConflictSourceBranch(rebase.HeadName, rebase.StoppedAt?.SHA ?? "----------", rebase.StoppedAt); + Mine = rebase.Onto; } else if (context is RevertInProgress revert) { Theirs = revert.Head; - Mine = repo.CurrentBranch; + Mine = new ConflictSourceBranch(repo, repo.CurrentBranch); } else if (context is MergeInProgress merge) { Theirs = merge.Source; - Mine = repo.CurrentBranch; + Mine = new ConflictSourceBranch(repo, repo.CurrentBranch); } else { Theirs = "Stash or Patch"; - Mine = repo.CurrentBranch; + Mine = new ConflictSourceBranch(repo, repo.CurrentBranch); } } diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index 01bff031..37db0065 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -19,11 +19,11 @@ namespace SourceGit.ViewModels get; } - public Models.DealWithLocalChanges PreAction + public bool DiscardLocalChanges { get; set; - } = Models.DealWithLocalChanges.DoNothing; + } public bool CheckoutAfterCreated { @@ -47,6 +47,7 @@ namespace SourceGit.ViewModels } BasedOn = branch; + DiscardLocalChanges = false; View = new Views.CreateBranch() { DataContext = this }; } @@ -56,6 +57,7 @@ namespace SourceGit.ViewModels _baseOnRevision = commit.SHA; BasedOn = commit; + DiscardLocalChanges = false; View = new Views.CreateBranch() { DataContext = this }; } @@ -65,6 +67,7 @@ namespace SourceGit.ViewModels _baseOnRevision = tag.SHA; BasedOn = tag; + DiscardLocalChanges = false; View = new Views.CreateBranch() { DataContext = this }; } @@ -98,7 +101,12 @@ namespace SourceGit.ViewModels var needPopStash = false; if (changes > 0) { - if (PreAction == Models.DealWithLocalChanges.StashAndReaply) + if (DiscardLocalChanges) + { + SetProgressDescription("Discard local changes..."); + Commands.Discard.All(_repo.FullPath, false); + } + else { SetProgressDescription("Stash local changes"); succ = new Commands.Stash(_repo.FullPath).Push("CREATE_BRANCH_AUTO_STASH"); @@ -110,11 +118,6 @@ namespace SourceGit.ViewModels needPopStash = true; } - else if (PreAction == Models.DealWithLocalChanges.Discard) - { - SetProgressDescription("Discard local changes..."); - Commands.Discard.All(_repo.FullPath, false); - } } SetProgressDescription($"Create new branch '{fixedName}'"); diff --git a/src/ViewModels/CreateTag.cs b/src/ViewModels/CreateTag.cs index a6d7255b..86ae7118 100644 --- a/src/ViewModels/CreateTag.cs +++ b/src/ViewModels/CreateTag.cs @@ -96,7 +96,7 @@ namespace SourceGit.ViewModels foreach (var remote in remotes) { SetProgressDescription($"Pushing tag to remote {remote.Name} ..."); - new Commands.Push(_repo.FullPath, remote.Name, _tagName, false).Exec(); + new Commands.Push(_repo.FullPath, remote.Name, $"refs/tags/{_tagName}", false).Exec(); } } diff --git a/src/ViewModels/ExecuteCustomAction.cs b/src/ViewModels/ExecuteCustomAction.cs index 8e34379f..16a6410c 100644 --- a/src/ViewModels/ExecuteCustomAction.cs +++ b/src/ViewModels/ExecuteCustomAction.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace SourceGit.ViewModels { @@ -13,7 +14,7 @@ namespace SourceGit.ViewModels public ExecuteCustomAction(Repository repo, Models.CustomAction action) { _repo = repo; - _args = action.Arguments.Replace("${REPO}", _repo.FullPath); + _args = action.Arguments.Replace("${REPO}", GetWorkdir()); CustomAction = action; View = new Views.ExecuteCustomAction() { DataContext = this }; } @@ -21,7 +22,7 @@ namespace SourceGit.ViewModels public ExecuteCustomAction(Repository repo, Models.CustomAction action, Models.Branch branch) { _repo = repo; - _args = action.Arguments.Replace("${REPO}", _repo.FullPath).Replace("${BRANCH}", branch.FriendlyName); + _args = action.Arguments.Replace("${REPO}", GetWorkdir()).Replace("${BRANCH}", branch.FriendlyName); CustomAction = action; View = new Views.ExecuteCustomAction() { DataContext = this }; } @@ -29,7 +30,7 @@ namespace SourceGit.ViewModels public ExecuteCustomAction(Repository repo, Models.CustomAction action, Models.Commit commit) { _repo = repo; - _args = action.Arguments.Replace("${REPO}", _repo.FullPath).Replace("${SHA}", commit.SHA); + _args = action.Arguments.Replace("${REPO}", GetWorkdir()).Replace("${SHA}", commit.SHA); CustomAction = action; View = new Views.ExecuteCustomAction() { DataContext = this }; } @@ -51,6 +52,11 @@ namespace SourceGit.ViewModels }); } + private string GetWorkdir() + { + return OperatingSystem.IsWindows() ? _repo.FullPath.Replace("/", "\\") : _repo.FullPath; + } + private readonly Repository _repo = null; private string _args = string.Empty; } diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 48ce114d..0e67915c 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -148,14 +148,14 @@ namespace SourceGit.ViewModels { if (commits.Count == 0) { - _repo.SearchResultSelectedCommit = null; + _repo.SelectedSearchedCommit = null; DetailContext = null; } else if (commits.Count == 1) { var commit = (commits[0] as Models.Commit)!; - if (_repo.SearchResultSelectedCommit == null || _repo.SearchResultSelectedCommit.SHA != commit.SHA) - _repo.SearchResultSelectedCommit = _repo.SearchedCommits.Find(x => x.SHA == commit.SHA); + if (_repo.SelectedSearchedCommit == null || _repo.SelectedSearchedCommit.SHA != commit.SHA) + _repo.SelectedSearchedCommit = _repo.SearchedCommits.Find(x => x.SHA == commit.SHA); AutoSelectedCommit = commit; NavigationId = _navigationId + 1; @@ -173,7 +173,7 @@ namespace SourceGit.ViewModels } else if (commits.Count == 2) { - _repo.SearchResultSelectedCommit = null; + _repo.SelectedSearchedCommit = null; var end = commits[0] as Models.Commit; var start = commits[1] as Models.Commit; @@ -181,7 +181,7 @@ namespace SourceGit.ViewModels } else { - _repo.SearchResultSelectedCommit = null; + _repo.SelectedSearchedCommit = null; DetailContext = commits.Count; } } @@ -599,7 +599,7 @@ namespace SourceGit.ViewModels var head = _commits.Find(x => x.SHA == current.Head); if (head == null) { - _repo.SearchResultSelectedCommit = null; + _repo.SelectedSearchedCommit = null; head = new Commands.QuerySingleCommit(_repo.FullPath, current.Head).Result(); if (head != null) DetailContext = new RevisionCompare(_repo.FullPath, commit, head); @@ -694,12 +694,7 @@ namespace SourceGit.ViewModels menu.Items.Add(archive); menu.Items.Add(new MenuItem() { Header = "-" }); - var actions = new List(); - foreach (var action in _repo.Settings.CustomActions) - { - if (action.Scope == Models.CustomActionScope.Commit) - actions.Add(action); - } + var actions = _repo.GetCustomActions(Models.CustomActionScope.Commit); if (actions.Count > 0) { var custom = new MenuItem(); diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 016fd4c4..0b1d841e 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Avalonia.Collections; @@ -66,9 +65,8 @@ namespace SourceGit.ViewModels get => _defaultFontFamily; set { - var name = FixFontFamilyName(value); - if (SetProperty(ref _defaultFontFamily, name) && !_isLoading) - App.SetFonts(_defaultFontFamily, _monospaceFontFamily, _onlyUseMonoFontInEditor); + if (SetProperty(ref _defaultFontFamily, value) && !_isLoading) + App.SetFonts(value, _monospaceFontFamily, _onlyUseMonoFontInEditor); } } @@ -77,9 +75,8 @@ namespace SourceGit.ViewModels get => _monospaceFontFamily; set { - var name = FixFontFamilyName(value); - if (SetProperty(ref _monospaceFontFamily, name) && !_isLoading) - App.SetFonts(_defaultFontFamily, _monospaceFontFamily, _onlyUseMonoFontInEditor); + if (SetProperty(ref _monospaceFontFamily, value) && !_isLoading) + App.SetFonts(_defaultFontFamily, value, _onlyUseMonoFontInEditor); } } @@ -342,6 +339,12 @@ namespace SourceGit.ViewModels set; } = []; + public AvaloniaList CustomActions + { + get; + set; + } = []; + public AvaloniaList OpenAIServices { get; @@ -614,35 +617,6 @@ namespace SourceGit.ViewModels return changed; } - private string FixFontFamilyName(string name) - { - var trimmed = name.Trim(); - if (string.IsNullOrEmpty(trimmed)) - return string.Empty; - - var builder = new StringBuilder(); - var lastIsSpace = false; - for (int i = 0; i < trimmed.Length; i++) - { - var c = trimmed[i]; - if (char.IsWhiteSpace(c)) - { - if (lastIsSpace) - continue; - - lastIsSpace = true; - } - else - { - lastIsSpace = false; - } - - builder.Append(c); - } - - return builder.ToString(); - } - private static Preferences _instance = null; private static bool _isLoading = false; diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index ff557792..62d68834 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -38,11 +38,11 @@ namespace SourceGit.ViewModels set => SetProperty(ref _selectedBranch, value, true); } - public Models.DealWithLocalChanges PreAction + public bool DiscardLocalChanges { get; set; - } = Models.DealWithLocalChanges.DoNothing; + } public bool UseRebase { @@ -124,7 +124,12 @@ namespace SourceGit.ViewModels var needPopStash = false; if (changes > 0) { - if (PreAction == Models.DealWithLocalChanges.StashAndReaply) + if (DiscardLocalChanges) + { + SetProgressDescription("Discard local changes ..."); + Commands.Discard.All(_repo.FullPath, false); + } + else { SetProgressDescription("Stash local changes..."); var succ = new Commands.Stash(_repo.FullPath).Push("PULL_AUTO_STASH"); @@ -136,11 +141,6 @@ namespace SourceGit.ViewModels needPopStash = true; } - else if (PreAction == Models.DealWithLocalChanges.Discard) - { - SetProgressDescription("Discard local changes ..."); - Commands.Discard.All(_repo.FullPath, false); - } } bool rs; diff --git a/src/ViewModels/PushTag.cs b/src/ViewModels/PushTag.cs index 54673fbe..de2941d2 100644 --- a/src/ViewModels/PushTag.cs +++ b/src/ViewModels/PushTag.cs @@ -43,13 +43,14 @@ namespace SourceGit.ViewModels return Task.Run(() => { - bool succ = true; + var succ = true; + var tag = $"refs/tags/{Target.Name}"; if (_pushAllRemotes) { foreach (var remote in _repo.Remotes) { SetProgressDescription($"Pushing tag to remote {remote.Name} ..."); - succ = new Commands.Push(_repo.FullPath, remote.Name, Target.Name, false).Exec(); + succ = new Commands.Push(_repo.FullPath, remote.Name, tag, false).Exec(); if (!succ) break; } @@ -57,7 +58,7 @@ namespace SourceGit.ViewModels else { SetProgressDescription($"Pushing tag to remote {SelectedRemote.Name} ..."); - succ = new Commands.Push(_repo.FullPath, SelectedRemote.Name, Target.Name, false).Exec(); + succ = new Commands.Push(_repo.FullPath, SelectedRemote.Name, tag, false).Exec(); } CallUIThread(() => _repo.SetWatcherEnabled(true)); diff --git a/src/ViewModels/RenameBranch.cs b/src/ViewModels/RenameBranch.cs index bd0b4664..0679a5b5 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -12,7 +12,7 @@ namespace SourceGit.ViewModels } [Required(ErrorMessage = "Branch name is required!!!")] - [RegularExpression(@"^[\w\-/\.#]+$", ErrorMessage = "Bad branch name format!")] + [RegularExpression(@"^[\w \-/\.#]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(RenameBranch), nameof(ValidateBranchName))] public string Name { @@ -32,9 +32,10 @@ namespace SourceGit.ViewModels { if (ctx.ObjectInstance is RenameBranch rename) { + var fixedName = rename.FixName(name); foreach (var b in rename._repo.Branches) { - if (b.IsLocal && b != rename.Target && b.Name == name) + if (b.IsLocal && b != rename.Target && b.Name == fixedName) { return new ValidationResult("A branch with same name already exists!!!"); } @@ -46,7 +47,8 @@ namespace SourceGit.ViewModels public override Task Sure() { - if (_name == Target.Name) + var fixedName = FixName(_name); + if (fixedName == Target.Name) return null; _repo.SetWatcherEnabled(false); @@ -55,7 +57,7 @@ namespace SourceGit.ViewModels return Task.Run(() => { var oldName = Target.FullName; - var succ = Commands.Branch.Rename(_repo.FullPath, Target.Name, _name); + var succ = Commands.Branch.Rename(_repo.FullPath, Target.Name, fixedName); CallUIThread(() => { if (succ) @@ -65,7 +67,7 @@ namespace SourceGit.ViewModels if (filter.Type == Models.FilterType.LocalBranch && filter.Pattern.Equals(oldName, StringComparison.Ordinal)) { - filter.Pattern = $"refs/heads/{_name}"; + filter.Pattern = $"refs/heads/{fixedName}"; break; } } @@ -78,6 +80,15 @@ namespace SourceGit.ViewModels }); } + private string FixName(string name) + { + if (!name.Contains(' ')) + return name; + + var parts = name.Split(' ', StringSplitOptions.RemoveEmptyEntries); + return string.Join("-", parts); + } + private readonly Repository _repo; private string _name; } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 2294fdde..97c52d8e 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -6,7 +6,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -269,16 +268,18 @@ namespace SourceGit.ViewModels { if (SetProperty(ref _isSearching, value)) { - SearchedCommits = new List(); - SearchCommitFilter = string.Empty; - SearchCommitFilterSuggestion.Clear(); - IsSearchCommitSuggestionOpen = false; - _revisionFiles.Clear(); - if (value) { SelectedViewIndex = 0; - UpdateCurrentRevisionFilesForSearchSuggestion(); + CalcWorktreeFilesForSearching(); + } + else + { + SearchedCommits = new List(); + SelectedSearchedCommit = null; + SearchCommitFilter = string.Empty; + MatchedFilesForSearching = null; + _worktreeFiles = null; } } } @@ -307,8 +308,7 @@ namespace SourceGit.ViewModels { if (SetProperty(ref _searchCommitFilterType, value)) { - UpdateCurrentRevisionFilesForSearchSuggestion(); - + CalcWorktreeFilesForSearching(); if (!string.IsNullOrEmpty(_searchCommitFilter)) StartSearchCommits(); } @@ -320,53 +320,33 @@ namespace SourceGit.ViewModels get => _searchCommitFilter; set { - if (SetProperty(ref _searchCommitFilter, value) && - _searchCommitFilterType == 3 && - !string.IsNullOrEmpty(value) && - value.Length >= 2 && - _revisionFiles.Count > 0) - { - var suggestion = new List(); - foreach (var file in _revisionFiles) - { - if (file.Contains(value, StringComparison.OrdinalIgnoreCase) && file.Length != value.Length) - { - suggestion.Add(file); - if (suggestion.Count > 100) - break; - } - } - - SearchCommitFilterSuggestion.Clear(); - SearchCommitFilterSuggestion.AddRange(suggestion); - IsSearchCommitSuggestionOpen = SearchCommitFilterSuggestion.Count > 0; - } - else if (SearchCommitFilterSuggestion.Count > 0) - { - SearchCommitFilterSuggestion.Clear(); - IsSearchCommitSuggestionOpen = false; - } + if (SetProperty(ref _searchCommitFilter, value) && IsSearchingCommitsByFilePath()) + CalcMatchedFilesForSearching(); } } - public bool IsSearchCommitSuggestionOpen + public List MatchedFilesForSearching { - get => _isSearchCommitSuggestionOpen; - set => SetProperty(ref _isSearchCommitSuggestionOpen, value); + get => _matchedFilesForSearching; + private set => SetProperty(ref _matchedFilesForSearching, value); } - public AvaloniaList SearchCommitFilterSuggestion - { - get; - private set; - } = new AvaloniaList(); - public List SearchedCommits { get => _searchedCommits; set => SetProperty(ref _searchedCommits, value); } + public Models.Commit SelectedSearchedCommit + { + get => _selectedSearchedCommit; + set + { + if (SetProperty(ref _selectedSearchedCommit, value) && value != null) + NavigateToCommit(value.SHA); + } + } + public bool IsLocalBranchGroupExpanded { get => _settings.IsLocalBranchesExpandedInSideBar; @@ -437,16 +417,6 @@ namespace SourceGit.ViewModels get => _workingCopy?.InProgressContext; } - public Models.Commit SearchResultSelectedCommit - { - get => _searchResultSelectedCommit; - set - { - if (SetProperty(ref _searchResultSelectedCommit, value) && value != null) - NavigateToCommit(value.SHA); - } - } - public bool IsAutoFetching { get => _isAutoFetching; @@ -518,7 +488,7 @@ namespace SourceGit.ViewModels { File.WriteAllText(Path.Combine(_gitDir, "sourcegit.settings"), settingsSerialized); } - catch (DirectoryNotFoundException) + catch { // Ignore } @@ -550,9 +520,10 @@ namespace SourceGit.ViewModels _submodules.Clear(); _visibleSubmodules.Clear(); _searchedCommits.Clear(); + _selectedSearchedCommit = null; - _revisionFiles.Clear(); - SearchCommitFilterSuggestion.Clear(); + _worktreeFiles = null; + _matchedFilesForSearching = null; } public bool CanCreatePopup() @@ -723,39 +694,33 @@ namespace SourceGit.ViewModels SearchCommitFilter = string.Empty; } + public void ClearMatchedFilesForSearching() + { + MatchedFilesForSearching = null; + } + public void StartSearchCommits() { if (_histories == null) return; IsSearchLoadingVisible = true; - SearchResultSelectedCommit = null; - IsSearchCommitSuggestionOpen = false; - SearchCommitFilterSuggestion.Clear(); + SelectedSearchedCommit = null; + MatchedFilesForSearching = null; Task.Run(() => { - var visible = new List(); + var visible = null as List; + var method = (Models.CommitSearchMethod)_searchCommitFilterType; - switch (_searchCommitFilterType) + if (method == Models.CommitSearchMethod.BySHA) { - case 0: - var commit = new Commands.QuerySingleCommit(_fullpath, _searchCommitFilter).Result(); - if (commit != null) - visible.Add(commit); - break; - case 1: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByAuthor, _onlySearchCommitsInCurrentBranch).Result(); - break; - case 2: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByCommitter, _onlySearchCommitsInCurrentBranch).Result(); - break; - case 3: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByMessage, _onlySearchCommitsInCurrentBranch).Result(); - break; - case 4: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByFile, _onlySearchCommitsInCurrentBranch).Result(); - break; + var commit = new Commands.QuerySingleCommit(_fullpath, _searchCommitFilter).Result(); + visible = commit == null ? [] : [commit]; + } + else + { + visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, method, _onlySearchCommitsInCurrentBranch).Result(); } Dispatcher.UIThread.Invoke(() => @@ -943,6 +908,25 @@ namespace SourceGit.ViewModels _workingCopy?.AbortMerge(); } + public List GetCustomActions(Models.CustomActionScope scope) + { + var actions = new List(); + + foreach (var act in Preferences.Instance.CustomActions) + { + if (act.Scope == scope) + actions.Add(act); + } + + foreach (var act in _settings.CustomActions) + { + if (act.Scope == scope) + actions.Add(act); + } + + return actions; + } + public void RefreshBranches() { var branches = new Commands.QueryBranches(_fullpath).Result(); @@ -1224,23 +1208,26 @@ namespace SourceGit.ViewModels App.GetLauncer()?.OpenRepositoryInTab(node, null); } - public AvaloniaList GetPreferedOpenAIServices() + public List GetPreferedOpenAIServices() { var services = Preferences.Instance.OpenAIServices; if (services == null || services.Count == 0) return []; if (services.Count == 1) - return services; + return [services[0]]; var prefered = _settings.PreferedOpenAIService; + var all = new List(); foreach (var service in services) { if (service.Name.Equals(prefered, StringComparison.Ordinal)) return [service]; + + all.Add(service); } - return services; + return all; } public ContextMenu CreateContextMenuForGitFlow() @@ -1443,16 +1430,10 @@ namespace SourceGit.ViewModels public ContextMenu CreateContextMenuForCustomAction() { - var actions = new List(); - foreach (var action in _settings.CustomActions) - { - if (action.Scope == Models.CustomActionScope.Repository) - actions.Add(action); - } - var menu = new ContextMenu(); menu.Placement = PlacementMode.BottomEdgeAlignedLeft; + var actions = GetCustomActions(Models.CustomActionScope.Repository); if (actions.Count > 0) { foreach (var action in actions) @@ -1643,7 +1624,7 @@ namespace SourceGit.ViewModels compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); compareWithWorktree.Click += (_, _) => { - SearchResultSelectedCommit = null; + SelectedSearchedCommit = null; if (_histories != null) { @@ -1925,7 +1906,7 @@ namespace SourceGit.ViewModels compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); compareWithWorktree.Click += (_, _) => { - SearchResultSelectedCommit = null; + SelectedSearchedCommit = null; if (_histories != null) { @@ -2349,13 +2330,7 @@ namespace SourceGit.ViewModels private void TryToAddCustomActionsToBranchContextMenu(ContextMenu menu, Models.Branch branch) { - var actions = new List(); - foreach (var action in Settings.CustomActions) - { - if (action.Scope == Models.CustomActionScope.Branch) - actions.Add(action); - } - + var actions = GetCustomActions(Models.CustomActionScope.Branch); if (actions.Count == 0) return; @@ -2384,42 +2359,52 @@ namespace SourceGit.ViewModels menu.Items.Add(new MenuItem() { Header = "-" }); } - private void UpdateCurrentRevisionFilesForSearchSuggestion() + private bool IsSearchingCommitsByFilePath() { - _revisionFiles.Clear(); + return _isSearching && _searchCommitFilterType == (int)Models.CommitSearchMethod.ByFile; + } - if (_searchCommitFilterType == 3) + private void CalcWorktreeFilesForSearching() + { + if (!IsSearchingCommitsByFilePath()) { - Task.Run(() => - { - var files = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); - Dispatcher.UIThread.Invoke(() => - { - if (_searchCommitFilterType != 3) - return; - - _revisionFiles.AddRange(files); - - if (!string.IsNullOrEmpty(_searchCommitFilter) && _searchCommitFilter.Length > 2 && _revisionFiles.Count > 0) - { - var suggestion = new List(); - foreach (var file in _revisionFiles) - { - if (file.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase) && file.Length != _searchCommitFilter.Length) - { - suggestion.Add(file); - if (suggestion.Count > 100) - break; - } - } - - SearchCommitFilterSuggestion.Clear(); - SearchCommitFilterSuggestion.AddRange(suggestion); - IsSearchCommitSuggestionOpen = SearchCommitFilterSuggestion.Count > 0; - } - }); - }); + _worktreeFiles = null; + MatchedFilesForSearching = null; + GC.Collect(); + return; } + + Task.Run(() => + { + _worktreeFiles = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); + Dispatcher.UIThread.Invoke(() => + { + if (IsSearchingCommitsByFilePath()) + CalcMatchedFilesForSearching(); + }); + }); + } + + private void CalcMatchedFilesForSearching() + { + if (_worktreeFiles == null || _worktreeFiles.Count == 0 || _searchCommitFilter.Length < 3) + { + MatchedFilesForSearching = null; + return; + } + + var matched = new List(); + foreach (var file in _worktreeFiles) + { + if (file.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase) && file.Length != _searchCommitFilter.Length) + { + matched.Add(file); + if (matched.Count > 100) + break; + } + } + + MatchedFilesForSearching = matched; } private void AutoFetchImpl(object sender) @@ -2468,13 +2453,13 @@ namespace SourceGit.ViewModels private bool _isSearching = false; private bool _isSearchLoadingVisible = false; - private bool _isSearchCommitSuggestionOpen = false; - private int _searchCommitFilterType = 3; + private int _searchCommitFilterType = (int)Models.CommitSearchMethod.ByMessage; private bool _onlySearchCommitsInCurrentBranch = false; private string _searchCommitFilter = string.Empty; private List _searchedCommits = new List(); - private Models.Commit _searchResultSelectedCommit = null; - private List _revisionFiles = new List(); + private Models.Commit _selectedSearchedCommit = null; + private List _worktreeFiles = null; + private List _matchedFilesForSearching = null; private string _filter = string.Empty; private object _lockRemotes = new object(); diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 35db11b9..f9ddb288 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -1452,28 +1452,24 @@ namespace SourceGit.ViewModels App.OpenDialog(dialog); return null; } - else + + var menu = new ContextMenu() { Placement = PlacementMode.TopEdgeAlignedLeft }; + foreach (var service in services) { - var menu = new ContextMenu() { Placement = PlacementMode.TopEdgeAlignedLeft }; - - foreach (var service in services) + var dup = service; + var item = new MenuItem(); + item.Header = service.Name; + item.Click += (_, e) => { - var dup = service; + var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _staged); + App.OpenDialog(dialog); + e.Handled = true; + }; - var item = new MenuItem(); - item.Header = service.Name; - item.Click += (_, e) => - { - var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _staged); - App.OpenDialog(dialog); - e.Handled = true; - }; - - menu.Items.Add(item); - } - - return menu; + menu.Items.Add(item); } + + return menu; } private List GetVisibleUnstagedChanges(List unstaged) diff --git a/src/Views/ChangeCollectionView.axaml b/src/Views/ChangeCollectionView.axaml index 6ce3d033..2b0f5bfa 100644 --- a/src/Views/ChangeCollectionView.axaml +++ b/src/Views/ChangeCollectionView.axaml @@ -39,7 +39,8 @@ + DoubleTapped="OnRowDoubleTapped" + ToolTip.Tip="{Binding FullPath}"> - + - + - + - - + - - + Content="{DynamicResource Text.CreateBranch.LocalChanges.StashAndReply}" + IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/> + diff --git a/src/Views/Checkout.axaml.cs b/src/Views/Checkout.axaml.cs index da6e6b31..f8398a1d 100644 --- a/src/Views/Checkout.axaml.cs +++ b/src/Views/Checkout.axaml.cs @@ -1,5 +1,4 @@ using Avalonia.Controls; -using Avalonia.Interactivity; namespace SourceGit.Views { @@ -9,51 +8,5 @@ namespace SourceGit.Views { InitializeComponent(); } - - protected override void OnLoaded(RoutedEventArgs e) - { - base.OnLoaded(e); - - var vm = DataContext as ViewModels.Checkout; - if (vm == null) - return; - - switch (vm.PreAction) - { - case Models.DealWithLocalChanges.DoNothing: - RadioDoNothing.IsChecked = true; - break; - case Models.DealWithLocalChanges.StashAndReaply: - RadioStashAndReply.IsChecked = true; - break; - default: - RadioDiscard.IsChecked = true; - break; - } - } - - private void OnLocalChangeActionIsCheckedChanged(object sender, RoutedEventArgs e) - { - var vm = DataContext as ViewModels.Checkout; - if (vm == null) - return; - - if (RadioDoNothing.IsChecked == true) - { - if (vm.PreAction != Models.DealWithLocalChanges.DoNothing) - vm.PreAction = Models.DealWithLocalChanges.DoNothing; - return; - } - - if (RadioStashAndReply.IsChecked == true) - { - if (vm.PreAction != Models.DealWithLocalChanges.StashAndReaply) - vm.PreAction = Models.DealWithLocalChanges.StashAndReaply; - return; - } - - if (vm.PreAction != Models.DealWithLocalChanges.Discard) - vm.PreAction = Models.DealWithLocalChanges.Discard; - } } } diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml index 3ee3943f..9b418823 100644 --- a/src/Views/CheckoutCommit.axaml +++ b/src/Views/CheckoutCommit.axaml @@ -30,16 +30,16 @@ + Margin="0,0,8,0" + IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/> + GroupName="LocalChanges"/> - - - - - + + + + diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs new file mode 100644 index 00000000..015eaca5 --- /dev/null +++ b/src/Views/CommitGraph.cs @@ -0,0 +1,228 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class CommitGraph : Control + { + public static readonly StyledProperty GraphProperty = + AvaloniaProperty.Register(nameof(Graph)); + + public Models.CommitGraph Graph + { + get => GetValue(GraphProperty); + set => SetValue(GraphProperty, value); + } + + public static readonly StyledProperty DotBrushProperty = + AvaloniaProperty.Register(nameof(DotBrush), Brushes.Transparent); + + public IBrush DotBrush + { + get => GetValue(DotBrushProperty); + set => SetValue(DotBrushProperty, value); + } + + public static readonly StyledProperty OnlyHighlightCurrentBranchProperty = + AvaloniaProperty.Register(nameof(OnlyHighlightCurrentBranch), true); + + public bool OnlyHighlightCurrentBranch + { + get => GetValue(OnlyHighlightCurrentBranchProperty); + set => SetValue(OnlyHighlightCurrentBranchProperty, value); + } + + static CommitGraph() + { + AffectsRender(GraphProperty, DotBrushProperty, OnlyHighlightCurrentBranchProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var graph = Graph; + if (graph == null) + return; + + var histories = this.FindAncestorOfType(); + if (histories == null) + return; + + var list = histories.CommitListContainer; + if (list == null) + return; + + // Calculate drawing area. + double width = Bounds.Width - 273 - histories.AuthorNameColumnWidth.Value; + double height = Bounds.Height; + double startY = list.Scroll?.Offset.Y ?? 0; + double endY = startY + height + 28; + + // Apply scroll offset and clip. + using (context.PushClip(new Rect(0, 0, width, height))) + using (context.PushTransform(Matrix.CreateTranslation(0, -startY))) + { + // Draw contents + DrawCurves(context, graph, startY, endY); + DrawAnchors(context, graph, startY, endY); + } + } + + private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom) + { + var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness); + var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; + + if (onlyHighlightCurrentBranch) + { + foreach (var link in graph.Links) + { + if (link.IsMerged) + continue; + if (link.End.Y < top) + continue; + if (link.Start.Y > bottom) + break; + + var geo = new StreamGeometry(); + using (var ctx = geo.Open()) + { + ctx.BeginFigure(link.Start, false); + ctx.QuadraticBezierTo(link.Control, link.End); + } + + context.DrawGeometry(null, grayedPen, geo); + } + } + + foreach (var line in graph.Paths) + { + var last = line.Points[0]; + var size = line.Points.Count; + + if (line.Points[size - 1].Y < top) + continue; + if (last.Y > bottom) + break; + + var geo = new StreamGeometry(); + var pen = Models.CommitGraph.Pens[line.Color]; + + using (var ctx = geo.Open()) + { + var started = false; + var ended = false; + for (int i = 1; i < size; i++) + { + var cur = line.Points[i]; + if (cur.Y < top) + { + last = cur; + continue; + } + + if (!started) + { + ctx.BeginFigure(last, false); + started = true; + } + + if (cur.Y > bottom) + { + cur = new Point(cur.X, bottom); + ended = true; + } + + if (cur.X > last.X) + { + ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur); + } + else if (cur.X < last.X) + { + if (i < size - 1) + { + var midY = (last.Y + cur.Y) / 2; + ctx.CubicBezierTo(new Point(last.X, midY + 4), new Point(cur.X, midY - 4), cur); + } + else + { + ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur); + } + } + else + { + ctx.LineTo(cur); + } + + if (ended) + break; + last = cur; + } + } + + if (!line.IsMerged && onlyHighlightCurrentBranch) + context.DrawGeometry(null, grayedPen, geo); + else + context.DrawGeometry(null, pen, geo); + } + + foreach (var link in graph.Links) + { + if (onlyHighlightCurrentBranch && !link.IsMerged) + continue; + if (link.End.Y < top) + continue; + if (link.Start.Y > bottom) + break; + + var geo = new StreamGeometry(); + using (var ctx = geo.Open()) + { + ctx.BeginFigure(link.Start, false); + ctx.QuadraticBezierTo(link.Control, link.End); + } + + context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo); + } + } + + private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, double top, double bottom) + { + var dotFill = DotBrush; + var dotFillPen = new Pen(dotFill, 2); + var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness); + var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; + + foreach (var dot in graph.Dots) + { + if (dot.Center.Y < top) + continue; + if (dot.Center.Y > bottom) + break; + + var pen = Models.CommitGraph.Pens[dot.Color]; + if (!dot.IsMerged && onlyHighlightCurrentBranch) + pen = grayedPen; + + switch (dot.Type) + { + case Models.CommitGraph.DotType.Head: + context.DrawEllipse(dotFill, pen, dot.Center, 6, 6); + context.DrawEllipse(pen.Brush, null, dot.Center, 3, 3); + break; + case Models.CommitGraph.DotType.Merge: + context.DrawEllipse(pen.Brush, null, dot.Center, 6, 6); + context.DrawLine(dotFillPen, new Point(dot.Center.X, dot.Center.Y - 3), new Point(dot.Center.X, dot.Center.Y + 3)); + context.DrawLine(dotFillPen, new Point(dot.Center.X - 3, dot.Center.Y), new Point(dot.Center.X + 3, dot.Center.Y)); + break; + default: + context.DrawEllipse(dotFill, pen, dot.Center, 3, 3); + break; + } + } + } + } +} diff --git a/src/Views/CommitStatusIndicator.cs b/src/Views/CommitStatusIndicator.cs new file mode 100644 index 00000000..c2f4184e --- /dev/null +++ b/src/Views/CommitStatusIndicator.cs @@ -0,0 +1,90 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace SourceGit.Views +{ + public class CommitStatusIndicator : Control + { + public static readonly StyledProperty CurrentBranchProperty = + AvaloniaProperty.Register(nameof(CurrentBranch)); + + public Models.Branch CurrentBranch + { + get => GetValue(CurrentBranchProperty); + set => SetValue(CurrentBranchProperty, value); + } + + public static readonly StyledProperty AheadBrushProperty = + AvaloniaProperty.Register(nameof(AheadBrush)); + + public IBrush AheadBrush + { + get => GetValue(AheadBrushProperty); + set => SetValue(AheadBrushProperty, value); + } + + public static readonly StyledProperty BehindBrushProperty = + AvaloniaProperty.Register(nameof(BehindBrush)); + + public IBrush BehindBrush + { + get => GetValue(BehindBrushProperty); + set => SetValue(BehindBrushProperty, value); + } + + enum Status + { + Normal, + Ahead, + Behind, + } + + public override void Render(DrawingContext context) + { + if (_status == Status.Normal) + return; + + context.DrawEllipse(_status == Status.Ahead ? AheadBrush : BehindBrush, null, new Rect(0, 0, 5, 5)); + } + + protected override Size MeasureOverride(Size availableSize) + { + if (DataContext is Models.Commit commit && CurrentBranch is not null) + { + var sha = commit.SHA; + var track = CurrentBranch.TrackStatus; + + if (track.Ahead.Contains(sha)) + _status = Status.Ahead; + else if (track.Behind.Contains(sha)) + _status = Status.Behind; + else + _status = Status.Normal; + } + else + { + _status = Status.Normal; + } + + return _status == Status.Normal ? new Size(0, 0) : new Size(9, 5); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + InvalidateMeasure(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == CurrentBranchProperty) + InvalidateMeasure(); + } + + private Status _status = Status.Normal; + } +} diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs new file mode 100644 index 00000000..32f6838d --- /dev/null +++ b/src/Views/CommitSubjectPresenter.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; + +namespace SourceGit.Views +{ + public partial class CommitSubjectPresenter : TextBlock + { + public static readonly StyledProperty SubjectProperty = + AvaloniaProperty.Register(nameof(Subject)); + + public string Subject + { + get => GetValue(SubjectProperty); + set => SetValue(SubjectProperty, value); + } + + public static readonly StyledProperty> IssueTrackerRulesProperty = + AvaloniaProperty.Register>(nameof(IssueTrackerRules)); + + public AvaloniaList IssueTrackerRules + { + get => GetValue(IssueTrackerRulesProperty); + set => SetValue(IssueTrackerRulesProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextBlock); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty) + { + Inlines!.Clear(); + _matches = null; + ClearHoveredIssueLink(); + + var subject = Subject; + if (string.IsNullOrEmpty(subject)) + return; + + var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject); + if (!keywordMatch.Success) + keywordMatch = REG_KEYWORD_FORMAT2().Match(subject); + + var rules = IssueTrackerRules ?? []; + var matches = new List(); + foreach (var rule in rules) + rule.Matches(matches, subject); + + if (matches.Count == 0) + { + if (keywordMatch.Success) + { + Inlines.Add(new Run(subject.Substring(0, keywordMatch.Length)) { FontWeight = FontWeight.Bold }); + Inlines.Add(new Run(subject.Substring(keywordMatch.Length))); + } + else + { + Inlines.Add(new Run(subject)); + } + return; + } + + matches.Sort((l, r) => l.Start - r.Start); + _matches = matches; + + var inlines = new List(); + var pos = 0; + foreach (var match in matches) + { + if (match.Start > pos) + { + if (keywordMatch.Success && pos < keywordMatch.Length) + { + if (keywordMatch.Length < match.Start) + { + inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold }); + inlines.Add(new Run(subject.Substring(keywordMatch.Length, match.Start - keywordMatch.Length))); + } + else + { + inlines.Add(new Run(subject.Substring(pos, match.Start - pos)) { FontWeight = FontWeight.Bold }); + } + } + else + { + inlines.Add(new Run(subject.Substring(pos, match.Start - pos))); + } + } + + var link = new Run(subject.Substring(match.Start, match.Length)); + link.Classes.Add("issue_link"); + inlines.Add(link); + + pos = match.Start + match.Length; + } + + if (pos < subject.Length) + { + if (keywordMatch.Success && pos < keywordMatch.Length) + { + inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold }); + inlines.Add(new Run(subject.Substring(keywordMatch.Length))); + } + else + { + inlines.Add(new Run(subject.Substring(pos))); + } + } + + Inlines.AddRange(inlines); + } + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + if (_matches != null) + { + var point = e.GetPosition(this) - new Point(Padding.Left, Padding.Top); + var x = Math.Min(Math.Max(point.X, 0), Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)); + var y = Math.Min(Math.Max(point.Y, 0), Math.Max(TextLayout.Height, 0)); + point = new Point(x, y); + + var textPosition = TextLayout.HitTestPoint(point).TextPosition; + foreach (var match in _matches) + { + if (!match.Intersect(textPosition, 1)) + continue; + + if (match == _lastHover) + return; + + _lastHover = match; + SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); + ToolTip.SetTip(this, match.Link); + ToolTip.SetIsOpen(this, true); + e.Handled = true; + return; + } + + ClearHoveredIssueLink(); + } + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + if (_lastHover != null) + Native.OS.OpenBrowser(_lastHover.Link); + } + + protected override void OnPointerExited(PointerEventArgs e) + { + base.OnPointerExited(e); + ClearHoveredIssueLink(); + } + + private void ClearHoveredIssueLink() + { + if (_lastHover != null) + { + ToolTip.SetTip(this, null); + SetCurrentValue(CursorProperty, Cursor.Parse("Arrow")); + _lastHover = null; + } + } + + [GeneratedRegex(@"^\[[\w\s]+\]")] + private static partial Regex REG_KEYWORD_FORMAT1(); + + [GeneratedRegex(@"^\S+([\<\(][\w\s_\-\*,]+[\>\)])?\!?\s?:\s")] + private static partial Regex REG_KEYWORD_FORMAT2(); + + private List _matches = null; + private Models.Hyperlink _lastHover = null; + } +} diff --git a/src/Views/CommitTimeTextBlock.cs b/src/Views/CommitTimeTextBlock.cs new file mode 100644 index 00000000..db63e8a6 --- /dev/null +++ b/src/Views/CommitTimeTextBlock.cs @@ -0,0 +1,166 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace SourceGit.Views +{ + public class CommitTimeTextBlock : TextBlock + { + public static readonly StyledProperty ShowAsDateTimeProperty = + AvaloniaProperty.Register(nameof(ShowAsDateTime), true); + + public bool ShowAsDateTime + { + get => GetValue(ShowAsDateTimeProperty); + set => SetValue(ShowAsDateTimeProperty, value); + } + + public static readonly StyledProperty DateTimeFormatProperty = + AvaloniaProperty.Register(nameof(DateTimeFormat), 0); + + public int DateTimeFormat + { + get => GetValue(DateTimeFormatProperty); + set => SetValue(DateTimeFormatProperty, value); + } + + public static readonly StyledProperty UseAuthorTimeProperty = + AvaloniaProperty.Register(nameof(UseAuthorTime), true); + + public bool UseAuthorTime + { + get => GetValue(UseAuthorTimeProperty); + set => SetValue(UseAuthorTimeProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextBlock); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == UseAuthorTimeProperty) + { + SetCurrentValue(TextProperty, GetDisplayText()); + } + else if (change.Property == ShowAsDateTimeProperty) + { + SetCurrentValue(TextProperty, GetDisplayText()); + + if (ShowAsDateTime) + StopTimer(); + else + StartTimer(); + } + else if (change.Property == DateTimeFormatProperty) + { + if (ShowAsDateTime) + SetCurrentValue(TextProperty, GetDisplayText()); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (!ShowAsDateTime) + StartTimer(); + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + StopTimer(); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + SetCurrentValue(TextProperty, GetDisplayText()); + } + + private void StartTimer() + { + if (_refreshTimer != null) + return; + + _refreshTimer = DispatcherTimer.Run(() => + { + Dispatcher.UIThread.Invoke(() => + { + var text = GetDisplayText(); + if (!text.Equals(Text, StringComparison.Ordinal)) + Text = text; + }); + + return true; + }, TimeSpan.FromSeconds(10)); + } + + private void StopTimer() + { + if (_refreshTimer != null) + { + _refreshTimer.Dispose(); + _refreshTimer = null; + } + } + + private string GetDisplayText() + { + var commit = DataContext as Models.Commit; + if (commit == null) + return string.Empty; + + if (ShowAsDateTime) + return UseAuthorTime ? commit.AuthorTimeStr : commit.CommitterTimeStr; + + var timestamp = UseAuthorTime ? commit.AuthorTime : commit.CommitterTime; + var now = DateTime.Now; + var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); + var span = now - localTime; + if (span.TotalMinutes < 1) + return App.Text("Period.JustNow"); + + if (span.TotalHours < 1) + return App.Text("Period.MinutesAgo", (int)span.TotalMinutes); + + if (span.TotalDays < 1) + { + var hours = (int)span.TotalHours; + return hours == 1 ? App.Text("Period.HourAgo") : App.Text("Period.HoursAgo", hours); + } + + var lastDay = now.AddDays(-1).Date; + if (localTime >= lastDay) + return App.Text("Period.Yesterday"); + + if ((localTime.Year == now.Year && localTime.Month == now.Month) || span.TotalDays < 28) + { + var diffDay = now.Date - localTime.Date; + return App.Text("Period.DaysAgo", (int)diffDay.TotalDays); + } + + var lastMonth = now.AddMonths(-1).Date; + if (localTime.Year == lastMonth.Year && localTime.Month == lastMonth.Month) + return App.Text("Period.LastMonth"); + + if (localTime.Year == now.Year || localTime > now.AddMonths(-11)) + { + var diffMonth = (12 + now.Month - localTime.Month) % 12; + return App.Text("Period.MonthsAgo", diffMonth); + } + + var diffYear = now.Year - localTime.Year; + if (diffYear == 1) + return App.Text("Period.LastYear"); + + return App.Text("Period.YearsAgo", diffYear); + } + + private IDisposable _refreshTimer = null; + } +} diff --git a/src/Views/ConfigureWorkspace.axaml.cs b/src/Views/ConfigureWorkspace.axaml.cs index 012c2e85..9e458f6f 100644 --- a/src/Views/ConfigureWorkspace.axaml.cs +++ b/src/Views/ConfigureWorkspace.axaml.cs @@ -11,8 +11,10 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { - ViewModels.Preferences.Instance.Save(); base.OnClosing(e); + + if (!Design.IsDesignMode) + ViewModels.Preferences.Instance.Save(); } } } diff --git a/src/Views/Conflict.axaml b/src/Views/Conflict.axaml new file mode 100644 index 00000000..9a056f9e --- /dev/null +++ b/src/Views/Conflict.axaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + diff --git a/src/Views/Preferences.axaml.cs b/src/Views/Preferences.axaml.cs index 8f9917be..73b2e995 100644 --- a/src/Views/Preferences.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -103,6 +103,15 @@ namespace SourceGit.Views set => SetValue(SelectedOpenAIServiceProperty, value); } + public static readonly StyledProperty SelectedCustomActionProperty = + AvaloniaProperty.Register(nameof(SelectedCustomAction)); + + public Models.CustomAction SelectedCustomAction + { + get => GetValue(SelectedCustomActionProperty); + set => SetValue(SelectedCustomActionProperty, value); + } + public Preferences() { var pref = ViewModels.Preferences.Instance; @@ -160,6 +169,11 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { + base.OnClosing(e); + + if (Design.IsDesignMode) + return; + var config = new Commands.Config(null).ListAll(); SetIfChanged(config, "user.name", DefaultUser, ""); SetIfChanged(config, "user.email", DefaultEmail, ""); @@ -190,7 +204,6 @@ namespace SourceGit.Views } ViewModels.Preferences.Instance.Save(); - base.OnClosing(e); } private async void SelectThemeOverrideFile(object _, RoutedEventArgs e) @@ -368,6 +381,40 @@ namespace SourceGit.Views e.Handled = true; } + private void OnAddCustomAction(object sender, RoutedEventArgs e) + { + var action = new Models.CustomAction() { Name = "Unnamed Action (Global)" }; + ViewModels.Preferences.Instance.CustomActions.Add(action); + SelectedCustomAction = action; + + e.Handled = true; + } + + private async void SelectExecutableForCustomAction(object sender, RoutedEventArgs e) + { + var options = new FilePickerOpenOptions() + { + FileTypeFilter = [new FilePickerFileType("Executable file(script)") { Patterns = ["*.*"] }], + AllowMultiple = false, + }; + + var selected = await StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1 && sender is Button { DataContext: Models.CustomAction action }) + action.Executable = selected[0].Path.LocalPath; + + e.Handled = true; + } + + private void OnRemoveSelectedCustomAction(object sender, RoutedEventArgs e) + { + if (SelectedCustomAction == null) + return; + + ViewModels.Preferences.Instance.CustomActions.Remove(SelectedCustomAction); + SelectedCustomAction = null; + e.Handled = true; + } + private void UpdateGitVersion() { GitVersion = Native.OS.GitVersionString; diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index 3e1f96d9..67121826 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -22,7 +22,7 @@ - + - - - + Content="{DynamicResource Text.Pull.LocalChanges.StashAndReply}" + IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/> + - + diff --git a/src/Views/Pull.axaml.cs b/src/Views/Pull.axaml.cs index 3003f02c..c6b4923e 100644 --- a/src/Views/Pull.axaml.cs +++ b/src/Views/Pull.axaml.cs @@ -1,5 +1,4 @@ using Avalonia.Controls; -using Avalonia.Interactivity; namespace SourceGit.Views { @@ -9,51 +8,5 @@ namespace SourceGit.Views { InitializeComponent(); } - - protected override void OnLoaded(RoutedEventArgs e) - { - base.OnLoaded(e); - - var vm = DataContext as ViewModels.Pull; - if (vm == null) - return; - - switch (vm.PreAction) - { - case Models.DealWithLocalChanges.DoNothing: - RadioDoNothing.IsChecked = true; - break; - case Models.DealWithLocalChanges.StashAndReaply: - RadioStashAndReply.IsChecked = true; - break; - default: - RadioDiscard.IsChecked = true; - break; - } - } - - private void OnLocalChangeActionIsCheckedChanged(object sender, RoutedEventArgs e) - { - var vm = DataContext as ViewModels.Pull; - if (vm == null) - return; - - if (RadioDoNothing.IsChecked == true) - { - if (vm.PreAction != Models.DealWithLocalChanges.DoNothing) - vm.PreAction = Models.DealWithLocalChanges.DoNothing; - return; - } - - if (RadioStashAndReply.IsChecked == true) - { - if (vm.PreAction != Models.DealWithLocalChanges.StashAndReaply) - vm.PreAction = Models.DealWithLocalChanges.StashAndReaply; - return; - } - - if (vm.PreAction != Models.DealWithLocalChanges.Discard) - vm.PreAction = Models.DealWithLocalChanges.Discard; - } } } diff --git a/src/Views/RenameBranch.axaml b/src/Views/RenameBranch.axaml index 59a849fe..efbbf323 100644 --- a/src/Views/RenameBranch.axaml +++ b/src/Views/RenameBranch.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.RenameBranch" x:DataType="vm:RenameBranch"> @@ -11,7 +12,7 @@ - + + + + + + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 30180f7d..b16447fa 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -86,7 +86,7 @@ - + - + + VerticalContentAlignment="Center"> - @@ -421,14 +421,20 @@ + HorizontalOffset="-8" VerticalAlignment="-8"> + + + + + + + @@ -478,11 +484,11 @@ BorderThickness="0" SelectedIndex="{Binding SearchCommitFilterType, Mode=TwoWay}"> - - - - - + + + + + @@ -490,26 +496,29 @@ Margin="4,0,0,0" IsChecked="{Binding OnlySearchCommitsInCurrentBranch, Mode=TwoWay}" IsVisible="{Binding SearchCommitFilterType, Converter={x:Static c:IntConverters.IsGreaterThanZero}}"> - + + ScrollViewer.VerticalScrollBarVisibility="Auto" + Grid.IsSharedSizeScope="True"> @@ -521,18 +530,22 @@ - - - - - - - - - - - - + + + + + + + + + + @@ -549,7 +562,7 @@ - + - + @@ -636,7 +649,7 @@ - + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 2b3b7c30..00218a85 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -134,7 +134,7 @@ namespace SourceGit.Views } else if (e.Key == Key.Down) { - if (repo.IsSearchCommitSuggestionOpen) + if (repo.MatchedFilesForSearching is { Count: > 0 }) { SearchSuggestionBox.Focus(NavigationMethod.Tab); SearchSuggestionBox.SelectedIndex = 0; @@ -144,12 +144,7 @@ namespace SourceGit.Views } else if (e.Key == Key.Escape) { - if (repo.IsSearchCommitSuggestionOpen) - { - repo.SearchCommitFilterSuggestion.Clear(); - repo.IsSearchCommitSuggestionOpen = false; - } - + repo.ClearMatchedFilesForSearching(); e.Handled = true; } } @@ -369,9 +364,7 @@ namespace SourceGit.Views if (e.Key == Key.Escape) { - repo.IsSearchCommitSuggestionOpen = false; - repo.SearchCommitFilterSuggestion.Clear(); - + repo.ClearMatchedFilesForSearching(); e.Handled = true; } else if (e.Key == Key.Enter && SearchSuggestionBox.SelectedItem is string content) diff --git a/src/Views/RepositoryConfigure.axaml.cs b/src/Views/RepositoryConfigure.axaml.cs index 3faba5ee..455731aa 100644 --- a/src/Views/RepositoryConfigure.axaml.cs +++ b/src/Views/RepositoryConfigure.axaml.cs @@ -13,8 +13,10 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { - (DataContext as ViewModels.RepositoryConfigure)?.Save(); base.OnClosing(e); + + if (!Design.IsDesignMode && DataContext is ViewModels.RepositoryConfigure configure) + configure.Save(); } private async void SelectExecutableForCustomAction(object sender, RoutedEventArgs e) diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index 6847b14b..e0c6577d 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -43,8 +43,14 @@ + HorizontalOffset="-8" VerticalAlignment="-8"> + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -