From cdae9168ed534256027bca2eb50fd1593c70a8b6 Mon Sep 17 00:00:00 2001 From: Gadfly Date: Mon, 10 Mar 2025 15:44:39 +0800 Subject: [PATCH 01/41] ci(windows): maintain SourceGit folder structure (#1076) --- build/scripts/package.windows.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From e65cb5049578f24d4cc033e5c63722afa645c733 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 10 Mar 2025 17:48:02 +0800 Subject: [PATCH 02/41] fix: use `\` as path delim on Windows when executing custom actions (#1077) Signed-off-by: leo --- src/ViewModels/ExecuteCustomAction.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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; } From 2b2f070c4aae5789f848a8fe56dead45cd425c34 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 10 Mar 2025 18:29:11 +0800 Subject: [PATCH 03/41] enhance: use `--no-optional-locks` parameter for `git status` command Signed-off-by: leo --- src/Commands/CountLocalChangesWithoutUntracked.cs | 2 +- src/Commands/QueryLocalChanges.cs | 2 +- src/Commands/QuerySubmodules.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/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; From b4fbc2372ba5c097ec611a33a4136ede8b438a79 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 10 Mar 2025 20:02:09 +0800 Subject: [PATCH 04/41] enhance: show commit info tip when hover SHA in conflict view Signed-off-by: leo --- src/ViewModels/Conflict.cs | 35 ++++++++-- src/Views/Conflict.axaml | 122 +++++++++++++++++++++++++++++++++ src/Views/Conflict.axaml.cs | 23 +++++++ src/Views/WorkingCopy.axaml | 79 +-------------------- src/Views/WorkingCopy.axaml.cs | 9 --- 5 files changed, 176 insertions(+), 92 deletions(-) create mode 100644 src/Views/Conflict.axaml create mode 100644 src/Views/Conflict.axaml.cs 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/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..324d2375 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; @@ -368,6 +377,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; From cf8cff6b64eb78c96800ce5dbf46e75ebe2e9903 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 10 Mar 2025 21:30:04 +0800 Subject: [PATCH 08/41] code_style: add `ViewModels.Repository.GetCustomAction(scope)` Signed-off-by: leo --- src/ViewModels/Histories.cs | 12 +-------- src/ViewModels/Repository.cs | 47 ++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 301cbb3c..707e8c43 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -694,17 +694,7 @@ namespace SourceGit.ViewModels menu.Items.Add(archive); menu.Items.Add(new MenuItem() { Header = "-" }); - var actions = new List(); - foreach (var action in Preferences.Instance.CustomActions) - { - if (action.Scope == Models.CustomActionScope.Commit) - actions.Add(action); - } - 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/Repository.cs b/src/ViewModels/Repository.cs index 5d2778c9..96d0b36c 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -943,6 +943,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(); @@ -1443,22 +1462,10 @@ namespace SourceGit.ViewModels public ContextMenu CreateContextMenuForCustomAction() { - var actions = new List(); - foreach (var action in Preferences.Instance.CustomActions) - { - if (action.Scope == Models.CustomActionScope.Repository) - actions.Add(action); - } - - 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) @@ -2355,19 +2362,7 @@ namespace SourceGit.ViewModels private void TryToAddCustomActionsToBranchContextMenu(ContextMenu menu, Models.Branch branch) { - var actions = new List(); - foreach (var action in Preferences.Instance.CustomActions) - { - if (action.Scope == Models.CustomActionScope.Branch) - actions.Add(action); - } - - 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; From f496d15f70a7ae5395170f6289bce511129baeff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB=20=D0=A3=D1=81=D0=BE?= =?UTF-8?q?=D1=86=D0=BA=D0=B8=D0=B9?= Date: Mon, 10 Mar 2025 20:15:29 +0300 Subject: [PATCH 09/41] update russian localization --- src/Resources/Locales/ru_RU.axaml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index b15a769b..ff24912e 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -11,9 +11,9 @@ • Исходный код можно найти по адресу Бесплатный графический клиент Git с исходным кодом Добавить рабочий каталог - Что проверить: - Существующую ветку - Создать новую ветку + Переключиться на: + ветку из списка + создать новую ветку Расположение: Путь к рабочему каталогу (поддерживается относительный путь) Имя ветки: @@ -126,7 +126,7 @@ Сбросить ${0}$ сюда Отменить ревизию Изменить комментарий - Сохранить как patch-файл... + Сохранить как заплатки... Объединить с предыдущей ревизией Объединить все следующие ревизии с этим ИЗМЕНЕНИЯ @@ -167,7 +167,7 @@ Адрес электронной почты Адрес электронной почты GIT - Автоматическая загрузка изменений + Автозагрузка изменений Минут(а/ы) Внешний репозиторий по умолчанию ОТСЛЕЖИВАНИЕ ПРОБЛЕМ @@ -218,15 +218,15 @@ Создать метку... Новая метка у: GPG подпись - Сообщение с меткой: + Сообщение с меткой: Необязательно. Имя метки: Рекомендуемый формат: v1.0.0-alpha Выложить на все внешние репозитории после создания Создать новую метку Вид: - Аннотированный - Лёгкий + С примечаниями + Простой Удерживайте Ctrl, чтобы начать сразу Вырезать Удалить ветку @@ -237,7 +237,7 @@ Вы пытаетесь удалить несколько веток одновременно. Обязательно перепроверьте, прежде чем предпринимать какие-либо действия! Удалить внешний репозиторий Внешний репозиторий: - Path: + Путь: Цель: Все дочерние элементы будут удалены из списка. Подтвердите удаление группы @@ -262,7 +262,7 @@ НИКАКИХ ИЗМЕНЕНИЙ ИЛИ МЕНЯЕТСЯ ТОЛЬКО EOL Предыдущее различие Сохранить как заплатку - Различие бок о бок + Различие рядом ПОДМОДУЛЬ НОВЫЙ Обмен @@ -553,7 +553,7 @@ Редактировать внешний репозиторий Имя: Имя внешнего репозитория - Адрес репозитория: + Адрес: Адрес внешнего репозитория git Копировать адрес Удалить... @@ -693,8 +693,8 @@ Копировать относительный путь Извлечение вложенных подмодулей Открыть подмодуль репозитория - Относительный путь: - Относительный каталог для хранения подмодуля. + Каталог: + Относительный путь для хранения подмодуля. Удалить подмодуль ОК Копировать имя метки From 5f2bd8ad943c3fb5af83d95ec2e1bb0483653b87 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Mar 2025 09:27:32 +0800 Subject: [PATCH 10/41] ux: layout for `Preferences` window Signed-off-by: leo --- src/Views/Preferences.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 236deb1f..7bc83b10 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -422,7 +422,7 @@ - + From 54d49a9edafbec871f4c862011bedbcf21b7e51e Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Mar 2025 09:36:13 +0800 Subject: [PATCH 11/41] fix: app crashes when close a repository on read-only drive (#1080) Signed-off-by: leo --- src/ViewModels/Repository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 96d0b36c..31de381a 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -518,7 +518,7 @@ namespace SourceGit.ViewModels { File.WriteAllText(Path.Combine(_gitDir, "sourcegit.settings"), settingsSerialized); } - catch (DirectoryNotFoundException) + catch { // Ignore } From f23e3478e625362bc23fc428757139e57d270fea Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Mar 2025 11:41:37 +0800 Subject: [PATCH 12/41] fix: do not save preference in design mode Signed-off-by: leo --- src/Views/ConfigureWorkspace.axaml.cs | 4 +++- src/Views/Launcher.axaml.cs | 4 +++- src/Views/Preferences.axaml.cs | 6 +++++- src/Views/RepositoryConfigure.axaml.cs | 4 +++- src/Views/WorkingCopy.axaml.cs | 1 - 5 files changed, 14 insertions(+), 5 deletions(-) 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/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index b8c09b35..9ccec78c 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -273,8 +273,10 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { - (DataContext as ViewModels.Launcher)?.Quit(Width, Height); base.OnClosing(e); + + if (!Design.IsDesignMode && DataContext is ViewModels.Launcher launcher) + launcher.Quit(Width, Height); } private void OnOpenWorkspaceMenu(object sender, RoutedEventArgs e) diff --git a/src/Views/Preferences.axaml.cs b/src/Views/Preferences.axaml.cs index 324d2375..73b2e995 100644 --- a/src/Views/Preferences.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -169,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, ""); @@ -199,7 +204,6 @@ namespace SourceGit.Views } ViewModels.Preferences.Instance.Save(); - base.OnClosing(e); } private async void SelectThemeOverrideFile(object _, RoutedEventArgs e) 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/WorkingCopy.axaml.cs b/src/Views/WorkingCopy.axaml.cs index dfaad858..4ae4d779 100644 --- a/src/Views/WorkingCopy.axaml.cs +++ b/src/Views/WorkingCopy.axaml.cs @@ -1,7 +1,6 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.VisualTree; namespace SourceGit.Views { From 471452646b48c58b6bf64a069e777ba6fe0d9afa Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Mar 2025 16:53:51 +0800 Subject: [PATCH 13/41] refactor: use `System.Threading.CancellationToken` instead of `SourceGit.Commands.Command.CancelToken` to cancel fetching information of selected commit Signed-off-by: leo --- src/Commands/Command.cs | 48 ++++++++++++++-------------------- src/ViewModels/CommitDetail.cs | 35 ++++++++++++++----------- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index a3e6673b..cedaf674 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; try { proc.Start(); + + // It not safe, please only use `CancellationToken` in readonly commands. + if (CancellationToken.CanBeCanceled) + { + dummy = proc; + CancellationToken.Register(() => + { + if (dummy is { HasExited: false }) + { + dummy.CancelErrorRead(); + dummy.CancelOutputRead(); + dummy.Kill(); + } + }); + } } catch (Exception e) { @@ -113,10 +104,11 @@ namespace SourceGit.Commands proc.BeginErrorReadLine(); proc.WaitForExit(); + dummy = null; int exitCode = proc.ExitCode; proc.Close(); - if (!isCancelled && exitCode != 0) + if (!CancellationToken.IsCancellationRequested && exitCode != 0) { if (RaiseError) { diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 456e99f8..c6ef8367 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(() => { @@ -873,7 +878,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; From 2fc03025eef98c6641e8045232256691675ce7db Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Mar 2025 19:52:50 +0800 Subject: [PATCH 14/41] fix: file suggestion popup did not show while searching commit by file path Signed-off-by: leo --- src/ViewModels/Repository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 31de381a..ebf9210e 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -321,7 +321,7 @@ namespace SourceGit.ViewModels set { if (SetProperty(ref _searchCommitFilter, value) && - _searchCommitFilterType == 3 && + _searchCommitFilterType == 4 && !string.IsNullOrEmpty(value) && value.Length >= 2 && _revisionFiles.Count > 0) @@ -2395,14 +2395,14 @@ namespace SourceGit.ViewModels { _revisionFiles.Clear(); - if (_searchCommitFilterType == 3) + if (_searchCommitFilterType == 4) { Task.Run(() => { var files = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); Dispatcher.UIThread.Invoke(() => { - if (_searchCommitFilterType != 3) + if (_searchCommitFilterType != 4) return; _revisionFiles.AddRange(files); From 91c5c96afc3a070ca8b491a3e992ef5dbcb70d54 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Mar 2025 20:05:05 +0800 Subject: [PATCH 15/41] fix: accessing `dummy` in multi-threads throws exception Signed-off-by: leo --- src/Commands/Command.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index cedaf674..5197de11 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -73,6 +73,7 @@ namespace SourceGit.Commands }; var dummy = null as Process; + var dummyProcLock = new object(); try { proc.Start(); @@ -83,11 +84,10 @@ namespace SourceGit.Commands dummy = proc; CancellationToken.Register(() => { - if (dummy is { HasExited: false }) + lock (dummyProcLock) { - dummy.CancelErrorRead(); - dummy.CancelOutputRead(); - dummy.Kill(); + if (dummy is { HasExited: false }) + dummy.Kill(); } }); } @@ -104,7 +104,14 @@ namespace SourceGit.Commands proc.BeginErrorReadLine(); proc.WaitForExit(); - dummy = null; + if (dummy != null) + { + lock (dummyProcLock) + { + dummy = null; + } + } + int exitCode = proc.ExitCode; proc.Close(); From fa4d9d24e9ab5f1dfd8bf8f75fadddd8b903a23c Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Mar 2025 20:22:32 +0800 Subject: [PATCH 16/41] enhance: hide suggestion popups when window is deactived (#963) Signed-off-by: leo --- src/Views/Repository.axaml | 10 ++++++++-- src/Views/RevisionFiles.axaml | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 30180f7d..bfa5a5e1 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -421,8 +421,14 @@ + HorizontalOffset="-8" VerticalAlignment="-8"> + + + + + + + + HorizontalOffset="-8" VerticalAlignment="-8"> + + + + + + + Date: Tue, 11 Mar 2025 20:55:39 +0800 Subject: [PATCH 17/41] refactor: rewrite searching commit by file path Signed-off-by: leo --- src/ViewModels/Repository.cs | 112 +++++++++++++++------------------- src/Views/Repository.axaml | 4 +- src/Views/Repository.axaml.cs | 13 +--- 3 files changed, 53 insertions(+), 76 deletions(-) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index ebf9210e..9e10390a 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -271,14 +271,13 @@ namespace SourceGit.ViewModels { SearchedCommits = new List(); SearchCommitFilter = string.Empty; - SearchCommitFilterSuggestion.Clear(); - IsSearchCommitSuggestionOpen = false; - _revisionFiles.Clear(); + MatchedFilesForSearching = null; + _worktreeFiles = null; if (value) { SelectedViewIndex = 0; - UpdateCurrentRevisionFilesForSearchSuggestion(); + CalcWorktreeFilesForSearching(); } } } @@ -307,7 +306,7 @@ namespace SourceGit.ViewModels { if (SetProperty(ref _searchCommitFilterType, value)) { - UpdateCurrentRevisionFilesForSearchSuggestion(); + CalcWorktreeFilesForSearching(); if (!string.IsNullOrEmpty(_searchCommitFilter)) StartSearchCommits(); @@ -320,47 +319,22 @@ namespace SourceGit.ViewModels get => _searchCommitFilter; set { - if (SetProperty(ref _searchCommitFilter, value) && - _searchCommitFilterType == 4 && - !string.IsNullOrEmpty(value) && - value.Length >= 2 && - _revisionFiles.Count > 0) + if (SetProperty(ref _searchCommitFilter, value)) { - 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 (_searchCommitFilterType == 4 && value is { Length: > 2 }) + CalcMatchedFilesForSearching(); + else if (_matchedFilesForSearching is { }) + MatchedFilesForSearching = null; } } } - 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; @@ -551,8 +525,8 @@ namespace SourceGit.ViewModels _visibleSubmodules.Clear(); _searchedCommits.Clear(); - _revisionFiles.Clear(); - SearchCommitFilterSuggestion.Clear(); + _worktreeFiles = null; + _matchedFilesForSearching = null; } public bool CanCreatePopup() @@ -723,6 +697,11 @@ namespace SourceGit.ViewModels SearchCommitFilter = string.Empty; } + public void ClearMatchedFilesForSearching() + { + MatchedFilesForSearching = null; + } + public void StartSearchCommits() { if (_histories == null) @@ -730,8 +709,7 @@ namespace SourceGit.ViewModels IsSearchLoadingVisible = true; SearchResultSelectedCommit = null; - IsSearchCommitSuggestionOpen = false; - SearchCommitFilterSuggestion.Clear(); + MatchedFilesForSearching = null; Task.Run(() => { @@ -2391,9 +2369,9 @@ namespace SourceGit.ViewModels menu.Items.Add(new MenuItem() { Header = "-" }); } - private void UpdateCurrentRevisionFilesForSearchSuggestion() + private void CalcWorktreeFilesForSearching() { - _revisionFiles.Clear(); + _worktreeFiles = null; if (_searchCommitFilterType == 4) { @@ -2405,30 +2383,36 @@ namespace SourceGit.ViewModels if (_searchCommitFilterType != 4) return; - _revisionFiles.AddRange(files); + _worktreeFiles = new List(); + foreach (var f in files) + _worktreeFiles.Add(f); - 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; - } + if (_searchCommitFilter is { Length: > 2 }) + CalcMatchedFilesForSearching(); }); }); } } + private void CalcMatchedFilesForSearching() + { + if (_worktreeFiles == null || _worktreeFiles.Count == 0) + 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) { if (!_settings.EnableAutoFetch || _isAutoFetching) @@ -2475,13 +2459,13 @@ namespace SourceGit.ViewModels private bool _isSearching = false; private bool _isSearchLoadingVisible = false; - private bool _isSearchCommitSuggestionOpen = false; private int _searchCommitFilterType = 3; 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 List _worktreeFiles = null; + private List _matchedFilesForSearching = null; private string _filter = string.Empty; private object _lockRemotes = new object(); diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index bfa5a5e1..9caac015 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -424,7 +424,7 @@ HorizontalOffset="-8" VerticalAlignment="-8"> - + @@ -434,7 +434,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) From f5d6e1264d6e587c3ac6b90a9df3e0df30650f9e Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Mar 2025 23:01:34 +0800 Subject: [PATCH 18/41] refactor: use `List` instead of `AvaloniaList` since it is not used for bindings Signed-off-by: leo --- src/ViewModels/Repository.cs | 10 ++++++---- src/ViewModels/WorkingCopy.cs | 32 ++++++++++++++------------------ 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 9e10390a..59ab6a1b 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; @@ -1221,23 +1220,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() 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) From 231f3bf6688485be75a5295f5307e115889e5296 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 12 Mar 2025 10:11:00 +0800 Subject: [PATCH 19/41] enhance: use `--ancestry-path=` to reduce unnecessary outpus while querying children commits Signed-off-by: leo --- src/Commands/QueryCommitChildren.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From ee7ccc0391ee5e7e03f5f8d9cf496804170cda4a Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 12 Mar 2025 11:05:19 +0800 Subject: [PATCH 20/41] refactor: re-write commit searching (part 2) Signed-off-by: leo --- src/Models/Commit.cs | 1 + src/ViewModels/Histories.cs | 12 ++-- src/ViewModels/Repository.cs | 132 +++++++++++++++++------------------ src/Views/Repository.axaml | 2 +- 4 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 5c48b0c0..72ce54bd 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, diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 707e8c43..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); diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 59ab6a1b..c7557356 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -268,16 +268,19 @@ namespace SourceGit.ViewModels { if (SetProperty(ref _isSearching, value)) { - SearchedCommits = new List(); - SearchCommitFilter = string.Empty; - MatchedFilesForSearching = null; - _worktreeFiles = null; - if (value) { SelectedViewIndex = 0; CalcWorktreeFilesForSearching(); } + else + { + SearchedCommits = new List(); + SelectedSearchedCommit = null; + SearchCommitFilter = string.Empty; + MatchedFilesForSearching = null; + _worktreeFiles = null; + } } } } @@ -306,7 +309,6 @@ namespace SourceGit.ViewModels if (SetProperty(ref _searchCommitFilterType, value)) { CalcWorktreeFilesForSearching(); - if (!string.IsNullOrEmpty(_searchCommitFilter)) StartSearchCommits(); } @@ -318,13 +320,8 @@ namespace SourceGit.ViewModels get => _searchCommitFilter; set { - if (SetProperty(ref _searchCommitFilter, value)) - { - if (_searchCommitFilterType == 4 && value is { Length: > 2 }) - CalcMatchedFilesForSearching(); - else if (_matchedFilesForSearching is { }) - MatchedFilesForSearching = null; - } + if (SetProperty(ref _searchCommitFilter, value) && IsSearchingCommitsByFilePath()) + CalcMatchedFilesForSearching(); } } @@ -340,6 +337,16 @@ namespace SourceGit.ViewModels 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; @@ -410,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; @@ -523,6 +520,7 @@ namespace SourceGit.ViewModels _submodules.Clear(); _visibleSubmodules.Clear(); _searchedCommits.Clear(); + _selectedSearchedCommit = null; _worktreeFiles = null; _matchedFilesForSearching = null; @@ -707,32 +705,22 @@ namespace SourceGit.ViewModels return; IsSearchLoadingVisible = true; - SearchResultSelectedCommit = null; + 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(() => @@ -1636,7 +1624,7 @@ namespace SourceGit.ViewModels compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); compareWithWorktree.Click += (_, _) => { - SearchResultSelectedCommit = null; + SelectedSearchedCommit = null; if (_histories != null) { @@ -1918,7 +1906,7 @@ namespace SourceGit.ViewModels compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); compareWithWorktree.Click += (_, _) => { - SearchResultSelectedCommit = null; + SelectedSearchedCommit = null; if (_histories != null) { @@ -2371,35 +2359,45 @@ namespace SourceGit.ViewModels menu.Items.Add(new MenuItem() { Header = "-" }); } + private bool IsSearchingCommitsByFilePath() + { + return _isSearching && _searchCommitFilterType == (int)Models.CommitSearchMethod.ByFile; + } + private void CalcWorktreeFilesForSearching() { - _worktreeFiles = null; - - if (_searchCommitFilterType == 4) + if (!IsSearchingCommitsByFilePath()) { - Task.Run(() => - { - var files = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); - Dispatcher.UIThread.Invoke(() => - { - if (_searchCommitFilterType != 4) - return; - - _worktreeFiles = new List(); - foreach (var f in files) - _worktreeFiles.Add(f); - - if (_searchCommitFilter is { Length: > 2 }) - CalcMatchedFilesForSearching(); - }); - }); + _worktreeFiles = null; + MatchedFilesForSearching = null; + GC.Collect(); + return; } + + Task.Run(() => + { + var files = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); + Dispatcher.UIThread.Invoke(() => + { + if (!IsSearchingCommitsByFilePath()) + return; + + _worktreeFiles = new List(); + foreach (var f in files) + _worktreeFiles.Add(f); + + CalcMatchedFilesForSearching(); + }); + }); } private void CalcMatchedFilesForSearching() { - if (_worktreeFiles == null || _worktreeFiles.Count == 0) + if (_worktreeFiles == null || _worktreeFiles.Count == 0 || _searchCommitFilter.Length < 3) + { + MatchedFilesForSearching = null; return; + } var matched = new List(); foreach (var file in _worktreeFiles) @@ -2461,11 +2459,11 @@ namespace SourceGit.ViewModels private bool _isSearching = false; private bool _isSearchLoadingVisible = 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 Models.Commit _selectedSearchedCommit = null; private List _worktreeFiles = null; private List _matchedFilesForSearching = null; diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 9caac015..1d94e9b2 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -504,7 +504,7 @@ Margin="0,8,0,0" ItemsSource="{Binding SearchedCommits}" SelectionMode="Single" - SelectedItem="{Binding SearchResultSelectedCommit, Mode=TwoWay}" + SelectedItem="{Binding SelectedSearchedCommit, Mode=TwoWay}" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" Background="{DynamicResource Brush.Contents}" From bb2284c4c91d4c95ddbc3bb0d5c0d3c951b2e756 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 12 Mar 2025 11:53:24 +0800 Subject: [PATCH 21/41] refactor: re-write commit searching (part 3) Signed-off-by: leo --- src/Commands/QueryRevisionFileNames.cs | 16 +++++++++++----- src/ViewModels/CommitDetail.cs | 5 +---- src/ViewModels/Repository.cs | 12 +++--------- 3 files changed, 15 insertions(+), 18 deletions(-) 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/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index c6ef8367..34ac8308 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -814,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(); } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index c7557356..97c52d8e 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -2376,17 +2376,11 @@ namespace SourceGit.ViewModels Task.Run(() => { - var files = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); + _worktreeFiles = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); Dispatcher.UIThread.Invoke(() => { - if (!IsSearchingCommitsByFilePath()) - return; - - _worktreeFiles = new List(); - foreach (var f in files) - _worktreeFiles.Add(f); - - CalcMatchedFilesForSearching(); + if (IsSearchingCommitsByFilePath()) + CalcMatchedFilesForSearching(); }); }); } From 0476a825efffa277e751eeb8352ed9c93d41f1c5 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 12 Mar 2025 14:58:59 +0800 Subject: [PATCH 22/41] code_style: move some code from `Histories.axaml` to separate files Signed-off-by: leo --- src/Views/CommitGraph.cs | 228 +++++++++ src/Views/CommitStatusIndicator.cs | 90 ++++ src/Views/CommitSubjectPresenter.cs | 189 ++++++++ src/Views/CommitTimeTextBlock.cs | 163 +++++++ src/Views/Histories.axaml | 12 +- src/Views/Histories.axaml.cs | 695 ++-------------------------- 6 files changed, 705 insertions(+), 672 deletions(-) create mode 100644 src/Views/CommitGraph.cs create mode 100644 src/Views/CommitStatusIndicator.cs create mode 100644 src/Views/CommitSubjectPresenter.cs create mode 100644 src/Views/CommitTimeTextBlock.cs 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..6947f7f2 --- /dev/null +++ b/src/Views/CommitTimeTextBlock.cs @@ -0,0 +1,163 @@ +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) + return App.Text("Period.HoursAgo", (int)span.TotalHours); + + 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/Histories.axaml b/src/Views/Histories.axaml index 40b2636f..583e17c1 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -10,18 +10,18 @@ x:Class="SourceGit.Views.Histories" x:DataType="vm:Histories" x:Name="ThisControl"> - - + + - + - + - + @@ -264,5 +264,5 @@ - + diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 8b5142ad..62c283e5 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -1,24 +1,18 @@ using System; -using System.Collections.Generic; using System.Text; -using System.Text.RegularExpressions; using Avalonia; using Avalonia.Collections; using Avalonia.Controls; -using Avalonia.Controls.Documents; using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Media; -using Avalonia.Threading; using Avalonia.VisualTree; namespace SourceGit.Views { - public class LayoutableGrid : Grid + public class HistoriesLayout : Grid { public static readonly StyledProperty UseHorizontalProperty = - AvaloniaProperty.Register(nameof(UseHorizontal)); + AvaloniaProperty.Register(nameof(UseHorizontal)); public bool UseHorizontal { @@ -28,17 +22,20 @@ namespace SourceGit.Views protected override Type StyleKeyOverride => typeof(Grid); - static LayoutableGrid() - { - UseHorizontalProperty.Changed.AddClassHandler((o, _) => o.RefreshLayout()); - } - public override void ApplyTemplate() { base.ApplyTemplate(); RefreshLayout(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == UseHorizontalProperty) + RefreshLayout(); + } + private void RefreshLayout() { if (UseHorizontal) @@ -74,639 +71,6 @@ 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; - } - - 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; - } - - 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) - return App.Text("Period.HoursAgo", (int)span.TotalHours); - - 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; - } - - 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; - } - } - } - } - public partial class Histories : UserControl { public static readonly StyledProperty AuthorNameColumnWidthProperty = @@ -754,36 +118,34 @@ namespace SourceGit.Views set => SetValue(NavigationIdProperty, value); } - static Histories() - { - NavigationIdProperty.Changed.AddClassHandler((h, _) => - { - if (h.DataContext == null) - return; - - // Force scroll selected item (current head) into view. see issue #58 - var list = h.CommitListContainer; - if (list != null && list.SelectedItems.Count == 1) - list.ScrollIntoView(list.SelectedIndex); - }); - - AuthorNameColumnWidthProperty.Changed.AddClassHandler((h, _) => - { - h.CommitGraph.InvalidateVisual(); - }); - } - public Histories() { InitializeComponent(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == NavigationIdProperty) + { + if (DataContext is ViewModels.Histories) + { + var list = CommitListContainer; + if (list != null && list.SelectedItems.Count == 1) + list.ScrollIntoView(list.SelectedIndex); + } + } + } + private void OnCommitListLayoutUpdated(object _1, EventArgs _2) { var y = CommitListContainer.Scroll?.Offset.Y ?? 0; - if (y != _lastScrollY) + var authorNameColumnWidth = AuthorNameColumnWidth.Value; + if (y != _lastScrollY || authorNameColumnWidth != _lastAuthorNameColumnWidth) { _lastScrollY = y; + _lastAuthorNameColumnWidth = authorNameColumnWidth; CommitGraph.InvalidateVisual(); } } @@ -863,5 +225,6 @@ namespace SourceGit.Views } private double _lastScrollY = 0; + private double _lastAuthorNameColumnWidth = 0; } } From 77d8afe056963fe23f78b15660948fcbb8bd1c91 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 12 Mar 2025 15:10:43 +0800 Subject: [PATCH 23/41] refactor: reduce the times to call `RefreshLayout` Signed-off-by: leo --- src/Views/Histories.axaml.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 62c283e5..f7055dfa 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -22,9 +22,8 @@ namespace SourceGit.Views protected override Type StyleKeyOverride => typeof(Grid); - public override void ApplyTemplate() + public HistoriesLayout() { - base.ApplyTemplate(); RefreshLayout(); } From eaa322dfabd9b91eb4afe360a68645b874f16ed8 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 12 Mar 2025 17:57:31 +0800 Subject: [PATCH 24/41] enhance: re-design commit search result display (#1083) Signed-off-by: leo --- src/Models/Commit.cs | 1 + src/Views/Repository.axaml | 49 ++++++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 72ce54bd..0bad8376 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -36,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/Views/Repository.axaml b/src/Views/Repository.axaml index 1d94e9b2..c8bbde5f 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -482,13 +482,13 @@ Padding="4,0" Background="Transparent" BorderThickness="0" - SelectedIndex="{Binding SearchCommitFilterType, Mode=TwoWay}"> + SelectedIndex="{Binding SearchCommitFilterType, Mode=TwoWay}"> - - - - - + + + + + @@ -496,12 +496,13 @@ 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"> @@ -527,18 +530,22 @@ - - - - - - - - - - - - + + + + + + + + + + From e4f5c34e0cbe62345e370e723d47b00aacddc626 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 12 Mar 2025 17:58:57 +0800 Subject: [PATCH 25/41] code_style: remove whitespaces Signed-off-by: leo --- src/Views/Repository.axaml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index c8bbde5f..b16447fa 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -86,7 +86,7 @@ - + - + + VerticalContentAlignment="Center"> - @@ -428,7 +428,7 @@ - + + SelectedIndex="{Binding SearchCommitFilterType, Mode=TwoWay}"> @@ -562,7 +562,7 @@ - + - + @@ -649,7 +649,7 @@ - + From f07832c38553cf35b4f7bc521290cc16507e954d Mon Sep 17 00:00:00 2001 From: Morgan Courbet Date: Wed, 12 Mar 2025 15:37:28 +0100 Subject: [PATCH 26/41] docs: fix typo in README.md (cherry picked from commit 59fd2aaab144aa8313ecbfcb5457457359fa0c4f) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4086a641..399916c9 100644 --- a/README.md +++ b/README.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 From 7331167be2ca09e0fc9378934fad18c48062d329 Mon Sep 17 00:00:00 2001 From: Gadfly Date: Thu, 13 Mar 2025 09:38:08 +0800 Subject: [PATCH 27/41] fix: schedule DWM frame extension to next render frame on Windows 10 (#1087) The DwmExtendFrameIntoClientArea call needs to be posted to the next render frame to ensure the window handle is fully initialized and avoid potential race conditions. --- src/Native/Windows.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 From e430e847ff957e23c9e146197f6ce8690730645e Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 13 Mar 2025 09:49:25 +0800 Subject: [PATCH 28/41] enhance: auto convert spaces with dashes while renaming a branch (#1088) Signed-off-by: leo --- src/ViewModels/RenameBranch.cs | 21 ++++++++++++++++----- src/Views/CreateBranch.axaml | 12 ++---------- src/Views/RenameBranch.axaml | 10 +++++++++- 3 files changed, 27 insertions(+), 16 deletions(-) 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/Views/CreateBranch.axaml b/src/Views/CreateBranch.axaml index ec56ff20..b757bd78 100644 --- a/src/Views/CreateBranch.axaml +++ b/src/Views/CreateBranch.axaml @@ -14,15 +14,7 @@ - - - - - - - - - + - + @@ -11,7 +12,7 @@ - + + + + + + From 0e261cffd20a39847f4a6377715eecb7a8c307aa Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 13 Mar 2025 10:21:54 +0800 Subject: [PATCH 29/41] refactor: rewrite the way to deal with uncommitted local changes when checkout/pull/create branch (#1085) Signed-off-by: leo --- src/Models/DealWithLocalChanges.cs | 9 ------ src/Resources/Locales/de_DE.axaml | 3 -- src/Resources/Locales/en_US.axaml | 3 -- src/Resources/Locales/es_ES.axaml | 3 -- src/Resources/Locales/fr_FR.axaml | 3 -- src/Resources/Locales/it_IT.axaml | 3 -- src/Resources/Locales/pt_BR.axaml | 3 -- src/Resources/Locales/ru_RU.axaml | 3 -- src/Resources/Locales/zh_CN.axaml | 3 -- src/Resources/Locales/zh_TW.axaml | 3 -- src/ViewModels/Checkout.cs | 17 ++++++----- src/ViewModels/CheckoutCommit.cs | 20 ++++++------- src/ViewModels/CreateBranch.cs | 19 +++++++----- src/ViewModels/Pull.cs | 16 +++++----- src/Views/Checkout.axaml | 22 +++++--------- src/Views/Checkout.axaml.cs | 47 ------------------------------ src/Views/CheckoutCommit.axaml | 12 ++++---- src/Views/CreateBranch.axaml | 18 ++++-------- src/Views/CreateBranch.axaml.cs | 47 ------------------------------ src/Views/Pull.axaml | 22 +++++--------- src/Views/Pull.axaml.cs | 47 ------------------------------ 21 files changed, 63 insertions(+), 260 deletions(-) delete mode 100644 src/Models/DealWithLocalChanges.cs 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/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 1d0ca895..759e3a53 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. @@ -517,7 +515,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..90125de8 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. @@ -520,7 +518,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..bcf09e1a 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. @@ -524,7 +522,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..76405b21 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. @@ -492,7 +490,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..f944689c 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. @@ -523,7 +521,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..ebff746c 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. @@ -506,7 +504,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 ff24912e..6815bbeb 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -91,7 +91,6 @@ Ветка: Локальные изменения: Отклонить - Ничего не делать Отложить и примненить повторно Частичный выбор Добавить источник для ревизии сообщения @@ -209,7 +208,6 @@ Проверить созданную ветку Локальные изменения: Отклонить - Ничего не делать Отложить и применить повторно Имя новой ветки: Введите имя ветки. @@ -524,7 +522,6 @@ В: Локальные изменения: Отклонить - Ничего не делать Отложить и применить повторно Забрать без меток Внешний репозиторий: diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 72b434f9..cf796805 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -91,7 +91,6 @@ 目标分支 : 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 挑选提交 提交信息中追加来源信息 @@ -208,7 +207,6 @@ 完成后切换到新分支 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 新分支名 : 填写分支名称。 @@ -524,7 +522,6 @@ 本地分支 : 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 不拉取远程标签 远程 : diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index bc9991f6..539001bd 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -91,7 +91,6 @@ 目標分支: 未提交變更: 捨棄變更 - 不做處理 擱置變更並自動復原 揀選提交 提交資訊中追加來源資訊 @@ -208,7 +207,6 @@ 完成後切換到新分支 未提交變更: 捨棄變更 - 不做處理 擱置變更並自動復原 新分支名稱: 輸入分支名稱。 @@ -523,7 +521,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/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/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/Views/Checkout.axaml b/src/Views/Checkout.axaml index eb1c9de0..3cdfd94e 100644 --- a/src/Views/Checkout.axaml +++ b/src/Views/Checkout.axaml @@ -18,7 +18,7 @@ - + - - + - - + 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"/> - - - - + Content="{DynamicResource Text.CreateBranch.LocalChanges.StashAndReply}" + IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/> + diff --git a/src/Views/CreateBranch.axaml.cs b/src/Views/CreateBranch.axaml.cs index 6499e1c7..6626871b 100644 --- a/src/Views/CreateBranch.axaml.cs +++ b/src/Views/CreateBranch.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.CreateBranch; - 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.CreateBranch; - 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/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; - } } } From 519bdf1ddc1bb7bba6fb0187d1d9d208797115e6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 13 Mar 2025 02:22:09 +0000 Subject: [PATCH 30/41] doc: Update translation status and missing keys --- README.md | 2 +- TRANSLATION.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 399916c9..cc37aae9 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.64%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.38%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) diff --git a/TRANSLATION.md b/TRANSLATION.md index a5abbbba..155e031a 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.64%
@@ -106,7 +106,7 @@
-### pt_BR.axaml: 91.41% +### pt_BR.axaml: 91.38%
From b9b684a83d58740d2bd802e08ad83746fcb97359 Mon Sep 17 00:00:00 2001 From: Gadfly Date: Thu, 13 Mar 2025 15:05:30 +0800 Subject: [PATCH 31/41] fix: improve font string processing in SetFonts method (#1092) --- src/App.axaml.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 25e32323..504981f7 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -181,6 +181,9 @@ namespace SourceGit app._fontsOverrides = null; } + defaultFont = ProcessFontString(defaultFont); + monospaceFont = ProcessFontString(monospaceFont); + var resDic = new ResourceDictionary(); if (!string.IsNullOrEmpty(defaultFont)) resDic.Add("Fonts.Default", new FontFamily(defaultFont)); @@ -437,6 +440,28 @@ namespace SourceGit return true; } + private static string ProcessFontString(string input) + { + if (string.IsNullOrEmpty(input)) return string.Empty; + + var parts = input.Split(','); + var result = new StringBuilder(); + var isFirst = true; + + foreach (var part in parts) + { + var trimmed = part.Trim(); + if (!string.IsNullOrEmpty(trimmed)) + { + if (!isFirst) result.Append(','); + result.Append(trimmed); + isFirst = false; + } + } + + return result.ToString(); + } + private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) { var args = desktop.Args; From 9560496c7bf9cec50f98f19d8b37dbe98d71fcf9 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 13 Mar 2025 15:17:20 +0800 Subject: [PATCH 32/41] code_review: PR #1092 - Remove `SourceGit.ViewModels.Preference.FixFontFamilyName` (it is not necessary any more) - Use `string.Join` instead of `StringBuilder` to make the logic more clear Signed-off-by: leo --- src/App.axaml.cs | 24 +++++++++------------ src/ViewModels/Preferences.cs | 40 ++++------------------------------- 2 files changed, 14 insertions(+), 50 deletions(-) diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 504981f7..79eeda4e 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -181,8 +181,8 @@ namespace SourceGit app._fontsOverrides = null; } - defaultFont = ProcessFontString(defaultFont); - monospaceFont = ProcessFontString(monospaceFont); + defaultFont = FixFontFamilyName(defaultFont); + monospaceFont = FixFontFamilyName(monospaceFont); var resDic = new ResourceDictionary(); if (!string.IsNullOrEmpty(defaultFont)) @@ -440,26 +440,22 @@ namespace SourceGit return true; } - private static string ProcessFontString(string input) + private static string FixFontFamilyName(string input) { - if (string.IsNullOrEmpty(input)) return string.Empty; + if (string.IsNullOrEmpty(input)) + return string.Empty; var parts = input.Split(','); - var result = new StringBuilder(); - var isFirst = true; + var trimmed = new List(); foreach (var part in parts) { - var trimmed = part.Trim(); - if (!string.IsNullOrEmpty(trimmed)) - { - if (!isFirst) result.Append(','); - result.Append(trimmed); - isFirst = false; - } + var t = part.Trim(); + if (!string.IsNullOrEmpty(t)) + trimmed.Add(t); } - return result.ToString(); + return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; } private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index dae90517..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); } } @@ -620,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; From 67f4330dd4799c4358be3852a566207aae30cb3f Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 13 Mar 2025 15:23:43 +0800 Subject: [PATCH 33/41] code_style: arrange methods in `App.axaml.cs` Signed-off-by: leo --- src/App.axaml.cs | 90 ++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 79eeda4e..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,8 +206,8 @@ namespace SourceGit app._fontsOverrides = null; } - defaultFont = FixFontFamilyName(defaultFont); - monospaceFont = FixFontFamilyName(monospaceFont); + defaultFont = app.FixFontFamilyName(defaultFont); + monospaceFont = app.FixFontFamilyName(monospaceFont); var resDic = new ResourceDictionary(); if (!string.IsNullOrEmpty(defaultFont)) @@ -328,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; @@ -440,24 +440,6 @@ namespace SourceGit return true; } - private static 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 bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) { var args = desktop.Args; @@ -567,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; From 9645b65db669af8fa2b5b24df0e2765d0d576182 Mon Sep 17 00:00:00 2001 From: Michael Pakhantsov Date: Fri, 14 Mar 2025 03:26:59 +0200 Subject: [PATCH 34/41] Explicitly provided fully qualified reference for the git branch, becase can be exists a tag and a branch with identical names (#1093) Fix push command for branch deletion Updated the `push` command to use `--delete refs/heads/{name}` instead of `--delete {name}` for clearer branch reference when deleting a remote branch. Co-authored-by: Michael Pakhantsov --- src/Commands/Branch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 391aeeb2..d56dfa97 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -62,7 +62,7 @@ if (exists) { cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - cmd.Args = $"push {remote} --delete {name}"; + cmd.Args = $"push {remote} --delete refs/heads/{name}"; } else { From c8bee2f6ba9f611e125126b7729a91ebdb2b7d31 Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 14 Mar 2025 09:36:34 +0800 Subject: [PATCH 35/41] code_review: PR #1093 Merge deleting branch and tag on remote into `SourceGit.Commands.Push(repo, remote, refname, isDelete)` Signed-off-by: leo --- src/Commands/Branch.cs | 17 +++++------------ src/Commands/Push.cs | 4 ++-- src/Commands/Tag.cs | 4 +--- src/ViewModels/CreateTag.cs | 2 +- src/ViewModels/PushTag.cs | 7 ++++--- 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index d56dfa97..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 refs/heads/{name}"; - } - else - { - cmd.Args = $"branch -D -r {remote}/{name}"; - } - + cmd.Args = $"branch -D -r {remote}/{name}"; return cmd.Exec(); } } 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/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/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/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)); From c3e1fb93b6fcd42b5156c999371e1f4f61537fb5 Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 14 Mar 2025 10:54:09 +0800 Subject: [PATCH 36/41] refactor: fix maxOS `PATH` env Signed-off-by: leo --- src/Commands/Command.cs | 4 ---- src/Commands/ExecuteCustomAction.cs | 8 -------- src/Native/MacOS.cs | 15 ++++++++++++++- src/Native/OS.cs | 6 ------ 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 5197de11..0fef1235 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -198,10 +198,6 @@ namespace SourceGit.Commands start.Environment.Add("LC_ALL", "C"); } - // Fix macOS `PATH` env - if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv)) - start.Environment.Add("PATH", Native.OS.CustomPathEnv); - // Force using this app as git editor. switch (Editor) { diff --git a/src/Commands/ExecuteCustomAction.cs b/src/Commands/ExecuteCustomAction.cs index 894637a5..000c8fd1 100644 --- a/src/Commands/ExecuteCustomAction.cs +++ b/src/Commands/ExecuteCustomAction.cs @@ -17,10 +17,6 @@ namespace SourceGit.Commands start.CreateNoWindow = true; start.WorkingDirectory = repo; - // Fix macOS `PATH` env - if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv)) - start.Environment.Add("PATH", Native.OS.CustomPathEnv); - try { Process.Start(start); @@ -44,10 +40,6 @@ namespace SourceGit.Commands start.StandardErrorEncoding = Encoding.UTF8; start.WorkingDirectory = repo; - // 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/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; From db504241eae8fc93813b4eca2ddbdab3c52306b8 Mon Sep 17 00:00:00 2001 From: Asurada <43401755+ousugo@users.noreply.github.com> Date: Fri, 14 Mar 2025 16:57:03 +0800 Subject: [PATCH 37/41] feat: add translation for "1 hour ago" in multiple languages (#1096) --- src/Resources/Locales/de_DE.axaml | 1 + src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/es_ES.axaml | 1 + src/Resources/Locales/fr_FR.axaml | 1 + src/Resources/Locales/it_IT.axaml | 1 + src/Resources/Locales/pt_BR.axaml | 1 + src/Resources/Locales/ru_RU.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/Views/CommitTimeTextBlock.cs | 5 ++++- 10 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 759e3a53..aff8ffc5 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -443,6 +443,7 @@ Einfügen Gerade eben Vor {0} Minuten + Vor 1 Stunde Vor {0} Stunden Gestern Vor {0} Tagen diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 90125de8..818bd9bb 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -444,6 +444,7 @@ Paste Just now {0} minutes ago + 1 hour ago {0} hours ago Yesterday {0} days ago diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index bcf09e1a..e909a14e 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -447,6 +447,7 @@ Pegar Justo ahora Hace {0} minutos + Hace 1 hora Hace {0} horas Ayer Hace {0} días diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 76405b21..aecea9ad 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -422,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 diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index f944689c..4dcc8771 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -448,6 +448,7 @@ Incolla Proprio ora {0} minuti fa + 1 ora fa {0} ore fa Ieri {0} giorni fa diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index ebff746c..b146bf0e 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -435,6 +435,7 @@ Colar Agora mesmo {0} minutos atrás + 1 hora atrás {0} horas atrás Ontem {0} dias atrás diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 6815bbeb..07fb7c94 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -448,6 +448,7 @@ Вставить Сейчас {0} минут назад + 1 час назад {0} часов назад Вчера {0} дней назад diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index cf796805..2d160ad2 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -447,6 +447,7 @@ 粘贴 刚刚 {0}分钟前 + 1小时前 {0}小时前 昨天 {0}天前 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 539001bd..e50a600d 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -447,6 +447,7 @@ 貼上 剛剛 {0} 分鐘前 + 1 小時前 {0} 小時前 昨天 {0} 天前 diff --git a/src/Views/CommitTimeTextBlock.cs b/src/Views/CommitTimeTextBlock.cs index 6947f7f2..db63e8a6 100644 --- a/src/Views/CommitTimeTextBlock.cs +++ b/src/Views/CommitTimeTextBlock.cs @@ -129,7 +129,10 @@ namespace SourceGit.Views return App.Text("Period.MinutesAgo", (int)span.TotalMinutes); if (span.TotalDays < 1) - return App.Text("Period.HoursAgo", (int)span.TotalHours); + { + 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) From a46e52582fd9f8e5930fe76bec86386380d4cf7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 14 Mar 2025 08:57:15 +0000 Subject: [PATCH 38/41] doc: Update translation status and missing keys --- README.md | 2 +- TRANSLATION.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc37aae9..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.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.64%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.38%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) diff --git a/TRANSLATION.md b/TRANSLATION.md index 155e031a..e3f9d2a1 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -24,7 +24,7 @@
-### fr_FR.axaml: 91.64% +### fr_FR.axaml: 91.66%
@@ -106,7 +106,7 @@
-### pt_BR.axaml: 91.38% +### pt_BR.axaml: 91.39%
From 66517fd4bf1ed05bd91a1002cfc9d9a508559131 Mon Sep 17 00:00:00 2001 From: Gadfly Date: Sun, 16 Mar 2025 11:23:42 +0800 Subject: [PATCH 39/41] enhance: add tooltips to various UI elements for better accessibility (#1097) * enhance: add tooltips to various UI elements for better accessibility * refactor: simplify user string conversion --- src/Models/User.cs | 5 +++++ src/Views/ChangeCollectionView.axaml | 13 ++++++++++--- src/Views/DiffView.axaml | 2 +- src/Views/Histories.axaml | 11 +++++++++-- src/Views/RevisionFiles.axaml | 2 +- 5 files changed, 26 insertions(+), 7 deletions(-) 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/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}"> - + - + - + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 583e17c1..afe2c1b7 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -126,7 +126,11 @@ - + - + - + From 84979b20b330144a0dea8ea41ae48986943e079c Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 17 Mar 2025 09:32:31 +0800 Subject: [PATCH 40/41] ux: force using `VertialAlignment="Center"` for sign info of commit (#1098) Signed-off-by: leo --- src/Views/CommitBaseInfo.axaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index 4ff8f20f..692d04b8 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -80,10 +80,10 @@ - - - - + + + + From 34f86189895c74e7ddd798ead684782f4263ff49 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 17 Mar 2025 09:37:40 +0800 Subject: [PATCH 41/41] version: Release 2025.09 Signed-off-by: leo --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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