Skip to content

Commit df5294b

Browse files
committed
feature: git bisect support
Signed-off-by: leo <longshuang@msn.cn>
1 parent 9eae1ee commit df5294b

16 files changed

+397
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* Search commits
4040
* GitFlow
4141
* Git LFS
42+
* Bisect
4243
* Issue Link
4344
* Workspace
4445
* Custom Action

src/Commands/Bisect.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace SourceGit.Commands
2+
{
3+
public class Bisect : Command
4+
{
5+
public Bisect(string repo, string subcmd)
6+
{
7+
WorkingDirectory = repo;
8+
Context = repo;
9+
Args = $"bisect {subcmd}";
10+
}
11+
}
12+
}

src/Models/Bisect.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Collections.Generic;
2+
3+
namespace SourceGit.Models
4+
{
5+
public enum BisectState
6+
{
7+
None = 0,
8+
WaitingForRange,
9+
Detecting,
10+
}
11+
12+
public enum BisectCommitFlag
13+
{
14+
None = 0,
15+
Good = 1,
16+
Bad = 2,
17+
}
18+
19+
public class Bisect
20+
{
21+
public HashSet<string> Bads
22+
{
23+
get;
24+
set;
25+
} = [];
26+
27+
public HashSet<string> Goods
28+
{
29+
get;
30+
set;
31+
} = [];
32+
}
33+
}

src/Models/Watcher.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ private void OnRepositoryChanged(object o, FileSystemEventArgs e)
168168
_updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime();
169169
}
170170
else if (name.Equals("HEAD", StringComparison.Ordinal) ||
171+
name.Equals("BISECT_START", StringComparison.Ordinal) ||
171172
name.StartsWith("refs/heads/", StringComparison.Ordinal) ||
172173
name.StartsWith("refs/remotes/", StringComparison.Ordinal) ||
173174
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))

src/Resources/Icons.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
<StreamGeometry x:Key="Icons.Action">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</StreamGeometry>
33
<StreamGeometry x:Key="Icons.AIAssist">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</StreamGeometry>
44
<StreamGeometry x:Key="Icons.Archive">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</StreamGeometry>
5+
<StreamGeometry x:Key="Icons.Bad">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</StreamGeometry>
56
<StreamGeometry x:Key="Icons.Binary">M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z</StreamGeometry>
7+
<StreamGeometry x:Key="Icons.Bisect">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</StreamGeometry>
68
<StreamGeometry x:Key="Icons.Blame">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</StreamGeometry>
79
<StreamGeometry x:Key="Icons.Bookmark">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</StreamGeometry>
810
<StreamGeometry x:Key="Icons.Bottom">M509 546 780 275 871 366 509 728 147 366 238 275zM509 728h-362v128h724v-128z</StreamGeometry>

src/Resources/Locales/en_US.axaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@
3636
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">NO FILES ASSUMED AS UNCHANGED</x:String>
3737
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">REMOVE</x:String>
3838
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">BINARY FILE NOT SUPPORTED!!!</x:String>
39+
<x:String x:Key="Text.Bisect">Bisect</x:String>
40+
<x:String x:Key="Text.Bisect.Abort">Abort</x:String>
41+
<x:String x:Key="Text.Bisect.Bad">Bad</x:String>
42+
<x:String x:Key="Text.Bisect.Detecting">Bisecting. Is current HEAD good or bad?</x:String>
43+
<x:String x:Key="Text.Bisect.Good">Good</x:String>
44+
<x:String x:Key="Text.Bisect.Skip">Skip</x:String>
45+
<x:String x:Key="Text.Bisect.WaitingForRange">Bisecting. Mark current commit as good or bad and checkout another one.</x:String>
3946
<x:String x:Key="Text.Blame" xml:space="preserve">Blame</x:String>
4047
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">BLAME ON THIS FILE IS NOT SUPPORTED!!!</x:String>
4148
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Checkout ${0}$...</x:String>

src/Resources/Locales/zh_CN.axaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">没有不跟踪更改的文件</x:String>
4141
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">移除</x:String>
4242
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">二进制文件不支持该操作!!!</x:String>
43+
<x:String x:Key="Text.Bisect">二分定位(bisect)</x:String>
44+
<x:String x:Key="Text.Bisect.Abort">终止</x:String>
45+
<x:String x:Key="Text.Bisect.Bad">标记错误</x:String>
46+
<x:String x:Key="Text.Bisect.Detecting">二分定位进行中。当前提交是 '正确' 还是 '错误'?</x:String>
47+
<x:String x:Key="Text.Bisect.Good">标记正确</x:String>
48+
<x:String x:Key="Text.Bisect.Skip">该提交无法判定</x:String>
49+
<x:String x:Key="Text.Bisect.WaitingForRange">二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。</x:String>
4350
<x:String x:Key="Text.Blame" xml:space="preserve">逐行追溯(blame)</x:String>
4451
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">选中文件不支持该操作!!!</x:String>
4552
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">检出(checkout) ${0}$...</x:String>

src/ViewModels/Histories.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.IO;
45
using System.Text;
56
using System.Threading.Tasks;
67

@@ -62,6 +63,12 @@ public object DetailContext
6263
set => SetProperty(ref _detailContext, value);
6364
}
6465

66+
public Models.Bisect Bisect
67+
{
68+
get => _bisect;
69+
private set => SetProperty(ref _bisect, value);
70+
}
71+
6572
public GridLength LeftArea
6673
{
6774
get => _leftArea;
@@ -111,6 +118,37 @@ public void Cleanup()
111118
_detailContext = null;
112119
}
113120

121+
public Models.BisectState UpdateBisectInfo()
122+
{
123+
var test = Path.Combine(_repo.GitDir, "BISECT_START");
124+
if (!File.Exists(test))
125+
{
126+
Bisect = null;
127+
return Models.BisectState.None;
128+
}
129+
130+
var info = new Models.Bisect();
131+
var dir = Path.Combine(_repo.GitDir, "refs", "bisect");
132+
if (Directory.Exists(dir))
133+
{
134+
var files = new DirectoryInfo(dir).GetFiles();
135+
foreach (var file in files)
136+
{
137+
if (file.Name.StartsWith("bad"))
138+
info.Bads.Add(File.ReadAllText(file.FullName).Trim());
139+
else if (file.Name.StartsWith("good"))
140+
info.Goods.Add(File.ReadAllText(file.FullName).Trim());
141+
}
142+
}
143+
144+
Bisect = info;
145+
146+
if (info.Bads.Count == 0 || info.Goods.Count == 0)
147+
return Models.BisectState.WaitingForRange;
148+
else
149+
return Models.BisectState.Detecting;
150+
}
151+
114152
public void NavigateTo(string commitSHA)
115153
{
116154
var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal));
@@ -1209,7 +1247,7 @@ private string GetPatchFileName(string dir, Models.Commit commit, int index = 0)
12091247
}
12101248
builder.Append(".patch");
12111249

1212-
return System.IO.Path.Combine(dir, builder.ToString());
1250+
return Path.Combine(dir, builder.ToString());
12131251
}
12141252

12151253
private Repository _repo = null;
@@ -1220,6 +1258,8 @@ private string GetPatchFileName(string dir, Models.Commit commit, int index = 0)
12201258
private long _navigationId = 0;
12211259
private object _detailContext = null;
12221260

1261+
private Models.Bisect _bisect = null;
1262+
12231263
private GridLength _leftArea = new GridLength(1, GridUnitType.Star);
12241264
private GridLength _rightArea = new GridLength(1, GridUnitType.Star);
12251265
private GridLength _topArea = new GridLength(1, GridUnitType.Star);

src/ViewModels/Repository.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,18 @@ public InProgressContext InProgressContext
398398
get => _workingCopy?.InProgressContext;
399399
}
400400

401+
public Models.BisectState BisectState
402+
{
403+
get => _bisectState;
404+
private set => SetProperty(ref _bisectState, value);
405+
}
406+
407+
public bool IsBisectCommandRunning
408+
{
409+
get => _isBisectCommandRunning;
410+
private set => SetProperty(ref _isBisectCommandRunning, value);
411+
}
412+
401413
public bool IsAutoFetching
402414
{
403415
get => _isAutoFetching;
@@ -939,6 +951,31 @@ public void AbortMerge()
939951
return actions;
940952
}
941953

954+
public void Bisect(string subcmd)
955+
{
956+
IsBisectCommandRunning = true;
957+
SetWatcherEnabled(false);
958+
959+
var log = CreateLog($"Bisect({subcmd})");
960+
Task.Run(() =>
961+
{
962+
var succ = new Commands.Bisect(_fullpath, subcmd).Use(log).Exec();
963+
log.Complete();
964+
965+
Dispatcher.UIThread.Invoke(() =>
966+
{
967+
if (!succ)
968+
App.RaiseException(_fullpath, log.Content);
969+
else if (log.Content.Contains("is the first bad commit"))
970+
App.SendNotification(_fullpath, log.Content);
971+
972+
MarkBranchesDirtyManually();
973+
SetWatcherEnabled(true);
974+
IsBisectCommandRunning = false;
975+
});
976+
});
977+
}
978+
942979
public void RefreshBranches()
943980
{
944981
var branches = new Commands.QueryBranches(_fullpath).Result();
@@ -1023,6 +1060,8 @@ public void RefreshCommits()
10231060
_histories.Commits = commits;
10241061
_histories.Graph = graph;
10251062

1063+
BisectState = _histories.UpdateBisectInfo();
1064+
10261065
if (!string.IsNullOrEmpty(_navigateToBranchDelayed))
10271066
{
10281067
var branch = _branches.Find(x => x.FullName == _navigateToBranchDelayed);
@@ -2627,6 +2666,9 @@ private void AutoFetchImpl(object sender)
26272666
private Timer _autoFetchTimer = null;
26282667
private DateTime _lastFetchTime = DateTime.MinValue;
26292668

2669+
private Models.BisectState _bisectState = Models.BisectState.None;
2670+
private bool _isBisectCommandRunning = false;
2671+
26302672
private string _navigateToBranchDelayed = string.Empty;
26312673
}
26322674
}

src/Views/BisectStateIndicator.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System;
2+
3+
using Avalonia;
4+
using Avalonia.Controls;
5+
using Avalonia.Media;
6+
7+
namespace SourceGit.Views
8+
{
9+
public class BisectStateIndicator : Control
10+
{
11+
public static readonly StyledProperty<IBrush> BackgroundProperty =
12+
AvaloniaProperty.Register<BisectStateIndicator, IBrush>(nameof(Background), Brushes.Transparent);
13+
14+
public IBrush Background
15+
{
16+
get => GetValue(BackgroundProperty);
17+
set => SetValue(BackgroundProperty, value);
18+
}
19+
20+
public static readonly StyledProperty<IBrush> ForegroundProperty =
21+
AvaloniaProperty.Register<BisectStateIndicator, IBrush>(nameof(Foreground), Brushes.White);
22+
23+
public IBrush Foreground
24+
{
25+
get => GetValue(ForegroundProperty);
26+
set => SetValue(ForegroundProperty, value);
27+
}
28+
29+
public static readonly StyledProperty<Models.Bisect> BisectProperty =
30+
AvaloniaProperty.Register<BisectStateIndicator, Models.Bisect>(nameof(Bisect));
31+
32+
public Models.Bisect Bisect
33+
{
34+
get => GetValue(BisectProperty);
35+
set => SetValue(BisectProperty, value);
36+
}
37+
38+
static BisectStateIndicator()
39+
{
40+
AffectsMeasure<BisectStateIndicator>(BisectProperty);
41+
AffectsRender<BisectStateIndicator>(BackgroundProperty, ForegroundProperty);
42+
}
43+
44+
public override void Render(DrawingContext context)
45+
{
46+
if (_flags == Models.BisectCommitFlag.None)
47+
return;
48+
49+
if (_prefix == null)
50+
{
51+
_prefix = LoadIcon("Icons.Bisect");
52+
_good = LoadIcon("Icons.Check");
53+
_bad = LoadIcon("Icons.Bad");
54+
}
55+
56+
var x = 0.0;
57+
58+
if (_flags.HasFlag(Models.BisectCommitFlag.Good))
59+
{
60+
RenderImpl(context, Brushes.Green, _good, x);
61+
x += 36;
62+
}
63+
64+
if (_flags.HasFlag(Models.BisectCommitFlag.Bad))
65+
RenderImpl(context, Brushes.Red, _bad, x);
66+
}
67+
68+
protected override Size MeasureOverride(Size availableSize)
69+
{
70+
var desiredFlags = Models.BisectCommitFlag.None;
71+
var desiredWidth = 0.0;
72+
if (Bisect is { } bisect && DataContext is Models.Commit commit)
73+
{
74+
var sha = commit.SHA;
75+
if (bisect.Goods.Contains(sha))
76+
{
77+
desiredFlags |= Models.BisectCommitFlag.Good;
78+
desiredWidth = 36;
79+
}
80+
81+
if (bisect.Bads.Contains(sha))
82+
{
83+
desiredFlags |= Models.BisectCommitFlag.Bad;
84+
desiredWidth += 36;
85+
}
86+
}
87+
88+
if (desiredFlags != _flags)
89+
{
90+
_flags = desiredFlags;
91+
InvalidateVisual();
92+
}
93+
94+
return new Size(desiredWidth, desiredWidth > 0 ? 16 : 0);
95+
}
96+
97+
private Geometry LoadIcon(string key)
98+
{
99+
var geo = this.FindResource(key) as StreamGeometry;
100+
var drawGeo = geo!.Clone();
101+
var iconBounds = drawGeo.Bounds;
102+
var translation = Matrix.CreateTranslation(-(Vector)iconBounds.Position);
103+
var scale = Math.Min(10.0 / iconBounds.Width, 10.0 / iconBounds.Height);
104+
var transform = translation * Matrix.CreateScale(scale, scale);
105+
if (drawGeo.Transform == null || drawGeo.Transform.Value == Matrix.Identity)
106+
drawGeo.Transform = new MatrixTransform(transform);
107+
else
108+
drawGeo.Transform = new MatrixTransform(drawGeo.Transform.Value * transform);
109+
110+
return drawGeo;
111+
}
112+
113+
private void RenderImpl(DrawingContext context, IBrush brush, Geometry icon, double x)
114+
{
115+
var entireRect = new RoundedRect(new Rect(x, 0, 32, 16), new CornerRadius(2));
116+
var stateRect = new RoundedRect(new Rect(x + 16, 0, 16, 16), new CornerRadius(0, 2, 2, 0));
117+
context.DrawRectangle(Background, new Pen(brush), entireRect);
118+
using (context.PushOpacity(.2))
119+
context.DrawRectangle(brush, null, stateRect);
120+
context.DrawLine(new Pen(brush), new Point(x + 16, 0), new Point(x + 16, 16));
121+
122+
using (context.PushTransform(Matrix.CreateTranslation(x + 3, 3)))
123+
context.DrawGeometry(Foreground, null, _prefix);
124+
125+
using (context.PushTransform(Matrix.CreateTranslation(x + 19, 3)))
126+
context.DrawGeometry(Foreground, null, icon);
127+
}
128+
129+
private Geometry _prefix = null;
130+
private Geometry _good = null;
131+
private Geometry _bad = null;
132+
private Models.BisectCommitFlag _flags = Models.BisectCommitFlag.None;
133+
}
134+
}

0 commit comments

Comments
 (0)