2 const GLib = imports.gi.GLib;
4 const XObject = imports.XObject.XObject;
5 const Event = imports.Scm.Git.Event.Event;
6 const Spawn = imports.Spawn.Spawn;
7 const File = imports.File.File;
17 Repo = XObject.define(
21 * @param {Object} cfg - Configuration
22 * (basically repopath is currently only critical one.)
28 XObject.extend(this,cfg);
29 this.gitdir = cfg.repopath + "/.git";
31 if (!File.isDirectory(this.gitdir)) {
32 this.gitdir = this.repopath;
34 //Repo.superclass.constructor.call(this,cfg);
37 imports.Scm.Repo.Repo, // or Object
40 currentBranch : false,
45 hasLocalChanges : false,
48 getMetaData : function() {
57 getStatus : function()
59 //git status --porcelain
60 // find out if we are up-to-date.
61 //git ls-remote origin -h refs/heads/master
62 var bl = this.git([ 'status', {
65 print(JSON.stringify(bl.length));
66 this.hasLocalChanges = bl.length > 0 ? true : false;
67 this.localChanges = bl;
73 getBranches : function()
75 if (this.branches !== false) {
80 var bl = this.git([ 'branch', {
92 bl.forEach(function(line) {
93 // * master 61e7e7d oneliner
94 var active = line[0]=='*';
98 line = line.substring(2);
100 //print(JSON.stringify(line));
101 var parts = line.split(/\s+/);
102 if (parts[1] == '->') {
103 return; // it's an alias like remotes/origin/HEAD -> origin/master
111 if (parts[0].match(/^remotes\//)) {
112 br.remote = parts[0];
113 bmap[br.remote] = br;
122 _this.currentBranch = br;
126 //print(JSON.stringify(local));
127 //print(JSON.stringify(remotes));
128 //print(JSON.stringify(bmap,null,4));
131 // overlay tracking informaion
134 { format : '%(refname:short):remotes/%(upstream:short)' },
137 //print(this.lastCmd);
139 //print(JSON.stringify(bl,null,4));
141 bl.forEach(function(line) {
145 var ar = line.split(':remotes/');
148 var rname = 'remotes/' + ar[1];
150 // we should always have a local version of it.
151 bmap[lname].remote = rname;
152 if (typeof(bmap[rname]) == 'undefined') {
155 bmap[lname].remoterev = bmap[rname].lastrev;
156 // flag it for not adding..
158 bmap[rname].name = lname;
164 // add any remotes that do not have name..
165 remotes.forEach(function(r) {
170 // create a tracking branch..
171 var name = r.remote.replace(/^remotes\//, '' ).replace(/^origin\//,'').replace('/', '.');
175 if (typeof(bmap[name]) != 'undefined') {
176 // already got aremote of that name.
178 r.remoterev = r.lastrev;
195 r.remoterev = r.lastrev;
200 this.branches = local;
201 // print(JSON.stringify(local,null,4));
204 return this.branches;
208 remotes: function(cfg)
212 this._remotes = false; // reset so we can query it..
214 var url = Repo.parseURL(cfg.url);
215 if ((url.scheme == 'http://' || url.scheme == 'https://' )
216 && url.user.length) {
217 // remove username - as it confuses netrc..
218 cfg.url = url.scheme + url.host + '/' +url.path;
229 this.branches = false;
239 //print(this.lastCmd);
244 if (this._remotes!== false) {
245 return this._remotes;
249 var bl = this.git([ 'remote', {
250 // 'no-color' : true,
255 bl.forEach(function(line) {
258 // * master 61e7e7d oneliner
260 // print(JSON.stringify( line));
261 var parts = line.split(/\t/);
263 parts[1] = parts[1].replace(/\([a-z]+\)$/, function( a) {
264 type = a.substr(1,a.length-2);
268 _this._remotes.push( {
275 return this._remotes;
280 lastupdated: function() {
283 autocommit: function(val) {
284 if (typeof(val) == 'undefined') {
286 if (File.exists(this.gitdir + '/.gitlive-disable-autocommit')) {
292 if (val == this.autocommit()) {
295 if (!val) { // turn off
296 File.write(this.gitdir + '/.gitlive-disable-autocommit', '.');
298 File.remove(this.gitdir + '/.gitlive-disable-autocommit');
306 autopush: function(val) {
308 if (typeof(val) == 'undefined') {
310 if (File.exists(this.gitdir + '/.gitlive-disable-autopush')) {
316 if (val == this.autocommit()) {
319 if (!val) { // turn off
320 File.write(this.gitdir + '/.gitlive-disable-autopush', '.');
322 File.remove(this.gitdir + '/.gitlive-disable-autopush');
328 public function getTags()
330 if ($this->tags !== null) {
333 $this->tags = array();
334 $fp = $this->git('tag');
335 while ($line = fgets($fp)) {
337 $this->tags[$line] = $line;
343 public function readdir($path, $object = null, $ident = null)
347 if ($object === null) {
351 $rev = $this->resolveRevision(null, $object, $ident);
354 $path = rtrim($path, '/') . '/';
357 $fp = $this->git('ls-tree', $rev, $path);
360 require_once 'MTrack/SCM/Git/File.php';
361 while ($line = fgets($fp)) {
362 // blob = file, tree = dir..
363 list($mode, $type, $hash, $name) = preg_split("/\s+/", $line);
364 //echo '<PRE>';echo $line ."\n</PRE>";
365 $res[] = new MTrack_SCM_Git_File($this, "$name", $rev, $type == 'tree', $hash);
370 public function file($path, $object = null, $ident = null)
372 if ($object == null) {
373 $branches = $this->getBranches();
374 if (isset($branches['master'])) {
382 $rev = $this->resolveRevision(null, $object, $ident);
383 require_once 'MTrack/SCM/Git/File.php';
384 return new MTrack_SCM_Git_File($this, $path, $rev);
389 * Get the log/history about repo or specific file(s)
391 * @param string path (can be empty - eg. '')
392 * @param {number|date} limit how many to fetch
393 * @param {string} object = eg. rev|tag|branch (use 'rev' here and ident=HASH to retrieve a speific revision
394 * @param {string} ident =
397 * - fetch on revision?: '.',1,'rev','xxxxxxxxxx'
400 * range... object ='rev' ident ='master..release'
403 history: function(path, limit , object, ident)
405 limit = limit || false;
406 object = object || false;
407 ident = ident || false;
409 var args = [ 'log' ];
412 if (object !== false) {
413 rev = this.resolveRevision(false, object, ident); // from scm...
414 args.push( '' + rev);
416 args.push( "master" );
421 //args.push("--name-status";
427 if (limit !== false) {
428 if (typeof(limit) == 'number') {
429 arg['max-count'] = limit;
430 } else if (typeof(limit) == 'object') {
432 arg['max-count']= limit[1];
445 //echo '<PRE>';print_r($args);echo '</PRE>';
449 args.push({ '' : true });
450 if (typeof(path) == 'string') {
451 path = path[0] == '/' ? path.substring(1) : path;
454 path.forEach(function(p) {
460 // print_R(array($args, '--' ,$path));exit;
461 var fp = this.git(args).split("\n");
466 var line = fp.shift() + "\n";
468 if (line.match(/^commit/)) {
469 if (commit !== false) {
470 commits.push( commit );
478 if (commit !== false) {
479 commits.push( commit );
482 //print(JSON.stringify(commits,null,4));
485 commits.forEach( function(c) {
486 // print(typeof(Event)); print(JSON.stringify(c));
487 var ev = new Event( {commit : c, repo: _t });
494 diff : function(path, from, to)
497 from = from || false;
503 if ($path instanceof MTrackSCMFile) {
504 if ($from === null) {
510 // if it's a file event.. we are even lucker..
511 if ($path instanceof MTrackSCMFileEvent) {
512 return $this->git('log', '--max-count=1', '--format=format:', '--patch', $from, '--', $path->name);
516 // diff ignoring white space..
517 args = [ 'diff' , { 'w' : true} ]
524 args.push(from+'..'+to);
525 args.push( { '' : true });
526 if (typeof(path) != 'string') {
527 path.forEach(function(p) { args.push(p); })
529 return this.git(args);
534 dayTree: function (path, limit , object, ident)
537 var ar = this.history(path, limit , object, ident);
539 // the point of this is to extract all the revisions, and group them.
543 //echo '<PRE>';print_R($ar);
545 // need to get a 2 dimensional array of
546 // files along top, and commints down.
552 ar.forEach(function( commit) {
554 var files = commit.files;
555 var day = commit.cday;
556 if (typeof(days[day]) == 'undefined') {
563 var time= commit.ctime;
564 if (typeof(days[day]['children'][time]) == 'undefined' ) {
565 days[day]['children'][time] = {
567 'rev' : day + ' ' + time,
571 days[day]['children'][time]['children'].push( {
572 'text' : commit.changelog,
581 dcn = dr['children'];
585 to['rev'] = to['children'][0]['rev'];
586 dr['children'].push( to);
588 dr['rev'] = dr['children'][0]['rev'];
600 changedFiles :function(path, object, ident)
602 object = object || false;
603 ident = ident || false;
605 var args = [ 'diff', { 'numstat' : true} , { 'w' : true } ];
608 if (object !== false) {
609 rev = this.resolveRevision(false, object, ident); // from scm...
610 args.push( '' + rev);
612 args.push( "master" );
614 path = path[0] == '/' ? path.substring(1) : path;
616 args.push({ '' : true });
618 // in theory you could click a number of them and only merge those changes..
619 // need to do a git diff.. and just get a list of files..
621 var res = this.git(args).split("\n");
622 res.forEach( function(line) {
626 var ar = line.split("\t");
627 if (ar.length !=3 ) {
643 checkout : function(branch)
645 this.git(['checkout', branch ]);
649 * Very basic stash the current changes (normally so you can checkout
654 this.git(['stash' ]);
659 applyPatch : function( diff )
663 args : [ 'patch' , '-p1' , '-f' ] ,
664 env : [ "HOME=" + GLib.get_home_dir() ],
670 print("sedning patch!");
680 * add files to track.
682 * @argument {Array} files the files to add.
684 add : function ( files )
686 // should really find out if these are untracked files each..
687 // we run multiple versions to make sure that if one failes, it does not ignore the whole lot..
688 // not sure if that is how git works.. but just be certian.
690 files.forEach(function(f) {
692 _t.git([ 'add', { '': true }, f ]);
693 } catch(e) {} // ignore errors..
699 * remove files to track.
701 * @argument {Array} files the files to add.
703 remove : function ( files )
705 // this may fail if files do not exist..
706 // should really find out if these are untracked files each..
707 // we run multiple versions to make sure that if one failes, it does not ignore the whole lot..
708 // not sure if that is how git works.. but just be certian.
710 files.forEach(function(f) {
712 _t.git([ 'rm', { f: true } , { '': true }, f ]);
713 } catch(e) {} // ignore errors..
721 * @argument {Object} cfg commit configuration
723 * @property {String} name (optional)
724 * @property {String} email (optional)
725 * @property {String} changed (date) (optional)
726 * @property {String} reason (optional)
727 * @property {Array} files - the files that have changed.
731 commit : function( cfg )
734 var args= [ 'commit' ];
737 if (typeof(cfg.name) != 'undefined') {
739 'author' : cfg.name + ' <' + cfg.email + '>'
742 "GIT_COMMITTER_NAME" + cfg.name,
743 "GIT_COMMITTER_EMAIL" + cfg.email
746 if (typeof(cfg.changed) != 'undefined') {
747 env.push("GIT_AUTHOR_DATE= " + cfg.changed )
751 { 'm' : (cfg.reason ? cfg.reason : 'Changed') },
755 cfg.files.forEach(function(f) { args.push(f); })
757 return this.git(args, env);
762 * Fetch and merge remote repo changes into current branch..
764 * At present we just need this to update the current working branch..
765 * -- maybe later it will have a few options and do more stuff..
770 // should probably hand error conditions better...
771 return this.git([ 'pull' ]);
777 * Send local changes to remote repo(s)
779 * At present we just need this to push the current branch.
780 * -- maybe later it will have a few options and do more stuff..
786 return this.git([ 'push' ]);
791 public function getWorkingCopy()
793 require_once 'MTrack/SCM/Git/WorkingCopy.php';
794 return new MTrack_SCM_Git_WorkingCopy($this);
797 public function getRelatedChanges($revision) // pretty nasty.. could end up with 1000's of changes..
802 $fp = $this->git('rev-parse', "$revision^");
803 while (($line = fgets($fp)) !== false) {
804 $parents[] = trim($line);
807 // Ugh!: http://stackoverflow.com/questions/1761825/referencing-the-child-of-a-commit-in-git
808 $fp = $this->git('rev-list', '--all', '--parents');
809 while (($line = fgets($fp)) !== false) {
810 $hashes = preg_split("/\s+/", $line);
811 $kid = array_shift($hashes);
812 if (in_array($revision, $hashes)) {
817 return array($parents, $kids);
823 git: function(args_in,env)
825 // convert arguments.
827 //print(JSON.stringify(args_in,null,4));
829 'git-dir' : this.gitdir,
834 if (this.gitdir != this.repopath) {
835 args_in.unshift( { "work-tree" : this.repopath } );
838 args_in.forEach(function(arg) {
839 if (typeof(arg) == 'string') {
843 if (typeof(arg) == 'object') {
847 args.push(k.length != 1 ? ('--' + k) : ('-' + k));
856 this.lastCmd = args.join(" ");
859 print( args.join(" "));
862 env.push( "HOME=" + GLib.get_home_dir() );
863 // do not need to set gitpath..
864 //if (File.exists(this.repo + '/.git/config')) {
865 //env.push("GITPATH=" + this.repo );
868 //print(JSON.stringify(args,null,4)); Seed.quit();
872 env : env, // optional
880 print(JSON.stringify(sp.result));
882 print(JSON.stringify(sp.args));
883 print(JSON.stringify(sp.stderr));
886 name : "RepoSpawnError",
892 //print("GOT: " + output)
893 // parse output for some commands ?
898 * break author string with name and email into parts
899 * @argument {String} author
900 * @returns {Object} with 'name' and 'email' properties.
902 parseAuthor : function(author)
904 var lp = author.indexOf('<');
907 name : author.substring(0, lp-1).replace(/\s+$/,''),
908 email : author.substring(0, author.length-1).substring(lp+1)
915 Repo.parseURL = function(url)
918 // git : git://github.com/roojs/roojs1.git
919 // http https://roojs@github.com/roojs/roojs1.git
920 // ssh git@github.com:roojs/roojs1.git
924 url = url.replace(/^[a-z]+:\/\//, function(v) {
928 var parts = url.split(ret.scheme == 'ssh://' ? ':' : "/");
929 var login_host = parts.shift().split('@');
930 var user_pass = (login_host.length == 2 ? login_host.shift() : '').split(':');
931 ret.user = user_pass[0];
932 ret.pass = user_pass.length == 2 ? user_pass[1] : '';
934 ret.host = login_host.shift();
935 ret.path = parts.join('/');