Skip to content

Commit 229f8b2

Browse files
authored
Handle spaces in project paths for git and package commands (#82)
1 parent 367e989 commit 229f8b2

File tree

5 files changed

+54
-6
lines changed

5 files changed

+54
-6
lines changed

Sources/DependencyCalculator/PackageMetadata.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct PackageTargetMetadata: Sendable {
2525
flags.append("--ignore-lock")
2626
}
2727

28-
let manifest = try Shell.execOrFail("(cd \(path) && swift package dump-package \(flags.joined(separator: " ")))")
28+
let manifest = try Shell.execOrFail("(cd \(path.string.shellQuoted) && swift package dump-package \(flags.joined(separator: " ")))")
2929
.trimmingCharacters(in: .newlines)
3030
guard let manifestData = manifest.data(using: .utf8),
3131
let manifestJson = try JSONSerialization.jsonObject(with: manifestData, options: []) as? [String: Any],

Sources/Git/Git+Changeset.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public extension Git {
1313
func changeset(baseBranch: String, verbose: Bool = false) throws -> Set<Path> {
1414
let gitRoot = try repoRoot()
1515

16-
var currentBranch = try Shell.execOrFail("(cd \(gitRoot) && git branch --show-current)").trimmingCharacters(in: .newlines)
16+
var currentBranch = try Shell.execOrFail("(cd \(gitRoot.string.shellQuoted) && git branch --show-current)").trimmingCharacters(in: .newlines)
1717
if verbose {
1818
logger.info("Current branch: \(currentBranch)")
1919
logger.info("Base branch: \(baseBranch)")
@@ -25,7 +25,7 @@ public extension Git {
2525
currentBranch = "HEAD"
2626
}
2727

28-
let changes = try Shell.execOrFail("(cd \(gitRoot) && git diff '\(baseBranch)'..'\(currentBranch)' --name-only)")
28+
let changes = try Shell.execOrFail("(cd \(gitRoot.string.shellQuoted) && git diff \(baseBranch.shellQuoted)..\(currentBranch.shellQuoted) --name-only)")
2929
let changesTrimmed = changes.trimmingCharacters(in: .whitespacesAndNewlines)
3030

3131
guard !changesTrimmed.isEmpty else {
@@ -38,7 +38,7 @@ public extension Git {
3838
func localChangeset() throws -> Set<Path> {
3939
let gitRoot = try repoRoot()
4040

41-
let changes = try Shell.execOrFail("(cd \(gitRoot) && git diff HEAD --name-only)")
41+
let changes = try Shell.execOrFail("(cd \(gitRoot.string.shellQuoted) && git diff HEAD --name-only)")
4242

4343
let changesTrimmed = changes.trimmingCharacters(in: .whitespacesAndNewlines)
4444

Sources/Git/Git.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ public struct Git {
1414
}
1515

1616
public func repoRoot() throws -> Path {
17-
let gitPath = try Shell.execOrFail("(cd \(path) && git rev-parse --show-toplevel)").trimmingCharacters(in: .newlines)
17+
let gitPath = try Shell.execOrFail("(cd \(path.string.shellQuoted) && git rev-parse --show-toplevel)").trimmingCharacters(in: .newlines)
1818

1919
return Path(gitPath).absolute()
2020
}
2121

2222
public func find(pattern: String) throws -> Set<Path> {
2323
let gitRoot = try repoRoot()
2424

25-
let result = try Shell.exec("(cd \(gitRoot) && git ls-files | grep \(pattern))").0.trimmingCharacters(in: .newlines)
25+
let result = try Shell.exec("(cd \(gitRoot.string.shellQuoted) && git ls-files | grep \(pattern.shellQuoted))").0.trimmingCharacters(in: .newlines)
2626

2727
guard !result.isEmpty else {
2828
return Set()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// Created by Mike Gerasymenko <mike@gera.cx>
3+
//
4+
5+
import Foundation
6+
7+
public extension String {
8+
/// Wrap string in single quotes for safe usage in shell commands.
9+
var shellQuoted: String {
10+
guard !isEmpty else { return "''" }
11+
return "'" + self.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
12+
}
13+
}

Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Created by Mike Gerasymenko <mike@gera.cx>
33
//
44

5+
import Foundation
56
import PathKit
67
@testable import SelectiveTestingCore
78
import SelectiveTestShell
@@ -33,6 +34,40 @@ struct SelectiveTestingProjectTests {
3334
]))
3435
}
3536

37+
@Test
38+
func projectBasePathWithSpaces() async throws {
39+
// given
40+
let testTool = try IntegrationTestTool()
41+
defer { try? testTool.tearDown() }
42+
43+
let projectRoot = testTool.projectPath.string.shellQuoted
44+
let originalName = "ExampleProject.xcodeproj"
45+
let spacedName = "Example Project.xcodeproj"
46+
try Shell.execOrFail("(cd \(projectRoot) && git checkout main)")
47+
try Shell.execOrFail("(cd \(projectRoot) && git mv \(originalName.shellQuoted) \(spacedName.shellQuoted))")
48+
try Shell.execOrFail("(cd \(projectRoot) && git commit -m 'Rename project with spaces')")
49+
try Shell.execOrFail("(cd \(projectRoot) && git checkout feature)")
50+
try Shell.execOrFail("(cd \(projectRoot) && git merge main)")
51+
52+
let renamedProject = testTool.projectPath + spacedName
53+
let tool = try testTool.createSUT(config: nil,
54+
basePath: renamedProject)
55+
56+
// when
57+
try testTool.changeFile(at: renamedProject + "project.pbxproj")
58+
59+
// then
60+
let result = try await tool.run()
61+
let expectedTargets: Set<TargetIdentity> = Set([
62+
TargetIdentity.project(path: renamedProject, targetName: "ExampleProject", testTarget: false),
63+
TargetIdentity.project(path: renamedProject, targetName: "ExampleProjectTests", testTarget: true),
64+
TargetIdentity.project(path: renamedProject, targetName: "ExampleProjectUITests", testTarget: true),
65+
TargetIdentity.project(path: renamedProject, targetName: "ExmapleTargetLibrary", testTarget: false),
66+
TargetIdentity.project(path: renamedProject, targetName: "ExmapleTargetLibraryTests", testTarget: true),
67+
])
68+
#expect(result == expectedTargets)
69+
}
70+
3671
@Test
3772
func projectDeepGroupPathChange_turbo() async throws {
3873
// given

0 commit comments

Comments
 (0)