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:
leo 2024-06-20 17:02:12 +08:00
parent 6c9f7e6da3
commit 7070a07e15
No known key found for this signature in database
17 changed files with 816 additions and 7 deletions

View file

@ -6,6 +6,7 @@ namespace SourceGit
[JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.JetBrainsState))]
[JsonSerializable(typeof(List<Models.InteractiveRebaseJob>))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(ViewModels.Preference))]
internal partial class JsonCodeGen : JsonSerializerContext { }

View file

@ -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)
{

View file

@ -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;

View file

@ -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}";
}
}
}

View 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);
}
}

View 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;
}
}
}
}

View file

@ -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.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.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>

View file

@ -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.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.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.Reset" xml:space="preserve">Reset ${0}$ to Here</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.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.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.Error" xml:space="preserve">ERROR</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String>

View file

@ -88,6 +88,7 @@
<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.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.Reset" xml:space="preserve">重置(reset) ${0}$ 到此处</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.Rebase" xml:space="preserve">变基Rebase操作进行中。点击【终止】回滚到操作前的状态。</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.Error" xml:space="preserve">出错了</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系统提示</x:String>

View file

@ -88,6 +88,7 @@
<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.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.Reset" xml:space="preserve">重置(reset) ${0}$ 到此處</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.Rebase" xml:space="preserve">變基Rebase操作進行中。點選【終止】回滾到操作前的狀態。</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.Error" xml:space="preserve">出錯了</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系統提示</x:String>

View file

@ -160,6 +160,11 @@
<Setter Property="FontSize" Value="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize}"/>
</Style>
<Style Selector="FlyoutPresenter">
<Setter Property="MaxWidth" Value="1024"/>
<Setter Property="MaxHeight" Value="768"/>
</Style>
<Style Selector="Path">
<Setter Property="Fill" Value="{DynamicResource Brush.FG1}"/>
<Setter Property="Stretch" Value="Uniform"/>

View file

@ -277,6 +277,18 @@ namespace SourceGit.ViewModels
e.Handled = true;
};
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)

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Diagnostics;
using System.IO;
namespace SourceGit.ViewModels
{
@ -57,12 +58,24 @@ namespace SourceGit.ViewModels
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)
{
var jobsFile = Path.Combine(_gitDir, "sourcegit_rebase_jobs.json");
var rebaseMergeHead = Path.Combine(_gitDir, "REBASE_HEAD");
var rebaseMergeFolder = Path.Combine(_gitDir, "rebase-merge");
var rebaseApplyFolder = Path.Combine(_gitDir, "rebase-apply");
if (File.Exists(jobsFile))
File.Delete(jobsFile);
if (File.Exists(rebaseMergeHead))
File.Delete(rebaseMergeHead);
if (Directory.Exists(rebaseMergeFolder))

View 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;
}
}

View 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>

View 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();
}
}
}