diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 7c2e4156a..ebe9869e9 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -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; @@ -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(); } @@ -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) @@ -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 + 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 , followed by 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

+ 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 '?': + // ? + 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;