diff --git a/SourceLink.sln b/SourceLink.sln
index 4ea4d37a4..1d2036b13 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/eng/runtimeconfig.template.json b/eng/runtimeconfig.template.json
new file mode 100644
index 000000000..384e2f49e
--- /dev/null
+++ b/eng/runtimeconfig.template.json
@@ -0,0 +1,3 @@
+{
+  "rollForwardOnNoCandidateFx": 2
+} 
diff --git a/global.json b/global.json
index cdba2f50c..ff9d23a0c 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/Common/ValueTuple.cs b/src/Common/ValueTuple.cs
new file mode 100644
index 000000000..074e9f605
--- /dev/null
+++ b/src/Common/ValueTuple.cs
@@ -0,0 +1,49 @@
+// 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.Collections.Generic;
+
+namespace System
+{
+    internal struct ValueTuple<T1, T2>
+    {
+        public T1 Item1;
+        public T2 Item2;
+
+        public ValueTuple(T1 item1, T2 item2)
+        {
+            Item1 = item1;
+            Item2 = item2;
+        }
+    }
+
+    internal struct ValueTuple<T1, T2, T3>
+    {
+        public T1 Item1;
+        public T2 Item2;
+        public T3 Item3;
+
+        public ValueTuple(T1 item1, T2 item2, T3 item3)
+        {
+            Item1 = item1;
+            Item2 = item2;
+            Item3 = item3;
+        }
+    }
+
+    namespace Runtime.CompilerServices
+    {
+        internal sealed class TupleElementNamesAttribute : Attribute
+        {
+            public IList<string> TransformNames { get; }
+
+            public TupleElementNamesAttribute(string[] transformNames)
+            {
+                TransformNames = transformNames;
+            }
+        }
+    }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 589606239..5f8976c38 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -3,10 +3,11 @@
   <Import Project="Sdk.props" Sdk="Microsoft.DotNet.Arcade.Sdk" />
 
   <PropertyGroup>
-    <LangVersion>Latest</LangVersion>
+    <LangVersion>Preview</LangVersion>
     <Copyright>$(CopyrightMicrosoft)</Copyright>
     <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
     <GenerateResxSource>true</GenerateResxSource>
+    <UserRuntimeConfig Condition="$(TargetFramework.StartsWith('netcoreapp'))">$(RepositoryEngineeringDir)runtimeconfig.template.json</UserRuntimeConfig>
   </PropertyGroup>
 
   <!-- 
diff --git a/src/Microsoft.Build.StandardCI/Microsoft.Build.StandardCI.csproj b/src/Microsoft.Build.StandardCI/Microsoft.Build.StandardCI.csproj
index 9f8e2bf7c..5ae9775e7 100644
--- a/src/Microsoft.Build.StandardCI/Microsoft.Build.StandardCI.csproj
+++ b/src/Microsoft.Build.StandardCI/Microsoft.Build.StandardCI.csproj
@@ -2,6 +2,7 @@
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
     <AutoGenerateAssemblyVersion>true</AutoGenerateAssemblyVersion>
+    <ExcludeFromSourceBuild>true</ExcludeFromSourceBuild>
 
     <!-- Using an explicit nuspec file since NuGet Pack target currently doesn't support including dependencies in tools packages -->
     <IsPackable>true</IsPackable>
diff --git a/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs b/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs
deleted file mode 100644
index d32bcbc25..000000000
--- a/src/Microsoft.Build.Tasks.Git.Operations/GitOperations.cs
+++ /dev/null
@@ -1,410 +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.Collections.Generic;
-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;
-
-namespace Microsoft.Build.Tasks.Git
-{
-    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);
-        }
-
-        public static string GetRepositoryUrl(IRepository repository, Action<string, object[]> logWarning = null, string remoteName = null)
-        {
-            var remotes = repository.Network.Remotes;
-            var remote = string.IsNullOrEmpty(remoteName) ? (remotes["origin"] ?? remotes.FirstOrDefault()) : remotes[remoteName];
-            if (remote == null)
-            {
-                logWarning?.Invoke(Resources.RepositoryHasNoRemote, Array.Empty<string>());
-                return null;
-            }
-
-            var url = NormalizeUrl(remote.Url, repository.Info.WorkingDirectory);
-            if (url == null)
-            {
-                logWarning?.Invoke(Resources.InvalidRepositoryRemoteUrl, new[] { remote.Name, remote.Url });
-            }
-
-            return url;
-        }
-
-        internal static string NormalizeUrl(string url, string root)
-        {
-            // Since git supports scp-like syntax for SSH URLs we convert it here, 
-            // so that RepositoryUrl is actually a valid URL in that case.
-            // See https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols and
-            // https://github.com/libgit2/libgit2/blob/master/src/transport.c#L72.
-
-            // Windows device path "X:"
-            if (url.Length == 2 && IsWindowsAbsoluteOrDriveRelativePath(url))
-            {
-                return "file:///" + url + "/";
-            }
-
-            if (TryParseScp(url, out var uri))
-            {
-                return uri.ToString();
-            }
-
-            if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
-            {
-                return null;
-            }
-
-            if (uri.IsAbsoluteUri)
-            {
-                return uri.ToString();
-            }
-
-            // Convert relative local path to absolute:
-            var rootUri = new Uri(root.EndWithSeparator('/'));
-            if (Uri.TryCreate(rootUri, uri, out uri))
-            {
-                return uri.ToString();
-            }
-
-            return null;
-        }
-
-        private static bool IsWindowsAbsoluteOrDriveRelativePath(string value)
-            => Path.DirectorySeparatorChar == '\\' &&
-               value.Length >= 2 &&
-               value[1] == ':' &&
-               (value[0] >= 'A' && value[0] <= 'Z' || value[0] >= 'a' && value[0] <= 'z');
-
-        private static bool TryParseScp(string value, out Uri uri)
-        {
-            uri = null;
-           
-            int colon = value.IndexOf(':');
-            if (colon == -1)
-            {
-                return false;
-            }
-
-            // URLs xxx://xxx
-            if (colon + 2 < value.Length && value[colon + 1] == '/' && value[colon + 2] == '/')
-            {
-                return false;
-            }
-
-            // Windows absolute or driver-relative paths "X:\xxx", "X:xxx"
-            if (IsWindowsAbsoluteOrDriveRelativePath(value))
-            {
-                return false;
-            }
-
-            // [user@]server:path
-            var url = "ssh://" + value.Substring(0, colon) + "/" + value.Substring(colon + 1);
-            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<string, bool> fileExists)
-        {
-            try
-            {
-                if (repository.Config.GetValueOrDefault<bool>("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<string, object[]> logWarning, Func<string, bool> fileExists)
-        {
-            var result = new List<TaskItem>();
-            var repoRoot = GetRepositoryRoot(repository);
-
-            var revisionId = GetRevisionId(repository);
-            if (revisionId != null)
-            {
-                // Don't report a warning since it has already been reported by GetRepositoryUrl task.
-                string repositoryUrl = GetRepositoryUrl(repository);
-
-                // Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata stores the value as specified.
-                // Escape msbuild special characters so that URL escapes in the URL are preserved when the URL is read by GetMetadata.
-
-                var item = new TaskItem(Evaluation.ProjectCollection.Escape(repoRoot));
-                item.SetMetadata(Names.SourceRoot.SourceControl, SourceControlName);
-                item.SetMetadata(Names.SourceRoot.ScmRepositoryUrl, Evaluation.ProjectCollection.Escape(repositoryUrl));
-                item.SetMetadata(Names.SourceRoot.RevisionId, revisionId);
-                result.Add(item);
-            }
-            else
-            {
-                logWarning(Resources.RepositoryHasNoCommit, Array.Empty<object>());
-            }
-
-            if (SubmodulesSupported(repository, fileExists))
-            {
-                foreach (var submodule in repository.Submodules)
-                {
-                    var commitId = submodule.WorkDirCommitId;
-                    if (commitId == null)
-                    {
-                        logWarning(Resources.SubmoduleWithoutCommit_SourceLink, new[] { submodule.Name });
-                        continue;
-                    }
-
-                    // https://git-scm.com/docs/git-submodule
-                    // <repository> 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)
-                    {
-                        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);
-                }
-            }
-
-            return result.ToArray();
-        }
-
-        private static string GetRepositoryRoot(IRepository repository)
-        {
-            Debug.Assert(!repository.Info.IsBare);
-            return Path.GetFullPath(repository.Info.WorkingDirectory).EndWithSeparator();
-        }
-
-        public static ITaskItem[] GetUntrackedFiles(
-            IRepository repository,
-            ITaskItem[] files, 
-            string projectDirectory,
-            Func<string, IRepository> repositoryFactory)
-        {
-            var directoryTree = BuildDirectoryTree(repository);
-
-            return files.Where(file =>
-            {
-                // file.ItemSpec are relative to projectDirectory.
-                var fullPath = Path.GetFullPath(Path.Combine(projectDirectory, file.ItemSpec));
-
-                var containingDirectory = GetContainingRepository(fullPath, directoryTree);
-
-                // Files that are outside of the repository are considered untracked.
-                if (containingDirectory == null)
-                {
-                    return true;
-                }   
-
-                // Note: libgit API doesn't work with backslashes.
-                return containingDirectory.GetRepository(repositoryFactory).Ignore.IsPathIgnored(fullPath.Replace('\\', '/'));
-            }).ToArray();
-        }
-
-        internal sealed class SourceControlDirectory
-        {
-            public readonly string Name;
-            public readonly List<SourceControlDirectory> OrderedChildren;
-
-            public string RepositoryFullPath;
-            private IRepository _lazyRepository;
-
-            public SourceControlDirectory(string name)
-                : this(name, null, new List<SourceControlDirectory>())
-            {
-            }
-
-            public SourceControlDirectory(string name, string repositoryFullPath)
-                : this(name, repositoryFullPath, new List<SourceControlDirectory>())
-            {
-            }
-
-            public SourceControlDirectory(string name, string repositoryFullPath, List<SourceControlDirectory> orderedChildren)
-            {
-                Name = name;
-                RepositoryFullPath = repositoryFullPath;
-                OrderedChildren = orderedChildren;
-            }
-
-            public void SetRepository(string fullPath, IRepository repository)
-            {
-                RepositoryFullPath = fullPath;
-                _lazyRepository = repository;
-            }
-
-            public int FindChildIndex(string name)
-                => BinarySearch(OrderedChildren, name, (n, v) => n.Name.CompareTo(v));
-
-            public IRepository GetRepository(Func<string, IRepository> repositoryFactory)
-                => _lazyRepository ?? (_lazyRepository = repositoryFactory(RepositoryFullPath));
-        }
-
-        internal static SourceControlDirectory BuildDirectoryTree(IRepository repository)
-        {
-            var repoRoot = Path.GetFullPath(repository.Info.WorkingDirectory);
-
-            var treeRoot = new SourceControlDirectory("");
-            AddTreeNode(treeRoot, repoRoot, repository);
-
-            foreach (var submodule in repository.Submodules)
-            {
-                string fullPath;
-
-                try
-                {
-                    fullPath = Path.GetFullPath(Path.Combine(repoRoot, submodule.Path));
-                }
-                catch
-                {
-                    // ignore submodules with bad paths
-                    continue;
-                }
-
-                AddTreeNode(treeRoot, fullPath, repositoryOpt: null);
-            }
-
-            return treeRoot;
-        }
-
-        private static void AddTreeNode(SourceControlDirectory root, string fullPath, IRepository repositoryOpt)
-        {
-            var segments = PathUtilities.Split(fullPath);
-
-            var node = root;
-
-            for (int i = 0; i < segments.Length; i++)
-            {
-                int index = node.FindChildIndex(segments[i]);
-                if (index >= 0)
-                {
-                    node = node.OrderedChildren[index];
-                }
-                else
-                {
-                    var newNode = new SourceControlDirectory(segments[i]);
-                    node.OrderedChildren.Insert(~index, newNode);
-                    node = newNode;
-                }
-
-                if (i == segments.Length - 1)
-                {
-                    node.SetRepository(fullPath, repositoryOpt);
-                }
-            }
-        }
-
-        // internal for testing
-        internal static SourceControlDirectory GetContainingRepository(string fullPath, SourceControlDirectory root)
-        {
-            var segments = PathUtilities.Split(fullPath);
-            Debug.Assert(segments.Length > 0);
-
-            Debug.Assert(root.RepositoryFullPath == null);
-            SourceControlDirectory containingRepositoryNode = null;
-
-            var node = root;
-            for (int i = 0; i < segments.Length - 1; i++)
-            {
-                int index = node.FindChildIndex(segments[i]);
-                if (index < 0)
-                {
-                    break;
-                }
-
-                node = node.OrderedChildren[index];
-                if (node.RepositoryFullPath != null)
-                {
-                    containingRepositoryNode = node;
-                }
-            }
-
-            return containingRepositoryNode;
-        }
-
-        private static readonly SequenceComparer<string> SplitPathComparer =
-             new SequenceComparer<string>(Path.DirectorySeparatorChar == '\\' ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
-
-        internal static int BinarySearch<T, TValue>(IReadOnlyList<T> list, TValue value, Func<T, TValue, int> compare)
-        {
-            var low = 0;
-            var high = list.Count - 1;
-
-            while (low <= high)
-            {
-                var middle = low + ((high - low) >> 1);
-                var midValue = list[middle];
-
-                var comparison = compare(midValue, value);
-                if (comparison == 0)
-                {
-                    return middle;
-                }
-
-                if (comparison > 0)
-                {
-                    high = middle - 1;
-                }
-                else
-                {
-                    low = middle + 1;
-                }
-            }
-
-            return ~low;
-        }
-    }
-}
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 cc9f016bd..9a6d59314 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 @@
     <PackageReference Include="Microsoft.Build.Tasks.Core" Version="$(MicrosoftBuildTasksCore)" />
   </ItemGroup>
   <ItemGroup>
-    <InternalsVisibleTo Include="Microsoft.Build.Tasks.Git.UnitTests" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Microsoft.Build.Tasks.Git\Microsoft.Build.Tasks.Git.csproj" />
diff --git a/src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.nuspec b/src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.nuspec
deleted file mode 100644
index 0d1548588..000000000
--- a/src/Microsoft.Build.Tasks.Git.Operations/Microsoft.Build.Tasks.Git.nuspec
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0"?>
-<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
-  <metadata>
-    $CommonMetadataElements$
-  </metadata>
-  <files>
-    <!-- 
-      List platforms explicitly. If a new platform is supported by libgit2 update RuntimeIdMap method to handle it.
-    -->
-    <file src="netcoreapp2.0\publish\runtimes\**\*.so" target="tools\netcoreapp2.0\runtimes" />
-    <file src="netcoreapp2.0\publish\runtimes\**\*.dylib" target="tools\netcoreapp2.0\runtimes" />
-    <file src="netcoreapp2.0\publish\runtimes\win-x86\native\*.dll" target="tools\netcoreapp2.0\runtimes\win-x86\native" />
-    <file src="netcoreapp2.0\publish\runtimes\win-x64\native\*.dll" target="tools\netcoreapp2.0\runtimes\win-x64\native" />
-
-    <file src="netcoreapp2.0\publish\LibGit2Sharp.dll" target="tools\netcoreapp2.0" />    
-    <file src="netcoreapp2.0\publish\Microsoft.Build.Tasks.Git.*" target="tools\netcoreapp2.0" />
-    <file src="netcoreapp2.0\publish\**\Microsoft.Build.Tasks.Git.resources.dll" target="tools\netcoreapp2.0" />
-
-    <file src="net461\Microsoft.Build.Tasks.Git.*" target="tools\net461" />
-    <file src="net461\**\Microsoft.Build.Tasks.Git.resources.dll" target="tools\net461" />
-    <file src="net461\LibGit2Sharp.*" target="tools\net461" />
-    <file src="net461\lib\**\*.dll" target="tools\net461\lib" />
-
-    <file src="$ProjectDirectory$\build\*.*" target="build" />
-    <file src="$ProjectDirectory$\build\*.*" target="buildCrossTargeting" />
-  </files>
-</package>
\ No newline at end of file
diff --git a/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs b/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs
deleted file mode 100644
index 79469fd5c..000000000
--- a/src/Microsoft.Build.Tasks.Git.Operations/RepositoryTasks.cs
+++ /dev/null
@@ -1,105 +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;
-using LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git
-{
-    internal static class RepositoryTasks
-    {
-        private static bool Execute<T>(T task, Action<Repository, T> 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;
-            }
-
-            Repository repo;
-            try
-            {
-                repo = new Repository(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 TaskImplementation.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.Operations/build/Microsoft.Build.Tasks.Git.targets b/src/Microsoft.Build.Tasks.Git.Operations/build/Microsoft.Build.Tasks.Git.targets
deleted file mode 100644
index 6390191be..000000000
--- a/src/Microsoft.Build.Tasks.Git.Operations/build/Microsoft.Build.Tasks.Git.targets
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project>
-  <UsingTask TaskName="Microsoft.Build.Tasks.Git.LocateRepository" AssemblyFile="$(MicrosoftBuildTasksGitAssemblyFile)"/>
-  <UsingTask TaskName="Microsoft.Build.Tasks.Git.GetSourceRoots" AssemblyFile="$(MicrosoftBuildTasksGitAssemblyFile)"/>
-  <UsingTask TaskName="Microsoft.Build.Tasks.Git.GetRepositoryUrl" AssemblyFile="$(MicrosoftBuildTasksGitAssemblyFile)"/>
-  <UsingTask TaskName="Microsoft.Build.Tasks.Git.GetSourceRevisionId" AssemblyFile="$(MicrosoftBuildTasksGitAssemblyFile)" />
-  <UsingTask TaskName="Microsoft.Build.Tasks.Git.GetUntrackedFiles" AssemblyFile="$(MicrosoftBuildTasksGitAssemblyFile)"/>
-  
-  <Target Name="InitializeSourceControlInformationFromSourceControlManager">
-    <!--
-      Determines id of the repository the project belongs to. Tasks use this id to identify repository without 
-      repeatedly traversing the directory structure.
-      
-      Reports and error if the given project doesn't belong to a repository under source control.
-    -->
-    <Microsoft.Build.Tasks.Git.LocateRepository Directory="$(MSBuildProjectDirectory)" >
-      <Output TaskParameter="Id" PropertyName="_GitRepositoryRoot" />
-    </Microsoft.Build.Tasks.Git.LocateRepository>
-
-    <PropertyGroup>
-      <RepositoryType Condition="'$(RepositoryType)' == ''">git</RepositoryType>
-    </PropertyGroup>
-
-    <Microsoft.Build.Tasks.Git.GetRepositoryUrl Root="$(_GitRepositoryRoot)">
-      <Output TaskParameter="Url" PropertyName="ScmRepositoryUrl" />
-    </Microsoft.Build.Tasks.Git.GetRepositoryUrl>
-
-    <Microsoft.Build.Tasks.Git.GetSourceRevisionId Root="$(_GitRepositoryRoot)" Condition="'$(SourceRevisionId)' == ''">
-      <Output TaskParameter="RevisionId" PropertyName="SourceRevisionId" />
-    </Microsoft.Build.Tasks.Git.GetSourceRevisionId>
-
-    <Microsoft.Build.Tasks.Git.GetSourceRoots Root="$(_GitRepositoryRoot)">
-      <Output TaskParameter="Roots" ItemName="SourceRoot" />
-    </Microsoft.Build.Tasks.Git.GetSourceRoots>
-  </Target>
-  
-  <!--
-    Embed files to the PDB that either do not belong to any of the SourceRoot directories
-    or belong to the source repository, but are explicitly ignored (excluded) from source control.
-  -->
-  <Target Name="SetEmbeddedFilesFromSourceControlManagerUntrackedFiles"
-          DependsOnTargets="InitializeSourceControlInformationFromSourceControlManager">
-
-    <Microsoft.Build.Tasks.Git.GetUntrackedFiles Root="$(_GitRepositoryRoot)" ProjectDirectory="$(MSBuildProjectDirectory)" Files="@(Compile)">
-      <Output TaskParameter="UntrackedFiles" ItemName="EmbeddedFiles" />
-    </Microsoft.Build.Tasks.Git.GetUntrackedFiles>
-  </Target>
-
-</Project>
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 000000000..89585b879
--- /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<string> Inspect(GitConfig config)
+            => config.EnumerateVariables().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.SetEqual(new[]
+            {
+                ".a=1",
+                "s2.b=2",
+                "s3.c=3 4  | 5 ",
+                "s3.d=",
+            }, Inspect(config));
+        }
+
+        [Fact]
+        public void Sections_Errors()
+        {
+            Assert.Throws<InvalidDataException>(() => 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.SetEqual(new[]
+            {
+                "c.n=cfg2|cfg3|cfg4|cfg5|cfg6|cfg7|cfg8|cfg9|cfg10",
+                "core.ignorecase=true",
+                "core.symlinks=false",
+                $"includeif.gitdir/i:{repoDir}/**/.GIT/.path=cfg8",
+                $"includeif.gitdir/i:~/**/.GIT/.path=cfg9",
+                $"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<InvalidDataException>(() => 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.SetEqual(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<InvalidDataException>(() => 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<InvalidDataException>(() => 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 013e94aa0..0aaf34085 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitDataTests.cs
@@ -1,5 +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.
-using LibGit2Sharp;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
@@ -16,6 +16,7 @@ public class GitDataTests
         [Fact]
         public void MinimalGitData()
         {
+            var environment = new GitEnvironment("/home");
             var repoDir = Temp.CreateDirectory();
 
             var gitDir = repoDir.CreateDirectory(".git");
@@ -41,20 +42,20 @@ public void MinimalGitData()
             gitDirSub.CreateDirectory("objects");
             gitDirSub.CreateDirectory("refs");
 
-            var repository = new Repository(gitDir.Path);
+            var repository = GitRepository.OpenRepository(repoDir.Path, environment);
 
             Assert.Equal("http://github.com/test-org/test-repo", GitOperations.GetRepositoryUrl(repository));
-            Assert.Equal("1111111111111111111111111111111111111111", GitOperations.GetRevisionId(repository));
+            Assert.Equal("1111111111111111111111111111111111111111", repository.GetHeadCommitSha());
 
             var warnings = new List<(string, object[])>();
-            var sourceRoots = GitOperations.GetSourceRoots(repository, (message, args) => warnings.Add((message, args)), File.Exists);
+            var sourceRoots = GitOperations.GetSourceRoots(repository, (message, args) => warnings.Add((message, args)));
             AssertEx.Equal(new[]
             {
                 $@"'{repoDir.Path}{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' ScmRepositoryUrl='http://github.com/test-org/test-repo'",
                 $@"'{repoDir.Path}{s}sub{s}' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/' ContainingRoot='{repoDir.Path}{s}' ScmRepositoryUrl='https://github.com/test-org/test-sub'",
-            }, sourceRoots.Select(GitOperationsTests.InspectSourceRoot));
+            }, sourceRoots.Select(TestUtilities.InspectSourceRoot));
 
-            AssertEx.Equal(new string[0], warnings.Select(GitOperationsTests.InspectDiagnostic));
+            AssertEx.Equal(new string[0], warnings.Select(TestUtilities.InspectDiagnostic));
 
             var files = new[] 
             {
@@ -64,7 +65,7 @@ public void MinimalGitData()
                 new MockItem(@"sub\ignore_in_submodule_d"),
             };
 
-            var untrackedFiles = GitOperations.GetUntrackedFiles(repository, files, repoDir.Path, path => new Repository(path));
+            var untrackedFiles = GitOperations.GetUntrackedFiles(repository, files, repoDir.Path, path => GitRepository.OpenRepository(path, environment));
 
             AssertEx.Equal(new[]
             {
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 000000000..1b260e6ca
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitIgnoreTests.cs
@@ -0,0 +1,216 @@
+﻿// 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 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, 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.SetEqual(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}"));
+        }
+
+        [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));
+
+            if (Path.DirectorySeparatorChar == '\\')
+            {
+                // matches "dir/" (existing directory path, the directory DIR only exists on case-insensitive FS)
+                Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "DIR")));
+            }
+
+            Assert.True(matcher.IsPathIgnored(Path.Combine(workingDir.Path, "A", "diR")));
+
+            AssertEx.SetEqual(new[]
+            {
+                "/Repo/A/DIr: True",
+                "/Repo/A: False",
+                "/Repo: False",
+            }, matcher.DirectoryIgnoreStateCache.Select(kvp => $"{kvp.Key.Substring(rootDir.Path.Length)}: {kvp.Value}"));
+        }
+    }
+}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
index ebae5bfb1..fe57377d3 100644
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
@@ -1,9 +1,9 @@
 ﻿// 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.IO;
 using System.Linq;
-using LibGit2Sharp;
 using Microsoft.Build.Framework;
 using TestUtilities;
 using Xunit;
@@ -16,65 +16,161 @@ public class GitOperationsTests
         private static readonly char s = Path.DirectorySeparatorChar;
         private static readonly string s_root = (s == '/') ? "/usr/src" : @"C:\src";
 
-        internal static string InspectSourceRoot(ITaskItem sourceRoot)
+        private static readonly GitEnvironment s_environment = new GitEnvironment("/home");
+
+        private string _workingDir = s_root;
+
+        private GitRepository CreateRepository(
+            string workingDir = null,
+            GitConfig config = null,
+            string commitSha = null,
+            ImmutableArray<GitSubmodule> submodules = default,
+            GitIgnore ignore = null)
+        {
+            workingDir ??= _workingDir;
+            var gitDir = Path.Combine(workingDir, ".git");
+            return new GitRepository(
+                s_environment,
+                config ?? GitConfig.Empty,
+                gitDir, 
+                gitDir,
+                _workingDir,
+                submodules.IsDefault ? ImmutableArray<GitSubmodule>.Empty : submodules,
+                submoduleDiagnostics: ImmutableArray<string>.Empty,
+                ignore ?? new GitIgnore(root: null, workingDir, ignoreCase: false),
+                commitSha);
+        }
+
+        private GitSubmodule CreateSubmodule(string name, string relativePath, string url, string headCommitSha)
+            => new GitSubmodule(name, relativePath, Path.GetFullPath(Path.Combine(_workingDir, relativePath)), url, headCommitSha);
+
+        internal static GitIgnore CreateIgnore(string workingDirectory, string[] filePathsRelativeToWorkingDirectory)
+        {
+            var patterns = filePathsRelativeToWorkingDirectory.Select(p => new GitIgnore.Pattern(p, GitIgnore.PatternFlags.FullPath));
+
+            return new GitIgnore(
+                new GitIgnore.PatternGroup(parent: null, PathUtils.ToPosixDirectoryPath(workingDirectory), patterns.ToImmutableArray()),
+                workingDirectory,
+                ignoreCase: false);
+        }
+
+        private static GitVariableName CreateVariableName(string str)
         {
-            var sourceControl = sourceRoot.GetMetadata("SourceControl");
-            var revisionId = sourceRoot.GetMetadata("RevisionId");
-            var nestedRoot = sourceRoot.GetMetadata("NestedRoot");
-            var containingRoot = sourceRoot.GetMetadata("ContainingRoot");
-            var scmRepositoryUrl = sourceRoot.GetMetadata("ScmRepositoryUrl");
-            var sourceLinkUrl = sourceRoot.GetMetadata("SourceLinkUrl");
-
-            return $"'{sourceRoot.ItemSpec}'" +
-              (string.IsNullOrEmpty(sourceControl) ? "" : $" SourceControl='{sourceControl}'") +
-              (string.IsNullOrEmpty(revisionId) ? "" : $" RevisionId='{revisionId}'") +
-              (string.IsNullOrEmpty(nestedRoot) ? "" : $" NestedRoot='{nestedRoot}'") +
-              (string.IsNullOrEmpty(containingRoot) ? "" : $" ContainingRoot='{containingRoot}'") +
-              (string.IsNullOrEmpty(scmRepositoryUrl) ? "" : $" ScmRepositoryUrl='{scmRepositoryUrl}'") +
-              (string.IsNullOrEmpty(sourceLinkUrl) ? "" : $" SourceLinkUrl='{sourceLinkUrl}'");
+            var parts = str.Split(new[] { '.' }, 3);
+            return parts.Length switch
+            {
+                2 => new GitVariableName(parts[0], "", parts[1]),
+                3 => new GitVariableName(parts[0], parts[1], parts[2]),
+                _ => throw new InvalidOperationException()
+            };
         }
 
-        public static string InspectDiagnostic((string Message, object[] Args) warning)
-            => string.Format(warning.Message, warning.Args);
+        private static GitConfig CreateConfig(params (string Name, string Value)[] variables)
+            => new GitConfig(ImmutableDictionary.CreateRange(
+                variables.Select(v => new KeyValuePair<GitVariableName, ImmutableArray<string>>(CreateVariableName(v.Name), ImmutableArray.Create(v.Value)))));
 
         [Fact]
-        public void GetRevisionId_RepoWithoutCommits()
+        public void GetRepositoryUrl_NoRemotes()
         {
-            var repo = new TestRepository(workingDir: "", commitSha: null);
-            Assert.Null(GitOperations.GetRevisionId(repo));
+            var repo = CreateRepository();
+            var warnings = new List<(string, object[])>();
+            Assert.Null(GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args))));
+            AssertEx.Equal(new[] { Resources.RepositoryHasNoRemote }, warnings.Select(TestUtilities.InspectDiagnostic));
         }
 
         [Fact]
-        public void GetRevisionId_RepoWithCommit()
+        public void GetRepositoryUrl_Origin()
         {
-            var repo = new TestRepository(workingDir: "", commitSha: "8398cdcd9043724b9bef1efda8a703dfaa336c0f");
-            Assert.Equal("8398cdcd9043724b9bef1efda8a703dfaa336c0f", GitOperations.GetRevisionId(repo));
+            var repo = CreateRepository(config: CreateConfig(
+                ("remote.abc.url", "http://github.com/abc"),
+                ("remote.origin.url", "http://github.com/origin")));
+
+            var warnings = new List<(string, object[])>();
+
+            Assert.Equal("http://github.com/origin", GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args))));
+
+            Assert.Empty(warnings);
         }
 
         [Fact]
-        public void GetRepositoryUrl_NoRemotes()
+        public void GetRepositoryUrl_NoOrigin()
         {
-            var repo = new TestRepository(workingDir: s_root, commitSha: "1111111111111111111111111111111111111111");
+            var repo = CreateRepository(config: CreateConfig(
+                ("remote.abc.url", "http://github.com/abc"),
+                ("remote.def.url", "http://github.com/def")));
 
             var warnings = new List<(string, object[])>();
-            Assert.Null(GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args))));
-            AssertEx.Equal(new[] { Resources.RepositoryHasNoRemote }, warnings.Select(InspectDiagnostic));
+
+            Assert.Equal("http://github.com/abc", GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args))));
+
+            Assert.Empty(warnings);
         }
 
-        private void ValidateGetRepositoryUrl(string workingDir, string actualUrl, string expectedUrl)
+        [Fact]
+        public void GetRepositoryUrl_Specified()
         {
-            var testRemote = new TestRemote("origin", actualUrl);
+            var repo = CreateRepository(config: CreateConfig(
+                ("remote.abc.url", "http://github.com/abc"),
+                ("remote.origin.url", "http://github.com/origin")));
 
-            var repo = new TestRepository(workingDir, commitSha: "1111111111111111111111111111111111111111",
-                remotes: new[] { testRemote });
+            var warnings = new List<(string, object[])>();
 
-            var expectedWarnings = (expectedUrl != null) ?
-                Array.Empty<string>() :
-                new[] { string.Format(Resources.InvalidRepositoryRemoteUrl, testRemote.Name, testRemote.Url) };
+            Assert.Equal("http://github.com/abc",
+                GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args)),
+                remoteName: "abc"));
+
+            Assert.Empty(warnings);
+        }
+
+        [Fact]
+        public void GetRepositoryUrl_SpecifiedNotFound_OriginFallback()
+        {
+            var repo = CreateRepository(config: CreateConfig(
+                ("remote.abc.url", "http://github.com/abc"),
+                ("remote.origin.url", "http://github.com/origin")));
+
+            var warnings = new List<(string, object[])>();
+
+            Assert.Equal("http://github.com/origin", 
+                GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args)),
+                remoteName: "myremote"));
+
+            AssertEx.Equal(new[]
+            {
+                string.Format(Resources.RepositoryDoesNotHaveSpecifiedRemote, "myremote", "origin")
+            }, warnings.Select(TestUtilities.InspectDiagnostic));
+        }
+
+        [Fact]
+        public void GetRepositoryUrl_SpecifiedNotFound_FirstFallback()
+        {
+            var repo = CreateRepository(config: CreateConfig(
+                ("remote.abc.url", "http://github.com/abc"),
+                ("remote.def.url", "http://github.com/def")));
+
+            var warnings = new List<(string, object[])>();
+
+            Assert.Equal("http://github.com/abc",
+                GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args)),
+                remoteName: "myremote"));
+
+            AssertEx.Equal(new[]
+            {
+                string.Format(Resources.RepositoryDoesNotHaveSpecifiedRemote, "myremote", "abc")
+            }, warnings.Select(TestUtilities.InspectDiagnostic));
+        }
+
+        [Fact]
+        public void GetRepositoryUrl_BadUrl()
+        {
+            var repo = CreateRepository(config: CreateConfig(("remote.origin.url", "http://?")));
 
             var warnings = new List<(string, object[])>();
-            Assert.Equal(expectedUrl, GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args))));
-            AssertEx.Equal(expectedWarnings, warnings.Select(InspectDiagnostic));
+            Assert.Null(GitOperations.GetRepositoryUrl(repo, (message, args) => warnings.Add((message, args))));
+            AssertEx.Equal(new[]
+            {
+                string.Format(Resources.InvalidRepositoryRemoteUrl, "origin", "http://?")
+            }, warnings.Select(TestUtilities.InspectDiagnostic));
         }
 
         [Theory]
@@ -83,9 +179,9 @@ private void ValidateGetRepositoryUrl(string workingDir, string actualUrl, strin
         [InlineData("http://github.com:102/org/repo")]
         [InlineData("ssh://user@github.com/org/repo")]
         [InlineData("abc://user@github.com/org/repo")]
-        public void GetRepositoryUrl_PlatformAgnostic1(string url)
+        public void NormalizeUrl_PlatformAgnostic1(string url)
         {
-            ValidateGetRepositoryUrl(s_root, url, url);
+            Assert.Equal(url, GitOperations.NormalizeUrl(url, s_root));
         }
 
         [Theory]
@@ -95,9 +191,9 @@ public void GetRepositoryUrl_PlatformAgnostic1(string url)
         [InlineData("ssh://github.com/org/../repo", "ssh://github.com/repo")]
         [InlineData("ssh://github.com/%32/repo", "ssh://github.com/2/repo")]
         [InlineData("ssh://github.com/%3F/repo", "ssh://github.com/%3F/repo")]
-        public void GetRepositoryUrl_PlatformAgnostic2(string url, string expectedUrl)
+        public void NormalizeUrl_PlatformAgnostic2(string url, string expectedUrl)
         {
-            ValidateGetRepositoryUrl(s_root, url, expectedUrl);
+            Assert.Equal(expectedUrl, GitOperations.NormalizeUrl(url, s_root));
         }
 
         [ConditionalTheory(typeof(WindowsOnly))]
@@ -123,9 +219,9 @@ public void GetRepositoryUrl_PlatformAgnostic2(string url, string expectedUrl)
         [InlineData(@".:/../../relative/path", "ssh://./relative/path")]
         [InlineData(@"..:/../../relative/path", "ssh://../relative/path")]
         [InlineData(@"@:org/repo", "file:///C:/src/a/b/@:org/repo")]
-        public void GetRepositoryUrl_Windows(string url, string expectedUrl)
+        public void NormalizeUrl_Windows(string url, string expectedUrl)
         {
-            ValidateGetRepositoryUrl(@"C:\src\a\b", url, expectedUrl);
+            Assert.Equal(expectedUrl, GitOperations.NormalizeUrl(url, @"C:\src\a\b"));
         }
 
         [ConditionalTheory(typeof(UnixOnly))]
@@ -142,9 +238,9 @@ public void GetRepositoryUrl_Windows(string url, string expectedUrl)
         [InlineData(@".:/../../relative/path", "ssh://./relative/path")]
         [InlineData(@"..:/../../relative/path", "ssh://../relative/path")]
         [InlineData(@"@:org/repo", @"file:///usr/src/a/b/@:org/repo")]
-        public void GetRepositoryUrl_Unix(string url, string expectedUrl)
+        public void NormalizeUrl_Unix(string url, string expectedUrl)
         {
-            ValidateGetRepositoryUrl("/usr/src/a/b", url, expectedUrl);
+            Assert.Equal(expectedUrl, GitOperations.NormalizeUrl(url, "/usr/src/a/b"));
         }
 
         [Theory]
@@ -156,90 +252,84 @@ public void GetRepositoryUrl_Unix(string url, string expectedUrl)
         [InlineData("http:x//y", "ssh://http/x//y")]
         public void GetRepositoryUrl_ScpSyntax(string url, string expectedUrl)
         {
-            ValidateGetRepositoryUrl(s_root, url, expectedUrl);
+            Assert.Equal(expectedUrl, GitOperations.NormalizeUrl(url, s_root));
         }
 
         [Fact]
         public void GetSourceRoots_RepoWithoutCommits()
         {
-            var repo = new TestRepository(workingDir: s_root, commitSha: null);
+            var repo = CreateRepository();
 
             var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
+            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)));
 
             Assert.Empty(items);
-            AssertEx.Equal(new[] { Resources.RepositoryHasNoCommit }, warnings.Select(InspectDiagnostic));
+            AssertEx.Equal(new[] { Resources.RepositoryHasNoCommit }, warnings.Select(TestUtilities.InspectDiagnostic));
         }
 
         [Fact]
         public void GetSourceRoots_RepoWithoutCommitsWithSubmodules()
         {
-            var repo = new TestRepository(
-                workingDir: s_root,
+            var repo = CreateRepository(
                 commitSha: null,
-                submodules: new[] 
-                {
-                    new TestSubmodule("1", "sub/1", "http://1.com", "1111111111111111111111111111111111111111"),
-                    new TestSubmodule("1", "sub/2", "http://2.com", "2222222222222222222222222222222222222222")
-                });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("1", "sub/1", "http://1.com", "1111111111111111111111111111111111111111"),
+                    CreateSubmodule("1", "sub/2", "http://2.com", "2222222222222222222222222222222222222222")));
 
             var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
+            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)));
 
             AssertEx.Equal(new[]
             {
-                $@"'{s_root}{s}sub{s}1{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='{s_root}{s}' ScmRepositoryUrl='http://1.com/'",
-                $@"'{s_root}{s}sub{s}2{s}' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/2/' ContainingRoot='{s_root}{s}' ScmRepositoryUrl='http://2.com/'",
-            }, items.Select(InspectSourceRoot));
+                $@"'{_workingDir}{s}sub{s}1{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='http://1.com/'",
+                $@"'{_workingDir}{s}sub{s}2{s}' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/2/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='http://2.com/'",
+            }, items.Select(TestUtilities.InspectSourceRoot));
 
-            AssertEx.Equal(new[] { Resources.RepositoryHasNoCommit }, warnings.Select(InspectDiagnostic));
+            AssertEx.Equal(new[] { Resources.RepositoryHasNoCommit }, warnings.Select(TestUtilities.InspectDiagnostic));
         }
 
         [Fact]
         public void GetSourceRoots_RepoWithCommitsWithSubmodules()
         {
-            var repo = new TestRepository(
-                workingDir: s_root,
+            var repo = CreateRepository(
                 commitSha: "0000000000000000000000000000000000000000",
-                submodules: new[]
-                {
-                    new TestSubmodule("1", "sub/1", "http://1.com", workDirCommitSha: null),
-                    new TestSubmodule("1", "sub/2", "http://2.com", "2222222222222222222222222222222222222222")
-                });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("1", "sub/1", "http://1.com", headCommitSha: null),
+                    CreateSubmodule("1", "sub/2", "http://2.com", "2222222222222222222222222222222222222222")));
 
             var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
+            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)));
 
             AssertEx.Equal(new[]
             {
-                $@"'{s_root}{s}' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
-                $@"'{s_root}{s}sub{s}2{s}' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/2/' ContainingRoot='{s_root}{s}' ScmRepositoryUrl='http://2.com/'",
-            }, items.Select(InspectSourceRoot));
+                $@"'{_workingDir}{s}' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
+                $@"'{_workingDir}{s}sub{s}2{s}' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/2/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='http://2.com/'",
+            }, items.Select(TestUtilities.InspectSourceRoot));
 
-            AssertEx.Equal(new[] { string.Format(Resources.SubmoduleWithoutCommit_SourceLink, "1") }, warnings.Select(InspectDiagnostic));
+            AssertEx.Equal(new[] { string.Format(Resources.SourceCodeWontBeAvailableViaSourceLink, string.Format(Resources.SubmoduleWithoutCommit, "1")) }, 
+                warnings.Select(TestUtilities.InspectDiagnostic));
         }
 
         [ConditionalFact(typeof(WindowsOnly))]
         public void GetSourceRoots_RelativeSubmodulePaths_Windows()
         {
-            var repo = new TestRepository(
-                workingDir: @"C:\src",
+            _workingDir = @"C:\src";
+
+            var repo = CreateRepository(
                 commitSha: "0000000000000000000000000000000000000000",
-                submodules: new[]
-                {
-                    new TestSubmodule("1", "sub/1", "./a/b", "1111111111111111111111111111111111111111"),
-                    new TestSubmodule("2", "sub/2", "../a", "2222222222222222222222222222222222222222"),
-                });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("1", "sub/1", "./a/b", "1111111111111111111111111111111111111111"),
+                    CreateSubmodule("2", "sub/2", "../a", "2222222222222222222222222222222222222222")));
 
             var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
+            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)));
 
             AssertEx.Equal(new[]
             {
                 $@"'C:\src\' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
                 $@"'C:\src\sub\1\' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='C:\src\' ScmRepositoryUrl='file:///C:/src/a/b'",
                 $@"'C:\src\sub\2\' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/2/' ContainingRoot='C:\src\' ScmRepositoryUrl='file:///C:/a'",
-            }, items.Select(InspectSourceRoot));
+            }, items.Select(TestUtilities.InspectSourceRoot));
 
             Assert.Empty(warnings);
         }
@@ -247,24 +337,23 @@ public void GetSourceRoots_RelativeSubmodulePaths_Windows()
         [ConditionalFact(typeof(WindowsOnly))]
         public void GetSourceRoots_RelativeSubmodulePaths_Windows_UnicodeAndEscapes()
         {
-            var repo = new TestRepository(
-                workingDir: @"C:\%25@噸",
+            _workingDir = @"C:\%25@噸";
+
+            var repo = CreateRepository(
                 commitSha: "0000000000000000000000000000000000000000",
-                submodules: new[]
-                {
-                    new TestSubmodule("%25ሴ", "sub/%25ሴ", "./a/b", "1111111111111111111111111111111111111111"),
-                    new TestSubmodule("%25ለ", "sub/%25ለ", "../a", "2222222222222222222222222222222222222222"),
-                });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("%25ሴ", "sub/%25ሴ", "./a/b", "1111111111111111111111111111111111111111"),
+                    CreateSubmodule("%25ለ", "sub/%25ለ", "../a", "2222222222222222222222222222222222222222")));
 
             var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
+            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)));
 
             AssertEx.Equal(new[]
             {
                 $@"'C:\%25@噸\' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
                 $@"'C:\%25@噸\sub\%25ሴ\' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/%25ሴ/' ContainingRoot='C:\%25@噸\' ScmRepositoryUrl='file:///C:/%25@噸/a/b'",
                 $@"'C:\%25@噸\sub\%25ለ\' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/%25ለ/' ContainingRoot='C:\%25@噸\' ScmRepositoryUrl='file:///C:/a'",
-            }, items.Select(InspectSourceRoot));
+            }, items.Select(TestUtilities.InspectSourceRoot));
 
             Assert.Empty(warnings);
         }
@@ -272,24 +361,23 @@ public void GetSourceRoots_RelativeSubmodulePaths_Windows_UnicodeAndEscapes()
         [ConditionalFact(typeof(UnixOnly))]
         public void GetSourceRoots_RelativeSubmodulePaths_Unix()
         {
-            var repo = new TestRepository(
-                workingDir: @"/src",
+            _workingDir = @"/src";
+
+            var repo = CreateRepository(
                 commitSha: "0000000000000000000000000000000000000000",
-                submodules: new[]
-                {
-                    new TestSubmodule("1", "sub/1", "./a/b", "1111111111111111111111111111111111111111"),
-                    new TestSubmodule("2", "sub/2", "../a", "2222222222222222222222222222222222222222"),
-                });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("1", "sub/1", "./a/b", "1111111111111111111111111111111111111111"),
+                    CreateSubmodule("2", "sub/2", "../a", "2222222222222222222222222222222222222222")));
 
             var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
+            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)));
 
             AssertEx.Equal(new[]
             {
                 $@"'/src/' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
                 $@"'/src/sub/1/' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='/src/' ScmRepositoryUrl='file:///src/a/b'",
                 $@"'/src/sub/2/' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/2/' ContainingRoot='/src/' ScmRepositoryUrl='file:///a'",
-            }, items.Select(InspectSourceRoot));
+            }, items.Select(TestUtilities.InspectSourceRoot));
 
             Assert.Empty(warnings);
         }
@@ -297,107 +385,49 @@ public void GetSourceRoots_RelativeSubmodulePaths_Unix()
         [ConditionalFact(typeof(UnixOnly), Skip = "https://github.com/dotnet/corefx/issues/34227")]
         public void GetSourceRoots_RelativeSubmodulePaths_Unix_UnicodeAndEscapes()
         {
-            var repo = new TestRepository(
-                workingDir: @"/%25@噸",
+            _workingDir = @"/%25@噸";
+
+            var repo = CreateRepository(
                 commitSha: "0000000000000000000000000000000000000000",
-                submodules: new[]
-                {
-                    new TestSubmodule("%25ሴ", "sub/%25ሴ", "./a/b", "1111111111111111111111111111111111111111"),
-                    new TestSubmodule("%25ለ", "sub/%25ለ", "../a", "2222222222222222222222222222222222222222"),
-                });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("%25ሴ", "sub/%25ሴ", "./a/b", "1111111111111111111111111111111111111111"),
+                    CreateSubmodule("%25ለ", "sub/%25ለ", "../a", "2222222222222222222222222222222222222222")));
 
             var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
+            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)));
 
             AssertEx.Equal(new[]
             {
                 $@"'/%25@噸/' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
                 $@"'/%25@噸/sub/%25ሴ/' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/%25ሴ/' ContainingRoot='/%25@噸/' ScmRepositoryUrl='file:///%25@噸/a/b'",
                 $@"'/%25@噸/sub/%25ለ/' SourceControl='git' RevisionId='2222222222222222222222222222222222222222' NestedRoot='sub/%25ለ/' ContainingRoot='/%25@噸/' ScmRepositoryUrl='file:///a'",
-            }, items.Select(InspectSourceRoot));
+            }, items.Select(TestUtilities.InspectSourceRoot));
 
             Assert.Empty(warnings);
         }
 
         [Fact]
-        public void GetSourceRoots_InvalidSubmoduleUrlOrPath()
+        public void GetSourceRoots_InvalidSubmoduleUrl()
         {
-            var repo = new TestRepository(
-                workingDir: s_root,
+            var repo = CreateRepository(
                 commitSha: "0000000000000000000000000000000000000000",
-                submodules: new[]
-                {
-                    new TestSubmodule("1", "sub/1", "http:///", "1111111111111111111111111111111111111111"),
-                    new TestSubmodule("2", "sub/\0*<>|:", "http://2.com", "2222222222222222222222222222222222222222"),
-                    new TestSubmodule("3", "sub/3", "http://3.com", "3333333333333333333333333333333333333333"),
-                });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("1", "sub/1", "http:///", "1111111111111111111111111111111111111111"),
+                    CreateSubmodule("3", "sub/3", "http://3.com", "3333333333333333333333333333333333333333")));
 
             var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
+            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)));
 
             AssertEx.Equal(new[]
             {
                 $@"'{s_root}{s}' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
                 $@"'{s_root}{s}sub{s}3{s}' SourceControl='git' RevisionId='3333333333333333333333333333333333333333' NestedRoot='sub/3/' ContainingRoot='{s_root}{s}' ScmRepositoryUrl='http://3.com/'",
-            }, items.Select(InspectSourceRoot));
+            }, items.Select(TestUtilities.InspectSourceRoot));
 
             AssertEx.Equal(new[] 
             {
-                string.Format(Resources.InvalidSubmoduleUrl_SourceLink, "1", "http:///"),
-                string.Format(Resources.InvalidSubmodulePath_SourceLink, "2", "sub/\0*<>|:")
-            }, warnings.Select(InspectDiagnostic));
-        }
-
-        [Fact]
-        public void GetSourceRoots_GvfsWithoutModules()
-        {
-            var repo = new TestRepository(
-                workingDir: s_root,
-                commitSha: "0000000000000000000000000000000000000000",
-                config: new Dictionary<string, object> { { "core.gvfs", true } },
-                submodulesSupported: false);
-
-            var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: _ => false);
-
-            AssertEx.Equal(new[]
-            {
-                $@"'{s_root}{s}' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
-            }, items.Select(InspectSourceRoot));
-        }
-
-        [Fact]
-        public void GetSourceRoots_GvfsWithModules()
-        {
-            var repo = new TestRepository(
-                workingDir: s_root,
-                commitSha: "0000000000000000000000000000000000000000",
-                config: new Dictionary<string, object> { { "core.gvfs", true } },
-                submodulesSupported: false);
-
-            Assert.Throws<LibGit2SharpException>(() => GitOperations.GetSourceRoots(repo, null, fileExists: _ => true));
-        }
-
-        [Fact]
-        public void GetSourceRoots_GvfsBadOptionType()
-        {
-            var repo = new TestRepository(
-                workingDir: s_root,
-                commitSha: "0000000000000000000000000000000000000000",
-                config: new Dictionary<string, object> { { "core.gvfs", 1 } },
-                submodules: new[]
-                {
-                    new TestSubmodule("1", "sub/1", "http://1.com/", "1111111111111111111111111111111111111111"),
-                });
-
-            var warnings = new List<(string, object[])>();
-            var items = GitOperations.GetSourceRoots(repo, (message, args) => warnings.Add((message, args)), fileExists: null);
-
-            AssertEx.Equal(new[]
-            {
-                $@"'{s_root}{s}' SourceControl='git' RevisionId='0000000000000000000000000000000000000000'",
-                $@"'{s_root}{s}sub{s}1{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='{s_root}{s}' ScmRepositoryUrl='http://1.com/'",
-            }, items.Select(InspectSourceRoot));
+                string.Format(Resources.SourceCodeWontBeAvailableViaSourceLink, string.Format(Resources.InvalidSubmoduleUrl, "1", "http:///")),
+            }, warnings.Select(TestUtilities.InspectDiagnostic));
         }
 
         [ConditionalTheory(typeof(WindowsOnly))]
@@ -420,24 +450,24 @@ public void GetSourceRoots_GvfsBadOptionType()
         public void GetContainingRepository_Windows(string path, string expectedDirectory)
         {
             var actual = GitOperations.GetContainingRepository(path,
-                new GitOperations.SourceControlDirectory("", null,
-                    new List<GitOperations.SourceControlDirectory>
+                new GitOperations.DirectoryNode("", null,
+                    new List<GitOperations.DirectoryNode>
                     {
-                        new GitOperations.SourceControlDirectory("C:", null, new List<GitOperations.SourceControlDirectory>
+                        new GitOperations.DirectoryNode("C:", null, new List<GitOperations.DirectoryNode>
                         {
-                            new GitOperations.SourceControlDirectory("src", @"C:\src", new List<GitOperations.SourceControlDirectory>
+                            new GitOperations.DirectoryNode("src", @"C:\src", new List<GitOperations.DirectoryNode>
                             {
-                                new GitOperations.SourceControlDirectory("a", @"C:\src\a"),
-                                new GitOperations.SourceControlDirectory("c", @"C:\src\c", new List<GitOperations.SourceControlDirectory>
+                                new GitOperations.DirectoryNode("a", @"C:\src\a"),
+                                new GitOperations.DirectoryNode("c", @"C:\src\c", new List<GitOperations.DirectoryNode>
                                 {
-                                    new GitOperations.SourceControlDirectory("x", @"C:\src\c\x")
+                                    new GitOperations.DirectoryNode("x", @"C:\src\c\x")
                                 }),
-                                new GitOperations.SourceControlDirectory("e", @"C:\src\e")
+                                new GitOperations.DirectoryNode("e", @"C:\src\e")
                             }),
                         })
                     }));
 
-            Assert.Equal(expectedDirectory, actual?.RepositoryFullPath);
+            Assert.Equal(expectedDirectory, actual?.WorkingDirectoryFullPath);
         }
 
         [ConditionalTheory(typeof(UnixOnly))]
@@ -460,46 +490,43 @@ public void GetContainingRepository_Windows(string path, string expectedDirector
         public void GetContainingRepository_Unix(string path, string expectedDirectory)
         {
             var actual = GitOperations.GetContainingRepository(path,
-                new GitOperations.SourceControlDirectory("", null,
-                    new List<GitOperations.SourceControlDirectory>
+                new GitOperations.DirectoryNode("", null,
+                    new List<GitOperations.DirectoryNode>
                     {
-                        new GitOperations.SourceControlDirectory("/", null, new List<GitOperations.SourceControlDirectory>
+                        new GitOperations.DirectoryNode("/", null, new List<GitOperations.DirectoryNode>
                         {
-                            new GitOperations.SourceControlDirectory("src", "/src", new List<GitOperations.SourceControlDirectory>
+                            new GitOperations.DirectoryNode("src", "/src", new List<GitOperations.DirectoryNode>
                             {
-                                new GitOperations.SourceControlDirectory("a", "/src/a"),
-                                new GitOperations.SourceControlDirectory("c", "/src/c", new List<GitOperations.SourceControlDirectory>
+                                new GitOperations.DirectoryNode("a", "/src/a"),
+                                new GitOperations.DirectoryNode("c", "/src/c", new List<GitOperations.DirectoryNode>
                                 {
-                                    new GitOperations.SourceControlDirectory("x", "/src/c/x"),
+                                    new GitOperations.DirectoryNode("x", "/src/c/x"),
                                 }),
-                                new GitOperations.SourceControlDirectory("e", "/src/e"),
+                                new GitOperations.DirectoryNode("e", "/src/e"),
                             }),
                         })
                     }));
 
-            Assert.Equal(expectedDirectory, actual?.RepositoryFullPath);
+            Assert.Equal(expectedDirectory, actual?.WorkingDirectoryFullPath);
         }
 
         [Fact]
         public void BuildDirectoryTree()
         {
-            var repo = new TestRepository(
-                workingDir: s_root,
+            var repo = CreateRepository(
                 commitSha: null,
-                submodules: new[]
-                {
-                    new TestSubmodule(null, "c/x", null, null),
-                    new TestSubmodule(null, "e", null, null),
-                    new TestSubmodule(null, "a", null, null),
-                    new TestSubmodule(null, "a/a/a/a/", null, null),
-                    new TestSubmodule(null, "c", null, null),
-                    new TestSubmodule(null, "a/z", null, null),
-                });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("1", "c/x", "http://github.com/1", null),
+                    CreateSubmodule("2", "e", "http://github.com/2", null),
+                    CreateSubmodule("3", "a", "http://github.com/3", null),
+                    CreateSubmodule("4", "a/a/a/a/", "http://github.com/4", null),
+                    CreateSubmodule("5", "c", "http://github.com/5", null),
+                    CreateSubmodule("6", "a/z", "http://github.com/6", null)));
 
             var root = GitOperations.BuildDirectoryTree(repo);
 
-            string inspect(GitOperations.SourceControlDirectory node)
-                => node.Name + (node.RepositoryFullPath != null ? $"!" : "") + "{" + string.Join(",", node.OrderedChildren.Select(inspect)) + "}";
+            string inspect(GitOperations.DirectoryNode node)
+                => node.Name + (node.WorkingDirectoryFullPath != null ? $"!" : "") + "{" + string.Join(",", node.OrderedChildren.Select(inspect)) + "}";
 
             var expected = IsUnix ?
                 "{/{usr{src!{a!{a{a{a!{}}},z!{}},c!{x!{}},e!{}}}}}" :
@@ -511,25 +538,20 @@ string inspect(GitOperations.SourceControlDirectory node)
         [Fact]
         public void GetUntrackedFiles_ProjectInMainRepoIncludesFilesInSubmodules()
         {
-            string gitRoot = s_root.Replace('\\', '/');
-
-            var repo = new TestRepository(
-                workingDir: s_root,
+            var repo = CreateRepository(
                 commitSha: "0000000000000000000000000000000000000000",
-                submodules: new[]
-                {
-                    new TestSubmodule("1", "sub/1", "http://1.com", "1111111111111111111111111111111111111111"),
-                    new TestSubmodule("2", "sub/2", "http://2.com", "2222222222222222222222222222222222222222")
-                },
-                ignoredPaths: new[] { gitRoot + @"/c.cs", gitRoot + @"/p/d.cs", gitRoot + @"/sub/1/x.cs" });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("1", "sub/1", "http://1.com", "1111111111111111111111111111111111111111"),
+                    CreateSubmodule("2", "sub/2", "http://2.com", "2222222222222222222222222222222222222222")),
+                ignore: CreateIgnore(_workingDir, new[] { "c.cs", "p/d.cs", "sub/1/x.cs" }));
 
-            var subRoot1 = Path.Combine(s_root, "sub", "1");
-            var subRoot2 = Path.Combine(s_root, "sub", "2");
+            var subRoot1 = Path.Combine(_workingDir, "sub", "1");
+            var subRoot2 = Path.Combine(_workingDir, "sub", "2");
 
-            var subRepos = new Dictionary<string, TestRepository>()
+            var subRepos = new Dictionary<string, GitRepository>()
             {
-                { subRoot1, new TestRepository(subRoot1, commitSha: null, ignoredPaths: new[] { gitRoot + @"/sub/1/obj/a.cs" }) },
-                { subRoot2, new TestRepository(subRoot2, commitSha: null, ignoredPaths: new[] { gitRoot + @"/sub/2/obj/b.cs" }) },
+                { subRoot1, CreateRepository(workingDir: subRoot1, commitSha: null, ignore: CreateIgnore(subRoot1, new[] { "obj/a.cs" })) },
+                { subRoot2, CreateRepository(workingDir: subRoot2, commitSha: null, ignore: CreateIgnore(subRoot2, new[] { "obj/b.cs" })) },
             };
 
             var actual = GitOperations.GetUntrackedFiles(repo,
@@ -542,7 +564,7 @@ public void GetUntrackedFiles_ProjectInMainRepoIncludesFilesInSubmodules()
                     new MockItem(@"..\..\w.cs"),                   // outside of repo
                     new MockItem(IsUnix ? "/d/w.cs" : @"D:\w.cs"), // outside of repo
                 },
-                projectDirectory: Path.Combine(s_root, "p"),
+                projectDirectory: Path.Combine(_workingDir, "p"),
                 root => subRepos[root]);
 
             AssertEx.Equal(new[] 
@@ -557,25 +579,20 @@ public void GetUntrackedFiles_ProjectInMainRepoIncludesFilesInSubmodules()
         [Fact]
         public void GetUntrackedFiles_ProjectInSubmodule()
         {
-            string gitRoot = s_root.Replace('\\', '/');
-
-            var repo = new TestRepository(
-                workingDir: s_root,
+            var repo = CreateRepository(
                 commitSha: "0000000000000000000000000000000000000000",
-                submodules: new[]
-                {
-                    new TestSubmodule("1", "sub/1", "http://1.com", "1111111111111111111111111111111111111111"),
-                    new TestSubmodule("2", "sub/2", "http://2.com", "2222222222222222222222222222222222222222")
-                },
-                ignoredPaths: new[] { gitRoot + "/c.cs", gitRoot + "/sub/1/x.cs" });
+                submodules: ImmutableArray.Create(
+                    CreateSubmodule("1", "sub/1", "http://1.com", "1111111111111111111111111111111111111111"),
+                    CreateSubmodule("2", "sub/2", "http://2.com", "2222222222222222222222222222222222222222")),
+                ignore: CreateIgnore(_workingDir, new[] { "c.cs", "sub/1/x.cs" }));
 
             var subRoot1 = Path.Combine(s_root, "sub", "1");
             var subRoot2 = Path.Combine(s_root, "sub", "2");
 
-            var subRepos = new Dictionary<string, TestRepository>()
+            var subRepos = new Dictionary<string, GitRepository>()
             {
-                { subRoot1, new TestRepository(subRoot1, commitSha: null, ignoredPaths: new[] { gitRoot + "/sub/1/obj/a.cs" }) },
-                { subRoot2, new TestRepository(subRoot2, commitSha: null, ignoredPaths: new[] { gitRoot + "/sub/2/obj/b.cs" }) },
+                { subRoot1, CreateRepository(subRoot1, commitSha: null, ignore: CreateIgnore(subRoot1, new[] { "obj/a.cs" })) },
+                { subRoot2, CreateRepository(subRoot2, commitSha: null, ignore: CreateIgnore(subRoot2, new[] { "obj/b.cs" })) },
             };
 
             var actual = GitOperations.GetUntrackedFiles(repo,
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 000000000..d0f278d92
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs
@@ -0,0 +1,376 @@
+﻿// 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<NotSupportedException>(() => 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 = GitRepository.EnumerateSubmoduleConfig(repository.ReadSubmoduleConfig());
+            AssertEx.Equal(new[]
+            {
+                "S1: 'subs/s1' 'http://github.com/test1'",
+                "S2: 's2' 'http://github.com/test3'",
+                "S3: 's3' '../repo2'",
+            }, submodules.Where(s => s.Url != null && s.Path != null).Select(s => $"{s.Name}: '{s.Path}' '{s.Url}'"));
+        }
+
+        [Fact]
+        public void Submodules_Errors()
+        {
+            using var temp = new TempRoot();
+
+            var workingDir = temp.CreateDirectory();
+            var gitDir = workingDir.CreateDirectory(".git");
+            gitDir.CreateDirectory("modules").CreateDirectory("sub10").CreateDirectory("commondir");
+
+            workingDir.CreateDirectory("sub6").CreateDirectory(".git");
+            workingDir.CreateDirectory("sub7").CreateFile(".git").WriteAllText("xyz");
+            workingDir.CreateDirectory("sub8").CreateFile(".git").WriteAllText("gitdir: \0<>");
+            workingDir.CreateDirectory("sub9").CreateFile(".git").WriteAllText("gitdir: ../.git/modules/sub9");
+            workingDir.CreateDirectory("sub10").CreateFile(".git").WriteAllText("gitdir: ../.git/modules/sub10");
+
+            workingDir.CreateFile(".gitmodules").WriteAllText(@"
+[submodule ""S1""]             # whitespace-only path
+  path = ""  ""
+  url = http://github.com
+
+[submodule ""S2""]             # empty path
+  path =                  
+  url = http://github.com
+
+[submodule ""S4""]             # invalid path
+  path = sub<>
+  url = http://github.com
+
+[submodule ""S3""]             # invalid url
+  path = sub3
+  url = http://?
+
+[submodule ""S4""]             # whitespace-only url
+  path = sub4
+  url = ""   ""             
+
+[submodule ""S5""]             # path does not exist
+  path = sub5
+  url = http://github.com
+
+[submodule ""S6""]             # sub6/.git is a directory, but should be a file
+  path = sub6
+  url = http://github.com
+
+[submodule ""S7""]             # sub7/.git has invalid format
+  path = sub7
+  url = http://github.com
+
+[submodule ""S8""]             # sub8/.git contains invalid path
+  path = sub8
+  url = http://github.com
+
+[submodule ""S9""]             # sub9/.git points to directory that does not exist
+  path = sub9
+  url = http://github.com
+
+[submodule ""S10""]            # sub10/.git points to directory that has commondir directory (it should be a file)
+  path = sub10
+  url = http://github.com
+");
+            var repository = new GitRepository(new GitEnvironment("/home"), GitConfig.Empty, gitDir.Path, gitDir.Path, workingDir.Path);
+
+            var submodules = repository.GetSubmodules();
+            AssertEx.Equal(new[]
+            {
+                "S10: 'sub10' 'http://github.com'",
+                "S9: 'sub9' 'http://github.com'"
+            }, submodules.Select(s => $"{s.Name}: '{s.WorkingDirectoryRelativePath}' '{s.Url}'"));
+
+            var diagnostics = repository.GetSubmoduleDiagnostics();
+            AssertEx.Equal(new[]
+            {
+              // The path of submodule 'S1' is missing or invalid: '  '
+              string.Format(Resources.InvalidSubmodulePath, "S1", "  "),
+              // The path of submodule 'S2' is missing or invalid: ''
+              string.Format(Resources.InvalidSubmodulePath, "S2", ""),
+              // Could not find a part of the path 'sub3\.git'.
+              TestUtilities.GetExceptionMessage(() => File.ReadAllText(Path.Combine(workingDir.Path, "sub3", ".git"))),
+              // The URL of submodule 'S4' is missing or invalid: '   '
+              string.Format(Resources.InvalidSubmoduleUrl, "S4", "   "),
+              // Could not find a part of the path 'sub5\.git'.
+              TestUtilities.GetExceptionMessage(() => File.ReadAllText(Path.Combine(workingDir.Path, "sub5", ".git"))),
+              // Access to the path 'sub6\.git' is denied
+              TestUtilities.GetExceptionMessage(() => File.ReadAllText(Path.Combine(workingDir.Path, "sub6", ".git"))),
+              // The format of the file 'sub7\.git' is invalid.
+              string.Format(Resources.FormatOfFileIsInvalid, Path.Combine(workingDir.Path, "sub7", ".git")),
+              // Path specified in file 'sub8\.git' is invalid.
+              string.Format(Resources.PathSpecifiedInFileIsInvalid, Path.Combine(workingDir.Path, "sub8", ".git"))
+            }, diagnostics);
+        }
+
+        [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));
+
+            // branch without commits (emtpy repository) will have not file in refs/heads:
+            Assert.Null(GitRepository.ResolveReference("ref: refs/heads/none", commonDir.Path));
+
+            Assert.Null(GitRepository.ResolveReference("ref: refs/heads/rec1   ", commonDir.Path));
+            Assert.Null(GitRepository.ResolveReference("ref: refs/heads/none" + string.Join("/", Path.GetInvalidPathChars()), 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<InvalidDataException>(() => GitRepository.ResolveReference("ref: refs/heads/rec1", commonDir.Path));
+            Assert.Throws<InvalidDataException>(() => GitRepository.ResolveReference("ref: xyz/heads/rec1", commonDir.Path));
+            Assert.Throws<InvalidDataException>(() => GitRepository.ResolveReference("ref:refs/heads/rec1", commonDir.Path));
+            Assert.Throws<InvalidDataException>(() => GitRepository.ResolveReference("refs/heads/rec1", commonDir.Path));
+            Assert.Throws<InvalidDataException>(() => GitRepository.ResolveReference(new string('0', 39), commonDir.Path));
+            Assert.Throws<InvalidDataException>(() => 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 \t\v\r\n");
+
+            var gitDir = temp.CreateDirectory();
+            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());
+        }
+
+        [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 000000000..f519c24c3
--- /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})");
+            }
+        }
+    }
+}
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 bf1de60df..5cd629ab4 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 @@
     <TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
   </PropertyGroup>
   <ItemGroup>
-    <ProjectReference Include="..\Microsoft.Build.Tasks.Git.Operations\Microsoft.Build.Tasks.Git.Operations.csproj" />
     <ProjectReference Include="..\Microsoft.Build.Tasks.Git\Microsoft.Build.Tasks.Git.csproj" />
     <ProjectReference Include="..\TestUtilities\TestUtilities.csproj" />
   </ItemGroup>
   <ItemGroup>
-    <PackageReference Include="System.ValueTuple" Version="$(SystemValueTupleVersion)"/>
+    <PackageReference Include="System.ValueTuple" Version="$(SystemValueTupleVersion)" Condition="'$(TargetFrameworkIdentifier)' != '.NETFramework'" />
   </ItemGroup>
 </Project>
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestBranch.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestBranch.cs
deleted file mode 100644
index 49b465e8e..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestBranch.cs
+++ /dev/null
@@ -1,18 +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 LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestBranch : Branch
-    {
-        private readonly Reference _reference;
-
-        public TestBranch(string tipCommitSha)
-        {
-            _reference = new TestReference(tipCommitSha);
-        }
-
-        public override Reference Reference => _reference;
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestCommit.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestCommit.cs
deleted file mode 100644
index d3d96098c..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestCommit.cs
+++ /dev/null
@@ -1,18 +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 LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestCommit : Commit
-    {
-        private readonly string _sha;
-
-        public TestCommit(string sha)
-        {
-            _sha = sha;
-        }
-
-        public override string Sha => _sha;
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestConfiguration.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestConfiguration.cs
deleted file mode 100644
index dc93f6b25..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestConfiguration.cs
+++ /dev/null
@@ -1,32 +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 LibGit2Sharp;
-using System.Collections.Generic;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestConfiguration : Configuration
-    {
-        private readonly IReadOnlyDictionary<string, object> _values;
-
-        public TestConfiguration(IReadOnlyDictionary<string, object> values)
-        {
-            _values = values;
-        }
-
-        public override T GetValueOrDefault<T>(string key)
-        {
-            if (!_values.TryGetValue(key, out var obj))
-            {
-                return default;
-            }
-
-            if (obj is T value)
-            {
-                return value;
-            }
-
-            throw new LibGit2SharpException();
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestIgnore.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestIgnore.cs
deleted file mode 100644
index f9790b6b5..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestIgnore.cs
+++ /dev/null
@@ -1,20 +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.Collections.Generic;
-using LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestIgnore : Ignore
-    {
-        private readonly HashSet<string> _ignoredPaths;
-
-        public TestIgnore(IEnumerable<string> ignoredPaths)
-        {
-            _ignoredPaths = new HashSet<string>(ignoredPaths);
-        }
-
-        public override bool IsPathIgnored(string relativePath)
-            => _ignoredPaths.Contains(relativePath);
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestNetwork.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestNetwork.cs
deleted file mode 100644
index 1c22ecd9f..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestNetwork.cs
+++ /dev/null
@@ -1,20 +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.Collections.Generic;
-using LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestNetwork : Network
-    {
-        private readonly IReadOnlyList<Remote> _remotes;
-
-        public TestNetwork(IReadOnlyList<Remote> remotes)
-        {
-            _remotes = remotes;
-        }
-
-        public override RemoteCollection Remotes 
-            => new TestRemoteCollection(_remotes);
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestReference.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestReference.cs
deleted file mode 100644
index c451b14f1..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestReference.cs
+++ /dev/null
@@ -1,18 +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 LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestReference : DirectReference
-    {
-        private readonly string _sha;
-
-        public TestReference(string sha)
-        {
-            _sha = sha;
-        }
-
-        public override string TargetIdentifier => _sha;
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRemote.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRemote.cs
deleted file mode 100644
index 85c990c14..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRemote.cs
+++ /dev/null
@@ -1,21 +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 LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestRemote : Remote
-    {
-        private readonly string _name;
-        private readonly string _url;
-                                
-        public TestRemote(string name, string url)
-        {
-            _name = name;
-            _url = url;
-        }
-
-        public override string Name => _name;
-        public override string Url => _url;
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRemoteCollection.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRemoteCollection.cs
deleted file mode 100644
index 397126daf..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRemoteCollection.cs
+++ /dev/null
@@ -1,24 +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.Collections.Generic;
-using System.Linq;
-using LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestRemoteCollection : RemoteCollection
-    {
-        private readonly IReadOnlyList<Remote> _remotes;
-
-        public TestRemoteCollection(IReadOnlyList<Remote> remotes)
-        {
-            _remotes = remotes;
-        }
-
-        public override Remote this[string name] 
-            => _remotes.FirstOrDefault(r => r.Name == name);
-
-        public override IEnumerator<Remote> GetEnumerator()
-            => _remotes.GetEnumerator();
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRepository.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRepository.cs
deleted file mode 100644
index 5b274d6d7..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRepository.cs
+++ /dev/null
@@ -1,187 +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.Collections.Generic;
-using LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestRepository : IRepository
-    {
-        private readonly IReadOnlyList<Remote> _remotes;
-        private readonly IReadOnlyList<Submodule> _submodules;
-        private readonly IReadOnlyList<string> _ignoredPaths;
-        private readonly IReadOnlyDictionary<string, object> _config;
-        private readonly string _headTipCommitShaOpt;
-        private readonly string _workingDir;
-
-        public TestRepository(
-            string workingDir,
-            string commitSha,
-            IReadOnlyList<Remote> remotes = null,
-            IReadOnlyList<Submodule> submodules = null,
-            IReadOnlyList<string> ignoredPaths = null,
-            IReadOnlyDictionary<string, object> config = null,
-            bool submodulesSupported = true)
-        {
-            _workingDir = workingDir;
-            _headTipCommitShaOpt = commitSha;
-            _remotes = remotes ?? new Remote[0];
-            _submodules = submodulesSupported ? (submodules ?? new Submodule[0]) : null;
-            _ignoredPaths = ignoredPaths ?? new string[0];
-            _config = config ?? new Dictionary<string, object>();
-        }
-
-        public RepositoryInformation Info
-            => new TestRepositoryInformation(_workingDir);
-
-        public Network Network 
-            => new TestNetwork(_remotes);
-
-        public Branch Head
-            => new TestBranch(_headTipCommitShaOpt);
-
-        public SubmoduleCollection Submodules
-            => new TestSubmoduleCollection(_submodules);
-
-        public Ignore Ignore
-            => new TestIgnore(_ignoredPaths);
-
-        public Configuration Config
-            => new TestConfiguration(_config);
-
-        #region Not Implemented
-
-        public Index Index => throw new NotImplementedException();
-
-        public ReferenceCollection Refs => throw new NotImplementedException();
-
-        public IQueryableCommitLog Commits => throw new NotImplementedException();
-
-        public BranchCollection Branches => throw new NotImplementedException();
-
-        public TagCollection Tags => throw new NotImplementedException();
-
-        public Diff Diff => throw new NotImplementedException();
-
-        public ObjectDatabase ObjectDatabase => throw new NotImplementedException();
-
-        public NoteCollection Notes => throw new NotImplementedException();
-
-        public Rebase Rebase => throw new NotImplementedException();
-        
-        public StashCollection Stashes => throw new NotImplementedException();
-
-        public BlameHunkCollection Blame(string path, BlameOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void Checkout(Tree tree, IEnumerable<string> paths, CheckoutOptions opts)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void CheckoutPaths(string committishOrBranchSpec, IEnumerable<string> paths, CheckoutOptions checkoutOptions)
-        {
-            throw new NotImplementedException();
-        }
-
-        public CherryPickResult CherryPick(Commit commit, Signature committer, CherryPickOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public Commit Commit(string message, Signature author, Signature committer, CommitOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public string Describe(Commit commit, DescribeOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void Dispose()
-        {
-            throw new NotImplementedException();
-        }
-
-        public GitObject Lookup(ObjectId id)
-        {
-            throw new NotImplementedException();
-        }
-
-        public GitObject Lookup(string objectish)
-        {
-            throw new NotImplementedException();
-        }
-
-        public GitObject Lookup(ObjectId id, ObjectType type)
-        {
-            throw new NotImplementedException();
-        }
-
-        public GitObject Lookup(string objectish, ObjectType type)
-        {
-            throw new NotImplementedException();
-        }
-
-        public MergeResult Merge(Commit commit, Signature merger, MergeOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public MergeResult Merge(Branch branch, Signature merger, MergeOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public MergeResult Merge(string committish, Signature merger, MergeOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public MergeResult MergeFetchedRefs(Signature merger, MergeOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void RemoveUntrackedFiles()
-        {
-            throw new NotImplementedException();
-        }
-
-        public void Reset(ResetMode resetMode, Commit commit)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void Reset(ResetMode resetMode, Commit commit, CheckoutOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public FileStatus RetrieveStatus(string filePath)
-        {
-            throw new NotImplementedException();
-        }
-
-        public RepositoryStatus RetrieveStatus(StatusOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public RevertResult Revert(Commit commit, Signature reverter, RevertOptions options)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void RevParse(string revision, out Reference reference, out GitObject obj)
-        {
-            throw new NotImplementedException();
-        }
-
-        #endregion
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRepositoryInformation.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRepositoryInformation.cs
deleted file mode 100644
index fd87230d9..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestRepositoryInformation.cs
+++ /dev/null
@@ -1,19 +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 LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestRepositoryInformation : RepositoryInformation
-    {
-        private readonly string _workingDirectory;
-
-        public TestRepositoryInformation(string workingDirectory)
-        {
-            _workingDirectory = workingDirectory;
-        }
-
-        public override string WorkingDirectory => _workingDirectory;
-        public override bool IsBare => false;
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestSubmodule.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestSubmodule.cs
deleted file mode 100644
index 1ab280aba..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestSubmodule.cs
+++ /dev/null
@@ -1,27 +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 LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestSubmodule : Submodule
-    {
-        private readonly string _name;
-        private readonly string _path;
-        private readonly string _url;
-        private readonly ObjectId _workDirCommitShaOpt;
-
-        public TestSubmodule(string name, string path, string url, string workDirCommitSha)
-        {
-            _name = name;
-            _path = path;
-            _url = url;
-            _workDirCommitShaOpt = (workDirCommitSha != null) ? new ObjectId(workDirCommitSha) : null;
-        }
-
-        public override string Name => _name;
-        public override string Path => _path;
-        public override string Url => _url;
-        public override ObjectId WorkDirCommitId => _workDirCommitShaOpt;
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestSubmoduleCollection.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestSubmoduleCollection.cs
deleted file mode 100644
index 429d007e9..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/Mocks/TestSubmoduleCollection.cs
+++ /dev/null
@@ -1,20 +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.Collections.Generic;
-using LibGit2Sharp;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    internal class TestSubmoduleCollection : SubmoduleCollection
-    {
-        private readonly IReadOnlyList<Submodule> _submodules;
-
-        public TestSubmoduleCollection(IReadOnlyList<Submodule> submodules)
-        {
-            _submodules = submodules;
-        }
-
-        public override IEnumerator<Submodule> GetEnumerator()
-            => _submodules?.GetEnumerator() ?? throw new LibGit2SharpException("Submodules not supported");
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/RuntimeIdMapTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/RuntimeIdMapTests.cs
deleted file mode 100644
index 65dc5d34f..000000000
--- a/src/Microsoft.Build.Tasks.Git.UnitTests/RuntimeIdMapTests.cs
+++ /dev/null
@@ -1,80 +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 Xunit;
-
-namespace Microsoft.Build.Tasks.Git.UnitTests
-{
-    public class RuntimeIdMapTests
-    {
-        [Theory]
-        [InlineData("win", "win", "", "")]
-        [InlineData("win.7", "win", "7", "")]
-        [InlineData("win7-x64", "win7", "", "x64")]
-        [InlineData("win8-x64", "win8", "", "x64")]
-        [InlineData("win81-x64", "win81", "", "x64")]
-        [InlineData("win10-x64", "win10", "", "x64")]
-        [InlineData("win11-x64", "win11", "", "x64")]
-        [InlineData("alpine.3.7-x86", "alpine", "3.7", "x86")]
-        [InlineData("ubuntu.19.01-x64", "ubuntu", "19.01", "x64")]
-        [InlineData("centos.7-x64", "centos", "7", "x64")]
-        public void ParseRuntimeId(string rid, string expectedOSName, string expectedVersion, string expectedQualifiers)
-        {
-            RuntimeIdMap.ParseRuntimeId(rid, out var actualOSName, out var actualVersion, out var actualQualifiers);
-            Assert.Equal(expectedVersion, string.Join(".", actualVersion));
-            Assert.Equal(expectedOSName, actualOSName);
-            Assert.Equal(expectedQualifiers, actualQualifiers);
-        }
-
-        [Theory]
-        [InlineData("1", "1.0.0", 0)]
-        [InlineData("1.1", "1.1", 0)]
-        [InlineData("1.2", "1.1", 1)]
-        [InlineData("1.1", "1.2", -1)]
-        [InlineData("1", "10", -1)]
-        [InlineData("10", "1", 1)]
-        [InlineData("19.01", "19.10", -1)]
-        [InlineData("a", "1", 1)]
-        [InlineData("a", "A", 1)]
-        [InlineData("1", "A", -1)]
-        public void CompareVersions(string left, string right, int expected)
-        {
-            int actual = RuntimeIdMap.CompareVersions(left.Split('.'), right.Split('.'));
-            Assert.Equal(expected, Math.Sign(actual));
-        }
-
-        [Theory]
-        [InlineData("win7-x64", "win-x64")]
-        [InlineData("win8-x64", "win-x64")]
-        [InlineData("win81-x64", "win-x64")]
-        [InlineData("win10-x86", "win-x86")]
-        [InlineData("alpine.3.7-x64", "alpine-x64")]
-        [InlineData("ubuntu.19.01-x64", "linux-x64")]
-        [InlineData("fedora.30-x64", "fedora-x64")]
-        [InlineData("centos.7-x64", "rhel-x64")]
-        [InlineData("centos.8-x64", "rhel-x64")]
-        [InlineData("debian.7-x64", "linux-x64")]
-        [InlineData("debian.8-x64", "linux-x64")]
-        [InlineData("debian.9-x64", "debian.9-x64")]
-        [InlineData("debian.10-x64", "debian.9-x64")]
-        [InlineData("osx.10.14-x64", "osx")]
-        [InlineData("ubuntu.18.04-x64", "ubuntu.18.04-x64")]
-        [InlineData("ubuntu.18.10-x64", "linux-x64")]
-        public void GetNativeLibraryDirectoryName(string rid, string expected)
-        {
-            string actual = RuntimeIdMap.GetNativeLibraryDirectoryName(rid);
-            Assert.Equal(expected, actual);
-        }
-
-        [Theory]
-        [InlineData("xxx")]
-        [InlineData("debian-x86")]
-        [InlineData("debian.8-x86")]
-        [InlineData("win11-x64")]
-        public void GetNativeLibraryDirectoryName_NotSupported(string rid)
-        {
-            Assert.Throws<PlatformNotSupportedException>(() => RuntimeIdMap.GetNativeLibraryDirectoryName(rid));
-        }
-    }
-}
-#endif
\ No newline at end of file
diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/TestUtilities.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/TestUtilities.cs
new file mode 100644
index 000000000..03254d587
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git.UnitTests/TestUtilities.cs
@@ -0,0 +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;
+
+namespace Microsoft.Build.Tasks.Git.UnitTests
+{
+    internal static class TestUtilities
+    {
+        public static string InspectSourceRoot(ITaskItem sourceRoot)
+        {
+            var sourceControl = sourceRoot.GetMetadata("SourceControl");
+            var revisionId = sourceRoot.GetMetadata("RevisionId");
+            var nestedRoot = sourceRoot.GetMetadata("NestedRoot");
+            var containingRoot = sourceRoot.GetMetadata("ContainingRoot");
+            var scmRepositoryUrl = sourceRoot.GetMetadata("ScmRepositoryUrl");
+            var sourceLinkUrl = sourceRoot.GetMetadata("SourceLinkUrl");
+
+            return $"'{sourceRoot.ItemSpec}'" +
+              (string.IsNullOrEmpty(sourceControl) ? "" : $" SourceControl='{sourceControl}'") +
+              (string.IsNullOrEmpty(revisionId) ? "" : $" RevisionId='{revisionId}'") +
+              (string.IsNullOrEmpty(nestedRoot) ? "" : $" NestedRoot='{nestedRoot}'") +
+              (string.IsNullOrEmpty(containingRoot) ? "" : $" ContainingRoot='{containingRoot}'") +
+              (string.IsNullOrEmpty(scmRepositoryUrl) ? "" : $" ScmRepositoryUrl='{scmRepositoryUrl}'") +
+              (string.IsNullOrEmpty(sourceLinkUrl) ? "" : $" SourceLinkUrl='{sourceLinkUrl}'");
+        }
+
+        public static string InspectDiagnostic((string Message, object[] Args) warning)
+            => string.Format(warning.Message, warning.Args);
+
+        public static string GetExceptionMessage(Action action)
+        {
+            try
+            {
+                action();
+                return null;
+            }
+            catch (Exception e)
+            {
+                return e.Message;
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs b/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs
new file mode 100644
index 000000000..1430a7f18
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/AssemblyResolver.cs
@@ -0,0 +1,74 @@
+// 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 = Path.GetDirectoryName(typeof(AssemblyResolver).Assembly.Location);
+        private static readonly List<string> s_loaderLog = new List<string>();
+
+        public static void Initialize()
+        {
+            AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
+        }
+
+        private static void Log(ResolveEventArgs args, string outcome)
+        {
+            lock (s_loaderLog)
+            {
+                s_loaderLog.Add($"Loading '{args.Name}' referenced by '{args.RequestingAssembly}': {outcome}.");
+            }
+        }
+
+        internal static string[] GetLog()
+        {
+            lock (s_loaderLog)
+            {
+                return s_loaderLog.ToArray();
+            }
+        }
+
+        private static Assembly AssemblyResolve(object sender, ResolveEventArgs args)
+        {
+            var name = new AssemblyName(args.Name);
+
+            if (!name.Name.Equals("System.Collections.Immutable", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
+            var fullPath = Path.Combine(s_taskDirectory, "System.Collections.Immutable.dll");
+
+            Assembly sci;
+            try
+            {
+                sci = Assembly.LoadFile(fullPath);
+            }
+            catch (Exception e)
+            {
+                Log(args, $"exception while loading '{fullPath}': {e.Message}");
+                return null;
+            }
+
+            if (name.Version <= sci.GetName().Version)
+            {
+                Log(args, $"loaded '{fullPath}' to {AppDomain.CurrentDomain.FriendlyName}");
+                return sci;
+            }
+
+            return null;
+        }
+    }
+}
+
+#endif
diff --git a/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs b/src/Microsoft.Build.Tasks.Git/GetRepositoryUrl.cs
deleted file mode 100644
index d8d4d7807..000000000
--- 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() => TaskImplementation.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 d3fd8e0e5..000000000
--- 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() => TaskImplementation.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 f81ef95e0..000000000
--- 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
-    {
-        /// <summary>
-        /// 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.
-        /// </summary>
-        [Output]
-        public ITaskItem[] Roots { get; internal set; }
-
-        public override bool Execute() => TaskImplementation.GetSourceRoots(this);
-    }
-}
diff --git a/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs b/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs
index b4c81f095..ad2134199 100644
--- a/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs
+++ b/src/Microsoft.Build.Tasks.Git/GetUntrackedFiles.cs
@@ -9,6 +9,8 @@ namespace Microsoft.Build.Tasks.Git
     /// </summary>
     public sealed class GetUntrackedFiles : RepositoryTask
     {
+        public string RepositoryId { get; set; }
+
         [Required]
         public ITaskItem[] Files { get; set; }
 
@@ -16,8 +18,15 @@ public sealed class GetUntrackedFiles : RepositoryTask
         public string ProjectDirectory { get; set; }
 
         [Output]
-        public ITaskItem[] UntrackedFiles { get; set; }
+        public ITaskItem[] UntrackedFiles { get; private set; }
+
+        protected override string GetRepositoryId() => RepositoryId;
+        protected override string GetInitialPath() => ProjectDirectory;
 
-        public override bool Execute() => TaskImplementation.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/CharUtils.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/CharUtils.cs
new file mode 100644
index 000000000..57cc99417
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/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/GitDataReader/GitConfig.Reader.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.Reader.cs
new file mode 100644
index 000000000..19ec770ed
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/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<string, TextReader> _fileOpener;
+            private readonly GitEnvironment _environment;
+
+            public Reader(string gitDirectory, string commonDirectory, GitEnvironment environment, Func<string, TextReader> fileOpener = null)
+            {
+                Debug.Assert(environment != null);
+
+                _environment = environment;
+                _gitDirectoryPosix = PathUtils.ToPosixDirectoryPath(gitDirectory);
+                _commonDirectory = commonDirectory;
+                _fileOpener = fileOpener ?? File.OpenText;
+            }
+
+            /// <exception cref="IOException"/>
+            /// <exception cref="InvalidDataException"/>
+            internal GitConfig Load()
+            {
+                var variables = new Dictionary<GitVariableName, List<string>>();
+
+                foreach (var path in EnumerateExistingConfigurationFiles())
+                {
+                    LoadVariablesFrom(path, variables, includeDepth: 0);
+                }
+
+                return new GitConfig(variables.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray()));
+            }
+
+            /// <exception cref="IOException"/>
+            /// <exception cref="InvalidDataException"/>
+            internal GitConfig LoadFrom(string path)
+            {
+                var variables = new Dictionary<GitVariableName, List<string>>();
+                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<string> 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
+            }
+
+            /// <exception cref="IOException"/>
+            /// <exception cref="InvalidDataException"/>
+            internal void LoadVariablesFrom(string path, Dictionary<GitVariableName, List<string>> 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(string.Format(Resources.ConfigurationFileRecursionExceededMaximumAllowedDepth, 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 GitVariableName(sectionName, subsectionName, variableName);
+                        if (!variables.TryGetValue(key, out var values))
+                        {
+                            variables.Add(key, values = new List<string>());
+                        }
+
+                        values.Add(variableValue);
+
+                        // Spec https://git-scm.com/docs/git-config#_includes:
+                        if (IsIncludePath(key, path))
+                        {
+                            string includedConfigPath = NormalizeRelativePath(relativePath: variableValue, basePath: path, key);
+                            LoadVariablesFrom(includedConfigPath, variables, includeDepth + 1);
+                        }
+                    }
+                }
+            }
+
+            /// <exception cref="InvalidDataException"/>
+            private string NormalizeRelativePath(string relativePath, string basePath, GitVariableName key)
+            {
+                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(string.Format(Resources.ValueOfIsNotValidPath, key.ToString(), relativePath));
+                }
+            }
+
+            private bool IsIncludePath(GitVariableName key, string configFilePath)
+            {
+                // unconditional:
+                if (key.Equals(new GitVariableName("include", "", "path")))
+                {
+                    return true;
+                }
+
+                // conditional:
+                if (GitVariableName.SectionNameComparer.Equals(key.SectionName, "includeIf") &&
+                    GitVariableName.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/GitDataReader/GitConfig.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs
new file mode 100644
index 000000000..9f930f526
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitConfig.cs
@@ -0,0 +1,126 @@
+﻿// 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<GitVariableName, ImmutableArray<string>>.Empty);
+
+        public readonly ImmutableDictionary<GitVariableName, ImmutableArray<string>> Variables;
+
+        public GitConfig(ImmutableDictionary<GitVariableName, ImmutableArray<string>> variables)
+        {
+            Debug.Assert(variables != null);
+            Variables = variables;
+        }
+
+        // for testing:
+        internal IEnumerable<KeyValuePair<string, ImmutableArray<string>>> EnumerateVariables()
+            => Variables.Select(kvp => new KeyValuePair<string, ImmutableArray<string>>(kvp.Key.ToString(), kvp.Value));
+
+        public ImmutableArray<string> GetVariableValues(string section, string name)
+            => GetVariableValues(section, subsection: "", name);
+
+        public ImmutableArray<string> GetVariableValues(string section, string subsection, string name)
+            => Variables.TryGetValue(new GitVariableName(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/GitDataReader/GitEnvironment.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitEnvironment.cs
new file mode 100644
index 000000000..129fdf2ab
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/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<string>();
+            }
+
+            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 // TODO
+            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/GitDataReader/GitIgnore.Matcher.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.Matcher.cs
new file mode 100644
index 000000000..d36ce8696
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.Matcher.cs
@@ -0,0 +1,238 @@
+﻿// 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; }
+
+            /// <summary>
+            /// Maps full posix slash-terminated directory name to a pattern group.
+            /// </summary>
+            private readonly Dictionary<string, PatternGroup> _patternGroups;
+
+            /// <summary>
+            /// The result of "is ignored" for directories.
+            /// </summary>
+            private readonly Dictionary<string, bool> _directoryIgnoreStateCache;
+
+            private readonly List<PatternGroup> _reusableGroupList;
+
+            internal Matcher(GitIgnore ignore)
+            {
+                Ignore = ignore;
+                _patternGroups = new Dictionary<string, PatternGroup>(StringComparer.Ordinal);
+                _directoryIgnoreStateCache = new Dictionary<string, bool>(Ignore.PathComparer);
+                _reusableGroupList = new List<PatternGroup>();
+            }
+
+            // test only:
+            internal IReadOnlyDictionary<string, bool> DirectoryIgnoreStateCache
+                => _directoryIgnoreStateCache;
+
+            private PatternGroup GetPatternGroup(string directory)
+            {
+                Debug.Assert(PathUtils.HasTrailingSlash(directory));
+
+                if (_patternGroups.TryGetValue(directory, out var group))
+                {
+                    return group;
+                }
+
+                PatternGroup parent;
+                if (directory.Equals(Ignore.WorkingDirectory, Ignore.PathComparison))
+                {
+                    parent = Ignore.Root;
+                }
+                else
+                {
+                    var parentDirectory = directory.Substring(0, directory.LastIndexOf('/', directory.Length - 2, directory.Length - 1) + 1);
+                    parent = GetPatternGroup(parentDirectory);
+                }
+
+                group = LoadFromFile(directory + GitIgnoreFileName, parent) ?? parent;
+
+                _patternGroups.Add(directory, group);
+                return group;
+            }
+
+            /// <summary>
+            /// Checks if the specified file path is ignored.
+            /// </summary>
+            /// <param name="fullPath">Normalized path.</param>
+            /// <returns>True if the path is ignored, fale if it is not, null if it is outside of the working directory.</returns>
+            public bool? IsNormalizedFilePathIgnored(string fullPath)
+            {
+                if (!PathUtils.IsAbsolute(fullPath))
+                {
+                    throw new ArgumentException(Resources.PathMustBeAbsolute, nameof(fullPath));
+                }
+
+                if (PathUtils.HasTrailingDirectorySeparator(fullPath))
+                {
+                    throw new ArgumentException(Resources.PathMustBeFilePath, nameof(fullPath));
+                }
+
+                return IsPathIgnored(PathUtils.ToPosixPath(fullPath), isDirectoryPath: false);
+            }
+
+            /// <summary>
+            /// Checks if the specified path is ignored.
+            /// </summary>
+            /// <param name="fullPath">Full path.</param>
+            /// <returns>True if the path is ignored, fale if it is not, null if it is outside of the working directory.</returns>
+            public bool? IsPathIgnored(string fullPath)
+            {
+                if (!PathUtils.IsAbsolute(fullPath))
+                {
+                    throw new ArgumentException(Resources.PathMustBeAbsolute, 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/GitDataReader/GitIgnore.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.cs
new file mode 100644
index 000000000..99f194367
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitIgnore.cs
@@ -0,0 +1,249 @@
+﻿// 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
+        {
+            /// <summary>
+            /// Directory of the .gitignore file that defines the pattern.
+            /// Full posix slash terminated path.
+            /// </summary>
+            public readonly string ContainingDirectory;
+
+            public readonly ImmutableArray<Pattern> Patterns;
+
+            public readonly PatternGroup Parent;
+
+            public PatternGroup(PatternGroup parent, string containingDirectory, ImmutableArray<Pattern> 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 ? " <dir>" : "")}{(IsFullPathPattern ? " <path>" : "")}";
+        }
+
+        [Flags]
+        internal enum PatternFlags
+        {
+            None = 0,
+            Negative = 1,
+            DirectoryPattern = 2,
+            FullPath = 4,
+        }
+
+        private const string GitIgnoreFileName = ".gitignore";
+
+        /// <summary>
+        /// Full posix slash terminated path.
+        /// </summary>
+        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.IsAbsolute(workingDirectory));
+
+            IgnoreCase = ignoreCase;
+            WorkingDirectory = PathUtils.ToPosixDirectoryPath(workingDirectory);
+            _workingDirectoryNoSlash = PathUtils.TrimTrailingSlash(WorkingDirectory);
+            Root = root;
+        }
+
+        private StringComparison PathComparison
+            => IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
+
+        private IEqualityComparer<string> PathComparer
+            => IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
+
+        public Matcher CreateMatcher()
+            => new Matcher(this);
+
+        /// <exception cref="IOException"/>
+        /// <exception cref="ArgumentException"><paramref name="path"/> is invalid</exception>
+        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<Pattern>();
+
+            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/GitDataReader/GitRepository.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs
new file mode 100644
index 000000000..a5d625c71
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs
@@ -0,0 +1,581 @@
+﻿// 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 => _lazyIgnore.Value;
+
+        /// <summary>
+        /// Normalized full path. OS specific directory separators.
+        /// </summary>
+        public string GitDirectory { get; }
+
+        /// <summary>
+        /// Normalized full path. OS specific directory separators.
+        /// </summary>
+        public string CommonDirectory { get; }
+
+        /// <summary>
+        /// Normalized full path. OS specific directory separators. Optional.
+        /// </summary>
+        public string WorkingDirectory { get; }
+
+        public GitEnvironment Environment { get; }
+
+        private readonly Lazy<(ImmutableArray<GitSubmodule> Submodules, ImmutableArray<string> Diagnostics)> _lazySubmodules;
+        private readonly Lazy<GitIgnore> _lazyIgnore;
+        private readonly Lazy<string> _lazyHeadCommitSha;
+
+        internal GitRepository(GitEnvironment environment, GitConfig config, string gitDirectory, string commonDirectory, string workingDirectory)
+        {
+            Debug.Assert(environment != null);
+            Debug.Assert(config != null);
+            Debug.Assert(PathUtils.IsNormalized(gitDirectory));
+            Debug.Assert(PathUtils.IsNormalized(commonDirectory));
+            Debug.Assert(workingDirectory == null || PathUtils.IsNormalized(workingDirectory));
+
+            Config = config;
+            GitDirectory = gitDirectory;
+            CommonDirectory = commonDirectory;
+            WorkingDirectory = workingDirectory;
+            Environment = environment;
+
+            _lazySubmodules = new Lazy<(ImmutableArray<GitSubmodule>, ImmutableArray<string>)>(ReadSubmodules);
+            _lazyIgnore = new Lazy<GitIgnore>(LoadIgnore);
+            _lazyHeadCommitSha = new Lazy<string>(() => ReadHeadCommitSha(GitDirectory, CommonDirectory));
+        }
+
+        // test only
+        internal GitRepository(
+            GitEnvironment environment,
+            GitConfig config,
+            string gitDirectory,
+            string commonDirectory,
+            string workingDirectory,
+            ImmutableArray<GitSubmodule> submodules,
+            ImmutableArray<string> submoduleDiagnostics,
+            GitIgnore ignore,
+            string headCommitSha)
+            : this(environment, config, gitDirectory, commonDirectory, workingDirectory)
+        {
+            _lazySubmodules = new Lazy<(ImmutableArray<GitSubmodule>, ImmutableArray<string>)>(() => (submodules, submoduleDiagnostics));
+            _lazyIgnore = new Lazy<GitIgnore>(() => ignore);
+            _lazyHeadCommitSha = new Lazy<string>(() => headCommitSha);
+        }
+
+        /// <summary>
+        /// Finds a git repository containing the specified path, if any.
+        /// </summary>
+        /// <exception cref="IOException" />
+        /// <exception cref="InvalidDataException" />
+        /// <exception cref="NotSupportedException">The repository found requires higher version of git repository format that is currently supported.</exception>
+        /// <returns>False if no git repository can be found that contains the specified path.</returns>
+        public static bool TryFindRepository(string path, out GitRepositoryLocation location)
+        {
+            if (!LocateRepository(path, out var gitDirectory, out var commonDirectory, out var defaultWorkingDirectory))
+            {
+                // unable to find repository
+                location = default;
+                return false;
+            }
+
+            location = new GitRepositoryLocation(gitDirectory, commonDirectory, defaultWorkingDirectory);
+            return true;
+        }
+
+        /// <summary>
+        /// Opens a repository at the specified location.
+        /// </summary>
+        /// <exception cref="IOException" />
+        /// <exception cref="InvalidDataException" />
+        /// <exception cref="NotSupportedException">The repository found requires higher version of git repository format that is currently supported.</exception>
+        /// <returns>null if no git repository can be found that contains the specified path.</returns>
+        internal static GitRepository OpenRepository(string path, GitEnvironment environment)
+            => TryFindRepository(path, out var location) ? OpenRepository(location, environment) : null;
+
+        /// <summary>
+        /// Opens a repository at the specified location.
+        /// </summary>
+        /// <exception cref="IOException" />
+        /// <exception cref="InvalidDataException" />
+        /// <exception cref="NotSupportedException">The repository found requires higher version of git repository format that is currently supported.</exception>
+        public static GitRepository OpenRepository(GitRepositoryLocation location, GitEnvironment environment)
+        {
+            Debug.Assert(environment != null);
+            Debug.Assert(location.GitDirectory != null);
+            Debug.Assert(location.CommonDirectory != null);
+
+            // See https://git-scm.com/docs/gitrepository-layout
+
+            var reader = new GitConfig.Reader(location.GitDirectory, location.CommonDirectory, environment);
+            var config = reader.Load();
+
+            var workingDirectory = GetWorkingDirectory(config, location.GitDirectory, location.CommonDirectory) ?? location.WorkingDirectory;
+
+            // 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(string.Format(Resources.UnsupportedRepositoryVersion, versionStr, SupportedGitRepoFormatVersion));
+            }
+
+            return new GitRepository(environment, config, location.GitDirectory, location.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(string.Format(Resources.PathSpecifiedInFileIsNotAbsolute, gitdirFilePath));
+                }
+
+                try
+                {
+                    return Path.GetFullPath(workingDirectory);
+                }
+                catch
+                {
+                    throw new InvalidDataException(string.Format(Resources.PathSpecifiedInFileIsInvalid, gitdirFilePath));
+                }
+            }
+
+            // 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(string.Format(Resources.ValueOfIsNotValidPath, "core.worktree", value));
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Returns the commit SHA of the current HEAD tip.
+        /// </summary>
+        /// <exception cref="IOException"/>
+        /// <exception cref="InvalidDataException"/>
+        /// <returns>Null if the HEAD tip reference can't be resolved.</returns>
+        public string GetHeadCommitSha()
+            => _lazyHeadCommitSha.Value;
+
+        /// <summary>
+        /// Returns the commit SHA of the current HEAD tip of the specified submodule.
+        /// </summary>
+        /// <exception cref="IOException"/>
+        /// <exception cref="InvalidDataException"/>
+        /// <returns>Null if the HEAD tip reference can't be resolved.</returns>
+        internal string GetSubmoduleHeadCommitSha(string submoduleWorkingDirectoryFullPath)
+        {
+            var gitDirectory = ReadDotGitFile(Path.Combine(submoduleWorkingDirectoryFullPath, GitDirName));
+            if (!IsGitDirectory(gitDirectory, out var commonDirectory))
+            {
+                return null;
+            }
+
+            return ReadHeadCommitSha(gitDirectory, commonDirectory);
+        }
+
+        /// <exception cref="IOException"/>
+        /// <exception cref="InvalidDataException"/>
+        private static string ReadHeadCommitSha(string gitDirectory, string 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
+        internal static string ResolveReference(string reference, string commonDirectory)
+        {
+            HashSet<string> lazyVisitedReferences = null;
+            return ResolveReference(reference, commonDirectory, ref lazyVisitedReferences);
+        }
+
+        /// <exception cref="IOException"/>
+        /// <exception cref="InvalidDataException"/>
+        private static string ResolveReference(string reference, string commonDirectory, ref HashSet<string> 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))
+            {
+                var symRef = reference.Substring(refPrefix.Length);
+
+                if (lazyVisitedReferences != null && !lazyVisitedReferences.Add(symRef))
+                {
+                    // infinite recursion
+                    throw new InvalidDataException(string.Format(Resources.RecursionDetectedWhileResolvingReference, reference));
+                }
+
+                string path;
+                try
+                {
+                    path = Path.Combine(commonDirectory, symRef);
+                }
+                catch
+                {
+                    return null;
+                }
+
+                string content;
+                try
+                {
+                    content = ReadReferenceFromFile(path);
+                }
+                catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
+                {
+                    return null;
+                }
+
+                // invalid path:
+                if (content == null)
+                {
+                    return null;
+                }
+
+                if (IsObjectId(content))
+                {
+                    return content;
+                }
+
+                lazyVisitedReferences ??= new HashSet<string>();
+
+                return ResolveReference(content, commonDirectory, ref lazyVisitedReferences);
+            }
+
+            if (IsObjectId(reference))
+            {
+                return reference;
+            }
+
+            throw new InvalidDataException(string.Format(Resources.InvalidReference, reference));
+        }
+
+        private static string ReadReferenceFromFile(string path)
+        {
+            try
+            {
+                return File.ReadAllText(path).TrimEnd(CharUtils.AsciiWhitespace);
+            }
+            catch (ArgumentException)
+            {
+                // bad path
+                return null;
+            }
+            catch (Exception e) when (!(e is IOException))
+            {
+                throw new IOException(e.Message, e);
+            }
+        }
+
+        private string GetWorkingDirectory()
+            => WorkingDirectory ?? throw new InvalidOperationException(Resources.RepositoryDoesNotHaveWorkingDirectory);
+
+        private static bool IsObjectId(string reference)
+            => reference.Length == 40 && reference.All(CharUtils.IsHexadecimalDigit);
+
+        /// <exception cref="IOException"/>
+        /// <exception cref="InvalidDataException"/>
+        public ImmutableArray<GitSubmodule> GetSubmodules()
+            => _lazySubmodules.Value.Submodules;
+
+        /// <exception cref="IOException"/>
+        /// <exception cref="InvalidDataException"/>
+        public ImmutableArray<string> GetSubmoduleDiagnostics()
+            => _lazySubmodules.Value.Diagnostics;
+
+        /// <exception cref="IOException"/>
+        /// <exception cref="InvalidDataException"/>
+        private (ImmutableArray<GitSubmodule> Submodules, ImmutableArray<string> Diagnostics) ReadSubmodules()
+        {
+            var workingDirectory = GetWorkingDirectory();
+            var submoduleConfig = ReadSubmoduleConfig();
+            if (submoduleConfig == null)
+            {
+                return (ImmutableArray<GitSubmodule>.Empty, ImmutableArray<string>.Empty);
+            }
+
+            ImmutableArray<string>.Builder lazyDiagnostics = null;
+
+            void reportDiagnostic(string diagnostic)
+                => (lazyDiagnostics ??= ImmutableArray.CreateBuilder<string>()).Add(diagnostic);
+
+            var builder = ImmutableArray.CreateBuilder<GitSubmodule>();
+
+            foreach (var (name, path, url) in EnumerateSubmoduleConfig(submoduleConfig))
+            {
+                if (string.IsNullOrWhiteSpace(path))
+                {
+                    reportDiagnostic(string.Format(Resources.InvalidSubmodulePath, name, path));
+                    continue;
+                }
+
+                if (string.IsNullOrWhiteSpace(url))
+                {
+                    reportDiagnostic(string.Format(Resources.InvalidSubmoduleUrl, name, url));
+                    continue;
+                }
+
+                string fullPath;
+                try
+                {
+                    fullPath = Path.GetFullPath(Path.Combine(workingDirectory, path));
+                }
+                catch
+                {
+                    reportDiagnostic(string.Format(Resources.InvalidSubmodulePath, name, path));
+                    continue;
+                }
+
+                string headCommitSha;
+                try
+                {
+                    headCommitSha = GetSubmoduleHeadCommitSha(fullPath);
+                }
+                catch (Exception e) when (e is IOException || e is InvalidDataException)
+                {
+                    reportDiagnostic(e.Message);
+                    continue;
+                }
+
+                builder.Add(new GitSubmodule(name, path, fullPath, url, headCommitSha));
+            }
+
+            return (builder.ToImmutable(), (lazyDiagnostics != null) ? lazyDiagnostics.ToImmutable() : ImmutableArray<string>.Empty);
+        }
+
+        // internal for testing
+        internal GitConfig ReadSubmoduleConfig()
+        {
+            var workingDirectory = GetWorkingDirectory();
+            var submodulesConfigFile = Path.Combine(workingDirectory, GitModulesFileName);
+            if (!File.Exists(submodulesConfigFile))
+            {
+                return null;
+            }
+
+            var reader = new GitConfig.Reader(GitDirectory, CommonDirectory, Environment);
+            return reader.LoadFrom(submodulesConfigFile);
+        }
+
+        // internal for testing
+        internal static IEnumerable<(string Name, string Path, string Url)> EnumerateSubmoduleConfig(GitConfig submoduleConfig)
+        {
+            foreach (var group in submoduleConfig.Variables.
+                Where(kvp => kvp.Key.SectionNameEquals("submodule")).
+                GroupBy(kvp => kvp.Key.SubsectionName, GitVariableName.SubsectionNameComparer).
+                OrderBy(group => group.Key))
+            {
+                string name = 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();
+                    }
+                }
+
+                yield return (name, path, url);
+            }
+        }
+
+        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);
+        }
+
+        /// <exception cref="IOException" />
+        /// <exception cref="InvalidDataException" />
+        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(string.Format(Resources.FormatOfFileIsInvalid, 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(string.Format(Resources.PathSpecifiedInFileIsInvalid, 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/GitDataReader/GitRepositoryLocation.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepositoryLocation.cs
new file mode 100644
index 000000000..62d1eff3a
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepositoryLocation.cs
@@ -0,0 +1,35 @@
+﻿// 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.Diagnostics;
+
+namespace Microsoft.Build.Tasks.Git
+{
+    internal readonly struct GitRepositoryLocation
+    {
+        /// <summary>
+        /// Normalized full path. OS specific directory separators.
+        /// </summary>
+        public readonly string GitDirectory { get; }
+
+        /// <summary>
+        /// Normalized full path. OS specific directory separators.
+        /// </summary>
+        public readonly string CommonDirectory { get; }
+
+        /// <summary>
+        /// Normalized full path. OS specific directory separators. Optional.
+        /// </summary>
+        public readonly string WorkingDirectory { get; }
+
+        internal GitRepositoryLocation(string gitDirectory, string commonDirectory, string workingDirectory)
+        {
+            Debug.Assert(PathUtils.IsNormalized(gitDirectory));
+            Debug.Assert(PathUtils.IsNormalized(commonDirectory));
+            Debug.Assert(workingDirectory == null || PathUtils.IsNormalized(workingDirectory));
+
+            GitDirectory = gitDirectory;
+            CommonDirectory = commonDirectory;
+            WorkingDirectory = workingDirectory;
+        }
+    }
+}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs
new file mode 100644
index 000000000..4797564f6
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitSubmodule.cs
@@ -0,0 +1,46 @@
+﻿// 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.Diagnostics;
+
+namespace Microsoft.Build.Tasks.Git
+{
+    internal readonly struct GitSubmodule
+    {
+        public string Name { get; }
+
+        /// <summary>
+        /// 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).
+        /// </summary>
+        public string WorkingDirectoryRelativePath { get; }
+
+        /// <summary>
+        /// Normalized full path.
+        /// </summary>
+        public string WorkingDirectoryFullPath { get; }
+
+        /// <summary>
+        /// An absolute URL or a relative path (if it starts with `./` or `../`) to the default remote of the containing repository.
+        /// </summary>
+        public string Url { get; }
+
+        /// <summary>
+        /// Head tip commit SHA. Null, if there is no commit.
+        /// </summary>
+        public string HeadCommitSha { get; }
+
+        internal GitSubmodule(string name, string workingDirectoryRelativePath, string workingDirectoryFullPath, string url, string headCommitSha)
+        {
+            Debug.Assert(name != null);
+            Debug.Assert(workingDirectoryRelativePath != null);
+            Debug.Assert(workingDirectoryFullPath != null);
+            Debug.Assert(url != null);
+
+            Name = name;
+            WorkingDirectoryRelativePath = workingDirectoryRelativePath;
+            WorkingDirectoryFullPath = workingDirectoryFullPath;
+            Url = url;
+            HeadCommitSha = headCommitSha;
+        }
+    }
+}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/GitVariableName.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitVariableName.cs
new file mode 100644
index 000000000..e0d8b66e4
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/GitVariableName.cs
@@ -0,0 +1,54 @@
+﻿// 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;
+
+namespace Microsoft.Build.Tasks.Git
+{
+    internal readonly struct GitVariableName : IEquatable<GitVariableName>
+    {
+        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 GitVariableName(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(GitVariableName other)
+            => SectionNameEquals(other.SectionName) &&
+               SubsectionNameEquals(other.SubsectionName) &&
+               VariableNameEquals(other.VariableName);
+
+        public override bool Equals(object obj)
+            => obj is GitVariableName 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;
+    }
+}
diff --git a/src/Microsoft.Build.Tasks.Git/GitDataReader/Glob.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/Glob.cs
new file mode 100644
index 000000000..3bdc2bd50
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/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/GitDataReader/PathUtils.cs b/src/Microsoft.Build.Tasks.Git/GitDataReader/PathUtils.cs
new file mode 100644
index 000000000..f2fb90317
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitDataReader/PathUtils.cs
@@ -0,0 +1,102 @@
+﻿// 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 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;
+
+        public static bool IsNormalized(string path)
+            => Path.GetFullPath(path) == path;
+
+        /// <summary>
+        /// True if the path is an absolute path (rooted to drive or network share)
+        /// </summary>
+        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]);
+        }
+
+        /// <summary>
+        /// Returns true if given path is absolute and starts with a drive specification ("C:\").
+        /// </summary>
+        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/GitLoaderContext.cs b/src/Microsoft.Build.Tasks.Git/GitLoaderContext.cs
deleted file mode 100644
index 17375efa9..000000000
--- 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/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs
new file mode 100644
index 000000000..78cca4cfc
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs
@@ -0,0 +1,380 @@
+﻿// 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;
+using System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Tasks.SourceControl;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.Build.Tasks.Git
+{
+    internal static class GitOperations
+    {
+        private const string SourceControlName = "git";
+        private const string RemoteSectionName = "remote";
+
+        public static string GetRepositoryUrl(GitRepository repository, Action<string, object[]> logWarning = null, string remoteName = null)
+        {
+            string unknownRemoteName = null;
+            string remoteUrl = null;
+            if (!string.IsNullOrEmpty(remoteName))
+            {
+                remoteUrl = repository.Config.GetVariableValue(RemoteSectionName, remoteName, "url");
+                if (remoteUrl == null)
+                {
+                    unknownRemoteName = remoteName;
+                }
+            }
+
+            if (remoteUrl == null && !TryGetRemote(repository.Config, out remoteName, out remoteUrl))
+            {
+                logWarning?.Invoke(Resources.RepositoryHasNoRemote, Array.Empty<string>());
+                return null;
+            }
+
+            if (unknownRemoteName != null)
+            {
+                logWarning?.Invoke(Resources.RepositoryDoesNotHaveSpecifiedRemote, new[] { unknownRemoteName, remoteName });
+            }
+
+            var url = NormalizeUrl(remoteUrl, repository.WorkingDirectory);
+            if (url == null)
+            {
+                logWarning?.Invoke(Resources.InvalidRepositoryRemoteUrl, new[] { remoteName, remoteUrl });
+            }
+
+            return url;
+        }
+
+        private static bool TryGetRemote(GitConfig config, out string remoteName, out string remoteUrl)
+        {
+            remoteName = "origin";
+            remoteUrl = config.GetVariableValue(RemoteSectionName, remoteName, "url");
+            if (remoteUrl != null)
+            {
+                return true;
+            }
+
+            var remoteVariable = config.Variables.
+                Where(kvp => kvp.Key.SectionNameEquals(RemoteSectionName)).
+                OrderBy(kvp => kvp.Key.SubsectionName, GitVariableName.SubsectionNameComparer).
+                FirstOrDefault();
+
+            remoteName = remoteVariable.Key.SubsectionName;
+            if (remoteName == null)
+            {
+                return false;
+            }
+
+            remoteUrl = remoteVariable.Value.Last();
+            return true;
+        }
+
+        internal static string NormalizeUrl(string url, string root)
+        {
+            // Since git supports scp-like syntax for SSH URLs we convert it here, 
+            // so that RepositoryUrl is actually a valid URL in that case.
+            // See https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols and
+            // https://github.com/libgit2/libgit2/blob/master/src/transport.c#L72.
+
+            // Windows device path "X:"
+            if (url.Length == 2 && IsWindowsAbsoluteOrDriveRelativePath(url))
+            {
+                return "file:///" + url + "/";
+            }
+
+            if (TryParseScp(url, out var uri))
+            {
+                return uri.ToString();
+            }
+
+            if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
+            {
+                return null;
+            }
+
+            if (uri.IsAbsoluteUri)
+            {
+                return uri.ToString();
+            }
+
+            // Convert relative local path to absolute:
+            var rootUri = new Uri(root.EndWithSeparator('/'));
+            if (Uri.TryCreate(rootUri, uri, out uri))
+            {
+                return uri.ToString();
+            }
+
+            return null;
+        }
+
+        private static bool IsWindowsAbsoluteOrDriveRelativePath(string value)
+            => Path.DirectorySeparatorChar == '\\' &&
+               value.Length >= 2 &&
+               value[1] == ':' &&
+               (value[0] >= 'A' && value[0] <= 'Z' || value[0] >= 'a' && value[0] <= 'z');
+
+        private static bool TryParseScp(string value, out Uri uri)
+        {
+            uri = null;
+           
+            int colon = value.IndexOf(':');
+            if (colon == -1)
+            {
+                return false;
+            }
+
+            // URLs xxx://xxx
+            if (colon + 2 < value.Length && value[colon + 1] == '/' && value[colon + 2] == '/')
+            {
+                return false;
+            }
+
+            // Windows absolute or driver-relative paths "X:\xxx", "X:xxx"
+            if (IsWindowsAbsoluteOrDriveRelativePath(value))
+            {
+                return false;
+            }
+
+            // [user@]server:path
+            var url = "ssh://" + value.Substring(0, colon) + "/" + value.Substring(colon + 1);
+            return Uri.TryCreate(url, UriKind.Absolute, out uri);
+        }
+
+        public static ITaskItem[] GetSourceRoots(GitRepository repository, Action<string, object[]> logWarning)
+        {
+            var result = new List<TaskItem>();
+            var repoRoot = GetRepositoryRoot(repository);
+
+            var revisionId = repository.GetHeadCommitSha();
+            if (revisionId != null)
+            {
+                // Don't report a warning since it has already been reported by GetRepositoryUrl task.
+                string repositoryUrl = GetRepositoryUrl(repository);
+
+                // Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata stores the value as specified.
+                // Escape msbuild special characters so that URL escapes in the URL are preserved when the URL is read by GetMetadata.
+
+                var item = new TaskItem(Evaluation.ProjectCollection.Escape(repoRoot));
+                item.SetMetadata(Names.SourceRoot.SourceControl, SourceControlName);
+                item.SetMetadata(Names.SourceRoot.ScmRepositoryUrl, Evaluation.ProjectCollection.Escape(repositoryUrl));
+                item.SetMetadata(Names.SourceRoot.RevisionId, revisionId);
+                result.Add(item);
+            }
+            else
+            {
+                logWarning(Resources.RepositoryHasNoCommit, Array.Empty<object>());
+            }
+
+            foreach (var submodule in repository.GetSubmodules())
+            {
+                var commitSha = submodule.HeadCommitSha;
+                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)
+                {
+                    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(GitRepository repository)
+            => repository.WorkingDirectory.EndWithSeparator();
+
+        public static ITaskItem[] GetUntrackedFiles(
+            GitRepository repository,
+            ITaskItem[] files, 
+            string projectDirectory,
+            Func<string, GitRepository> repositoryFactory)
+        {
+            var directoryTree = BuildDirectoryTree(repository);
+
+            return files.Where(file =>
+            {
+                // file.ItemSpec are relative to projectDirectory.
+                var fullPath = Path.GetFullPath(Path.Combine(projectDirectory, file.ItemSpec));
+
+                var containingDirectory = GetContainingRepository(fullPath, directoryTree);
+
+                // Files that are outside of the repository are considered untracked.
+                if (containingDirectory == null)
+                {
+                    return true;
+                }
+
+                return containingDirectory.GetMatcher(repositoryFactory).IsNormalizedFilePathIgnored(fullPath) ?? true;
+            }).ToArray();
+        }
+
+        internal sealed class DirectoryNode
+        {
+            public readonly string Name;
+            public readonly List<DirectoryNode> OrderedChildren;
+
+            // set on nodes that represent submodule working directory:
+            public string WorkingDirectoryFullPath;
+            private GitIgnore.Matcher _lazyMatcher;
+
+            public DirectoryNode(string name)
+                : this(name, null, new List<DirectoryNode>())
+            {
+            }
+
+            public DirectoryNode(string name, string fullPath)
+                : this(name, fullPath, new List<DirectoryNode>())
+            {
+            }
+
+            public DirectoryNode(string name, string workingDirectoryFullPath, List<DirectoryNode> orderedChildren)
+            {
+                Name = name;
+                WorkingDirectoryFullPath = workingDirectoryFullPath;
+                OrderedChildren = orderedChildren;
+            }
+
+            public void SetMatcher(string workingDirectory, GitIgnore.Matcher matcher)
+            {
+                WorkingDirectoryFullPath = workingDirectory;
+                _lazyMatcher = matcher;
+            }
+
+            public int FindChildIndex(string name)
+                => BinarySearch(OrderedChildren, name, (n, v) => n.Name.CompareTo(v));
+
+            public GitIgnore.Matcher GetMatcher(Func<string, GitRepository> repositoryFactory)
+                => _lazyMatcher ?? (_lazyMatcher = repositoryFactory(WorkingDirectoryFullPath).Ignore.CreateMatcher());
+        }
+
+        internal static DirectoryNode BuildDirectoryTree(GitRepository repository)
+        {
+            var workingDirectory = repository.WorkingDirectory;
+
+            var treeRoot = new DirectoryNode("");
+            AddTreeNode(treeRoot, workingDirectory, repository.Ignore.CreateMatcher());
+
+            foreach (var submodule in repository.GetSubmodules())
+            {
+                AddTreeNode(treeRoot, submodule.WorkingDirectoryFullPath, matcherOpt: null);
+            }
+
+            return treeRoot;
+        }
+
+        private static void AddTreeNode(DirectoryNode root, string workingDirectory, GitIgnore.Matcher matcherOpt)
+        {
+            var segments = PathUtilities.Split(workingDirectory);
+
+            var node = root;
+
+            for (int i = 0; i < segments.Length; i++)
+            {
+                int index = node.FindChildIndex(segments[i]);
+                if (index >= 0)
+                {
+                    node = node.OrderedChildren[index];
+                }
+                else
+                {
+                    var newNode = new DirectoryNode(segments[i]);
+                    node.OrderedChildren.Insert(~index, newNode);
+                    node = newNode;
+                }
+
+                if (i == segments.Length - 1)
+                {
+                    node.SetMatcher(workingDirectory, matcherOpt);
+                }
+            }
+        }
+
+        // internal for testing
+        internal static DirectoryNode GetContainingRepository(string fullPath, DirectoryNode root)
+        {
+            var segments = PathUtilities.Split(fullPath);
+            Debug.Assert(segments.Length > 0);
+
+            Debug.Assert(root.WorkingDirectoryFullPath == null);
+            DirectoryNode containingRepositoryNode = null;
+
+            var node = root;
+            for (int i = 0; i < segments.Length - 1; i++)
+            {
+                int index = node.FindChildIndex(segments[i]);
+                if (index < 0)
+                {
+                    break;
+                }
+
+                node = node.OrderedChildren[index];
+                if (node.WorkingDirectoryFullPath != null)
+                {
+                    containingRepositoryNode = node;
+                }
+            }
+
+            return containingRepositoryNode;
+        }
+
+        internal static int BinarySearch<T, TValue>(IReadOnlyList<T> list, TValue value, Func<T, TValue, int> compare)
+        {
+            var low = 0;
+            var high = list.Count - 1;
+
+            while (low <= high)
+            {
+                var middle = low + ((high - low) >> 1);
+                var midValue = list[middle];
+
+                var comparison = compare(midValue, value);
+                if (comparison == 0)
+                {
+                    return middle;
+                }
+
+                if (comparison > 0)
+                {
+                    high = middle - 1;
+                }
+                else
+                {
+                    low = middle + 1;
+                }
+            }
+
+            return ~low;
+        }
+    }
+}
diff --git a/src/Microsoft.Build.Tasks.Git/LocateRepository.cs b/src/Microsoft.Build.Tasks.Git/LocateRepository.cs
index d536f6bd8..899fd81ed 100644
--- a/src/Microsoft.Build.Tasks.Git/LocateRepository.cs
+++ b/src/Microsoft.Build.Tasks.Git/LocateRepository.cs
@@ -1,20 +1,55 @@
 ﻿// 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
     {
+        public string RemoteName { get; set; }
+
         [Required]
-        public string Directory { get; set; }
+        public string Path { get; set; }
+
+        [Output]
+        public string RepositoryId { get; private set; }
+
+        [Output]
+        public string WorkingDirectory { get; private set; }
 
         [Output]
-        public string Id { get; set; }
+        public string Url { get; private set; }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        [Output]
+        public ITaskItem[] Roots { get; private set; }
+
+        /// <summary>
+        /// Head tip commit SHA.
+        /// </summary>
+        [Output]
+        public string RevisionId { get; private set; }
+
+        protected override string GetRepositoryId() => null;
+        protected override string GetInitialPath() => Path;
 
-        public override bool Execute() => TaskImplementation.LocateRepository(this);
+        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);
+            RevisionId = repository.GetHeadCommitSha();
+        }
     }
 }
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 33cdb3234..3f5e8f463 100644
--- a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj
+++ b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj
@@ -2,9 +2,19 @@
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
     <AutoGenerateAssemblyVersion>true</AutoGenerateAssemblyVersion>
+    
+    <!-- Using an explicit nuspec file since NuGet Pack target currently doesn't support including dependencies in tools packages -->
+    <IsPackable>true</IsPackable>
+    <PackageId>Microsoft.Build.Tasks.Git</PackageId>
+    <BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
+
+    <PackageDescription>MSBuild tasks providing git repository information.</PackageDescription>
+    <PackageTags>MSBuild Tasks source control git</PackageTags>
+    <DevelopmentDependency>true</DevelopmentDependency>
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="..\Common\Names.cs" Link="Common\Names.cs" />
+    <Compile Include="..\Common\ValueTuple.cs" Link="Common\ValueTuple.cs" />
     <Compile Include="..\Common\PathUtilities.cs" Link="Common\PathUtilities.cs" />
     <Compile Include="..\Common\SequenceComparer.cs" Link="Common\SequenceComparer.cs" />
   </ItemGroup>
@@ -18,4 +28,8 @@
     <InternalsVisibleTo Include="Microsoft.Build.Tasks.Git.UnitTests" />
     <InternalsVisibleTo Include="Microsoft.SourceLink.Git.IntegrationTests" />
   </ItemGroup>
+  <ItemGroup>
+    <None Include="build/**/*.*" Pack="true" PackagePath="build/%(RecursiveDir)%(Filename)%(Extension)" />
+    <None Include="build/**/*.*" Pack="true" PackagePath="buildCrossTargeting/%(RecursiveDir)%(Filename)%(Extension)" />
+  </ItemGroup>
 </Project>
diff --git a/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs b/src/Microsoft.Build.Tasks.Git/RepositoryTask.cs
index b5530d410..77fc73777 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,152 @@ 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_cacheKeyPrefix = "3AE29AB7-AE6B-48BA-9851-98A15ED51C94:";
+
+        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);
+
+        protected abstract string GetRepositoryId();
+        protected abstract string GetInitialPath();
+
+        [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()
+        {
+            GitRepository repository;
+
+            var repositoryId = GetRepositoryId();
+            if (repositoryId != null)
+            {
+                if (TryGetCachedRepositoryInstance(GetCacheKey(repositoryId), requireCached: true, out repository))
+                {
+                    return repository;
+                }
+
+                return null;
+            }
+
+            var initialPath = GetInitialPath();
+
+            GitRepositoryLocation location;
+            try
+            {
+                if (!GitRepository.TryFindRepository(initialPath, out location))
+                {
+                    Log.LogWarning(Resources.UnableToLocateRepository, initialPath);
+                    return null;
+                }
+            }
+            catch (Exception e) when (e is IOException || e is InvalidDataException || e is NotSupportedException)
+            {
+                Log.LogError(Resources.ErrorReadingGitRepositoryInformation, e.Message);
+                return null;
+            }
+
+            var cacheKey = GetCacheKey(location.GitDirectory);
+            if (TryGetCachedRepositoryInstance(cacheKey, requireCached: false, out repository))
+            {
+                return repository;
+            }
+
+            try
+            {
+                // TODO: configure environment
+                repository = GitRepository.OpenRepository(location, 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, initialPath);
+                repository = null;
+            }
+
+            CacheRepositoryInstance(cacheKey, repository);
+
+            return repository;
+        }
+
+        private string GetCacheKey(string repositoryId)
+            => s_cacheKeyPrefix + repositoryId;
+
+        private bool TryGetCachedRepositoryInstance(string cacheKey, bool requireCached, out GitRepository repository)
+        {
+            var entry = (StrongBox<GitRepository>)BuildEngine4.GetRegisteredTaskObject(cacheKey, RegisteredTaskObjectLifetime.Build);
+
+            if (entry != null)
+            {
+                Log.LogMessage(MessageImportance.Low, $"SourceLink: Reusing cached git repository information.");
+                repository = entry.Value;
+                return repository != null;
+            }
+
+            if (requireCached)
+            {
+                Log.LogError($"SourceLink: Repository instance not found in cache: '{cacheKey.Substring(s_cacheKeyPrefix.Length)}'");
+            }
+
+            repository = null;
+            return false;
+        }
+
+        private void CacheRepositoryInstance(string cacheKey, GitRepository repository)
+        {
+            BuildEngine4.RegisterTaskObject(
+                  cacheKey,
+                  new StrongBox<GitRepository>(repository),
+                  RegisteredTaskObjectLifetime.Build,
+                  allowEarlyCollection: true);
+        }
     }
 }
diff --git a/src/Microsoft.Build.Tasks.Git/Resources.resx b/src/Microsoft.Build.Tasks.Git/Resources.resx
index 977511399..5d89413dd 100644
--- a/src/Microsoft.Build.Tasks.Git/Resources.resx
+++ b/src/Microsoft.Build.Tasks.Git/Resources.resx
@@ -117,14 +117,59 @@
   <resheader name="writer">
     <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </resheader>
-  <data name="SubmoduleWithoutCommit_SourceLink" xml:space="preserve">
-    <value>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</value>
+  <data name="PathMustBeAbsolute" xml:space="preserve">
+    <value>Path must be absolute</value>
   </data>
-  <data name="InvalidSubmoduleUrl_SourceLink" xml:space="preserve">
-    <value>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</value>
+  <data name="PathMustBeFilePath" xml:space="preserve">
+    <value>Path must be a file path</value>
   </data>
-  <data name="InvalidSubmodulePath_SourceLink" xml:space="preserve">
-    <value>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</value>
+  <data name="ErrorReadingGitRepositoryInformation" xml:space="preserve">
+    <value>Error reading git repository information: {0}</value>
+  </data>
+  <data name="UnsupportedRepositoryVersion" xml:space="preserve">
+    <value>Unsupported repository version {0}. Only versions up to {1} are supported.</value>
+  </data>
+  <data name="PathSpecifiedInFileIsNotAbsolute" xml:space="preserve">
+    <value>Path specified in file '{0}' is not absolute.</value>
+  </data>
+  <data name="PathSpecifiedInFileIsInvalid" xml:space="preserve">
+    <value>Path specified in file '{0}' is invalid.</value>
+  </data>
+  <data name="ValueOfIsNotValidPath" xml:space="preserve">
+    <value>The value of {0} is not a valid path: '{1}'.</value>
+  </data>
+  <data name="InvalidModulePath" xml:space="preserve">
+    <value>Invalid module path: '{0}'.</value>
+  </data>
+  <data name="RecursionDetectedWhileResolvingReference" xml:space="preserve">
+    <value>Recursion detected while resolving reference: '{0}'.</value>
+  </data>
+  <data name="RepositoryDoesNotHaveWorkingDirectory" xml:space="preserve">
+    <value>Repository does not have a working directory.</value>
+  </data>
+  <data name="InvalidFormatOfFileAtPath" xml:space="preserve">
+    <value>Repository does not have a working directory.</value>
+  </data>
+  <data name="FormatOfFileIsInvalid" xml:space="preserve">
+    <value>The format of the file '{0}' is invalid.</value>
+  </data>
+  <data name="InvalidReference" xml:space="preserve">
+    <value>Invalid reference: '{0}'.</value>
+  </data>
+  <data name="ConfigurationFileRecursionExceededMaximumAllowedDepth" xml:space="preserve">
+    <value>Configuration file recursion exceeded maximum allowed depth of {0}.</value>
+  </data>
+  <data name="SubmoduleWithoutCommit" xml:space="preserve">
+    <value>Submodule '{0}' doesn't have any commit</value>
+  </data>
+  <data name="InvalidSubmoduleUrl" xml:space="preserve">
+    <value>The URL of submodule '{0}' is missing or invalid: '{1}'</value>
+  </data>
+  <data name="InvalidSubmodulePath" xml:space="preserve">
+    <value>The path of submodule '{0}' is missing or invalid: '{1}'</value>
+  </data>
+  <data name="SourceCodeWontBeAvailableViaSourceLink" xml:space="preserve">
+    <value>{0} -- the source code won't be available via Source Link.</value>
   </data>
   <data name="RepositoryHasNoCommit" xml:space="preserve">
     <value>Repository has no commit.</value>
@@ -136,9 +181,9 @@
     <value>The URL of repository remote '{0}' is invalid: '{1}'.</value>
   </data>
   <data name="UnableToLocateRepository" xml:space="preserve">
-    <value>Unable to locate repository containing directory '{0}'.</value>
+    <value>Unable to locate repository with working directory that contains directory '{0}'.</value>
   </data>
-  <data name="BareRepositoriesNotSupported" xml:space="preserve">
-    <value>Bare repositories are not supported: '{0}'.</value>
+  <data name="RepositoryDoesNotHaveSpecifiedRemote" xml:space="preserve">
+    <value>Repository does not have the specified remote '{0}'; using '{1}' instead.</value>
   </data>
 </root>
\ No newline at end of file
diff --git a/src/Microsoft.Build.Tasks.Git/RuntimeIdMap.cs b/src/Microsoft.Build.Tasks.Git/RuntimeIdMap.cs
deleted file mode 100644
index 458825672..000000000
--- 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<string>();
-                }
-
-                qualifiers = runtimeId.Substring(architectureSeparator + 1);
-            }
-            else
-            {
-                if (versionSeparator >= 0)
-                {
-                    version = runtimeId.Substring(versionSeparator + 1).Split('.');
-                }
-                else
-                {
-                    osName = runtimeId;
-                    version = Array.Empty<string>();
-                }
-
-                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/TaskImplementation.cs b/src/Microsoft.Build.Tasks.Git/TaskImplementation.cs
deleted file mode 100644
index f9d6628c1..000000000
--- a/src/Microsoft.Build.Tasks.Git/TaskImplementation.cs
+++ /dev/null
@@ -1,96 +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.Collections.Generic;
-using System.IO;
-using System.Reflection;
-
-namespace Microsoft.Build.Tasks.Git
-{
-    internal static class TaskImplementation
-    {
-        public static Func<LocateRepository, bool> LocateRepository;
-        public static Func<GetRepositoryUrl, bool> GetRepositoryUrl;
-        public static Func<GetSourceRevisionId, bool> GetSourceRevisionId;
-        public static Func<GetSourceRoots, bool> GetSourceRoots;
-        public static Func<GetUntrackedFiles, bool> GetUntrackedFiles;
-
-        private static readonly string s_taskDirectory;
-        private const string GitOperationsAssemblyName = "Microsoft.Build.Tasks.Git.Operations";
-
-        static TaskImplementation()
-        {
-            s_taskDirectory = Path.GetDirectoryName(typeof(TaskImplementation).Assembly.Location);
-#if NET461
-            s_nullVersion = new Version(0, 0, 0, 0);
-            s_loaderLog = new List<string>();
-
-            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<LocateRepository, bool>)type.GetDeclaredMethod(nameof(LocateRepository)).CreateDelegate(typeof(Func<LocateRepository, bool>));
-            GetRepositoryUrl = (Func<GetRepositoryUrl, bool>)type.GetDeclaredMethod(nameof(GetRepositoryUrl)).CreateDelegate(typeof(Func<GetRepositoryUrl, bool>));
-            GetSourceRevisionId = (Func<GetSourceRevisionId, bool>)type.GetDeclaredMethod(nameof(GetSourceRevisionId)).CreateDelegate(typeof(Func<GetSourceRevisionId, bool>));
-            GetSourceRoots = (Func<GetSourceRoots, bool>)type.GetDeclaredMethod(nameof(GetSourceRoots)).CreateDelegate(typeof(Func<GetSourceRoots, bool>));
-            GetUntrackedFiles = (Func<GetUntrackedFiles, bool>)type.GetDeclaredMethod(nameof(GetUntrackedFiles)).CreateDelegate(typeof(Func<GetUntrackedFiles, bool>));
-        }
-
-#if NET461
-        private static readonly Version s_nullVersion;
-        private static readonly List<string> s_loaderLog;
-
-        private static void Log(ResolveEventArgs args, string outcome)
-        {
-            lock (s_loaderLog)
-            {
-                s_loaderLog.Add($"Loading '{args.Name}' referenced by '{args.RequestingAssembly}': {outcome}.");
-            }
-        }
-
-        internal static string[] GetLog()
-        {
-            lock (s_loaderLog)
-            {
-                return s_loaderLog.ToArray();
-            }
-        }
-
-        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 referenceName = new AssemblyName(args.Name);
-            if (!referenceName.Name.StartsWith("System.", StringComparison.OrdinalIgnoreCase))
-            {
-                Log(args, "not System");
-                return null;
-            }
-
-            if (referenceName.Version != s_nullVersion)
-            {
-                Log(args, "not null version");
-                return null;
-            }
-
-            var referencePath = Path.Combine(s_taskDirectory, referenceName.Name + ".dll");
-            if (!File.Exists(referencePath))
-            {
-                Log(args, $"file '{referencePath}' not found");
-                return null;
-            }
-
-            Log(args, $"loading from '{referencePath}'");
-            return Assembly.Load(AssemblyName.GetAssemblyName(referencePath));
-        }
-#endif
-    }
-}
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/build/Microsoft.Build.Tasks.Git.targets b/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets
new file mode 100644
index 000000000..667542743
--- /dev/null
+++ b/src/Microsoft.Build.Tasks.Git/build/Microsoft.Build.Tasks.Git.targets
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project>
+  <UsingTask TaskName="Microsoft.Build.Tasks.Git.LocateRepository" AssemblyFile="$(MicrosoftBuildTasksGitAssemblyFile)"/>
+  <UsingTask TaskName="Microsoft.Build.Tasks.Git.GetUntrackedFiles" AssemblyFile="$(MicrosoftBuildTasksGitAssemblyFile)"/>
+  
+  <Target Name="InitializeSourceControlInformationFromSourceControlManager">
+    <!--
+      Reports a warning if the given project doesn't belong to a repository under source control.
+    -->
+    <Microsoft.Build.Tasks.Git.LocateRepository Path="$(MSBuildProjectDirectory)" >
+      <Output TaskParameter="RepositoryId" PropertyName="_GitRepositoryId" />
+      <Output TaskParameter="Url" PropertyName="ScmRepositoryUrl" />
+      <Output TaskParameter="Roots" ItemName="SourceRoot" />
+      <Output TaskParameter="RevisionId" PropertyName="SourceRevisionId" Condition="'$(SourceRevisionId)' == ''" />
+    </Microsoft.Build.Tasks.Git.LocateRepository>
+
+    <PropertyGroup>
+      <RepositoryType Condition="'$(RepositoryType)' == ''">git</RepositoryType>
+    </PropertyGroup>
+  </Target>
+  
+  <!--
+    Embed files to the PDB that either do not belong to any of the SourceRoot directories
+    or belong to the source repository, but are explicitly ignored (excluded) from source control.
+  -->
+  <Target Name="SetEmbeddedFilesFromSourceControlManagerUntrackedFiles"
+          DependsOnTargets="InitializeSourceControlInformationFromSourceControlManager">
+
+    <Microsoft.Build.Tasks.Git.GetUntrackedFiles RepositoryId="$(_GitRepositoryId)" ProjectDirectory="$(MSBuildProjectDirectory)" Files="@(Compile)">
+      <Output TaskParameter="UntrackedFiles" ItemName="EmbeddedFiles" />
+    </Microsoft.Build.Tasks.Git.GetUntrackedFiles>
+  </Target>
+
+</Project>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.cs.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.cs.xlf
index 1174f5eee..e857c51ce 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.cs.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.cs.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="cs" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">Adresa URL vzdáleného úložiště {0} je neplatná: {1}.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Cesta dílčího modulu {0} je neplatná: {1}, zdrojový kód nebude přes zdrojový odkaz dostupný.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Adresa URL dílčího modulu {0} je neplatná: {1}, zdrojový kód nebude přes zdrojový odkaz dostupný.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">Úložiště nemá žádné potvrzení.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">Dílčí modul {0} nemá žádné potvrzení, zdrojový kód nebude přes zdrojový odkaz dostupný.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">Nejde najít úložiště obsahující adresář {0}.</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">Nejde najít úložiště obsahující adresář {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">Holá úložiště (bez pracovních adresářů) nejsou podporovaná: {0}.</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.de.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.de.xlf
index 64af3d65b..3a78b141e 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.de.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.de.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="de" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">Die URL des Remote-Objekts "{0}" des Repositorys ist ungültig: "{1}".</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Der Pfad von Untermodul "{0}" ist ungültig: "{1}". Der Quellcode steht nicht über den Quelllink zur Verfügung.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Die URL von Untermodul "{0}" ist ungültig: "{1}". Der Quellcode steht nicht über den Quelllink zur Verfügung.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">Kein Commit für Repository vorhanden.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">Es liegt kein Commit für das Untermodul "{0}" vor, der Quellcode steht nicht über den Quelllink zur Verfügung.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">Das Repository mit dem Verzeichnis "{0}" wurde nicht gefunden.</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">Das Repository mit dem Verzeichnis "{0}" wurde nicht gefunden.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">Leere Repositorys werden nicht unterstützt: "{0}".</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.es.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.es.xlf
index f85cc3d60..c86bcd419 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.es.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.es.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="es" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">La URL del "{0}" remoto del repositorio no es válida: "{1}".</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">La ruta de acceso del submódulo "{0}" no es válida: "{1}", el código fuente no estará disponible mediante el vínculo de origen.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">La dirección URL del submódulo "{0}" no es válida: "{1}", el código fuente no estará disponible mediante el vínculo de origen.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">El repositorio no tiene commit.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">El submódulo "{0}" no tiene ninguna confirmación, el código fuente no estará disponible mediante el vínculo de origen.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">No se encuentra el repositorio que contiene el directorio "{0}".</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">No se encuentra el repositorio que contiene el directorio "{0}".</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">No se admiten los repositorios vacíos: "{0}".</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.fr.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.fr.xlf
index 2b7b77a0a..16acfed9e 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.fr.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.fr.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="fr" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">L'URL du dépôt distant « {0} » n'est pas valide : « {1} ».</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Le chemin du sous-module '{0}' n'est pas valide : '{1}', le code source ne sera pas disponible via le lien source.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">L'URL du sous-module '{0}' n'est pas valide : '{1}', le code source ne sera pas disponible via le lien source.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">Le dépôt n'a aucune validation.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">Le sous-module '{0}' ne présente aucune validation, le code source ne sera pas disponible via le lien source.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">Impossible de localiser le référentiel contenant le répertoire '{0}'.</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">Impossible de localiser le référentiel contenant le répertoire '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">Les référentiels nus ne sont pas pris en charge : '{0}'.</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.it.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.it.xlf
index 9a1521b33..601581cc3 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.it.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.it.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="it" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">L'URL del repository remoto '{0}' non è valido: '{1}'.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Il percorso del modulo secondario '{0}' non è valido: '{1}'. Il codice sorgente non sarà disponibile tramite il collegamento all'origine.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">L'URL del modulo secondario '{0}' non è valido: '{1}'. Il codice sorgente non sarà disponibile tramite il collegamento all'origine.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">Non esiste alcun commit per il repository.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">Non esiste alcun commit per il modulo secondario '{0}'. Il codice sorgente non sarà disponibile tramite il collegamento all'origine.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">Non è stato individuato alcun repository contenente la directory '{0}'.</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">Non è stato individuato alcun repository contenente la directory '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">I repository bare non sono supportati: '{0}'.</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.ja.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.ja.xlf
index dc12872b9..7fb48af1b 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.ja.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.ja.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="ja" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">リモート リポジトリ '{0}' の URL が有効ではありません: '{1}'。</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">サブモジュール '{0}' のパスが無効です: '{1}'。ソース リンクを使用してソース コードを使用することはできません。</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">サブモジュール '{0}' の URL が無効です: '{1}'。ソース リンクを使用してソース コードを使用することはできません。</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">リポジトリにコミットがありません。</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">サブモジュール '{0}' にコミットがありません。ソース リンクを使用してソース コードを使用することはできません。</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">ディレクトリ '{0}' が入っているリポジトリが見つかりません。</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">ディレクトリ '{0}' が入っているリポジトリが見つかりません。</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">ベア リポジトリはサポートされていません: '{0}'。</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.ko.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.ko.xlf
index c22deec9e..32cc61206 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.ko.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.ko.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="ko" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">리포지토리 원격 '{0}'의 URL이 잘못되었습니다. '{1}'.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">하위 모듈의 경로 '{0}'이(가) 잘못되었습니다. '{1}', 소스 링크를 통해 소스 코드를 사용할 수 없습니다.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">하위 모듈의 URL '{0}'이(가) 잘못되었습니다. '{1}', 소스 링크를 통해 소스 코드를 사용할 수 없습니다.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">저장소에 커밋 내역이 없습니다.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">하위 모듈 '{0}'에 커밋이 없으며, 소스 링크를 통해 소스 코드를 사용할 수 없습니다.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">'{0}' 디렉터리를 포함하는 리포지토리를 찾을 수 없습니다.</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">'{0}' 디렉터리를 포함하는 리포지토리를 찾을 수 없습니다.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">Bare 리포지토리는 지원되지 않습니다. '{0}'</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.pl.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.pl.xlf
index 039a0763f..6cd29b81d 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.pl.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.pl.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="pl" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">Adres URL lokalizacji zdalnej „{0}” repozytorium jest nieprawidłowy: „{1}”.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Ścieżka modułu podrzędnego „{0}” jest nieprawidłowa: „{1}”. Kod źródłowy nie będzie dostępny za pośrednictwem linku do źródła.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Adres URL modułu podrzędnego „{0}” jest nieprawidłowy: „{1}”. Kod źródłowy nie będzie dostępny za pośrednictwem linku do źródła.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">W repozytorium nie ma zatwierdzenia.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">Moduł podrzędny „{0}” nie zawiera żadnego zatwierdzenia. Kod źródłowy nie będzie dostępny za pośrednictwem linku do źródła.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">Nie można zlokalizować repozytorium zawierającego katalog „{0}”.</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">Nie można zlokalizować repozytorium zawierającego katalog „{0}”.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">Repozytoria surowe są nieobsługiwane: „{0}”.</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.pt-BR.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.pt-BR.xlf
index a21a2346a..4556ea1e9 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.pt-BR.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.pt-BR.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="pt-BR" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">A URL do repositório remoto '{0}' não é válida: '{1}'.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">O caminho do submódulo '{0}' é inválido: '{1}'. O código-fonte não estará disponível por meio do link de origem.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">A URL do submódulo '{0}' é inválido: '{1}'. O código-fonte não estará disponível por meio do link de origem.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">O repositório não tem nenhuma confirmação.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">O submódulo '{0}' não tem nenhuma confirmação. O código-fonte não estará disponível por meio do link de origem.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">Não foi possível localizar o repositório que contém o diretório '{0}'.</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">Não foi possível localizar o repositório que contém o diretório '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">Repositórios básicos não têm suporte: '{0}'.</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.ru.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.ru.xlf
index 4c079f811..d0fd3c5dc 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.ru.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.ru.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="ru" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">URL-адрес удаленного репозитория "{0}" является недопустимым: "{1}".</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Подмодуль "{0}" имеет недопустимый путь: "{1}". Исходный код по ссылке будет недоступен.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">Подмодуль "{0}" имеет недопустимый URL: "{1}". Исходный код по ссылке будет недоступен.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">Хранилище не содержит фиксаций.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">В подмодуле "{0}" нет фиксаций. Исходный код по ссылке будет недоступен.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">Не удается найти репозиторий с каталогом "{0}".</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">Не удается найти репозиторий с каталогом "{0}".</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">Чистые (bare) репозитории не поддерживаются: "{0}".</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.tr.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.tr.xlf
index 9ca5d525e..d31467c98 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.tr.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.tr.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="tr" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">'{0}' uzak deposunun URL'si geçersiz: '{1}'.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">'{0}' alt modülünün yolu geçersiz: '{1}', kaynak koda kaynak bağlantısı aracılığıyla ulaşılamayacak.</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">'{0}' alt modülünün URL’si geçersiz: '{1}', kaynak koda kaynak bağlantısı aracılığıyla ulaşılamayacak.</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">Depoda işleme yok.</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">'{0}' alt modülünde işleme yok, kaynak koda kaynak bağlantısı aracılığıyla ulaşılamayacak.</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">'{0}' dizinini içeren depo bulunamıyor.</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">'{0}' dizinini içeren depo bulunamıyor.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">Paylaşım depoları desteklenmiyor: '{0}'.</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.zh-Hans.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.zh-Hans.xlf
index f209106a3..d74efb6c2 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.zh-Hans.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.zh-Hans.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="zh-Hans" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">远程存储库“{0}”的 URL 无效:“{1}”。</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">子模块 '{0}' 的路径无效: '{1}'，不可通过源链接访问源代码。</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">子模块 '{0}' 的 URL 无效: '{1}'，不可通过源链接访问源代码。</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">存储库没有提交。</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">子模块 '{0}' 不含任何提交，不可通过源链接访问源代码。</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">无法定位包含目录 '{0}' 的存储库。</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">无法定位包含目录 '{0}' 的存储库。</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">不支持裸存储库: '{0}'.</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
diff --git a/src/Microsoft.Build.Tasks.Git/xlf/Resources.zh-Hant.xlf b/src/Microsoft.Build.Tasks.Git/xlf/Resources.zh-Hant.xlf
index 4c448e6dc..bce63603c 100644
--- a/src/Microsoft.Build.Tasks.Git/xlf/Resources.zh-Hant.xlf
+++ b/src/Microsoft.Build.Tasks.Git/xlf/Resources.zh-Hant.xlf
@@ -2,19 +2,84 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="zh-Hant" original="../Resources.resx">
     <body>
+      <trans-unit id="ConfigurationFileRecursionExceededMaximumAllowedDepth">
+        <source>Configuration file recursion exceeded maximum allowed depth of {0}.</source>
+        <target state="new">Configuration file recursion exceeded maximum allowed depth of {0}.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="ErrorReadingGitRepositoryInformation">
+        <source>Error reading git repository information: {0}</source>
+        <target state="new">Error reading git repository information: {0}</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="FormatOfFileIsInvalid">
+        <source>The format of the file '{0}' is invalid.</source>
+        <target state="new">The format of the file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidFormatOfFileAtPath">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidModulePath">
+        <source>Invalid module path: '{0}'.</source>
+        <target state="new">Invalid module path: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidReference">
+        <source>Invalid reference: '{0}'.</source>
+        <target state="new">Invalid reference: '{0}'.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="InvalidRepositoryRemoteUrl">
         <source>The URL of repository remote '{0}' is invalid: '{1}'.</source>
         <target state="translated">存放庫遠端 ‘{0}’ 的 URL 無效: ‘{1}’。</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmodulePath_SourceLink">
-        <source>The path of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">子模組 '{0}' 的路徑無效: '{1}'。來源連結中的原始程式碼將無法使用。</target>
+      <trans-unit id="InvalidSubmodulePath">
+        <source>The path of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The path of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="InvalidSubmoduleUrl">
+        <source>The URL of submodule '{0}' is missing or invalid: '{1}'</source>
+        <target state="new">The URL of submodule '{0}' is missing or invalid: '{1}'</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeAbsolute">
+        <source>Path must be absolute</source>
+        <target state="new">Path must be absolute</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathMustBeFilePath">
+        <source>Path must be a file path</source>
+        <target state="new">Path must be a file path</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsInvalid">
+        <source>Path specified in file '{0}' is invalid.</source>
+        <target state="new">Path specified in file '{0}' is invalid.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="PathSpecifiedInFileIsNotAbsolute">
+        <source>Path specified in file '{0}' is not absolute.</source>
+        <target state="new">Path specified in file '{0}' is not absolute.</target>
         <note />
       </trans-unit>
-      <trans-unit id="InvalidSubmoduleUrl_SourceLink">
-        <source>The URL of submodule '{0}' is invalid: '{1}', the source code won't be available via source link.</source>
-        <target state="translated">子模組 '{0}' 的 URL 無效: '{1}'。來源連結中的原始程式碼將無法使用。</target>
+      <trans-unit id="RecursionDetectedWhileResolvingReference">
+        <source>Recursion detected while resolving reference: '{0}'.</source>
+        <target state="new">Recursion detected while resolving reference: '{0}'.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveSpecifiedRemote">
+        <source>Repository does not have the specified remote '{0}'; using '{1}' instead.</source>
+        <target state="new">Repository does not have the specified remote '{0}'; using '{1}' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="RepositoryDoesNotHaveWorkingDirectory">
+        <source>Repository does not have a working directory.</source>
+        <target state="new">Repository does not have a working directory.</target>
         <note />
       </trans-unit>
       <trans-unit id="RepositoryHasNoRemote">
@@ -27,19 +92,29 @@
         <target state="translated">存放庫沒有認可。</target>
         <note />
       </trans-unit>
-      <trans-unit id="SubmoduleWithoutCommit_SourceLink">
-        <source>Submodule '{0}' doesn't have any commit, the source code won't be available via source link.</source>
-        <target state="translated">子模組 '{0}' 中不具任何認可。來源連結中的原始程式碼將無法使用。</target>
+      <trans-unit id="SourceCodeWontBeAvailableViaSourceLink">
+        <source>{0} -- the source code won't be available via Source Link.</source>
+        <target state="new">{0} -- the source code won't be available via Source Link.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="SubmoduleWithoutCommit">
+        <source>Submodule '{0}' doesn't have any commit</source>
+        <target state="new">Submodule '{0}' doesn't have any commit</target>
         <note />
       </trans-unit>
       <trans-unit id="UnableToLocateRepository">
-        <source>Unable to locate repository containing directory '{0}'.</source>
-        <target state="translated">找不到包含 '{0}' 的存放庫。</target>
+        <source>Unable to locate repository with working directory that contains directory '{0}'.</source>
+        <target state="needs-review-translation">找不到包含 '{0}' 的存放庫。</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="UnsupportedRepositoryVersion">
+        <source>Unsupported repository version {0}. Only versions up to {1} are supported.</source>
+        <target state="new">Unsupported repository version {0}. Only versions up to {1} are supported.</target>
         <note />
       </trans-unit>
-      <trans-unit id="BareRepositoriesNotSupported">
-        <source>Bare repositories are not supported: '{0}'.</source>
-        <target state="translated">不支援空的存放庫: '{0}'。</target>
+      <trans-unit id="ValueOfIsNotValidPath">
+        <source>The value of {0} is not a valid path: '{1}'.</source>
+        <target state="new">The value of {0} is not a valid path: '{1}'.</target>
         <note />
       </trans-unit>
     </body>
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 e582685f9..dc06724bc 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 @@
   <PropertyGroup>
     <TargetFramework>net46</TargetFramework>
     <AutoGenerateAssemblyVersion>true</AutoGenerateAssemblyVersion>
+    <ExcludeFromSourceBuild>true</ExcludeFromSourceBuild>
 
     <!-- Using an explicit nuspec file since NuGet Pack target currently doesn't support including dependencies in tools packages -->
     <IsPackable>true</IsPackable>
diff --git a/src/SourceLink.Git.IntegrationTests/GitHubTests.cs b/src/SourceLink.Git.IntegrationTests/GitHubTests.cs
index 3926b204d..681804e2d 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(@"
+<Project Sdk='Microsoft.NET.Sdk'>
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+  </PropertyGroup>
+</Project>
+");
+
+            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: $@"
+<ItemGroup>
+  <ProjectReference Include='{project2.Path}'/>
+</ItemGroup>
+",
+                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/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj b/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj
index 551518f9e..efc7ea5d7 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 @@
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="LibGit2Sharp" Version="$(LibGit2SharpVersion)" />
-    <ProjectReference Include="..\Microsoft.Build.Tasks.Git.Operations\Microsoft.Build.Tasks.Git.Operations.csproj" />
     <ProjectReference Include="..\Microsoft.Build.Tasks.Git\Microsoft.Build.Tasks.Git.csproj" />
     <ProjectReference Include="..\SourceLink.Common\Microsoft.SourceLink.Common.csproj" />
     <ProjectReference Include="..\SourceLink.GitHub\Microsoft.SourceLink.GitHub.csproj" />
diff --git a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs
index 03caabbbb..615772273 100644
--- a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs
+++ b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs
@@ -55,11 +55,13 @@ private static string GetLocalNuGetConfigContent(string packagesDir) =>
 </configuration>
 ";
 
+        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 @@ private static string GetLocalNuGetConfigContent(string packagesDir) =>
         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(
 $@"<Project>
   <ItemGroup>
     {string.Join(Environment.NewLine, packages.Select(packageName => $"<PackageReference Include='{packageName}' Version='1.0.0-*' PrivateAssets='all' />"))}
   </ItemGroup>
 </Project>
 ");
-            ProjectDir.CreateFile("Directory.Build.targets").WriteAllText("<Project/>");
-            ProjectDir.CreateFile(".editorconfig").WriteAllText("root = true");
-            ProjectDir.CreateFile("nuget.config").WriteAllText(GetLocalNuGetConfigContent(s_buildInfo.PackagesDirectory));
+            RootDir.CreateFile("Directory.Build.targets").WriteAllText("<Project/>");
+            RootDir.CreateFile(".editorconfig").WriteAllText("root = true");
+            RootDir.CreateFile("nuget.config").WriteAllText(GetLocalNuGetConfigContent(s_buildInfo.PackagesDirectory));
+
+            SourceRoot = RootDir.Path + Path.DirectorySeparatorChar;
 
             EnvironmentVariables = new Dictionary<string, string>()
             {
@@ -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 @@ protected void VerifyValues(
             string[] expectedResults = null, 
             string[] expectedErrors = null, 
             string[] expectedWarnings = null,
-            string additionalCommandLineArgs = null)
+            string additionalCommandLineArgs = null,
+            string buildVerbosity = "minimal",
+            Func<string, bool> 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<string>(), 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 { }
                 }
             }
         }
diff --git a/src/TestUtilities/TestUtilities.csproj b/src/TestUtilities/TestUtilities.csproj
index bd61a5026..168f87687 100644
--- a/src/TestUtilities/TestUtilities.csproj
+++ b/src/TestUtilities/TestUtilities.csproj
@@ -2,6 +2,7 @@
   <PropertyGroup>
     <TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
     <IsShipping>false</IsShipping>
+    <ExcludeFromSourceBuild>true</ExcludeFromSourceBuild>
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="NuGet.Versioning" Version="$(NuGetVersioningVersion)" />
