diff --git a/VERSION b/VERSION index 90c5b336..b9d71048 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13 \ No newline at end of file +8.14 \ No newline at end of file diff --git a/build/build.linux.sh b/build/build.linux.sh old mode 100644 new mode 100755 diff --git a/build/resources/_common/usr/bin/sourcegit b/build/resources/_common/usr/bin/sourcegit index f056d708..74670167 100644 --- a/build/resources/_common/usr/bin/sourcegit +++ b/build/resources/_common/usr/bin/sourcegit @@ -1,2 +1,2 @@ #!/bin/bash -exec /opt/sourcegit/sourcegit +exec /opt/sourcegit/sourcegit $1 diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 21210238..660a5daa 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -36,7 +36,7 @@ return cmd.Exec(); } - public static bool Delete(string repo, string name) + public static bool DeleteLocal(string repo, string name) { var cmd = new Command(); cmd.WorkingDirectory = repo; @@ -44,5 +44,25 @@ cmd.Args = $"branch -D {name}"; return cmd.Exec(); } + + public static bool DeleteRemote(string repo, string remote, string name) + { + var cmd = new Command(); + cmd.WorkingDirectory = repo; + cmd.Context = repo; + + var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); + if (!string.IsNullOrEmpty(sshKey)) + { + cmd.Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } + else + { + cmd.Args = "-c credential.helper=manager "; + } + + cmd.Args += $"push {remote} --delete {name}"; + return cmd.Exec(); + } } } diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index d65e9e73..a1a151aa 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -62,6 +62,14 @@ namespace SourceGit.Commands return Exec(); } + public bool Commit(string commitId, Action onProgress) + { + Args = $"checkout --detach --progress {commitId}"; + TraitErrorAsOutput = true; + _outputHandler = onProgress; + return Exec(); + } + public bool Files(List files) { StringBuilder builder = new StringBuilder(); diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index e92b2234..aaa6e125 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -12,11 +12,11 @@ namespace SourceGit.Commands private const string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/"; private const string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/"; - public Diff(string repo, Models.DiffOption opt) + public Diff(string repo, Models.DiffOption opt, int unified) { WorkingDirectory = repo; Context = repo; - Args = $"diff --ignore-cr-at-eol --unified=4 {opt}"; + Args = $"diff --ignore-cr-at-eol --unified={unified} {opt}"; } public Models.DiffResult Result() diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index b3e4814a..0aac37a5 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -33,31 +33,6 @@ namespace SourceGit.Commands Args += $"{remote} {local}:{remoteBranch}"; } - /// - /// Only used to delete a remote branch!!!!!! - /// - /// - /// - /// - public Push(string repo, string remote, string branch) - { - WorkingDirectory = repo; - Context = repo; - TraitErrorAsOutput = true; - - var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); - if (!string.IsNullOrEmpty(sshKey)) - { - Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - else - { - Args = "-c credential.helper=manager "; - } - - Args += $"push {remote} --delete {branch}"; - } - public Push(string repo, string remote, string tag, bool isDelete) { WorkingDirectory = repo; diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index cc726fd9..ed5282f7 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -8,6 +8,7 @@ namespace SourceGit.Commands { private const string PREFIX_LOCAL = "refs/heads/"; private const string PREFIX_REMOTE = "refs/remotes/"; + private const string PREFIX_DETACHED = "(HEAD detached at"; [GeneratedRegex(@"^(\d+)\s(\d+)$")] private static partial Regex REG_AHEAD_BEHIND(); @@ -51,7 +52,12 @@ namespace SourceGit.Commands var refName = parts[0]; if (refName.EndsWith("/HEAD", StringComparison.Ordinal)) return; - + + if (refName.StartsWith(PREFIX_DETACHED, StringComparison.Ordinal)) + { + branch.IsHead = true; + } + if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) { branch.Name = refName.Substring(PREFIX_LOCAL.Length); diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 7d6ad169..618ff014 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -146,6 +146,15 @@ namespace SourceGit.Commands Name = d.Substring(19).Trim(), }); } + else if (d.Equals("HEAD")) + { + isHeadOfCurrent = true; + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.CurrentCommitHead, + Name = d.Trim(), + }); + } else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) { decorators.Add(new Models.Decorator() diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs index f96d3bc7..5fe57f94 100644 --- a/src/Commands/Tag.cs +++ b/src/Commands/Tag.cs @@ -5,12 +5,22 @@ namespace SourceGit.Commands { public static class Tag { - public static bool Add(string repo, string name, string basedOn, string message) + public static bool Add(string repo, string name, string basedOn) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.Args = $"tag -a {name} {basedOn} "; + cmd.Args = $"tag {name} {basedOn}"; + return cmd.Exec(); + } + + public static bool Add(string repo, string name, string basedOn, string message, bool sign) + { + var param = sign ? "-s -a" : "-a"; + var cmd = new Command(); + cmd.WorkingDirectory = repo; + cmd.Context = repo; + cmd.Args = $"tag {param} {name} {basedOn} "; if (!string.IsNullOrEmpty(message)) { diff --git a/src/Converters/DecoratorTypeConverters.cs b/src/Converters/DecoratorTypeConverters.cs index eb016360..9f3d9447 100644 --- a/src/Converters/DecoratorTypeConverters.cs +++ b/src/Converters/DecoratorTypeConverters.cs @@ -38,6 +38,9 @@ namespace SourceGit.Converters }); public static readonly FuncValueConverter ToFontWeight = - new FuncValueConverter(v => v == Models.DecoratorType.CurrentBranchHead ? FontWeight.Bold : FontWeight.Regular); + new FuncValueConverter(v => + v is Models.DecoratorType.CurrentBranchHead or Models.DecoratorType.CurrentCommitHead + ? FontWeight.Bold : FontWeight.Regular + ); } } diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 820f62c5..8235a3ef 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -7,6 +7,9 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsGreaterThanZero = new FuncValueConverter(v => v > 0); + public static readonly FuncValueConverter IsGreaterThanFour = + new FuncValueConverter(v => v > 4); + public static readonly FuncValueConverter IsZero = new FuncValueConverter(v => v == 0); diff --git a/src/Converters/WindowStateConverters.cs b/src/Converters/WindowStateConverters.cs index c73c86a8..2c3b2ac6 100644 --- a/src/Converters/WindowStateConverters.cs +++ b/src/Converters/WindowStateConverters.cs @@ -31,7 +31,7 @@ namespace SourceGit.Converters { if (state == WindowState.Maximized) { - return new GridLength(30); + return new GridLength(OperatingSystem.IsMacOS() ? 34 : 30); } else { diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index c23718d4..a22ee553 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -10,5 +10,6 @@ public string Upstream { get; set; } public string UpstreamTrackStatus { get; set; } public string Remote { get; set; } + public bool IsHead { get; set; } } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 4b3f0ed3..d8355e81 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -32,7 +32,7 @@ namespace SourceGit.Models public bool IsCurrentHead { - get => Decorators.Find(x => x.Type == DecoratorType.CurrentBranchHead) != null; + get => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; } public string FullMessage diff --git a/src/Models/Decorator.cs b/src/Models/Decorator.cs index 10967b45..60ffc1ee 100644 --- a/src/Models/Decorator.cs +++ b/src/Models/Decorator.cs @@ -7,6 +7,7 @@ namespace SourceGit.Models None, CurrentBranchHead, LocalBranchHead, + CurrentCommitHead, RemoteBranchHead, Tag, } diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index e3eaf36d..c1cce340 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -6,9 +6,9 @@ namespace SourceGit.Models { [GeneratedRegex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/]+/[\w\-\.]+\.git$")] private static partial Regex REG_HTTPS(); - [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-\.]+\.git$")] + [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/]+/[\w\-\.]+\.git$")] private static partial Regex REG_SSH1(); - [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$")] + [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/]+/[\w\-\.]+\.git$")] private static partial Regex REG_SSH2(); private static readonly Regex[] URL_FORMATS = [ diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index f33f0073..b61e6839 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -95,4 +95,6 @@ M939 94v710L512 998 85 805V94h-64A21 21 0 010 73v-0C0 61 10 51 21 51h981c12 0 21 10 21 21v0c0 12-10 21-21 21h-64zm-536 588L512 624l109 58c6 3 13 4 20 3a32 32 0 0026-37l-21-122 88-87c5-5 8-11 9-18a32 32 0 00-27-37l-122-18-54-111a32 32 0 00-57 0l-54 111-122 18c-7 1-13 4-18 9a33 33 0 001 46l88 87-21 122c-1 7-0 14 3 20a32 32 0 0043 14z M236 542a32 32 0 109 63l86-12a180 180 0 0022 78l-71 47a32 32 0 1035 53l75-50a176 176 0 00166 40L326 529zM512 16C238 16 16 238 16 512s222 496 496 496 496-222 496-496S786 16 512 16zm0 896c-221 0-400-179-400-400a398 398 0 0186-247l561 561A398 398 0 01512 912zm314-154L690 622a179 179 0 004-29l85 12a32 32 0 109-63l-94-13v-49l94-13a32 32 0 10-9-63l-87 12a180 180 0 00-20-62l71-47A32 32 0 10708 252l-75 50a181 181 0 00-252 10l-115-115A398 398 0 01512 112c221 0 400 179 400 400a398 398 0 01-86 247z M884 159l-18-18a43 43 0 00-38-12l-235 43a166 166 0 00-101 60L400 349a128 128 0 00-148 47l-120 171a21 21 0 005 29l17 12a128 128 0 00178-32l27-38 124 124-38 27a128 128 0 00-32 178l12 17a21 21 0 0029 5l171-120a128 128 0 0047-148l117-92A166 166 0 00853 431l43-235a43 43 0 00-12-38zm-177 249a64 64 0 110-90 64 64 0 010 90zm-373 312a21 21 0 010 30l-139 139a21 21 0 01-30 0l-30-30a21 21 0 010-30l139-139a21 21 0 0130 0z + M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z + M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index bf7d660e..685cb1be 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -35,6 +35,7 @@ Checkout${0}$ Copy Branch Name Delete${0}$ + Delete selected {0} branches Discard all changes Fast-Forward to${0}$ Git Flow - Finish${0}$ @@ -53,6 +54,9 @@ Show as List Show as Tree Checkout Branch + Checkout Commit + Warning: By doing a commit checkout, your Head will be detached + Commit : Branch : Local Changes : Stash & Reapply @@ -73,6 +77,7 @@ CLOSE Cherry-Pick This Commit Copy SHA + Checkout Commit Rebase${0}$to Here Reset${0}$to Here Revert Commit @@ -80,9 +85,10 @@ Save as Patch ... Squash Into Parent CHANGES - Search Files ... + Search Changes ... FILES LFS File + Search Files ... Submodule Tag Tree @@ -109,20 +115,28 @@ Local Changes : Discard Stash & Reapply + Do Nothing New Branch Name : Enter branch name. Create Local Branch Create Tag New Tag At : + GPG signing Tag Message : Optional. Tag Name : Recommended format :v1.0.0-alpha + Push to all remotes after created + Kind : + annotated + lightweight Cut Delete Branch Branch : You are about to delete a remote branch!!! Also delete remote branch${0}$ + Delete Multiple Branches + You are trying to delete multiple branches at one time. Be sure to double-check before taking action! Delete Remote Remote : Target : @@ -145,6 +159,8 @@ Side-By-Side Diff Syntax Highlighting Open In Merge Tool + Decrease Number of Visible Lines + Increase Number of Visible Lines SELECT FILE TO VIEW CHANGES Open In Merge Tool Discard Changes @@ -310,8 +326,10 @@ Remote : Push Changes To Remote Remote Branch : + Tracking remote branch(--set-upstream) Push all tags Push Tag To Remote + Push to all remotes Remote : Tag : Quit diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 9f464b79..ac48aec9 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -35,6 +35,7 @@ 检出(checkout)${0}$ 复制分支名 删除${0}$ + 删除选中的 {0} 个分支 放弃所有更改 快进(fast-forward)到${0}$ GIT工作流 - 完成${0}$ @@ -53,6 +54,9 @@ 列表模式 树形模式 检出(checkout)分支 + 检出(checkout)提交 + 注意:执行该操作后,当前HEAD会变为游离(detached)状态! + 提交 : 目标分支 : 未提交更改 : 贮藏(stash)并自动恢复 @@ -72,6 +76,7 @@ 远程仓库 : 关闭 挑选(cherry-pick)此提交 + 检出此提交 复制提交指纹 变基(rebase)${0}$到此处 重置(reset)${0}$到此处 @@ -80,9 +85,10 @@ 另存为补丁 ... 合并此提交到上一个提交 变更对比 - 查找文件... + 查找变更... 文件列表 LFS文件 + 查找文件... 子模块 标签文件 子树 @@ -107,22 +113,30 @@ 新分支基于 : 完成后切换到新分支 未提交更改 : - 忽略 - 贮藏(stash)并自动恢复 + 放弃所有 + 贮藏并自动恢复 + GIT默认 新分支名 : 填写分支名称。 创建本地分支 新建标签 标签位于 : + 使用GPG签名 标签描述 : 选填。 标签名 : 推荐格式 :v1.0.0-alpha + 推送到所有远程仓库 + 类型 : + 附注标签 + 轻量标签 剪切 删除分支确认 分支名 : 您正在删除远程上的分支,请务必小心!!! 同时删除远程分支${0}$ + 删除多个分支 + 您正在尝试一次性删除多个分支,请务必仔细检查后再执行操作! 删除远程确认 远程名 : 目标 : @@ -145,6 +159,8 @@ 分列对比 语法高亮 使用外部合并工具查看 + 减少可见的行数 + 增加可见的行数 请选择需要对比的文件 使用外部比对工具查看 放弃更改确认 @@ -310,8 +326,10 @@ 远程仓库 : 推送到远程仓库 远程分支 : + 跟踪远程分支(--set-upstream) 同时推送标签 推送标签到远程仓库 + 推送到所有远程仓库 远程仓库 : 标签 : 退出 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index afac434c..376f9424 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -13,6 +13,9 @@ 12 + + + + - - @@ -897,7 +881,7 @@ @@ -1075,11 +1062,11 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/DeleteMultipleBranches.axaml.cs b/src/Views/DeleteMultipleBranches.axaml.cs new file mode 100644 index 00000000..7b3552d5 --- /dev/null +++ b/src/Views/DeleteMultipleBranches.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class DeleteMultipleBranches : UserControl + { + public DeleteMultipleBranches() + { + InitializeComponent(); + } + } +} + diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml index 5ba5a211..6fa344fd 100644 --- a/src/Views/DiffView.axaml +++ b/src/Views/DiffView.axaml @@ -34,6 +34,14 @@ + + + + + Text="{DynamicResource Text.Remote.EditTitle}"/> diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index 6b696d5a..60ecf610 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -396,17 +396,10 @@ - - - - @@ -417,15 +410,19 @@ - - + + diff --git a/src/Views/Preference.axaml.cs b/src/Views/Preference.axaml.cs index 1dd77b50..1b5a432c 100644 --- a/src/Views/Preference.axaml.cs +++ b/src/Views/Preference.axaml.cs @@ -207,10 +207,17 @@ namespace SourceGit.Views private async void SelectGPGExecutable(object sender, RoutedEventArgs e) { - var pattern = OperatingSystem.IsWindows() ? "gpg.exe" : "gpg"; + var patterns = new List(); + if (OperatingSystem.IsWindows()) + patterns.Add("gpg.exe"); + else if (OperatingSystem.IsLinux()) + patterns.AddRange(new string[] { "gpg", "gpg2" }); + else + patterns.Add("gpg"); + var options = new FilePickerOpenOptions() { - FileTypeFilter = [new FilePickerFileType("GPG Executable") { Patterns = [pattern] }], + FileTypeFilter = [new FilePickerFileType("GPG Executable") { Patterns = patterns }], AllowMultiple = false, }; diff --git a/src/Views/Push.axaml b/src/Views/Push.axaml index bb76ad26..00cdafbf 100644 --- a/src/Views/Push.axaml +++ b/src/Views/Push.axaml @@ -13,7 +13,7 @@ Classes="bold" Text="{DynamicResource Text.Push.Title}"/> - + + + - diff --git a/src/Views/PushTag.axaml b/src/Views/PushTag.axaml index f62d538a..1b24dc1b 100644 --- a/src/Views/PushTag.axaml +++ b/src/Views/PushTag.axaml @@ -12,7 +12,7 @@ - + + SelectedItem="{Binding SelectedRemote, Mode=TwoWay}" + IsEnabled="{Binding !PushAllRemotes}"> @@ -41,6 +42,10 @@ + + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 6b62a0b1..b06cf587 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -109,7 +109,7 @@ - + @@ -119,14 +119,14 @@ - + - + @@ -142,7 +142,7 @@ - + @@ -198,19 +198,31 @@ + + + + - + @@ -250,20 +262,32 @@ + + + + - + @@ -294,7 +318,9 @@ + + + + + + + + + + @@ -364,6 +407,7 @@ + + + + + + + + + + @@ -390,7 +452,7 @@ - + @@ -408,6 +470,7 @@ BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" Background="{DynamicResource Brush.Contents}" + CornerRadius="4" Watermark="{DynamicResource Text.Repository.SearchTip}" Text="{Binding SearchCommitFilter, Mode=TwoWay}" VerticalContentAlignment="Center" diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index c459dd33..ed26905a 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Avalonia; @@ -6,6 +7,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace SourceGit.Views { @@ -66,56 +68,47 @@ namespace SourceGit.Views if (sender is Button button && DataContext is ViewModels.Repository repo) { var menu = repo.CreateContextMenuForExternalTools(); - if (menu != null) - { - menu.Open(button); - e.Handled = true; - } + button.OpenContextMenu(menu); + e.Handled = true; } } - private void OnLocalBranchTreeLostFocus(object sender, RoutedEventArgs e) - { - if (sender is TreeView tree) - tree.UnselectAll(); - } - - private void OnRemoteBranchTreeLostFocus(object sender, RoutedEventArgs e) - { - if (sender is TreeView tree) - tree.UnselectAll(); - } - - private void OnTagDataGridLostFocus(object sender, RoutedEventArgs e) - { - if (sender is DataGrid datagrid) - datagrid.SelectedItem = null; - } - private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) { - if (sender is TreeView tree && tree.SelectedItem != null) + if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) { remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch && DataContext is ViewModels.Repository repo) + ViewModels.BranchTreeNode prev = null; + foreach (var node in repo.LocalBranchTrees) + node.UpdateCornerRadius(ref prev); + + if (tree.SelectedItems.Count == 1) { - repo.NavigateToCommit((node.Backend as Models.Branch).Head); + 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) + if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) { localBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch && DataContext is ViewModels.Repository repo) + ViewModels.BranchTreeNode prev = null; + foreach (var node in repo.RemoteBranchTrees) + node.UpdateCornerRadius(ref prev); + + if (tree.SelectedItems.Count == 1) { - repo.NavigateToCommit((node.Backend as Models.Branch).Head); + var node = tree.SelectedItem as ViewModels.BranchTreeNode; + if (node.IsBranch) + repo.NavigateToCommit((node.Backend as Models.Branch).Head); } } } @@ -124,11 +117,12 @@ namespace SourceGit.Views { 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); - } } } @@ -136,9 +130,7 @@ namespace SourceGit.Views { var grid = sender as Grid; if (e.Property == IsVisibleProperty && grid.IsVisible) - { txtSearchCommitsBox.Focus(); - } } private void OnSearchKeyDown(object sender, KeyEventArgs e) @@ -146,9 +138,8 @@ namespace SourceGit.Views if (e.Key == Key.Enter) { if (DataContext is ViewModels.Repository repo) - { repo.StartSearchCommits(); - } + e.Handled = true; } } @@ -174,9 +165,7 @@ namespace SourceGit.Views 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) { @@ -195,16 +184,43 @@ namespace SourceGit.Views private void OnLocalBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) { remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - if (sender is Grid grid && grid.DataContext is ViewModels.BranchTreeNode node) + var repo = DataContext as ViewModels.Repository; + var tree = sender as TreeView; + if (tree.SelectedItems.Count == 0) { - if (node.IsBranch && DataContext is ViewModels.Repository repo) + 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(node.Backend as Models.Branch); - if (menu != null) - menu.Open(grid); + 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; } @@ -212,22 +228,60 @@ namespace SourceGit.Views private void OnRemoteBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) { localBranchTree.UnselectAll(); - - if (sender is Grid grid && grid.DataContext is ViewModels.BranchTreeNode node && DataContext is ViewModels.Repository repo) + tagsList.SelectedItem = null; + + var repo = DataContext as ViewModels.Repository; + var tree = sender as TreeView; + if (tree.SelectedItems.Count == 0) { - if (node.IsRemote) + e.Handled = true; + return; + } + + if (tree.SelectedItems.Count == 1) + { + var node = tree.SelectedItem as ViewModels.BranchTreeNode; + if (node != null && node.IsRemote) { - var menu = repo.CreateContextMenuForRemote(node.Backend as Models.Remote); - if (menu != null) - menu.Open(grid); + 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; } - else if (node.IsBranch) + } + + 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(node.Backend as Models.Branch); - if (menu != null) - menu.Open(grid); + 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; } @@ -238,8 +292,7 @@ namespace SourceGit.Views { var tag = datagrid.SelectedItem as Models.Tag; var menu = repo.CreateContextMenuForTag(tag); - if (menu != null) - menu.Open(datagrid); + datagrid.OpenContextMenu(menu); } e.Handled = true; @@ -251,8 +304,7 @@ namespace SourceGit.Views { var submodule = datagrid.SelectedItem as string; var menu = repo.CreateContextMenuForSubmodule(submodule); - if (menu != null) - menu.Open(datagrid); + datagrid.OpenContextMenu(menu); } e.Handled = true; @@ -263,8 +315,7 @@ namespace SourceGit.Views if (DataContext is ViewModels.Repository repo) { var menu = repo.CreateContextMenuForGitFlow(); - if (menu != null) - menu.Open(sender as Button); + (sender as Control)?.OpenContextMenu(menu); } e.Handled = true; @@ -313,5 +364,23 @@ namespace SourceGit.Views 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); + } + } } } diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index 8e440089..3bcc7fff 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -47,23 +47,18 @@ Text="{Binding HttpProxy, Mode=TwoWay}"/> - - - - + Text="{Binding GPGUserSigningKey, Mode=TwoWay}"/> + + diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index ae81316a..c541cfe2 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -61,6 +61,7 @@ Height="26" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" Background="Transparent" + CornerRadius="4" Watermark="{DynamicResource Text.CommitDetail.Changes.Search}" Text="{Binding SearchFilter, Mode=TwoWay}"> diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index fe71c672..a9e80676 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -25,7 +25,7 @@ namespace SourceGit.Views { var compare = DataContext as ViewModels.RevisionCompare; var menu = compare.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); - menu.Open(datagrid); + datagrid.OpenContextMenu(menu); } e.Handled = true; @@ -40,7 +40,7 @@ namespace SourceGit.Views if (node != null && !node.IsFolder) { var menu = compare.CreateChangeContextMenu(node.Backend as Models.Change); - menu.Open(view); + view.OpenContextMenu(menu); } } diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index e91c6e0b..2f1eb8d2 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -23,9 +23,9 @@ Height="26" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" Background="Transparent" - Watermark="{DynamicResource Text.CommitDetail.Changes.Search}" - Text="{Binding SearchFileFilter, Mode=TwoWay}" - v:AutoFocusBehaviour.IsEnabled="True"> + CornerRadius="4" + Watermark="{DynamicResource Text.CommitDetail.Files.Search}" + Text="{Binding SearchFileFilter, Mode=TwoWay}"> diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index 5d9966ab..7865542d 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -198,7 +198,8 @@ namespace SourceGit.Views var menu = new ContextMenu(); menu.Items.Add(copy); - menu.Open(TextArea.TextView); + + TextArea.TextView.OpenContextMenu(menu); e.Handled = true; } @@ -219,7 +220,7 @@ namespace SourceGit.Views if (!node.IsFolder) { var menu = detail.CreateRevisionFileContextMenu(node.Backend as Models.Object); - menu.Open(sender as Control); + (sender as Control)?.OpenContextMenu(menu); } e.Handled = true; diff --git a/src/Views/SelfUpdate.axaml b/src/Views/SelfUpdate.axaml index f29da5b5..a83895fb 100644 --- a/src/Views/SelfUpdate.axaml +++ b/src/Views/SelfUpdate.axaml @@ -69,7 +69,7 @@ - + diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index 2caa1d02..b0d73bec 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -172,7 +172,7 @@ diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 25573b41..20978345 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -137,6 +137,9 @@ namespace SourceGit.Views var width = textView.Bounds.Width; foreach (var line in textView.VisualLines) { + if (line.FirstDocumentLine == null) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > _editor.DiffData.Lines.Count) break; @@ -320,7 +323,8 @@ namespace SourceGit.Views }; menu.Items.Add(copy); - menu.Open(TextArea.TextView); + + TextArea.TextView.OpenContextMenu(menu); e.Handled = true; } @@ -516,6 +520,9 @@ namespace SourceGit.Views var infos = _editor.IsOld ? _editor.DiffData.Old : _editor.DiffData.New; foreach (var line in textView.VisualLines) { + if (line.FirstDocumentLine == null) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > infos.Count) break; @@ -731,7 +738,8 @@ namespace SourceGit.Views }; menu.Items.Add(copy); - menu.Open(TextArea.TextView); + + TextArea.TextView.OpenContextMenu(menu); e.Handled = true; } diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index f0e3a1fc..aa5054c4 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -42,7 +42,7 @@ namespace SourceGit.Views if (sender is Grid grid && DataContext is ViewModels.Welcome vm) { var menu = vm.CreateContextMenu(grid.DataContext as ViewModels.RepositoryNode); - menu?.Open(grid); + grid.OpenContextMenu(menu); e.Handled = true; } } @@ -248,7 +248,7 @@ namespace SourceGit.Views { Dispatcher.UIThread.Invoke(() => { - (DataContext as ViewModels.Welcome).InitRepository(path); + (DataContext as ViewModels.Welcome).InitRepository(path, parent); }); return; } @@ -257,7 +257,7 @@ namespace SourceGit.Views Dispatcher.UIThread.Invoke(() => { var repo = ViewModels.Preference.AddRepository(root, gitDir); - var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, parent); + var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, parent, true); launcher.OpenRepositoryInTab(node, page); }); }); diff --git a/src/Views/WorkingCopy.axaml.cs b/src/Views/WorkingCopy.axaml.cs index 46c82a40..e4746f1f 100644 --- a/src/Views/WorkingCopy.axaml.cs +++ b/src/Views/WorkingCopy.axaml.cs @@ -205,8 +205,7 @@ namespace SourceGit.Views } var menu = vm.CreateContextMenuForUnstagedChanges(selected); - if (menu != null) - menu.Open(datagrid); + datagrid.OpenContextMenu(menu); } e.Handled = true; @@ -225,8 +224,7 @@ namespace SourceGit.Views } var menu = vm.CreateContextMenuForUnstagedChanges(selected); - if (menu != null) - menu.Open(tree); + tree.OpenContextMenu(menu); } e.Handled = true; @@ -245,8 +243,7 @@ namespace SourceGit.Views } var menu = vm.CreateContextMenuForStagedChanges(selected); - if (menu != null) - menu.Open(datagrid); + datagrid.OpenContextMenu(menu); } e.Handled = true; @@ -265,8 +262,7 @@ namespace SourceGit.Views } var menu = vm.CreateContextMenuForStagedChanges(selected); - if (menu != null) - menu.Open(tree); + tree.OpenContextMenu(menu); } e.Handled = true; @@ -331,7 +327,7 @@ namespace SourceGit.Views { var menu = vm.CreateContextMenuForCommitMessages(); menu.Placement = PlacementMode.TopEdgeAlignedLeft; - menu.Open(button); + button.OpenContextMenu(menu); e.Handled = true; } }