/// <param name="updateLatestVersion">If true, updates Latest.txt with a prerelease moniker. If there isn't one, makes the file empty.</param>
        /// <param name="updateLatestPackageList">If true, updates Latest_Packages.txt.</param>
        /// <param name="updateLastBuildPackageList">If true, updates Last_Build_Packages.txt, and enables keeping old packages in Latest_Packages.txt.</param>
        public async Task UpdateBuildInfoAsync(
            IEnumerable<string> packagePaths,
            string versionsRepoPath,
            bool updateLatestPackageList = true,
            bool updateLatestVersion = true,
            bool updateLastBuildPackageList = true)
        {
            if (packagePaths == null)
            {
                throw new ArgumentNullException(nameof(packagePaths));
            }
            if (versionsRepoPath == null)
            {
                throw new ArgumentNullException(nameof(versionsRepoPath));
            }

            NupkgNameInfo[] packages = packagePaths
                .Select(path => new NupkgNameInfo(path))
                // Ignore symbol packages.
                .Where(t => !t.SymbolPackage)
                .ToArray();

            string prereleaseVersion = packages
                .Select(t => t.Prerelease)
                .FirstOrDefault(prerelease => !string.IsNullOrEmpty(prerelease))
                ?? "";

            Dictionary<string, string> packageDictionary = packages.ToDictionary(
                    t => t.Id,
                    t => t.Version);

            using (GitHubClient client = new GitHubClient(_gitHubAuth))
            {
                for (int i = 0; i < MaxTries; i++)
                {
                    try
                    {
                        // Master commit to use as new commit's parent.
                        string masterRef = "heads/master";
                        GitReference currentMaster = await client.GetReferenceAsync(_project, masterRef);
                        string masterSha = currentMaster.Object.Sha;

                        List<GitObject> objects = new List<GitObject>();

                        if (updateLastBuildPackageList)
                        {
                            objects.Add(new GitObject
                            {
                                Path = $"{versionsRepoPath}/Last_Build_Packages.txt",
                                Type = GitObject.TypeBlob,
                                Mode = GitObject.ModeFile,
                                Content = CreatePackageListFile(packageDictionary)
                            });
                        }

                        if (updateLatestPackageList)
                        {
                            string latestPackagesPath = $"{versionsRepoPath}/Latest_Packages.txt";

                            var allPackages = new Dictionary<string, string>(packageDictionary);

                            if (updateLastBuildPackageList)
                            {
                                Dictionary<string, string> existingPackages = await GetPackagesAsync(client, latestPackagesPath);

                                // Add each existing package if there isn't a new package with the same id.
                                foreach (var package in existingPackages)
                                {
                                    if (!allPackages.ContainsKey(package.Key))
                                    {
                                        allPackages[package.Key] = package.Value;
                                    }
                                }
                            }

                            objects.Add(new GitObject
                            {
                                Path = latestPackagesPath,
                                Type = GitObject.TypeBlob,
                                Mode = GitObject.ModeFile,
                                Content = CreatePackageListFile(allPackages)
                            });
                        }

                        if (updateLatestVersion)
                        {
                            objects.Add(new GitObject
                            {
                                Path = $"{versionsRepoPath}/Latest.txt",
                                Type = GitObject.TypeBlob,
                                Mode = GitObject.ModeFile,
                                Content = prereleaseVersion
                            });
                        }

                        string message = $"Updating {versionsRepoPath}";
                        if (string.IsNullOrEmpty(prereleaseVersion))
                        {
                            message += ". No prerelease versions published.";
                        }
                        else
                        {
                            message += $" for {prereleaseVersion}";
                        }

                        GitTree tree = await client.PostTreeAsync(_project, masterSha, objects.ToArray());
                        GitCommit commit = await client.PostCommitAsync(_project, message, tree.Sha, new[] { masterSha });

                        // Only fast-forward. Don't overwrite other changes: throw exception instead.
                        await client.PatchReferenceAsync(_project, masterRef, commit.Sha, force: false);

                        Trace.TraceInformation($"Committed build-info update on attempt {i + 1}.");
                        break;
                    }
                    catch (HttpRequestException ex)
                    {
                        int nextTry = i + 1;
                        if (nextTry < MaxTries)
                        {
                            Trace.TraceInformation($"Encountered exception committing build-info update: {ex.Message}");
                            Trace.TraceInformation($"Trying again in {RetryMillisecondsDelay}ms. {MaxTries - nextTry} tries left.");
                            await Task.Delay(RetryMillisecondsDelay);
                        }
                        else
                        {
                            Trace.TraceInformation("Encountered exception committing build-info update.");
                            throw;
                        }
                    }
                }
            }
        }
        protected override void TraceListenedExecute()
        {
            // Use the commit sha of versions repo master (not just "master") for stable upgrade.
            var gitHubAuth = new GitHubAuth(GitHubAuthToken, GitHubUser, GitHubEmail);
            var client = new GitHubClient(gitHubAuth);
            string masterSha = client
                .GetReferenceAsync(new GitHubProject("versions", "dotnet"), "heads/master")
                .Result.Object.Sha;

            foreach (ITaskItem item in DependencyBuildInfo)
            {
                if (!string.IsNullOrEmpty(item.GetMetadata(CurrentRefMetadataName)))
                {
                    item.SetMetadata(CurrentRefMetadataName, masterSha);
                }
                string autoUpgradeBranch = item.GetMetadata(AutoUpgradeBranchMetadataName);
                if (!string.IsNullOrEmpty(autoUpgradeBranch))
                {
                    item.SetMetadata(CurrentBranchMetadataName, autoUpgradeBranch);
                }
            }

            DependencyUpdateResults updateResults = DependencyUpdateUtils.Update(
                CreateUpdaters().ToArray(),
                CreateBuildInfoDependencies().ToArray());

            // Update CurrentRef and CurrentBranch for each applicable build info used.
            if (!string.IsNullOrEmpty(CurrentRefXmlPath))
            {
                foreach (BuildInfo info in updateResults.UsedBuildInfos)
                {
                    ITaskItem infoItem = FindDependencyBuildInfo(info.Name);

                    if (!string.IsNullOrEmpty(infoItem.GetMetadata(CurrentRefMetadataName)))
                    {
                        UpdateProperty(
                            CurrentRefXmlPath,
                            $"{info.Name}{CurrentRefMetadataName}",
                            masterSha);
                    }

                    string autoUpgradeBranch = infoItem.GetMetadata(AutoUpgradeBranchMetadataName);
                    if (!string.IsNullOrEmpty(autoUpgradeBranch))
                    {
                        UpdateProperty(
                            CurrentRefXmlPath,
                            $"{info.Name}{CurrentBranchMetadataName}",
                            autoUpgradeBranch);
                    }
                }
            }

            if (updateResults.ChangesDetected())
            {
                var origin = new GitHubProject(ProjectRepoName, GitHubUser);

                var upstreamBranch = new GitHubBranch(
                    ProjectRepoBranch,
                    new GitHubProject(ProjectRepoName, ProjectRepoOwner));

                string suggestedMessage = updateResults.GetSuggestedCommitMessage();
                string body = string.Empty;
                if (NotifyGitHubUsers != null)
                {
                    body += PullRequestCreator.NotificationString(NotifyGitHubUsers.Select(item => item.ItemSpec));
                }

                var prCreator = new PullRequestCreator(gitHubAuth, origin, upstreamBranch, GitHubAuthor);
                prCreator.CreateOrUpdateAsync(
                    suggestedMessage,
                    suggestedMessage + $" ({ProjectRepoBranch})",
                    body,
                    forceCreate: AlwaysCreateNewPullRequest).Wait();
            }
            else
            {
                Log.LogMessage("No update required: no changes detected.");
            }
        }
        private async Task<Dictionary<string, string>> GetPackagesAsync(GitHubClient client, string path)
        {
            string latestPackages = await client.GetGitHubFileContentsAsync(
                path,
                new GitHubBranch("master", _project));

            using (var reader = new StringReader(latestPackages))
            {
                return await BuildInfo.ReadPackageListAsync(reader);
            }
        }
        protected override void TraceListenedExecute()
        {
            // Use the commit sha of versions repo master (not just "master") for stable upgrade.
            var gitHubAuth = new GitHubAuth(GitHubAuthToken, GitHubUser, GitHubEmail);
            var client = new GitHubClient(gitHubAuth);
            string masterSha = client
                .GetReferenceAsync(new GitHubProject("versions", "dotnet"), "heads/master")
                .Result.Object.Sha;

            foreach (ITaskItem item in DependencyBuildInfo)
            {
                if (!string.IsNullOrEmpty(item.GetMetadata(s_currentRef)))
                {
                    item.SetMetadata(s_currentRef, masterSha);
                }
            }

            DependencyUpdateResults updateResults = DependencyUpdateUtils.Update(
                CreateUpdaters().ToArray(),
                CreateBuildInfoDependencies().ToArray());

            if (!string.IsNullOrEmpty(CurrentRefXmlPath))
            {
                // Update the build info commit sha for each applicable build info used.
                foreach (BuildInfo info in updateResults.UsedBuildInfos)
                {
                    ITaskItem infoItem = FindDependencyBuildInfo(info.Name);
                    if (string.IsNullOrEmpty(infoItem.GetMetadata(s_currentRef)))
                    {
                        continue;
                    }

                    Regex upgrader = CreateXmlUpdateRegex($"{info.Name}{s_currentRef}", s_currentRef);

                    Action replace = FileUtils.ReplaceFileContents(
                        CurrentRefXmlPath,
                        contents =>
                        {
                            Match m = upgrader.Match(contents);
                            Group g = m.Groups[s_currentRef];

                            return contents
                                .Remove(g.Index, g.Length)
                                .Insert(g.Index, masterSha);
                        });
                    replace();
                }
            }

            if (updateResults.ChangesDetected())
            {
                var origin = new GitHubProject(ProjectRepoName, GitHubUser);

                var upstreamBranch = new GitHubBranch(
                    ProjectRepoBranch,
                    new GitHubProject(ProjectRepoName, ProjectRepoOwner));

                string suggestedMessage = updateResults.GetSuggestedCommitMessage();
                string body = string.Empty;
                if (NotifyGitHubUsers != null)
                {
                    body += PullRequestCreator.NotificationString(NotifyGitHubUsers.Select(item => item.ItemSpec));
                }

                var prCreator = new PullRequestCreator(gitHubAuth, origin, upstreamBranch, GitHubAuthor);
                prCreator.CreateOrUpdateAsync(
                    suggestedMessage,
                    suggestedMessage + $" ({ProjectRepoBranch})",
                    body,
                    forceCreate: AlwaysCreateNewPullRequest).Wait();
            }
            else
            {
                Log.LogMessage("No update required: no changes detected.");
            }
        }
        private async Task UpdateGitHubFileAsync(string path, string newFileContent, string commitMessage)
        {
            using (GitHubClient client = new GitHubClient(_gitHubAuth))
            {
                string fileUrl = $"https://api.github.com/repos/{_project.Segments}/contents/{path}";

                await client.PutGitHubFileAsync(fileUrl, commitMessage, newFileContent);
            }
        }
        public async Task CreateOrUpdateAsync(
            string commitMessage,
            string title,
            string description,
            bool forceCreate = false)
        {
            var upstream = UpstreamBranch.Project;

            using (var client = new GitHubClient(_auth))
            {
                GitHubBranch originBranch = null;
                GitHubPullRequest pullRequestToUpdate = null;

                string upgradeBranchPrefix = _namingStrategy.Prefix(UpstreamBranch.Name);

                if (!forceCreate)
                {
                    pullRequestToUpdate = await client.SearchPullRequestsAsync(
                        upstream,
                        upgradeBranchPrefix,
                        _auth.User);

                    if (pullRequestToUpdate == null)
                    {
                        Trace.TraceInformation($"No existing pull request found.");
                    }
                    else
                    {
                        Trace.TraceInformation(
                            $"Pull request already exists for {upgradeBranchPrefix} in {upstream.Segments}. " +
                            $"#{pullRequestToUpdate.Number}, '{pullRequestToUpdate.Title}'");

                        string blockedReason = GetUpdateBlockedReason(client, pullRequestToUpdate, upgradeBranchPrefix);

                        if (blockedReason == null)
                        {
                            originBranch = new GitHubBranch(
                                pullRequestToUpdate.Head.Ref,
                                Origin);
                        }
                        else
                        {
                            string comment =
                                $"Couldn't update this pull request: {blockedReason}\n" +
                                $"Would have applied '{commitMessage}'";

                            await client.PostCommentAsync(upstream, pullRequestToUpdate.Number, comment);
                            return;
                        }
                    }
                }

                // No existing branch to update: push to a new one.
                if (originBranch == null)
                {
                    string newBranchName =
                        _namingStrategy.Prefix(UpstreamBranch.Name) +
                        _namingStrategy.CreateFreshBranchNameSuffix(UpstreamBranch.Name);

                    originBranch = new GitHubBranch(newBranchName, Origin);
                }

                PushNewCommit(originBranch, commitMessage);

                if (pullRequestToUpdate != null)
                {
                    await client.UpdateGitHubPullRequestAsync(upstream, pullRequestToUpdate.Number, title, description);
                }
                else
                {
                    await client.PostGitHubPullRequestAsync(title, description, originBranch, UpstreamBranch);
                }
            }
        }
        private string GetUpdateBlockedReason(
            GitHubClient client,
            GitHubPullRequest pullRequest,
            string upgradeBranchPrefix)
        {
            if (pullRequest.Head.User.Login != Origin.Owner)
            {
                return $"Owner of head repo '{pullRequest.Head.User.Login}' is not '{Origin.Owner}'";
            }
            if (!pullRequest.Head.Ref.StartsWith(upgradeBranchPrefix))
            {
                return $"Ref name '{pullRequest.Head.Ref}' does not start with '{upgradeBranchPrefix}'";
            }

            GitCommit commit = client.GetCommitAsync(Origin, pullRequest.Head.Sha).Result;
            if (commit.Author.Name != GitAuthorName)
            {
                return $"Head commit author '{commit.Author.Name}' is not '{GitAuthorName}'";
            }
            if (commit.Committer.Name != GitAuthorName)
            {
                return $"Head commit committer '{commit.Committer.Name}' is not '{GitAuthorName}'";
            }
            return null;
        }