mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-23 20:47:25 -08:00
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
This commit is contained in:
parent
6c9f7e6da3
commit
7070a07e15
17 changed files with 816 additions and 7 deletions
|
@ -6,6 +6,7 @@ namespace SourceGit
|
||||||
[JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)]
|
[JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)]
|
||||||
[JsonSerializable(typeof(Models.Version))]
|
[JsonSerializable(typeof(Models.Version))]
|
||||||
[JsonSerializable(typeof(Models.JetBrainsState))]
|
[JsonSerializable(typeof(Models.JetBrainsState))]
|
||||||
|
[JsonSerializable(typeof(List<Models.InteractiveRebaseJob>))]
|
||||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||||
[JsonSerializable(typeof(ViewModels.Preference))]
|
[JsonSerializable(typeof(ViewModels.Preference))]
|
||||||
internal partial class JsonCodeGen : JsonSerializerContext { }
|
internal partial class JsonCodeGen : JsonSerializerContext { }
|
||||||
|
|
|
@ -45,7 +45,10 @@ namespace SourceGit
|
||||||
{
|
{
|
||||||
try
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -43,9 +43,7 @@ namespace SourceGit.Commands
|
||||||
|
|
||||||
// Force using en_US.UTF-8 locale to avoid GCM crash
|
// Force using en_US.UTF-8 locale to avoid GCM crash
|
||||||
if (OperatingSystem.IsLinux())
|
if (OperatingSystem.IsLinux())
|
||||||
{
|
|
||||||
start.Environment.Add("LANG", "en_US.UTF-8");
|
start.Environment.Add("LANG", "en_US.UTF-8");
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(WorkingDirectory))
|
if (!string.IsNullOrEmpty(WorkingDirectory))
|
||||||
start.WorkingDirectory = WorkingDirectory;
|
start.WorkingDirectory = WorkingDirectory;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
namespace SourceGit.Commands
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SourceGit.Commands
|
||||||
{
|
{
|
||||||
public class Rebase : Command
|
public class Rebase : Command
|
||||||
{
|
{
|
||||||
|
@ -12,4 +14,17 @@
|
||||||
Args += basedOn;
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
51
src/Converters/InteractiveRebaseActionConverters.cs
Normal file
51
src/Converters/InteractiveRebaseActionConverters.cs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace SourceGit.Converters
|
||||||
|
{
|
||||||
|
public static class InteractiveRebaseActionConverters
|
||||||
|
{
|
||||||
|
public static readonly FuncValueConverter<Models.InteractiveRebaseAction, IBrush> ToIconBrush =
|
||||||
|
new FuncValueConverter<Models.InteractiveRebaseAction, IBrush>(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<Models.InteractiveRebaseAction, string> ToName =
|
||||||
|
new FuncValueConverter<Models.InteractiveRebaseAction, string>(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<Models.InteractiveRebaseAction, bool> CanEditMessage =
|
||||||
|
new FuncValueConverter<Models.InteractiveRebaseAction, bool>(v => v == Models.InteractiveRebaseAction.Reword || v == Models.InteractiveRebaseAction.Squash);
|
||||||
|
}
|
||||||
|
}
|
116
src/Models/InteractiveRebaseEditor.cs
Normal file
116
src/Models/InteractiveRebaseEditor.cs
Normal file
|
@ -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<string>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -102,4 +102,5 @@
|
||||||
<StreamGeometry x:Key="Icons.Unlock">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</StreamGeometry>
|
<StreamGeometry x:Key="Icons.Unlock">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</StreamGeometry>
|
||||||
<StreamGeometry x:Key="Icons.Track">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</StreamGeometry>
|
<StreamGeometry x:Key="Icons.Track">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</StreamGeometry>
|
||||||
<StreamGeometry x:Key="Icons.Whitespace">M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z</StreamGeometry>
|
<StreamGeometry x:Key="Icons.Whitespace">M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z</StreamGeometry>
|
||||||
|
<StreamGeometry x:Key="Icons.InteractiveRebase">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</StreamGeometry>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
@ -85,6 +85,7 @@
|
||||||
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">Compare with HEAD</x:String>
|
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">Compare with HEAD</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">Compare with Worktree</x:String>
|
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">Compare with Worktree</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">Copy SHA</x:String>
|
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">Copy SHA</x:String>
|
||||||
|
<x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">Interactive Rebase ${0}$ to Here</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">Rebase ${0}$ to Here</x:String>
|
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">Rebase ${0}$ to Here</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">Reset ${0}$ to Here</x:String>
|
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">Reset ${0}$ to Here</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">Revert Commit</x:String>
|
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">Revert Commit</x:String>
|
||||||
|
@ -287,6 +288,11 @@
|
||||||
<x:String x:Key="Text.InProgress.Merge" xml:space="preserve">Merge request in progress. Press 'Abort' to restore original HEAD.</x:String>
|
<x:String x:Key="Text.InProgress.Merge" xml:space="preserve">Merge request in progress. Press 'Abort' to restore original HEAD.</x:String>
|
||||||
<x:String x:Key="Text.InProgress.Rebase" xml:space="preserve">Rebase in progress. Press 'Abort' to restore original HEAD.</x:String>
|
<x:String x:Key="Text.InProgress.Rebase" xml:space="preserve">Rebase in progress. Press 'Abort' to restore original HEAD.</x:String>
|
||||||
<x:String x:Key="Text.InProgress.Revert" xml:space="preserve">Revert in progress. Press 'Abort' to restore original HEAD.</x:String>
|
<x:String x:Key="Text.InProgress.Revert" xml:space="preserve">Revert in progress. Press 'Abort' to restore original HEAD.</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase" xml:space="preserve">Interactive Rebase</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">Target Branch:</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">On:</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.MoveUp" xml:space="preserve">Move Up</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.MoveDown" xml:space="preserve">Move Down</x:String>
|
||||||
<x:String x:Key="Text.Launcher" xml:space="preserve">Source Git</x:String>
|
<x:String x:Key="Text.Launcher" xml:space="preserve">Source Git</x:String>
|
||||||
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">ERROR</x:String>
|
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">ERROR</x:String>
|
||||||
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String>
|
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String>
|
||||||
|
|
|
@ -88,6 +88,7 @@
|
||||||
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">与当前HEAD比较</x:String>
|
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">与当前HEAD比较</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">与本地工作树比较</x:String>
|
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">与本地工作树比较</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">复制提交指纹</x:String>
|
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">复制提交指纹</x:String>
|
||||||
|
<x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">交互式变基(rebase -i) ${0}$ 到此处</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">变基(rebase) ${0}$ 到此处</x:String>
|
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">变基(rebase) ${0}$ 到此处</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">重置(reset) ${0}$ 到此处</x:String>
|
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">重置(reset) ${0}$ 到此处</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">回滚此提交</x:String>
|
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">回滚此提交</x:String>
|
||||||
|
@ -290,6 +291,11 @@
|
||||||
<x:String x:Key="Text.InProgress.Merge" xml:space="preserve">合并操作进行中。点击【终止】回滚到操作前的状态。</x:String>
|
<x:String x:Key="Text.InProgress.Merge" xml:space="preserve">合并操作进行中。点击【终止】回滚到操作前的状态。</x:String>
|
||||||
<x:String x:Key="Text.InProgress.Rebase" xml:space="preserve">变基(Rebase)操作进行中。点击【终止】回滚到操作前的状态。</x:String>
|
<x:String x:Key="Text.InProgress.Rebase" xml:space="preserve">变基(Rebase)操作进行中。点击【终止】回滚到操作前的状态。</x:String>
|
||||||
<x:String x:Key="Text.InProgress.Revert" xml:space="preserve">回滚提交操作进行中。点击【终止】回滚到操作前的状态。</x:String>
|
<x:String x:Key="Text.InProgress.Revert" xml:space="preserve">回滚提交操作进行中。点击【终止】回滚到操作前的状态。</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase" xml:space="preserve">交互式变基</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">目标分支 :</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">起始提交 :</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.MoveUp" xml:space="preserve">向上移动</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.MoveDown" xml:space="preserve">向下移动</x:String>
|
||||||
<x:String x:Key="Text.Launcher" xml:space="preserve">Source Git</x:String>
|
<x:String x:Key="Text.Launcher" xml:space="preserve">Source Git</x:String>
|
||||||
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出错了</x:String>
|
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出错了</x:String>
|
||||||
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系统提示</x:String>
|
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系统提示</x:String>
|
||||||
|
|
|
@ -88,6 +88,7 @@
|
||||||
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">與當前HEAD比較</x:String>
|
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">與當前HEAD比較</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">與本地工作樹比較</x:String>
|
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">與本地工作樹比較</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">複製提交指紋</x:String>
|
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">複製提交指紋</x:String>
|
||||||
|
<x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">互動式變基(rebase -i) ${0}$ 到此處</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">變基(rebase) ${0}$ 到此處</x:String>
|
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">變基(rebase) ${0}$ 到此處</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">重置(reset) ${0}$ 到此處</x:String>
|
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">重置(reset) ${0}$ 到此處</x:String>
|
||||||
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">回滾此提交</x:String>
|
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">回滾此提交</x:String>
|
||||||
|
@ -290,6 +291,11 @@
|
||||||
<x:String x:Key="Text.InProgress.Merge" xml:space="preserve">合併操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
|
<x:String x:Key="Text.InProgress.Merge" xml:space="preserve">合併操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
|
||||||
<x:String x:Key="Text.InProgress.Rebase" xml:space="preserve">變基(Rebase)操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
|
<x:String x:Key="Text.InProgress.Rebase" xml:space="preserve">變基(Rebase)操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
|
||||||
<x:String x:Key="Text.InProgress.Revert" xml:space="preserve">回滾提交操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
|
<x:String x:Key="Text.InProgress.Revert" xml:space="preserve">回滾提交操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase" xml:space="preserve">互動式變基</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">目標分支 :</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">起始提交 :</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.MoveUp" xml:space="preserve">向上移動</x:String>
|
||||||
|
<x:String x:Key="Text.InteractiveRebase.MoveDown" xml:space="preserve">向下移動</x:String>
|
||||||
<x:String x:Key="Text.Launcher" xml:space="preserve">Source Git</x:String>
|
<x:String x:Key="Text.Launcher" xml:space="preserve">Source Git</x:String>
|
||||||
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出錯了</x:String>
|
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出錯了</x:String>
|
||||||
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系統提示</x:String>
|
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系統提示</x:String>
|
||||||
|
|
|
@ -160,6 +160,11 @@
|
||||||
<Setter Property="FontSize" Value="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize}"/>
|
<Setter Property="FontSize" Value="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="FlyoutPresenter">
|
||||||
|
<Setter Property="MaxWidth" Value="1024"/>
|
||||||
|
<Setter Property="MaxHeight" Value="768"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Path">
|
<Style Selector="Path">
|
||||||
<Setter Property="Fill" Value="{DynamicResource Brush.FG1}"/>
|
<Setter Property="Fill" Value="{DynamicResource Brush.FG1}"/>
|
||||||
<Setter Property="Stretch" Value="Uniform"/>
|
<Setter Property="Stretch" Value="Uniform"/>
|
||||||
|
|
|
@ -277,6 +277,18 @@ namespace SourceGit.ViewModels
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
};
|
};
|
||||||
menu.Items.Add(revert);
|
menu.Items.Add(revert);
|
||||||
|
|
||||||
|
var interactiveRebase = new MenuItem();
|
||||||
|
interactiveRebase.Header = new Views.NameHighlightedTextBlock("CommitCM.InteractiveRebase", current.Name);
|
||||||
|
interactiveRebase.Icon = App.CreateMenuIcon("Icons.InteractiveRebase");
|
||||||
|
interactiveRebase.IsVisible = current.Head != commit.SHA;
|
||||||
|
interactiveRebase.Click += (o, e) =>
|
||||||
|
{
|
||||||
|
var dialog = new Views.InteractiveRebase() { DataContext = new InteractiveRebase(_repo, current, commit) };
|
||||||
|
dialog.ShowDialog(App.GetTopLevel() as Window);
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
menu.Items.Add(interactiveRebase);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.Head != commit.SHA)
|
if (current.Head != commit.SHA)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.IO;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace SourceGit.ViewModels
|
namespace SourceGit.ViewModels
|
||||||
{
|
{
|
||||||
|
@ -57,12 +58,24 @@ namespace SourceGit.ViewModels
|
||||||
|
|
||||||
public override bool Continue()
|
public override bool Continue()
|
||||||
{
|
{
|
||||||
var succ = base.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",
|
||||||
|
}.Exec();
|
||||||
|
|
||||||
if (succ)
|
if (succ)
|
||||||
{
|
{
|
||||||
|
var jobsFile = Path.Combine(_gitDir, "sourcegit_rebase_jobs.json");
|
||||||
var rebaseMergeHead = Path.Combine(_gitDir, "REBASE_HEAD");
|
var rebaseMergeHead = Path.Combine(_gitDir, "REBASE_HEAD");
|
||||||
var rebaseMergeFolder = Path.Combine(_gitDir, "rebase-merge");
|
var rebaseMergeFolder = Path.Combine(_gitDir, "rebase-merge");
|
||||||
var rebaseApplyFolder = Path.Combine(_gitDir, "rebase-apply");
|
var rebaseApplyFolder = Path.Combine(_gitDir, "rebase-apply");
|
||||||
|
if (File.Exists(jobsFile))
|
||||||
|
File.Delete(jobsFile);
|
||||||
if (File.Exists(rebaseMergeHead))
|
if (File.Exists(rebaseMergeHead))
|
||||||
File.Delete(rebaseMergeHead);
|
File.Delete(rebaseMergeHead);
|
||||||
if (Directory.Exists(rebaseMergeFolder))
|
if (Directory.Exists(rebaseMergeFolder))
|
||||||
|
|
199
src/ViewModels/InteractiveRebase.cs
Normal file
199
src/ViewModels/InteractiveRebase.cs
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace SourceGit.ViewModels
|
||||||
|
{
|
||||||
|
public class InteractiveRebaseItem : ObservableObject
|
||||||
|
{
|
||||||
|
public Models.Commit Commit
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Models.InteractiveRebaseAction Action
|
||||||
|
{
|
||||||
|
get => _action;
|
||||||
|
private set => SetProperty(ref _action, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Subject
|
||||||
|
{
|
||||||
|
get => _subject;
|
||||||
|
private set => SetProperty(ref _subject, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FullMessage
|
||||||
|
{
|
||||||
|
get => _fullMessage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _fullMessage, value))
|
||||||
|
{
|
||||||
|
var normalized = value.ReplaceLineEndings("\n");
|
||||||
|
var idx = normalized.IndexOf("\n\n", StringComparison.Ordinal);
|
||||||
|
if (idx > 0)
|
||||||
|
Subject = normalized.Substring(0, idx).ReplaceLineEndings(" ");
|
||||||
|
else
|
||||||
|
Subject = value.ReplaceLineEndings(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public InteractiveRebaseItem(Models.Commit c, string message)
|
||||||
|
{
|
||||||
|
Commit = c;
|
||||||
|
|
||||||
|
_subject = c.Subject;
|
||||||
|
_fullMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetAction(object param)
|
||||||
|
{
|
||||||
|
Action = (Models.InteractiveRebaseAction)param;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Models.InteractiveRebaseAction _action = Models.InteractiveRebaseAction.Pick;
|
||||||
|
private string _subject = string.Empty;
|
||||||
|
private string _fullMessage = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InteractiveRebase : ObservableObject
|
||||||
|
{
|
||||||
|
public Models.Branch Current
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Models.Commit On
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLoading
|
||||||
|
{
|
||||||
|
get => _isLoading;
|
||||||
|
private set => SetProperty(ref _isLoading, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<InteractiveRebaseItem> Items
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
} = new AvaloniaList<InteractiveRebaseItem>();
|
||||||
|
|
||||||
|
public InteractiveRebaseItem SelectedItem
|
||||||
|
{
|
||||||
|
get => _selectedItem;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedItem, value))
|
||||||
|
DetailContext.Commit = value != null ? value.Commit : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommitDetail DetailContext
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InteractiveRebase(Repository repo, Models.Branch current, Models.Commit on)
|
||||||
|
{
|
||||||
|
var repoPath = repo.FullPath;
|
||||||
|
_repo = repo;
|
||||||
|
|
||||||
|
Current = current;
|
||||||
|
On = on;
|
||||||
|
IsLoading = true;
|
||||||
|
DetailContext = new CommitDetail(repoPath);
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
var commits = new Commands.QueryCommits(repoPath, $"{on.SHA}...HEAD", false).Result();
|
||||||
|
var messages = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var c in commits)
|
||||||
|
{
|
||||||
|
var fullMessage = new Commands.QueryCommitFullMessage(repoPath, c.SHA).Result();
|
||||||
|
messages.Add(c.SHA, fullMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
var list = new List<InteractiveRebaseItem>();
|
||||||
|
foreach (var c in commits)
|
||||||
|
list.Add(new InteractiveRebaseItem(c, messages[c.SHA]));
|
||||||
|
|
||||||
|
Items.AddRange(list);
|
||||||
|
IsLoading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveItemUp(InteractiveRebaseItem item)
|
||||||
|
{
|
||||||
|
var idx = Items.IndexOf(item);
|
||||||
|
if (idx > 0)
|
||||||
|
{
|
||||||
|
var prev = Items[idx - 1];
|
||||||
|
Items.RemoveAt(idx - 1);
|
||||||
|
Items.Insert(idx, prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveItemDown(InteractiveRebaseItem item)
|
||||||
|
{
|
||||||
|
var idx = Items.IndexOf(item);
|
||||||
|
if (idx < Items.Count - 1)
|
||||||
|
{
|
||||||
|
var next = Items[idx + 1];
|
||||||
|
Items.RemoveAt(idx + 1);
|
||||||
|
Items.Insert(idx, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> Start()
|
||||||
|
{
|
||||||
|
_repo.SetWatcherEnabled(false);
|
||||||
|
|
||||||
|
var saveFile = Path.Combine(_repo.GitDir, "sourcegit_rebase_jobs.json");
|
||||||
|
var jobs = new List<Models.InteractiveRebaseJob>();
|
||||||
|
for (int i = Items.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var item = Items[i];
|
||||||
|
jobs.Add(new Models.InteractiveRebaseJob()
|
||||||
|
{
|
||||||
|
SHA = item.Commit.SHA,
|
||||||
|
Action = item.Action,
|
||||||
|
Message = item.FullMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
File.WriteAllText(saveFile, JsonSerializer.Serialize(jobs, JsonCodeGen.Default.ListInteractiveRebaseJob));
|
||||||
|
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
var succ = new Commands.InteractiveRebase(_repo.FullPath, On.SHA).Exec();
|
||||||
|
if (succ)
|
||||||
|
File.Delete(saveFile);
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Invoke(() => _repo.SetWatcherEnabled(true));
|
||||||
|
return succ;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Repository _repo = null;
|
||||||
|
private bool _isLoading = false;
|
||||||
|
private InteractiveRebaseItem _selectedItem = null;
|
||||||
|
}
|
||||||
|
}
|
298
src/Views/InteractiveRebase.axaml
Normal file
298
src/Views/InteractiveRebase.axaml
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
<v:ChromelessWindow xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:m="using:SourceGit.Models"
|
||||||
|
xmlns:vm="using:SourceGit.ViewModels"
|
||||||
|
xmlns:c="using:SourceGit.Converters"
|
||||||
|
xmlns:v="using:SourceGit.Views"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="SourceGit.Views.InteractiveRebase"
|
||||||
|
x:DataType="vm:InteractiveRebase"
|
||||||
|
Title="{DynamicResource Text.InteractiveRebase}"
|
||||||
|
Width="1080" Height="720"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||||
|
<!-- TitleBar -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="30">
|
||||||
|
<Border Grid.Column="0" Grid.ColumnSpan="3"
|
||||||
|
Background="{DynamicResource Brush.TitleBar}"
|
||||||
|
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"
|
||||||
|
PointerPressed="BeginMoveWindow"/>
|
||||||
|
|
||||||
|
<Path Grid.Column="0"
|
||||||
|
Width="14" Height="14"
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
Data="{StaticResource Icons.InteractiveRebase}"
|
||||||
|
IsVisible="{OnPlatform True, macOS=False}"/>
|
||||||
|
|
||||||
|
<Grid Grid.Column="0" Classes="caption_button_box" Margin="2,4,0,0" IsVisible="{OnPlatform False, macOS=True}">
|
||||||
|
<Button Classes="caption_button_macos" Click="CloseWindow">
|
||||||
|
<Grid>
|
||||||
|
<Ellipse Fill="{DynamicResource Brush.MacOS.Close}"/>
|
||||||
|
<Path Height="6" Width="6" Stretch="Fill" Fill="#404040" Stroke="#404040" StrokeThickness="1" Data="{StaticResource Icons.Window.Close}"/>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="0" Grid.ColumnSpan="3"
|
||||||
|
Classes="bold"
|
||||||
|
Text="{DynamicResource Text.InteractiveRebase}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
IsHitTestVisible="False"/>
|
||||||
|
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Classes="caption_button"
|
||||||
|
Click="CloseWindow"
|
||||||
|
IsVisible="{OnPlatform True, macOS=False}">
|
||||||
|
<Path Data="{StaticResource Icons.Window.Close}"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Operation Information -->
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*" Margin="8">
|
||||||
|
<TextBlock Grid.Column="0" Text="{DynamicResource Text.InteractiveRebase.Target}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
|
||||||
|
<Path Grid.Column="1" Width="14" Height="14" Margin="8,0,0,0" Data="{StaticResource Icons.Branch}"/>
|
||||||
|
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding Current, Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="3" Margin="48,0,0,0" Text="{DynamicResource Text.InteractiveRebase.On}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
|
||||||
|
<Path Grid.Column="4" Width="14" Height="14" Margin="8,8,0,0" Data="{StaticResource Icons.Commit}"/>
|
||||||
|
<TextBlock Grid.Column="5" Classes="monospace" VerticalAlignment="Center" Text="{Binding On.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0"/>
|
||||||
|
<TextBlock Grid.Column="6" VerticalAlignment="Center" Text="{Binding On.Subject}" Margin="4,0,0,0" TextTrimming="CharacterEllipsis"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<Border Grid.Row="2" Margin="8,0,8,8" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border0}">
|
||||||
|
<Grid RowDefinitions="*,3,*">
|
||||||
|
<DataGrid Grid.Row="0"
|
||||||
|
Background="{DynamicResource Brush.Contents}"
|
||||||
|
ItemsSource="{Binding Items}"
|
||||||
|
SelectionMode="Single"
|
||||||
|
SelectedItem="{Binding SelectedItem, Mode=OneWayToSource}"
|
||||||
|
CanUserReorderColumns="False"
|
||||||
|
CanUserResizeColumns="False"
|
||||||
|
CanUserSortColumns="False"
|
||||||
|
DragDrop.AllowDrop="True"
|
||||||
|
IsReadOnly="True"
|
||||||
|
HeadersVisibility="None"
|
||||||
|
Focusable="False"
|
||||||
|
RowHeight="28"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
KeyDown="OnDataGridKeyDown">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTemplateColumn Header="Option">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
|
||||||
|
<Button Opacity="1" Margin="4,0,0,0" Padding="8,2" Background="Transparent">
|
||||||
|
<Button.Flyout>
|
||||||
|
<MenuFlyout Placement="BottomEdgeAlignedLeft" VerticalOffset="-4">
|
||||||
|
<MenuItem InputGesture="P" Command="{Binding SetAction}" CommandParameter="{x:Static m:InteractiveRebaseAction.Pick}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<Ellipse Width="14" Height="14" Fill="Green"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<Grid ColumnDefinitions="64,240">
|
||||||
|
<TextBlock Grid.Column="0" Classes="monospace" Margin="4,0" Text="Pick"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Use this commit" Foreground="{DynamicResource Brush.FG2}"/>
|
||||||
|
</Grid>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem InputGesture="E" Command="{Binding SetAction}" CommandParameter="{x:Static m:InteractiveRebaseAction.Edit}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<Ellipse Width="14" Height="14" Fill="Orange"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<Grid ColumnDefinitions="64,240">
|
||||||
|
<TextBlock Grid.Column="0" Classes="monospace" Margin="4,0" Text="Edit"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Stop for amending" Foreground="{DynamicResource Brush.FG2}"/>
|
||||||
|
</Grid>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem InputGesture="R" Command="{Binding SetAction}" CommandParameter="{x:Static m:InteractiveRebaseAction.Reword}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<Ellipse Width="14" Height="14" Fill="Orange"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<Grid ColumnDefinitions="64,240">
|
||||||
|
<TextBlock Grid.Column="0" Classes="monospace" Margin="4,0" Text="Reword"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Edit the commit message" Foreground="{DynamicResource Brush.FG2}"/>
|
||||||
|
</Grid>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem InputGesture="S" Command="{Binding SetAction}" CommandParameter="{x:Static m:InteractiveRebaseAction.Squash}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<Ellipse Width="14" Height="14" Fill="LightGray"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<Grid ColumnDefinitions="64,240">
|
||||||
|
<TextBlock Grid.Column="0" Classes="monospace" Margin="4,0" Text="Squash"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Meld into previous commit" Foreground="{DynamicResource Brush.FG2}"/>
|
||||||
|
</Grid>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem InputGesture="F" Command="{Binding SetAction}" CommandParameter="{x:Static m:InteractiveRebaseAction.Fixup}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<Ellipse Width="14" Height="14" Fill="LightGray"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<Grid ColumnDefinitions="64,240">
|
||||||
|
<TextBlock Grid.Column="0" Classes="monospace" Margin="4,0" Text="Fixup"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Like 'Squash' but discard message" Foreground="{DynamicResource Brush.FG2}"/>
|
||||||
|
</Grid>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem InputGesture="D" Command="{Binding SetAction}" CommandParameter="{x:Static m:InteractiveRebaseAction.Drop}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<Ellipse Width="14" Height="14" Fill="Red"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<Grid ColumnDefinitions="64,240">
|
||||||
|
<TextBlock Grid.Column="0" Classes="monospace" Margin="4,0" Text="Drop"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Remove commit" Foreground="{DynamicResource Brush.FG2}"/>
|
||||||
|
</Grid>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse Grid.Column="0" Width="14" Height="14" Fill="{Binding Action, Converter={x:Static c:InteractiveRebaseActionConverters.ToIconBrush}}"/>
|
||||||
|
<TextBlock Grid.Column="1" Classes="monospace" Margin="8,0" Text="{Binding Action, Converter={x:Static c:InteractiveRebaseActionConverters.ToName}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Width="*" Header="SUBJECT">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Classes="icon_button" IsVisible="{Binding Action, Converter={x:Static c:InteractiveRebaseActionConverters.CanEditMessage}}">
|
||||||
|
<Button.Flyout>
|
||||||
|
<Flyout Placement="BottomEdgeAlignedLeft">
|
||||||
|
<Panel Width="600" Height="200">
|
||||||
|
<TextBox Grid.Row="0"
|
||||||
|
CornerRadius="2"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
VerticalContentAlignment="Top"
|
||||||
|
Text="{Binding FullMessage, Mode=TwoWay}"
|
||||||
|
v:AutoFocusBehaviour.IsEnabled="True"/>
|
||||||
|
</Panel>
|
||||||
|
</Flyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
<Path Width="14" Height="14" Margin="0,4,0,0" Data="{StaticResource Icons.Edit}"/>
|
||||||
|
</Button>
|
||||||
|
<TextBlock Classes="monospace" Text="{Binding Subject}" Margin="8,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Header="AVATAR">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
|
||||||
|
<v:Avatar Width="16" Height="16"
|
||||||
|
Margin="16,0,8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsHitTestVisible="False"
|
||||||
|
User="{Binding Commit.Author}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn MaxWidth="100" Header="AUTHOR">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
|
||||||
|
<TextBlock Classes="monospace" Text="{Binding Commit.Author.Name}" Margin="0,0,8,0"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Header="SHA">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
|
||||||
|
<TextBlock Classes="monospace"
|
||||||
|
Text="{Binding Commit.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"
|
||||||
|
Margin="12,0"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Header="TIME">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
|
||||||
|
<TextBlock Classes="monospace" Text="{Binding Commit.CommitterTimeStr}" Margin="8,0"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Width="32" Header="MOVE UP">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
|
||||||
|
<Button Classes="icon_button" Click="OnMoveItemUp" ToolTip.Tip="{DynamicResource Text.InteractiveRebase.MoveUp}">
|
||||||
|
<Path Width="14" Height="14" Margin="0,4,0,0" Data="{StaticResource Icons.Up}"/>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Width="32" Header="MOVE DOWN">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="{x:Type vm:InteractiveRebaseItem}">
|
||||||
|
<Button Classes="icon_button" Click="OnMoveItemDown" ToolTip.Tip="{DynamicResource Text.InteractiveRebase.MoveDown}">
|
||||||
|
<Path Width="14" Height="14" Margin="0,4,0,0" Data="{StaticResource Icons.Down}"/>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<v:LoadingIcon Grid.Row="0" Width="48" Height="48" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding IsLoading}"/>
|
||||||
|
|
||||||
|
<GridSplitter Grid.Row="1"
|
||||||
|
MinHeight="1"
|
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
|
Background="Transparent"/>
|
||||||
|
|
||||||
|
<Border Grid.Row="2" Background="{DynamicResource Brush.Window}">
|
||||||
|
<Path Width="128" Height="128"
|
||||||
|
Data="{StaticResource Icons.Detail}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Fill="{DynamicResource Brush.FG2}"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" IsVisible="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
|
<ContentControl Content="{Binding DetailContext}">
|
||||||
|
<ContentControl.DataTemplates>
|
||||||
|
<DataTemplate DataType="vm:CommitDetail">
|
||||||
|
<v:CommitDetail/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ContentControl.DataTemplates>
|
||||||
|
</ContentControl>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Options -->
|
||||||
|
<Grid Grid.Row="3" ColumnDefinitions="*,Auto,Auto" Margin="8,0,8,8">
|
||||||
|
<ProgressBar x:Name="Running"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="0,0,32,0"
|
||||||
|
Background="{DynamicResource Brush.FG2}"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="100"
|
||||||
|
IsVisible="False"/>
|
||||||
|
<Button Grid.Column="1" Classes="flat primary" MinWidth="80" Content="{DynamicResource Text.Start}" Click="StartJobs"/>
|
||||||
|
<Button Grid.Column="2" Classes="flat" MinWidth="80" Content="{DynamicResource Text.Cancel}" Click="CloseWindow"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</v:ChromelessWindow>
|
79
src/Views/InteractiveRebase.axaml.cs
Normal file
79
src/Views/InteractiveRebase.axaml.cs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace SourceGit.Views
|
||||||
|
{
|
||||||
|
public partial class InteractiveRebase : ChromelessWindow
|
||||||
|
{
|
||||||
|
public InteractiveRebase()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginMoveWindow(object sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
BeginMoveDrag(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseWindow(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMoveItemUp(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Control control && DataContext is ViewModels.InteractiveRebase vm)
|
||||||
|
{
|
||||||
|
vm.MoveItemUp(control.DataContext as ViewModels.InteractiveRebaseItem);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMoveItemDown(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Control control && DataContext is ViewModels.InteractiveRebase vm)
|
||||||
|
{
|
||||||
|
vm.MoveItemDown(control.DataContext as ViewModels.InteractiveRebaseItem);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataGridKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
var datagrid = sender as DataGrid;
|
||||||
|
var item = datagrid.SelectedItem as ViewModels.InteractiveRebaseItem;
|
||||||
|
if (item == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var vm = DataContext as ViewModels.InteractiveRebase;
|
||||||
|
if (e.Key == Key.P)
|
||||||
|
item.SetAction(Models.InteractiveRebaseAction.Pick);
|
||||||
|
else if (e.Key == Key.E)
|
||||||
|
item.SetAction(Models.InteractiveRebaseAction.Edit);
|
||||||
|
else if (e.Key == Key.R)
|
||||||
|
item.SetAction(Models.InteractiveRebaseAction.Reword);
|
||||||
|
else if (e.Key == Key.S)
|
||||||
|
item.SetAction(Models.InteractiveRebaseAction.Squash);
|
||||||
|
else if (e.Key == Key.F)
|
||||||
|
item.SetAction(Models.InteractiveRebaseAction.Fixup);
|
||||||
|
else if (e.Key == Key.D)
|
||||||
|
item.SetAction(Models.InteractiveRebaseAction.Drop);
|
||||||
|
else if (e.Key == Key.Up && e.KeyModifiers == KeyModifiers.Alt)
|
||||||
|
vm.MoveItemUp(item);
|
||||||
|
else if (e.Key == Key.Down && e.KeyModifiers == KeyModifiers.Alt)
|
||||||
|
vm.MoveItemDown(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void StartJobs(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Running.IsVisible = true;
|
||||||
|
Running.IsIndeterminate = true;
|
||||||
|
var vm = DataContext as ViewModels.InteractiveRebase;
|
||||||
|
await vm.Start();
|
||||||
|
Running.IsIndeterminate = false;
|
||||||
|
Running.IsVisible = false;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,7 +33,7 @@
|
||||||
AcceptsReturn="True"
|
AcceptsReturn="True"
|
||||||
VerticalContentAlignment="Top"
|
VerticalContentAlignment="Top"
|
||||||
Text="{Binding Message, Mode=TwoWay}"
|
Text="{Binding Message, Mode=TwoWay}"
|
||||||
v:AutoFocusBehaviour.IsEnabled="True"/>
|
v:AutoFocusBehaviour.IsEnabled="True"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
Loading…
Reference in a new issue