From 7f389b2e6f7e08b5522844f75748dc39bfe0870c Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 11:57:13 +0800 Subject: [PATCH 01/43] refactor: move settings of repository, such as filters, commit message, from `Preference` to each repository's gitdir. * avoid invalid repository setting remains in preference.json * supports to restore tabs that not added to the Welcome page --- src/App.JsonCodeGen.cs | 1 + src/App.axaml.cs | 44 +++++--- src/ViewModels/Blame.cs | 5 +- src/ViewModels/BranchCompare.cs | 5 +- src/ViewModels/Clone.cs | 4 +- src/ViewModels/CommitDetail.cs | 5 +- src/ViewModels/DiffContext.cs | 2 +- src/ViewModels/Init.cs | 8 +- src/ViewModels/Launcher.cs | 49 +++++++-- src/ViewModels/Preference.cs | 58 +---------- src/ViewModels/Pull.cs | 4 +- src/ViewModels/Repository.cs | 160 +++++++++++++++++------------- src/ViewModels/RevisionCompare.cs | 5 +- src/ViewModels/Welcome.cs | 4 - src/ViewModels/WorkingCopy.cs | 27 +---- src/Views/Blame.axaml.cs | 6 +- src/Views/Repository.axaml | 4 +- src/Views/Welcome.axaml.cs | 5 +- 18 files changed, 187 insertions(+), 209 deletions(-) diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 61f00074..50d589a2 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -9,5 +9,6 @@ namespace SourceGit [JsonSerializable(typeof(List))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(ViewModels.Preference))] + [JsonSerializable(typeof(ViewModels.RepositorySettings))] internal partial class JsonCodeGen : JsonSerializerContext { } } diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 0b319792..db0a4644 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -187,6 +187,18 @@ namespace SourceGit } } + public static async Task GetClipboardTextAsync() + { + if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow.Clipboard is { } clipboard) + { + return await clipboard.GetTextAsync(); + } + } + return default; + } + public static string Text(string key, params object[] args) { var fmt = Current.FindResource($"Text.{key}") as string; @@ -259,6 +271,21 @@ namespace SourceGit }); } + public static ViewModels.Repository FindOpenedRepository(string repoPath) + { + if (Current is App app && app._launcher != null) + { + foreach (var page in app._launcher.Pages) + { + var id = page.Node.Id.Replace("\\", "/"); + if (id == repoPath && page.Data is ViewModels.Repository repo) + return repo; + } + } + + return null; + } + public static void Quit() { if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) @@ -288,9 +315,10 @@ namespace SourceGit _launcher = new ViewModels.Launcher(); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; - if (ViewModels.Preference.Instance.ShouldCheck4UpdateOnStartup) + var pref = ViewModels.Preference.Instance; + if (pref.ShouldCheck4UpdateOnStartup) { - ViewModels.Preference.Save(); + pref.Save(); Check4Update(); } } @@ -317,18 +345,6 @@ namespace SourceGit }); } - public static async Task GetClipboardTextAsync() - { - if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - if (desktop.MainWindow.Clipboard is { } clipboard) - { - return await clipboard.GetTextAsync(); - } - } - return default; - } - private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; private ResourceDictionary _colorOverrides = null; diff --git a/src/ViewModels/Blame.cs b/src/ViewModels/Blame.cs index bef29219..79770c6f 100644 --- a/src/ViewModels/Blame.cs +++ b/src/ViewModels/Blame.cs @@ -49,9 +49,8 @@ namespace SourceGit.ViewModels public void NavigateToCommit(string commitSHA) { - var repo = Preference.FindRepository(_repo); - if (repo != null) - repo.NavigateToCommit(commitSHA); + var repo = App.FindOpenedRepository(_repo); + repo?.NavigateToCommit(commitSHA); } private readonly string _repo = string.Empty; diff --git a/src/ViewModels/BranchCompare.cs b/src/ViewModels/BranchCompare.cs index be133bd2..634d18a6 100644 --- a/src/ViewModels/BranchCompare.cs +++ b/src/ViewModels/BranchCompare.cs @@ -110,9 +110,8 @@ namespace SourceGit.ViewModels public void NavigateTo(string commitSHA) { - var repo = Preference.FindRepository(_repo); - if (repo != null) - repo.NavigateToCommit(commitSHA); + var repo = App.FindOpenedRepository(_repo); + repo?.NavigateToCommit(commitSHA); } public void ClearSearchFilter() diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index 89eff488..bd74b754 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -128,8 +128,8 @@ namespace SourceGit.ViewModels CallUIThread(() => { - var repo = Preference.AddRepository(path, Path.Combine(path, ".git")); - var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null, true); + var normalizedPath = path.Replace("\\", "/"); + var node = Preference.FindOrAddNodeByRepositoryPath(normalizedPath, null, true); _launcher.OpenRepositoryInTab(node, _page); }); diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 04fe8129..6039db68 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -110,9 +110,8 @@ namespace SourceGit.ViewModels public void NavigateTo(string commitSHA) { - var repo = Preference.FindRepository(_repo); - if (repo != null) - repo.NavigateToCommit(commitSHA); + var repo = App.FindOpenedRepository(_repo); + repo?.NavigateToCommit(commitSHA); } public void ClearSearchChangeFilter() diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index cb84e54d..0e561cdd 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -111,7 +111,7 @@ namespace SourceGit.ViewModels if (latest.TextDiff != null) { - var repo = Preference.FindRepository(_repo); + var repo = App.FindOpenedRepository(_repo); if (repo != null && repo.Submodules.Contains(_option.Path)) { var submoduleDiff = new Models.SubmoduleDiff(); diff --git a/src/ViewModels/Init.cs b/src/ViewModels/Init.cs index a4e27341..2e582ccc 100644 --- a/src/ViewModels/Init.cs +++ b/src/ViewModels/Init.cs @@ -1,5 +1,4 @@ -using System.IO; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace SourceGit.ViewModels { @@ -29,11 +28,10 @@ namespace SourceGit.ViewModels if (!succ) return false; - var gitDir = Path.GetFullPath(Path.Combine(_targetPath, ".git")); CallUIThread(() => { - var repo = Preference.AddRepository(_targetPath, gitDir); - Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, _parentNode, true); + var normalizedPath = _targetPath.Replace("\\", "/"); + Preference.FindOrAddNodeByRepositoryPath(normalizedPath, _parentNode, true); }); return true; diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 11f17d29..df5cf641 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -48,9 +48,8 @@ namespace SourceGit.ViewModels return; } - var gitDir = new Commands.QueryGitDir(root).Result(); - var repo = Preference.AddRepository(root, gitDir); - var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null, false); + var normalized = root.Replace("\\", "/"); + var node = Preference.FindOrAddNodeByRepositoryPath(normalized, null, false); OpenRepositoryInTab(node, null); } else if (Preference.Instance.RestoreTabs) @@ -59,7 +58,15 @@ namespace SourceGit.ViewModels { var node = Preference.FindNode(id); if (node == null) - continue; + { + node = new RepositoryNode() + { + Id = id, + Name = Path.GetFileName(id), + Bookmark = 0, + IsRepository = true, + }; + } OpenRepositoryInTab(node, null); } @@ -72,19 +79,26 @@ namespace SourceGit.ViewModels public void Quit() { - Preference.Instance.OpenedTabs.Clear(); + var pref = Preference.Instance; + pref.OpenedTabs.Clear(); - if (Preference.Instance.RestoreTabs) + if (pref.RestoreTabs) { foreach (var page in Pages) { if (page.Node.IsRepository) - Preference.Instance.OpenedTabs.Add(page.Node.Id); + pref.OpenedTabs.Add(page.Node.Id); } } - Preference.Instance.LastActiveTabIdx = Pages.IndexOf(ActivePage); - Preference.Save(); + pref.LastActiveTabIdx = Pages.IndexOf(ActivePage); + pref.Save(); + + foreach (var page in Pages) + { + if (page.Data is Repository repo) + repo.Close(); + } } public void AddNewTab() @@ -211,14 +225,27 @@ namespace SourceGit.ViewModels } } - var repo = Preference.FindRepository(node.Id); - if (repo == null || !Path.Exists(repo.FullPath)) + if (!Path.Exists(node.Id)) { var ctx = page == null ? ActivePage.Node.Id : page.Node.Id; App.RaiseException(ctx, "Repository does NOT exists any more. Please remove it."); return; } + var gitDir = new Commands.QueryGitDir(node.Id).Result(); + if (string.IsNullOrEmpty(gitDir)) + { + var ctx = page == null ? ActivePage.Node.Id : page.Node.Id; + App.RaiseException(ctx, "Given path is not a valid git repository!"); + return; + } + + var repo = new Repository() + { + FullPath = node.Id, + GitDir = gitDir, + }; + repo.Open(); Commands.AutoFetch.AddRepository(repo.FullPath); diff --git a/src/ViewModels/Preference.cs b/src/ViewModels/Preference.cs index edcbc7e1..5198a8b8 100644 --- a/src/ViewModels/Preference.cs +++ b/src/ViewModels/Preference.cs @@ -37,23 +37,14 @@ namespace SourceGit.ViewModels } } - // It will cause some issue on Linux. See https://github.com/sourcegit-scm/sourcegit/issues/99 - // _instance.Repositories.RemoveAll(x => !Directory.Exists(x.FullPath)); - if (_instance.DefaultFont == null) - { _instance.DefaultFont = FontManager.Current.DefaultFontFamily; - } if (_instance.MonospaceFont == null) - { _instance.MonospaceFont = new FontFamily("fonts:SourceGit#JetBrains Mono"); - } if (!_instance.IsGitConfigured) - { _instance.GitInstallPath = Native.OS.FindGitExecutable(); - } return _instance; } @@ -274,9 +265,8 @@ namespace SourceGit.ViewModels set { if (value is null or < 1) - { return; - } + if (Commands.AutoFetch.Interval != value) { Commands.AutoFetch.Interval = (int)value; @@ -308,12 +298,6 @@ namespace SourceGit.ViewModels set => SetProperty(ref _externalMergeToolPath, value); } - public List Repositories - { - get; - set; - } = new List(); - public AvaloniaList RepositoryNodes { get => _repositoryNodes; @@ -366,20 +350,14 @@ namespace SourceGit.ViewModels list.Sort((l, r) => { if (l.IsRepository != r.IsRepository) - { return l.IsRepository ? 1 : -1; - } else - { return l.Name.CompareTo(r.Name); - } }); collection.Clear(); foreach (var one in list) - { collection.Add(one); - } } public static RepositoryNode FindNode(string id) @@ -451,39 +429,9 @@ namespace SourceGit.ViewModels container.Add(one); } - public static Repository FindRepository(string path) + public void Save() { - foreach (var repo in _instance.Repositories) - { - if (repo.FullPath == path) - return repo; - } - return null; - } - - public static Repository AddRepository(string rootDir, string gitDir) - { - var normalized = rootDir.Replace('\\', '/'); - var repo = FindRepository(normalized); - if (repo != null) - { - repo.GitDir = gitDir; - return repo; - } - - repo = new Repository() - { - FullPath = normalized, - GitDir = gitDir - }; - - _instance.Repositories.Add(repo); - return repo; - } - - public static void Save() - { - var data = JsonSerializer.Serialize(_instance, JsonCodeGen.Default.Preference); + var data = JsonSerializer.Serialize(this, JsonCodeGen.Default.Preference); File.WriteAllText(_savePath, data); } diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index 657f52a6..5a62f5b3 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -55,8 +55,8 @@ namespace SourceGit.ViewModels public bool UseRebase { - get => _repo.PreferRebaseInsteadOfMerge; - set => _repo.PreferRebaseInsteadOfMerge = value; + get => _repo.Settings.PreferRebaseInsteadOfMerge; + set => _repo.Settings.PreferRebaseInsteadOfMerge = value; } public Pull(Repository repo, Models.Branch specifiedRemoteBranch) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index d4bffa0d..dec5486d 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text.Json.Serialization; +using System.Text.Json; using System.Threading.Tasks; using Avalonia.Collections; @@ -14,6 +14,45 @@ using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { + public class RepositorySettings : ObservableObject + { + public bool PreferRebaseInsteadOfMerge + { + get; + set; + } = true; + + public AvaloniaList Filters + { + get; + set; + } = new AvaloniaList(); + + public AvaloniaList CommitMessages + { + get; + set; + } = new AvaloniaList(); + + public void PushCommitMessage(string message) + { + var existIdx = CommitMessages.IndexOf(message); + if (existIdx == 0) + return; + + if (existIdx > 0) + { + CommitMessages.Move(existIdx, 0); + return; + } + + if (CommitMessages.Count > 9) + CommitMessages.RemoveRange(9, CommitMessages.Count - 9); + + CommitMessages.Insert(0, message); + } + } + public class Repository : ObservableObject, Models.IRepository { public string FullPath @@ -39,25 +78,12 @@ namespace SourceGit.ViewModels set => SetProperty(ref _gitDir, value); } - public bool PreferRebaseInsteadOfMerge + public RepositorySettings Settings { - get; - set; - } = true; + get => _settings; + private set => SetProperty(ref _settings, value); + } - public AvaloniaList Filters - { - get; - set; - } = new AvaloniaList(); - - public AvaloniaList CommitMessages - { - get; - set; - } = new AvaloniaList(); - - [JsonIgnore] public int SelectedViewIndex { get => _selectedViewIndex; @@ -81,14 +107,12 @@ namespace SourceGit.ViewModels } } - [JsonIgnore] public object SelectedView { get => _selectedView; set => SetProperty(ref _selectedView, value); } - [JsonIgnore] public string SearchBranchFilter { get => _searchBranchFilter; @@ -104,75 +128,64 @@ namespace SourceGit.ViewModels } } - [JsonIgnore] public List Remotes { get => _remotes; private set => SetProperty(ref _remotes, value); } - [JsonIgnore] public List Branches { get => _branches; private set => SetProperty(ref _branches, value); } - [JsonIgnore] public List LocalBranchTrees { get => _localBranchTrees; private set => SetProperty(ref _localBranchTrees, value); } - [JsonIgnore] public List RemoteBranchTrees { get => _remoteBranchTrees; private set => SetProperty(ref _remoteBranchTrees, value); } - [JsonIgnore] public List Worktrees { get => _worktrees; private set => SetProperty(ref _worktrees, value); } - [JsonIgnore] public List Tags { get => _tags; private set => SetProperty(ref _tags, value); } - [JsonIgnore] public List VisibleTags { get => _visibleTags; private set => SetProperty(ref _visibleTags, value); } - [JsonIgnore] public List Submodules { get => _submodules; private set => SetProperty(ref _submodules, value); } - [JsonIgnore] public int WorkingCopyChangesCount { get => _workingCopy == null ? 0 : _workingCopy.Count; } - [JsonIgnore] public int StashesCount { get => _stashesPage == null ? 0 : _stashesPage.Stashes.Count; } - [JsonIgnore] public bool IncludeUntracked { get => _includeUntracked; @@ -183,7 +196,6 @@ namespace SourceGit.ViewModels } } - [JsonIgnore] public bool IsSearching { get => _isSearching; @@ -199,63 +211,54 @@ namespace SourceGit.ViewModels } } - [JsonIgnore] public int SearchCommitFilterType { get => _searchCommitFilterType; set => SetProperty(ref _searchCommitFilterType, value); } - [JsonIgnore] public string SearchCommitFilter { get => _searchCommitFilter; set => SetProperty(ref _searchCommitFilter, value); } - [JsonIgnore] public List SearchedCommits { get => _searchedCommits; set => SetProperty(ref _searchedCommits, value); } - [JsonIgnore] public bool IsTagGroupExpanded { get => _isTagGroupExpanded; set => SetProperty(ref _isTagGroupExpanded, value); } - [JsonIgnore] public bool IsSubmoduleGroupExpanded { get => _isSubmoduleGroupExpanded; set => SetProperty(ref _isSubmoduleGroupExpanded, value); } - [JsonIgnore] public bool IsWorktreeGroupExpanded { get => _isWorktreeGroupExpanded; set => SetProperty(ref _isWorktreeGroupExpanded, value); } - [JsonIgnore] public InProgressContext InProgressContext { get => _inProgressContext; private set => SetProperty(ref _inProgressContext, value); } - [JsonIgnore] public bool HasUnsolvedConflicts { get => _hasUnsolvedConflicts; private set => SetProperty(ref _hasUnsolvedConflicts, value); } - [JsonIgnore] public Models.Commit SearchResultSelectedCommit { get => _searchResultSelectedCommit; @@ -264,6 +267,16 @@ namespace SourceGit.ViewModels public void Open() { + var settingsFile = Path.Combine(_gitDir, "sourcegit.settings"); + try + { + _settings = JsonSerializer.Deserialize(File.ReadAllText(settingsFile), JsonCodeGen.Default.RepositorySettings); + } + catch + { + _settings = new RepositorySettings(); + } + _watcher = new Models.Watcher(this); _histories = new Histories(this); _workingCopy = new WorkingCopy(this); @@ -280,6 +293,10 @@ namespace SourceGit.ViewModels { SelectedView = 0.0; // Do NOT modify. Used to remove exists widgets for GC.Collect + var settingsSerialized = JsonSerializer.Serialize(_settings, JsonCodeGen.Default.RepositorySettings); + File.WriteAllText(Path.Combine(_gitDir, "sourcegit.settings"), settingsSerialized); + _settings = null; + _watcher.Dispose(); _histories.Cleanup(); _workingCopy.Cleanup(); @@ -436,7 +453,7 @@ namespace SourceGit.ViewModels public void ClearHistoriesFilter() { - Filters.Clear(); + _settings.Filters.Clear(); Task.Run(() => { @@ -525,15 +542,15 @@ namespace SourceGit.ViewModels var changed = false; if (toggle) { - if (!Filters.Contains(filter)) + if (!_settings.Filters.Contains(filter)) { - Filters.Add(filter); + _settings.Filters.Add(filter); changed = true; } } else { - changed = Filters.Remove(filter); + changed = _settings.Filters.Remove(filter); } if (changed) @@ -637,7 +654,7 @@ namespace SourceGit.ViewModels { var tags = new Commands.QueryTags(FullPath).Result(); foreach (var tag in tags) - tag.IsFiltered = Filters.Contains(tag.Name); + tag.IsFiltered = _settings.Filters.Contains(tag.Name); Dispatcher.UIThread.Invoke(() => { @@ -652,7 +669,7 @@ namespace SourceGit.ViewModels var limits = $"-{Preference.Instance.MaxHistoryCommits} "; var validFilters = new List(); - foreach (var filter in Filters) + foreach (var filter in _settings.Filters) { if (filter.StartsWith("refs/", StringComparison.Ordinal)) { @@ -670,12 +687,12 @@ namespace SourceGit.ViewModels { limits += string.Join(" ", validFilters); - if (Filters.Count != validFilters.Count) + if (_settings.Filters.Count != validFilters.Count) { Dispatcher.UIThread.Post(() => { - Filters.Clear(); - Filters.AddRange(validFilters); + _settings.Filters.Clear(); + _settings.Filters.AddRange(validFilters); }); } } @@ -851,22 +868,23 @@ namespace SourceGit.ViewModels public void OpenSubmodule(string submodule) { var root = Path.GetFullPath(Path.Combine(_fullpath, submodule)); - var gitDir = new Commands.QueryGitDir(root).Result(); - var repo = Preference.AddRepository(root, gitDir); + var normalizedPath = root.Replace("\\", "/"); - var node = new RepositoryNode() + var node = Preference.FindNode(normalizedPath); + if (node == null) { - Id = repo.FullPath, - Name = Path.GetFileName(repo.FullPath), - Bookmark = 0, - IsRepository = true, - }; + node = new RepositoryNode() + { + Id = normalizedPath, + Name = Path.GetFileName(normalizedPath), + Bookmark = 0, + IsRepository = true, + }; + } var launcher = App.GetTopLevel().DataContext as Launcher; if (launcher != null) - { launcher.OpenRepositoryInTab(node, null); - } } public void AddWorktree() @@ -883,16 +901,17 @@ namespace SourceGit.ViewModels public void OpenWorktree(Models.Worktree worktree) { - var gitDir = new Commands.QueryGitDir(worktree.FullPath).Result(); - var repo = Preference.AddRepository(worktree.FullPath, gitDir); - - var node = new RepositoryNode() + var node = Preference.FindNode(worktree.FullPath); + if (node == null) { - Id = repo.FullPath, - Name = Path.GetFileName(repo.FullPath), - Bookmark = 0, - IsRepository = true, - }; + node = new RepositoryNode() + { + Id = worktree.FullPath, + Name = Path.GetFileName(worktree.FullPath), + Bookmark = 0, + IsRepository = true, + }; + } var launcher = App.GetTopLevel().DataContext as Launcher; if (launcher != null) @@ -1814,7 +1833,7 @@ namespace SourceGit.ViewModels private BranchTreeNode.Builder BuildBranchTree(List branches, List remotes) { var builder = new BranchTreeNode.Builder(); - builder.SetFilters(Filters); + builder.SetFilters(_settings.Filters); if (string.IsNullOrEmpty(_searchBranchFilter)) { @@ -1858,6 +1877,7 @@ namespace SourceGit.ViewModels private string _fullpath = string.Empty; private string _gitDir = string.Empty; + private RepositorySettings _settings = null; private Models.Watcher _watcher = null; private Histories _histories = null; diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 80a06757..2f288ab6 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -118,9 +118,8 @@ namespace SourceGit.ViewModels public void NavigateTo(string commitSHA) { - var repo = Preference.FindRepository(_repo); - if (repo != null) - repo.NavigateToCommit(commitSHA); + var repo = App.FindOpenedRepository(_repo); + repo?.NavigateToCommit(commitSHA); } public void ClearSearchFilter() diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 415c56ee..bb5690a7 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -90,7 +90,6 @@ namespace SourceGit.ViewModels public ContextMenu CreateContextMenu(RepositoryNode node) { var menu = new ContextMenu(); - var hasRepo = Preference.FindRepository(node.Id) != null; if (!node.IsRepository && node.SubNodes.Count > 0) { @@ -115,7 +114,6 @@ namespace SourceGit.ViewModels var edit = new MenuItem(); edit.Header = App.Text("Welcome.Edit"); edit.Icon = App.CreateMenuIcon("Icons.Edit"); - edit.IsEnabled = !node.IsRepository || hasRepo; edit.Click += (_, e) => { node.Edit(); @@ -128,7 +126,6 @@ namespace SourceGit.ViewModels var explore = new MenuItem(); explore.Header = App.Text("Repository.Explore"); explore.Icon = App.CreateMenuIcon("Icons.Folder.Open"); - explore.IsEnabled = hasRepo; explore.Click += (_, e) => { node.OpenInFileManager(); @@ -139,7 +136,6 @@ namespace SourceGit.ViewModels var terminal = new MenuItem(); terminal.Header = App.Text("Repository.Terminal"); terminal.Icon = App.CreateMenuIcon("Icons.Terminal"); - terminal.IsEnabled = hasRepo; terminal.Click += (_, e) => { node.OpenTerminal(); diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 5d99da24..8b32240c 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -1113,7 +1113,7 @@ namespace SourceGit.ViewModels public ContextMenu CreateContextMenuForCommitMessages() { var menu = new ContextMenu(); - if (_repo.CommitMessages.Count == 0) + if (_repo.Settings.CommitMessages.Count == 0) { var empty = new MenuItem(); empty.Header = App.Text("WorkingCopy.NoCommitHistories"); @@ -1128,7 +1128,7 @@ namespace SourceGit.ViewModels menu.Items.Add(tip); menu.Items.Add(new MenuItem() { Header = "-" }); - foreach (var message in _repo.CommitMessages) + foreach (var message in _repo.Settings.CommitMessages) { var dump = message; @@ -1228,7 +1228,7 @@ namespace SourceGit.ViewModels return; } - PushCommitMessage(); + _repo.Settings.PushCommitMessage(_commitMessage); SetDetail(null); IsCommitting = true; @@ -1257,27 +1257,6 @@ namespace SourceGit.ViewModels }); } - private void PushCommitMessage() - { - var existIdx = _repo.CommitMessages.IndexOf(CommitMessage); - if (existIdx == 0) - { - return; - } - else if (existIdx > 0) - { - _repo.CommitMessages.Move(existIdx, 0); - return; - } - - if (_repo.CommitMessages.Count > 9) - { - _repo.CommitMessages.RemoveRange(9, _repo.CommitMessages.Count - 9); - } - - _repo.CommitMessages.Insert(0, CommitMessage); - } - private Repository _repo = null; private bool _isLoadingData = false; private bool _isStaging = false; diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs index d4c2da28..1dacad12 100644 --- a/src/Views/Blame.axaml.cs +++ b/src/Views/Blame.axaml.cs @@ -378,11 +378,9 @@ namespace SourceGit.Views private void OnCommitSHAPointerPressed(object sender, PointerPressedEventArgs e) { - if (DataContext is ViewModels.Blame blame) - { - var txt = sender as TextBlock; + if (sender is TextBlock txt && DataContext is ViewModels.Blame blame) blame.NavigateToCommit(txt.Text); - } + e.Handled = true; } diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 6e7873e9..edbf9432 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -794,14 +794,14 @@ - + - + diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index aa5054c4..0fb89989 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -253,11 +253,10 @@ namespace SourceGit.Views return; } - var gitDir = new Commands.QueryGitDir(root).Result(); Dispatcher.UIThread.Invoke(() => { - var repo = ViewModels.Preference.AddRepository(root, gitDir); - var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, parent, true); + var normalizedPath = root.Replace("\\", "/"); + var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(normalizedPath, parent, true); launcher.OpenRepositoryInTab(node, page); }); }); From f08acebb5e6cb5d8fd67cbb988a1b2f3cffad614 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 13:13:18 +0800 Subject: [PATCH 02/43] enhance: better implementation to diff submodule changes while repository is not opened --- src/ViewModels/DiffContext.cs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index 0e561cdd..c3531ffc 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -111,27 +111,34 @@ namespace SourceGit.ViewModels if (latest.TextDiff != null) { - var repo = App.FindOpenedRepository(_repo); - if (repo != null && repo.Submodules.Contains(_option.Path)) + var count = latest.TextDiff.Lines.Count; + var isSubmodule = false; + if (count <= 3) { var submoduleDiff = new Models.SubmoduleDiff(); var submoduleRoot = $"{_repo}/{_option.Path}".Replace("\\", "/"); - foreach (var line in latest.TextDiff.Lines) + isSubmodule = true; + for (int i = 1; i < count; i++) { + var line = latest.TextDiff.Lines[i]; + if (!line.Content.StartsWith("Subproject commit ", StringComparison.Ordinal)) + { + isSubmodule = false; + break; + } + + var sha = line.Content.Substring(18); if (line.Type == Models.TextDiffLineType.Added) - { - var sha = line.Content.Substring("Subproject commit ".Length); submoduleDiff.New = QuerySubmoduleRevision(submoduleRoot, sha); - } else if (line.Type == Models.TextDiffLineType.Deleted) - { - var sha = line.Content.Substring("Subproject commit ".Length); submoduleDiff.Old = QuerySubmoduleRevision(submoduleRoot, sha); - } } - rs = submoduleDiff; + + if (isSubmodule) + rs = submoduleDiff; } - else + + if (!isSubmodule) { latest.TextDiff.File = _option.Path; rs = latest.TextDiff; From 0da30b6b89927145dd0eb82a2490fef8c0239598 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 13:30:57 +0800 Subject: [PATCH 03/43] enhance: better LFS file detect method --- src/Commands/IsLFSFiltered.cs | 10 ++++++- src/Commands/QueryFileSize.cs | 3 -- src/Commands/SaveRevisionFile.cs | 2 +- src/ViewModels/CommitDetail.cs | 49 +++++++++++++------------------- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/Commands/IsLFSFiltered.cs b/src/Commands/IsLFSFiltered.cs index b29039de..2a7234bb 100644 --- a/src/Commands/IsLFSFiltered.cs +++ b/src/Commands/IsLFSFiltered.cs @@ -6,7 +6,15 @@ { WorkingDirectory = repo; Context = repo; - Args = $"check-attr -a -z \"{path}\""; + Args = $"check-attr -z filter \"{path}\""; + RaiseError = false; + } + + public IsLFSFiltered(string repo, string sha, string path) + { + WorkingDirectory = repo; + Context = repo; + Args = $"check-attr --source {sha} -z filter \"{path}\""; RaiseError = false; } diff --git a/src/Commands/QueryFileSize.cs b/src/Commands/QueryFileSize.cs index 5ce7641e..c36984dd 100644 --- a/src/Commands/QueryFileSize.cs +++ b/src/Commands/QueryFileSize.cs @@ -4,7 +4,6 @@ namespace SourceGit.Commands { public partial class QueryFileSize : Command { - [GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")] private static partial Regex REG_FORMAT(); @@ -25,9 +24,7 @@ namespace SourceGit.Commands { var match = REG_FORMAT().Match(rs.StdOut); if (match.Success) - { return long.Parse(match.Groups[1].Value); - } } return 0; diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index 6c200940..99e89093 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -10,7 +10,7 @@ namespace SourceGit.Commands { public static void Run(string repo, string revision, string file, string saveTo) { - var isLFSFiltered = new IsLFSFiltered(repo, file).Result(); + var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result(); if (isLFSFiltered) { var tmpFile = saveTo + ".tmp"; diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 6039db68..35014cf4 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Controls; @@ -12,7 +13,7 @@ using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class CommitDetail : ObservableObject + public partial class CommitDetail : ObservableObject { public DiffContext DiffContext { @@ -164,39 +165,26 @@ namespace SourceGit.ViewModels var contentStream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path); var content = new StreamReader(contentStream).ReadToEnd(); - if (content.StartsWith("version https://git-lfs.github.com/spec/", StringComparison.Ordinal)) + var matchLFS = REG_LFS_FORMAT().Match(content); + if (matchLFS.Success) { var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() }; - var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries); - if (lines.Length == 3) - { - foreach (var line in lines) - { - if (line.StartsWith("oid sha256:", StringComparison.Ordinal)) - { - obj.Object.Oid = line.Substring(11); - } - else if (line.StartsWith("size ", StringComparison.Ordinal)) - { - obj.Object.Size = long.Parse(line.Substring(5)); - } - } - Dispatcher.UIThread.Invoke(() => - { - ViewRevisionFileContent = obj; - }); - return; - } - } + obj.Object.Oid = matchLFS.Groups[1].Value; + obj.Object.Size = long.Parse(matchLFS.Groups[2].Value); - Dispatcher.UIThread.Invoke(() => + Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = obj); + } + else { - ViewRevisionFileContent = new Models.RevisionTextFile() + Dispatcher.UIThread.Invoke(() => { - FileName = file.Path, - Content = content - }; - }); + ViewRevisionFileContent = new Models.RevisionTextFile() + { + FileName = file.Path, + Content = content + }; + }); + } }); break; case Models.ObjectType.Commit: @@ -464,6 +452,9 @@ namespace SourceGit.ViewModels } } + [GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")] + private static partial Regex REG_LFS_FORMAT(); + private static readonly HashSet IMG_EXTS = new HashSet() { ".ico", ".bmp", ".jpg", ".png", ".jpeg" From de1a4d14e8a7a4e4a53bfb8aa649bf0cc24df3d0 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 13:45:48 +0800 Subject: [PATCH 04/43] enhance: disable blame on submodules --- src/ViewModels/CommitDetail.cs | 1 + src/Views/RevisionFiles.axaml.cs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 35014cf4..c401f20a 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -319,6 +319,7 @@ namespace SourceGit.ViewModels var blame = new MenuItem(); blame.Header = App.Text("Blame"); blame.Icon = App.CreateMenuIcon("Icons.Blame"); + blame.IsEnabled = file.Type == Models.ObjectType.Blob; blame.Click += (o, ev) => { var window = new Views.Blame() { DataContext = new Blame(_repo, file.Path, _commit.SHA) }; diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index 4b4fc449..954f0ecc 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -9,8 +9,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Interactivity; using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Styling; using AvaloniaEdit; using AvaloniaEdit.Document; From 6dad466eefd9255f9b814da31e4eeddd55e0456c Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 16:19:08 +0800 Subject: [PATCH 05/43] feature: supports `--no-tags` on fetch and pull operation (#226) --- src/Commands/Fetch.cs | 12 +++++++++--- src/Commands/Pull.cs | 5 ++++- src/Resources/Locales/en_US.axaml | 2 ++ src/Resources/Locales/zh_CN.axaml | 2 ++ src/Resources/Locales/zh_TW.axaml | 2 ++ src/ViewModels/AddRemote.cs | 2 +- src/ViewModels/Fetch.cs | 13 +++++++++---- src/ViewModels/Pull.cs | 8 +++++++- src/Views/Fetch.axaml | 6 +++++- src/Views/Pull.axaml | 6 +++++- 10 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index d65fee4e..ca1d83d6 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -7,7 +7,7 @@ namespace SourceGit.Commands { public class Fetch : Command { - public Fetch(string repo, string remote, bool prune, Action outputHandler) + public Fetch(string repo, string remote, bool prune, bool noTags, Action outputHandler) { _outputHandler = outputHandler; WorkingDirectory = repo; @@ -24,9 +24,15 @@ namespace SourceGit.Commands Args = "-c credential.helper=manager "; } - Args += "fetch --force --progress --verbose "; + Args += "fetch --progress --verbose "; if (prune) Args += "--prune "; + + if (noTags) + Args += "--no-tags "; + else + Args += "--force "; + Args += remote; AutoFetch.MarkFetched(repo); @@ -132,7 +138,7 @@ namespace SourceGit.Commands { var job = new Job { - Cmd = new Fetch(repo, "--all", true, null) { RaiseError = false }, + Cmd = new Fetch(repo, "--all", true, false, null) { RaiseError = false }, NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)), }; diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index d4f15dda..43418825 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Pull : Command { - public Pull(string repo, string remote, string branch, bool useRebase, Action outputHandler) + public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action outputHandler) { _outputHandler = outputHandler; WorkingDirectory = repo; @@ -24,6 +24,9 @@ namespace SourceGit.Commands Args += "pull --verbose --progress --tags "; if (useRebase) Args += "--rebase "; + if (noTags) + Args += "--no-tags "; + Args += $"{remote} {branch}"; } diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 0586ee99..d3d41a8e 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -200,6 +200,7 @@ Fast-Forward (without checkout) Fetch Fetch all remotes + Fetch without tags Prune remote dead branches Remote: Fetch Remote Changes @@ -386,6 +387,7 @@ Discard Do Nothing Stash & Reapply + Fetch without tags Remote: Pull (Fetch & Merge) Use rebase instead of merge diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 3b6bfb9d..80d4c3b9 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -203,6 +203,7 @@ 快进(fast-forward,无需checkout) 拉取(fetch) 拉取所有的远程仓库 + 不拉取远程标签 自动清理远程已删除分支 远程仓库 : 拉取远程仓库内容 @@ -389,6 +390,7 @@ 丢弃更改 不做处理 贮藏并自动恢复 + 不拉取远程标签 远程 : 拉回(拉取并合并) 使用变基方式合并分支 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 5563a21a..b37b681f 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -203,6 +203,7 @@ 快進(fast-forward,無需checkout) 拉取(fetch) 拉取所有的遠端倉庫 + 不拉取遠端標籤 自動清理遠端已刪除分支 遠端倉庫 : 拉取遠端倉庫內容 @@ -389,6 +390,7 @@ 丟棄更改 不做處理 儲藏並自動恢復 + 不拉取遠端標籤 遠端 : 拉回(拉取併合並) 使用變基方式合併分支 diff --git a/src/ViewModels/AddRemote.cs b/src/ViewModels/AddRemote.cs index 296ddfdb..104a4ed6 100644 --- a/src/ViewModels/AddRemote.cs +++ b/src/ViewModels/AddRemote.cs @@ -100,7 +100,7 @@ namespace SourceGit.ViewModels { SetProgressDescription("Fetching from added remote ..."); new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); - new Commands.Fetch(_repo.FullPath, _name, true, SetProgressDescription).Exec(); + new Commands.Fetch(_repo.FullPath, _name, true, false, SetProgressDescription).Exec(); } CallUIThread(() => { diff --git a/src/ViewModels/Fetch.cs b/src/ViewModels/Fetch.cs index 43ad49dc..3b33bbf2 100644 --- a/src/ViewModels/Fetch.cs +++ b/src/ViewModels/Fetch.cs @@ -26,14 +26,19 @@ namespace SourceGit.ViewModels { get; set; - } + } = true; + + public bool NoTags + { + get; + set; + } = false; public Fetch(Repository repo, Models.Remote preferedRemote = null) { _repo = repo; _fetchAllRemotes = preferedRemote == null; SelectedRemote = preferedRemote != null ? preferedRemote : _repo.Remotes[0]; - Prune = true; View = new Views.Fetch() { DataContext = this }; } @@ -47,13 +52,13 @@ namespace SourceGit.ViewModels foreach (var remote in _repo.Remotes) { SetProgressDescription($"Fetching remote: {remote.Name}"); - new Commands.Fetch(_repo.FullPath, remote.Name, Prune, SetProgressDescription).Exec(); + new Commands.Fetch(_repo.FullPath, remote.Name, Prune, NoTags, SetProgressDescription).Exec(); } } else { SetProgressDescription($"Fetching remote: {SelectedRemote.Name}"); - new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, Prune, SetProgressDescription).Exec(); + new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, Prune, NoTags, SetProgressDescription).Exec(); } CallUIThread(() => _repo.SetWatcherEnabled(true)); diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index 5a62f5b3..c210017f 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -59,6 +59,12 @@ namespace SourceGit.ViewModels set => _repo.Settings.PreferRebaseInsteadOfMerge = value; } + public bool NoTags + { + get; + set; + } = false; + public Pull(Repository repo, Models.Branch specifiedRemoteBranch) { _repo = repo; @@ -145,7 +151,7 @@ namespace SourceGit.ViewModels } SetProgressDescription($"Pull {_selectedRemote.Name}/{_selectedBranch.Name}..."); - var rs = new Commands.Pull(_repo.FullPath, _selectedRemote.Name, _selectedBranch.Name, UseRebase, SetProgressDescription).Exec(); + var rs = new Commands.Pull(_repo.FullPath, _selectedRemote.Name, _selectedBranch.Name, UseRebase, NoTags, SetProgressDescription).Exec(); if (rs && needPopStash) { SetProgressDescription("Re-apply local changes..."); diff --git a/src/Views/Fetch.axaml b/src/Views/Fetch.axaml index aaf9bec6..fbcf094b 100644 --- a/src/Views/Fetch.axaml +++ b/src/Views/Fetch.axaml @@ -12,7 +12,7 @@ - + + + diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index d2d1bebb..23d86e1e 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -13,7 +13,7 @@ - + + + From fca20965f8db0388b4711223dcb8f23fff748a95 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 16:27:22 +0800 Subject: [PATCH 06/43] feature: add two color keys `Color.DecoratorBranch` and `Color.DecoratorTag` (#225) --- README.md | 4 ++++ src/Converters/DecoratorTypeConverters.cs | 4 ++-- src/Models/Decorator.cs | 12 +----------- src/Resources/Themes.axaml | 12 +++++++++--- src/Views/CommitBaseInfo.axaml | 2 +- src/Views/Histories.axaml | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 1f73b3c9..a2b36b58 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,10 @@ This app supports open repository in external tools listed in the table below. | Color.BadgeFG | Badge foreground color | | Color.Conflict | Conflict panel background color | | Color.ConflictForeground | Conflict panel foreground color | +| Color.DecoratorIconBG | Background color for commit ref icon | +| Color.DecoratorIcon | Foreground color for commit ref icon | +| Color.DecoratorBranch | Background color for commit branch ref name | +| Color.DecoratorTag | Background color for commit tag ref name | | Color.Border0 | Border color used in some controls, like Window, Tab, Toolbar, etc. | | Color.Border1 | Border color used in inputs, like TextBox, ComboBox, etc. | | Color.Border2 | Border color used in visual lines, like seperators, Rectange, etc. | diff --git a/src/Converters/DecoratorTypeConverters.cs b/src/Converters/DecoratorTypeConverters.cs index e19cb37c..f730b613 100644 --- a/src/Converters/DecoratorTypeConverters.cs +++ b/src/Converters/DecoratorTypeConverters.cs @@ -11,8 +11,8 @@ namespace SourceGit.Converters new FuncValueConverter(v => { if (v == Models.DecoratorType.Tag) - return Models.DecoratorResources.Backgrounds[0]; - return Models.DecoratorResources.Backgrounds[1]; + return Application.Current.FindResource("Brush.DecoratorTag") as IBrush; + return Application.Current.FindResource("Brush.DecoratorBranch") as IBrush; }); public static readonly FuncValueConverter ToIcon = diff --git a/src/Models/Decorator.cs b/src/Models/Decorator.cs index 60ffc1ee..235101cc 100644 --- a/src/Models/Decorator.cs +++ b/src/Models/Decorator.cs @@ -1,6 +1,4 @@ -using Avalonia.Media; - -namespace SourceGit.Models +namespace SourceGit.Models { public enum DecoratorType { @@ -17,12 +15,4 @@ namespace SourceGit.Models public DecoratorType Type { get; set; } = DecoratorType.None; public string Name { get; set; } = ""; } - - public static class DecoratorResources - { - public static readonly IBrush[] Backgrounds = [ - new SolidColorBrush(0xFF02C302), - new SolidColorBrush(0xFFFFB835), - ]; - } } diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml index 56189cab..1f3a0d10 100644 --- a/src/Resources/Themes.axaml +++ b/src/Resources/Themes.axaml @@ -13,8 +13,10 @@ #FFFAFAFA #FFB0CEE8 #FF1F1F1F - #FF6F6F6F + #FF6F6F6F #FFF8F8F8 + #FFFFB835 + #FF02C302 #FF836C2E #FFFFFFFF #FFCFCFCF @@ -43,8 +45,10 @@ #FF1B1B1B #FF8F8F8F #FFDDDDDD - #FF505050 + #FF505050 #FFF8F8F8 + #FFFFB835 + #FF02C302 #FFFAFAD2 #FF252525 #FF181818 @@ -73,8 +77,10 @@ - + + + diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index 42fa2b48..6e0429aa 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -90,7 +90,7 @@ - + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 565daa8e..d1aae81d 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -46,7 +46,7 @@ - + Date: Mon, 1 Jul 2024 17:19:26 +0800 Subject: [PATCH 07/43] readme: remove blockquote in external editors tips --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a2b36b58..3f75cca6 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,9 @@ This app supports open repository in external tools listed in the table below. | JetBrains Fleet | YES | YES | YES | FLEET_PATH | | Sublime Text | YES | YES | YES | SUBLIME_TEXT_PATH | -> * You can set the given environment variable for special tool if it can NOT be found by this app automatically. -> * Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device. -> * On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables. +* You can set the given environment variable for special tool if it can NOT be found by this app automatically. +* Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device. +* On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables. ## Screenshots From 366e467578aa6c8c0caafe430f1b738c9a15db90 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 20:27:59 +0800 Subject: [PATCH 08/43] feature: add new color key `Color.DecoratorFG` to customize foreground color of commit refs (#225) --- README.md | 1 + src/Resources/Themes.axaml | 3 +++ src/Views/CommitBaseInfo.axaml | 2 +- src/Views/Histories.axaml | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f75cca6..0a7817c8 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ This app supports open repository in external tools listed in the table below. | Color.DecoratorIcon | Foreground color for commit ref icon | | Color.DecoratorBranch | Background color for commit branch ref name | | Color.DecoratorTag | Background color for commit tag ref name | +| Color.DecoratorFG | Foreground color for commit tag ref name | | Color.Border0 | Border color used in some controls, like Window, Tab, Toolbar, etc. | | Color.Border1 | Border color used in inputs, like TextBox, ComboBox, etc. | | Color.Border2 | Border color used in visual lines, like seperators, Rectange, etc. | diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml index 1f3a0d10..3dc7943f 100644 --- a/src/Resources/Themes.axaml +++ b/src/Resources/Themes.axaml @@ -17,6 +17,7 @@ #FFF8F8F8 #FFFFB835 #FF02C302 + Black #FF836C2E #FFFFFFFF #FFCFCFCF @@ -49,6 +50,7 @@ #FFF8F8F8 #FFFFB835 #FF02C302 + Black #FFFAFAD2 #FF252525 #FF181818 @@ -81,6 +83,7 @@ + diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index 6e0429aa..3d3dd826 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -94,7 +94,7 @@ - + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index d1aae81d..e07b8d0b 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -58,7 +58,7 @@ Text="{Binding Name}" FontSize="10" Margin="4,0" - Foreground="Black" + Foreground="{DynamicResource Brush.DecoratorFG}" FontWeight="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToFontWeight}}"/> From 6e724d255716c5ada027f541bd73763140074ef5 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 20:29:52 +0800 Subject: [PATCH 09/43] readme: description of colors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a7817c8..303f9136 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ This app supports open repository in external tools listed in the table below. | Color.DecoratorIcon | Foreground color for commit ref icon | | Color.DecoratorBranch | Background color for commit branch ref name | | Color.DecoratorTag | Background color for commit tag ref name | -| Color.DecoratorFG | Foreground color for commit tag ref name | +| Color.DecoratorFG | Foreground color for commit ref name | | Color.Border0 | Border color used in some controls, like Window, Tab, Toolbar, etc. | | Color.Border1 | Border color used in inputs, like TextBox, ComboBox, etc. | | Color.Border2 | Border color used in visual lines, like seperators, Rectange, etc. | From 4998f14547f7a541f9ad872f46c98f4344634c81 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 20:31:43 +0800 Subject: [PATCH 10/43] ux: option order --- src/Views/Fetch.axaml | 8 ++++---- src/Views/Pull.axaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Views/Fetch.axaml b/src/Views/Fetch.axaml index fbcf094b..1f91b24a 100644 --- a/src/Views/Fetch.axaml +++ b/src/Views/Fetch.axaml @@ -38,12 +38,12 @@ IsChecked="{Binding FetchAllRemotes, Mode=TwoWay}"/> - - + + diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index 23d86e1e..fca58231 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -86,12 +86,12 @@ - - + + From 254eac11a199cf334b1e12c83b4e7b278c4f6dc0 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 21:02:01 +0800 Subject: [PATCH 11/43] enhance: paste text on subject * for single line text, all text will be pasted in subject text box * if paste in the middle of subject content, multi-line text will be transformed into single line text by `ReplaceLineEndings(" ")` * if paste in the end of subject content, multi-line text will be split into two parts. - first line will be pasted in the subject box - remains will be pasted in the start of description box --- src/Views/CommitMessageTextBox.axaml.cs | 37 ++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Views/CommitMessageTextBox.axaml.cs b/src/Views/CommitMessageTextBox.axaml.cs index 043c05aa..70f978b8 100644 --- a/src/Views/CommitMessageTextBox.axaml.cs +++ b/src/Views/CommitMessageTextBox.axaml.cs @@ -20,6 +20,11 @@ namespace SourceGit.Views protected override Type StyleKeyOverride => typeof(TextBox); + public void Paste(string text) + { + OnTextInput(new TextInputEventArgs() { Text = text }); + } + protected override void OnKeyDown(KeyEventArgs e) { var dump = new KeyEventArgs() @@ -112,7 +117,7 @@ namespace SourceGit.Views } } - private void OnSubjectTextBoxPreviewKeyDown(object sender, KeyEventArgs e) + private async void OnSubjectTextBoxPreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter || (e.Key == Key.Right && SubjectEditor.CaretIndex == Subject.Length)) { @@ -120,6 +125,36 @@ namespace SourceGit.Views DescriptionEditor.CaretIndex = 0; e.Handled = true; } + else if (e.Key == Key.V && ((OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Meta) || (!OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Control))) + { + var text = await App.GetClipboardTextAsync(); + text = text.Trim(); + + if (!string.IsNullOrEmpty(text)) + { + if (SubjectEditor.CaretIndex == Subject.Length) + { + var idx = text.IndexOf('\n'); + if (idx == -1) + { + SubjectEditor.Paste(text); + } + else + { + SubjectEditor.Paste(text.Substring(0, idx)); + DescriptionEditor.Focus(); + DescriptionEditor.CaretIndex = 0; + DescriptionEditor.Paste(text.Substring(idx + 1) + "\n"); + } + } + else + { + SubjectEditor.Paste(text.ReplaceLineEndings(" ")); + } + } + + e.Handled = true; + } } private void OnDescriptionTextBoxPreviewKeyDown(object sender, KeyEventArgs e) From 68abb2d78b2720722a343b4bdcc45160af22307e Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 1 Jul 2024 21:08:37 +0800 Subject: [PATCH 12/43] fix: crash when clipboard is empty --- src/Views/CommitMessageTextBox.axaml.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Views/CommitMessageTextBox.axaml.cs b/src/Views/CommitMessageTextBox.axaml.cs index 70f978b8..1b458ede 100644 --- a/src/Views/CommitMessageTextBox.axaml.cs +++ b/src/Views/CommitMessageTextBox.axaml.cs @@ -128,10 +128,10 @@ namespace SourceGit.Views else if (e.Key == Key.V && ((OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Meta) || (!OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Control))) { var text = await App.GetClipboardTextAsync(); - text = text.Trim(); - - if (!string.IsNullOrEmpty(text)) + if (!string.IsNullOrWhiteSpace(text)) { + text = text.Trim(); + if (SubjectEditor.CaretIndex == Subject.Length) { var idx = text.IndexOf('\n'); From 7965338d7c54492bfa441f36ed4fcd39de67bc4b Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 2 Jul 2024 09:42:49 +0800 Subject: [PATCH 13/43] readme: move custom theme to another repo sourcegit-themes (#227) --- README.md | 46 ++-------------------------------------------- 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 303f9136..3636e12e 100644 --- a/README.md +++ b/README.md @@ -94,51 +94,9 @@ This app supports open repository in external tools listed in the table below. ![Theme Light](./screenshots/theme_light.png) -## How to Customize Theme +* Custom Themes -1. Create a new json file, and provide your favorite colors with follow keys: - -| Key | Description | -| --- | --- | -| Color.Window | Window background color | -| Color.WindowBorder | Window border color. Only used on Linux. | -| Color.TitleBar | Title bar background color | -| Color.ToolBar | Tool bar background color | -| Color.Popup | Popup panel background color | -| Color.Contents | Background color used in inputs, data grids, file content viewer, change lists, text diff viewer, etc. | -| Color.Badge | Badge background color | -| Color.BadgeFG | Badge foreground color | -| Color.Conflict | Conflict panel background color | -| Color.ConflictForeground | Conflict panel foreground color | -| Color.DecoratorIconBG | Background color for commit ref icon | -| Color.DecoratorIcon | Foreground color for commit ref icon | -| Color.DecoratorBranch | Background color for commit branch ref name | -| Color.DecoratorTag | Background color for commit tag ref name | -| Color.DecoratorFG | Foreground color for commit ref name | -| Color.Border0 | Border color used in some controls, like Window, Tab, Toolbar, etc. | -| Color.Border1 | Border color used in inputs, like TextBox, ComboBox, etc. | -| Color.Border2 | Border color used in visual lines, like seperators, Rectange, etc. | -| Color.FlatButton.Background | Flat button background color, like `Cancel`, `Commit & Push` button | -| Color.FlatButton.BackgroundHovered | Flat button background color when hovered, like `Cancel` button | -| Color.FG1 | Primary foreground color for all text elements | -| Color.FG2 | Secondary foreground color for all text elements | -| Color.Diff.EmptyBG | Background color used in empty lines in diff viewer | -| Color.Diff.AddedBG | Background color used in added lines in diff viewer | -| Color.Diff.DeletedBG | Background color used in deleted lines in diff viewer | -| Color.Diff.AddedHighlight | Background color used for changed words in added lines in diff viewer | -| Color.Diff.DeletedHighlight | Background color used for changed words in deleted lines in diff viewer | - -For example: - -```json -{ - "Color.Window": "#FFFF6059" -} -``` - -2. Open `Preference` -> `Appearance`, choose the json file you just created in `Custom Color Schema`. - -> **NOTE**: The `Custom Color Schema` will override the colors with same keys in current active theme. +You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git) ## Contributing From 4ef0f1180fa62937612b1ead79773048ffb22880 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 2 Jul 2024 10:23:21 +0800 Subject: [PATCH 14/43] feature: add tooltip for branches (#228) --- src/Converters/BranchConverters.cs | 10 ---------- src/Models/Branch.cs | 2 ++ src/ViewModels/AddWorktree.cs | 2 +- src/ViewModels/BranchTreeNode.cs | 11 +++++++++++ src/ViewModels/CreateBranch.cs | 3 +-- src/ViewModels/DeleteBranch.cs | 5 ++--- src/ViewModels/DeleteMultipleBranches.cs | 2 +- src/ViewModels/GitFlowStart.cs | 3 +-- src/ViewModels/Histories.cs | 4 ++-- src/ViewModels/Repository.cs | 19 ++++++++++--------- src/Views/Archive.axaml | 2 +- src/Views/BranchCompare.axaml | 4 ++-- src/Views/CreateBranch.axaml | 2 +- src/Views/CreateTag.axaml | 2 +- src/Views/DeleteBranch.axaml | 2 +- src/Views/DeleteMultipleBranches.axaml | 3 +-- src/Views/FastForwardWithoutCheckout.axaml | 2 +- src/Views/InteractiveRebase.axaml | 2 +- src/Views/Pull.axaml | 2 +- src/Views/Rebase.axaml | 2 +- src/Views/Repository.axaml | 18 ++++++++++++------ 21 files changed, 54 insertions(+), 48 deletions(-) delete mode 100644 src/Converters/BranchConverters.cs diff --git a/src/Converters/BranchConverters.cs b/src/Converters/BranchConverters.cs deleted file mode 100644 index d20ed89f..00000000 --- a/src/Converters/BranchConverters.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Avalonia.Data.Converters; - -namespace SourceGit.Converters -{ - public static class BranchConverters - { - public static readonly FuncValueConverter ToName = - new FuncValueConverter(v => v.IsLocal ? v.Name : $"{v.Remote}/{v.Name}"); - } -} diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index a22ee553..07f5374e 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -11,5 +11,7 @@ public string UpstreamTrackStatus { get; set; } public string Remote { get; set; } public bool IsHead { get; set; } + + public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}"; } } diff --git a/src/ViewModels/AddWorktree.cs b/src/ViewModels/AddWorktree.cs index 04cda5b5..00cf604b 100644 --- a/src/ViewModels/AddWorktree.cs +++ b/src/ViewModels/AddWorktree.cs @@ -71,7 +71,7 @@ namespace SourceGit.ViewModels if (branch.IsLocal) LocalBranches.Add(branch.Name); else - RemoteBranches.Add($"{branch.Remote}/{branch.Name}"); + RemoteBranches.Add(branch.FriendlyName); } if (RemoteBranches.Count > 0) diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index ef61579d..11486c20 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -69,6 +69,17 @@ namespace SourceGit.ViewModels set => SetProperty(ref _isSelected, value); } + public string Tooltip + { + get + { + if (Backend is Models.Branch b) + return b.FriendlyName; + + return null; + } + } + public CornerRadius CornerRadius { get => _cornerRadius; diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index e80dec15..2f7ac4f8 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -72,8 +72,7 @@ namespace SourceGit.ViewModels foreach (var b in creator._repo.Branches) { - var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}"; - if (test == name) + if (b.FriendlyName == name) return new ValidationResult("A branch with same name already exists!"); } diff --git a/src/ViewModels/DeleteBranch.cs b/src/ViewModels/DeleteBranch.cs index 787e00fc..292cc2aa 100644 --- a/src/ViewModels/DeleteBranch.cs +++ b/src/ViewModels/DeleteBranch.cs @@ -35,9 +35,8 @@ namespace SourceGit.ViewModels if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream)) { - var upstream = branch.Upstream.Substring(13); - TrackingRemoteBranch = repo.Branches.Find(x => !x.IsLocal && $"{x.Remote}/{x.Name}" == upstream); - DeleteTrackingRemoteTip = new Views.NameHighlightedTextBlock("DeleteBranch.WithTrackingRemote", upstream); + TrackingRemoteBranch = repo.Branches.Find(x => x.FullName == branch.Upstream); + DeleteTrackingRemoteTip = new Views.NameHighlightedTextBlock("DeleteBranch.WithTrackingRemote", TrackingRemoteBranch.FriendlyName); } View = new Views.DeleteBranch() { DataContext = this }; diff --git a/src/ViewModels/DeleteMultipleBranches.cs b/src/ViewModels/DeleteMultipleBranches.cs index 433d80c6..6048ec68 100644 --- a/src/ViewModels/DeleteMultipleBranches.cs +++ b/src/ViewModels/DeleteMultipleBranches.cs @@ -37,7 +37,7 @@ namespace SourceGit.ViewModels { foreach (var target in Targets) { - SetProgressDescription($"Deleting remote branch : {target.Remote}/{target.Name}"); + SetProgressDescription($"Deleting remote branch : {target.FriendlyName}"); Commands.Branch.DeleteRemote(_repo.FullPath, target.Remote, target.Name); } } diff --git a/src/ViewModels/GitFlowStart.cs b/src/ViewModels/GitFlowStart.cs index 0806dfb4..ca580d99 100644 --- a/src/ViewModels/GitFlowStart.cs +++ b/src/ViewModels/GitFlowStart.cs @@ -39,8 +39,7 @@ namespace SourceGit.ViewModels var check = $"{starter._prefix}{name}"; foreach (var b in starter._repo.Branches) { - var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}"; - if (test == check) + if (b.FriendlyName == check) return new ValidationResult("A branch with same name already exists!"); } } diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 7bdd0bf2..5d035f3b 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -175,7 +175,7 @@ namespace SourceGit.ViewModels } else if (d.Type == Models.DecoratorType.RemoteBranchHead) { - var b = _repo.Branches.Find(x => !x.IsLocal && d.Name == $"{x.Remote}/{x.Name}"); + var b = _repo.Branches.Find(x => !x.IsLocal && d.Name == x.FriendlyName); FillRemoteBranchMenu(menu, b, current, commit.IsMerged); } else if (d.Type == Models.DecoratorType.Tag) @@ -583,7 +583,7 @@ namespace SourceGit.ViewModels private void FillRemoteBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) { - var name = $"{branch.Remote}/{branch.Name}"; + var name = branch.FriendlyName; var submenu = new MenuItem(); submenu.Icon = App.CreateMenuIcon("Icons.Branch"); diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index dec5486d..3246b967 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1202,7 +1202,7 @@ namespace SourceGit.ViewModels if (upstream != null) { var fastForward = new MenuItem(); - fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", $"{upstream.Remote}/{upstream.Name}"); + fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", upstream.FriendlyName); fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); fastForward.IsEnabled = !string.IsNullOrEmpty(branch.UpstreamTrackStatus) && branch.UpstreamTrackStatus.IndexOf('↑') < 0; fastForward.Click += (o, e) => @@ -1491,9 +1491,10 @@ namespace SourceGit.ViewModels { var menu = new ContextMenu(); var current = Branches.Find(x => x.IsCurrent); + var name = branch.FriendlyName; var checkout = new MenuItem(); - checkout.Header = new Views.NameHighlightedTextBlock("BranchCM.Checkout", $"{branch.Remote}/{branch.Name}"); + checkout.Header = new Views.NameHighlightedTextBlock("BranchCM.Checkout", name); checkout.Icon = App.CreateMenuIcon("Icons.Check"); checkout.Click += (o, e) => { @@ -1506,7 +1507,7 @@ namespace SourceGit.ViewModels if (current != null) { var pull = new MenuItem(); - pull.Header = new Views.NameHighlightedTextBlock("BranchCM.PullInto", $"{branch.Remote}/{branch.Name}", current.Name); + pull.Header = new Views.NameHighlightedTextBlock("BranchCM.PullInto", name, current.Name); pull.Icon = App.CreateMenuIcon("Icons.Pull"); pull.Click += (o, e) => { @@ -1516,17 +1517,17 @@ namespace SourceGit.ViewModels }; var merge = new MenuItem(); - merge.Header = new Views.NameHighlightedTextBlock("BranchCM.Merge", $"{branch.Remote}/{branch.Name}", current.Name); + merge.Header = new Views.NameHighlightedTextBlock("BranchCM.Merge", name, current.Name); merge.Icon = App.CreateMenuIcon("Icons.Merge"); merge.Click += (o, e) => { if (PopupHost.CanCreatePopup()) - PopupHost.ShowPopup(new Merge(this, $"{branch.Remote}/{branch.Name}", current.Name)); + PopupHost.ShowPopup(new Merge(this, name, current.Name)); e.Handled = true; }; var rebase = new MenuItem(); - rebase.Header = new Views.NameHighlightedTextBlock("BranchCM.Rebase", current.Name, $"{branch.Remote}/{branch.Name}"); + rebase.Header = new Views.NameHighlightedTextBlock("BranchCM.Rebase", current.Name, name); rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); rebase.Click += (o, e) => { @@ -1573,7 +1574,7 @@ namespace SourceGit.ViewModels menu.Items.Add(new MenuItem() { Header = "-" }); var delete = new MenuItem(); - delete.Header = new Views.NameHighlightedTextBlock("BranchCM.Delete", $"{branch.Remote}/{branch.Name}"); + delete.Header = new Views.NameHighlightedTextBlock("BranchCM.Delete", name); delete.Icon = App.CreateMenuIcon("Icons.Clear"); delete.Click += (o, e) => { @@ -1617,7 +1618,7 @@ namespace SourceGit.ViewModels copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Click += (o, e) => { - App.CopyText(branch.Remote + "/" + branch.Name); + App.CopyText(name); e.Handled = true; }; @@ -1810,7 +1811,7 @@ namespace SourceGit.ViewModels { var dup = b; var target = new MenuItem(); - target.Header = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}"; + target.Header = b.FriendlyName; target.Icon = App.CreateMenuIcon(b.IsCurrent ? "Icons.Check" : "Icons.Branch"); target.Click += (_, e) => { diff --git a/src/Views/Archive.axaml b/src/Views/Archive.axaml index 33b58fe4..6d9bc3b2 100644 --- a/src/Views/Archive.axaml +++ b/src/Views/Archive.axaml @@ -23,7 +23,7 @@ - + diff --git a/src/Views/BranchCompare.axaml b/src/Views/BranchCompare.axaml index 9ad7db03..1ba30119 100644 --- a/src/Views/BranchCompare.axaml +++ b/src/Views/BranchCompare.axaml @@ -61,7 +61,7 @@ User="{Binding BaseHead.Author}"/> - + @@ -82,7 +82,7 @@ User="{Binding ToHead.Author}"/> - + diff --git a/src/Views/CreateBranch.axaml b/src/Views/CreateBranch.axaml index a6565807..f69ea150 100644 --- a/src/Views/CreateBranch.axaml +++ b/src/Views/CreateBranch.axaml @@ -24,7 +24,7 @@ - + diff --git a/src/Views/CreateTag.axaml b/src/Views/CreateTag.axaml index f3de6717..a3f901a4 100644 --- a/src/Views/CreateTag.axaml +++ b/src/Views/CreateTag.axaml @@ -23,7 +23,7 @@ - + diff --git a/src/Views/DeleteBranch.axaml b/src/Views/DeleteBranch.axaml index 8245c091..07230de9 100644 --- a/src/Views/DeleteBranch.axaml +++ b/src/Views/DeleteBranch.axaml @@ -18,7 +18,7 @@ - + diff --git a/src/Views/DeleteMultipleBranches.axaml b/src/Views/DeleteMultipleBranches.axaml index 75f28674..9fd87b90 100644 --- a/src/Views/DeleteMultipleBranches.axaml +++ b/src/Views/DeleteMultipleBranches.axaml @@ -64,8 +64,7 @@ - + diff --git a/src/Views/FastForwardWithoutCheckout.axaml b/src/Views/FastForwardWithoutCheckout.axaml index cb33c09e..713e9c3b 100644 --- a/src/Views/FastForwardWithoutCheckout.axaml +++ b/src/Views/FastForwardWithoutCheckout.axaml @@ -16,7 +16,7 @@ - + diff --git a/src/Views/InteractiveRebase.axaml b/src/Views/InteractiveRebase.axaml index 8e966509..d4238d65 100644 --- a/src/Views/InteractiveRebase.axaml +++ b/src/Views/InteractiveRebase.axaml @@ -54,7 +54,7 @@ - + diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index fca58231..0d749e60 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -48,7 +48,7 @@ - + diff --git a/src/Views/Rebase.axaml b/src/Views/Rebase.axaml index aa52a15b..f98756af 100644 --- a/src/Views/Rebase.axaml +++ b/src/Views/Rebase.axaml @@ -31,7 +31,7 @@ - + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index edbf9432..0a3a9478 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -269,7 +269,7 @@ - + @@ -281,7 +281,10 @@ - + @@ -294,7 +297,8 @@ IsVisible="{Binding IsBranch}" Checked="OnToggleFilter" Unchecked="OnToggleFilter" - IsChecked="{Binding IsFiltered}"/> + IsChecked="{Binding IsFiltered}" + ToolTip.Tip="{DynamicResource Text.Filter}"/> @@ -343,7 +347,7 @@ - + @@ -357,7 +361,8 @@ Checked="OnToggleFilter" Unchecked="OnToggleFilter" IsVisible="{Binding IsBranch}" - IsChecked="{Binding IsFiltered}"/> + IsChecked="{Binding IsFiltered}" + ToolTip.Tip="{DynamicResource Text.Filter}"/> @@ -444,7 +449,8 @@ Background="Transparent" Checked="OnToggleFilter" Unchecked="OnToggleFilter" - IsChecked="{Binding IsFiltered}"/> + IsChecked="{Binding IsFiltered}" + ToolTip.Tip="{DynamicResource Text.Filter}"/> From 57a2144777f6362d75212d6b3d160715a5e0c28b Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 2 Jul 2024 12:30:12 +0800 Subject: [PATCH 15/43] fix: try to fix the timezone issue (#229) --- src/Commands/Blame.cs | 3 +-- src/Models/Commit.cs | 11 +++++------ src/Models/Stash.cs | 4 +--- src/Models/Statistics.cs | 4 +--- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs index 5d047d8c..e4c7f12a 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -9,7 +9,6 @@ namespace SourceGit.Commands [GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")] private static partial Regex REG_FORMAT(); - private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); public Blame(string repo, string file, string revision) { @@ -67,7 +66,7 @@ namespace SourceGit.Commands var commit = match.Groups[1].Value; var author = match.Groups[2].Value; var timestamp = int.Parse(match.Groups[3].Value); - var when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd"); + var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString("yyyy/MM/dd"); var info = new Models.BlameLineInfo() { diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 363b4b08..0a95eac3 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -19,14 +19,13 @@ namespace SourceGit.Models public bool IsMerged { get; set; } = false; public Thickness Margin { get; set; } = new Thickness(0); - public string AuthorTimeStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd HH:mm:ss"); - public string CommitterTimeStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd HH:mm:ss"); - public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd"); - public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd"); + + public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); + public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); + public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd"); + public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToString("yyyy/MM/dd"); public bool IsCommitterVisible => Author != Committer || AuthorTime != CommitterTime; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; - - private static readonly DateTime _utcStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); } } diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs index 2fab0f2f..06da763a 100644 --- a/src/Models/Stash.cs +++ b/src/Models/Stash.cs @@ -4,13 +4,11 @@ namespace SourceGit.Models { public class Stash { - private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); - public string Name { get; set; } = ""; public string SHA { get; set; } = ""; public ulong Time { get; set; } = 0; public string Message { get; set; } = ""; - public string TimeStr => UTC_START.AddSeconds(Time).ToString("yyyy/MM/dd HH:mm:ss"); + public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); } } diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs index b0d619e1..abdd24df 100644 --- a/src/Models/Statistics.cs +++ b/src/Models/Statistics.cs @@ -49,7 +49,6 @@ namespace SourceGit.Models public Statistics() { - _utcStart = DateTime.UnixEpoch; _today = DateTime.Today; _thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24 - _today.Hour * 3600 - _today.Minute * 60 - _today.Second); _thisWeekEnd = _thisWeekStart.AddDays(7); @@ -115,7 +114,7 @@ namespace SourceGit.Models public void AddCommit(string committer, double timestamp) { - var time = _utcStart.AddSeconds(timestamp).ToLocalTime(); + var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); if (time.CompareTo(_thisWeekStart) >= 0 && time.CompareTo(_thisWeekEnd) < 0) { Week.AddCommit((int)time.DayOfWeek, committer); @@ -136,7 +135,6 @@ namespace SourceGit.Models Week.Complete(); } - private readonly DateTime _utcStart; private readonly DateTime _today; private readonly DateTime _thisWeekStart; private readonly DateTime _thisWeekEnd; From 21ec16b4ccb700a33a8b43e46aa95b8b9b0796fe Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 2 Jul 2024 12:35:09 +0800 Subject: [PATCH 16/43] readme: add `winget` install tips --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3636e12e..f79c774b 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,9 @@ This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationD For **Windows** users: * **MSYS Git is NOT supported**. Please use official [Git for Windows](https://git-scm.com/download/win) instead. -* `sourcegit_x.y.win-x64.zip` may be reported as virus by Windows Defender. I don't know why. I have manually tested the zip to be uploaded using Windows Defender before uploading and no virus was found. If you have installed .NET 8 SDK locally, I suggest you to compile it yourself. And if you have any idea about how to fix this, please open an issue. +* You can install the latest stable by `winget install SourceGit`. + - 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. +* Portable versions can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) For **macOS** users: From 50fe25a631967af4107fa7bf40bd335d4839c5b1 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 2 Jul 2024 14:17:21 +0800 Subject: [PATCH 17/43] enhance: only load repository settings if file exists --- src/ViewModels/Repository.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 3246b967..86f47656 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -268,11 +268,18 @@ namespace SourceGit.ViewModels public void Open() { var settingsFile = Path.Combine(_gitDir, "sourcegit.settings"); - try + if (File.Exists(settingsFile)) { - _settings = JsonSerializer.Deserialize(File.ReadAllText(settingsFile), JsonCodeGen.Default.RepositorySettings); + try + { + _settings = JsonSerializer.Deserialize(File.ReadAllText(settingsFile), JsonCodeGen.Default.RepositorySettings); + } + catch + { + _settings = new RepositorySettings(); + } } - catch + else { _settings = new RepositorySettings(); } From c170f261dbd42db1faa1c26f4c67e99af4a9942f Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 2 Jul 2024 20:59:28 +0800 Subject: [PATCH 18/43] ux: re-design commit detail information page to avoid commit message hidden in scoll view --- src/Converters/ListConverters.cs | 7 +++ src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/Views/CommitBaseInfo.axaml | 4 +- src/Views/CommitDetail.axaml | 95 +++++++++++++++---------------- src/Views/CommitDetail.axaml.cs | 28 +++------ 7 files changed, 64 insertions(+), 73 deletions(-) diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs index dac55076..fef0f584 100644 --- a/src/Converters/ListConverters.cs +++ b/src/Converters/ListConverters.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using Avalonia.Data.Converters; @@ -11,5 +12,11 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsNotNullOrEmpty = new FuncValueConverter(v => v != null && v.Count > 0); + + public static readonly FuncValueConverter, List> Top100Changes = + new FuncValueConverter, List>(v => (v == null || v.Count < 100) ? v : v.GetRange(0, 100)); + + public static readonly FuncValueConverter IsOnlyTop100Shows = + new FuncValueConverter(v => v != null && v.Count > 100); } } diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index d3d41a8e..546a300b 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -111,6 +111,7 @@ AUTHOR CHANGED COMMITTER + Shows only the first 100 changes. See all changes on the CHANGES tab. MESSAGE PARENTS REFS diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 80d4c3b9..fcc2f40f 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -114,6 +114,7 @@ 修改者 变更列表 提交者 + 仅显示前100项变更。请前往【变更对比】页面查看全部。 提交信息 父提交 相关引用 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index b37b681f..159009b9 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -114,6 +114,7 @@ 修改者 變更列表 提交者 + 僅顯示前100項變更。 請前往『變更對比』頁面查看全部。 提交資訊 父提交 相關引用 diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index 3d3dd826..19b75a50 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -104,9 +104,7 @@ - - - + diff --git a/src/Views/CommitDetail.axaml b/src/Views/CommitDetail.axaml index a46d5e54..05a521e1 100644 --- a/src/Views/CommitDetail.axaml +++ b/src/Views/CommitDetail.axaml @@ -17,58 +17,55 @@ - - - + + + + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index 1209e41a..999d1c07 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -10,37 +10,23 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnChangeListDoubleTapped(object sender, TappedEventArgs e) + private void OnChangeDoubleTapped(object sender, TappedEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail) + if (DataContext is ViewModels.CommitDetail detail && sender is Grid grid && grid.DataContext is Models.Change change) { - var datagrid = sender as DataGrid; - if (datagrid.SelectedItem == null) - { - e.Handled = true; - return; - } - detail.ActivePageIndex = 1; - detail.SelectedChanges = new() { datagrid.SelectedItem as Models.Change }; + detail.SelectedChanges = new() { change }; } e.Handled = true; } - private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail) + if (DataContext is ViewModels.CommitDetail detail && sender is Grid grid && grid.DataContext is Models.Change change) { - var datagrid = sender as DataGrid; - if (datagrid.SelectedItem == null) - { - e.Handled = true; - return; - } - - var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); - datagrid.OpenContextMenu(menu); + var menu = detail.CreateChangeContextMenu(change); + grid.OpenContextMenu(menu); } e.Handled = true; From 80f72676ecd7eaffd5bb9d4bf205b7a74a643ec0 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 2 Jul 2024 22:54:26 +0800 Subject: [PATCH 19/43] ux: new custom theme configuration format * supports customize the commit graph --- src/App.JsonCodeGen.cs | 6 ++-- src/App.axaml.cs | 19 ++++++++++-- src/Models/CommitGraph.cs | 52 ++++++++++++++++++++++++--------- src/Models/CustomColorSchema.cs | 10 +++++++ src/ViewModels/Repository.cs | 2 +- 5 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 src/Models/CustomColorSchema.cs diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 50d589a2..901a9b5b 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -4,10 +4,10 @@ using System.Text.Json.Serialization; namespace SourceGit { [JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)] - [JsonSerializable(typeof(Models.Version))] - [JsonSerializable(typeof(Models.JetBrainsState))] [JsonSerializable(typeof(List))] - [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Models.JetBrainsState))] + [JsonSerializable(typeof(Models.Version))] + [JsonSerializable(typeof(Models.CustomColorSchema))] [JsonSerializable(typeof(ViewModels.Preference))] [JsonSerializable(typeof(ViewModels.RepositorySettings))] internal partial class JsonCodeGen : JsonSerializerContext { } diff --git a/src/App.axaml.cs b/src/App.axaml.cs index db0a4644..df438ecc 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Reflection; @@ -159,15 +160,27 @@ namespace SourceGit app._colorOverrides = null; } + Models.CommitGraph.SetDefaultPens(); + if (!string.IsNullOrEmpty(colorsFile) && File.Exists(colorsFile)) { try { var resDic = new ResourceDictionary(); - var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.DictionaryStringString); - foreach (var kv in schema) - resDic[kv.Key] = Color.Parse(kv.Value); + var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.CustomColorSchema); + foreach (var kv in schema.Basic) + resDic[$"Color.{kv.Key}"] = Color.Parse(kv.Value); + + if (schema.Graph.Count > 0) + { + var penColors = new List(); + + foreach (var c in schema.Graph) + penColors.Add(Color.Parse(c)); + + Models.CommitGraph.SetPenColors(penColors); + } app.Resources.MergedDictionaries.Add(resDic); app._colorOverrides = resDic; diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 6b26eba5..bc0ea8e1 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -8,17 +8,6 @@ namespace SourceGit.Models { public class CommitGraph { - public static readonly Pen[] Pens = [ - new Pen(Brushes.Orange, 2), - new Pen(Brushes.ForestGreen, 2), - new Pen(Brushes.Gold, 2), - new Pen(Brushes.Magenta, 2), - new Pen(Brushes.Red, 2), - new Pen(Brushes.Gray, 2), - new Pen(Brushes.Turquoise, 2), - new Pen(Brushes.Olive, 2), - ]; - public class Path { public List Points = new List(); @@ -113,7 +102,28 @@ namespace SourceGit.Models public List Links { get; set; } = new List(); public List Dots { get; set; } = new List(); - public static CommitGraph Parse(List commits, int colorCount) + public static List Pens + { + get; + private set; + } = new List(); + + public static void SetDefaultPens() + { + SetPenColors(_defaultPenColors); + } + + public static void SetPenColors(List colors) + { + Pens.Clear(); + + foreach (var c in colors) + Pens.Add(new Pen(c.ToUInt32(), 2)); + + _penCount = colors.Count; + } + + public static CommitGraph Parse(List commits) { double UNIT_WIDTH = 12; double HALF_WIDTH = 6; @@ -184,7 +194,7 @@ namespace SourceGit.Models major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY)); unsolved.Add(major); temp.Paths.Add(major.Path); - colorIdx = (colorIdx + 1) % colorCount; + colorIdx = (colorIdx + 1) % _penCount; } // Calculate link position of this commit. @@ -223,7 +233,7 @@ namespace SourceGit.Models var l = new PathHelper(commit.Parents[j], isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT)); unsolved.Add(l); temp.Paths.Add(l.Path); - colorIdx = (colorIdx + 1) % colorCount; + colorIdx = (colorIdx + 1) % _penCount; } } @@ -257,5 +267,19 @@ namespace SourceGit.Models return temp; } + + private static int _penCount = 0; + private static readonly List _defaultPenColors = [ + Colors.Orange, + Colors.ForestGreen, + Colors.Gold, + Colors.Magenta, + Colors.Red, + Colors.Gray, + Colors.Turquoise, + Colors.Olive, + Colors.Khaki, + Colors.Lime, + ]; } } diff --git a/src/Models/CustomColorSchema.cs b/src/Models/CustomColorSchema.cs new file mode 100644 index 00000000..4266b98e --- /dev/null +++ b/src/Models/CustomColorSchema.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public class CustomColorSchema + { + public Dictionary Basic { get; set; } = new Dictionary(); + public List Graph { get; set; } = new List(); + } +} diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 86f47656..0a51c384 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -709,7 +709,7 @@ namespace SourceGit.ViewModels } var commits = new Commands.QueryCommits(FullPath, limits).Result(); - var graph = Models.CommitGraph.Parse(commits, 8); + var graph = Models.CommitGraph.Parse(commits); Dispatcher.UIThread.Invoke(() => { From 54fb25257eeb926f8add0df29e760114ab2c7092 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 2 Jul 2024 23:14:49 +0800 Subject: [PATCH 20/43] fix: binding error --- src/Resources/Styles.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 460a6ce5..3710a8cd 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -29,7 +29,7 @@ - + Red From 997edae8587f7d7b50059fc19d4a0171c00762e1 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 3 Jul 2024 00:03:24 +0800 Subject: [PATCH 21/43] fix: tooltip not updated after theme changed (#233) --- src/Resources/Styles.axaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 3710a8cd..f3aedd4b 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -160,6 +160,11 @@ + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - + + + + + + + + + + - + + + + + - + + + + + + - - + - - - - - - - - + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - + - + + + + + - + + + + + + + + - - + + + + + + + - - - - - - - - + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 43e9a4a6..a7ba1a33 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; +using AvaloniaEdit.Utils; namespace SourceGit.Views { @@ -280,18 +281,6 @@ namespace SourceGit.Views e.Handled = true; } - private void OnTagPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == DataGrid.ItemsSourceProperty && DataContext is ViewModels.Repository vm) - { - if (vm.VisibleTags == null) - return; - - var desiredHeight = tagsList.RowHeight * vm.VisibleTags.Count; - tagsList.Height = Math.Min(200, desiredHeight); - } - } - private void OnToggleFilter(object sender, RoutedEventArgs e) { if (sender is ToggleButton toggle) @@ -379,5 +368,81 @@ namespace SourceGit.Views outs.Add(b); } } + + private void OnDashboardLayoutUpdated(object sender, EventArgs e) + { + var vm = DataContext as ViewModels.Repository; + if (vm == null || vm.Settings == null) + return; + + var grid = sender as Grid; + if (grid == null) + return; + + var leftHeight = grid.Bounds.Height - 28.0 * 5; + if (vm.IsTagGroupExpanded) + { + var desiredHeight = Math.Min(200.0, tagsList.RowHeight * vm.VisibleTags.Count); + leftHeight -= desiredHeight; + if (!tagsList.Height.IsClose(desiredHeight)) + tagsList.Height = desiredHeight; + } + + if (vm.IsSubmoduleGroupExpanded) + { + var desiredHeight = Math.Min(200.0, submoduleList.RowHeight * vm.Submodules.Count); + leftHeight -= desiredHeight; + if (!submoduleList.Height.IsClose(desiredHeight)) + submoduleList.Height = desiredHeight; + } + + if (vm.IsWorktreeGroupExpanded) + { + var desiredHeight = Math.Min(200.0, worktreeList.RowHeight * vm.Worktrees.Count); + leftHeight -= desiredHeight; + if (!worktreeList.Height.IsClose(desiredHeight)) + worktreeList.Height = desiredHeight; + } + + var desiredLocalBranchHeight = GetTreeRowsCount(vm.LocalBranchTrees) * 24; + var desiredRemoteHeight = GetTreeRowsCount(vm.RemoteBranchTrees) * 24; + + if (!vm.IsRemoteGroupExpanded) + { + if (vm.IsLocalBranchGroupExpanded) + { + var localBranchHeight = Math.Min(leftHeight, desiredLocalBranchHeight); + localBranchTree.Height = localBranchHeight; + } + } + else + { + if (vm.IsLocalBranchGroupExpanded) + { + var localBranchHeight = Math.Min(leftHeight * 0.5, desiredLocalBranchHeight); + if (!localBranchTree.Height.IsClose(localBranchHeight)) + localBranchTree.Height = localBranchHeight; + + leftHeight -= localBranchHeight; + } + + var remoteHeight = Math.Min(leftHeight, desiredRemoteHeight); + if (!remoteBranchTree.Height.IsClose(remoteHeight)) + remoteBranchTree.Height = remoteHeight; + } + } + + private int GetTreeRowsCount(List nodes) + { + int count = nodes.Count; + + foreach (var node in nodes) + { + if (!node.IsBranch && node.IsExpanded) + count += GetTreeRowsCount(node.Children); + } + + return count; + } } } From 6553e27d368f4eb018ce5541810454d83f41396b Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 4 Jul 2024 18:11:10 +0800 Subject: [PATCH 30/43] refactor: rewrite the way to get local & remote branch tree height --- src/Views/Repository.axaml.cs | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index a7ba1a33..012eca75 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -404,29 +404,20 @@ namespace SourceGit.Views worktreeList.Height = desiredHeight; } - var desiredLocalBranchHeight = GetTreeRowsCount(vm.LocalBranchTrees) * 24; - var desiredRemoteHeight = GetTreeRowsCount(vm.RemoteBranchTrees) * 24; - - if (!vm.IsRemoteGroupExpanded) + if (vm.IsLocalBranchGroupExpanded) { - if (vm.IsLocalBranchGroupExpanded) - { - var localBranchHeight = Math.Min(leftHeight, desiredLocalBranchHeight); + var localBranchMax = vm.IsRemoteGroupExpanded ? leftHeight * 0.5 : leftHeight; + var desiredHeight = GetTreeRowsCount(vm.LocalBranchTrees) * 24; + var localBranchHeight = Math.Min(localBranchMax, desiredHeight); + if (!localBranchTree.Height.IsClose(localBranchHeight)) localBranchTree.Height = localBranchHeight; - } + leftHeight -= localBranchHeight; } - else + + if (vm.IsRemoteGroupExpanded) { - if (vm.IsLocalBranchGroupExpanded) - { - var localBranchHeight = Math.Min(leftHeight * 0.5, desiredLocalBranchHeight); - if (!localBranchTree.Height.IsClose(localBranchHeight)) - localBranchTree.Height = localBranchHeight; - - leftHeight -= localBranchHeight; - } - - var remoteHeight = Math.Min(leftHeight, desiredRemoteHeight); + var desiredHeight = GetTreeRowsCount(vm.RemoteBranchTrees) * 24; + var remoteHeight = Math.Min(leftHeight, desiredHeight); if (!remoteBranchTree.Height.IsClose(remoteHeight)) remoteBranchTree.Height = remoteHeight; } From c03cc85232e1a5952f6c5be2ede7fdfe3fccd94b Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 4 Jul 2024 18:18:35 +0800 Subject: [PATCH 31/43] enhance: skip update layout after detached from visual tree --- src/Views/Repository.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 012eca75..28c493a4 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -376,7 +376,7 @@ namespace SourceGit.Views return; var grid = sender as Grid; - if (grid == null) + if (grid == null || !grid.IsAttachedToVisualTree()) return; var leftHeight = grid.Bounds.Height - 28.0 * 5; From f904b3b71aa35389d524d2680e8320ffacf43ddf Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 4 Jul 2024 19:12:22 +0800 Subject: [PATCH 32/43] enhance: improve performance --- src/Views/Repository.axaml | 13 +++++++--- src/Views/Repository.axaml.cs | 48 ++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index a18f8b3c..feb3881a 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -231,7 +231,7 @@ - + @@ -245,7 +245,8 @@ ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto" ContextRequested="OnLocalBranchContextMenuRequested" - SelectionChanged="OnLocalBranchTreeSelectionChanged"> + SelectionChanged="OnLocalBranchTreeSelectionChanged" + PropertyChanged="OnLeftSidebarTreeViewPropertyChanged"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs new file mode 100644 index 00000000..deab507c --- /dev/null +++ b/src/Views/BranchTree.axaml.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class BranchTreeNodeIcon : UserControl + { + public static readonly StyledProperty NodeProperty = + AvaloniaProperty.Register(nameof(Node), null); + + public ViewModels.BranchTreeNode Node + { + get => GetValue(NodeProperty); + set => SetValue(NodeProperty, value); + } + + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register(nameof(IsExpanded), false); + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + static BranchTreeNodeIcon() + { + NodeProperty.Changed.AddClassHandler((icon, e) => icon.UpdateContent()); + IsExpandedProperty.Changed.AddClassHandler((icon, e) => icon.UpdateContent()); + } + + private void UpdateContent() + { + var node = Node; + if (node == null) + { + Content = null; + return; + } + + if (node.Backend is Models.Remote) + { + CreateContent(12, new Thickness(0,2,0,0), "Icons.Remote"); + } + else if (node.Backend is Models.Branch branch) + { + if (branch.IsCurrent) + CreateContent(12, new Thickness(0,2,0,0), "Icons.Check"); + else + CreateContent(12, new Thickness(2,0,0,0), "Icons.Branch"); + } + else + { + if (node.IsExpanded) + CreateContent(10, new Thickness(0,2,0,0), "Icons.Folder.Open"); + else + CreateContent(10, new Thickness(0,2,0,0), "Icons.Folder.Fill"); + } + } + + private void CreateContent(double size, Thickness margin, string iconKey) + { + var geo = this.FindResource(iconKey) as StreamGeometry; + if (geo == null) + return; + + Content = new Path() + { + Width = size, + Height = size, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = margin, + Data = geo, + }; + } + } + + public partial class BranchTree : UserControl + { + public static readonly StyledProperty> NodesProperty = + AvaloniaProperty.Register>(nameof(Nodes), null); + + public List Nodes + { + get => GetValue(NodesProperty); + set => SetValue(NodesProperty, value); + } + + public AvaloniaList Rows + { + get; + private set; + } = new AvaloniaList(); + + public static readonly RoutedEvent SelectionChangedEvent = + RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler SelectionChanged + { + add { AddHandler(SelectionChangedEvent, value); } + remove { RemoveHandler(SelectionChangedEvent, value); } + } + + public BranchTree() + { + InitializeComponent(); + } + + public void UnselectAll() + { + BranchesPresenter.SelectedItem = null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == NodesProperty) + { + Rows.Clear(); + + if (Nodes is { Count: > 0 }) + { + var rows = new List(); + MakeRows(rows, Nodes, 0); + Rows.AddRange(rows); + } + + var repo = this.FindAncestorOfType(); + repo?.UpdateLeftSidebarLayout(); + } + else if (change.Property == IsVisibleProperty) + { + var repo = this.FindAncestorOfType(); + repo?.UpdateLeftSidebarLayout(); + } + } + + private void OnNodesSelectionChanged(object sender, SelectionChangedEventArgs e) + { + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + var set = new HashSet(); + foreach (var item in selected) + { + if (item is ViewModels.BranchTreeNode node) + set.Add(node); + } + + var prev = null as ViewModels.BranchTreeNode; + var isPrevSelected = false; + foreach (var row in Rows) + { + var isSelected = set.Contains(row); + if (isSelected) + { + if (isPrevSelected) + { + var prevTop = prev.CornerRadius.TopLeft; + prev.CornerRadius = new CornerRadius(prevTop, 0); + row.CornerRadius = new CornerRadius(0, 4); + } + else + { + row.CornerRadius = new CornerRadius(4); + } + } + + isPrevSelected = isSelected; + prev = row; + } + + RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); + } + + private void OnTreeContextRequested(object sender, ContextRequestedEventArgs e) + { + var repo = DataContext as ViewModels.Repository; + if (repo?.Settings == null) + return; + + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) + { + var menu = repo.CreateContextMenuForRemote(remote); + this.OpenContextMenu(menu); + return; + } + + var branches = new List(); + foreach (var item in selected) + { + if (item is ViewModels.BranchTreeNode node) + CollectBranchesInNode(branches, node); + } + + if (branches.Count == 1) + { + var branch = branches[0]; + var menu = branch.IsLocal ? + repo.CreateContextMenuForLocalBranch(branch) : + repo.CreateContextMenuForRemoteBranch(branch); + this.OpenContextMenu(menu); + } + else if (branches.Find(x => x.IsCurrent) == null) + { + var menu = new ContextMenu(); + var deleteMulti = new MenuItem(); + deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); + deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); + deleteMulti.Click += (_, ev) => + { + repo.DeleteMultipleBranches(branches, branches[0].IsLocal); + ev.Handled = true; + }; + menu.Items.Add(deleteMulti); + this.OpenContextMenu(menu); + } + } + + private void OnDoubleTappedBranchNode(object sender, TappedEventArgs e) + { + if (sender is Grid { DataContext: ViewModels.BranchTreeNode node }) + { + if (node.Backend is Models.Branch branch) + { + if (branch.IsCurrent) + return; + + if (DataContext is ViewModels.Repository { Settings: not null } repo) + repo.CheckoutBranch(branch); + } + else + { + node.IsExpanded = !node.IsExpanded; + + var rows = Rows; + var depth = node.Depth; + var idx = rows.IndexOf(node); + if (idx == -1) + return; + + if (node.IsExpanded) + { + var subtree = new List(); + MakeRows(subtree, node.Children, depth + 1); + rows.InsertRange(idx + 1, subtree); + } + else + { + var removeCount = 0; + for (int i = idx + 1; i < rows.Count; i++) + { + var row = rows[i]; + if (row.Depth <= depth) + break; + + removeCount++; + } + rows.RemoveRange(idx + 1, removeCount); + } + + var repo = this.FindAncestorOfType(); + repo?.UpdateLeftSidebarLayout(); + } + } + } + + private void OnToggleFilter(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo) + { + if (toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + repo.UpdateFilter(branch.FullName, toggle.IsChecked == true); + } + + e.Handled = true; + } + + private void MakeRows(List rows, List nodes, int depth) + { + foreach (var node in nodes) + { + node.Depth = depth; + rows.Add(node); + + if (!node.IsExpanded || node.Backend is Models.Branch) + continue; + + MakeRows(rows, node.Children, depth + 1); + } + } + + private void CollectBranchesInNode(List outs, ViewModels.BranchTreeNode node) + { + if (node.Backend is Models.Branch branch && !outs.Contains(branch)) + { + outs.Add(branch); + return; + } + + foreach (var sub in node.Children) + CollectBranchesInNode(outs, sub); + } + } +} + diff --git a/src/Views/DeleteBranch.axaml b/src/Views/DeleteBranch.axaml index 07230de9..b2693bf0 100644 --- a/src/Views/DeleteBranch.axaml +++ b/src/Views/DeleteBranch.axaml @@ -2,10 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" - xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.DeleteBranch" x:DataType="vm:DeleteBranch"> @@ -18,7 +15,7 @@ - + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 2db2f166..2ffcc485 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -236,77 +236,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -317,64 +252,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - + @@ -455,8 +338,8 @@ diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 9046a375..f5612d70 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.VisualTree; -using AvaloniaEdit.Utils; namespace SourceGit.Views { @@ -18,393 +15,7 @@ namespace SourceGit.Views InitializeComponent(); } - protected override void OnLoaded(RoutedEventArgs e) - { - base.OnLoaded(e); - - if (DataContext is ViewModels.Repository repo && !repo.IsSearching) - { - UpdateLeftSidebarLayout(); - } - } - - private void OpenWithExternalTools(object sender, RoutedEventArgs e) - { - if (sender is Button button && DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForExternalTools(); - button.OpenContextMenu(menu); - e.Handled = true; - } - } - - private void OpenGitFlowMenu(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForGitFlow(); - (sender as Control)?.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OpenGitLFSMenu(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForGitLFS(); - (sender as Control)?.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private async void OpenStatistics(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; - await dialog.ShowDialog(TopLevel.GetTopLevel(this) as Window); - e.Handled = true; - } - } - - private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - var grid = sender as Grid; - if (e.Property == IsVisibleProperty && grid.IsVisible) - txtSearchCommitsBox.Focus(); - } - - private void OnSearchKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - if (DataContext is ViewModels.Repository repo) - repo.StartSearchCommits(); - - e.Handled = true; - } - } - - private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) - { - if (DataContext is ViewModels.Repository repo) - { - var commit = datagrid.SelectedItem as Models.Commit; - repo.NavigateToCommit(commit.SHA); - } - } - e.Handled = true; - } - - private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - remoteBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - ViewModels.BranchTreeNode prev = null; - foreach (var node in repo.LocalBranchTrees) - node.UpdateCornerRadius(ref prev); - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch) - repo.NavigateToCommit((node.Backend as Models.Branch).Head); - } - } - } - - private void OnRemoteBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - localBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - ViewModels.BranchTreeNode prev = null; - foreach (var node in repo.RemoteBranchTrees) - node.UpdateCornerRadius(ref prev); - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch) - repo.NavigateToCommit((node.Backend as Models.Branch).Head); - } - } - } - - private void OnLocalBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) - { - remoteBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - var repo = DataContext as ViewModels.Repository; - var tree = sender as TreeView; - if (tree.SelectedItems.Count == 0) - { - e.Handled = true; - return; - } - - var branches = new List(); - foreach (var item in tree.SelectedItems) - CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); - - if (branches.Count == 1) - { - var item = (e.Source as Control)?.FindAncestorOfType(true); - if (item != null) - { - var menu = repo.CreateContextMenuForLocalBranch(branches[0]); - item.OpenContextMenu(menu); - } - } - else if (branches.Count > 1 && branches.Find(x => x.IsCurrent) == null) - { - var menu = new ContextMenu(); - var deleteMulti = new MenuItem(); - deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); - deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); - deleteMulti.Click += (_, ev) => - { - repo.DeleteMultipleBranches(branches, true); - ev.Handled = true; - }; - menu.Items.Add(deleteMulti); - tree.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnRemoteBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) - { - localBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - var repo = DataContext as ViewModels.Repository; - var tree = sender as TreeView; - if (tree.SelectedItems.Count == 0) - { - e.Handled = true; - return; - } - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node != null && node.IsRemote) - { - var item = (e.Source as Control)?.FindAncestorOfType(true); - if (item != null && item.DataContext == node) - { - var menu = repo.CreateContextMenuForRemote(node.Backend as Models.Remote); - item.OpenContextMenu(menu); - } - - e.Handled = true; - return; - } - } - - var branches = new List(); - foreach (var item in tree.SelectedItems) - CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); - - if (branches.Count == 1) - { - var item = (e.Source as Control)?.FindAncestorOfType(true); - if (item != null) - { - var menu = repo.CreateContextMenuForRemoteBranch(branches[0]); - item.OpenContextMenu(menu); - } - } - else if (branches.Count > 1) - { - var menu = new ContextMenu(); - var deleteMulti = new MenuItem(); - deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); - deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); - deleteMulti.Click += (_, ev) => - { - repo.DeleteMultipleBranches(branches, false); - ev.Handled = true; - }; - menu.Items.Add(deleteMulti); - tree.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnDoubleTappedBranchNode(object sender, TappedEventArgs e) - { - if (!ViewModels.PopupHost.CanCreatePopup()) - return; - - if (sender is Grid grid && DataContext is ViewModels.Repository repo) - { - var node = grid.DataContext as ViewModels.BranchTreeNode; - if (node == null) - return; - - if (node.IsBranch) - { - var branch = node.Backend as Models.Branch; - if (branch.IsCurrent) - return; - - repo.CheckoutBranch(branch); - } - else - { - node.IsExpanded = !node.IsExpanded; - UpdateLeftSidebarLayout(); - } - - e.Handled = true; - } - } - - private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) - { - localBranchTree.UnselectAll(); - remoteBranchTree.UnselectAll(); - - var tag = datagrid.SelectedItem as Models.Tag; - if (DataContext is ViewModels.Repository repo) - repo.NavigateToCommit(tag.SHA); - } - } - - private void OnTagContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var tag = datagrid.SelectedItem as Models.Tag; - var menu = repo.CreateContextMenuForTag(tag); - datagrid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnToggleFilter(object sender, RoutedEventArgs e) - { - if (sender is ToggleButton toggle) - { - var filter = string.Empty; - if (toggle.DataContext is ViewModels.BranchTreeNode node) - { - if (node.IsBranch) - filter = (node.Backend as Models.Branch).FullName; - } - else if (toggle.DataContext is Models.Tag tag) - { - filter = tag.Name; - } - - if (!string.IsNullOrEmpty(filter) && DataContext is ViewModels.Repository repo) - { - repo.UpdateFilter(filter, toggle.IsChecked == true); - } - } - - e.Handled = true; - } - - private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var submodule = datagrid.SelectedItem as string; - var menu = repo.CreateContextMenuForSubmodule(submodule); - datagrid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var submodule = datagrid.SelectedItem as string; - (DataContext as ViewModels.Repository).OpenSubmodule(submodule); - } - - e.Handled = true; - } - - private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var worktree = datagrid.SelectedItem as Models.Worktree; - var menu = repo.CreateContextMenuForWorktree(worktree); - datagrid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnDoubleTappedWorktree(object sender, TappedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var worktree = datagrid.SelectedItem as Models.Worktree; - (DataContext as ViewModels.Repository).OpenWorktree(worktree); - } - - e.Handled = true; - } - - private void CollectBranchesFromNode(List outs, ViewModels.BranchTreeNode node) - { - if (node == null || node.IsRemote) - return; - - if (node.IsFolder) - { - foreach (var child in node.Children) - CollectBranchesFromNode(outs, child); - } - else - { - var b = node.Backend as Models.Branch; - if (b != null && !outs.Contains(b)) - outs.Add(b); - } - } - - private void OnLeftSidebarTreeViewPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == TreeView.ItemsSourceProperty || e.Property == TreeView.IsVisibleProperty) - { - UpdateLeftSidebarLayout(); - } - } - - private void OnLeftSidebarDataGridPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == DataGrid.ItemsSourceProperty || e.Property == DataGrid.IsVisibleProperty) - { - UpdateLeftSidebarLayout(); - } - } - - private void UpdateLeftSidebarLayout() + public void UpdateLeftSidebarLayout() { var vm = DataContext as ViewModels.Repository; if (vm == null || vm.Settings == null) @@ -414,8 +25,8 @@ namespace SourceGit.Views return; var leftHeight = leftSidebarGroups.Bounds.Height - 28.0 * 5; - var localBranchRows = vm.IsLocalBranchGroupExpanded ? GetTreeRowsCount(vm.LocalBranchTrees) : 0; - var remoteBranchRows = vm.IsRemoteGroupExpanded ? GetTreeRowsCount(vm.RemoteBranchTrees) : 0; + var localBranchRows = vm.IsLocalBranchGroupExpanded ? localBranchTree.Rows.Count : 0; + var remoteBranchRows = vm.IsRemoteGroupExpanded ? remoteBranchTree.Rows.Count : 0; var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; var desiredTag = vm.IsTagGroupExpanded ? tagsList.RowHeight * vm.VisibleTags.Count : 0; var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? submoduleList.RowHeight * vm.Submodules.Count : 0; @@ -524,17 +135,212 @@ namespace SourceGit.Views } } - private int GetTreeRowsCount(List nodes) + protected override void OnLoaded(RoutedEventArgs e) { - int count = nodes.Count; + base.OnLoaded(e); - foreach (var node in nodes) + if (DataContext is ViewModels.Repository { IsSearching: false }) + UpdateLeftSidebarLayout(); + } + + private void OpenWithExternalTools(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) { - if (!node.IsBranch && node.IsExpanded) - count += GetTreeRowsCount(node.Children); + var menu = repo.CreateContextMenuForExternalTools(); + button.OpenContextMenu(menu); + e.Handled = true; } + } + + private void OpenGitFlowMenu(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForGitFlow(); + (sender as Control)?.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OpenGitLFSMenu(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForGitLFS(); + (sender as Control)?.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private async void OpenStatistics(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner) + { + var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; + await dialog.ShowDialog(owner); + e.Handled = true; + } + } + + private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true} grid) + txtSearchCommitsBox.Focus(); + } + + private void OnSearchKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + if (DataContext is ViewModels.Repository repo) + repo.StartSearchCommits(); + + e.Handled = true; + } + } + + private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + { + var commit = grid.SelectedItem as Models.Commit; + repo.NavigateToCommit(commit.SHA); + } + + e.Handled = true; + } + + private void OnLocalBranchTreeSelectionChanged(object sender, RoutedEventArgs e) + { + if (sender is BranchTree tree && DataContext is ViewModels.Repository repo) + { + var selected = tree.BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; - return count; + remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + repo.NavigateToCommit(branch.Head); + } + } + + private void OnRemoteBranchTreeSelectionChanged(object sender, RoutedEventArgs e) + { + if (sender is BranchTree tree && DataContext is ViewModels.Repository repo) + { + var selected = tree.BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + localBranchTree.UnselectAll(); + tagsList.SelectedItem = null; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + repo.NavigateToCommit(branch.Head); + } + } + + private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid) + { + localBranchTree.UnselectAll(); + remoteBranchTree.UnselectAll(); + + var tag = grid.SelectedItem as Models.Tag; + if (DataContext is ViewModels.Repository repo) + repo.NavigateToCommit(tag.SHA); + } + } + + private void OnTagContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + { + var tag = datagrid.SelectedItem as Models.Tag; + var menu = repo.CreateContextMenuForTag(tag); + datagrid.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnToggleTagFilter(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton toggle) + { + var filter = string.Empty; + if (toggle.DataContext is Models.Tag tag) + { + filter = tag.Name; + } + + if (!string.IsNullOrEmpty(filter) && DataContext is ViewModels.Repository repo) + { + repo.UpdateFilter(filter, toggle.IsChecked == true); + } + } + + e.Handled = true; + } + + private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + { + var submodule = datagrid.SelectedItem as string; + var menu = repo.CreateContextMenuForSubmodule(submodule); + datagrid.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + { + var submodule = grid.SelectedItem as string; + repo.OpenSubmodule(submodule); + } + + e.Handled = true; + } + + private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + { + var worktree = grid.SelectedItem as Models.Worktree; + var menu = repo.CreateContextMenuForWorktree(worktree); + grid.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnDoubleTappedWorktree(object sender, TappedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + { + var worktree = grid.SelectedItem as Models.Worktree; + repo.OpenWorktree(worktree); + } + + e.Handled = true; + } + + private void OnLeftSidebarDataGridPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == DataGrid.ItemsSourceProperty || e.Property == DataGrid.IsVisibleProperty) + { + UpdateLeftSidebarLayout(); + } } } } From 36b8472d023b3e6c3bc9f2739dbb091690ada4d9 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 6 Jul 2024 17:29:24 +0800 Subject: [PATCH 37/43] enhance: improve selection changed event handler performance --- src/ViewModels/BranchTreeNode.cs | 1 + src/Views/BranchTree.axaml.cs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index 63848b9f..a29e6a2c 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -16,6 +16,7 @@ namespace SourceGit.ViewModels public object Backend { get; private set; } = null; public int Depth { get; set; } = 0; public bool IsFiltered { get; set; } = false; + public bool IsSelected { get; set; } = false; public List Children { get; private set; } = new List(); public bool IsExpanded diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index deab507c..4fe4fce5 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -154,21 +154,24 @@ namespace SourceGit.Views if (selected == null || selected.Count == 0) return; - var set = new HashSet(); - foreach (var item in selected) + foreach (var item in e.AddedItems) { if (item is ViewModels.BranchTreeNode node) - set.Add(node); + node.IsSelected = true; + } + + foreach (var item in e.RemovedItems) + { + if (item is ViewModels.BranchTreeNode node) + node.IsSelected = false; } var prev = null as ViewModels.BranchTreeNode; - var isPrevSelected = false; foreach (var row in Rows) { - var isSelected = set.Contains(row); - if (isSelected) + if (row.IsSelected) { - if (isPrevSelected) + if (prev is { IsSelected: true }) { var prevTop = prev.CornerRadius.TopLeft; prev.CornerRadius = new CornerRadius(prevTop, 0); @@ -180,7 +183,6 @@ namespace SourceGit.Views } } - isPrevSelected = isSelected; prev = row; } From b03ee19e5481f6c7f94dcf2a58c7f248cde9c5d3 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 6 Jul 2024 20:46:26 +0800 Subject: [PATCH 38/43] refactor: trigger nagivation in BranchTree --- src/Views/BranchTree.axaml.cs | 30 ++++++++++------- src/Views/Repository.axaml.cs | 61 +++++++++-------------------------- 2 files changed, 33 insertions(+), 58 deletions(-) diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index 4fe4fce5..cb0d1c58 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -17,7 +17,7 @@ namespace SourceGit.Views public class BranchTreeNodeIcon : UserControl { public static readonly StyledProperty NodeProperty = - AvaloniaProperty.Register(nameof(Node), null); + AvaloniaProperty.Register(nameof(Node)); public ViewModels.BranchTreeNode Node { @@ -26,7 +26,7 @@ namespace SourceGit.Views } public static readonly StyledProperty IsExpandedProperty = - AvaloniaProperty.Register(nameof(IsExpanded), false); + AvaloniaProperty.Register(nameof(IsExpanded)); public bool IsExpanded { @@ -36,8 +36,8 @@ namespace SourceGit.Views static BranchTreeNodeIcon() { - NodeProperty.Changed.AddClassHandler((icon, e) => icon.UpdateContent()); - IsExpandedProperty.Changed.AddClassHandler((icon, e) => icon.UpdateContent()); + NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); } private void UpdateContent() @@ -90,7 +90,7 @@ namespace SourceGit.Views public partial class BranchTree : UserControl { public static readonly StyledProperty> NodesProperty = - AvaloniaProperty.Register>(nameof(Nodes), null); + AvaloniaProperty.Register>(nameof(Nodes)); public List Nodes { @@ -148,12 +148,8 @@ namespace SourceGit.Views } } - private void OnNodesSelectionChanged(object sender, SelectionChangedEventArgs e) + private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e) { - var selected = BranchesPresenter.SelectedItems; - if (selected == null || selected.Count == 0) - return; - foreach (var item in e.AddedItems) { if (item is ViewModels.BranchTreeNode node) @@ -165,6 +161,16 @@ namespace SourceGit.Views if (item is ViewModels.BranchTreeNode node) node.IsSelected = false; } + + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + { + var repo = DataContext as ViewModels.Repository; + repo?.NavigateToCommit(branch.Head); + } var prev = null as ViewModels.BranchTreeNode; foreach (var row in Rows) @@ -189,7 +195,7 @@ namespace SourceGit.Views RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); } - private void OnTreeContextRequested(object sender, ContextRequestedEventArgs e) + private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2) { var repo = DataContext as ViewModels.Repository; if (repo?.Settings == null) @@ -237,7 +243,7 @@ namespace SourceGit.Views } } - private void OnDoubleTappedBranchNode(object sender, TappedEventArgs e) + private void OnDoubleTappedBranchNode(object sender, TappedEventArgs _) { if (sender is Grid { DataContext: ViewModels.BranchTreeNode node }) { diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index f5612d70..e2ffc0d6 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -175,7 +175,7 @@ namespace SourceGit.Views e.Handled = true; } - private async void OpenStatistics(object sender, RoutedEventArgs e) + private async void OpenStatistics(object _, RoutedEventArgs e) { if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner) { @@ -187,11 +187,11 @@ namespace SourceGit.Views private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { - if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true} grid) + if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true}) txtSearchCommitsBox.Focus(); } - private void OnSearchKeyDown(object sender, KeyEventArgs e) + private void OnSearchKeyDown(object _, KeyEventArgs e) { if (e.Key == Key.Enter) { @@ -204,55 +204,33 @@ namespace SourceGit.Views private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) { - if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: Models.Commit commit } && DataContext is ViewModels.Repository repo) { - var commit = grid.SelectedItem as Models.Commit; repo.NavigateToCommit(commit.SHA); } e.Handled = true; } - private void OnLocalBranchTreeSelectionChanged(object sender, RoutedEventArgs e) + private void OnLocalBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { - if (sender is BranchTree tree && DataContext is ViewModels.Repository repo) - { - var selected = tree.BranchesPresenter.SelectedItems; - if (selected == null || selected.Count == 0) - return; - - remoteBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) - repo.NavigateToCommit(branch.Head); - } + remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; } - private void OnRemoteBranchTreeSelectionChanged(object sender, RoutedEventArgs e) + private void OnRemoteBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { - if (sender is BranchTree tree && DataContext is ViewModels.Repository repo) - { - var selected = tree.BranchesPresenter.SelectedItems; - if (selected == null || selected.Count == 0) - return; - - localBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) - repo.NavigateToCommit(branch.Head); - } + localBranchTree.UnselectAll(); + tagsList.SelectedItem = null; } - private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs _) { - if (sender is DataGrid { SelectedItem: not null } grid) + if (sender is DataGrid { SelectedItem: Models.Tag tag }) { localBranchTree.UnselectAll(); remoteBranchTree.UnselectAll(); - var tag = grid.SelectedItem as Models.Tag; if (DataContext is ViewModels.Repository repo) repo.NavigateToCommit(tag.SHA); } @@ -272,18 +250,9 @@ namespace SourceGit.Views private void OnToggleTagFilter(object sender, RoutedEventArgs e) { - if (sender is ToggleButton toggle) + if (sender is ToggleButton { DataContext: Models.Tag tag } toggle && DataContext is ViewModels.Repository repo) { - var filter = string.Empty; - if (toggle.DataContext is Models.Tag tag) - { - filter = tag.Name; - } - - if (!string.IsNullOrEmpty(filter) && DataContext is ViewModels.Repository repo) - { - repo.UpdateFilter(filter, toggle.IsChecked == true); - } + repo.UpdateFilter(tag.Name, toggle.IsChecked == true); } e.Handled = true; @@ -335,7 +304,7 @@ namespace SourceGit.Views e.Handled = true; } - private void OnLeftSidebarDataGridPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void OnLeftSidebarDataGridPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e) { if (e.Property == DataGrid.ItemsSourceProperty || e.Property == DataGrid.IsVisibleProperty) { From 294f856150d69fbc11b5d010f518b1e4e68489e4 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 6 Jul 2024 23:50:54 +0800 Subject: [PATCH 39/43] fix: bounds not updated after left side bar layout updated --- src/Views/BranchTree.axaml | 32 ++--- src/Views/BranchTree.axaml.cs | 35 +++-- src/Views/Repository.axaml | 6 +- src/Views/Repository.axaml.cs | 254 +++++++++++++++++----------------- 4 files changed, 174 insertions(+), 153 deletions(-) diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml index 3ccadbc9..66eca538 100644 --- a/src/Views/BranchTree.axaml +++ b/src/Views/BranchTree.axaml @@ -10,13 +10,11 @@ x:Name="ThisControl"> - + - + - - + - + - diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index cb0d1c58..95e70f48 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -113,6 +113,15 @@ namespace SourceGit.Views remove { RemoveHandler(SelectionChangedEvent, value); } } + public static readonly RoutedEvent RowsChangedEvent = + RoutedEvent.Register(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler RowsChanged + { + add { AddHandler(RowsChangedEvent, value); } + remove { RemoveHandler(RowsChangedEvent, value); } + } + public BranchTree() { InitializeComponent(); @@ -123,6 +132,14 @@ namespace SourceGit.Views BranchesPresenter.SelectedItem = null; } + protected override void OnSizeChanged(SizeChangedEventArgs e) + { + base.OnSizeChanged(e); + + if (Bounds.Height >= 23.0) + BranchesPresenter.Height = Bounds.Height; + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -138,18 +155,20 @@ namespace SourceGit.Views Rows.AddRange(rows); } - var repo = this.FindAncestorOfType(); - repo?.UpdateLeftSidebarLayout(); + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } else if (change.Property == IsVisibleProperty) { - var repo = this.FindAncestorOfType(); - repo?.UpdateLeftSidebarLayout(); + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } } private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e) { + var repo = DataContext as ViewModels.Repository; + if (repo?.Settings == null) + return; + foreach (var item in e.AddedItems) { if (item is ViewModels.BranchTreeNode node) @@ -167,10 +186,7 @@ namespace SourceGit.Views return; if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) - { - var repo = DataContext as ViewModels.Repository; - repo?.NavigateToCommit(branch.Head); - } + repo.NavigateToCommit(branch.Head); var prev = null as ViewModels.BranchTreeNode; foreach (var row in Rows) @@ -285,8 +301,7 @@ namespace SourceGit.Views rows.RemoveRange(idx + 1, removeCount); } - var repo = this.FindAncestorOfType(); - repo?.UpdateLeftSidebarLayout(); + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } } } diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 2ffcc485..2c3ecbe2 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -241,7 +241,8 @@ Margin="8,0,4,0" Nodes="{Binding LocalBranchTrees}" IsVisible="{Binding IsLocalBranchGroupExpanded}" - SelectionChanged="OnLocalBranchTreeSelectionChanged"/> + SelectionChanged="OnLocalBranchTreeSelectionChanged" + RowsChanged="OnBranchTreeRowsChanged"/> @@ -257,7 +258,8 @@ Margin="8,0,4,0" Nodes="{Binding RemoteBranchTrees}" IsVisible="{Binding IsRemoteGroupExpanded}" - SelectionChanged="OnRemoteBranchTreeSelectionChanged"/> + SelectionChanged="OnRemoteBranchTreeSelectionChanged" + RowsChanged="OnBranchTreeRowsChanged"/> diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index e2ffc0d6..0e3d7a19 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -15,132 +15,10 @@ namespace SourceGit.Views InitializeComponent(); } - public void UpdateLeftSidebarLayout() - { - var vm = DataContext as ViewModels.Repository; - if (vm == null || vm.Settings == null) - return; - - if (!IsLoaded) - return; - - var leftHeight = leftSidebarGroups.Bounds.Height - 28.0 * 5; - var localBranchRows = vm.IsLocalBranchGroupExpanded ? localBranchTree.Rows.Count : 0; - var remoteBranchRows = vm.IsRemoteGroupExpanded ? remoteBranchTree.Rows.Count : 0; - var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; - var desiredTag = vm.IsTagGroupExpanded ? tagsList.RowHeight * vm.VisibleTags.Count : 0; - var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? submoduleList.RowHeight * vm.Submodules.Count : 0; - var desiredWorktree = vm.IsWorktreeGroupExpanded ? worktreeList.RowHeight * vm.Worktrees.Count : 0; - var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree; - var hasOverflow = (desiredBranches + desiredOthers > leftHeight); - - if (vm.IsTagGroupExpanded) - { - var height = desiredTag; - if (hasOverflow) - { - var test = leftHeight - desiredBranches - desiredSubmodule - desiredWorktree; - if (test < 0) - height = Math.Min(200, height); - else - height = Math.Max(200, test); - } - - leftHeight -= height; - tagsList.Height = height; - hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight; - } - - if (vm.IsSubmoduleGroupExpanded) - { - var height = desiredSubmodule; - if (hasOverflow) - { - var test = leftHeight - desiredBranches - desiredWorktree; - if (test < 0) - height = Math.Min(200, height); - else - height = Math.Max(200, test); - } - - leftHeight -= height; - submoduleList.Height = height; - hasOverflow = (desiredBranches + desiredWorktree) > leftHeight; - } - - if (vm.IsWorktreeGroupExpanded) - { - var height = desiredWorktree; - if (hasOverflow) - { - var test = leftHeight - desiredBranches; - if (test < 0) - height = Math.Min(200, height); - else - height = Math.Max(200, test); - } - - leftHeight -= height; - worktreeList.Height = height; - } - - if (desiredBranches > leftHeight) - { - var local = localBranchRows * 24.0; - var remote = remoteBranchRows * 24.0; - var half = leftHeight / 2; - if (vm.IsLocalBranchGroupExpanded) - { - if (vm.IsRemoteGroupExpanded) - { - if (local < half) - { - localBranchTree.Height = local; - remoteBranchTree.Height = leftHeight - local; - } - else if (remote < half) - { - remoteBranchTree.Height = remote; - localBranchTree.Height = leftHeight - remote; - } - else - { - localBranchTree.Height = half; - remoteBranchTree.Height = half; - } - } - else - { - localBranchTree.Height = leftHeight; - } - } - else if (vm.IsRemoteGroupExpanded) - { - remoteBranchTree.Height = leftHeight; - } - } - else - { - if (vm.IsLocalBranchGroupExpanded) - { - var height = localBranchRows * 24; - localBranchTree.Height = height; - } - - if (vm.IsRemoteGroupExpanded) - { - var height = remoteBranchRows * 24; - remoteBranchTree.Height = height; - } - } - } - protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); - - if (DataContext is ViewModels.Repository { IsSearching: false }) - UpdateLeftSidebarLayout(); + UpdateLeftSidebarLayout(); } private void OpenWithExternalTools(object sender, RoutedEventArgs e) @@ -211,7 +89,13 @@ namespace SourceGit.Views e.Handled = true; } - + + private void OnBranchTreeRowsChanged(object _, RoutedEventArgs e) + { + UpdateLeftSidebarLayout(); + e.Handled = true; + } + private void OnLocalBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { remoteBranchTree.UnselectAll(); @@ -311,5 +195,127 @@ namespace SourceGit.Views UpdateLeftSidebarLayout(); } } + + private void UpdateLeftSidebarLayout() + { + var vm = DataContext as ViewModels.Repository; + if (vm == null || vm.Settings == null) + return; + + if (!IsLoaded) + return; + + var leftHeight = leftSidebarGroups.Bounds.Height - 28.0 * 5; + var localBranchRows = vm.IsLocalBranchGroupExpanded ? localBranchTree.Rows.Count : 0; + var remoteBranchRows = vm.IsRemoteGroupExpanded ? remoteBranchTree.Rows.Count : 0; + var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; + var desiredTag = vm.IsTagGroupExpanded ? tagsList.RowHeight * vm.VisibleTags.Count : 0; + var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? submoduleList.RowHeight * vm.Submodules.Count : 0; + var desiredWorktree = vm.IsWorktreeGroupExpanded ? worktreeList.RowHeight * vm.Worktrees.Count : 0; + var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree; + var hasOverflow = (desiredBranches + desiredOthers > leftHeight); + + if (vm.IsTagGroupExpanded) + { + var height = desiredTag; + if (hasOverflow) + { + var test = leftHeight - desiredBranches - desiredSubmodule - desiredWorktree; + if (test < 0) + height = Math.Min(200, height); + else + height = Math.Max(200, test); + } + + leftHeight -= height; + tagsList.Height = height; + hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight; + } + + if (vm.IsSubmoduleGroupExpanded) + { + var height = desiredSubmodule; + if (hasOverflow) + { + var test = leftHeight - desiredBranches - desiredWorktree; + if (test < 0) + height = Math.Min(200, height); + else + height = Math.Max(200, test); + } + + leftHeight -= height; + submoduleList.Height = height; + hasOverflow = (desiredBranches + desiredWorktree) > leftHeight; + } + + if (vm.IsWorktreeGroupExpanded) + { + var height = desiredWorktree; + if (hasOverflow) + { + var test = leftHeight - desiredBranches; + if (test < 0) + height = Math.Min(200, height); + else + height = Math.Max(200, test); + } + + leftHeight -= height; + worktreeList.Height = height; + } + + if (desiredBranches > leftHeight) + { + var local = localBranchRows * 24.0; + var remote = remoteBranchRows * 24.0; + var half = leftHeight / 2; + if (vm.IsLocalBranchGroupExpanded) + { + if (vm.IsRemoteGroupExpanded) + { + if (local < half) + { + localBranchTree.Height = local; + remoteBranchTree.Height = leftHeight - local; + } + else if (remote < half) + { + remoteBranchTree.Height = remote; + localBranchTree.Height = leftHeight - remote; + } + else + { + localBranchTree.Height = half; + remoteBranchTree.Height = half; + } + } + else + { + localBranchTree.Height = leftHeight; + } + } + else if (vm.IsRemoteGroupExpanded) + { + remoteBranchTree.Height = leftHeight; + } + } + else + { + if (vm.IsLocalBranchGroupExpanded) + { + var height = localBranchRows * 24; + localBranchTree.Height = height; + } + + if (vm.IsRemoteGroupExpanded) + { + var height = remoteBranchRows * 24; + remoteBranchTree.Height = height; + } + } + + leftSidebarGroups.InvalidateMeasure(); + } } } From 1d7b77e45fdf603a888abd03a116a6ac013d71de Mon Sep 17 00:00:00 2001 From: leo Date: Sun, 7 Jul 2024 00:04:39 +0800 Subject: [PATCH 40/43] fix: virtualization not working --- src/Views/Repository.axaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 2c3ecbe2..569709a6 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -238,6 +238,7 @@ Date: Sun, 7 Jul 2024 09:56:22 +0800 Subject: [PATCH 41/43] enhance: supports virtualization in sub context menu --- src/Resources/Styles.axaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index b19709e7..139e5bd5 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -772,6 +772,7 @@ + Grid.IsSharedSizeScope="True"> + + + + + + From 09b418374a2f55fc07f820a01d4a4afd21829a15 Mon Sep 17 00:00:00 2001 From: leo Date: Sun, 7 Jul 2024 10:28:14 +0800 Subject: [PATCH 42/43] feature: add an option to enable `-a,--all` in commit command (#244) --- src/Commands/Commit.cs | 4 +++- src/Resources/Locales/en_US.axaml | 2 ++ src/Resources/Locales/zh_CN.axaml | 2 ++ src/Resources/Locales/zh_TW.axaml | 2 ++ src/ViewModels/Repository.cs | 6 ++++++ src/ViewModels/Reword.cs | 2 +- src/ViewModels/Squash.cs | 2 +- src/ViewModels/WorkingCopy.cs | 17 +++++++++++++++-- src/Views/WorkingCopy.axaml | 16 ++++++++++++---- 9 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs index 8ac6501f..492d00c7 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Commit : Command { - public Commit(string repo, string message, bool amend, bool allowEmpty = false) + public Commit(string repo, string message, bool autoStage, bool amend, bool allowEmpty = false) { var file = Path.GetTempFileName(); File.WriteAllText(file, message); @@ -12,6 +12,8 @@ namespace SourceGit.Commands WorkingDirectory = repo; Context = repo; Args = $"commit --file=\"{file}\""; + if (autoStage) + Args += " --all"; if (amend) Args += " --amend --no-edit"; if (allowEmpty) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 546a300b..55b05a01 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -543,6 +543,8 @@ Ignore files in the same folder Ignore this file only Amend + Auto-Stage + Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected. You can stage this file now. COMMIT COMMIT & PUSH diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index fcc2f40f..58a72d47 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -545,6 +545,8 @@ 忽略同目录下所有文件 忽略本文件 修补(--amend) + 自动暂存(--all) + 提交前自动将修改过和删除的文件加入暂存区,但新增文件需要手动添加。 现在您已可将其加入暂存区中 提交 提交并推送 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 159009b9..73176126 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -545,6 +545,8 @@ 忽略同路徑下所有檔案 忽略本檔案 修補(--amend) + 自動暫存(--all) + 提交前自動將修改過和刪除的檔案加入暫存區,但新增檔案需要手動添加。 現在您已可將其加入暫存區中 提交 提交併推送 diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 67250c4b..1c087506 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -64,6 +64,12 @@ namespace SourceGit.ViewModels set; } = true; + public bool AutoStageBeforeCommit + { + get; + set; + } = false; + public AvaloniaList Filters { get; diff --git a/src/ViewModels/Reword.cs b/src/ViewModels/Reword.cs index 47b1bd06..b3f0e9f8 100644 --- a/src/ViewModels/Reword.cs +++ b/src/ViewModels/Reword.cs @@ -39,7 +39,7 @@ namespace SourceGit.ViewModels return Task.Run(() => { - var succ = new Commands.Commit(_repo.FullPath, _message, true, true).Exec(); + var succ = new Commands.Commit(_repo.FullPath, _message, false, true, true).Exec(); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; }); diff --git a/src/ViewModels/Squash.cs b/src/ViewModels/Squash.cs index 4b35266b..8e78658d 100644 --- a/src/ViewModels/Squash.cs +++ b/src/ViewModels/Squash.cs @@ -43,7 +43,7 @@ namespace SourceGit.ViewModels { var succ = new Commands.Reset(_repo.FullPath, Parent.SHA, "--soft").Exec(); if (succ) - succ = new Commands.Commit(_repo.FullPath, _message, true).Exec(); + succ = new Commands.Commit(_repo.FullPath, _message, false, true).Exec(); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; }); diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 8b32240c..a4d8b84e 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -77,6 +77,12 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _isCommitting, value); } + public bool AutoStageBeforeCommit + { + get => _repo.Settings.AutoStageBeforeCommit; + set => _repo.Settings.AutoStageBeforeCommit = value; + } + public bool UseAmend { get => _useAmend; @@ -1216,7 +1222,13 @@ namespace SourceGit.ViewModels return; } - if (_staged.Count == 0) + if (_count == 0) + { + App.RaiseException(_repo.FullPath, "No files added to commit!"); + return; + } + + if (!AutoStageBeforeCommit && _staged.Count == 0) { App.RaiseException(_repo.FullPath, "No files added to commit!"); return; @@ -1234,9 +1246,10 @@ namespace SourceGit.ViewModels IsCommitting = true; _repo.SetWatcherEnabled(false); + var autoStage = AutoStageBeforeCommit; Task.Run(() => { - var succ = new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec(); + var succ = new Commands.Commit(_repo.FullPath, _commitMessage, autoStage, _useAmend).Exec(); Dispatcher.UIThread.Post(() => { if (succ) diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 49d693ae..8e77d9d8 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -174,7 +174,7 @@ - +