Skip to content

Commit a5dca08

Browse files
committed
Model a small subset of the git command
1 parent aba929f commit a5dca08

File tree

3 files changed

+257
-3
lines changed

3 files changed

+257
-3
lines changed

Sources/SwiftlyCore/Commands.swift

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,207 @@ extension SystemCommand.GetentCommand: Output {
279279
return entries
280280
}
281281
}
282+
283+
extension SystemCommand {
284+
public static func git(executable: Executable = GitCommand.defaultExecutable, workingDir: FilePath? = nil) -> GitCommand {
285+
GitCommand(executable: executable, workingDir: workingDir)
286+
}
287+
288+
public struct GitCommand {
289+
public static var defaultExecutable: Executable { .name("git") }
290+
291+
var executable: Executable
292+
293+
var workingDir: FilePath?
294+
295+
internal init(executable: Executable, workingDir: FilePath?) {
296+
self.executable = executable
297+
self.workingDir = workingDir
298+
}
299+
300+
func config() -> Configuration {
301+
var args: [String] = []
302+
303+
if let workingDir {
304+
args += ["-C", "\(workingDir)"]
305+
}
306+
307+
return Configuration(
308+
executable: self.executable,
309+
arguments: Arguments(args),
310+
environment: .inherit
311+
)
312+
}
313+
314+
public func _init() -> InitCommand {
315+
InitCommand(self)
316+
}
317+
318+
public struct InitCommand {
319+
var git: GitCommand
320+
321+
internal init(_ git: GitCommand) {
322+
self.git = git
323+
}
324+
325+
public func config() -> Configuration {
326+
var c = self.git.config()
327+
328+
var args = c.arguments.storage.map(\.description)
329+
330+
args += ["init"]
331+
332+
c.arguments = .init(args)
333+
334+
return c
335+
}
336+
}
337+
338+
public func commit(_ options: CommitCommand.Option...) -> CommitCommand {
339+
self.commit(options: options)
340+
}
341+
342+
public func commit(options: [CommitCommand.Option]) -> CommitCommand {
343+
CommitCommand(self, options: options)
344+
}
345+
346+
public struct CommitCommand {
347+
var git: GitCommand
348+
349+
var options: [Option]
350+
351+
internal init(_ git: GitCommand, options: [Option]) {
352+
self.git = git
353+
self.options = options
354+
}
355+
356+
public enum Option {
357+
case allowEmpty
358+
case allowEmptyMessage
359+
case message(String)
360+
361+
public func args() -> [String] {
362+
switch self {
363+
case .allowEmpty:
364+
["--allow-empty"]
365+
case .allowEmptyMessage:
366+
["--allow-empty-message"]
367+
case let .message(message):
368+
["-m", message]
369+
}
370+
}
371+
}
372+
373+
public func config() -> Configuration {
374+
var c = self.git.config()
375+
376+
var args = c.arguments.storage.map(\.description)
377+
378+
args += ["commit"]
379+
for option in self.options {
380+
args += option.args()
381+
}
382+
383+
c.arguments = .init(args)
384+
385+
return c
386+
}
387+
}
388+
389+
public func log(_ options: LogCommand.Option...) -> LogCommand {
390+
LogCommand(self, options)
391+
}
392+
393+
public struct LogCommand {
394+
var git: GitCommand
395+
var options: [Option]
396+
397+
internal init(_ git: GitCommand, _ options: [Option]) {
398+
self.git = git
399+
self.options = options
400+
}
401+
402+
public enum Option {
403+
case maxCount(Int)
404+
case pretty(String)
405+
406+
func args() -> [String] {
407+
switch self {
408+
case let .maxCount(num):
409+
return ["--max-count=\(num)"]
410+
case let .pretty(format):
411+
return ["--pretty=\(format)"]
412+
}
413+
}
414+
}
415+
416+
public func config() -> Configuration {
417+
var c = self.git.config()
418+
419+
var args = c.arguments.storage.map(\.description)
420+
421+
args += ["log"]
422+
423+
for opt in self.options {
424+
args += opt.args()
425+
}
426+
427+
c.arguments = .init(args)
428+
429+
return c
430+
}
431+
}
432+
433+
public func diffIndex(_ options: DiffIndexCommand.Option..., treeIsh: String?) -> DiffIndexCommand {
434+
DiffIndexCommand(self, options, treeIsh: treeIsh)
435+
}
436+
437+
public struct DiffIndexCommand {
438+
var git: GitCommand
439+
var options: [Option]
440+
var treeIsh: String?
441+
442+
internal init(_ git: GitCommand, _ options: [Option], treeIsh: String?) {
443+
self.git = git
444+
self.options = options
445+
self.treeIsh = treeIsh
446+
}
447+
448+
public enum Option {
449+
case quiet
450+
451+
func args() -> [String] {
452+
switch self {
453+
case .quiet:
454+
return ["--quiet"]
455+
}
456+
}
457+
}
458+
459+
public func config() -> Configuration {
460+
var c = self.git.config()
461+
462+
var args = c.arguments.storage.map(\.description)
463+
464+
args += ["diff-index"]
465+
466+
for opt in self.options {
467+
args += opt.args()
468+
}
469+
470+
if let treeIsh = self.treeIsh {
471+
args += [treeIsh]
472+
}
473+
474+
c.arguments = .init(args)
475+
476+
return c
477+
}
478+
}
479+
}
480+
}
481+
482+
extension SystemCommand.GitCommand.LogCommand: Output {}
483+
extension SystemCommand.GitCommand.DiffIndexCommand: Runnable {}
484+
extension SystemCommand.GitCommand.InitCommand: Runnable {}
485+
extension SystemCommand.GitCommand.CommitCommand: Runnable {}

Tests/SwiftlyTests/CommandLineTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,54 @@ public struct CommandLineTests {
6161
config = sys.getent(database: "foo", keys: "abc", "def").config()
6262
#expect(String(describing: config) == "getent foo abc def")
6363
}
64+
65+
@Test func testGitModel() {
66+
var config = sys.git().log(.maxCount(1), .pretty("format:%d")).config()
67+
#expect(String(describing: config) == "git log --max-count=1 --pretty=format:%d")
68+
69+
config = sys.git().log().config()
70+
#expect(String(describing: config) == "git log")
71+
72+
config = sys.git().log(.pretty("foo")).config()
73+
#expect(String(describing: config) == "git log --pretty=foo")
74+
75+
config = sys.git().diffIndex(.quiet, treeIsh: "HEAD").config()
76+
#expect(String(describing: config) == "git diff-index --quiet HEAD")
77+
78+
config = sys.git().diffIndex(treeIsh: "main").config()
79+
#expect(String(describing: config) == "git diff-index main")
80+
}
81+
82+
@Test(
83+
.tags(.medium),
84+
.enabled {
85+
try await sys.GitCommand.defaultExecutable.exists()
86+
}
87+
)
88+
func testGit() async throws {
89+
// GIVEN a simple git repository
90+
let tmp = fs.mktemp()
91+
try await fs.mkdir(atPath: tmp)
92+
try await sys.git(workingDir: tmp)._init().run(Swiftly.currentPlatform)
93+
94+
// AND a simple history
95+
try "Some text".write(to: tmp / "foo.txt", atomically: true)
96+
try await Swiftly.currentPlatform.runProgram("git", "-C", "\(tmp)", "add", "foo.txt")
97+
try await Swiftly.currentPlatform.runProgram("git", "-C", "\(tmp)", "config", "user.email", "[email protected]")
98+
try await sys.git(workingDir: tmp).commit(.message("Initial commit")).run(Swiftly.currentPlatform)
99+
try await sys.git(workingDir: tmp).diffIndex(.quiet, treeIsh: "HEAD").run(Swiftly.currentPlatform)
100+
101+
// WHEN inspecting the log
102+
let log = try await sys.git(workingDir: tmp).log(.maxCount(1)).output(Swiftly.currentPlatform)!
103+
// THEN it is not empty
104+
#expect(log != "")
105+
106+
// WHEN there is a change to the work tree
107+
try "Some new text".write(to: tmp / "foo.txt", atomically: true)
108+
109+
// THEN diff index finds a change
110+
try await #expect(throws: Error.self) {
111+
try await sys.git(workingDir: tmp).diffIndex(.quiet, treeIsh: "HEAD").run(Swiftly.currentPlatform)
112+
}
113+
}
64114
}

Tools/build-swiftly-release/BuildSwiftlyRelease.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,17 @@ struct BuildSwiftlyRelease: AsyncParsableCommand {
236236
return swift
237237
}
238238

239-
func checkGitRepoStatus(_ git: String) async throws {
239+
func checkGitRepoStatus(_: String) async throws {
240240
guard !self.skip else {
241241
return
242242
}
243243

244-
guard let gitTags = try await runProgramOutput(git, "log", "-n1", "--pretty=format:%d"), gitTags.contains("tag: \(self.version)") else {
244+
guard let gitTags = try await sys.git().log(.maxCount(1), .pretty("format:%d")).output(currentPlatform), gitTags.contains("tag: \(self.version)") else {
245245
throw Error(message: "Git repo is not yet tagged for release \(self.version). Please tag this commit with that version and push it to GitHub.")
246246
}
247247

248248
do {
249-
try runProgram(git, "diff-index", "--quiet", "HEAD")
249+
try await sys.git().diffIndex(.quiet, treeIsh: "HEAD").run(currentPlatform)
250250
} catch {
251251
throw Error(message: "Git repo has local changes. First commit these changes, tag the commit with release \(self.version) and push the tag to GitHub.")
252252
}

0 commit comments

Comments
 (0)