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($id = null) {
19         
20         parent::__construct($id);
21         if ($id !== 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 = MTrackRepo::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 {
240                 $args[] = "--since=$limit";
241             }
242         }
243         $args[] = "--no-color";
244         //$args[] = "--name-status";
245         $args[] = "--raw";
246         $args[] = "--no-abbrev";
247         $args[] = "--numstat";
248         $args[] = "--date=rfc";
249         
250         
251         //echo '<PRE>';print_r($args);echo '</PRE>';
252         $path = ltrim($path, '/');
253         //print_R(array($args, '--' ,$path));
254         $fp = $this->git('log', $args, '--', $path);
255
256         $commits = array();
257         $commit = null;
258         while (true) {
259           $line = fgets($fp);
260           if ($line === false) {
261             if ($commit !== null) {
262               $commits[] = $commit;
263             }
264             break;
265           }
266           if (preg_match("/^commit/", $line)) {
267             if ($commit !== null) {
268               $commits[] = $commit;
269             }
270             $commit = $line;
271             continue;
272           }
273           $commit .= $line;
274         }
275         require_once 'MTrack/SCM/Git/Event.php';
276         foreach ($commits as $commit) {
277             $res[] = MTrack_SCM_Git_Event::newFromCommit($commit, $this);
278         }
279         $fp = null;
280         
281         return $res;
282     }
283
284     public function diff($path, $from = null, $to = null)
285     {
286         require_once 'MTrack/SCMFile.php';
287         
288         if ($path instanceof MTrackSCMFile) {
289             if ($from === null) {
290                 $from = $path->rev;
291             }
292             $path = $path->name;
293             
294         }
295         // if it's a file event.. we are even lucker..
296         if ($path instanceof MTrackSCMFileEvent) {
297             return $this->git('log', '--max-count=1', '--format=format:', '--patch', $from, '--', $path->name);
298             
299         }
300         
301         
302         if ($to !== null) {
303           return $this->git('diff', "$from..$to", '--', $path);
304         }
305         return $this->git('diff', "$from^..$from", '--', $path);
306       }
307
308   public function getWorkingCopy()
309   {
310     require_once 'MTrack/SCM/Git/WorkingCopy.php';
311     return new MTrack_SCM_Git_WorkingCopy($this);
312   }
313
314   public function getRelatedChanges($revision) // pretty nasty.. could end up with 1000's of changes..
315   {
316     $parents = array();
317     $kids = array();
318
319     $fp = $this->git('rev-parse', "$revision^");
320     while (($line = fgets($fp)) !== false) {
321       $parents[] = trim($line);
322     }
323
324     // Ugh!: http://stackoverflow.com/questions/1761825/referencing-the-child-of-a-commit-in-git
325     $fp = $this->git('rev-list', '--all', '--parents');
326     while (($line = fgets($fp)) !== false) {
327       $hashes = preg_split("/\s+/", $line);
328       $kid = array_shift($hashes);
329       if (in_array($revision, $hashes)) {
330         $kids[] = $kid;
331       }
332     }
333
334     return array($parents, $kids);
335   }
336
337   function git()
338   {
339     $args = func_get_args();
340     $a = array(
341       "--git-dir=$this->gitdir"
342     );
343     
344     
345     
346     if ($this->gitdir != $this->repopath) {
347     //    print_r(array($this->gitdir , $this->repopath));
348         
349       //$a[] = "--work-tree=$this->repopath";
350     }
351     foreach ($args as $arg) {
352         if (is_array($arg)) {
353             $a = array_merge($a, $arg);
354             continue;
355         }
356         $a[] = $arg;
357     }
358     var_dump('git ' . join (' ' , $a));
359     //print_r($a);
360     return MTrackSCM::run('git', 'read', $a);
361   }
362 }
363
364