diff --git a/README.md b/README.md index 50d00f58..2d222996 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ * Search commits * GitFlow * Git LFS +* Bisect * Issue Link * Workspace * Custom Action diff --git a/src/Commands/Bisect.cs b/src/Commands/Bisect.cs new file mode 100644 index 00000000..20beeab9 --- /dev/null +++ b/src/Commands/Bisect.cs @@ -0,0 +1,12 @@ +namespace SourceGit.Commands +{ + public class Bisect : Command + { + public Bisect(string repo, string subcmd) + { + WorkingDirectory = repo; + Context = repo; + Args = $"bisect {subcmd}"; + } + } +} diff --git a/src/Models/Bisect.cs b/src/Models/Bisect.cs new file mode 100644 index 00000000..8ef5ec78 --- /dev/null +++ b/src/Models/Bisect.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public enum BisectState + { + None = 0, + WaitingForRange, + Detecting, + } + + public enum BisectCommitFlag + { + None = 0, + Good = 1, + Bad = 2, + } + + public class Bisect + { + public HashSet Bads + { + get; + set; + } = []; + + public HashSet Goods + { + get; + set; + } = []; + } +} diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 3ccecad4..e930f412 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -168,6 +168,7 @@ namespace SourceGit.Models _updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); } else if (name.Equals("HEAD", StringComparison.Ordinal) || + name.Equals("BISECT_START", StringComparison.Ordinal) || name.StartsWith("refs/heads/", StringComparison.Ordinal) || name.StartsWith("refs/remotes/", StringComparison.Ordinal) || (name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal))) diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index 51e3d8bf..9da3a51e 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -2,7 +2,9 @@ M41 512c0-128 46-241 138-333C271 87 384 41 512 41s241 46 333 138c92 92 138 205 138 333s-46 241-138 333c-92 92-205 138-333 138s-241-46-333-138C87 753 41 640 41 512zm87 0c0 108 36 195 113 271s164 113 271 113c108 0 195-36 271-113s113-164 113-271-36-195-113-271c-77-77-164-113-271-113-108 0-195 36-271 113C164 317 128 404 128 512zm256 148V292l195 113L768 512l-195 113-195 113v-77zm148-113-61 36V440l61 36 61 36-61 36z M304 464a128 128 0 01128-128c71 0 128 57 128 128v224a32 32 0 01-64 0V592h-128v95a32 32 0 01-64 0v-224zm64 1v64h128v-64a64 64 0 00-64-64c-35 0-64 29-64 64zM688 337c18 0 32 14 32 32v319a32 32 0 01-32 32c-18 0-32-14-32-32v-319a32 32 0 0132-32zM84 911l60-143A446 446 0 0164 512C64 265 265 64 512 64s448 201 448 448-201 448-448 448c-54 0-105-9-153-27l-242 22a32 32 0 01-32-44zm133-150-53 126 203-18 13 5c41 15 85 23 131 23 212 0 384-172 384-384S724 128 512 128 128 300 128 512c0 82 26 157 69 220l20 29z M296 392h64v64h-64zM296 582v160h128V582h-64v-62h-64v62zm80 48v64h-32v-64h32zM360 328h64v64h-64zM296 264h64v64h-64zM360 456h64v64h-64zM360 200h64v64h-64zM855 289 639 73c-6-6-14-9-23-9H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V311c0-9-3-17-9-23zM790 326H602V138L790 326zm2 562H232V136h64v64h64v-64h174v216c0 23 19 42 42 42h216v494z + M851 755q0 23-16 39l-78 78q-16 16-39 16t-39-16l-168-168-168 168q-16 16-39 16t-39-16l-78-78q-16-16-16-39t16-39l168-168-168-168q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 168 168-168q16-16 39-16t39 16l78 78q16 16 16 39t-16 39l-168 168 168 168q16 16 16 39z M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z + M128 384a43 43 0 0043-43V213a43 43 0 0143-43h128a43 43 0 000-85H213a128 128 0 00-128 128v128a43 43 0 0043 43zm213 469H213a43 43 0 01-43-43v-128a43 43 0 00-85 0v128a128 128 0 00128 128h128a43 43 0 000-85zm384-299a43 43 0 000-85h-49A171 171 0 00555 347V299a43 43 0 00-85 0v49A171 171 0 00347 469H299a43 43 0 000 85h49A171 171 0 00469 677V725a43 43 0 0085 0v-49A171 171 0 00677 555zm-213 43a85 85 0 1185-85 85 85 0 01-85 85zm384 43a43 43 0 00-43 43v128a43 43 0 01-43 43h-128a43 43 0 000 85h128a128 128 0 00128-128v-128a43 43 0 00-43-43zM811 85h-128a43 43 0 000 85h128a43 43 0 0143 43v128a43 43 0 0085 0V213a128 128 0 00-128-128z M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z M832 64H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V96c0-18-14-32-32-32zM736 596 624 502 506 596V131h230v318z M509 546 780 275 871 366 509 728 147 366 238 275zM509 728h-362v128h724v-128z diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 20a70f22..3c710727 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -36,6 +36,13 @@ NO FILES ASSUMED AS UNCHANGED REMOVE BINARY FILE NOT SUPPORTED!!! + Bisect + Abort + Bad + Bisecting. Is current HEAD good or bad? + Good + Skip + Bisecting. Mark current commit as good or bad and checkout another one. Blame BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout ${0}$... diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 3d41bccd..843c3735 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -40,6 +40,13 @@ 没有不跟踪更改的文件 移除 二进制文件不支持该操作!!! + 二分定位(bisect) + 终止 + 标记错误 + 二分定位进行中。当前提交是 '正确' 还是 '错误'? + 标记正确 + 该提交无法判定 + 二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。 逐行追溯(blame) 选中文件不支持该操作!!! 检出(checkout) ${0}$... diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index a9f529e2..555954d9 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; @@ -62,6 +63,12 @@ namespace SourceGit.ViewModels set => SetProperty(ref _detailContext, value); } + public Models.Bisect Bisect + { + get => _bisect; + private set => SetProperty(ref _bisect, value); + } + public GridLength LeftArea { get => _leftArea; @@ -111,6 +118,37 @@ namespace SourceGit.ViewModels _detailContext = null; } + public Models.BisectState UpdateBisectInfo() + { + var test = Path.Combine(_repo.GitDir, "BISECT_START"); + if (!File.Exists(test)) + { + Bisect = null; + return Models.BisectState.None; + } + + var info = new Models.Bisect(); + var dir = Path.Combine(_repo.GitDir, "refs", "bisect"); + if (Directory.Exists(dir)) + { + var files = new DirectoryInfo(dir).GetFiles(); + foreach (var file in files) + { + if (file.Name.StartsWith("bad")) + info.Bads.Add(File.ReadAllText(file.FullName).Trim()); + else if (file.Name.StartsWith("good")) + info.Goods.Add(File.ReadAllText(file.FullName).Trim()); + } + } + + Bisect = info; + + if (info.Bads.Count == 0 || info.Goods.Count == 0) + return Models.BisectState.WaitingForRange; + else + return Models.BisectState.Detecting; + } + public void NavigateTo(string commitSHA) { var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal)); @@ -1209,7 +1247,7 @@ namespace SourceGit.ViewModels } builder.Append(".patch"); - return System.IO.Path.Combine(dir, builder.ToString()); + return Path.Combine(dir, builder.ToString()); } private Repository _repo = null; @@ -1220,6 +1258,8 @@ namespace SourceGit.ViewModels private long _navigationId = 0; private object _detailContext = null; + private Models.Bisect _bisect = null; + private GridLength _leftArea = new GridLength(1, GridUnitType.Star); private GridLength _rightArea = new GridLength(1, GridUnitType.Star); private GridLength _topArea = new GridLength(1, GridUnitType.Star); diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 04f938ed..ddc967fd 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -398,6 +398,18 @@ namespace SourceGit.ViewModels get => _workingCopy?.InProgressContext; } + public Models.BisectState BisectState + { + get => _bisectState; + private set => SetProperty(ref _bisectState, value); + } + + public bool IsBisectCommandRunning + { + get => _isBisectCommandRunning; + private set => SetProperty(ref _isBisectCommandRunning, value); + } + public bool IsAutoFetching { get => _isAutoFetching; @@ -939,6 +951,31 @@ namespace SourceGit.ViewModels return actions; } + public void Bisect(string subcmd) + { + IsBisectCommandRunning = true; + SetWatcherEnabled(false); + + var log = CreateLog($"Bisect({subcmd})"); + Task.Run(() => + { + var succ = new Commands.Bisect(_fullpath, subcmd).Use(log).Exec(); + log.Complete(); + + Dispatcher.UIThread.Invoke(() => + { + if (!succ) + App.RaiseException(_fullpath, log.Content); + else if (log.Content.Contains("is the first bad commit")) + App.SendNotification(_fullpath, log.Content); + + MarkBranchesDirtyManually(); + SetWatcherEnabled(true); + IsBisectCommandRunning = false; + }); + }); + } + public void RefreshBranches() { var branches = new Commands.QueryBranches(_fullpath).Result(); @@ -1023,6 +1060,8 @@ namespace SourceGit.ViewModels _histories.Commits = commits; _histories.Graph = graph; + BisectState = _histories.UpdateBisectInfo(); + if (!string.IsNullOrEmpty(_navigateToBranchDelayed)) { var branch = _branches.Find(x => x.FullName == _navigateToBranchDelayed); @@ -2627,6 +2666,9 @@ namespace SourceGit.ViewModels private Timer _autoFetchTimer = null; private DateTime _lastFetchTime = DateTime.MinValue; + private Models.BisectState _bisectState = Models.BisectState.None; + private bool _isBisectCommandRunning = false; + private string _navigateToBranchDelayed = string.Empty; } } diff --git a/src/Views/BisectStateIndicator.cs b/src/Views/BisectStateIndicator.cs new file mode 100644 index 00000000..c93192a6 --- /dev/null +++ b/src/Views/BisectStateIndicator.cs @@ -0,0 +1,134 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace SourceGit.Views +{ + public class BisectStateIndicator : Control + { + public static readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background), Brushes.Transparent); + + public IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground), Brushes.White); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static readonly StyledProperty BisectProperty = + AvaloniaProperty.Register(nameof(Bisect)); + + public Models.Bisect Bisect + { + get => GetValue(BisectProperty); + set => SetValue(BisectProperty, value); + } + + static BisectStateIndicator() + { + AffectsMeasure(BisectProperty); + AffectsRender(BackgroundProperty, ForegroundProperty); + } + + public override void Render(DrawingContext context) + { + if (_flags == Models.BisectCommitFlag.None) + return; + + if (_prefix == null) + { + _prefix = LoadIcon("Icons.Bisect"); + _good = LoadIcon("Icons.Check"); + _bad = LoadIcon("Icons.Bad"); + } + + var x = 0.0; + + if (_flags.HasFlag(Models.BisectCommitFlag.Good)) + { + RenderImpl(context, Brushes.Green, _good, x); + x += 36; + } + + if (_flags.HasFlag(Models.BisectCommitFlag.Bad)) + RenderImpl(context, Brushes.Red, _bad, x); + } + + protected override Size MeasureOverride(Size availableSize) + { + var desiredFlags = Models.BisectCommitFlag.None; + var desiredWidth = 0.0; + if (Bisect is { } bisect && DataContext is Models.Commit commit) + { + var sha = commit.SHA; + if (bisect.Goods.Contains(sha)) + { + desiredFlags |= Models.BisectCommitFlag.Good; + desiredWidth = 36; + } + + if (bisect.Bads.Contains(sha)) + { + desiredFlags |= Models.BisectCommitFlag.Bad; + desiredWidth += 36; + } + } + + if (desiredFlags != _flags) + { + _flags = desiredFlags; + InvalidateVisual(); + } + + return new Size(desiredWidth, desiredWidth > 0 ? 16 : 0); + } + + private Geometry LoadIcon(string key) + { + var geo = this.FindResource(key) as StreamGeometry; + var drawGeo = geo!.Clone(); + var iconBounds = drawGeo.Bounds; + var translation = Matrix.CreateTranslation(-(Vector)iconBounds.Position); + var scale = Math.Min(10.0 / iconBounds.Width, 10.0 / iconBounds.Height); + var transform = translation * Matrix.CreateScale(scale, scale); + if (drawGeo.Transform == null || drawGeo.Transform.Value == Matrix.Identity) + drawGeo.Transform = new MatrixTransform(transform); + else + drawGeo.Transform = new MatrixTransform(drawGeo.Transform.Value * transform); + + return drawGeo; + } + + private void RenderImpl(DrawingContext context, IBrush brush, Geometry icon, double x) + { + var entireRect = new RoundedRect(new Rect(x, 0, 32, 16), new CornerRadius(2)); + var stateRect = new RoundedRect(new Rect(x + 16, 0, 16, 16), new CornerRadius(0, 2, 2, 0)); + context.DrawRectangle(Background, new Pen(brush), entireRect); + using (context.PushOpacity(.2)) + context.DrawRectangle(brush, null, stateRect); + context.DrawLine(new Pen(brush), new Point(x + 16, 0), new Point(x + 16, 16)); + + using (context.PushTransform(Matrix.CreateTranslation(x + 3, 3))) + context.DrawGeometry(Foreground, null, _prefix); + + using (context.PushTransform(Matrix.CreateTranslation(x + 19, 3))) + context.DrawGeometry(Foreground, null, icon); + } + + private Geometry _prefix = null; + private Geometry _good = null; + private Geometry _bad = null; + private Models.BisectCommitFlag _flags = Models.BisectCommitFlag.None; + } +} diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 96340d1e..c6c43aa9 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -131,14 +131,20 @@ ClipToBounds="True" Background="Transparent" ToolTip.Tip="{Binding Subject}"> - + + + - - SetValue(CurrentBranchProperty, value); } + public static readonly StyledProperty BisectProperty = + AvaloniaProperty.Register(nameof(Bisect)); + + public Models.Bisect Bisect + { + get => GetValue(BisectProperty); + set => SetValue(BisectProperty, value); + } + public static readonly StyledProperty> IssueTrackerRulesProperty = AvaloniaProperty.Register>(nameof(IssueTrackerRules)); diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 86026bed..16908f81 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -585,7 +585,7 @@ BorderBrush="{DynamicResource Brush.Border0}"/> - + @@ -665,7 +665,73 @@ Command="{Binding AbortMerge}"/> - + + + + + + + + + + diff --git a/src/Views/RepositoryToolbar.axaml.cs b/src/Views/RepositoryToolbar.axaml.cs index 9ddc64c8..80b5544d 100644 --- a/src/Views/RepositoryToolbar.axaml.cs +++ b/src/Views/RepositoryToolbar.axaml.cs @@ -116,6 +116,21 @@ namespace SourceGit.Views e.Handled = true; } + private void StartBisect(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository { IsBisectCommandRunning: false } repo && + repo.InProgressContext == null && + repo.CanCreatePopup()) + { + if (repo.LocalChangesCount > 0) + App.RaiseException(repo.FullPath, "You have un-committed local changes. Please discard or stash them first."); + else + repo.Bisect("start"); + } + + e.Handled = true; + } + private void OpenCustomActionMenu(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.Repository repo && sender is Control control)