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