From 7070a07e1532cc3132fb560812fce872e78688c7 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 20 Jun 2024 17:02:12 +0800 Subject: [PATCH] feature: simple interactive rebase support (#188) * Only allow to start interactive rebase from merged commit in current branch * The order of commits in the interactive rebase window is as same as it's in histories page. * Unlike anthor git frontend app `Fork`, you should edit the final message on the last commit rather than the previous commit that will be meld into while squashing commits --- src/App.JsonCodeGen.cs | 1 + src/App.axaml.cs | 5 +- src/Commands/Command.cs | 2 - src/Commands/Rebase.cs | 17 +- .../InteractiveRebaseActionConverters.cs | 51 +++ src/Models/InteractiveRebaseEditor.cs | 116 +++++++ src/Resources/Icons.axaml | 1 + src/Resources/Locales/en_US.axaml | 6 + src/Resources/Locales/zh_CN.axaml | 6 + src/Resources/Locales/zh_TW.axaml | 6 + src/Resources/Styles.axaml | 5 + src/ViewModels/Histories.cs | 12 + src/ViewModels/InProgressContexts.cs | 17 +- src/ViewModels/InteractiveRebase.cs | 199 ++++++++++++ src/Views/InteractiveRebase.axaml | 298 ++++++++++++++++++ src/Views/InteractiveRebase.axaml.cs | 79 +++++ src/Views/Reword.axaml | 2 +- 17 files changed, 816 insertions(+), 7 deletions(-) create mode 100644 src/Converters/InteractiveRebaseActionConverters.cs create mode 100644 src/Models/InteractiveRebaseEditor.cs create mode 100644 src/ViewModels/InteractiveRebase.cs create mode 100644 src/Views/InteractiveRebase.axaml create mode 100644 src/Views/InteractiveRebase.axaml.cs diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index af2e9913..61f00074 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -6,6 +6,7 @@ namespace SourceGit [JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)] [JsonSerializable(typeof(Models.Version))] [JsonSerializable(typeof(Models.JetBrainsState))] + [JsonSerializable(typeof(List))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(ViewModels.Preference))] internal partial class JsonCodeGen : JsonSerializerContext { } diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 619ab60c..b0cf56f8 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -45,7 +45,10 @@ namespace SourceGit { try { - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + if (args.Length > 1 && args[0].Equals("--rebase-editor", StringComparison.Ordinal)) + Environment.Exit(Models.InteractiveRebaseEditor.Process(args[1])); + else + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } catch (Exception ex) { diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index c30a3d45..00f36c85 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -43,9 +43,7 @@ namespace SourceGit.Commands // Force using en_US.UTF-8 locale to avoid GCM crash if (OperatingSystem.IsLinux()) - { start.Environment.Add("LANG", "en_US.UTF-8"); - } if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory; diff --git a/src/Commands/Rebase.cs b/src/Commands/Rebase.cs index d08d55ad..2576d0e6 100644 --- a/src/Commands/Rebase.cs +++ b/src/Commands/Rebase.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Diagnostics; + +namespace SourceGit.Commands { public class Rebase : Command { @@ -12,4 +14,17 @@ Args += basedOn; } } + + public class InteractiveRebase : Command + { + 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}"; + } + } } diff --git a/src/Converters/InteractiveRebaseActionConverters.cs b/src/Converters/InteractiveRebaseActionConverters.cs new file mode 100644 index 00000000..dbd183bd --- /dev/null +++ b/src/Converters/InteractiveRebaseActionConverters.cs @@ -0,0 +1,51 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace SourceGit.Converters +{ + public static class InteractiveRebaseActionConverters + { + public static readonly FuncValueConverter ToIconBrush = + new FuncValueConverter(v => + { + switch (v) + { + case Models.InteractiveRebaseAction.Pick: + return Brushes.Green; + case Models.InteractiveRebaseAction.Edit: + return Brushes.Orange; + case Models.InteractiveRebaseAction.Reword: + return Brushes.Orange; + case Models.InteractiveRebaseAction.Squash: + return Brushes.LightGray; + case Models.InteractiveRebaseAction.Fixup: + return Brushes.LightGray; + default: + return Brushes.Red; + } + }); + + public static readonly FuncValueConverter ToName = + new FuncValueConverter(v => + { + switch (v) + { + case Models.InteractiveRebaseAction.Pick: + return "Pick"; + case Models.InteractiveRebaseAction.Edit: + return "Edit"; + case Models.InteractiveRebaseAction.Reword: + return "Reword"; + case Models.InteractiveRebaseAction.Squash: + return "Squash"; + case Models.InteractiveRebaseAction.Fixup: + return "Fixup"; + default: + return "Drop"; + } + }); + + public static readonly FuncValueConverter CanEditMessage = + new FuncValueConverter(v => v == Models.InteractiveRebaseAction.Reword || v == Models.InteractiveRebaseAction.Squash); + } +} diff --git a/src/Models/InteractiveRebaseEditor.cs b/src/Models/InteractiveRebaseEditor.cs new file mode 100644 index 00000000..c0c1b24e --- /dev/null +++ b/src/Models/InteractiveRebaseEditor.cs @@ -0,0 +1,116 @@ +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) + { + + File.AppendAllLines("E:\\unknown.txt", ["------------", file]); + + try + { + var filename = Path.GetFileName(file); + if (filename.Equals("git-rebase-todo", StringComparison.OrdinalIgnoreCase)) + { + File.AppendAllLines("E:\\unknown.txt", ["git-rebase-todo start"]); + var dirInfo = new DirectoryInfo(Path.GetDirectoryName(file)); + if (!dirInfo.Exists || !dirInfo.Name.Equals("rebase-merge", StringComparison.Ordinal)) + { + File.WriteAllLines("E:\\test.txt", ["git-rebase-todo", file]); + return -1; + } + + var jobsFile = Path.Combine(dirInfo.Parent.FullName, "sourcegit_rebase_jobs.json"); + if (!File.Exists(jobsFile)) + { + File.WriteAllLines("E:\\test.txt", ["git-rebase-todo", file, 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.AppendAllLines("E:\\unknown.txt", ["git-rebase-todo end"]); + File.WriteAllLines(file, lines); + } + else if (filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase)) + { + File.AppendAllLines("E:\\unknown.txt", ["COMMIT_EDITMSG start"]); + 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(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); + if (done.Length > jobs.Count) + return -1; + + var job = jobs[done.Length - 1]; + File.WriteAllText(file, job.Message); + + File.AppendAllLines("E:\\unknown.txt", ["COMMIT_EDITMSG end", File.ReadAllText(doneFile).ReplaceLineEndings("|")]); + } + else + { + File.AppendAllLines("E:\\unknown.txt", [file]); + } + + return 0; + } + catch + { + return -1; + } + } + } +} diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index e818c8d0..8574c130 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -102,4 +102,5 @@ M832 464H332V240c0-31 25-56 56-56h248c31 0 56 25 56 56v68c0 4 4 8 8 8h56c4 0 8-4 8-8v-68c0-71-57-128-128-128H388c-71 0-128 57-128 128v224h-68c-18 0-32 14-32 32v384c0 18 14 32 32 32h640c18 0 32-14 32-32V496c0-18-14-32-32-32zM540 701v53c0 4-4 8-8 8h-40c-4 0-8-4-8-8v-53c-12-9-20-23-20-39 0-27 22-48 48-48s48 22 48 48c0 16-8 30-20 39z M897 673v13c0 51-42 93-93 93h-10c-1 0-2 0-2 0H220c-23 0-42 19-42 42v13c0 23 19 42 42 42h552c14 0 26 12 26 26 0 14-12 26-26 26H220c-51 0-93-42-93-93v-13c0-51 42-93 93-93h20c1-0 2-0 2-0h562c23 0 42-19 42-42v-13c0-11-5-22-13-29-8-7-17-11-28-10H660c-14 0-26-12-26-26 0-14 12-26 26-26h144c24-1 47 7 65 24 18 17 29 42 29 67zM479 98c-112 0-203 91-203 203 0 44 14 85 38 118l132 208c15 24 50 24 66 0l133-209c23-33 37-73 37-117 0-112-91-203-203-203zm0 327c-68 0-122-55-122-122s55-122 122-122 122 55 122 122-55 122-122 122z M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z + M512 64A447 447 0 0064 512c0 248 200 448 448 448s448-200 448-448S760 64 512 64zM218 295h31c54 0 105 19 145 55 13 12 13 31 3 43a35 35 0 01-22 10 36 36 0 01-21-7 155 155 0 00-103-39h-31a32 32 0 01-31-31c0-18 13-31 30-31zm31 433h-31a32 32 0 01-31-31c0-16 13-31 31-31h31A154 154 0 00403 512 217 217 0 01620 295h75l-93-67a33 33 0 01-7-43 33 33 0 0143-7l205 148-205 148a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67H620a154 154 0 00-154 154c0 122-97 220-217 220zm390 118a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67h-75c-52 0-103-19-143-54-12-12-13-31-1-43a30 30 0 0142-3 151 151 0 00102 39h75L602 599a33 33 0 01-7-43 33 33 0 0143-7l205 148-203 151z diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 5971af6c..d6b6f56b 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -85,6 +85,7 @@ Compare with HEAD Compare with Worktree Copy SHA + Interactive Rebase ${0}$ to Here Rebase ${0}$ to Here Reset ${0}$ to Here Revert Commit @@ -287,6 +288,11 @@ Merge request in progress. Press 'Abort' to restore original HEAD. Rebase in progress. Press 'Abort' to restore original HEAD. Revert in progress. Press 'Abort' to restore original HEAD. + Interactive Rebase + Target Branch: + On: + Move Up + Move Down Source Git ERROR NOTICE diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index a6189e27..14af9f76 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -88,6 +88,7 @@ 与当前HEAD比较 与本地工作树比较 复制提交指纹 + 交互式变基(rebase -i) ${0}$ 到此处 变基(rebase) ${0}$ 到此处 重置(reset) ${0}$ 到此处 回滚此提交 @@ -290,6 +291,11 @@ 合并操作进行中。点击【终止】回滚到操作前的状态。 变基(Rebase)操作进行中。点击【终止】回滚到操作前的状态。 回滚提交操作进行中。点击【终止】回滚到操作前的状态。 + 交互式变基 + 目标分支 : + 起始提交 : + 向上移动 + 向下移动 Source Git 出错了 系统提示 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 96c1535a..613d40ce 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -88,6 +88,7 @@ 與當前HEAD比較 與本地工作樹比較 複製提交指紋 + 互動式變基(rebase -i) ${0}$ 到此處 變基(rebase) ${0}$ 到此處 重置(reset) ${0}$ 到此處 回滾此提交 @@ -290,6 +291,11 @@ 合併操作進行中。點選【終止】回滾到操作前的狀態。 變基(Rebase)操作進行中。點選【終止】回滾到操作前的狀態。 回滾提交操作進行中。點選【終止】回滾到操作前的狀態。 + 互動式變基 + 目標分支 : + 起始提交 : + 向上移動 + 向下移動 Source Git 出錯了 系統提示 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index ca15f55f..c044d10c 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -160,6 +160,11 @@ + +