mirror of
https://fastgit.cc/github.com/sourcegit-scm/sourcegit
synced 2026-04-21 05:10:25 +08:00
refactor: rewrite file-history to support detecting renames (--follow) (#2174)
Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
98
src/Commands/QueryFileHistory.cs
Normal file
98
src/Commands/QueryFileHistory.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public partial class QueryFileHistory : Command
|
||||
{
|
||||
[GeneratedRegex(@"^([MADC])\s+(.+)$")]
|
||||
private static partial Regex REG_FORMAT();
|
||||
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
|
||||
private static partial Regex REG_RENAME_FORMAT();
|
||||
|
||||
public QueryFileHistory(string repo, string path, string head)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
RaiseError = false;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("log --follow --no-show-signature --date-order -n 10000 --decorate=no --format=\"@%H%x00%P%x00%aN±%aE%x00%at%x00%s\" --name-status ");
|
||||
if (!string.IsNullOrEmpty(head))
|
||||
builder.Append(head).Append(" ");
|
||||
builder.Append("-- ").Append(path.Quoted());
|
||||
|
||||
Args = builder.ToString();
|
||||
}
|
||||
|
||||
public async Task<List<Models.FileVersion>> GetResultAsync()
|
||||
{
|
||||
var versions = new List<Models.FileVersion>();
|
||||
var rs = await ReadToEndAsync().ConfigureAwait(false);
|
||||
if (!rs.IsSuccess)
|
||||
return versions;
|
||||
|
||||
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lines.Length == 0)
|
||||
return versions;
|
||||
|
||||
Models.FileVersion last = null;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.StartsWith('@'))
|
||||
{
|
||||
var parts = line.Split('\0');
|
||||
if (parts.Length != 5)
|
||||
continue;
|
||||
|
||||
last = new Models.FileVersion();
|
||||
last.SHA = parts[0].Substring(1);
|
||||
last.HasParent = !string.IsNullOrEmpty(parts[1]);
|
||||
last.Author = Models.User.FindOrAdd(parts[2]);
|
||||
last.AuthorTime = ulong.Parse(parts[3]);
|
||||
last.Subject = parts[4];
|
||||
versions.Add(last);
|
||||
}
|
||||
else if (last != null)
|
||||
{
|
||||
var match = REG_FORMAT().Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
match = REG_RENAME_FORMAT().Match(line);
|
||||
if (match.Success)
|
||||
{
|
||||
last.Change.Path = match.Groups[1].Value;
|
||||
last.Change.Set(Models.ChangeState.Renamed);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
last.Change.Path = match.Groups[2].Value;
|
||||
|
||||
var status = match.Groups[1].Value;
|
||||
switch (status[0])
|
||||
{
|
||||
case 'M':
|
||||
last.Change.Set(Models.ChangeState.Modified);
|
||||
break;
|
||||
case 'A':
|
||||
last.Change.Set(Models.ChangeState.Added);
|
||||
break;
|
||||
case 'D':
|
||||
last.Change.Set(Models.ChangeState.Deleted);
|
||||
break;
|
||||
case 'C':
|
||||
last.Change.Set(Models.ChangeState.Copied);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SourceGit.Models
|
||||
@@ -79,6 +80,53 @@ namespace SourceGit.Models
|
||||
_path = file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to diff in `FileHistory`
|
||||
/// </summary>
|
||||
/// <param name="ver"></param>
|
||||
public DiffOption(FileVersion ver)
|
||||
{
|
||||
_revisions.Add(ver.HasParent ? $"{ver.SHA}^" : Commit.EmptyTreeSHA1);
|
||||
_revisions.Add(ver.SHA);
|
||||
_path = ver.Path;
|
||||
_orgPath = ver.Change.OriginalPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to diff two revisions in `FileHistory`
|
||||
/// </summary>
|
||||
/// <param name="start"></param>
|
||||
/// <param name="end"></param>
|
||||
public DiffOption(FileVersion start, FileVersion end)
|
||||
{
|
||||
if (start.Change.Index == ChangeState.Deleted)
|
||||
{
|
||||
_revisions.Add(Commit.EmptyTreeSHA1);
|
||||
_revisions.Add(end.SHA);
|
||||
_path = end.Path;
|
||||
}
|
||||
else if (end.Change.Index == ChangeState.Deleted)
|
||||
{
|
||||
_revisions.Add(start.SHA);
|
||||
_revisions.Add(Commit.EmptyTreeSHA1);
|
||||
_path = start.Path;
|
||||
}
|
||||
else if (!end.Path.Equals(start.Path, StringComparison.Ordinal))
|
||||
{
|
||||
_revisions.Add($"{start.SHA}:{start.Path.Quoted()}");
|
||||
_revisions.Add($"{end.SHA}:{end.Path.Quoted()}");
|
||||
_path = end.Path;
|
||||
_orgPath = start.Path;
|
||||
_ignorePaths = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_revisions.Add(start.SHA);
|
||||
_revisions.Add(end.SHA);
|
||||
_path = start.Path;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to show differences between two revisions.
|
||||
/// </summary>
|
||||
@@ -104,6 +152,9 @@ namespace SourceGit.Models
|
||||
foreach (var r in _revisions)
|
||||
builder.Append($"{r} ");
|
||||
|
||||
if (_ignorePaths)
|
||||
return builder.ToString();
|
||||
|
||||
builder.Append("-- ");
|
||||
if (!string.IsNullOrEmpty(_orgPath))
|
||||
builder.Append($"{_orgPath.Quoted()} ");
|
||||
@@ -118,5 +169,6 @@ namespace SourceGit.Models
|
||||
private readonly string _orgPath = string.Empty;
|
||||
private readonly string _extra = string.Empty;
|
||||
private readonly List<string> _revisions = [];
|
||||
private readonly bool _ignorePaths = false;
|
||||
}
|
||||
}
|
||||
|
||||
18
src/Models/FileVersion.cs
Normal file
18
src/Models/FileVersion.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace SourceGit.Models
|
||||
{
|
||||
public class FileVersion
|
||||
{
|
||||
public string SHA { get; set; } = string.Empty;
|
||||
public bool HasParent { get; set; } = false;
|
||||
public User Author { get; set; } = User.Invalid;
|
||||
public ulong AuthorTime { get; set; } = 0;
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public Change Change { get; set; } = new();
|
||||
|
||||
public string Path => Change.Path;
|
||||
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime);
|
||||
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Collections;
|
||||
@@ -36,10 +35,10 @@ namespace SourceGit.ViewModels
|
||||
set => SetProperty(ref _viewContent, value);
|
||||
}
|
||||
|
||||
public FileHistoriesSingleRevision(string repo, string file, Models.Commit revision, bool prevIsDiffMode)
|
||||
public FileHistoriesSingleRevision(string repo, Models.FileVersion revision, bool prevIsDiffMode)
|
||||
{
|
||||
_repo = repo;
|
||||
_file = file;
|
||||
_file = revision.Path;
|
||||
_revision = revision;
|
||||
_isDiffMode = prevIsDiffMode;
|
||||
_viewContent = null;
|
||||
@@ -155,26 +154,25 @@ namespace SourceGit.ViewModels
|
||||
|
||||
private void SetViewContentAsDiff()
|
||||
{
|
||||
var option = new Models.DiffOption(_revision, _file);
|
||||
ViewContent = new DiffContext(_repo, option, _viewContent as DiffContext);
|
||||
ViewContent = new DiffContext(_repo, new Models.DiffOption(_revision), _viewContent as DiffContext);
|
||||
}
|
||||
|
||||
private string _repo = null;
|
||||
private string _file = null;
|
||||
private Models.Commit _revision = null;
|
||||
private Models.FileVersion _revision = null;
|
||||
private bool _isDiffMode = false;
|
||||
private object _viewContent = null;
|
||||
}
|
||||
|
||||
public class FileHistoriesCompareRevisions : ObservableObject
|
||||
{
|
||||
public Models.Commit StartPoint
|
||||
public Models.FileVersion StartPoint
|
||||
{
|
||||
get => _startPoint;
|
||||
set => SetProperty(ref _startPoint, value);
|
||||
}
|
||||
|
||||
public Models.Commit EndPoint
|
||||
public Models.FileVersion EndPoint
|
||||
{
|
||||
get => _endPoint;
|
||||
set => SetProperty(ref _endPoint, value);
|
||||
@@ -186,19 +184,18 @@ namespace SourceGit.ViewModels
|
||||
set => SetProperty(ref _viewContent, value);
|
||||
}
|
||||
|
||||
public FileHistoriesCompareRevisions(string repo, string file, Models.Commit start, Models.Commit end)
|
||||
public FileHistoriesCompareRevisions(string repo, Models.FileVersion start, Models.FileVersion end)
|
||||
{
|
||||
_repo = repo;
|
||||
_file = file;
|
||||
_startPoint = start;
|
||||
_endPoint = end;
|
||||
RefreshViewContent();
|
||||
_viewContent = new(_repo, new(start, end));
|
||||
}
|
||||
|
||||
public void Swap()
|
||||
{
|
||||
(StartPoint, EndPoint) = (_endPoint, _startPoint);
|
||||
RefreshViewContent();
|
||||
ViewContent = new(_repo, new(_startPoint, _endPoint), _viewContent);
|
||||
}
|
||||
|
||||
public async Task<bool> SaveAsPatch(string saveTo)
|
||||
@@ -208,27 +205,9 @@ namespace SourceGit.ViewModels
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void RefreshViewContent()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
_changes = await new Commands.CompareRevisions(_repo, _startPoint.SHA, _endPoint.SHA, _file).ReadAsync().ConfigureAwait(false);
|
||||
if (_changes.Count == 0)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => ViewContent = null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var option = new Models.DiffOption(_startPoint.SHA, _endPoint.SHA, _changes[0]);
|
||||
Dispatcher.UIThread.Post(() => ViewContent = new DiffContext(_repo, option, _viewContent));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private string _repo = null;
|
||||
private string _file = null;
|
||||
private Models.Commit _startPoint = null;
|
||||
private Models.Commit _endPoint = null;
|
||||
private Models.FileVersion _startPoint = null;
|
||||
private Models.FileVersion _endPoint = null;
|
||||
private List<Models.Change> _changes = [];
|
||||
private DiffContext _viewContent = null;
|
||||
}
|
||||
@@ -246,13 +225,13 @@ namespace SourceGit.ViewModels
|
||||
private set => SetProperty(ref _isLoading, value);
|
||||
}
|
||||
|
||||
public List<Models.Commit> Commits
|
||||
public List<Models.FileVersion> Revisions
|
||||
{
|
||||
get => _commits;
|
||||
set => SetProperty(ref _commits, value);
|
||||
get => _revisions;
|
||||
set => SetProperty(ref _revisions, value);
|
||||
}
|
||||
|
||||
public AvaloniaList<Models.Commit> SelectedCommits
|
||||
public AvaloniaList<Models.FileVersion> SelectedRevisions
|
||||
{
|
||||
get;
|
||||
set;
|
||||
@@ -275,41 +254,34 @@ namespace SourceGit.ViewModels
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var argsBuilder = new StringBuilder();
|
||||
argsBuilder
|
||||
.Append("--date-order -n 10000 ")
|
||||
.Append(commit ?? string.Empty)
|
||||
.Append(" -- ")
|
||||
.Append(file.Quoted());
|
||||
|
||||
var commits = await new Commands.QueryCommits(_repo, argsBuilder.ToString(), false)
|
||||
var revisions = await new Commands.QueryFileHistory(_repo, file, commit)
|
||||
.GetResultAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
IsLoading = false;
|
||||
Commits = commits;
|
||||
if (Commits.Count > 0)
|
||||
SelectedCommits.Add(Commits[0]);
|
||||
Revisions = revisions;
|
||||
if (revisions.Count > 0)
|
||||
SelectedRevisions.Add(revisions[0]);
|
||||
});
|
||||
});
|
||||
|
||||
SelectedCommits.CollectionChanged += (_, _) =>
|
||||
SelectedRevisions.CollectionChanged += (_, _) =>
|
||||
{
|
||||
if (_viewContent is FileHistoriesSingleRevision singleRevision)
|
||||
_prevIsDiffMode = singleRevision.IsDiffMode;
|
||||
|
||||
ViewContent = SelectedCommits.Count switch
|
||||
ViewContent = SelectedRevisions.Count switch
|
||||
{
|
||||
1 => new FileHistoriesSingleRevision(_repo, file, SelectedCommits[0], _prevIsDiffMode),
|
||||
2 => new FileHistoriesCompareRevisions(_repo, file, SelectedCommits[0], SelectedCommits[1]),
|
||||
_ => SelectedCommits.Count,
|
||||
1 => new FileHistoriesSingleRevision(_repo, SelectedRevisions[0], _prevIsDiffMode),
|
||||
2 => new FileHistoriesCompareRevisions(_repo, SelectedRevisions[0], SelectedRevisions[1]),
|
||||
_ => SelectedRevisions.Count,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
public void NavigateToCommit(Models.Commit commit)
|
||||
public void NavigateToCommit(Models.FileVersion revision)
|
||||
{
|
||||
var launcher = App.GetLauncher();
|
||||
if (launcher != null)
|
||||
@@ -318,16 +290,16 @@ namespace SourceGit.ViewModels
|
||||
{
|
||||
if (page.Data is Repository repo && repo.FullPath.Equals(_repo, StringComparison.Ordinal))
|
||||
{
|
||||
repo.NavigateToCommit(commit.SHA);
|
||||
repo.NavigateToCommit(revision.SHA);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GetCommitFullMessage(Models.Commit commit)
|
||||
public string GetCommitFullMessage(Models.FileVersion revision)
|
||||
{
|
||||
var sha = commit.SHA;
|
||||
var sha = revision.SHA;
|
||||
if (_fullCommitMessages.TryGetValue(sha, out var msg))
|
||||
return msg;
|
||||
|
||||
@@ -339,7 +311,7 @@ namespace SourceGit.ViewModels
|
||||
private readonly string _repo = null;
|
||||
private bool _isLoading = true;
|
||||
private bool _prevIsDiffMode = true;
|
||||
private List<Models.Commit> _commits = null;
|
||||
private List<Models.FileVersion> _revisions = null;
|
||||
private Dictionary<string, string> _fullCommitMessages = new();
|
||||
private object _viewContent = null;
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
BorderThickness="1"
|
||||
Margin="8,4,4,8"
|
||||
BorderBrush="{DynamicResource Brush.Border2}"
|
||||
ItemsSource="{Binding Commits}"
|
||||
SelectedItems="{Binding SelectedCommits, Mode=TwoWay}"
|
||||
ItemsSource="{Binding Revisions}"
|
||||
SelectedItems="{Binding SelectedRevisions, Mode=TwoWay}"
|
||||
SelectionMode="Multiple"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
@@ -79,7 +79,7 @@
|
||||
</ListBox.ItemsPanel>
|
||||
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="m:Commit">
|
||||
<DataTemplate DataType="m:FileVersion">
|
||||
<Border BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="0,0,0,1" Padding="4">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,96">
|
||||
@@ -196,16 +196,13 @@
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" Margin="4,6" ColumnDefinitions="*,32,*,Auto">
|
||||
<Grid.DataTemplates>
|
||||
<DataTemplate DataType="m:Commit">
|
||||
<DataTemplate DataType="m:FileVersion">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto,Auto">
|
||||
<v:Avatar Width="16" Height="16" VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding Author}"/>
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<v:Avatar Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding Author}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Author.Name}" Margin="8,0,0,0"/>
|
||||
<Border Grid.Column="2" Background="{DynamicResource Brush.Accent}" CornerRadius="4" IsVisible="{Binding IsCurrentHead}">
|
||||
<TextBlock Text="HEAD" Margin="4,0" Foreground="#FFDDDDDD"/>
|
||||
</Border>
|
||||
<TextBlock Grid.Column="3" Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0" TextDecorations="Underline" Cursor="Hand" PointerPressed="OnPressCommitSHA" />
|
||||
<TextBlock Grid.Column="4" Text="{Binding CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0" TextDecorations="Underline" Cursor="Hand" PointerPressed="OnPressCommitSHA" />
|
||||
<TextBlock Grid.Column="3" Text="{Binding AuthorTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Grid.Row="1" Text="{Binding Subject}" VerticalAlignment="Bottom"/>
|
||||
|
||||
@@ -16,10 +16,10 @@ namespace SourceGit.Views
|
||||
|
||||
private void OnPressCommitSHA(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is TextBlock { DataContext: Models.Commit commit } &&
|
||||
if (sender is TextBlock { DataContext: Models.FileVersion ver } &&
|
||||
DataContext is ViewModels.FileHistories vm)
|
||||
{
|
||||
vm.NavigateToCommit(commit);
|
||||
vm.NavigateToCommit(ver);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
@@ -76,12 +76,12 @@ namespace SourceGit.Views
|
||||
|
||||
private void OnCommitSubjectPointerMoved(object sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border { DataContext: Models.Commit commit } border &&
|
||||
if (sender is Border { DataContext: Models.FileVersion ver } border &&
|
||||
DataContext is ViewModels.FileHistories vm)
|
||||
{
|
||||
var tooltip = ToolTip.GetTip(border);
|
||||
if (tooltip == null)
|
||||
ToolTip.SetTip(border, vm.GetCommitFullMessage(commit));
|
||||
ToolTip.SetTip(border, vm.GetCommitFullMessage(ver));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user