enhance: file rename detection and path resolution in file history

This commit is contained in:
Gadfly 2025-05-28 18:02:43 +08:00
parent 46231a759c
commit a86a9cfd2b
No known key found for this signature in database
4 changed files with 199 additions and 24 deletions

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
[GeneratedRegex(@"^([MADC])\s+(.+)$")]
private static partial Regex REG_FORMAT();
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)\s+(.+)$")]
private static partial Regex REG_RENAME_FORMAT();
public CompareRevisions(string repo, string start, string end)
@ -51,7 +51,11 @@ namespace SourceGit.Commands
match = REG_RENAME_FORMAT().Match(line);
if (match.Success)
{
var renamed = new Models.Change() { Path = match.Groups[1].Value };
var renamed = new Models.Change()
{
OriginalPath = match.Groups[1].Value,
Path = match.Groups[2].Value
};
renamed.Set(Models.ChangeState.Renamed);
_changes.Add(renamed);
}

View file

@ -43,7 +43,7 @@ namespace SourceGit.Commands
}
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
search += $"--follow -- \"{filter}\"";
}
else
{

View file

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryFilePathInRevision : Command
{
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)\s+(.+)$")]
private static partial Regex REG_RENAME_FORMAT();
public QueryFilePathInRevision(string repo, string revision, string currentPath)
{
WorkingDirectory = repo;
Context = repo;
_revision = revision;
_currentPath = currentPath;
}
public string Result()
{
if (CheckPathExistsInRevision(_currentPath))
return _currentPath;
string mappedPath = FindRenameHistory();
return mappedPath ?? _currentPath;
}
private bool CheckPathExistsInRevision(string path)
{
Args = $"ls-tree -r {_revision} -- \"{path}\"";
var rs = ReadToEnd();
return rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut);
}
private string FindRenameHistory()
{
var fileHistory = BuildFileHistory();
if (fileHistory == null || fileHistory.Count == 0)
return null;
foreach (var entry in fileHistory)
{
if (!IsTargetRevisionBefore(entry.CommitSHA))
continue;
if (CheckPathExistsInRevision(entry.OldPath))
return entry.OldPath;
}
if (fileHistory.Count > 0)
{
var oldestPath = fileHistory[^1].OldPath;
if (CheckPathExistsInRevision(oldestPath))
return oldestPath;
}
return null;
}
private bool IsTargetRevisionBefore(string commitSHA)
{
Args = $"merge-base --is-ancestor {_revision} {commitSHA}";
var rs = ReadToEnd();
return rs.IsSuccess;
}
private List<RenameHistoryEntry> BuildFileHistory()
{
Args = $"log --follow --name-status --pretty=format:\"commit %H\" -M -- \"{_currentPath}\"";
var rs = ReadToEnd();
if (!rs.IsSuccess)
return null;
var result = new List<RenameHistoryEntry>();
var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
string currentCommit = null;
string currentPath = _currentPath;
foreach (var t in lines)
{
var line = t.Trim();
if (line.StartsWith("commit ", StringComparison.Ordinal))
{
currentCommit = line.Substring("commit ".Length);
continue;
}
var match = REG_RENAME_FORMAT().Match(line);
if (match.Success && currentCommit != null)
{
var oldPath = match.Groups[1].Value;
var newPath = match.Groups[2].Value;
if (newPath == currentPath)
{
result.Add(new RenameHistoryEntry
{
CommitSHA = currentCommit,
OldPath = oldPath,
NewPath = newPath
});
currentPath = oldPath;
}
}
}
return result;
}
private class RenameHistoryEntry
{
public string CommitSHA { get; set; }
public string OldPath { get; set; }
public string NewPath { get; set; }
}
private readonly string _revision;
private readonly string _currentPath;
}
}

View file

@ -49,7 +49,8 @@ namespace SourceGit.ViewModels
public void ResetToSelectedRevision()
{
new Commands.Checkout(_repo.FullPath).FileWithRevision(_file, $"{_revision.SHA}");
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();
new Commands.Checkout(_repo.FullPath).FileWithRevision(revisionFilePath, $"{_revision.SHA}");
}
private void RefreshViewContent()
@ -62,10 +63,12 @@ namespace SourceGit.ViewModels
private void SetViewContentAsRevisionFile()
{
var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _revision.SHA, _file).Result();
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();
var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _revision.SHA, revisionFilePath).Result();
if (objs.Count == 0)
{
ViewContent = new FileHistoriesRevisionFile(_file, null);
ViewContent = new FileHistoriesRevisionFile(revisionFilePath, null);
return;
}
@ -75,30 +78,29 @@ namespace SourceGit.ViewModels
case Models.ObjectType.Blob:
Task.Run(() =>
{
var isBinary = new Commands.IsBinary(_repo.FullPath, _revision.SHA, _file).Result();
var isBinary = new Commands.IsBinary(_repo.FullPath, _revision.SHA, revisionFilePath).Result();
if (isBinary)
{
var ext = Path.GetExtension(_file);
var ext = Path.GetExtension(revisionFilePath);
if (IMG_EXTS.Contains(ext))
{
var stream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, _file);
var stream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, revisionFilePath);
var fileSize = stream.Length;
var bitmap = fileSize > 0 ? new Bitmap(stream) : null;
var imageType = Path.GetExtension(_file).TrimStart('.').ToUpper(CultureInfo.CurrentCulture);
var imageType = Path.GetExtension(revisionFilePath).TrimStart('.').ToUpper(CultureInfo.CurrentCulture);
var image = new Models.RevisionImageFile() { Image = bitmap, FileSize = fileSize, ImageType = imageType };
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, image));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, image));
}
else
{
var size = new Commands.QueryFileSize(_repo.FullPath, _file, _revision.SHA).Result();
var size = new Commands.QueryFileSize(_repo.FullPath, revisionFilePath, _revision.SHA).Result();
var binaryFile = new Models.RevisionBinaryFile() { Size = size };
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, binaryFile));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, binaryFile));
}
return;
}
var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, _file);
var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, revisionFilePath);
var content = new StreamReader(contentStream).ReadToEnd();
var matchLFS = REG_LFS_FORMAT().Match(content);
if (matchLFS.Success)
@ -106,19 +108,19 @@ namespace SourceGit.ViewModels
var lfs = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
lfs.Object.Oid = matchLFS.Groups[1].Value;
lfs.Object.Size = long.Parse(matchLFS.Groups[2].Value);
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, lfs));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, lfs));
}
else
{
var txt = new Models.RevisionTextFile() { FileName = obj.Path, Content = content };
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, txt));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, txt));
}
});
break;
case Models.ObjectType.Commit:
Task.Run(() =>
{
var submoduleRoot = Path.Combine(_repo.FullPath, _file);
var submoduleRoot = Path.Combine(_repo.FullPath, revisionFilePath);
var commit = new Commands.QuerySingleCommit(submoduleRoot, obj.SHA).Result();
if (commit != null)
{
@ -128,7 +130,7 @@ namespace SourceGit.ViewModels
Commit = commit,
FullMessage = new Models.CommitFullMessage { Message = message }
};
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, module));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, module));
}
else
{
@ -137,19 +139,21 @@ namespace SourceGit.ViewModels
Commit = new Models.Commit() { SHA = obj.SHA },
FullMessage = null
};
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, module));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, module));
}
});
break;
default:
ViewContent = new FileHistoriesRevisionFile(_file, null);
ViewContent = new FileHistoriesRevisionFile(revisionFilePath, null);
break;
}
}
private void SetViewContentAsDiff()
{
var option = new Models.DiffOption(_revision, _file);
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();
var option = new Models.DiffOption(_revision, revisionFilePath);
ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext);
}
@ -216,7 +220,50 @@ namespace SourceGit.ViewModels
{
Task.Run(() =>
{
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
var startFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _startPoint.SHA, _file).Result();
var endFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _endPoint.SHA, _file).Result();
var allChanges = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA).Result();
Models.Change renamedChange = null;
foreach (var change in allChanges)
{
if (change.WorkTree != Models.ChangeState.Renamed && change.Index != Models.ChangeState.Renamed)
continue;
if (change.Path != endFilePath && change.OriginalPath != startFilePath)
continue;
renamedChange = change;
break;
}
if (renamedChange != null)
{
_changes = [renamedChange];
}
else
{
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, startFilePath).Result();
if (_changes.Count == 0 && startFilePath != endFilePath)
{
var renamed = new Models.Change()
{
OriginalPath = startFilePath,
Path = endFilePath
};
renamed.Set(Models.ChangeState.Renamed);
_changes = [renamed];
}
else if (_changes.Count == 0)
{
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, endFilePath).Result();
if (_changes.Count == 0)
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
}
}
if (_changes.Count == 0)
{
Dispatcher.UIThread.Invoke(() => ViewContent = null);
@ -270,7 +317,7 @@ namespace SourceGit.ViewModels
Task.Run(() =>
{
var based = commit ?? string.Empty;
var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order -n 10000 {based} -- \"{file}\"", false).Result();
var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order --follow -n 10000 {based} -- \"{file}\"", false).Result();
Dispatcher.UIThread.Invoke(() =>
{
IsLoading = false;