From 4d7b16dc758537269ade586760c70e8d68de3249 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 21 Aug 2023 20:04:25 +0800 Subject: [PATCH 0001/2773] fix: fix filter not work when there's no files under .git/refs/xxx/ --- src/Models/Repository.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Models/Repository.cs b/src/Models/Repository.cs index e0be31bf..7774e54d 100644 --- a/src/Models/Repository.cs +++ b/src/Models/Repository.cs @@ -100,10 +100,12 @@ namespace SourceGit.Models { // 未填写参数就检测,去掉无效的过滤 if (Filters.Count > 0) { var invalidFilters = new List(); + var branches = new Commands.Branches(Path).Result(); var tags = new Commands.Tags(Path).Result(); + foreach (var filter in Filters) { if (filter.StartsWith("refs/")) { - if (!ExistsInGitDir(filter)) invalidFilters.Add(filter); + if (branches.FindIndex(b => b.FullName == filter) < 0) invalidFilters.Add(filter); } else { if (tags.FindIndex(t => t.Name == filter) < 0) invalidFilters.Add(filter); } From f92f5746b9b147c4bdb524039fc9a07db115e7cd Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 21 Aug 2023 20:33:41 +0800 Subject: [PATCH 0002/2773] feature: supports clear all stashes --- src/Resources/Locales/en_US.xaml | 1 + src/Resources/Locales/zh_CN.xaml | 1 + src/Views/Widgets/Stashes.xaml | 22 ++++++++++++++++++++-- src/Views/Widgets/Stashes.xaml.cs | 24 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Resources/Locales/en_US.xaml b/src/Resources/Locales/en_US.xaml index da900303..30ea054c 100644 --- a/src/Resources/Locales/en_US.xaml +++ b/src/Resources/Locales/en_US.xaml @@ -542,4 +542,5 @@ Invalid path for archive file This field is required You are removing repository '{0}'. Are you sure to continue? + You are trying to clear all stashes. Are you sure to continue? \ No newline at end of file diff --git a/src/Resources/Locales/zh_CN.xaml b/src/Resources/Locales/zh_CN.xaml index 9eafebf9..e7a48dfc 100644 --- a/src/Resources/Locales/zh_CN.xaml +++ b/src/Resources/Locales/zh_CN.xaml @@ -541,4 +541,5 @@ 非法的存档文件路径! 内容未填写! 正在将 '{0}' 从列表中移除,是否要继续? + 您正在丢弃所有的贮藏,一经操作,无法回退,是否继续? \ No newline at end of file diff --git a/src/Views/Widgets/Stashes.xaml b/src/Views/Widgets/Stashes.xaml index d83b3715..6faf5a6c 100644 --- a/src/Views/Widgets/Stashes.xaml +++ b/src/Views/Widgets/Stashes.xaml @@ -26,25 +26,43 @@ - + + + + + + + + + - + + + diff --git a/src/Views/Widgets/Stashes.xaml.cs b/src/Views/Widgets/Stashes.xaml.cs index 118436d9..a7dac347 100644 --- a/src/Views/Widgets/Stashes.xaml.cs +++ b/src/Views/Widgets/Stashes.xaml.cs @@ -25,6 +25,30 @@ namespace SourceGit.Views.Widgets { changeList.ItemsSource = null; } + private void ClearAll(object sender, RoutedEventArgs e) { + var confirmDialog = new ConfirmDialog( + App.Text("Apply.Warn"), + App.Text("ConfirmClearStashes"), + async () => { + waiting.Visibility = Visibility.Visible; + waiting.IsAnimating = true; + Models.Watcher.SetEnabled(repo, false); + await Task.Run(() => { + new Commands.Command() { + Cwd = repo, + Args = "stash clear", + }.Exec(); + }); + Models.Watcher.SetEnabled(repo, true); + waiting.Visibility = Visibility.Collapsed; + waiting.IsAnimating = false; + }); + + confirmDialog.Owner = App.Current.MainWindow; + confirmDialog.ShowDialog(); + e.Handled = true; + } + private async void OnStashSelectionChanged(object sender, SelectionChangedEventArgs e) { changeList.ItemsSource = null; selected = null; From 9006752705d8e2984e0e4818f992cb0ae1495e2b Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 21 Aug 2023 20:39:38 +0800 Subject: [PATCH 0003/2773] fix: fix stash and re-apply is not working on pull command --- src/Views/Popups/Pull.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/Popups/Pull.xaml.cs b/src/Views/Popups/Pull.xaml.cs index 1e297871..866d3fdc 100644 --- a/src/Views/Popups/Pull.xaml.cs +++ b/src/Views/Popups/Pull.xaml.cs @@ -47,7 +47,7 @@ namespace SourceGit.Views.Popups { return Task.Run(() => { Models.Watcher.SetEnabled(repo.Path, false); - var succ = new Commands.Pull(repo.Path, branch.Remote, branch.Name, rebase, autoStash, UpdateProgress).Exec(); + var succ = new Commands.Pull(repo.Path, branch.Remote, branch.Name, rebase, autoStash, UpdateProgress).Run(); Models.Watcher.SetEnabled(repo.Path, true); return succ; }); From 63a6ef256e77732b568e5c243a268752ea96a573 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 23 Aug 2023 11:39:47 +0800 Subject: [PATCH 0004/2773] feature: supports for customizing max number of displayed history commits --- src/Models/Preference.cs | 8 +++++- src/Resources/Locales/en_US.xaml | 7 ++--- src/Resources/Locales/zh_CN.xaml | 5 +--- src/Views/Preference.xaml | 40 ++++++++++++++++++++++++----- src/Views/Widgets/Histories.xaml.cs | 2 +- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/Models/Preference.cs b/src/Models/Preference.cs index ddb1fab1..dfc76c62 100644 --- a/src/Models/Preference.cs +++ b/src/Models/Preference.cs @@ -54,7 +54,8 @@ namespace SourceGit.Models { /// public string FontFamilyContentSetting { get; set; } = "Consolas"; - [JsonIgnore] public string FontFamilyContent { + [JsonIgnore] + public string FontFamilyContent { get => FontFamilyContentSetting + ",Microsoft YaHei UI"; set => FontFamilyContentSetting = value; } @@ -64,6 +65,11 @@ namespace SourceGit.Models { /// public bool UseDarkTheme { get; set; } = false; + /// + /// 历史提交记录最多显示的条目数 + /// + public uint MaxHistoryCommits { get; set; } = 20000; + /// /// 起始页仓库列表排序规则 /// diff --git a/src/Resources/Locales/en_US.xaml b/src/Resources/Locales/en_US.xaml index 30ea054c..02edad22 100644 --- a/src/Resources/Locales/en_US.xaml +++ b/src/Resources/Locales/en_US.xaml @@ -355,15 +355,12 @@ Preference GENERAL - Language + Display Language Window Font Content Font - Avatar Server - Check for update Use dark theme Restore windows - Enable crash report (maybe include related path) - Use Windows Terminal instead of cmd.exe + Max History Commits GIT Install Path Input path for git.exe diff --git a/src/Resources/Locales/zh_CN.xaml b/src/Resources/Locales/zh_CN.xaml index e7a48dfc..766c80eb 100644 --- a/src/Resources/Locales/zh_CN.xaml +++ b/src/Resources/Locales/zh_CN.xaml @@ -357,12 +357,9 @@ 显示语言 系统字体 文本字体 - 头像服务 - 启用检测更新 启用暗色主题 启动时恢复上次打开的仓库 - 开启崩溃上报(可能涉及上报相关路径) - 使用 Windows Terminal 打开 Git 终端 + 最大历史提交数 GIT配置 安装路径 填写git.exe所在位置 diff --git a/src/Views/Preference.xaml b/src/Views/Preference.xaml index ec9b371a..26fd6b03 100644 --- a/src/Views/Preference.xaml +++ b/src/Views/Preference.xaml @@ -68,7 +68,7 @@ - + @@ -130,17 +130,45 @@ + + + + + + + + + + + + + + + @@ -160,7 +188,7 @@ - + @@ -297,7 +325,7 @@ - + @@ -358,7 +386,7 @@ - + diff --git a/src/Views/Widgets/Histories.xaml.cs b/src/Views/Widgets/Histories.xaml.cs index 58e05639..46bc1095 100644 --- a/src/Views/Widgets/Histories.xaml.cs +++ b/src/Views/Widgets/Histories.xaml.cs @@ -51,7 +51,7 @@ namespace SourceGit.Views.Widgets { }); Task.Run(() => { - var limits = "-20000 "; + var limits = $"-{Models.Preference.Instance.General.MaxHistoryCommits} "; repo.UpdateFilters(); if (repo.Filters.Count > 0) { From dfc452b2a6ac635d69ed6258acfffa5689fa84d8 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 23 Aug 2023 14:05:19 +0800 Subject: [PATCH 0005/2773] update: using manager instead of manager-core for credential.helper --- src/Commands/Clone.cs | 2 +- src/Commands/Fetch.cs | 4 ++-- src/Commands/Pull.cs | 2 +- src/Commands/Push.cs | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index 1f845cc8..d1542a4c 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -19,7 +19,7 @@ namespace SourceGit.Commands { Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); Args = ""; } else { - Args = "-c credential.helper=manager-core "; + Args = "-c credential.helper=manager "; } Args += "clone --progress --verbose --recurse-submodules "; diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 4bf156f9..49359462 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -19,7 +19,7 @@ namespace SourceGit.Commands { Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); Args = ""; } else { - Args = "-c credential.helper=manager-core "; + Args = "-c credential.helper=manager "; } Args += "fetch --progress --verbose "; @@ -38,7 +38,7 @@ namespace SourceGit.Commands { Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); Args = ""; } else { - Args = "-c credential.helper=manager-core "; + Args = "-c credential.helper=manager "; } Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}"; diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index f447c52e..eb3b4f4c 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -20,7 +20,7 @@ namespace SourceGit.Commands { Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); Args = ""; } else { - Args = "-c credential.helper=manager-core "; + Args = "-c credential.helper=manager "; } Args += "pull --verbose --progress --tags "; diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index 7bfd05e6..c2faed97 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -17,7 +17,7 @@ namespace SourceGit.Commands { Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); Args = ""; } else { - Args = "-c credential.helper=manager-core "; + Args = "-c credential.helper=manager "; } Args += "push --progress --verbose "; @@ -37,7 +37,7 @@ namespace SourceGit.Commands { Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); Args = ""; } else { - Args = "-c credential.helper=manager-core "; + Args = "-c credential.helper=manager "; } Args += $"push {remote} --delete {branch}"; @@ -51,7 +51,7 @@ namespace SourceGit.Commands { Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); Args = ""; } else { - Args = "-c credential.helper=manager-core "; + Args = "-c credential.helper=manager "; } Args += "push "; From fc43edb6d20f404000d9b2bf4b63110926413288 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 23 Aug 2023 16:45:45 +0800 Subject: [PATCH 0006/2773] fix: fix parse repository's SSH URL --- src/Views/Clone.xaml.cs | 2 +- src/Views/Validations/GitURL.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Views/Clone.xaml.cs b/src/Views/Clone.xaml.cs index abc66330..bac20bfa 100644 --- a/src/Views/Clone.xaml.cs +++ b/src/Views/Clone.xaml.cs @@ -14,7 +14,7 @@ namespace SourceGit.Views { /// public partial class Clone : Controls.Window { private static readonly Regex[] SSH_PROTOCOAL = new Regex[] { - new Regex(@"[\w\-]+@[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), + new Regex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-]+\.git$"), new Regex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), }; diff --git a/src/Views/Validations/GitURL.cs b/src/Views/Validations/GitURL.cs index ea13e198..b726265b 100644 --- a/src/Views/Validations/GitURL.cs +++ b/src/Views/Validations/GitURL.cs @@ -7,7 +7,7 @@ namespace SourceGit.Views.Validations { public class GitURL : ValidationRule { private static readonly Regex[] VALID_FORMATS = new Regex[] { new Regex(@"^http[s]?://[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), - new Regex(@"[\w\-]+@[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), + new Regex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-]+\.git$"), new Regex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), }; From 1c10d9a286838863131bb5556f8a4e8a327678b7 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 23 Aug 2023 20:45:12 +0800 Subject: [PATCH 0007/2773] optimize: using core.sshCommand instead of environment parameter GIT_SSH_COMMAND --- src/Commands/Clone.cs | 5 ++--- src/Commands/Command.cs | 7 ------- src/Commands/Fetch.cs | 6 ++---- src/Commands/Pull.cs | 3 +-- src/Commands/Push.cs | 9 +++------ src/Views/Popups/Remote.xaml.cs | 16 ++++++++-------- 6 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index d1542a4c..fefc0f3e 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -15,9 +15,8 @@ namespace SourceGit.Commands { handler = outputHandler; onError = errHandler; - if (!string.IsNullOrEmpty(sshKey)) { - Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); - Args = ""; + if (string.IsNullOrEmpty(sshKey)) { + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; } else { Args = "-c credential.helper=manager "; } diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 6e0d1dd5..f0ff0d72 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -52,11 +52,6 @@ namespace SourceGit.Commands { /// public bool TraitErrorAsOutput { get; set; } = false; - /// - /// 用于设置该进程独有的环境变量 - /// - public Dictionary Envs { get; set; } = new Dictionary(); - /// /// 运行 /// @@ -73,8 +68,6 @@ namespace SourceGit.Commands { if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd; - foreach (var kv in Envs) start.EnvironmentVariables[kv.Key] = kv.Value; - var progressFilter = new Regex(@"\s\d+%\s"); var errs = new List(); var proc = new Process() { StartInfo = start }; diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 49359462..ad3925cc 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -16,8 +16,7 @@ namespace SourceGit.Commands { var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); if (!string.IsNullOrEmpty(sshKey)) { - Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); - Args = ""; + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; } else { Args = "-c credential.helper=manager "; } @@ -35,8 +34,7 @@ namespace SourceGit.Commands { var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); if (!string.IsNullOrEmpty(sshKey)) { - Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); - Args = ""; + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; } else { Args = "-c credential.helper=manager "; } diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index eb3b4f4c..83b27dc4 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -17,8 +17,7 @@ namespace SourceGit.Commands { var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); if (!string.IsNullOrEmpty(sshKey)) { - Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); - Args = ""; + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; } else { Args = "-c credential.helper=manager "; } diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index c2faed97..7784c581 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -14,8 +14,7 @@ namespace SourceGit.Commands { var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); if (!string.IsNullOrEmpty(sshKey)) { - Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); - Args = ""; + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; } else { Args = "-c credential.helper=manager "; } @@ -34,8 +33,7 @@ namespace SourceGit.Commands { var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); if (!string.IsNullOrEmpty(sshKey)) { - Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); - Args = ""; + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; } else { Args = "-c credential.helper=manager "; } @@ -48,8 +46,7 @@ namespace SourceGit.Commands { var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); if (!string.IsNullOrEmpty(sshKey)) { - Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{sshKey}'"); - Args = ""; + Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; } else { Args = "-c credential.helper=manager "; } diff --git a/src/Views/Popups/Remote.xaml.cs b/src/Views/Popups/Remote.xaml.cs index deb09322..24a2762d 100644 --- a/src/Views/Popups/Remote.xaml.cs +++ b/src/Views/Popups/Remote.xaml.cs @@ -52,13 +52,16 @@ namespace SourceGit.Views.Popups { return Task.Run(() => { Models.Watcher.SetEnabled(repo.Path, false); + + if (string.IsNullOrEmpty(sshKey)) { + new Commands.Config(repo.Path).Set($"remote.{RemoteName}.sshkey", null); + } else { + new Commands.Config(repo.Path).Set($"remote.{RemoteName}.sshkey", sshKey); + } + if (remote == null) { var succ = new Commands.Remote(repo.Path).Add(RemoteName, RemoteURL); if (succ) new Commands.Fetch(repo.Path, RemoteName, true, UpdateProgress).Exec(); - - if (!string.IsNullOrEmpty(sshKey)) { - new Commands.Config(repo.Path).Set($"remote.{RemoteName}.sshkey", sshKey); - } } else { if (remote.URL != RemoteURL) { var succ = new Commands.Remote(repo.Path).SetURL(remote.Name, RemoteURL); @@ -69,11 +72,8 @@ namespace SourceGit.Views.Popups { var succ = new Commands.Remote(repo.Path).Rename(remote.Name, RemoteName); if (succ) remote.Name = RemoteName; } - - if (!string.IsNullOrEmpty(sshKey)) { - new Commands.Config(repo.Path).Set($"remote.{RemoteName}.sshkey", sshKey); - } } + Models.Watcher.SetEnabled(repo.Path, true); return true; }); From 697879b6a504df4e5799910a71681b4235276182 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 23 Aug 2023 20:48:29 +0800 Subject: [PATCH 0008/2773] feature: supports for providing user on the HTTP/HTTPS git URL --- src/Views/Clone.xaml.cs | 21 ++++++--------------- src/Views/Popups/Remote.xaml.cs | 6 +++--- src/Views/Validations/GitURL.cs | 12 +++++++++++- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Views/Clone.xaml.cs b/src/Views/Clone.xaml.cs index bac20bfa..4161fbf9 100644 --- a/src/Views/Clone.xaml.cs +++ b/src/Views/Clone.xaml.cs @@ -1,8 +1,6 @@ using Microsoft.Win32; -using SourceGit.Views.Validations; using System; using System.IO; -using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -13,11 +11,6 @@ namespace SourceGit.Views { /// 克隆 /// public partial class Clone : Controls.Window { - private static readonly Regex[] SSH_PROTOCOAL = new Regex[] { - new Regex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-]+\.git$"), - new Regex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), - }; - public string Uri { get; set; } public string Folder { get; set; } public string LocalName { get; set; } @@ -118,14 +111,12 @@ namespace SourceGit.Views { } private void OnUrlChanged(object sender, TextChangedEventArgs e) { - foreach (var check in SSH_PROTOCOAL) { - if (check.IsMatch(txtUrl.Text)) { - rowSSHKey.Height = new GridLength(32, GridUnitType.Pixel); - return; - } - } - - rowSSHKey.Height = new GridLength(0, GridUnitType.Pixel); + var isSSHProtocal = Validations.GitURL.IsSSH(txtUrl.Text); + if (isSSHProtocal) { + rowSSHKey.Height = new GridLength(32, GridUnitType.Pixel); + } else { + rowSSHKey.Height = new GridLength(0, GridUnitType.Pixel); + } } private void OnCloseException(object s, RoutedEventArgs e) { diff --git a/src/Views/Popups/Remote.xaml.cs b/src/Views/Popups/Remote.xaml.cs index 24a2762d..6054d2a2 100644 --- a/src/Views/Popups/Remote.xaml.cs +++ b/src/Views/Popups/Remote.xaml.cs @@ -28,7 +28,7 @@ namespace SourceGit.Views.Popups { InitializeComponent(); ruleName.Repo = repo; - if (!string.IsNullOrEmpty(RemoteURL) && RemoteURL.StartsWith("git@")) { + if (Validations.GitURL.IsSSH(RemoteURL)) { txtSSHKey.Text = new Commands.Config(repo.Path).Get($"remote.{remote.Name}.sshkey"); } else { txtSSHKey.Text = ""; @@ -94,8 +94,8 @@ namespace SourceGit.Views.Popups { } private void OnUrlChanged(object sender, TextChangedEventArgs e) { - if (!string.IsNullOrEmpty(txtUrl.Text)) { - rowSSHKey.Height = new GridLength(txtUrl.Text.StartsWith("git@") ? 32 : 0, GridUnitType.Pixel); + if (Validations.GitURL.IsSSH(txtUrl.Text)) { + rowSSHKey.Height = new GridLength(32, GridUnitType.Pixel); } else { rowSSHKey.Height = new GridLength(0, GridUnitType.Pixel); } diff --git a/src/Views/Validations/GitURL.cs b/src/Views/Validations/GitURL.cs index b726265b..ae34da1e 100644 --- a/src/Views/Validations/GitURL.cs +++ b/src/Views/Validations/GitURL.cs @@ -6,11 +6,21 @@ namespace SourceGit.Views.Validations { public class GitURL : ValidationRule { private static readonly Regex[] VALID_FORMATS = new Regex[] { - new Regex(@"^http[s]?://[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), + new Regex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), new Regex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-]+\.git$"), new Regex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-]+\.git$"), }; + public static bool IsSSH(string url) { + if (string.IsNullOrEmpty(url)) return false; + + for (int i = 1; i < VALID_FORMATS.Length; i++) { + if (VALID_FORMATS[i].IsMatch(url)) return true; + } + + return false; + } + public override ValidationResult Validate(object value, CultureInfo cultureInfo) { string url = value as string; if (!string.IsNullOrEmpty(url)) { From a1bfbfe02e9051460bea03923317e6d0273fe8b4 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 24 Aug 2023 13:39:49 +0800 Subject: [PATCH 0009/2773] refactor: new blame tool --- src/Commands/Blame.cs | 17 +++++ src/Resources/Locales/en_US.xaml | 4 - src/Resources/Locales/zh_CN.xaml | 4 - src/Views/Blame.xaml | 97 ++++++++++++------------ src/Views/Blame.xaml.cs | 122 +++++++++++++------------------ 5 files changed, 121 insertions(+), 123 deletions(-) diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs index 1a12a9b6..5fc8332a 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -9,6 +9,8 @@ namespace SourceGit.Commands { public class Blame : Command { private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); private Data data = new Data(); + private bool needUnifyCommitSHA = false; + private int minSHALen = 0; public class Data { public List Lines = new List(); @@ -22,6 +24,15 @@ namespace SourceGit.Commands { public Data Result() { Exec(); + + if (needUnifyCommitSHA) { + foreach (var line in data.Lines) { + if (line.CommitSHA.Length > minSHALen) { + line.CommitSHA = line.CommitSHA.Substring(0, minSHALen); + } + } + } + return data; } @@ -52,6 +63,12 @@ namespace SourceGit.Commands { Content = content, }; + if (line[0] == '^') { + needUnifyCommitSHA = true; + if (minSHALen == 0) minSHALen = commit.Length; + else if (commit.Length < minSHALen) minSHALen = commit.Length; + } + data.Lines.Add(blameLine); } } diff --git a/src/Resources/Locales/en_US.xaml b/src/Resources/Locales/en_US.xaml index 02edad22..d71f24d0 100644 --- a/src/Resources/Locales/en_US.xaml +++ b/src/Resources/Locales/en_US.xaml @@ -49,10 +49,6 @@ Select archive file path Blame - Right click to see commit info - COMMIT SHA - AUTHOR - MODIFY TIME SUBMODULES Add Submodule diff --git a/src/Resources/Locales/zh_CN.xaml b/src/Resources/Locales/zh_CN.xaml index 766c80eb..b01417c2 100644 --- a/src/Resources/Locales/zh_CN.xaml +++ b/src/Resources/Locales/zh_CN.xaml @@ -48,10 +48,6 @@ 选择存档文件的存放路径 逐行追溯 - 右键点击查看所选行修改记录 - 提交指纹 - 修改者 - 修改时间 子模块 添加子模块 diff --git a/src/Views/Blame.xaml b/src/Views/Blame.xaml index c5a5cdec..299cca9a 100644 --- a/src/Views/Blame.xaml +++ b/src/Views/Blame.xaml @@ -15,13 +15,13 @@ - + @@ -34,8 +34,17 @@ + + + - + @@ -49,49 +58,72 @@ HorizontalAlignment="Stretch" Fill="{DynamicResource Brush.Border0}"/> - - - - - - - - - - - - - + SizeChanged="OnViewerSizeChanged" + SelectionChanged="OnSelectionChanged"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + @@ -111,30 +143,5 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/Blame.xaml.cs b/src/Views/Blame.xaml.cs index f84ce549..8d1340e9 100644 --- a/src/Views/Blame.xaml.cs +++ b/src/Views/Blame.xaml.cs @@ -1,10 +1,8 @@ using System.Collections.ObjectModel; using System.ComponentModel; -using System.Globalization; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Documents; using System.Windows.Media; namespace SourceGit.Views { @@ -12,33 +10,45 @@ namespace SourceGit.Views { /// 逐行追溯 /// public partial class Blame : Controls.Window { - private static readonly Brush[] BG = new Brush[] { - Brushes.Transparent, - new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)) - }; - - private string repo = null; - private string lastSHA = null; - private int lastBG = 1; - + /// + /// DataGrid数据源结构 + /// public class Record : INotifyPropertyChanged { - private Brush bg = null; - public event PropertyChangedEventHandler PropertyChanged; + /// + /// 原始Blame行数据 + /// public Models.BlameLine Line { get; set; } - public Brush OrgBG { get; set; } - public Brush BG { - get { return bg; } + + /// + /// 是否是第一行 + /// + public bool IsFirstLine { get; set; } = false; + + /// + /// 前一行与本行的提交不同 + /// + public bool IsFirstLineInGroup { get; set; } = false; + + /// + /// 是否当前选中,会影响背景色 + /// + private bool isSelected = false; + public bool IsSelected { + get { return isSelected; } set { - if (value != bg) { - bg = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("BG")); + if (isSelected != value) { + isSelected = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsSelected")); } } } } + /// + /// Blame数据 + /// public ObservableCollection Records { get; set; } public Blame(string repo, string file, string revision) { @@ -67,48 +77,33 @@ namespace SourceGit.Views { notSupport.Visibility = Visibility.Visible; }); } else { + string lastSHA = null; foreach (var line in rs.Lines) { var r = new Record(); r.Line = line; - r.BG = GetBG(line.CommitSHA); - r.OrgBG = r.BG; + r.IsSelected = false; + + if (line.CommitSHA != lastSHA) { + lastSHA = line.CommitSHA; + r.IsFirstLineInGroup = true; + } else { + r.IsFirstLineInGroup = false; + } + Records.Add(r); } + if (Records.Count > 0) Records[0].IsFirstLine = true; + Dispatcher.Invoke(() => { loading.IsAnimating = false; loading.Visibility = Visibility.Collapsed; - - var formatted = new FormattedText( - $"{Records.Count}", - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - new Typeface(blame.FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal), - 12.0, - Brushes.Black, - VisualTreeHelper.GetDpi(this).PixelsPerDip); - - var lineNumberWidth = formatted.Width + 16; - var minWidth = blame.ActualWidth - lineNumberWidth; - if (Records.Count * 16 > blame.ActualHeight) minWidth -= 8; - blame.Columns[0].Width = lineNumberWidth; - blame.Columns[1].MinWidth = minWidth; blame.ItemsSource = Records; - blame.UpdateLayout(); }); } }); } - private Brush GetBG(string sha) { - if (lastSHA != sha) { - lastSHA = sha; - lastBG = 1 - lastBG; - } - - return BG[lastBG]; - } - #region WINDOW_COMMANDS private void Minimize(object sender, RoutedEventArgs e) { SystemCommands.MinimizeWindow(this); @@ -148,8 +143,8 @@ namespace SourceGit.Views { var scroller = GetVisualChild(blame); if (scroller != null && scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8; - blame.Columns[1].MinWidth = minWidth; - blame.Columns[1].Width = DataGridLength.SizeToCells; + blame.Columns[2].MinWidth = minWidth; + blame.Columns[2].Width = DataGridLength.SizeToCells; blame.UpdateLayout(); } @@ -157,31 +152,18 @@ namespace SourceGit.Views { e.Handled = true; } - private void OnViewerContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var record = (sender as DataGridRow).DataContext as Record; - if (record == null) return; + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { + var r = blame.SelectedItem as Record; + if (r == null) return; - foreach (var r in Records) { - if (r.Line.CommitSHA == record.Line.CommitSHA) { - r.BG = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)); - } else { - r.BG = r.OrgBG; - } + Models.Watcher.Get(repo).NavigateTo(r.Line.CommitSHA); + + foreach (var one in Records) { + one.IsSelected = one.Line.CommitSHA == r.Line.CommitSHA; } - - Hyperlink link = new Hyperlink(new Run(record.Line.CommitSHA)); - link.ToolTip = App.Text("Goto"); - link.Click += (o, e) => { - Models.Watcher.Get(repo).NavigateTo(record.Line.CommitSHA); - e.Handled = true; - }; - - commitID.Content = link; - authorName.Text = record.Line.Author; - authorTime.Text = record.Line.Time; - popup.IsOpen = true; - ev.Handled = true; } #endregion + + private string repo = null; } } From 0a7a0bff4278909d759cf758c37a4a9a82c30e9a Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 24 Aug 2023 16:20:38 +0800 Subject: [PATCH 0010/2773] feature: show notification after saving patch successfully --- src/Resources/Locales/en_US.xaml | 2 ++ src/Resources/Locales/zh_CN.xaml | 2 ++ src/Views/ConfirmDialog.xaml | 6 ++---- src/Views/ConfirmDialog.xaml.cs | 22 +++++++++++++++++--- src/Views/Widgets/Histories.xaml.cs | 1 + src/Views/Widgets/Stashes.xaml.cs | 2 -- src/Views/Widgets/Welcome.xaml.cs | 1 - src/Views/Widgets/WorkingCopyChanges.xaml.cs | 2 ++ 8 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/Resources/Locales/en_US.xaml b/src/Resources/Locales/en_US.xaml index d71f24d0..f5fc5d01 100644 --- a/src/Resources/Locales/en_US.xaml +++ b/src/Resources/Locales/en_US.xaml @@ -15,6 +15,7 @@ FILTER Optional. SELECT FOLDER + NOTICE URL : Git Repository URL @@ -536,4 +537,5 @@ This field is required You are removing repository '{0}'. Are you sure to continue? You are trying to clear all stashes. Are you sure to continue? + Patch has been saved successfully! \ No newline at end of file diff --git a/src/Resources/Locales/zh_CN.xaml b/src/Resources/Locales/zh_CN.xaml index b01417c2..d0999df0 100644 --- a/src/Resources/Locales/zh_CN.xaml +++ b/src/Resources/Locales/zh_CN.xaml @@ -14,6 +14,7 @@ 过滤 选填 选择文件夹 + 系统提示 仓库地址 : 远程仓库地址 @@ -535,4 +536,5 @@ 内容未填写! 正在将 '{0}' 从列表中移除,是否要继续? 您正在丢弃所有的贮藏,一经操作,无法回退,是否继续? + 补丁已成功保存! \ No newline at end of file diff --git a/src/Views/ConfirmDialog.xaml b/src/Views/ConfirmDialog.xaml index b660f616..867fedb1 100644 --- a/src/Views/ConfirmDialog.xaml +++ b/src/Views/ConfirmDialog.xaml @@ -6,11 +6,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:SourceGit.Views.Controls" - xmlns:validations="clr-namespace:SourceGit.Views.Validations" mc:Ignorable="d" WindowStartupLocation="CenterOwner" Width="500" SizeToContent="Height" - ResizeMode="NoResize"> + ResizeMode="NoResize"> @@ -61,7 +60,6 @@ @@ -72,7 +70,7 @@ Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/About.axaml.cs b/src/Views/About.axaml.cs new file mode 100644 index 00000000..90d047ef --- /dev/null +++ b/src/Views/About.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using System.Reflection; + +namespace SourceGit.Views { + public partial class About : Window { + public string Version { + get; + private set; + } + + public About() { + var ver = Assembly.GetExecutingAssembly().GetName().Version; + Version = $"{ver.Major}.{ver.Minor}"; + DataContext = this; + InitializeComponent(); + } + + private void CloseWindow(object sender, RoutedEventArgs e) { + Close(); + } + + private void OnVisitAvaloniaUI(object sender, PointerPressedEventArgs e) { + Native.OS.OpenBrowser("https://www.avaloniaui.net/"); + e.Handled = true; + } + + private void OnVisitAvaloniaEdit(object sender, PointerPressedEventArgs e) { + Native.OS.OpenBrowser("https://www.nuget.org/packages/OneWare.AvaloniaEdit"); + e.Handled = true; + } + + private void OnVisitJetBrainsMonoFont(object sender, PointerPressedEventArgs e) { + Native.OS.OpenBrowser("https://www.jetbrains.com/lp/mono/"); + e.Handled = true; + } + } +} diff --git a/src/Views/About.xaml b/src/Views/About.xaml deleted file mode 100644 index 1b754a30..00000000 --- a/src/Views/About.xaml +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/About.xaml.cs b/src/Views/About.xaml.cs deleted file mode 100644 index cce5e618..00000000 --- a/src/Views/About.xaml.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Reflection; -using System.Windows; - -namespace SourceGit.Views { - - /// - /// 关于对话框 - /// - public partial class About : Controls.Window { - - public class Keymap { - public string Key { get; set; } - public string Desc { get; set; } - public Keymap(string k, string d) { Key = k; Desc = App.Text($"Hotkeys.{d}"); } - } - - public About() { - InitializeComponent(); - - var asm = Assembly.GetExecutingAssembly().GetName(); - version.Text = $"VERSION : v{asm.Version.Major}.{asm.Version.Minor}"; - - hotkeys.ItemsSource = new List() { - new Keymap("CTRL + T", "NewTab"), - new Keymap("CTRL + W", "CloseTab"), - new Keymap("CTRL + TAB", "NextTab"), - new Keymap("CTRL + [1-9]", "SwitchTo"), - new Keymap("CTRL + F", "Search"), - new Keymap("F5", "Refresh"), - new Keymap("SPACE", "ToggleStage"), - new Keymap("ESC", "CancelPopup"), - }; - } - - private void OnRequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) { - var info = new ProcessStartInfo("cmd", $"/c start {e.Uri.AbsoluteUri}"); - info.CreateNoWindow = true; - Process.Start(info); - } - - private void Quit(object sender, RoutedEventArgs e) { - Close(); - } - } -} diff --git a/src/Views/AddRemote.axaml b/src/Views/AddRemote.axaml new file mode 100644 index 00000000..4b460d79 --- /dev/null +++ b/src/Views/AddRemote.axaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/AddRemote.axaml.cs b/src/Views/AddRemote.axaml.cs new file mode 100644 index 00000000..f3640e30 --- /dev/null +++ b/src/Views/AddRemote.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views { + public partial class AddRemote : UserControl { + public AddRemote() { + InitializeComponent(); + } + + private async void SelectSSHKey(object sender, RoutedEventArgs e) { + var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] }; + var toplevel = TopLevel.GetTopLevel(this); + var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1) { + txtSSHKey.Text = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + } +} diff --git a/src/Views/AddSubmodule.axaml b/src/Views/AddSubmodule.axaml new file mode 100644 index 00000000..f7c70bfd --- /dev/null +++ b/src/Views/AddSubmodule.axaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/src/Views/AddSubmodule.axaml.cs b/src/Views/AddSubmodule.axaml.cs new file mode 100644 index 00000000..358faaa7 --- /dev/null +++ b/src/Views/AddSubmodule.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class AddSubmodule : UserControl { + public AddSubmodule() { + InitializeComponent(); + } + } +} diff --git a/src/Views/Apply.axaml b/src/Views/Apply.axaml new file mode 100644 index 00000000..979741c8 --- /dev/null +++ b/src/Views/Apply.axaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Apply.axaml.cs b/src/Views/Apply.axaml.cs new file mode 100644 index 00000000..3f61a4dd --- /dev/null +++ b/src/Views/Apply.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views { + public partial class Apply : UserControl { + public Apply() { + InitializeComponent(); + } + + private async void SelectPatchFile(object sender, RoutedEventArgs e) { + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel == null) return; + + var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [ new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }] }; + var selected = await topLevel.StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1) { + txtPatchFile.Text = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + } +} diff --git a/src/Views/Archive.axaml b/src/Views/Archive.axaml new file mode 100644 index 00000000..be2d0ddc --- /dev/null +++ b/src/Views/Archive.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Archive.axaml.cs b/src/Views/Archive.axaml.cs new file mode 100644 index 00000000..63e73448 --- /dev/null +++ b/src/Views/Archive.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views { + public partial class Archive : UserControl { + public Archive() { + InitializeComponent(); + } + + private async void SelectOutputFile(object sender, RoutedEventArgs e) { + var options = new FilePickerSaveOptions() { DefaultExtension = ".zip", FileTypeChoices = [ new FilePickerFileType("ZIP") { Patterns = [ "*.zip" ]}] }; + var toplevel = TopLevel.GetTopLevel(this); + var selected = await toplevel.StorageProvider.SaveFilePickerAsync(options); + if (selected != null) { + txtSaveFile.Text = selected.Path.LocalPath; + } + + e.Handled = true; + } + } +} diff --git a/src/Views/AssumeUnchanged.xaml b/src/Views/AssumeUnchanged.xaml deleted file mode 100644 index 01d5413f..00000000 --- a/src/Views/AssumeUnchanged.xaml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/AssumeUnchanged.xaml.cs b/src/Views/AssumeUnchanged.xaml.cs deleted file mode 100644 index 1336f0bb..00000000 --- a/src/Views/AssumeUnchanged.xaml.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; - -namespace SourceGit.Views { - /// - /// 管理不跟踪变更的文件 - /// - public partial class AssumeUnchanged : Controls.Window { - private string repo = null; - - public ObservableCollection Files { get; set; } - - public AssumeUnchanged(string repo) { - this.repo = repo; - this.Files = new ObservableCollection(); - - InitializeComponent(); - - Task.Run(() => { - var unchanged = new Commands.AssumeUnchanged(repo).View(); - Dispatcher.Invoke(() => { - if (unchanged.Count > 0) { - foreach (var file in unchanged) Files.Add(file); - - mask.Visibility = Visibility.Collapsed; - list.Visibility = Visibility.Visible; - list.ItemsSource = Files; - } else { - list.Visibility = Visibility.Collapsed; - mask.Visibility = Visibility.Visible; - } - }); - }); - } - - private void OnQuit(object sender, RoutedEventArgs e) { - Close(); - } - - private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { - e.Handled = true; - } - - private void Remove(object sender, RoutedEventArgs e) { - var btn = sender as Button; - if (btn == null) return; - - var file = btn.DataContext as string; - if (file == null) return; - - new Commands.AssumeUnchanged(repo).Remove(file); - Files.Remove(file); - - if (Files.Count == 0) { - list.Visibility = Visibility.Collapsed; - mask.Visibility = Visibility.Visible; - } - - e.Handled = true; - } - } -} diff --git a/src/Views/AssumeUnchangedManager.axaml b/src/Views/AssumeUnchangedManager.axaml new file mode 100644 index 00000000..b7c59691 --- /dev/null +++ b/src/Views/AssumeUnchangedManager.axaml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/AssumeUnchangedManager.axaml.cs b/src/Views/AssumeUnchangedManager.axaml.cs new file mode 100644 index 00000000..616db87d --- /dev/null +++ b/src/Views/AssumeUnchangedManager.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace SourceGit.Views { + public partial class AssumeUnchangedManager : Window { + public AssumeUnchangedManager() { + InitializeComponent(); + } + + private void CloseWindow(object sender, RoutedEventArgs e) { + Close(); + } + } +} diff --git a/src/Views/Avatar.cs b/src/Views/Avatar.cs new file mode 100644 index 00000000..3feb04bb --- /dev/null +++ b/src/Views/Avatar.cs @@ -0,0 +1,122 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using System; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; + +namespace SourceGit.Views { + public class Avatar : Control, Models.IAvatarHost { + private static readonly GradientStops[] FALLBACK_GRADIENTS = [ + new GradientStops() { new GradientStop(Colors.Orange, 0), new GradientStop(Color.FromRgb(255, 213, 134), 1) }, + new GradientStops() { new GradientStop(Colors.DodgerBlue, 0), new GradientStop(Colors.LightSkyBlue, 1) }, + new GradientStops() { new GradientStop(Colors.LimeGreen, 0), new GradientStop(Color.FromRgb(124, 241, 124), 1) }, + new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) }, + new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) }, + ]; + + public static readonly StyledProperty FallbackFontFamilyProperty = + AvaloniaProperty.Register(nameof(FallbackFontFamily)); + + public FontFamily FallbackFontFamily { + get => GetValue(FallbackFontFamilyProperty); + set => SetValue(FallbackFontFamilyProperty, value); + } + + public static readonly StyledProperty UserProperty = + AvaloniaProperty.Register(nameof(User)); + + public Models.User User { + get => GetValue(UserProperty); + set => SetValue(UserProperty, value); + } + + static Avatar() { + AffectsRender(FallbackFontFamilyProperty); + UserProperty.Changed.AddClassHandler(OnUserPropertyChanged); + } + + public Avatar() { + var refetch = new MenuItem() { Header = App.Text("RefetchAvatar") }; + refetch.Click += (o, e) => { + if (User != null) { + _image = Models.AvatarManager.Request(_emailMD5, true); + InvalidateVisual(); + } + }; + + ContextMenu = new ContextMenu(); + ContextMenu.Items.Add(refetch); + + Models.AvatarManager.Subscribe(this); + } + + public override void Render(DrawingContext context) { + if (User == null) return; + + float corner = (float)Math.Max(2, Bounds.Width / 16); + if (_image != null) { + var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); + context.PushClip(new RoundedRect(rect, corner)); + context.DrawImage(_image, rect); + } else { + Point textOrigin = new Point((Bounds.Width - _fallbackLabel.Width) * 0.5, (Bounds.Height - _fallbackLabel.Height) * 0.5); + context.DrawRectangle(_fallbackBrush, null, new Rect(0, 0, Bounds.Width, Bounds.Height), corner, corner); + context.DrawText(_fallbackLabel, textOrigin); + } + } + + public void OnAvatarResourceReady(string md5, Bitmap bitmap) { + if (_emailMD5 == md5) { + _image = bitmap; + InvalidateVisual(); + } + } + + private static void OnUserPropertyChanged(Avatar avatar, AvaloniaPropertyChangedEventArgs e) { + if (avatar.User == null) { + avatar._emailMD5 = null; + return; + } + + var placeholder = string.IsNullOrWhiteSpace(avatar.User.Name) ? "?" : avatar.User.Name.Substring(0, 1); + var chars = placeholder.ToCharArray(); + var sum = 0; + foreach (var c in chars) sum += Math.Abs(c); + + var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(avatar.User.Email.ToLower().Trim())); + var builder = new StringBuilder(); + foreach (var c in hash) builder.Append(c.ToString("x2")); + var md5 = builder.ToString(); + if (avatar._emailMD5 != md5) { + avatar._emailMD5 = md5; + avatar._image = Models.AvatarManager.Request(md5, false); + } + + avatar._fallbackBrush = new LinearGradientBrush { + GradientStops = FALLBACK_GRADIENTS[sum % FALLBACK_GRADIENTS.Length], + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }; + + var typeface = avatar.FallbackFontFamily == null ? Typeface.Default : new Typeface(avatar.FallbackFontFamily); + + avatar._fallbackLabel = new FormattedText( + placeholder, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + avatar.Width * 0.65, + Brushes.White); + + avatar.InvalidateVisual(); + } + + private FormattedText _fallbackLabel = null; + private LinearGradientBrush _fallbackBrush = null; + private string _emailMD5 = null; + private Bitmap _image = null; + } +} diff --git a/src/Views/Blame.axaml b/src/Views/Blame.axaml new file mode 100644 index 00000000..d3d054da --- /dev/null +++ b/src/Views/Blame.axaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs new file mode 100644 index 00000000..5492cba7 --- /dev/null +++ b/src/Views/Blame.axaml.cs @@ -0,0 +1,261 @@ +using Avalonia; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Styling; +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using AvaloniaEdit.Rendering; +using AvaloniaEdit.TextMate; +using AvaloniaEdit.Utils; +using System; +using System.Globalization; +using System.IO; +using TextMateSharp.Grammars; + +namespace SourceGit.Views { + public class BlameTextEditor : TextEditor { + public class CommitInfoMargin : AbstractMargin { + public CommitInfoMargin(BlameTextEditor editor) { + _editor = editor; + ClipToBounds = true; + } + + public override void Render(DrawingContext context) { + if (_editor.BlameData == null) return; + + var view = TextView; + if (view != null && view.VisualLinesValid) { + var typeface = view.CreateTypeface(); + var underlinePen = new Pen(Brushes.DarkOrange, 1); + + foreach (var line in view.VisualLines) { + var lineNumber = line.FirstDocumentLine.LineNumber; + if (lineNumber > _editor.BlameData.LineInfos.Count) break; + + var info = _editor.BlameData.LineInfos[lineNumber - 1]; + if (!info.IsFirstInGroup) continue; + + var x = 0.0; + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; + + var shaLink = new FormattedText( + info.CommitSHA, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + Brushes.DarkOrange); + context.DrawText(shaLink, new Point(x, y)); + context.DrawLine(underlinePen, new Point(x, y + shaLink.Baseline + 2), new Point(x + shaLink.Width, y + shaLink.Baseline + 2)); + x += shaLink.Width + 8; + + var time = new FormattedText( + info.Time, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + _editor.Foreground); + context.DrawText(time, new Point(x, y)); + x += time.Width + 8; + + var author = new FormattedText( + info.Author, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + _editor.Foreground); + context.DrawText(author, new Point(x, y)); + } + } + } + + protected override Size MeasureOverride(Size availableSize) { + return new Size(250, 0); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) { + base.OnPointerPressed(e); + + var view = TextView; + if (!e.Handled && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && view != null && view.VisualLinesValid) { + var pos = e.GetPosition(this); + var typeface = view.CreateTypeface(); + + foreach (var line in view.VisualLines) { + var lineNumber = line.FirstDocumentLine.LineNumber; + if (lineNumber >= _editor.BlameData.LineInfos.Count) break; + + var info = _editor.BlameData.LineInfos[lineNumber - 1]; + if (!info.IsFirstInGroup) continue; + + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; + var shaLink = new FormattedText( + info.CommitSHA, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + Brushes.DarkOrange); + + var rect = new Rect(0, y, shaLink.Width, shaLink.Height); + if (rect.Contains(pos)) { + _editor.OnCommitSHAClicked(info.CommitSHA); + e.Handled = true; + break; + } + } + } + } + + private BlameTextEditor _editor = null; + } + + public class VerticalSeperatorMargin : AbstractMargin { + public VerticalSeperatorMargin(BlameTextEditor editor) { + _editor = editor; + } + + public override void Render(DrawingContext context) { + var pen = new Pen(_editor.BorderBrush, 1); + context.DrawLine(pen, new Point(0, 0), new Point(0, Bounds.Height)); + } + + protected override Size MeasureOverride(Size availableSize) { + return new Size(1, 0); + } + + private BlameTextEditor _editor = null; + } + + public static readonly StyledProperty BlameDataProperty = + AvaloniaProperty.Register(nameof(BlameData)); + + public Models.BlameData BlameData { + get => GetValue(BlameDataProperty); + set => SetValue(BlameDataProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextEditor); + + public BlameTextEditor() : base(new TextArea(), new TextDocument()) { + IsReadOnly = true; + ShowLineNumbers = false; + WordWrap = false; + } + + public void OnCommitSHAClicked(string sha) { + if (DataContext is ViewModels.Blame blame) { + blame.NavigateToCommit(sha); + } + } + + protected override void OnLoaded(RoutedEventArgs e) { + base.OnLoaded(e); + + TextArea.LeftMargins.Add(new LineNumberMargin() { Margin = new Thickness(8, 0) }); + TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this)); + TextArea.LeftMargins.Add(new CommitInfoMargin(this) { Margin = new Thickness(8, 0) }); + TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this)); + TextArea.TextView.ContextRequested += OnTextViewContextRequested; + TextArea.TextView.Margin = new Thickness(4, 0); + + if (App.Current?.ActualThemeVariant == ThemeVariant.Dark) { + _registryOptions = new RegistryOptions(ThemeName.DarkPlus); + } else { + _registryOptions = new RegistryOptions(ThemeName.LightPlus); + } + + _textMate = this.InstallTextMate(_registryOptions); + + if (BlameData != null) { + _textMate.SetGrammar(_registryOptions.GetScopeByExtension(Path.GetExtension(BlameData.File))); + } + } + + protected override void OnUnloaded(RoutedEventArgs e) { + base.OnUnloaded(e); + + TextArea.LeftMargins.Clear(); + TextArea.TextView.ContextRequested -= OnTextViewContextRequested; + + _registryOptions = null; + _textMate.Dispose(); + _textMate = null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { + base.OnPropertyChanged(change); + + if (change.Property == BlameDataProperty) { + if (BlameData != null) { + Text = BlameData.Content; + if (_textMate != null) _textMate.SetGrammar(_registryOptions.GetScopeByExtension(Path.GetExtension(BlameData.File))); + } else { + Text = string.Empty; + } + } else if (change.Property.Name == "ActualThemeVariant" && change.NewValue != null && _textMate != null) { + if (App.Current?.ActualThemeVariant == ThemeVariant.Dark) { + _textMate.SetTheme(_registryOptions.LoadTheme(ThemeName.DarkPlus)); + } else { + _textMate.SetTheme(_registryOptions.LoadTheme(ThemeName.LightPlus)); + } + } + } + + private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) { + var selected = SelectedText; + if (string.IsNullOrEmpty(selected)) return; + + var icon = new Avalonia.Controls.Shapes.Path(); + icon.Width = 10; + icon.Height = 10; + icon.Stretch = Stretch.Uniform; + icon.Data = App.Current?.FindResource("Icons.Copy") as StreamGeometry; + + var copy = new MenuItem(); + copy.Header = App.Text("Copy"); + copy.Icon = icon; + copy.Click += (o, ev) => { + App.CopyText(selected); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(copy); + menu.Open(TextArea.TextView); + e.Handled = true; + } + + private RegistryOptions _registryOptions = null; + private TextMate.Installation _textMate = null; + } + + public partial class Blame : Window { + public Blame() { + if (App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + Owner = desktop.MainWindow; + } + + InitializeComponent(); + } + + protected override void OnClosed(EventArgs e) { + base.OnClosed(e); + GC.Collect(); + } + + private void OnCommitSHAPointerPressed(object sender, PointerPressedEventArgs e) { + if (DataContext is ViewModels.Blame blame) { + var txt = sender as TextBlock; + blame.NavigateToCommit(txt.Text); + } + e.Handled = true; + } + } +} diff --git a/src/Views/Blame.xaml b/src/Views/Blame.xaml deleted file mode 100644 index f2c46655..00000000 --- a/src/Views/Blame.xaml +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/Blame.xaml.cs b/src/Views/Blame.xaml.cs deleted file mode 100644 index 2a8c1598..00000000 --- a/src/Views/Blame.xaml.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Navigation; - -namespace SourceGit.Views { - /// - /// 逐行追溯 - /// - public partial class Blame : Controls.Window { - /// - /// DataGrid数据源结构 - /// - public class Record : INotifyPropertyChanged { - public event PropertyChangedEventHandler PropertyChanged; - - /// - /// 原始Blame行数据 - /// - public Models.BlameLine Line { get; set; } - - /// - /// 是否是第一行 - /// - public bool IsFirstLine { get; set; } = false; - - /// - /// 前一行与本行的提交不同 - /// - public bool IsFirstLineInGroup { get; set; } = false; - - /// - /// 是否当前选中,会影响背景色 - /// - private bool isSelected = false; - public bool IsSelected { - get { return isSelected; } - set { - if (isSelected != value) { - isSelected = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsSelected")); - } - } - } - } - - /// - /// Blame数据 - /// - public ObservableCollection Records { get; set; } - - public Blame(string repo, string file, string revision) { - InitializeComponent(); - - this.repo = repo; - Records = new ObservableCollection(); - txtFile.Text = $"{file}@{revision.Substring(0, 8)}"; - - Task.Run(() => { - var lfs = new Commands.LFS(repo).IsFiltered(file); - if (lfs) { - Dispatcher.Invoke(() => { - loading.IsAnimating = false; - loading.Visibility = Visibility.Collapsed; - notSupport.Visibility = Visibility.Visible; - }); - return; - } - - var rs = new Commands.Blame(repo, file, revision).Result(); - if (rs.IsBinary) { - Dispatcher.Invoke(() => { - loading.IsAnimating = false; - loading.Visibility = Visibility.Collapsed; - notSupport.Visibility = Visibility.Visible; - }); - } else { - string lastSHA = null; - foreach (var line in rs.Lines) { - var r = new Record(); - r.Line = line; - r.IsSelected = false; - - if (line.CommitSHA != lastSHA) { - lastSHA = line.CommitSHA; - r.IsFirstLineInGroup = true; - } else { - r.IsFirstLineInGroup = false; - } - - Records.Add(r); - } - - if (Records.Count > 0) Records[0].IsFirstLine = true; - - Dispatcher.Invoke(() => { - loading.IsAnimating = false; - loading.Visibility = Visibility.Collapsed; - blame.ItemsSource = Records; - }); - } - }); - } - - #region WINDOW_COMMANDS - private void Minimize(object sender, RoutedEventArgs e) { - SystemCommands.MinimizeWindow(this); - } - - private void Quit(object sender, RoutedEventArgs e) { - Close(); - } - #endregion - - #region EVENTS - private T GetVisualChild(DependencyObject parent) where T : Visual { - T child = null; - - int count = VisualTreeHelper.GetChildrenCount(parent); - for (int i = 0; i < count; i++) { - Visual v = (Visual)VisualTreeHelper.GetChild(parent, i); - child = v as T; - - if (child == null) { - child = GetVisualChild(v); - } - - if (child != null) { - break; - } - } - - return child; - } - - private void OnViewerSizeChanged(object sender, SizeChangedEventArgs e) { - var total = blame.ActualWidth; - var offset = blame.NonFrozenColumnsViewportHorizontalOffset; - var minWidth = total - offset; - - var scroller = GetVisualChild(blame); - if (scroller != null && scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8; - - blame.Columns[2].MinWidth = minWidth; - blame.Columns[2].Width = DataGridLength.SizeToCells; - blame.UpdateLayout(); - } - - private void OnViewerRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { - e.Handled = true; - } - - private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { - var r = blame.SelectedItem as Record; - if (r == null) return; - - foreach (var one in Records) { - one.IsSelected = one.Line.CommitSHA == r.Line.CommitSHA; - } - } - - private void GotoCommit(object sender, RequestNavigateEventArgs e) { - Models.Watcher.Get(repo).NavigateTo(e.Uri.OriginalString); - e.Handled = true; - } - #endregion - - private string repo = null; - } -} diff --git a/src/Views/CaptionButtons.axaml b/src/Views/CaptionButtons.axaml new file mode 100644 index 00000000..581f7be1 --- /dev/null +++ b/src/Views/CaptionButtons.axaml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/src/Views/CaptionButtons.axaml.cs b/src/Views/CaptionButtons.axaml.cs new file mode 100644 index 00000000..09f70dad --- /dev/null +++ b/src/Views/CaptionButtons.axaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace SourceGit.Views { + public partial class CaptionButtons : UserControl { + public CaptionButtons() { + InitializeComponent(); + } + + private void MinimizeWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.WindowState = WindowState.Minimized; + } + } + + private void MaximizeOrRestoreWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + } + + private void CloseWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.Close(); + } + } + } +} + diff --git a/src/Views/CaptionButtonsMacOS.axaml b/src/Views/CaptionButtonsMacOS.axaml new file mode 100644 index 00000000..fa3f1f34 --- /dev/null +++ b/src/Views/CaptionButtonsMacOS.axaml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/src/Views/CaptionButtonsMacOS.axaml.cs b/src/Views/CaptionButtonsMacOS.axaml.cs new file mode 100644 index 00000000..64b93d21 --- /dev/null +++ b/src/Views/CaptionButtonsMacOS.axaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace SourceGit.Views { + public partial class CaptionButtonsMacOS : UserControl { + public CaptionButtonsMacOS() { + InitializeComponent(); + } + + private void MinimizeWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.WindowState = WindowState.Minimized; + } + } + + private void MaximizeOrRestoreWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + } + + private void CloseWindow(object sender, RoutedEventArgs e) { + var window = this.FindAncestorOfType(); + if (window != null) { + window.Close(); + } + } + } +} + diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs new file mode 100644 index 00000000..8d0001e6 --- /dev/null +++ b/src/Views/ChangeStatusIcon.cs @@ -0,0 +1,112 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using System; +using System.Globalization; + +namespace SourceGit.Views { + public class ChangeStatusIcon : Control { + private static readonly IBrush[] BACKGROUNDS = [ + Brushes.Transparent, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + new LinearGradientBrush { + GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) }, + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }, + ]; + + private static readonly string[] INDICATOR = ["?", "±", "+", "−", "➜", "❏", "U", "★"]; + + public static readonly StyledProperty IsWorkingCopyChangeProperty = + AvaloniaProperty.Register(nameof(IsWorkingCopyChange)); + + public bool IsWorkingCopyChange { + get => GetValue(IsWorkingCopyChangeProperty); + set => SetValue(IsWorkingCopyChangeProperty, value); + } + + public static readonly StyledProperty ChangeProperty = + AvaloniaProperty.Register(nameof(Change)); + + public Models.Change Change { + get => GetValue(ChangeProperty); + set => SetValue(ChangeProperty, value); + } + + public static readonly StyledProperty IconFontFamilyProperty = + AvaloniaProperty.Register(nameof(IconFontFamily)); + + public FontFamily IconFontFamily { + get => GetValue(IconFontFamilyProperty); + set => SetValue(IconFontFamilyProperty, value); + } + + static ChangeStatusIcon() { + AffectsRender(IsWorkingCopyChangeProperty, ChangeProperty, IconFontFamilyProperty); + } + + public override void Render(DrawingContext context) { + if (Change == null || Bounds.Width <= 0) return; + + var typeface = IconFontFamily == null ? Typeface.Default : new Typeface(IconFontFamily); + + IBrush background = null; + string indicator; + if (IsWorkingCopyChange) { + if (Change.IsConflit) { + background = Brushes.OrangeRed; + indicator = "!"; + } else { + background = BACKGROUNDS[(int)Change.WorkTree]; + indicator = INDICATOR[(int)Change.WorkTree]; + } + } else { + background = BACKGROUNDS[(int)Change.Index]; + indicator = INDICATOR[(int)Change.Index]; + } + + var txt = new FormattedText( + indicator, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + Bounds.Width * 0.8, + Brushes.White); + + float corner = (float)Math.Max(2, Bounds.Width / 16); + Point textOrigin = new Point((Bounds.Width - txt.Width) * 0.5, (Bounds.Height - txt.Height) * 0.5); + context.DrawRectangle(background, null, new Rect(0, 0, Bounds.Width, Bounds.Height), corner, corner); + context.DrawText(txt, textOrigin); + } + } +} diff --git a/src/Views/ChangeViewModeSwitcher.axaml b/src/Views/ChangeViewModeSwitcher.axaml new file mode 100644 index 00000000..32d4f1e9 --- /dev/null +++ b/src/Views/ChangeViewModeSwitcher.axaml @@ -0,0 +1,35 @@ + + + diff --git a/src/Views/ChangeViewModeSwitcher.axaml.cs b/src/Views/ChangeViewModeSwitcher.axaml.cs new file mode 100644 index 00000000..59a041e9 --- /dev/null +++ b/src/Views/ChangeViewModeSwitcher.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class ChangeViewModeSwitcher : UserControl { + public static readonly StyledProperty ViewModeProperty = + AvaloniaProperty.Register(nameof(ViewMode)); + + public Models.ChangeViewMode ViewMode { + get => GetValue(ViewModeProperty); + set => SetValue(ViewModeProperty, value); + } + + public ChangeViewModeSwitcher() { + DataContext = this; + InitializeComponent(); + } + + public void SwitchMode(object param) { + ViewMode = (Models.ChangeViewMode)param; + } + } +} diff --git a/src/Views/Checkout.axaml b/src/Views/Checkout.axaml new file mode 100644 index 00000000..f921326b --- /dev/null +++ b/src/Views/Checkout.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/src/Views/Checkout.axaml.cs b/src/Views/Checkout.axaml.cs new file mode 100644 index 00000000..d2d9edca --- /dev/null +++ b/src/Views/Checkout.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class Checkout : UserControl { + public Checkout() { + InitializeComponent(); + } + } +} diff --git a/src/Views/CherryPick.axaml b/src/Views/CherryPick.axaml new file mode 100644 index 00000000..a4b654fd --- /dev/null +++ b/src/Views/CherryPick.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/src/Views/CherryPick.axaml.cs b/src/Views/CherryPick.axaml.cs new file mode 100644 index 00000000..306d4702 --- /dev/null +++ b/src/Views/CherryPick.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class CherryPick : UserControl { + public CherryPick() { + InitializeComponent(); + } + } +} diff --git a/src/Views/Cleanup.axaml b/src/Views/Cleanup.axaml new file mode 100644 index 00000000..7e5d8ff9 --- /dev/null +++ b/src/Views/Cleanup.axaml @@ -0,0 +1,19 @@ + + + + + + diff --git a/src/Views/Cleanup.axaml.cs b/src/Views/Cleanup.axaml.cs new file mode 100644 index 00000000..a098d9a4 --- /dev/null +++ b/src/Views/Cleanup.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class Cleanup : UserControl { + public Cleanup() { + InitializeComponent(); + } + } +} diff --git a/src/Views/ClearStashes.axaml b/src/Views/ClearStashes.axaml new file mode 100644 index 00000000..666bf273 --- /dev/null +++ b/src/Views/ClearStashes.axaml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/src/Views/ClearStashes.axaml.cs b/src/Views/ClearStashes.axaml.cs new file mode 100644 index 00000000..d95e18c1 --- /dev/null +++ b/src/Views/ClearStashes.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class ClearStashes : UserControl { + public ClearStashes() { + InitializeComponent(); + } + } +} diff --git a/src/Views/Clone.axaml b/src/Views/Clone.axaml new file mode 100644 index 00000000..ca3a4d3f --- /dev/null +++ b/src/Views/Clone.axaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Clone.axaml.cs b/src/Views/Clone.axaml.cs new file mode 100644 index 00000000..cd52e0bf --- /dev/null +++ b/src/Views/Clone.axaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views { + public partial class Clone : UserControl { + public Clone() { + InitializeComponent(); + } + + private async void SelectParentFolder(object sender, RoutedEventArgs e) { + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + var toplevel = TopLevel.GetTopLevel(this); + var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options); + if (selected.Count == 1) { + txtParentFolder.Text = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + + private async void SelectSSHKey(object sender, RoutedEventArgs e) { + var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] }; + var toplevel = TopLevel.GetTopLevel(this); + var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1) { + txtSSHKey.Text = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + } +} diff --git a/src/Views/Clone.xaml b/src/Views/Clone.xaml deleted file mode 100644 index 07536e82..00000000 --- a/src/Views/Clone.xaml +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitChanges.axaml.cs b/src/Views/CommitChanges.axaml.cs new file mode 100644 index 00000000..db65922d --- /dev/null +++ b/src/Views/CommitChanges.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; + +namespace SourceGit.Views { + public partial class CommitChanges : UserControl { + public CommitChanges() { + InitializeComponent(); + } + + private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) { + if (sender is DataGrid datagrid && datagrid.IsVisible && datagrid.SelectedItem != null) { + datagrid.ScrollIntoView(datagrid.SelectedItem, null); + } + e.Handled = true; + } + + private void OnDataGridContextRequested(object sender, ContextRequestedEventArgs e) { + if (sender is DataGrid datagrid && datagrid.SelectedItem != null) { + var detail = DataContext as ViewModels.CommitDetail; + var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); + menu.Open(datagrid); + } + + e.Handled = true; + } + + private void OnTreeViewContextRequested(object sender, ContextRequestedEventArgs e) { + if (sender is TreeView view && view.SelectedItem != null) { + var detail = DataContext as ViewModels.CommitDetail; + var node = view.SelectedItem as ViewModels.FileTreeNode; + if (node != null && !node.IsFolder) { + var menu = detail.CreateChangeContextMenu(node.Backend as Models.Change); + menu.Open(view); + } + } + + e.Handled = true; + } + } +} diff --git a/src/Views/CommitDetail.axaml b/src/Views/CommitDetail.axaml new file mode 100644 index 00000000..47d9fde7 --- /dev/null +++ b/src/Views/CommitDetail.axaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs new file mode 100644 index 00000000..326c612d --- /dev/null +++ b/src/Views/CommitDetail.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views { + public partial class CommitDetail : UserControl { + public CommitDetail() { + InitializeComponent(); + } + + private void OnChangeListDoubleTapped(object sender, TappedEventArgs e) { + if (DataContext is ViewModels.CommitDetail detail) { + var datagrid = sender as DataGrid; + detail.ActivePageIndex = 1; + detail.SelectedChange = datagrid.SelectedItem as Models.Change; + } + e.Handled = true; + } + + private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) { + if (DataContext is ViewModels.CommitDetail detail) { + var datagrid = sender as DataGrid; + var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); + menu.Open(datagrid); + } + e.Handled = true; + } + } +} diff --git a/src/Views/ConfirmDialog.xaml b/src/Views/ConfirmDialog.xaml deleted file mode 100644 index 867fedb1..00000000 --- a/src/Views/ConfirmDialog.xaml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -