Skip to content

Commit e1a9804

Browse files
authored
Merge pull request #1242 from dotnet/copilot/fix-1019
Add --what-if option to prepare-release command for simulating version changes
2 parents d5de993 + 387167b commit e1a9804

File tree

3 files changed

+293
-44
lines changed

3 files changed

+293
-44
lines changed

src/NerdBank.GitVersioning/ReleaseManager.cs

Lines changed: 109 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ public enum ReleaseManagerOutputMode
104104
Json = 1,
105105
}
106106

107+
public void WriteToOutput(ReleaseInfo releaseInfo)
108+
{
109+
string json = JsonConvert.SerializeObject(releaseInfo, Formatting.Indented, new SemanticVersionJsonConverter());
110+
this.stdout.WriteLine(json);
111+
}
112+
107113
/// <summary>
108114
/// Prepares a release for the specified directory by creating a release branch and incrementing the version in the current branch.
109115
/// </summary>
@@ -131,7 +137,70 @@ public enum ReleaseManagerOutputMode
131137
/// <param name="unformattedCommitMessage">
132138
/// An optional, custom message to use for the commit that sets the new version number. May use <c>{0}</c> to substitute the new version number.
133139
/// </param>
134-
public void PrepareRelease(string projectDirectory, string releaseUnstableTag = null, Version nextVersion = null, VersionOptions.ReleaseVersionIncrement? versionIncrement = null, ReleaseManagerOutputMode outputMode = default, string unformattedCommitMessage = null)
140+
/// <param name="whatIf">
141+
/// If true, simulates the prepare-release operation and returns the versions that would be set without making any changes.
142+
/// </param>
143+
/// <returns>
144+
/// A <see cref="ReleaseInfo"/> object containing information about the release when <paramref name="whatIf"/> is true; otherwise null.
145+
/// </returns>
146+
public ReleaseInfo PrepareRelease(string projectDirectory, string releaseUnstableTag = null, Version nextVersion = null, VersionOptions.ReleaseVersionIncrement? versionIncrement = null, ReleaseManagerOutputMode outputMode = default, string unformattedCommitMessage = null, bool whatIf = false)
147+
{
148+
return this.PrepareReleaseCore(projectDirectory, releaseUnstableTag, nextVersion, versionIncrement, outputMode, unformattedCommitMessage, whatIf);
149+
}
150+
151+
private static bool IsVersionDecrement(SemanticVersion oldVersion, SemanticVersion newVersion)
152+
{
153+
if (newVersion.Version > oldVersion.Version)
154+
{
155+
return false;
156+
}
157+
else if (newVersion.Version == oldVersion.Version)
158+
{
159+
return string.IsNullOrEmpty(oldVersion.Prerelease) &&
160+
!string.IsNullOrEmpty(newVersion.Prerelease);
161+
}
162+
else
163+
{
164+
// newVersion.Version < oldVersion.Version
165+
return true;
166+
}
167+
}
168+
169+
/// <summary>
170+
/// Core implementation of prepare-release functionality that can either simulate or execute the operation.
171+
/// </summary>
172+
/// <param name="projectDirectory">
173+
/// The path to the directory that may (or its ancestors may) define the version file.
174+
/// </param>
175+
/// <param name="releaseUnstableTag">
176+
/// An optional prerelease tag to apply on the release branch.
177+
/// If not specified, any existing prerelease tag will be removed from the release.
178+
/// The preceding hyphen may be omitted.
179+
/// </param>
180+
/// <param name="nextVersion">
181+
/// The version to use for the next release.
182+
/// If not specified, the next version will be determined automatically by incrementing the current
183+
/// version based on the current version and the <paramref name="versionIncrement"/> setting in <c>version.json</c>.
184+
/// Parameter will be ignored if the current branch is a release branch.
185+
/// </param>
186+
/// <param name="versionIncrement">
187+
/// The increment to apply in order to determine the next version on the current branch.
188+
/// If specified, value will be used instead of the increment specified in <c>version.json</c>.
189+
/// Parameter will be ignored if the current branch is a release branch.
190+
/// </param>
191+
/// <param name="outputMode">
192+
/// The output format to use for writing to stdout.
193+
/// </param>
194+
/// <param name="unformattedCommitMessage">
195+
/// An optional, custom message to use for the commit that sets the new version number. May use <c>{0}</c> to substitute the new version number.
196+
/// </param>
197+
/// <param name="whatIf">
198+
/// If true, simulates the prepare-release operation and returns the versions that would be set without making any changes.
199+
/// </param>
200+
/// <returns>
201+
/// A <see cref="ReleaseInfo"/> object containing information about the release when <paramref name="whatIf"/> is true; otherwise null.
202+
/// </returns>
203+
private ReleaseInfo PrepareReleaseCore(string projectDirectory, string releaseUnstableTag, Version nextVersion, VersionOptions.ReleaseVersionIncrement? versionIncrement, ReleaseManagerOutputMode outputMode, string unformattedCommitMessage, bool whatIf)
135204
{
136205
Requires.NotNull(projectDirectory, nameof(projectDirectory));
137206

@@ -164,16 +233,30 @@ public void PrepareRelease(string projectDirectory, string releaseUnstableTag =
164233
{
165234
if (outputMode == ReleaseManagerOutputMode.Text)
166235
{
167-
this.stdout.WriteLine($"{releaseBranchName} branch advanced from {versionOptions.Version} to {releaseVersion}.");
236+
if (whatIf)
237+
{
238+
this.stdout.WriteLine($"What-if: {releaseBranchName} branch would be advanced from {versionOptions.Version} to {releaseVersion}.");
239+
}
240+
else
241+
{
242+
this.stdout.WriteLine($"{releaseBranchName} branch advanced from {versionOptions.Version} to {releaseVersion}.");
243+
}
244+
}
245+
246+
var releaseInfo = new ReleaseInfo(new ReleaseBranchInfo(releaseBranchName, repository.Head.Tip.Id.ToString(), releaseVersion));
247+
248+
if (whatIf)
249+
{
250+
return releaseInfo;
168251
}
169-
else
252+
253+
if (outputMode == ReleaseManagerOutputMode.Json)
170254
{
171-
var releaseInfo = new ReleaseInfo(new ReleaseBranchInfo(releaseBranchName, repository.Head.Tip.Id.ToString(), releaseVersion));
172255
this.WriteToOutput(releaseInfo);
173256
}
174257

175258
this.UpdateVersion(context, versionOptions.Version, releaseVersion, unformattedCommitMessage);
176-
return;
259+
return null;
177260
}
178261

179262
SemanticVersion nextDevVersion = this.GetNextDevVersion(versionOptions, nextVersion, versionIncrement);
@@ -193,6 +276,21 @@ public void PrepareRelease(string projectDirectory, string releaseUnstableTag =
193276
throw new ReleasePreparationException(ReleasePreparationError.BranchAlreadyExists);
194277
}
195278

279+
if (outputMode == ReleaseManagerOutputMode.Text && whatIf)
280+
{
281+
this.stdout.WriteLine($"What-if: {releaseBranchName} branch would track v{releaseVersion} stabilization and release.");
282+
this.stdout.WriteLine($"What-if: {originalBranchName} branch would track v{nextDevVersion} development.");
283+
}
284+
285+
var originalBranchInfo = new ReleaseBranchInfo(originalBranchName, repository.Head.Tip.Sha, nextDevVersion);
286+
var releaseBranchInfo = new ReleaseBranchInfo(releaseBranchName, repository.Head.Tip.Id.ToString(), releaseVersion);
287+
var releaseInfoResult = new ReleaseInfo(originalBranchInfo, releaseBranchInfo);
288+
289+
if (whatIf)
290+
{
291+
return releaseInfoResult;
292+
}
293+
196294
// create release branch and update version
197295
Branch releaseBranch = repository.CreateBranch(releaseBranchName);
198296
global::LibGit2Sharp.Commands.Checkout(repository, releaseBranch);
@@ -222,30 +320,15 @@ public void PrepareRelease(string projectDirectory, string releaseUnstableTag =
222320

223321
if (outputMode == ReleaseManagerOutputMode.Json)
224322
{
225-
var originalBranchInfo = new ReleaseBranchInfo(originalBranchName, repository.Head.Tip.Sha, nextDevVersion);
226-
var releaseBranchInfo = new ReleaseBranchInfo(releaseBranchName, repository.Branches[releaseBranchName].Tip.Id.ToString(), releaseVersion);
227-
var releaseInfo = new ReleaseInfo(originalBranchInfo, releaseBranchInfo);
323+
// Update the commit IDs with the actual final commit IDs after all operations
324+
var finalOriginalBranchInfo = new ReleaseBranchInfo(originalBranchName, repository.Head.Tip.Sha, nextDevVersion);
325+
var finalReleaseBranchInfo = new ReleaseBranchInfo(releaseBranchName, repository.Branches[releaseBranchName].Tip.Id.ToString(), releaseVersion);
326+
var finalReleaseInfo = new ReleaseInfo(finalOriginalBranchInfo, finalReleaseBranchInfo);
228327

229-
this.WriteToOutput(releaseInfo);
328+
this.WriteToOutput(finalReleaseInfo);
230329
}
231-
}
232330

233-
private static bool IsVersionDecrement(SemanticVersion oldVersion, SemanticVersion newVersion)
234-
{
235-
if (newVersion.Version > oldVersion.Version)
236-
{
237-
return false;
238-
}
239-
else if (newVersion.Version == oldVersion.Version)
240-
{
241-
return string.IsNullOrEmpty(oldVersion.Prerelease) &&
242-
!string.IsNullOrEmpty(newVersion.Prerelease);
243-
}
244-
else
245-
{
246-
// newVersion.Version < oldVersion.Version
247-
return true;
248-
}
331+
return null;
249332
}
250333

251334
private string GetReleaseBranchName(VersionOptions versionOptions)
@@ -387,12 +470,6 @@ private SemanticVersion GetNextDevVersion(VersionOptions versionOptions, Version
387470
return nextDevVersion.SetFirstPrereleaseTag(versionOptions.ReleaseOrDefault.FirstUnstableTagOrDefault);
388471
}
389472

390-
private void WriteToOutput(ReleaseInfo releaseInfo)
391-
{
392-
string json = JsonConvert.SerializeObject(releaseInfo, Formatting.Indented, new SemanticVersionJsonConverter());
393-
this.stdout.WriteLine(json);
394-
}
395-
396473
/// <summary>
397474
/// Exception indicating an error during preparation of a release.
398475
/// </summary>

src/nbgv/Program.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ private static RootCommand BuildCommandLine()
9898
var source = new Option<string[]>("--source", ["-s"])
9999
{
100100
Description = $"The URI(s) of the NuGet package source(s) used to determine the latest stable version of the {PackageId} package. This setting overrides all of the sources specified in the NuGet.Config files.",
101-
DefaultValueFactory = _ => Array.Empty<string>(),
102101
Arity = ArgumentArity.OneOrMore,
103102
AllowMultipleArgumentsPerToken = true,
104103
};
@@ -123,7 +122,7 @@ private static RootCommand BuildCommandLine()
123122
{
124123
Description = "The path to the project or project directory. The default is the current directory.",
125124
};
126-
var metadata = new Option<string[]>("--metadata", Array.Empty<string>())
125+
var metadata = new Option<string[]>("--metadata")
127126
{
128127
Description = "Adds an identifier to the build metadata part of a semantic version.",
129128
Arity = ArgumentArity.OneOrMore,
@@ -262,7 +261,7 @@ private static RootCommand BuildCommandLine()
262261
{
263262
Description = "The path to the project or project directory used to calculate the version. The default is the current directory. Ignored if the -v option is specified.",
264263
};
265-
var metadata = new Option<string[]>("--metadata", Array.Empty<string>())
264+
var metadata = new Option<string[]>("--metadata")
266265
{
267266
Description = "Adds an identifier to the build metadata part of a semantic version.",
268267
Arity = ArgumentArity.OneOrMore,
@@ -284,14 +283,13 @@ private static RootCommand BuildCommandLine()
284283
{
285284
Description = "Defines a few common version variables as cloud build variables, with a \"Git\" prefix (e.g. GitBuildVersion, GitBuildVersionSimple, GitAssemblyInformationalVersion).",
286285
};
287-
var skipCloudBuildNumber = new Option<bool>("--skip-cloud-build-number", Array.Empty<string>())
286+
var skipCloudBuildNumber = new Option<bool>("--skip-cloud-build-number")
288287
{
289288
Description = "Do not emit the cloud build variable to set the build number. This is useful when you want to set other cloud build variables but not the build number.",
290289
};
291290
var define = new Option<string[]>("--define", ["-d"])
292291
{
293292
Description = "Additional cloud build variables to define. Each should be in the NAME=VALUE syntax.",
294-
DefaultValueFactory = _ => Array.Empty<string>(),
295293
Arity = ArgumentArity.OneOrMore,
296294
AllowMultipleArgumentsPerToken = true,
297295
};
@@ -327,22 +325,26 @@ private static RootCommand BuildCommandLine()
327325
{
328326
Description = "The path to the project or project directory. The default is the current directory.",
329327
};
330-
var nextVersion = new Option<string>("--nextVersion", Array.Empty<string>())
328+
var nextVersion = new Option<string>("--nextVersion")
331329
{
332330
Description = "The version to set for the current branch. If omitted, the next version is determined automatically by incrementing the current version.",
333331
};
334-
var versionIncrement = new Option<string>("--versionIncrement", Array.Empty<string>())
332+
var versionIncrement = new Option<string>("--versionIncrement")
335333
{
336334
Description = "Overrides the 'versionIncrement' setting set in version.json for determining the next version of the current branch.",
337335
};
338336
var format = new Option<string>("--format", ["-f"])
339337
{
340338
Description = $"The format to write information about the release. Allowed values are: {string.Join(", ", SupportedFormats)}. The default is {DefaultOutputFormat}.",
341339
};
342-
var unformattedCommitMessage = new Option<string>("--commit-message-pattern", Array.Empty<string>())
340+
var unformattedCommitMessage = new Option<string>("--commit-message-pattern")
343341
{
344342
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}'\".",
345343
};
344+
var whatIf = new Option<bool>("--what-if")
345+
{
346+
Description = "Simulates the prepare-release operation and prints the new version that would be set, but does not actually make any changes.",
347+
};
346348
var tagArgument = new Argument<string>("tag")
347349
{
348350
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.",
@@ -355,6 +357,7 @@ private static RootCommand BuildCommandLine()
355357
versionIncrement,
356358
format,
357359
unformattedCommitMessage,
360+
whatIf,
358361
tagArgument,
359362
};
360363

@@ -366,7 +369,8 @@ private static RootCommand BuildCommandLine()
366369
var formatValue = parseResult.GetValue(format);
367370
var tagArgumentValue = parseResult.GetValue(tagArgument);
368371
var unformattedCommitMessageValue = parseResult.GetValue(unformattedCommitMessage);
369-
return await OnPrepareReleaseCommand(projectValue, nextVersionValue, versionIncrementValue, formatValue, tagArgumentValue, unformattedCommitMessageValue);
372+
var whatIfValue = parseResult.GetValue(whatIf);
373+
return await OnPrepareReleaseCommand(projectValue, nextVersionValue, versionIncrementValue, formatValue, tagArgumentValue, unformattedCommitMessageValue, whatIfValue);
370374
});
371375
}
372376

@@ -915,7 +919,7 @@ private static Task<int> OnCloudCommand(string project, string[] metadata, strin
915919
return Task.FromResult((int)ExitCodes.OK);
916920
}
917921

918-
private static Task<int> OnPrepareReleaseCommand(string project, string nextVersion, string versionIncrement, string format, string tag, string unformattedCommitMessage)
922+
private static Task<int> OnPrepareReleaseCommand(string project, string nextVersion, string versionIncrement, string format, string tag, string unformattedCommitMessage, bool whatIf)
919923
{
920924
// validate project path property
921925
string searchPath = GetSpecifiedOrCurrentDirectoryPath(project);
@@ -981,11 +985,18 @@ private static Task<int> OnPrepareReleaseCommand(string project, string nextVers
981985
}
982986
}
983987

984-
// run prepare-release
988+
// run prepare-release or simulate
985989
try
986990
{
987991
var releaseManager = new ReleaseManager(Console.Out, Console.Error);
988-
releaseManager.PrepareRelease(searchPath, tag, nextVersionParsed, versionIncrementParsed, outputMode, unformattedCommitMessage);
992+
993+
ReleaseManager.ReleaseInfo releaseInfo = releaseManager.PrepareRelease(searchPath, tag, nextVersionParsed, versionIncrementParsed, outputMode, unformattedCommitMessage, whatIf);
994+
995+
if (whatIf && outputMode == ReleaseManager.ReleaseManagerOutputMode.Json)
996+
{
997+
releaseManager.WriteToOutput(releaseInfo);
998+
}
999+
9891000
return Task.FromResult((int)ExitCodes.OK);
9901001
}
9911002
catch (ReleaseManager.ReleasePreparationException ex)

0 commit comments

Comments
 (0)