feature(Histories): supports to diff selected 2 commits

This commit is contained in:
leo 2020-09-25 17:24:43 +08:00
parent b177f541be
commit 5592c652f7
5 changed files with 479 additions and 13 deletions

View file

@ -282,8 +282,7 @@ namespace SourceGit.UI {
e.Handled = true; e.Handled = true;
} }
private void ChangeListMouseDoubleClick(object sender, MouseButtonEventArgs e) private void ChangeListMouseDoubleClick(object sender, MouseButtonEventArgs e) {
{
var row = sender as DataGridRow; var row = sender as DataGridRow;
if (row == null) return; if (row == null) return;

View file

@ -161,13 +161,16 @@
<!-- Detail for selected commit --> <!-- Detail for selected commit -->
<Grid x:Name="commitDetailPanel"> <Grid x:Name="commitDetailPanel">
<!-- Selected commit detail --> <!-- Selected commit detail -->
<local:CommitViewer x:Name="commitViewer"/> <local:CommitViewer x:Name="commitViewer" Visibility="Collapsed"/>
<!-- Diff with selected 2 commit -->
<local:TwoCommitsDiff x:Name="twoCommitDiff" Visibility="Collapsed"/>
<!-- Mask for select multi rows in commit list --> <!-- Mask for select multi rows in commit list -->
<Border x:Name="mask4MultiSelection" Background="{StaticResource Brush.BG1}" Visibility="Collapsed"> <Border x:Name="mask4MultiSelection" Background="{StaticResource Brush.BG1}">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2"> <StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
<Path Width="160" Height="160" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}"/> <Path Width="160" Height="160" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}"/>
<Label x:Name="txtTotalSelected" Margin="0,16,0,0" FontSize="24" FontWeight="UltraBold" HorizontalAlignment="Center"/> <Label x:Name="txtTotalSelected" Content="SELECT COMMIT TO VIEW DETAIL" Margin="0,16,0,0" FontSize="24" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel> </StackPanel>
</Border> </Border>

View file

@ -41,10 +41,6 @@ namespace SourceGit.UI {
/// </summary> /// </summary>
public Histories() { public Histories() {
InitializeComponent(); InitializeComponent();
mask4MultiSelection.Visibility = Visibility.Visible;
txtTotalSelected.Content = "SELECT COMMIT TO VIEW DETAIL";
ChangeOrientation(null, null); ChangeOrientation(null, null);
} }
@ -271,14 +267,22 @@ namespace SourceGit.UI {
private void CommitSelectChanged(object sender, SelectionChangedEventArgs e) { private void CommitSelectChanged(object sender, SelectionChangedEventArgs e) {
mask4MultiSelection.Visibility = Visibility.Collapsed; mask4MultiSelection.Visibility = Visibility.Collapsed;
commitViewer.Visibility = Visibility.Collapsed;
twoCommitDiff.Visibility = Visibility.Collapsed;
var selected = commitList.SelectedItems; var selected = commitList.SelectedItems;
if (selected.Count == 1) { if (selected.Count == 1) {
var commit = selected[0] as Git.Commit; commitViewer.Visibility = Visibility.Visible;
if (commit != null) commitViewer.SetData(Repo, commit); commitViewer.SetData(Repo, selected[0] as Git.Commit);
} else if (selected.Count > 1) { } else if (selected.Count == 2) {
twoCommitDiff.Visibility = Visibility.Visible;
twoCommitDiff.SetData(Repo, (selected[0] as Git.Commit).ShortSHA, (selected[1] as Git.Commit).ShortSHA);
} else if (selected.Count > 2) {
mask4MultiSelection.Visibility = Visibility.Visible; mask4MultiSelection.Visibility = Visibility.Visible;
txtTotalSelected.Content = $"SELECTED {selected.Count} COMMITS"; txtTotalSelected.Content = $"SELECTED {selected.Count} COMMITS";
} else {
mask4MultiSelection.Visibility = Visibility.Visible;
txtTotalSelected.Content = $"SELECT COMMIT TO VIEW DETAIL";
} }
} }

146
src/UI/TwoCommitsDiff.xaml Normal file
View file

@ -0,0 +1,146 @@
<UserControl x:Class="SourceGit.UI.TwoCommitsDiff"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:source="clr-namespace:SourceGit"
xmlns:converters="clr-namespace:SourceGit.Converters"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="4,0">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" x:Name="txtTitle" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" Foreground="{StaticResource Brush.FG2}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="2,0">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.Resources>
<converters:BoolToCollapsed x:Key="BoolToCollapsed"/>
<converters:InverseBoolToCollapsed x:Key="InverseBoolToCollapsed"/>
</Grid.Resources>
<Grid Grid.Row="0" Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="24"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Grid.ColumnSpan="2" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" Background="{StaticResource Brush.BG3}"/>
<Path Grid.Column="0" Width="14" Height="14" Fill="{StaticResource Brush.FG2}" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Search}"/>
<TextBox Grid.Column="1" x:Name="txtChangeFilter" BorderThickness="0" helpers:TextBoxHelper.Placeholder="Search File ..." TextChanged="SearchChangeFileTextChanged"/>
<ToggleButton
Grid.Column="2"
x:Name="toggleSwitchMode"
Margin="4,0,0,0"
ToolTip="SWITCH TO LIST/TREE VIEW"
Style="{StaticResource Style.ToggleButton.ListOrTree}"
IsChecked="{Binding Source={x:Static source:App.Preference}, Path=UIUseListInChanges, Mode=TwoWay}"/>
</Grid>
<TreeView
Grid.Row="1"
x:Name="changeTree"
FontFamily="Consolas"
Visibility="{Binding ElementName=toggleSwitchMode, Path=IsChecked, Converter={StaticResource InverseBoolToCollapsed}}"
Background="{StaticResource Brush.BG2}"
SelectedItemChanged="ChangeTreeItemSelected"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Border x:Name="status" Width="14" Height="14" Visibility="Collapsed" Background="{Binding Change, Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="0,0,4,0">
<TextBlock Text="{Binding Change, Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="10" RenderOptions.BitmapScalingMode="HighQuality"/>
</Border>
<Path x:Name="icon" Width="14" Style="{StaticResource Style.Icon}" Fill="Goldenrod" Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock Text="{Binding Name}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" Margin="4,0,0,0" FontSize="11"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFile}" Value="True">
<Setter TargetName="status" Property="Visibility" Value="Visible"/>
<Setter TargetName="icon" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFile}" Value="False"/>
<Condition Binding="{Binding IsNodeExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<DataGrid
Grid.Row="1"
x:Name="changeList2"
Visibility="{Binding ElementName=toggleSwitchMode, Path=IsChecked, Converter={StaticResource BoolToCollapsed}}"
RowHeight="24"
SelectionChanged="ChangeListSelectionChanged"
SelectionMode="Single"
SelectionUnit="FullRow"
Background="{StaticResource Brush.BG2}">
<DataGrid.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
<Style x:Key="Style.DataGridText.VerticalCenter" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="22">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Width="14" Height="14" x:Name="status" Background="{Binding ., Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="2,0,4,0">
<TextBlock Text="{Binding ., Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="8"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Binding="{Binding Path}" Foreground="{StaticResource Brush.FG}" FontFamily="Consolas" ElementStyle="{StaticResource Style.DataGridText.VerticalCenter}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="ChangeListContextMenuOpening"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<local:DiffViewer Grid.Column="2" x:Name="diffViewer" Background="{StaticResource Brush.BG3}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,314 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Diff with selected 2 commits.
/// </summary>
public partial class TwoCommitsDiff : UserControl {
private Git.Repository repo = null;
private string sha1 = null;
private string sha2 = null;
private List<Git.Change> cachedChanges = new List<Git.Change>();
private List<Git.Change> displayChanges = new List<Git.Change>();
private string changeFilter = null;
/// <summary>
/// Node for file tree.
/// </summary>
public class Node {
public string FilePath { get; set; } = "";
public string OriginalPath { get; set; } = "";
public string Name { get; set; } = "";
public bool IsFile { get; set; } = false;
public bool IsNodeExpanded { get; set; } = true;
public Git.Change Change { get; set; } = null;
public Git.Commit.Object CommitObject { get; set; } = null;
public List<Node> Children { get; set; } = new List<Node>();
}
/// <summary>
/// Constructor.
/// </summary>
public TwoCommitsDiff() {
InitializeComponent();
}
/// <summary>
/// Show.
/// </summary>
/// <param name="repo"></param>
/// <param name="sha1"></param>
/// <param name="sha2"></param>
public void SetData(Git.Repository repo, string sha1, string sha2) {
this.repo = repo;
this.sha1 = sha1;
this.sha2 = sha2;
txtTitle.Content = $"COMMIT: {sha1} -> {sha2}";
Task.Run(() => LoadChanges(true));
}
/// <summary>
/// Cleanup.
/// </summary>
public void Cleanup() {
repo = null;
cachedChanges.Clear();
displayChanges.Clear();
}
private void LoadChanges(bool reload = false) {
if (reload) {
cachedChanges.Clear();
repo.RunCommand($"diff --name-status {sha1} {sha2}", line => {
var c = Git.Change.Parse(line, true);
if (c != null) cachedChanges.Add(c);
});
}
displayChanges.Clear();
if (string.IsNullOrEmpty(changeFilter)) {
displayChanges.AddRange(cachedChanges);
} else {
foreach (var c in cachedChanges) {
if (c.Path.ToUpper().Contains(changeFilter)) displayChanges.Add(c);
}
}
List<Node> changeTreeSource = new List<Node>();
Dictionary<string, Node> folders = new Dictionary<string, Node>();
bool isDefaultExpanded = displayChanges.Count < 50;
foreach (var c in displayChanges) {
var sepIdx = c.Path.IndexOf('/');
if (sepIdx == -1) {
Node node = new Node();
node.FilePath = c.Path;
node.IsFile = true;
node.Name = c.Path;
node.Change = c;
node.IsNodeExpanded = isDefaultExpanded;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
changeTreeSource.Add(node);
} else {
Node lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = c.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new Node();
lastFolder.FilePath = folder;
lastFolder.Name = folder.Substring(start);
lastFolder.IsNodeExpanded = isDefaultExpanded;
changeTreeSource.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var folderNode = new Node();
folderNode.FilePath = folder;
folderNode.Name = folder.Substring(start);
folderNode.IsNodeExpanded = isDefaultExpanded;
folders.Add(folder, folderNode);
lastFolder.Children.Add(folderNode);
lastFolder = folderNode;
}
start = sepIdx + 1;
sepIdx = c.Path.IndexOf('/', start);
}
Node node = new Node();
node.FilePath = c.Path;
node.Name = c.Path.Substring(start);
node.IsFile = true;
node.Change = c;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
lastFolder.Children.Add(node);
}
}
folders.Clear();
SortTreeNodes(changeTreeSource);
Dispatcher.Invoke(() => {
changeList2.ItemsSource = null;
changeList2.ItemsSource = displayChanges;
changeTree.ItemsSource = changeTreeSource;
diffViewer.Reset();
});
}
private void SearchChangeFileTextChanged(object sender, TextChangedEventArgs e) {
changeFilter = txtChangeFilter.Text.ToUpper();
Task.Run(() => LoadChanges());
}
private void ChangeTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
diffViewer.Reset();
var node = e.NewValue as Node;
if (node == null || !node.IsFile) return;
diffViewer.Diff(repo, new DiffViewer.Option() {
RevisionRange = new string[] { sha1, sha2 },
Path = node.FilePath,
OrgPath = node.OriginalPath
});
}
private void ChangeListSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return;
var change = e.AddedItems[0] as Git.Change;
if (change == null) return;
diffViewer.Diff(repo, new DiffViewer.Option() {
RevisionRange = new string[] { sha1, sha2 },
Path = change.Path,
OrgPath = change.OriginalPath
});
}
private void ChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) {
var row = sender as DataGridRow;
if (row == null) return;
var change = row.DataContext as Git.Change;
if (change == null) return;
var path = change.Path;
var menu = new ContextMenu();
if (change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
history.Header = "File History";
history.Click += (o, ev) => {
var viewer = new FileHistories(repo, path);
viewer.Show();
};
menu.Items.Add(history);
MenuItem explore = new MenuItem();
explore.Header = "Reveal in File Explorer";
explore.Click += (o, ev) => {
var absPath = Path.GetFullPath(repo.Path + "\\" + path);
Process.Start("explorer", $"/select,{absPath}");
e.Handled = true;
};
menu.Items.Add(explore);
}
MenuItem copyPath = new MenuItem();
copyPath.Header = "Copy Path";
copyPath.Click += (obj, ev) => {
Clipboard.SetText(path);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
private void ChangeListMouseDoubleClick(object sender, MouseButtonEventArgs e) {
var row = sender as DataGridRow;
if (row == null) return;
var change = row.DataContext as Git.Change;
if (change == null) return;
var viewer = new FileHistories(repo, change.Path);
viewer.Show();
}
private void SortTreeNodes(List<Node> list) {
list.Sort((l, r) => {
if (l.IsFile) {
return r.IsFile ? l.Name.CompareTo(r.Name) : 1;
} else {
return r.IsFile ? -1 : l.Name.CompareTo(r.Name);
}
});
foreach (var sub in list) {
if (sub.Children.Count > 0) SortTreeNodes(sub.Children);
}
}
private ScrollViewer GetScrollViewer(FrameworkElement owner) {
if (owner == null) return null;
if (owner is ScrollViewer) return owner as ScrollViewer;
int n = VisualTreeHelper.GetChildrenCount(owner);
for (int i = 0; i < n; i++) {
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement;
var deep = GetScrollViewer(child);
if (deep != null) return deep;
}
return null;
}
private void TreeMouseWheel(object sender, MouseWheelEventArgs e) {
var scroll = GetScrollViewer(sender as TreeView);
if (scroll == null) return;
if (e.Delta > 0) {
scroll.LineUp();
} else {
scroll.LineDown();
}
e.Handled = true;
}
private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = sender as TreeViewItem;
if (item == null) return;
var node = item.DataContext as Node;
if (node == null || !node.IsFile) return;
item.IsSelected = true;
ContextMenu menu = new ContextMenu();
if (node.Change == null || node.Change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
history.Header = "File History";
history.Click += (o, ev) => {
var viewer = new FileHistories(repo, node.FilePath);
viewer.Show();
};
menu.Items.Add(history);
MenuItem explore = new MenuItem();
explore.Header = "Reveal in File Explorer";
explore.Click += (o, ev) => {
var path = Path.GetFullPath(repo.Path + "\\" + node.FilePath);
Process.Start("explorer", $"/select,{path}");
e.Handled = true;
};
menu.Items.Add(explore);
}
MenuItem copyPath = new MenuItem();
copyPath.Header = "Copy Path";
copyPath.Click += (obj, ev) => {
Clipboard.SetText(node.FilePath);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
}
}