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
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
using Microsoft.Build.FileSystem;
using Microsoft.Build.Graph;
using Microsoft.Extensions.Logging;

namespace ProjectDiff.Core.Entrypoints;

public sealed class DirectoryScanEntrypointProvider : IEntrypointProvider
{
private readonly string _directory;
private readonly ILogger<DirectoryScanEntrypointProvider> _logger;

public DirectoryScanEntrypointProvider(string directory)
public DirectoryScanEntrypointProvider(string directory, ILogger<DirectoryScanEntrypointProvider> logger)
{
_directory = directory;
_logger = logger;
}

public Task<IEnumerable<ProjectGraphEntryPoint>> GetEntrypoints(
MSBuildFileSystemBase fs,
CancellationToken cancellationToken
)
{
_logger.LogDebug("Scanning directory '{Directory}' for project files", _directory);
var entrypoints = fs.EnumerateFiles(_directory, "*.csproj", SearchOption.AllDirectories)
.Select(it => new ProjectGraphEntryPoint(it));

Expand Down
19 changes: 17 additions & 2 deletions src/ProjectDiff.Core/Entrypoints/SolutionEntrypointProvider.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using Microsoft.Build.FileSystem;
using Microsoft.Build.Graph;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.SolutionPersistence.Serializer;

namespace ProjectDiff.Core.Entrypoints;

public sealed class SolutionEntrypointProvider : IEntrypointProvider
{
private readonly FileInfo _solution;
private readonly ILogger<SolutionEntrypointProvider> _logger;

public SolutionEntrypointProvider(FileInfo solution)
public SolutionEntrypointProvider(FileInfo solution, ILogger<SolutionEntrypointProvider> logger)
{
_solution = solution;
_logger = logger;
}

public async Task<IEnumerable<ProjectGraphEntryPoint>> GetEntrypoints(
Expand All @@ -30,7 +33,7 @@ CancellationToken cancellationToken
}


private static async Task<IEnumerable<ProjectGraphEntryPoint>> GetProjectEntrypoints(
private async Task<IEnumerable<ProjectGraphEntryPoint>> GetProjectEntrypoints(
FileInfo solutionFile,
Stream stream,
CancellationToken cancellationToken
Expand All @@ -40,17 +43,29 @@ CancellationToken cancellationToken
{
case ".sln":
{
_logger.LogDebug("Reading {SolutionFile} as a .sln file", solutionFile.FullName);
var solutionModel = await SolutionSerializers.SlnFileV12.OpenAsync(stream, cancellationToken);

_logger.LogDebug(
"Found {ProjectCount} projects in solution {SolutionFile}",
solutionModel.SolutionProjects.Count,
solutionFile.FullName
);
return solutionModel.SolutionProjects
.Select(it =>
new ProjectGraphEntryPoint(Path.GetFullPath(it.FilePath, solutionFile.DirectoryName!))
);
}
case ".slnx":
{
_logger.LogDebug("Reading {SolutionFile} as a .slnx file", solutionFile.FullName);
var solutionModel = await SolutionSerializers.SlnXml.OpenAsync(stream, cancellationToken);

_logger.LogDebug(
"Found {ProjectCount} projects in solution {SolutionFile}",
solutionModel.SolutionProjects.Count,
solutionFile.FullName
);
return solutionModel.SolutionProjects
.Select(it =>
new ProjectGraphEntryPoint(Path.GetFullPath(it.FilePath, solutionFile.DirectoryName!))
Expand Down
28 changes: 19 additions & 9 deletions src/ProjectDiff.Core/GitTreeFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Build.Definition;
using Microsoft.Build.Evaluation;
using Microsoft.Build.FileSystem;
using Microsoft.Extensions.Logging;

namespace ProjectDiff.Core;

Expand All @@ -14,21 +15,24 @@ public sealed class GitTreeFileSystem : MSBuildFileSystemBase
private readonly Tree _tree;
private readonly ProjectCollection _projectCollection;
private readonly Dictionary<string, string> _globalProperties;
private readonly ILogger<GitTreeFileSystem> _logger;

public GitTreeFileSystem(
Repository repository,
Tree tree,
ProjectCollection projectCollection,
Dictionary<string, string> globalProperties
Dictionary<string, string> globalProperties,
ILogger<GitTreeFileSystem> logger
)
{
_repository = repository;
_tree = tree;
_projectCollection = projectCollection;
_globalProperties = globalProperties;
_logger = logger;
}

public bool LazyLoadProjects { get; set; } = true;
public bool EagerLoadProjects { get; set; }

public override TextReader ReadFile(string path)
{
Expand All @@ -37,7 +41,7 @@ public override TextReader ReadFile(string path)
return base.ReadFile(path);
}

using var stream = GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var stream = GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
return new StreamReader(stream);
}

Expand All @@ -50,12 +54,12 @@ public override Stream GetFileStream(string path, FileMode mode, FileAccess acce

if (mode != FileMode.Open)
{
throw new ArgumentException("Only reading files is supported", nameof(mode));
throw new NotSupportedException("Mode must be FileMode.Open");
}

if (access != FileAccess.Read)
{
throw new ArgumentException("Only reading files is supported", nameof(access));
throw new NotSupportedException("Access mode must be FileAccess.Read");
}

var relativePath = RelativePath(path);
Expand Down Expand Up @@ -152,7 +156,8 @@ public override bool DirectoryExists(string path)
if (!ShouldUseTree(path))
return base.DirectoryExists(path);

var entry = _tree[RelativePath(path)];
var relativePath = RelativePath(path);
var entry = _tree[relativePath];

return entry is { TargetType: TreeEntryTargetType.Tree };
}
Expand All @@ -175,14 +180,15 @@ public override bool FileExists(string path)
return false;
}

if (LazyLoadProjects && IsProject(entry))
if (EagerLoadProjects && IsProject(entry))
{
// HACK: Since Imports doesn't use the file system we have to manually load the projects
// whenever msbuild tries to load them.
lock (_projectLoadLock)
{
if (_projectCollection.GetLoadedProjects(path).Count == 0)
{
_logger.LogDebug("Eagerly loading project from path '{Path}'", path);
LoadProject(path, _globalProperties, _projectCollection);
}
}
Expand Down Expand Up @@ -230,6 +236,7 @@ ProjectCollection projects
throw new NotImplementedException();
}

_logger.LogInformation("Loading project from path '{Path}'", path);
var relativePath = RelativePath(path);

var entry = _tree[relativePath];
Expand All @@ -239,13 +246,13 @@ ProjectCollection projects
throw new InvalidOperationException("Tried loading a project that is not a blob");
}

var blob = (Blob)entry.Target;
var blob = entry.Target.Peel<Blob>();

using var xml = new XmlTextReader(new StringReader(blob.GetContentText()));
var projectRootElement = ProjectRootElement.Create(xml, projects);
projectRootElement.FullPath = path;

return Project.FromProjectRootElement(
var project = Project.FromProjectRootElement(
projectRootElement,
new ProjectOptions
{
Expand All @@ -254,6 +261,9 @@ ProjectCollection projects
LoadSettings = ProjectLoadSettings.Default | ProjectLoadSettings.RecordDuplicateButNotCircularImports
}
);

_logger.LogDebug("Loaded project '{ProjectName}' from git tree '{Tree}'", project.FullPath, _tree.Sha);
return project;
}


Expand Down
1 change: 1 addition & 0 deletions src/ProjectDiff.Core/ProjectDiff.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="LibGit2Sharp" Version="0.31.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
79 changes: 56 additions & 23 deletions src/ProjectDiff.Core/ProjectDiffExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
using System.Collections.Frozen;
using LibGit2Sharp;
using Microsoft.Build.Graph;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ProjectDiff.Core.Entrypoints;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;

namespace ProjectDiff.Core;

public class ProjectDiffExecutor
{
private readonly ProjectDiffExecutorOptions _options;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<ProjectDiffExecutor> _logger;

public ProjectDiffExecutor(ProjectDiffExecutorOptions options)
public ProjectDiffExecutor(ProjectDiffExecutorOptions options, ILoggerFactory? loggerFactory = null)
{
_options = options;
}

public async Task<ProjectDiffResult> GetProjectDiff(
FileInfo solutionFile,
string baseCommitRef = "HEAD",
string? headCommitRef = null,
CancellationToken cancellationToken = default
)
{
return await GetProjectDiff(
solutionFile.DirectoryName ?? throw new ArgumentException(
$"{nameof(solutionFile)}.DirectoryName is null",
nameof(solutionFile)
),
new SolutionEntrypointProvider(solutionFile),
baseCommitRef,
headCommitRef,
cancellationToken
);
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
_logger = _loggerFactory.CreateLogger<ProjectDiffExecutor>();
}

public async Task<ProjectDiffResult> GetProjectDiff(
Expand All @@ -41,19 +29,26 @@ public async Task<ProjectDiffResult> GetProjectDiff(
CancellationToken cancellationToken = default
)
{
_logger.LogDebug("Discovering repository from path '{Path}'", path);
var repoPath = Repository.Discover(path);
if (repoPath is null)
{
_logger.LogError("Could not find a Git repository for path '{Path}'", path);
return new ProjectDiffResult
{
Status = ProjectDiffExecutionStatus.RepositoryNotFound
};
}

_logger.LogDebug("Found repository at '{RepoPath}'", repoPath);

using var repo = new Repository(repoPath);

_logger.LogDebug("Looking up base commit '{BaseCommitRef}'", baseCommitRef);
var baseCommit = repo.Lookup<Commit>(baseCommitRef);
if (baseCommit is null)
{
_logger.LogError("Base commit '{BaseCommitRef}' not found in repository", baseCommitRef);
return new ProjectDiffResult
{
Status = ProjectDiffExecutionStatus.BaseCommitNotFound,
Expand All @@ -63,10 +58,12 @@ public async Task<ProjectDiffResult> GetProjectDiff(
Commit? headCommit;
if (headCommitRef is not null)
{
_logger.LogDebug("Looking up head commit '{HeadCommitRef}'", headCommitRef);
headCommit = repo.Lookup<Commit>(headCommitRef);

if (headCommit is null)
{
_logger.LogError("Head commit '{HeadCommitRef}' not found in repository", headCommitRef);
return new ProjectDiffResult
{
Status = ProjectDiffExecutionStatus.HeadCommitNotFound
Expand All @@ -75,29 +72,56 @@ public async Task<ProjectDiffResult> GetProjectDiff(
}
else
{
_logger.LogDebug("No head commit specified, using working directory state");
headCommit = null;
}

if (_options.FindMergeBase)
{
_logger.LogDebug(
"Finding merge base between base commit '{BaseCommitRef}' and head commit '{HeadCommitRef}'",
baseCommitRef,
headCommitRef
);
var mergeBaseCommit = repo.ObjectDatabase.FindMergeBase(baseCommit, headCommit ?? repo.Head.Tip);
if (mergeBaseCommit is null)
{
_logger.LogError(
"Could not find merge base between base commit '{BaseCommitRef}' and head commit '{HeadCommitRef}'",
baseCommitRef,
headCommitRef
);
return new ProjectDiffResult
{
Status = ProjectDiffExecutionStatus.MergeBaseNotFound
};
}

_logger.LogDebug(
"Found merge base commit '{MergeBaseCommit}'",
mergeBaseCommit.Sha
);

baseCommit = mergeBaseCommit;
}

_logger.LogInformation(
"Finding changed files between commits '{BaseCommitRef}' and '{HeadCommitRef}'",
baseCommitRef,
headCommitRef ?? "working directory"
);
var changedFiles = GetGitModifiedFiles(repo, baseCommit, headCommit)
.Where(ShouldIncludeFile)
.ToFrozenSet();


if (changedFiles.Count == 0)
{
_logger.LogInformation(
"No changed files found between commits '{BaseCommitRef}' and '{HeadCommitRef}'",
baseCommitRef,
headCommitRef ?? "working directory"
);
return new ProjectDiffResult
{
Status = ProjectDiffExecutionStatus.Success,
Expand All @@ -106,7 +130,16 @@ public async Task<ProjectDiffResult> GetProjectDiff(
};
}

var fromGraph = await ProjectGraphFactory.BuildForGitTree(

_logger.LogInformation("Found {NumChangedFiles} changed files", changedFiles.Count);
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Found changed files: {ChangedFiles}", changedFiles);
}

var projectGraphFactory = new ProjectGraphFactory(_loggerFactory);

var fromGraph = await projectGraphFactory.BuildForGitTree(
repo,
baseCommit.Tree,
entrypointProvider,
Expand All @@ -116,14 +149,14 @@ public async Task<ProjectDiffResult> GetProjectDiff(
ProjectGraph toGraph;
if (headCommit is null)
{
toGraph = await ProjectGraphFactory.BuildForWorkingDirectory(
toGraph = await projectGraphFactory.BuildForWorkingDirectory(
entrypointProvider,
cancellationToken
);
}
else
{
toGraph = await ProjectGraphFactory.BuildForGitTree(
toGraph = await projectGraphFactory.BuildForGitTree(
repo,
headCommit.Tree,
entrypointProvider,
Expand Down
Loading
Loading