Skip to content

Commit 75b8352

Browse files
authored
Merge pull request #2925 from abertelrud/eng/better-git-diagnostics
Improve the diagnostics that are emitted when a package dependency specifies a non-existent branch or commit
2 parents bc34fd9 + aded72d commit 75b8352

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)