Skip to content

Commit 49e89ab

Browse files
CopilotAArnott
andcommitted
Add --what-if option to prepare-release command
Co-authored-by: AArnott <[email protected]>
1 parent d6eba4a commit 49e89ab

File tree

2 files changed

+122
-5
lines changed

2 files changed

+122
-5
lines changed

src/NerdBank.GitVersioning/ReleaseManager.cs

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,101 @@ public void PrepareRelease(string projectDirectory, string releaseUnstableTag =
230230
}
231231
}
232232

233+
/// <summary>
234+
/// Simulates the prepare-release operation and returns the versions that would be set without making any changes.
235+
/// </summary>
236+
/// <param name="projectDirectory">
237+
/// The path to the directory that may (or its ancestors may) define the version file.
238+
/// </param>
239+
/// <param name="releaseUnstableTag">
240+
/// An optional prerelease tag to apply on the release branch.
241+
/// If not specified, any existing prerelease tag will be removed from the release.
242+
/// The preceding hyphen may be omitted.
243+
/// </param>
244+
/// <param name="nextVersion">
245+
/// The version to use for the next release.
246+
/// If not specified, the next version will be determined automatically by incrementing the current
247+
/// version based on the current version and the <paramref name="versionIncrement"/> setting in <c>version.json</c>.
248+
/// Parameter will be ignored if the current branch is a release branch.
249+
/// </param>
250+
/// <param name="versionIncrement">
251+
/// The increment to apply in order to determine the next version on the current branch.
252+
/// If specified, value will be used instead of the increment specified in <c>version.json</c>.
253+
/// Parameter will be ignored if the current branch is a release branch.
254+
/// </param>
255+
/// <param name="outputMode">
256+
/// The output format to use for writing to stdout.
257+
/// </param>
258+
/// <returns>
259+
/// A <see cref="ReleaseInfo"/> object containing information about the simulated release.
260+
/// </returns>
261+
public ReleaseInfo SimulatePrepareRelease(string projectDirectory, string releaseUnstableTag = null, Version nextVersion = null, VersionOptions.ReleaseVersionIncrement? versionIncrement = null, ReleaseManagerOutputMode outputMode = default)
262+
{
263+
Requires.NotNull(projectDirectory, nameof(projectDirectory));
264+
265+
// open the git repository
266+
LibGit2Context context = this.GetRepository(projectDirectory);
267+
Repository repository = context.Repository;
268+
269+
if (repository.Info.IsHeadDetached)
270+
{
271+
this.stderr.WriteLine("Detached head. Check out a branch first.");
272+
throw new ReleasePreparationException(ReleasePreparationError.DetachedHead);
273+
}
274+
275+
// get the current version
276+
VersionOptions versionOptions = context.VersionFile.GetVersion();
277+
if (versionOptions is null)
278+
{
279+
this.stderr.WriteLine($"Failed to load version file for directory '{projectDirectory}'.");
280+
throw new ReleasePreparationException(ReleasePreparationError.NoVersionFile);
281+
}
282+
283+
string releaseBranchName = this.GetReleaseBranchName(versionOptions);
284+
string originalBranchName = repository.Head.FriendlyName;
285+
SemanticVersion releaseVersion = string.IsNullOrEmpty(releaseUnstableTag)
286+
? versionOptions.Version.WithoutPrepreleaseTags()
287+
: versionOptions.Version.SetFirstPrereleaseTag(releaseUnstableTag);
288+
289+
// check if the current branch is the release branch
290+
if (string.Equals(originalBranchName, releaseBranchName, StringComparison.OrdinalIgnoreCase))
291+
{
292+
if (outputMode == ReleaseManagerOutputMode.Text)
293+
{
294+
this.stdout.WriteLine($"What-if: {releaseBranchName} branch would be advanced from {versionOptions.Version} to {releaseVersion}.");
295+
}
296+
297+
return new ReleaseInfo(new ReleaseBranchInfo(releaseBranchName, repository.Head.Tip.Id.ToString(), releaseVersion));
298+
}
299+
300+
SemanticVersion nextDevVersion = this.GetNextDevVersion(versionOptions, nextVersion, versionIncrement);
301+
302+
// check if the current version on the current branch is different from the next version
303+
// otherwise, both the release branch and the dev branch would have the same version
304+
if (versionOptions.Version.Version == nextDevVersion.Version)
305+
{
306+
this.stderr.WriteLine($"Version on '{originalBranchName}' is already set to next version {nextDevVersion.Version}.");
307+
throw new ReleasePreparationException(ReleasePreparationError.NoVersionIncrement);
308+
}
309+
310+
// check if the release branch already exists
311+
if (repository.Branches[releaseBranchName] is not null)
312+
{
313+
this.stderr.WriteLine($"Cannot create branch '{releaseBranchName}' because it already exists.");
314+
throw new ReleasePreparationException(ReleasePreparationError.BranchAlreadyExists);
315+
}
316+
317+
if (outputMode == ReleaseManagerOutputMode.Text)
318+
{
319+
this.stdout.WriteLine($"What-if: {releaseBranchName} branch would track v{releaseVersion} stabilization and release.");
320+
this.stdout.WriteLine($"What-if: {originalBranchName} branch would track v{nextDevVersion} development.");
321+
}
322+
323+
var originalBranchInfo = new ReleaseBranchInfo(originalBranchName, repository.Head.Tip.Sha, nextDevVersion);
324+
var releaseBranchInfo = new ReleaseBranchInfo(releaseBranchName, repository.Head.Tip.Id.ToString(), releaseVersion);
325+
return new ReleaseInfo(originalBranchInfo, releaseBranchInfo);
326+
}
327+
233328
private static bool IsVersionDecrement(SemanticVersion oldVersion, SemanticVersion newVersion)
234329
{
235330
if (newVersion.Version > oldVersion.Version)
@@ -384,7 +479,7 @@ private SemanticVersion GetNextDevVersion(VersionOptions versionOptions, Version
384479
return nextDevVersion.SetFirstPrereleaseTag(versionOptions.ReleaseOrDefault.FirstUnstableTagOrDefault);
385480
}
386481

387-
private void WriteToOutput(ReleaseInfo releaseInfo)
482+
public void WriteToOutput(ReleaseInfo releaseInfo)
388483
{
389484
string json = JsonConvert.SerializeObject(releaseInfo, Formatting.Indented, new SemanticVersionJsonConverter());
390485
this.stdout.WriteLine(json);

src/nbgv/Program.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ private static RootCommand BuildCommandLine()
339339
{
340340
Description = "A custom message to use for the commit that changes the version number. May include {0} for the version number. If not specified, the default is \"Set version to '{0}'\".",
341341
};
342+
var whatIf = new Option<bool>("--what-if", Array.Empty<string>())
343+
{
344+
Description = "Simulates the prepare-release operation and prints the new version that would be set, but does not actually make any changes.",
345+
};
342346
var tagArgument = new Argument<string>("tag")
343347
{
344348
Description = "The prerelease tag to apply on the release branch (if any). If not specified, any existing prerelease tag will be removed. The preceding hyphen may be omitted.",
@@ -351,6 +355,7 @@ private static RootCommand BuildCommandLine()
351355
versionIncrement,
352356
format,
353357
unformattedCommitMessage,
358+
whatIf,
354359
tagArgument,
355360
};
356361

@@ -362,7 +367,8 @@ private static RootCommand BuildCommandLine()
362367
var formatValue = parseResult.GetValue(format);
363368
var tagArgumentValue = parseResult.GetValue(tagArgument);
364369
var unformattedCommitMessageValue = parseResult.GetValue(unformattedCommitMessage);
365-
return await OnPrepareReleaseCommand(projectValue, nextVersionValue, versionIncrementValue, formatValue, tagArgumentValue, unformattedCommitMessageValue);
370+
var whatIfValue = parseResult.GetValue(whatIf);
371+
return await OnPrepareReleaseCommand(projectValue, nextVersionValue, versionIncrementValue, formatValue, tagArgumentValue, unformattedCommitMessageValue, whatIfValue);
366372
});
367373
}
368374

@@ -883,7 +889,7 @@ private static Task<int> OnCloudCommand(string project, string[] metadata, strin
883889
return Task.FromResult((int)ExitCodes.OK);
884890
}
885891

886-
private static Task<int> OnPrepareReleaseCommand(string project, string nextVersion, string versionIncrement, string format, string tag, string unformattedCommitMessage)
892+
private static Task<int> OnPrepareReleaseCommand(string project, string nextVersion, string versionIncrement, string format, string tag, string unformattedCommitMessage, bool whatIf)
887893
{
888894
// validate project path property
889895
string searchPath = GetSpecifiedOrCurrentDirectoryPath(project);
@@ -949,11 +955,27 @@ private static Task<int> OnPrepareReleaseCommand(string project, string nextVers
949955
}
950956
}
951957

952-
// run prepare-release
958+
// run prepare-release or simulate
953959
try
954960
{
955961
var releaseManager = new ReleaseManager(Console.Out, Console.Error);
956-
releaseManager.PrepareRelease(searchPath, tag, nextVersionParsed, versionIncrementParsed, outputMode, unformattedCommitMessage);
962+
963+
if (whatIf)
964+
{
965+
// Simulate the release without making changes
966+
ReleaseManager.ReleaseInfo simulationResult = releaseManager.SimulatePrepareRelease(searchPath, tag, nextVersionParsed, versionIncrementParsed, outputMode);
967+
968+
if (outputMode == ReleaseManager.ReleaseManagerOutputMode.Json)
969+
{
970+
releaseManager.WriteToOutput(simulationResult);
971+
}
972+
}
973+
else
974+
{
975+
// Actually perform the release
976+
releaseManager.PrepareRelease(searchPath, tag, nextVersionParsed, versionIncrementParsed, outputMode, unformattedCommitMessage);
977+
}
978+
957979
return Task.FromResult((int)ExitCodes.OK);
958980
}
959981
catch (ReleaseManager.ReleasePreparationException ex)

0 commit comments

Comments
 (0)