GitRepo.vala
[gitlive] / GitRepo.vala
1
2 /**
3  * @class Scm.Git.Repo
4  *
5  * @extends Scm.Repo
6  * 
7  *
8  *
9  */
10 static GitRepo  _GitRepo; 
11  
12 public class GitRepo : Object
13 {
14      
15     public Gee.ArrayList<GitMonitorQueue> cmds;
16
17     public string name;
18     public string gitdir;
19     public string git_working_dir;
20     public bool debug = false;
21     public bool has_local_changes = false;
22     
23     
24     public Gee.HashMap<string,bool> ignore_files;
25     public GitBranch currentBranch;
26     public Gee.HashMap<string,GitBranch> branches; // accessed in GitBranch..
27         public RooTicket? activeTicket;
28     public  Gee.HashMap<string,GitRepo> cache;
29     
30     
31     
32         public static GitRepo singleton()
33     {
34         if (_GitRepo == null) {
35             _GitRepo = new GitRepo.single();
36             _GitRepo.cache = new Gee.HashMap<string,GitRepo>();
37         }
38         return _GitRepo;
39     }
40  
41     /**
42     * index of.. matching gitpath..
43     */
44     public static int indexOf( Array<GitRepo> repos, string gitpath) {
45         // make a fake object to compare against..
46         var test_repo = GitRepo.get(gitpath);
47         
48         for(var i =0; i < repos.length; i++) {
49             if (repos.index(i).gitdir == test_repo.gitdir) {
50                 return i;
51             }
52         }
53         return -1;
54     
55     }
56     
57
58     
59     
60     
61     public static   Array<GitRepo> list()
62     {
63
64         //if (GitRepo.list_cache !=  null) {
65         //    unowned  Array<GitRepo>    ret = GitRepo.list_cache;
66          //   return ret;
67         //}
68         var cache = GitRepo.singleton().cache;
69         var list_cache = new Array<GitRepo>();
70         
71         var dir = Environment.get_home_dir() + "/gitlive";
72         
73         var f = File.new_for_path(dir);
74         FileEnumerator file_enum;
75         try {
76             file_enum = f.enumerate_children(
77                 FileAttribute.STANDARD_DISPLAY_NAME + ","+ 
78                 FileAttribute.STANDARD_TYPE,
79                 FileQueryInfoFlags.NONE,
80                 null);
81         } catch (Error e) {
82             
83             return list_cache;
84             
85         }
86         
87         FileInfo next_file; 
88         
89         while (true) {
90             
91             try {
92                 next_file = file_enum.next_file(null);
93                 if (next_file == null) {
94                     break;
95                 }
96                 
97             } catch (Error e) {
98                 GLib.debug("Error: %s",e.message);
99                 break;
100             }
101          
102             //print("got a file " + next_file.sudo () + '?=' + Gio.FileType.DIRECTORY);
103             
104             if (next_file.get_file_type() !=  FileType.DIRECTORY) {
105                 next_file = null;
106                 continue;
107             }
108             
109             if (next_file.get_file_type() ==  FileType.SYMBOLIC_LINK) {
110                 next_file = null;
111                 continue;
112             }
113             
114             if (next_file.get_display_name()[0] == '.') {
115                 next_file = null;
116                 continue;
117             }
118             var sp = dir+"/"+next_file.get_display_name();
119            
120             var gitdir = dir + "/" + next_file.get_display_name() + "/.git";
121             
122             if (!FileUtils.test(gitdir, FileTest.IS_DIR)) {
123                 continue;
124             }
125             
126                 var rep =  GitRepo.get(  sp );
127                 list_cache.append_val(rep);             
128             
129         }
130     
131         return list_cache;
132         
133          
134           
135         }
136         
137         public static GitRepo get(string path) 
138         {
139                 var cache = GitRepo.singleton().cache;
140                 if (cache.has_key(path)) {
141                         return cache.get(path);
142                 }
143                 return new GitRepo(path);
144         }
145         
146     private GitRepo.single() {
147                 // used to create the signleton
148         }
149     /**
150      * constructor:
151      * 
152      * @param {Object} cfg - Configuration
153      *     (basically repopath is currently only critical one.)
154      *
155      */
156      
157     private GitRepo(string path) {
158         // cal parent?
159         this.name =   File.new_for_path(path).get_basename();
160         this.ignore_files = new Gee.HashMap<string,bool>();
161         
162         this.git_working_dir = path;
163         this.gitdir = path + "/.git";
164         if (!FileUtils.test(this.gitdir , FileTest.IS_DIR)) {
165             this.gitdir = path; // naked...
166         }
167         this.cmds = new  Gee.ArrayList<GitMonitorQueue> ();
168         
169                 var cache = GitRepo.singleton().cache;
170         //Repo.superclass.constructor.call(this,cfg);
171                 if ( !cache.has_key(path) ) {
172                         cache.set( path, this);
173         }
174         this.loadBranches();
175         this.loadActiveTicket();
176         this.loadStatus();
177     } 
178     
179     public bool is_wip_branch()
180     {
181         return this.currentBranch.name.has_prefix("wip_");
182                 
183     }
184     
185     public bool is_autocommit ()
186     {
187         return !FileUtils.test(this.gitdir + "/.gitlive-disable-autocommit" , FileTest.EXISTS);
188     }
189     public void set_autocommit(bool val)
190     {
191
192                 var cur = this.is_autocommit();
193                 GLib.debug("SET auto commit : %s <= %s", val ? "ON" : "OFF",  cur  ? "ON" : "OFF");
194                 if (cur == val) {
195                         return; // no change..
196                 }
197                 if (!val) {
198                         FileUtils.set_contents(this.gitdir + "/.gitlive-disable-autocommit" , "x");
199                 } else {
200                         // it exists...
201                         FileUtils.remove(this.gitdir + "/.gitlive-disable-autocommit" ); 
202                 }
203     
204     }
205     
206     public bool is_auto_branch ()
207     {
208         return FileUtils.test(this.gitdir + "/.gitlive-enable-auto-branch" , FileTest.EXISTS);
209     }
210     
211     public void set_auto_branch(bool val)
212     {
213
214                 var cur = this.is_auto_branch();
215                 GLib.debug("SET auto branch : %s <= %s", val ? "ON" : "OFF",  cur  ? "ON" : "OFF");
216                 
217                 if (cur == val) {
218                         return; // no change..
219                 }
220                 if (val) {
221                         FileUtils.set_contents(this.gitdir + "/.gitlive-enable-auto-branch" , "x");
222                 } else {
223                         // it exists...
224                         FileUtils.remove(this.gitdir + "/.gitlive-enable-auto-branch" ); 
225                 }
226     
227     }
228     public bool is_autopush ()
229     {
230         return !FileUtils.test(this.gitdir + "/.gitlive-disable-autopush" , FileTest.EXISTS);
231     }
232     public void set_autopush(bool val)
233     {
234
235                 var cur = this.is_autopush();
236                 GLib.debug("SET auto push : %s <= %s", val ? "ON" : "OFF",  cur  ? "ON" : "OFF");
237                 if (cur == val) {
238                         return; // no change..
239                 }
240                 if (!val) {
241                         FileUtils.set_contents(this.gitdir + "/.gitlive-disable-autopush" , "");
242                 } else {
243                         // it exists...
244                         FileUtils.remove(this.gitdir + "/.gitlive-disable-autopush" ); 
245                 }
246     
247     }
248     
249     
250     
251
252     
253     public void loadBranches()
254     {
255         GitBranch.loadBranches(this);
256     }
257      
258     
259     
260     
261     public string branchesToString()
262     {
263         var ret = "";
264                 foreach( var br in this.branches.values) {
265                         if (br.name == "") {
266                                 continue; 
267                         }
268                         ret += ret.length > 0 ? "\n"  : "";
269                         ret += br.name;
270                 
271                 }
272                 return ret;
273         
274     }
275      public static void doMerges(string action, string ticket_id, string commit_message)
276     {
277        GitMonitor.gitmonitor.stop();
278        
279        var commitrevs = "";
280        var sucess = true;
281        foreach(var  repo in GitRepo.singleton().cache.values) {
282                if (repo.activeTicket != null && repo.activeTicket.id == ticket_id) {
283                        var res = repo.doMerge(action,ticket_id, commit_message);
284                        if (!res) {
285                                sucess = false;
286                                continue;
287                        }
288                        commitrevs += commitrevs.length > 0 ? " " : "";
289                        commitrevs += repo.currentBranch.lastrev;
290                }
291        }
292        if (sucess && action == "CLOSE") {
293                RooTicket.singleton().getById(ticket_id).close(commitrevs);
294        }
295        GitMonitor.gitmonitor.start();
296     }
297      
298
299     public bool doMerge(string action, string ticket_id, string commit_message)
300     {
301        // in theory we should check to see if other repo's have got the same branch and merge all them at the same time.
302        // also need to decide which branch we will merge into?
303                    var ret = "";
304                    if (action == "CLOSE" || action == "LEAVE") {
305                                    
306                try {
307                    var oldbranch = this.currentBranch.name;
308                    this.setActiveTicket(null, "master");
309                            string [] cmd = { "merge",   "--squash",  oldbranch };
310                            this.git( cmd );
311                            cmd = { "commit",   "-a" , "-m",  commit_message };
312                            this.git( cmd );
313                            this.push();
314                            this.loadBranches(); // updates lastrev..
315                
316                        var notification = new Notify.Notification(
317                                "Merged branch %s to master".printf(oldbranch),
318                                "",
319                                 "dialog-information"
320                                
321                        );
322
323                        notification.set_timeout(5);
324                        notification.show();   
325                
326                // close ticket..
327                return true; 
328                
329            } catch (Error e) {
330
331                GitMonitor.gitmonitor.pauseError(e.message);
332                return false;
333            }
334            // error~?? -- show the error dialog...
335                    return false;
336        }
337        if (action == "MASTER") {
338                // merge master into ours..
339                        try {
340                        string[] cmd = { "merge",  "master" };
341                        this.git( cmd );
342                        var notification = new Notify.Notification(
343                                        "Merged code from master to %s".printf(this.currentBranch.name),
344                                        "",
345                                         "dialog-information"
346                                        
347                                );
348                                notification.set_timeout(5);
349                                notification.show();   
350                       
351                        return true;
352                        } catch (Error e) {
353                        GitMonitor.gitmonitor.pauseError(e.message);
354                        return false;
355                    }
356            }
357        if (action == "EXIT") {
358                        try {
359                        var oldbranch  = this.currentBranch.name;
360                          this.setActiveTicket(null, "master");
361                        this.loadBranches();
362                        var notification = new Notify.Notification(
363                                        "Left branch %s".printf(oldbranch),
364                                        "",
365                                         "dialog-information"
366                                        
367                                );
368                                notification.set_timeout(5);
369                                notification.show();   
370                        
371                        return true;
372                    } catch (Error e) {
373                        GitMonitor.gitmonitor.pauseError(e.message);
374
375                        return false;                   
376                    }
377                    // error~?? -- show the error dialog...
378
379        }
380        return false;
381     }
382         
383     public void loadActiveTicket()
384     {
385         this.activeTicket = null;
386         if (!FileUtils.test(this.gitdir + "/.gitlive-active-ticket" , FileTest.EXISTS)) {
387                 return;
388         }
389         string ticket_id;
390         FileUtils.get_contents(this.gitdir + "/.gitlive-active-ticket" , out ticket_id);  
391         if (ticket_id.length < 1) {
392                 return;
393                 }
394                 this.activeTicket = RooTicket.singleton().getById(ticket_id.strip());
395         
396         
397     }
398     
399     
400     
401     public bool setActiveTicket(RooTicket? ticket, string branchname)
402     {
403         if (!this.createBranchNamed(branchname)) {
404                 return false;
405                 }
406                 if (ticket != null) {
407                 FileUtils.set_contents(this.gitdir + "/.gitlive-active-ticket" , ticket.id);
408         } else {
409                 FileUtils.remove(this.gitdir + "/.gitlive-active-ticket" );
410         }
411         this.activeTicket = ticket;
412         return true;
413     }
414     
415     public bool createBranchNamed(string branchname)
416     {   
417                 
418
419                      if (this.branches.has_key(branchname)) {
420                         this.switchToExistingBranchNamed(branchname);
421                     
422                     } else {
423                                  this.createNewBranchNamed(branchname); 
424                             
425                     }
426                        var notification = new Notify.Notification(
427                        "Changed to branch %s".printf(branchname),
428                        "",
429                         "dialog-information"
430                        
431                );
432
433                notification.set_timeout(5);
434                notification.show();   
435        
436          
437          this.loadBranches(); // update branch list...
438          //GitMonitor.gitmonitor.runQueue(); // no point - we have hidden the queue..
439          return true;
440     }
441      bool switchToExistingBranchNamed(string branchname)
442      {
443                 var stash = false;
444                                          // this is where it get's tricky...
445                 string files = "";
446                 try {                   
447                                 string[] cmd = { "ls-files" ,  "-m" };                   // list the modified files..
448                                 files = this.git(cmd);
449                                 stash = files.length> 1 ;
450                                 
451                                 
452                                 cmd = { "stash" };                      
453                                 if (stash) { this.git(cmd); }
454                                 
455                                 this.pull();
456                                 
457                                 cmd = { "checkout", branchname  };
458                                 this.git(cmd);
459                   } catch(Error e) {
460                                 GitMonitor.gitmonitor.pauseError(e.message);
461                                 return false;           
462                   }
463                 try {
464                    if (branchname != "master") {
465                        string[] cmd = { "merge", "master"  };
466                             this.git(cmd);
467                             this.push();
468                        
469                     }
470                     
471                 } catch(Error e) {
472                     string[] cmd = { "checkout", "master"  };
473                     this.git(cmd);
474                         GitMonitor.gitmonitor.pauseError(
475                                 "Use\n\na) git checkout %s\nb) git mergetool\nc) git commit\nd) git push\n d) stash pop \ne) start gitlive again\n".printf(
476                                         branchname)
477                                  + e.message
478                         );
479                         return false;           
480                  
481                 }
482                 try {                                   
483                     string[]  cmd = { "stash", "pop"  };
484                     if (stash) { 
485                         this.git(cmd); 
486                         var fl = files.split("\n");
487                         cmd = { "commit", "-m" , "Changed " + string.joinv("",fl) };
488                         foreach(var f in fl) {
489                                 if (f.length < 1) continue;
490                                 cmd += f;
491                         }
492                         this.git(cmd);                              
493                 }
494              
495
496                    
497                 } catch(Error ee) {
498                         GitMonitor.gitmonitor.pauseError(ee.message);
499                         return false;           
500                 }
501        this.push();
502        return true;                             
503                  
504      }
505     
506     
507     
508      bool createNewBranchNamed(string branchname)
509      {
510                 var stash = false;
511                  try {                                  
512                                 string[] cmd = { "ls-files" ,  "-m" };                   // list the modified files..
513                                 var files = this.git(cmd);
514                                 stash = files.length> 1 ;
515                         
516                          cmd = { "checkout", "-b" , branchname  };
517                         this.git(cmd);
518
519                cmd = { "push", "-u" , "origin" ,"HEAD"  };
520                         this.git(cmd);
521                                 if (stash) { 
522
523                                 var fl = files.split("\n");
524                                 cmd = { "commit", "-m" , "Changed " + string.joinv("",fl) };
525                                 foreach(var f in fl) {
526                                         if (f.length < 1) continue;
527                                         cmd += f;
528                                 }
529                                 this.git(cmd);  
530                                 this.push();                        
531                         }
532
533              
534                 } catch(Error ee) {
535                                 GitMonitor.gitmonitor.pauseError(ee.message);
536                                 return false;           
537                         }
538                         return true;
539      
540      }
541     
542     
543     
544     /**
545      * add:
546      * add files to track.
547      *
548      * @argument {Array} files the files to add.
549      */
550     public string add ( Gee.ArrayList<GitMonitorQueue> files ) throws Error, SpawnError
551     {
552         // should really find out if these are untracked files each..
553         // we run multiple versions to make sure that if one failes, it does not ignore the whole lot..
554         // not sure if that is how git works.. but just be certian.
555         var ret = "";
556         for (var i = 0; i < files.size;i++) {
557             var f = files.get(i).vname;
558             try {
559                 string[] cmd = { "add",    f  };
560                 this.git( cmd );
561             } catch (Error e) {
562                 ret += e.message  + "\n";
563             }        
564
565         }
566         return ret;
567     }
568         
569     public bool is_ignore(string fname) throws Error, SpawnError
570     {
571                 if (fname == ".gitignore") {
572                         this.ignore_files.clear();
573                 }
574                 
575                 if (this.ignore_files.has_key(fname)) {
576                         return this.ignore_files.get(fname);
577                 }
578                 
579                 try {
580                         var ret = this.git( { "check-ignore" , fname } );
581                         this.ignore_files.set(fname, ret.length >  0);
582                         return ret.length > 0;
583                 } catch (SpawnError e) {
584                         this.ignore_files.set(fname, false);
585                         return false;
586                 }
587                  
588     } 
589     
590     
591       /**
592      * remove:
593      * remove files to track.
594      *
595      * @argument {Array} files the files to add.
596      */
597     public string remove  ( Gee.ArrayList<GitMonitorQueue> files ) throws Error, SpawnError
598     {
599         // this may fail if files do not exist..
600         // should really find out if these are untracked files each..
601         // we run multiple versions to make sure that if one failes, it does not ignore the whole lot..
602         // not sure if that is how git works.. but just be certian.
603         var ret = "";
604
605         for (var i = 0; i < files.size;i++) {
606             var f = files.get(i).vname;
607             try {
608                 string[] cmd = { "rm",  "-f" ,  f  };
609                 this.git( cmd );
610             } catch (Error e) {
611                 ret += e.message  + "\n";
612             }        
613         }
614
615         return ret;
616
617     }
618     
619     
620     /**
621      * commit:
622      * perform a commit.
623      *
624      * @argument {Object} cfg commit configuration
625      * 
626      * @property {String} name (optional)
627      * @property {String} email (optional)
628      * @property {String} changed (date) (optional)
629      * @property {String} reason (optional)
630      * @property {Array} files - the files that have changed. 
631      * 
632      */
633      
634     public string commit ( string message, Gee.ArrayList<GitMonitorQueue> files  ) throws Error, SpawnError
635     {
636         
637
638         /*
639         var env = [];
640
641         if (typeof(cfg.name) != 'undefined') {
642             args.push( {
643                 'author' : cfg.name + ' <' + cfg.email + '>'
644             });
645             env.push(
646                 "GIT_COMMITTER_NAME" + cfg.name,
647                 "GIT_COMMITTER_EMAIL" + cfg.email
648             );
649         }
650
651         if (typeof(cfg.changed) != 'undefined') {
652             env.push("GIT_AUTHOR_DATE= " + cfg.changed )
653             
654         }
655         */
656         string[] args = { "commit", "-m" };
657         args +=  (message.length > 0  ? message : "Changed" );
658         for (var i = 0; i< files.size ; i++ ) {
659             args += files.get(i).vname; // full path?
660         }
661          
662         return this.git(args);
663     }
664     
665     /**
666      * pull:
667      * Fetch and merge remote repo changes into current branch..
668      *
669      * At present we just need this to update the current working branch..
670      * -- maybe later it will have a few options and do more stuff..
671      *
672      */
673     public string pull () throws Error, SpawnError
674     {
675         // should probably hand error conditions better... 
676         string[] cmd = { "pull" , "--no-edit" };
677         return this.git( cmd );
678
679         
680     }
681     
682     public delegate void GitAsyncCallback (GitRepo repo, int err, string str);
683     public void pull_async(GitAsyncCallback cb) 
684     {
685     
686         string[] cmd = { "pull" , "--no-edit" };
687          this.git_async( cmd , cb);
688          
689     
690     }
691     
692     /**
693      * push:
694      * Send local changes to remote repo(s)
695      *
696      * At present we just need this to push the current branch.
697      * -- maybe later it will have a few options and do more stuff..
698      *
699      */
700     public string push () throws Error, SpawnError
701     {
702         // should 
703         return this.git({ "push"  });
704         
705     }
706     
707     
708     
709      /**
710      * git:
711      * The meaty part.. run spawn.. with git..
712      *
713      *
714      */
715     
716     public string git(string[] args_in ) throws Error, SpawnError
717     {
718         // convert arguments.
719         
720         string[]  args = { "git" };
721         //args +=  "--git-dir";
722         //args +=  this.gitdir;
723         args +=  "--no-pager";
724  
725  
726         //if (this.gitdir != this.repopath) {
727         //    args +=   "--work-tree";
728          //   args += this.repopath; 
729         //}
730         for (var i = 0; i < args_in.length;i++) {
731             args += args_in[i];
732         }            
733
734         //this.lastCmd = args.join(" ");
735         //if(this.debug) {
736             GLib.debug( "CWD=%s",  this.git_working_dir ); 
737             GLib.debug( "cmd: %s", string.joinv (" ", args)); 
738         //}
739
740         string[]   env = {};
741         string  home = "HOME=" + Environment.get_home_dir() ;
742         env +=  home ;
743         // do not need to set gitpath..
744         //if (File.exists(this.repo + '/.git/config')) {
745             //env.push("GITPATH=" + this.repo );
746         //}
747         
748
749         var cfg = new SpawnConfig(this.git_working_dir , args , env);
750         
751
752        // may throw error...
753         var sp = new Spawn(cfg);
754       
755
756         GLib.debug( "GOT: %s" , sp.output);
757         // parse output for some commands ?
758         return sp.output;
759     }
760         
761    unowned GitAsyncCallback git_async_on_callback;
762         public void  git_async( string[] args_in,   GitAsyncCallback cb ) throws Error, SpawnError
763     {
764         // convert arguments.
765        this.git_async_on_callback = cb;
766         string[]  args = { "git" };
767         //args +=  "--git-dir";
768         //args +=  this.gitdir;
769         args +=  "--no-pager";
770  
771  
772         //if (this.gitdir != this.repopath) {
773         //    args +=   "--work-tree";
774          //   args += this.repopath; 
775         //}
776         for (var i = 0; i < args_in.length;i++) {
777             args += args_in[i];
778         }            
779
780         //this.lastCmd = args.join(" ");
781         //if(this.debug) {
782             GLib.debug( "CWD=%s",  this.git_working_dir ); 
783             //print( "cmd: %s\n", string.joinv (" ", args)); 
784         //}
785
786         string[]   env = {};
787         string  home = "HOME=" + Environment.get_home_dir() ;
788         env +=  home ;
789         // do not need to set gitpath..
790         //if (File.exists(this.repo + '/.git/config')) {
791             //env.push("GITPATH=" + this.repo );
792         //}
793         
794
795         var cfg = new SpawnConfig(this.git_working_dir , args , env);
796         cfg.async = true;
797        
798
799        // may throw error...
800         var sp = new Spawn(cfg);
801                 //sp.ref();
802         //this.ref();
803         sp.run(this.git_async_on_complete); 
804          
805     }
806     
807     void git_async_on_complete(int err, string output)
808     {
809                 GLib.debug("GOT %d : %s", err, output);
810                 this.git_async_on_callback(this, err, output);
811 //              this.unref();   
812         //      sp.unref();             
813     
814     
815     }
816     
817 }