Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 111 additions & 3 deletions eng/update-dependencies/SpecificCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,15 @@ await prCreator.CreateOrUpdateAsync(
}
else
{
UpdateExistingGitHubPullRequest(gitHubAuth, prOptions, commitMessage, upstreamBranch);
await UpdateExistingGitHubPullRequestAsync(
client, upstreamProject, pullRequestToUpdate.Number,
gitHubAuth, prOptions, commitMessage, upstreamBranch);
}
}
}

private static void UpdateExistingGitHubPullRequest(
private static async Task UpdateExistingGitHubPullRequestAsync(
GitHubClient client, GitHubProject upstreamProject, int pullRequestNumber,
GitHubAuth gitHubAuth, PullRequestOptions prOptions, string commitMessage, GitHubBranch upstreamBranch)
{
// PullRequestCreator ends up force-pushing updates to an existing PR which is not great when the logic
Expand Down Expand Up @@ -308,13 +311,46 @@ private static void UpdateExistingGitHubPullRequest(
// Clone the PR's repo/branch to a temp location
Repository.Clone($"https://github.com/{gitHubAuth.User}/{Options.GitHubProject}", tempRepoPath, cloneOptions);

using Repository repo = new(tempRepoPath);

// Add upstream remote and fetch the target branch so we can find the merge base
string upstreamUrl = $"https://github.com/{Options.GitHubUpstreamOwner}/{Options.GitHubProject}";
repo.Network.Remotes.Add("upstream", upstreamUrl);
Commands.Fetch(repo, "upstream", [$"refs/heads/{upstreamBranch.Name}:refs/remotes/upstream/{upstreamBranch.Name}"], null, null);

// Check for non-bot commits on the PR branch. If someone other than the bot
// has pushed commits, we should not overwrite them.
List<string> nonBotCommits = GetNonBotCommits(repo, upstreamBranch.Name);
if (nonBotCommits.Count > 0)
{
Trace.WriteLine($"Found {nonBotCommits.Count} non-bot commit(s) on the PR branch - skipping update.");

string localCommand = BuildLocalUpdateCommand();
string nonBotCommitList = string.Join("\n", nonBotCommits.Select(c => $"- {c}"));
string comment = $"""
Automatic update skipped because additional commits were detected on this branch:

{nonBotCommitList}

To apply this update manually, run:

```
{localCommand}
```

To allow automatic updates to resume, merge or close this PR.
""";

await client.PostCommentAsync(upstreamProject, pullRequestNumber, comment);
return;
}

// Remove all existing directories and files from the temp repo
ClearRepoContents(tempRepoPath);

// Copy contents of local repo changes to temp repo
DirectoryCopy(".", tempRepoPath);

using Repository repo = new(tempRepoPath);
RepositoryStatus status = repo.RetrieveStatus(new StatusOptions());

// If there are any changes from what exists in the PR
Expand Down Expand Up @@ -355,6 +391,78 @@ private static void UpdateExistingGitHubPullRequest(
}
}

/// <summary>
/// Finds commits on the current branch that were not authored by the bot user.
/// Walks the commit log from HEAD back to the merge base with the target branch.
/// </summary>
private static List<string> GetNonBotCommits(Repository repo, string targetBranchName)
{
List<string> nonBotCommits = [];

Branch? targetBranch = repo.Branches[$"upstream/{targetBranchName}"];
if (targetBranch is null)
{
Trace.WriteLine($"Could not find remote tracking branch 'upstream/{targetBranchName}' - skipping non-bot commit check.");
return nonBotCommits;
}

Commit headCommit = repo.Head.Tip;
Commit targetCommit = targetBranch.Tip;

Commit? mergeBase = repo.ObjectDatabase.FindMergeBase(headCommit, targetCommit);
if (mergeBase is null)
{
Trace.WriteLine("Could not find merge base - skipping non-bot commit check.");
return nonBotCommits;
}

foreach (Commit commit in repo.Commits)
{
if (commit.Sha == mergeBase.Sha)
{
break;
}

if (!string.Equals(commit.Author.Name, Options.User, StringComparison.OrdinalIgnoreCase))
{
nonBotCommits.Add($"`{commit.Sha[..7]}` {commit.MessageShort} (by {commit.Author.Name})");
}
}

return nonBotCommits;
}

/// <summary>
/// Builds a CLI command string that can be run locally to apply the same dependency update.
/// Uses the <c>specific</c> subcommand with already-resolved versions, so no BAR auth is needed.
/// </summary>
private static string BuildLocalUpdateCommand()
{
List<string> parts =
[
"dotnet run --project eng/update-dependencies/update-dependencies.csproj --",
"specific",
Options.DockerfileVersion,
];

foreach (var (name, version) in Options.ProductVersions)
{
parts.Add($"--product-version {name}={version}");
}

foreach (string tool in Options.Tools)
{
parts.Add($"--tool {tool}");
}

if (!string.IsNullOrEmpty(Options.VersionSourceName))
{
parts.Add($"--version-source-name '{Options.VersionSourceName}'");
}

return string.Join(" ", parts);
}

private static void DeleteRepoDirectory(string repoPath)
{
if (Directory.Exists(repoPath))
Expand Down