diff --git a/.editorconfig b/.editorconfig index 3ad9d05b..83b15884 100644 --- a/.editorconfig +++ b/.editorconfig @@ -293,6 +293,10 @@ end_of_line = lf [*.{cmd,bat}] end_of_line = crlf +# Package manifests +[{*.spec,control}] +end_of_line = lf + # YAML files [*.{yml,yaml}] indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 7410eb08..bd1dfea9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,10 +3,12 @@ *.png binary *.ico binary *.sh text eol=lf +*.spec text eol=lf +control text eol=lf *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf *.json text .gitattributes export-ignore -.gitignore export-ignore \ No newline at end of file +.gitignore export-ignore diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 53affe3d..e973c1ab 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -3,7 +3,7 @@ on: workflow_call: inputs: version: - description: Source Git package version + description: SourceGit package version required: true type: string jobs: diff --git a/README.md b/README.md index e464f74c..85e3ae64 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ ## Translation Status -[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-98.70%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-100.00%25-brightgreen)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-86.58%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-100.00%25-brightgreen)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-100.00%25-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](TRANSLATION.md) +[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-100.00%25-brightgreen)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-99.57%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-86.31%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-99.57%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-100.00%25-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](TRANSLATION.md) ## How to Use diff --git a/TRANSLATION.md b/TRANSLATION.md index cda4a773..c646acf4 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -1,22 +1,4 @@ -### de_DE.axaml: 98.70% - - -
-Missing Keys - -- Text.Diff.SaveAsPatch -- Text.Diff.VisualLines.All -- Text.Hotkeys.Repo.CreateBranchOnCommit -- Text.Hotkeys.Repo.Fetch -- Text.Hotkeys.Repo.Pull -- Text.Hotkeys.Repo.Push -- Text.IssueLinkCM.OpenInBrowser -- Text.IssueLinkCM.CopyLink -- Text.Preference.Appearance.EditorFontSize - -
- -### es_ES.axaml: 100.00% +### de_DE.axaml: 100.00%
@@ -26,7 +8,19 @@
-### fr_FR.axaml: 86.58% +### es_ES.axaml: 99.57% + + +
+Missing Keys + +- Text.Preference.Appearance.FontSize +- Text.Preference.Appearance.FontSize.Default +- Text.Preference.Appearance.FontSize.Editor + +
+ +### fr_FR.axaml: 86.31%
@@ -99,7 +93,9 @@ - Text.Preference.AI.Model - Text.Preference.AI.Name - Text.Preference.AI.Server -- Text.Preference.Appearance.EditorFontSize +- Text.Preference.Appearance.FontSize +- Text.Preference.Appearance.FontSize.Default +- Text.Preference.Appearance.FontSize.Editor - Text.Preference.General.ShowAuthorTime - Text.Preference.Integration - Text.Preference.Shell @@ -128,13 +124,15 @@
-### pt_BR.axaml: 100.00% +### pt_BR.axaml: 99.57%
Missing Keys - +- Text.Preference.Appearance.FontSize +- Text.Preference.Appearance.FontSize.Default +- Text.Preference.Appearance.FontSize.Editor
diff --git a/build/resources/_common/applications/sourcegit.desktop b/build/resources/_common/applications/sourcegit.desktop index ff7ef135..bcf9c813 100644 --- a/build/resources/_common/applications/sourcegit.desktop +++ b/build/resources/_common/applications/sourcegit.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Name=Source Git +Name=SourceGit Comment=Open-source & Free Git GUI Client Exec=/opt/sourcegit/sourcegit Icon=/usr/share/icons/sourcegit.png diff --git a/build/resources/rpm/SPECS/build.spec b/build/resources/rpm/SPECS/build.spec index 9dda5f96..d31b6587 100644 --- a/build/resources/rpm/SPECS/build.spec +++ b/build/resources/rpm/SPECS/build.spec @@ -5,8 +5,8 @@ Summary: Open-source & Free Git Gui Client License: MIT URL: https://sourcegit-scm.github.io/ Source: https://github.com/sourcegit-scm/sourcegit/archive/refs/tags/v%_version.tar.gz -Requires: (libX11 or libX11-6) -Requires: (libSM or libSM6) +Requires: libX11.so.6()(%{__isa_bits}bit) +Requires: libSM.so.6()(%{__isa_bits}bit) %define _build_id_links none diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh index 5ffd5a27..5abb058b 100755 --- a/build/scripts/package.linux.sh +++ b/build/scripts/package.linux.sh @@ -5,16 +5,6 @@ set -o set -u set pipefail -if [[ -z "$VERSION" ]]; then - echo "Provide the version as environment variable VERSION" - exit 1 -fi - -if [[ -z "$RUNTIME" ]]; then - echo "Provide the runtime as environment variable RUNTIME" - exit 1 -fi - arch= appimage_arch= target= diff --git a/build/scripts/package.osx-app.sh b/build/scripts/package.osx-app.sh index 4a50a860..2d43e24a 100755 --- a/build/scripts/package.osx-app.sh +++ b/build/scripts/package.osx-app.sh @@ -5,16 +5,6 @@ set -o set -u set pipefail -if [[ -z "$VERSION" ]]; then - echo "Provide the version as environment variable VERSION" - exit 1 -fi - -if [[ -z "$RUNTIME" ]]; then - echo "Provide the runtime as environment variable RUNTIME" - exit 1 -fi - cd build mkdir -p SourceGit.app/Contents/Resources diff --git a/build/scripts/package.windows-portable.sh b/build/scripts/package.windows-portable.sh index 9ba29216..6bd3879b 100755 --- a/build/scripts/package.windows-portable.sh +++ b/build/scripts/package.windows-portable.sh @@ -5,16 +5,6 @@ set -o set -u set pipefail -if [[ -z "$VERSION" ]]; then - echo "Provide the version as environment variable VERSION" - exit 1 -fi - -if [[ -z "$RUNTIME" ]]; then - echo "Provide the runtime as environment variable RUNTIME" - exit 1 -fi - cd build rm -rf SourceGit/*.pdb diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 890b54ee..4ec8da82 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -48,8 +48,18 @@ var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - cmd.Args = $"push {remote} --delete {name}"; + + bool exists = new Remote(repo).HasBranch(remote, name); + if (exists) + { + cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); + cmd.Args = $"push {remote} --delete {name}"; + } + else + { + cmd.Args = $"branch -D -r {remote}/{name}"; + } + return cmd.Exec(); } } diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 04103d68..cb9e6a18 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -129,7 +129,7 @@ namespace SourceGit.Commands _oldLine = int.Parse(match.Groups[1].Value); _newLine = int.Parse(match.Groups[2].Value); _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0)); - } + } } else { diff --git a/src/Commands/ExecuteCustomAction.cs b/src/Commands/ExecuteCustomAction.cs index f5fec82c..4981b2f9 100644 --- a/src/Commands/ExecuteCustomAction.cs +++ b/src/Commands/ExecuteCustomAction.cs @@ -44,7 +44,7 @@ namespace SourceGit.Commands { outputHandler?.Invoke(e.Data); builder.AppendLine(e.Data); - } + } }; try diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 08d2d1c6..834cd7fc 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -16,7 +16,7 @@ namespace SourceGit.Commands if (noTags) Args += "--no-tags "; else - Args += "--force "; + Args += "--tags "; if (prune) Args += "--prune "; diff --git a/src/Commands/Remote.cs b/src/Commands/Remote.cs index f2e8d09e..beaf412b 100644 --- a/src/Commands/Remote.cs +++ b/src/Commands/Remote.cs @@ -45,5 +45,14 @@ Args = "remote set-url" + (isPush ? " --push " : " ") + $"{name} {url}"; return Exec(); } + + public bool HasBranch(string remote, string branch) + { + SSHKey = new Config(WorkingDirectory).Get($"remote.{remote}.sshkey"); + Args = $"ls-remote {remote} {branch}"; + + var rs = ReadToEnd(); + return rs.IsSuccess && rs.StdOut.Trim().Length > 0; + } } } diff --git a/src/Models/Filter.cs b/src/Models/Filter.cs new file mode 100644 index 00000000..8ffd27c7 --- /dev/null +++ b/src/Models/Filter.cs @@ -0,0 +1,60 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models +{ + public enum FilterType + { + LocalBranch = 0, + LocalBranchFolder, + RemoteBranch, + RemoteBranchFolder, + Tag, + } + + public enum FilterMode + { + None = 0, + Included, + Excluded, + } + + public class Filter : ObservableObject + { + public string Pattern + { + get => _pattern; + set => SetProperty(ref _pattern, value); + } + + public FilterType Type + { + get; + set; + } = FilterType.LocalBranch; + + public FilterMode Mode + { + get => _mode; + set => SetProperty(ref _mode, value); + } + + public bool IsBranch + { + get => Type != FilterType.Tag; + } + + public Filter() + { + } + + public Filter(string pattern, FilterType type, FilterMode mode) + { + _pattern = pattern; + _mode = mode; + Type = type; + } + + private string _pattern = string.Empty; + private FilterMode _mode = FilterMode.None; + } +} diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 77c58ee7..f0156bc2 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -1,4 +1,9 @@ -using Avalonia.Collections; +using System; +using System.Collections.Generic; +using System.Text; + +using Avalonia.Collections; +using Avalonia.Threading; namespace SourceGit.Models { @@ -76,11 +81,11 @@ namespace SourceGit.Models set; } = true; - public AvaloniaList Filters + public AvaloniaList HistoriesFilters { get; set; - } = new AvaloniaList(); + } = new AvaloniaList(); public AvaloniaList CommitTemplates { @@ -148,6 +153,177 @@ namespace SourceGit.Models set; } = "---"; + public FilterMode GetHistoriesFilterMode(string pattern, FilterType type) + { + foreach (var filter in HistoriesFilters) + { + if (filter.Type != type) + continue; + + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + return filter.Mode; + } + + return FilterMode.None; + } + + public bool UpdateHistoriesFilter(string pattern, FilterType type, FilterMode mode) + { + for (int i = 0; i < HistoriesFilters.Count; i++) + { + var filter = HistoriesFilters[i]; + if (filter.Type != type) + continue; + + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + { + if (mode == FilterMode.None) + { + HistoriesFilters.RemoveAt(i); + return true; + } + + if (mode != filter.Mode) + { + filter.Mode = mode; + return true; + } + } + } + + if (mode != FilterMode.None) + { + HistoriesFilters.Add(new Filter(pattern, type, mode)); + return true; + } + + return false; + } + + public string BuildHistoriesFilter() + { + var builder = new StringBuilder(); + + var excludedBranches = new List(); + var excludedRemotes = new List(); + var excludedTags = new List(); + var includedBranches = new List(); + var includedRemotes = new List(); + var includedTags = new List(); + foreach (var filter in HistoriesFilters) + { + if (filter.Type == FilterType.LocalBranch) + { + var name = filter.Pattern.Substring(11); + var b = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]"; + + if (filter.Mode == FilterMode.Included) + includedBranches.Add(b); + else if (filter.Mode == FilterMode.Excluded) + excludedBranches.Add(b); + } + else if (filter.Type == FilterType.LocalBranchFolder) + { + if (filter.Mode == FilterMode.Included) + includedBranches.Add($"{filter.Pattern.Substring(11)}/*"); + else if (filter.Mode == FilterMode.Excluded) + excludedBranches.Add($"{filter.Pattern.Substring(11)}/*"); + } + else if (filter.Type == FilterType.RemoteBranch) + { + var name = filter.Pattern.Substring(13); + var r = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]"; + + if (filter.Mode == FilterMode.Included) + includedRemotes.Add(r); + else if (filter.Mode == FilterMode.Excluded) + excludedRemotes.Add(r); + } + else if (filter.Type == FilterType.RemoteBranchFolder) + { + if (filter.Mode == FilterMode.Included) + includedRemotes.Add($"{filter.Pattern.Substring(13)}/*"); + else if (filter.Mode == FilterMode.Excluded) + excludedRemotes.Add($"{filter.Pattern.Substring(13)}/*"); + } + else if (filter.Type == FilterType.Tag) + { + var name = filter.Pattern; + var t = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]"; + + if (filter.Mode == FilterMode.Included) + includedTags.Add(t); + else if (filter.Mode == FilterMode.Excluded) + excludedTags.Add(t); + } + } + + foreach (var b in excludedBranches) + { + builder.Append("--exclude="); + builder.Append(b); + builder.Append(' '); + } + + if (includedBranches.Count > 0) + { + foreach (var b in includedBranches) + { + builder.Append("--branches="); + builder.Append(b); + builder.Append(' '); + } + } + else if (excludedBranches.Count > 0) + { + builder.Append("--branches "); + } + + foreach (var r in excludedRemotes) + { + builder.Append("--exclude="); + builder.Append(r); + builder.Append(' '); + } + + if (includedRemotes.Count > 0) + { + foreach (var r in includedRemotes) + { + builder.Append("--remotes="); + builder.Append(r); + builder.Append(' '); + } + } + else if (excludedRemotes.Count > 0) + { + builder.Append("--remotes "); + } + + foreach (var t in excludedTags) + { + builder.Append("--exclude="); + builder.Append(t); + builder.Append(' '); + } + + if (includedTags.Count > 0) + { + foreach (var t in includedTags) + { + builder.Append("--tags="); + builder.Append(t); + builder.Append(' '); + } + } + else if (excludedTags.Count > 0) + { + builder.Append("--tags "); + } + + return builder.ToString(); + } + public void PushCommitMessage(string message) { var existIdx = CommitMessages.IndexOf(message); diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 2ec9e093..2e8f2c8e 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -1,10 +1,19 @@ -namespace SourceGit.Models +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models { - public class Tag + public class Tag : ObservableObject { - public string Name { get; set; } - public string SHA { get; set; } - public string Message { get; set; } - public bool IsFiltered { get; set; } + public string Name { get; set; } = string.Empty; + public string SHA { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + + public FilterMode FilterMode + { + get => _filterMode; + set => SetProperty(ref _filterMode, value); + } + + private FilterMode _filterMode = FilterMode.None; } } diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index d1f43695..ae2e89d7 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -73,7 +73,7 @@ ABBRECHEN Auf diese Revision zurücksetzen Auf Vorgänger-Revision zurücksetzen - Generiere Commit-Nachricht + Generiere Commit-Nachricht ANZEIGE MODUS ÄNDERN Zeige als Datei- und Ordnerliste Zeige als Pfadliste @@ -240,6 +240,7 @@ Nächste Änderung KEINE ÄNDERUNG ODER NUR ZEILEN-ENDE ÄNDERUNGEN Vorherige Änderung + Als Patch speichern Zeige versteckte Symbole Nebeneinander SUBMODUL @@ -248,6 +249,7 @@ Syntax Hervorhebung Zeilenumbruch Öffne in Merge Tool + Alle Zeilen anzeigen Weniger Zeilen anzeigen Mehr Zeilen anzeigen WÄHLE EINE DATEI AUS UM ÄNDERUNGEN ANZUZEIGEN @@ -364,8 +366,12 @@ Gestagte Änderungen committen Gestagte Änderungen committen und pushen Alle Änderungen stagen und committen + Neuen Branch basierend auf ausgewählten Commit erstellen Ausgewählte Änderungen verwerfen + Fetch, wird direkt ausgeführt Dashboard Modus (Standard) + Pull, wird direkt ausgeführt + Push, wird direkt ausgeführt Erzwinge Neuladen des Repositorys Ausgewählte Änderungen stagen/unstagen Commit-Suchmodus @@ -389,6 +395,8 @@ Interaktiver Rebase Ziel Branch: Auf: + In Browser öffnen + Link kopieren FEHLER INFO Branch mergen @@ -429,7 +437,9 @@ Modell DARSTELLUNG Standardschriftart - Standardschriftgröße + Schriftgröße + Standard + Texteditor Monospace-Schriftart Verwende die Monospace-Schriftart nur im Texteditor Design diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 3803bd39..281f13a3 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -434,8 +434,9 @@ Server APPEARANCE Default Font - Default Font Size - Editor Font Size + Font Size + Default + Editor Monospace Font Only use monospace font in text editor Theme diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 6bc96dd1..365cebfb 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -438,8 +438,6 @@ Servidor APARIENCIA Fuente por defecto - Tamaño de fuente por defecto - Tamaño de fuente del editor Fuente Monospace Usar solo fuente monospace en el editor de texto Tema diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 043e8f97..5668db44 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -371,7 +371,6 @@ Préférences APPARENCE Police par défaut - Taille de police par défaut Police monospace N'utiliser que des polices monospace pour l'éditeur de texte Thème diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 61ded334..ecd1654c 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -460,8 +460,6 @@ Servidor INTELIGÊNCIA ARTIFICIAL Fonte Padrão - Tamanho da fonte padrão - Tamanho da fonte do editor Fonte Monoespaçada Usar fonte monoespaçada apenas no editor de texto Tema diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 3ea6a922..ac94ee77 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -438,8 +438,9 @@ Сервер ВИД Шрифт по-умолчанию - Размер шрифта по-умолчанию - Размер шрифта редактора + Размер шрифта + По-умолчанию + Редактор Моноширный шрифт В текстовом редакторе используется только моноширный шрифт Тема diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 3df54899..5b1f1403 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -437,7 +437,9 @@ 服务地址 外观配置 缺省字体 - 默认字体大小 + 字体大小 + 默认 + 代码编辑器 代码字体大小 等宽字体 仅在文本编辑器中使用等宽字体 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 4dcb31aa..0587f089 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -437,8 +437,9 @@ 產生提交訊息提示詞 外觀設定 預設字型 - 預設字型大小 - 程式碼字型大小 + 字型大小 + 預設 + 程式碼 等寬字型 僅在文字編輯器中使用等寬字型 佈景主題 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 50f4d830..8fdfaa3c 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -168,12 +168,13 @@ - + @@ -1037,35 +1038,6 @@ - - - - + + @@ -44,7 +48,7 @@ @@ -67,15 +71,8 @@ Foreground="{DynamicResource Brush.BadgeFG}" Background="{DynamicResource Brush.Badge}"/> - - + + diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index e96b2594..92c2b043 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -428,28 +428,6 @@ namespace SourceGit.Views } } - private void OnToggleFilterClicked(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo && - sender is ToggleButton toggle && - toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch } node) - { - bool filtered = toggle.IsChecked == true; - List filters = [branch.FullName]; - if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream)) - { - filters.Add(branch.Upstream); - - node.IsFiltered = filtered; - UpdateUpstreamFilterState(repo.RemoteBranchTrees, branch.Upstream, filtered); - } - - repo.UpdateFilters(filters, filtered); - } - - e.Handled = true; - } - private void MakeRows(List rows, List nodes, int depth) { foreach (var node in nodes) @@ -477,23 +455,6 @@ namespace SourceGit.Views CollectBranchesInNode(outs, sub); } - private bool UpdateUpstreamFilterState(List collection, string upstream, bool isFiltered) - { - foreach (var node in collection) - { - if (node.Backend is Models.Branch b && b.FullName == upstream) - { - node.IsFiltered = isFiltered; - return true; - } - - if (node.Backend is Models.Remote r && upstream.StartsWith($"refs/remotes/{r.Name}/", StringComparison.Ordinal)) - return UpdateUpstreamFilterState(node.Children, upstream, isFiltered); - } - - return false; - } - private bool _disableSelectionChangingEvent = false; } } diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index 76ea0227..d8b77a18 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -117,7 +117,28 @@ TextDecorations="Underline" Cursor="Hand" Margin="0,0,16,0" - PointerPressed="OnSHAPressed"/> + PointerEntered="OnSHAPointerEntered" + PointerPressed="OnSHAPressed"> + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitBaseInfo.axaml.cs b/src/Views/CommitBaseInfo.axaml.cs index 228fbe8e..6eb31a60 100644 --- a/src/Views/CommitBaseInfo.axaml.cs +++ b/src/Views/CommitBaseInfo.axaml.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + using Avalonia; using Avalonia.Collections; using Avalonia.Controls; @@ -113,6 +115,29 @@ namespace SourceGit.Views e.Handled = true; } + private async void OnSHAPointerEntered(object sender, PointerEventArgs e) + { + if (DataContext is ViewModels.CommitDetail detail && sender is Control { DataContext: string sha } ctl) + { + var tooltip = ToolTip.GetTip(ctl); + if (tooltip is Models.Commit commit && commit.SHA == sha) + { + ToolTip.SetIsOpen(ctl, true); + } + else + { + var c = await Task.Run(() => detail.GetParent(sha)); + if (c != null) + { + ToolTip.SetTip(ctl, c); + ToolTip.SetIsOpen(ctl, true); + } + } + } + + e.Handled = true; + } + private void OnSHAPressed(object sender, PointerPressedEventArgs e) { if (DataContext is ViewModels.CommitDetail detail && sender is Control { DataContext: string sha }) diff --git a/src/Views/FilterModeSwitchButton.axaml b/src/Views/FilterModeSwitchButton.axaml new file mode 100644 index 00000000..5b6d5341 --- /dev/null +++ b/src/Views/FilterModeSwitchButton.axaml @@ -0,0 +1,41 @@ + + + diff --git a/src/Views/FilterModeSwitchButton.axaml.cs b/src/Views/FilterModeSwitchButton.axaml.cs new file mode 100644 index 00000000..7bcf3100 --- /dev/null +++ b/src/Views/FilterModeSwitchButton.axaml.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public partial class FilterModeSwitchButton : UserControl + { + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode)); + + public Models.FilterMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + public static readonly StyledProperty IsNoneVisibleProperty = + AvaloniaProperty.Register(nameof(IsNoneVisible)); + + public bool IsNoneVisible + { + get => GetValue(IsNoneVisibleProperty); + set => SetValue(IsNoneVisibleProperty, value); + } + + public static readonly StyledProperty IsContextMenuOpeningProperty = + AvaloniaProperty.Register(nameof(IsContextMenuOpening)); + + public bool IsContextMenuOpening + { + get => GetValue(IsContextMenuOpeningProperty); + set => SetValue(IsContextMenuOpeningProperty, value); + } + + public FilterModeSwitchButton() + { + InitializeComponent(); + } + + private void OnChangeFilterModeButtonClicked(object sender, RoutedEventArgs e) + { + var repoView = this.FindAncestorOfType(); + if (repoView == null) + return; + + var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + + var button = sender as Button; + if (button == null) + return; + + if (DataContext is Models.Tag tag) + { + var mode = tag.FilterMode; + + var none = new MenuItem(); + none.Icon = App.CreateMenuIcon("Icons.Eye"); + none.Header = "Default"; + none.IsEnabled = mode != Models.FilterMode.None; + none.Click += (_, ev) => + { + UpdateTagFilterMode(repo, tag, Models.FilterMode.None); + ev.Handled = true; + }; + + var include = new MenuItem(); + include.Icon = App.CreateMenuIcon("Icons.Filter"); + include.Header = "Filter"; + include.IsEnabled = mode != Models.FilterMode.Included; + include.Click += (_, ev) => + { + UpdateTagFilterMode(repo, tag, Models.FilterMode.Included); + ev.Handled = true; + }; + + var exclude = new MenuItem(); + exclude.Icon = App.CreateMenuIcon("Icons.EyeClose"); + exclude.Header = "Hide"; + exclude.IsEnabled = mode != Models.FilterMode.Excluded; + exclude.Click += (_, ev) => + { + UpdateTagFilterMode(repo, tag, Models.FilterMode.Excluded); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(none); + menu.Items.Add(include); + menu.Items.Add(exclude); + + if (mode == Models.FilterMode.None) + { + IsContextMenuOpening = true; + menu.Closed += (_, _) => IsContextMenuOpening = false; + } + + menu.Open(button); + } + else if (DataContext is ViewModels.BranchTreeNode node) + { + var mode = node.FilterMode; + + var none = new MenuItem(); + none.Icon = App.CreateMenuIcon("Icons.Eye"); + none.Header = "Default"; + none.IsEnabled = mode != Models.FilterMode.None; + none.Click += (_, ev) => + { + UpdateBranchFilterMode(repo, node, Models.FilterMode.None); + ev.Handled = true; + }; + + var include = new MenuItem(); + include.Icon = App.CreateMenuIcon("Icons.Filter"); + include.Header = "Filter"; + include.IsEnabled = mode != Models.FilterMode.Included; + include.Click += (_, ev) => + { + UpdateBranchFilterMode(repo, node, Models.FilterMode.Included); + ev.Handled = true; + }; + + var exclude = new MenuItem(); + exclude.Icon = App.CreateMenuIcon("Icons.EyeClose"); + exclude.Header = "Hide"; + exclude.IsEnabled = mode != Models.FilterMode.Excluded; + exclude.Click += (_, ev) => + { + UpdateBranchFilterMode(repo, node, Models.FilterMode.Excluded); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(none); + menu.Items.Add(include); + menu.Items.Add(exclude); + + if (mode == Models.FilterMode.None) + { + IsContextMenuOpening = true; + menu.Closed += (_, _) => IsContextMenuOpening = false; + } + + menu.Open(button); + } + + e.Handled = true; + } + + private void UpdateTagFilterMode(ViewModels.Repository repo, Models.Tag tag, Models.FilterMode mode) + { + var changed = repo.Settings.UpdateHistoriesFilter(tag.Name, Models.FilterType.Tag, mode); + if (changed) + { + tag.FilterMode = mode; + Task.Run(repo.RefreshCommits); + } + } + + private void UpdateBranchFilterMode(ViewModels.Repository repo, ViewModels.BranchTreeNode node, Models.FilterMode mode) + { + var isLocal = node.Path.StartsWith("refs/heads/", StringComparison.Ordinal); + var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch; + var tree = isLocal ? repo.LocalBranchTrees : repo.RemoteBranchTrees; + + if (node.Backend is Models.Branch branch) + { + var changed = repo.Settings.UpdateHistoriesFilter(node.Path, type, mode); + if (!changed) + return; + + node.FilterMode = mode; + } + else + { + var changed = repo.Settings.UpdateHistoriesFilter(node.Path, isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder, mode); + if (!changed) + return; + + node.FilterMode = mode; + ResetChildrenBranchNodeFilterMode(repo, node, isLocal); + } + + var parentType = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; + var cur = node; + do + { + var lastSepIdx = cur.Path.LastIndexOf('/'); + if (lastSepIdx <= 0) + break; + + var parentPath = cur.Path.Substring(0, lastSepIdx); + var parent = FindParentNode(tree, parentPath); + if (parent == null) + break; + + repo.Settings.UpdateHistoriesFilter(parent.Path, parentType, Models.FilterMode.None); + parent.FilterMode = Models.FilterMode.None; + cur = parent; + } while (true); + + Task.Run(repo.RefreshCommits); + } + + private void ResetChildrenBranchNodeFilterMode(ViewModels.Repository repo, ViewModels.BranchTreeNode node, bool isLocal) + { + foreach (var child in node.Children) + { + child.FilterMode = Models.FilterMode.None; + + if (child.IsBranch) + { + var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch; + repo.Settings.UpdateHistoriesFilter(child.Path, type, Models.FilterMode.None); + } + else + { + var type = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; + repo.Settings.UpdateHistoriesFilter(child.Path, type, Models.FilterMode.None); + ResetChildrenBranchNodeFilterMode(repo, child, isLocal); + } + } + } + + private ViewModels.BranchTreeNode FindParentNode(List nodes, string parent) + { + foreach (var node in nodes) + { + if (node.IsBranch) + continue; + + if (node.Path.Equals(parent, StringComparison.Ordinal)) + return node; + + if (parent.StartsWith(node.Path, StringComparison.Ordinal)) + { + var founded = FindParentNode(node.Children, parent); + if (founded != null) + return founded; + } + } + + return null; + } + } +} + + diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 9f436346..85092b64 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -192,35 +192,26 @@ namespace SourceGit.Views if (string.IsNullOrEmpty(subject)) return; - var offset = 0; var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject); if (!keywordMatch.Success) keywordMatch = REG_KEYWORD_FORMAT2().Match(subject); - if (keywordMatch.Success) - { - var keyword = new Run(subject.Substring(0, keywordMatch.Length)); - keyword.FontWeight = FontWeight.Bold; - Inlines.Add(keyword); - - offset = keywordMatch.Length; - subject = subject.Substring(offset); - } - - var rules = IssueTrackerRules; - if (rules == null || rules.Count == 0) - { - Inlines.Add(new Run(subject)); - return; - } - + var rules = IssueTrackerRules ?? []; var matches = new List(); foreach (var rule in rules) rule.Matches(matches, subject); if (matches.Count == 0) { - Inlines.Add(new Run(subject)); + 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; } @@ -232,18 +223,44 @@ namespace SourceGit.Views foreach (var match in matches) { if (match.Start > pos) - inlines.Add(new Run(subject.Substring(pos, 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; - match.Start += offset; // Because we use this index of whole subject to detect mouse event. } if (pos < subject.Length) - inlines.Add(new Run(subject.Substring(pos))); + { + 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); } diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index 73be0f7c..9b84604a 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -121,7 +121,7 @@ - + - + + Value="{Binding DefaultFontSize, Mode=TwoWay}"> + + + + + + + + + + + + + + - - - - @@ -196,16 +205,16 @@ - - - - + - + @@ -592,10 +592,24 @@ - - - - + + + + + + + + + + + + diff --git a/src/Views/RepositoryToolbar.axaml.cs b/src/Views/RepositoryToolbar.axaml.cs index e2c7df8c..afc8ac5a 100644 --- a/src/Views/RepositoryToolbar.axaml.cs +++ b/src/Views/RepositoryToolbar.axaml.cs @@ -52,7 +52,7 @@ namespace SourceGit.Views var startDirectly = launcher.HasKeyModifier(KeyModifiers.Control); if (!startDirectly && OperatingSystem.IsMacOS()) startDirectly = launcher.HasKeyModifier(KeyModifiers.Meta); - + repo.Fetch(startDirectly); e.Handled = true; } @@ -66,7 +66,7 @@ namespace SourceGit.Views var startDirectly = launcher.HasKeyModifier(KeyModifiers.Control); if (!startDirectly && OperatingSystem.IsMacOS()) startDirectly = launcher.HasKeyModifier(KeyModifiers.Meta); - + repo.Pull(startDirectly); e.Handled = true; } @@ -80,7 +80,7 @@ namespace SourceGit.Views var startDirectly = launcher.HasKeyModifier(KeyModifiers.Control); if (!startDirectly && OperatingSystem.IsMacOS()) startDirectly = launcher.HasKeyModifier(KeyModifiers.Meta); - + repo.Push(startDirectly); e.Handled = true; } diff --git a/src/Views/TagsView.axaml b/src/Views/TagsView.axaml index a30f63de..78165f2c 100644 --- a/src/Views/TagsView.axaml +++ b/src/Views/TagsView.axaml @@ -12,6 +12,10 @@ + + @@ -43,15 +47,14 @@ Classes="primary" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="8,0,0,0"/> - - + + + + + + + + @@ -60,32 +63,28 @@ - + Margin="8,0,0,0" + TextTrimming="CharacterEllipsis"/> - + diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs index 8d4168b2..c83cfd28 100644 --- a/src/Views/TagsView.axaml.cs +++ b/src/Views/TagsView.axaml.cs @@ -65,7 +65,7 @@ namespace SourceGit.Views } if (node.Tag != null) - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Tag"); + CreateContent(new Thickness(0, 0, 0, 0), "Icons.Tag"); else if (node.IsExpanded) CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open"); else @@ -247,23 +247,6 @@ namespace SourceGit.Views } } - private void OnToggleFilterClicked(object sender, RoutedEventArgs e) - { - if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo) - { - var target = null as Models.Tag; - if (toggle.DataContext is ViewModels.TagTreeNode node) - target = node.Tag; - else if (toggle.DataContext is Models.Tag tag) - target = tag; - - if (target != null) - repo.UpdateFilters([target.Name], toggle.IsChecked == true); - } - - e.Handled = true; - } - private void MakeTreeRows(List rows, List nodes) { foreach (var node in nodes)