Skip to content

Commit a02bb4c

Browse files
committed
Cleaned up settings loading code
1 parent ebeee83 commit a02bb4c

File tree

2 files changed

+81
-69
lines changed

2 files changed

+81
-69
lines changed

Sources/CodeOwnersTool/CodeOwnersTool.swift

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,42 @@ import ArgumentParser
33
import CodeOwners
44
import SwiftParser
55

6+
private let defaultSettings: Settings = {
7+
let fm = FileManager.default
8+
if let root = fm.gitRoot, let settings = try? readSettings(atRoot: root) { return settings }
9+
return try! readSettings(atRoot: fm.pwd, evenIfMissing: true)!
10+
}()
11+
612
@main
713
struct CodeOwnersTool: AsyncParsableCommand {
8-
14+
915
static let configuration: CommandConfiguration = .init(
1016
commandName: "swift-codeowners",
1117
abstract: "Generates code ownership information into Swift files"
1218
)
13-
19+
1420
@Argument(help: "The Swift source files to process")
1521
var sources: [URL]
16-
22+
1723
@Option(name: [.long, .customShort("r")], help: "The root directory where the CODEOWNERS file patterns are based from.")
18-
var codeOwnersRoot: URL = URL(filePath: settings.codeowners!.root!)
19-
24+
var codeOwnersRoot: URL = URL(fileURLWithPath: defaultSettings.codeowners!.root!)
25+
2026
@Option(name: .shortAndLong, help: "The CODEOWNERS file to use for determining ownership.")
21-
var codeOwnersFile: URL = URL(filePath: settings.codeowners!.file!)
22-
27+
var codeOwnersFile: URL = URL(fileURLWithPath: defaultSettings.codeowners!.file!)
28+
2329
@Option(name: .shortAndLong, help: "The path to store the generated output CodeOwners attribution file")
24-
var outputFile: URL =
25-
FileManager.default.pwd.appendingPathComponent("GeneratedSources/CodeOwners.swift")
26-
30+
var outputFile: URL = FileManager.default.pwd.appendingPathComponent("GeneratedSources/CodeOwners.swift")
31+
2732
@Option(name: [.customLong("rename")], help: "Regex pattern to rename ownership names, in <regex>=<replacement> format)")
28-
var renames: [RenameRule] = settings.renames?.map(asRenameRule) ?? []
29-
33+
var renames: [RenameRule] = defaultSettings.renames?.map(asRenameRule) ?? []
34+
3035
@Flag(name: .shortAndLong, inversion: .prefixedNo, help: "Enable verbose output for debugging purposes.")
31-
var verbose: Bool = settings.verbose ?? false
32-
36+
var verbose: Bool = defaultSettings.verbose ?? false
37+
3338
@Flag(name: .shortAndLong, inversion: .prefixedNo, help: "Suppress non-error output.")
34-
var quiet: Bool = settings.quiet ?? false
35-
39+
var quiet: Bool = defaultSettings.quiet ?? false
40+
3641
func run() throws {
37-
let fm = FileManager.default
38-
3942
if (quiet && verbose) {
4043
print("Cannot use --quiet and --verbose flags together.", to: &stdErr)
4144
return
@@ -44,22 +47,24 @@ struct CodeOwnersTool: AsyncParsableCommand {
4447
print("No source files provided.", to: &stdErr)
4548
return
4649
}
50+
51+
let fm = FileManager.default
4752
if (!fm.fileExists(atPath: codeOwnersFile.path)) {
4853
print("CODEOWNERS file not found at path: \(codeOwnersFile.path).", to: &stdErr)
4954
return
5055
}
51-
56+
5257
let codeOwners = try parseCodeOwners(codeOwnersFile)
53-
58+
5459
var mappings: [Substring: Set<String>] = [:]
55-
60+
5661
try fm.walkFiles(at: sources) { source in
5762
if source.pathExtension != "swift" { return }
5863
if verbose { print("Processing source file: \(source.path)") }
59-
64+
6065
guard let relativePath = source.relativePathTo(codeOwnersRoot) else { return }
6166
guard let owners = codeOwners.codeOwner(pattern: relativePath)?.owners.map(asLiteral) else { return }
62-
67+
6368
do {
6469
for typeName in try collectTypes(from: source) {
6570
mappings[typeName] = (mappings[typeName] ?? []).union(owners)
@@ -68,11 +73,11 @@ struct CodeOwnersTool: AsyncParsableCommand {
6873
print("warning: Failed to process source file '\(relativePath)': \(error)", to: &stdErr)
6974
}
7075
}
71-
76+
7277
let content = generateContent(mappings)
7378
try fm.createDirectory(at: outputFile.deletingLastPathComponent(), withIntermediateDirectories: true)
7479
try content.write(to: outputFile, atomically: true, encoding: .utf8)
75-
80+
7681
if (!quiet) {
7782
print("Generated CodeOwners attribution for \(mappings.count) types at: \(outputFile.path)")
7883
}
@@ -105,16 +110,16 @@ struct CodeOwnersTool: AsyncParsableCommand {
105110

106111
private func asLiteral(_ owner: Owner) -> String {
107112
switch owner {
108-
case .user(let userId):
109-
switch userId {
110-
case .userName(let name): return "\(name)"
111-
case .email(let email): return "\(email)"
112-
}
113-
case .team(let teamId):
114-
return "\(teamId.organization)/\(teamId.name)"
113+
case .user(let userId):
114+
switch userId {
115+
case .userName(let name): return "\(name)"
116+
case .email(let email): return "\(email)"
115117
}
118+
case .team(let teamId):
119+
return "\(teamId.organization)/\(teamId.name)"
120+
}
116121
}
117-
122+
118123
private func collectTypes(from source: URL) throws -> Set<Substring> {
119124
let swiftFileContent = try String(contentsOf: source, encoding: .utf8)
120125
let swiftFile = Parser.parse(source: swiftFileContent)
@@ -125,27 +130,26 @@ struct CodeOwnersTool: AsyncParsableCommand {
125130

126131
private func generateContent(_ mappings: [Substring: Set<String>]) -> String {
127132
if mappings.isEmpty { return "" }
128-
133+
129134
var content = """
130-
import CodeOwnersAPI
131-
132-
internal class _CodeOwners : CodeOwnersMappingProvider {
133-
static let codeOwners: [Substring: CodeOwners]? = [
134-
135-
"""
135+
import CodeOwnersAPI
136+
137+
internal class _CodeOwners : CodeOwnersMappingProvider {
138+
static let codeOwners: [Substring: CodeOwners]? = [
139+
140+
"""
136141
for typeName in mappings.keys.sorted() {
137142
let owners = mappings[typeName]!.sorted().map { "\"\($0)\"" }.joined(separator: ", ")
138143

139144
content += " \"\(typeName)\": [\(owners)],\n"
140145
}
141146
content += """
142-
]
143-
}
144-
145-
"""
147+
]
148+
}
149+
150+
"""
146151
return content
147152
}
148-
149153
}
150154

151155
private func asRenameRule(regex: String, replace: String) -> RenameRule {
Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import Foundation
22

3-
let settings = readSettings()
4-
53
struct Settings : Decodable, Equatable {
64
var codeowners: CodeOwners?
75
var renames: [String: String]?
@@ -14,35 +12,45 @@ struct Settings : Decodable, Equatable {
1412
}
1513
}
1614

17-
private func readSettings(pwd: URL = FileManager.default.pwd) -> Settings {
18-
lazy var defaultLocation: Settings.CodeOwners = { findCodeOwnersFile(pwd) }()
15+
func readSettings(atRoot: URL, evenIfMissing: Bool = false) throws -> Settings? {
16+
lazy var defaultFile: URL? = { findCodeOwnersFile(atRoot: atRoot) }()
1917

20-
let file = pwd.appending(component: ".codeowners-tool.json")
21-
if !FileManager.default.fileExists(atPath: file.path) { return Settings(codeowners: defaultLocation) }
18+
let configFile = atRoot.appendingPathComponent(".codeowners-tool.json")
19+
if !FileManager.default.fileExists(atPath: configFile.path) {
20+
if (!evenIfMissing && defaultFile == nil) { return nil }
21+
22+
return Settings(codeowners: Settings.CodeOwners(
23+
root: atRoot.relativePath,
24+
file: (defaultFile ?? atRoot.defaultCodeOwnersFile)?.relativePath
25+
))
26+
}
2227

23-
let data = try! Data(contentsOf: file)
24-
var settings = try! JSONDecoder().decode(Settings.self, from: data)
28+
let data = try Data(contentsOf: configFile)
29+
var settings = try JSONDecoder().decode(Settings.self, from: data)
2530
settings.codeowners = Settings.CodeOwners(
26-
root: settings.codeowners?.root ?? defaultLocation.root,
27-
file: settings.codeowners?.file ?? defaultLocation.file
31+
root: settings.codeowners?.root ?? atRoot.relativePath,
32+
file: settings.codeowners?.file ?? (defaultFile ?? atRoot.defaultCodeOwnersFile)?.relativePath
2833
)
2934
return settings
3035
}
31-
32-
private func findCodeOwnersFile(_ pwd: URL) -> Settings.CodeOwners {
36+
37+
private func findCodeOwnersFile(atRoot: URL) -> URL? {
3338
let fm = FileManager.default
34-
let roots = [ pwd, fm.gitRoot ].compactMap { $0 }
35-
let candidates = [
36-
"CODEOWNERS",
37-
".github/CODEOWNERS",
38-
".gitlab/CODEOWNERS",
39-
"docs/CODEOWNERS",
40-
]
39+
40+
for candidate in [ "CODEOWNERS", ".github/CODEOWNERS", ".gitlab/CODEOWNERS", "docs/CODEOWNERS" ] {
41+
let file = atRoot.appendingPathComponent(candidate)
42+
43+
if fm.fileExists(atPath: file.path) {
44+
return file
45+
}
46+
}
47+
return nil
48+
}
4149

42-
let (root, file) = roots
43-
.flatMap { root in candidates.map { path in (root, root.appendingPathComponent(path)) } }
44-
.filter { (_, file) in fm.fileExists(atPath: file.path) }
45-
.first ?? (pwd, pwd.appendingPathComponent("CODEOWNERS"))
50+
extension URL {
51+
52+
var defaultCodeOwnersFile: URL {
53+
get { self.appendingPathComponent("CODEOWNERS") }
54+
}
4655

47-
return Settings.CodeOwners(root: root.relativePath, file: file.relativePath)
4856
}

0 commit comments

Comments
 (0)