Skip to content

Commit

Permalink
Merge branch 'master' of github.com:WeTransfer/GitBuddy
Browse files Browse the repository at this point in the history
  • Loading branch information
AvdLee committed Jan 26, 2022
2 parents c59c006 + 37bca05 commit f188609
Show file tree
Hide file tree
Showing 14 changed files with 579 additions and 31 deletions.
12 changes: 6 additions & 6 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
},
{
"package": "OctoKit",
"repositoryURL": "https://github.com/nerdishbynature/octokit.swift",
"repositoryURL": "https://github.com/AvdLee/octokit.swift",
"state": {
"branch": null,
"revision": "9521cdff919053868ab13cd08a228f7bc1bde2a9",
"version": "0.11.0"
"branch": "master",
"revision": "c1d49d82fa34d8022b0cdc95ca94113a54a4ec81",
"version": null
}
},
{
"package": "RequestKit",
"repositoryURL": "https://github.com/nerdishbynature/RequestKit.git",
"state": {
"branch": null,
"revision": "fd5e9e99aada7432170366c9e95967011ce13bad",
"version": "2.4.0"
"revision": "e266fd8ac7d71caf4422ffc56ad98ff368329fde",
"version": "3.1.0"
}
},
{
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "2.1.0")),
.package(url: "https://github.com/nerdishbynature/octokit.swift", .upToNextMajor(from: "0.10.1")),
// .package(path: "../../Forks/octokit.swift"),
// .package(url: "https://github.com/nerdishbynature/octokit.swift", .upToNextMajor(from: "0.10.1")),
.package(url: "https://github.com/AvdLee/octokit.swift", .branch("master")),
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.0.0"))
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion Sources/GitBuddyCore/Commands/GitBuddy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public struct GitBuddy: ParsableCommand {
commandName: "gitbuddy",
abstract: "Manage your GitHub repositories with ease",
version: Self.version,
subcommands: [ChangelogCommand.self, ReleaseCommand.self]
subcommands: [ChangelogCommand.self, ReleaseCommand.self, TagDeletionsCommand.self]
)

public init() { }
Expand Down
35 changes: 35 additions & 0 deletions Sources/GitBuddyCore/Commands/TagDeletionsCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import ArgumentParser

struct TagDeletionsCommand: ParsableCommand {

public static let configuration = CommandConfiguration(commandName: "tagDeletion", abstract: "Delete a batch of tags based on given predicates.")

@Option(name: .shortAndLong, help: "The date of this tag will be used as a limit. Defaults to the latest tag.")
private var upUntilTag: String?

@Option(name: .shortAndLong, help: "The limit of tags to delete in this batch. Defaults to 50")
private var limit: Int = 50

@Flag(name: .long, help: "Delete pre releases only")
private var prereleaseOnly: Bool = false

@Flag(name: .long, help: "Does not actually delete but just logs which tags would be deleted")
private var dryRun: Bool = false

@Flag(name: .long, help: "Show extra logging for debugging purposes")
var verbose: Bool = false

func run() throws {
Log.isVerbose = verbose

let tagsDeleter = try TagsDeleter(upUntilTagName: upUntilTag, limit: limit, prereleaseOnly: prereleaseOnly, dryRun: dryRun)
let deletedTags = try tagsDeleter.run()

guard !deletedTags.isEmpty else {
Log.message("There were no tags found to be deleted.")
return
}
Log.message("Deleted tags: \(deletedTags)")
}
}
19 changes: 19 additions & 0 deletions Sources/GitBuddyCore/Helpers/DateFormatters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// DateFormatters.swift
//
//
// Created by Antoine van der Lee on 25/01/2022.
// Copyright © 2020 WeTransfer. All rights reserved.
//

import Foundation

enum Formatter {
static let gitDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter
}()
}
11 changes: 8 additions & 3 deletions Sources/GitBuddyCore/Helpers/Shell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum ShellCommand {
case previousTag
case repositoryName
case tagCreationDate(tag: String)
case commitDate(commitish: String)

var rawValue: String {
switch self {
Expand All @@ -27,6 +28,8 @@ enum ShellCommand {
return "git remote show origin -n | ruby -ne 'puts /^\\s*Fetch.*(:|\\/){1}([^\\/]+\\/[^\\/]+).git/.match($_)[2] rescue nil'"
case .tagCreationDate(let tag):
return "git log -1 --format=%ai \(tag)"
case .commitDate(let commitish):
return "git show -s --format=%ai \(commitish)"
}
}
}
Expand All @@ -43,9 +46,11 @@ extension Process {
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)
}.trimmingCharacters(in: .whitespacesAndNewlines)
return outputData
.reduce("") { (result, value) in
return result + String(value)
}
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}

Expand Down
9 changes: 2 additions & 7 deletions Sources/GitBuddyCore/Models/Tag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,13 @@ struct Tag: ShellInjectable, Encodable {
} else {
let tagCreationDate = Self.shell.execute(.tagCreationDate(tag: name))
if tagCreationDate.isEmpty {
Log.debug("Tag creation date could not be found")
Log.debug("Tag creation date could not be found for \(name)")
throw Error.missingTagCreationDate
}

Log.debug("Tag \(name) is created at \(tagCreationDate)")

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)

guard let date = dateFormatter.date(from: tagCreationDate) else {
guard let date = Formatter.gitDateFormatter.date(from: tagCreationDate) else {
throw Error.missingTagCreationDate
}
self.created = date
Expand Down
47 changes: 36 additions & 11 deletions Sources/GitBuddyCore/Release/ReleaseProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,17 @@ final class ReleaseProducer: URLSessionInjectable, ShellInjectable {
}

@discardableResult public func run(isSectioned: Bool) throws -> Release {
/// If a tagname exists, it means we're creating a new tag.
/// In this case, we need another way to fetch the from date for the changelog.
if tagName != nil, changelogToTag == nil {
throw Error.changelogTargetDateMissing
}

let changelogToTag = try changelogToTag.map { try Tag(name: $0) } ?? Tag.latest()
let changelogSinceTag = lastReleaseTag ?? Self.shell.execute(.previousTag)
let changelogToDate = try fetchChangelogToDate()

/// We're adding 60 seconds to make sure the tag commit itself is included in the changelog as well.
let toDate = changelogToTag.created.addingTimeInterval(60)
let changelogProducer = try ChangelogProducer(since: .tag(tag: changelogSinceTag), to: toDate, baseBranch: baseBranch)
let adjustedChangelogToDate = changelogToDate.addingTimeInterval(60)

let changelogSinceTag = lastReleaseTag ?? Self.shell.execute(.previousTag)
let changelogProducer = try ChangelogProducer(since: .tag(tag: changelogSinceTag), to: adjustedChangelogToDate, baseBranch: baseBranch)
let changelog = try changelogProducer.run(isSectioned: isSectioned)
Log.debug("\(changelog)\n")

let tagName = tagName ?? changelogToTag.name
let tagName = try tagName ?? Tag.latest().name
try updateChangelogFile(adding: changelog.description, for: tagName)

let repositoryName = Self.shell.execute(.repositoryName)
Expand All @@ -81,6 +76,36 @@ final class ReleaseProducer: URLSessionInjectable, ShellInjectable {
return release
}

private func fetchChangelogToDate() throws -> Date {
if tagName != nil {
/// If a tagname exists, it means we're creating a new tag.
/// In this case, we need another way to fetch the `to` date for the changelog.
///
/// One option is using the `changelogToTag`:
if let changelogToTag = changelogToTag {
return try Tag(name: changelogToTag).created
} else if let targetCommitishDate = targetCommitishDate() {
/// We fallback to the target commit date, covering cases in which we create a release
/// from a certain branch
return targetCommitishDate
} else {
/// Since we were unable to fetch the date
throw Error.changelogTargetDateMissing
}
} else {
/// This is the scenario of creating a release for an already created tag.
return try Tag.latest().created
}
}

private func targetCommitishDate() -> Date? {
guard let targetCommitish = targetCommitish else {
return nil
}
let commitishDate = Self.shell.execute(.commitDate(commitish: targetCommitish))
return Formatter.gitDateFormatter.date(from: commitishDate)
}

private func postComments(for changelog: Changelog, project: GITProject, release: Release) {
guard !skipComments else {
Log.debug("Skipping comments")
Expand Down
121 changes: 121 additions & 0 deletions Sources/GitBuddyCore/Tag Deletions/TagsDeleter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Foundation
import OctoKit

final class TagsDeleter: URLSessionInjectable, ShellInjectable {

private lazy var octoKit: Octokit = Octokit()
let upUntilTagName: String?
let limit: Int
let prereleaseOnly: Bool
let dryRun: Bool

init(upUntilTagName: String? = nil, limit: Int, prereleaseOnly: Bool, dryRun: Bool) throws {
try Octokit.authenticate()

self.upUntilTagName = upUntilTagName
self.limit = limit
self.prereleaseOnly = prereleaseOnly
self.dryRun = dryRun
}

@discardableResult public func run() throws -> [String] {
let upUntilTag = try upUntilTagName.map { try Tag(name: $0) } ?? Tag.latest()
Log.debug("Deleting up to \(limit) tags before \(upUntilTag.name) (Dry run: \(dryRun.description))")

let currentProject = GITProject.current()
let releases = try fetchReleases(project: currentProject, upUntil: upUntilTag.created)

guard !releases.isEmpty else {
return []
}
try deleteReleases(releases, project: currentProject)
try deleteTags(releases, project: currentProject)

return releases.map { $0.tagName }
}

private func fetchReleases(project: GITProject, upUntil: Date) throws -> [OctoKit.Release] {
let group = DispatchGroup()
group.enter()

var result: Result<[OctoKit.Release], Swift.Error>!
octoKit.listReleases(urlSession, owner: project.organisation, repository: project.repository, perPage: 100) { response in
result = response
group.leave()
}
group.wait()

let releases = try result.get()
Log.debug("Fetched releases: \(releases.map { $0.tagName }.joined(separator: ", "))")

return releases
.filter({ release in
guard !prereleaseOnly || release.prerelease else {
return false
}
return release.createdAt < upUntil
})
.suffix(limit)
}

private func deleteReleases(_ releases: [OctoKit.Release], project: GITProject) throws {
let releasesToDelete = releases.map { $0.tagName }
Log.debug("Deleting tags: \(releasesToDelete.joined(separator: ", "))")

let group = DispatchGroup()
var lastError: Error?
for release in releases {
group.enter()
Log.debug("Deleting release \(release.tagName) with id \(release.id) url: \(release.htmlURL)")
guard !dryRun else {
group.leave()
return
}

octoKit.deleteRelease(urlSession, owner: project.organisation, repository: project.repository, releaseId: release.id) { error in
defer { group.leave() }
guard let error = error else {
Log.debug("Successfully deleted release \(release.tagName)")
return
}
Log.debug("Deletion of release \(release.tagName) failed: \(error)")
lastError = error
}
}
group.wait()

if let lastError = lastError {
throw lastError
}
}

private func deleteTags(_ releases: [OctoKit.Release], project: GITProject) throws {
let tagsToDelete = releases.map { $0.tagName }
Log.debug("Deleting tags: \(tagsToDelete.joined(separator: ", "))")

let group = DispatchGroup()
var lastError: Error?
for release in releases {
group.enter()
Log.debug("Deleting tag \(release.tagName) with id \(release.id) url: \(release.htmlURL)")
guard !dryRun else {
group.leave()
return
}

octoKit.deleteReference(urlSession, owner: project.organisation, repository: project.repository, ref: "tags/\(release.tagName)") { error in
defer { group.leave() }
guard let error = error else {
Log.debug("Successfully deleted tag \(release.tagName)")
return
}
Log.debug("Deletion of tag \(release.tagName) failed: \(error)")
lastError = error
}
}
group.wait()
if let lastError = lastError {
throw lastError
}
}
}
2 changes: 1 addition & 1 deletion Submodules/WeTransfer-iOS-CI
34 changes: 34 additions & 0 deletions Tests/GitBuddyTests/Release/ReleaseProducerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,40 @@ final class ReleaseProducerTests: XCTestCase {
wait(for: [mockExpectation], timeout: 0.3)
}

func testUsingTargetCommitishDate() throws {
let existingChangelog = """
### 1.0.0
- Initial release
"""
let tagName = "2.0.0b1233"
let changelogToTag = "2.0.0b1232"
let commitish = "1e88145e2101dc7203af3095d6e"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let date = dateFormatter.date(from: "2020-01-05")!
MockedShell.mockCommitish(commitish, date: date)
MockedShell.mockRelease(tag: changelogToTag, date: date)

let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("Changelog.md")
XCTAssertTrue(FileManager.default.createFile(atPath: tempFileURL.path, contents: Data(existingChangelog.utf8), attributes: nil))

try executeCommand("gitbuddy release --target-commitish \(commitish) -s -n \(tagName) -c \(tempFileURL.path)")

let updatedChangelogContents = try String(contentsOfFile: tempFileURL.path)

// Merged at: 2020-01-06 - Add charset utf-8 to html head
// Closed at: 2020-01-03 - Get warning for file
// Setting the tag date to 2020-01-05 should remove the Add charset

XCTAssertEqual(updatedChangelogContents, """
### 2.0.0b1233
- Get warning for file \'style.css\' after building \
([#39](https://github.com/WeTransfer/Diagnostics/issues/39)) via [@AvdLee](https://github.com/AvdLee)
\(existingChangelog)
""")
}

func testThrowsMissingTargetDateError() {
XCTAssertThrowsError(try executeCommand("gitbuddy release -s -n 3.0.0"), "Missing target date error should be thrown") { error in
XCTAssertEqual(error as? ReleaseProducer.Error, .changelogTargetDateMissing)
Expand Down
Loading

0 comments on commit f188609

Please sign in to comment.