Files
lime/tools/haxelib/client/Vcs.hx
2017-05-18 11:20:53 -07:00

379 lines
10 KiB
Haxe

/*
* Copyright (C)2005-2016 Haxe Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package haxelib.client;
import sys.FileSystem;
using haxelib.client.Vcs;
interface IVcs {
var name(default, null):String;
var directory(default, null):String;
var executable(default, null):String;
var available(get, null):Bool;
var settings(default, null):Settings;
/**
Clone repo into `libPath`.
**/
function clone(libPath:String, vcsPath:String, ?branch:String, ?version:String):Void;
/**
Update to HEAD repo contains in CWD or CWD/`Vcs.directory`.
CWD must be like "...haxelib-repo/lib/git" for Git.
Returns `true` if update successful.
**/
function update(libName:String):Bool;
}
@:enum abstract VcsID(String) to String {
var Hg = "hg";
var Git = "git";
}
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);
}
typedef Settings = {
@:optional var flat:Bool;
@:optional var debug:Bool;
@:optional var quiet:Bool;
}
class Vcs implements IVcs {
static var reg:Map<VcsID, Vcs>;
public var name(default, null):String;
public var directory(default, null):String;
public var executable(default, null):String;
public var settings(default, null):Settings;
public var available(get, null):Bool;
var availabilityChecked = false;
var executableSearched = false;
public static function initialize(settings:Settings) {
if (reg == null) {
reg = [
VcsID.Git => new Git(settings),
VcsID.Hg => new Mercurial(settings)
];
} else {
if (reg.get(VcsID.Git) == null)
reg.set(VcsID.Git, new Git(settings));
if (reg.get(VcsID.Hg) == null)
reg.set(VcsID.Hg, new Mercurial(settings));
}
}
function new(executable:String, directory:String, name:String, settings:Settings) {
this.name = name;
this.directory = directory;
this.executable = executable;
this.settings = {
flat: settings.flat != null ? settings.flat : false,
debug: settings.debug != null ? settings.debug : false,
quiet: settings.quiet != null ? settings.quiet : false
}
if (settings.debug) {
this.settings.quiet = false;
}
}
public static function get(id:VcsID, settings:Settings):Null<Vcs> {
initialize(settings);
return reg.get(id);
}
static function set(id:VcsID, vcs:Vcs, settings:Settings, ?rewrite:Bool):Void {
initialize(settings);
var existing = reg.get(id) != null;
if (!existing || rewrite)
reg.set(id, vcs);
}
public static function getVcsForDevLib(libPath:String, settings:Settings):Null<Vcs> {
initialize(settings);
for (k in reg.keys()) {
if (FileSystem.exists(libPath + "/" + k) && FileSystem.isDirectory(libPath + "/" + k))
return reg.get(k);
}
return null;
}
function sure(commandResult:{code:Int, out:String}):Void {
switch (commandResult) {
case {code: 0}: //pass
case {code: code, out:out}:
if (!settings.debug)
Sys.stderr().writeString(out);
Sys.exit(code);
}
}
function command(cmd:String, args:Array<String>):{
code: Int,
out: String
} {
var p = try {
new sys.io.Process(cmd, args);
} catch(e:Dynamic) {
return {
code: -1,
out: Std.string(e)
}
}
var out = p.stdout.readAll().toString();
var err = p.stderr.readAll().toString();
if (settings.debug && out != "")
Sys.println(out);
if (settings.debug && err != "")
Sys.stderr().writeString(err);
var code = p.exitCode();
var ret = {
code: code,
out: code == 0 ? out : err
};
p.close();
return ret;
}
function searchExecutable():Void {
executableSearched = true;
}
function checkExecutable():Bool {
available = (executable != null) && try command(executable, []).code == 0 catch(_:Dynamic) false;
availabilityChecked = true;
if (!available && !executableSearched)
searchExecutable();
return available;
}
@:final function get_available():Bool {
if (!availabilityChecked)
checkExecutable();
return available;
}
public function clone(libPath:String, vcsPath:String, ?branch:String, ?version:String):Void {
throw "This method must be overriden.";
}
public function update(libName:String):Bool {
throw "This method must be overriden.";
}
}
class Git extends Vcs {
public function new(settings:Settings)
super("git", "git", "Git", settings);
override function checkExecutable():Bool {
// with `help` cmd because without any cmd `git` can return exit-code = 1.
available = (executable != null) && try command(executable, ["help"]).code == 0 catch(_:Dynamic) false;
availabilityChecked = true;
if (!available && !executableSearched)
searchExecutable();
return available;
}
override function searchExecutable():Void {
super.searchExecutable();
if (available)
return;
// if we have already msys git/cmd in our PATH
var match = ~/(.*)git([\\|\/])cmd$/;
for (path in Sys.getEnv("PATH").split(";")) {
if (match.match(path.toLowerCase())) {
var newPath = match.matched(1) + executable + match.matched(2) + "bin";
Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + newPath);
}
}
if (checkExecutable())
return;
// 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;
}
}
}
override public function update(libName:String):Bool {
if (
command(executable, ["diff", "--exit-code"]).code != 0
||
command(executable, ["diff", "--cached", "--exit-code"]).code != 0
) {
if (Cli.ask("Reset changes to " + libName + " " + name + " repo so we can pull latest version?")) {
sure(command(executable, ["reset", "--hard"]));
} else {
if (!settings.quiet)
Sys.println(name + " repo left untouched");
return false;
}
}
var code = command(executable, ["pull"]).code;
// But if before we pulled specified branch/tag/rev => then possibly currently we haxe "HEAD detached at ..".
if (code != 0) {
// get parent-branch:
var branch = command(executable, ["show-branch"]).out;
var regx = ~/\[([^]]*)\]/;
if (regx.match(branch))
branch = regx.matched(1);
sure(command(executable, ["checkout", branch, "--force"]));
sure(command(executable, ["pull"]));
}
return true;
}
override public function clone(libPath:String, url:String, ?branch:String, ?version:String):Void {
var oldCwd = Sys.getCwd();
var vcsArgs = ["clone", url, libPath];
if (settings == null || !settings.flat)
vcsArgs.push('--recursive');
//TODO: move to Vcs.run(vcsArgs)
//TODO: use settings.quiet
if (command(executable, vcsArgs).code != 0)
throw VcsError.CantCloneRepo(this, url/*, ret.out*/);
Sys.setCwd(libPath);
if (version != null && version != "") {
var ret = command(executable, ["checkout", "tags/" + version]);
if (ret.code != 0)
throw VcsError.CantCheckoutVersion(this, version, ret.out);
} else if (branch != null) {
var ret = command(executable, ["checkout", branch]);
if (ret.code != 0)
throw VcsError.CantCheckoutBranch(this, branch, ret.out);
}
// return prev. cwd:
Sys.setCwd(oldCwd);
}
}
class Mercurial extends Vcs {
public function new(settings:Settings)
super("hg", "hg", "Mercurial", settings);
override function searchExecutable():Void {
super.searchExecutable();
if (available)
return;
// if we have already msys git/cmd in our PATH
var match = ~/(.*)hg([\\|\/])cmd$/;
for(path in Sys.getEnv("PATH").split(";")) {
if(match.match(path.toLowerCase())) {
var newPath = match.matched(1) + executable + match.matched(2) + "bin";
Sys.putEnv("PATH", Sys.getEnv("PATH") + ";" + newPath);
}
}
checkExecutable();
}
override public function update(libName:String):Bool {
command(executable, ["pull"]);
var summary = command(executable, ["summary"]).out;
var diff = command(executable, ["diff", "-U", "2", "--git", "--subrepos"]);
var status = command(executable, ["status"]);
// get new pulled changesets:
// (and search num of sets)
summary = summary.substr(0, summary.length - 1);
summary = summary.substr(summary.lastIndexOf("\n") + 1);
// we don't know any about locale then taking only Digit-exising:s
var changed = ~/(\d)/.match(summary);
if (changed && !settings.quiet)
// print new pulled changesets:
Sys.println(summary);
if (diff.code + status.code + diff.out.length + status.out.length != 0) {
if (!settings.quiet)
Sys.println(diff.out);
if (Cli.ask("Reset changes to " + libName + " " + name + " repo so we can update to latest version?")) {
sure(command(executable, ["update", "--clean"]));
} else {
changed = false;
if (!settings.quiet)
Sys.println(name + " repo left untouched");
}
} else if (changed) {
sure(command(executable, ["update"]));
}
return changed;
}
override public function clone(libPath:String, url:String, ?branch:String, ?version:String):Void {
var vcsArgs = ["clone", url, libPath];
if (branch != null) {
vcsArgs.push("--branch");
vcsArgs.push(branch);
}
if (version != null) {
vcsArgs.push("--rev");
vcsArgs.push(version);
}
if (command(executable, vcsArgs).code != 0)
throw VcsError.CantCloneRepo(this, url/*, ret.out*/);
}
}