Skip to content

Commit 735ebd1

Browse files
authored
move manifest validation that requires source control to Workspace (#3955)
motivation: all access to git operations should be done via the repositoriesProvider handed to the workspace, but some was hard wired to using Git changes: * move git related validation from manfeist JSON parser and graph loader to workspace and use the workspace repositoryProvider to perform them * add methods on RepositoryProvider to hande validation of directory and ref formats. * remove PackageLoading depedency on SourceControl which was a problem for a while and only needed for these validations * add and adjust tests rdar://84393257
1 parent 3570f6e commit 735ebd1

File tree

13 files changed

+146
-35
lines changed

13 files changed

+146
-35
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// swift-tools-version:5.5
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Foo",
6+
dependencies: [
7+
.package(url: "https://localhost/foo/bar", branch: "#!~")
8+
],
9+
targets: []
10+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// swift-tools-version:5.5
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Foo",
6+
dependencies: [
7+
.package(url: "https://localhost/foo/bar", revision: "#!~")
8+
],
9+
targets: []
10+
)

Package.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,7 @@ let package = Package(
191191
name: "PackageLoading",
192192
dependencies: [
193193
"Basics",
194-
"PackageModel",
195-
"SourceControl"
194+
"PackageModel"
196195
],
197196
exclude: ["CMakeLists.txt", "README.md"]
198197
),

Sources/PackageGraph/PackageGraphRoot.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,9 @@ extension PackageDependency.SourceControl.Requirement {
116116
case .range(let range):
117117
return .versionSet(.range(range))
118118
case .revision(let identifier):
119-
// FIXME: this validation could/should move somewhere more appropriate
120-
guard Git.checkRefFormat(ref: identifier) else {
121-
throw StringError("Could not find revision: '\(identifier)'")
122-
}
123-
return .revision(identifier)
124-
case .branch(let identifier):
125-
// FIXME: this validation could/should move somewhere more appropriate
126-
guard Git.checkRefFormat(ref: identifier) else {
127-
throw StringError("Could not find branch: '\(identifier)'")
128-
}
129119
return .revision(identifier)
120+
case .branch(let name):
121+
return .revision(name)
130122
case .exact(let version):
131123
return .versionSet(.exact(version))
132124
}

Sources/PackageLoading/ManifestJSONParser.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import Basics
1212
import Foundation
1313
import PackageModel
14-
import SourceControl // FIXME: remove this dependency
1514
import TSCBasic
1615
import TSCUtility
1716

@@ -188,15 +187,6 @@ enum ManifestJSONParser {
188187
location = identityResolver.mappedLocation(for: location)
189188
// a package in a git location, may be a remote URL or on disk
190189
if let localPath = try? AbsolutePath(validating: location) {
191-
// if exists, validate location is in fact a git repo
192-
// there is a case to be made to throw early (here) if the path does not exists
193-
// but many of our tests assume they can pass a non existent path
194-
if fileSystem.exists(localPath) {
195-
let gitRepoProvider = GitRepositoryProvider()
196-
guard gitRepoProvider.isValidDirectory(location) else {
197-
throw StringError("Cannot clone from local directory \(localPath)\nPlease git init or use \"path:\" for \(location)")
198-
}
199-
}
200190
// in the future this will check with the registries for the identity of the URL
201191
let identity = try identityResolver.resolveIdentity(for: localPath)
202192
return .localSourceControl(

Sources/SPMTestSupport/InMemoryGitRepository.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,4 +430,12 @@ public final class InMemoryGitRepositoryProvider: RepositoryProvider {
430430
public func openWorkingCopy(at path: AbsolutePath) throws -> WorkingCheckout {
431431
return checkoutsMap[path]!
432432
}
433+
434+
public func isValidDirectory(_ directory: AbsolutePath) -> Bool {
435+
return true
436+
}
437+
438+
public func isValidRefFormat(_ ref: String) -> Bool {
439+
return true
440+
}
433441
}

Sources/SourceControl/GitRepository.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,19 @@ public struct GitRepositoryProvider: RepositoryProvider {
111111
progress: progressHandler)
112112
}
113113

114-
public func isValidDirectory(_ directory: String) -> Bool {
115-
// Provides better feedback when mistakingly using url: for a dependency that
116-
// is a local package. Still allows for using url with a local package that has
117-
// also been initialized by git
114+
public func isValidDirectory(_ directory: AbsolutePath) -> Bool {
118115
do {
119-
_ = try self.git.run(["-C", directory, "rev-parse", "--git-dir"])
116+
_ = try self.git.run(["-C", directory.pathString, "rev-parse", "--git-dir"])
117+
return true
118+
} catch {
119+
return false
120+
}
121+
}
122+
123+
/// Returns true if the git reference name is well formed.
124+
public func isValidRefFormat(_ ref: String) -> Bool {
125+
do {
126+
_ = try self.git.run(["check-ref-format", "--allow-onelevel", ref])
120127
return true
121128
} catch {
122129
return false

Sources/SourceControl/Repository.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ public protocol RepositoryProvider {
142142
/// - sourcePath: the source path.
143143
/// - destinationPath: the destination path.
144144
func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws
145+
146+
/// Returns true if the directory is valid git location.
147+
func isValidDirectory(_ directory: AbsolutePath) -> Bool
148+
149+
/// Returns true if the git reference name is well formed.
150+
func isValidRefFormat(_ ref: String) -> Bool
145151
}
146152

147153
/// Abstract repository operations.

Sources/SourceControl/RepositoryManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ public class RepositoryManager {
351351
}
352352
}
353353

354+
/// Returns true if the directory is valid git location.
355+
public func isValidDirectory(_ directory: AbsolutePath) -> Bool {
356+
self.provider.isValidDirectory(directory)
357+
}
358+
359+
/// Returns true if the git reference name is well formed.
360+
public func isValidRefFormat(_ ref: String) -> Bool {
361+
self.provider.isValidRefFormat(ref)
362+
}
363+
354364
/// Reset the repository manager.
355365
///
356366
/// Note: This also removes the cloned repositories from the disk.

Sources/Workspace/Workspace.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,6 +1874,7 @@ extension Workspace {
18741874
manifestLoadingScope.emit(error)
18751875
self.delegate?.didLoadManifest(packagePath: packagePath, url: packageLocation, version: version, packageKind: packageKind, manifest: nil, diagnostics: manifestLoadingDiagnostics.get())
18761876
case .success(let manifest):
1877+
manifestLoadingScope.trap { try self.validateManifest(manifest) }
18771878
self.delegate?.didLoadManifest(packagePath: packagePath, url: packageLocation, version: version, packageKind: packageKind, manifest: manifest, diagnostics: manifestLoadingDiagnostics.get())
18781879
}
18791880
completion(result)
@@ -1885,6 +1886,48 @@ extension Workspace {
18851886
//}
18861887
}
18871888

1889+
// TODO: move more manifest validation in here from other parts of the code, e.g. from ManifestLoader
1890+
private func validateManifest(_ manifest: Manifest) throws {
1891+
// validate dependency requirements
1892+
for dependency in manifest.dependencies {
1893+
switch dependency {
1894+
case .sourceControl(let sourceControl):
1895+
try validateSourceControlDependency(sourceControl)
1896+
default:
1897+
break
1898+
}
1899+
}
1900+
1901+
func validateSourceControlDependency(_ dependency: PackageDependency.SourceControl) throws {
1902+
// validate source control ref
1903+
switch dependency.requirement {
1904+
case .branch(let name):
1905+
guard self.repositoryManager.isValidRefFormat(name) else {
1906+
throw StringError("Invalid branch name: '\(name)'")
1907+
}
1908+
case .revision(let revision):
1909+
guard self.repositoryManager.isValidRefFormat(revision) else {
1910+
throw StringError("Invalid revision: '\(revision)'")
1911+
}
1912+
default:
1913+
break
1914+
}
1915+
// if a location is on file system, validate it is in fact a git repo
1916+
// there is a case to be made to throw early (here) if the path does not exists
1917+
// but many of our tests assume they can pass a non existent path
1918+
if case .local(let localPath) = dependency.location, self.fileSystem.exists(localPath) {
1919+
guard self.repositoryManager.isValidDirectory(localPath) else {
1920+
// Provides better feedback when mistakingly using url: for a dependency that
1921+
// is a local package. Still allows for using url with a local package that has
1922+
// also been initialized by git
1923+
throw StringError("Cannot clone from local directory \(localPath)\nPlease git init or use \"path:\" for \(location)")
1924+
}
1925+
}
1926+
}
1927+
1928+
}
1929+
1930+
18881931
/// Validates that all the edited dependencies are still present in the file system.
18891932
/// If some checkout dependency is removed form the file system, clone it again.
18901933
/// If some edited dependency is removed from the file system, mark it as unedited and

Tests/FunctionalTests/MiscellaneousTests.swift

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -416,22 +416,34 @@ class MiscellaneousTestCase: XCTestCase {
416416
}
417417
}
418418

419-
func testLocalPackageUsedAsURL() throws {
420-
fixture(name: "Miscellaneous/LocalPackageAsURL", createGitRepo: false) { prefix in
419+
func testLocalPackageUsedAsURLValidation() throws {
420+
fixture(name: "Miscellaneous/LocalPackageAsURL", createGitRepo: false) { path in
421421
// This fixture has a setup that is trying to use a local package
422422
// as a url that hasn't been initialized as a repo
423-
424-
// Launch swift-build.
425-
let app = prefix.appending(component: "Bar")
426-
427-
let result = try SwiftPMProduct.SwiftBuild.executeProcess([], packagePath: app)
428-
423+
let result = try SwiftPMProduct.SwiftBuild.executeProcess([], packagePath: path.appending(component: "Bar"))
429424
XCTAssert(result.exitStatus != .terminated(code: 0))
430425
let output = try result.utf8stderrOutput()
431426
XCTAssert(output.contains("Cannot clone from local directory"), "Didn't find expected output: \(output)")
432427
}
433428
}
434429

430+
func testInvalidRefsValidation() throws {
431+
fixture(name: "Miscellaneous/InvalidRefs", createGitRepo: false) { path in
432+
do {
433+
let result = try SwiftPMProduct.SwiftBuild.executeProcess([], packagePath: path.appending(component: "InvalidBranch"))
434+
XCTAssert(result.exitStatus != .terminated(code: 0))
435+
let output = try result.utf8stderrOutput()
436+
XCTAssert(output.contains("Invalid branch name: "), "Didn't find expected output: \(output)")
437+
}
438+
do {
439+
let result = try SwiftPMProduct.SwiftBuild.executeProcess([], packagePath: path.appending(component: "InvalidRevision"))
440+
XCTAssert(result.exitStatus != .terminated(code: 0))
441+
let output = try result.utf8stderrOutput()
442+
XCTAssert(output.contains("Invalid revision: "), "Didn't find expected output: \(output)")
443+
}
444+
}
445+
}
446+
435447
func testUnicode() {
436448
#if !os(Linux) && !os(Android) // TODO: - Linux has trouble with this and needs investigation.
437449
fixture(name: "Miscellaneous/Unicode") { prefix in

Tests/SourceControlTests/RepositoryManagerTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ private class DummyRepository: Repository {
4242
fatalError("unexpected API call")
4343
}
4444

45+
func isValidDirectory(_ directory: AbsolutePath) -> Bool {
46+
fatalError("unexpected API call")
47+
}
48+
49+
func isValidRefFormat(_ ref: String) -> Bool {
50+
fatalError("unexpected API call")
51+
}
52+
4553
func fetch() throws {
4654
self.provider.increaseFetchCount()
4755
}
@@ -108,6 +116,14 @@ private class DummyRepositoryProvider: RepositoryProvider {
108116
return DummyWorkingCheckout(at: path)
109117
}
110118

119+
func isValidDirectory(_ directory: AbsolutePath) -> Bool {
120+
return true
121+
}
122+
123+
func isValidRefFormat(_ ref: String) -> Bool {
124+
return true
125+
}
126+
111127
func increaseFetchCount() {
112128
self.lock.withLock {
113129
self._numFetches += 1

Tests/WorkspaceTests/SourceControlPackageContainerTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ private class MockRepositories: RepositoryProvider {
132132
func openWorkingCopy(at path: AbsolutePath) throws -> WorkingCheckout {
133133
fatalError("unexpected API call")
134134
}
135+
136+
func isValidDirectory(_ directory: AbsolutePath) -> Bool {
137+
return true
138+
}
139+
140+
func isValidRefFormat(_ ref: String) -> Bool {
141+
return true
142+
}
135143
}
136144

137145
private class MockResolverDelegate: RepositoryManagerDelegate {

0 commit comments

Comments
 (0)