diff --git a/run.n b/run.n index c4e600a05..2be7ace4d 100644 Binary files a/run.n and b/run.n differ diff --git a/src/haxelib/VersionData.hx b/src/haxelib/VersionData.hx index 1570955e7..fd91ddb19 100644 --- a/src/haxelib/VersionData.hx +++ b/src/haxelib/VersionData.hx @@ -19,6 +19,13 @@ package haxelib; else throw 'Invalid VscID $s'; } + + public function getName() { + return switch cast(this, VcsID) { + case Git: "Git"; + case Hg: "Mercurial"; + }; + } } /** Class containing repoducible git or hg library data. **/ @@ -28,7 +35,7 @@ class VcsData { var url:String; /** Commit hash **/ @:optional - var ref:Null; + var commit:Null; /** The git tag or mercurial revision **/ @:optional var tag:Null; @@ -42,6 +49,35 @@ class VcsData { **/ @:optional var subDir:Null; + + public function isReproducible() { + return commit != null; + } + + /** + Returns an object containing the filled-in VcsData fields, + without the empty ones. + **/ + public function getCleaned() { + var data:{ + url:String, + ?commit:String, + ?tag:String, + ?branch:String, + ?subDir:String + } = { url : url }; + + if (commit != null) + data.commit = commit; + if (tag != null) + data.tag = tag; + if (!(branch == null || branch == "")) + data.branch = branch; + if (!(subDir == null || haxe.io.Path.normalize(subDir) == "")) + data.subDir = subDir; + + return data; + } } /** Data required to reproduce a library version **/ @@ -77,7 +113,7 @@ class VersionDataHelper { type: type, data: { url: vcsRegex.matched(2), - ref: vcsRegex.matched(3), + commit: vcsRegex.matched(3), branch: vcsRegex.matched(4), subDir: null, tag: null diff --git a/src/haxelib/api/FsUtils.hx b/src/haxelib/api/FsUtils.hx index 778c4296e..af23122fe 100644 --- a/src/haxelib/api/FsUtils.hx +++ b/src/haxelib/api/FsUtils.hx @@ -216,4 +216,21 @@ class FsUtils { seek(root); return ret; } + + /** + Switches to directory found at `path`, executes `f()` in this directory, + before switching back to the previous directory. + **/ + public static function runInDirectory(path:String, f:() -> T):T { + final oldCwd = Sys.getCwd(); + try { + Sys.setCwd(path); + final value = f(); + Sys.setCwd(oldCwd); + return value; + } catch (e) { + Sys.setCwd(oldCwd); + throw e; + } + } } diff --git a/src/haxelib/api/GlobalScope.hx b/src/haxelib/api/GlobalScope.hx index cb0567b1b..0f8518c15 100644 --- a/src/haxelib/api/GlobalScope.hx +++ b/src/haxelib/api/GlobalScope.hx @@ -59,9 +59,11 @@ class GlobalScope extends Scope { } public function setVcsVersion(library:ProjectName, vcsVersion:VcsID, ?data:VcsData):Void { - if (data == null) data = {url: "unknown"}; + if (data != null) { + repository.setVcsData(library, vcsVersion, data); + } - if (data.subDir != null) { + if (!(data == null || data.subDir == "" || data.subDir == null)) { final devDir = repository.getValidVersionPath(library, vcsVersion) + data.subDir; repository.setDevPath(library, devDir); } else { @@ -87,7 +89,11 @@ class GlobalScope extends Scope { if (devPath != null) return devPath; - final current = repository.getCurrentVersion(library); + final current = try { + repository.getCurrentVersion(library); + } catch (e:Repository.CurrentVersionException) { + throw new ScopeException('Library `$library` has no version set in the global scope'); + }; return repository.getValidVersionPath(library, current); } @@ -273,4 +279,15 @@ class GlobalScope extends Scope { return {path: path, version: current}; } + public function resolve(library:ProjectName):VersionData { + final version = repository.getCurrentVersion(library); + + return switch version { + case vcs if (VcsID.isValid(vcs)): + final vcsId = VcsID.ofString(vcs); + VcsInstall(vcsId, repository.getVcsData(library, vcsId)); + case semVer: + Haxelib(SemVer.ofString(semVer)); + }; + } } diff --git a/src/haxelib/api/Installer.hx b/src/haxelib/api/Installer.hx index 18d21a969..f54fa976f 100644 --- a/src/haxelib/api/Installer.hx +++ b/src/haxelib/api/Installer.hx @@ -9,7 +9,6 @@ import haxelib.VersionData; import haxelib.api.Repository; import haxelib.api.Vcs; import haxelib.api.LibraryData; -import haxelib.api.LibFlagData; using StringTools; using Lambda; @@ -17,6 +16,10 @@ using haxelib.MetaData; /** Exception thrown when an error occurs during installation. **/ class InstallationException extends haxe.Exception {} + +/** Exception thrown when an update is cancelled. **/ +class UpdateCancelled extends InstallationException {} + /** Exception thrown when a `vcs` error interrupts installation. **/ class VcsCommandFailed extends InstallationException { public final type:VcsID; @@ -157,19 +160,28 @@ private function getLatest(versions:Array):SemVer { **/ class Installer { /** If set to `true` library dependencies will not be installed. **/ - public static var skipDependencies = false; + public var skipDependencies = false; /** - If this is set to true, dependency versions will be reinstalled + If this is set to `true`, dependency versions will be reinstalled even if already installed. **/ - public static var forceInstallDependencies = false; + public var forceInstallDependencies = false; + + /** + If set to `true`, submodules will not get cloned or updated when + installing VCS libraries. + + This setting only works for libraries installed via a VCS that allows + cloning a repository without its submodules (only `git`). + **/ + public var noVcsSubmodules = false; final scope:Scope; final repository:Repository; final userInterface:UserInterface; - final vcsBranchesByLibraryName = new Map(); + final vcsDataByName = new Map(); /** Creates a new Installer object that installs projects to `scope`. @@ -207,15 +219,15 @@ class Installer { } /** - Clears memory on git or hg library branches. + Clears cached data for git or hg libraries. An installer instance keeps track of updated vcs dependencies - to avoid cloning the same branch twice. + to avoid cloning the same version twice. This function can be used to clear that memory. **/ - public function forgetVcsBranches():Void { - vcsBranchesByLibraryName.clear(); + public function forgetVcsDataCache():Void { + vcsDataByName.clear(); } /** Installs libraries from the `haxelib.json` file at `path`. **/ @@ -351,14 +363,8 @@ class Installer { library = getVcsLibraryName(library, id, vcsData.subDir); - scope.setVcsVersion(library, id, vcsData); + setVcsVersion(library, id, vcsData); - if (vcsData.subDir != null) { - final path = scope.getPath(library); - userInterface.log(' Development directory set to $path'); - } else { - userInterface.log(' Current version is now $id'); - } userInterface.log("Done"); handleDependenciesVcs(library, id, vcsData.subDir); @@ -369,15 +375,86 @@ class Installer { or pull latest changes with git or hg. **/ public function update(library:ProjectName) { + final version = scope.resolve(library); + library = getCorrectName(library, version); + // check if update is needed + if (isUpToDate(library, version)) { + userInterface.log('Library $library is already up to date'); + return; + } + try { - updateIfNeeded(library); - } catch (e:AlreadyUpToDate) { - userInterface.log(e.toString()); - } catch (e:VcsUpdateCancelled) { + updateResolved(library, version); + } catch (e:UpdateCancelled) { + // perhaps we should exit with an error? return; } } + function getCorrectName(library:ProjectName, versionData:VersionData) { + return switch versionData { + case VcsInstall(version, {subDir: subDir}): + getVcsLibraryName(library, version, subDir); + case Haxelib(_): + ProjectName.ofString(Connection.getInfo(library).name); + }; + } + + function isUpToDate(library:ProjectName, versionData:VersionData):Bool { + return switch versionData { + case Haxelib(version): + version == Connection.getLatestVersion(library); + case VcsInstall(version, _): + final vcs = getVcs(version); + !FsUtils.runInDirectory(repository.getVersionPath(library, version), vcs.checkRemoteChanges); + }; + } + + function updateResolved(library:ProjectName, versionData:VersionData) + switch versionData { + case VcsInstall(version, vcsData): + final vcs = getVcs(version); + // with version locking we'll be able to be smarter with this + final libPath = repository.getVersionPath(library, version); + + final repoVcsData = if (scope.isLocal) repository.getVcsData(library, version) else vcsData; + FsUtils.runInDirectory( + libPath, + function() { + if (vcs.getRef() != repoVcsData.commit) { + throw 'Cannot update ${version.getName()} version of $library. There are local changes.'; + } + if (vcs.hasLocalChanges()) { + if (!userInterface.confirm('Reset changes to $library $version repository in order to update to latest version')) { + userInterface.log('$library repository has not been modified', Optional); + throw new UpdateCancelled('${version.getName()} update in ${Sys.getCwd()} was cancelled'); + } + vcs.resetLocalChanges(); + } + }); + + updateVcs(library, libPath, vcs); + vcsData.commit = FsUtils.runInDirectory(libPath, vcs.getRef); + setVcsVersion(library, version, vcsData); + + // TODO: Properly handle sub directories + handleDependenciesVcs(library, version, null); + case Haxelib(_): + final latest = Connection.getLatestVersion(library); + if (repository.isVersionInstalled(library, latest)) { + userInterface.log('Latest version $latest of $library is already installed'); + // only ask if running in a global scope + if (!scope.isLocal && !userInterface.confirm('Set $library to $latest')) + return; + } else { + downloadAndInstall(library, latest); + } + scope.setVersion(library, latest); + userInterface.log(' Current version is now $latest'); + userInterface.log("Done"); + handleDependencies(library, latest); + } + /** Updates all libraries in the scope. @@ -390,14 +467,18 @@ class Installer { for (library in libraries) { userInterface.log('Checking $library'); + + final version = scope.resolve(library); + if (isUpToDate(library, version)) { + continue; + } + try { - updateIfNeeded(library); + updateResolved(library, version); updated = true; - } catch(e:AlreadyUpToDate) { - continue; - } catch (e:VcsUpdateCancelled) { + } catch (e:UpdateCancelled) { continue; - } catch(e) { + } catch (e) { ++failures; userInterface.log("Failed to update: " + e.toString()); userInterface.log(e.stack.toString(), Debug); @@ -414,47 +495,6 @@ class Installer { userInterface.log("All libraries are already up-to-date"); } - function updateIfNeeded(library:ProjectName) { - final current = try scope.getVersion(library) catch (_:CurrentVersionException) null; - - final vcsId = try VcsID.ofString(current) catch (_) null; - if (vcsId != null) { - final vcs = Vcs.get(vcsId); - if (vcs == null || !vcs.available) - throw 'Could not use $vcsId, please make sure it is installed and available in your PATH.'; - // with version locking we'll be able to be smarter with this - updateVcs(library, vcsId, vcs); - - scope.setVcsVersion(library, vcsId); - - handleDependenciesVcs(library, vcsId, null); - // we dont know if a subdirectory was given anymore - return; - } - - final semVer = try SemVer.ofString(current) catch (_) null; - - final info = Connection.getInfo(library); - final library = ProjectName.ofString(info.name); - final latest = info.getLatest(); - - if (semVer != null && semVer == latest) { - throw new AlreadyUpToDate('Library $library is already up to date'); - } else if (repository.isVersionInstalled(library, latest)) { - userInterface.log('Latest version $latest of $library is already installed'); - // only ask if running in a global scope - if (!scope.isLocal && !userInterface.confirm('Set $library to $latest')) - return; - } else { - downloadAndInstall(library, latest); - } - scope.setVersion(library, latest); - userInterface.log(' Current version is now $latest'); - userInterface.log("Done"); - - handleDependencies(library, latest); - } - function getDependencies(path:String):Dependencies { final jsonPath = path + Data.JSON; if (!FileSystem.exists(jsonPath)) @@ -567,19 +607,48 @@ class Installer { function setVersionAndLog(library:ProjectName, installData:VersionData) { switch installData { case VcsInstall(version, vcsData): - scope.setVcsVersion(library, version, vcsData); - if (vcsData.subDir == null){ - userInterface.log(' Current version is now $version'); - } else { - final path = scope.getPath(library); - userInterface.log(' Development directory set to $path'); - } + setVcsVersion(library, version, vcsData); case Haxelib(version): scope.setVersion(library, version); userInterface.log(' Current version is now $version'); } } + function getReproducibleVcsData(library:ProjectName, version:VcsID, data:VcsData):VcsData { + final vcs = getVcs(version); + final libPath = repository.getVersionPath(library, version); + return FsUtils.runInDirectory(libPath, function():VcsData { + return { + url: data.url, + commit: data.commit ?? vcs.getRef(), + branch: if (data.branch == null && data.tag == null) vcs.getBranchName() else data.branch, + tag: data.tag, + subDir: if (data.subDir != null) haxe.io.Path.normalize(data.subDir) else null + }; + }); + } + + /** + Retrieves fully reproducible vcs data if necessary, + and then uses it to lock down the current version. + **/ + function setVcsVersion(library:ProjectName, version:VcsID, data:VcsData) { + // save here prior to modification + vcsDataByName[library] = data; + if (!data.isReproducible()) { + // always get reproducible data for local scope + data = getReproducibleVcsData(library, version, data); + } + + scope.setVcsVersion(library, version, data); + if (data.subDir == "" || data.subDir == null) { + userInterface.log(' Current version is now $version'); + } else { + final path = scope.getPath(library); + userInterface.log(' Development directory set to $path'); + } + } + static function getInstallData(libs:List<{name:ProjectName, data:Option}>):List { final installData = new List(); @@ -652,7 +721,7 @@ class Installer { case [VcsInstall(a, vcsData1), VcsInstall(b, vcsData2)] if ((a == b) && (vcsData1.url == vcsData2.url) - && (vcsData1.ref == vcsData2.ref) + && (vcsData1.commit == vcsData2.commit) && (vcsData1.branch == vcsData2.branch) && (vcsData1.tag == vcsData2.tag) && (vcsData1.subDir == vcsData2.subDir) @@ -748,32 +817,36 @@ class Installer { userInterface.logInstallationProgress('Done installing $library $version', total, total); } - function installVcs(library:ProjectName, id:VcsID, vcsData:VcsData) { - final vcs = Vcs.get(id); + function getVcs(id:VcsID):Vcs { + final vcs = Vcs.create(id, userInterface.log.bind(_, Debug)); if (vcs == null || !vcs.available) throw 'Could not use $id, please make sure it is installed and available in your PATH.'; + return vcs; + } - final libPath = repository.getVersionPath(library, id); + function installVcs(library:ProjectName, id:VcsID, vcsData:VcsData) { + final vcs = getVcs(id); - final branch = vcsData.ref != null ? vcsData.ref : vcsData.branch; - final url:String = vcsData.url; + final libPath = repository.getVersionPath(library, id); function doVcsClone() { - userInterface.log('Installing $library from $url' + (branch != null ? " branch: " + branch : "")); - final tag = vcsData.tag; + FsUtils.safeDir(libPath); + userInterface.log('Installing $library from ${vcsData.url}' + + (vcsData.branch != null ? " branch: " + vcsData.branch : "") + + (vcsData.tag != null ? " tag: " + vcsData.tag : "") + + (vcsData.commit != null ? " commit: " + vcsData.commit : "") + ); try { - vcs.clone(libPath, url, branch, tag, userInterface.log.bind(_, Debug)); + vcs.clone(libPath, vcsData, noVcsSubmodules); } catch (error:VcsError) { FsUtils.deleteRec(libPath); switch (error) { case VcsUnavailable(vcs): throw 'Could not use ${vcs.executable}, please make sure it is installed and available in your PATH.'; - case CantCloneRepo(vcs, _, stderr): - throw 'Could not clone ${vcs.name} repository' + (stderr != null ? ":\n" + stderr : "."); - case CantCheckoutBranch(_, branch, stderr): - throw 'Could not checkout branch, tag or path "$branch": ' + stderr; - case CantCheckoutVersion(_, version, stderr): - throw 'Could not checkout tag "$version": ' + stderr; + case CantCloneRepo(_, _, stderr): + throw 'Could not clone ${id.getName()} repository' + (stderr != null ? ":\n" + stderr : "."); + case CantCheckout(_, ref, stderr): + throw 'Could not checkout commit or tag "$ref": ' + stderr; case CommandFailed(_, code, stdout, stderr): throw new VcsCommandFailed(id, code, stdout, stderr); }; @@ -781,72 +854,46 @@ class Installer { } if (repository.isVersionInstalled(library, id)) { - userInterface.log('You already have $library version ${vcs.directory} installed.'); + userInterface.log('You already have $library version $id installed.'); - final wasUpdated = vcsBranchesByLibraryName.exists(library); - // difference between a key not having a value and the value being null + final currentData = vcsDataByName[library] ?? repository.getVcsData(library, id); + FsUtils.runInDirectory(libPath, function() { + if (vcs.hasLocalChanges() || vcs.getRef() != currentData.commit) { + throw 'Cannot overwrite currently installed $id version of $library. There are local changes.'; + } + }); - final currentBranch = vcsBranchesByLibraryName[library]; + final wasUpdated = vcsDataByName.exists(library); - // TODO check different urls as well - if (branch != null && (!wasUpdated || currentBranch != branch)) { - final currentBranchStr = currentBranch != null ? currentBranch : ""; - if (!userInterface.confirm('Overwrite branch: "$currentBranchStr" with "$branch"')) { - userInterface.log('Library $library $id repository remains at "$currentBranchStr"'); - return; - } + final requiresNewClone = !(vcsData.url == currentData.url + && vcsData.branch == currentData.branch + && vcsData.tag == currentData.tag); + final sameCommit = vcsData.commit == currentData.commit; + + if (requiresNewClone) { FsUtils.deleteRec(libPath); doVcsClone(); - } else if (wasUpdated) { - userInterface.log('Library $library version ${vcs.directory} already up to date.'); - return; + } else if (!sameCommit && vcsData.commit != null) { + // update to given commit + FsUtils.runInDirectory(libPath, function() { + vcs.updateWithData(vcsData); + }); + } else if (wasUpdated || sameCommit || !FsUtils.runInDirectory(libPath, vcs.checkRemoteChanges)) { + userInterface.log('Library $library version $id already up to date'); } else { - userInterface.log('Updating $library version ${vcs.directory}...'); - try { - updateVcs(library, id, vcs); - } catch (e:AlreadyUpToDate){ - userInterface.log(e.toString()); - } + // the data is identical (apart from commit) so we just update instead of reinstalling + userInterface.log('Updating $library version $id...'); + updateVcs(library, libPath, vcs); } } else { - FsUtils.safeDir(libPath); doVcsClone(); } - - vcsBranchesByLibraryName[library] = branch; + vcsData.commit = FsUtils.runInDirectory(libPath, vcs.getRef); } - function updateVcs(library:ProjectName, id:VcsID, vcs:Vcs) { - final dir = repository.getVersionPath(library, id); - - final oldCwd = Sys.getCwd(); - Sys.setCwd(dir); - - final success = try { - vcs.update( - function() { - if (userInterface.confirm('Reset changes to $library $id repository in order to update to latest version')) - return true; - userInterface.log('$library repository has not been modified', Optional); - return false; - }, - userInterface.log.bind(_, Debug), - userInterface.log.bind(_, Optional) - ); - } catch (e:VcsError) { - Sys.setCwd(oldCwd); - switch e { - case CommandFailed(_, code, stdout, stderr): - throw new VcsCommandFailed(id, code, stdout, stderr); - default: Util.rethrow(e); // other errors aren't expected here - } - } catch (e:haxe.Exception) { - Sys.setCwd(oldCwd); - Util.rethrow(e); - } - Sys.setCwd(oldCwd); - if (!success) - throw new AlreadyUpToDate('Library $library $id repository is already up to date'); - userInterface.log('$library was updated'); - } + function updateVcs(library:ProjectName, path:String, vcs:Vcs) + FsUtils.runInDirectory(path, function() { + vcs.mergeRemoteChanges(); + userInterface.log('$library was updated'); + }); } diff --git a/src/haxelib/api/Repository.hx b/src/haxelib/api/Repository.hx index 71cf02f26..c9037334d 100644 --- a/src/haxelib/api/Repository.hx +++ b/src/haxelib/api/Repository.hx @@ -3,6 +3,7 @@ package haxelib.api; import sys.FileSystem; import sys.io.File; +import haxelib.VersionData.VcsData; import haxelib.VersionData.VcsID; import haxelib.api.RepoManager; import haxelib.api.LibraryData; @@ -281,6 +282,44 @@ class Repository { return getProjectVersionPath(name, version); } + private function getVcsDataPath(name:ProjectName, version:VcsID) { + return haxe.io.Path.join([getProjectPath(name), '.${version}data']); + } + + public function setVcsData(name:ProjectName, version:VcsID, vcsData:VcsData) { + File.saveContent( + getVcsDataPath(name, version), + haxe.Json.stringify(vcsData.getCleaned(), "\t") + ); + } + + public function getVcsData(name:ProjectName, version:VcsID):VcsData { + final vcsDataPath = getVcsDataPath(name, version); + if (!FileSystem.exists(vcsDataPath) || FileSystem.isDirectory(vcsDataPath)) { + final versionPath = getProjectVersionPath(name, version); + + if (!FileSystem.exists(versionPath)) { + throw 'Library $name version $version is not installed'; + } + + return FsUtils.runInDirectory(versionPath, function():VcsData { + final vcs = Vcs.create(version); + return { + url: vcs.getOriginUrl(), + commit: vcs.getRef() + }; + } ); + } + final data = haxe.Json.parse(File.getContent(vcsDataPath)); + return { + url: data.url, + commit: data.commit, + tag: data.tag, + branch: data.branch, + subDir: data.subDir + }; + } + /** Returns the correctly capitalized name for library `name`. diff --git a/src/haxelib/api/Scope.hx b/src/haxelib/api/Scope.hx index 9b50076bb..6afc71709 100644 --- a/src/haxelib/api/Scope.hx +++ b/src/haxelib/api/Scope.hx @@ -14,6 +14,8 @@ typedef InstallationInfo = { final devPath:Null; } +class ScopeException extends haxe.Exception {} + /** Returns scope for directory `dir`. If `dir` is omitted, uses the current working directory. @@ -127,6 +129,11 @@ abstract class Scope { abstract function resolveCompiler():LibraryData; + /** + Returns the full version data for `library`. + **/ + public abstract function resolve(library:ProjectName):VersionData; + // TODO: placeholders until https://github.com/HaxeFoundation/haxe/wiki/Haxe-haxec-haxelib-plan static function loadOverrides():LockFormat { return {}; diff --git a/src/haxelib/api/Vcs.hx b/src/haxelib/api/Vcs.hx index dd147b729..dc0337009 100644 --- a/src/haxelib/api/Vcs.hx +++ b/src/haxelib/api/Vcs.hx @@ -24,142 +24,124 @@ package haxelib.api; import sys.FileSystem; import sys.thread.Thread; import sys.thread.Lock; -import haxelib.VersionData.VcsID; +import haxelib.VersionData; using haxelib.api.Vcs; using StringTools; -interface IVcs { - /** The name of the vcs system. **/ - final name:String; - /** The directory used to install vcs library versions to. **/ - final directory:String; +private interface IVcs { /** The vcs executable. **/ final executable:String; /** Whether or not the executable can be accessed successfully. **/ var available(get, null):Bool; /** - Clone repository at `vcsPath` into `libPath`. + Clone repository specified in `data` into `libPath`. - If `branch` is specified, the repository is checked out to that branch. + If `flat` is set to true, recursive cloning is disabled. + **/ + function clone(libPath:String, data:VcsData, flat:Bool = false):Void; - `version` can also be specified for tags in git or revisions in mercurial. + /** + Checks out the repository based on the data provided + **/ + function updateWithData(data:{?branch:String, ?tag:String, ?commit:String}):Void; - `debugLog` will be used to log executable output. + /** + Merges remote changes into repository. **/ - function clone(libPath:String, vcsPath:String, ?branch:String, ?version:String, ?debugLog:(msg:String)->Void):Void; + function mergeRemoteChanges():Void; /** - Updates repository in CWD or CWD/`Vcs.directory` to HEAD. - For git CWD must be in the format "...haxelib-repo/lib/git". + Checks for possible remote changes, and returns whether there are any available. + **/ + function checkRemoteChanges():Bool; - By default, uncommitted changes prevent updating. - If `confirm` is passed in, the changes may occur - if `confirm` returns true. + /** + Returns whether any uncommited local changes exist. + **/ + function hasLocalChanges():Bool; - `debugLog` will be used to log executable output. + /** + Resets all local changes present in the working tree. + **/ + function resetLocalChanges():Void; - `summaryLog` may be used to log summaries of changes. + function getRef():String; - Returns `true` if update successful. - **/ - function update(?confirm:()->Bool, ?debugLog:(msg:String)->Void, ?summaryLog:(msg:String)->Void):Bool; + function getOriginUrl():String; + + function getBranchName():Null; } /** Enum representing errors that can be thrown during a vcs operation. **/ enum VcsError { VcsUnavailable(vcs:Vcs); CantCloneRepo(vcs:Vcs, repo:String, ?stderr:String); - CantCheckoutBranch(vcs:Vcs, branch:String, stderr:String); - CantCheckoutVersion(vcs:Vcs, version:String, stderr:String); + CantCheckout(vcs:Vcs, ref:String, stderr:String); CommandFailed(vcs:Vcs, code:Int, stdout:String, stderr:String); } -/** Exception thrown when a vcs update is cancelled. **/ -class VcsUpdateCancelled extends haxe.Exception {} - /** Base implementation of `IVcs` for `Git` and `Mercurial` to extend. **/ abstract class Vcs implements IVcs { /** If set to true, recursive cloning is disabled **/ - public static var flat = false; - - public final name:String; - public final directory:String; public final executable:String; public var available(get, null):Bool; - var availabilityChecked = false; - var executableSearched = false; - - function new(executable:String, directory:String, name:String) { - this.name = name; - this.directory = directory; + function new(executable:String, ?debugLog:(message:String)->Void) { this.executable = executable; + if (debugLog != null) + this.debugLog = debugLog; } - static var reg:Map; - - /** Returns the Vcs instance for `id`. **/ - public static function get(id:VcsID):Null { - if (reg == null) - reg = [ - VcsID.Git => new Git("git", "git", "Git"), - VcsID.Hg => new Mercurial("hg", "hg", "Mercurial") - ]; + /** + Creates and returns a Vcs instance for `id`. - return reg.get(id); + If `debugLog` is specified, it is used to log debug information + for executable calls. + **/ + public static function create(id:VcsID, ?debugLog:(message:String)->Void):Null { + return switch id { + case Hg: + new Mercurial("hg", debugLog); + case Git: + new Git("git", debugLog); + }; } /** Returns the sub directory to use for library versions of `id`. **/ - public static function getDirectoryFor(id:VcsID):String { - return switch (get(id)) { - case null: throw 'Unable to get directory for $id'; - case vcs: vcs.directory; + public static function getDirectoryFor(id:VcsID) { + return switch id { + case Git: "git"; + case Hg: "hg"; } } - static function set(id:VcsID, vcs:Vcs, ?rewrite:Bool):Void { - final existing = reg.get(id) != null; - if (!existing || rewrite) - reg.set(id, vcs); - } + dynamic function debugLog(msg:String) {} - /** Returns the relevant Vcs if a vcs version is installed at `libPath`. **/ - public static function getVcsForDevLib(libPath:String):Null { - for (k in reg.keys()) { - if (FileSystem.exists(libPath + "/" + k) && FileSystem.isDirectory(libPath + "/" + k)) - return reg.get(k); - } - return null; - } + abstract function searchExecutable():Bool; - function searchExecutable():Void { - executableSearched = true; + function getCheckArgs() { + return []; } - function checkExecutable():Bool { - available = (executable != null) && try run([]).code == 0 catch(_:Dynamic) false; - availabilityChecked = true; - - if (!available && !executableSearched) - searchExecutable(); - - return available; + final function checkExecutable():Bool { + return (executable != null) && try run(getCheckArgs()).code == 0 catch (_:Dynamic) false; } final function get_available():Bool { - if (!availabilityChecked) - checkExecutable(); + if (available == null) { + available = checkExecutable() || searchExecutable(); + } return available; } - final function run(args:Array, ?debugLog:(msg:String) -> Void, strict = false):{ + final function run(args:Array, strict = false):{ code:Int, out:String, err:String, } { inline function print(msg) - if (debugLog != null && msg != "") + if (msg != "") debugLog(msg); print("# Running command: " + executable + " " + args.toString() + "\n"); @@ -229,36 +211,21 @@ abstract class Vcs implements IVcs { return ret; } - public abstract function clone(libPath:String, vcsPath:String, ?branch:String, ?version:String, ?debugLog:(msg:String)->Void):Void; - - public abstract function update(?confirm:() -> Bool, ?debugLog:(msg:String) -> Void, ?summaryLog:(msg:String) -> Void):Bool; } /** Class wrapping `git` operations. **/ class Git extends Vcs { - @:allow(haxelib.api.Vcs.get) - function new(executable:String, directory:String, name:String) { - super(executable, directory, name); + @:allow(haxelib.api.Vcs.create) + function new(executable:String, ?debugLog:Null<(message:String) -> Void>) { + super(executable, debugLog); } - override function checkExecutable():Bool { - // with `help` cmd because without any cmd `git` can return exit-code = 1. - available = (executable != null) && try run(["help"]).code == 0 catch(_:Dynamic) false; - availabilityChecked = true; - - if (!available && !executableSearched) - searchExecutable(); - - return available; + override function getCheckArgs() { + return ["help"]; } - override function searchExecutable():Void { - super.searchExecutable(); - - if (available) - return; - + function searchExecutable():Bool { // if we have already msys git/cmd in our PATH final match = ~/(.*)git([\\|\/])cmd$/; for (path in Sys.getEnv("PATH").split(";")) { @@ -269,100 +236,160 @@ class Git extends Vcs { } if (checkExecutable()) - return; + return true; // look at a few default paths for (path in ["C:\\Program Files (x86)\\Git\\bin", "C:\\Progra~1\\Git\\bin"]) { if (FileSystem.exists(path)) { Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + path); if (checkExecutable()) - return; + return true; } } + return false; } - public function update(?confirm:()->Bool, ?debugLog:(msg:String)->Void, ?_):Bool { - if ( - run(["diff", "--exit-code", "--no-ext-diff"], debugLog).code != 0 - || run(["diff", "--cached", "--exit-code", "--no-ext-diff"], debugLog).code != 0 - ) { - if (confirm == null || !confirm()) - throw new VcsUpdateCancelled('$name update in ${Sys.getCwd()} was cancelled'); - run(["reset", "--hard"], debugLog, true); + public function checkRemoteChanges():Bool { + run(["fetch", "--depth=1"], true); + + // `git rev-parse @{u}` will fail if detached + final checkUpstream = run(["rev-parse", "@{u}"]); + if (checkUpstream.code != 0) { + return false; } + return checkUpstream.out != run(["rev-parse", "HEAD"], true).out; + } - run(["fetch"], debugLog, true); + public function mergeRemoteChanges() { + run(["reset", "--hard", "@{u}"], true); + } - // `git rev-parse @{u}` will fail if detached - final checkUpstream = run(["rev-parse", "@{u}"], debugLog); + public function clone(libPath:String, data:VcsData, flat = false):Void { + final vcsArgs = ["clone", data.url, libPath]; - if (checkUpstream.out == run(["rev-parse", "HEAD"], debugLog, true).out) - return false; // already up to date + if (data.branch != null) { + vcsArgs.push('--single-branch'); + vcsArgs.push('--branch'); + vcsArgs.push(data.branch); + } else if (data.commit == null) { + vcsArgs.push('--single-branch'); + } - // But if before we pulled specified branch/tag/rev => then possibly currently we haxe "HEAD detached at ..". - if (checkUpstream.code != 0) { - // get parent-branch: - final branch = { - final raw = run(["show-branch"], debugLog).out; - final regx = ~/\[([^]]*)\]/; - if (regx.match(raw)) - regx.matched(1); - else - raw; + final cloneDepth1 = data.commit == null || data.commit.length == 40; + // we cannot clone like this if the commit hash is short, + // as fetch requires full hash + if (cloneDepth1) { + vcsArgs.push('--depth=1'); + } + + if (run(vcsArgs).code != 0) + throw VcsError.CantCloneRepo(this, data.url/*, ret.out*/); + + if (data.branch != null && data.commit != null) { + FsUtils.runInDirectory(libPath, () -> { + if (cloneDepth1) { + runCheckoutRelatedCommand(data.commit, ["fetch", "--depth=1", getRemoteName(), data.commit]); + } + run(["reset", "--hard", data.commit], true); + }); + } else if (data.commit != null) { + FsUtils.runInDirectory(libPath, checkout.bind(data.commit, cloneDepth1)); + } else if (data.tag != null) { + FsUtils.runInDirectory(libPath, () -> { + final tagRef = 'tags/${data.tag}'; + runCheckoutRelatedCommand(tagRef, ["fetch", "--depth=1", getRemoteName(), '$tagRef:$tagRef']); + checkout('tags/${data.tag}', false); + }); + } + + if (!flat) { + FsUtils.runInDirectory(libPath, () -> { + run(["submodule", "update", "--init", "--depth=1", "--single-branch"], true); + }); + } + } + + public function updateWithData(data:{?commit:String, ?branch:Null, ?tag:Null}) { + if (data.commit != null && data.commit.length == 40) { + // full commit hash + checkout(data.commit, true); + } else if (data.branch != null) { + // short commit hash, but branch was provided + // we have to fetch the entire branch + runCheckoutRelatedCommand(data.branch, ["fetch", getRemoteName(), data.branch]); + checkout(data.commit, false); + } else if (data.tag != null) { + // short commit hash, but tag was provided + runCheckoutRelatedCommand(data.tag, ["fetch", "--depth=1", getRemoteName(), data.tag]); + if (!run(["rev-parse", data.tag], true).out.trim().startsWith(data.commit)) { + // the tag commit has changed from the one we expected... + // last resort: fetch entire remote repo + runCheckoutRelatedCommand(data.tag, ["fetch", getRemoteName()]); } + checkout(data.commit, false); + } else { + // last resort: fetch entire remote repo + runCheckoutRelatedCommand(data.tag, ["fetch", getRemoteName()]); + checkout(data.commit, false); + } + } - run(["checkout", branch, "--force"], debugLog, true); + inline function runCheckoutRelatedCommand(ref, args:Array) { + final ret = run(args); + if (ret.code != 0) { + throw VcsError.CantCheckout(this, ref, ret.out); } - run(["merge"], debugLog, true); - return true; } - public function clone(libPath:String, url:String, ?branch:String, ?version:String, ?debugLog:(msg:String)->Void):Void { - final oldCwd = Sys.getCwd(); + function checkout(ref:String, fetch:Bool) { + if (fetch) { + runCheckoutRelatedCommand(ref, ["fetch", "--depth=1", getRemoteName(), ref]); + } - final vcsArgs = ["clone", url, libPath]; + runCheckoutRelatedCommand(ref, ["checkout", ref]); - if (!Vcs.flat) - vcsArgs.push('--recursive'); + // clean up excess branch + run(["branch", "-D", "@{-1}"]); + } - if (run(vcsArgs, debugLog).code != 0) - throw VcsError.CantCloneRepo(this, url/*, ret.out*/); + function getRemoteName() { + return run(["remote"], true).out.split("\n")[0].trim(); + } - Sys.setCwd(libPath); + public function getRef():String { + return run(["rev-parse", "--verify", "HEAD"], true).out.trim(); + } - if (version != null && version != "") { - final ret = run(["checkout", "tags/" + version], debugLog); - if (ret.code != 0) { - Sys.setCwd(oldCwd); - throw VcsError.CantCheckoutVersion(this, version, ret.out); - } - } else if (branch != null) { - final ret = run(["checkout", branch], debugLog); - if (ret.code != 0){ - Sys.setCwd(oldCwd); - throw VcsError.CantCheckoutBranch(this, branch, ret.out); - } - } + public function getOriginUrl():String { + return run(["ls-remote", "--get-url", getRemoteName()], true).out.trim(); + } + + public function getBranchName():Null { + final ret = run(["symbolic-ref", "--short", "HEAD"]); + if (ret.code != 0) + return null; + return ret.out.trim(); + } + + public function hasLocalChanges():Bool { + return run(["diff", "--exit-code", "--no-ext-diff"]).code != 0 + || run(["diff", "--cached", "--exit-code", "--no-ext-diff"]).code != 0; + } - // return prev. cwd: - Sys.setCwd(oldCwd); + public function resetLocalChanges() { + run(["reset", "--hard"], true); } } /** Class wrapping `hg` operations. **/ class Mercurial extends Vcs { - @:allow(haxelib.api.Vcs.get) - function new(executable:String, directory:String, name:String) { - super(executable, directory, name); + @:allow(haxelib.api.Vcs.create) + function new(executable:String, ?debugLog:Null<(message:String) -> Void>) { + super(executable, debugLog); } - override function searchExecutable():Void { - super.searchExecutable(); - - if (available) - return; - + function searchExecutable():Bool { // if we have already msys git/cmd in our PATH final match = ~/(.*)hg([\\|\/])cmd$/; for(path in Sys.getEnv("PATH").split(";")) { @@ -371,53 +398,79 @@ class Mercurial extends Vcs { Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + newPath); } } - checkExecutable(); + return checkExecutable(); } - public function update(?confirm:()->Bool, ?debugLog:(msg:String)->Void, ?summaryLog:(msg:String)->Void):Bool { - inline function log(msg:String) if(summaryLog != null) summaryLog(msg); - - run(["pull"], debugLog); - var summary = run(["summary"], debugLog).out; - final diff = run(["diff", "-U", "2", "--git", "--subrepos"], debugLog); - final status = run(["status"], debugLog); + public function checkRemoteChanges():Bool { + run(["pull"]); // get new pulled changesets: - // (and search num of sets) - summary = summary.substr(0, summary.length - 1); - summary = summary.substr(summary.lastIndexOf("\n") + 1); + final summary = { + final out = run(["summary"]).out.rtrim(); + out.substr(out.lastIndexOf("\n") + 1); + }; + // we don't know any about locale then taking only Digit-exising:s - final changed = ~/(\d)/.match(summary); - if (changed) - // print new pulled changesets: - log(summary); - - if (diff.code + status.code + diff.out.length + status.out.length != 0) { - log(diff.out); - if (confirm == null || !confirm()) - throw new VcsUpdateCancelled('$name update in ${Sys.getCwd()} was cancelled'); - run(["update", "--clean"], debugLog, true); - } else if (changed) { - run(["update"], debugLog, true); - } + return ~/(\d)/.match(summary); + } - return changed; + public function mergeRemoteChanges() { + run(["update"], true); } - public function clone(libPath:String, url:String, ?branch:String, ?version:String, ?debugLog:(msg:String)->Void):Void { - final vcsArgs = ["clone", url, libPath]; + public function clone(libPath:String, data:VcsData, _ = false):Void { + final vcsArgs = ["clone", data.url, libPath]; - if (branch != null) { + if (data.branch != null) { vcsArgs.push("--branch"); - vcsArgs.push(branch); + vcsArgs.push(data.branch); } - if (version != null) { + if (data.commit != null) { vcsArgs.push("--rev"); - vcsArgs.push(version); + vcsArgs.push(data.commit); } - if (run(vcsArgs, debugLog).code != 0) - throw VcsError.CantCloneRepo(this, url/*, ret.out*/); + if (run(vcsArgs).code != 0) + throw VcsError.CantCloneRepo(this, data.url/*, ret.out*/); + } + + inline function runCheckoutRelatedCommand(ref, args:Array) { + final ret = run(args); + if (ret.code != 0) { + throw VcsError.CantCheckout(this, ref, ret.out); + } + } + + public function updateWithData(data:{?commit:String, ?branch:Null, ?tag:Null}) { + runCheckoutRelatedCommand(data.commit, ["pull", "--rev", data.commit]); + runCheckoutRelatedCommand(data.commit, ["checkout", data.commit]); + } + + public function getRef():String { + final out = run(["id", "-i"], true).out.trim(); + // if the hash ends with +, there are edits + if (StringTools.endsWith(out, "+")) + return out.substr(0, out.length - 2); + return out; + } + + public function getOriginUrl():String { + return run(["paths", "default"], true).out.trim(); + } + + public function getBranchName():Null { + return run(["id", "-b"], true).out.trim(); + } + + public function hasLocalChanges():Bool { + final diff = run(["diff", "-U", "2", "--git", "--subrepos"]); + final status = run(["status"]); + + return diff.code + status.code + diff.out.length + status.out.length > 0; + } + + public function resetLocalChanges() { + run(["clean"], true); } } diff --git a/src/haxelib/client/Main.hx b/src/haxelib/client/Main.hx index 1e922f011..df7916b0a 100644 --- a/src/haxelib/client/Main.hx +++ b/src/haxelib/client/Main.hx @@ -27,9 +27,9 @@ import haxe.iterators.ArrayIterator; import sys.FileSystem; import sys.io.File; -import haxelib.api.*; import haxelib.VersionData.VcsID; -import haxelib.api.LibraryData; +import haxelib.api.*; +import haxelib.api.LibraryData.Version; import haxelib.client.Args; import haxelib.Util.rethrow; @@ -56,6 +56,7 @@ class Main { final command:Command; final mainArgs:Array; + final flags:Array; final argsIterator:ArrayIterator; final useGlobalRepo:Bool; @@ -71,10 +72,6 @@ class Main { else if (args.flags.contains(Debug)) Cli.mode = Debug; - if (args.flags.contains(SkipDependencies)) - Installer.skipDependencies = true; - Vcs.flat = args.flags.contains(Flat); - // connection setup if (args.flags.contains(NoTimeout)) Connection.hasTimeout = false; @@ -94,6 +91,7 @@ class Main { command = args.command; mainArgs = args.mainArgs; + flags = args.flags; argsIterator = mainArgs.iterator(); useGlobalRepo = args.flags.contains(Global); @@ -430,7 +428,10 @@ class Main { logInstallationProgress: (Cli.mode == Debug) ? Cli.printInstallStatus: null, logDownloadProgress: (Cli.mode != Quiet) ? Cli.printDownloadStatus : null } - return new Installer(scope, userInterface); + final installer = new Installer(scope, userInterface); + installer.skipDependencies = flags.contains(SkipDependencies); + installer.noVcsSubmodules = flags.contains(Flat); + return installer; } function install() { @@ -731,25 +732,20 @@ class Main { } function vcs(id:VcsID) { - // Prepare check vcs.available: - final vcs = Vcs.get(id); - if (vcs == null || !vcs.available) - throw 'Could not use $id, please make sure it is installed and available in your PATH.'; - // get args final library = ProjectName.ofString(getArgument("Library name")); - final url = getArgument(vcs.name + " path"); + final url = getArgument(id.getName() + " path"); final ref = argsIterator.next(); final isRefHash = ref == null || LibraryData.isCommitHash(ref); - final hash = isRefHash ? ref : null; + final commit = isRefHash ? ref : null; final branch = isRefHash ? null : ref; final installer = setupAndGetInstaller(); installer.installVcsLibrary(library, id, { url: url, - ref: hash, + commit: commit, branch: branch, subDir: argsIterator.next(), tag: argsIterator.next() diff --git a/test/tests/TestGit.hx b/test/tests/TestGit.hx index 47a3de293..478a0d20c 100644 --- a/test/tests/TestGit.hx +++ b/test/tests/TestGit.hx @@ -12,6 +12,6 @@ class TestGit extends TestVcs { } public function new():Void { - super(VcsID.Git, "Git", FileSystem.fullPath(REPO_PATH), "0.9.2"); + super(VcsID.Git, "git", FileSystem.fullPath(REPO_PATH), "2feb1476dadd66ee0aa20587b1ee30a6b4faac0f"); } } diff --git a/test/tests/TestHg.hx b/test/tests/TestHg.hx index fe3a2019e..931bac878 100644 --- a/test/tests/TestHg.hx +++ b/test/tests/TestHg.hx @@ -1,17 +1,17 @@ package tests; import sys.FileSystem; -import haxelib.api.Vcs; +import haxelib.VersionData.VcsID; class TestHg extends TestVcs { static final REPO_PATH = 'test/repo/hg'; static public function init() { HaxelibTests.deleteDirectory(REPO_PATH); - HaxelibTests.runCommand('hg', ['clone', 'https://bitbucket.org/fzzr/hx.signal', REPO_PATH]); + HaxelibTests.runCommand('hg', ['clone', 'https://github.com/fzzr-/hx.signal.git', REPO_PATH]); } public function new():Void { - super(VcsID.Hg, "Mercurial", FileSystem.fullPath(REPO_PATH), "78edb4b"); + super(VcsID.Hg, "hg", FileSystem.fullPath(REPO_PATH), "78edb4b"); } } diff --git a/test/tests/TestVcs.hx b/test/tests/TestVcs.hx index 29690030b..bf6bfb216 100644 --- a/test/tests/TestVcs.hx +++ b/test/tests/TestVcs.hx @@ -17,19 +17,19 @@ class TestVcs extends TestBase static var CWD:String = null; final id:VcsID = null; - final vcsName:String = null; + final vcsExecutable:String = null; final url:String = null; final rev:String = null; var counter:Int = 0; //--------------- constructor ---------------// - public function new(id:VcsID, vcsName:String, url:String, ?rev:String) { + public function new(id:VcsID, vcsExecutable:String, url:String, ?rev:String) { super(); this.id = id; this.url = url; this.rev = rev; - this.vcsName = vcsName; + this.vcsExecutable = vcsExecutable; CWD = Sys.getCwd(); counter = 0; @@ -59,79 +59,84 @@ class TestVcs extends TestBase //----------------- tests -------------------// - public function testGetVcs():Void { - assertTrue(Vcs.get(id) != null); - assertTrue(Vcs.get(id).name == vcsName); + public function testCreateVcs():Void { + assertTrue(Vcs.create(id) != null); + assertTrue(Vcs.create(id).executable == vcsExecutable); } public function testAvailable():Void { - assertTrue(getVcs().available); + assertTrue(createVcs().available); } // --------------- clone --------------- // - public function testGetVcsByDir():Void { - final vcs = getVcs(); - testCloneSimple(); - - assertEquals(vcs, Vcs.get(id)); - } - public function testCloneSimple():Void { - final vcs = getVcs(); - final dir = vcs.directory + counter++; - vcs.clone(dir, url); + final vcs = createVcs(); + final dir = id.getName() + counter++; + vcs.clone(dir, {url: url}); assertTrue(FileSystem.exists(dir)); assertTrue(FileSystem.isDirectory(dir)); - assertTrue(FileSystem.exists('$dir/.${vcs.directory}')); - assertTrue(FileSystem.isDirectory('$dir/.${vcs.directory}')); + assertTrue(FileSystem.exists('$dir/.${Vcs.getDirectoryFor(id)}')); + assertTrue(FileSystem.isDirectory('$dir/.${Vcs.getDirectoryFor(id)}')); } public function testCloneBranch():Void { - final vcs = getVcs(); - final dir = vcs.directory + counter++; - vcs.clone(dir, url, "develop"); + final vcs = createVcs(); + final dir = id.getName() + counter++; + vcs.clone(dir, {url: url, branch: "develop"}); assertTrue(FileSystem.exists(dir)); assertTrue(FileSystem.isDirectory(dir)); - assertTrue(FileSystem.exists('$dir/.${vcs.directory}')); - assertTrue(FileSystem.isDirectory('$dir/.${vcs.directory}')); + assertTrue(FileSystem.exists('$dir/.${Vcs.getDirectoryFor(id)}')); + assertTrue(FileSystem.isDirectory('$dir/.${Vcs.getDirectoryFor(id)}')); } public function testCloneBranchTag_0_9_2():Void { - final vcs = getVcs(); - final dir = vcs.directory + counter++; - vcs.clone(dir, url, "develop", "0.9.2"); + final vcs = createVcs(); + final dir = id.getName() + counter++; + vcs.clone(dir, {url: url, tag: "0.9.2"}); Sys.sleep(3); assertTrue(FileSystem.exists(dir)); - assertTrue(FileSystem.exists('$dir/.${vcs.directory}')); + assertTrue(FileSystem.exists('$dir/.${Vcs.getDirectoryFor(id)}')); // if that repo "README.md" was added in tag/rev.: "0.9.3" assertFalse(FileSystem.exists(dir + "/README.md")); } public function testCloneBranchTag_0_9_3():Void { - final vcs = getVcs(); - final dir = vcs.directory + counter++; - vcs.clone(dir, url, "develop", "0.9.3"); + final vcs = createVcs(); + final dir = id.getName() + counter++; + vcs.clone(dir, {url: url, tag: "0.9.3"}); assertTrue(FileSystem.exists(dir)); - assertTrue(FileSystem.exists('$dir/.${vcs.directory}')); + assertTrue(FileSystem.exists('$dir/.${Vcs.getDirectoryFor(id)}')); // if that repo "README.md" was added in tag/rev.: "0.9.3" assertTrue(FileSystem.exists(dir + "/README.md")); } - public function testCloneBranchRev():Void { - final vcs = getVcs(); - final dir = vcs.directory + counter++; - vcs.clone(dir, url, "develop", rev); + public function testCloneBranchCommit():Void { + final vcs = createVcs(); + final dir = id.getName() + counter++; + vcs.clone(dir, {url: url, branch: "develop", commit: rev}); assertTrue(FileSystem.exists(dir)); - assertTrue(FileSystem.exists('$dir/.${vcs.directory}')); + assertTrue(FileSystem.exists('$dir/.${Vcs.getDirectoryFor(id)}')); + + // if that repo "README.md" was added in tag/rev.: "0.9.3" + assertFalse(FileSystem.exists(dir + "/README.md")); + } + + public function testCloneCommit():Void { + final vcs = createVcs(); + final dir = id.getName() + counter++; + vcs.clone(dir, {url: url, commit: rev}); + + assertTrue(FileSystem.exists(dir)); + assertTrue(FileSystem.exists('$dir/.${Vcs.getDirectoryFor(id)}')); // if that repo "README.md" was added in tag/rev.: "0.9.3" assertFalse(FileSystem.exists(dir + "/README.md")); @@ -140,9 +145,9 @@ class TestVcs extends TestBase // --------------- update --------------- // - public function testUpdateBranchTag_0_9_2__toLatest():Void { - final vcs = getVcs(); - final dir = vcs.directory + counter;// increment will do in `testCloneBranchTag_0_9_2` + public function testUpdateBranchTag_0_9_2():Void { + final vcs = createVcs(); + final dir = id.getName() + counter; // increment will do in `testCloneBranchTag_0_9_2` testCloneBranchTag_0_9_2(); assertFalse(FileSystem.exists("README.md")); @@ -150,76 +155,101 @@ class TestVcs extends TestBase // save CWD: final cwd = Sys.getCwd(); Sys.setCwd(cwd + dir); - assertTrue(FileSystem.exists("." + vcs.directory)); + assertTrue(FileSystem.exists("." + Vcs.getDirectoryFor(id))); - // in this case `libName` can get any value: - vcs.update(()-> { - assertTrue(false); - // we are not expecting to be asked for confirmation - return false; - } ); + assertFalse(vcs.checkRemoteChanges()); + try { + vcs.mergeRemoteChanges(); + assertFalse(true); + } catch (e:VcsError) { + assertTrue(e.match(CommandFailed(_))); + } - // Now we get actual version (0.9.3 or newer) with README.md. - assertTrue(FileSystem.exists("README.md")); + // Since originally we installed 0.9.2, we are locked down to that so still no README.md. + assertFalse(FileSystem.exists("README.md")); // restore CWD: Sys.setCwd(cwd); } + public function testUpdateRev():Void { + final vcs = createVcs(); + final dir = id.getName() + counter; // increment will do in `testCloneCommit` - public function testUpdateBranchTag_0_9_2__toLatest__afterUserChanges_withReset():Void { - final vcs = getVcs(); - final dir = vcs.directory + counter;// increment will do in `testCloneBranchTag_0_9_2` - - testCloneBranchTag_0_9_2(); + testCloneCommit(); assertFalse(FileSystem.exists("README.md")); // save CWD: final cwd = Sys.getCwd(); Sys.setCwd(cwd + dir); + assertTrue(FileSystem.exists("." + Vcs.getDirectoryFor(id))); - // creating user-changes: - FileSystem.deleteFile("build.hxml"); - File.saveContent("file", "new file \"file\" with content"); + assertFalse(vcs.checkRemoteChanges()); + try { + vcs.mergeRemoteChanges(); + assertFalse(true); + } catch (e:VcsError) { + assertTrue(e.match(CommandFailed(_))); + } - // update to HEAD: - vcs.update(() -> true); + // Since originally we installed 0.9.2, we are locked down to that so still no README.md. + assertFalse(FileSystem.exists("README.md")); + + // restore CWD: + Sys.setCwd(cwd); + } + + public function testUpdateBranch():Void { + final vcs = createVcs(); + final dir = id.getName() + counter; + // clone old commit from branch + testCloneBranchCommit(); + assertFalse(FileSystem.exists("README.md")); + + // save CWD: + final cwd = Sys.getCwd(); + Sys.setCwd(cwd + dir); + assertTrue(FileSystem.exists("." + Vcs.getDirectoryFor(id))); + + assertTrue(vcs.checkRemoteChanges()); + vcs.mergeRemoteChanges(); - // Now we get actual version (0.9.3 or newer) with README.md. + // Now we have the current version of develop with README.md. assertTrue(FileSystem.exists("README.md")); // restore CWD: Sys.setCwd(cwd); } - public function testUpdateBranchTag_0_9_2__toLatest__afterUserChanges_withoutReset():Void { - final vcs = getVcs(); - final dir = vcs.directory + counter;// increment will do in `testCloneBranchTag_0_9_2` + public function testUpdateBranch__afterUserChanges_withReset():Void { + final vcs = createVcs(); + final dir = id.getName() + counter; - testCloneBranchTag_0_9_2(); + testCloneBranchCommit(); assertFalse(FileSystem.exists("README.md")); // save CWD: final cwd = Sys.getCwd(); Sys.setCwd(cwd + dir); + assertTrue(FileSystem.exists("." + Vcs.getDirectoryFor(id))); // creating user-changes: FileSystem.deleteFile("build.hxml"); File.saveContent("file", "new file \"file\" with content"); // update to HEAD: + assertTrue(vcs.hasLocalChanges()); - try { - vcs.update(() -> false); - assertTrue(false); - } catch (e:VcsUpdateCancelled) { - assertTrue(true); - } + vcs.resetLocalChanges(); + assertTrue(FileSystem.exists("build.hxml")); - // We get no reset and update: - assertTrue(FileSystem.exists("file")); - assertFalse(FileSystem.exists("build.hxml")); - assertFalse(FileSystem.exists("README.md")); + + assertFalse(vcs.hasLocalChanges()); + assertTrue(vcs.checkRemoteChanges()); + vcs.mergeRemoteChanges(); + + // Now we have the current version of develop with README.md. + assertTrue(FileSystem.exists("README.md")); // restore CWD: Sys.setCwd(cwd); @@ -227,7 +257,7 @@ class TestVcs extends TestBase //----------------- tools -------------------// - inline function getVcs():Vcs { - return Vcs.get(id); + inline function createVcs():Vcs { + return Vcs.create(id); } } diff --git a/test/tests/TestVcsNotFound.hx b/test/tests/TestVcsNotFound.hx index a12547aaa..268da7379 100644 --- a/test/tests/TestVcsNotFound.hx +++ b/test/tests/TestVcsNotFound.hx @@ -55,7 +55,7 @@ class TestVcsNotFound extends TestBase public function testCloneHg():Void { final vcs = getHg(); try { - vcs.clone(vcs.directory, "https://bitbucket.org/fzzr/hx.signal"); + vcs.clone("no-hg", {url: "https://bitbucket.org/fzzr/hx.signal"}); assertFalse(true); } catch(error:VcsError) { @@ -69,7 +69,7 @@ class TestVcsNotFound extends TestBase public function testCloneGit():Void { final vcs = getGit(); try { - vcs.clone(vcs.directory, "https://github.com/fzzr-/hx.signal.git"); + vcs.clone("no-git", {url: "https://github.com/fzzr-/hx.signal.git"}); assertFalse(true); } catch(error:VcsError) { @@ -95,16 +95,11 @@ class TestVcsNotFound extends TestBase class WrongHg extends Mercurial { public function new() { - super("no-hg", "no-hg", "Mercurial-not-found"); + super("no-hg"); } // copy of Mercurial.searchExecutablebut have a one change - regexp. - override private function searchExecutable():Void { - super.searchExecutable(); - - if (available) - return; - + override private function searchExecutable():Bool { // if we have already msys git/cmd in our PATH final match = ~/(.*)no-hg-no([\\|\/])cmd$/; for(path in Sys.getEnv("PATH").split(";")) { @@ -113,22 +108,17 @@ class WrongHg extends Mercurial { Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + newPath); } } - checkExecutable(); + return checkExecutable(); } } class WrongGit extends Git { public function new() { - super("no-git", "no-git", "Git-not-found"); + super("no-git"); } // copy of Mercurial.searchExecutablebut have a one change - regexp. - override private function searchExecutable():Void { - super.searchExecutable(); - - if(available) - return; - + override private function searchExecutable():Bool { // if we have already msys git/cmd in our PATH final match = ~/(.*)no-git-no([\\|\/])cmd$/; for(path in Sys.getEnv("PATH").split(";")) { @@ -138,13 +128,14 @@ class WrongGit extends Git { } } if(checkExecutable()) - return; + return true; // look at a few default paths for(path in ["C:\\Program Files (x86)\\Git\\bin", "C:\\Progra~1\\Git\\bin"]) if(FileSystem.exists(path)) { Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + path); if(checkExecutable()) - return; + return true; } + return false; } } diff --git a/test/tests/TestVersionData.hx b/test/tests/TestVersionData.hx index ca6dc0b89..2ef77e101 100644 --- a/test/tests/TestVersionData.hx +++ b/test/tests/TestVersionData.hx @@ -18,7 +18,7 @@ class TestVersionData extends TestBase { assertVersionDataEquals(extractVersion("git:https://some.url"), VcsInstall(VcsID.ofString("git"), { url: "https://some.url", branch: null, - ref: null, + commit: null, tag: null, subDir: null })); @@ -26,7 +26,7 @@ class TestVersionData extends TestBase { assertVersionDataEquals(extractVersion("git:https://some.url#branch"), VcsInstall(VcsID.ofString("git"), { url: "https://some.url", branch: "branch", - ref: null, + commit: null, tag: null, subDir: null })); @@ -34,7 +34,7 @@ class TestVersionData extends TestBase { assertVersionDataEquals(extractVersion("git:https://some.url#abcdef0"), VcsInstall(VcsID.ofString("git"), { url: "https://some.url", branch: null, - ref: "abcdef0", + commit: "abcdef0", tag: null, subDir: null })); @@ -44,7 +44,7 @@ class TestVersionData extends TestBase { assertVersionDataEquals(extractVersion("hg:https://some.url"), VcsInstall(VcsID.ofString("hg"), { url: "https://some.url", branch: null, - ref: null, + commit: null, tag: null, subDir: null })); @@ -52,7 +52,7 @@ class TestVersionData extends TestBase { assertVersionDataEquals(extractVersion("hg:https://some.url#branch"), VcsInstall(VcsID.ofString("hg"), { url: "https://some.url", branch: "branch", - ref: null, + commit: null, tag: null, subDir: null })); @@ -60,7 +60,7 @@ class TestVersionData extends TestBase { assertVersionDataEquals(extractVersion("hg:https://some.url#abcdef0"), VcsInstall(VcsID.ofString("hg"), { url: "https://some.url", branch: null, - ref: "abcdef0", + commit: "abcdef0", tag: null, subDir: null })); diff --git a/test/tests/integration/TestGit.hx b/test/tests/integration/TestGit.hx index 36e6b80c6..ff69f194e 100644 --- a/test/tests/integration/TestGit.hx +++ b/test/tests/integration/TestGit.hx @@ -1,5 +1,7 @@ package tests.integration; +import haxelib.api.FsUtils; +import haxelib.api.Vcs; import tests.util.Vcs; class TestGit extends TestVcs { @@ -10,7 +12,9 @@ class TestGit extends TestVcs { override function setup() { super.setup(); - makeGitRepo(vcsLibPath); + makeGitRepo(vcsLibPath, ["haxelib.xml"]); + createGitTag(vcsLibPath, vcsTag); + makeGitRepo(vcsLibNoHaxelibJson); makeGitRepo(vcsBrokenDependency); } @@ -22,4 +26,23 @@ class TestGit extends TestVcs { super.tearDown(); } + + public function updateVcsRepo() { + addToGitRepo(vcsLibPath, "haxelib.xml"); + } + + public function getVcsCommit():String { + return FsUtils.runInDirectory(vcsLibPath, Vcs.create(Git).getRef); + } + + function testInstallShortcommit() { + + final shortCommitId = getVcsCommit().substr(0, 7); + + updateVcsRepo(); + + final r = haxelib([cmd, "Bar", vcsLibPath, shortCommitId]).result(); + assertSuccess(r); + + } } diff --git a/test/tests/integration/TestHg.hx b/test/tests/integration/TestHg.hx index bf387179d..1298c24e5 100644 --- a/test/tests/integration/TestHg.hx +++ b/test/tests/integration/TestHg.hx @@ -1,5 +1,7 @@ package tests.integration; +import haxelib.api.FsUtils; +import haxelib.api.Vcs; import tests.util.Vcs; class TestHg extends TestVcs { @@ -10,7 +12,9 @@ class TestHg extends TestVcs { override function setup() { super.setup(); - makeHgRepo(vcsLibPath); + makeHgRepo(vcsLibPath, ["haxelib.xml"]); + createHgTag(vcsLibPath, vcsTag); + makeHgRepo(vcsLibNoHaxelibJson); makeHgRepo(vcsBrokenDependency); } @@ -22,4 +26,12 @@ class TestHg extends TestVcs { super.tearDown(); } + + public function updateVcsRepo() { + addToHgRepo(vcsLibPath, "haxelib.xml"); + } + + public function getVcsCommit():String { + return FsUtils.runInDirectory(vcsLibPath, Vcs.create(Hg).getRef); + } } diff --git a/test/tests/integration/TestUpdate.hx b/test/tests/integration/TestUpdate.hx index 3d3fae730..3500f755f 100644 --- a/test/tests/integration/TestUpdate.hx +++ b/test/tests/integration/TestUpdate.hx @@ -160,7 +160,7 @@ class TestUpdate extends IntegrationTests { final r = haxelib(["update", "Bar"]).result(); assertSuccess(r); - assertTrue(r.out.indexOf('Library Bar $type repository is already up to date') >= 0); + assertTrue(r.out.indexOf('Library Bar is already up to date') >= 0); // Don't show update message if vcs lib was already up to date assertTrue(r.out.indexOf("Bar was updated") < 0); diff --git a/test/tests/integration/TestVcs.hx b/test/tests/integration/TestVcs.hx index 40073e3da..7d2798447 100644 --- a/test/tests/integration/TestVcs.hx +++ b/test/tests/integration/TestVcs.hx @@ -6,12 +6,17 @@ abstract class TestVcs extends IntegrationTests { final vcsLibPath = "libraries/libBar"; final vcsLibNoHaxelibJson = "libraries/libNoHaxelibJson"; final vcsBrokenDependency = "libraries/libBrokenDep"; + final vcsTag = "v1.0.0"; function new(cmd:String) { super(); this.cmd = cmd; } + abstract function updateVcsRepo():Void; + + abstract function getVcsCommit():String; + function test() { final r = haxelib([cmd, "Bar", vcsLibPath]).result(); @@ -135,4 +140,59 @@ abstract class TestVcs extends IntegrationTests { } + + function testVcsUpdateBranch() { + + final r = haxelib([cmd, "Bar", vcsLibPath, "main"]).result(); + assertSuccess(r); + + final r = haxelib(["update", "Bar"]).result(); + assertSuccess(r); + assertOutputEquals(["Library Bar is already up to date"], r.out.trim()); + + updateVcsRepo(); + + final r = haxelib(["update", "Bar"]).result(); + assertSuccess(r); + assertOutputEquals([ + "Bar was updated", + ' Current version is now $cmd' + ], r.out.trim()); + + } + + function testVcsUpdateCommit() { + + final r = haxelib([cmd, "Bar", vcsLibPath, getVcsCommit()]).result(); + assertSuccess(r); + + updateVcsRepo(); + + // TODO: Doesn't work with hg + if (cmd == "hg") + return; + + final r = haxelib(["update", "Bar"]).result(); + assertSuccess(r); + assertOutputEquals(["Library Bar is already up to date"], r.out.trim()); + + } + + function testVcsUpdateTag() { + + final r = haxelib([cmd, "Bar", vcsLibPath, "main", "", "v1.0.0"]).result(); + assertSuccess(r); + + updateVcsRepo(); + + // TODO: Doesn't work with hg + if (cmd == "hg") + return; + + final r = haxelib(["update", "Bar"]).result(); + assertSuccess(r); + assertOutputEquals(["Library Bar is already up to date"], r.out.trim()); + + } + } diff --git a/test/tests/util/Vcs.hx b/test/tests/util/Vcs.hx index a7b731178..4abeb8752 100644 --- a/test/tests/util/Vcs.hx +++ b/test/tests/util/Vcs.hx @@ -5,7 +5,7 @@ import sys.io.Process; /** Makes library at `libPath` into a git repo and commits all files. **/ -function makeGitRepo(libPath:String) { +function makeGitRepo(libPath:String, ?exclude:Array) { final oldCwd = Sys.getCwd(); Sys.setCwd(libPath); @@ -17,6 +17,12 @@ function makeGitRepo(libPath:String) { runCommand(cmd, ["config", "user.name", "Your Name"]); runCommand(cmd, ["add", "-A"]); + if (exclude != null) { + for (file in exclude) { + runCommand(cmd, ["rm", "--cached", file]); + } + } + runCommand(cmd, ["commit", "-m", "Create repo"]); // different systems may have different default branch names set runCommand(cmd, ["branch", "--move", "main"]); @@ -24,6 +30,31 @@ function makeGitRepo(libPath:String) { Sys.setCwd(oldCwd); } +function createGitTag(libPath:String, name:String) { + final oldCwd = Sys.getCwd(); + + Sys.setCwd(libPath); + + final cmd = "git"; + + runCommand(cmd, ["tag", "-a", name, "-m", name]); + + Sys.setCwd(oldCwd); +} + +function addToGitRepo(libPath:String, item:String) { + final oldCwd = Sys.getCwd(); + + Sys.setCwd(libPath); + + final cmd = "git"; + + runCommand(cmd, ["add", item]); + runCommand(cmd, ["commit", "-m", 'Add $item']); + + Sys.setCwd(oldCwd); +} + private function runCommand(cmd:String, args:Array) { final process = new sys.io.Process(cmd, args); final code = process.exitCode(); @@ -42,7 +73,7 @@ function resetGitRepo(libPath:String) { HaxelibTests.deleteDirectory(gitDirectory); } -function makeHgRepo(libPath:String) { +function makeHgRepo(libPath:String, ?exclude:Array) { final oldCwd = Sys.getCwd(); Sys.setCwd(libPath); @@ -51,7 +82,39 @@ function makeHgRepo(libPath:String) { runCommand(cmd, ["init"]); runCommand(cmd, ["add"]); + if (exclude != null) { + for (file in exclude) { + runCommand(cmd, ["forget", file]); + } + } + runCommand(cmd, ["commit", "-m", "Create repo"]); + runCommand(cmd, ["branch", "main"]); + + Sys.setCwd(oldCwd); +} + +function createHgTag(libPath:String, name:String) { + final oldCwd = Sys.getCwd(); + + Sys.setCwd(libPath); + + final cmd = "hg"; + + runCommand(cmd, ["tag", name]); + + Sys.setCwd(oldCwd); +} + +function addToHgRepo(libPath:String, item:String) { + final oldCwd = Sys.getCwd(); + + Sys.setCwd(libPath); + + final cmd = "hg"; + + runCommand(cmd, ["add", item]); + runCommand(cmd, ["commit", "-m", 'Add $item']); Sys.setCwd(oldCwd); }