diff --git a/README.md b/README.md index 8188c579..e2cf9da2 100644 --- a/README.md +++ b/README.md @@ -91,5 +91,5 @@ This app supports open repository in external tools listed in the table below. Thanks to all the people who contribute. - + diff --git a/VERSION b/VERSION index 3578724f..9c57ca32 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.11 \ No newline at end of file +8.12 \ No newline at end of file diff --git a/build/resources/_common/usr/bin/sourcegit b/build/resources/_common/usr/bin/sourcegit new file mode 100644 index 00000000..f056d708 --- /dev/null +++ b/build/resources/_common/usr/bin/sourcegit @@ -0,0 +1,2 @@ +#!/bin/bash +exec /opt/sourcegit/sourcegit diff --git a/src/App.axaml b/src/App.axaml index 768ff267..ffb634c7 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -1,5 +1,6 @@ @@ -21,4 +22,16 @@ + + + + + + + + + + + + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index eb4655c4..ed92b621 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text; using System.Text.Json; using System.Threading.Tasks; +using System.Windows.Input; using Avalonia; using Avalonia.Controls; @@ -18,9 +19,27 @@ using Avalonia.Threading; namespace SourceGit { + public class SimpleCommand : ICommand + { + public event EventHandler CanExecuteChanged + { + add { } + remove { } + } + + public SimpleCommand(Action action) + { + _action = action; + } + + public bool CanExecute(object parameter) => _action != null; + public void Execute(object parameter) => _action?.Invoke(); + + private Action _action = null; + } + public partial class App : Application { - [STAThread] public static void Main(string[] args) { @@ -67,6 +86,31 @@ namespace SourceGit return builder; } + public static readonly SimpleCommand OpenPreferenceCommand = new SimpleCommand(() => + { + var dialog = new Views.Preference(); + dialog.ShowDialog(GetTopLevel() as Window); + }); + + public static readonly SimpleCommand OpenHotkeysCommand = new SimpleCommand(() => + { + var dialog = new Views.Hotkeys(); + dialog.ShowDialog(GetTopLevel() as Window); + }); + + public static readonly SimpleCommand OpenAboutCommand = new SimpleCommand(() => + { + var dialog = new Views.About(); + dialog.ShowDialog(GetTopLevel() as Window); + }); + + public static readonly SimpleCommand CheckForUpdateCommand = new SimpleCommand(() => + { + Check4Update(true); + }); + + public static readonly SimpleCommand QuitCommand = new SimpleCommand(Quit); + public static void RaiseException(string context, string message) { if (Current is App app && app._notificationReceiver != null) diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 8f84c346..d65fee4e 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -62,14 +62,30 @@ namespace SourceGit.Commands public class AutoFetch { - private const double INTERVAL = 10 * 60; - public static bool IsEnabled { get; set; } = false; + public static int Interval + { + get => _interval; + set + { + if (value < 1) + return; + _interval = value; + lock (_lock) + { + foreach (var job in _jobs) + { + job.Value.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(_interval)); + } + } + } + } + class Job { public Fetch Cmd = null; @@ -104,7 +120,7 @@ namespace SourceGit.Commands foreach (var job in uptodate) { job.Cmd.Exec(); - job.NextRunTimepoint = DateTime.Now.AddSeconds(INTERVAL); + job.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)); } Thread.Sleep(2000); @@ -117,7 +133,7 @@ namespace SourceGit.Commands var job = new Job { Cmd = new Fetch(repo, "--all", true, null) { RaiseError = false }, - NextRunTimepoint = DateTime.Now.AddSeconds(INTERVAL), + NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)), }; lock (_lock) @@ -147,12 +163,13 @@ namespace SourceGit.Commands { if (_jobs.TryGetValue(repo, out var value)) { - value.NextRunTimepoint = DateTime.Now.AddSeconds(INTERVAL); + value.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)); } } } private static readonly Dictionary _jobs = new Dictionary(); private static readonly object _lock = new object(); + private static int _interval = 10; } } diff --git a/src/Models/User.cs b/src/Models/User.cs index 2ef770cf..ba1bebd6 100644 --- a/src/Models/User.cs +++ b/src/Models/User.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; namespace SourceGit.Models { @@ -36,11 +36,11 @@ namespace SourceGit.Models var email = data.Substring(nameEndIdx + 1); User user = new User() { Name = name, Email = email }; - _caches.Add(data, user); + _caches.TryAdd(data, user); return user; } } - private static Dictionary _caches = new Dictionary(); + private static ConcurrentDictionary _caches = new ConcurrentDictionary(); } } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index e6e30fb0..e4eb6851 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -180,7 +180,8 @@ namespace SourceGit.Models } else if (name.Equals("HEAD", StringComparison.Ordinal) || name.StartsWith("refs/heads/", StringComparison.Ordinal) || - name.StartsWith("refs/remotes/", StringComparison.Ordinal)) + name.StartsWith("refs/remotes/", StringComparison.Ordinal) || + (name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal))) { _updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime(); } diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index 465dcdf9..d5b58fb3 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -22,7 +22,6 @@ namespace SourceGit.Native builder.With(new MacOSPlatformOptions() { - DisableNativeMenus = true, DisableDefaultApplicationMenuItems = true, }); } diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 563809eb..b095a6a9 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1,7 +1,8 @@ About + About SourceGit • Build with - Copyright © 2024 sourcegit-scm. + © 2024 sourcegit-scm • TextEditor from • Monospace fonts come from • Source code can be found at @@ -210,12 +211,14 @@ CLEAR SELECTED {0} COMMITS HotKeys + About HotKeys GLOBAL Cancel current popup Close current page Go to previous page Go to next page Create new page + Open preference dialog REPOSITORY Force to reload this repository Stage/Unstage selected changes @@ -272,6 +275,8 @@ Use fixed tab width in titlebar GIT Fetch remotes automatically + Auto Fetch Interval + Minute(s) Enable Auto CRLF Default Clone Dir User Email @@ -310,6 +315,7 @@ Push Tag To Remote Remote : Tag : + Quit Rebase Current Branch Stash & reapply local changes On : diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 76385fe0..1c972de5 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1,7 +1,8 @@ 关于软件 + 关于本软件 • 项目依赖于 - Copyright © 2024 sourcegit-scm. + © 2024 sourcegit-scm • 文本编辑器使用 • 等宽字体来自于 • 项目源代码地址 @@ -210,12 +211,14 @@ 清空 已选中 {0} 项提交 快捷键 + 显示快捷键 全局快捷键 取消弹出面板 关闭当前页面 切换到上一个页面 切换到下一个页面 新建页面 + 打开偏好设置面板 仓库页面快捷键 重新加载仓库状态 将选中的变更暂存或从暂存列表中移除 @@ -272,6 +275,8 @@ 使用固定宽度的标题栏标签 GIT配置 启用定时自动拉取远程更新 + 自动拉取间隔 + 分钟 自动换行转换 默认克隆路径 邮箱 @@ -310,6 +315,7 @@ 推送标签到远程仓库 远程仓库 : 标签 : + 退出 变基(rebase)操作 自动贮藏并恢复本地变更 目标提交 : diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 866fc987..afac434c 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -230,16 +230,16 @@ - - - diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index d1702860..103f0505 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; @@ -114,15 +113,7 @@ namespace SourceGit.ViewModels CallUIThread(() => { var repo = Preference.AddRepository(path, Path.Combine(path, ".git")); - var node = new RepositoryNode() - { - Id = repo.FullPath, - Name = Path.GetFileName(repo.FullPath), - Bookmark = 0, - IsRepository = true, - }; - Preference.AddNode(node); - + var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null); _launcher.OpenRepositoryInTab(node, _page); }); diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index 18c03936..3400b5ea 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -133,7 +133,7 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Post(() => { - if (string.IsNullOrEmpty(_option.OrgPath)) + if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null") Title = _option.Path; else Title = $"{_option.OrgPath} → {_option.Path}"; diff --git a/src/ViewModels/EditRepositoryNode.cs b/src/ViewModels/EditRepositoryNode.cs index 99567c13..4877ca9b 100644 --- a/src/ViewModels/EditRepositoryNode.cs +++ b/src/ViewModels/EditRepositoryNode.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -50,8 +49,13 @@ namespace SourceGit.ViewModels public override Task Sure() { + bool needSort = _node.Name != _name; _node.Name = _name; _node.Bookmark = _bookmark; + + if (needSort) + Preference.SortByRenamedNode(_node); + return null; } diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index a9f62516..8cab66c1 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -134,11 +134,14 @@ namespace SourceGit.ViewModels { if (commits.Count == 0) { + _repo.SearchResultSelectedCommit = null; DetailContext = null; } else if (commits.Count == 1) { var commit = commits[0] as Models.Commit; + _repo.SearchResultSelectedCommit = commit; + AutoSelectedCommit = commit; NavigationId = _navigationId + 1; @@ -155,12 +158,15 @@ namespace SourceGit.ViewModels } else if (commits.Count == 2) { + _repo.SearchResultSelectedCommit = null; + var end = commits[0] as Models.Commit; var start = commits[1] as Models.Commit; DetailContext = new RevisionCompare(_repo.FullPath, start, end); } else { + _repo.SearchResultSelectedCommit = null; DetailContext = new CountSelectedCommits() { Count = commits.Count }; } } diff --git a/src/ViewModels/Init.cs b/src/ViewModels/Init.cs index 5f1e846e..b2c48fc7 100644 --- a/src/ViewModels/Init.cs +++ b/src/ViewModels/Init.cs @@ -28,18 +28,10 @@ namespace SourceGit.ViewModels return false; var gitDir = Path.GetFullPath(Path.Combine(_targetPath, ".git")); - CallUIThread(() => { var repo = Preference.AddRepository(_targetPath, gitDir); - var node = new RepositoryNode() - { - Id = repo.FullPath, - Name = Path.GetFileName(repo.FullPath), - Bookmark = 0, - IsRepository = true, - }; - Preference.AddNode(node); + Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null); }); return true; diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 3d1fc1c1..7d0c9197 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -107,7 +107,22 @@ namespace SourceGit.ViewModels { if (Pages.Count == 1) { - App.Quit(); + var last = Pages[0]; + if (last.Data is Repository repo) + { + Commands.AutoFetch.RemoveRepository(repo.FullPath); + repo.Close(); + + last.Node = new RepositoryNode() { Id = Guid.NewGuid().ToString() }; + last.Data = Welcome.Instance; + + GC.Collect(); + } + else + { + App.Quit(); + } + return; } @@ -119,15 +134,7 @@ namespace SourceGit.ViewModels var activeIdx = Pages.IndexOf(_activePage); if (removeIdx == activeIdx) { - if (removeIdx == Pages.Count - 1) - { - ActivePage = Pages[removeIdx - 1]; - } - else - { - ActivePage = Pages[removeIdx + 1]; - } - + ActivePage = Pages[removeIdx == Pages.Count - 1 ? removeIdx - 1 : removeIdx + 1]; CloseRepositoryInTab(page); Pages.RemoveAt(removeIdx); OnPropertyChanged(nameof(Pages)); diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index c8765c0d..5fce2bf3 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -26,14 +26,8 @@ namespace SourceGit.ViewModels public LauncherPage() { - _node = new RepositoryNode() - { - Id = Guid.NewGuid().ToString(), - Name = "WelcomePage", - Bookmark = 0, - IsRepository = false, - }; - _data = new Welcome(); + _node = new RepositoryNode() { Id = Guid.NewGuid().ToString() }; + _data = Welcome.Instance; } public LauncherPage(RepositoryNode node, Repository repo) diff --git a/src/ViewModels/Preference.cs b/src/ViewModels/Preference.cs index cf488328..85f26611 100644 --- a/src/ViewModels/Preference.cs +++ b/src/ViewModels/Preference.cs @@ -232,6 +232,23 @@ namespace SourceGit.ViewModels } } + public int? GitAutoFetchInterval + { + get => Commands.AutoFetch.Interval; + set + { + if (value is null or < 1) + { + return; + } + if (Commands.AutoFetch.Interval != value) + { + Commands.AutoFetch.Interval = (int)value; + OnPropertyChanged(nameof(GitAutoFetchInterval)); + } + } + } + public int ExternalMergeToolType { get => _externalMergeToolType; @@ -346,6 +363,29 @@ namespace SourceGit.ViewModels return FindNodeRecursive(id, _instance.RepositoryNodes); } + public static RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode parent) + { + var node = FindNodeRecursive(repo, _instance.RepositoryNodes); + if (node == null) + { + node = new RepositoryNode() + { + Id = repo, + Name = Path.GetFileName(repo), + Bookmark = 0, + IsRepository = true, + }; + + AddNode(node, parent); + } + else + { + MoveNode(node, parent); + } + + return node; + } + public static void MoveNode(RepositoryNode node, RepositoryNode to = null) { if (to == null && _instance._repositoryNodes.Contains(node)) @@ -362,6 +402,31 @@ namespace SourceGit.ViewModels RemoveNodeRecursive(node, _instance._repositoryNodes); } + public static void SortByRenamedNode(RepositoryNode node) + { + var container = FindNodeContainer(node, _instance._repositoryNodes); + if (container == null) + return; + + var list = new List(); + list.AddRange(container); + list.Sort((l, r) => + { + if (l.IsRepository != r.IsRepository) + { + return l.IsRepository ? 1 : -1; + } + else + { + return l.Name.CompareTo(r.Name); + } + }); + + container.Clear(); + foreach (var one in list) + container.Add(one); + } + public static Repository FindRepository(string path) { foreach (var repo in _instance.Repositories) @@ -417,6 +482,21 @@ namespace SourceGit.ViewModels return null; } + private static AvaloniaList FindNodeContainer(RepositoryNode node, AvaloniaList collection) + { + foreach (var sub in collection) + { + if (node == sub) + return collection; + + var subCollection = FindNodeContainer(node, sub.SubNodes); + if (subCollection != null) + return subCollection; + } + + return null; + } + private static bool RemoveNodeRecursive(RepositoryNode node, AvaloniaList collection) { if (collection.Contains(node)) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 73a500b5..0e67f11b 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -8,7 +8,6 @@ using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Platform; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -222,6 +221,12 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _hasUnsolvedConflicts, value); } + public Models.Commit SearchResultSelectedCommit + { + get => _searchResultSelectedCommit; + set => SetProperty(ref _searchResultSelectedCommit, value); + } + public void Open() { _watcher = new Models.Watcher(this); @@ -1378,5 +1383,6 @@ namespace SourceGit.ViewModels private InProgressContext _inProgressContext = null; private bool _hasUnsolvedConflicts = false; + private Models.Commit _searchResultSelectedCommit = null; } } diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index a6fa1a8a..d4569d05 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -9,10 +9,7 @@ namespace SourceGit.ViewModels { public class Welcome : ObservableObject { - public bool IsClearSearchVisible - { - get => !string.IsNullOrEmpty(_searchFilter); - } + public static Welcome Instance => _instance; public AvaloniaList RepositoryNodes { @@ -25,10 +22,7 @@ namespace SourceGit.ViewModels set { if (SetProperty(ref _searchFilter, value)) - { Referesh(); - OnPropertyChanged(nameof(IsClearSearchVisible)); - } } } @@ -205,6 +199,7 @@ namespace SourceGit.ViewModels } } + private static Welcome _instance = new Welcome(); private string _searchFilter = string.Empty; } } diff --git a/src/Views/Hotkeys.axaml b/src/Views/Hotkeys.axaml index 96f0a9a2..79a06cb7 100644 --- a/src/Views/Hotkeys.axaml +++ b/src/Views/Hotkeys.axaml @@ -67,21 +67,24 @@ FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:FontSizeModifyConverters.Increase}}" Margin="0,0,0,8"/> - - - + + + + + + - - + + - - + + - - + + - - + + - + + PointerWheelChanged="ScrollTabs" + ScrollChanged="OnTabsScrollChanged"> - + diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index d6f4b54f..17a5e78a 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -98,6 +98,15 @@ namespace SourceGit.Views protected override void OnKeyDown(KeyEventArgs e) { var vm = DataContext as ViewModels.Launcher; + + // Ctrl+Shift+P opens preference dialog (macOS use hotkeys in system menu bar) + if (!OperatingSystem.IsMacOS() && e.KeyModifiers == (KeyModifiers.Control | KeyModifiers.Shift) && e.Key == Key.P) + { + App.OpenPreferenceCommand.Execute(null); + e.Handled = true; + return; + } + if ((OperatingSystem.IsMacOS() && e.KeyModifiers.HasFlag(KeyModifiers.Meta)) || (!OperatingSystem.IsMacOS() && e.KeyModifiers.HasFlag(KeyModifiers.Control))) { @@ -223,7 +232,7 @@ namespace SourceGit.Views { if (e.Delta.Y < 0) launcherTabsScroller.LineRight(); - else + else if (e.Delta.Y > 0) launcherTabsScroller.LineLeft(); e.Handled = true; } @@ -256,6 +265,15 @@ namespace SourceGit.Views e.Handled = true; } + private void OnTabsScrollChanged(object sender, ScrollChangedEventArgs e) + { + if (sender is ScrollViewer scrollViewer) + { + leftScrollIndicator.IsEnabled = scrollViewer.Offset.X > 0; + rightScrollIndicator.IsEnabled = scrollViewer.Offset.X < scrollViewer.Extent.Width - scrollViewer.Viewport.Width; + } + } + private void SetupDragAndDrop(object sender, RoutedEventArgs e) { if (sender is Border border) @@ -337,33 +355,6 @@ namespace SourceGit.Views OnPopupCancel(sender, e); } - private async void OpenPreference(object sender, RoutedEventArgs e) - { - var dialog = new Preference(); - await dialog.ShowDialog(this); - e.Handled = true; - } - - private async void OpenHotkeys(object sender, RoutedEventArgs e) - { - var dialog = new Hotkeys(); - await dialog.ShowDialog(this); - e.Handled = true; - } - - private void Check4Update(object sender, RoutedEventArgs e) - { - App.Check4Update(true); - e.Handled = true; - } - - private async void OpenAboutDialog(object sender, RoutedEventArgs e) - { - var dialog = new About(); - await dialog.ShowDialog(this); - e.Handled = true; - } - private bool _pressedTab = false; private Point _pressedTabPosition = new Point(); private bool _startDrag = false; diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index 4579fe42..6b696d5a 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -231,7 +231,7 @@ - + + + + + + + + + + + + + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 1423807d..c854faa2 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -403,6 +403,7 @@ - diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index 53ef2b5a..f0e3a1fc 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -257,14 +257,7 @@ namespace SourceGit.Views Dispatcher.UIThread.Invoke(() => { var repo = ViewModels.Preference.AddRepository(root, gitDir); - var node = new ViewModels.RepositoryNode() - { - Id = repo.FullPath, - Name = Path.GetFileName(repo.FullPath), - Bookmark = 0, - IsRepository = true, - }; - ViewModels.Preference.AddNode(node, parent); + var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, parent); launcher.OpenRepositoryInTab(node, page); }); });