Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base setup #3

Merged
merged 18 commits into from
Jan 20, 2020
Merged
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
23 changes: 15 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@ import PackageDescription

let package = Package(
name: "ChangelogProducer",
platforms: [
.macOS(.v10_15)
],
products: [
.executable(name: "ChangelogProducer", targets: ["ChangelogProducer"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/nerdishbynature/octokit.swift", from: "0.9.0"),
.package(url: "https://github.com/apple/swift-package-manager.git", from: "0.1.0"),
.package(url: "https://github.com/WeTransfer/Mocker.git", from: "2.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "ChangelogProducer",
dependencies: ["ChangelogProducerCore"]),
.target(name: "ChangelogProducerCore"),
.testTarget(
name: "ChangelogProducerTests",
dependencies: ["ChangelogProducer"]),
.target(name: "ChangelogProducer",
dependencies: ["ChangelogProducerCore"]),
.target(name: "ChangelogProducerCore",
dependencies: ["OctoKit", "SPMUtility"]),
.testTarget(name: "ChangelogProducerTests",
dependencies: ["ChangelogProducer", "Mocker"]),
]
)
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
# ChangelogProducer
A changelog generator written in Swift for GitHub repositories.

### Installation using [Mint](https://github.com/yonaskolb/mint)
You can install the Changelog Producer using Mint as follows:

```
$ mint install WeTransfer/ChangelogProducer
```

After that you can directly use it:

```bash
$ changelogproducer --help
OVERVIEW: Create a changelog for GitHub repositories

USAGE: ChangelogProducer <options>

OPTIONS:
--baseBranch, -b The base branch to compare with
--sinceTag, -s The tag to use as a base
--verbose Show extra logging for debugging purposes
--help Display available options
```

### Development
- `cd` into the repository
- run `swift package generate-xcodeproj` (Generates an Xcode project for development)
- run `swift run` to test out any changes
- Run the following command from the project you're using it for:

```bash
swift run --package-path ../ChangelogProducer/ ChangelogProducer -s 4.3.0b13951 -b develop --verbose
```

### Useful resources
- [Building a command line tool using the Swift Package Manager](https://www.swiftbysundell.com/articles/building-a-command-line-tool-using-the-swift-package-manager/)
11 changes: 9 additions & 2 deletions Sources/ChangelogProducer/main.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import ChangelogProducerCore
//
// Main.swift
// ChangelogProducer
//
// Created by Antoine van der Lee on 16/01/2020.
// Copyright © 2020 WeTransfer. All rights reserved.
//

let changelogProducer = ChangelogProducer()
import ChangelogProducerCore

do {
let changelogProducer = try ChangelogProducer()
try changelogProducer.run()
} catch {
print("Whoops! An error occurred: \(error)")
Expand Down
22 changes: 22 additions & 0 deletions Sources/ChangelogProducerCore/ChangelogBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// ChangelogBuilder.swift
// ChangelogProducerCore
//
// Created by Antoine van der Lee on 16/01/2020.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the copyright be included in the files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll add that!

// Copyright © 2020 WeTransfer. All rights reserved.
//

import Foundation
import OctoKit

struct ChangelogBuilder {
let items: [ChangelogItem]

func build() -> String {
return items
.compactMap { $0.title }
.filter { !$0.lowercased().contains("#trivial") }
.map { "- \($0)" }
.joined(separator: "\n")
}
}
28 changes: 28 additions & 0 deletions Sources/ChangelogProducerCore/ChangelogItemsFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// ChangelogItemsFactory.swift
// ChangelogProducerCore
//
// Created by Antoine van der Lee on 16/01/2020.
// Copyright © 2020 WeTransfer. All rights reserved.
//

import Foundation
import OctoKit

struct ChangelogItemsFactory {
let octoKit: Octokit
let pullRequests: [PullRequest]
let project: GITProject

func items(using session: URLSession = URLSession.shared) -> [ChangelogItem] {
return pullRequests.flatMap { pullRequest -> [ChangelogItem] in
let issuesResolver = IssuesResolver(octoKit: octoKit, project: project, input: pullRequest)
guard let resolvedIssues = issuesResolver.resolve(using: session), !resolvedIssues.isEmpty else {
return [ChangelogItem(input: pullRequest, closedBy: pullRequest)]
}
return resolvedIssues.map { issue -> ChangelogItem in
return ChangelogItem(input: issue, closedBy: pullRequest)
}
}
}
}
59 changes: 53 additions & 6 deletions Sources/ChangelogProducerCore/ChangelogProducer.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,67 @@
//
// ChangelogProducer.swift
//
// ChangelogProducerCore
//
// Created by Antoine van der Lee on 10/01/2020.
// Copyright © 2020 WeTransfer. All rights reserved.
//

import Foundation
import OctoKit
import SPMUtility

public final class ChangelogProducer {
private let arguments: [String]

public init(arguments: [String] = CommandLine.arguments) {
self.arguments = arguments
enum Error: Swift.Error {
case missingDangerToken
}

public func run() throws {
print("Hello world")
let octoKit: Octokit
let base: Branch
let latestRelease: Release
let project: GITProject

public init(
environment: [String: String] = ProcessInfo.processInfo.environment,
arguments: [String] = ProcessInfo.processInfo.arguments) throws {
let parser = ArgumentParser(usage: "<options>", overview: "Create a changelog for GitHub repositories")
let sinceTag = parser.add(option: "--sinceTag", shortName: "-s", kind: String.self, usage: "The tag to use as a base")
let baseBranch = parser.add(option: "--baseBranch", shortName: "-b", kind: String.self, usage: "The base branch to compare with")
let verbose = parser.add(option: "--verbose", kind: Bool.self, usage: "Show extra logging for debugging purposes")

guard let gitHubAPIToken = environment["DANGER_GITHUB_API_TOKEN"] else {
throw Error.missingDangerToken
}

let config = TokenConfiguration(gitHubAPIToken)
octoKit = Octokit(config)

// The first argument is always the executable, drop it
let arguments = Array(arguments.dropFirst())
let parsedArguments = try parser.parse(arguments)

Log.isVerbose = parsedArguments.get(verbose) ?? false

if let tag = parsedArguments.get(sinceTag) {
latestRelease = try Release(tag: tag)
} else {
latestRelease = try Release.latest()
}
base = parsedArguments.get(baseBranch) ?? "master"
project = GITProject.current()
}

@discardableResult public func run(using session: URLSession = URLSession.shared) throws -> String {
Log.debug("Latest release is \(latestRelease.tag)")

let pullRequestsFetcher = PullRequestFetcher(octoKit: octoKit, base: base, project: project)
let pullRequests = try pullRequestsFetcher.fetchAllAfter(latestRelease, using: session)
let items = ChangelogItemsFactory(octoKit: octoKit, pullRequests: pullRequests, project: project).items(using: session)
let changelog = ChangelogBuilder(items: items).build()

Log.debug("Generated changelog:\n")
Log.message(changelog)

return changelog
}
}
22 changes: 22 additions & 0 deletions Sources/ChangelogProducerCore/Helpers/Log.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Log.swift
// ChangelogProducerCore
//
// Created by Antoine van der Lee on 10/01/2020.
// Copyright © 2020 WeTransfer. All rights reserved.
//

import Foundation

struct Log {
static var isVerbose: Bool = false

static func debug(_ message: Any) {
guard isVerbose else { return }
print(message)
}

static func message(_ message: Any) {
print(message)
}
}
48 changes: 48 additions & 0 deletions Sources/ChangelogProducerCore/Helpers/Shell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// Shell.swift
// ChangelogProducerCore
//
// Created by Antoine van der Lee on 10/01/2020.
// Copyright © 2020 WeTransfer. All rights reserved.
//

import Foundation

extension Process {
public func shell(command: String) -> String {
launchPath = "/bin/bash"
arguments = ["-c", command]

let outputPipe = Pipe()
standardOutput = outputPipe
launch()

let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
guard let outputData = String(data: data, encoding: String.Encoding.utf8) else { return "" }

return outputData.reduce("") { (result, value) in
return result + String(value)
}
}
}

protocol ShellExecuting {
@discardableResult static func execute(_ command: String) -> String
}

private enum Shell: ShellExecuting {
@discardableResult static func execute(_ command: String) -> String {
return Process().shell(command: command)
}
}

/// Adds a `shell` property which defaults to `Shell.self`.
protocol ShellInjectable { }

extension ShellInjectable {
static var shell: ShellExecuting.Type { ShellInjector.shell }
}

enum ShellInjector {
static var shell: ShellExecuting.Type = Shell.self
}
88 changes: 88 additions & 0 deletions Sources/ChangelogProducerCore/IssuesResolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// IssuesResolver.swift
// ChangelogProducerCore
//
// Created by Antoine van der Lee on 10/01/2020.
// Copyright © 2020 WeTransfer. All rights reserved.
//

import Foundation
import OctoKit

struct IssuesResolver {
let octoKit: Octokit
let project: GITProject
let input: ChangelogInput

func resolve(using session: URLSession = URLSession.shared) -> [Issue]? {
guard let resolvedIssueNumbers = input.body?.resolvingIssues(), !resolvedIssueNumbers.isEmpty else {
return nil
}
return issues(for: resolvedIssueNumbers, using: session)
}

private func issues(for issueNumbers: [Int], using session: URLSession) -> [Issue] {
var issues: [Issue] = []
let dispatchGroup = DispatchGroup()
for issueNumber in issueNumbers {
dispatchGroup.enter()
octoKit.issue(session, owner: project.organisation, repository: project.repository, number: issueNumber) { response in
switch response {
case .success(let issue):
issues.append(issue)
case .failure(let error):
print("Fetching issue \(issueNumber) failed with \(error)")
}
dispatchGroup.leave()
}
}
dispatchGroup.wait()
return issues
}
}

extension String {

/// Extracts the resolved issues from a Pull Request body.
func resolvingIssues() -> [Int] {
var resolvedIssues = Set<Int>()

let splits = split(separator: "#")

let issueClosingKeywords = [
"close ",
"closes ",
"closed ",
"fix ",
"fixes ",
"fixed ",
"resolve ",
"resolves ",
"resolved "
]

for (index, split) in splits.enumerated() {
let lowerCaseSplit = split.lowercased()

for keyword in issueClosingKeywords {
if lowerCaseSplit.hasSuffix(keyword) {
guard index + 1 <= splits.count - 1 else { break }
let nextSplit = splits[index + 1]

let numberPrefixString = nextSplit.prefix { (character) -> Bool in
return character.isNumber
}

if !numberPrefixString.isEmpty, let numberPrefix = Int(numberPrefixString.description) {
resolvedIssues.insert(numberPrefix)
break
} else {
continue
}
}
}
}

return Array(resolvedIssues)
}
}
Loading