Skip to content

Commit

Permalink
[SourceControl] Start sketching a CheckoutManager class.
Browse files Browse the repository at this point in the history
 - This is intended to abstract out and own the raw checkouts that we need to do
   to perform various operations -- dependency resolution in particular may have
   a need to clone a large number of repositories (some of which are never
   used).

 - My hope is to be able to have a relative abstract interface that simply owns
   the raw checkouts (which I am planning to attempt to take bare clones of, for
   `git`) in such a way that we can clone them in parallel and potentially share
   them across projects.

 - I suspect the SCM subsystem is likely to grow to have a sizeable number of
   pieces, so I carved out a SourceControl module for it.
  • Loading branch information
ddunbar committed May 20, 2016
1 parent 0f70396 commit 301d726
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Package.swift
Expand Up @@ -44,6 +44,10 @@ let package = Package(
/** Basic support library */
name: "Basic",
dependencies: ["libc", "POSIX"]),
Target(
/** Source control operations */
name: "SourceControl",
dependencies: ["Basic", "Utility"]),

// MARK: Project Model

Expand Down
147 changes: 147 additions & 0 deletions Sources/SourceControl/CheckoutManager.swift
@@ -0,0 +1,147 @@
/*
This source file is part of the Swift.org open source project
Copyright 2016 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Utility

/// Specifies a repository address.
public struct RepositorySpecifier {
/// The URL of the repository.
public let url: String

/// Create a specifier.
public init(url: String) {

This comment has been minimized.

Copy link
@czechboy0

czechboy0 May 20, 2016

Contributor

Are we trusting the caller to normalize the url, so that Zewo/zewo and zewo/Zewo are still the same repository? Otherwise the hash value below will be different for each.

self.url = url
}

/// A unique identifier for this specifier.
///
/// This identifier is suitable for use in a file system path, and
/// unique for each repository.
public var fileSystemIdentifier: String {
// FIXME: Need to do something better here.
return url.basename + "-" + String(url.hashValue)
}
}

/// A repository provider.
public protocol RepositoryProvider {
/// Fetch the complete repository at the given location to `path`.
func fetch(repository: RepositorySpecifier, to path: String) throws
}

/// Manages a collection of repository checkouts.
public class CheckoutManager {
/// Handle to a managed repository.
public class RepositoryHandle {
enum Status {
/// The repository has not be requested.

This comment has been minimized.

Copy link
@czechboy0

czechboy0 May 20, 2016

Contributor

Is that a typo? has not been requested?

case uninitialized

/// The repository is being fetched.
case pending

/// The repository is available.
case available

/// The repository was unable to be fetched.
case error
}

/// The manager this repository is owned by.
private unowned let manager: CheckoutManager

This comment has been minimized.

Copy link
@czechboy0

czechboy0 May 20, 2016

Contributor

What's the advantage of unowned over weak?


/// The subpath of the repository within the manager.
private let subpath: String

/// The status of the repository.
private var status: Status = .uninitialized

private init(manager: CheckoutManager, subpath: String) {
self.manager = manager
self.subpath = subpath
}

/// Check if the repository has been fetched.
public var isAvailable: Bool {
switch status {
case .available:
return true
default:
return false
}
}

/// Add a function to be called when the repository is available.
///
/// This function will be called on an unspecified thread when the
/// repository fetch operation is complete.
public func addObserver(whenFetched body: (RepositoryHandle) -> ()) {
fatalError("FIXME: Not implemented")
}
}

/// The path under which repositories are stored.
private let path: String

/// The repository provider.
private let provider: RepositoryProvider

/// The map of registered repositories.
//
// FIXME: We should use a more sophisticated map here, which tracks the full
// specifier but then is capable of efficiently determining if two
// repositories map to the same location.
private var repositories: [String: RepositoryHandle] = [:]

/// Create a new empty manager.
///
/// - path: The path under which to store repositories. This should be a
/// directory in which the content can be completely managed by this
/// instance.
public init(path: String, provider: RepositoryProvider) {
self.path = path
self.provider = provider
}

/// Get a handle to a repository.
///
/// This will initiate a clone of the repository automatically, if
/// necessary, and immediately return. The client can add observers to the
/// result in order to know when the repository is available.
public func lookup(repository: RepositorySpecifier) -> RepositoryHandle {
// Check to see if the repository has been provided.
if let handle = repositories[repository.url] {
return handle
}

// Otherwise, fetch the repository and return a handle.
let subpath = repository.fileSystemIdentifier
let handle = RepositoryHandle(manager: self, subpath: subpath)
repositories[repository.url] = handle

// FIXME: This should run on a background thread.
do {
handle.status = .pending
try provider.fetch(repository: repository, to: Path.join(path, subpath))
handle.status = .available
} catch {
// FIXME: Handle failure more sensibly.
handle.status = .error
}

return handle
}
}

extension CheckoutManager.RepositoryHandle: CustomStringConvertible {
public var description: String {
return "<\(self.dynamicType) subpath:\(subpath.debugDescription)>"
}
}
2 changes: 2 additions & 0 deletions Tests/LinuxMain.swift
Expand Up @@ -22,6 +22,7 @@ import PackageDescriptionTestSuite
import PackageGraphTestSuite
import PackageLoadingTestSuite
import PackageModelTestSuite
import SourceControlTestSuite
import UtilityTestSuite

var tests = [XCTestCaseEntry]()
Expand All @@ -33,5 +34,6 @@ tests += PackageDescriptionTestSuite.allTests()
tests += PackageGraphTestSuite.allTests()
tests += PackageLoadingTestSuite.allTests()
tests += PackageModelTestSuite.allTests()
tests += SourceControlTestSuite.allTests()
tests += UtilityTestSuite.allTests()
XCTMain(tests)
61 changes: 61 additions & 0 deletions Tests/SourceControl/CheckoutManagerTests.swift
@@ -0,0 +1,61 @@
/*
This source file is part of the Swift.org open source project
Copyright 2016 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import XCTest

import SourceControl

import func POSIX.mkdtemp

private enum DummyError: ErrorProtocol {
case InvalidRepository
}

private class DummyRepositoryProvider: RepositoryProvider {
func fetch(repository: RepositorySpecifier, to path: String) throws {
// We only support one dummy URL.
if repository.url.basename != "dummy" {
throw DummyError.InvalidRepository
}
}
}

class CheckoutManagerTests: XCTestCase {
func testBasics() {
try! POSIX.mkdtemp(#function) { path in
let manager = CheckoutManager(path: path, provider: DummyRepositoryProvider())

// Check that we can "fetch" a repository.
let dummyRepo = RepositorySpecifier(url: "dummy")
let handle = manager.lookup(repository: dummyRepo)

// We should always get back the same handle once fetched.
XCTAssert(handle === manager.lookup(repository: dummyRepo))

// Validate that the repo is available.
XCTAssertTrue(handle.isAvailable)

// Get a bad repository.
let badDummyRepo = RepositorySpecifier(url: "badDummy")
let badHandle = manager.lookup(repository: badDummyRepo)

// Validate that the repo is unavailable.
XCTAssertFalse(badHandle.isAvailable)
}
}
}

extension CheckoutManagerTests {
static var allTests: [(String, (CheckoutManagerTests) -> () throws -> Void)] {
return [
("testBasic", testBasics),
]
}
}
19 changes: 19 additions & 0 deletions Tests/SourceControl/XCTestManifests.swift
@@ -0,0 +1,19 @@
/*
This source file is part of the Swift.org open source project
Copyright 2016 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import XCTest

#if !os(OSX)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(CheckoutManagerTests.allTests),
]
}
#endif

0 comments on commit 301d726

Please sign in to comment.