public void ShouldAppendToExistingChangelog() { var parser = new ConventionalCommitParser(); var plainLinkBuilder = new PlainLinkBuilder(); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 0, 0), new DateTimeOffset(), plainLinkBuilder, new List <ConventionalCommit> { parser.Parse(new TestCommit("a360d6a307909c6e571b29d4a329fd786c5d4543", "fix: a fix in version 1.0.0")), }); changelog.Write(new Version(1, 1, 0), new DateTimeOffset(), plainLinkBuilder, new List <ConventionalCommit> { parser.Parse(new TestCommit("b360d6a307909c6e571b29d4a329fd786c5d4543", "fix: a fix in version 1.1.0")), }); var changelogContents = File.ReadAllText(changelog.FilePath); changelogContents.ShouldContain("<a name=\"1.0.0\"></a>"); changelogContents.ShouldContain("a fix in version 1.0.0"); changelogContents.ShouldContain("<a name=\"1.1.0\"></a>"); changelogContents.ShouldContain("a fix in version 1.1.0"); }
public void ShouldHideFixSectionWhenHideIsTrue() { var plainLinkBuilder = new PlainLinkBuilder(); var changelog = ChangelogBuilder.CreateForPath(_testDirectory); changelog.Write( new Version(1, 1, 0), new DateTimeOffset(), plainLinkBuilder, new List <ConventionalCommit> { ConventionalCommitParser.Parse(new TestCommit("a360d6a307909c6e571b29d4a329fd786c5d4543", "fix: a fix")), ConventionalCommitParser.Parse(new TestCommit("b360d6a307909c6e571b29d4a329fd786c5d4543", "feat: a feature")), }, ChangelogOptions.Default with { Sections = new[] { new ChangelogSection { Type = "feat", Hidden = false, Section = "Features" }, new ChangelogSection { Type = "fix", Hidden = true, Section = "Bug Fixes" }, } });
public void ConventionalCommitParser_Parse_SuccessfullyParsesMultipleLineCommit(string message, bool hasScope, bool hasBreakingToken) { // given commit message does not meet conventional commit standards var commit = new Mock <Commit>(); commit.Setup(x => x.Message).Returns(message); // when we parse commit message var parser = new ConventionalCommitParser(); var conventionalCommits = parser.Parse(new[] { commit.Object }); ConventionalCommit conventionalCommit = conventionalCommits.FirstOrDefault(); var trailer = conventionalCommit.Trailers.FirstOrDefault(); // then we return a null value Assert.NotNull(conventionalCommit); Assert.NotNull(conventionalCommit.Type); Assert.NotNull(conventionalCommit.Subject); if (hasScope) { Assert.NotNull(conventionalCommit.Scope); } if (hasBreakingToken) { Assert.True(conventionalCommit.HasBreakingToken); } Assert.NotEmpty(conventionalCommit.Trailers); Assert.NotNull(trailer); Assert.NotNull(trailer.Type); Assert.NotNull(trailer.Subject); }
public void ShouldGenerateAChangelogForFixFeatAndBreakingCommits() { var plainLinkBuilder = new PlainLinkBuilder(); var changelog = ChangelogBuilder.CreateForPath(_testDirectory); changelog.Write( new Version(1, 1, 0), new DateTimeOffset(), plainLinkBuilder, new List <ConventionalCommit> { ConventionalCommitParser.Parse(new TestCommit("a360d6a307909c6e571b29d4a329fd786c5d4543", "fix: a fix")), ConventionalCommitParser.Parse(new TestCommit("b360d6a307909c6e571b29d4a329fd786c5d4543", "feat: a feature")), ConventionalCommitParser.Parse( new TestCommit("c360d6a307909c6e571b29d4a329fd786c5d4543", "feat: a breaking change feature\nBREAKING CHANGE: this will break everything")), }, ChangelogOptions.Default); var changelogContents = File.ReadAllText(changelog.FilePath); var sb = new ChangelogStringBuilder(); sb.Append(ChangelogOptions.Preamble); sb.Append("<a name=\"1.1.0\"></a>"); sb.Append("## 1.1.0 (1-1-1)", 2); sb.Append("### Features", 2); sb.Append("* a breaking change feature"); sb.Append("* a feature", 2); sb.Append("### Bug Fixes", 2); sb.Append("* a fix", 2); sb.Append("### Breaking Changes", 2); sb.Append("* a breaking change feature", 2); Assert.Equal(sb.Build(), changelogContents); }
public void ShouldUseFullHeaderAsSubjectIfNoTypeWasGivenButSubjectUsesColon() { var testCommit = new TestCommit("c360d6a307909c6e571b29d4a329fd786c5d4543", "broadcast $destroy event: on scope destruction"); var conventionalCommit = ConventionalCommitParser.Parse(testCommit); Assert.Equal(testCommit.Message, conventionalCommit.Subject); }
public void ShouldIncludeAllCommitsInChangelogWhenGiven() { var parser = new ConventionalCommitParser(); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 1, 0), new DateTimeOffset(), new List <ConventionalCommit> { parser.Parse(new TestCommit("chore: nothing important")), parser.Parse(new TestCommit("chore: some foo bar")), }, true); var changelogContents = File.ReadAllText(changelog.FilePath); changelogContents.ShouldContain("nothing important"); changelogContents.ShouldContain("some foo bar"); }
public void ShouldSupportExclamationMarkToSignifyingBreakingChanges(string commitMessage) { var testCommit = new TestCommit("c360d6a307909c6e571b29d4a329fd786c5d4543", commitMessage); var conventionalCommit = ConventionalCommitParser.Parse(testCommit); conventionalCommit.Notes.ShouldHaveSingleItem(); conventionalCommit.Notes[0].Title.ShouldBe("BREAKING CHANGE"); conventionalCommit.Notes[0].Text.ShouldBe(string.Empty); }
public void ShouldParseTypeScopeAndSubjectFromSingleLineCommitMessage() { var testCommit = new TestCommit("c360d6a307909c6e571b29d4a329fd786c5d4543", "feat(scope): broadcast $destroy event on scope destruction"); var conventionalCommit = ConventionalCommitParser.Parse(testCommit); Assert.Equal("feat", conventionalCommit.Type); Assert.Equal("scope", conventionalCommit.Scope); Assert.Equal("broadcast $destroy event on scope destruction", conventionalCommit.Subject); }
public void ShouldUseFullHeaderAsSubjectIfNoTypeWasGivenButSubjectUsesColon() { var parser = new ConventionalCommitParser(); var testCommit = new TestCommit("broadcast $destroy event: on scope destruction"); var conventionalCommit = parser.Parse(testCommit); Assert.Equal(testCommit.Message, conventionalCommit.Subject); }
public void ShouldGenerateAChangelogForFixFeatAndBreakingCommits() { ConventionalCommitParser parser = new ConventionalCommitParser(); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 1, 0), new DateTimeOffset(), new List <ConventionalCommit> { parser.Parse(new TestCommit("fix: a fix")), parser.Parse(new TestCommit("feat: a feature")), parser.Parse(new TestCommit("feat: a breaking change feature\nBREAKING CHANGE: this will break everything")), }); var wasChangelogWritten = File.Exists(Path.Join(_testDirectory, "CHANGELOG.md")); Assert.True(wasChangelogWritten); // TODO: Assert changelog entries }
public void ShouldAppendToExistingChangelog() { File.WriteAllText(Path.Combine(_testDirectory, "CHANGELOG.md"), "# Some preamble\n\n##SomeVersion"); ConventionalCommitParser parser = new ConventionalCommitParser(); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 1, 0), new DateTimeOffset(), new List <ConventionalCommit> { parser.Parse(new TestCommit("fix: a fix")), parser.Parse(new TestCommit("feat: a feature")), parser.Parse(new TestCommit("feat: a breaking change feature\nBREAKING CHANGE: this will break everything")), }); var wasChangelogWritten = File.Exists(Path.Join(_testDirectory, "CHANGELOG.md")); Assert.True(wasChangelogWritten); // TODO: Assert changelog entries }
public void ShouldParseTypeScopeAndSubjectFromSingleLineCommitMessageIfSubjectUsesColon() { var parser = new ConventionalCommitParser(); var testCommit = new TestCommit("feat(scope): broadcast $destroy: event on scope destruction"); var conventionalCommit = parser.Parse(testCommit); Assert.Equal("feat", conventionalCommit.Type); Assert.Equal("scope", conventionalCommit.Scope); Assert.Equal("broadcast $destroy: event on scope destruction", conventionalCommit.Subject); }
public void ShouldExtractCommitNotes() { var testCommit = new TestCommit("c360d6a307909c6e571b29d4a329fd786c5d4543", "feat(scope): broadcast $destroy: event on scope destruction\nBREAKING CHANGE: this will break rc1 compatibility"); var conventionalCommit = ConventionalCommitParser.Parse(testCommit); Assert.Single(conventionalCommit.Notes); var breakingChangeNote = conventionalCommit.Notes.Single(); Assert.Equal("BREAKING CHANGE", breakingChangeNote.Title); Assert.Equal("this will break rc1 compatibility", breakingChangeNote.Text); }
public void ShouldBuildGithubSSHCommitLinks() { var linkBuilder = new GithubLinkBuilder("[email protected]:organization/repository.git"); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 0, 0), new DateTimeOffset(), linkBuilder, new List <ConventionalCommit> { ConventionalCommitParser.Parse(new TestCommit("a360d6a307909c6e571b29d4a329fd786c5d4543", "fix: a fix in version 1.0.0")), }); var changelogContents = File.ReadAllText(changelog.FilePath); changelogContents.ShouldContain("* a fix in version 1.0.0 ([a360d6a](https://www.github.com/organization/repository/commit/a360d6a307909c6e571b29d4a329fd786c5d4543))"); }
public void ShouldAppendToExistingChangelog() { var parser = new ConventionalCommitParser(); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 0, 0), new DateTimeOffset(), new List <ConventionalCommit> { parser.Parse(new TestCommit("fix: a fix in version 1.0.0")), }); changelog.Write(new Version(1, 1, 0), new DateTimeOffset(), new List <ConventionalCommit> { parser.Parse(new TestCommit("fix: a fix in version 1.1.0")), }); var changelogContents = File.ReadAllText(changelog.FilePath); changelogContents.ShouldContain("<a name=\"1.0.0\"></a>"); changelogContents.ShouldContain("a fix in version 1.0.0"); changelogContents.ShouldContain("<a name=\"1.1.0\"></a>"); changelogContents.ShouldContain("a fix in version 1.1.0"); }
public void ConventionalCommitParser_Parse_FailsToParseSingleLineCommit() { // given commit message does not meet conventional commit standards var commit = new Mock <Commit>(); commit.Setup(x => x.Message).Returns("This is a failure test."); // when we parse commit message var parser = new ConventionalCommitParser(); var conventionalCommits = parser.Parse(new[] { commit.Object }); ConventionalCommit conventionalCommit = conventionalCommits.FirstOrDefault(); // then we return a null value Assert.Null(conventionalCommit); }
public void ShouldIncludeAllCommitsInChangelogWhenGiven() { var plainLinkBuilder = new PlainLinkBuilder(); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 1, 0), new DateTimeOffset(), plainLinkBuilder, new List <ConventionalCommit> { ConventionalCommitParser.Parse(new TestCommit("a360d6a307909c6e571b29d4a329fd786c5d4543", "chore: nothing important")), ConventionalCommitParser.Parse(new TestCommit("b360d6a307909c6e571b29d4a329fd786c5d4543", "chore: some foo bar")), }, true); var changelogContents = File.ReadAllText(changelog.FilePath); changelogContents.ShouldContain("nothing important"); changelogContents.ShouldContain("some foo bar"); }
public void ShouldAppendAtEndIfChangelogContainsExtraInformation() { File.WriteAllText(Path.Combine(_testDirectory, "CHANGELOG.md"), "# Should be kept by versionize\n\nSome information about the changelog"); var plainLinkBuilder = new PlainLinkBuilder(); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 0, 0), new DateTimeOffset(), plainLinkBuilder, new List <ConventionalCommit> { ConventionalCommitParser.Parse(new TestCommit("a360d6a307909c6e571b29d4a329fd786c5d4543", "fix: a fix in version 1.0.0")), }); var changelogContents = File.ReadAllText(changelog.FilePath); changelogContents.ShouldBe("# Should be kept by versionize\n\nSome information about the changelog\n\n<a name=\"1.0.0\"></a>\n## 1.0.0 (1-1-1)\n\n### Bug Fixes\n\n* a fix in version 1.0.0\n\n"); }
public void ShouldGenerateWithoutLiteralLineBreakCharacters() { var plainLinkBuilder = new PlainLinkBuilder(); var changelog = ChangelogBuilder.CreateForPath(_testDirectory); changelog.Write( new Version(1, 1, 0), new DateTimeOffset(), plainLinkBuilder, new List <ConventionalCommit> { ConventionalCommitParser.Parse(new TestCommit("a360d6a307909c6e571b29d4a329fd786c5d4543", "fix: a fix")), }, ChangelogOptions.Default); var contents = File.ReadAllText(Path.Join(_testDirectory, "CHANGELOG.md")); contents.ShouldNotContain("\\n"); }
public void ShouldGenerateAChangelogForFixFeatAndBreakingCommits() { var plainLinkBuilder = new PlainLinkBuilder(); var changelog = Changelog.Discover(_testDirectory); changelog.Write(new Version(1, 1, 0), new DateTimeOffset(), plainLinkBuilder, new List <ConventionalCommit> { ConventionalCommitParser.Parse(new TestCommit("a360d6a307909c6e571b29d4a329fd786c5d4543", "fix: a fix")), ConventionalCommitParser.Parse(new TestCommit("b360d6a307909c6e571b29d4a329fd786c5d4543", "feat: a feature")), ConventionalCommitParser.Parse( new TestCommit("c360d6a307909c6e571b29d4a329fd786c5d4543", "feat: a breaking change feature\nBREAKING CHANGE: this will break everything")), }); var wasChangelogWritten = File.Exists(Path.Join(_testDirectory, "CHANGELOG.md")); Assert.True(wasChangelogWritten); // TODO: Assert changelog entries }
public SemanticVersion Versionize(VersionizeOptions options) { var workingDirectory = _directory.FullName; using var repo = new Repository(workingDirectory); var isDirty = repo.RetrieveStatus(new StatusOptions()).IsDirty; if (!options.SkipDirty && isDirty) { Exit($"Repository {workingDirectory} is dirty. Please commit your changes.", 1); } var projects = Projects.Discover(workingDirectory); if (projects.IsEmpty()) { Exit($"Could not find any projects files in {workingDirectory} that have a <Version> defined in their csproj file.", 1); } if (projects.HasInconsistentVersioning()) { Exit($"Some projects in {workingDirectory} have an inconsistent <Version> defined in their csproj file. Please update all versions to be consistent or remove the <Version> elements from projects that should not be versioned", 1); } Information($"Discovered {projects.GetProjectFiles().Count()} versionable projects"); foreach (var project in projects.GetProjectFiles()) { Information($" * {project}"); } var versionTag = repo.SelectVersionTag(projects.Version); var isInitialRelease = versionTag == null; var commitsInVersion = repo.GetCommitsSinceLastVersion(versionTag); var conventionalCommits = ConventionalCommitParser.Parse(commitsInVersion); var versionIncrement = new VersionIncrementStrategy(conventionalCommits); var nextVersion = isInitialRelease ? projects.Version : versionIncrement.NextVersion(projects.Version, options.Prerelease); // For non initial releases: for insignificant commits such as chore increment the patch version if IgnoreInsignificantCommits is not set if (!isInitialRelease && nextVersion == projects.Version) { if (options.IgnoreInsignificantCommits || options.ExitInsignificantCommits) { var exitCode = options.ExitInsignificantCommits ? 1 : 0; Exit($"Version was not affected by commits since last release ({projects.Version})", exitCode); } else { nextVersion = nextVersion.IncrementPatchVersion(); } } if (!string.IsNullOrWhiteSpace(options.ReleaseAs)) { try { nextVersion = SemanticVersion.Parse(options.ReleaseAs); } catch (Exception) { Exit($"Could not parse the specified release version {options.ReleaseAs} as valid version", 1); } } if (nextVersion < projects.Version) { Exit($"Semantic versioning conflict: the next version {nextVersion} would be lower than the current version {projects.Version}. This can be caused by using a wrong pre-release label or release as version", 1); } if (!options.DryRun && !options.SkipCommit && repo.VersionTagsExists(nextVersion)) { Exit($"Version {nextVersion} already exists. Please use a different version.", 1); } var versionTime = DateTimeOffset.Now; // Commit changelog and version source if (!options.DryRun && (nextVersion != projects.Version)) { projects.WriteVersion(nextVersion); foreach (var projectFile in projects.GetProjectFiles()) { Commands.Stage(repo, projectFile); } } Step($"bumping version from {projects.Version} to {nextVersion} in projects"); var changelog = ChangelogBuilder.CreateForPath(workingDirectory); var changelogLinkBuilder = LinkBuilderFactory.CreateFor(repo); if (options.DryRun) { string markdown = ChangelogBuilder.GenerateMarkdown(nextVersion, versionTime, changelogLinkBuilder, conventionalCommits, options.Changelog); DryRun(markdown.TrimEnd('\n')); } else { changelog.Write(nextVersion, versionTime, changelogLinkBuilder, conventionalCommits, options.Changelog); } Step("updated CHANGELOG.md"); if (!options.DryRun && !options.SkipCommit) { if (!repo.IsConfiguredForCommits()) { Exit(@"Warning: Git configuration is missing. Please configure git before running versionize: git config --global user.name ""John Doe"" $ git config --global user.email [email protected]", 1); } Commands.Stage(repo, changelog.FilePath); foreach (var projectFile in projects.GetProjectFiles()) { Commands.Stage(repo, projectFile); } var author = repo.Config.BuildSignature(versionTime); var committer = author; var releaseCommitMessage = $"chore(release): {nextVersion} {options.CommitSuffix}".TrimEnd(); var versionCommit = repo.Commit(releaseCommitMessage, author, committer); Step("committed changes in projects and CHANGELOG.md"); repo.Tags.Add($"v{nextVersion}", versionCommit, author, $"{nextVersion}"); Step($"tagged release as {nextVersion}"); Information(""); Information("i Run `git push --follow-tags origin master` to push all changes including tags"); } else if (options.SkipCommit) { Information(""); Information($"i Commit and tagging of release was skipped. Tag this release as `v{nextVersion}` to make versionize detect the release"); } return(nextVersion); }
public async Task StartAsync(CancellationToken cancellationToken) { var repositoryPath = Directory.GetCurrentDirectory(); var repository = new Repository(repositoryPath); var gitRepository = new GitRepository(repository); IEnumerable <Commit> commits = new List <Commit>(); if (string.IsNullOrEmpty(_options.EndTag)) { commits = await gitRepository.GetLatestCommitsFromTagAsync( _options.StartTag, cancellationToken); } else { commits = await gitRepository.GetLatestCommitsBetweenTagsAsync( _options.StartTag, _options.EndTag, cancellationToken); } var parser = new ConventionalCommitParser(); var conventionalCommits = parser.Parse(commits); IDictionary <string, IDictionary <string, ICollection <string> > > changelogMessages = new Dictionary <string, IDictionary <string, ICollection <string> > >(); foreach (var conventionalCommit in conventionalCommits) { if (conventionalCommit is null) { continue; } AppendChangelogMessage(conventionalCommit, changelogMessages); foreach (var conventionalCommitTrailer in conventionalCommit.Trailers) { AppendChangelogMessage(conventionalCommitTrailer, changelogMessages); } } StringBuilder builder = new StringBuilder(); _ = builder.AppendLine($"# {_options.Version} - {DateTimeOffset.UtcNow:yyyy/MM/dd}"); foreach (var keyPair in changelogMessages.OrderBy(x => x.Key)) { _ = builder.AppendLine($"## **{keyPair.Key.Humanize().Transform(To.SentenceCase).Pluralize()}**"); foreach (var pair in keyPair.Value.OrderBy(x => x.Key)) { _ = builder.AppendLine($"* **{pair.Key.Replace("_", "").Humanize().Transform(To.SentenceCase)}**"); foreach (var message in pair.Value) { _ = builder.AppendLine($" * {message.Humanize().Transform(To.SentenceCase)}"); } _ = builder.AppendLine(); } _ = builder.AppendLine(); } using var readFileStream = File.Open(_options.OutputPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); using var streamReader = new StreamReader(readFileStream); _ = builder.AppendLine(streamReader.ReadToEnd()); streamReader.Close(); using var writeFileStream = File.Open(_options.OutputPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); using var streamWriter = new StreamWriter(writeFileStream); _ = writeFileStream.Seek(0, SeekOrigin.Begin); await streamWriter.WriteAsync(builder, cancellationToken); streamWriter.Close(); _applicationLifetime.StopApplication(); }