From 8786d0973e7f9a733774ec3b7f2c2a3f51e670a6 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Wed, 24 Aug 2022 01:48:19 +0100 Subject: [PATCH 01/13] Use ScopeException when no current version exists This paves the way for local scopes, which won't depend on the current version set in the repository. --- src/haxelib/api/GlobalScope.hx | 6 +++++- src/haxelib/api/Scope.hx | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/haxelib/api/GlobalScope.hx b/src/haxelib/api/GlobalScope.hx index cb0567b1b..22ad5ab78 100644 --- a/src/haxelib/api/GlobalScope.hx +++ b/src/haxelib/api/GlobalScope.hx @@ -87,7 +87,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); } diff --git a/src/haxelib/api/Scope.hx b/src/haxelib/api/Scope.hx index 9b50076bb..7e57712be 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. From aaed3a2f72ed0b72dccc84c103872c1f5280eb07 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Wed, 26 Oct 2022 21:50:55 +0100 Subject: [PATCH 02/13] Fix field name --- src/haxelib/VersionData.hx | 4 ++-- src/haxelib/api/Installer.hx | 4 ++-- src/haxelib/client/Main.hx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/haxelib/VersionData.hx b/src/haxelib/VersionData.hx index 1570955e7..a5091c2f4 100644 --- a/src/haxelib/VersionData.hx +++ b/src/haxelib/VersionData.hx @@ -28,7 +28,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; @@ -77,7 +77,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/Installer.hx b/src/haxelib/api/Installer.hx index 18d21a969..24874b603 100644 --- a/src/haxelib/api/Installer.hx +++ b/src/haxelib/api/Installer.hx @@ -652,7 +652,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) @@ -755,7 +755,7 @@ class Installer { final libPath = repository.getVersionPath(library, id); - final branch = vcsData.ref != null ? vcsData.ref : vcsData.branch; + final branch = vcsData.commit != null ? vcsData.commit : vcsData.branch; final url:String = vcsData.url; function doVcsClone() { diff --git a/src/haxelib/client/Main.hx b/src/haxelib/client/Main.hx index 1e922f011..c3904d11f 100644 --- a/src/haxelib/client/Main.hx +++ b/src/haxelib/client/Main.hx @@ -742,14 +742,14 @@ class Main { 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() From b782e69dcada7236ba4f80b355482c14982371f0 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Thu, 27 Oct 2022 11:15:31 +0100 Subject: [PATCH 03/13] Refactor vcs and update code - Less global state - Slightly fewer exceptions --- src/haxelib/VersionData.hx | 7 + src/haxelib/api/FsUtils.hx | 17 ++ src/haxelib/api/GlobalScope.hx | 10 ++ src/haxelib/api/Installer.hx | 235 +++++++++++++------------- src/haxelib/api/Scope.hx | 5 + src/haxelib/api/Vcs.hx | 293 ++++++++++++++------------------- src/haxelib/client/Main.hx | 22 +-- 7 files changed, 293 insertions(+), 296 deletions(-) diff --git a/src/haxelib/VersionData.hx b/src/haxelib/VersionData.hx index a5091c2f4..7951fee29 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. **/ 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 22ad5ab78..1e4ff95be 100644 --- a/src/haxelib/api/GlobalScope.hx +++ b/src/haxelib/api/GlobalScope.hx @@ -277,4 +277,14 @@ 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)): + VcsInstall(VcsID.ofString(vcs), {url: ""}); + case semVer: + Haxelib(SemVer.ofString(semVer)); + }; + } } diff --git a/src/haxelib/api/Installer.hx b/src/haxelib/api/Installer.hx index 24874b603..0dead99d3 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,13 +160,22 @@ 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; @@ -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,68 @@ 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); + vcs.checkUpdate(); + }; + } + + 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 + updateVcs(library, version, vcs); + + setVcsVersion(library, version, vcsData); + + // TODO: Properly handle sub directories + handleDependenciesVcs(library, version, null); + case Haxelib(version): + 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, version); + userInterface.log(' Current version is now $version'); + userInterface.log("Done"); + handleDependencies(library, version); + } + /** Updates all libraries in the scope. @@ -390,14 +449,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) { + } catch (e:UpdateCancelled) { continue; - } catch (e:VcsUpdateCancelled) { - continue; - } catch(e) { + } catch (e) { ++failures; userInterface.log("Failed to update: " + e.toString()); userInterface.log(e.stack.toString(), Debug); @@ -414,47 +477,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 +589,23 @@ 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 setVcsVersion(library:ProjectName, version:VcsID, data:VcsData) { + scope.setVcsVersion(library, version, data); + if (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(); @@ -748,10 +774,15 @@ 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; + } + + function installVcs(library:ProjectName, id:VcsID, vcsData:VcsData) { + final vcs = getVcs(id); final libPath = repository.getVersionPath(library, id); @@ -762,14 +793,14 @@ class Installer { userInterface.log('Installing $library from $url' + (branch != null ? " branch: " + branch : "")); final tag = vcsData.tag; 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 : "."); + throw 'Could not clone ${id.getName()} repository' + (stderr != null ? ":\n" + stderr : "."); case CantCheckoutBranch(_, branch, stderr): throw 'Could not checkout branch, tag or path "$branch": ' + stderr; case CantCheckoutVersion(_, version, stderr): @@ -781,7 +812,7 @@ 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 @@ -797,16 +828,14 @@ class Installer { } FsUtils.deleteRec(libPath); doVcsClone(); - } else if (wasUpdated) { - userInterface.log('Library $library version ${vcs.directory} already up to date.'); - return; - } else { - userInterface.log('Updating $library version ${vcs.directory}...'); - try { - updateVcs(library, id, vcs); - } catch (e:AlreadyUpToDate){ - userInterface.log(e.toString()); + } else if (wasUpdated || !FsUtils.runInDirectory(libPath, vcs.checkUpdate)) { + userInterface.log('Library $library version $id already up to date'); + if (wasUpdated) { + return; } + } else { + userInterface.log('Updating $library version $id...'); + updateVcs(library, libPath, vcs); } } else { FsUtils.safeDir(libPath); @@ -816,37 +845,17 @@ class Installer { vcsBranchesByLibraryName[library] = branch; } - 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; + function updateVcs(library:ProjectName, id:VcsID, vcs:Vcs) + FsUtils.runInDirectory(repository.getVersionPath(library, id), function() { + if (vcs.hasLocalChanges()) { + if (!userInterface.confirm('Reset changes to $library $id repository in order to update to latest version')) { 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 + throw new UpdateCancelled('${id.getName()} update in ${Sys.getCwd()} was cancelled'); + } + vcs.resetLocalChanges(); } - } 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'); - } + + vcs.update(); + userInterface.log('$library was updated'); + }); } diff --git a/src/haxelib/api/Scope.hx b/src/haxelib/api/Scope.hx index 7e57712be..6afc71709 100644 --- a/src/haxelib/api/Scope.hx +++ b/src/haxelib/api/Scope.hx @@ -129,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..d9dd47977 100644 --- a/src/haxelib/api/Vcs.hx +++ b/src/haxelib/api/Vcs.hx @@ -24,46 +24,42 @@ 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. - - `version` can also be specified for tags in git or revisions in mercurial. - - `debugLog` will be used to log executable output. + If `flat` is set to true, recursive cloning is disabled. **/ - function clone(libPath:String, vcsPath:String, ?branch:String, ?version:String, ?debugLog:(msg:String)->Void):Void; + function clone(libPath:String, data:VcsData, flat:Bool = false):Void; /** - Updates repository in CWD or CWD/`Vcs.directory` to HEAD. - For git CWD must be in the format "...haxelib-repo/lib/git". - - By default, uncommitted changes prevent updating. - If `confirm` is passed in, the changes may occur - if `confirm` returns true. + Updates repository. + **/ + function update():Void; - `debugLog` will be used to log executable output. + /** + Fetches possible remote changes, and returns whether there are any available. + **/ + function checkUpdate():Bool; - `summaryLog` may be used to log summaries of changes. + /** + Returns whether any uncommited local changes exist. + **/ + function hasLocalChanges():Bool; - Returns `true` if update successful. + /** + Resets all local changes present in the working tree. **/ - function update(?confirm:()->Bool, ?debugLog:(msg:String)->Void, ?summaryLog:(msg:String)->Void):Bool; + function resetLocalChanges():Void; } /** Enum representing errors that can be thrown during a vcs operation. **/ @@ -75,91 +71,67 @@ enum VcsError { 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 +201,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,41 +226,34 @@ 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); - } - - run(["fetch"], debugLog, true); + public function checkUpdate():Bool { + run(["fetch"], true); // `git rev-parse @{u}` will fail if detached - final checkUpstream = run(["rev-parse", "@{u}"], debugLog); + final checkUpstream = run(["rev-parse", "@{u}"]); - if (checkUpstream.out == run(["rev-parse", "HEAD"], debugLog, true).out) - return false; // already up to date + return checkUpstream.out != run(["rev-parse", "HEAD"], true).out; + } + public function update() { // But if before we pulled specified branch/tag/rev => then possibly currently we haxe "HEAD detached at ..". - if (checkUpstream.code != 0) { + if (run(["rev-parse", "@{u}"]).code != 0) { // get parent-branch: final branch = { - final raw = run(["show-branch"], debugLog).out; + final raw = run(["show-branch"]).out; final regx = ~/\[([^]]*)\]/; if (regx.match(raw)) regx.matched(1); @@ -311,33 +261,34 @@ class Git extends Vcs { raw; } - run(["checkout", branch, "--force"], debugLog, true); + run(["checkout", branch, "--force"], true); } - run(["merge"], debugLog, true); - return true; + run(["merge"], true); } - public function clone(libPath:String, url:String, ?branch:String, ?version:String, ?debugLog:(msg:String)->Void):Void { + public function clone(libPath:String, data:VcsData, flat = false):Void { final oldCwd = Sys.getCwd(); - final vcsArgs = ["clone", url, libPath]; + final vcsArgs = ["clone", data.url, libPath]; - if (!Vcs.flat) + if (!flat) vcsArgs.push('--recursive'); - 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*/); Sys.setCwd(libPath); - if (version != null && version != "") { - final ret = run(["checkout", "tags/" + version], debugLog); + final branch = data.commit ?? data.branch; + + if (data.tag != null) { + final ret = run(["checkout", "tags/" + data.tag]); if (ret.code != 0) { Sys.setCwd(oldCwd); - throw VcsError.CantCheckoutVersion(this, version, ret.out); + throw VcsError.CantCheckoutVersion(this, data.tag, ret.out); } } else if (branch != null) { - final ret = run(["checkout", branch], debugLog); + final ret = run(["checkout", branch]); if (ret.code != 0){ Sys.setCwd(oldCwd); throw VcsError.CantCheckoutBranch(this, branch, ret.out); @@ -347,22 +298,26 @@ class Git extends Vcs { // return prev. cwd: Sys.setCwd(oldCwd); } + + public function hasLocalChanges():Bool { + return run(["diff", "--exit-code", "--no-ext-diff"]).code != 0 + || run(["diff", "--cached", "--exit-code", "--no-ext-diff"]).code != 0; + } + + 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 +326,51 @@ 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 checkUpdate():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 update() { + 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*/); + } + + 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 c3904d11f..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,14 +732,9 @@ 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); From 5af73091fc749660e314f8360a1461ec1ec6d10f Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Sun, 9 Jul 2023 22:42:32 +0100 Subject: [PATCH 04/13] Clone git libraries with --depth=1 where possible --- src/haxelib/VersionData.hx | 29 +++++ src/haxelib/api/GlobalScope.hx | 7 +- src/haxelib/api/Installer.hx | 128 ++++++++++++++-------- src/haxelib/api/Repository.hx | 39 +++++++ src/haxelib/api/Vcs.hx | 187 +++++++++++++++++++++++++-------- 5 files changed, 298 insertions(+), 92 deletions(-) diff --git a/src/haxelib/VersionData.hx b/src/haxelib/VersionData.hx index 7951fee29..0a49074b8 100644 --- a/src/haxelib/VersionData.hx +++ b/src/haxelib/VersionData.hx @@ -49,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() { + final 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 **/ diff --git a/src/haxelib/api/GlobalScope.hx b/src/haxelib/api/GlobalScope.hx index 1e4ff95be..ac9c5c804 100644 --- a/src/haxelib/api/GlobalScope.hx +++ b/src/haxelib/api/GlobalScope.hx @@ -59,7 +59,9 @@ 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) { final devDir = repository.getValidVersionPath(library, vcsVersion) + data.subDir; @@ -282,7 +284,8 @@ class GlobalScope extends Scope { return switch version { case vcs if (VcsID.isValid(vcs)): - VcsInstall(VcsID.ofString(vcs), {url: ""}); + 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 0dead99d3..428cf4ed6 100644 --- a/src/haxelib/api/Installer.hx +++ b/src/haxelib/api/Installer.hx @@ -181,7 +181,7 @@ class Installer { final repository:Repository; final userInterface:UserInterface; - final vcsBranchesByLibraryName = new Map(); + final vcsDataByName = new Map(); /** Creates a new Installer object that installs projects to `scope`. @@ -219,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`. **/ @@ -406,7 +406,7 @@ class Installer { version == Connection.getLatestVersion(library); case VcsInstall(version, _): final vcs = getVcs(version); - vcs.checkUpdate(); + !FsUtils.runInDirectory(repository.getVersionPath(library, version), vcs.checkRemoteChanges); }; } @@ -415,8 +415,26 @@ class Installer { case VcsInstall(version, vcsData): final vcs = getVcs(version); // with version locking we'll be able to be smarter with this - updateVcs(library, version, vcs); + 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 @@ -596,7 +614,32 @@ class Installer { } } + 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 == null) { userInterface.log(' Current version is now $version'); @@ -786,12 +829,13 @@ class Installer { final libPath = repository.getVersionPath(library, id); - final branch = vcsData.commit != null ? vcsData.commit : vcsData.branch; - final url:String = vcsData.url; - 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, vcsData, noVcsSubmodules); } catch (error:VcsError) { @@ -799,12 +843,10 @@ class Installer { 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): + case CantCloneRepo(_, _, stderr): throw 'Could not clone ${id.getName()} 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 CantCheckout(_, ref, stderr): + throw 'Could not checkout commit or tag "$ref": ' + stderr; case CommandFailed(_, code, stdout, stderr): throw new VcsCommandFailed(id, code, stdout, stderr); }; @@ -814,48 +856,44 @@ class Installer { if (repository.isVersionInstalled(library, id)) { 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 || !FsUtils.runInDirectory(libPath, vcs.checkUpdate)) { + } 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'); - if (wasUpdated) { - return; - } } else { + // 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) - FsUtils.runInDirectory(repository.getVersionPath(library, id), function() { - if (vcs.hasLocalChanges()) { - if (!userInterface.confirm('Reset changes to $library $id repository in order to update to latest version')) { - userInterface.log('$library repository has not been modified', Optional); - throw new UpdateCancelled('${id.getName()} update in ${Sys.getCwd()} was cancelled'); - } - vcs.resetLocalChanges(); - } - - vcs.update(); + 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/Vcs.hx b/src/haxelib/api/Vcs.hx index d9dd47977..41b91bd70 100644 --- a/src/haxelib/api/Vcs.hx +++ b/src/haxelib/api/Vcs.hx @@ -42,14 +42,19 @@ private interface IVcs { function clone(libPath:String, data:VcsData, flat:Bool = false):Void; /** - Updates repository. + Checks out the repository based on the data provided **/ - function update():Void; + function updateWithData(data:{?branch:String, ?tag:String, ?commit:String}):Void; /** - Fetches possible remote changes, and returns whether there are any available. + Merges remote changes into repository. **/ - function checkUpdate():Bool; + function mergeRemoteChanges():Void; + + /** + Checks for possible remote changes, and returns whether there are any available. + **/ + function checkRemoteChanges():Bool; /** Returns whether any uncommited local changes exist. @@ -60,14 +65,19 @@ private interface IVcs { Resets all local changes present in the working tree. **/ function resetLocalChanges():Void; + + function getRef():String; + + 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); } @@ -239,64 +249,123 @@ class Git extends Vcs { return false; } - public function checkUpdate():Bool { - run(["fetch"], 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; } - public function update() { - // But if before we pulled specified branch/tag/rev => then possibly currently we haxe "HEAD detached at ..". - if (run(["rev-parse", "@{u}"]).code != 0) { - // get parent-branch: - final branch = { - final raw = run(["show-branch"]).out; - final regx = ~/\[([^]]*)\]/; - if (regx.match(raw)) - regx.matched(1); - else - raw; - } - - run(["checkout", branch, "--force"], true); - } - run(["merge"], true); + public function mergeRemoteChanges() { + run(["reset", "--hard", "@{u}"], true); } public function clone(libPath:String, data:VcsData, flat = false):Void { - final oldCwd = Sys.getCwd(); - final vcsArgs = ["clone", data.url, libPath]; if (!flat) vcsArgs.push('--recursive'); + if (data.branch != null) { + vcsArgs.push('--single-branch'); + vcsArgs.push('--branch'); + vcsArgs.push(data.branch); + } else if (data.commit == null) { + vcsArgs.push('--single-branch'); + } + + 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*/); - Sys.setCwd(libPath); - - final branch = data.commit ?? data.branch; + 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 (data.tag != null) { - final ret = run(["checkout", "tags/" + data.tag]); - if (ret.code != 0) { - Sys.setCwd(oldCwd); - throw VcsError.CantCheckoutVersion(this, data.tag, ret.out); - } - } else if (branch != null) { - final ret = run(["checkout", branch]); - if (ret.code != 0){ - Sys.setCwd(oldCwd); - throw VcsError.CantCheckoutBranch(this, branch, ret.out); + 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); } + } - // return prev. cwd: - Sys.setCwd(oldCwd); + inline function runCheckoutRelatedCommand(ref, args:Array) { + final ret = run(args); + if (ret.code != 0) { + throw VcsError.CantCheckout(this, ref, ret.out); + } + } + + function checkout(ref:String, fetch:Bool) { + if (fetch) { + runCheckoutRelatedCommand(ref, ["fetch", "--depth=1", getRemoteName(), ref]); + } + + runCheckoutRelatedCommand(ref, ["checkout", ref]); + + // clean up excess branch + run(["branch", "-D", "@{-1}"]); + } + + function getRemoteName() { + return run(["remote"], true).out.split("\n")[0].trim(); + } + + public function getRef():String { + return run(["rev-parse", "--verify", "HEAD"], true).out.trim(); + } + + 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 { @@ -329,7 +398,7 @@ class Mercurial extends Vcs { return checkExecutable(); } - public function checkUpdate():Bool { + public function checkRemoteChanges():Bool { run(["pull"]); // get new pulled changesets: @@ -342,7 +411,7 @@ class Mercurial extends Vcs { return ~/(\d)/.match(summary); } - public function update() { + public function mergeRemoteChanges() { run(["update"], true); } @@ -363,6 +432,34 @@ class Mercurial extends Vcs { 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"]); From 646c96cfa5e32b5f01ecd64240390cf347dcecdc Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Sun, 9 Jul 2023 22:43:14 +0100 Subject: [PATCH 05/13] Optimize git submodule checkout Also fix submodules being checked out from wrong branch Closes #593 --- src/haxelib/api/Vcs.hx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/haxelib/api/Vcs.hx b/src/haxelib/api/Vcs.hx index 41b91bd70..dc0337009 100644 --- a/src/haxelib/api/Vcs.hx +++ b/src/haxelib/api/Vcs.hx @@ -267,9 +267,6 @@ class Git extends Vcs { public function clone(libPath:String, data:VcsData, flat = false):Void { final vcsArgs = ["clone", data.url, libPath]; - if (!flat) - vcsArgs.push('--recursive'); - if (data.branch != null) { vcsArgs.push('--single-branch'); vcsArgs.push('--branch'); @@ -304,6 +301,12 @@ class Git extends Vcs { 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}) { From 5244498cdc59474f5f80bce57622eb233cc1c37f Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Thu, 27 Oct 2022 11:15:54 +0100 Subject: [PATCH 06/13] [tests] Update vcs unit tests --- test/tests/TestGit.hx | 2 +- test/tests/TestHg.hx | 6 +- test/tests/TestVcs.hx | 182 ++++++++++++++++++++-------------- test/tests/TestVcsNotFound.hx | 29 ++---- test/tests/TestVersionData.hx | 12 +-- 5 files changed, 126 insertions(+), 105 deletions(-) diff --git a/test/tests/TestGit.hx b/test/tests/TestGit.hx index 47a3de293..9bf67abb2 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), "0.9.2"); } } 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 })); From 1c7e728fc0133a39198022d377dd3c44abe3a268 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Sun, 9 Jul 2023 22:50:10 +0100 Subject: [PATCH 07/13] [tests] Use actual commit for git vcs unit tests --- test/tests/TestGit.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tests/TestGit.hx b/test/tests/TestGit.hx index 9bf67abb2..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"); } } From a7dc1dbe21ee5d3142710036e8f4ab1afed99195 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Sun, 9 Jul 2023 22:53:23 +0100 Subject: [PATCH 08/13] [tests] Update git integration tests --- test/tests/integration/TestGit.hx | 25 +++++++++++- test/tests/integration/TestHg.hx | 14 ++++++- test/tests/integration/TestVcs.hx | 60 +++++++++++++++++++++++++++ test/tests/util/Vcs.hx | 67 ++++++++++++++++++++++++++++++- 4 files changed, 162 insertions(+), 4 deletions(-) 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/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); } From 603b1758afda2ad04946f3c279e7123aa11ed923 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Mon, 10 Jul 2023 17:58:25 +0100 Subject: [PATCH 09/13] [tests] Fix update command tests --- test/tests/integration/TestUpdate.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From e4965af88db581404c91be0bd42c34fbfc8c59a7 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Mon, 10 Jul 2023 13:32:48 +0100 Subject: [PATCH 10/13] Fix haxe 3 compatibility issue --- src/haxelib/VersionData.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/haxelib/VersionData.hx b/src/haxelib/VersionData.hx index 0a49074b8..fd91ddb19 100644 --- a/src/haxelib/VersionData.hx +++ b/src/haxelib/VersionData.hx @@ -59,7 +59,7 @@ class VcsData { without the empty ones. **/ public function getCleaned() { - final data:{ + var data:{ url:String, ?commit:String, ?tag:String, From 275a8f1551cf1a6495b577bd768bc0f259eebc51 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Mon, 10 Jul 2023 13:56:42 +0100 Subject: [PATCH 11/13] Fix subdir checks for git/hg installs --- src/haxelib/api/GlobalScope.hx | 2 +- src/haxelib/api/Installer.hx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/haxelib/api/GlobalScope.hx b/src/haxelib/api/GlobalScope.hx index ac9c5c804..0f8518c15 100644 --- a/src/haxelib/api/GlobalScope.hx +++ b/src/haxelib/api/GlobalScope.hx @@ -63,7 +63,7 @@ class GlobalScope extends Scope { 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 { diff --git a/src/haxelib/api/Installer.hx b/src/haxelib/api/Installer.hx index 428cf4ed6..2170709ea 100644 --- a/src/haxelib/api/Installer.hx +++ b/src/haxelib/api/Installer.hx @@ -641,7 +641,7 @@ class Installer { } scope.setVcsVersion(library, version, data); - if (data.subDir == null) { + if (data.subDir == "" || data.subDir == null) { userInterface.log(' Current version is now $version'); } else { final path = scope.getPath(library); From 9f5c2501223bec9ee6bea8705d1088fe2b47add2 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Mon, 10 Jul 2023 17:58:57 +0100 Subject: [PATCH 12/13] Fix update not setting version --- src/haxelib/api/Installer.hx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/haxelib/api/Installer.hx b/src/haxelib/api/Installer.hx index 2170709ea..f54fa976f 100644 --- a/src/haxelib/api/Installer.hx +++ b/src/haxelib/api/Installer.hx @@ -439,7 +439,7 @@ class Installer { // TODO: Properly handle sub directories handleDependenciesVcs(library, version, null); - case Haxelib(version): + case Haxelib(_): final latest = Connection.getLatestVersion(library); if (repository.isVersionInstalled(library, latest)) { userInterface.log('Latest version $latest of $library is already installed'); @@ -449,10 +449,10 @@ class Installer { } else { downloadAndInstall(library, latest); } - scope.setVersion(library, version); - userInterface.log(' Current version is now $version'); + scope.setVersion(library, latest); + userInterface.log(' Current version is now $latest'); userInterface.log("Done"); - handleDependencies(library, version); + handleDependencies(library, latest); } /** From 0a593660ca764d06969aba5b98a1121081694a48 Mon Sep 17 00:00:00 2001 From: tobil4sk Date: Mon, 10 Jul 2023 18:03:46 +0100 Subject: [PATCH 13/13] Update run.n --- run.n | Bin 323251 -> 327747 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/run.n b/run.n index c4e600a055327b3f9dafed6d566c6b8b46faa3c9..2be7ace4dcf7e329d0a12ddb982d9c0e95d71128 100644 GIT binary patch delta 56134 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—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