feature: add context menu for both branch and commit to compare selected with current HEAD

This commit is contained in:
leo 2024-05-27 17:21:28 +08:00
parent de6375da7c
commit 4249653ed6
8 changed files with 254 additions and 10 deletions

View file

@ -17,6 +17,7 @@ namespace SourceGit.Commands
public QueryCommits(string repo, string limits, bool needFindHead = true) public QueryCommits(string repo, string limits, bool needFindHead = true)
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo;
Args = "log --date-order --decorate=full --pretty=raw " + limits; Args = "log --date-order --decorate=full --pretty=raw " + limits;
findFirstMerged = needFindHead; findFirstMerged = needFindHead;
} }

View file

@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QuerySingleCommit : Command
{
private const string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private const string GPGSIG_END = " -----END PGP SIGNATURE-----";
public QuerySingleCommit(string repo, string sha) {
WorkingDirectory = repo;
Context = repo;
Args = $"show --pretty=raw --decorate=full -s {sha}";
}
public Models.Commit Result()
{
var succ = Exec();
if (!succ)
return null;
_commit.Message.Trim();
return _commit;
}
protected override void OnReadline(string line)
{
if (isSkipingGpgsig)
{
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal))
isSkipingGpgsig = false;
return;
}
else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal))
{
isSkipingGpgsig = true;
return;
}
if (line.StartsWith("commit ", StringComparison.Ordinal))
{
line = line.Substring(7);
var decoratorStart = line.IndexOf('(', StringComparison.Ordinal);
if (decoratorStart < 0)
{
_commit.SHA = line.Trim();
}
else
{
_commit.SHA = line.Substring(0, decoratorStart).Trim();
ParseDecorators(_commit.Decorators, line.Substring(decoratorStart + 1));
}
return;
}
if (line.StartsWith("tree ", StringComparison.Ordinal))
{
return;
}
else if (line.StartsWith("parent ", StringComparison.Ordinal))
{
_commit.Parents.Add(line.Substring("parent ".Length));
}
else if (line.StartsWith("author ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
_commit.Author = user;
_commit.AuthorTime = time;
}
else if (line.StartsWith("committer ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time);
_commit.Committer = user;
_commit.CommitterTime = time;
}
else if (string.IsNullOrEmpty(_commit.Subject))
{
_commit.Subject = line.Trim();
}
else
{
_commit.Message += (line.Trim() + "\n");
}
}
private bool ParseDecorators(List<Models.Decorator> decorators, string data)
{
bool isHeadOfCurrent = false;
var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs)
{
var d = sub.Trim();
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.Tag,
Name = d.Substring(15).Trim(),
});
}
else if (d.EndsWith("/HEAD", StringComparison.Ordinal))
{
continue;
}
else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal))
{
isHeadOfCurrent = true;
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim(),
});
}
else if (d.Equals("HEAD"))
{
isHeadOfCurrent = true;
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.CurrentCommitHead,
Name = d.Trim(),
});
}
else if (d.StartsWith("refs/heads/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim(),
});
}
else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim(),
});
}
}
decorators.Sort((l, r) =>
{
if (l.Type != r.Type)
{
return (int)l.Type - (int)r.Type;
}
else
{
return l.Name.CompareTo(r.Name);
}
});
return isHeadOfCurrent;
}
private Models.Commit _commit = new Models.Commit();
private bool isSkipingGpgsig = false;
}
}

View file

@ -97,4 +97,5 @@
<StreamGeometry x:Key="Icons.GitFlow.Release">M884 159l-18-18a43 43 0 00-38-12l-235 43a166 166 0 00-101 60L400 349a128 128 0 00-148 47l-120 171a21 21 0 005 29l17 12a128 128 0 00178-32l27-38 124 124-38 27a128 128 0 00-32 178l12 17a21 21 0 0029 5l171-120a128 128 0 0047-148l117-92A166 166 0 00853 431l43-235a43 43 0 00-12-38zm-177 249a64 64 0 110-90 64 64 0 010 90zm-373 312a21 21 0 010 30l-139 139a21 21 0 01-30 0l-30-30a21 21 0 010-30l139-139a21 21 0 0130 0z</StreamGeometry> <StreamGeometry x:Key="Icons.GitFlow.Release">M884 159l-18-18a43 43 0 00-38-12l-235 43a166 166 0 00-101 60L400 349a128 128 0 00-148 47l-120 171a21 21 0 005 29l17 12a128 128 0 00178-32l27-38 124 124-38 27a128 128 0 00-32 178l12 17a21 21 0 0029 5l171-120a128 128 0 0047-148l117-92A166 166 0 00853 431l43-235a43 43 0 00-12-38zm-177 249a64 64 0 110-90 64 64 0 010 90zm-373 312a21 21 0 010 30l-139 139a21 21 0 01-30 0l-30-30a21 21 0 010-30l139-139a21 21 0 0130 0z</StreamGeometry>
<StreamGeometry x:Key="Icons.Lines.Incr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z</StreamGeometry> <StreamGeometry x:Key="Icons.Lines.Incr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z</StreamGeometry>
<StreamGeometry x:Key="Icons.Lines.Decr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z</StreamGeometry> <StreamGeometry x:Key="Icons.Lines.Decr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z</StreamGeometry>
<StreamGeometry x:Key="Icons.Compare">M645 448l64 64 220-221L704 64l-64 64 115 115H128v90h628zM375 576l-64-64-220 224L314 960l64-64-116-115H896v-90H262z</StreamGeometry>
</ResourceDictionary> </ResourceDictionary>

View file

@ -33,6 +33,7 @@
<x:String x:Key="Text.Blame" xml:space="preserve">Blame</x:String> <x:String x:Key="Text.Blame" xml:space="preserve">Blame</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">BLAME ON THIS FILE IS NOT SUPPORTED!!!</x:String> <x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">BLAME ON THIS FILE IS NOT SUPPORTED!!!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Checkout${0}$</x:String> <x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Checkout${0}$</x:String>
<x:String x:Key="Text.BranchCM.CompareWithHead" xml:space="preserve">Compare with HEAD</x:String>
<x:String x:Key="Text.BranchCM.CopyName" xml:space="preserve">Copy Branch Name</x:String> <x:String x:Key="Text.BranchCM.CopyName" xml:space="preserve">Copy Branch Name</x:String>
<x:String x:Key="Text.BranchCM.Delete" xml:space="preserve">Delete${0}$</x:String> <x:String x:Key="Text.BranchCM.Delete" xml:space="preserve">Delete${0}$</x:String>
<x:String x:Key="Text.BranchCM.DeleteMultiBranches" xml:space="preserve">Delete selected {0} branches</x:String> <x:String x:Key="Text.BranchCM.DeleteMultiBranches" xml:space="preserve">Delete selected {0} branches</x:String>
@ -76,8 +77,9 @@
<x:String x:Key="Text.Clone.RemoteURL" xml:space="preserve">Repository URL :</x:String> <x:String x:Key="Text.Clone.RemoteURL" xml:space="preserve">Repository URL :</x:String>
<x:String x:Key="Text.Close" xml:space="preserve">CLOSE</x:String> <x:String x:Key="Text.Close" xml:space="preserve">CLOSE</x:String>
<x:String x:Key="Text.CommitCM.CherryPick" xml:space="preserve">Cherry-Pick This Commit</x:String> <x:String x:Key="Text.CommitCM.CherryPick" xml:space="preserve">Cherry-Pick This Commit</x:String>
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">Copy SHA</x:String>
<x:String x:Key="Text.CommitCM.Checkout" xml:space="preserve">Checkout Commit</x:String> <x:String x:Key="Text.CommitCM.Checkout" xml:space="preserve">Checkout Commit</x:String>
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">Compare with HEAD</x:String>
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">Copy SHA</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>

View file

@ -33,6 +33,7 @@
<x:String x:Key="Text.Blame" xml:space="preserve">逐行追溯(blame)</x:String> <x:String x:Key="Text.Blame" xml:space="preserve">逐行追溯(blame)</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">选中文件不支持该操作!!!</x:String> <x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">选中文件不支持该操作!!!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">检出(checkout)${0}$</x:String> <x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">检出(checkout)${0}$</x:String>
<x:String x:Key="Text.BranchCM.CompareWithHead" xml:space="preserve">与当前HEAD比较</x:String>
<x:String x:Key="Text.BranchCM.CopyName" xml:space="preserve">复制分支名</x:String> <x:String x:Key="Text.BranchCM.CopyName" xml:space="preserve">复制分支名</x:String>
<x:String x:Key="Text.BranchCM.Delete" xml:space="preserve">删除${0}$</x:String> <x:String x:Key="Text.BranchCM.Delete" xml:space="preserve">删除${0}$</x:String>
<x:String x:Key="Text.BranchCM.DeleteMultiBranches" xml:space="preserve">删除选中的 {0} 个分支</x:String> <x:String x:Key="Text.BranchCM.DeleteMultiBranches" xml:space="preserve">删除选中的 {0} 个分支</x:String>
@ -77,6 +78,7 @@
<x:String x:Key="Text.Close" xml:space="preserve">关闭</x:String> <x:String x:Key="Text.Close" xml:space="preserve">关闭</x:String>
<x:String x:Key="Text.CommitCM.CherryPick" xml:space="preserve">挑选(cherry-pick)此提交</x:String> <x:String x:Key="Text.CommitCM.CherryPick" xml:space="preserve">挑选(cherry-pick)此提交</x:String>
<x:String x:Key="Text.CommitCM.Checkout" xml:space="preserve">检出此提交</x:String> <x:String x:Key="Text.CommitCM.Checkout" xml:space="preserve">检出此提交</x:String>
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">与当前HEAD比较</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.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>

View file

@ -69,7 +69,7 @@ namespace SourceGit.ViewModels
public Models.Commit AutoSelectedCommit public Models.Commit AutoSelectedCommit
{ {
get => _autoSelectedCommit; get => _autoSelectedCommit;
private set => SetProperty(ref _autoSelectedCommit, value); set => SetProperty(ref _autoSelectedCommit, value);
} }
public long NavigationId public long NavigationId
@ -81,7 +81,7 @@ namespace SourceGit.ViewModels
public object DetailContext public object DetailContext
{ {
get => _detailContext; get => _detailContext;
private set => SetProperty(ref _detailContext, value); set => SetProperty(ref _detailContext, value);
} }
public Histories(Repository repo) public Histories(Repository repo)
@ -171,17 +171,16 @@ namespace SourceGit.ViewModels
} }
} }
public ContextMenu MakeContextMenu() public ContextMenu MakeContextMenu(DataGrid datagrid)
{ {
var detail = _detailContext as CommitDetail; if (datagrid.SelectedItems.Count != 1)
if (detail == null)
return null; return null;
var current = _repo.Branches.Find(x => x.IsCurrent); var current = _repo.Branches.Find(x => x.IsCurrent);
if (current == null) if (current == null)
return null; return null;
var commit = detail.Commit; var commit = datagrid.SelectedItem as Models.Commit;
var menu = new ContextMenu(); var menu = new ContextMenu();
var tags = new List<Models.Tag>(); var tags = new List<Models.Tag>();
@ -317,6 +316,33 @@ namespace SourceGit.ViewModels
menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(new MenuItem() { Header = "-" });
if (current.Head != commit.SHA)
{
var compare = new MenuItem();
compare.Header = App.Text("CommitCM.CompareWithHead");
compare.Icon = App.CreateMenuIcon("Icons.Compare");
compare.Click += (o, e) =>
{
var head = _commits.Find(x => x.SHA == current.Head);
if (head == null)
{
_repo.SearchResultSelectedCommit = null;
head = new Commands.QuerySingleCommit(_repo.FullPath, current.Head).Result();
if (head != null)
DetailContext = new RevisionCompare(_repo.FullPath, commit, head);
}
else
{
datagrid.SelectedItems.Add(head);
}
e.Handled = true;
};
menu.Items.Add(compare);
menu.Items.Add(new MenuItem() { Header = "-" });
}
var createBranch = new MenuItem(); var createBranch = new MenuItem();
createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add");
createBranch.Header = App.Text("CreateBranch"); createBranch.Header = App.Text("CreateBranch");

View file

@ -928,6 +928,27 @@ namespace SourceGit.ViewModels
menu.Items.Add(merge); menu.Items.Add(merge);
menu.Items.Add(rebase); menu.Items.Add(rebase);
var compare = new MenuItem();
compare.Header = App.Text("BranchCM.CompareWithHead");
compare.Icon = App.CreateMenuIcon("Icons.Compare");
compare.Click += (o, e) =>
{
SearchResultSelectedCommit = null;
if (_histories != null)
{
var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result();
var head = new Commands.QuerySingleCommit(FullPath, current.Head).Result();
_histories.AutoSelectedCommit = null;
_histories.DetailContext = new RevisionCompare(FullPath, target, head);
}
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(compare);
} }
var type = GitFlow.GetBranchType(branch.Name); var type = GitFlow.GetBranchType(branch.Name);
@ -1197,6 +1218,30 @@ namespace SourceGit.ViewModels
menu.Items.Add(merge); menu.Items.Add(merge);
menu.Items.Add(rebase); menu.Items.Add(rebase);
menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(new MenuItem() { Header = "-" });
if (current.Head != branch.Head)
{
var compare = new MenuItem();
compare.Header = App.Text("BranchCM.CompareWithHead");
compare.Icon = App.CreateMenuIcon("Icons.Compare");
compare.Click += (o, e) =>
{
SearchResultSelectedCommit = null;
if (_histories != null)
{
var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result();
var head = new Commands.QuerySingleCommit(FullPath, current.Head).Result();
_histories.AutoSelectedCommit = null;
_histories.DetailContext = new RevisionCompare(FullPath, target, head);
}
e.Handled = true;
};
menu.Items.Add(compare);
menu.Items.Add(new MenuItem() { Header = "-" });
}
} }
var delete = new MenuItem(); var delete = new MenuItem();

View file

@ -297,10 +297,10 @@ namespace SourceGit.Views
private void OnCommitDataGridContextRequested(object sender, ContextRequestedEventArgs e) private void OnCommitDataGridContextRequested(object sender, ContextRequestedEventArgs e)
{ {
if (DataContext is ViewModels.Histories histories) if (DataContext is ViewModels.Histories histories && sender is DataGrid datagrid)
{ {
var menu = histories.MakeContextMenu(); var menu = histories.MakeContextMenu(datagrid);
(sender as Control)?.OpenContextMenu(menu); datagrid.OpenContextMenu(menu);
} }
e.Handled = true; e.Handled = true;
} }