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