Skip to content
Open
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
16 changes: 16 additions & 0 deletions src/Fallout.Migrate/Common/IMigrationStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Fallout.Migrate.Common;

/// <summary>
/// One unit of work performed against a repo during <c>fallout-migrate</c>. Add a new step by
/// implementing this interface and registering it in <see cref="Migration"/>'s step list.
/// </summary>
internal interface IMigrationStep
{
/// <summary>
/// Performs this step's work against <paramref name="context"/>, recording the outcome in
/// <paramref name="summary"/>.
/// </summary>
/// <param name="context">The current migration context.</param>
/// <param name="summary">The summary to update with files changed, edits made, or warnings.</param>
void Execute(MigrationContext context, Summary summary);
}
29 changes: 29 additions & 0 deletions src/Fallout.Migrate/Common/MigrationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.IO;
using Fallout.Common.IO;

namespace Fallout.Migrate.Common;

/// <summary>
/// Plain data carried between <see cref="Migration"/> and each <see cref="IMigrationStep"/>.
/// Holds no behavior itself; see <see cref="MigrationFileOperations"/> for the shared
/// file-walking / rewrite-application helpers steps call into.
/// </summary>
internal sealed class MigrationContext(AbsolutePath rootDirectory, bool dryRun, TextWriter log)
{
/// <summary>The repository root being migrated.</summary>
public AbsolutePath RootDirectory { get; } = rootDirectory ?? throw new ArgumentNullException(nameof(rootDirectory));

/// <summary>When <c>true</c>, steps must report intended changes without writing them.</summary>
public bool DryRun { get; } = dryRun;

/// <summary>The writer steps use to report progress.</summary>
public TextWriter Log { get; } = log ?? throw new ArgumentNullException(nameof(log));

/// <summary>
/// The Fallout version to pin in rewritten package references.
/// Set by <see cref="Fallout.Migrate.Steps.ResolveFalloutVersionStep"/>, which always runs first;
/// subsequent steps read it.
/// </summary>
public string FalloutVersion { get; internal set; }
}
100 changes: 100 additions & 0 deletions src/Fallout.Migrate/Common/MigrationFileOperations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.IO;
using Fallout.Common.IO;

namespace Fallout.Migrate.Common;

/// <summary>
/// Shared file-walking and rewrite-application helpers used by multiple <see cref="IMigrationStep"/>
/// implementations. Kept as static, dependency-free functions so steps stay small and independent.
/// </summary>
internal static class MigrationFileOperations
{
/// <summary>
/// Recursively enumerates files under <paramref name="rootDirectory"/> matching
/// <paramref name="pattern"/>, skipping <c>bin/</c>, <c>obj/</c>, and <c>.git/</c>.
/// </summary>
/// <param name="rootDirectory">The directory to search from.</param>
/// <param name="pattern">A file-name glob pattern, e.g. <c>*.csproj</c>.</param>
/// <returns>The matching, non-ignored files.</returns>
public static IEnumerable<AbsolutePath> EnumerateFiles(AbsolutePath rootDirectory, string pattern)
{
foreach (var file in rootDirectory.GetFiles(pattern, depth: int.MaxValue))
{
if (IsIgnored(file))
{
continue;
}

yield return file;
}
}

/// <summary>
/// Reads <paramref name="path"/>, applies <paramref name="rewriter"/>, logs and records the edit,
/// and writes the result back unless <see cref="MigrationContext.DryRun"/> is set.
/// </summary>
/// <param name="context">The current migration context.</param>
/// <param name="path">The file to rewrite.</param>
/// <param name="rewriter">The rewrite function to apply to the file's content.</param>
/// <param name="summary">The summary to update with the outcome.</param>
public static void ApplyRewrite(
MigrationContext context,
AbsolutePath path,
Func<string, RewriteResult> rewriter,
Summary summary)
{
string original;
try
{
original = path.ReadAllText();
}
catch (IOException ex)
{
summary.Warnings.Add($"could not read {RelativePath(context.RootDirectory, path)}: {ex.Message}");
return;
}

var result = rewriter(original);
if (result.EditCount == 0)
{
return;
}

context.Log.WriteLine(
$"edit {RelativePath(context.RootDirectory, path)} ({result.EditCount} change{(result.EditCount == 1 ? "" : "s")})");

summary.FilesChanged++;
summary.EditCount += result.EditCount;

if (!context.DryRun)
{
// eofLineBreak: false preserves today's exact byte-for-byte write behavior;
// AbsolutePath.WriteAllText's default normalizes trailing line endings, which
// would otherwise sneak unrelated diff noise into migrated consumer repos.
path.WriteAllText(result.Content, eofLineBreak: false);
}
}

/// <summary>
/// Formats <paramref name="absolute"/> as a path relative to <paramref name="rootDirectory"/>,
/// using forward slashes, for log output.
/// </summary>
/// <param name="rootDirectory">The directory to make the path relative to.</param>
/// <param name="absolute">The path to format.</param>
public static string RelativePath(AbsolutePath rootDirectory, AbsolutePath absolute) =>
rootDirectory.GetUnixRelativePathTo(absolute);

/// <summary>
/// Returns <c>true</c> if <paramref name="path"/> sits under a <c>bin/</c>, <c>obj/</c>, or
/// <c>.git/</c> directory and should be skipped by <see cref="EnumerateFiles"/>.
/// </summary>
private static bool IsIgnored(AbsolutePath path)
{
string text = path;
return text.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|| text.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|| text.Contains($"{Path.DirectorySeparatorChar}.git{Path.DirectorySeparatorChar}", StringComparison.Ordinal);
}
}
9 changes: 9 additions & 0 deletions src/Fallout.Migrate/Common/RewriteResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Fallout.Migrate.Common;

/// <summary>
/// The result of rewriting a file's content: the new <paramref name="Content"/> and how many
/// individual edits were made.
/// </summary>
/// <param name="Content">The rewritten file content.</param>
/// <param name="EditCount">The number of edits applied; zero means the content is unchanged.</param>
internal readonly record struct RewriteResult(string Content, int EditCount);
22 changes: 22 additions & 0 deletions src/Fallout.Migrate/Common/Summary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Generic;

namespace Fallout.Migrate.Common;

/// <summary>
/// Aggregate result of a migration run: how many files changed, how many edits were made, how many
/// directories were renamed, and any warnings that need manual follow-up.
/// </summary>
internal sealed class Summary
{
/// <summary>The number of files that had at least one edit applied.</summary>
public int FilesChanged { get; set; }

/// <summary>The total number of individual edits made across all rewritten files.</summary>
public int EditCount { get; set; }

/// <summary>The number of directories renamed (currently just <c>.nuke/</c> → <c>.fallout/</c>).</summary>
public int DirectoriesRenamed { get; set; }

/// <summary>Human-readable warnings about conditions the migration could not resolve automatically.</summary>
public List<string> Warnings { get; } = new();
}
34 changes: 31 additions & 3 deletions src/Fallout.Migrate/MigrateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
using System.IO;
using Fallout.Common;
using Fallout.Common.IO;
using Fallout.Migrate.Common;
using JetBrains.Annotations;
using Spectre.Console.Cli;

namespace Fallout.Migrate;

/// <summary>
/// The <c>fallout-migrate</c> CLI command. See the <see cref="DescriptionAttribute"/> below for the
/// user-facing summary of what the migration does.
/// </summary>
[Description("""
Migrate a NUKE consumer repo to Fallout.

Expand All @@ -22,6 +27,12 @@ in build.cmd / build.ps1 / build.sh
[UsedImplicitly]
internal sealed class MigrateCommand : Command<MigrateSettings>
{
/// <summary>
/// Resolves the repository root, runs the migration, and prints a summary.
/// </summary>
/// <param name="context">The Spectre.Console.Cli command context (unused).</param>
/// <param name="settings">The parsed <see cref="MigrateSettings"/> for this invocation.</param>
/// <returns>0 on success, 1 if the repository root could not be resolved.</returns>
public override int Execute(CommandContext context, MigrateSettings settings)
{
var rootDirectory = ResolveRootDirectory(settings.Path);
Expand All @@ -37,12 +48,18 @@ public override int Execute(CommandContext context, MigrateSettings settings)
PrintBanner(rootDirectory, settings.DryRun);

var migration = new Migration(rootDirectory, settings.DryRun, Console.Out);
var summary = migration.Run();
Summary summary = migration.Run();

PrintSummary(summary, settings.DryRun);
return 0;
}

/// <summary>
/// Resolves the repository root to migrate: the explicit <paramref name="explicitArg"/> if given,
/// otherwise the nearest ancestor of the working directory that looks like a Fallout/NUKE build repo.
/// </summary>
/// <param name="explicitArg">The path argument passed on the command line, or <c>null</c>.</param>
/// <returns>The resolved repository root, or <c>null</c> if none could be found.</returns>
private static AbsolutePath ResolveRootDirectory(string explicitArg)
{
if (explicitArg != null)
Expand All @@ -59,6 +76,11 @@ private static AbsolutePath ResolveRootDirectory(string explicitArg)
(current / "build.sh").FileExists());
}

/// <summary>
/// Prints the tool banner and, when applicable, the dry-run notice.
/// </summary>
/// <param name="rootDirectory">The repository root being migrated.</param>
/// <param name="dryRun">Whether the migration is running in dry-run mode.</param>
private static void PrintBanner(AbsolutePath rootDirectory, bool dryRun)
{
Console.WriteLine($"fallout-migrate — migrating: {rootDirectory}");
Expand All @@ -70,12 +92,18 @@ private static void PrintBanner(AbsolutePath rootDirectory, bool dryRun)
Console.WriteLine();
}

private static void PrintSummary(Migration.Summary summary, bool dryRun)
/// <summary>
/// Prints the migration <see cref="Summary"/> (counts, warnings, and next-steps guidance) to the console.
/// </summary>
/// <param name="summary">The result of <see cref="Migration.Run"/>.</param>
/// <param name="dryRun">Whether the migration ran in dry-run mode.</param>
private static void PrintSummary(Summary summary, bool dryRun)
{
Console.WriteLine();
Console.WriteLine($"Files changed: {summary.FilesChanged}");
Console.WriteLine($"Edits made: {summary.EditCount}");
Console.WriteLine($"Directories: {summary.DirectoriesRenamed} renamed");

if (summary.Warnings.Count > 0)
{
Console.WriteLine();
Expand All @@ -89,7 +117,7 @@ private static void PrintSummary(Migration.Summary summary, bool dryRun)
Console.WriteLine();
Console.WriteLine(dryRun
? "Dry-run complete. Re-run without --dry-run to apply changes."
: "Migration complete. Verify the build: ./build.ps1 (or ./build.sh on unix)");
: "Migration complete. Verify the build: ./build.ps1 (or ./build.sh on UNIX)");

Console.WriteLine("Migration guide: https://fallout.build (see #37 for the full guide)");
}
Expand Down
10 changes: 10 additions & 0 deletions src/Fallout.Migrate/MigrateSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@

namespace Fallout.Migrate;

/// <summary>
/// Command-line arguments accepted by <see cref="MigrateCommand"/>.
/// </summary>
[UsedImplicitly]
internal sealed class MigrateSettings : CommandSettings
{
/// <summary>
/// The repository root to migrate. When <c>null</c>, <see cref="MigrateCommand"/> resolves it by
/// walking up from the working directory.
/// </summary>
[CommandArgument(0, "[path]")]
[Description("Repository root. Defaults to walking up from the working directory to find " +
"one (looking for build.cmd / build.ps1 / build.sh, .nuke/, or build/).")]
public string Path { get; init; }

/// <summary>
/// When <c>true</c>, reports the changes <see cref="Migration"/> would make without writing them.
/// </summary>
[CommandOption("-n|--dry-run")]
[Description("Show what would change without writing.")]
public bool DryRun { get; init; }
Expand Down
Loading
Loading