Skip to content

Commit aded72d

Browse files
committed
Improve the diagnostics that are emitted when a package dependency specifies a non-existent branch or commit
This also sets the code up to make it easier to do other similar heuristics as needed, and as a bonus, adds one that is related to the `master` -> `main` renaming (suggesting `main` if `master` is requested and doesn't exist, but `main` does). There isn't yet any support for fix-its in SwiftPM, but this would be a case for such a facility once it is supported. Note that tags go through different processing, since they are the inputs to the semantic versioning and missing tags are reported differently. There is no way in SwiftPM to define an explicit dependency on a non-semver tag, which is why this code only needs to handle branches and commits. The error message does not list all the branches, since there can be many, but that could be another future improvement (or at least to list all the branches whose names are similar to what was requested). rdar://34099187
1 parent 33f6e2b commit aded72d

File tree

3 files changed

+103
-21
lines changed

3 files changed

+103
-21
lines changed

Sources/PackageGraph/RepositoryPackageContainerProvider.swift

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -251,16 +251,28 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve
251251

252252
// A wrapper for getDependencies() errors. This adds additional information
253253
// about the container to identify it for diagnostics.
254-
public struct GetDependenciesErrorWrapper: Swift.Error {
254+
public struct GetDependenciesError: Error, CustomStringConvertible {
255255

256-
/// The container which had this error.
256+
/// The container (repository) that encountered the error.
257257
public let containerIdentifier: String
258258

259-
/// The source control reference i.e. version, branch, revsion etc.
259+
/// The source control reference (version, branch, revision, etc) that was involved.
260260
public let reference: String
261261

262262
/// The actual error that occurred.
263-
public let underlyingError: Swift.Error
263+
public let underlyingError: Error
264+
265+
/// Optional suggestion for how to resolve the error.
266+
public let suggestion: String?
267+
268+
/// Description shown for errors of this kind.
269+
public var description: String {
270+
var desc = "\(underlyingError) in \(containerIdentifier)"
271+
if let suggestion = suggestion {
272+
desc += " (\(suggestion))"
273+
}
274+
return desc
275+
}
264276
}
265277

266278
/// This is used to remember if tools version of a particular version is
@@ -370,8 +382,8 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve
370382
return try getDependencies(at: revision, version: version, productFilter: productFilter)
371383
}.1
372384
} catch {
373-
throw GetDependenciesErrorWrapper(
374-
containerIdentifier: identifier.repository.url, reference: version.description, underlyingError: error)
385+
throw GetDependenciesError(
386+
containerIdentifier: identifier.repository.url, reference: version.description, underlyingError: error, suggestion: nil)
375387
}
376388
}
377389

@@ -383,8 +395,28 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve
383395
return try getDependencies(at: revision, productFilter: productFilter)
384396
}.1
385397
} catch {
386-
throw GetDependenciesErrorWrapper(
387-
containerIdentifier: identifier.repository.url, reference: revision, underlyingError: error)
398+
// Examine the error to see if we can come up with a more informative and actionable error message. We know that the revision is expected to be a branch name or a hash (tags are handled through a different code path).
399+
if let gitInvocationError = error as? ProcessResult.Error, gitInvocationError.description.contains("Needed a single revision") {
400+
// It was a Git process invocation error. Take a look at the repository to see if we can come up with a reasonable diagnostic.
401+
if let rev = try? repository.resolveRevision(identifier: revision), repository.exists(revision: rev) {
402+
// Revision does exist, so something else must be wrong.
403+
throw GetDependenciesError(
404+
containerIdentifier: identifier.repository.url, reference: revision, underlyingError: error, suggestion: nil)
405+
}
406+
else {
407+
// Revision does not exist, so we customize the error.
408+
let sha1RegEx = try! RegEx(pattern: #"\A[:xdigit:]{40}\Z"#)
409+
let isBranchRev = sha1RegEx.matchGroups(in: revision).compactMap{ $0 }.isEmpty
410+
let errorMessage = "could not find " + (isBranchRev ? "a branch named ‘\(revision)" : "the commit \(revision)")
411+
let mainBranchExists = (try? repository.resolveRevision(identifier: "main")) != nil
412+
let suggestion = (revision == "master" && mainBranchExists) ? "did you mean ‘main’?" : nil
413+
throw GetDependenciesError(
414+
containerIdentifier: identifier.repository.url, reference: revision,
415+
underlyingError: StringError(errorMessage), suggestion: suggestion)
416+
}
417+
}
418+
// If we get this far without having thrown an error, we wrap and throw the underlying error.
419+
throw GetDependenciesError(containerIdentifier: identifier.repository.url, reference: revision, underlyingError: error, suggestion: nil)
388420
}
389421
}
390422

Sources/Workspace/Workspace.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2064,16 +2064,7 @@ extension Workspace {
20642064
return bindings
20652065

20662066
case .error(let error):
2067-
switch error {
2068-
// Emit proper error if we were not able to parse some manifest during dependency resolution.
2069-
case let error as RepositoryPackageContainer.GetDependenciesErrorWrapper:
2070-
let location = PackageLocation.Remote(url: error.containerIdentifier, reference: error.reference)
2071-
diagnostics.emit(error.underlyingError, location: location)
2072-
2073-
default:
2074-
diagnostics.emit(error)
2075-
}
2076-
2067+
diagnostics.emit(error)
20772068
return []
20782069
}
20792070
}

Tests/PackageGraphTests/RepositoryPackageContainerProviderTests.swift

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -11,6 +11,7 @@
1111
import XCTest
1212

1313
import TSCBasic
14+
import TSCUtility
1415
import PackageLoading
1516
import PackageModel
1617
import PackageGraph
@@ -274,7 +275,7 @@ class RepositoryPackageContainerProviderTests: XCTestCase {
274275
let revision = try container.getRevision(forTag: "1.0.0")
275276
do {
276277
_ = try container.getDependencies(at: revision.identifier, productFilter: .specific([]))
277-
} catch let error as RepositoryPackageContainer.GetDependenciesErrorWrapper {
278+
} catch let error as RepositoryPackageContainer.GetDependenciesError {
278279
let error = error.underlyingError as! UnsupportedToolsVersion
279280
XCTAssertMatch(error.description, .and(.prefix("package at '/' @"), .suffix("is using Swift tools version 3.1.0 which is no longer supported; consider using '// swift-tools-version:4.0' to specify the current tools version")))
280281
}
@@ -495,4 +496,62 @@ class RepositoryPackageContainerProviderTests: XCTestCase {
495496
)
496497
}
497498
}
499+
500+
501+
func testMissingBranchDiagnostics() {
502+
mktmpdir { tmpDir in
503+
// Create a repository.
504+
let packageDir = tmpDir.appending(component: "SomePackage")
505+
try localFileSystem.createDirectory(packageDir)
506+
initGitRepo(packageDir)
507+
let packageRepo = GitRepository(path: packageDir)
508+
509+
// Create a package manifest in it (it only needs the `swift-tools-version` part, because we'll supply the manifest later).
510+
let manifestFile = packageDir.appending(component: "Package.swift")
511+
try localFileSystem.writeFileContents(manifestFile, bytes: ByteString("// swift-tools-version:4.2"))
512+
513+
// Commit it and tag it.
514+
try packageRepo.stage(file: "Package.swift")
515+
try packageRepo.commit(message: "Initial")
516+
try packageRepo.tag(name: "1.0.0")
517+
518+
// Rename the `master` branch to `main`.
519+
try systemQuietly([Git.tool, "-C", packageDir.pathString, "branch", "-m", "main"])
520+
521+
// Create a repository manager for it.
522+
let repoProvider = GitRepositoryProvider()
523+
let repositoryManager = RepositoryManager(path: packageDir, provider: repoProvider, delegate: nil, fileSystem: localFileSystem)
524+
525+
// Create a container provider, configured with a mock manifest loader that will return the package manifest.
526+
let manifest = Manifest.createV4Manifest(
527+
name: packageDir.basename,
528+
path: packageDir.pathString,
529+
url: packageDir.pathString,
530+
packageKind: .root,
531+
targets: [
532+
TargetDescription(name: packageDir.basename, path: packageDir.pathString)
533+
]
534+
)
535+
let containerProvider = RepositoryPackageContainerProvider(repositoryManager: repositoryManager, manifestLoader: MockManifestLoader(manifests: [.init(url: packageDir.pathString, version: nil) : manifest]))
536+
537+
// Get a hold of the container for the test package.
538+
let packageRef = PackageReference(identity: "somepackage", path: packageDir.pathString)
539+
let container = try await { containerProvider.getContainer(for: packageRef, completion: $0) } as! RepositoryPackageContainer
540+
541+
// Simulate accessing a fictitious dependency on the `master` branch, and check that we get back the expected error.
542+
do { _ = try container.getDependencies(at: "master", productFilter: .everything) }
543+
catch let error as RepositoryPackageContainer.GetDependenciesError {
544+
// We expect to get an error message that mentions main.
545+
XCTAssertMatch(error.description, .and(.prefix("could not find a branch named ‘master’"), .suffix("(did you mean ‘main’?)")))
546+
}
547+
548+
// Simulate accessing a fictitious dependency on some random commit that doesn't exist, and check that we get back the expected error.
549+
do { _ = try container.getDependencies(at: "535f4cb5b4a0872fa691473e82d7b27b9894df00", productFilter: .everything) }
550+
catch let error as RepositoryPackageContainer.GetDependenciesError {
551+
// We expect to get an error message that mentions main.
552+
XCTAssertMatch(error.description, .prefix("could not find the commit 535f4cb5b4a0872fa691473e82d7b27b9894df00"))
553+
}
554+
}
555+
}
556+
498557
}

0 commit comments

Comments
 (0)