diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 618ff014..c165a658 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -17,6 +17,7 @@ namespace SourceGit.Commands public QueryCommits(string repo, string limits, bool needFindHead = true) { WorkingDirectory = repo; + Context = repo; Args = "log --date-order --decorate=full --pretty=raw " + limits; findFirstMerged = needFindHead; } diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs new file mode 100644 index 00000000..5c0fd760 --- /dev/null +++ b/src/Commands/QuerySingleCommit.cs @@ -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 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; + } +} diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index b61e6839..b6369398 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -97,4 +97,5 @@ 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 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 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 + M645 448l64 64 220-221L704 64l-64 64 115 115H128v90h628zM375 576l-64-64-220 224L314 960l64-64-116-115H896v-90H262z diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 685cb1be..68f92ec1 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -33,6 +33,7 @@ Blame BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout${0}$ + Compare with HEAD Copy Branch Name Delete${0}$ Delete selected {0} branches @@ -76,8 +77,9 @@ Repository URL : CLOSE Cherry-Pick This Commit - Copy SHA Checkout Commit + Compare with HEAD + Copy SHA Rebase${0}$to Here Reset${0}$to Here Revert Commit diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index ac48aec9..12dccf35 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -33,6 +33,7 @@ 逐行追溯(blame) 选中文件不支持该操作!!! 检出(checkout)${0}$ + 与当前HEAD比较 复制分支名 删除${0}$ 删除选中的 {0} 个分支 @@ -77,6 +78,7 @@ 关闭 挑选(cherry-pick)此提交 检出此提交 + 与当前HEAD比较 复制提交指纹 变基(rebase)${0}$到此处 重置(reset)${0}$到此处 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index c28b4ade..98659e2f 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -69,7 +69,7 @@ namespace SourceGit.ViewModels public Models.Commit AutoSelectedCommit { get => _autoSelectedCommit; - private set => SetProperty(ref _autoSelectedCommit, value); + set => SetProperty(ref _autoSelectedCommit, value); } public long NavigationId @@ -81,7 +81,7 @@ namespace SourceGit.ViewModels public object DetailContext { get => _detailContext; - private set => SetProperty(ref _detailContext, value); + set => SetProperty(ref _detailContext, value); } 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 (detail == null) + if (datagrid.SelectedItems.Count != 1) return null; var current = _repo.Branches.Find(x => x.IsCurrent); if (current == null) return null; - var commit = detail.Commit; + var commit = datagrid.SelectedItem as Models.Commit; var menu = new ContextMenu(); var tags = new List(); @@ -317,6 +316,33 @@ namespace SourceGit.ViewModels 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(); createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); createBranch.Header = App.Text("CreateBranch"); diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 94a04172..c35ddecf 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -928,6 +928,27 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); 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); @@ -1197,6 +1218,30 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); 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(); diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 79458d10..f1bd9698 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -297,10 +297,10 @@ namespace SourceGit.Views 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(); - (sender as Control)?.OpenContextMenu(menu); + var menu = histories.MakeContextMenu(datagrid); + datagrid.OpenContextMenu(menu); } e.Handled = true; }