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         public void loadStatus()
251         {
252                 var r = this.git({ "status" , "--porcelain" });
253                 this.has_local_changes = r.length > 0;
254         
255         }    
256
257     
258     public void loadBranches()
259     {
260         GitBranch.loadBranches(this);
261     }
262      
263     
264     
265     
266     public string branchesToString()
267     {
268         var ret = "";
269                 foreach( var br in this.branches.values) {
270                         if (br.name == "") {
271                                 continue; 
272                         }
273                         ret += ret.length > 0 ? "\n"  : "";
274                         ret += br.name;
275                 
276                 }
277                 return ret;
278         
279     }
280      public static void doMerges(string action, string ticket_id, string commit_message)
281     {
282        GitMonitor.gitmonitor.stop();
283        
284        var commitrevs = "";
285        var sucess = true;
286        foreach(var  repo in GitRepo.singleton().cache.values) {
287                if (repo.activeTicket != null && repo.activeTicket.id == ticket_id) {
288                        var res = repo.doMerge(action,ticket_id, commit_message);
289                        if (!res) {
290                                sucess = false;
291                                continue;
292                        }
293                        commitrevs += commitrevs.length > 0 ? " " : "";
294                        commitrevs += repo.currentBranch.lastrev;
295                }
296        }
297        if (sucess && action == "CLOSE") {
298                RooTicket.singleton().getById(ticket_id).close(commitrevs);
299        }
300        GitMonitor.gitmonitor.start();
301     }
302      
303
304     public bool doMerge(string action, string ticket_id, string commit_message)
305     {
306        // in theory we should check to see if other repo's have got the same branch and merge all them at the same time.
307        // also need to decide which branch we will merge into?
308                    var ret = "";
309                    if (action == "CLOSE" || action == "LEAVE") {
310                                    
311                try {
312                    var oldbranch = this.currentBranch.name;
313                    this.setActiveTicket(null, "master");
314                            string [] cmd = { "merge",   "--squash",  oldbranch };
315                            this.git( cmd );
316                            cmd = { "commit",   "-a" , "-m",  commit_message };
317                            this.git( cmd );
318                            this.push();
319                            this.loadBranches(); // updates lastrev..
320                
321                        var notification = new Notify.Notification(
322                                "Merged branch %s to master".printf(oldbranch),
323                                "",
324                                 "dialog-information"
325                                
326                        );
327
328                        notification.set_timeout(5);
329                        notification.show();   
330                
331                // close ticket..
332                return true; 
333                
334            } catch (Error e) {
335
336                GitMonitor.gitmonitor.pauseError(e.message);
337                return false;
338            }
339            // error~?? -- show the error dialog...
340                    return false;
341        }
342        if (action == "MASTER") {
343                // merge master into ours..
344                        try {
345                        string[] cmd = { "merge",  "master" };
346                        this.git( cmd );
347                        var notification = new Notify.Notification(
348                                        "Merged code from master to %s".printf(this.currentBranch.name),
349                                        "",
350                                         "dialog-information"
351                                        
352                                );
353                                notification.set_timeout(5);
354                                notification.show();   
355                       
356                        return true;
357                        } catch (Error e) {
358                        GitMonitor.gitmonitor.pauseError(e.message);
359                        return false;
360                    }
361            }
362        if (action == "EXIT") {
363                        try {
364                        var oldbranch  = this.currentBranch.name;
365                          this.setActiveTicket(null, "master");
366                        this.loadBranches();
367                        var notification = new Notify.Notification(
368                                        "Left branch %s".printf(oldbranch),
369                                        "",
370                                         "dialog-information"
371                                        
372                                );
373                                notification.set_timeout(5);
374                                notification.show();   
375                        
376                        return true;
377                    } catch (Error e) {
378                        GitMonitor.gitmonitor.pauseError(e.message);
379
380                        return false;                   
381                    }
382                    // error~?? -- show the error dialog...
383
384        }
385        return false;
386     }
387         
388     public void loadActiveTicket()
389     {
390         this.activeTicket = null;
391         if (!FileUtils.test(this.gitdir + "/.gitlive-active-ticket" , FileTest.EXISTS)) {
392                 return;
393         }
394         string ticket_id;
395         FileUtils.get_contents(this.gitdir + "/.gitlive-active-ticket" , out ticket_id);  
396         if (ticket_id.length < 1) {
397                 return;
398                 }
399                 this.activeTicket = RooTicket.singleton().getById(ticket_id.strip());
400         
401         
402     }
403     
404     
405     
406     public bool setActiveTicket(RooTicket? ticket, string branchname)
407     {
408         if (!this.createBranchNamed(branchname)) {
409                 return false;
410                 }
411                 if (ticket != null) {
412                 FileUtils.set_contents(this.gitdir + "/.gitlive-active-ticket" , ticket.id);
413         } else {
414                 FileUtils.remove(this.gitdir + "/.gitlive-active-ticket" );
415         }
416         this.activeTicket = ticket;
417         return true;
418     }
419     
420     public bool createBranchNamed(string branchname)
421     {   
422                 
423
424                      if (this.branches.has_key(branchname)) {
425                         this.switchToExistingBranchNamed(branchname);
426                     
427                     } else {
428                                  this.createNewBranchNamed(branchname); 
429                             
430                     }
431                        var notification = new Notify.Notification(
432                        "Changed to branch %s".printf(branchname),
433                        "",
434                         "dialog-information"
435                        
436                );
437
438                notification.set_timeout(5);
439                notification.show();   
440        
441          
442          this.loadBranches(); // update branch list...
443          //GitMonitor.gitmonitor.runQueue(); // no point - we have hidden the queue..
444          return true;
445     }
446      bool switchToExistingBranchNamed(string branchname)
447      {
448                 var stash = false;
449                                          // this is where it get's tricky...
450                 string files = "";
451                 try {                   
452                                 string[] cmd = { "ls-files" ,  "-m" };                   // list the modified files..
453                                 files = this.git(cmd);
454                                 stash = files.length> 1 ;
455                                 
456                                 
457                                 cmd = { "stash" };                      
458                                 if (stash) { this.git(cmd); }
459                                 
460                                 this.pull();
461                                 
462                                 cmd = { "checkout", branchname  };
463                                 this.git(cmd);
464                   } catch(Error e) {
465                                 GitMonitor.gitmonitor.pauseError(e.message);
466                                 return false;           
467                   }
468                 try {
469                    if (branchname != "master") {
470                        string[] cmd = { "merge", "master"  };
471                             this.git(cmd);
472                             this.push();
473                        
474                     }
475                     
476                 } catch(Error e) {
477                     string[] cmd = { "checkout", "master"  };
478                     this.git(cmd);
479                         GitMonitor.gitmonitor.pauseError(
480                                 "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(
481                                         branchname)
482                                  + e.message
483                         );
484                         return false;           
485                  
486                 }
487                 try {                                   
488                     string[]  cmd = { "stash", "pop"  };
489                     if (stash) { 
490                         this.git(cmd); 
491                         var fl = files.split("\n");
492                         cmd = { "commit", "-m" , "Changed " + string.joinv("",fl) };
493                         foreach(var f in fl) {
494                                 if (f.length < 1) continue;
495                                 cmd += f;
496                         }
497                         this.git(cmd);                              
498                 }
499              
500
501                    
502                 } catch(Error ee) {
503                         GitMonitor.gitmonitor.pauseError(ee.message);
504                         return false;           
505                 }
506        this.push();
507        return true;                             
508                  
509      }
510     
511     
512     
513      bool createNewBranchNamed(string branchname)
514      {
515                 var stash = false;
516                  try {                                  
517                                 string[] cmd = { "ls-files" ,  "-m" };                   // list the modified files..
518                                 var files = this.git(cmd);
519                                 stash = files.length> 1 ;
520                         
521                          cmd = { "checkout", "-b" , branchname  };
522                         this.git(cmd);
523
524                cmd = { "push", "-u" , "origin" ,"HEAD"  };
525                         this.git(cmd);
526                                 if (stash) { 
527
528                                 var fl = files.split("\n");
529                                 cmd = { "commit", "-m" , "Changed " + string.joinv("",fl) };
530                                 foreach(var f in fl) {
531                                         if (f.length < 1) continue;
532                                         cmd += f;
533                                 }
534                                 this.git(cmd);  
535                                 this.push();                        
536                         }
537
538              
539                 } catch(Error ee) {
540                                 GitMonitor.gitmonitor.pauseError(ee.message);
541                                 return false;           
542                         }
543                         return true;
544      
545      }
546     
547     
548     
549     /**
550      * add:
551      * add files to track.
552      *
553      * @argument {Array} files the files to add.
554      */
555     public string add ( Gee.ArrayList<GitMonitorQueue> files ) throws Error, SpawnError
556     {
557         // should really find out if these are untracked files each..
558         // we run multiple versions to make sure that if one failes, it does not ignore the whole lot..
559         // not sure if that is how git works.. but just be certian.
560         var ret = "";
561         for (var i = 0; i < files.size;i++) {
562             var f = files.get(i).vname;
563             try {
564                 string[] cmd = { "add",    f  };
565                 this.git( cmd );
566             } catch (Error e) {
567                 ret += e.message  + "\n";
568             }        
569
570         }
571         return ret;
572     }
573         
574     public bool is_ignore(string fname) throws Error, SpawnError
575     {
576                 if (fname == ".gitignore") {
577                         this.ignore_files.clear();
578                 }
579                 
580                 if (this.ignore_files.has_key(fname)) {
581                         return this.ignore_files.get(fname);
582                 }
583                 
584                 try {
585                         var ret = this.git( { "check-ignore" , fname } );
586                         this.ignore_files.set(fname, ret.length >  0);
587                         return ret.length > 0;
588                 } catch (SpawnError e) {
589                         this.ignore_files.set(fname, false);
590                         return false;
591                 }
592                  
593     } 
594     
595     
596       /**
597      * remove:
598      * remove files to track.
599      *
600      * @argument {Array} files the files to add.
601      */
602     public string remove  ( Gee.ArrayList<GitMonitorQueue> files ) throws Error, SpawnError
603     {
604         // this may fail if files do not exist..
605         // should really find out if these are untracked files each..
606         // we run multiple versions to make sure that if one failes, it does not ignore the whole lot..
607         // not sure if that is how git works.. but just be certian.
608         var ret = "";
609
610         for (var i = 0; i < files.size;i++) {
611             var f = files.get(i).vname;
612             try {
613                 string[] cmd = { "rm",  "-f" ,  f  };
614                 this.git( cmd );
615             } catch (Error e) {
616                 ret += e.message  + "\n";
617             }        
618         }
619
620         return ret;
621
622     }
623     
624     
625     /**
626      * commit:
627      * perform a commit.
628      *
629      * @argument {Object} cfg commit configuration
630      * 
631      * @property {String} name (optional)
632      * @property {String} email (optional)
633      * @property {String} changed (date) (optional)
634      * @property {String} reason (optional)
635      * @property {Array} files - the files that have changed. 
636      * 
637      */
638      
639     public string commit ( string message, Gee.ArrayList<GitMonitorQueue> files  ) throws Error, SpawnError
640     {
641         
642
643         /*
644         var env = [];
645
646         if (typeof(cfg.name) != 'undefined') {
647             args.push( {
648                 'author' : cfg.name + ' <' + cfg.email + '>'
649             });
650             env.push(
651                 "GIT_COMMITTER_NAME" + cfg.name,
652                 "GIT_COMMITTER_EMAIL" + cfg.email
653             );
654         }
655
656         if (typeof(cfg.changed) != 'undefined') {
657             env.push("GIT_AUTHOR_DATE= " + cfg.changed )
658             
659         }
660         */
661         string[] args = { "commit", "-m" };
662         args +=  (message.length > 0  ? message : "Changed" );
663         for (var i = 0; i< files.size ; i++ ) {
664             args += files.get(i).vname; // full path?
665         }
666          
667         return this.git(args);
668     }
669     
670     /**
671      * pull:
672      * Fetch and merge remote repo changes into current branch..
673      *
674      * At present we just need this to update the current working branch..
675      * -- maybe later it will have a few options and do more stuff..
676      *
677      */
678     public string pull () throws Error, SpawnError
679     {
680         // should probably hand error conditions better... 
681         string[] cmd = { "pull" , "--no-edit" };
682         return this.git( cmd );
683
684         
685     }
686     
687     public delegate void GitAsyncCallback (GitRepo repo, int err, string str);
688     public void pull_async(GitAsyncCallback cb) 
689     {
690     
691          string[] cmd = { "pull" , "--no-edit" };
692          this.git_async( cmd , cb);
693          
694     
695     }
696     
697     /**
698      * push:
699      * Send local changes to remote repo(s)
700      *
701      * At present we just need this to push the current branch.
702      * -- maybe later it will have a few options and do more stuff..
703      *
704      */
705     public string push () throws Error, SpawnError
706     {
707         // should 
708         return this.git({ "push"  });
709         
710     }
711     
712     
713     
714      /**
715      * git:
716      * The meaty part.. run spawn.. with git..
717      *
718      *
719      */
720     
721     public string git(string[] args_in ) throws Error, SpawnError
722     {
723         // convert arguments.
724         
725         string[]  args = { "git" };
726         //args +=  "--git-dir";
727         //args +=  this.gitdir;
728         args +=  "--no-pager";
729  
730  
731         //if (this.gitdir != this.repopath) {
732         //    args +=   "--work-tree";
733          //   args += this.repopath; 
734         //}
735         for (var i = 0; i < args_in.length;i++) {
736             args += args_in[i];
737         }            
738
739         //this.lastCmd = args.join(" ");
740         //if(this.debug) {
741             GLib.debug( "CWD=%s",  this.git_working_dir ); 
742             GLib.debug( "cmd: %s", string.joinv (" ", args)); 
743         //}
744
745         string[]   env = {};
746         string  home = "HOME=" + Environment.get_home_dir() ;
747         env +=  home ;
748         // do not need to set gitpath..
749         //if (File.exists(this.repo + '/.git/config')) {
750             //env.push("GITPATH=" + this.repo );
751         //}
752         
753
754         var cfg = new SpawnConfig(this.git_working_dir , args , env);
755         
756
757        // may throw error...
758         var sp = new Spawn(cfg);
759       
760
761         GLib.debug( "GOT: %s" , sp.output);
762         // parse output for some commands ?
763         return sp.output;
764     }
765         
766    unowned GitAsyncCallback git_async_on_callback;
767         public void  git_async( string[] args_in,   GitAsyncCallback cb ) throws Error, SpawnError
768     {
769         // convert arguments.
770        this.git_async_on_callback = cb;
771         string[]  args = { "git" };
772         //args +=  "--git-dir";
773         //args +=  this.gitdir;
774         args +=  "--no-pager";
775  
776  
777         //if (this.gitdir != this.repopath) {
778         //    args +=   "--work-tree";
779          //   args += this.repopath; 
780         //}
781         for (var i = 0; i < args_in.length;i++) {
782             args += args_in[i];
783         }            
784
785         //this.lastCmd = args.join(" ");
786         //if(this.debug) {
787             GLib.debug( "CWD=%s",  this.git_working_dir ); 
788             //print( "cmd: %s\n", string.joinv (" ", args)); 
789         //}
790
791         string[]   env = {};
792         string  home = "HOME=" + Environment.get_home_dir() ;
793         env +=  home ;
794         // do not need to set gitpath..
795         //if (File.exists(this.repo + '/.git/config')) {
796             //env.push("GITPATH=" + this.repo );
797         //}
798         
799
800         var cfg = new SpawnConfig(this.git_working_dir , args , env);
801         cfg.async = true;
802        
803
804        // may throw error...
805         var sp = new Spawn(cfg);
806                 //sp.ref();
807         //this.ref();
808         sp.run(this.git_async_on_complete); 
809          
810     }
811     
812     void git_async_on_complete(int err, string output)
813     {
814                 GLib.debug("GOT %d : %s", err, output);
815                 this.git_async_on_callback(this, err, output);
816 //              this.unref();   
817         //      sp.unref();             
818     
819     
820     }
821     
822  
823          
824     
825  
826     public void update_async(GitAsyncCallback cb) 
827     {
828          string[] cmd = { "fetch" , "--all" };
829          this.git_async( cmd , cb);
830          
831     }
832     
833     
834     static uint update_all_total = 0;
835     static string update_all_after = "";
836      
837     public static void updateAll(string after)
838     {
839                         update_all_after = after;
840                 var tr =  GitRepo.singleton().cache;
841                 
842             
843            update_all_total = tr.size;
844            foreach(var repo  in tr.values) {
845
846                                 
847                 repo.update_rsync(updateAllCallback); 
848                             // do not care if it's already in sycn..
849                     
850                  
851
852             } 
853
854               
855     
856     }
857     public static void  updateAllCallback(GitRepo repo, int err, string res)
858     {
859         repo.loadBranch();
860         repo.loadStatus();
861         
862         update_all_total--;
863         if (update_all_total > 0 ) {
864                 return;
865                 }
866                 switch (update_all_after) {
867                         case "show_clones":
868                                 // show the clones dialog..
869                                 break;
870                         default:
871                                 break;
872                 }
873                 return;
874     }
875     
876     
877     
878 }