refactor<Blame>: new blame tool

This commit is contained in:
leo 2023-08-24 13:39:49 +08:00
parent 697879b6a5
commit a1bfbfe02e
5 changed files with 121 additions and 123 deletions

View file

@ -9,6 +9,8 @@ namespace SourceGit.Commands {
public class Blame : Command { public class Blame : Command {
private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)");
private Data data = new Data(); private Data data = new Data();
private bool needUnifyCommitSHA = false;
private int minSHALen = 0;
public class Data { public class Data {
public List<Models.BlameLine> Lines = new List<Models.BlameLine>(); public List<Models.BlameLine> Lines = new List<Models.BlameLine>();
@ -22,6 +24,15 @@ namespace SourceGit.Commands {
public Data Result() { public Data Result() {
Exec(); Exec();
if (needUnifyCommitSHA) {
foreach (var line in data.Lines) {
if (line.CommitSHA.Length > minSHALen) {
line.CommitSHA = line.CommitSHA.Substring(0, minSHALen);
}
}
}
return data; return data;
} }
@ -52,6 +63,12 @@ namespace SourceGit.Commands {
Content = content, Content = content,
}; };
if (line[0] == '^') {
needUnifyCommitSHA = true;
if (minSHALen == 0) minSHALen = commit.Length;
else if (commit.Length < minSHALen) minSHALen = commit.Length;
}
data.Lines.Add(blameLine); data.Lines.Add(blameLine);
} }
} }

View file

@ -49,10 +49,6 @@
<sys:String x:Key="Text.Archive.File.Placeholder">Select archive file path</sys:String> <sys:String x:Key="Text.Archive.File.Placeholder">Select archive file path</sys:String>
<sys:String x:Key="Text.Blame">Blame</sys:String> <sys:String x:Key="Text.Blame">Blame</sys:String>
<sys:String x:Key="Text.Blame.Tip">Right click to see commit info</sys:String>
<sys:String x:Key="Text.Blame.SHA">COMMIT SHA</sys:String>
<sys:String x:Key="Text.Blame.Author">AUTHOR</sys:String>
<sys:String x:Key="Text.Blame.ModifyTime">MODIFY TIME</sys:String>
<sys:String x:Key="Text.Submodule">SUBMODULES</sys:String> <sys:String x:Key="Text.Submodule">SUBMODULES</sys:String>
<sys:String x:Key="Text.Submodule.Add">Add Submodule</sys:String> <sys:String x:Key="Text.Submodule.Add">Add Submodule</sys:String>

View file

@ -48,10 +48,6 @@
<sys:String x:Key="Text.Archive.File.Placeholder">选择存档文件的存放路径</sys:String> <sys:String x:Key="Text.Archive.File.Placeholder">选择存档文件的存放路径</sys:String>
<sys:String x:Key="Text.Blame">逐行追溯</sys:String> <sys:String x:Key="Text.Blame">逐行追溯</sys:String>
<sys:String x:Key="Text.Blame.Tip">右键点击查看所选行修改记录</sys:String>
<sys:String x:Key="Text.Blame.SHA">提交指纹</sys:String>
<sys:String x:Key="Text.Blame.Author">修改者</sys:String>
<sys:String x:Key="Text.Blame.ModifyTime">修改时间</sys:String>
<sys:String x:Key="Text.Submodule">子模块</sys:String> <sys:String x:Key="Text.Submodule">子模块</sys:String>
<sys:String x:Key="Text.Submodule.Add">添加子模块</sys:String> <sys:String x:Key="Text.Submodule.Add">添加子模块</sys:String>

View file

@ -15,13 +15,13 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="28"/> <RowDefinition Height="28"/>
<RowDefinition Height="1"/> <RowDefinition Height="1"/>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Title Bar --> <!-- Title Bar -->
<Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}"> <Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@ -34,8 +34,17 @@
<!-- Title --> <!-- Title -->
<TextBlock Grid.Column="1" Text="{DynamicResource Text.Blame}"/> <TextBlock Grid.Column="1" Text="{DynamicResource Text.Blame}"/>
<!-- Description -->
<TextBlock
Grid.Column="2"
x:Name="txtFile"
Margin="8,0,0,0"
FontFamily="{Binding Source={x:Static models:Preference.Instance}, Path=General.FontFamilyContent, Mode=OneWay}"
Foreground="{DynamicResource Brush.FG2}"
FontSize="11"/>
<!-- Window Commands --> <!-- Window Commands -->
<StackPanel Grid.Column="3" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True"> <StackPanel Grid.Column="4" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<controls:IconButton Click="Minimize" Width="48" IconSize="10" Icon="{StaticResource Icon.Minimize}" HoverBackground="#40000000" Opacity="1"/> <controls:IconButton Click="Minimize" Width="48" IconSize="10" Icon="{StaticResource Icon.Minimize}" HoverBackground="#40000000" Opacity="1"/>
<ToggleButton Style="{StaticResource Style.ToggleButton.MaxOrRestore}" Width="48" IsChecked="{Binding ElementName=me, Path=IsMaximized}"/> <ToggleButton Style="{StaticResource Style.ToggleButton.MaxOrRestore}" Width="48" IsChecked="{Binding ElementName=me, Path=IsMaximized}"/>
<controls:IconButton Click="Quit" Width="48" IconSize="10" Icon="{StaticResource Icon.Close}" HoverBackground="Red" Opacity="1"/> <controls:IconButton Click="Quit" Width="48" IconSize="10" Icon="{StaticResource Icon.Close}" HoverBackground="Red" Opacity="1"/>
@ -49,49 +58,72 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Fill="{DynamicResource Brush.Border0}"/> Fill="{DynamicResource Brush.Border0}"/>
<!-- Description -->
<Border Grid.Row="2">
<Grid Margin="4,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="txtFile" FontFamily="{Binding Source={x:Static models:Preference.Instance}, Path=General.FontFamilyContent, Mode=OneWay}" FontSize="11" Foreground="{DynamicResource Brush.FG2}"/>
<TextBlock Grid.Column="1" HorizontalAlignment="Right" Foreground="{DynamicResource Brush.FG2}" FontFamily="{Binding Source={x:Static models:Preference.Instance}, Path=General.FontFamilyContent, Mode=OneWay}" FontSize="11" Text="{DynamicResource Text.Blame.Tip}"/>
</Grid>
</Border>
<!-- Viewer --> <!-- Viewer -->
<DataGrid <DataGrid
Grid.Row="3" Grid.Row="2"
x:Name="blame" x:Name="blame"
GridLinesVisibility="Vertical" GridLinesVisibility="Vertical"
VerticalGridLinesBrush="{DynamicResource Brush.Border2}" VerticalGridLinesBrush="{DynamicResource Brush.Border2}"
BorderBrush="{DynamicResource Brush.Border2}" BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1" BorderThickness="1"
FrozenColumnCount="1" FrozenColumnCount="2"
RowHeight="16" RowHeight="16"
SelectionUnit="FullRow" SelectionUnit="FullRow"
SelectionMode="Single" SelectionMode="Single"
SizeChanged="OnViewerSizeChanged"> SizeChanged="OnViewerSizeChanged"
SelectionChanged="OnSelectionChanged">
<DataGrid.RowStyle> <DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}"> <Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnViewerRequestBringIntoView"/> <EventSetter Event="RequestBringIntoView" Handler="OnViewerRequestBringIntoView"/>
<EventSetter Event="ContextMenuOpening" Handler="OnViewerContextMenuOpening"/>
</Style> </Style>
</DataGrid.RowStyle> </DataGrid.RowStyle>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="BG">
<Border x:Name="Content" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Margin="4,0" MaxWidth="300">
<TextBlock Text="{Binding Line.CommitSHA}" Foreground="#FFFFB835"/>
<TextBlock Text="{Binding Line.Time}" Margin="8,0"/>
<TextBlock Text="{Binding Line.Author}" Foreground="#FFFFB835"/>
</StackPanel>
</Border>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFirstLine}" Value="True">
<Setter TargetName="Content" Property="BorderThickness" Value="0"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsFirstLineInGroup}" Value="False">
<Setter TargetName="Content" Property="Visibility" Value="Hidden"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="Auto" IsReadOnly="True" Binding="{Binding Line.LineNumber}" ElementStyle="{StaticResource Style.TextBlock.LineNumber}"/> <DataGridTextColumn Width="Auto" IsReadOnly="True" Binding="{Binding Line.LineNumber}" ElementStyle="{StaticResource Style.TextBlock.LineNumber}"/>
<DataGridTemplateColumn Width="SizeToCells" MinWidth="1" IsReadOnly="True"> <DataGridTemplateColumn Width="SizeToCells" MinWidth="1" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Border Background="{Binding BG}" BorderThickness="0"> <Border x:Name="BG" BorderThickness="0">
<TextBlock Text="{Binding Line.Content}" Style="{DynamicResource Style.TextBlock.LineContent}"/> <TextBlock Text="{Binding Line.Content}" Style="{DynamicResource Style.TextBlock.LineContent}"/>
</Border> </Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="BG" Property="Background" Value="#44000000"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -111,30 +143,5 @@
<!-- Loading --> <!-- Loading -->
<controls:Loading x:Name="loading" Grid.Row="3" Width="48" Height="48" IsAnimating="True"/> <controls:Loading x:Name="loading" Grid.Row="3" Width="48" Height="48" IsAnimating="True"/>
<!-- Popup to show commit info -->
<Popup x:Name="popup" Grid.Row="3" Placement="MousePoint" IsOpen="False" StaysOpen="False" Focusable="True">
<Border BorderBrush="{DynamicResource Brush.Accent1}" BorderThickness="1" Background="{DynamicResource Brush.Popup}">
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{DynamicResource Text.Blame.SHA}" Foreground="{DynamicResource Brush.FG2}" Margin="4,0"/>
<Label Grid.Row="0" Grid.Column="1" x:Name="commitID" Margin="8,0,4,0" Padding="0" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="{DynamicResource Text.Blame.Author}" Foreground="{DynamicResource Brush.FG2}" Margin="4,0"/>
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="authorName" Margin="8,0,4,0"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="{DynamicResource Text.Blame.ModifyTime}" Foreground="{DynamicResource Brush.FG2}" Margin="4,0"/>
<TextBlock Grid.Row="2" Grid.Column="1" x:Name="authorTime" Margin="8,0,4,0"/>
</Grid>
</Border>
</Popup>
</Grid> </Grid>
</controls:Window> </controls:Window>

View file

@ -1,10 +1,8 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media; using System.Windows.Media;
namespace SourceGit.Views { namespace SourceGit.Views {
@ -12,33 +10,45 @@ namespace SourceGit.Views {
/// 逐行追溯 /// 逐行追溯
/// </summary> /// </summary>
public partial class Blame : Controls.Window { public partial class Blame : Controls.Window {
private static readonly Brush[] BG = new Brush[] { /// <summary>
Brushes.Transparent, /// DataGrid数据源结构
new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)) /// </summary>
};
private string repo = null;
private string lastSHA = null;
private int lastBG = 1;
public class Record : INotifyPropertyChanged { public class Record : INotifyPropertyChanged {
private Brush bg = null;
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 原始Blame行数据
/// </summary>
public Models.BlameLine Line { get; set; } public Models.BlameLine Line { get; set; }
public Brush OrgBG { get; set; }
public Brush BG { /// <summary>
get { return bg; } /// 是否是第一行
/// </summary>
public bool IsFirstLine { get; set; } = false;
/// <summary>
/// 前一行与本行的提交不同
/// </summary>
public bool IsFirstLineInGroup { get; set; } = false;
/// <summary>
/// 是否当前选中,会影响背景色
/// </summary>
private bool isSelected = false;
public bool IsSelected {
get { return isSelected; }
set { set {
if (value != bg) { if (isSelected != value) {
bg = value; isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("BG")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsSelected"));
} }
} }
} }
} }
/// <summary>
/// Blame数据
/// </summary>
public ObservableCollection<Record> Records { get; set; } public ObservableCollection<Record> Records { get; set; }
public Blame(string repo, string file, string revision) { public Blame(string repo, string file, string revision) {
@ -67,48 +77,33 @@ namespace SourceGit.Views {
notSupport.Visibility = Visibility.Visible; notSupport.Visibility = Visibility.Visible;
}); });
} else { } else {
string lastSHA = null;
foreach (var line in rs.Lines) { foreach (var line in rs.Lines) {
var r = new Record(); var r = new Record();
r.Line = line; r.Line = line;
r.BG = GetBG(line.CommitSHA); r.IsSelected = false;
r.OrgBG = r.BG;
if (line.CommitSHA != lastSHA) {
lastSHA = line.CommitSHA;
r.IsFirstLineInGroup = true;
} else {
r.IsFirstLineInGroup = false;
}
Records.Add(r); Records.Add(r);
} }
if (Records.Count > 0) Records[0].IsFirstLine = true;
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
loading.IsAnimating = false; loading.IsAnimating = false;
loading.Visibility = Visibility.Collapsed; loading.Visibility = Visibility.Collapsed;
var formatted = new FormattedText(
$"{Records.Count}",
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(blame.FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
12.0,
Brushes.Black,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
var lineNumberWidth = formatted.Width + 16;
var minWidth = blame.ActualWidth - lineNumberWidth;
if (Records.Count * 16 > blame.ActualHeight) minWidth -= 8;
blame.Columns[0].Width = lineNumberWidth;
blame.Columns[1].MinWidth = minWidth;
blame.ItemsSource = Records; blame.ItemsSource = Records;
blame.UpdateLayout();
}); });
} }
}); });
} }
private Brush GetBG(string sha) {
if (lastSHA != sha) {
lastSHA = sha;
lastBG = 1 - lastBG;
}
return BG[lastBG];
}
#region WINDOW_COMMANDS #region WINDOW_COMMANDS
private void Minimize(object sender, RoutedEventArgs e) { private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this); SystemCommands.MinimizeWindow(this);
@ -148,8 +143,8 @@ namespace SourceGit.Views {
var scroller = GetVisualChild<ScrollViewer>(blame); var scroller = GetVisualChild<ScrollViewer>(blame);
if (scroller != null && scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8; if (scroller != null && scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8;
blame.Columns[1].MinWidth = minWidth; blame.Columns[2].MinWidth = minWidth;
blame.Columns[1].Width = DataGridLength.SizeToCells; blame.Columns[2].Width = DataGridLength.SizeToCells;
blame.UpdateLayout(); blame.UpdateLayout();
} }
@ -157,31 +152,18 @@ namespace SourceGit.Views {
e.Handled = true; e.Handled = true;
} }
private void OnViewerContextMenuOpening(object sender, ContextMenuEventArgs ev) { private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) {
var record = (sender as DataGridRow).DataContext as Record; var r = blame.SelectedItem as Record;
if (record == null) return; if (r == null) return;
foreach (var r in Records) { Models.Watcher.Get(repo).NavigateTo(r.Line.CommitSHA);
if (r.Line.CommitSHA == record.Line.CommitSHA) {
r.BG = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)); foreach (var one in Records) {
} else { one.IsSelected = one.Line.CommitSHA == r.Line.CommitSHA;
r.BG = r.OrgBG;
} }
} }
Hyperlink link = new Hyperlink(new Run(record.Line.CommitSHA));
link.ToolTip = App.Text("Goto");
link.Click += (o, e) => {
Models.Watcher.Get(repo).NavigateTo(record.Line.CommitSHA);
e.Handled = true;
};
commitID.Content = link;
authorName.Text = record.Line.Author;
authorTime.Text = record.Line.Time;
popup.IsOpen = true;
ev.Handled = true;
}
#endregion #endregion
private string repo = null;
} }
} }