Skip to content
Open
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
274 changes: 143 additions & 131 deletions src/Commands/QueryLocalChanges.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace SourceGit.Commands
{
public partial class QueryLocalChanges : Command
public class QueryLocalChanges : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
private static partial Regex REG_FORMAT();

public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptionalLocks = true)
{
WorkingDirectory = repo;
Expand All @@ -20,9 +17,9 @@ public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptio
if (noOptionalLocks)
builder.Append("--no-optional-locks ");
if (includeUntracked)
builder.Append("-c core.untrackedCache=true -c status.showUntrackedFiles=all status -uall --ignore-submodules=dirty --porcelain");
builder.Append("-c core.untrackedCache=true -c status.showUntrackedFiles=all status -uall --ignore-submodules=dirty --porcelain=v2 -z");
else
builder.Append("status -uno --ignore-submodules=dirty --porcelain");
builder.Append("status -uno --ignore-submodules=dirty --porcelain=v2 -z");

Args = builder.ToString();
}
Expand Down Expand Up @@ -52,133 +49,49 @@ public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptio
});
}

while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
continue;

var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
var buffer = new char[8192];
var record = new StringBuilder();
var waitingOriginalPath = false;
Models.Change renamed = null;
int count;

switch (status)
while ((count = await proc.StandardOutput.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
{
var start = 0;
while (start < count)
{
case " M":
change.Set(Models.ChangeState.None, Models.ChangeState.Modified);
break;
case " T":
change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged);
break;
case " A":
change.Set(Models.ChangeState.None, Models.ChangeState.Added);
break;
case " D":
change.Set(Models.ChangeState.None, Models.ChangeState.Deleted);
break;
case " R":
change.Set(Models.ChangeState.None, Models.ChangeState.Renamed);
break;
case " C":
change.Set(Models.ChangeState.None, Models.ChangeState.Copied);
break;
case "M":
change.Set(Models.ChangeState.Modified);
break;
case "MM":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified);
break;
case "MT":
change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged);
break;
case "MD":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted);
break;
case "T":
change.Set(Models.ChangeState.TypeChanged);
break;
case "TM":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified);
break;
case "TT":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged);
break;
case "TD":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted);
break;
case "A":
change.Set(Models.ChangeState.Added);
break;
case "AM":
change.Set(Models.ChangeState.Added, Models.ChangeState.Modified);
break;
case "AT":
change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged);
break;
case "AD":
change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted);
break;
case "D":
change.Set(Models.ChangeState.Deleted);
break;
case "R":
change.Set(Models.ChangeState.Renamed);
break;
case "RM":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified);
break;
case "RT":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged);
break;
case "RD":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted);
break;
case "C":
change.Set(Models.ChangeState.Copied);
break;
case "CM":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified);
break;
case "CT":
change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged);
break;
case "CD":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted);
break;
case "DD":
change.ConflictReason = Models.ConflictReason.BothDeleted;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "AU":
change.ConflictReason = Models.ConflictReason.AddedByUs;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UD":
change.ConflictReason = Models.ConflictReason.DeletedByThem;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UA":
change.ConflictReason = Models.ConflictReason.AddedByThem;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "DU":
change.ConflictReason = Models.ConflictReason.DeletedByUs;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "AA":
change.ConflictReason = Models.ConflictReason.BothAdded;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UU":
change.ConflictReason = Models.ConflictReason.BothModified;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "??":
change.Set(Models.ChangeState.None, Models.ChangeState.Untracked);
var end = Array.IndexOf(buffer, '\0', start, count - start);
if (end < 0)
{
record.Append(buffer, start, count - start);
break;
}
}

if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None)
outs.Add(change);
record.Append(buffer, start, end - start);
start = end + 1;

// For renamed/copied entries, the original path is output as an extra field.
if (waitingOriginalPath)
{
if (renamed != null)
renamed.OriginalPath = record.ToString();

waitingOriginalPath = false;
renamed = null;
}
else
{
var change = ParseRecord(record.ToString(), out waitingOriginalPath);
if (change != null)
{
outs.Add(change);
if (waitingOriginalPath)
renamed = change;
}
}

record.Clear();
}
}

lock (capturedLock)
Expand All @@ -194,6 +107,105 @@ public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptio
return outs;
}

private static Models.Change ParseRecord(string record, out bool hasOriginalPath)
{
hasOriginalPath = false;
if (record.Length < 3)
return null;

var change = new Models.Change();
switch (record[0])
{
case '1':
// 1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
var ordinary = record.Split(' ', 9);
if (ordinary.Length != 9)
return null;

change.Path = ordinary[8];
(change.Index, change.WorkTree) = ParseState(ordinary[1]);
break;
case '2':
// 2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path>, followed by <origPath> as the next field
hasOriginalPath = true;

var renamed = record.Split(' ', 10);
if (renamed.Length != 10)
return null;

change.Path = renamed[9];
(change.Index, change.WorkTree) = ParseState(renamed[1]);
break;
case 'u':
// u <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
var unmerged = record.Split(' ', 11);
if (unmerged.Length != 11)
return null;

change.Path = unmerged[10];
change.ConflictReason = unmerged[1] switch
{
"DD" => Models.ConflictReason.BothDeleted,
"AU" => Models.ConflictReason.AddedByUs,
"UD" => Models.ConflictReason.DeletedByThem,
"UA" => Models.ConflictReason.AddedByThem,
"DU" => Models.ConflictReason.DeletedByUs,
"AA" => Models.ConflictReason.BothAdded,
"UU" => Models.ConflictReason.BothModified,
_ => Models.ConflictReason.None,
};

if (change.ConflictReason != Models.ConflictReason.None)
change.WorkTree = Models.ChangeState.Conflicted;
break;
case '?':
// ? <path>
change.Path = record.Substring(2);
change.WorkTree = Models.ChangeState.Untracked;
break;
}

if (change.Index == Models.ChangeState.None && change.WorkTree == Models.ChangeState.None)
return null;

return change;
}

private static (Models.ChangeState, Models.ChangeState) ParseState(string xy)
{
return xy switch
{
".M" => (Models.ChangeState.None, Models.ChangeState.Modified),
".T" => (Models.ChangeState.None, Models.ChangeState.TypeChanged),
".A" => (Models.ChangeState.None, Models.ChangeState.Added),
".D" => (Models.ChangeState.None, Models.ChangeState.Deleted),
".R" => (Models.ChangeState.None, Models.ChangeState.Renamed),
".C" => (Models.ChangeState.None, Models.ChangeState.Copied),
"M." => (Models.ChangeState.Modified, Models.ChangeState.None),
"MM" => (Models.ChangeState.Modified, Models.ChangeState.Modified),
"MT" => (Models.ChangeState.Modified, Models.ChangeState.TypeChanged),
"MD" => (Models.ChangeState.Modified, Models.ChangeState.Deleted),
"T." => (Models.ChangeState.TypeChanged, Models.ChangeState.None),
"TM" => (Models.ChangeState.TypeChanged, Models.ChangeState.Modified),
"TT" => (Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged),
"TD" => (Models.ChangeState.TypeChanged, Models.ChangeState.Deleted),
"A." => (Models.ChangeState.Added, Models.ChangeState.None),
"AM" => (Models.ChangeState.Added, Models.ChangeState.Modified),
"AT" => (Models.ChangeState.Added, Models.ChangeState.TypeChanged),
"AD" => (Models.ChangeState.Added, Models.ChangeState.Deleted),
"D." => (Models.ChangeState.Deleted, Models.ChangeState.None),
"R." => (Models.ChangeState.Renamed, Models.ChangeState.None),
"RM" => (Models.ChangeState.Renamed, Models.ChangeState.Modified),
"RT" => (Models.ChangeState.Renamed, Models.ChangeState.TypeChanged),
"RD" => (Models.ChangeState.Renamed, Models.ChangeState.Deleted),
"C." => (Models.ChangeState.Copied, Models.ChangeState.None),
"CM" => (Models.ChangeState.Copied, Models.ChangeState.Modified),
"CT" => (Models.ChangeState.Copied, Models.ChangeState.TypeChanged),
"CD" => (Models.ChangeState.Copied, Models.ChangeState.Deleted),
_ => (Models.ChangeState.None, Models.ChangeState.None),
};
}

private class CapturedProcess
{
public Process Process { get; set; } = null;
Expand Down