Skip to content

enhance: file rename detection and path resolution in file history #1372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/Commands/CompareRevisions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public partial class CompareRevisions : Command
{
[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)
Expand Down Expand Up @@ -51,7 +51,11 @@ private void ParseLine(string line)
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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/QueryCommits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method
}
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
search += $"--follow -- \"{filter}\"";
}
else
{
Expand Down
124 changes: 124 additions & 0 deletions src/Commands/QueryFilePathInRevision.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
73 changes: 50 additions & 23 deletions src/Models/Change.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace SourceGit.Models
{
Expand All @@ -9,17 +10,18 @@ public enum ChangeViewMode
Tree,
}

[Flags]
public enum ChangeState
{
None,
Modified,
TypeChanged,
Added,
Deleted,
Renamed,
Copied,
Untracked,
Conflicted,
None = 0,
Modified = 1 << 0,
TypeChanged = 1 << 1,
Added = 1 << 2,
Deleted = 1 << 3,
Renamed = 1 << 4,
Copied = 1 << 5,
Untracked = 1 << 6,
Conflicted = 1 << 7,
}

public enum ConflictReason
Expand Down Expand Up @@ -54,8 +56,8 @@ public class Change
public string ConflictMarker => CONFLICT_MARKERS[(int)ConflictReason];
public string ConflictDesc => CONFLICT_DESCS[(int)ConflictReason];

public string WorkTreeDesc => TYPE_DESCS[(int)WorkTree];
public string IndexDesc => TYPE_DESCS[(int)Index];
public string WorkTreeDesc => TYPE_DESCS[GetPrimaryState(WorkTree)];
public string IndexDesc => TYPE_DESCS[GetPrimaryState(Index)];

public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
{
Expand Down Expand Up @@ -88,18 +90,43 @@ public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
}

private static readonly string[] TYPE_DESCS =
[
"Unknown",
"Modified",
"Type Changed",
"Added",
"Deleted",
"Renamed",
"Copied",
"Untracked",
"Conflict"
];
public static ChangeState GetPrimaryState(ChangeState state)
{
if (state == ChangeState.None)
return ChangeState.None;
if ((state & ChangeState.Conflicted) != 0)
return ChangeState.Conflicted;
if ((state & ChangeState.Untracked) != 0)
return ChangeState.Untracked;
if ((state & ChangeState.Renamed) != 0)
return ChangeState.Renamed;
if ((state & ChangeState.Copied) != 0)
return ChangeState.Copied;
if ((state & ChangeState.Deleted) != 0)
return ChangeState.Deleted;
if ((state & ChangeState.Added) != 0)
return ChangeState.Added;
if ((state & ChangeState.TypeChanged) != 0)
return ChangeState.TypeChanged;
if ((state & ChangeState.Modified) != 0)
return ChangeState.Modified;

return ChangeState.None;
}

private static readonly Dictionary<ChangeState, string> TYPE_DESCS = new Dictionary<ChangeState, string>
{
{ ChangeState.None, "Unknown" },
{ ChangeState.Modified, "Modified" },
{ ChangeState.TypeChanged, "Type Changed" },
{ ChangeState.Added, "Added" },
{ ChangeState.Deleted, "Deleted" },
{ ChangeState.Renamed, "Renamed" },
{ ChangeState.Copied, "Copied" },
{ ChangeState.Untracked, "Untracked" },
{ ChangeState.Conflicted, "Conflict" }
};

private static readonly string[] CONFLICT_MARKERS =
[
string.Empty,
Expand Down
Loading
Loading