diff --git a/Makefile b/Makefile index 94574ba..f77e193 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # By default export all variables export -.PHONY: install release debug build setup clean +.PHONY: install release debug build setup clean test PROJECT ?= 'modulo.xcodeproj' SCHEME ?= 'modulo' @@ -11,6 +11,9 @@ CONFIGURATION ?= 'Debug' # Build for debugging debug: build +test: + xcodebuild -project $(PROJECT) -scheme ModuloKit test + # Install `modulo` to `/usr/local/bin` install: release cp $(SYMROOT)/Release/modulo /usr/local/bin/ diff --git a/Modules/ELCLI/ELCLI/CLI.swift b/Modules/ELCLI/ELCLI/CLI.swift index 84a3a70..b320799 100755 --- a/Modules/ELCLI/ELCLI/CLI.swift +++ b/Modules/ELCLI/ELCLI/CLI.swift @@ -154,7 +154,20 @@ open class CLI { // it's a "--flag value" type argument. if index < arguments.count - 1 { value = arguments[index + 1] - skipNext = true + // If we're assuming `--flag value` make sure + // our `value` isn't a stop marker or flag. If it is + // our value should instead be `nil`'d out so our + // command does not get a value where it is our next + // argument. + if let argValue = value, + isStopMarker(argValue) || isFlag(argValue) { + value = nil + } else { + // However if that's not the case we should skip + // the next command because we really did get + // `--flag value` + skipNext = true + } } } } diff --git a/ModuloKit/Actions.swift b/ModuloKit/Actions.swift index faadac5..98244de 100644 --- a/ModuloKit/Actions.swift +++ b/ModuloKit/Actions.swift @@ -22,8 +22,8 @@ open class Actions { } } - open func addDependency(_ url: String, version: SemverRange?, unmanaged: Bool) -> ErrorCode { - let dep = DependencySpec(repositoryURL: url, version: version) + open func addDependency(_ url: String, version: SemverRange?, unmanagedValue: String?, unmanaged: Bool) -> ErrorCode { + let dep = DependencySpec(repositoryURL: url, version: version, unmanagedValue: unmanagedValue) if var workingSpec = ModuleSpec.workingSpec() { // does this dep already exist in here?? if let _ = workingSpec.dependencyForURL(url) { @@ -77,6 +77,16 @@ open class Actions { exit(checkoutResult.errorMessage()) } } + if let unmanagedValue = dep.unmanagedValue { + let checkoutResult = scm.checkout(branchOrHash: unmanagedValue, path: clonePath) + if checkoutResult != .success { + exit(checkoutResult.errorMessage()) + } + let pullResult = scm.pull(clonePath, remoteData: nil) + if pullResult != .success { + exit(pullResult.errorMessage()) + } + } // things worked, so add it to the approprate place in the overall state. if explicit { @@ -92,7 +102,7 @@ open class Actions { } // if they're unmanaged and on a branch, tracking a remote, just do a pull - if dep.unmanaged == true, let currentBranch = scm.branchName(clonePath) { + if dep.unmanaged == true && dep.unmanagedValue == nil, let currentBranch = scm.branchName(clonePath) { if scm.remoteTrackingBranch(currentBranch) != nil { let pullResult = scm.pull(clonePath, remoteData: nil) if pullResult != .success { @@ -106,8 +116,13 @@ open class Actions { if checkoutResult != .success { exit(checkoutResult.errorMessage()) } + } else if let unmanagedValue = dep.unmanagedValue { + let checkoutResult = scm.checkout(branchOrHash: unmanagedValue, path: clonePath) + if checkoutResult != .success { + exit(checkoutResult.errorMessage()) + } } else { - exit("\(dep.name()) doesn't have a version and isn't unmanaged, not sure what to do.") + exit("\(dep.name()) doesn't have a version and isn't marked as 'unmanaged', not sure what to do.") } } } diff --git a/ModuloKit/Commands/AddCommand.swift b/ModuloKit/Commands/AddCommand.swift index 66de609..599e004 100644 --- a/ModuloKit/Commands/AddCommand.swift +++ b/ModuloKit/Commands/AddCommand.swift @@ -19,6 +19,7 @@ open class AddCommand: NSObject, Command { fileprivate var repositoryURL: String! = nil fileprivate var shouldUpdate: Bool = false fileprivate var unmanaged: Bool = false + fileprivate var unmanagedValue: String? = nil // Protocol conformance open var name: String { return "add" } @@ -41,8 +42,9 @@ open class AddCommand: NSObject, Command { } } - addOption(["--unmanaged"], usage: "specifies that this module will be unmanaged") { (option, value) in + addOptionValue(["--unmanaged"], usage: "specifies that this module will be unmanaged", valueSignature: "<[hash|branch|nothing]>") { (option, value) -> Void in self.unmanaged = true + self.unmanagedValue = value } addOption(["-u", "--update"], usage: "performs the update command after adding a module") { (option, value) in @@ -58,7 +60,7 @@ open class AddCommand: NSObject, Command { let actions = Actions() if version == nil && unmanaged == false { - writeln(.stderr, "A version or range must be specified via --version, or --unmanaged must be used.") + writeln(.stderr, "A version or range must be specified via --version or --unmanaged must be used.") return ErrorCode.commandError.rawValue } @@ -69,7 +71,7 @@ open class AddCommand: NSObject, Command { } } - let result = actions.addDependency(repositoryURL, version: version, unmanaged: unmanaged) + let result = actions.addDependency(repositoryURL, version: version, unmanagedValue: unmanagedValue, unmanaged: unmanaged) if result == .success { if shouldUpdate { writeln(.stdout, "Added \(String(describing: repositoryURL)).") diff --git a/ModuloKit/SCM/Git.swift b/ModuloKit/SCM/Git.swift index f1fa95a..c4bd287 100644 --- a/ModuloKit/SCM/Git.swift +++ b/ModuloKit/SCM/Git.swift @@ -100,7 +100,7 @@ open class Git: SCM { let initialWorkingPath = FileManager.workingPath() FileManager.setWorkingPath(path) - let updateCommand = "git fetch --recurse-submodules --all --tags" + let updateCommand = "git fetch --recurse-submodules --all" let status = runCommand(updateCommand) FileManager.setWorkingPath(initialWorkingPath) @@ -175,6 +175,49 @@ open class Git: SCM { return .success } + + open func checkout(branchOrHash: String, path: String) -> SCMResult { + if !FileManager.fileExists(path) { + return .error(code: 1, message: "Module path '\(path)' does not exist.") + } + + var checkoutCommand = "" + + let initialWorkingPath = FileManager.workingPath() + FileManager.setWorkingPath(path) + + // try fetching it directly + let fetchResult = runCommand("git fetch origin \(branchOrHash)") + + if branches(".").contains(branchOrHash) { + checkoutCommand = "git checkout origin/\(branchOrHash) --quiet" + } else { + checkoutCommand = "git checkout \(branchOrHash) --quiet" + } + + if fetchResult != 0 { + if verbose { + writeln(.stderr, "Unable to find unmanaged value '\(branchOrHash)'.") + } + return .error(code: SCMDefaultError, message: "Unable to find a match for \(branchOrHash).") + } + + let status = runCommand(checkoutCommand) + + let submodulesResult = collectAnySubmodules() + + FileManager.setWorkingPath(initialWorkingPath) + + if status != 0 { + return .error(code: status, message: "Unable to checkout '\(branchOrHash)'.") + } + + if submodulesResult != .success { + return submodulesResult + } + + return .success + } open func adjustIgnoreFile(pattern: String, removing: Bool) -> SCMResult { let localModulesPath = State.instance.modulePathName diff --git a/ModuloKit/SCM/SCM.swift b/ModuloKit/SCM/SCM.swift index 5f130a4..710a859 100644 --- a/ModuloKit/SCM/SCM.swift +++ b/ModuloKit/SCM/SCM.swift @@ -76,6 +76,9 @@ public protocol SCM { func fetch(_ path: String) -> SCMResult func pull(_ path: String, remoteData: String?) -> SCMResult func checkout(version: SemverRange, path: String) -> SCMResult + /// Check out an arbitrary point or the HEAD of a branch (in git) + /// or the equivalent in other SCM solutions + func checkout(branchOrHash: String, path: String) -> SCMResult func remove(_ path: String) -> SCMResult func adjustIgnoreFile(pattern: String, removing: Bool) -> SCMResult func checkStatus(_ path: String) -> SCMResult diff --git a/ModuloKit/Specs/DependencySpec.swift b/ModuloKit/Specs/DependencySpec.swift index 03f8f63..8ff1b0c 100644 --- a/ModuloKit/Specs/DependencySpec.swift +++ b/ModuloKit/Specs/DependencySpec.swift @@ -17,6 +17,9 @@ public struct DependencySpec { var repositoryURL: String // version or version range var version: SemverRange? + /// Optional unmanaged property to track + /// such as a branch name, commit hash, or nothing + var unmanagedValue: String? var unmanaged: Bool { get { @@ -29,7 +32,8 @@ extension DependencySpec: ELDecodable { public static func decode(_ json: JSON?) throws -> DependencySpec { return try DependencySpec( repositoryURL: json ==> "repositoryURL", - version: json ==> "version" + version: json ==> "version", + unmanagedValue: json ==> "unmanagedValue" ) } @@ -42,7 +46,8 @@ extension DependencySpec: ELEncodable { public func encode() throws -> JSON { return try encodeToJSON([ "repositoryURL" <== repositoryURL, - "version" <== version + "version" <== version, + "unmanagedValue" <== unmanagedValue ]) } } diff --git a/ModuloKitTests/TestAdd.swift b/ModuloKitTests/TestAdd.swift index fcc41ee..113fe3a 100644 --- a/ModuloKitTests/TestAdd.swift +++ b/ModuloKitTests/TestAdd.swift @@ -78,4 +78,61 @@ class TestAdd: XCTestCase { FileManager.setWorkingPath("..") } + + func testAddUnmanagedModuleWithBranch() { + let status = Git().clone("git@github.com:modulo-dm/test-add.git", path: "test-add") + XCTAssertTrue(status == .success) + + FileManager.setWorkingPath("test-add") + + let repoURL = "git@github.com:modulo-dm/test-add-update.git" + + let result = Modulo.run(["add", repoURL, "--unmanaged", "master", "-v"]) + XCTAssertTrue(result == .success) + + + guard let spec = ModuleSpec.load(contentsOfFile: specFilename) else { + XCTFail("Failed to get spec from file \(specFilename)") + return } + XCTAssertTrue(spec.dependencies.count > 0) + guard let dep = spec.dependencyForURL(repoURL) else { + XCTFail("Failed to find dependency for url \(repoURL) in spec \(spec)") + return } + XCTAssertNil(dep.version) + XCTAssertTrue(dep.unmanaged) + XCTAssertNotNil(dep.unmanagedValue) + XCTAssertTrue(dep.unmanagedValue == "master") + + FileManager.setWorkingPath("..") + + Git().remove("test-add") + } + + func testAddModuleUnmanagedNoArgs() { + let status = Git().clone("git@github.com:modulo-dm/test-add.git", path: "test-add") + XCTAssertTrue(status == .success) + + FileManager.setWorkingPath("test-add") + + let repoURL = "git@github.com:modulo-dm/test-add-update.git" + + let result = Modulo.run(["add", repoURL, "--unmanaged", "-v"]) + XCTAssertTrue(result == .success) + + + guard let spec = ModuleSpec.load(contentsOfFile: specFilename) else { + XCTFail("Failed to get spec from file \(specFilename)") + return } + XCTAssertTrue(spec.dependencies.count > 0) + guard let dep = spec.dependencyForURL(repoURL) else { + XCTFail("Failed to find dependency for url \(repoURL) in spec \(spec)") + return } + XCTAssertNil(dep.version) + XCTAssertTrue(dep.unmanaged) + XCTAssertNil(dep.unmanagedValue) + + FileManager.setWorkingPath("..") + + Git().remove("test-add") + } } diff --git a/ModuloKitTests/TestDummyApp.swift b/ModuloKitTests/TestDummyApp.swift index 871646c..1259c23 100644 --- a/ModuloKitTests/TestDummyApp.swift +++ b/ModuloKitTests/TestDummyApp.swift @@ -41,7 +41,7 @@ class TestDummyApp: XCTestCase { XCTAssertTrue(spec!.dependencyForURL("git@github.com:modulo-dm/test-add-update.git") != nil) let checkedOut = Git().branchName("modules/test-add-update") - XCTAssertTrue(checkedOut == "master") + XCTAssertTrue(checkedOut == "master", "checkedOut should have been 'master' but was '\(String(describing: checkedOut))'") XCTAssertTrue(FileManager.fileExists("modules/test-add-update")) XCTAssertTrue(FileManager.fileExists("modules/test-dep1")) diff --git a/modulo.xcodeproj/xcshareddata/xcschemes/modulo.xcscheme b/modulo.xcodeproj/xcshareddata/xcschemes/modulo.xcscheme index b00a1fb..19a2b13 100644 --- a/modulo.xcodeproj/xcshareddata/xcschemes/modulo.xcscheme +++ b/modulo.xcodeproj/xcshareddata/xcschemes/modulo.xcscheme @@ -69,11 +69,11 @@ + isEnabled = "NO"> + isEnabled = "YES">