diff --git a/Templating.sln b/Templating.sln
index 8fdd9e5dc6..c37115b9dc 100644
--- a/Templating.sln
+++ b/Templating.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26730.0
+VisualStudioVersion = 15.0.26730.16
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Test", "test\Templates.Test\Templates.Test.csproj", "{D43A4D24-D514-44C2-9438-54F6EDF58680}"
EndProject
@@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Web.Client
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Web.ProjectTemplates.2.1", "src\Microsoft.DotNet.Web.ProjectTemplates.2.1\Microsoft.DotNet.Web.ProjectTemplates.2.1.csproj", "{260EBA09-DEF5-429C-99BF-90CA1456A576}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{973DC5B6-710B-4FC8-AF20-E94B93859DE8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PullRequestSubmitter", "tools\PullRequestSubmitter\PullRequestSubmitter.csproj", "{AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -65,6 +69,10 @@ Global
{260EBA09-DEF5-429C-99BF-90CA1456A576}.Debug|Any CPU.Build.0 = Debug|Any CPU
{260EBA09-DEF5-429C-99BF-90CA1456A576}.Release|Any CPU.ActiveCfg = Release|Any CPU
{260EBA09-DEF5-429C-99BF-90CA1456A576}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -73,10 +81,12 @@ Global
{ABC9D95C-7FBD-4F8D-827A-00942EA3D0C0} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
{B8EB8821-9B58-465A-9693-5F9289AA7B29} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
{62D00388-8824-4661-8CC8-8D8436FF97E6} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
+ {15806338-550D-4418-99AE-42FDAE03808A} = {973DC5B6-710B-4FC8-AF20-E94B93859DE8}
{01E12D5E-8540-4BC8-9A54-41EDD55E762E} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
{402E62D1-7FD0-4E07-812C-0E385D98D6D9} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
{1731F6D9-1DFC-49D6-8F28-471194B1962C} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
{260EBA09-DEF5-429C-99BF-90CA1456A576} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
+ {AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A} = {973DC5B6-710B-4FC8-AF20-E94B93859DE8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E9B27B0D-4F85-431B-9C26-80CFE4393D36}
diff --git a/build/repo.targets b/build/repo.targets
index e8f57fdaf7..7307370b90 100644
--- a/build/repo.targets
+++ b/build/repo.targets
@@ -1,10 +1,41 @@
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)..\tools\DependencyUpdater\bin\Debug\netstandard2.0\DependencyUpdater.deps.json
+ $([System.IO.File]::ReadAllText('$(DepsFilePath)'))
+ $([System.Text.RegularExpressions.Regex]::Match($(DepsFileContent), `\s+"Microsoft.AspNetCore": "([^"]+)"`).Groups[1].Value)
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/PullRequestSubmitter/Helpers/GitHubUtil.cs b/tools/PullRequestSubmitter/Helpers/GitHubUtil.cs
new file mode 100644
index 0000000000..538bfe4fc5
--- /dev/null
+++ b/tools/PullRequestSubmitter/Helpers/GitHubUtil.cs
@@ -0,0 +1,161 @@
+using Octokit;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace PullRequestSubmitter.Helpers
+{
+ static class GitHubUtil
+ {
+ public static async Task> GetEditsToCommit(
+ GitHubClient client, Repository upstreamRepo, string baseSha,
+ IEnumerable propertyUpdates)
+ {
+ // Find the file to update
+ var existingTree = await client.Git.Tree.GetRecursive(upstreamRepo.Id, baseSha);
+
+ // Update the files' contents
+ var result = new Dictionary();
+ var filesToUpdate = propertyUpdates.GroupBy(p => p.Filename);
+ foreach (var fileToUpdate in filesToUpdate)
+ {
+ var fileContents = await GetFileContentsAsString(
+ client, upstreamRepo, existingTree.Tree, fileToUpdate.Key);
+
+ foreach (var propToUpdate in fileToUpdate)
+ {
+ var propName = propToUpdate.PropertyName;
+ var patternToReplace = new Regex($"<{propName}>[^<]+{propName}>");
+ if (!patternToReplace.IsMatch(fileContents))
+ {
+ throw new Exception($"The file {fileToUpdate.Key} does not contain a match for regex " + patternToReplace.ToString());
+ }
+
+ fileContents = patternToReplace.Replace(
+ fileContents,
+ $"<{propName}>{propToUpdate.NewValue}{propName}>");
+ }
+
+ var newBlob = new NewBlob { Content = fileContents, Encoding = EncodingType.Utf8 };
+ result.Add(fileToUpdate.Key, newBlob);
+ }
+
+ return result;
+ }
+
+ public static async Task GetLatestCommitSha(
+ GitHubClient client, Repository repo, string branchName)
+ {
+ var commitRef = await client.Git.Reference.Get(
+ repo.Id,
+ $"heads/{branchName}");
+ return commitRef.Object.Sha;
+ }
+
+ public static async Task CommitModifiedFiles(
+ GitHubClient client, Repository toRepo, string toBranchName, string parentCommitSha,
+ IDictionary modifiedFiles, string commitMessage)
+ {
+ // Build and commit a new tree representing the updated state
+ var newTree = new NewTree { BaseTree = parentCommitSha };
+ foreach (var kvp in modifiedFiles)
+ {
+ newTree.Tree.Remove(new NewTreeItem { Path = kvp.Key });
+ newTree.Tree.Add(new NewTreeItem()
+ {
+ Type = TreeType.Blob,
+ Mode = "100644",
+ Sha = (await client.Git.Blob.Create(toRepo.Id, kvp.Value)).Sha,
+ Path = kvp.Key
+ });
+ }
+ var createdTree = await client.Git.Tree.Create(toRepo.Id, newTree);
+ var commit = await client.Git.Commit.Create(
+ toRepo.Id,
+ new NewCommit(commitMessage, createdTree.Sha, parentCommitSha));
+
+ // Update the target branch to point to the new commit
+ await client.Git.Reference.Update(
+ toRepo.Id,
+ $"heads/{toBranchName}",
+ new ReferenceUpdate(commit.Sha, force: true));
+
+ return commit.Sha;
+ }
+
+ public static async Task FindExistingPullRequestToUpdate(
+ GitHubClient client, User currentUser, Repository upstreamRepo,
+ Repository forkRepo, string forkBranch)
+ {
+ // Search for candidate PRs (same author, still open, etc.)
+ var fromBaseRef = $"{forkRepo.Owner.Login}:{forkBranch}";
+ var searchInRepos = new RepositoryCollection();
+ searchInRepos.Add(upstreamRepo.Owner.Login, upstreamRepo.Name);
+ var searchRequest = new SearchIssuesRequest
+ {
+ Repos = searchInRepos,
+ Type = IssueTypeQualifier.PullRequest,
+ Author = currentUser.Login,
+ State = ItemState.Open
+ };
+ var searchResults = await client.Search.SearchIssues(searchRequest);
+
+ // Of the candidates, find the highest-numbered one that is requesting a
+ // pull from the same fork and branch. GitHub only allows there to be one
+ // of these at any given time, but we're more likely to find it faster
+ // by searching from newest to oldest.
+ var candidates = searchResults.Items.OrderByDescending(item => item.Number);
+ foreach (var prInfo in candidates)
+ {
+ var pr = await client.PullRequest.Get(upstreamRepo.Id, prInfo.Number);
+ if (pr.Head?.Repository?.Id == forkRepo.Id && pr.Head?.Ref == forkBranch)
+ {
+ return prInfo;
+ }
+ }
+
+ return null;
+ }
+
+ public static async Task CreateNewPullRequest(
+ GitHubClient client, Repository upstreamRepo, string upstreamBranch,
+ Repository forkRepo, string forkBranch, string prBodyText)
+ {
+ var fromBaseRef = $"{forkRepo.Owner.Login}:{forkBranch}";
+ var newPr = new NewPullRequest(
+ prBodyText,
+ fromBaseRef,
+ upstreamBranch);
+ return await client.PullRequest.Create(upstreamRepo.Id, newPr);
+ }
+
+ public static async Task UpdateExistingPullRequestTitle(
+ GitHubClient client, Repository upstreamRepo, int prNumber, string newTitle)
+ {
+ var updateInfo = new PullRequestUpdate { Title = newTitle };
+ await client.PullRequest.Update(upstreamRepo.Id, prNumber, updateInfo);
+ }
+
+ private static async Task GetFileContentsAsString(
+ GitHubClient client, Repository repo, IReadOnlyList tree, string path)
+ {
+ var existingFile = tree.FirstOrDefault(item => item.Path == path);
+ var blob = await client.Git.Blob.Get(repo.Id, existingFile.Sha);
+
+ switch (blob.Encoding.Value)
+ {
+ case EncodingType.Utf8:
+ return blob.Content;
+ case EncodingType.Base64:
+ return Encoding.UTF8.GetString(Convert.FromBase64String(blob.Content));
+ default:
+ throw new InvalidDataException($"Unsupported encoding: {blob.Encoding.StringValue}");
+ }
+ }
+ }
+}
diff --git a/tools/PullRequestSubmitter/Helpers/PropertyUpdate.cs b/tools/PullRequestSubmitter/Helpers/PropertyUpdate.cs
new file mode 100644
index 0000000000..7f96cde2f5
--- /dev/null
+++ b/tools/PullRequestSubmitter/Helpers/PropertyUpdate.cs
@@ -0,0 +1,9 @@
+namespace PullRequestSubmitter.Helpers
+{
+ class PropertyUpdate
+ {
+ public string Filename { get; set; }
+ public string PropertyName { get; set; }
+ public string NewValue { get; set; }
+ }
+}
diff --git a/tools/PullRequestSubmitter/PullRequestSubmitter.csproj b/tools/PullRequestSubmitter/PullRequestSubmitter.csproj
new file mode 100644
index 0000000000..0cf93739cd
--- /dev/null
+++ b/tools/PullRequestSubmitter/PullRequestSubmitter.csproj
@@ -0,0 +1,14 @@
+
+
+
+ netcoreapp2.0
+ true
+
+
+
+
+
+
+
+
+
diff --git a/tools/PullRequestSubmitter/PullRequestTask.cs b/tools/PullRequestSubmitter/PullRequestTask.cs
new file mode 100644
index 0000000000..7420b04c72
--- /dev/null
+++ b/tools/PullRequestSubmitter/PullRequestTask.cs
@@ -0,0 +1,105 @@
+using Microsoft.Build.Framework;
+using Octokit;
+using PullRequestSubmitter.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace PullRequestSubmitter
+{
+ public class PullRequestTask : Microsoft.Build.Utilities.Task
+ {
+ [Required] public string ApiToken { get; set; }
+ [Required] public string UpstreamRepoOwner { get; set; }
+ [Required] public string UpstreamRepoName { get; set; }
+ [Required] public string UpstreamRepoBranch { get; set; }
+ [Required] public string ForkRepoName { get; set; }
+ [Required] public string ForkRepoBranch { get; set; }
+ [Required] public string Message { get; set; }
+ [Required] public string FileToUpdate { get; set; }
+ [Required] public ITaskItem[] PropertyUpdates { get; set; }
+
+ public override bool Execute()
+ {
+ return ExecuteAsync().Result;
+ }
+
+ private IEnumerable GetPropertyUpdates()
+ {
+ return PropertyUpdates.Select(item => new PropertyUpdate
+ {
+ Filename = FileToUpdate,
+ PropertyName = item.ItemSpec,
+ NewValue = item.GetMetadata("NewValue")
+ });
+ }
+
+ private async Task ExecuteAsync()
+ {
+ var client = new GitHubClient(new ProductHeaderValue("PullRequestSubmitter"))
+ {
+ Credentials = new Credentials(ApiToken),
+ };
+
+ // Find the upstream repo and determine what edits we want to make
+ LogHigh($"Finding upstream repo: {UpstreamRepoOwner}/{UpstreamRepoName}...");
+ var upstreamRepo = await client.Repository.Get(UpstreamRepoOwner, UpstreamRepoName);
+ var upstreamCommitSha = await GitHubUtil.GetLatestCommitSha(client, upstreamRepo, UpstreamRepoBranch);
+ LogHigh($"Found upstream commit to update: {upstreamCommitSha} ({UpstreamRepoBranch})");
+ var editsToCommit = await GitHubUtil.GetEditsToCommit(
+ client, upstreamRepo, upstreamCommitSha, GetPropertyUpdates());
+ if (editsToCommit.Any())
+ {
+ var filesList = string.Join('\n',
+ editsToCommit.Select(e => " - " + e.Key));
+ LogHigh($"Will apply edits to file(s):\n{filesList}");
+ }
+ else
+ {
+ Log.LogError("Found no edits to apply. Aborting.");
+ return false;
+ }
+
+ // Commit the edits into the fork repo, updating its head to point to a new tree
+ // formed by updating the tree from the upstream SHA
+ var currentUser = await client.User.Current();
+ LogHigh($"Finding fork repo: {currentUser.Login}/{ForkRepoName}...");
+ var forkRepo = await client.Repository.Get(currentUser.Login, ForkRepoName);
+ var newCommitSha = await GitHubUtil.CommitModifiedFiles(
+ client,
+ forkRepo,
+ ForkRepoBranch,
+ upstreamCommitSha,
+ editsToCommit,
+ Message);
+ LogHigh($"Committed edits. {currentUser.Login}/{ForkRepoName} branch {ForkRepoBranch} is now at {newCommitSha}");
+
+ // If applicable, submit a new PR
+ LogHigh($"Checking if there is already an open PR we can update...");
+ var prToUpdate = await GitHubUtil.FindExistingPullRequestToUpdate(
+ client, currentUser, upstreamRepo, forkRepo, ForkRepoBranch);
+ if (prToUpdate == null)
+ {
+ LogHigh($"No existing open PR found. Creating new PR...");
+ var newPr = await GitHubUtil.CreateNewPullRequest(
+ client, upstreamRepo, UpstreamRepoBranch, forkRepo, ForkRepoBranch, Message);
+ LogHigh($"Created pull request #{newPr.Number} at {newPr.HtmlUrl}");
+ }
+ else
+ {
+ LogHigh($"Found existing PR #{prToUpdate.Number}. Updating details...");
+ await GitHubUtil.UpdateExistingPullRequestTitle(
+ client, upstreamRepo, prToUpdate.Number, Message);
+ LogHigh($"Finished updating PR #{prToUpdate.Number} at {prToUpdate.HtmlUrl}");
+ }
+
+ return true;
+ }
+
+ private void LogHigh(string message)
+ {
+ Log.LogMessage(MessageImportance.High, message);
+ }
+ }
+}