/// <summary> /// Parse the output of calling git ls-tree /// </summary> /// <param name="line">A line that was output from calling git ls-tree</param> /// <param name="repoRoot">The root path of the repo that the git ls-tree was ran against</param> /// <returns>A DiffTreeResult build from the output line</returns> /// <remarks> /// The call to ls-tree could be any of the following /// git ls-tree (treeish) /// git ls-tree -r (treeish) /// git ls-tree -t (treeish) /// git ls-tree -r -t (treeish) /// </remarks> public static DiffTreeResult ParseFromLsTreeLine(string line, string repoRoot) { if (string.IsNullOrEmpty(line)) { throw new ArgumentException("Line to parse cannot be null or empty", nameof(line)); } /* * Example output lines from ls-tree * * 040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tGVFS * 100644 blob 44c5f5cba4b29d31c2ad06eed51ea02af76c27c0\tReadme.md * 100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts/BuildGVFSForMac.sh * ^-mode ^-marker ^-tab * ^-sha ^-path */ // Everything from ls-tree is an add. int treeIndex = line.IndexOf(TreeMarker); if (treeIndex >= 0) { DiffTreeResult treeAdd = new DiffTreeResult(); treeAdd.TargetIsDirectory = true; treeAdd.TargetPath = AppendPathSeparatorIfNeeded(ConvertPathToAbsoluteUtf8Path(repoRoot, line.Substring(line.LastIndexOf("\t") + 1))); treeAdd.Operation = DiffTreeResult.Operations.Add; return(treeAdd); } else { int blobIndex = line.IndexOf(BlobMarker); if (blobIndex >= 0) { DiffTreeResult blobAdd = new DiffTreeResult(); blobAdd.TargetSha = line.Substring(blobIndex + BlobMarker.Length, GVFSConstants.ShaStringLength); blobAdd.TargetPath = ConvertPathToAbsoluteUtf8Path(repoRoot, line.Substring(line.LastIndexOf("\t") + 1)); blobAdd.Operation = DiffTreeResult.Operations.Add; return(blobAdd); } else { return(null); } } }
public static DiffTreeResult ParseFromDiffTreeLine(string line, string repoRoot) { line = line.Substring(1); // Filenames may contain spaces, but always follow a \t. Other fields are space delimited. string[] parts = line.Split('\t'); parts = parts[0].Split(' ').Concat(parts.Skip(1)).ToArray(); DiffTreeResult result = new DiffTreeResult(); result.SourceIsDirectory = ValidTreeModes.Contains(parts[0]); result.TargetIsDirectory = ValidTreeModes.Contains(parts[1]); result.SourceSha = parts[2]; result.TargetSha = parts[3]; result.Operation = DiffTreeResult.ParseOperation(parts[4]); result.TargetFilename = ConvertPathToAbsoluteUtf8Path(repoRoot, parts.Last()); result.SourceFilename = parts.Length == 7 ? ConvertPathToAbsoluteUtf8Path(repoRoot, parts[5]) : null; return(result); }
private void EnqueueOperationsFromLsTreeLine(string line) { DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(line, this.enlistment.EnlistmentRoot); if (result == null) { this.tracer.RelatedError("Unrecognized ls-tree line: {0}", line); } if (!this.ResultIsInWhitelist(result)) { return; } if (result.TargetIsDirectory) { this.DirectoryOperations.Enqueue(result); } else { this.EnqueueFileAddOperation(result); } }
/// <remarks> /// This is not used in a multithreaded method, it doesn't need to be thread-safe /// </remarks> private void EnqueueFileAddOperation(ITracer activity, DiffTreeResult operation) { // Each filepath should be case-insensitive unique. If there are duplicates, only the last parsed one should remain. if (!this.filesAdded.Add(operation.TargetFilename)) { foreach (KeyValuePair <string, HashSet <string> > kvp in this.FileAddOperations) { if (kvp.Value.Remove(operation.TargetFilename)) { break; } } } if (this.stagedFileDeletes.Remove(operation.TargetFilename)) { EventMetadata metadata = new EventMetadata(); metadata.Add("Filename", operation.TargetFilename); metadata.Add("Message", "A case change was attempted. It will not be reflected in the working directory."); activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); } this.FileAddOperations.AddOrUpdate( operation.TargetSha, new HashSet <string>(StringComparer.OrdinalIgnoreCase) { operation.TargetFilename }, (key, oldValue) => { oldValue.Add(operation.TargetFilename); return(oldValue); }); this.RequiredBlobs.Add(operation.TargetSha); }
private bool ResultIsInWhitelist(DiffTreeResult blobAdd) { return(blobAdd.TargetFilename == null || this.pathWhitelist.Count == 0 || this.pathWhitelist.Any(path => blobAdd.TargetFilename.StartsWith(path, StringComparison.OrdinalIgnoreCase))); }
private void EnqueueOperationsFromDiffTreeLine(ITracer activity, string repoRoot, string line) { if (!line.StartsWith(":")) { // Diff-tree starts with metadata we can ignore. // Real diff lines always start with a colon return; } DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(line, repoRoot); if (!this.ResultIsInWhitelist(result)) { return; } if (result.Operation == DiffTreeResult.Operations.Unknown || result.Operation == DiffTreeResult.Operations.Unmerged) { EventMetadata metadata = new EventMetadata(); metadata.Add("Path", result.TargetFilename); metadata.Add("ErrorMessage", "Unexpected diff operation: " + result.Operation); activity.RelatedError(metadata); this.HasFailures = true; return; } // Separate and enqueue all directory operations first. if (result.SourceIsDirectory || result.TargetIsDirectory) { switch (result.Operation) { case DiffTreeResult.Operations.Delete: if (!this.stagedDirectoryOperations.Add(result)) { EventMetadata metadata = new EventMetadata(); metadata.Add("Filename", result.TargetFilename); metadata.Add("Message", "A case change was attempted. It will not be reflected in the working directory."); activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); } break; case DiffTreeResult.Operations.RenameEdit: if (!this.stagedDirectoryOperations.Add(result)) { // This could happen if a directory was deleted and an existing directory was renamed to replace it, but with a different case. EventMetadata metadata = new EventMetadata(); metadata.Add("Filename", result.TargetFilename); metadata.Add("Message", "A case change was attempted. It will not be reflected in the working directory."); activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); // The target of RenameEdit is always akin to an Add, so replacing the delete is the safer thing to do. this.stagedDirectoryOperations.Remove(result); this.stagedDirectoryOperations.Add(result); } if (!result.TargetIsDirectory) { // Handle when a directory becomes a file. // Files becoming directories is handled by HandleAllDirectoryOperations this.EnqueueFileAddOperation(activity, result); } break; case DiffTreeResult.Operations.Add: case DiffTreeResult.Operations.Modify: case DiffTreeResult.Operations.CopyEdit: if (!this.stagedDirectoryOperations.Add(result)) { EventMetadata metadata = new EventMetadata(); metadata.Add("Filename", result.TargetFilename); metadata.Add("Message", "A case change was attempted. It will not be reflected in the working directory."); activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); // Replace the delete with the add to make sure we don't delete a folder from under ourselves this.stagedDirectoryOperations.Remove(result); this.stagedDirectoryOperations.Add(result); } break; default: activity.RelatedError("Unexpected diff operation from line: {0}", line); break; } } else { switch (result.Operation) { case DiffTreeResult.Operations.Delete: this.EnqueueFileDeleteOperation(activity, result.TargetFilename); break; case DiffTreeResult.Operations.RenameEdit: this.EnqueueFileAddOperation(activity, result); this.EnqueueFileDeleteOperation(activity, result.SourceFilename); break; case DiffTreeResult.Operations.Modify: case DiffTreeResult.Operations.CopyEdit: case DiffTreeResult.Operations.Add: this.EnqueueFileAddOperation(activity, result); break; default: activity.RelatedError("Unexpected diff operation from line: {0}", line); break; } } }
public static DiffTreeResult ParseFromDiffTreeLine(string line) { if (string.IsNullOrEmpty(line)) { throw new ArgumentException("Line to parse cannot be null or empty", nameof(line)); } /* * The lines passed to this method should be the result of a call to git diff-tree -r -t (sourceTreeish) (targetTreeish) * * Example output lines from git diff-tree * :000000 040000 0000000000000000000000000000000000000000 cee82f9d431bf610404f67bcdda3fee76f0c1dd5 A\tGVFS/FastFetch/Git * :000000 100644 0000000000000000000000000000000000000000 cdc036f9d561f14d908e0a0c337105b53c778e5e A\tGVFS/FastFetch/Git/FastFetchGitObjects.cs * :040000 000000 f68b90da732791438d67c0326997a2d26e4c2de4 0000000000000000000000000000000000000000 D\tGVFS/GVFS.CLI * :100644 000000 1242fc97c612ff286a5f1221d569508600ca5e06 0000000000000000000000000000000000000000 D\tGVFS/GVFS.CLI/GVFS.CLI.csproj * :040000 040000 3823348f91113a619eed8f48fe597cc9c7d088d8 fd56ff77b12a0b76567cb55ed4950272eac8b8f6 M\tGVFS/GVFS.Common * :100644 100644 57d9c737c8a48632cfbb12cae00c97d512b9f155 524d7dbcebd33e4007c52711d3f21b17373de454 M\tGVFS/GVFS.Common/GVFS.Common.csproj * ^-[0] ^-[1] ^-[2] ^-[3] ^-[4] * ^-tab * ^-[5] * * This output will only happen if -C or -M is passed to the diff-tree command * Since we are not passing those options we shouldn't have to handle this format. * :100644 100644 3ac7d60a25bb772af1d5843c76e8a070c062dc5d c31a95125b8a6efd401488839a7ed1288ce01634 R094\tGVFS/GVFS.CLI/CommandLine/CloneVerb.cs\tGVFS/GVFS/CommandLine/CloneVerb.cs */ if (!line.StartsWith(":")) { throw new ArgumentException($"diff-tree lines should start with a :", nameof(line)); } // Skip the colon at the front line = line.Substring(1); // Filenames may contain spaces, but always follow a \t. Other fields are space delimited. // Splitting on \t will give us the mode, sha, operation in parts[0] and that path in parts[1] and optionally in paths[2] string[] parts = line.Split(new[] { '\t' }, count: 2); // Take the mode, sha, operation part and split on a space then add the paths that were split on a tab to the end parts = parts[0].Split(' ').Concat(parts.Skip(1)).ToArray(); if (parts.Length != 6 || parts[5].Contains('\t')) { // Look at file history to see how -C -M with 7 parts could be handled throw new ArgumentException($"diff-tree lines should have 6 parts unless passed -C or -M which this method doesn't handle", nameof(line)); } DiffTreeResult result = new DiffTreeResult(); result.SourceIsDirectory = ValidTreeModes.Contains(parts[0]); result.TargetIsDirectory = ValidTreeModes.Contains(parts[1]); result.SourceMode = Convert.ToUInt16(parts[0], 8); result.TargetMode = Convert.ToUInt16(parts[1], 8); if (!result.TargetIsDirectory) { result.TargetIsSymLink = result.TargetMode == SymLinkFileIndexEntry; } result.SourceSha = parts[2]; result.TargetSha = parts[3]; result.Operation = DiffTreeResult.ParseOperation(parts[4]); result.TargetPath = ConvertPathToUtf8Path(parts[5]); if (result.TargetIsDirectory || result.SourceIsDirectory) { // Since diff-tree is not doing rename detection, file->directory or directory->file transformations are always multiple lines // with a delete line and an add line // :000000 040000 0000000000000000000000000000000000000000 cee82f9d431bf610404f67bcdda3fee76f0c1dd5 A\tGVFS/FastFetch/Git // :040000 040000 3823348f91113a619eed8f48fe597cc9c7d088d8 fd56ff77b12a0b76567cb55ed4950272eac8b8f6 M\tGVFS/GVFS.Common // :040000 000000 f68b90da732791438d67c0326997a2d26e4c2de4 0000000000000000000000000000000000000000 D\tGVFS/GVFS.CLI result.TargetPath = AppendPathSeparatorIfNeeded(result.TargetPath); } return(result); }
private void EnqueueOperationsFromDiffTreeLine(ITracer activity, string repoRoot, string line) { if (!line.StartsWith(":")) { // Diff-tree starts with metadata we can ignore. // Real diff lines always start with a colon return; } DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(line, repoRoot); if (!this.ResultIsInWhitelist(result)) { return; } if (result.Operation == DiffTreeResult.Operations.Unknown || result.Operation == DiffTreeResult.Operations.Unmerged) { EventMetadata metadata = new EventMetadata(); metadata.Add("Path", result.TargetFilename); metadata.Add("ErrorMessage", "Unexpected diff operation: " + result.Operation); activity.RelatedError(metadata); this.HasFailures = true; return; } if (result.Operation == DiffTreeResult.Operations.Delete) { // Don't enqueue deletes that will be handled by recursively deleting their parent. // Git traverses diffs in pre-order, so we are guaranteed to ignore child deletes here. // Append trailing slash terminator to avoid matches with directory prefixes (Eg. \GVFS and \GVFS.Common) string pathWithSlash = result.TargetFilename + "\\"; if (this.deletedPaths.Any(path => pathWithSlash.StartsWith(path, StringComparison.OrdinalIgnoreCase))) { if (result.SourceIsDirectory || result.TargetIsDirectory) { Interlocked.Increment(ref this.additionalDirDeletes); } else { Interlocked.Increment(ref this.additionalFileDeletes); } return; } this.deletedPaths.Add(pathWithSlash); } // Separate and enqueue all directory operations first. if (result.SourceIsDirectory || result.TargetIsDirectory) { // Handle when a directory becomes a file. // Files becoming directories is handled by HandleAllDirectoryOperations if (result.Operation == DiffTreeResult.Operations.RenameEdit && !result.TargetIsDirectory) { this.EnqueueFileAddOperation(result); } this.DirectoryOperations.Enqueue(result); } else { switch (result.Operation) { case DiffTreeResult.Operations.Delete: this.FileDeleteOperations.Enqueue(result.TargetFilename); break; case DiffTreeResult.Operations.RenameEdit: this.FileDeleteOperations.Enqueue(result.SourceFilename); this.EnqueueFileAddOperation(result); break; case DiffTreeResult.Operations.Modify: case DiffTreeResult.Operations.CopyEdit: case DiffTreeResult.Operations.Add: this.EnqueueFileAddOperation(result); break; default: activity.RelatedError("Unexpected diff operation from line: {0}", line); break; } } }