Skip to content

Commit

Permalink
Save unreleased changelogs as Markdown
Browse files Browse the repository at this point in the history
- Improve readability when code reviewing changelog entries
- Add versioning information
- Improve error messaging when a corrupted file is encountered
- Fix some typos
  • Loading branch information
pg8wood committed Mar 11, 2021
1 parent 86c1a18 commit e018761
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 64 deletions.
22 changes: 20 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
> This file is generated. To add a new changelog entry, run the `changelog` tool. For more info, run `changelog help`.
<!--Latest Release-->
## [0.2.0] - 03-11-2021

### Added
- Add all changelog entry types from the [Keep a Changelog spec](https://keepachangelog.com/en/1.0.0/)
- Types of changes are `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security`
- Add versioning information to the tool
- Run `changelog --version`
- Improve error messaging when a corrupted file is encountered

### Changed
- Save changelog entries as Markdown files to facilitate easier code review
- Use imperative mood for changelog commands. This feels better when executing a command and will feel more familiar to git users

### Fixed
- Fix a few typos in the tool's output
- Use `yyyy` date formatting to avoid [weird, sneaky bugs](https://stackoverflow.com/q/15133549/1181439)

## [0.1.1] - 02-26-2021

### Fixed
Expand All @@ -16,9 +33,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Initial release
- Created the Swift package
- Added `help`, `log`, and `publish` commands
- Create the Swift package
- Add `help`, `log`, and `publish` commands
- Set up unit test suite

[0.2.0]: https://github.com/pg8wood/changelog-generator/compare/0.1.1...0.2.0
[0.1.1]: https://github.com/pg8wood/changelog-generator/compare/0.1.0...0.1.1
[0.1.0]: https://github.com/pg8wood/changelog-generator/releases/tag/0.1.0
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ Teams that keep an up-to-date changelog are often plagued by merge conflicts int

## Installation

### [Mint](https://github.com/yonaskolb/Mint) (system-wide installation)
### [Mint](https://github.com/yonaskolb/Mint)


```sh
$ mint install pg8wood/changelog-generator
```

### Manual Installation
```sh
$ swift build -c release
$ cp -f .build/release/changelog /usr/local/bin/changelog
```

## Usage
### Help
To view all the available options, run `$ changelog help`
Expand All @@ -23,7 +29,7 @@ To view all the available options, run `$ changelog help`
Changelog entries may be added interactively with your favorite text editor, or quick entries can be passed as command-line arguments.

```sh
$ changelog log addition "I added something cool" "And something boring"
$ changelog add "I added something cool" "And something boring"

### Added
- I added something cool
Expand All @@ -34,25 +40,26 @@ $ changelog log addition "I added something cool" "And something boring"

#### Arguments
```
<entry-type> The type of changelog entry to create.
Valid entry types are addition, change, and fix.
<entry-type> The type of changelog entry to create.
Valid entry types are add, change, deprecate, remove, fix, and security.
<text> A list of strings separated by spaces to be recorded as a bulleted changelog entry.
If <text> is supplied, the --editor option is ignored and the changelog entry is created for you without opening an interactive text editor.
<text> A list of quoted strings separated by spaces to be recorded as a bulleted changelog entry.
If <text> is supplied, the --editor option is ignored and the changelog entry is created for you without opening an interactive text editor.
```

#### Options
```
-d, --directory <path> A directory where unpublished changelog entries will be written to / read from as Markdown files. (default: changelogs/unreleased/)
-e, --editor <editor> A terminal-based text editor executable in your $PATH used to write your changelog entry with more precision than the default bulleted list of changes. (default: vim)
--version Show the version.
-h, --help Show help information.
```

### Publish a Release
```
$ changelog publish 1.0.1
## [1.0.1] - 02-26-2021
## [1.0.1] - <release-date>
### Added
- help
Expand All @@ -63,8 +70,8 @@ Nice! CHANGELOG.md was updated. Congrats on the release! 🥳🍻

#### Arguments
```
<version> The version number associated with the changelog entries to be published.
<release-date> A string representing the date the version was published. Format MM-dd-YYYY. (default: <today>)
<version> The version number associated with the changelog entries to be published.
<release-date> A string representing the date the version was published. Format MM-dd-yyyy. (default: <today>)
```

#### Options
Expand Down
1 change: 1 addition & 0 deletions Sources/ChangelogCore/Commands/Changelog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct Changelog: ParsableCommand {
public static let configuration = CommandConfiguration(
abstract: "Curbing Cumbersome Changelog Conflicts.",
discussion: "Creates changelog entries and stores them as single files to avoid merge conflictss in version control. When it's time to release, `changelog publish` collects these files and appends them to your changelog file.",
version: "0.2.0",
subcommands: [Log.self, Publish.self],
defaultSubcommand: Log.self)

Expand Down
38 changes: 21 additions & 17 deletions Sources/ChangelogCore/Commands/Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ struct Log: ParsableCommand {
}

public func run() throws {
guard fileManager.fileExists(atPath: options.unreleasedChangelogsDirectory.path) else {
throw ChangelogError.changelogDirectoryNotFound(expectedPath: options.unreleasedChangelogsDirectory.relativeString)
}

if text.isEmpty {
try openEditor(editor, for: entryType)
} else {
Expand All @@ -61,9 +65,11 @@ struct Log: ParsableCommand {
}

private func openEditor(_ editor: String, for entryType: EntryType) throws {
let temporaryFilePath = createUniqueChangelogFilepath()
let hint = "<!-- Enter your changelog message below this line exactly how you want it to appear in the changelog. Lines surrounded in markdown (HTML) comments will be ignored.-->"
let temporaryFilePath = options.unreleasedChangelogsDirectory
.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString)
.appendingPathExtension("md")

let hint = "<!-- Enter your changelog message below this line exactly how you want it to appear in the changelog. Lines surrounded in markdown (HTML) comments will be ignored.-->"
try Data(hint.utf8)
.write(to: temporaryFilePath)

Expand All @@ -89,27 +95,25 @@ struct Log: ParsableCommand {
}

private func write(entryText: String) throws {
let entry = ChangelogEntry(type: entryType, text: entryText)
let uniqueFilepath = createUniqueChangelogFilepath()
let data = try JSONEncoder().encode(entry)
try data.write(to: uniqueFilepath)
let uniqueFilepath = options.unreleasedChangelogsDirectory
.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString)
.appendingPathExtension("md")

let entry = """
### \(entryType.title)
\(entryText)
"""

try entry.write(toFile: uniqueFilepath.path, atomically: true, encoding: .utf8)

let filePathString = OutputController.tryWrap(uniqueFilepath.relativePath, inColor: .white, bold: true)
let successString = OutputController.tryWrap("🙌 Created changelog entry at \(filePathString)", inColor: .green, bold: true)

OutputController.write(
"""
### \(entryType.title)
\(entry.text)
OutputController.write("""
\(entry)
\(successString)
""", inColor: .cyan)
}

private func createUniqueChangelogFilepath() -> URL {
options.unreleasedChangelogsDirectory
.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString)
.appendingPathExtension("json")
}
}
12 changes: 5 additions & 7 deletions Sources/ChangelogCore/Commands/Publish.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,8 @@ struct Publish: ParsableCommand {
}

func run() throws {
let decoder = JSONDecoder()
let changelogFilePaths = try fileManager.contentsOfDirectory(at: unreleasedChangelogsDirectory, includingPropertiesForKeys: nil)
let uncategorizedEntries = try changelogFilePaths.map {
try decoder.decode(ChangelogEntry.self, from: Data(contentsOf: $0))
}
let uncategorizedEntries = try changelogFilePaths.map(ChangelogEntry.init(contentsOf:))

guard !uncategorizedEntries.isEmpty else {
throw ChangelogError.noEntriesFound
Expand All @@ -75,7 +72,8 @@ struct Publish: ParsableCommand {
printChangelogSummary(groupedEntries: groupedEntries, changelogFilePaths: changelogFilePaths)

if dryRun {
OutputController.write("\n(Dry run) would have deleted \(changelogFilePaths.count) unreleased changelog entries.", inColor: .yellow)
let entryNoun = changelogFilePaths.count == 1 ? "entry" : "entries"
OutputController.write("\n(Dry run) would have deleted \(changelogFilePaths.count) unreleased changelog \(entryNoun).", inColor: .yellow)
return
}

Expand All @@ -89,7 +87,7 @@ struct Publish: ParsableCommand {
changelongString.append("\n### \(entryType.title)\n")

groupedEntries[entryType]?.forEach { entry in
changelongString.append("\(entry.text)\n")
changelongString.append("\(entry.text)")
}
})

Expand Down Expand Up @@ -123,7 +121,7 @@ struct Publish: ParsableCommand {
changelog.write(headerData)

groupedEntries[entryType]?.forEach { entry in
changelog.write(Data("\n\(entry.text)".utf8))
changelog.write(Data("\n\(entry.text.trimmingCharacters(in: .newlines))".utf8))
}
}

Expand Down
50 changes: 40 additions & 10 deletions Sources/ChangelogCore/Entry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,30 @@
import Foundation
import ArgumentParser

struct ChangelogEntry: Codable {
struct ChangelogEntry {
let type: EntryType
let text: String

init(type: EntryType, text: String) {
self.type = type
self.text = text
}

init(contentsOf fileURL: URL) throws {
let fileContents = try String(contentsOf: fileURL)
var lines = fileContents.components(separatedBy: .newlines)
let header = lines.removeFirst()

guard let type = EntryType(title: header.components(separatedBy: .whitespaces).last) else {
throw ChangelogError.malformattedEntry(atPath: fileURL.path)
}

self.type = type
text = lines.joined(separator: "\n")
}
}

/// A type of changelog change as defined by Keep a Changelog 1.0.0.
/// A type of changelog change as defined by Keep a Changelog.
///
/// https://keepachangelog.com/en/1.0.0/
enum EntryType: String, Codable, Comparable, ExpressibleByArgument, CaseIterable {
Expand All @@ -34,21 +52,33 @@ enum EntryType: String, Codable, Comparable, ExpressibleByArgument, CaseIterable
case security

var title: String {
switch self {
case .add: return "Added"
case .change: return "Changed"
case .deprecate: return "Deprecated"
case .remove: return "Removed"
case .fix: return "Fixed"
case .security: return "Security"
}
EntryType.titles[self]!
}

private static let titles: [EntryType: String] = [
.add: "Added",
.change: "Changed",
.deprecate: "Deprecated",
.remove: "Removed",
.fix: "Fixed",
.security: "Security"
]
private static var titlesToValues = Dictionary(uniqueKeysWithValues: titles.map({ ($1, $0) }))

init(_ string: String) throws {
guard let entryType = EntryType(rawValue: string) else {
throw ValidationError("Valid types are \(EntryType.allCasesSentenceString).")
}

self = entryType
}

init?(title: String?) {
guard let title = title,
let entryType = EntryType.titlesToValues[title] else {
return nil
}

self = entryType
}
}
8 changes: 7 additions & 1 deletion Sources/ChangelogCore/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@

import Foundation

enum ChangelogError: LocalizedError {
enum ChangelogError: LocalizedError, Equatable {
case changelogDirectoryNotFound(expectedPath: String)
case malformattedEntry(atPath: String)
case noEntriesFound
case noTextEntered
case changelogNotFound
case changelogReleaseAnchorNotFound

var errorDescription: String? {
switch self {
case .changelogDirectoryNotFound(let expectedPath):
return "Couldn't find the changelog directory. Please check that `\(expectedPath)` exists and is readable from your current working directory."
case .malformattedEntry(let path):
return "The changelog entry at \(path) is malformatted. Please fix or remove the file and try again."
case .noEntriesFound:
return "No unreleased changelog entries were found."
case .noTextEntered:
Expand Down
1 change: 0 additions & 1 deletion Sources/changelog-generator/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Created by Patrick Gatewood on 2/19/21.
//

import Foundation
import ChangelogCore

Changelog.main()
23 changes: 12 additions & 11 deletions Tests/changelog-generatorTests/LogCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ class LogCommandTests: XCTestCase {
logCommand.options = Changelog.Options(unreleasedChangelogsDirectory: Changelog.Options.defaultUnreleasedChangelogDirectory)

XCTAssertThrowsError(try logCommand.run()) { error in
XCTAssertEqual((error as NSError).code, NSFileNoSuchFileError)
guard let changelogError = error as? ChangelogError,
case .changelogDirectoryNotFound(_) = changelogError else {
XCTFail("Expected ChangelogError.changelogDirectoryNotFound to be thrown")
return
}
}
}

Expand All @@ -36,11 +40,9 @@ class LogCommandTests: XCTestCase {
try logCommand.run()

let entryFile = try XCTUnwrap(try FileManager.default.contentsOfDirectory(at: directory.asURL, includingPropertiesForKeys: nil).first)
let entry = try JSONDecoder().decode(
ChangelogEntry.self,
from: Data(contentsOf: entryFile))
let entry = try ChangelogEntry(contentsOf: entryFile)

let formattedSampleText = "- \(sampleAdditionText)"
let formattedSampleText = "- \(sampleAdditionText)\n"

XCTAssertEqual(entry.type, .add)
XCTAssertEqual(entry.text, formattedSampleText)
Expand All @@ -61,15 +63,14 @@ class LogCommandTests: XCTestCase {
try logCommand.run()

let entryFile = try XCTUnwrap(try FileManager.default.contentsOfDirectory(at: directory.asURL, includingPropertiesForKeys: nil).first)
let entry = try JSONDecoder().decode(
ChangelogEntry.self,
from: Data(contentsOf: entryFile))
let entry = try ChangelogEntry(contentsOf: entryFile)

let formattedSampleText =
"""
- \(sampleFixBullets[0])
- \(sampleFixBullets[1])
"""
- \(sampleFixBullets[0])
- \(sampleFixBullets[1])
"""

XCTAssertEqual(entry.type, .fix)
XCTAssertEqual(entry.text, formattedSampleText)
Expand Down
Loading

0 comments on commit e018761

Please sign in to comment.