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