Skip to content

Commit f188609

Browse files
committed
Merge branch 'master' of github.com:WeTransfer/GitBuddy
2 parents c59c006 + 37bca05 commit f188609

File tree

14 files changed

+579
-31
lines changed

14 files changed

+579
-31
lines changed

Package.resolved

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ let package = Package(
1212
],
1313
dependencies: [
1414
.package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "2.1.0")),
15-
.package(url: "https://github.com/nerdishbynature/octokit.swift", .upToNextMajor(from: "0.10.1")),
15+
// .package(path: "../../Forks/octokit.swift"),
16+
// .package(url: "https://github.com/nerdishbynature/octokit.swift", .upToNextMajor(from: "0.10.1")),
17+
.package(url: "https://github.com/AvdLee/octokit.swift", .branch("master")),
1618
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.0.0"))
1719
],
1820
targets: [

Sources/GitBuddyCore/Commands/GitBuddy.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public struct GitBuddy: ParsableCommand {
1717
commandName: "gitbuddy",
1818
abstract: "Manage your GitHub repositories with ease",
1919
version: Self.version,
20-
subcommands: [ChangelogCommand.self, ReleaseCommand.self]
20+
subcommands: [ChangelogCommand.self, ReleaseCommand.self, TagDeletionsCommand.self]
2121
)
2222

2323
public init() { }
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Foundation
2+
import ArgumentParser
3+
4+
struct TagDeletionsCommand: ParsableCommand {
5+
6+
public static let configuration = CommandConfiguration(commandName: "tagDeletion", abstract: "Delete a batch of tags based on given predicates.")
7+
8+
@Option(name: .shortAndLong, help: "The date of this tag will be used as a limit. Defaults to the latest tag.")
9+
private var upUntilTag: String?
10+
11+
@Option(name: .shortAndLong, help: "The limit of tags to delete in this batch. Defaults to 50")
12+
private var limit: Int = 50
13+
14+
@Flag(name: .long, help: "Delete pre releases only")
15+
private var prereleaseOnly: Bool = false
16+
17+
@Flag(name: .long, help: "Does not actually delete but just logs which tags would be deleted")
18+
private var dryRun: Bool = false
19+
20+
@Flag(name: .long, help: "Show extra logging for debugging purposes")
21+
var verbose: Bool = false
22+
23+
func run() throws {
24+
Log.isVerbose = verbose
25+
26+
let tagsDeleter = try TagsDeleter(upUntilTagName: upUntilTag, limit: limit, prereleaseOnly: prereleaseOnly, dryRun: dryRun)
27+
let deletedTags = try tagsDeleter.run()
28+
29+
guard !deletedTags.isEmpty else {
30+
Log.message("There were no tags found to be deleted.")
31+
return
32+
}
33+
Log.message("Deleted tags: \(deletedTags)")
34+
}
35+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// DateFormatters.swift
3+
//
4+
//
5+
// Created by Antoine van der Lee on 25/01/2022.
6+
// Copyright © 2020 WeTransfer. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
enum Formatter {
12+
static let gitDateFormatter: DateFormatter = {
13+
let dateFormatter = DateFormatter()
14+
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
15+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
16+
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
17+
return dateFormatter
18+
}()
19+
}

Sources/GitBuddyCore/Helpers/Shell.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ enum ShellCommand {
1414
case previousTag
1515
case repositoryName
1616
case tagCreationDate(tag: String)
17+
case commitDate(commitish: String)
1718

1819
var rawValue: String {
1920
switch self {
@@ -27,6 +28,8 @@ enum ShellCommand {
2728
return "git remote show origin -n | ruby -ne 'puts /^\\s*Fetch.*(:|\\/){1}([^\\/]+\\/[^\\/]+).git/.match($_)[2] rescue nil'"
2829
case .tagCreationDate(let tag):
2930
return "git log -1 --format=%ai \(tag)"
31+
case .commitDate(let commitish):
32+
return "git show -s --format=%ai \(commitish)"
3033
}
3134
}
3235
}
@@ -43,9 +46,11 @@ extension Process {
4346
let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
4447
guard let outputData = String(data: data, encoding: String.Encoding.utf8) else { return "" }
4548

46-
return outputData.reduce("") { (result, value) in
47-
return result + String(value)
48-
}.trimmingCharacters(in: .whitespacesAndNewlines)
49+
return outputData
50+
.reduce("") { (result, value) in
51+
return result + String(value)
52+
}
53+
.trimmingCharacters(in: .whitespacesAndNewlines)
4954
}
5055
}
5156

Sources/GitBuddyCore/Models/Tag.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,13 @@ struct Tag: ShellInjectable, Encodable {
4040
} else {
4141
let tagCreationDate = Self.shell.execute(.tagCreationDate(tag: name))
4242
if tagCreationDate.isEmpty {
43-
Log.debug("Tag creation date could not be found")
43+
Log.debug("Tag creation date could not be found for \(name)")
4444
throw Error.missingTagCreationDate
4545
}
4646

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

49-
let dateFormatter = DateFormatter()
50-
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
51-
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
52-
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
53-
54-
guard let date = dateFormatter.date(from: tagCreationDate) else {
49+
guard let date = Formatter.gitDateFormatter.date(from: tagCreationDate) else {
5550
throw Error.missingTagCreationDate
5651
}
5752
self.created = date

Sources/GitBuddyCore/Release/ReleaseProducer.swift

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,17 @@ final class ReleaseProducer: URLSessionInjectable, ShellInjectable {
5353
}
5454

5555
@discardableResult public func run(isSectioned: Bool) throws -> Release {
56-
/// If a tagname exists, it means we're creating a new tag.
57-
/// In this case, we need another way to fetch the from date for the changelog.
58-
if tagName != nil, changelogToTag == nil {
59-
throw Error.changelogTargetDateMissing
60-
}
61-
62-
let changelogToTag = try changelogToTag.map { try Tag(name: $0) } ?? Tag.latest()
63-
let changelogSinceTag = lastReleaseTag ?? Self.shell.execute(.previousTag)
56+
let changelogToDate = try fetchChangelogToDate()
6457

6558
/// We're adding 60 seconds to make sure the tag commit itself is included in the changelog as well.
66-
let toDate = changelogToTag.created.addingTimeInterval(60)
67-
let changelogProducer = try ChangelogProducer(since: .tag(tag: changelogSinceTag), to: toDate, baseBranch: baseBranch)
59+
let adjustedChangelogToDate = changelogToDate.addingTimeInterval(60)
60+
61+
let changelogSinceTag = lastReleaseTag ?? Self.shell.execute(.previousTag)
62+
let changelogProducer = try ChangelogProducer(since: .tag(tag: changelogSinceTag), to: adjustedChangelogToDate, baseBranch: baseBranch)
6863
let changelog = try changelogProducer.run(isSectioned: isSectioned)
6964
Log.debug("\(changelog)\n")
7065

71-
let tagName = tagName ?? changelogToTag.name
66+
let tagName = try tagName ?? Tag.latest().name
7267
try updateChangelogFile(adding: changelog.description, for: tagName)
7368

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

79+
private func fetchChangelogToDate() throws -> Date {
80+
if tagName != nil {
81+
/// If a tagname exists, it means we're creating a new tag.
82+
/// In this case, we need another way to fetch the `to` date for the changelog.
83+
///
84+
/// One option is using the `changelogToTag`:
85+
if let changelogToTag = changelogToTag {
86+
return try Tag(name: changelogToTag).created
87+
} else if let targetCommitishDate = targetCommitishDate() {
88+
/// We fallback to the target commit date, covering cases in which we create a release
89+
/// from a certain branch
90+
return targetCommitishDate
91+
} else {
92+
/// Since we were unable to fetch the date
93+
throw Error.changelogTargetDateMissing
94+
}
95+
} else {
96+
/// This is the scenario of creating a release for an already created tag.
97+
return try Tag.latest().created
98+
}
99+
}
100+
101+
private func targetCommitishDate() -> Date? {
102+
guard let targetCommitish = targetCommitish else {
103+
return nil
104+
}
105+
let commitishDate = Self.shell.execute(.commitDate(commitish: targetCommitish))
106+
return Formatter.gitDateFormatter.date(from: commitishDate)
107+
}
108+
84109
private func postComments(for changelog: Changelog, project: GITProject, release: Release) {
85110
guard !skipComments else {
86111
Log.debug("Skipping comments")
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import Foundation
2+
import OctoKit
3+
4+
final class TagsDeleter: URLSessionInjectable, ShellInjectable {
5+
6+
private lazy var octoKit: Octokit = Octokit()
7+
let upUntilTagName: String?
8+
let limit: Int
9+
let prereleaseOnly: Bool
10+
let dryRun: Bool
11+
12+
init(upUntilTagName: String? = nil, limit: Int, prereleaseOnly: Bool, dryRun: Bool) throws {
13+
try Octokit.authenticate()
14+
15+
self.upUntilTagName = upUntilTagName
16+
self.limit = limit
17+
self.prereleaseOnly = prereleaseOnly
18+
self.dryRun = dryRun
19+
}
20+
21+
@discardableResult public func run() throws -> [String] {
22+
let upUntilTag = try upUntilTagName.map { try Tag(name: $0) } ?? Tag.latest()
23+
Log.debug("Deleting up to \(limit) tags before \(upUntilTag.name) (Dry run: \(dryRun.description))")
24+
25+
let currentProject = GITProject.current()
26+
let releases = try fetchReleases(project: currentProject, upUntil: upUntilTag.created)
27+
28+
guard !releases.isEmpty else {
29+
return []
30+
}
31+
try deleteReleases(releases, project: currentProject)
32+
try deleteTags(releases, project: currentProject)
33+
34+
return releases.map { $0.tagName }
35+
}
36+
37+
private func fetchReleases(project: GITProject, upUntil: Date) throws -> [OctoKit.Release] {
38+
let group = DispatchGroup()
39+
group.enter()
40+
41+
var result: Result<[OctoKit.Release], Swift.Error>!
42+
octoKit.listReleases(urlSession, owner: project.organisation, repository: project.repository, perPage: 100) { response in
43+
result = response
44+
group.leave()
45+
}
46+
group.wait()
47+
48+
let releases = try result.get()
49+
Log.debug("Fetched releases: \(releases.map { $0.tagName }.joined(separator: ", "))")
50+
51+
return releases
52+
.filter({ release in
53+
guard !prereleaseOnly || release.prerelease else {
54+
return false
55+
}
56+
return release.createdAt < upUntil
57+
})
58+
.suffix(limit)
59+
}
60+
61+
private func deleteReleases(_ releases: [OctoKit.Release], project: GITProject) throws {
62+
let releasesToDelete = releases.map { $0.tagName }
63+
Log.debug("Deleting tags: \(releasesToDelete.joined(separator: ", "))")
64+
65+
let group = DispatchGroup()
66+
var lastError: Error?
67+
for release in releases {
68+
group.enter()
69+
Log.debug("Deleting release \(release.tagName) with id \(release.id) url: \(release.htmlURL)")
70+
guard !dryRun else {
71+
group.leave()
72+
return
73+
}
74+
75+
octoKit.deleteRelease(urlSession, owner: project.organisation, repository: project.repository, releaseId: release.id) { error in
76+
defer { group.leave() }
77+
guard let error = error else {
78+
Log.debug("Successfully deleted release \(release.tagName)")
79+
return
80+
}
81+
Log.debug("Deletion of release \(release.tagName) failed: \(error)")
82+
lastError = error
83+
}
84+
}
85+
group.wait()
86+
87+
if let lastError = lastError {
88+
throw lastError
89+
}
90+
}
91+
92+
private func deleteTags(_ releases: [OctoKit.Release], project: GITProject) throws {
93+
let tagsToDelete = releases.map { $0.tagName }
94+
Log.debug("Deleting tags: \(tagsToDelete.joined(separator: ", "))")
95+
96+
let group = DispatchGroup()
97+
var lastError: Error?
98+
for release in releases {
99+
group.enter()
100+
Log.debug("Deleting tag \(release.tagName) with id \(release.id) url: \(release.htmlURL)")
101+
guard !dryRun else {
102+
group.leave()
103+
return
104+
}
105+
106+
octoKit.deleteReference(urlSession, owner: project.organisation, repository: project.repository, ref: "tags/\(release.tagName)") { error in
107+
defer { group.leave() }
108+
guard let error = error else {
109+
Log.debug("Successfully deleted tag \(release.tagName)")
110+
return
111+
}
112+
Log.debug("Deletion of tag \(release.tagName) failed: \(error)")
113+
lastError = error
114+
}
115+
}
116+
group.wait()
117+
if let lastError = lastError {
118+
throw lastError
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)