Skip to content
Merged
Show file tree
Hide file tree
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
106 changes: 84 additions & 22 deletions _atom/RepoUtils/IApiSurfaceHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ namespace Atom.RepoUtils;

internal sealed record BreakingChanges(IReadOnlyList<Change> MajorChanges, IReadOnlyList<Change> MinorChanges);

internal sealed record Change(RootedPath Path, List<Line> AddedLines, List<Line> DeletedLines);
internal sealed record Change(RootedPath Path, List<string> AddedLines, List<string> DeletedLines);

internal interface IApiSurfaceHelper : IBuildAccessor
{
BreakingChanges IdentifyBreakingChanges(
async Task<BreakingChanges> IdentifyBreakingChanges(
SemVer oldVersion,
string oldCommitHash,
SemVer newVersion,
string newCommitHash,
CancellationToken cancellationToken,
params RootedPath[] filesToCheck)
{
var filesToCheckDisplay = string.Join(", ", filesToCheck);
Expand All @@ -26,34 +27,41 @@ BreakingChanges IdentifyBreakingChanges(
});

var targetFiles = FormatTargetFiles(filesToCheck);

using var repo = new Repository(RootedFileSystem.AtomRootDirectory);
var oldCommit = repo.Lookup<Commit>(oldCommitHash);

if (oldCommit?.IsMissing is not false)
throw new InvalidOperationException($"Commit {oldCommitHash} is missing.");

var newCommit = repo.Lookup<Commit>(newCommitHash);

if (newCommit?.IsMissing is not false)
throw new InvalidOperationException($"Commit {newCommitHash} is missing.");

var changes = repo.Diff.Compare<Patch>(oldCommit.Tree, newCommit.Tree);
var arguments = new List<string>
{
"-c",
"core.quotePath=false",
"diff",
"--unified=0",
"--no-color",
"--no-ext-diff",
oldCommitHash,
newCommitHash,
"--",
};
arguments.AddRange(targetFiles);

var result = await ProcessRunner.RunAsync(new("git", [.. arguments])
{
WorkingDirectory = RootedFileSystem.AtomRootDirectory,
OutputLogLevel = LogLevel.Debug,
},
cancellationToken);
var changes = ParseChanges(result.Output, targetFiles);

Logger.LogDebug("Changes: {@Changes}",
new
{
changes.Content,
changes.LinesDeleted,
changes.LinesAdded,
Content = result.Output,
LinesDeleted = changes.Sum(x => x.DeletedLines.Count),
LinesAdded = changes.Sum(x => x.AddedLines.Count),
});

if (changes is null or { LinesAdded: 0, LinesDeleted: 0 })
if (changes.Count is 0)
return new([], []);

IReadOnlyList<Change> suspiciousChanges = changes
.Where(x => targetFiles.Contains(x.Path) && x.LinesDeleted > 0)
.Select(x => new Change(RootedFileSystem.AtomRootDirectory / x.Path, x.AddedLines, x.DeletedLines))
.Where(x => x.DeletedLines.Count > 0)
.ToList();

Logger.LogDebug("Suspicious changes: {@SuspiciousChanges}", suspiciousChanges);
Expand All @@ -62,7 +70,7 @@ BreakingChanges IdentifyBreakingChanges(
.Where(x => x.DeletedLines.Count > 0 &&
x
.DeletedLines
.Select(l => l.Content.Trim())
.Select(line => line.Trim())
.All(deletedLine => !deletedLine.StartsWith(',') && !deletedLine.EndsWith(',')))
.ToList();

Expand All @@ -78,6 +86,60 @@ BreakingChanges IdentifyBreakingChanges(
return new(majorChanges, minorChanges);
}

private List<Change> ParseChanges(string patch, HashSet<string> targetFiles)
{
var changes = new List<Change>();
Change? currentChange = null;
string? oldPath = null;

using var reader = new StringReader(patch);

while (reader.ReadLine() is { } line)
{
if (line.StartsWith("diff --git ", StringComparison.Ordinal))
{
currentChange = null;
oldPath = null;
}
else if (line.StartsWith("--- ", StringComparison.Ordinal))
{
oldPath = ParseDiffPath(line[4..]);
}
else if (line.StartsWith("+++ ", StringComparison.Ordinal))
{
var newPath = ParseDiffPath(line[4..]);
var path = newPath ?? oldPath;

if (path is not null && targetFiles.Contains(path))
{
currentChange = new(RootedFileSystem.AtomRootDirectory / path, [], []);
changes.Add(currentChange);
}
}
else if (currentChange is not null && line.StartsWith('+'))
{
currentChange.AddedLines.Add(line[1..]);
}
else if (currentChange is not null && line.StartsWith('-'))
{
currentChange.DeletedLines.Add(line[1..]);
}
}

return changes;
}

private static string? ParseDiffPath(string value)
{
if (value is "/dev/null")
return null;

return value.StartsWith("a/", StringComparison.Ordinal) ||
value.StartsWith("b/", StringComparison.Ordinal)
? value[2..]
: value;
}

private HashSet<string> FormatTargetFiles(RootedPath[] filesToCheck)
{
var targetFiles = filesToCheck
Expand Down
50 changes: 33 additions & 17 deletions _atom/RepoUtils/ICheckPrForBreakingChanges.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ internal interface ICheckPrForBreakingChanges : IGithubHelper, IPullRequestHelpe
var owner = Github.Variables.RepositoryOwner;
Logger.LogDebug("Target repository owner: {Owner}", owner);

using var repo = new Repository(RootedFileSystem.AtomRootDirectory);

var currentCommitHash = repo.Head.Tip.Sha;
var currentCommitResult = await ProcessRunner.RunAsync(new("git", "rev-parse HEAD")
{
WorkingDirectory = RootedFileSystem.AtomRootDirectory,
OutputLogLevel = LogLevel.Debug,
},
cancellationToken);
var currentCommitHash = currentCommitResult.Output.Trim();
Logger.LogDebug("Current commit hash: {CommitHash}", currentCommitHash);

var currentVersion = BuildVersion;
Logger.LogDebug("Current version: {Version}", currentVersion);

var latestReleaseInfo = FindLatestReleaseInfo(repo, currentVersion);
var latestReleaseInfo = await FindLatestReleaseInfo(currentVersion, cancellationToken);
Logger.LogDebug("Latest release info: {ReleaseInfo}", latestReleaseInfo);

if (latestReleaseInfo is null)
Expand All @@ -43,10 +47,11 @@ internal interface ICheckPrForBreakingChanges : IGithubHelper, IPullRequestHelpe
currentVersion,
latestReleaseInfo.Version);

var breakingChanges = IdentifyBreakingChanges(latestReleaseInfo.Version,
var breakingChanges = await IdentifyBreakingChanges(latestReleaseInfo.Version,
latestReleaseInfo.CommitHash,
currentVersion,
currentCommitHash,
cancellationToken,
FilesToCheck);

Logger.LogInformation(
Expand Down Expand Up @@ -136,23 +141,30 @@ await AddCheckStatus(owner,
cancellationToken);
});

ReleaseInfo? FindLatestReleaseInfo(Repository repo, SemVer currentVersion)
private async Task<ReleaseInfo?> FindLatestReleaseInfo(
SemVer currentVersion,
CancellationToken cancellationToken)
{
var releaseVersions = repo
.Tags
.Select(x => new
var tagsResult = await ProcessRunner.RunAsync(new("git", ["tag", "--list", "v*"])
{
Tag = x,
Version = !x.FriendlyName.StartsWith('v')
? null
: !SemVer.TryParse(x.FriendlyName[1..], out var version)
WorkingDirectory = RootedFileSystem.AtomRootDirectory,
OutputLogLevel = LogLevel.Debug,
},
cancellationToken);
var releaseVersions = tagsResult
.Output
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(tag => new
{
Tag = tag,
Version = !SemVer.TryParse(tag[1..], out var version)
? null
: version,
})
Comment thread
DecSmith42 marked this conversation as resolved.
.Where(x => x.Version is not null && x.Version < currentVersion)
.Select(x => new
{
Tag = x.Tag!,
x.Tag,
Version = x.Version!,
})
.ToList();
Expand All @@ -165,8 +177,14 @@ await AddCheckStatus(owner,
}

var version = releaseVersions.MaxBy(x => x.Version)!;
var commitResult = await ProcessRunner.RunAsync(new("git", ["rev-list", "-n", "1", version.Tag])
{
WorkingDirectory = RootedFileSystem.AtomRootDirectory,
OutputLogLevel = LogLevel.Debug,
},
cancellationToken);

return new(version.Tag.Target.Sha, version.Version);
return new(commitResult.Output.Trim(), version.Version);
}

private async Task AddCheckStatus(
Expand Down Expand Up @@ -213,8 +231,6 @@ private async Task AddCheckStatus(
if (prQueryResult.Id.Value is null)
throw new StepFailedException("Could not find pull request.");

using var repo = new Repository(RootedFileSystem.AtomRootDirectory);

var checkRunMutation = new Mutation()
.CreateCheckRun(new CreateCheckRunInput
{
Expand Down
1 change: 0 additions & 1 deletion _atom/_atom.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="LibGit2Sharp" Version="0.31.0" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="10.0.10" />
<PackageReference Include="Octokit.GraphQL" Version="0.4.0-beta" />
<PackageReference Include="Roslynator.Analyzers" Version="4.15.0">
Expand Down
3 changes: 0 additions & 3 deletions _atom/_usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@
global using Invex.StructuredText.Expressions;
global using Invex.StructuredText.GithubActions.DependabotConfigModel.Model;
global using Invex.StructuredText.GithubActions.GithubActionModel;
global using LibGit2Sharp;
global using Microsoft.Extensions.Logging;
global using Octokit.GraphQL;
global using Octokit.GraphQL.Internal;
global using Octokit.GraphQL.Model;
global using Commit = LibGit2Sharp.Commit;
global using Repository = LibGit2Sharp.Repository;