sync
[gitlive] / old_seed_version / Scm / Git / Repo.js
1
2 const GLib   = imports.gi.GLib;
3
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;
8
9 /**
10  * @class Scm.Git.Repo
11  *
12  * @extends Scm.Repo
13  * 
14  *
15  *
16  */
17 Repo = XObject.define(
18     /**
19      * constructor:
20      * 
21      * @param {Object} cfg - Configuration
22      *     (basically repopath is currently only critical one.)
23      *
24      */
25      
26     function(cfg) {
27         // cal parent?
28         XObject.extend(this,cfg);
29         this.gitdir = cfg.repopath + "/.git";
30         
31         if (!File.isDirectory(this.gitdir)) {
32             this.gitdir = this.repopath;
33         }
34         //Repo.superclass.constructor.call(this,cfg);
35         
36     },
37     imports.Scm.Repo.Repo, // or Object
38     {
39         branches : false,
40         currentBranch : false,
41         tags : false,
42         gitdir : false,
43         debug : true,
44         lastCmd : false,
45         hasLocalChanges : false,
46         localChanges : false,
47         
48         getMetaData : function() {
49             return {
50               name :  'Git',
51               tool : ['git' ]  
52                 
53             };
54         },
55         
56         
57         getStatus : function()
58         {
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', {
63                 'porcelain' : true
64             }]);
65             print(JSON.stringify(bl.length));
66             this.hasLocalChanges = bl.length > 0 ? true : false; 
67             this.localChanges = bl;
68             
69             
70         },
71         
72         
73         getBranches : function()
74         {
75             if (this.branches !== false) {
76                 return this.branches;
77             }
78             this.branches = [];
79             
80             var bl = this.git([ 'branch', {
81                 'no-color' : true,
82                 'verbose' : true,
83                 'no-abbrev'  : true,
84                 'a' : true
85             }]).split("\n");
86             var bmap = {};
87             var _this=this;
88             
89             var local = [];
90             var remotes = [];
91             
92             bl.forEach(function(line) {
93                   // * master 61e7e7d oneliner
94                 var active = line[0]=='*';
95                 if (!line.length) {
96                     return;
97                 }
98                 line = line.substring(2);
99                 
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
104                 }
105                 var br = {
106                     lastrev :  parts[1],
107                     name :      '',
108                     remote :    '',
109                     remoterev : ''
110                 };
111                 if (parts[0].match(/^remotes\//)) {
112                     br.remote = parts[0];
113                     bmap[br.remote] = br;
114                     remotes.push(br);
115                 } else { 
116                     br.name = parts[0];
117                     bmap[br.name] = br;
118                     local.push(br);
119                 }
120                 
121                 if (active) {
122                     _this.currentBranch = br;
123                 }
124             });
125             
126             //print(JSON.stringify(local));
127             //print(JSON.stringify(remotes));
128             //print(JSON.stringify(bmap,null,4));
129
130             
131             // overlay tracking informaion
132             bl = this.git([
133                 'for-each-ref',
134                 { format :  '%(refname:short):remotes/%(upstream:short)' },
135                 'refs/heads'
136             ]).split("\n");
137             //print(this.lastCmd);
138             
139             //print(JSON.stringify(bl,null,4));
140             
141             bl.forEach(function(line) {
142                 if (!line.length) {
143                     return;
144                 }
145                 var ar = line.split(':remotes/');
146                 
147                 var lname= ar[0];
148                 var rname = 'remotes/' + ar[1];
149                 //print(rname);
150                 // we should always have a local version of it.
151                 bmap[lname].remote = rname;
152                 if (typeof(bmap[rname]) == 'undefined') {
153                     return;
154                 }
155                 bmap[lname].remoterev =  bmap[rname].lastrev;
156                 // flag it for not adding..
157                 
158                 bmap[rname].name = lname;
159             });
160             var _this =this;
161             
162             
163             
164             // add any remotes that do not have name..
165             remotes.forEach(function(r) {
166                 if (r.name.length) {
167                     return;
168                 }
169                 
170                 // create a tracking branch..
171                 var name = r.remote.replace(/^remotes\//, '' ).replace(/^origin\//,'').replace('/', '.');
172                 
173                  
174               
175                 if (typeof(bmap[name]) != 'undefined') {
176                     // already got aremote of that name.
177                     // skip it...
178                     r.remoterev = r.lastrev;
179                     r.lastrev = '';
180                     local.push(r);
181                     return;
182                 }
183                 
184                 
185                 _this.git([ 
186                     'branch',
187                     {
188                         f : true,
189                         track : true
190                     },
191                     name,
192                     r.remote
193                 ]);
194                 
195                 r.remoterev = r.lastrev;
196                 r.name = name;
197                 local.push(r);
198             });
199             
200             this.branches = local;
201          //    print(JSON.stringify(local,null,4));
202             
203             
204             return this.branches;
205         },
206         _remotes : false,
207         
208         remotes: function(cfg)
209         {
210             
211             if (cfg) {
212                 this._remotes = false; // reset so we can query it..
213                 
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;
219                     
220                 }
221                 
222                 
223                 this.git([
224                     'remote',
225                     'add',
226                     cfg.name,
227                     cfg.url
228                 ]);
229                 this.branches = false;
230                 
231                 this.git([
232                     'fetch',
233                     cfg.name,
234                     { 'a' : true }
235                 ]);
236                 
237                 
238                 
239                 //print(this.lastCmd);
240
241             }
242             
243             
244             if (this._remotes!== false) {
245                 return this._remotes;
246             }
247             this._remotes = [];
248             
249             var bl = this.git([ 'remote', {
250             //    'no-color' : true,
251                 'verbose' : true
252             }]).split("\n");
253             
254             var _this=this;
255             bl.forEach(function(line) {
256                 if (!line.length) {
257                     return;
258                   // * master 61e7e7d oneliner
259                 }
260                // print(JSON.stringify( line));
261                 var parts = line.split(/\t/);
262                 var type = false;
263                 parts[1] = parts[1].replace(/\([a-z]+\)$/, function( a) {
264                     type = a.substr(1,a.length-2);
265                     return '';
266                 });
267                 
268                 _this._remotes.push( {
269                     name: parts[0],
270                     url : parts[1],
271                     type:  type
272                 });
273                 
274             });
275             return this._remotes;
276         },
277         
278         
279         
280         lastupdated: function() {
281             return 'tbc';
282         },
283         autocommit: function(val) {
284             if (typeof(val) == 'undefined') {
285                 
286                 if (File.exists(this.gitdir + '/.gitlive-disable-autocommit')) {
287                     return false;
288                 }
289                 
290                 return true;
291             }
292             if (val == this.autocommit()) {
293                 return val;
294             }
295             if (!val) { // turn off
296                 File.write(this.gitdir + '/.gitlive-disable-autocommit', '.');
297             } else {
298                 File.remove(this.gitdir + '/.gitlive-disable-autocommit');
299             }
300             return val;
301             
302         },
303         
304         
305         
306         autopush: function(val) {
307             
308             if (typeof(val) == 'undefined') {
309                 
310                 if (File.exists(this.gitdir + '/.gitlive-disable-autopush')) {
311                     return false;
312                 }
313                 
314                 return true;
315             }
316             if (val == this.autocommit()) {
317                 return val;
318             }
319             if (!val) { // turn off
320                 File.write(this.gitdir + '/.gitlive-disable-autopush', '.');
321             } else {
322                 File.remove(this.gitdir + '/.gitlive-disable-autopush');
323             }
324             return val; 
325         },
326           /*
327         
328           public function getTags()
329           {
330             if ($this->tags !== null) {
331               return $this->tags;
332             }
333             $this->tags = array();
334             $fp = $this->git('tag');
335             while ($line = fgets($fp)) {
336               $line = trim($line);
337               $this->tags[$line] = $line;
338             }
339             $fp = null;
340             return $this->tags;
341           }
342         
343           public function readdir($path, $object = null, $ident = null)
344           {
345             $res = array();
346         
347             if ($object === null) {
348               $object = 'branch';
349               $ident = 'master';
350             }
351             $rev = $this->resolveRevision(null, $object, $ident);
352         
353             if (strlen($path)) {
354               $path = rtrim($path, '/') . '/';
355             }
356         
357             $fp = $this->git('ls-tree', $rev, $path);
358         
359             $dirs = array();
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);
366             }
367             return $res;
368           }
369         
370           public function file($path, $object = null, $ident = null)
371           {
372             if ($object == null) {
373               $branches = $this->getBranches();
374               if (isset($branches['master'])) {
375                 $object = 'branch';
376                 $ident = 'master';
377               } else {
378                 // fresh/empty repo
379                 return null;
380               }
381             }
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);
385           }
386        */
387         /**
388          * history:
389          * Get the log/history about repo or specific file(s)
390          * 
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 = 
395          *
396          * Example:
397          *   - fetch on revision?:  '.',1,'rev','xxxxxxxxxx'
398          *
399          *
400          * range... object ='rev' ident ='master..release'
401          * 
402          */
403         history: function(path, limit , object, ident)
404         {
405             limit = limit || false;
406             object = object || false;
407             ident = ident || false; 
408             var res = [];
409             var args = [ 'log' ];
410              
411             
412             if (object !== false) {
413                 rev = this.resolveRevision(false, object, ident); // from scm...
414                 args.push( '' + rev);  
415             } else {
416                 args.push( "master" );
417             }
418            
419             var arg = {
420                 "no-color" : true, 
421             //args.push("--name-status";
422                 "raw" : true,
423                  
424                 "numstat" : true,
425                 "date" : 'iso8601'
426             };
427             if (limit !== false) {
428                 if (typeof(limit) == 'number') {
429                     arg['max-count'] =  limit;
430                 } else if (typeof(limit) == 'object') {
431                     arg.skip=  limit[0];
432                     arg['max-count']=  limit[1];
433      
434                 } else {
435                     arg.since = limit;
436                 }
437             }
438             
439             args.push(arg);
440             
441             
442             
443        
444             
445             //echo '<PRE>';print_r($args);echo '</PRE>';
446             
447             
448             
449             args.push({ '' : true });
450             if (typeof(path) == 'string') {
451                 path = path[0] == '/' ? path.substring(1) : path;
452                 args.push(path);
453             } else {
454                 path.forEach(function(p) {
455                      args.push(p);
456                 })
457             }
458             
459             
460             //   print_R(array($args, '--' ,$path));exit;
461             var fp = this.git(args).split("\n");
462     
463             var commits = [];
464             var commit = false;
465             while (fp.length) {
466                 var line = fp.shift() + "\n";
467                 
468                 if (line.match(/^commit/)) {
469                     if (commit !== false) {
470                         commits.push( commit );
471                     }
472                     commit = line;
473                     continue;
474                 }
475                 commit += line ;
476             }
477              
478             if (commit !== false) {
479                 commits.push( commit );
480             }
481             var res = [];
482             //print(JSON.stringify(commits,null,4));
483             
484             var _t = this;
485             commits.forEach( function(c) {
486               //       print(typeof(Event)); print(JSON.stringify(c));
487                 var ev = new Event( {commit : c, repo: _t });
488                 res.push(ev);
489                   
490             });
491             // close 'fp'
492             return res;
493         },
494         diff : function(path, from, to)
495         {
496             to = to || false;
497             from = from || false;
498             
499              
500             
501             
502             /*
503             if ($path instanceof MTrackSCMFile) {
504                 if ($from === null) {
505                     $from = $path->rev;
506                 }
507                 $path = $path->name;
508                 
509             }
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);
513                 
514             }
515             */
516             // diff ignoring white space..
517             args = [ 'diff' , { 'w' : true} ]
518         
519             if (to == false) {
520                 to = from;
521                 from = from + '^';
522             }
523             
524             args.push(from+'..'+to);
525             args.push( { '' : true });
526             if (typeof(path) != 'string') {
527                 path.forEach(function(p) { args.push(p); })
528             }
529             return this.git(args); 
530         },
531         
532         
533         
534         dayTree: function (path, limit , object, ident)
535         {
536             
537             var ar = this.history(path, limit , object, ident);
538             
539             // the point of this is to extract all the revisions, and group them.
540             
541             
542             
543             //echo '<PRE>';print_R($ar);
544             
545             // need to get a 2 dimensional array of
546             // files along top, and commints down.
547             var cfiles = []
548             var rows = [];
549             
550             var days = {};
551             
552             ar.forEach(function( commit) {
553                 
554                 var files = commit.files;
555                 var day = commit.cday;
556                 if (typeof(days[day]) == 'undefined') { 
557                     days[day] = {
558                         'text' : day,
559                         'rev' : day,
560                         'children' : {}
561                     }
562                 }
563                 var time= commit.ctime;
564                 if (typeof(days[day]['children'][time]) == 'undefined' ) { 
565                     days[day]['children'][time] = {
566                         'text' : time,
567                         'rev' : day + ' ' + time,
568                         'children' : [] 
569                     };
570                 }
571                 days[day]['children'][time]['children'].push( {
572                     'text' : commit.changelog,
573                     'rev' : commit.rev,
574                     'leaf' :  true
575                 });
576             });
577             var out = [];
578             
579             for(var d in days) {
580                 var dr = days[d];
581                 dcn = dr['children'];
582                 dr['children'] = [];
583                 for(var t in dcn) {
584                     var to = dcn[t];
585                     to['rev']  = to['children'][0]['rev'];
586                     dr['children'].push( to);
587                 }
588                 dr['rev'] = dr['children'][0]['rev'];
589                 out.push( dr);
590             }
591             
592             return out;            
593              
594             
595         },
596         
597         
598     
599     
600         changedFiles :function(path,  object, ident)
601         {
602              object = object || false;
603             ident = ident || false; 
604             var res = [];
605             var args = [ 'diff', { 'numstat' : true}  , { 'w' : true } ];
606              
607             
608             if (object !== false) {
609                 rev = this.resolveRevision(false, object, ident); // from scm...
610                 args.push( '' + rev);  
611             } else {
612                 args.push( "master" );
613             }
614             path = path[0] == '/' ? path.substring(1) : path; 
615             
616             args.push({ '' : true });
617             args.push(path);
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..
620             var rows = [];
621             var res = this.git(args).split("\n"); 
622             res.forEach( function(line) {
623                 
624                 
625                 
626                 var ar = line.split("\t"); 
627                 if (ar.length !=3 ) {
628                     return;
629                 }
630                 rows.push({ 
631                     'added' : ar[0],
632                     'removed' : ar[1],
633                     'filename' : ar[2]
634                 });
635                 
636                 
637             });
638             return rows;  
639     
640         },
641         
642         
643         checkout : function(branch)
644         {
645             this.git(['checkout', branch ]);
646         },
647         /**
648          * stash:
649          * Very basic stash the current changes (normally so you can checkout
650          * another branch..)
651          */
652         stash: function()
653         {
654             this.git(['stash' ]);
655         },
656         
657         
658             
659         applyPatch : function( diff )
660         {
661             var sp = new Spawn({
662                 cwd : this.repopath,
663                 args : [ 'patch' , '-p1' , '-f' ] ,
664                 env :  [  "HOME=" + GLib.get_home_dir() ],
665                 debug: true,
666                 exceptions : false,
667                 async : false,
668                 listeners : {
669                     input : function() {
670                         print("sedning patch!");
671                         return diff;
672                     }
673                 }
674             });
675             sp.run();
676             return sp.output;
677         } ,
678         /**
679          * add:
680          * add files to track.
681          *
682          * @argument {Array} files the files to add.
683          */
684         add : function ( files )
685         {
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.
689             var _t = this;
690             files.forEach(function(f) {
691                 try { 
692                     _t.git([ 'add', { '': true }, f ]);
693                 } catch(e) {} // ignore errors..
694             });  
695         },
696         
697           /**
698          * remove:
699          * remove files to track.
700          *
701          * @argument {Array} files the files to add.
702          */
703         remove : function ( files )
704         {
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.
709             var _t = this;
710             files.forEach(function(f) {
711                 try {
712                     _t.git([ 'rm', { f: true } , { '': true }, f ]);
713                 } catch(e) {} // ignore errors..
714             });  
715         },
716         
717         /**
718          * commit:
719          * perform a commit.
720          *
721          * @argument {Object} cfg commit configuration
722          * 
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. 
728          * 
729          */
730          
731         commit : function( cfg )
732         {
733             
734             var args= [  'commit'  ];
735             var env = [];
736             
737             if (typeof(cfg.name) != 'undefined') {
738                 args.push( {
739                     'author' : cfg.name + ' <' + cfg.email + '>'
740                 });
741                 env.push(
742                     "GIT_COMMITTER_NAME" + cfg.name,
743                     "GIT_COMMITTER_EMAIL" + cfg.email
744                 );
745             }
746             if (typeof(cfg.changed) != 'undefined') {
747                 env.push("GIT_AUTHOR_DATE= " + cfg.changed )
748                 
749             }
750             args.push( 
751                 { 'm' : (cfg.reason ? cfg.reason : 'Changed') },
752                 { '': true }
753             );
754             
755             cfg.files.forEach(function(f) { args.push(f); })
756              
757             return this.git(args, env);
758         },
759         
760         /**
761          * pull:
762          * Fetch and merge remote repo changes into current branch..
763          *
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..
766          *
767          */
768         pull : function ()
769         {
770             // should probably hand error conditions better... 
771             return this.git([ 'pull' ]);
772             
773             
774         },
775         /**
776          * push:
777          * Send local changes to remote repo(s)
778          *
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..
781          *
782          */
783         push : function ()
784         {
785             // should 
786             return this.git([ 'push' ]);
787             
788         },
789         
790     /*
791         public function getWorkingCopy()
792         {
793             require_once 'MTrack/SCM/Git/WorkingCopy.php';
794             return new MTrack_SCM_Git_WorkingCopy($this);
795         }
796     
797       public function getRelatedChanges($revision) // pretty nasty.. could end up with 1000's of changes..
798       {
799         $parents = array();
800         $kids = array();
801     
802         $fp = $this->git('rev-parse', "$revision^");
803         while (($line = fgets($fp)) !== false) {
804           $parents[] = trim($line);
805         }
806     
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)) {
813             $kids[] = $kid;
814           }
815         }
816     
817         return array($parents, $kids);
818       } */
819     
820         
821          
822         
823         git: function(args_in,env)
824         {
825             // convert arguments.
826             
827             //print(JSON.stringify(args_in,null,4));
828             args_in.unshift( {
829                 'git-dir' : this.gitdir,
830                 'no-pager' : true 
831             });
832             var args = ['git' ];
833             
834             if (this.gitdir != this.repopath) {
835                 args_in.unshift( { "work-tree" :  this.repopath } ); 
836             }
837             
838             args_in.forEach(function(arg) { 
839                  if (typeof(arg) == 'string') {
840                     args.push(arg);
841                     return;
842                 }
843                 if (typeof(arg) == 'object') {
844                     for(var k in arg) {
845                         var v = arg[k];
846                         
847                         args.push(k.length != 1 ? ('--' + k) : ('-' + k));
848                         
849                         if (v === true) {
850                             continue;;
851                         }
852                         args.push(v);
853                     }
854                 }
855             });
856             this.lastCmd = args.join(" ");
857             if(this.debug) {
858                
859                 print( args.join(" ")); 
860             }
861             env = env || [];
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 );
866             //}
867             
868             //print(JSON.stringify(args,null,4));  Seed.quit();
869             var sp = new Spawn({
870                 cwd : this.repopath,
871                 args : args,
872                 env : env, // optional
873                 debug: this.debug,
874                 exceptions : false,
875                 async : false
876             });
877             sp.run();
878             
879             if (sp.result) {
880                 print(JSON.stringify(sp.result));
881                 
882                 print(JSON.stringify(sp.args));
883                 print(JSON.stringify(sp.stderr));
884                 
885                 throw {
886                     name    : "RepoSpawnError",
887                     message : sp.stderr,
888                     spawn   : sp
889                 };                
890             }
891              
892             //print("GOT: " + output)
893             // parse output for some commands ?
894             return sp.output;
895         },
896         /**
897          * parseAuthor:
898          * break author string with name and email into parts
899          * @argument {String} author
900          * @returns {Object} with 'name' and 'email' properties.
901          */ 
902         parseAuthor : function(author)
903         {
904             var lp = author.indexOf('<');
905              
906             return {
907                 name : author.substring(0, lp-1).replace(/\s+$/,''),
908                 email : author.substring(0, author.length-1).substring(lp+1)
909             };
910             
911         }
912           
913 });
914
915 Repo.parseURL = function(url)
916 {
917     
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
921     var ret = {
922        scheme : 'ssh://'
923     }
924     url = url.replace(/^[a-z]+:\/\//, function(v) {
925         ret.scheme = v;
926         return '';
927     });
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] : ''; 
933     
934     ret.host = login_host.shift();
935     ret.path = parts.join('/');
936     return ret;
937     
938     
939 }