From 6930b51c64b3298cfa3c793d96c50cbe6624ea56 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 9 Jul 2024 10:16:15 +0800 Subject: [PATCH] refactor: commandline parsing * `--rebase-todo-editor` launches this app as a git `sequence.editor` * `--rebase-message-editor` launches this app as a git `core.editor` which runs on background by reading rebasing jobs * `--core-editor` launches this app as a git `core.editor` * `--askpass` launches this app as a SSH askpass program --- src/App.JsonCodeGen.cs | 2 +- src/App.axaml.cs | 162 +++++++++++++++++++++----- src/Commands/Command.cs | 31 ++++- src/Commands/Rebase.cs | 10 +- src/Models/InteractiveRebase.cs | 26 +++++ src/Models/InteractiveRebaseEditor.cs | 98 ---------------- src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/ViewModels/InProgressContexts.cs | 12 +- src/ViewModels/InteractiveRebase.cs | 6 +- src/ViewModels/Launcher.cs | 9 +- src/Views/CodeEditor.axaml | 62 ++++++++++ src/Views/CodeEditor.axaml.cs | 54 +++++++++ 14 files changed, 320 insertions(+), 155 deletions(-) create mode 100644 src/Models/InteractiveRebase.cs delete mode 100644 src/Models/InteractiveRebaseEditor.cs create mode 100644 src/Views/CodeEditor.axaml create mode 100644 src/Views/CodeEditor.axaml.cs diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index fd4d274e..9b96d415 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -59,7 +59,7 @@ namespace SourceGit typeof(GridLengthConverter), ] )] - [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] [JsonSerializable(typeof(Models.JetBrainsState))] [JsonSerializable(typeof(Models.ThemeOverrides))] [JsonSerializable(typeof(Models.Version))] diff --git a/src/App.axaml.cs b/src/App.axaml.cs index b21953a2..5f730c9b 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -47,8 +47,10 @@ namespace SourceGit { try { - if (args.Length > 1 && args[0].Equals("--rebase-editor", StringComparison.Ordinal)) - Environment.Exit(Models.InteractiveRebaseEditor.Process(args[1])); + if (TryLaunchedAsRebaseTodoEditor(args, out int exitTodo)) + Environment.Exit(exitTodo); + else if (TryLaunchedAsRebaseMessageEditor(args, out int exitMessage)) + Environment.Exit(exitMessage); else BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } @@ -326,28 +328,14 @@ namespace SourceGit { BindingPlugins.DataValidators.RemoveAt(0); - var commandlines = Environment.GetCommandLineArgs(); - if (TryParseAskpass(commandlines, out var keyname)) - { - desktop.MainWindow = new Views.Askpass(Path.GetFileName(keyname)); - } - else - { - Native.OS.SetupEnternalTools(); + if (TryLaunchedAsCoreEditor(desktop)) + return; - _launcher = new ViewModels.Launcher(commandlines); - desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; + if (TryLaunchedAsAskpass(desktop)) + return; - var pref = ViewModels.Preference.Instance; - if (pref.ShouldCheck4UpdateOnStartup) - { - pref.Save(); - Check4Update(); - } - } + TryLaunchedAsNormal(desktop); } - - base.OnFrameworkInitializationCompleted(); } private static void ShowSelfUpdateResult(object data) @@ -358,10 +346,7 @@ namespace SourceGit { var dialog = new Views.SelfUpdate() { - DataContext = new ViewModels.SelfUpdate - { - Data = data - } + DataContext = new ViewModels.SelfUpdate() { Data = data } }; dialog.Show(desktop.MainWindow); @@ -369,18 +354,133 @@ namespace SourceGit }); } - private static bool TryParseAskpass(string[] args, out string keyname) + private static bool TryLaunchedAsRebaseTodoEditor(string[] args, out int exitCode) { - keyname = string.Empty; + exitCode = -1; - if (args.Length != 2) + if (args.Length <= 1 || !args[0].Equals("--rebase-todo-editor", StringComparison.Ordinal)) + return false; + + var file = args[1]; + var filename = Path.GetFileName(file); + if (!filename.Equals("git-rebase-todo", StringComparison.OrdinalIgnoreCase)) + return true; + + var dirInfo = new DirectoryInfo(Path.GetDirectoryName(file)); + if (!dirInfo.Exists || !dirInfo.Name.Equals("rebase-merge", StringComparison.Ordinal)) + return true; + + var jobsFile = Path.Combine(dirInfo.Parent.FullName, "sourcegit_rebase_jobs.json"); + if (!File.Exists(jobsFile)) + return true; + + var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); + var lines = new List(); + foreach (var job in collection.Jobs) + { + switch (job.Action) + { + case Models.InteractiveRebaseAction.Pick: + lines.Add($"p {job.SHA}"); + break; + case Models.InteractiveRebaseAction.Edit: + lines.Add($"e {job.SHA}"); + break; + case Models.InteractiveRebaseAction.Reword: + lines.Add($"r {job.SHA}"); + break; + case Models.InteractiveRebaseAction.Squash: + lines.Add($"s {job.SHA}"); + break; + case Models.InteractiveRebaseAction.Fixup: + lines.Add($"f {job.SHA}"); + break; + default: + lines.Add($"d {job.SHA}"); + break; + } + } + + File.WriteAllLines(file, lines); + + exitCode = 0; + return true; + } + + private static bool TryLaunchedAsRebaseMessageEditor(string[] args, out int exitCode) + { + exitCode = -1; + + if (args.Length <= 1 || !args[0].Equals("--rebase-message-editor", StringComparison.Ordinal)) + return false; + + var file = args[1]; + var filename = Path.GetFileName(file); + if (!filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase)) + return true; + + var jobsFile = Path.Combine(Path.GetDirectoryName(file), "sourcegit_rebase_jobs.json"); + if (!File.Exists(jobsFile)) + return true; + + var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); + var doneFile = Path.Combine(Path.GetDirectoryName(file), "rebase-merge", "done"); + if (!File.Exists(doneFile)) + return true; + + var done = File.ReadAllText(doneFile).Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + if (done.Length > collection.Jobs.Count) + return true; + + var job = collection.Jobs[done.Length - 1]; + File.WriteAllText(file, job.Message); + + exitCode = 0; + return true; + } + + private bool TryLaunchedAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) + { + var args = desktop.Args; + if (args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal)) + return false; + + var file = args[1]; + if (!File.Exists(file)) + Environment.Exit(-1); + + desktop.MainWindow = new Views.CodeEditor(file); + return true; + } + + private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop) + { + var args = desktop.Args; + if (args.Length <= 1 || !args[0].Equals("--askpass", StringComparison.Ordinal)) return false; var match = REG_ASKPASS().Match(args[1]); - if (match.Success) - keyname = match.Groups[1].Value; + if (!match.Success) + return false; + + desktop.MainWindow = new Views.Askpass(Path.GetFileName(match.Groups[1].Value)); + return true; + } - return match.Success; + private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop) + { + Native.OS.SetupEnternalTools(); + + var startupRepo = desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0]) ? desktop.Args[0] : null; + _launcher = new ViewModels.Launcher(startupRepo); + desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; + + var pref = ViewModels.Preference.Instance; + if (pref.ShouldCheck4UpdateOnStartup) + { + pref.Save(); + Check4Update(); + } } [GeneratedRegex(@"Enter\s+passphrase\s*for\s*key\s*['""]([^'""]+)['""]\:\s*", RegexOptions.IgnoreCase)] diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 3e440a13..e3abcf04 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; - using Avalonia.Threading; namespace SourceGit.Commands @@ -22,9 +21,17 @@ namespace SourceGit.Commands public string StdErr { get; set; } } + public enum EditorType + { + None, + CoreEditor, + RebaseEditor, + } + public string Context { get; set; } = string.Empty; public CancelToken Cancel { get; set; } = null; public string WorkingDirectory { get; set; } = null; + public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode public string Args { get; set; } = string.Empty; public bool RaiseError { get; set; } = true; public bool TraitErrorAsOutput { get; set; } = false; @@ -33,7 +40,7 @@ namespace SourceGit.Commands public void UseSSHKey(string key) { Envs.Add("DISPLAY", "required"); - Envs.Add("SSH_ASKPASS", Process.GetCurrentProcess().MainModule.FileName); + Envs.Add("SSH_ASKPASS", $"\"{Process.GetCurrentProcess().MainModule.FileName}\" --askpass"); Envs.Add("SSH_ASKPASS_REQUIRE", "prefer"); Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{key}'"); } @@ -42,7 +49,7 @@ namespace SourceGit.Commands { var start = new ProcessStartInfo(); start.FileName = Native.OS.GitExecutable; - start.Arguments = "--no-pager -c core.quotepath=off " + Args; + start.Arguments = "--no-pager -c core.quotepath=off "; start.UseShellExecute = false; start.CreateNoWindow = true; start.RedirectStandardOutput = true; @@ -50,6 +57,24 @@ namespace SourceGit.Commands start.StandardOutputEncoding = Encoding.UTF8; start.StandardErrorEncoding = Encoding.UTF8; + // Editors + var editorProgram = $"\\\"{Process.GetCurrentProcess().MainModule.FileName}\\\""; + switch (Editor) + { + case EditorType.CoreEditor: + start.Arguments += $"-c core.editor=\"{editorProgram} --core-editor\" "; + break; + case EditorType.RebaseEditor: + start.Arguments += $"-c core.editor=\"{editorProgram} --rebase-message-editor\" -c sequence.editor=\"{editorProgram} --rebase-todo-editor\" -c rebase.abbreviateCommands=true "; + break; + default: + start.Arguments += "-c core.editor=true "; + break; + } + + // Append command args + start.Arguments += Args; + // User environment overrides. foreach (var kv in Envs) start.Environment.Add(kv.Key, kv.Value); diff --git a/src/Commands/Rebase.cs b/src/Commands/Rebase.cs index 2576d0e6..2ec50f3c 100644 --- a/src/Commands/Rebase.cs +++ b/src/Commands/Rebase.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Rebase : Command { @@ -19,12 +17,10 @@ namespace SourceGit.Commands { public InteractiveRebase(string repo, string basedOn) { - var exec = Process.GetCurrentProcess().MainModule.FileName; - var editor = $"\\\"{exec}\\\" --rebase-editor"; - WorkingDirectory = repo; Context = repo; - Args = $"-c core.editor=\"{editor}\" -c sequence.editor=\"{editor}\" -c rebase.abbreviateCommands=true rebase -i --autosquash {basedOn}"; + Editor = EditorType.RebaseEditor; + Args = $"rebase -i --autosquash {basedOn}"; } } } diff --git a/src/Models/InteractiveRebase.cs b/src/Models/InteractiveRebase.cs new file mode 100644 index 00000000..0980587a --- /dev/null +++ b/src/Models/InteractiveRebase.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public enum InteractiveRebaseAction + { + Pick, + Edit, + Reword, + Squash, + Fixup, + Drop, + } + + public class InteractiveRebaseJob + { + public string SHA { get; set; } = string.Empty; + public InteractiveRebaseAction Action { get; set; } = InteractiveRebaseAction.Pick; + public string Message { get; set; } = string.Empty; + } + + public class InteractiveRebaseJobCollection + { + public List Jobs { get; set; } = new List(); + } +} diff --git a/src/Models/InteractiveRebaseEditor.cs b/src/Models/InteractiveRebaseEditor.cs deleted file mode 100644 index 911258d4..00000000 --- a/src/Models/InteractiveRebaseEditor.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; - -namespace SourceGit.Models -{ - public enum InteractiveRebaseAction - { - Pick, - Edit, - Reword, - Squash, - Fixup, - Drop, - } - - public class InteractiveRebaseJob - { - public string SHA { get; set; } = string.Empty; - public InteractiveRebaseAction Action { get; set; } = InteractiveRebaseAction.Pick; - public string Message { get; set; } = string.Empty; - } - - public static class InteractiveRebaseEditor - { - public static int Process(string file) - { - try - { - var filename = Path.GetFileName(file); - if (filename.Equals("git-rebase-todo", StringComparison.OrdinalIgnoreCase)) - { - var dirInfo = new DirectoryInfo(Path.GetDirectoryName(file)); - if (!dirInfo.Exists || !dirInfo.Name.Equals("rebase-merge", StringComparison.Ordinal)) - return -1; - - var jobsFile = Path.Combine(dirInfo.Parent.FullName, "sourcegit_rebase_jobs.json"); - if (!File.Exists(jobsFile)) - return -1; - - var jobs = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.ListInteractiveRebaseJob); - var lines = new List(); - foreach (var job in jobs) - { - switch (job.Action) - { - case InteractiveRebaseAction.Pick: - lines.Add($"p {job.SHA}"); - break; - case InteractiveRebaseAction.Edit: - lines.Add($"e {job.SHA}"); - break; - case InteractiveRebaseAction.Reword: - lines.Add($"r {job.SHA}"); - break; - case InteractiveRebaseAction.Squash: - lines.Add($"s {job.SHA}"); - break; - case InteractiveRebaseAction.Fixup: - lines.Add($"f {job.SHA}"); - break; - default: - lines.Add($"d {job.SHA}"); - break; - } - } - - File.WriteAllLines(file, lines); - } - else if (filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase)) - { - var jobsFile = Path.Combine(Path.GetDirectoryName(file), "sourcegit_rebase_jobs.json"); - if (!File.Exists(jobsFile)) - return 0; - - var jobs = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.ListInteractiveRebaseJob); - var doneFile = Path.Combine(Path.GetDirectoryName(file), "rebase-merge", "done"); - if (!File.Exists(doneFile)) - return -1; - - var done = File.ReadAllText(doneFile).Split(new char[] {'\n', '\r'}, StringSplitOptions.RemoveEmptyEntries); - if (done.Length > jobs.Count) - return -1; - - var job = jobs[done.Length - 1]; - File.WriteAllText(file, job.Message); - } - - return 0; - } - catch - { - return -1; - } - } - } -} diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 3a6ef2bd..74048aa4 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -91,6 +91,7 @@ Parent Folder: Repository URL: CLOSE + Editor Cherry-Pick This Commit Checkout Commit Compare with HEAD diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 51ece14d..26c0b7b2 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -94,6 +94,7 @@ 父级目录 : 远程仓库 : 关闭 + 提交信息编辑器 挑选(cherry-pick)此提交 检出此提交 与当前HEAD比较 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index bb5bd996..801fb8b1 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -94,6 +94,7 @@ 父級目錄 : 遠端倉庫 : 關閉 + 提交資訊編輯器 挑選(cherry-pick)此提交 檢出此提交 與當前HEAD比較 diff --git a/src/ViewModels/InProgressContexts.cs b/src/ViewModels/InProgressContexts.cs index fce178f0..f7b85032 100644 --- a/src/ViewModels/InProgressContexts.cs +++ b/src/ViewModels/InProgressContexts.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.IO; +using System.IO; namespace SourceGit.ViewModels { @@ -39,7 +38,8 @@ namespace SourceGit.ViewModels { WorkingDirectory = Repository, Context = Repository, - Args = $"-c core.editor=true {Cmd} --continue", + Editor = Commands.Command.EditorType.None, + Args = $"{Cmd} --continue", }.Exec(); } } @@ -58,14 +58,12 @@ namespace SourceGit.ViewModels public override bool Continue() { - var exec = Process.GetCurrentProcess().MainModule.FileName; - var editor = $"\\\"{exec}\\\" --rebase-editor"; - var succ = new Commands.Command() { WorkingDirectory = Repository, Context = Repository, - Args = $"-c core.editor=\"{editor}\" rebase --continue", + Editor = Commands.Command.EditorType.RebaseEditor, + Args = $"rebase --continue", }.Exec(); if (succ) diff --git a/src/ViewModels/InteractiveRebase.cs b/src/ViewModels/InteractiveRebase.cs index f22e9852..bc6dde62 100644 --- a/src/ViewModels/InteractiveRebase.cs +++ b/src/ViewModels/InteractiveRebase.cs @@ -168,18 +168,18 @@ namespace SourceGit.ViewModels _repo.SetWatcherEnabled(false); var saveFile = Path.Combine(_repo.GitDir, "sourcegit_rebase_jobs.json"); - var jobs = new List(); + var collection = new Models.InteractiveRebaseJobCollection(); for (int i = Items.Count - 1; i >= 0; i--) { var item = Items[i]; - jobs.Add(new Models.InteractiveRebaseJob() + collection.Jobs.Add(new Models.InteractiveRebaseJob() { SHA = item.Commit.SHA, Action = item.Action, Message = item.FullMessage, }); } - File.WriteAllText(saveFile, JsonSerializer.Serialize(jobs, JsonCodeGen.Default.ListInteractiveRebaseJob)); + File.WriteAllText(saveFile, JsonSerializer.Serialize(collection, JsonCodeGen.Default.InteractiveRebaseJobCollection)); return Task.Run(() => { diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index c0b376ba..b097c7a5 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -28,21 +28,20 @@ namespace SourceGit.ViewModels } } - public Launcher(string[] commandlines) + public Launcher(string startupRepo) { Pages = new AvaloniaList(); AddNewTab(); - if (commandlines.Length == 2) + if (!string.IsNullOrEmpty(startupRepo)) { - var path = commandlines[1]; - var root = new Commands.QueryRepositoryRootPath(path).Result(); + var root = new Commands.QueryRepositoryRootPath(startupRepo).Result(); if (string.IsNullOrEmpty(root)) { Pages[0].Notifications.Add(new Notification { IsError = true, - Message = $"Given path: '{path}' is NOT a valid repository!" + Message = $"Given path: '{startupRepo}' is NOT a valid repository!" }); return; } diff --git a/src/Views/CodeEditor.axaml b/src/Views/CodeEditor.axaml new file mode 100644 index 00000000..25b306b4 --- /dev/null +++ b/src/Views/CodeEditor.axaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + +