feature: git bisect support

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo 2025-04-22 15:45:15 +08:00
parent 9eae1eeb81
commit df5294bcb7
No known key found for this signature in database
16 changed files with 397 additions and 7 deletions

View file

@ -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<IBrush> BackgroundProperty =
AvaloniaProperty.Register<BisectStateIndicator, IBrush>(nameof(Background), Brushes.Transparent);
public IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public static readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<BisectStateIndicator, IBrush>(nameof(Foreground), Brushes.White);
public IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static readonly StyledProperty<Models.Bisect> BisectProperty =
AvaloniaProperty.Register<BisectStateIndicator, Models.Bisect>(nameof(Bisect));
public Models.Bisect Bisect
{
get => GetValue(BisectProperty);
set => SetValue(BisectProperty, value);
}
static BisectStateIndicator()
{
AffectsMeasure<BisectStateIndicator>(BisectProperty);
AffectsRender<BisectStateIndicator>(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;
}
}

View file

@ -131,14 +131,20 @@
ClipToBounds="True"
Background="Transparent"
ToolTip.Tip="{Binding Subject}">
<Grid ColumnDefinitions="Auto,Auto,*" Margin="2,0,4,0" ClipToBounds="True">
<Grid ColumnDefinitions="Auto,Auto,Auto,*" Margin="2,0,4,0" ClipToBounds="True">
<v:CommitStatusIndicator Grid.Column="0"
CurrentBranch="{Binding $parent[v:Histories].CurrentBranch}"
AheadBrush="{DynamicResource Brush.Accent}"
BehindBrush="{DynamicResource Brush.FG1}"
VerticalAlignment="Center"/>
<v:BisectStateIndicator Grid.Column="1"
Background="{DynamicResource Brush.Contents}"
Foreground="{DynamicResource Brush.FG1}"
Bisect="{Binding $parent[v:Histories].Bisect}"
VerticalAlignment="Center"/>
<v:CommitRefsPresenter Grid.Column="1"
<v:CommitRefsPresenter Grid.Column="2"
Background="{DynamicResource Brush.Contents}"
Foreground="{DynamicResource Brush.FG1}"
FontFamily="{DynamicResource Fonts.Primary}"
@ -153,7 +159,7 @@
</v:CommitRefsPresenter.UseGraphColor>
</v:CommitRefsPresenter>
<v:CommitSubjectPresenter Grid.Column="2"
<v:CommitSubjectPresenter Grid.Column="3"
Classes="primary"
Subject="{Binding Subject}"
IssueTrackerRules="{Binding $parent[v:Histories].IssueTrackerRules}"

View file

@ -91,6 +91,15 @@ namespace SourceGit.Views
set => SetValue(CurrentBranchProperty, value);
}
public static readonly StyledProperty<Models.Bisect> BisectProperty =
AvaloniaProperty.Register<Histories, Models.Bisect>(nameof(Bisect));
public Models.Bisect Bisect
{
get => GetValue(BisectProperty);
set => SetValue(BisectProperty, value);
}
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> IssueTrackerRulesProperty =
AvaloniaProperty.Register<Histories, AvaloniaList<Models.IssueTrackerRule>>(nameof(IssueTrackerRules));

View file

@ -585,7 +585,7 @@
BorderBrush="{DynamicResource Brush.Border0}"/>
<!-- Right -->
<Grid Grid.Column="2" RowDefinitions="Auto,Auto,*">
<Grid Grid.Column="2" RowDefinitions="Auto,Auto,Auto,*">
<Grid Grid.Row="0" Height="28" ColumnDefinitions="*,Auto" Background="{DynamicResource Brush.Conflict}" IsVisible="{Binding InProgressContext, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentControl Grid.Column="0" Margin="8,0" Content="{Binding InProgressContext}">
<ContentControl.DataTemplates>
@ -665,7 +665,73 @@
Command="{Binding AbortMerge}"/>
</Grid>
<Border Grid.Row="1" Background="{DynamicResource Brush.ToolBar}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
<Grid Grid.Row="1"
Height="28"
ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto"
Background="{DynamicResource Brush.Conflict}"
IsVisible="{Binding BisectState, Converter={x:Static ObjectConverters.NotEqual}, ConverterParameter={x:Static m:BisectState.None}}">
<TextBlock Grid.Column="0"
FontWeight="Bold"
Margin="8,0"
Foreground="{DynamicResource Brush.ConflictForeground}"
Text="{DynamicResource Text.Bisect.WaitingForRange}"
IsVisible="{Binding BisectState, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:BisectState.WaitingForRange}}"/>
<TextBlock Grid.Column="0"
FontWeight="Bold"
Margin="8,0"
Foreground="{DynamicResource Brush.ConflictForeground}"
Text="{DynamicResource Text.Bisect.Detecting}"
IsVisible="{Binding BisectState, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:BisectState.Detecting}}"/>
<v:LoadingIcon Grid.Column="1"
Width="14" Height="14"
Margin="4,0"
IsVisible="{Binding IsBisectCommandRunning}"/>
<Button Grid.Column="2"
Classes="flat"
Margin="4,0"
FontWeight="Regular"
BorderThickness="0"
Content="{DynamicResource Text.Bisect.Good}"
IsEnabled="{Binding !IsBisectCommandRunning}"
Padding="8,2"
Click="OnBisectCommand"
Tag="good"/>
<Button Grid.Column="3"
Classes="flat"
Margin="4,0"
FontWeight="Regular"
BorderThickness="0"
Content="{DynamicResource Text.Bisect.Bad}"
IsEnabled="{Binding !IsBisectCommandRunning}"
Padding="8,2"
Click="OnBisectCommand"
Tag="bad"/>
<Button Grid.Column="4"
Classes="flat"
Margin="4,0"
FontWeight="Regular"
BorderThickness="0"
Content="{DynamicResource Text.Bisect.Skip}"
IsEnabled="{Binding !IsBisectCommandRunning}"
Padding="8,2"
Click="OnBisectCommand"
Tag="skip"/>
<Button Grid.Column="5"
Classes="flat"
Margin="4,0"
FontWeight="Regular"
BorderThickness="0"
Content="{DynamicResource Text.Bisect.Abort}"
IsEnabled="{Binding !IsBisectCommandRunning}"
Padding="8,2"
Click="OnBisectCommand"
Tag="reset"/>
</Grid>
<Border Grid.Row="2" Background="{DynamicResource Brush.ToolBar}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectedViewIndex" Converter="{x:Static c:IntConverters.IsZero}"/>
@ -721,10 +787,11 @@
</Grid>
</Border>
<ContentControl Grid.Row="2" Content="{Binding SelectedView}">
<ContentControl Grid.Row="3" Content="{Binding SelectedView}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:Histories">
<v:Histories CurrentBranch="{Binding Repo.CurrentBranch}"
Bisect="{Binding Bisect}"
AuthorNameColumnWidth="{Binding Source={x:Static vm:Preferences.Instance}, Path=Layout.HistoriesAuthorColumnWidth, Mode=TwoWay}"
IssueTrackerRules="{Binding Repo.Settings.IssueTrackerRules}"
OnlyHighlightCurrentBranch="{Binding Repo.OnlyHighlightCurrentBranchInHistories}"

View file

@ -429,5 +429,15 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnBisectCommand(object sender, RoutedEventArgs e)
{
if (sender is Button button &&
DataContext is ViewModels.Repository { IsBisectCommandRunning: false } repo &&
repo.CanCreatePopup())
repo.Bisect(button.Tag as string);
e.Handled = true;
}
}
}

View file

@ -100,6 +100,10 @@
<Path Width="14" Height="14" Data="{StaticResource Icons.LFS}"/>
</Button>
<Button Classes="icon_button" Width="32" Margin="8,0,0,0" Click="StartBisect" ToolTip.Tip="{DynamicResource Text.Bisect}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Bisect}"/>
</Button>
<Button Classes="icon_button" Width="32" Margin="8,0,0,0" Click="OpenCustomActionMenu" ToolTip.Tip="{DynamicResource Text.Repository.CustomActions}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Action}"/>
</Button>

View file

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