Skip to content
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
16 changes: 9 additions & 7 deletions Sources/CLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import AppKit
enum CLI {
static let version = "0.2.0" // keep in step with Resources/Info.plist

static func run(_ args: [String]) -> Int32 {
static func run(_ args: [String],
invokedFrom directory: String = FileManager.default.currentDirectoryPath) -> Int32 {
guard let first = args.first else { printUsage(); return 0 }

switch first {
Expand All @@ -21,17 +22,17 @@ enum CLI {
case "stop": return stopDaemon()
case "config": return config(Array(args.dropFirst()))
case "autostart": return autostart(Array(args.dropFirst()))
case "add": return add(Array(args.dropFirst()))
case "add": return add(Array(args.dropFirst()), invokedFrom: directory)
default:
// Bare form: `gitwatchd [flags] <target>`: implicit add.
return add(args)
return add(args, invokedFrom: directory)
}
}

// MARK: - add

private static func add(_ args: [String]) -> Int32 {
let (spec, err) = RepoSpecParser.parse(args)
private static func add(_ args: [String], invokedFrom directory: String) -> Int32 {
let (spec, err, resolved) = RepoSpecParser.parse(args, invokedFrom: directory)
guard let spec else { warn(err ?? "could not parse arguments"); return 1 }

guard FileManager.default.fileExists(atPath: spec.path) else {
Expand All @@ -44,8 +45,9 @@ enum CLI {
warn("already watching \(spec.name) (\(spec.path))"); return 1
}

// Persist exactly what the user typed (gitwatch-style line).
let line = args.map(Config.quoteIfNeeded).joined(separator: " ")
// Persist what the user typed (gitwatch-style line), with the target
// resolved: the daemon reads this file from its own directory.
let line = resolved.map(Config.quoteIfNeeded).joined(separator: " ")
Config.append(line)
ensureDaemonRunning()

Expand Down
29 changes: 18 additions & 11 deletions Sources/RepoSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ enum RepoSpecParser {
/// Parse a gitwatch-style argument list into a RepoSpec.
/// Returns nil + an error message if there's no valid target.
static func parse(_ args: [String]) -> (spec: RepoSpec?, error: String?) {
let parsed = parse(args, invokedFrom: FileManager.default.currentDirectoryPath)
return (parsed.spec, parsed.error)
}

static func parse(_ args: [String], invokedFrom directory: String)
-> (spec: RepoSpec?, error: String?, resolved: [String]) {
var spec = RepoSpec(path: "")
var target: String? = nil
var target: (token: String, index: Int)? = nil
var i = 0
func next() -> String? { i += 1; return i < args.count ? args[i] : nil }

Expand All @@ -64,7 +70,7 @@ enum RepoSpecParser {
switch a {
case "-s":
guard let v = next(), let d = Double(v), d >= 0 else {
return (nil, "-s needs a number of seconds, 0 or more")
return (nil, "-s needs a number of seconds, 0 or more", args)
}
spec.settle = d
case "-d": if let v = next() { spec.dateFormat = v }
Expand All @@ -76,13 +82,13 @@ enum RepoSpecParser {
case "-C": spec.pipeChangedFiles = true // boolean: takes no argument
case "-l", "-L":
guard let v = next(), let n = Int(v), n >= 0 else {
return (nil, "\(a) needs a number of lines, 0 or more")
return (nil, "\(a) needs a number of lines, 0 or more", args)
}
spec.listChanges = n
if a == "-L" { spec.listChangesColor = false }
case "-x":
guard let v = next(), (try? NSRegularExpression(pattern: v)) != nil else {
return (nil, "-x needs a valid regular expression")
return (nil, "-x needs a valid regular expression", args)
}
spec.exclude = v
case "-M": spec.noMergeCommit = true
Expand All @@ -93,22 +99,23 @@ enum RepoSpecParser {
case "--paused": spec.paused = true // gitwatchd extension (see RepoSpec)
default:
if a.hasPrefix("-") {
return (nil, "unknown flag \(a)")
return (nil, "unknown flag \(a)", args)
} else {
target = a // last bare arg wins as the target
target = (a, i) // last bare arg wins as the target
}
}
i += 1
}

guard let t = target else { return (nil, "no target path given") }
spec.path = (t as NSString).expandingTildeInPath
guard let target else { return (nil, "no target path given", args) }
spec.path = (target.token as NSString).expandingTildeInPath
if !(spec.path as NSString).isAbsolutePath {
spec.path = (FileManager.default.currentDirectoryPath as NSString)
.appendingPathComponent(spec.path)
spec.path = (directory as NSString).appendingPathComponent(spec.path)
}
spec.path = (spec.path as NSString).standardizingPath
return (spec, nil)
var resolved = args
resolved[target.index] = spec.path
return (spec, nil, resolved)
}

/// The -d value goes to date(1) verbatim, as upstream: formats need a
Expand Down
30 changes: 30 additions & 0 deletions Tests/CLITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,36 @@ struct CLIContract {
}
}

@Test("`gitwatchd .` persists the repo it was run in, not the dot")
func addResolvesRelativeTarget() {
withTemporaryConfig {
let repo = TestRepo()
#expect(CLI.run(["."], invokedFrom: repo.path) == 0)
#expect(Config.rawLines() == [repo.path])
#expect(Config.specs()[0].path == repo.path,
"the daemon watches that repo, wherever it reads the config from")
}
}

@Test("a tilde target expands and an absolute target survives unchanged")
func targetResolutionForms() {
let elsewhere = TestDirs.root
#expect(RepoSpecParser.parse(["~/code"], invokedFrom: elsewhere).resolved
== [(NSHomeDirectory() as NSString).appendingPathComponent("code")])
#expect(RepoSpecParser.parse(["/tmp/x"], invokedFrom: elsewhere).resolved == ["/tmp/x"])
}

@Test("an invocation with no target is rejected as before, its args untouched")
func addWithoutTarget() {
withTemporaryConfig {
#expect(CLI.run(["add", "-s", "5"], invokedFrom: TestDirs.root) == 1)
#expect(Config.rawLines().isEmpty)
}
let parsed = RepoSpecParser.parse(["-s", "5"], invokedFrom: TestDirs.root)
#expect(parsed.error == "no target path given")
#expect(parsed.resolved == ["-s", "5"])
}

@Test("adding the same repo twice fails and leaves one entry")
func duplicateAdd() {
withTemporaryConfig {
Expand Down