+
+const GLib = imports.gi.GLib;
+
+const XObject = imports.XObject.XObject;
+const Event = imports.Scm.Git.Event.Event;
+const Spawn = imports.Spawn.Spawn;
+const File = imports.File.File;
+
+/**
+ * @class Scm.Git.Repo
+ *
+ * @extends Scm.Repo
+ *
+ *
+ *
+ */
+Repo = XObject.define(
+ /**
+ * constructor:
+ *
+ * @param {Object} cfg - Configuration
+ * (basically repopath is currently only critical one.)
+ *
+ */
+
+ function(cfg) {
+ // cal parent?
+ XObject.extend(this,cfg);
+ this.gitdir = cfg.repopath + "/.git";
+
+ if (!File.isDirectory(this.gitdir)) {
+ this.gitdir = this.repopath;
+ }
+ //Repo.superclass.constructor.call(this,cfg);
+
+ },
+ imports.Scm.Repo.Repo, // or Object
+ {
+ branches : false,
+ currentBranch : false,
+ tags : false,
+ gitdir : false,
+ debug : true,
+ lastCmd : false,
+ hasLocalChanges : false,
+ localChanges : false,
+
+ getMetaData : function() {
+ return {
+ name : 'Git',
+ tool : ['git' ]
+
+ };
+ },
+
+
+ getStatus : function()
+ {
+ //git status --porcelain
+ // find out if we are up-to-date.
+ //git ls-remote origin -h refs/heads/master
+ var bl = this.git([ 'status', {
+ 'porcelain' : true
+ }]);
+ print(JSON.stringify(bl.length));
+ this.hasLocalChanges = bl.length > 0 ? true : false;
+ this.localChanges = bl;
+
+
+ },
+
+
+ getBranches : function()
+ {
+ if (this.branches !== false) {
+ return this.branches;
+ }
+ this.branches = [];
+
+ var bl = this.git([ 'branch', {
+ 'no-color' : true,
+ 'verbose' : true,
+ 'no-abbrev' : true,
+ 'a' : true
+ }]).split("\n");
+ var bmap = {};
+ var _this=this;
+
+ var local = [];
+ var remotes = [];
+
+ bl.forEach(function(line) {
+ // * master 61e7e7d oneliner
+ var active = line[0]=='*';
+ if (!line.length) {
+ return;
+ }
+ line = line.substring(2);
+
+ //print(JSON.stringify(line));
+ var parts = line.split(/\s+/);
+ if (parts[1] == '->') {
+ return; // it's an alias like remotes/origin/HEAD -> origin/master
+ }
+ var br = {
+ lastrev : parts[1],
+ name : '',
+ remote : '',
+ remoterev : ''
+ };
+ if (parts[0].match(/^remotes\//)) {
+ br.remote = parts[0];
+ bmap[br.remote] = br;
+ remotes.push(br);
+ } else {
+ br.name = parts[0];
+ bmap[br.name] = br;
+ local.push(br);
+ }
+
+ if (active) {
+ _this.currentBranch = br;
+ }
+ });
+
+ //print(JSON.stringify(local));
+ //print(JSON.stringify(remotes));
+ //print(JSON.stringify(bmap,null,4));
+
+
+ // overlay tracking informaion
+ bl = this.git([
+ 'for-each-ref',
+ { format : '%(refname:short):remotes/%(upstream:short)' },
+ 'refs/heads'
+ ]).split("\n");
+ //print(this.lastCmd);
+
+ //print(JSON.stringify(bl,null,4));
+
+ bl.forEach(function(line) {
+ if (!line.length) {
+ return;
+ }
+ var ar = line.split(':remotes/');
+
+ var lname= ar[0];
+ var rname = 'remotes/' + ar[1];
+ //print(rname);
+ // we should always have a local version of it.
+ bmap[lname].remote = rname;
+ if (typeof(bmap[rname]) == 'undefined') {
+ return;
+ }
+ bmap[lname].remoterev = bmap[rname].lastrev;
+ // flag it for not adding..
+
+ bmap[rname].name = lname;
+ });
+ var _this =this;
+
+
+
+ // add any remotes that do not have name..
+ remotes.forEach(function(r) {
+ if (r.name.length) {
+ return;
+ }
+
+ // create a tracking branch..
+ var name = r.remote.replace(/^remotes\//, '' ).replace(/^origin\//,'').replace('/', '.');
+
+
+
+ if (typeof(bmap[name]) != 'undefined') {
+ // already got aremote of that name.
+ // skip it...
+ r.remoterev = r.lastrev;
+ r.lastrev = '';
+ local.push(r);
+ return;
+ }
+
+
+ _this.git([
+ 'branch',
+ {
+ f : true,
+ track : true
+ },
+ name,
+ r.remote
+ ]);
+
+ r.remoterev = r.lastrev;
+ r.name = name;
+ local.push(r);
+ });
+
+ this.branches = local;
+ // print(JSON.stringify(local,null,4));
+
+
+ return this.branches;
+ },
+ _remotes : false,
+
+ remotes: function(cfg)
+ {
+
+ if (cfg) {
+ this._remotes = false; // reset so we can query it..
+
+ var url = Repo.parseURL(cfg.url);
+ if ((url.scheme == 'http://' || url.scheme == 'https://' )
+ && url.user.length) {
+ // remove username - as it confuses netrc..
+ cfg.url = url.scheme + url.host + '/' +url.path;
+
+ }
+
+
+ this.git([
+ 'remote',
+ 'add',
+ cfg.name,
+ cfg.url
+ ]);
+ this.branches = false;
+
+ this.git([
+ 'fetch',
+ cfg.name,
+ { 'a' : true }
+ ]);
+
+
+
+ //print(this.lastCmd);
+
+ }
+
+
+ if (this._remotes!== false) {
+ return this._remotes;
+ }
+ this._remotes = [];
+
+ var bl = this.git([ 'remote', {
+ // 'no-color' : true,
+ 'verbose' : true
+ }]).split("\n");
+
+ var _this=this;
+ bl.forEach(function(line) {
+ if (!line.length) {
+ return;
+ // * master 61e7e7d oneliner
+ }
+ // print(JSON.stringify( line));
+ var parts = line.split(/\t/);
+ var type = false;
+ parts[1] = parts[1].replace(/\([a-z]+\)$/, function( a) {
+ type = a.substr(1,a.length-2);
+ return '';
+ });
+
+ _this._remotes.push( {
+ name: parts[0],
+ url : parts[1],
+ type: type
+ });
+
+ });
+ return this._remotes;
+ },
+
+
+
+ lastupdated: function() {
+ return 'tbc';
+ },
+ autocommit: function(val) {
+ if (typeof(val) == 'undefined') {
+
+ if (File.exists(this.gitdir + '/.gitlive-disable-autocommit')) {
+ return false;
+ }
+
+ return true;
+ }
+ if (val == this.autocommit()) {
+ return val;
+ }
+ if (!val) { // turn off
+ File.write(this.gitdir + '/.gitlive-disable-autocommit', '.');
+ } else {
+ File.remove(this.gitdir + '/.gitlive-disable-autocommit');
+ }
+ return val;
+
+ },
+
+
+
+ autopush: function(val) {
+
+ if (typeof(val) == 'undefined') {
+
+ if (File.exists(this.gitdir + '/.gitlive-disable-autopush')) {
+ return false;
+ }
+
+ return true;
+ }
+ if (val == this.autocommit()) {
+ return val;
+ }
+ if (!val) { // turn off
+ File.write(this.gitdir + '/.gitlive-disable-autopush', '.');
+ } else {
+ File.remove(this.gitdir + '/.gitlive-disable-autopush');
+ }
+ return val;
+ },
+ /*
+
+ public function getTags()
+ {
+ if ($this->tags !== null) {
+ return $this->tags;
+ }
+ $this->tags = array();
+ $fp = $this->git('tag');
+ while ($line = fgets($fp)) {
+ $line = trim($line);
+ $this->tags[$line] = $line;
+ }
+ $fp = null;
+ return $this->tags;
+ }
+
+ public function readdir($path, $object = null, $ident = null)
+ {
+ $res = array();
+
+ if ($object === null) {
+ $object = 'branch';
+ $ident = 'master';
+ }
+ $rev = $this->resolveRevision(null, $object, $ident);
+
+ if (strlen($path)) {
+ $path = rtrim($path, '/') . '/';
+ }
+
+ $fp = $this->git('ls-tree', $rev, $path);
+
+ $dirs = array();
+ require_once 'MTrack/SCM/Git/File.php';
+ while ($line = fgets($fp)) {
+ // blob = file, tree = dir..
+ list($mode, $type, $hash, $name) = preg_split("/\s+/", $line);
+ //echo '<PRE>';echo $line ."\n</PRE>";
+ $res[] = new MTrack_SCM_Git_File($this, "$name", $rev, $type == 'tree', $hash);
+ }
+ return $res;
+ }
+
+ public function file($path, $object = null, $ident = null)
+ {
+ if ($object == null) {
+ $branches = $this->getBranches();
+ if (isset($branches['master'])) {
+ $object = 'branch';
+ $ident = 'master';
+ } else {
+ // fresh/empty repo
+ return null;
+ }
+ }
+ $rev = $this->resolveRevision(null, $object, $ident);
+ require_once 'MTrack/SCM/Git/File.php';
+ return new MTrack_SCM_Git_File($this, $path, $rev);
+ }
+ */
+ /**
+ * history:
+ * Get the log/history about repo or specific file(s)
+ *
+ * @param string path (can be empty - eg. '')
+ * @param {number|date} limit how many to fetch
+ * @param {string} object = eg. rev|tag|branch (use 'rev' here and ident=HASH to retrieve a speific revision
+ * @param {string} ident =
+ *
+ * Example:
+ * - fetch on revision?: '.',1,'rev','xxxxxxxxxx'
+ *
+ *
+ * range... object ='rev' ident ='master..release'
+ *
+ */
+ history: function(path, limit , object, ident)
+ {
+ limit = limit || false;
+ object = object || false;
+ ident = ident || false;
+ var res = [];
+ var args = [ 'log' ];
+
+
+ if (object !== false) {
+ rev = this.resolveRevision(false, object, ident); // from scm...
+ args.push( '' + rev);
+ } else {
+ args.push( "master" );
+ }
+
+ var arg = {
+ "no-color" : true,
+ //args.push("--name-status";
+ "raw" : true,
+
+ "numstat" : true,
+ "date" : 'iso8601'
+ };
+ if (limit !== false) {
+ if (typeof(limit) == 'number') {
+ arg['max-count'] = limit;
+ } else if (typeof(limit) == 'object') {
+ arg.skip= limit[0];
+ arg['max-count']= limit[1];
+
+ } else {
+ arg.since = limit;
+ }
+ }
+
+ args.push(arg);
+
+
+
+
+
+ //echo '<PRE>';print_r($args);echo '</PRE>';
+
+
+
+ args.push({ '' : true });
+ if (typeof(path) == 'string') {
+ path = path[0] == '/' ? path.substring(1) : path;
+ args.push(path);
+ } else {
+ path.forEach(function(p) {
+ args.push(p);
+ })
+ }
+
+
+ // print_R(array($args, '--' ,$path));exit;
+ var fp = this.git(args).split("\n");
+
+ var commits = [];
+ var commit = false;
+ while (fp.length) {
+ var line = fp.shift() + "\n";
+
+ if (line.match(/^commit/)) {
+ if (commit !== false) {
+ commits.push( commit );
+ }
+ commit = line;
+ continue;
+ }
+ commit += line ;
+ }
+
+ if (commit !== false) {
+ commits.push( commit );
+ }
+ var res = [];
+ //print(JSON.stringify(commits,null,4));
+
+ var _t = this;
+ commits.forEach( function(c) {
+ // print(typeof(Event)); print(JSON.stringify(c));
+ var ev = new Event( {commit : c, repo: _t });
+ res.push(ev);
+
+ });
+ // close 'fp'
+ return res;
+ },
+ diff : function(path, from, to)
+ {
+ to = to || false;
+ from = from || false;
+
+
+
+
+ /*
+ if ($path instanceof MTrackSCMFile) {
+ if ($from === null) {
+ $from = $path->rev;
+ }
+ $path = $path->name;
+
+ }
+ // if it's a file event.. we are even lucker..
+ if ($path instanceof MTrackSCMFileEvent) {
+ return $this->git('log', '--max-count=1', '--format=format:', '--patch', $from, '--', $path->name);
+
+ }
+ */
+ // diff ignoring white space..
+ args = [ 'diff' , { 'w' : true} ]
+
+ if (to == false) {
+ to = from;
+ from = from + '^';
+ }
+
+ args.push(from+'..'+to);
+ args.push( { '' : true });
+ if (typeof(path) != 'string') {
+ path.forEach(function(p) { args.push(p); })
+ }
+ return this.git(args);
+ },
+
+
+
+ dayTree: function (path, limit , object, ident)
+ {
+
+ var ar = this.history(path, limit , object, ident);
+
+ // the point of this is to extract all the revisions, and group them.
+
+
+
+ //echo '<PRE>';print_R($ar);
+
+ // need to get a 2 dimensional array of
+ // files along top, and commints down.
+ var cfiles = []
+ var rows = [];
+
+ var days = {};
+
+ ar.forEach(function( commit) {
+
+ var files = commit.files;
+ var day = commit.cday;
+ if (typeof(days[day]) == 'undefined') {
+ days[day] = {
+ 'text' : day,
+ 'rev' : day,
+ 'children' : {}
+ }
+ }
+ var time= commit.ctime;
+ if (typeof(days[day]['children'][time]) == 'undefined' ) {
+ days[day]['children'][time] = {
+ 'text' : time,
+ 'rev' : day + ' ' + time,
+ 'children' : []
+ };
+ }
+ days[day]['children'][time]['children'].push( {
+ 'text' : commit.changelog,
+ 'rev' : commit.rev,
+ 'leaf' : true
+ });
+ });
+ var out = [];
+
+ for(var d in days) {
+ var dr = days[d];
+ dcn = dr['children'];
+ dr['children'] = [];
+ for(var t in dcn) {
+ var to = dcn[t];
+ to['rev'] = to['children'][0]['rev'];
+ dr['children'].push( to);
+ }
+ dr['rev'] = dr['children'][0]['rev'];
+ out.push( dr);
+ }
+
+ return out;
+
+
+ },
+
+
+
+
+ changedFiles :function(path, object, ident)
+ {
+ object = object || false;
+ ident = ident || false;
+ var res = [];
+ var args = [ 'diff', { 'numstat' : true} , { 'w' : true } ];
+
+
+ if (object !== false) {
+ rev = this.resolveRevision(false, object, ident); // from scm...
+ args.push( '' + rev);
+ } else {
+ args.push( "master" );
+ }
+ path = path[0] == '/' ? path.substring(1) : path;
+
+ args.push({ '' : true });
+ args.push(path);
+ // in theory you could click a number of them and only merge those changes..
+ // need to do a git diff.. and just get a list of files..
+ var rows = [];
+ var res = this.git(args).split("\n");
+ res.forEach( function(line) {
+
+
+
+ var ar = line.split("\t");
+ if (ar.length !=3 ) {
+ return;
+ }
+ rows.push({
+ 'added' : ar[0],
+ 'removed' : ar[1],
+ 'filename' : ar[2]
+ });
+
+
+ });
+ return rows;
+
+ },
+
+
+ checkout : function(branch)
+ {
+ this.git(['checkout', branch ]);
+ },
+ /**
+ * stash:
+ * Very basic stash the current changes (normally so you can checkout
+ * another branch..)
+ */
+ stash: function()
+ {
+ this.git(['stash' ]);
+ },
+
+
+
+ applyPatch : function( diff )
+ {
+ var sp = new Spawn({
+ cwd : this.repopath,
+ args : [ 'patch' , '-p1' , '-f' ] ,
+ env : [ "HOME=" + GLib.get_home_dir() ],
+ debug: true,
+ exceptions : false,
+ async : false,
+ listeners : {
+ input : function() {
+ print("sedning patch!");
+ return diff;
+ }
+ }
+ });
+ sp.run();
+ return sp.output;
+ } ,
+ /**
+ * add:
+ * add files to track.
+ *
+ * @argument {Array} files the files to add.
+ */
+ add : function ( files )
+ {
+ // should really find out if these are untracked files each..
+ // we run multiple versions to make sure that if one failes, it does not ignore the whole lot..
+ // not sure if that is how git works.. but just be certian.
+ var _t = this;
+ files.forEach(function(f) {
+ try {
+ _t.git([ 'add', { '': true }, f ]);
+ } catch(e) {} // ignore errors..
+ });
+ },
+
+ /**
+ * remove:
+ * remove files to track.
+ *
+ * @argument {Array} files the files to add.
+ */
+ remove : function ( files )
+ {
+ // this may fail if files do not exist..
+ // should really find out if these are untracked files each..
+ // we run multiple versions to make sure that if one failes, it does not ignore the whole lot..
+ // not sure if that is how git works.. but just be certian.
+ var _t = this;
+ files.forEach(function(f) {
+ try {
+ _t.git([ 'rm', { f: true } , { '': true }, f ]);
+ } catch(e) {} // ignore errors..
+ });
+ },
+
+ /**
+ * commit:
+ * perform a commit.
+ *
+ * @argument {Object} cfg commit configuration
+ *
+ * @property {String} name (optional)
+ * @property {String} email (optional)
+ * @property {String} changed (date) (optional)
+ * @property {String} reason (optional)
+ * @property {Array} files - the files that have changed.
+ *
+ */
+
+ commit : function( cfg )
+ {
+
+ var args= [ 'commit' ];
+ var env = [];
+
+ if (typeof(cfg.name) != 'undefined') {
+ args.push( {
+ 'author' : cfg.name + ' <' + cfg.email + '>'
+ });
+ env.push(
+ "GIT_COMMITTER_NAME" + cfg.name,
+ "GIT_COMMITTER_EMAIL" + cfg.email
+ );
+ }
+ if (typeof(cfg.changed) != 'undefined') {
+ env.push("GIT_AUTHOR_DATE= " + cfg.changed )
+
+ }
+ args.push(
+ { 'm' : (cfg.reason ? cfg.reason : 'Changed') },
+ { '': true }
+ );
+
+ cfg.files.forEach(function(f) { args.push(f); })
+
+ return this.git(args, env);
+ },
+
+ /**
+ * pull:
+ * Fetch and merge remote repo changes into current branch..
+ *
+ * At present we just need this to update the current working branch..
+ * -- maybe later it will have a few options and do more stuff..
+ *
+ */
+ pull : function ()
+ {
+ // should probably hand error conditions better...
+ return this.git([ 'pull' ]);
+
+
+ },
+ /**
+ * push:
+ * Send local changes to remote repo(s)
+ *
+ * At present we just need this to push the current branch.
+ * -- maybe later it will have a few options and do more stuff..
+ *
+ */
+ push : function ()
+ {
+ // should
+ return this.git([ 'push' ]);
+
+ },
+
+ /*
+ public function getWorkingCopy()
+ {
+ require_once 'MTrack/SCM/Git/WorkingCopy.php';
+ return new MTrack_SCM_Git_WorkingCopy($this);
+ }
+
+ public function getRelatedChanges($revision) // pretty nasty.. could end up with 1000's of changes..
+ {
+ $parents = array();
+ $kids = array();
+
+ $fp = $this->git('rev-parse', "$revision^");
+ while (($line = fgets($fp)) !== false) {
+ $parents[] = trim($line);
+ }
+
+ // Ugh!: http://stackoverflow.com/questions/1761825/referencing-the-child-of-a-commit-in-git
+ $fp = $this->git('rev-list', '--all', '--parents');
+ while (($line = fgets($fp)) !== false) {
+ $hashes = preg_split("/\s+/", $line);
+ $kid = array_shift($hashes);
+ if (in_array($revision, $hashes)) {
+ $kids[] = $kid;
+ }
+ }
+
+ return array($parents, $kids);
+ } */
+
+
+
+
+ git: function(args_in,env)
+ {
+ // convert arguments.
+
+ //print(JSON.stringify(args_in,null,4));
+ args_in.unshift( {
+ 'git-dir' : this.gitdir,
+ 'no-pager' : true
+ });
+ var args = ['git' ];
+
+ if (this.gitdir != this.repopath) {
+ args_in.unshift( { "work-tree" : this.repopath } );
+ }
+
+ args_in.forEach(function(arg) {
+ if (typeof(arg) == 'string') {
+ args.push(arg);
+ return;
+ }
+ if (typeof(arg) == 'object') {
+ for(var k in arg) {
+ var v = arg[k];
+
+ args.push(k.length != 1 ? ('--' + k) : ('-' + k));
+
+ if (v === true) {
+ continue;;
+ }
+ args.push(v);
+ }
+ }
+ });
+ this.lastCmd = args.join(" ");
+ if(this.debug) {
+
+ print( args.join(" "));
+ }
+ env = env || [];
+ env.push( "HOME=" + GLib.get_home_dir() );
+ // do not need to set gitpath..
+ //if (File.exists(this.repo + '/.git/config')) {
+ //env.push("GITPATH=" + this.repo );
+ //}
+
+ //print(JSON.stringify(args,null,4)); Seed.quit();
+ var sp = new Spawn({
+ cwd : this.repopath,
+ args : args,
+ env : env, // optional
+ debug: this.debug,
+ exceptions : false,
+ async : false
+ });
+ sp.run();
+
+ if (sp.result) {
+ print(JSON.stringify(sp.result));
+
+ print(JSON.stringify(sp.args));
+ print(JSON.stringify(sp.stderr));
+
+ throw {
+ name : "RepoSpawnError",
+ message : sp.stderr,
+ spawn : sp
+ };
+ }
+
+ //print("GOT: " + output)
+ // parse output for some commands ?
+ return sp.output;
+ },
+ /**
+ * parseAuthor:
+ * break author string with name and email into parts
+ * @argument {String} author
+ * @returns {Object} with 'name' and 'email' properties.
+ */
+ parseAuthor : function(author)
+ {
+ var lp = author.indexOf('<');
+
+ return {
+ name : author.substring(0, lp-1).replace(/\s+$/,''),
+ email : author.substring(0, author.length-1).substring(lp+1)
+ };
+
+ }
+
+});
+
+Repo.parseURL = function(url)
+{
+
+ // git : git://github.com/roojs/roojs1.git
+ // http https://roojs@github.com/roojs/roojs1.git
+ // ssh git@github.com:roojs/roojs1.git
+ var ret = {
+ scheme : 'ssh://'
+ }
+ url = url.replace(/^[a-z]+:\/\//, function(v) {
+ ret.scheme = v;
+ return '';
+ });
+ var parts = url.split(ret.scheme == 'ssh://' ? ':' : "/");
+ var login_host = parts.shift().split('@');
+ var user_pass = (login_host.length == 2 ? login_host.shift() : '').split(':');
+ ret.user = user_pass[0];
+ ret.pass = user_pass.length == 2 ? user_pass[1] : '';
+
+ ret.host = login_host.shift();
+ ret.path = parts.join('/');
+ return ret;
+
+
+}
\ No newline at end of file