From ce4ee16d921cd2ea5e6170f1b8ba74c6ed8858df Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Mon, 10 Jun 2019 09:55:03 -0700 Subject: [PATCH 01/16] Managed implementation of git repo data reading --- src/Directory.Build.props | 2 +- .../GitOperations.cs | 14 +- .../Managed/CharUtils.cs | 14 + .../Managed/GitConfig.Reader.cs | 593 ++++++++++++++++++ .../Managed/GitConfig.cs | 173 +++++ .../Managed/GitEnvironment.cs | 112 ++++ .../Managed/GitIgnore.Matcher.cs | 235 +++++++ .../Managed/GitIgnore.cs | 250 ++++++++ .../Managed/GitRepository.cs | 474 ++++++++++++++ .../Managed/GitSubmodule.cs | 27 + .../Managed/Glob.cs | 251 ++++++++ .../Managed/PathUtils.cs | 98 +++ .../RepositoryTasks.cs | 6 +- .../GitConfigTests.cs | 408 ++++++++++++ .../GitDataTests.cs | 12 + .../GitIgnoreTests.cs | 210 +++++++ .../GitRepositoryTests.cs | 284 +++++++++ .../GlobTests.cs | 187 ++++++ 18 files changed, 3337 insertions(+), 13 deletions(-) create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/CharUtils.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.Reader.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/GitEnvironment.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.Matcher.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/GitRepository.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/GitSubmodule.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/Glob.cs create mode 100644 src/Microsoft.Build.Tasks.Git.Operations/Managed/PathUtils.cs create mode 100644 src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs create mode 100644 src/Microsoft.Build.Tasks.Git.UnitTests/GitIgnoreTests.cs create mode 100644 src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs create mode 100644 src/Microsoft.Build.Tasks.Git.UnitTests/GlobTests.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 58960623..04ab4914 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ - Latest + Preview $(CopyrightMicrosoft) Apache-2.0 true diff --git a/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs b/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs index d32bcbc2..2a692f80 100644 --- a/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs +++ b/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs @@ -26,8 +26,13 @@ public static string LocateRepository(string directory) return Repository.Discover(directory); } + internal static IRepository CreateRepository(string root) + => new Repository(root); + public static string GetRepositoryUrl(IRepository repository, Action logWarning = null, string remoteName = null) { + // GetVariableValue("remote", name, "url"); + var remotes = repository.Network.Remotes; var remote = string.IsNullOrEmpty(remoteName) ? (remotes["origin"] ?? remotes.FirstOrDefault()) : remotes[remoteName]; if (remote == null) @@ -179,15 +184,6 @@ public static ITaskItem[] GetSourceRoots(IRepository repository, Action is the URL of the new submodule's origin repository. This may be either an absolute URL, or (if it begins with ./ or ../), - // the location relative to the superproject's default remote repository (Please note that to specify a repository foo.git which is located - // right next to a superproject bar.git, you'll have to use ../foo.git instead of ./foo.git - as one might expect when following the rules - // for relative URLs -- because the evaluation of relative URLs in Git is identical to that of relative directories). - // - // The given URL is recorded into .gitmodules for use by subsequent users cloning the superproject. - // If the URL is given relative to the superproject's repository, the presumption is the superproject and submodule repositories - // will be kept together in the same relative location, and only the superproject's URL needs to be provided.git -- - // submodule will correctly locate the submodule using the relative URL in .gitmodules. var submoduleUrl = NormalizeUrl(submodule.Url, repoRoot); if (submoduleUrl == null) { diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/CharUtils.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/CharUtils.cs new file mode 100644 index 00000000..57cc9941 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/CharUtils.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Build.Tasks.Git +{ + internal static class CharUtils + { + public static char[] AsciiWhitespace = { ' ', '\t', '\n', '\f', '\r', '\v' }; + + public static bool IsHexadecimalDigit(char c) + => c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f'; + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.Reader.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.Reader.cs new file mode 100644 index 00000000..669b1032 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.Reader.cs @@ -0,0 +1,593 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Microsoft.Build.Tasks.Git +{ + partial class GitConfig + { + internal class Reader + { + private const int MaxIncludeDepth = 10; + + // reused for parsing names + private readonly StringBuilder _reusableBuffer = new StringBuilder(); + + // slash terminated posix path + private readonly string _gitDirectoryPosix; + + private readonly string _commonDirectory; + private readonly Func _fileOpener; + private readonly GitEnvironment _environment; + + public Reader(string gitDirectory, string commonDirectory, GitEnvironment environment, Func fileOpener = null) + { + Debug.Assert(environment != null); + + _environment = environment; + _gitDirectoryPosix = PathUtils.ToPosixDirectoryPath(gitDirectory); + _commonDirectory = commonDirectory; + _fileOpener = fileOpener ?? File.OpenText; + } + + /// + /// + internal GitConfig Load() + { + var variables = new Dictionary>(); + + foreach (var path in EnumerateExistingConfigurationFiles()) + { + LoadVariablesFrom(path, variables, includeDepth: 0); + } + + return new GitConfig(variables.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray())); + } + + /// + /// + internal GitConfig LoadFrom(string path) + { + var variables = new Dictionary>(); + LoadVariablesFrom(path, variables, includeDepth: 0); + return new GitConfig(variables.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray())); + } + + private string GetXdgDirectory() + { + var xdgConfigHome = _environment.XdgConfigHomeDirectory; + if (xdgConfigHome != null) + { + return Path.Combine(xdgConfigHome, "git"); + } + else + { + return Path.Combine(_environment.HomeDirectory, ".config", "git"); + } + } + + internal IEnumerable EnumerateExistingConfigurationFiles() + { + // program data (Windows only) + if (_environment.ProgramDataDirectory != null) + { + var programDataConfig = Path.Combine(_environment.ProgramDataDirectory, "git", "config"); + if (File.Exists(programDataConfig)) + { + yield return programDataConfig; + } + } + + // system + var systemDir = _environment.SystemDirectory; + if (systemDir != null) + { + var systemConfig = Path.Combine(systemDir, "gitconfig"); + if (systemConfig != null) + { + yield return systemConfig; + } + } + + // XDG + var xdgConfig = Path.Combine(GetXdgDirectory(), "config"); + if (File.Exists(xdgConfig)) + { + yield return xdgConfig; + } + + // global (user home) + var globalConfig = Path.Combine(_environment.HomeDirectory, ".gitconfig"); + if (File.Exists(globalConfig)) + { + yield return globalConfig; + } + + // local + var localConfig = Path.Combine(_commonDirectory, "config"); + if (File.Exists(localConfig)) + { + yield return localConfig; + } + + // TODO: worktree config + } + + /// + /// + internal void LoadVariablesFrom(string path, Dictionary> variables, int includeDepth) + { + // https://git-scm.com/docs/git-config#_syntax + + // The following is allowed: + // [section][section]var = x + // [section]#[section] + + if (includeDepth > MaxIncludeDepth) + { + throw new InvalidDataException($"Configuration files recursion exceeded maximum allowed depth of {MaxIncludeDepth}"); + } + + TextReader reader; + + try + { + reader = _fileOpener(path); + } + catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException) + { + return; + } + catch (Exception e) when (!(e is IOException)) + { + throw new IOException(e.Message, e); + } + + using (reader) + { + string sectionName = ""; + string subsectionName = ""; + + while (true) + { + SkipMultilineWhitespace(reader); + + int c = reader.Peek(); + if (c == -1) + { + break; + } + + // Comment to the end of the line: + if (IsCommentStart(c)) + { + ReadToLineEnd(reader); + continue; + } + + if (c == '[') + { + ReadSectionHeader(reader, _reusableBuffer, out sectionName, out subsectionName); + continue; + } + + ReadVariableDeclaration(reader, _reusableBuffer, out var variableName, out var variableValue); + + // Variable declared outside of a section is allowed (has no section name prefix). + + var key = new VariableKey(sectionName, subsectionName, variableName); + if (!variables.TryGetValue(key, out var values)) + { + variables.Add(key, values = new List()); + } + + values.Add(variableValue); + + // Spec https://git-scm.com/docs/git-config#_includes: + if (IsIncludePath(key, path)) + { + string includedConfigPath = NormalizeRelativePath(relativePath: variableValue, basePath: path); + LoadVariablesFrom(includedConfigPath, variables, includeDepth + 1); + } + } + } + } + + /// + private string NormalizeRelativePath(string relativePath, string basePath) + { + string root; + if (relativePath.Length >= 2 && relativePath[0] == '~' && PathUtils.IsDirectorySeparator(relativePath[1])) + { + root = _environment.HomeDirectory; + relativePath = relativePath.Substring(2); + } + else + { + root = Path.GetDirectoryName(basePath); + } + + try + { + return Path.GetFullPath(Path.Combine(root, relativePath)); + } + catch + { + throw new InvalidDataException($"Invalid path: {relativePath}"); + } + } + + private bool IsIncludePath(VariableKey key, string configFilePath) + { + // unconditional: + if (key.Equals(new VariableKey("include", "", "path"))) + { + return true; + } + + // conditional: + if (VariableKey.SectionNameComparer.Equals(key.SectionName, "includeIf") && + VariableKey.VariableNameComparer.Equals(key.VariableName, "path") && + key.SubsectionName != "") + { + bool ignoreCase; + string pattern; + + const string caseSensitiveGitDirPrefix = "gitdir:"; + const string caseInsensitiveGitDirPrefix = "gitdir/i:"; + + if (key.SubsectionName.StartsWith(caseSensitiveGitDirPrefix, StringComparison.Ordinal)) + { + pattern = key.SubsectionName.Substring(caseSensitiveGitDirPrefix.Length); + ignoreCase = false; + } + else if (key.SubsectionName.StartsWith(caseInsensitiveGitDirPrefix, StringComparison.Ordinal)) + { + pattern = key.SubsectionName.Substring(caseInsensitiveGitDirPrefix.Length); + ignoreCase = true; + } + else + { + return false; + } + + if (pattern.Length >= 2 && (pattern[0] == '.' || pattern[0] == '~') && PathUtils.IsDirectorySeparator(pattern[1])) + { + // leading './' is substituted with the path to the directory containing the current config file. + // leading '~/' is substituted with HOME path + var root = (pattern[0] == '.') ? Path.GetDirectoryName(configFilePath) : _environment.HomeDirectory; + + pattern = PathUtils.CombinePosixPaths(PathUtils.ToPosixPath(root), pattern.Substring(2)); + } + else if (!PathUtils.IsAbsolute(pattern)) + { + pattern = "**/" + pattern; + } + + if (PathUtils.IsDirectorySeparator(pattern[pattern.Length - 1])) + { + pattern += "**"; + } + + return Glob.IsMatch(pattern, _gitDirectoryPosix, ignoreCase, matchWildCardWithDirectorySeparator: true); + } + + return false; + } + + // internal for testing + internal static void ReadSectionHeader(TextReader reader, StringBuilder reusableBuffer, out string name, out string subsectionName) + { + var nameBuilder = reusableBuffer.Clear(); + + int c = reader.Read(); + Debug.Assert(c == '['); + + while (true) + { + c = reader.Read(); + if (c == ']') + { + name = nameBuilder.ToString(); + subsectionName = ""; + break; + } + + if (IsWhitespace(c)) + { + name = nameBuilder.ToString(); + subsectionName = ReadSubsectionName(reader, reusableBuffer); + + c = reader.Read(); + if (c != ']') + { + throw new InvalidDataException(); + } + + break; + } + + if (IsAlphaNumeric(c) || c == '-' || c == '.') + { + // Allowed characters: alpha-numeric, '-', '.'; no restriction on the name start character. + nameBuilder.Append((char)c); + } + else + { + throw new InvalidDataException(); + } + } + + name = name.ToLowerInvariant(); + + // Deprecated syntax: [section.subsection] + int firstDot = name.IndexOf('.'); + if (firstDot != -1) + { + // "[.x]" parses to section "", subsection ".x" (lookup ".x.var" suceeds, ".X.var" fails) + // "[..x]" parses to section ".", subsection "x" (lookup "..x.var" suceeds, "..X.var" fails) + // "[x.]" parses to section "x.", subsection "" (lookups "X..var" and "x..var" suceed) + // "[x..]" parses to section "x", subsection "." (lookups "X...var" and "x...var" suceed) + + var prefix = (firstDot == name.Length - 1) ? name : name.Substring(0, firstDot); + var suffix = name.Substring(firstDot + 1); + + subsectionName = (subsectionName.Length > 0) ? suffix + "." + subsectionName : suffix; + name = prefix; + } + } + + private static string ReadSubsectionName(TextReader reader, StringBuilder reusableBuffer) + { + SkipWhitespace(reader); + + int c = reader.Read(); + if (c != '"') + { + throw new InvalidDataException(); + } + + var subsectionName = reusableBuffer.Clear(); + while (true) + { + c = reader.Read(); + if (c <= 0) + { + throw new InvalidDataException(); + } + + if (c == '"') + { + return subsectionName.ToString(); + } + + // Escaping: backslashes are skipped. + // Section headers can't span multiple lines. + if (c == '\\') + { + c = reader.Read(); + if (c <= 0) + { + throw new InvalidDataException(); + } + } + + subsectionName.Append((char)c); + } + } + + // internal for testing + internal static void ReadVariableDeclaration(TextReader reader, StringBuilder reusableBuffer, out string name, out string value) + { + name = ReadVariableName(reader, reusableBuffer); + if (name.Length == 0) + { + throw new InvalidDataException(); + } + + SkipWhitespace(reader); + + // Not allowed: + // name # + // = value + + int c = reader.Peek(); + if (c == -1 || IsCommentStart(c) || IsEndOfLine(c)) + { + ReadToLineEnd(reader); + + // If the value is not specified the variable is considered of type Boolean with value "true" + value = "true"; + return; + } + + if (c != '=') + { + throw new InvalidDataException(); + } + + reader.Read(); + + SkipWhitespace(reader); + + value = ReadVariableValue(reader, reusableBuffer); + } + + private static string ReadVariableName(TextReader reader, StringBuilder reusableBuffer) + { + var nameBuilder = reusableBuffer.Clear(); + int c; + + // Allowed characters: alpha-numeric, '-'; starts with alphabetic. + while (IsAlphabetic(c = reader.Peek()) || (c == '-' || IsNumeric(c)) && nameBuilder.Length > 0) + { + nameBuilder.Append((char)c); + reader.Read(); + } + + return nameBuilder.ToString().ToLowerInvariant(); + } + + private static string ReadVariableValue(TextReader reader, StringBuilder reusableBuffer) + { + // Allowed: + // name = "a"x"b" `axb` + // name = "b"#"a" `b` + // name = \ + // abc `abc` + // name = "a\ + // bc" `a bc` + // name = a\ + // bc `abc` + // name = a\ + // bc `a bc` + + // read until comment/eoln, quote + bool inQuotes = false; + var builder = reusableBuffer.Clear(); + int lengthIgnoringTrailingWhitespace = 0; + + while (true) + { + int c = reader.Read(); + if (c == -1 || IsEndOfLine(c)) + { + if (inQuotes) + { + throw new InvalidDataException(); + } + + break; + } + + if (c == '\\') + { + switch (reader.Peek()) + { + case '\r': + case '\n': + ReadToLineEnd(reader); + continue; + + case 'n': + reader.Read(); + builder.Append('\n'); + + // escaped \n is not considered trailing whitespace: + lengthIgnoringTrailingWhitespace = builder.Length; + continue; + + case 't': + reader.Read(); + builder.Append('\t'); + + // escaped \t is not considered trailing whitespace: + lengthIgnoringTrailingWhitespace = builder.Length; + continue; + + case '\\': + case '"': + builder.Append((char)reader.Read()); + lengthIgnoringTrailingWhitespace = builder.Length; + continue; + + default: + throw new InvalidDataException(); + } + } + + if (c == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (IsCommentStart(c) && !inQuotes) + { + ReadToLineEnd(reader); + break; + } + + builder.Append((char)c); + + if (!IsWhitespace(c) || inQuotes) + { + lengthIgnoringTrailingWhitespace = builder.Length; + } + } + + return builder.ToString(0, lengthIgnoringTrailingWhitespace); + } + + private static void SkipMultilineWhitespace(TextReader reader) + { + while (IsWhitespaceOrEndOfLine(reader.Peek())) + { + reader.Read(); + } + } + + private static void SkipWhitespace(TextReader reader) + { + while (IsWhitespace(reader.Peek())) + { + reader.Read(); + } + } + + private static void ReadToLineEnd(TextReader reader) + { + while (true) + { + int c = reader.Read(); + if (c == -1) + { + return; + } + + if (c == '\r') + { + if (reader.Peek() == '\n') + { + reader.Read(); + return; + } + + return; + } + + if (c == '\n') + { + return; + } + } + } + + private static bool IsCommentStart(int c) + => c == ';' || c == '#'; + + private static bool IsAlphabetic(int c) + => c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; + + private static bool IsNumeric(int c) + => c >= '0' && c <= '9'; + + private static bool IsAlphaNumeric(int c) + => IsAlphabetic(c) || IsNumeric(c); + + private static bool IsWhitespace(int c) + => c == ' ' || c == '\t' || c == '\f' || c == '\v'; + + private static bool IsEndOfLine(int c) + => c == '\r' || c == '\n'; + + private static bool IsWhitespaceOrEndOfLine(int c) + => IsWhitespace(c) || IsEndOfLine(c); + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.cs new file mode 100644 index 00000000..f6b9e8d2 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Microsoft.Build.Tasks.Git +{ + internal sealed partial class GitConfig + { + public static readonly GitConfig Empty = new GitConfig(ImmutableDictionary>.Empty); + + internal readonly struct VariableKey : IEquatable + { + public static readonly StringComparer SectionNameComparer = StringComparer.OrdinalIgnoreCase; + public static readonly StringComparer SubsectionNameComparer = StringComparer.Ordinal; + public static readonly StringComparer VariableNameComparer = StringComparer.OrdinalIgnoreCase; + + public readonly string SectionName; + public readonly string SubsectionName; + public readonly string VariableName; + + public VariableKey(string sectionName, string subsectionName, string variableName) + { + Debug.Assert(sectionName != null); + Debug.Assert(subsectionName != null); + Debug.Assert(variableName != null); + + SectionName = sectionName; + SubsectionName = subsectionName; + VariableName = variableName; + } + + public bool SectionNameEquals(string name) + => SectionNameComparer.Equals(SectionName, name); + + public bool SubsectionNameEquals(string name) + => SubsectionNameComparer.Equals(SubsectionName, name); + + public bool VariableNameEquals(string name) + => VariableNameComparer.Equals(VariableName, name); + + public bool Equals(VariableKey other) + => SectionNameEquals(other.SectionName) && + SubsectionNameEquals(other.SubsectionName) && + VariableNameEquals(other.VariableName); + + public override bool Equals(object obj) + => obj is VariableKey other && Equals(other); + + public override int GetHashCode() + => SectionName.GetHashCode() ^ SubsectionName.GetHashCode() ^ VariableName.GetHashCode(); + + public override string ToString() + => (SubsectionName.Length == 0) ? + SectionName + "." + VariableName : + SectionName + "." + SubsectionName + "." + VariableName; + } + + public readonly ImmutableDictionary> Variables; + + public GitConfig(ImmutableDictionary> variables) + { + Debug.Assert(variables != null); + Variables = variables; + } + + // for testing: + internal IEnumerable>> EnumerateVariables() + => Variables.Select(kvp => new KeyValuePair>(kvp.Key.ToString(), kvp.Value)); + + public ImmutableArray GetVariableValues(string section, string name) + => GetVariableValues(section, subsection: "", name); + + public ImmutableArray GetVariableValues(string section, string subsection, string name) + => Variables.TryGetValue(new VariableKey(section, subsection, name), out var multiValue) ? multiValue : default; + + public string GetVariableValue(string section, string name) + => GetVariableValue(section, "", name); + + public string GetVariableValue(string section, string subsection, string name) + { + var values = GetVariableValues(section, subsection, name); + return values.IsDefault ? null : values[values.Length - 1]; + } + + public static bool ParseBooleanValue(string str, bool defaultValue = false) + => TryParseBooleanValue(str, out var value) ? value : defaultValue; + + public static bool TryParseBooleanValue(string str, out bool value) + { + // https://git-scm.com/docs/git-config#Documentation/git-config.txt-boolean + + if (str == null) + { + value = false; + return false; + } + + var comparer = StringComparer.OrdinalIgnoreCase; + + if (str == "1" || comparer.Equals(str, "true") || comparer.Equals(str, "on") || comparer.Equals(str, "yes")) + { + value = true; + return true; + } + + if (str == "0" || comparer.Equals(str, "false") || comparer.Equals(str, "off") || comparer.Equals(str, "no") || str == "") + { + value = false; + return true; + } + + value = false; + return false; + } + + internal static long ParseInt64Value(string str, long defaultValue = 0) + => TryParseInt64Value(str, out var value) ? value : defaultValue; + + internal static bool TryParseInt64Value(string str, out long value) + { + if (string.IsNullOrEmpty(str)) + { + value = 0; + return false; + } + + long multiplier; + switch (str[str.Length - 1]) + { + case 'K': + case 'k': + multiplier = 1024; + break; + + case 'M': + case 'm': + multiplier = 1024 * 1024; + break; + + case 'G': + case 'g': + multiplier = 1024 * 1024 * 1024; + break; + + default: + multiplier = 1; + break; + } + + if (!long.TryParse(multiplier > 1 ? str.Substring(0, str.Length - 1) : str, out value)) + { + return false; + } + + try + { + value = checked(value * multiplier); + } + catch (OverflowException) + { + return false; + } + + return true; + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitEnvironment.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitEnvironment.cs new file mode 100644 index 00000000..4fd0d3b5 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitEnvironment.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Microsoft.Build.Tasks.Git +{ + internal sealed class GitEnvironment + { + public string HomeDirectory { get; } + public string XdgConfigHomeDirectory { get; } + public string ProgramDataDirectory { get; } + public string SystemDirectory { get; } + + // TODO: consider adding environment variables: GIT_DIR, GIT_DISCOVERY_ACROSS_FILESYSTEM, GIT_CEILING_DIRECTORIES + // https://git-scm.com/docs/git#Documentation/git.txt-codeGITDIRcode + // https://git-scm.com/docs/git#Documentation/git.txt-codeGITCEILINGDIRECTORIEScode + // https://git-scm.com/docs/git#Documentation/git.txt-codeGITDISCOVERYACROSSFILESYSTEMcode + // + // if GIT_COMMON_DIR is set config worktree is ignored + // https://git-scm.com/docs/git#Documentation/git.txt-codeGITCOMMONDIRcode + // + // GIT_WORK_TREE overrides all other work tree settings: + // https://git-scm.com/docs/git#Documentation/git.txt-codeGITWORKTREEcode + + public GitEnvironment( + string homeDirectory, + string xdgConfigHomeDirectory = null, + string programDataDirectory = null, + string systemDirectory = null) + { + Debug.Assert(!string.IsNullOrEmpty(homeDirectory)); + + HomeDirectory = homeDirectory; + + if (!string.IsNullOrWhiteSpace(xdgConfigHomeDirectory)) + { + XdgConfigHomeDirectory = xdgConfigHomeDirectory; + } + + if (!string.IsNullOrWhiteSpace(programDataDirectory)) + { + ProgramDataDirectory = programDataDirectory; + } + + if (!string.IsNullOrWhiteSpace(systemDirectory)) + { + SystemDirectory = systemDirectory; + } + } + + public static GitEnvironment CreateFromProcessEnvironment() + { + var systemDir = PathUtils.IsUnixLikePlatform ? "/etc" : + Path.Combine(FindWindowsGitInstallation(), "mingw64", "etc"); + + return new GitEnvironment( + homeDirectory: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify), + xdgConfigHomeDirectory: Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"), + programDataDirectory: Environment.GetEnvironmentVariable("PROGRAMDATA"), + systemDirectory: systemDir); + } + + public static string FindWindowsGitInstallation() + { + Debug.Assert(!PathUtils.IsUnixLikePlatform); + + string[] paths; + try + { + paths = Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator); + } + catch + { + paths = Array.Empty(); + } + + var gitExe = paths.FirstOrDefault(dir => File.Exists(Path.Combine(dir, "git.exe"))); + if (gitExe != null) + { + return Path.GetDirectoryName(gitExe); + } + + var gitCmd = paths.FirstOrDefault(dir => File.Exists(Path.Combine(dir, "git.cmd"))); + if (gitCmd != null) + { + return Path.GetDirectoryName(gitCmd); + } + +#if REGISTRY + string registryInstallLocation; + try + { + using var regKey = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1"); + registryInstallLocation = regKey?.GetValue("InstallLocation") as string; + } + catch + { + registryInstallLocation = null; + } + + if (registryInstallLocation != null) + { + yield return Path.Combine(registryInstallLocation, subdirectory); + } +#endif + return null; + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.Matcher.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.Matcher.cs new file mode 100644 index 00000000..f17e3283 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.Matcher.cs @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.Build.Tasks.Git +{ + partial class GitIgnore + { + internal sealed class Matcher + { + public GitIgnore Ignore { get; } + + /// + /// Maps full posix slash-terminated directory name to a pattern group. + /// + private readonly Dictionary _patternGroups; + + /// + /// The result of "is ignored" for directories. + /// + private readonly Dictionary _directoryIgnoreStateCache; + + private readonly List _reusableGroupList; + + internal Matcher(GitIgnore ignore) + { + Ignore = ignore; + _patternGroups = new Dictionary(); + _directoryIgnoreStateCache = new Dictionary(Ignore.PathComparer); + _reusableGroupList = new List(); + } + + // test only: + internal IReadOnlyDictionary DirectoryIgnoreStateCache + => _directoryIgnoreStateCache; + + private PatternGroup GetPatternGroup(string directory) + { + if (_patternGroups.TryGetValue(directory, out var group)) + { + return group; + } + + PatternGroup parent; + if (directory.Equals(Ignore.WorkingDirectory, Ignore.PathComparison)) + { + parent = Ignore.Root; + } + else + { + parent = GetPatternGroup(PathUtils.ToPosixDirectoryPath(Path.GetDirectoryName(PathUtils.TrimTrailingSlash(directory)))); + } + + group = LoadFromFile(Path.Combine(directory, GitIgnoreFileName), parent) ?? parent; + + _patternGroups.Add(directory, group); + return group; + } + + /// + /// Checks if the specified file path is ignored. + /// + /// Normalized path. + /// True if the path is ignored, fale if it is not, null if it is outside of the working directory. + public bool? IsNormalizedFilePathIgnored(string fullPath) + { + if (!PathUtils.IsAbsolute(fullPath)) + { + throw new ArgumentException("Path must be absolute", nameof(fullPath)); + } + + if (PathUtils.HasTrailingDirectorySeparator(fullPath)) + { + throw new ArgumentException("Path must be a file path", nameof(fullPath)); + } + + return IsPathIgnored(PathUtils.ToPosixPath(fullPath), isDirectoryPath: false); + } + + /// + /// Checks if the specified path is ignored. + /// + /// Full path. + /// True if the path is ignored, fale if it is not, null if it is outside of the working directory. + public bool? IsPathIgnored(string fullPath) + { + if (!PathUtils.IsAbsolute(fullPath)) + { + throw new ArgumentException("Path must be absolute", nameof(fullPath)); + } + + // git uses the FS case-sensitivity for checking directory existence: + bool isDirectoryPath = PathUtils.HasTrailingDirectorySeparator(fullPath) || Directory.Exists(fullPath); + + var fullPathNoSlash = PathUtils.TrimTrailingSlash(PathUtils.ToPosixPath(Path.GetFullPath(fullPath))); + if (isDirectoryPath && fullPathNoSlash.Equals(Ignore._workingDirectoryNoSlash, Ignore.PathComparison)) + { + return false; + } + + return IsPathIgnored(fullPathNoSlash, isDirectoryPath); + } + + private bool? IsPathIgnored(string normalizedPosixPath, bool isDirectoryPath) + { + Debug.Assert(PathUtils.IsAbsolute(normalizedPosixPath)); + Debug.Assert(PathUtils.IsPosixPath(normalizedPosixPath)); + Debug.Assert(!PathUtils.HasTrailingSlash(normalizedPosixPath)); + + // paths outside of working directory: + if (!normalizedPosixPath.StartsWith(Ignore.WorkingDirectory, Ignore.PathComparison)) + { + return null; + } + + if (isDirectoryPath && _directoryIgnoreStateCache.TryGetValue(normalizedPosixPath, out var isIgnored)) + { + return isIgnored; + } + + isIgnored = IsIgnoredRecursive(normalizedPosixPath, isDirectoryPath); + if (isDirectoryPath) + { + _directoryIgnoreStateCache.Add(normalizedPosixPath, isIgnored); + } + + return isIgnored; + } + + private bool IsIgnoredRecursive(string normalizedPosixPath, bool isDirectoryPath) + { + SplitPath(normalizedPosixPath, out var directory, out var fileName); + if (directory == null || !directory.StartsWith(Ignore.WorkingDirectory, Ignore.PathComparison)) + { + return false; + } + + var isIgnored = IsIgnored(normalizedPosixPath, directory, fileName, isDirectoryPath); + if (isIgnored) + { + return true; + } + + // The target file/directory itself is not ignored, but its containing directory might be. + normalizedPosixPath = PathUtils.TrimTrailingSlash(directory); + if (_directoryIgnoreStateCache.TryGetValue(normalizedPosixPath, out isIgnored)) + { + return isIgnored; + } + + isIgnored = IsIgnoredRecursive(normalizedPosixPath, isDirectoryPath: true); + _directoryIgnoreStateCache.Add(normalizedPosixPath, isIgnored); + return isIgnored; + } + + private static void SplitPath(string fullPath, out string directoryWithSlash, out string fileName) + { + Debug.Assert(!PathUtils.HasTrailingSlash(fullPath)); + int i = fullPath.LastIndexOf('/'); + if (i < 0) + { + directoryWithSlash = null; + fileName = fullPath; + } + else + { + directoryWithSlash = fullPath.Substring(0, i + 1); + fileName = fullPath.Substring(i + 1); + } + } + + private bool IsIgnored(string normalizedPosixPath, string directory, string fileName, bool isDirectoryPath) + { + // Default patterns can't be overriden by a negative pattern: + if (fileName.Equals(".git", Ignore.PathComparison)) + { + return true; + } + + bool isIgnored = false; + + // Visit groups in reverse order. + // Patterns specified closer to the target file override those specified above. + _reusableGroupList.Clear(); + var groups = _reusableGroupList; + for (var patternGroup = GetPatternGroup(directory); patternGroup != null; patternGroup = patternGroup.Parent) + { + groups.Add(patternGroup); + } + + for (int i = groups.Count - 1; i >= 0; i--) + { + var patternGroup = groups[i]; + + if (!normalizedPosixPath.StartsWith(patternGroup.ContainingDirectory, Ignore.PathComparison)) + { + continue; + } + + string lazyRelativePath = null; + + foreach (var pattern in patternGroup.Patterns) + { + // If a pattern is matched as ignored only look for a negative pattern that matches as well. + // If a pattern is not matched then skip negative patterns. + if (isIgnored != pattern.IsNegative) + { + continue; + } + + if (pattern.IsDirectoryPattern && !isDirectoryPath) + { + continue; + } + + string matchPath = pattern.IsFullPathPattern ? + lazyRelativePath ??= normalizedPosixPath.Substring(patternGroup.ContainingDirectory.Length) : + fileName; + + if (Glob.IsMatch(pattern.Glob, matchPath, Ignore.IgnoreCase, matchWildCardWithDirectorySeparator: false)) + { + // TODO: optimize negative pattern lookup (once we match, do we need to continue matching?) + isIgnored = !pattern.IsNegative; + } + } + } + + return isIgnored; + } + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.cs new file mode 100644 index 00000000..9008c0bc --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.cs @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Microsoft.Build.Tasks.Git +{ + internal sealed partial class GitIgnore + { + internal sealed class PatternGroup + { + /// + /// Directory of the .gitignore file that defines the pattern. + /// Full posix slash terminated path. + /// + public readonly string ContainingDirectory; + + public readonly ImmutableArray Patterns; + + public readonly PatternGroup Parent; + + public PatternGroup(PatternGroup parent, string containingDirectory, ImmutableArray patterns) + { + Debug.Assert(PathUtils.IsPosixPath(containingDirectory)); + Debug.Assert(PathUtils.HasTrailingSlash(containingDirectory)); + + Parent = parent; + ContainingDirectory = containingDirectory; + Patterns = patterns; + } + } + + internal readonly struct Pattern + { + public readonly PatternFlags Flags; + public readonly string Glob; + + public Pattern(string glob, PatternFlags flags) + { + Debug.Assert(glob != null); + + Glob = glob; + Flags = flags; + } + + public bool IsDirectoryPattern => (Flags & PatternFlags.DirectoryPattern) != 0; + public bool IsFullPathPattern => (Flags & PatternFlags.FullPath) != 0; + public bool IsNegative => (Flags & PatternFlags.Negative) != 0; + + public override string ToString() + => $"{(IsNegative ? "!" : "")}{Glob}{(IsDirectoryPattern ? " " : "")}{(IsFullPathPattern ? " " : "")}"; + } + + [Flags] + internal enum PatternFlags + { + None = 0, + Negative = 1, + DirectoryPattern = 2, + FullPath = 4, + } + + private const string GitIgnoreFileName = ".gitignore"; + + /// + /// Full posix slash terminated path. + /// + public string WorkingDirectory { get; } + private readonly string _workingDirectoryNoSlash; + + public bool IgnoreCase { get; } + + public PatternGroup Root { get; } + + internal GitIgnore(PatternGroup root, string workingDirectory, bool ignoreCase) + { + Debug.Assert(PathUtils.IsPosixPath(workingDirectory)); + Debug.Assert(PathUtils.HasTrailingSlash(workingDirectory)); + + IgnoreCase = ignoreCase; + WorkingDirectory = workingDirectory; + _workingDirectoryNoSlash = PathUtils.TrimTrailingSlash(workingDirectory); + Root = root; + } + + private StringComparison PathComparison + => IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + + private IEqualityComparer PathComparer + => IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; + + public Matcher CreateMatcher() + => new Matcher(this); + + /// + /// is invalid + internal static PatternGroup LoadFromFile(string path, PatternGroup parent) + { + // See https://git-scm.com/docs/gitignore#_pattern_format + + if (!File.Exists(path)) + { + return null; + } + + StreamReader reader; + try + { + reader = File.OpenText(path); + } + catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException) + { + return null; + } + + var reusableBuffer = new StringBuilder(); + + var directory = PathUtils.ToPosixDirectoryPath(Path.GetFullPath(Path.GetDirectoryName(path))); + var patterns = ImmutableArray.CreateBuilder(); + + using (reader) + { + while (true) + { + string line = reader.ReadLine(); + if (line == null) + { + break; + } + + if (TryParsePattern(line, reusableBuffer, out var glob, out var flags)) + { + patterns.Add(new Pattern(glob, flags)); + } + } + } + + if (patterns.Count == 0) + { + return null; + } + + return new PatternGroup(parent, directory, patterns.ToImmutable()); + } + + internal static bool TryParsePattern(string line, StringBuilder reusableBuffer, out string glob, out PatternFlags flags) + { + glob = null; + flags = PatternFlags.None; + + // Trailing spaces are ignored unless '\'-escaped. + // Leading spaces are significant. + // Other whitespace (\t, \v, \f) is significant. + int e = line.Length - 1; + while (e >= 0 && line[e] == ' ') + { + e--; + } + + e++; + + // Skip blank line. + if (e == 0) + { + return false; + } + + // put trailing space back if escaped: + if (e < line.Length && line[e] == ' ' && line[e - 1] == '\\') + { + e++; + } + + int s = 0; + + // Skip comment. + if (line[s] == '#') + { + return false; + } + + // Pattern negation. + if (line[s] == '!') + { + flags |= PatternFlags.Negative; + s++; + } + + if (s == e) + { + return false; + } + + if (line[e - 1] == '/') + { + flags |= PatternFlags.DirectoryPattern; + e--; + } + + if (s == e) + { + return false; + } + + if (line.IndexOf('/', s, e - s) >= 0) + { + flags |= PatternFlags.FullPath; + } + + if (line[s] == '/') + { + s++; + } + + if (s == e) + { + return false; + } + + int escape = line.IndexOf('\\', s, e - s); + if (escape < 0) + { + glob = line.Substring(s, e - s); + return true; + } + + reusableBuffer.Clear(); + reusableBuffer.Append(line, s, escape - s); + + int i = escape; + while (i < e) + { + var c = line[i++]; + if (c == '\\' && i < e) + { + c = line[i++]; + } + + reusableBuffer.Append(c); + } + + glob = reusableBuffer.ToString(); + return true; + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitRepository.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitRepository.cs new file mode 100644 index 00000000..e8929138 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitRepository.cs @@ -0,0 +1,474 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Microsoft.Build.Tasks.Git +{ + internal sealed class GitRepository + { + private const int SupportedGitRepoFormatVersion = 0; + + private const string CommonDirFileName = "commondir"; + private const string GitDirName = ".git"; + private const string GitDirPrefix = "gitdir: "; + private const string GitDirFileName = "gitdir"; + + // See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD + private const string GitHeadFileName = "HEAD"; + + private const string GitModulesFileName = ".gitmodules"; + + public GitConfig Config { get; } + + public GitIgnore Ignore => _gitIgnore.Value; + + /// + /// Full path. + /// + public string GitDirectory { get; } + + /// + /// Full path. + /// + public string CommonDirectory { get; } + + /// + /// Optional, full path. + /// + public string WorkingDirectory { get; } + + public GitEnvironment Environment { get; } + + private readonly Lazy> _submodules; + private readonly Lazy _gitIgnore; + + internal GitRepository(GitEnvironment environment, GitConfig config, string gitDirectory, string commonDirectory, string workingDirectory) + { + Debug.Assert(environment != null); + Debug.Assert(config != null); + Debug.Assert(gitDirectory != null); + Debug.Assert(commonDirectory != null); + + Config = config; + GitDirectory = gitDirectory; + CommonDirectory = commonDirectory; + WorkingDirectory = workingDirectory; + Environment = environment; + + _submodules = new Lazy>(LoadSubmoduleConfiguration); + _gitIgnore = new Lazy(LoadIgnore); + } + + /// + /// Finds a git repository contining the specified path, if any. + /// + /// + /// + /// The repository found requires higher version of git repository format that is currently supported. + /// null if no git repository can be found that contains the specified path. + public static GitRepository OpenRepository(string path, GitEnvironment environment) + { + Debug.Assert(path != null); + Debug.Assert(environment != null); + + // See https://git-scm.com/docs/gitrepository-layout + + if (!LocateRepository(path, out var gitDirectory, out var commonDirectory, out var defaultWorkingDirectory)) + { + // unable to find repository + return null; + } + + Debug.Assert(gitDirectory != null); + Debug.Assert(commonDirectory != null); + + var reader = new GitConfig.Reader(gitDirectory, commonDirectory, environment); + var config = reader.Load(); + + var workingDirectory = GetWorkingDirectory(config, gitDirectory, commonDirectory) ?? defaultWorkingDirectory; + + // See https://github.com/git/git/blob/master/Documentation/technical/repository-version.txt + string versionStr = config.GetVariableValue("core", "repositoryformatversion"); + if (GitConfig.TryParseInt64Value(versionStr, out var version) && version > SupportedGitRepoFormatVersion) + { + throw new NotSupportedException($"Unsupported repository version {versionStr}. Only versions up to {SupportedGitRepoFormatVersion} are supported."); + } + + return new GitRepository(environment, config, gitDirectory, commonDirectory, workingDirectory); + } + + // internal for testing + internal static string GetWorkingDirectory(GitConfig config, string gitDirectory, string commonDirectory) + { + // Working trees cannot have the same common directory and git directory. + // 'gitdir' file in a git directory indicates a working tree. + + var gitdirFilePath = Path.Combine(gitDirectory, GitDirFileName); + + var isLinkedWorkingTree = PathUtils.ToPosixDirectoryPath(commonDirectory) != PathUtils.ToPosixDirectoryPath(gitDirectory) && + File.Exists(gitdirFilePath); + + if (isLinkedWorkingTree) + { + // https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-worktreesltidgtgitdir + + string workingDirectory; + try + { + workingDirectory = File.ReadAllText(gitdirFilePath); + } + catch (Exception e) when (!(e is IOException)) + { + throw new IOException(e.Message, e); + } + + workingDirectory = workingDirectory.TrimEnd(CharUtils.AsciiWhitespace); + + // Path in gitdir file must be absolute. + if (!PathUtils.IsAbsolute(workingDirectory)) + { + throw new InvalidDataException($"Path specified in '{gitdirFilePath}' is not absolute."); + } + + try + { + return Path.GetFullPath(workingDirectory); + } + catch + { + throw new InvalidDataException($"Path specified in '{gitdirFilePath}' is invalid."); + } + } + + // See https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreworktree + string value = config.GetVariableValue("core", "worktree"); + if (value != null) + { + // git does not expand home dir relative path ("~/") + try + { + return Path.GetFullPath(Path.Combine(gitDirectory, value)); + } + catch + { + throw new InvalidDataException($"The value of core.worktree is not a valid path: '{value}'"); + } + } + + return null; + } + + /// + /// Returns the commit SHA of the current HEAD tip. + /// + /// + /// + /// Null if the HEAD tip reference can't be resolved. + public string GetHeadCommitSha() + => GetHeadCommitSha(GitDirectory, CommonDirectory); + + /// + /// Returns the commit SHA of the current HEAD tip of the specified submodule. + /// + /// The path to the submodule working directory relative to the working directory of this repository. + /// + /// + /// Null if the HEAD tip reference can't be resolved. + public string GetSubmoduleHeadCommitSha(string submoduleWorkingDirectory) + { + Debug.Assert(submoduleWorkingDirectory != null); + + string dotGitPath; + try + { + dotGitPath = Path.Combine(GetWorkingDirectory(), submoduleWorkingDirectory, GitDirName); + } + catch + { + throw new InvalidDataException($"Invalid module path: '{submoduleWorkingDirectory}'"); + } + + var gitDirectory = ReadDotGitFile(dotGitPath); + if (!IsGitDirectory(gitDirectory, out var commonDirectory)) + { + return null; + } + + return GetHeadCommitSha(gitDirectory, commonDirectory); + } + + /// + /// + private static string GetHeadCommitSha(string gitDirectory, string commonDirectory) + { + // See + // https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD + // https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-refs + + string headRef; + try + { + headRef = File.ReadAllText(Path.Combine(gitDirectory, GitHeadFileName)); + } + catch (Exception e) when (!(e is IOException)) + { + throw new IOException(e.Message, e); + } + + return ResolveReference(headRef, commonDirectory); + } + + // internal for testing + internal static string ResolveReference(string reference, string commonDirectory) + { + HashSet lazyVisitedReferences = null; + return ResolveReference(reference, commonDirectory, ref lazyVisitedReferences); + } + + /// + /// + private static string ResolveReference(string reference, string commonDirectory, ref HashSet lazyVisitedReferences) + { + const string refPrefix = "ref: "; + if (reference.StartsWith(refPrefix + "refs/", StringComparison.Ordinal)) + { + var symRef = reference.Substring(refPrefix.Length); + + if (lazyVisitedReferences != null && !lazyVisitedReferences.Add(symRef)) + { + // infinite recursion + throw new InvalidDataException($"Recursion detected while resolving reference: '{reference}'"); + } + + string content; + try + { + content = File.ReadAllText(Path.Combine(commonDirectory, symRef)); + } + catch (ArgumentException) + { + throw new InvalidDataException($"Invalid reference: '{reference}'"); + } + catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException) + { + return null; + } + catch (Exception e) when (!(e is IOException)) + { + throw new IOException(e.Message, e); + } + + if (IsObjectId(reference)) + { + return reference; + } + + lazyVisitedReferences ??= new HashSet(); + + return ResolveReference(content, commonDirectory, ref lazyVisitedReferences); + } + + if (IsObjectId(reference)) + { + return reference; + } + + throw new InvalidDataException($"Invalid reference: '{reference}'"); + } + + private string GetWorkingDirectory() + => WorkingDirectory ?? throw new InvalidOperationException("Repository does not have a working directory"); + + private static bool IsObjectId(string reference) + => reference.Length == 40 && reference.All(CharUtils.IsHexadecimalDigit); + + /// + /// + public ImmutableArray GetSubmodules() + => _submodules.Value; + + /// + /// + private ImmutableArray LoadSubmoduleConfiguration() + { + var submodulesConfigFile = Path.Combine(GetWorkingDirectory(), GitModulesFileName); + if (!File.Exists(submodulesConfigFile)) + { + return ImmutableArray.Empty; + } + + var builder = ImmutableArray.CreateBuilder(); + var reader = new GitConfig.Reader(GitDirectory, CommonDirectory, Environment); + var submoduleConfig = reader.LoadFrom(submodulesConfigFile); + + foreach (var group in submoduleConfig.Variables. + Where(kvp => kvp.Key.SectionNameEquals("submodule")). + GroupBy(kvp => kvp.Key.SubsectionName, GitConfig.VariableKey.SubsectionNameComparer). + OrderBy(group => group.Key)) + { + string url = null; + string path = null; + + foreach (var variable in group) + { + if (variable.Key.VariableNameEquals("path")) + { + path = variable.Value.Last(); + } + else if (variable.Key.VariableNameEquals("url")) + { + url = variable.Value.Last(); + } + } + + if (path != null && url != null) + { + builder.Add(new GitSubmodule(group.Key, path, url)); + } + } + + return builder.ToImmutable(); + } + + private GitIgnore LoadIgnore() + { + var workingDirectory = GetWorkingDirectory(); + var ignoreCase = GitConfig.ParseBooleanValue(Config.GetVariableValue("core", "ignorecase")); + var excludesFile = Config.GetVariableValue("core", "excludesFile"); + var commonInfoExclude = Path.Combine(CommonDirectory, "info", "exclude"); + + var root = GitIgnore.LoadFromFile(commonInfoExclude, GitIgnore.LoadFromFile(excludesFile, parent: null)); + return new GitIgnore(root, workingDirectory, ignoreCase); + } + + /// + /// + internal static bool LocateRepository(string directory, out string gitDirectory, out string commonDirectory, out string workingDirectory) + { + gitDirectory = commonDirectory = workingDirectory = null; + + try + { + directory = Path.GetFullPath(directory); + } + catch + { + return false; + } + + while (directory != null) + { + // TODO: stop on device boundary + + var dotGitPath = Path.Combine(directory, GitDirName); + + if (Directory.Exists(dotGitPath)) + { + if (IsGitDirectory(dotGitPath, out commonDirectory)) + { + gitDirectory = dotGitPath; + workingDirectory = directory; + return true; + } + } + else if (File.Exists(dotGitPath)) + { + var link = ReadDotGitFile(dotGitPath); + if (IsGitDirectory(link, out commonDirectory)) + { + gitDirectory = link; + workingDirectory = directory; + return true; + } + + return false; + } + + if (Directory.Exists(directory)) + { + if (IsGitDirectory(directory, out commonDirectory)) + { + gitDirectory = directory; + workingDirectory = null; + return true; + } + } + + directory = Path.GetDirectoryName(directory); + } + + return false; + } + + private static string ReadDotGitFile(string path) + { + string content; + try + { + content = File.ReadAllText(path); + } + catch (Exception e) when (!(e is IOException)) + { + throw new IOException(e.Message, e); + } + + if (!content.StartsWith(GitDirPrefix)) + { + throw new InvalidDataException($"Invalid format of '.git' file at '{path}'"); + } + + // git does not trim whitespace: + var link = content.Substring(GitDirPrefix.Length); + + try + { + // link is relative to the directory containing the file: + return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), link)); + } + catch + { + throw new InvalidDataException($"Invalid path specified in '.git' file at '{path}'"); + } + } + + private static bool IsGitDirectory(string directory, out string commonDirectory) + { + // HEAD file is required + if (!File.Exists(Path.Combine(directory, GitHeadFileName))) + { + commonDirectory = null; + return false; + } + + // Spec https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-commondir: + var commonLinkPath = Path.Combine(directory, CommonDirFileName); + if (File.Exists(commonLinkPath)) + { + try + { + // note: git does not trim whitespace + commonDirectory = Path.Combine(directory, File.ReadAllText(commonLinkPath)); + } + catch + { + // git does not consider the directory valid git directory if the content of commondir file is malformed + commonDirectory = null; + return false; + } + } + else + { + commonDirectory = directory; + } + + // Git also requires objects and refs directories, but we allow them to be missing. + // See https://github.com/dotnet/sourcelink/tree/master/docs#minimal-git-repository-metadata + return Directory.Exists(commonDirectory); + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitSubmodule.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitSubmodule.cs new file mode 100644 index 00000000..bd42b467 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitSubmodule.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Build.Tasks.Git +{ + internal readonly struct GitSubmodule + { + public string Name { get; } + + /// + /// Working directory path as specified in .gitmodules file. + /// Expected to be relative to the working directory of the containing repository and have Posix directory separators (not normalized). + /// + public string WorkingDirectoryPath { get; } + + /// + /// An absolute URL or a relative path (if it starts with `./` or `../`) to the default remote of the containing repository. + /// + public string Url { get; } + + public GitSubmodule(string name, string workingDirectoryPath, string url) + { + Name = name; + WorkingDirectoryPath = workingDirectoryPath; + Url = url; + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/Glob.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/Glob.cs new file mode 100644 index 00000000..3bdc2bd5 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/Glob.cs @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// Implementation based on documentation: +// - https://git-scm.com/docs/gitignore +// - http://man7.org/linux/man-pages/man7/glob.7.html +// - https://research.swtch.com/glob + +using System; +using System.Diagnostics; + +namespace Microsoft.Build.Tasks.Git +{ + // https://github.com/dotnet/corefx/issues/18922 + // https://github.com/dotnet/corefx/issues/25873 + + public static class Glob + { + internal static bool IsMatch(string pattern, string path, bool ignoreCase, bool matchWildCardWithDirectorySeparator) + { + int patternIndex = 0; + int pathIndex = 0; + + // true if the next matching character must be the first character of a directory name + bool matchDirectoryNameStart = false; + + bool stopAtPathSlash = false; + + int nextSinglePatternIndex = -1; + int nextSinglePathIndex = -1; + int nextDoublePatternIndex = -1; + int nextDoublePathIndex = -1; + + bool equal(char x, char y) + => x == y || ignoreCase && char.ToLowerInvariant(x) == char.ToLowerInvariant(y); + + while (patternIndex < pattern.Length) + { + var c = pattern[patternIndex++]; + if (c == '*') + { + // "a/**/*" does not match "a", although it might appear from the spec that it should. + + bool isDoubleAsterisk = patternIndex < pattern.Length && pattern[patternIndex] == '*' && + (patternIndex == pattern.Length - 1 || pattern[patternIndex + 1] == '/') && + (patternIndex == 1 || pattern[patternIndex - 2] == '/'); + + if (isDoubleAsterisk) + { + // trailing "/**" + if (patternIndex == pattern.Length - 1) + { + // remaining path definitely matches + return true; + } + + // At this point the initial '/' (if any) is already matched. + Debug.Assert(pattern[patternIndex] == '*' && pattern[patternIndex + 1] == '/'); + + // Continue matching remainder of the pattern following "**/" with the remainder of the path. + // The next path character only matches if it is preceded by '/'. + // Consider the following cases + // "**/a*" ~ "abc" + // "**/b" ~ "x/yb/b" (do not match the first 'b', only the second one) + // "**/?" ~ "x/yz/u" (do not match 'y', 'z'; match 'u') + // "a/**/b*" ~ "a/bcd" + // "a/**/b" ~ "a/x/yb/b" (do not match the first 'b', only the second one) + patternIndex += 2; + + stopAtPathSlash = false; + matchDirectoryNameStart = true; + } + else + { + // trailing "*" + if (patternIndex == pattern.Length) + { + return matchWildCardWithDirectorySeparator || path.IndexOf('/', pathIndex) == -1; + } + + stopAtPathSlash = !matchWildCardWithDirectorySeparator; + matchDirectoryNameStart = false; + } + + // If the rest of the pattern fails to match the rest of the path, we restart matching at the following indices. + // A sequence of consecutive resets is effectively searching the path for a substring that matches the span of the pattern + // in between the current wildcard and the next one. + // + // For example, consider matching pattern "A/**/B/**/C/*.D" to path "A/z/u/B/q/r/C/z.D". + // Processing the first ** wildcard keeps resetting until the pattern is alligned with "/B/" in the path (wildcard matches "z/u"). + // Processing the next ** wildcard keeps resetting until the pattern is alligned with "/C/" in the path (wildcard matches "q/r"). + // Finally, processing the * wildcard aligns on ".D" and the wildcard matches "z". + // + // If ** and * differ in matching '/' (matchWildCardWithDirectorySeparator is false) we need to reset them independently. + // Consider pattern "A/**/B*C*D/**/E" matching to "A/u/v/BaaCaaX/u/BoCoD/u/E". + // If we aligned on substring "/B" in between the first ** and the next * we would not match the path correctly. + // Instead, we need to align on the sub-pattern "/B*C*D/" in between the first and the second **. + if (stopAtPathSlash) + { + nextSinglePatternIndex = patternIndex; + nextSinglePathIndex = pathIndex + 1; + } + else + { + nextDoublePatternIndex = patternIndex; + nextDoublePathIndex = pathIndex + 1; + } + + continue; + } + + bool matching; + + if (c == '?') + { + // "?" matches any character except for "/" (when matchWildCardWithDirectorySeparator is false) + matching = pathIndex < path.Length && (matchWildCardWithDirectorySeparator || path[pathIndex] != '/'); + } + else if (c == '[') + { + // "[]" matches a single character in the range + matching = pathIndex < path.Length && IsRangeMatch(pattern, ref patternIndex, path[pathIndex], ignoreCase, matchWildCardWithDirectorySeparator); + } + else + { + if (c == '\\' && patternIndex < pattern.Length) + { + c = pattern[patternIndex++]; + } + + // match specific character: + matching = pathIndex < path.Length && equal(c, path[pathIndex]); + } + + if (matching && (!matchDirectoryNameStart || pathIndex == 0 || path[pathIndex - 1] == '/')) + { + matchDirectoryNameStart = false; + pathIndex++; + } + else if (nextDoublePatternIndex >= 0 || nextSinglePatternIndex >= 0) + { + // mismatch while matching pattern following a wildcard ** or * + + // "*" matches anything but "/" (when matchWildCardWithDirectorySeparator is false) + if (!stopAtPathSlash || pathIndex < path.Length && path[pathIndex] == '/') + { + // Reset to the last saved ** position, if any. + // Also handles reset of * when matchWildCardWithDirectorySeparator is true. + + if (nextDoublePatternIndex < 0) + { + return false; + } + + patternIndex = nextDoublePatternIndex; + pathIndex = nextDoublePathIndex; + + nextDoublePathIndex++; + } + else + { + // Reset to the last saved * position. + + patternIndex = nextSinglePatternIndex; + pathIndex = nextSinglePathIndex; + + nextSinglePathIndex++; + } + + Debug.Assert(patternIndex >= 0); + Debug.Assert(pathIndex >= 0); + + if (pathIndex >= path.Length) + { + return false; + } + } + else + { + // character mismatch + return false; + } + } + + return pathIndex == path.Length; + } + + private static bool IsRangeMatch(string pattern, ref int patternIndex, char pathChar, bool ignoreCase, bool matchWildCardWithDirectorySeparator) + { + Debug.Assert(pattern[patternIndex - 1] == '['); + + if (patternIndex == pattern.Length) + { + return false; + } + + if (ignoreCase) + { + pathChar = char.ToLowerInvariant(pathChar); + } + + bool negate = false; + bool isEmpty = true; + bool isMatching = false; + + var c = pattern[patternIndex]; + if (c == '!' || c == '^') + { + negate = true; + patternIndex++; + } + + while (patternIndex < pattern.Length) + { + c = pattern[patternIndex++]; + if (c == ']' && !isEmpty) + { + // Range does not match '/', but [^a] matches '/' if matchWildCardWithDirectorySeparator=true. + return (pathChar != '/' || matchWildCardWithDirectorySeparator) && (negate ? !isMatching : isMatching); + } + + if (ignoreCase) + { + c = char.ToLowerInvariant(c); + } + + char d; + + if (patternIndex + 1 < pattern.Length && pattern[patternIndex] == '-' && (d = pattern[patternIndex + 1]) != ']') + { + if (ignoreCase) + { + d = char.ToLowerInvariant(d); + } + + isMatching |= pathChar == c || pathChar > c && pathChar <= d; + patternIndex += 2; + } + else + { + // continue parsing to validate the range is well-formed + isMatching |= pathChar == c; + } + + isEmpty = false; + } + + // malformed range + return false; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/PathUtils.cs b/src/Microsoft.Build.Tasks.Git.Operations/Managed/PathUtils.cs new file mode 100644 index 00000000..b7001f1d --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.Operations/Managed/PathUtils.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.Build.Tasks.Git +{ + internal static class PathUtils + { + public static bool IsUnixLikePlatform => Path.DirectorySeparatorChar == '/'; + public const char VolumeSeparatorChar = ':'; + public static readonly string DirectorySeparatorStr = Path.DirectorySeparatorChar.ToString(); + private static readonly char[] s_slash = new char[] { '/' }; + private static readonly char[] s_directorySeparators = new char[] { '/' }; + + public static string EnsureTrailingSlash(string path) + => HasTrailingSlash(path) ? path : path + "/"; + + public static string TrimTrailingSlash(string path) + => path.TrimEnd(s_slash); + + public static string TrimTrailingDirectorySeparator(string path) + => path.TrimEnd(s_directorySeparators); + + public static bool HasTrailingSlash(string path) + => path.Length > 0 && path[path.Length - 1] == '/'; + + public static bool HasTrailingDirectorySeparator(string path) + => path.Length > 0 && (path[path.Length - 1] == '/' || path[path.Length - 1] == '\\'); + + public static string ToPosixPath(string path) + => (Path.DirectorySeparatorChar == '\\') ? path.Replace('\\', '/') : path; + + internal static string ToPosixDirectoryPath(string path) + => EnsureTrailingSlash(ToPosixPath(path)); + + internal static bool IsPosixPath(string path) + => Path.DirectorySeparatorChar == '/' || path.IndexOf('\\') < 0; + + public static string CombinePosixPaths(string root, string relativePath) + => CombinePaths(root, relativePath, "/"); + + public static string CombinePaths(string root, string relativePath, string separator) + { + Debug.Assert(!string.IsNullOrEmpty(root)); + + char c = root[root.Length - 1]; + if (!IsDirectorySeparator(c) && c != VolumeSeparatorChar) + { + return root + separator + relativePath; + } + + return root + relativePath; + } + + public static bool IsDirectorySeparator(char c) + => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + + /// + /// True if the path is an absolute path (rooted to drive or network share) + /// + public static bool IsAbsolute(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + + if (IsUnixLikePlatform) + { + return path[0] == Path.DirectorySeparatorChar; + } + + // "C:\" + if (IsDriveRootedAbsolutePath(path)) + { + // Including invalid paths (e.g. "*:\") + return true; + } + + // "\\machine\share" + // Including invalid/incomplete UNC paths (e.g. "\\goo") + return path.Length >= 2 && + IsDirectorySeparator(path[0]) && + IsDirectorySeparator(path[1]); + } + + /// + /// Returns true if given path is absolute and starts with a drive specification ("C:\"). + /// + private static bool IsDriveRootedAbsolutePath(string path) + { + Debug.Assert(!IsUnixLikePlatform); + return path.Length >= 3 && path[1] == VolumeSeparatorChar && IsDirectorySeparator(path[2]); + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs b/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs index 79469fd5..5c4068f1 100644 --- a/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs +++ b/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs @@ -8,7 +8,7 @@ namespace Microsoft.Build.Tasks.Git { internal static class RepositoryTasks { - private static bool Execute(T task, Action action) + private static bool Execute(T task, Action action) where T: RepositoryTask { var log = task.Log; @@ -19,10 +19,10 @@ private static bool Execute(T task, Action action) return true; } - Repository repo; + IRepository repo; try { - repo = new Repository(task.Root); + repo = GitOperations.CreateRepository(task.Root); } catch (RepositoryNotFoundException e) { diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs new file mode 100644 index 00000000..f1fe1e4b --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitConfigTests.cs @@ -0,0 +1,408 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using TestUtilities; +using Xunit; + +namespace Microsoft.Build.Tasks.Git.UnitTests +{ + public class GitConfigTests + { + private static IEnumerable Inspect(GitConfig config) + => config.EnumerateVariables().OrderBy(x => x.Key).Select(kvp => $"{kvp.Key}={string.Join("|", kvp.Value)}"); + + private static GitConfig LoadFromString(string gitDirectory, string configPath, string configContent) + => new GitConfig.Reader(gitDirectory, gitDirectory, new GitEnvironment(Path.GetTempPath()), _ => new StringReader(configContent)). + LoadFrom(configPath); + + [Fact] + public void Sections() + { + var config = LoadFromString("/", "/config", $@" +a = 1 ; variable without section +[s1][s2]b = 2 # section without variable followed by another section +[s3]#[s4] + +c = 3 4 "" "" +;xxx +c = "" 5 "" + +d = +#xxx +"); + + AssertEx.Equal(new[] + { + ".a=1", + "s2.b=2", + "s3.c=3 4 | 5 ", + "s3.d=", + }, Inspect(config)); + } + + [Fact] + public void Sections_Errors() + { + Assert.Throws(() => LoadFromString("/", "/config", @" +[s] +a = +1")); + + } + + [Fact] + public void ConditionalInclude() + { + var repoDir = PathUtils.ToPosixPath(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar)); + var gitDirectory = PathUtils.ToPosixPath(Path.Combine(repoDir, ".git")) + "/"; + + TextReader openFile(string path) + { + Assert.Equal(gitDirectory, PathUtils.EnsureTrailingSlash(PathUtils.ToPosixPath(Path.GetDirectoryName(path)))); + + return new StringReader(Path.GetFileName(path) switch + { + "config" => $@" +[x ""y""] a = 1 +[x ""Y""] a = 2 +[x.Y] A = 3 + +[core] + symlinks = false + ignorecase = true + +[includeIf ""gitdir:{repoDir}""] # not included + path = cfg0 + +[includeIf ""gitdir:{repoDir}/<>""] # not included (does not throw) + path = cfg0 + +[includeIf ""gitdir:{repoDir}\\.git/""] # not included (Windows separator) + path = cfg0 + +[includeIf ""gitdir:{repoDir}/.git/""] # not included (file does not exist) + path = cfg0 + +[includeIf ""gitdir:{repoDir}/.git""] # not included (path doesn't end with slash) + path = cfg0 + +[includeIf ""gitdir:{repoDir}?.git/""] # included + path = cfg2 + +[includeIf ""gitdir:{repoDir}[^a].git/""] # included + path = cfg3 + +[includeIf ""gitdir:{repoDir}/""] # included + path = cfg4 + +[includeIf ""gitdir:{repoDir}/*""] # included + path = cfg5 + +[includeIf ""gitdir:{repoDir}/**""] # included + path = cfg6 + +[includeIf ""gitdir:{repoDir}/**/.git/""] # included + path = cfg7 + +[includeIf ""gitdir/i:{repoDir}/**/.GIT/""] # included + path = cfg8 + +[includeIf ""gitdir/i:~/**/.GIT/""] # included + path = cfg9 + +[includeIf ""gitdir:.""] # not included + path = cfg0 + +[includeIf ""gitdir:./""] # included + path = cfg10 +", + "cfg1" => "[c]n = cfg1", + "cfg2" => "[c]n = cfg2", + "cfg3" => "[c]n = cfg3", + "cfg4" => "[c]n = cfg4", + "cfg5" => "[c]n = cfg5", + "cfg6" => "[c]n = cfg6", + "cfg7" => "[c]n = cfg7", + "cfg8" => "[c]n = cfg8", + "cfg9" => "[c]n = cfg9", + "cfg10" => "[c]n = cfg10", + _ => throw new FileNotFoundException(path) + }); + } + + var config = new GitConfig.Reader(gitDirectory, gitDirectory, new GitEnvironment(repoDir), openFile).LoadFrom(Path.Combine(gitDirectory, "config")); + + AssertEx.Equal(new[] + { + "c.n=cfg2|cfg3|cfg4|cfg5|cfg6|cfg7|cfg8|cfg9|cfg10", + "core.ignorecase=true", + "core.symlinks=false", + $"includeif.gitdir/i:~/**/.GIT/.path=cfg9", + $"includeif.gitdir/i:{repoDir}/**/.GIT/.path=cfg8", + $"includeif.gitdir:..path=cfg0", + $"includeif.gitdir:./.path=cfg10", + $"includeif.gitdir:{repoDir}.path=cfg0", + $"includeif.gitdir:{repoDir}/**.path=cfg6", + $"includeif.gitdir:{repoDir}/**/.git/.path=cfg7", + $"includeif.gitdir:{repoDir}/*.path=cfg5", + $"includeif.gitdir:{repoDir}/.git.path=cfg0", + $"includeif.gitdir:{repoDir}/.git/.path=cfg0", + $"includeif.gitdir:{repoDir}/.path=cfg4", + $"includeif.gitdir:{repoDir}/<>.path=cfg0", + $"includeif.gitdir:{repoDir}?.git/.path=cfg2", + $"includeif.gitdir:{repoDir}[^a].git/.path=cfg3", + $"includeif.gitdir:{repoDir}\\.git/.path=cfg0", + "x.y.a=1|3", + "x.Y.a=2", + }, Inspect(config)); + } + + [Fact] + public void IncludeRecursion() + { + var gitDirectory = PathUtils.ToPosixPath(Path.Combine(Path.GetTempPath(), ".git")) + "/"; + + TextReader openFile(string path) + { + Assert.Equal(gitDirectory, PathUtils.EnsureTrailingSlash(PathUtils.ToPosixPath(Path.GetDirectoryName(path)))); + + return new StringReader(Path.GetFileName(path) switch + { + "config" => @" +[x] + a = 1 + +[include] + path = cfg1 +", + "cfg1" => @" +[x] + a = 2 + +[include] + path = config +", + _ => throw new FileNotFoundException(path) + }); + } + + Assert.Throws(() => new GitConfig.Reader(gitDirectory, gitDirectory, new GitEnvironment("/home"), openFile).LoadFrom(Path.Combine(gitDirectory, "config"))); + } + + [Theory] + [InlineData(true, true, "programdata|sys|xdg|home1|common")] + [InlineData(true, false, "programdata|sys|home2|home1|common")] + [InlineData(false, true, "sys|xdg|home1|common")] + public void HierarchicalLoad(bool enableProgramData, bool enableXdg, string expected) + { + using var temp = new TempRoot(); + var root = temp.CreateDirectory(); + + var gitDir = root.CreateDirectory(".git"); + + var commonDir = root.CreateDirectory("common"); + commonDir.CreateFile("config").WriteAllText("[cfg]dir=common"); + + var homeDir = root.CreateDirectory("home"); + homeDir.CreateFile(".gitconfig").WriteAllText("[cfg]dir=home1"); + homeDir.CreateDirectory(".config").CreateDirectory("git").CreateFile("config").WriteAllText("[cfg]dir=home2"); + + TempDirectory xdgDir = null; + if (enableXdg) + { + xdgDir = root.CreateDirectory("xdg"); + xdgDir.CreateDirectory("git").CreateFile("config").WriteAllText("[cfg]dir=xdg"); + } + + TempDirectory programDataDir = null; + if (enableProgramData) + { + programDataDir = root.CreateDirectory("programdata"); + programDataDir.CreateDirectory("git").CreateFile("config").WriteAllText("[cfg]dir=programdata"); + } + + var systemDir = root.CreateDirectory("sys"); + systemDir.CreateFile("gitconfig").WriteAllText("[cfg]dir=sys"); + + var gitDirectory = PathUtils.EnsureTrailingSlash(PathUtils.ToPosixPath(gitDir.Path)); + var commonDirectory = PathUtils.EnsureTrailingSlash(PathUtils.ToPosixPath(commonDir.Path)); + + var environment = new GitEnvironment( + homeDirectory: homeDir.Path, + xdgConfigHomeDirectory: xdgDir?.Path, + programDataDirectory: programDataDir?.Path, + systemDirectory : systemDir.Path); + + var reader = new GitConfig.Reader(gitDirectory, commonDirectory, environment, File.OpenText); + var gitConfig = reader.Load(); + + AssertEx.Equal(new[] + { + "cfg.dir=" + expected + }, Inspect(gitConfig)); + } + + [Theory] + [InlineData("[X]", "x", "")] + [InlineData("[-]", "-", "")] + [InlineData("[.]", ".", "")] + [InlineData("[..]", "", ".")] + [InlineData("[...]", "", "..")] + [InlineData("[.x]", "", "x")] + [InlineData("[..x]", "", ".x")] + [InlineData("[.X]", "", "x")] + [InlineData("[X.]", "x.", "")] + [InlineData("[X..]", "x", ".")] + [InlineData("[X. \"z\"]", "x.", ".z")] + [InlineData("[X.y]", "x", "y")] + [InlineData("[X.y.z]", "x", "y.z")] + [InlineData("[X-]", "x-", "")] + [InlineData("[-x]", "-x", "")] + [InlineData("[X-y]", "x-y", "")] + [InlineData("[X \"y\"]", "x", "y")] + [InlineData("[X \t\f\v\"y\"]", "x", "y")] + [InlineData("[X.y \"z\"]", "x", "y.z")] + [InlineData("[X.Y \"z\"]", "x", "y.z")] + [InlineData("[X \"/*-\\a\"]", "x", "/*-a")] + public void ReadSectionHeader(string str, string name, string subsectionName) + { + GitConfig.Reader.ReadSectionHeader(new StringReader(str), new StringBuilder(), out var actualName, out var actualSubsectionName); + Assert.Equal(name, actualName); + Assert.Equal(subsectionName, actualSubsectionName); + } + + [Theory] + [InlineData("[")] + [InlineData("[x")] + [InlineData("[x x x]")] + [InlineData("[* \"\\")] + [InlineData("[* \"\\\"]")] + [InlineData("[* \"*\"]")] + [InlineData("[x \"y\" ]")] + public void ReadSectionHeader_Errors(string str) + { + Assert.Throws(() => GitConfig.Reader.ReadSectionHeader(new StringReader(str), new StringBuilder(), out _, out _)); + } + + [Theory] + [InlineData("a", "a", "true")] + [InlineData("A", "a", "true")] + [InlineData("a\r", "a", "true")] + [InlineData("a\r\n", "a", "true")] + [InlineData("a\n", "a", "true")] + [InlineData("a\n\r", "a", "true")] + [InlineData("a \n", "a", "true")] + [InlineData("a# ", "a", "true")] + [InlineData("a;xxx\n", "a", "true")] + [InlineData("a #", "a", "true")] + [InlineData("a=1", "a", "1")] + [InlineData("a-=1", "a-", "1")] + [InlineData("a-4=1", "a-4", "1")] + [InlineData("a-4 =1", "a-4", "1")] + [InlineData("a=1\nb=1", "a", "1")] + [InlineData("a=\"1\\\nb=1\"", "a", "1b=1")] + [InlineData("a=\"1\\nb=1\"", "a", "1\nb=1")] + [InlineData("name=\"a\"x\"b\"", "name", "axb")] + [InlineData("name=\"b\"#\"a\"", "name", "b")] + [InlineData("name=\"b\";\"a\"", "name", "b")] + [InlineData("name=\\\r\nabc", "name", "abc")] + [InlineData("name=\"a\\\n bc\"", "name", "a bc")] + [InlineData("name=a\\\nbc", "name", "abc")] + [InlineData("name=a\\\n bc", "name", "a bc")] + [InlineData("name= 3 4 \" \" ", "name", "3 4 ")] + [InlineData("name= 1\\t", "name", "1\t")] + [InlineData("name= 1\\n", "name", "1\n")] + [InlineData("name= 1\\\\", "name", "1\\")] + [InlineData("name= 1\\\"", "name", "1\"")] + [InlineData("name= ", "name", "")] + [InlineData("name=", "name", "")] + public void ReadVariableDeclaration(string str, string name, string value) + { + GitConfig.Reader.ReadVariableDeclaration(new StringReader(str), new StringBuilder(), out var actualName, out var actualValue); + Assert.Equal(name, actualName); + Assert.Equal(value, actualValue); + } + + [Theory] + [InlineData("")] + [InlineData("*")] + [InlineData("-=1")] + [InlineData("_=1")] + [InlineData("5=1")] + [InlineData("a_=1")] + [InlineData("a*=1")] + [InlineData("name=\\j")] + [InlineData("name=\"")] + [InlineData("name=\"a")] + [InlineData("name=\"a\n")] + [InlineData("name=\"a\nb")] + public void ReadVariableDeclaration_Errors(string str) + { + Assert.Throws(() => GitConfig.Reader.ReadVariableDeclaration(new StringReader(str), new StringBuilder(), out _, out _)); + } + + [Theory] + [InlineData("0", 0)] + [InlineData("10", 10)] + [InlineData("-10", -10)] + [InlineData("10k", 10 * 1024)] + [InlineData("-10K", -10 * 1024)] + [InlineData("10M", 10 * 1024 * 1024)] + [InlineData("-10m", -10 * 1024 * 1024)] + [InlineData("10G", 10L * 1024 * 1024 * 1024)] + [InlineData("-10g", -10L * 1024 * 1024 * 1024)] + [InlineData("-9223372036854775808", long.MinValue)] + [InlineData("9223372036854775807", long.MaxValue)] + public void TryParseInt64Value_Success(string str, long value) + { + Assert.True(GitConfig.TryParseInt64Value(str, out var actualValue)); + Assert.Equal(value, actualValue); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData(" ")] + [InlineData("-")] + [InlineData("k")] + [InlineData("-9223372036854775809")] + [InlineData("9223372036854775808")] + [InlineData("922337203685477580k")] + [InlineData("922337203685477580G")] + public void TryParseInt64Value_Error(string str) + { + Assert.False(GitConfig.TryParseInt64Value(str, out _)); + } + + [Theory] + [InlineData("", false)] + [InlineData("no", false)] + [InlineData("NO", false)] + [InlineData("No", false)] + [InlineData("Off", false)] + [InlineData("0", false)] + [InlineData("False", false)] + [InlineData("1", true)] + [InlineData("tRue", true)] + [InlineData("oN", true)] + [InlineData("yeS", true)] + public void TryParseBooleanValue_Success(string str, bool value) + { + Assert.True(GitConfig.TryParseBooleanValue(str, out var actualValue)); + Assert.Equal(value, actualValue); + } + + [Theory] + [InlineData(null)] + [InlineData("2")] + [InlineData(" ")] + [InlineData("x")] + public void TryParseBooleanValue_Error(string str) + { + Assert.False(GitConfig.TryParseBooleanValue(str, out _)); + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs index 013e94aa..0d78504a 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using LibGit2Sharp; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -73,5 +74,16 @@ public void MinimalGitData() MockItem.AdjustSeparators(@"sub\ignore_in_submodule_d"), }, untrackedFiles.Select(item => item.ItemSpec)); } + + [Fact] + public void MinimalGitData2() + { + //Environment.SetEnvironmentVariable("GIT_WORK_TREE", @"D:\temp\x"); + + // ignores GIT_WORK_TREE + // var repository = new Repository(@"D:\git-experiments\r3\main\.git\worktrees\wt1"); + var repository = new Repository(@"D:\git-experiments\r3\main"); + var x = repository.Head.Reference.ResolveToDirectReference()?.TargetIdentifier; + } } } diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitIgnoreTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitIgnoreTests.cs new file mode 100644 index 00000000..142a7011 --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitIgnoreTests.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.IO; +using System.Linq; +using System.Text; +using TestUtilities; +using Xunit; + +namespace Microsoft.Build.Tasks.Git.UnitTests +{ + public class GitIgnoreTests + { + [Theory] + [InlineData("\t", "\t", GitIgnore.PatternFlags.None)] + [InlineData("\v", "\v", GitIgnore.PatternFlags.None)] + [InlineData("\f", "\f", GitIgnore.PatternFlags.None)] + [InlineData("\\ ", " ", GitIgnore.PatternFlags.None)] + [InlineData(" #", " #", GitIgnore.PatternFlags.None)] + [InlineData("!x ", "x", GitIgnore.PatternFlags.Negative)] + [InlineData("!x/", "x", GitIgnore.PatternFlags.Negative | GitIgnore.PatternFlags.DirectoryPattern)] + [InlineData("!/x", "x", GitIgnore.PatternFlags.Negative | GitIgnore.PatternFlags.FullPath)] + [InlineData("x/", "x", GitIgnore.PatternFlags.DirectoryPattern)] + [InlineData("/x", "x", GitIgnore.PatternFlags.FullPath)] + [InlineData("//x//", "/x/", GitIgnore.PatternFlags.DirectoryPattern | GitIgnore.PatternFlags.FullPath)] + [InlineData("\\", "\\", GitIgnore.PatternFlags.None)] + [InlineData("\\x", "x", GitIgnore.PatternFlags.None)] + [InlineData("x\\", "x\\", GitIgnore.PatternFlags.None)] + [InlineData("\\\\", "\\", GitIgnore.PatternFlags.None)] + [InlineData("\\abc\\xy\\z", "abcxyz", GitIgnore.PatternFlags.None)] + internal void TryParsePattern(string line, string glob, GitIgnore.PatternFlags flags) + { + Assert.True(GitIgnore.TryParsePattern(line, new StringBuilder(), out var actualGlob, out var actualFlags)); + Assert.Equal(glob, actualGlob); + Assert.Equal(flags, actualFlags); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + [InlineData("#")] + [InlineData("!")] + [InlineData("/")] + [InlineData("//")] + [InlineData("!/")] + [InlineData("!//")] + public void TryParsePattern_None(string line) + { + Assert.False(GitIgnore.TryParsePattern(line, new StringBuilder(), out _, out _)); + } + + [Fact] + public void IsIgnored_CaseSensitive() + { + using var temp = new TempRoot(); + + var rootDir = temp.CreateDirectory(); + var workingDir = rootDir.CreateDirectory("Repo"); + + // root + // A (.gitignore) + // B + // C (.gitignore) + // D1, D2, D3 + var dirA = workingDir.CreateDirectory("A"); + var dirB = dirA.CreateDirectory("B"); + var dirC = dirB.CreateDirectory("C"); + dirC.CreateDirectory("D1"); + dirC.CreateDirectory("D2"); + dirC.CreateDirectory("D3"); + + dirA.CreateFile(".gitignore").WriteAllText(@" +!z.txt +*.txt +!u.txt +!v.txt +!.git +b/ +D3/ +Bar/**/*.xyz +v.txt +"); + dirC.CreateFile(".gitignore").WriteAllText(@" +!a.txt +D2 +D1/c.cs +/*.c +"); + + var ignore = new GitIgnore(root: null, PathUtils.ToPosixDirectoryPath(workingDir.Path), ignoreCase: false); + var matcher = ignore.CreateMatcher(); + + // outside of the working directory: + Assert.Null(matcher.IsPathIgnored(rootDir.Path)); + Assert.Null(matcher.IsPathIgnored(workingDir.Path.ToUpperInvariant())); + + // special case: + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, ".git") + Path.DirectorySeparatorChar)); + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, ".git", "config"))); + + Assert.False(matcher.IsPathIgnored(workingDir.Path)); + Assert.False(matcher.IsPathIgnored(workingDir.Path + Path.DirectorySeparatorChar)); + Assert.False(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "X"))); + + // matches "*.txt" + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "D1", "b.txt"))); + + // matches "!a.txt" + Assert.False(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "D1", "a.txt"))); + + // matches "*.txt", "!z.txt" is ignored + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "z.txt"))); + + // matches "*.txt", overriden by "!u.txt" + Assert.False(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "u.txt"))); + + // matches "*.txt", overriden by "!v.txt", which is overriden by "v.txt" + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "v.txt"))); + + // matches directory name "D2" + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "D2", "E", "a.txt"))); + + // does not match "b/" (treated as a file path) + Assert.False(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "D1", "b"))); + + // matches "b/" (treated as a directory path) + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "D1", "b") + Path.DirectorySeparatorChar)); + + // matches "D3/" (existing directory path) + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "D3"))); + + // matches "D1/c.cs" + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "D1", "c.cs"))); + + // matches "Bar/**/*.xyz" + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "Bar", "Baz", "Goo", ".xyz"))); + + // matches "/*.c" + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "x.c"))); + + // does not match "/*.c" + Assert.False(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "B", "C", "D1", "x.c"))); + + AssertEx.Equal(new[] + { + "/Repo/.git: True", + "/Repo/A/B/C/D1/b: True", + "/Repo/A/B/C/D1: False", + "/Repo/A/B/C/D2/E: True", + "/Repo/A/B/C/D2: True", + "/Repo/A/B/C/D3: True", + "/Repo/A/B/C: False", + "/Repo/A/B: False", + "/Repo/A: False", + "/Repo: False" + }, matcher.DirectoryIgnoreStateCache.Select(kvp => $"{kvp.Key.Substring(rootDir.Path.Length)}: {kvp.Value}").OrderBy(s => s)); + } + + [Fact] + public void IsIgnored_IgnoreCase() + { + using var temp = new TempRoot(); + + var rootDir = temp.CreateDirectory(); + var workingDir = rootDir.CreateDirectory("Repo"); + + // root + // A (.gitignore) + // diR + var dirA = workingDir.CreateDirectory("A"); + dirA.CreateDirectory("diR"); + + dirA.CreateFile(".gitignore").WriteAllText(@" +*.txt +!a.TXT +dir/ +"); + + var ignore = new GitIgnore(root: null, PathUtils.ToPosixDirectoryPath(workingDir.Path), ignoreCase: true); + var matcher = ignore.CreateMatcher(); + + // outside of the working directory: + Assert.Null(matcher.IsPathIgnored(rootDir.Path.ToUpperInvariant())); + + // special case: + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, ".GIT"))); + + // matches "*.txt" + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "b.TXT"))); + + // matches "!a.TXT" + Assert.False(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "a.txt"))); + + // matches directory name "dir/" + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "DIr", "a.txt"))); + + // matches "dir/" (treated as a directory path) + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "DiR") + Path.DirectorySeparatorChar)); + + // matches "dir/" (existing directory path) + Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "DIR"))); + + AssertEx.Equal(new[] + { + "/Repo/A/DIr: True", + "/Repo/A: False", + "/Repo: False", + }, matcher.DirectoryIgnoreStateCache.Select(kvp => $"{kvp.Key.Substring(rootDir.Path.Length)}: {kvp.Value}").OrderBy(s => s)); + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs new file mode 100644 index 00000000..0236cf2a --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs @@ -0,0 +1,284 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.IO; +using System.Linq; +using TestUtilities; +using Xunit; + +namespace Microsoft.Build.Tasks.Git.UnitTests +{ + public class GitRepositoryTests + { + [Fact] + public void LocateRepository_Worktree() + { + using var temp = new TempRoot(); + + var mainWorkingDir = temp.CreateDirectory(); + var mainWorkingSubDir = mainWorkingDir.CreateDirectory("A"); + var mainGitDir = mainWorkingDir.CreateDirectory(".git"); + mainGitDir.CreateFile("HEAD"); + + var worktreeGitDir = temp.CreateDirectory(); + var worktreeGitSubDir = worktreeGitDir.CreateDirectory("B"); + var worktreeDir = temp.CreateDirectory(); + var worktreeSubDir = worktreeDir.CreateDirectory("C"); + var worktreeGitFile = worktreeDir.CreateFile(".git").WriteAllText("gitdir: " + worktreeGitDir); + + worktreeGitDir.CreateFile("HEAD"); + worktreeGitDir.CreateFile("commondir").WriteAllText(mainGitDir.Path); + worktreeGitDir.CreateFile("gitdir").WriteAllText(worktreeGitFile.Path); + + // start under main repository directory: + Assert.True(GitRepository.LocateRepository( + mainWorkingSubDir.Path, + out var locatedGitDirectory, + out var locatedCommonDirectory, + out var locatedWorkingDirectory)); + + Assert.Equal(mainGitDir.Path, locatedGitDirectory); + Assert.Equal(mainGitDir.Path, locatedCommonDirectory); + Assert.Equal(mainWorkingDir.Path, locatedWorkingDirectory); + + // start at main git directory (git config works from this dir, but git status requires work dir): + Assert.True(GitRepository.LocateRepository( + mainGitDir.Path, + out locatedGitDirectory, + out locatedCommonDirectory, + out locatedWorkingDirectory)); + + Assert.Equal(mainGitDir.Path, locatedGitDirectory); + Assert.Equal(mainGitDir.Path, locatedCommonDirectory); + Assert.Null(locatedWorkingDirectory); + + // start under worktree directory: + Assert.True(GitRepository.LocateRepository( + worktreeSubDir.Path, + out locatedGitDirectory, + out locatedCommonDirectory, + out locatedWorkingDirectory)); + + Assert.Equal(worktreeGitDir.Path, locatedGitDirectory); + Assert.Equal(mainGitDir.Path, locatedCommonDirectory); + Assert.Equal(worktreeDir.Path, locatedWorkingDirectory); + + // start under worktree git directory (git config works from this dir, but git status requires work dir): + Assert.True(GitRepository.LocateRepository( + worktreeGitSubDir.Path, + out locatedGitDirectory, + out locatedCommonDirectory, + out locatedWorkingDirectory)); + + Assert.Equal(worktreeGitDir.Path, locatedGitDirectory); + Assert.Equal(mainGitDir.Path, locatedCommonDirectory); + Assert.Null(locatedWorkingDirectory); + } + + [Fact] + public void LocateRepository_Submodule() + { + using var temp = new TempRoot(); + + var mainWorkingDir = temp.CreateDirectory(); + var mainGitDir = mainWorkingDir.CreateDirectory(".git"); + mainGitDir.CreateFile("HEAD"); + + var submoduleGitDir = mainGitDir.CreateDirectory("modules").CreateDirectory("sub"); + + var submoduleWorkDir = temp.CreateDirectory(); + submoduleWorkDir.CreateFile(".git").WriteAllText("gitdir: " + submoduleGitDir.Path); + + submoduleGitDir.CreateFile("HEAD"); + submoduleGitDir.CreateDirectory("objects"); + submoduleGitDir.CreateDirectory("refs"); + + // start under submodule working directory: + Assert.True(GitRepository.LocateRepository( + submoduleWorkDir.Path, + out var locatedGitDirectory, + out var locatedCommonDirectory, + out var locatedWorkingDirectory)); + + Assert.Equal(submoduleGitDir.Path, locatedGitDirectory); + Assert.Equal(submoduleGitDir.Path, locatedCommonDirectory); + Assert.Equal(submoduleWorkDir.Path, locatedWorkingDirectory); + + // start under submodule git directory: + Assert.True(GitRepository.LocateRepository( + submoduleGitDir.Path, + out locatedGitDirectory, + out locatedCommonDirectory, + out locatedWorkingDirectory)); + + Assert.Equal(submoduleGitDir.Path, locatedGitDirectory); + Assert.Equal(submoduleGitDir.Path, locatedCommonDirectory); + Assert.Null(locatedWorkingDirectory); + } + + [Fact] + public void OpenRepository() + { + using var temp = new TempRoot(); + + var homeDir = temp.CreateDirectory(); + + var workingDir = temp.CreateDirectory(); + var gitDir = workingDir.CreateDirectory(".git"); + + gitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master"); + gitDir.CreateDirectory("refs").CreateDirectory("heads").CreateFile("master").WriteAllText("0000000000000000000000000000000000000000"); + gitDir.CreateDirectory("objects"); + + gitDir.CreateFile("config").WriteAllText("[x]a = 1"); + + var src = workingDir.CreateDirectory("src"); + + var repository = GitRepository.OpenRepository(src.Path, new GitEnvironment(homeDir.Path)); + + Assert.Equal(gitDir.Path, repository.CommonDirectory); + Assert.Equal(gitDir.Path, repository.GitDirectory); + Assert.Equal("1", repository.Config.GetVariableValue("x", "a")); + Assert.Empty(repository.GetSubmodules()); + Assert.Equal("0000000000000000000000000000000000000000", repository.GetHeadCommitSha()); + } + + [Fact] + public void OpenReopsitory_VersionNotSupported() + { + using var temp = new TempRoot(); + + var homeDir = temp.CreateDirectory(); + + var workingDir = temp.CreateDirectory(); + var gitDir = workingDir.CreateDirectory(".git"); + + gitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master"); + gitDir.CreateDirectory("refs").CreateDirectory("heads").CreateFile("master").WriteAllText("0000000000000000000000000000000000000000"); + gitDir.CreateDirectory("objects"); + + gitDir.CreateFile("config").WriteAllText("[core]repositoryformatversion = 1"); + + var src = workingDir.CreateDirectory("src"); + + Assert.Throws(() => GitRepository.OpenRepository(src.Path, new GitEnvironment(homeDir.Path))); + } + + [Fact] + public void Submodules() + { + using var temp = new TempRoot(); + + var workingDir = temp.CreateDirectory(); + var gitDir = workingDir.CreateDirectory(".git"); + workingDir.CreateFile(".gitmodules").WriteAllText(@" +[submodule ""S1""] + path = subs/s1 + url = http://github.com/test1 +[submodule ""S2""] + path = s2 + url = http://github.com/test2 +[submodule ""S3""] + path = s3 + url = ../repo2 +[abc ""S3""] # ignore other sections + path = s3 + url = ../repo2 +[submodule ""S2""] # use the latest + url = http://github.com/test3 +[submodule ""S4""] # ignore if path unspecified + url = http://github.com/test3 +[submodule ""S5""] # ignore if url unspecified + path = s4 +"); + + var repository = new GitRepository(new GitEnvironment("/home"), GitConfig.Empty, gitDir.Path, gitDir.Path, workingDir.Path); + + var submodules = repository.GetSubmodules(); + AssertEx.Equal(new[] + { + "S1: 'subs/s1' 'http://github.com/test1'", + "S2: 's2' 'http://github.com/test3'", + "S3: 's3' '../repo2'", + }, submodules.Select(s => $"{s.Name}: '{s.WorkingDirectoryPath}' '{s.Url}'")); + } + + [Fact] + public void ResolveReference() + { + using var temp = new TempRoot(); + + var commonDir = temp.CreateDirectory(); + var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads"); + + refsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000"); + refsHeadsDir.CreateFile("br1").WriteAllText("ref: refs/heads/br2"); + refsHeadsDir.CreateFile("br2").WriteAllText("ref: refs/heads/master"); + + Assert.Equal("0123456789ABCDEFabcdef000000000000000000", GitRepository.ResolveReference("0123456789ABCDEFabcdef000000000000000000", commonDir.Path)); + + Assert.Equal("0000000000000000000000000000000000000000", GitRepository.ResolveReference("ref: refs/heads/master", commonDir.Path)); + Assert.Equal("0000000000000000000000000000000000000000", GitRepository.ResolveReference("ref: refs/heads/br1", commonDir.Path)); + Assert.Equal("0000000000000000000000000000000000000000", GitRepository.ResolveReference("ref: refs/heads/br2", commonDir.Path)); + + Assert.Null(GitRepository.ResolveReference("ref: refs/heads/none", commonDir.Path)); + } + + [Fact] + public void ResolveReference_Errors() + { + using var temp = new TempRoot(); + + var commonDir = temp.CreateDirectory(); + var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads"); + + refsHeadsDir.CreateFile("rec1").WriteAllText("ref: refs/heads/rec2"); + refsHeadsDir.CreateFile("rec2").WriteAllText("ref: refs/heads/rec1"); + + Assert.Throws(() => GitRepository.ResolveReference("ref: refs/heads/rec1", commonDir.Path)); + Assert.Throws(() => GitRepository.ResolveReference("ref: xyz/heads/rec1", commonDir.Path)); + Assert.Throws(() => GitRepository.ResolveReference("ref: refs/heads/<>", commonDir.Path)); + Assert.Throws(() => GitRepository.ResolveReference("ref:refs/heads/rec1", commonDir.Path)); + Assert.Throws(() => GitRepository.ResolveReference("ref: refs/heads/rec1 ", commonDir.Path)); + Assert.Throws(() => GitRepository.ResolveReference("refs/heads/rec1", commonDir.Path)); + Assert.Throws(() => GitRepository.ResolveReference(new string('0', 39), commonDir.Path)); + Assert.Throws(() => GitRepository.ResolveReference(new string('0', 41), commonDir.Path)); + } + + [Fact] + public void GetHeadCommitSha() + { + using var temp = new TempRoot(); + + var commonDir = temp.CreateDirectory(); + var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads"); + refsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000"); + + var gitDir = temp.CreateDirectory(); + gitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master"); + + var repository = new GitRepository(new GitEnvironment("/home"), GitConfig.Empty, gitDir.Path, commonDir.Path, workingDirectory: null); + Assert.Equal("0000000000000000000000000000000000000000", repository.GetHeadCommitSha()); + } + + [Fact] + public void GetSubmoduleHeadCommitSha() + { + using var temp = new TempRoot(); + + var gitDir = temp.CreateDirectory(); + var workingDir = temp.CreateDirectory(); + + var submoduleGitDir = temp.CreateDirectory(); + + var submoduleWorkingDir = workingDir.CreateDirectory("sub").CreateDirectory("abc"); + submoduleWorkingDir.CreateFile(".git").WriteAllText("gitdir: " + submoduleGitDir.Path); + + var submoduleRefsHeadsDir = submoduleGitDir.CreateDirectory("refs").CreateDirectory("heads"); + submoduleRefsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000"); + submoduleGitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master"); + + var repository = new GitRepository(new GitEnvironment("/home"), GitConfig.Empty, gitDir.Path, gitDir.Path, workingDir.Path); + Assert.Equal("0000000000000000000000000000000000000000", repository.GetSubmoduleHeadCommitSha(submoduleWorkingDir.Path)); + } + } +} diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GlobTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GlobTests.cs new file mode 100644 index 00000000..f519c24c --- /dev/null +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GlobTests.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.Build.Tasks.Git.UnitTests +{ + public class GlobTests + { + [Theory] + [InlineData("?", "?")] + [InlineData("*", "")] + [InlineData("*", "a")] + [InlineData("*", "abc")] + [InlineData("a*", "abc")] + [InlineData("a**", "abc")] + [InlineData("a*************************", "abc")] + [InlineData(".", ".")] + [InlineData("./", "./")] + [InlineData(".x", ".x")] + [InlineData("?x", ".x")] + [InlineData("*x", ".x")] + [InlineData("a/**", "a/")] + [InlineData("a/**", "a/b")] + [InlineData("**/b", "b")] + [InlineData("**/a*", "abc")] + [InlineData("**/b", "a/b")] + [InlineData("a/**/b", "a/b")] + [InlineData("a/**/b", "a/x/yb/b")] + [InlineData("A/**/B/**/C/*.D", "A/z/u/B/q/r/C/z.D")] + [InlineData("A/**/B*C*D/**/E", "A/u/v/BaaCaaX/u/BoCoD/u/E")] + [InlineData("a/**/*.x", "a/b/c/d.x")] + [InlineData("a*b*c*d", "axxbyyczzd")] + [InlineData("a*?b", "abb")] + [InlineData("a*bcd", "axbbcybcd")] + [InlineData("a*bcd*", "axbbcdybcd")] + [InlineData("*/b", "/b")] + [InlineData(@"\", @"\")] + [InlineData(@"\/", @"/")] + [InlineData(@"\t", @"t")] + [InlineData(@"\?", @"?")] + [InlineData(@"\\", @"\")] + [InlineData("*", @"\")] + [InlineData("?", @"\")] + [InlineData("[a-]]", "a]")] + [InlineData("[a-]]", "-]")] + public void Matching(string pattern, string path) + { + Assert.True(Glob.IsMatch(pattern, path, ignoreCase: false, matchWildCardWithDirectorySeparator: true)); + Assert.True(Glob.IsMatch(pattern, path, ignoreCase: false, matchWildCardWithDirectorySeparator: false)); + } + + [Theory] + [InlineData("?", "/")] + [InlineData("*", "/")] + [InlineData("*", "a/")] + [InlineData("[--0]", "/")] + [InlineData("[/]", "/")] + [InlineData("a*?b", "a/b")] + [InlineData("a*?b", "ab/b")] + public void Matching_WildCardMatchesDirectorySeparator(string pattern, string path) + { + Assert.True(Glob.IsMatch(pattern, path, ignoreCase: false, matchWildCardWithDirectorySeparator: true)); + } + + [Theory] + [InlineData("?", "")] + [InlineData("?", "/")] + [InlineData("*", "/")] + [InlineData("*.txt", "")] + [InlineData("a/**", "a")] + [InlineData("a/**/*", "a")] + [InlineData("*", "a/")] + [InlineData("a*b*c*d", "axxbyyczz")] + [InlineData("a*d", "abc/de")] + [InlineData("***/b", "b")] + [InlineData("a*?b", "a/b")] + [InlineData("a*?b", "ab/b")] + [InlineData("a*bcd", "axbbcybcdz")] + [InlineData("a*bcd", "axbbcdybcd")] + [InlineData("[/]", "/")] + [InlineData("[--0]", "/")] + [InlineData("[", "[")] + [InlineData("[!", "[!")] + [InlineData("[a", "[a")] + [InlineData("[a-", "[a-")] + [InlineData("[a-]]", "]]")] + public void NonMatching(string pattern, string path) + { + Assert.False(Glob.IsMatch(pattern, path, ignoreCase: false, matchWildCardWithDirectorySeparator: false)); + } + + [Theory] + [InlineData("[][!]", new[] { '[', ']', '!' })] + [InlineData("[A-Ca-b0-1]", new[] { 'A', 'B', 'C', 'a', 'b', '0', '1' })] + [InlineData("[--0]", new[] { '-', '.', '0' }, new[] { '-', '.', '0', '/' })] // range contains '-', '.', '/', '0', but '/' should not match + [InlineData("[]-]", new[] { ']', '-' })] + [InlineData("[a-]", new[] { 'a', '-' })] + [InlineData(@"[\]", new[] { '\\' })] + [InlineData(@"[[?*\]", new[] { '[', '?', '*', '\\' })] + [InlineData("[b-a]", new[] { 'b' })] + [InlineData("[!]", new char[0])] + [InlineData("[^]", new char[0])] + [InlineData("[]", new char[0])] + [InlineData("[a-]]", new char[0])] + public void MatchingRange(string pattern, char[] matchingChars, char[] matchingCharsWildCardMatchesSeparator = null) + { + for (int i = 0; i < 255; i++) + { + var c = (char)i; + bool shouldMatch = Array.IndexOf(matchingChars, c) >= 0; + + Assert.True(shouldMatch == Glob.IsMatch(pattern, c.ToString(), ignoreCase: false, matchWildCardWithDirectorySeparator: false), + $"character: '{(i != 0 ? c.ToString() : "\\0")}' (0x{i:X2})"); + + if (matchingCharsWildCardMatchesSeparator != null) + { + shouldMatch = Array.IndexOf(matchingCharsWildCardMatchesSeparator, c) >= 0; + } + + Assert.True(shouldMatch == Glob.IsMatch(pattern, c.ToString(), ignoreCase: false, matchWildCardWithDirectorySeparator: true), + $"character: '{(i != 0 ? c.ToString() : "\\0")}' (0x{i:X2})"); + } + } + + [Theory] + [InlineData("[^/]", new[] { '/' })] + [InlineData("[^--0]", new[] { '-', '.', '/', '0' })] // range contains '-', '.', '/', '0' + public void NonMatchingRange(string pattern, char[] nonMatchingChars) + { + for (int i = 0; i < 255; i++) + { + var c = (char)i; + bool shouldMatch = Array.IndexOf(nonMatchingChars, c) < 0; + Assert.True(shouldMatch == Glob.IsMatch(pattern, c.ToString(), ignoreCase: false, matchWildCardWithDirectorySeparator: false), + $"character: '{(i != 0 ? c.ToString() : "\\0")}' (0x{i:X2})"); + + Assert.True(shouldMatch == Glob.IsMatch(pattern, c.ToString(), ignoreCase: false, matchWildCardWithDirectorySeparator: true), + $"character: '{(i != 0 ? c.ToString() : "\\0")}' (0x{i:X2})"); + } + } + + [Theory] + [InlineData("[!]a-]", new[] { ']', 'a', '-', '/' })] + public void NonMatchingRange_WildCardDoesNotMatchDirectorySeparator(string pattern, char[] nonMatchingChars) + { + for (int i = 0; i < 255; i++) + { + var c = (char)i; + bool shouldMatch = Array.IndexOf(nonMatchingChars, c) < 0; + Assert.True(shouldMatch == Glob.IsMatch(pattern, c.ToString(), ignoreCase: false, matchWildCardWithDirectorySeparator: false), + $"character: '{(i != 0 ? c.ToString() : "\\0")}' (0x{i:X2})"); + } + } + + [Theory] + [InlineData("[!]a-]", new[] { ']', 'a', '-' })] + public void NonMatchingRange_WildCardMatchesDirectorySeparator(string pattern, char[] nonMatchingChars) + { + for (int i = 0; i < 255; i++) + { + var c = (char)i; + bool shouldMatch = Array.IndexOf(nonMatchingChars, c) < 0; + Assert.True(shouldMatch == Glob.IsMatch(pattern, c.ToString(), ignoreCase: false, matchWildCardWithDirectorySeparator: true), + $"character: '{(i != 0 ? c.ToString() : "\\0")}' (0x{i:X2})"); + } + } + + [Theory] + [InlineData("[a-b0-1]", new[] { 'A', 'B', 'a', 'b', '0', '1' })] + [InlineData("[a-]", new[] { 'a', 'A', '-' })] + [InlineData("[b-a]", new[] { 'b', 'B' })] + public void MatchingRangeIgnoreCase(string pattern, char[] matchingChars) + { + for (int i = 0; i < 255; i++) + { + var c = (char)i; + bool shouldMatch = Array.IndexOf(matchingChars, c) >= 0; + Assert.True(shouldMatch == Glob.IsMatch(pattern, c.ToString(), ignoreCase: true, matchWildCardWithDirectorySeparator: false), + $"character: '{(i != 0 ? c.ToString() : "\\0")}' (0x{i:X2})"); + + Assert.True(shouldMatch == Glob.IsMatch(pattern, c.ToString(), ignoreCase: true, matchWildCardWithDirectorySeparator: true), + $"character: '{(i != 0 ? c.ToString() : "\\0")}' (0x{i:X2})"); + } + } + } +} From 1f66afcedee3d51d40e4dcf2d883503f28cbca70 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Wed, 19 Jun 2019 14:18:07 -0700 Subject: [PATCH 02/16] Move git data reader to Tasks.Git --- .../GitDataReader}/CharUtils.cs | 0 .../GitDataReader}/GitConfig.Reader.cs | 0 .../GitDataReader}/GitConfig.cs | 0 .../GitDataReader}/GitEnvironment.cs | 0 .../GitDataReader}/GitIgnore.Matcher.cs | 0 .../GitDataReader}/GitIgnore.cs | 0 .../GitDataReader}/GitRepository.cs | 0 .../GitDataReader}/GitSubmodule.cs | 0 .../Managed => Microsoft.Build.Tasks.Git/GitDataReader}/Glob.cs | 0 .../GitDataReader}/PathUtils.cs | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/CharUtils.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/GitConfig.Reader.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/GitConfig.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/GitEnvironment.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/GitIgnore.Matcher.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/GitIgnore.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/GitRepository.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/GitSubmodule.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/Glob.cs (100%) rename src/{Microsoft.Build.Tasks.Git.Operations/Managed => Microsoft.Build.Tasks.Git/GitDataReader}/PathUtils.cs (100%) diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/CharUtils.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/CharUtils.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/CharUtils.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/CharUtils.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.Reader.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.Reader.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/GitConfig.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitEnvironment.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitEnvironment.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/GitEnvironment.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/GitEnvironment.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.Matcher.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.Matcher.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.Matcher.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.Matcher.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/GitIgnore.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitRepository.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/GitRepository.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/GitSubmodule.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/GitSubmodule.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/Glob.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/Glob.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/Glob.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/Glob.cs diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Managed/PathUtils.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/PathUtils.cs similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Managed/PathUtils.cs rename to src/Microsoft.Build.Tasks.Git/GitDataReader/PathUtils.cs From 799c2445c63c3dcf6168d919cf6e3a567f305975 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Wed, 19 Jun 2019 14:28:31 -0700 Subject: [PATCH 03/16] Remove Operations --- SourceLink.sln | 10 +- ...icrosoft.Build.Tasks.Git.Operations.csproj | 1 - ...Microsoft.Build.Tasks.Git.UnitTests.csproj | 3 +- ...kImplementation.cs => AssemblyResolver.cs} | 34 +- .../GetRepositoryUrl.cs | 2 +- .../GetSourceRevisionId.cs | 2 +- .../GetSourceRoots.cs | 2 +- .../GetUntrackedFiles.cs | 2 +- .../GitLoaderContext.cs | 81 ---- .../GitOperations.cs | 5 +- .../LocateRepository.cs | 2 +- .../Microsoft.Build.Tasks.Git.csproj | 10 + .../Microsoft.Build.Tasks.Git.nuspec | 0 .../RepositoryTasks.cs | 7 +- src/Microsoft.Build.Tasks.Git/RuntimeIdMap.cs | 378 ------------------ .../build/Microsoft.Build.Tasks.Git.props | 0 .../build/Microsoft.Build.Tasks.Git.targets | 0 ...oft.SourceLink.Git.IntegrationTests.csproj | 1 - 18 files changed, 28 insertions(+), 512 deletions(-) rename src/Microsoft.Build.Tasks.Git/{TaskImplementation.cs => AssemblyResolver.cs} (53%) delete mode 100644 src/Microsoft.Build.Tasks.Git/GitLoaderContext.cs rename src/{Microsoft.Build.Tasks.Git.Operations => Microsoft.Build.Tasks.Git}/GitOperations.cs (98%) rename src/{Microsoft.Build.Tasks.Git.Operations => Microsoft.Build.Tasks.Git}/Microsoft.Build.Tasks.Git.nuspec (100%) rename src/{Microsoft.Build.Tasks.Git.Operations => Microsoft.Build.Tasks.Git}/RepositoryTasks.cs (93%) delete mode 100644 src/Microsoft.Build.Tasks.Git/RuntimeIdMap.cs rename src/{Microsoft.Build.Tasks.Git.Operations => Microsoft.Build.Tasks.Git}/build/Microsoft.Build.Tasks.Git.props (100%) rename src/{Microsoft.Build.Tasks.Git.Operations => Microsoft.Build.Tasks.Git}/build/Microsoft.Build.Tasks.Git.targets (100%) diff --git a/SourceLink.sln b/SourceLink.sln index 4ea4d37a..1d2036b1 100644 --- a/SourceLink.sln +++ b/SourceLink.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27214.1 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29011.400 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Tasks.Git", "src\Microsoft.Build.Tasks.Git\Microsoft.Build.Tasks.Git.csproj", "{A86F9DC3-9595-44AC-ACC6-025FB74813E6}" EndProject @@ -29,8 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitHub EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.Vsts.Git.UnitTests", "src\SourceLink.Vsts.Git.UnitTests\Microsoft.SourceLink.Vsts.Git.UnitTests.csproj", "{60C82684-6A13-4AEF-A4F5-C429BEDE1913}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Tasks.Git.Operations", "src\Microsoft.Build.Tasks.Git.Operations\Microsoft.Build.Tasks.Git.Operations.csproj", "{BC24CED9-324E-4AF9-939F-BDB0C2C5F644}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitLab", "src\SourceLink.GitLab\Microsoft.SourceLink.GitLab.csproj", "{B8F63D05-BF7E-4F09-B87F-2FD2E6D58149}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitLab.UnitTests", "src\SourceLink.GitLab.UnitTests\Microsoft.SourceLink.GitLab.UnitTests.csproj", "{46C6BD7C-ABB7-4444-B095-C63868FACC41}" @@ -113,10 +111,6 @@ Global {60C82684-6A13-4AEF-A4F5-C429BEDE1913}.Debug|Any CPU.Build.0 = Debug|Any CPU {60C82684-6A13-4AEF-A4F5-C429BEDE1913}.Release|Any CPU.ActiveCfg = Release|Any CPU {60C82684-6A13-4AEF-A4F5-C429BEDE1913}.Release|Any CPU.Build.0 = Release|Any CPU - {BC24CED9-324E-4AF9-939F-BDB0C2C5F644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BC24CED9-324E-4AF9-939F-BDB0C2C5F644}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BC24CED9-324E-4AF9-939F-BDB0C2C5F644}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BC24CED9-324E-4AF9-939F-BDB0C2C5F644}.Release|Any CPU.Build.0 = Release|Any CPU {B8F63D05-BF7E-4F09-B87F-2FD2E6D58149}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B8F63D05-BF7E-4F09-B87F-2FD2E6D58149}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8F63D05-BF7E-4F09-B87F-2FD2E6D58149}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.Operations.csproj b/src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.Operations.csproj index cc9f016b..9a6d5931 100644 --- a/src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.Operations.csproj +++ b/src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.Operations.csproj @@ -19,7 +19,6 @@ - diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Microsoft.Build.Tasks.Git.UnitTests.csproj b/src/Microsoft.Build.Tasks.Git.UnitTests/Microsoft.Build.Tasks.Git.UnitTests.csproj index bf1de60d..c519c135 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/Microsoft.Build.Tasks.Git.UnitTests.csproj +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/Microsoft.Build.Tasks.Git.UnitTests.csproj @@ -3,11 +3,10 @@ net461;netcoreapp2.0 - - + diff --git a/src/Microsoft.Build.Tasks.Git/TaskImplementation.cs b/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs similarity index 53% rename from src/Microsoft.Build.Tasks.Git/TaskImplementation.cs rename to src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs index f9d6628c..6d327771 100644 --- a/src/Microsoft.Build.Tasks.Git/TaskImplementation.cs +++ b/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#if NET461 using System; using System.Collections.Generic; @@ -7,43 +8,19 @@ namespace Microsoft.Build.Tasks.Git { - internal static class TaskImplementation + internal static class AssemblyResolver { - public static Func LocateRepository; - public static Func GetRepositoryUrl; - public static Func GetSourceRevisionId; - public static Func GetSourceRoots; - public static Func GetUntrackedFiles; - private static readonly string s_taskDirectory; - private const string GitOperationsAssemblyName = "Microsoft.Build.Tasks.Git.Operations"; - static TaskImplementation() + static AssemblyResolver() { - s_taskDirectory = Path.GetDirectoryName(typeof(TaskImplementation).Assembly.Location); -#if NET461 + s_taskDirectory = Path.GetDirectoryName(typeof(AssemblyResolver).Assembly.Location); s_nullVersion = new Version(0, 0, 0, 0); s_loaderLog = new List(); AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve; - - var assemblyName = typeof(TaskImplementation).Assembly.GetName(); - assemblyName.Name = GitOperationsAssemblyName; - var assembly = Assembly.Load(assemblyName); -#else - var operationsPath = Path.Combine(s_taskDirectory, GitOperationsAssemblyName + ".dll"); - var assembly = GitLoaderContext.Instance.LoadFromAssemblyPath(operationsPath); -#endif - var type = assembly.GetType("Microsoft.Build.Tasks.Git.RepositoryTasks", throwOnError: true).GetTypeInfo(); - - LocateRepository = (Func)type.GetDeclaredMethod(nameof(LocateRepository)).CreateDelegate(typeof(Func)); - GetRepositoryUrl = (Func)type.GetDeclaredMethod(nameof(GetRepositoryUrl)).CreateDelegate(typeof(Func)); - GetSourceRevisionId = (Func)type.GetDeclaredMethod(nameof(GetSourceRevisionId)).CreateDelegate(typeof(Func)); - GetSourceRoots = (Func)type.GetDeclaredMethod(nameof(GetSourceRoots)).CreateDelegate(typeof(Func)); - GetUntrackedFiles = (Func)type.GetDeclaredMethod(nameof(GetUntrackedFiles)).CreateDelegate(typeof(Func)); } -#if NET461 private static readonly Version s_nullVersion; private static readonly List s_loaderLog; @@ -91,6 +68,7 @@ private static Assembly AssemblyResolve(object sender, ResolveEventArgs args) Log(args, $"loading from '{referencePath}'"); return Assembly.Load(AssemblyName.GetAssemblyName(referencePath)); } -#endif } } +#endif + diff --git a/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs b/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs index d8d4d780..3e79be92 100644 --- a/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs +++ b/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs @@ -11,6 +11,6 @@ public sealed class GetRepositoryUrl : RepositoryTask [Output] public string Url { get; internal set; } - public override bool Execute() => TaskImplementation.GetRepositoryUrl(this); + public override bool Execute() => RepositoryTasks.GetRepositoryUrl(this); } } diff --git a/src/Microsoft.Build.Tasks.Git/GetSourceRevisionId.cs b/src/Microsoft.Build.Tasks.Git/GetSourceRevisionId.cs index d3fd8e0e..1c77d036 100644 --- a/src/Microsoft.Build.Tasks.Git/GetSourceRevisionId.cs +++ b/src/Microsoft.Build.Tasks.Git/GetSourceRevisionId.cs @@ -9,6 +9,6 @@ public sealed class GetSourceRevisionId : RepositoryTask [Output] public string RevisionId { get; internal set; } - public override bool Execute() => TaskImplementation.GetSourceRevisionId(this); + public override bool Execute() => RepositoryTasks.GetSourceRevisionId(this); } } diff --git a/src/Microsoft.Build.Tasks.Git/GetSourceRoots.cs b/src/Microsoft.Build.Tasks.Git/GetSourceRoots.cs index f81ef95e..0bd29243 100644 --- a/src/Microsoft.Build.Tasks.Git/GetSourceRoots.cs +++ b/src/Microsoft.Build.Tasks.Git/GetSourceRoots.cs @@ -20,6 +20,6 @@ public sealed class GetSourceRoots : RepositoryTask [Output] public ITaskItem[] Roots { get; internal set; } - public override bool Execute() => TaskImplementation.GetSourceRoots(this); + public override bool Execute() => RepositoryTasks.GetSourceRoots(this); } } diff --git a/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs b/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs index b4c81f09..03232081 100644 --- a/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs +++ b/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs @@ -18,6 +18,6 @@ public sealed class GetUntrackedFiles : RepositoryTask [Output] public ITaskItem[] UntrackedFiles { get; set; } - public override bool Execute() => TaskImplementation.GetUntrackedFiles(this); + public override bool Execute() => RepositoryTasks.GetUntrackedFiles(this); } } diff --git a/src/Microsoft.Build.Tasks.Git/GitLoaderContext.cs b/src/Microsoft.Build.Tasks.Git/GitLoaderContext.cs deleted file mode 100644 index 17375efa..00000000 --- a/src/Microsoft.Build.Tasks.Git/GitLoaderContext.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -#if !NET461 -using System; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.Loader; -using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; - -namespace Microsoft.Build.Tasks.Git -{ - internal sealed class GitLoaderContext : AssemblyLoadContext - { - public static readonly GitLoaderContext Instance = new GitLoaderContext(); - - protected override Assembly Load(AssemblyName assemblyName) - { - if (assemblyName.Name == "LibGit2Sharp") - { - var path = Path.Combine(Path.GetDirectoryName(typeof(TaskImplementation).Assembly.Location), assemblyName.Name + ".dll"); - return LoadFromAssemblyPath(path); - } - - return Default.LoadFromAssemblyName(assemblyName); - } - - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - var modulePtr = IntPtr.Zero; - - if (unmanagedDllName.StartsWith("git2-", StringComparison.Ordinal) || - unmanagedDllName.StartsWith("libgit2-", StringComparison.Ordinal)) - { - var directory = GetNativeLibraryDirectory(); - var extension = GetNativeLibraryExtension(); - - if (!unmanagedDllName.EndsWith(extension, StringComparison.Ordinal)) - { - unmanagedDllName += extension; - } - - var nativeLibraryPath = Path.Combine(directory, unmanagedDllName); - if (!File.Exists(nativeLibraryPath)) - { - nativeLibraryPath = Path.Combine(directory, "lib" + unmanagedDllName); - } - - modulePtr = LoadUnmanagedDllFromPath(nativeLibraryPath); - } - - return (modulePtr != IntPtr.Zero) ? modulePtr : base.LoadUnmanagedDll(unmanagedDllName); - } - - internal static string GetNativeLibraryDirectory() - { - var dir = Path.GetDirectoryName(typeof(GitLoaderContext).Assembly.Location); - return Path.Combine(dir, "runtimes", RuntimeIdMap.GetNativeLibraryDirectoryName(RuntimeEnvironment.GetRuntimeIdentifier()), "native"); - } - - private static string GetNativeLibraryExtension() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return ".dll"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return ".dylib"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return ".so"; - } - - throw new PlatformNotSupportedException(); - } - } -} -#endif \ No newline at end of file diff --git a/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs similarity index 98% rename from src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs rename to src/Microsoft.Build.Tasks.Git/GitOperations.cs index 2a692f80..4e17dc55 100644 --- a/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs +++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs @@ -5,8 +5,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; -using LibGit2Sharp; using Microsoft.Build.Framework; using Microsoft.Build.Tasks.SourceControl; using Microsoft.Build.Utilities; @@ -17,13 +15,12 @@ internal static class GitOperations { private const string SourceControlName = "git"; - [MethodImpl(MethodImplOptions.NoInlining)] public static string LocateRepository(string directory) { // Repository.Discover returns the path to .git directory for repositories with a working directory. // For bare repositories it returns the repository directory. // Returns null if the path is invalid or no repository is found. - return Repository.Discover(directory); + return GitRepository.LocateRepository((directory); } internal static IRepository CreateRepository(string root) diff --git a/src/Microsoft.Build.Tasks.Git/LocateRepository.cs b/src/Microsoft.Build.Tasks.Git/LocateRepository.cs index d536f6bd..af71f2a5 100644 --- a/src/Microsoft.Build.Tasks.Git/LocateRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/LocateRepository.cs @@ -15,6 +15,6 @@ public class LocateRepository : Task [Output] public string Id { get; set; } - public override bool Execute() => TaskImplementation.LocateRepository(this); + public override bool Execute() => RepositoryTasks.LocateRepository(this); } } diff --git a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj index 33cdb323..c4d76941 100644 --- a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj +++ b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj @@ -2,6 +2,16 @@ net461;netcoreapp2.0 true + + + true + Microsoft.Build.Tasks.Git + Microsoft.Build.Tasks.Git.nuspec + $(OutputPath) + + MSBuild tasks providing git repository information. + MSBuild Tasks source control git + true diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.nuspec b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.nuspec similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.nuspec rename to src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.nuspec diff --git a/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs b/src/Microsoft.Build.Tasks.Git/RepositoryTasks.cs similarity index 93% rename from src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs rename to src/Microsoft.Build.Tasks.Git/RepositoryTasks.cs index 5c4068f1..7aaeaff0 100644 --- a/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs +++ b/src/Microsoft.Build.Tasks.Git/RepositoryTasks.cs @@ -2,13 +2,12 @@ using System; using System.IO; -using LibGit2Sharp; namespace Microsoft.Build.Tasks.Git { internal static class RepositoryTasks { - private static bool Execute(T task, Action action) + private static bool Execute(T task, Action action) where T: RepositoryTask { var log = task.Log; @@ -19,7 +18,7 @@ private static bool Execute(T task, Action action) return true; } - IRepository repo; + GitRepository repo; try { repo = GitOperations.CreateRepository(task.Root); @@ -60,7 +59,7 @@ public static bool LocateRepository(LocateRepository task) catch (Exception e) { #if NET461 - foreach (var message in TaskImplementation.GetLog()) + foreach (var message in AssemblyResolver.GetLog()) { task.Log.LogMessage(message); } diff --git a/src/Microsoft.Build.Tasks.Git/RuntimeIdMap.cs b/src/Microsoft.Build.Tasks.Git/RuntimeIdMap.cs deleted file mode 100644 index 45882567..00000000 --- a/src/Microsoft.Build.Tasks.Git/RuntimeIdMap.cs +++ /dev/null @@ -1,378 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -#if !NET461 - -using System; -using System.Diagnostics; - -namespace Microsoft.Build.Tasks.Git -{ - internal static class RuntimeIdMap - { - // This functionality needs to be provided as .NET Core API. - // Releated issues: - // https://github.com/dotnet/core-setup/issues/1846 - // https://github.com/NuGet/Home/issues/5862 - - public static string GetNativeLibraryDirectoryName(string runtimeIdentifier) - { -#if DEBUG - Debug.Assert(s_directories.Length == s_rids.Length); - - for (int i = 1; i < s_rids.Length; i++) - { - Debug.Assert(StringComparer.Ordinal.Compare(s_rids[i - 1], s_rids[i]) < 0); - } -#endif - int index = Array.BinarySearch(s_rids, runtimeIdentifier, StringComparer.Ordinal); - if (index < 0) - { - // Take the runtime id with highest version of matching OS. - // The runtimes in the table are currently sorted so that this works. - - ParseRuntimeId(runtimeIdentifier, out var runtimeOS, out var runtimeVersion, out var runtimeQualifiers); - - // find version-less rid: - int bestMatchIndex = -1; - string[] bestVersion = null; - - void FindBestCandidate(int startIndex, int increment) - { - int i = startIndex; - while (i >= 0 && i < s_rids.Length) - { - string candidate = s_rids[i]; - ParseRuntimeId(candidate, out var candidateOS, out var candidateVersion, out var candidateQualifiers); - if (candidateOS != runtimeOS) - { - break; - } - - // Find the highest available version that is lower than or equal to the runtime version - // among candidates that have the same qualifiers. - if (candidateQualifiers == runtimeQualifiers && - CompareVersions(candidateVersion, runtimeVersion) <= 0 && - (bestVersion == null || CompareVersions(candidateVersion, bestVersion) > 0)) - { - bestMatchIndex = i; - bestVersion = candidateVersion; - } - - i += increment; - } - } - - FindBestCandidate(~index - 1, -1); - FindBestCandidate(~index, +1); - - if (bestMatchIndex < 0) - { - throw new PlatformNotSupportedException(runtimeIdentifier); - } - - index = bestMatchIndex; - } - - return s_directories[index]; - } - - internal static int CompareVersions(string[] left, string[] right) - { - for (int i = 0; i < Math.Max(left.Length, right.Length); i++) - { - // pad with zeros (consider "1.2" == "1.2.0") - var leftPart = (i < left.Length) ? left[i] : "0"; - var rightPart = (i < right.Length) ? right[i] : "0"; - - int result; - if (!int.TryParse(leftPart, out var leftNumber) || !int.TryParse(rightPart, out var rightNumber)) - { - // alphabetical order: - result = StringComparer.Ordinal.Compare(leftPart, rightPart); - } - else - { - // numerical order: - result = leftNumber.CompareTo(rightNumber); - } - - if (result != 0) - { - return result; - } - } - - return 0; - } - - internal static void ParseRuntimeId(string runtimeId, out string osName, out string[] version, out string qualifiers) - { - // We use the following convention in all newly-defined RIDs. Some RIDs (win7-x64, win8-x64) predate this convention and don't follow it, but all new RIDs should follow it. - // [os name].[version]-[architecture]-[additional qualifiers] - // See https://github.com/dotnet/corefx/blob/master/pkg/Microsoft.NETCore.Platforms/readme.md#naming-convention - - int versionSeparator = runtimeId.IndexOf('.'); - if (versionSeparator >= 0) - { - osName = runtimeId.Substring(0, versionSeparator); - } - else - { - osName = null; - } - - int architectureSeparator = runtimeId.IndexOf('-', versionSeparator + 1); - if (architectureSeparator >= 0) - { - if (versionSeparator >= 0) - { - version = runtimeId.Substring(versionSeparator + 1, architectureSeparator - versionSeparator - 1).Split('.'); - } - else - { - osName = runtimeId.Substring(0, architectureSeparator); - version = Array.Empty(); - } - - qualifiers = runtimeId.Substring(architectureSeparator + 1); - } - else - { - if (versionSeparator >= 0) - { - version = runtimeId.Substring(versionSeparator + 1).Split('.'); - } - else - { - osName = runtimeId; - version = Array.Empty(); - } - - qualifiers = string.Empty; - } - } - - // The following tables were generated by scripts/RuntimeIdMapGenerator.csx. - // Regenerate when upgrading LibGit2Sharp to a new version that supports more platforms. - - private static readonly string[] s_rids = new[] - { - "alpine-x64", - "alpine.3.6-x64", - "alpine.3.7-x64", - "alpine.3.8-x64", - "alpine.3.9-x64", - "centos-x64", - "centos.7-x64", - "debian-x64", - "debian.8-x64", - "debian.9-x64", - "fedora-x64", - "fedora.23-x64", - "fedora.24-x64", - "fedora.25-x64", - "fedora.26-x64", - "fedora.27-x64", - "fedora.28-x64", - "fedora.29-x64", - "gentoo-x64", - "linux-musl-x64", - "linux-x64", - "linuxmint.17-x64", - "linuxmint.17.1-x64", - "linuxmint.17.2-x64", - "linuxmint.17.3-x64", - "linuxmint.18-x64", - "linuxmint.18.1-x64", - "linuxmint.18.2-x64", - "linuxmint.18.3-x64", - "linuxmint.19-x64", - "ol-x64", - "ol.7-x64", - "ol.7.0-x64", - "ol.7.1-x64", - "ol.7.2-x64", - "ol.7.3-x64", - "ol.7.4-x64", - "ol.7.5-x64", - "ol.7.6-x64", - "opensuse-x64", - "opensuse.13.2-x64", - "opensuse.15.0-x64", - "opensuse.42.1-x64", - "opensuse.42.2-x64", - "opensuse.42.3-x64", - "osx", - "osx-x64", - "osx.10.10", - "osx.10.10-x64", - "osx.10.11", - "osx.10.11-x64", - "osx.10.12", - "osx.10.12-x64", - "osx.10.13", - "osx.10.13-x64", - "osx.10.14", - "osx.10.14-x64", - "rhel-x64", - "rhel.6-x64", - "rhel.7-x64", - "rhel.7.0-x64", - "rhel.7.1-x64", - "rhel.7.2-x64", - "rhel.7.3-x64", - "rhel.7.4-x64", - "rhel.7.5-x64", - "rhel.7.6-x64", - "rhel.8-x64", - "rhel.8.0-x64", - "sles-x64", - "sles.12-x64", - "sles.12.1-x64", - "sles.12.2-x64", - "sles.12.3-x64", - "sles.15-x64", - "ubuntu-x64", - "ubuntu.14.04-x64", - "ubuntu.14.10-x64", - "ubuntu.15.04-x64", - "ubuntu.15.10-x64", - "ubuntu.16.04-x64", - "ubuntu.16.10-x64", - "ubuntu.17.04-x64", - "ubuntu.17.10-x64", - "ubuntu.18.04-x64", - "ubuntu.18.10-x64", - "win-x64", - "win-x64-aot", - "win-x86", - "win-x86-aot", - "win10-x64", - "win10-x64-aot", - "win10-x86", - "win10-x86-aot", - "win7-x64", - "win7-x64-aot", - "win7-x86", - "win7-x86-aot", - "win8-x64", - "win8-x64-aot", - "win8-x86", - "win8-x86-aot", - "win81-x64", - "win81-x64-aot", - "win81-x86", - "win81-x86-aot", - }; - - private static readonly string[] s_directories = new[] - { - "alpine-x64", - "alpine-x64", - "alpine-x64", - "alpine-x64", - "alpine-x64", - "rhel-x64", - "rhel-x64", - "linux-x64", - "linux-x64", - "debian.9-x64", - "fedora-x64", - "fedora-x64", - "fedora-x64", - "fedora-x64", - "fedora-x64", - "fedora-x64", - "fedora-x64", - "fedora-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "ubuntu.18.04-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "osx", - "osx", - "osx", - "osx", - "osx", - "osx", - "osx", - "osx", - "osx", - "osx", - "osx", - "osx", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "rhel-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "linux-x64", - "ubuntu.18.04-x64", - "linux-x64", - "win-x64", - "win-x64", - "win-x86", - "win-x86", - "win-x64", - "win-x64", - "win-x86", - "win-x86", - "win-x64", - "win-x64", - "win-x86", - "win-x86", - "win-x64", - "win-x64", - "win-x86", - "win-x86", - "win-x64", - "win-x64", - "win-x86", - "win-x86", - }; - } -} -#endif \ No newline at end of file diff --git a/src/Microsoft.Build.Tasks.Git.Operations/build/Microsoft.Build.Tasks.Git.props b/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.props similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/build/Microsoft.Build.Tasks.Git.props rename to src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.props diff --git a/src/Microsoft.Build.Tasks.Git.Operations/build/Microsoft.Build.Tasks.Git.targets b/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets similarity index 100% rename from src/Microsoft.Build.Tasks.Git.Operations/build/Microsoft.Build.Tasks.Git.targets rename to src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets diff --git a/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj b/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj index 551518f9..efc7ea5d 100644 --- a/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj +++ b/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj @@ -4,7 +4,6 @@ - From ff0c01b17da494596471d79f22ffdc4781d180fa Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Wed, 19 Jun 2019 18:51:12 -0700 Subject: [PATCH 04/16] Use managed git data reader implementation --- .../GitRepositoryTests.cs | 2 +- .../AssemblyResolver.cs | 48 ++--- .../GetRepositoryUrl.cs | 16 -- .../GetSourceRevisionId.cs | 14 -- .../GetSourceRoots.cs | 25 --- .../GetUntrackedFiles.cs | 8 +- .../GitDataReader/GitConfig.Reader.cs | 8 +- .../GitDataReader/GitEnvironment.cs | 2 +- .../GitDataReader/GitIgnore.Matcher.cs | 6 +- .../GitDataReader/GitRepository.cs | 88 ++++++-- .../GitDataReader/GitSubmodule.cs | 12 +- .../GitOperations.cs | 198 +++++++----------- .../LocateRepository.cs | 41 +++- .../RepositoryTask.cs | 95 ++++++++- .../RepositoryTasks.cs | 104 --------- src/Microsoft.Build.Tasks.Git/Resources.resx | 62 +++++- .../build/Microsoft.Build.Tasks.Git.targets | 24 +-- .../xlf/Resources.cs.xlf | 98 +++++++-- .../xlf/Resources.de.xlf | 98 +++++++-- .../xlf/Resources.es.xlf | 98 +++++++-- .../xlf/Resources.fr.xlf | 98 +++++++-- .../xlf/Resources.it.xlf | 98 +++++++-- .../xlf/Resources.ja.xlf | 98 +++++++-- .../xlf/Resources.ko.xlf | 98 +++++++-- .../xlf/Resources.pl.xlf | 98 +++++++-- .../xlf/Resources.pt-BR.xlf | 98 +++++++-- .../xlf/Resources.ru.xlf | 98 +++++++-- .../xlf/Resources.tr.xlf | 98 +++++++-- .../xlf/Resources.zh-Hans.xlf | 98 +++++++-- .../xlf/Resources.zh-Hant.xlf | 98 +++++++-- 30 files changed, 1471 insertions(+), 556 deletions(-) delete mode 100644 src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs delete mode 100644 src/Microsoft.Build.Tasks.Git/GetSourceRevisionId.cs delete mode 100644 src/Microsoft.Build.Tasks.Git/GetSourceRoots.cs delete mode 100644 src/Microsoft.Build.Tasks.Git/RepositoryTasks.cs diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs index 0236cf2a..535d6664 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs @@ -199,7 +199,7 @@ public void Submodules() "S1: 'subs/s1' 'http://github.com/test1'", "S2: 's2' 'http://github.com/test3'", "S3: 's3' '../repo2'", - }, submodules.Select(s => $"{s.Name}: '{s.WorkingDirectoryPath}' '{s.Url}'")); + }, submodules.Select(s => $"{s.Name}: '{s.WorkingDirectoryRelativePath}' '{s.Url}'")); } [Fact] diff --git a/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs b/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs index 6d327771..b0ec16f1 100644 --- a/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs +++ b/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs @@ -1,29 +1,27 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + #if NET461 using System; using System.Collections.Generic; using System.IO; using System.Reflection; +using Microsoft.Build.Framework; namespace Microsoft.Build.Tasks.Git { internal static class AssemblyResolver { - private static readonly string s_taskDirectory; + private static readonly string s_taskDirectory = Path.GetDirectoryName(typeof(AssemblyResolver).Assembly.Location); + private static readonly List s_loaderLog; - static AssemblyResolver() + public static void Initialize() { - s_taskDirectory = Path.GetDirectoryName(typeof(AssemblyResolver).Assembly.Location); - s_nullVersion = new Version(0, 0, 0, 0); - s_loaderLog = new List(); - AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve; } - private static readonly Version s_nullVersion; - private static readonly List s_loaderLog; - private static void Log(ResolveEventArgs args, string outcome) { lock (s_loaderLog) @@ -42,33 +40,35 @@ internal static string[] GetLog() private static Assembly AssemblyResolve(object sender, ResolveEventArgs args) { - // Limit resolution scope to minimum to affect the rest of msbuild as little as possible. - // Only resolve System.* assemblies from the task directory that are referenced with 0.0.0.0 version (from netstandard.dll). + var name = new AssemblyName(args.Name); - var referenceName = new AssemblyName(args.Name); - if (!referenceName.Name.StartsWith("System.", StringComparison.OrdinalIgnoreCase)) + if (!name.Name.Equals("System.Collections.Immutable", StringComparison.OrdinalIgnoreCase)) { - Log(args, "not System"); return null; } - if (referenceName.Version != s_nullVersion) + var fullPath = Path.Combine(s_taskDirectory, "System.Collections.Immutable.dll"); + + Assembly sci; + try { - Log(args, "not null version"); + sci = Assembly.LoadFile(fullPath); + } + catch (Exception e) + { + Log(args, $"exception while loading '{fullPath}': {e.Message}"); return null; } - var referencePath = Path.Combine(s_taskDirectory, referenceName.Name + ".dll"); - if (!File.Exists(referencePath)) + if (name.Version <= sci.GetName().Version) { - Log(args, $"file '{referencePath}' not found"); - return null; + Log(args, $"loaded '{fullPath}' to {AppDomain.CurrentDomain.FriendlyName}"); + return sci; } - Log(args, $"loading from '{referencePath}'"); - return Assembly.Load(AssemblyName.GetAssemblyName(referencePath)); + return null; } } } -#endif +#endif diff --git a/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs b/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs deleted file mode 100644 index 3e79be92..00000000 --- a/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.Build.Framework; - -namespace Microsoft.Build.Tasks.Git -{ - public sealed class GetRepositoryUrl : RepositoryTask - { - public string RemoteName { get; set; } - - [Output] - public string Url { get; internal set; } - - public override bool Execute() => RepositoryTasks.GetRepositoryUrl(this); - } -} diff --git a/src/Microsoft.Build.Tasks.Git/GetSourceRevisionId.cs b/src/Microsoft.Build.Tasks.Git/GetSourceRevisionId.cs deleted file mode 100644 index 1c77d036..00000000 --- a/src/Microsoft.Build.Tasks.Git/GetSourceRevisionId.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.Build.Framework; - -namespace Microsoft.Build.Tasks.Git -{ - public sealed class GetSourceRevisionId : RepositoryTask - { - [Output] - public string RevisionId { get; internal set; } - - public override bool Execute() => RepositoryTasks.GetSourceRevisionId(this); - } -} diff --git a/src/Microsoft.Build.Tasks.Git/GetSourceRoots.cs b/src/Microsoft.Build.Tasks.Git/GetSourceRoots.cs deleted file mode 100644 index 0bd29243..00000000 --- a/src/Microsoft.Build.Tasks.Git/GetSourceRoots.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.Build.Framework; - -namespace Microsoft.Build.Tasks.Git -{ - public sealed class GetSourceRoots : RepositoryTask - { - /// - /// Returns items describing repository source roots: - /// - /// Metadata - /// Identity: Normalized path. Ends with a directory separator. - /// SourceControl: "Git" - /// RepositoryUrl: URL of the repository. - /// RevisionId: Revision (commit SHA). - /// ContainingRoot: Identity of the containing source root. - /// NestedRoot: For a submodule root, a path of the submodule root relative to the repository root. Ends with a slash. - /// - [Output] - public ITaskItem[] Roots { get; internal set; } - - public override bool Execute() => RepositoryTasks.GetSourceRoots(this); - } -} diff --git a/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs b/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs index 03232081..a186ef84 100644 --- a/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs +++ b/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs @@ -16,8 +16,12 @@ public sealed class GetUntrackedFiles : RepositoryTask public string ProjectDirectory { get; set; } [Output] - public ITaskItem[] UntrackedFiles { get; set; } + public ITaskItem[] UntrackedFiles { get; private set; } - public override bool Execute() => RepositoryTasks.GetUntrackedFiles(this); + private protected override void Execute(GitRepository repository) + { + UntrackedFiles = GitOperations.GetUntrackedFiles( + repository, Files, ProjectDirectory, dir => GitRepository.OpenRepository(dir, repository.Environment)); + } } } diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs index 669b1032..43853443 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs @@ -130,7 +130,7 @@ internal void LoadVariablesFrom(string path, Dictionary MaxIncludeDepth) { - throw new InvalidDataException($"Configuration files recursion exceeded maximum allowed depth of {MaxIncludeDepth}"); + throw new InvalidDataException(string.Format(Resources.ConfigurationFileRecursionExceededMaximumAllowedDepth, MaxIncludeDepth)); } TextReader reader; @@ -191,7 +191,7 @@ internal void LoadVariablesFrom(string path, Dictionary - private string NormalizeRelativePath(string relativePath, string basePath) + private string NormalizeRelativePath(string relativePath, string basePath, VariableKey key) { string root; if (relativePath.Length >= 2 && relativePath[0] == '~' && PathUtils.IsDirectorySeparator(relativePath[1])) @@ -218,7 +218,7 @@ private string NormalizeRelativePath(string relativePath, string basePath) } catch { - throw new InvalidDataException($"Invalid path: {relativePath}"); + throw new InvalidDataException(string.Format(Resources.ValueOfIsNotValidPath, key.ToString(), relativePath)); } } diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitEnvironment.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitEnvironment.cs index 4fd0d3b5..129fdf2a 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitEnvironment.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitEnvironment.cs @@ -89,7 +89,7 @@ public static string FindWindowsGitInstallation() return Path.GetDirectoryName(gitCmd); } -#if REGISTRY +#if REGISTRY // TODO string registryInstallLocation; try { diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.Matcher.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.Matcher.cs index f17e3283..ab066189 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.Matcher.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.Matcher.cs @@ -69,12 +69,12 @@ private PatternGroup GetPatternGroup(string directory) { if (!PathUtils.IsAbsolute(fullPath)) { - throw new ArgumentException("Path must be absolute", nameof(fullPath)); + throw new ArgumentException(Resources.PathMustBeAbsolute, nameof(fullPath)); } if (PathUtils.HasTrailingDirectorySeparator(fullPath)) { - throw new ArgumentException("Path must be a file path", nameof(fullPath)); + throw new ArgumentException(Resources.PathMustBeFilePath, nameof(fullPath)); } return IsPathIgnored(PathUtils.ToPosixPath(fullPath), isDirectoryPath: false); @@ -89,7 +89,7 @@ private PatternGroup GetPatternGroup(string directory) { if (!PathUtils.IsAbsolute(fullPath)) { - throw new ArgumentException("Path must be absolute", nameof(fullPath)); + throw new ArgumentException(Resources.PathMustBeAbsolute, nameof(fullPath)); } // git uses the FS case-sensitivity for checking directory existence: diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs index e8929138..179e73b2 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs @@ -11,6 +11,21 @@ namespace Microsoft.Build.Tasks.Git { internal sealed class GitRepository { + private readonly struct SubmoduleInfo + { + public readonly ImmutableArray Submodules; + public readonly ImmutableArray Diagnostics; + + public SubmoduleInfo(ImmutableArray submodules, ImmutableArray diagnostics) + { + Debug.Assert(!submodules.IsDefault); + Debug.Assert(!diagnostics.IsDefault); + + Submodules = submodules; + Diagnostics = diagnostics; + } + } + private const int SupportedGitRepoFormatVersion = 0; private const string CommonDirFileName = "commondir"; @@ -44,7 +59,7 @@ internal sealed class GitRepository public GitEnvironment Environment { get; } - private readonly Lazy> _submodules; + private readonly Lazy _submodules; private readonly Lazy _gitIgnore; internal GitRepository(GitEnvironment environment, GitConfig config, string gitDirectory, string commonDirectory, string workingDirectory) @@ -60,7 +75,7 @@ internal GitRepository(GitEnvironment environment, GitConfig config, string gitD WorkingDirectory = workingDirectory; Environment = environment; - _submodules = new Lazy>(LoadSubmoduleConfiguration); + _submodules = new Lazy(LoadSubmoduleConfiguration); _gitIgnore = new Lazy(LoadIgnore); } @@ -96,7 +111,7 @@ public static GitRepository OpenRepository(string path, GitEnvironment environme string versionStr = config.GetVariableValue("core", "repositoryformatversion"); if (GitConfig.TryParseInt64Value(versionStr, out var version) && version > SupportedGitRepoFormatVersion) { - throw new NotSupportedException($"Unsupported repository version {versionStr}. Only versions up to {SupportedGitRepoFormatVersion} are supported."); + throw new NotSupportedException(string.Format(Resources.UnsupportedRepositoryVersion, versionStr, SupportedGitRepoFormatVersion)); } return new GitRepository(environment, config, gitDirectory, commonDirectory, workingDirectory); @@ -132,7 +147,7 @@ internal static string GetWorkingDirectory(GitConfig config, string gitDirectory // Path in gitdir file must be absolute. if (!PathUtils.IsAbsolute(workingDirectory)) { - throw new InvalidDataException($"Path specified in '{gitdirFilePath}' is not absolute."); + throw new InvalidDataException(string.Format(Resources.PathSpecifiedInFileIsNotAbsolute, gitdirFilePath)); } try @@ -141,7 +156,7 @@ internal static string GetWorkingDirectory(GitConfig config, string gitDirectory } catch { - throw new InvalidDataException($"Path specified in '{gitdirFilePath}' is invalid."); + throw new InvalidDataException(string.Format(Resources.PathSpecifiedInFileIsInvalid, gitdirFilePath)); } } @@ -156,7 +171,7 @@ internal static string GetWorkingDirectory(GitConfig config, string gitDirectory } catch { - throw new InvalidDataException($"The value of core.worktree is not a valid path: '{value}'"); + throw new InvalidDataException(string.Format(Resources.ValueOfIsNotValidPath, "core.worktree", value)); } } @@ -190,7 +205,7 @@ public string GetSubmoduleHeadCommitSha(string submoduleWorkingDirectory) } catch { - throw new InvalidDataException($"Invalid module path: '{submoduleWorkingDirectory}'"); + throw new InvalidDataException(string.Format(Resources.InvalidModulePath, submoduleWorkingDirectory)); } var gitDirectory = ReadDotGitFile(dotGitPath); @@ -242,7 +257,7 @@ private static string ResolveReference(string reference, string commonDirectory, if (lazyVisitedReferences != null && !lazyVisitedReferences.Add(symRef)) { // infinite recursion - throw new InvalidDataException($"Recursion detected while resolving reference: '{reference}'"); + throw new InvalidDataException(string.Format(Resources.RecursionDetectedWhileResolvingReference, reference)); } string content; @@ -252,7 +267,7 @@ private static string ResolveReference(string reference, string commonDirectory, } catch (ArgumentException) { - throw new InvalidDataException($"Invalid reference: '{reference}'"); + throw new InvalidDataException(string.Format(Resources.InvalidReference, reference)); } catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException) { @@ -278,11 +293,11 @@ private static string ResolveReference(string reference, string commonDirectory, return reference; } - throw new InvalidDataException($"Invalid reference: '{reference}'"); + throw new InvalidDataException(string.Format(Resources.InvalidReference, reference)); } private string GetWorkingDirectory() - => WorkingDirectory ?? throw new InvalidOperationException("Repository does not have a working directory"); + => WorkingDirectory ?? throw new InvalidOperationException(Resources.RepositoryDoesNotHaveWorkingDirectory); private static bool IsObjectId(string reference) => reference.Length == 40 && reference.All(CharUtils.IsHexadecimalDigit); @@ -290,18 +305,29 @@ private static bool IsObjectId(string reference) /// /// public ImmutableArray GetSubmodules() - => _submodules.Value; + => _submodules.Value.Submodules; /// /// - private ImmutableArray LoadSubmoduleConfiguration() + public ImmutableArray GetSubmoduleDiagnostics() + => _submodules.Value.Diagnostics; + + /// + /// + private SubmoduleInfo LoadSubmoduleConfiguration() { - var submodulesConfigFile = Path.Combine(GetWorkingDirectory(), GitModulesFileName); + var workingDirectory = GetWorkingDirectory(); + var submodulesConfigFile = Path.Combine(workingDirectory, GitModulesFileName); if (!File.Exists(submodulesConfigFile)) { - return ImmutableArray.Empty; + return new SubmoduleInfo(ImmutableArray.Empty, ImmutableArray.Empty); } + ImmutableArray.Builder lazyDiagnostics = null; + + void reportDiagnostic(string diagnostic) + => (lazyDiagnostics ??= ImmutableArray.CreateBuilder()).Add(diagnostic); + var builder = ImmutableArray.CreateBuilder(); var reader = new GitConfig.Reader(GitDirectory, CommonDirectory, Environment); var submoduleConfig = reader.LoadFrom(submodulesConfigFile); @@ -311,6 +337,7 @@ private ImmutableArray LoadSubmoduleConfiguration() GroupBy(kvp => kvp.Key.SubsectionName, GitConfig.VariableKey.SubsectionNameComparer). OrderBy(group => group.Key)) { + string name = group.Key; string url = null; string path = null; @@ -326,13 +353,34 @@ private ImmutableArray LoadSubmoduleConfiguration() } } - if (path != null && url != null) + if (string.IsNullOrWhiteSpace(path)) + { + reportDiagnostic(string.Format(Resources.InvalidSubmodulePath, name, path)); + } + else if (string.IsNullOrWhiteSpace(url)) + { + reportDiagnostic(string.Format(Resources.InvalidSubmoduleUrl, name, url)); + } + else { - builder.Add(new GitSubmodule(group.Key, path, url)); + string fullPath; + try + { + fullPath = Path.GetFullPath(Path.Combine(workingDirectory, path)); + } + catch + { + reportDiagnostic(string.Format(Resources.InvalidSubmodulePath, name, path)); + continue; + } + + builder.Add(new GitSubmodule(name, path, fullPath, url)); } } - return builder.ToImmutable(); + return new SubmoduleInfo( + builder.ToImmutable(), + (lazyDiagnostics != null) ? lazyDiagnostics.ToImmutable() : ImmutableArray.Empty); } private GitIgnore LoadIgnore() @@ -419,7 +467,7 @@ private static string ReadDotGitFile(string path) if (!content.StartsWith(GitDirPrefix)) { - throw new InvalidDataException($"Invalid format of '.git' file at '{path}'"); + throw new InvalidDataException(string.Format(Resources.FormatOfFileIsInvalid, path)); } // git does not trim whitespace: @@ -432,7 +480,7 @@ private static string ReadDotGitFile(string path) } catch { - throw new InvalidDataException($"Invalid path specified in '.git' file at '{path}'"); + throw new InvalidDataException(string.Format(Resources.PathSpecifiedInFileIsInvalid, path)); } } diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs index bd42b467..36a92e49 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs @@ -10,17 +10,23 @@ namespace Microsoft.Build.Tasks.Git /// Working directory path as specified in .gitmodules file. /// Expected to be relative to the working directory of the containing repository and have Posix directory separators (not normalized). /// - public string WorkingDirectoryPath { get; } + public string WorkingDirectoryRelativePath { get; } + + /// + /// Normalized full path. + /// + public string WorkingDirectoryFullPath { get; } /// /// An absolute URL or a relative path (if it starts with `./` or `../`) to the default remote of the containing repository. /// public string Url { get; } - public GitSubmodule(string name, string workingDirectoryPath, string url) + public GitSubmodule(string name, string workingDirectoryRelativePath, string workingDirectoryFullPath, string url) { Name = name; - WorkingDirectoryPath = workingDirectoryPath; + WorkingDirectoryRelativePath = workingDirectoryRelativePath; + WorkingDirectoryFullPath = workingDirectoryFullPath; Url = url; } } diff --git a/src/Microsoft.Build.Tasks.Git/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs index 4e17dc55..7c59f26d 100644 --- a/src/Microsoft.Build.Tasks.Git/GitOperations.cs +++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs @@ -15,33 +15,37 @@ internal static class GitOperations { private const string SourceControlName = "git"; - public static string LocateRepository(string directory) + public static string GetRepositoryUrl(GitRepository repository, Action logWarning = null, string remoteName = null) { - // Repository.Discover returns the path to .git directory for repositories with a working directory. - // For bare repositories it returns the repository directory. - // Returns null if the path is invalid or no repository is found. - return GitRepository.LocateRepository((directory); - } - - internal static IRepository CreateRepository(string root) - => new Repository(root); + const string RemoteSectionName = "remote"; - public static string GetRepositoryUrl(IRepository repository, Action logWarning = null, string remoteName = null) - { - // GetVariableValue("remote", name, "url"); + if (string.IsNullOrEmpty(remoteName)) + { + remoteName = "origin"; + } - var remotes = repository.Network.Remotes; - var remote = string.IsNullOrEmpty(remoteName) ? (remotes["origin"] ?? remotes.FirstOrDefault()) : remotes[remoteName]; - if (remote == null) + string remoteUrl = repository.Config.GetVariableValue(RemoteSectionName, remoteName, "url"); + if (remoteUrl == null) { - logWarning?.Invoke(Resources.RepositoryHasNoRemote, Array.Empty()); - return null; + var remoteVariable = repository.Config.Variables. + Where(kvp => kvp.Key.SectionNameEquals(RemoteSectionName)). + OrderBy(kvp => kvp.Key.SubsectionName, GitConfig.VariableKey.SubsectionNameComparer). + FirstOrDefault(); + + remoteName = remoteVariable.Key.SubsectionName; + if (remoteName == null) + { + logWarning?.Invoke(Resources.RepositoryHasNoRemote, Array.Empty()); + return null; + } + + remoteUrl = remoteVariable.Value.Last(); } - var url = NormalizeUrl(remote.Url, repository.Info.WorkingDirectory); + var url = NormalizeUrl(remoteUrl, repository.WorkingDirectory); if (url == null) { - logWarning?.Invoke(Resources.InvalidRepositoryRemoteUrl, new[] { remote.Name, remote.Url }); + logWarning?.Invoke(Resources.InvalidRepositoryRemoteUrl, new[] { remoteName, remoteUrl }); } return url; @@ -118,38 +122,12 @@ private static bool TryParseScp(string value, out Uri uri) return Uri.TryCreate(url, UriKind.Absolute, out uri); } - public static string GetRevisionId(IRepository repository) - { - // The HEAD reference in an empty repository doesn't resolve to a direct reference. - // The target identifier of a direct reference is the commit SHA. - return repository.Head.Reference.ResolveToDirectReference()?.TargetIdentifier; - } - - // GVFS doesn't support submodules. gitlib throws when submodule enumeration is attempted. - private static bool SubmodulesSupported(IRepository repository, Func fileExists) - { - try - { - if (repository.Config.GetValueOrDefault("core.gvfs")) - { - // Checking core.gvfs is not sufficient, check the presence of the file as well: - return fileExists(Path.Combine(repository.Info.WorkingDirectory, ".gitmodules")); - } - } - catch (LibGit2SharpException) - { - // exception thrown if the value is not Boolean - } - - return true; - } - - public static ITaskItem[] GetSourceRoots(IRepository repository, Action logWarning, Func fileExists) + public static ITaskItem[] GetSourceRoots(GitRepository repository, Action logWarning) { var result = new List(); var repoRoot = GetRepositoryRoot(repository); - var revisionId = GetRevisionId(repository); + var revisionId = repository.GetHeadCommitSha(); if (revisionId != null) { // Don't report a warning since it has already been reported by GetRepositoryUrl task. @@ -169,66 +147,59 @@ public static ITaskItem[] GetSourceRoots(IRepository repository, Action()); } - if (SubmodulesSupported(repository, fileExists)) + foreach (var submodule in repository.GetSubmodules()) { - foreach (var submodule in repository.Submodules) + var commitSha = repository.GetSubmoduleHeadCommitSha(submodule.WorkingDirectoryRelativePath); + if (commitSha == null) + { + logWarning(Resources.SourceCodeWontBeAvailableViaSourceLink, + new[] { string.Format(Resources.SubmoduleWithoutCommit, new[] { submodule.Name }) }); + + continue; + } + + // https://git-scm.com/docs/git-submodule + var submoduleUrl = NormalizeUrl(submodule.Url, repoRoot); + if (submoduleUrl == null) { - var commitId = submodule.WorkDirCommitId; - if (commitId == null) - { - logWarning(Resources.SubmoduleWithoutCommit_SourceLink, new[] { submodule.Name }); - continue; - } - - // https://git-scm.com/docs/git-submodule - var submoduleUrl = NormalizeUrl(submodule.Url, repoRoot); - if (submoduleUrl == null) - { - logWarning(Resources.InvalidSubmoduleUrl_SourceLink, new[] { submodule.Name, submodule.Url }); - continue; - } - - string submoduleRoot; - try - { - submoduleRoot = Path.GetFullPath(Path.Combine(repoRoot, submodule.Path)).EndWithSeparator(); - } - catch - { - logWarning(Resources.InvalidSubmodulePath_SourceLink, new[] { submodule.Name, submodule.Path }); - continue; - } - - // Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata stores the value as specified. - // Escape msbuild special characters so that URL escapes and non-ascii characters in the URL and paths are - // preserved when read by GetMetadata. - - var item = new TaskItem(Evaluation.ProjectCollection.Escape(submoduleRoot)); - item.SetMetadata(Names.SourceRoot.SourceControl, SourceControlName); - item.SetMetadata(Names.SourceRoot.ScmRepositoryUrl, Evaluation.ProjectCollection.Escape(submoduleUrl)); - item.SetMetadata(Names.SourceRoot.RevisionId, commitId.Sha); - item.SetMetadata(Names.SourceRoot.ContainingRoot, Evaluation.ProjectCollection.Escape(repoRoot)); - item.SetMetadata(Names.SourceRoot.NestedRoot, Evaluation.ProjectCollection.Escape(submodule.Path.EndWithSeparator('/'))); - result.Add(item); + logWarning(Resources.SourceCodeWontBeAvailableViaSourceLink, + new[] { string.Format(Resources.InvalidSubmoduleUrl, submodule.Name, submodule.Url) }); + + continue; } + + // Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata stores the value as specified. + // Escape msbuild special characters so that URL escapes and non-ascii characters in the URL and paths are + // preserved when read by GetMetadata. + + var item = new TaskItem(Evaluation.ProjectCollection.Escape(submodule.WorkingDirectoryFullPath.EndWithSeparator())); + item.SetMetadata(Names.SourceRoot.SourceControl, SourceControlName); + item.SetMetadata(Names.SourceRoot.ScmRepositoryUrl, Evaluation.ProjectCollection.Escape(submoduleUrl)); + item.SetMetadata(Names.SourceRoot.RevisionId, commitSha); + item.SetMetadata(Names.SourceRoot.ContainingRoot, Evaluation.ProjectCollection.Escape(repoRoot)); + item.SetMetadata(Names.SourceRoot.NestedRoot, Evaluation.ProjectCollection.Escape(submodule.WorkingDirectoryRelativePath.EndWithSeparator('/'))); + result.Add(item); + } + + foreach (var diagnostic in repository.GetSubmoduleDiagnostics()) + { + logWarning(Resources.SourceCodeWontBeAvailableViaSourceLink, new[] { diagnostic }); } return result.ToArray(); } - private static string GetRepositoryRoot(IRepository repository) - { - Debug.Assert(!repository.Info.IsBare); - return Path.GetFullPath(repository.Info.WorkingDirectory).EndWithSeparator(); - } + private static string GetRepositoryRoot(GitRepository repository) + => repository.WorkingDirectory.EndWithSeparator(); public static ITaskItem[] GetUntrackedFiles( - IRepository repository, + GitRepository repository, ITaskItem[] files, string projectDirectory, - Func repositoryFactory) + Func repositoryFactory) { var directoryTree = BuildDirectoryTree(repository); + var ignoreMatcher = repository.Ignore.CreateMatcher(); return files.Where(file => { @@ -241,10 +212,9 @@ private static string GetRepositoryRoot(IRepository repository) if (containingDirectory == null) { return true; - } + } - // Note: libgit API doesn't work with backslashes. - return containingDirectory.GetRepository(repositoryFactory).Ignore.IsPathIgnored(fullPath.Replace('\\', '/')); + return containingDirectory.GetMatcher(repositoryFactory).IsNormalizedFilePathIgnored(fullPath) ?? true; }).ToArray(); } @@ -254,7 +224,7 @@ internal sealed class SourceControlDirectory public readonly List OrderedChildren; public string RepositoryFullPath; - private IRepository _lazyRepository; + private GitIgnore.Matcher _lazyMatcher; public SourceControlDirectory(string name) : this(name, null, new List()) @@ -273,47 +243,35 @@ public SourceControlDirectory(string name, string repositoryFullPath, List BinarySearch(OrderedChildren, name, (n, v) => n.Name.CompareTo(v)); - public IRepository GetRepository(Func repositoryFactory) - => _lazyRepository ?? (_lazyRepository = repositoryFactory(RepositoryFullPath)); + public GitIgnore.Matcher GetMatcher(Func repositoryFactory) + => _lazyMatcher ?? (_lazyMatcher = repositoryFactory(RepositoryFullPath).Ignore.CreateMatcher()); } - internal static SourceControlDirectory BuildDirectoryTree(IRepository repository) + internal static SourceControlDirectory BuildDirectoryTree(GitRepository repository) { - var repoRoot = Path.GetFullPath(repository.Info.WorkingDirectory); + var repoRoot = repository.WorkingDirectory; var treeRoot = new SourceControlDirectory(""); - AddTreeNode(treeRoot, repoRoot, repository); + AddTreeNode(treeRoot, repoRoot, repository.Ignore.CreateMatcher()); - foreach (var submodule in repository.Submodules) + foreach (var submodule in repository.GetSubmodules()) { - string fullPath; - - try - { - fullPath = Path.GetFullPath(Path.Combine(repoRoot, submodule.Path)); - } - catch - { - // ignore submodules with bad paths - continue; - } - - AddTreeNode(treeRoot, fullPath, repositoryOpt: null); + AddTreeNode(treeRoot, submodule.WorkingDirectoryFullPath, matcherOpt: null); } return treeRoot; } - private static void AddTreeNode(SourceControlDirectory root, string fullPath, IRepository repositoryOpt) + private static void AddTreeNode(SourceControlDirectory root, string fullPath, GitIgnore.Matcher matcherOpt) { var segments = PathUtilities.Split(fullPath); @@ -335,7 +293,7 @@ private static void AddTreeNode(SourceControlDirectory root, string fullPath, IR if (i == segments.Length - 1) { - node.SetRepository(fullPath, repositoryOpt); + node.SetMatcher(fullPath, matcherOpt); } } } diff --git a/src/Microsoft.Build.Tasks.Git/LocateRepository.cs b/src/Microsoft.Build.Tasks.Git/LocateRepository.cs index af71f2a5..fe936812 100644 --- a/src/Microsoft.Build.Tasks.Git/LocateRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/LocateRepository.cs @@ -1,20 +1,45 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.IO; using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; namespace Microsoft.Build.Tasks.Git { - public class LocateRepository : Task + public sealed class LocateRepository : RepositoryTask { - [Required] - public string Directory { get; set; } + public string RemoteName { get; set; } [Output] - public string Id { get; set; } + public string WorkingDirectory { get; private set; } - public override bool Execute() => RepositoryTasks.LocateRepository(this); + [Output] + public string Url { get; private set; } + + /// + /// Returns items describing repository source roots: + /// + /// Metadata + /// Identity: Normalized path. Ends with a directory separator. + /// SourceControl: "Git" + /// RepositoryUrl: URL of the repository. + /// RevisionId: Revision (commit SHA). + /// ContainingRoot: Identity of the containing source root. + /// NestedRoot: For a submodule root, a path of the submodule root relative to the repository root. Ends with a slash. + /// + [Output] + public ITaskItem[] Roots { get; private set; } + + /// + /// Head tip commit SHA. + /// + [Output] + public string RevisionId { get; private set; } + + private protected override void Execute(GitRepository repository) + { + WorkingDirectory = repository.WorkingDirectory; + Url = GitOperations.GetRepositoryUrl(repository, Log.LogWarning, RemoteName); + Roots = GitOperations.GetSourceRoots(repository, Log.LogWarning); + RevisionId = repository.GetHeadCommitSha(); + } } } diff --git a/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs b/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs index b5530d41..96ce455a 100644 --- a/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs +++ b/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.IO; +using System.Runtime.CompilerServices; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -7,6 +10,96 @@ namespace Microsoft.Build.Tasks.Git { public abstract class RepositoryTask : Task { - public string Root { get; set; } +#if NET461 + static RepositoryTask() => AssemblyResolver.Initialize(); +#endif + private static readonly string s_cacheKey = "SourceLinkLocateRepository-3AE29AB7-AE6B-48BA-9851-98A15ED51C94"; + + [Required] + public string Directory { get; set; } + + public sealed override bool Execute() + { +#if NET461 + bool logAssemblyLoadingErrors() + { + foreach (var message in AssemblyResolver.GetLog()) + { + Log.LogMessage(message); + } + return false; + } + + try + { + ExecuteImpl(); + } + catch when (logAssemblyLoadingErrors()) + { + } +#else + ExecuteImpl(); +#endif + + return !Log.HasLoggedErrors; + } + + private protected abstract void Execute(GitRepository repository); + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ExecuteImpl() + { + var repository = GetOrCreateRepositoryInstance(); + if (repository == null) + { + // error has already been reported + return; + } + + try + { + Execute(repository); + } + catch (Exception e) when (e is IOException || e is InvalidDataException || e is NotSupportedException) + { + Log.LogError(Resources.ErrorReadingGitRepositoryInformation, e.Message); + } + } + + private GitRepository GetOrCreateRepositoryInstance() + { + var cachedEntry = (StrongBox)BuildEngine4.GetRegisteredTaskObject(s_cacheKey, RegisteredTaskObjectLifetime.Build); + if (cachedEntry != null) + { + Log.LogMessage(MessageImportance.Low, $"SourceLink: Reusing cached git repository information."); + return cachedEntry.Value; + } + + GitRepository repository; + try + { + // TODO: configure environment + repository = GitRepository.OpenRepository(Directory, GitEnvironment.CreateFromProcessEnvironment()); + } + catch (Exception e) when (e is IOException || e is InvalidDataException || e is NotSupportedException) + { + Log.LogError(Resources.ErrorReadingGitRepositoryInformation, e.Message); + repository = null; + } + + if (repository?.WorkingDirectory == null) + { + Log.LogWarning(Resources.UnableToLocateRepository, Directory); + repository = null; + } + + BuildEngine4.RegisterTaskObject( + s_cacheKey, + new StrongBox(repository), + RegisteredTaskObjectLifetime.Build, + allowEarlyCollection: true); + + return repository; + } } } diff --git a/src/Microsoft.Build.Tasks.Git/RepositoryTasks.cs b/src/Microsoft.Build.Tasks.Git/RepositoryTasks.cs deleted file mode 100644 index 7aaeaff0..00000000 --- a/src/Microsoft.Build.Tasks.Git/RepositoryTasks.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.IO; - -namespace Microsoft.Build.Tasks.Git -{ - internal static class RepositoryTasks - { - private static bool Execute(T task, Action action) - where T: RepositoryTask - { - var log = task.Log; - - // Unable to determine repository root, warning has already been reported. - if (string.IsNullOrEmpty(task.Root)) - { - return true; - } - - GitRepository repo; - try - { - repo = GitOperations.CreateRepository(task.Root); - } - catch (RepositoryNotFoundException e) - { - log.LogErrorFromException(e); - return false; - } - - if (repo.Info.IsBare) - { - log.LogWarning(Resources.BareRepositoriesNotSupported, task.Root); - return true; - } - - using (repo) - { - try - { - action(repo, task); - } - catch (LibGit2SharpException e) - { - log.LogErrorFromException(e); - } - } - - return !log.HasLoggedErrors; - } - - public static bool LocateRepository(LocateRepository task) - { - try - { - task.Id = GitOperations.LocateRepository(task.Directory); - } - catch (Exception e) - { -#if NET461 - foreach (var message in AssemblyResolver.GetLog()) - { - task.Log.LogMessage(message); - } -#endif - task.Log.LogWarningFromException(e, showStackTrace: true); - - return true; - } - - if (task.Id == null) - { - task.Log.LogWarning(Resources.UnableToLocateRepository, task.Directory); - } - - return !task.Log.HasLoggedErrors; - } - - public static bool GetRepositoryUrl(GetRepositoryUrl task) => - Execute(task, (repo, t) => - { - t.Url = GitOperations.GetRepositoryUrl(repo, t.Log.LogWarning, t.RemoteName); - }); - - public static bool GetSourceRevisionId(GetSourceRevisionId task) => - Execute(task, (repo, t) => - { - t.RevisionId = GitOperations.GetRevisionId(repo); - }); - - public static bool GetSourceRoots(GetSourceRoots task) => - Execute(task, (repo, t) => - { - t.Roots = GitOperations.GetSourceRoots(repo, t.Log.LogWarning, File.Exists); - }); - - public static bool GetUntrackedFiles(GetUntrackedFiles task) => - Execute(task, (repo, t) => - { - t.UntrackedFiles = GitOperations.GetUntrackedFiles(repo, t.Files, t.ProjectDirectory, dir => new Repository(dir)); - }); - } -} diff --git a/src/Microsoft.Build.Tasks.Git/Resources.resx b/src/Microsoft.Build.Tasks.Git/Resources.resx index 97751139..3f0a0491 100644 --- a/src/Microsoft.Build.Tasks.Git/Resources.resx +++ b/src/Microsoft.Build.Tasks.Git/Resources.resx @@ -117,14 +117,59 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Submodule '{0}' doesn't have any commit, the source code won't be available via source link. + + Path must be absolute - - The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link. + + Path must be a file path - - The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link. + + Error reading git repository information: {0} + + + Unsupported repository version {0}. Only versions up to {1} are supported. + + + Path specified in file '{0}' is not absolute. + + + Path specified in file '{0}' is invalid. + + + The value of {0} is not a valid path: '{1}'. + + + Invalid module path: '{0}'. + + + Recursion detected while resolving reference: '{0}'. + + + Repository does not have a working directory. + + + Repository does not have a working directory. + + + The format of the file '{0}' is invalid. + + + Invalid reference: '{0}'. + + + Configuration file recursion exceeded maximum allowed depth of {0}. + + + Submodule '{0}' doesn't have any commit + + + The URL of submodule '{0}' is missing or invalid: '{1}' + + + The path of submodule '{0}' is missing or invalid: '{1}' + + + {0} -- the source code won't be available via Source Link. Repository has no commit. @@ -136,9 +181,6 @@ The URL of repository remote '{0}' is invalid: '{1}'. - Unable to locate repository containing directory '{0}'. - - - Bare repositories are not supported: '{0}'. + Unable to locate repository with working directory that contains directory '{0}'. \ No newline at end of file diff --git a/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets b/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets index 6390191b..07ea0a67 100644 --- a/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets +++ b/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets @@ -8,30 +8,18 @@ - + + + + git - - - - - - - - - - - - - - + + @@ -29,7 +29,7 @@ - + From 13a8c865ac506574dcebecdd77e16b354d4a6fdf Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Thu, 20 Jun 2019 17:53:00 -0700 Subject: [PATCH 08/16] Rename --- src/Common/{Tuple.cs => ValueTuple.cs} | 0 src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Common/{Tuple.cs => ValueTuple.cs} (100%) diff --git a/src/Common/Tuple.cs b/src/Common/ValueTuple.cs similarity index 100% rename from src/Common/Tuple.cs rename to src/Common/ValueTuple.cs diff --git a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj index c0ea1c71..3c749cf9 100644 --- a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj +++ b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj @@ -15,7 +15,7 @@ - + From 1a02678a6b8b77caf51099d52bcec29a0f4d7532 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Thu, 20 Jun 2019 18:03:35 -0700 Subject: [PATCH 09/16] Update packaging --- .../Microsoft.Build.Tasks.Git.csproj | 6 +++-- .../Microsoft.Build.Tasks.Git.nuspec | 27 ------------------- .../build/Microsoft.Build.Tasks.Git.targets | 3 --- 3 files changed, 4 insertions(+), 32 deletions(-) delete mode 100644 src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.nuspec diff --git a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj index 3c749cf9..35350654 100644 --- a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj +++ b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj @@ -6,8 +6,6 @@ true Microsoft.Build.Tasks.Git - Microsoft.Build.Tasks.Git.nuspec - $(OutputPath) MSBuild tasks providing git repository information. MSBuild Tasks source control git @@ -29,4 +27,8 @@ + + + + diff --git a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.nuspec b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.nuspec deleted file mode 100644 index 0d154858..00000000 --- a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.nuspec +++ /dev/null @@ -1,27 +0,0 @@ - - - - $CommonMetadataElements$ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets b/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets index d38c9025..66754274 100644 --- a/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets +++ b/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets @@ -1,9 +1,6 @@ - - - From 38d981812155942f4bb43439bdab4bfdeeb3b4f2 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Thu, 20 Jun 2019 18:55:18 -0700 Subject: [PATCH 10/16] Move to Core 3.0 --- eng/runtimeconfig.template.json | 3 +++ global.json | 2 +- src/Directory.Build.props | 6 ++++++ .../Microsoft.Build.StandardCI.csproj | 1 + .../GitRepositoryTests.cs | 4 ++-- .../GitDataReader/GitRepository.cs | 4 ++-- src/Microsoft.Build.Tasks.Git/LocateRepository.cs | 3 ++- .../Microsoft.Build.Tasks.Git.csproj | 1 + .../Microsoft.Build.Tasks.Tfvc.csproj | 1 + src/TestUtilities/TestUtilities.csproj | 1 + 10 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 eng/runtimeconfig.template.json diff --git a/eng/runtimeconfig.template.json b/eng/runtimeconfig.template.json new file mode 100644 index 00000000..384e2f49 --- /dev/null +++ b/eng/runtimeconfig.template.json @@ -0,0 +1,3 @@ +{ + "rollForwardOnNoCandidateFx": 2 +} diff --git a/global.json b/global.json index cdba2f50..ff9d23a0 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "tools": { - "dotnet": "2.2.203" + "dotnet": "3.0.100-preview5-011568" }, "msbuild-sdks": { "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.19255.2" diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 04ab4914..f848fff0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,6 +7,12 @@ $(CopyrightMicrosoft) Apache-2.0 true + + + $(RepositoryEngineeringDir)runtimeconfig.template.json true diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs index 794feb87..3b9da0b9 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs @@ -342,10 +342,10 @@ public void GetHeadCommitSha() var commonDir = temp.CreateDirectory(); var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads"); - refsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000"); + refsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000 \t\v\r\n"); var gitDir = temp.CreateDirectory(); - gitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master"); + gitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master \t\v\r\n"); var repository = new GitRepository(new GitEnvironment("/home"), GitConfig.Empty, gitDir.Path, commonDir.Path, workingDirectory: null); Assert.Equal("0000000000000000000000000000000000000000", repository.GetHeadCommitSha()); diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs index aaea652b..eefe7332 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs @@ -242,7 +242,7 @@ private static string ReadHeadCommitSha(string gitDirectory, string commonDirect string headRef; try { - headRef = File.ReadAllText(Path.Combine(gitDirectory, GitHeadFileName)); + headRef = File.ReadAllText(Path.Combine(gitDirectory, GitHeadFileName)).TrimEnd(CharUtils.AsciiWhitespace); } catch (Exception e) when (!(e is IOException)) { @@ -277,7 +277,7 @@ private static string ResolveReference(string reference, string commonDirectory, string content; try { - content = File.ReadAllText(Path.Combine(commonDirectory, symRef)); + content = File.ReadAllText(Path.Combine(commonDirectory, symRef)).TrimEnd(CharUtils.AsciiWhitespace); } catch (ArgumentException) { diff --git a/src/Microsoft.Build.Tasks.Git/LocateRepository.cs b/src/Microsoft.Build.Tasks.Git/LocateRepository.cs index d7ee2fdd..899fd81e 100644 --- a/src/Microsoft.Build.Tasks.Git/LocateRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/LocateRepository.cs @@ -12,7 +12,7 @@ public sealed class LocateRepository : RepositoryTask public string Path { get; set; } [Output] - public string GitDirectory { get; private set; } + public string RepositoryId { get; private set; } [Output] public string WorkingDirectory { get; private set; } @@ -45,6 +45,7 @@ public sealed class LocateRepository : RepositoryTask private protected override void Execute(GitRepository repository) { + RepositoryId = repository.GitDirectory; WorkingDirectory = repository.WorkingDirectory; Url = GitOperations.GetRepositoryUrl(repository, Log.LogWarning, RemoteName); Roots = GitOperations.GetSourceRoots(repository, Log.LogWarning); diff --git a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj index 35350654..3f5e8f46 100644 --- a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj +++ b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj @@ -6,6 +6,7 @@ true Microsoft.Build.Tasks.Git + tools MSBuild tasks providing git repository information. MSBuild Tasks source control git diff --git a/src/Microsoft.Build.Tasks.Tfvc/Microsoft.Build.Tasks.Tfvc.csproj b/src/Microsoft.Build.Tasks.Tfvc/Microsoft.Build.Tasks.Tfvc.csproj index e582685f..dc06724b 100644 --- a/src/Microsoft.Build.Tasks.Tfvc/Microsoft.Build.Tasks.Tfvc.csproj +++ b/src/Microsoft.Build.Tasks.Tfvc/Microsoft.Build.Tasks.Tfvc.csproj @@ -2,6 +2,7 @@ net46 true + true true diff --git a/src/TestUtilities/TestUtilities.csproj b/src/TestUtilities/TestUtilities.csproj index bd61a502..168f8768 100644 --- a/src/TestUtilities/TestUtilities.csproj +++ b/src/TestUtilities/TestUtilities.csproj @@ -2,6 +2,7 @@ net461;netstandard2.0 false + true From 95796b18b5eb56bd47ffb69fcd9287fb08c88ccc Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 21 Jun 2019 09:58:20 -0700 Subject: [PATCH 11/16] Refactoring --- .../GitDataReader/GitRepository.cs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs index eefe7332..cf91236d 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs @@ -235,21 +235,8 @@ internal string GetSubmoduleHeadCommitSha(string submoduleWorkingDirectoryFullPa /// private static string ReadHeadCommitSha(string gitDirectory, string commonDirectory) { - // See - // https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD - // https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-refs - - string headRef; - try - { - headRef = File.ReadAllText(Path.Combine(gitDirectory, GitHeadFileName)).TrimEnd(CharUtils.AsciiWhitespace); - } - catch (Exception e) when (!(e is IOException)) - { - throw new IOException(e.Message, e); - } - - return ResolveReference(headRef, commonDirectory); + // See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD + return ResolveReference(ReadReferenceFromFile(Path.Combine(gitDirectory, GitHeadFileName)), commonDirectory); } // internal for testing @@ -263,6 +250,8 @@ internal static string ResolveReference(string reference, string commonDirectory /// private static string ResolveReference(string reference, string commonDirectory, ref HashSet lazyVisitedReferences) { + // See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD + const string refPrefix = "ref: "; if (reference.StartsWith(refPrefix + "refs/", StringComparison.Ordinal)) { @@ -274,22 +263,24 @@ private static string ResolveReference(string reference, string commonDirectory, throw new InvalidDataException(string.Format(Resources.RecursionDetectedWhileResolvingReference, reference)); } - string content; + string path; try { - content = File.ReadAllText(Path.Combine(commonDirectory, symRef)).TrimEnd(CharUtils.AsciiWhitespace); + path = Path.Combine(commonDirectory, symRef); } - catch (ArgumentException) + catch { throw new InvalidDataException(string.Format(Resources.InvalidReference, reference)); } - catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException) + + string content; + try { - return null; + content = ReadReferenceFromFile(path); } - catch (Exception e) when (!(e is IOException)) + catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException) { - throw new IOException(e.Message, e); + return null; } if (IsObjectId(reference)) @@ -310,6 +301,18 @@ private static string ResolveReference(string reference, string commonDirectory, throw new InvalidDataException(string.Format(Resources.InvalidReference, reference)); } + private static string ReadReferenceFromFile(string path) + { + try + { + return File.ReadAllText(path).TrimEnd(CharUtils.AsciiWhitespace); + } + catch (Exception e) when (!(e is IOException)) + { + throw new IOException(e.Message, e); + } + } + private string GetWorkingDirectory() => WorkingDirectory ?? throw new InvalidOperationException(Resources.RepositoryDoesNotHaveWorkingDirectory); From c35dae27006182dafb428d74016e529fe818e3ff Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 21 Jun 2019 14:43:08 -0700 Subject: [PATCH 12/16] Add multi-project integration test --- .../GitDataReader/GitRepository.cs | 4 +- .../GitHubTests.cs | 54 +++++++++++++ .../DotNetSdk/DotNetSdkTestBase.cs | 75 ++++++++++++------- 3 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs index cf91236d..1d1ab77b 100644 --- a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs +++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs @@ -283,9 +283,9 @@ private static string ResolveReference(string reference, string commonDirectory, return null; } - if (IsObjectId(reference)) + if (IsObjectId(content)) { - return reference; + return content; } lazyVisitedReferences ??= new HashSet(); diff --git a/src/SourceLink.Git.IntegrationTests/GitHubTests.cs b/src/SourceLink.Git.IntegrationTests/GitHubTests.cs index 3926b204..681804e2 100644 --- a/src/SourceLink.Git.IntegrationTests/GitHubTests.cs +++ b/src/SourceLink.Git.IntegrationTests/GitHubTests.cs @@ -52,6 +52,60 @@ public void EmptyRepository() }); } + [ConditionalFact(typeof(DotNetSdkAvailable))] + public void MutlipleProjects() + { + var repoUrl = "http://github.com/test-org/test-repo"; + var repoName = "test-repo"; + + var projectName2 = "Project2"; + var projectFileName2 = projectName2 + ".csproj"; + + var project2 = RootDir.CreateDirectory(projectName2).CreateFile(projectFileName2).WriteAllText(@" + + + netstandard2.0 + + +"); + + using var repo = GitUtilities.CreateGitRepositoryWithSingleCommit( + RootDir.Path, + new[] { Path.Combine(ProjectName, ProjectFileName), Path.Combine(projectName2, projectFileName2), }, + repoUrl); + + var commitSha = repo.Head.Tip.Sha; + + VerifyValues( + customProps: $@" + + + +", + customTargets: "", + targets: new[] + { + "Build" + }, + expressions: new[] + { + "@(SourceRoot)", + "@(SourceRoot->'%(SourceLinkUrl)')", + "$(SourceLink)", + "$(PrivateRepositoryUrl)", + }, + expectedResults: new[] + { + SourceRoot, + $"https://raw.githubusercontent.com/test-org/{repoName}/{commitSha}/*", + s_relativeSourceLinkJsonPath, + $"http://github.com/test-org/{repoName}", + }, + // the second project should reuse the repository info cached by the first project: + buildVerbosity: "detailed", + expectedBuildOutputFilter: line => line.Contains("SourceLink: Reusing cached git repository information.")); + } + [ConditionalFact(typeof(DotNetSdkAvailable))] public void FullValidation_Https() { diff --git a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs index 03caabbb..61577227 100644 --- a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs +++ b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs @@ -55,11 +55,13 @@ public void F() "; + protected readonly TempDirectory RootDir; protected readonly TempDirectory ProjectDir; - protected readonly TempDirectory ObjDir; + protected readonly TempDirectory ProjectObjDir; protected readonly TempDirectory NuGetCacheDir; - protected readonly TempDirectory OutDir; + protected readonly TempDirectory ProjectOutDir; protected readonly TempFile Project; + protected readonly string SourceRoot; protected readonly string ProjectSourceRoot; protected readonly string ProjectName; protected readonly string ProjectFileName; @@ -72,6 +74,7 @@ public void F() protected static readonly string s_relativeOutputFilePath = Path.Combine("obj", "Debug", "netstandard2.0", "test.dll"); protected static readonly string s_relativePackagePath = Path.Combine("bin", "Debug", "test.1.0.0.nupkg"); + private bool _projectRestored; private int _logIndex; static DotNetSdkTestBase() @@ -163,25 +166,21 @@ public DotNetSdkTestBase(params string[] packages) Configuration = "Debug"; TargetFramework = "netstandard2.0"; - ProjectDir = Temp.CreateDirectory(); - ProjectSourceRoot = ProjectDir.Path + Path.DirectorySeparatorChar; - NuGetCacheDir = ProjectDir.CreateDirectory(".packages"); - ObjDir = ProjectDir.CreateDirectory("obj"); - OutDir = ProjectDir.CreateDirectory("bin").CreateDirectory(Configuration).CreateDirectory(TargetFramework); + RootDir = Temp.CreateDirectory(); + NuGetCacheDir = RootDir.CreateDirectory(".packages"); - Project = ProjectDir.CreateFile(ProjectFileName).WriteAllText(s_projectSource); - ProjectDir.CreateFile("TestClass.cs").WriteAllText(s_classSource); - - ProjectDir.CreateFile("Directory.Build.props").WriteAllText( + RootDir.CreateFile("Directory.Build.props").WriteAllText( $@" {string.Join(Environment.NewLine, packages.Select(packageName => $""))} "); - ProjectDir.CreateFile("Directory.Build.targets").WriteAllText(""); - ProjectDir.CreateFile(".editorconfig").WriteAllText("root = true"); - ProjectDir.CreateFile("nuget.config").WriteAllText(GetLocalNuGetConfigContent(s_buildInfo.PackagesDirectory)); + RootDir.CreateFile("Directory.Build.targets").WriteAllText(""); + RootDir.CreateFile(".editorconfig").WriteAllText("root = true"); + RootDir.CreateFile("nuget.config").WriteAllText(GetLocalNuGetConfigContent(s_buildInfo.PackagesDirectory)); + + SourceRoot = RootDir.Path + Path.DirectorySeparatorChar; EnvironmentVariables = new Dictionary() { @@ -190,13 +189,13 @@ public DotNetSdkTestBase(params string[] packages) { "NUGET_PACKAGES", NuGetCacheDir.Path } }; - var restoreResult = ProcessUtilities.Run(DotNetPath, $@"msbuild ""{Project.Path}"" /t:restore /bl:{Path.Combine(ProjectDir.Path, "restore.binlog")}", - additionalEnvironmentVars: EnvironmentVariables); - Assert.True(restoreResult.ExitCode == 0, $"Failed with exit code {restoreResult.ExitCode}: {restoreResult.Output}"); + ProjectDir = RootDir.CreateDirectory(ProjectName); + ProjectSourceRoot = ProjectDir.Path + Path.DirectorySeparatorChar; + ProjectObjDir = ProjectDir.CreateDirectory("obj"); + ProjectOutDir = ProjectDir.CreateDirectory("bin").CreateDirectory(Configuration).CreateDirectory(TargetFramework); - Assert.True(File.Exists(Path.Combine(ObjDir.Path, "project.assets.json"))); - Assert.True(File.Exists(Path.Combine(ObjDir.Path, ProjectFileName + ".nuget.g.props"))); - Assert.True(File.Exists(Path.Combine(ObjDir.Path, ProjectFileName + ".nuget.g.targets"))); + Project = ProjectDir.CreateFile(ProjectFileName).WriteAllText(s_projectSource); + ProjectDir.CreateFile("TestClass.cs").WriteAllText(s_classSource); } protected void VerifyValues( @@ -207,25 +206,40 @@ public DotNetSdkTestBase(params string[] packages) string[] expectedResults = null, string[] expectedErrors = null, string[] expectedWarnings = null, - string additionalCommandLineArgs = null) + string additionalCommandLineArgs = null, + string buildVerbosity = "minimal", + Func expectedBuildOutputFilter = null) { Debug.Assert(targets != null); Debug.Assert(expressions != null); Debug.Assert(expectedResults == null ^ expectedErrors == null); - var evaluationResultsFile = Path.Combine(OutDir.Path, "EvaluationResult.txt"); + var evaluationResultsFile = Path.Combine(ProjectOutDir.Path, "EvaluationResult.txt"); - EmitTestHelperProps(ObjDir.Path, ProjectFileName, customProps); - EmitTestHelperTargets(ObjDir.Path, evaluationResultsFile, ProjectFileName, expressions, customTargets); + EmitTestHelperProps(ProjectObjDir.Path, ProjectFileName, customProps); + EmitTestHelperTargets(ProjectObjDir.Path, evaluationResultsFile, ProjectFileName, expressions, customTargets); var targetsArg = string.Join(";", targets.Concat(new[] { "Test_EvaluateExpressions" })); var testBinDirectory = Path.GetDirectoryName(typeof(DotNetSdkTestBase).Assembly.Location); - var buildLog = Path.Combine(ProjectDir.Path, $"build{_logIndex++}.binlog"); + var buildLog = Path.Combine(RootDir.Path, $"build{_logIndex++}.binlog"); bool success = false; try { - var buildResult = ProcessUtilities.Run(DotNetPath, $@"msbuild ""{Project.Path}"" /t:{targetsArg} /p:Configuration={Configuration} /bl:""{buildLog}"" {additionalCommandLineArgs}", + if (!_projectRestored) + { + var restoreResult = ProcessUtilities.Run(DotNetPath, $@"msbuild ""{Project.Path}"" /t:restore /bl:{Path.Combine(RootDir.Path, "restore.binlog")}", + additionalEnvironmentVars: EnvironmentVariables); + Assert.True(restoreResult.ExitCode == 0, $"Failed with exit code {restoreResult.ExitCode}: {restoreResult.Output}"); + + Assert.True(File.Exists(Path.Combine(ProjectObjDir.Path, "project.assets.json"))); + Assert.True(File.Exists(Path.Combine(ProjectObjDir.Path, ProjectFileName + ".nuget.g.props"))); + Assert.True(File.Exists(Path.Combine(ProjectObjDir.Path, ProjectFileName + ".nuget.g.targets"))); + + _projectRestored = true; + } + + var buildResult = ProcessUtilities.Run(DotNetPath, $@"msbuild ""{Project.Path}"" /t:{targetsArg} /p:Configuration={Configuration} /bl:""{buildLog}"" /v:{buildVerbosity} {additionalCommandLineArgs}", additionalEnvironmentVars: EnvironmentVariables); string[] getDiagnostics(string[] lines, bool error) @@ -245,7 +259,7 @@ bool diagnosticsEqual(string expected, string actual) } var outputLines = buildResult.Output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - + if (expectedErrors == null) { Assert.True(buildResult.ExitCode == 0, $"Build failed with exit code {buildResult.ExitCode}: {buildResult.Output}"); @@ -264,13 +278,18 @@ bool diagnosticsEqual(string expected, string actual) var actualWarnings = getDiagnostics(outputLines, error: false); AssertEx.Equal(expectedWarnings ?? Array.Empty(), actualWarnings, diagnosticsEqual); + if (expectedBuildOutputFilter != null) + { + Assert.True(outputLines.Any(expectedBuildOutputFilter)); + } + success = true; } finally { if (!success) { - try { File.Copy(buildLog, Path.Combine(s_buildInfo.LogDirectory, "test_build_" + Path.GetFileName(ProjectDir.Path) + ".binlog"), overwrite: true); } catch { } + try { File.Copy(buildLog, Path.Combine(s_buildInfo.LogDirectory, "test_build_" + Path.GetFileName(RootDir.Path) + ".binlog"), overwrite: true); } catch { } } } } From 33a3a9102c95a8df206524ae05ce571164776753 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 21 Jun 2019 15:25:07 -0700 Subject: [PATCH 13/16] .NET Core fixes --- src/Directory.Build.props | 7 +------ .../GitRepositoryTests.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f848fff0..5f8976c3 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,12 +7,7 @@ $(CopyrightMicrosoft) Apache-2.0 true - - - $(RepositoryEngineeringDir)runtimeconfig.template.json + $(RepositoryEngineeringDir)runtimeconfig.template.json