Skip to content

Commit 0c8e9b6

Browse files
committed
[5.3] Improve the diagnostics that are emitted when a package dependency specifies a non-existent branch or commit
This improves the handling of errors thrown by Git, and is quite safe because it does not affect the successful code path. It checks for the common case of a missing branch name, and as a bonus, adds a specific check to provide a suggestion related to the `master` -> `main` renaming (if `master` is requested but doesn't exist, but if `main` does exist). 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. This fix is already in the main branch. rdar://69413377
1 parent 2bec212 commit 0c8e9b6

File tree

3 files changed

+85
-4
lines changed

3 files changed

+85
-4
lines changed

Sources/PackageGraph/RepositoryPackageContainerProvider.swift

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve
261261

262262
/// The actual error that occurred.
263263
public let underlyingError: Swift.Error
264+
265+
/// Optional suggestion for how to resolve the error.
266+
public let suggestion: String?
264267
}
265268

266269
/// This is used to remember if tools version of a particular version is
@@ -371,7 +374,7 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve
371374
}.1
372375
} catch {
373376
throw GetDependenciesErrorWrapper(
374-
containerIdentifier: identifier.repository.url, reference: version.description, underlyingError: error)
377+
containerIdentifier: identifier.repository.url, reference: version.description, underlyingError: error, suggestion: nil)
375378
}
376379
}
377380

@@ -383,8 +386,28 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve
383386
return try getDependencies(at: revision)
384387
}.1
385388
} catch {
386-
throw GetDependenciesErrorWrapper(
387-
containerIdentifier: identifier.repository.url, reference: revision, underlyingError: error)
389+
// 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).
390+
if let gitInvocationError = error as? ProcessResult.Error, gitInvocationError.description.contains("Needed a single revision") {
391+
// It was a Git process invocation error. Take a look at the repository to see if we can come up with a reasonable diagnostic.
392+
if let rev = try? repository.resolveRevision(identifier: revision), repository.exists(revision: rev) {
393+
// Revision does exist, so something else must be wrong.
394+
throw GetDependenciesErrorWrapper(
395+
containerIdentifier: identifier.repository.url, reference: revision, underlyingError: error, suggestion: nil)
396+
}
397+
else {
398+
// Revision does not exist, so we customize the error.
399+
let sha1RegEx = try! RegEx(pattern: #"\A[:xdigit:]{40}\Z"#)
400+
let isBranchRev = sha1RegEx.matchGroups(in: revision).compactMap{ $0 }.isEmpty
401+
let errorMessage = "could not find " + (isBranchRev ? "a branch named ‘\(revision)" : "the commit \(revision)")
402+
let mainBranchExists = (try? repository.resolveRevision(identifier: "main")) != nil
403+
let suggestion = (revision == "master" && mainBranchExists) ? "did you mean ‘main’?" : nil
404+
throw GetDependenciesErrorWrapper(
405+
containerIdentifier: identifier.repository.url, reference: revision,
406+
underlyingError: StringError(errorMessage), suggestion: suggestion)
407+
}
408+
}
409+
// If we get this far without having thrown an error, we wrap and throw the underlying error.
410+
throw GetDependenciesErrorWrapper(containerIdentifier: identifier.repository.url, reference: revision, underlyingError: error, suggestion: nil)
388411
}
389412
}
390413

Sources/Workspace/Workspace.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2007,7 +2007,8 @@ extension Workspace {
20072007
// Emit proper error if we were not able to parse some manifest during dependency resolution.
20082008
case let error as RepositoryPackageContainer.GetDependenciesErrorWrapper:
20092009
let location = PackageLocation.Remote(url: error.containerIdentifier, reference: error.reference)
2010-
diagnostics.emit(error.underlyingError, location: location)
2010+
let suggestion = error.suggestion.flatMap{ " (\($0))" }
2011+
diagnostics.emit(StringError("\(error.underlyingError)\(suggestion ?? "")"), location: location)
20112012

20122013
default:
20132014
diagnostics.emit(error)

Tests/PackageGraphTests/RepositoryPackageContainerProviderTests.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import XCTest
1212

1313
import TSCBasic
14+
import TSCUtility
1415
import PackageLoading
1516
import PackageModel
1617
import PackageGraph
@@ -478,4 +479,60 @@ class RepositoryPackageContainerProviderTests: XCTestCase {
478479
)
479480
}
480481
}
482+
483+
func testMissingBranchDiagnostics() {
484+
mktmpdir { tmpDir in
485+
// Create a repository.
486+
let packageDir = tmpDir.appending(component: "SomePackage")
487+
try localFileSystem.createDirectory(packageDir)
488+
initGitRepo(packageDir)
489+
let packageRepo = GitRepository(path: packageDir)
490+
491+
// Create a package manifest in it (it only needs the `swift-tools-version` part, because we'll supply the manifest later).
492+
let manifestFile = packageDir.appending(component: "Package.swift")
493+
try localFileSystem.writeFileContents(manifestFile, bytes: ByteString("// swift-tools-version:4.2"))
494+
495+
// Commit it and tag it.
496+
try packageRepo.stage(file: "Package.swift")
497+
try packageRepo.commit(message: "Initial")
498+
try packageRepo.tag(name: "1.0.0")
499+
500+
// Rename the `master` branch to `main`.
501+
try systemQuietly([Git.tool, "-C", packageDir.pathString, "branch", "-m", "main"])
502+
503+
// Create a repository manager for it.
504+
let repoProvider = GitRepositoryProvider()
505+
let repositoryManager = RepositoryManager(path: packageDir, provider: repoProvider, delegate: nil, fileSystem: localFileSystem)
506+
507+
// Create a container provider, configured with a mock manifest loader that will return the package manifest.
508+
let manifest = Manifest.createV4Manifest(
509+
name: packageDir.basename,
510+
path: packageDir.pathString,
511+
url: packageDir.pathString,
512+
packageKind: .root,
513+
targets: [
514+
TargetDescription(name: packageDir.basename, path: packageDir.pathString)
515+
]
516+
)
517+
let containerProvider = RepositoryPackageContainerProvider(repositoryManager: repositoryManager, manifestLoader: MockManifestLoader(manifests: [.init(url: packageDir.pathString, version: nil) : manifest]))
518+
519+
// Get a hold of the container for the test package.
520+
let packageRef = PackageReference(identity: "somepackage", path: packageDir.pathString)
521+
let container = try await { containerProvider.getContainer(for: packageRef, completion: $0) } as! RepositoryPackageContainer
522+
523+
// Simulate accessing a fictitious dependency on the `master` branch, and check that we get back the expected error.
524+
do { _ = try container.getDependencies(at: "master") }
525+
catch let error as RepositoryPackageContainer.GetDependenciesErrorWrapper {
526+
// We expect to get an error message that mentions main.
527+
XCTAssertMatch("\(error)", .and(.prefix("could not find a branch named ‘master’"), .suffix("(did you mean ‘main’?)")))
528+
}
529+
530+
// Simulate accessing a fictitious dependency on some random commit that doesn't exist, and check that we get back the expected error.
531+
do { _ = try container.getDependencies(at: "535f4cb5b4a0872fa691473e82d7b27b9894df00") }
532+
catch let error as RepositoryPackageContainer.GetDependenciesErrorWrapper {
533+
// We expect to get an error message that mentions main.
534+
XCTAssertMatch("\(error)", .prefix("could not find the commit 535f4cb5b4a0872fa691473e82d7b27b9894df00"))
535+
}
536+
}
537+
}
481538
}

0 commit comments

Comments
 (0)