diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index 7b4a496d..b1383911 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -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); } diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 9e1d9918..53c8b370 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -43,7 +43,7 @@ namespace SourceGit.Commands } else if (method == Models.CommitSearchMethod.ByFile) { - search += $"-- \"{filter}\""; + search += $"--follow -- \"{filter}\""; } else { diff --git a/src/Commands/QueryFilePathInRevision.cs b/src/Commands/QueryFilePathInRevision.cs new file mode 100644 index 00000000..3edd199e --- /dev/null +++ b/src/Commands/QueryFilePathInRevision.cs @@ -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 BuildFileHistory() + { + Args = $"log --follow --name-status --pretty=format:\"commit %H\" -M -- \"{_currentPath}\""; + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return null; + + var result = new List(); + 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; + } +} diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 9f91205e..ae6776ff 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -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;