MTrack/SCM/Git/Repo.php
[web.mtrack] / MTrack / SCM / Git / Repo.php
1 <?php
2 require_once 'MTrack/Repo.php';
3
4
5 class MTrack_SCM_Git_Repo extends MTrack_Repo 
6 {
7   protected $branches = null;
8   protected $tags = null;
9   public $gitdir = null;
10
11   public $debug = false;
12
13   public function getSCMMetaData() {
14     return array(
15       'name' => 'Git',
16       'tools' => array('git'),
17     );
18   }
19
20     function __construct($ar = null) {
21         
22         parent::__construct($ar);
23         if ($ar !== null) {
24           /* transparently handle bare vs. non bare repos */
25             $this->gitdir = $this->repopath;
26             if (is_dir("$this->repopath/.git")) {
27                 $this->gitdir .= "/.git";
28             }
29         }
30     }
31
32   function getServerURL() {
33     $url = parent::getServerURL();
34     if ($url) return $url;
35     $url = MTrackConfig::get('repos', 'serverurl');
36     if ($url) {
37       return "$url:" . $this->getBrowseRootName();
38     }
39     return null;
40   }
41
42   public function reconcileRepoSettings(MTrackSCM $r = null) {
43     if ($r == null) {
44       $r = $this;
45     }
46
47     if (!is_dir($r->repopath)) {
48       $userdata = MTrackAuth::getUserData(MTrackAuth::whoami());
49       $who = $userdata['email'];
50       putenv("GIT_AUTHOR_NAME=$who");
51       putenv("GIT_AUTHOR_EMAIL=$who");
52
53       if ($r->clonedfrom) {
54         $S = MTrack_Repo::loadById($r->clonedfrom);
55
56         $stm = MTrackSCM::run('git', 'read',
57             array('clone', '--bare', $S->repopath, $r->repopath));
58         $out = stream_get_contents($stm);
59         if (pclose($stm)) {
60           throw new Exception("git init failed: $out");
61         }
62
63       } else {
64         /* a little peculiar, but bear with it.
65          * We need to have a bare repo so that git doesn't mess around
66          * trying to deal with a checkout in the repo dir.
67          * So we need to create two repos; one bare, one not bare.
68          * We populate the non-bare repo with a dummy file just to have
69          * something to commit, then push non-bare -> bare, and remove non-bare.
70          */
71
72         $stm = MTrackSCM::run('git', 'read',
73             array('init', '--bare', $r->repopath));
74         $out = stream_get_contents($stm);
75         if (pclose($stm)) {
76           throw new Exception("git init failed: $out");
77         }
78
79         $alt = "$r->repopath.MTRACKINIT";
80
81         $stm = MTrackSCM::run('git', 'read',
82             array('init', $alt));
83         $out = stream_get_contents($stm);
84         if (pclose($stm)) {
85           throw new Exception("git init failed: $out");
86         }
87
88         $dir = getcwd();
89         chdir($alt);
90
91         file_put_contents("$alt/.gitignore", "#\n");
92         $stm = MTrackSCM::run('git', 'read',
93             array('add', '.gitignore'));
94         $out = stream_get_contents($stm);
95         if (pclose($stm)) {
96           throw new Exception("git add .gitignore failed: $out");
97         }
98         $stm = MTrackSCM::run('git', 'read',
99             array('commit', '-a', '-m', 'init'));
100         $out = stream_get_contents($stm);
101         if (pclose($stm)) {
102           throw new Exception("git commit failed: $out");
103         }
104         $stm = MTrackSCM::run('git', 'read',
105             array('push', $r->repopath, 'master'));
106         $out = stream_get_contents($stm);
107         if (pclose($stm)) {
108           throw new Exception("git push failed: $out");
109         }
110         chdir($dir);
111         system("rm -rf $alt");
112       }
113
114       $php = MTrackConfig::get('tools', 'php');
115       $hook = realpath(dirname(__FILE__) . '/../../bin/git-commit-hook');
116       $conffile = realpath(MTrackConfig::getLocation());
117       foreach (array('pre', 'post') as $step) {
118         $script = <<<HOOK
119 #!/bin/sh
120 exec $php $hook $step $conffile
121
122 HOOK;
123         $target = "$r->repopath/hooks/$step-receive";
124         if (file_put_contents("$target.mtrack", $script)) {
125           chmod("$target.mtrack", 0755);
126           rename("$target.mtrack", $target);
127         }
128       }
129     }
130
131     system("chmod -R 02777 $r->repopath");
132   }
133
134   function canFork() {
135     return true;
136   }
137
138
139   public function getBranches()
140   {
141     if ($this->branches !== null) {
142          return $this->branches;
143     }
144     $this->branches = array();
145     $fp = $this->git('branch', '--no-color', '--verbose');
146     while ($line = fgets($fp)) {
147       // * master 61e7e7d oneliner
148       $line = substr($line, 2);
149       list($branch, $rev) = preg_split('/\s+/', $line);
150       $this->branches[$branch] = $rev;
151     }
152     $fp = null;
153     return $this->branches;
154   }
155
156   public function getTags()
157   {
158     if ($this->tags !== null) {
159       return $this->tags;
160     }
161     $this->tags = array();
162     $fp = $this->git('tag');
163     while ($line = fgets($fp)) {
164       $line = trim($line);
165       $this->tags[$line] = $line;
166     }
167     $fp = null;
168     return $this->tags;
169   }
170
171   public function readdir($path, $object = null, $ident = null)
172   {
173     $res = array();
174
175     if ($object === null) {
176       $object = 'branch';
177       $ident = 'master';
178     }
179     $rev = $this->resolveRevision(null, $object, $ident);
180
181     if (strlen($path)) {
182       $path = rtrim($path, '/') . '/';
183     }
184
185     $fp = $this->git('ls-tree', $rev, $path);
186
187     $dirs = array();
188     require_once 'MTrack/SCM/Git/File.php';
189     while ($line = fgets($fp)) {
190         // blob = file, tree = dir..
191         list($mode, $type, $hash, $name) = preg_split("/\s+/", $line);
192         //echo '<PRE>';echo $line ."\n</PRE>";
193         $res[] = new MTrack_SCM_Git_File($this, "$name", $rev, $type == 'tree', $hash);
194     }
195     return $res;
196   }
197
198   public function file($path, $object = null, $ident = null)
199   {
200     if ($object == null) {
201       $branches = $this->getBranches();
202       if (isset($branches['master'])) {
203         $object = 'branch';
204         $ident = 'master';
205       } else {
206         // fresh/empty repo
207         return null;
208       }
209     }
210     $rev = $this->resolveRevision(null, $object, $ident);
211     require_once 'MTrack/SCM/Git/File.php';
212     return new MTrack_SCM_Git_File($this, $path, $rev);
213   }
214     
215     /**
216      * 
217      * @param  string path (can be empty - eg. '')
218      * @param  {number|date} limit  how many to fetch
219      *
220      *      -- prefered args:
221      *          
222      * @param  {string} object = eg. rev|tag|branch  (use 'rev' here and ident=HASH to retrieve a speific revision
223      * @param  {string} ident = 
224      *
225      * Example:
226      *   - fetch on revision?:  '.',1,'rev','xxxxxxxxxx'
227      *
228      *
229      * range... object ='rev' ident ='master..release'
230      * 
231      */
232     public function history($path, $limit = null, $object = null, $ident = null)
233     {
234         
235         
236         $res = array();
237         
238         $args = array();
239         if ($object !== null) {
240             $rev = $this->resolveRevision(null, $object, $ident); // from scm...
241             $args[] = "$rev";
242         } else {
243             $args[] = "master";
244         }
245      
246         if ($limit !== null) {
247             if (is_int($limit)) {
248                 $args[] = "--max-count=$limit";
249             } else if (is_array($limit) && isset($limit[0]) && isset($limit[1])) {
250                 
251                 $args[] = "--skip={$limit[0]} --max-count={$limit[1]}";
252                 
253                 
254             /// oh what a horible hack.. - bad api design here.
255             } else if (is_array($limit) ) {
256                 foreach($limit as $k=>$v) {
257                      
258                     $args[] = ($k == '-') ? $v : ('--'. $k .'='. $v);
259                     
260                 }
261                  
262             } else if (strpos($limit,'..') !== false ) {
263                  $args[] = $limit;
264             } else {   
265                 $args[] = "--since=$limit";
266             }
267         }
268         $args[] = "--no-color";
269         //$args[] = "--name-status";
270         $args[] = "--raw";
271         $args[] = "--no-abbrev";
272         $args[] = "--numstat";
273         $args[] = "--date=rfc";
274         
275         
276         //echo '<PRE>';print_r($args);echo '</PRE>';
277         $path = ltrim($path, '/');
278         
279         
280         
281         //   print_R(array($args, '--' ,$path));exit;
282         $fp = $this->git('log', $args, '--', $path);
283
284         $commits = array();
285         $commit = null;
286         while (true) {
287           $line = fgets($fp);
288           if ($line === false) {
289             if ($commit !== null) {
290               $commits[] = $commit;
291             }
292             break;
293           }
294           if (preg_match("/^commit/", $line)) {
295             if ($commit !== null) {
296               $commits[] = $commit;
297             }
298             $commit = $line;
299             continue;
300           }
301           $commit .= $line;
302         }
303         require_once 'MTrack/SCM/Git/Event.php';
304         foreach ($commits as $commit) {
305             $res[] = MTrack_SCM_Git_Event::newFromCommit($commit, $this);
306         }
307         $fp = null;
308         
309         return $res;
310     }
311
312     public function diff($path, $from = null, $to = null)
313     {
314         require_once 'MTrack/SCMFile.php';
315         
316         if ($path instanceof MTrackSCMFile) {
317             if ($from === null) {
318                 $from = $path->rev;
319             }
320             $path = $path->name;
321             
322         }
323         // if it's a file event.. we are even lucker..
324         if ($path instanceof MTrackSCMFileEvent) {
325             return $this->git('log', '--max-count=1', '--format=format:', '--patch', $from, '--', $path->name);
326             
327         }
328         
329         
330         if ($to !== null) {
331           return $this->git('diff', "$from..$to", '--', $path);
332         }
333         return $this->git('diff', "$from^..$from", '--', $path);
334       }
335
336     public function getWorkingCopy()
337     {
338         require_once 'MTrack/SCM/Git/WorkingCopy.php';
339         return new MTrack_SCM_Git_WorkingCopy($this);
340     }
341
342   public function getRelatedChanges($revision) // pretty nasty.. could end up with 1000's of changes..
343   {
344     $parents = array();
345     $kids = array();
346
347     $fp = $this->git('rev-parse', "$revision^");
348     while (($line = fgets($fp)) !== false) {
349       $parents[] = trim($line);
350     }
351
352     // Ugh!: http://stackoverflow.com/questions/1761825/referencing-the-child-of-a-commit-in-git
353     $fp = $this->git('rev-list', '--all', '--parents');
354     while (($line = fgets($fp)) !== false) {
355       $hashes = preg_split("/\s+/", $line);
356       $kid = array_shift($hashes);
357       if (in_array($revision, $hashes)) {
358         $kids[] = $kid;
359       }
360     }
361
362     return array($parents, $kids);
363   }
364
365     function git()
366     {
367            $args = func_get_args();
368           $a = array(
369             "--git-dir=$this->gitdir"
370           );
371           
372           
373           
374           if ($this->gitdir != $this->repopath) {
375           //    print_r(array($this->gitdir , $this->repopath));
376               
377             //$a[] = "--work-tree=$this->repopath";
378           }
379           foreach ($args as $arg) {
380               if (is_array($arg)) {
381                   $a = array_merge($a, $arg);
382                   continue;
383               }
384               $a[] = $arg;
385           }
386           if ($this->debug) { 
387               var_dump('git ' . join (' ' , $a));
388             //  die("oops");
389           }
390           echo "git " . implode(" " , $a) . "\n";
391           return MTrackSCM::run('git', 'read', $a);
392     }
393   
394    
395     function commitLogToEvent($str) {
396         require_once 'MTrack/SCM/Git/Event.php';
397         return  MTrack_SCM_Git_Event::newFromCommit($str,$this);
398     }
399
400   
401 }
402
403