Indefero

Indefero Git Source Tree

Root/src/IDF/Scm/Git.php

1<?php
2/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3/*
4# ***** BEGIN LICENSE BLOCK *****
5# This file is part of InDefero, an open source project management application.
6# Copyright (C) 2008 Céondo Ltd and contributors.
7#
8# InDefero is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# InDefero is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21#
22# ***** END LICENSE BLOCK ***** */
23
24/**
25 * Git utils.
26 *
27 */
28class IDF_Scm_Git
29{
30 public $repo = '';
31 public $mediumtree_fmt = 'commit %H%nAuthor: %an <%ae>%nTree: %T%nDate: %ai%n%n%s%n%n%b';
32
33 public function __construct($repo)
34 {
35 $this->repo = $repo;
36 }
37
38 /**
39 * Given the string describing the author from the log find the
40 * author in the database.
41 *
42 * @param string Author
43 * @return mixed Pluf_User or null
44 */
45 public function findAuthor($author)
46 {
47 // We extract the email.
48 $match = array();
49 if (!preg_match('/<(.*)>/', $author, $match)) {
50 return null;
51 }
52 $sql = new Pluf_SQL('email=%s', array($match[1]));
53 $users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
54 return ($users->count() > 0) ? $users[0] : null;
55 }
56
57
58 /**
59 * Returns the URL of the git daemon.
60 *
61 * @param IDF_Project
62 * @return string URL
63 */
64 public static function getRemoteAccessUrl($project)
65 {
66 return sprintf(Pluf::f('git_remote_url'), $project->shortname);
67 }
68
69 /**
70 * Returns the URL for SSH access
71 *
72 * @param IDF_Project
73 * @return string URL
74 */
75 public static function getWriteRemoteAccessUrl($project)
76 {
77 return sprintf(Pluf::f('git_write_remote_url'), $project->shortname);
78 }
79
80 /**
81 * Returns this object correctly initialized for the project.
82 *
83 * @param IDF_Project
84 * @return IDF_Scm_Git
85 */
86 public static function factory($project)
87 {
88 $rep = sprintf(Pluf::f('git_repositories'), $project->shortname);
89 return new IDF_Scm_Git($rep);
90 }
91
92 /**
93 * Test a given object hash.
94 *
95 * @param string Object hash.
96 * @param null to be svn client compatible
97 * @return mixed false if not valid or 'blob', 'tree', 'commit'
98 */
99 public function testHash($hash, $dummy=null)
100 {
101 $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' cat-file -t %s',
102 escapeshellarg($this->repo),
103 escapeshellarg($hash));
104 $ret = 0; $out = array();
105 IDF_Scm::exec($cmd, $out, $ret);
106 if ($ret != 0) return false;
107 return trim($out[0]);
108 }
109
110 /**
111 * Given a commit hash returns an array of files in it.
112 *
113 * A file is a class with the following properties:
114 *
115 * 'perm', 'type', 'size', 'hash', 'file'
116 *
117 * @param string Commit ('HEAD')
118 * @param string Base folder ('')
119 * @return array
120 */
121 public function filesAtCommit($commit='HEAD', $folder='')
122 {
123 // now we grab the info about this commit including its tree.
124 $co = $this->getCommit($commit);
125 if ($folder) {
126 // As we are limiting to a given folder, we need to find
127 // the tree corresponding to this folder.
128 $found = false;
129 foreach ($this->getTreeInfo($co->tree, true, $folder) as $file) {
130 if ($file->type == 'tree' and $file->file == $folder) {
131 $found = true;
132 $tree = $file->hash;
133 break;
134 }
135 }
136 if (!$found) {
137 throw new Exception(sprintf(__('Folder %1$s not found in commit %2$s.'), $folder, $commit));
138 }
139 } else {
140 $tree = $co->tree;
141 }
142 $res = array();
143 // get the raw log corresponding to this commit to find the
144 // origin of each file.
145 $rawlog = array();
146 $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log --raw --abbrev=40 --pretty=oneline -5000 %s',
147 escapeshellarg($this->repo), escapeshellarg($commit));
148 IDF_Scm::exec($cmd, $rawlog);
149 // We reverse the log to be able to use a fixed efficient
150 // regex without back tracking.
151 $rawlog = implode("\n", array_reverse($rawlog));
152 foreach ($this->getTreeInfo($tree, false) as $file) {
153 // Now we grab the files in the current tree with as much
154 // information as possible.
155 $matches = array();
156 if ($file->type == 'blob' and preg_match('/^\:\d{6} \d{6} [0-9a-f]{40} '.$file->hash.' .*^([0-9a-f]{40})/msU',
157 $rawlog, $matches)) {
158 $fc = $this->getCommit($matches[1]);
159 $file->date = $fc->date;
160 $file->log = $fc->title;
161 $file->author = $fc->author;
162 } else if ($file->type == 'blob') {
163 $file->date = $co->date;
164 $file->log = '----';
165 $file->author = 'Unknown';
166 }
167 $file->fullpath = ($folder) ? $folder.'/'.$file->file : $file->file;
168 $res[] = $file;
169 }
170 return $res;
171 }
172
173 /**
174 * Get the tree info.
175 *
176 * @param string Tree hash
177 * @param bool Do we recurse in subtrees (true)
178 * @return array Array of file information.
179 */
180 public function getTreeInfo($tree, $recurse=true, $folder='')
181 {
182 if ('tree' != $this->testHash($tree)) {
183 throw new Exception(sprintf(__('Not a valid tree: %s.'), $tree));
184 }
185 $cmd_tmpl = 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' ls-tree%s -t -l %s %s';
186 $cmd = sprintf($cmd_tmpl,
187 escapeshellarg($this->repo),
188 ($recurse) ? ' -r' : '',
189 escapeshellarg($tree), escapeshellarg($folder));
190 $out = array();
191 $res = array();
192 IDF_Scm::exec($cmd, $out);
193 foreach ($out as $line) {
194 list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY);
195 $res[] = (object) array('perm' => $perm, 'type' => $type,
196 'size' => $size, 'hash' => $hash,
197 'file' => $file);
198 }
199 return $res;
200 }
201
202
203 /**
204 * Get the file info.
205 *
206 * @param string File
207 * @param string Commit ('HEAD')
208 * @return false Information
209 */
210 public function getFileInfo($totest, $commit='HEAD')
211 {
212 $cmd_tmpl = 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' ls-tree -r -t -l %s';
213 $cmd = sprintf($cmd_tmpl,
214 escapeshellarg($this->repo),
215 escapeshellarg($commit));
216 $out = array();
217 IDF_Scm::exec($cmd, $out);
218 foreach ($out as $line) {
219 list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY);
220 if ($totest == $file) {
221 return (object) array('perm' => $perm, 'type' => $type,
222 'size' => $size, 'hash' => $hash,
223 'file' => $file);
224 }
225 }
226 return false;
227 }
228
229 /**
230 * Get a blob.
231 *
232 * @param string request_file_info
233 * @param null to be svn client compatible
234 * @return string Raw blob
235 */
236 public function getBlob($request_file_info, $dummy=null)
237 {
238 return shell_exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').
239 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' cat-file blob %s',
240 escapeshellarg($this->repo),
241 escapeshellarg($request_file_info->hash)));
242 }
243
244 /**
245 * Get the branches.
246 *
247 * @return array Branches.
248 */
249 public function getBranches()
250 {
251 $out = array();
252 IDF_Scm::exec(sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' branch',
253 escapeshellarg($this->repo)), $out);
254 $res = array();
255 foreach ($out as $b) {
256 $res[] = substr($b, 2);
257 }
258 return $res;
259 }
260
261 /**
262 * Get commit details.
263 *
264 * @param string Commit ('HEAD').
265 * @param bool Get commit diff (false).
266 * @return array Changes.
267 */
268 public function getCommit($commit='HEAD', $getdiff=false)
269 {
270 if ($getdiff) {
271 $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' show --date=iso --pretty=format:%s %s',
272 escapeshellarg($this->repo),
273 "'".$this->mediumtree_fmt."'",
274 escapeshellarg($commit));
275 } else {
276 $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log -1 --date=iso --pretty=format:%s %s',
277 escapeshellarg($this->repo),
278 "'".$this->mediumtree_fmt."'",
279 escapeshellarg($commit));
280 }
281 $out = array();
282 IDF_Scm::exec($cmd, $out);
283 $log = array();
284 $change = array();
285 $inchange = false;
286 foreach ($out as $line) {
287 if (!$inchange and 0 === strpos($line, 'diff --git a')) {
288 $inchange = true;
289 }
290 if ($inchange) {
291 $change[] = $line;
292 } else {
293 $log[] = $line;
294 }
295 }
296 $out = self::parseLog($log, 4);
297 $out[0]->changes = implode("\n", $change);
298 return $out[0];
299 }
300
301 /**
302 * Check if a commit is big.
303 *
304 * @param string Commit ('HEAD')
305 * @return bool The commit is big
306 */
307 public function isCommitLarge($commit='HEAD')
308 {
309 $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log --numstat -1 --pretty=format:%s %s',
310 escapeshellarg($this->repo),
311 "'commit %H%n'",
312 escapeshellarg($commit));
313 $out = array();
314 IDF_Scm::exec($cmd, $out);
315 $affected = count($out) - 2;
316 $added = 0;
317 $removed = 0;
318 $c=0;
319 foreach ($out as $line) {
320 $c++;
321 if ($c < 3) {
322 continue;
323 }
324 list($a, $r, $f) = preg_split("/[\s]+/", $line, 3, PREG_SPLIT_NO_EMPTY);
325 $added+=$a;
326 $removed+=$r;
327 }
328 return ($affected > 100 or ($added + $removed) > 20000);
329 }
330
331 /**
332 * Get latest changes.
333 *
334 * @param string Commit ('HEAD').
335 * @param int Number of changes (10).
336 * @return array Changes.
337 */
338 public function getChangeLog($commit='HEAD', $n=10)
339 {
340 if ($n === null) $n = '';
341 else $n = ' -'.$n;
342 $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log%s --date=iso --pretty=format:\'%s\' %s',
343 escapeshellarg($this->repo), $n, $this->mediumtree_fmt,
344 escapeshellarg($commit));
345 $out = array();
346 IDF_Scm::exec($cmd, $out);
347 return self::parseLog($out, 4);
348 }
349
350 /**
351 * Parse the log lines of a --pretty=medium log output.
352 *
353 * @param array Lines.
354 * @param int Number of lines in the headers (3)
355 * @return array Change log.
356 */
357 public static function parseLog($lines, $hdrs=3)
358 {
359 $res = array();
360 $c = array();
361 $hdrs += 2;
362 $inheads = true;
363 $next_is_title = false;
364 foreach ($lines as $line) {
365 if (preg_match('/^commit (\w{40})$/', $line)) {
366 if (count($c) > 0) {
367 $c['full_message'] = trim($c['full_message']);
368 $res[] = (object) $c;
369 }
370 $c = array();
371 $c['commit'] = trim(substr($line, 7, 40));
372 $c['full_message'] = '';
373 $inheads = true;
374 $next_is_title = false;
375 continue;
376 }
377 if ($next_is_title) {
378 $c['title'] = trim($line);
379 $next_is_title = false;
380 continue;
381 }
382 $match = array();
383 if ($inheads and preg_match('/(\S+)\s*:\s*(.*)/', $line, $match)) {
384 $match[1] = strtolower($match[1]);
385 $c[$match[1]] = trim($match[2]);
386 if ($match[1] == 'date') {
387 $c['date'] = gmdate('Y-m-d H:i:s', strtotime($match[2]));
388 }
389 continue;
390 }
391 if ($inheads and !$next_is_title and $line == '') {
392 $next_is_title = true;
393 $inheads = false;
394 }
395 if (!$inheads) {
396 $c['full_message'] .= trim($line)."\n";
397 continue;
398 }
399 }
400 $c['full_message'] = trim($c['full_message']);
401 $res[] = (object) $c;
402 return $res;
403 }
404
405 /**
406 * Generate the command to create a zip archive at a given commit.
407 *
408 * @param string Commit
409 * @param string Prefix ('git-repo-dump')
410 * @return string Command
411 */
412 public function getArchiveCommand($commit, $prefix='git-repo-dump/')
413 {
414 return sprintf(Pluf::f('idf_exec_cmd_prefix', '').
415 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' archive --format=zip --prefix=%s %s',
416 escapeshellarg($this->repo),
417 escapeshellarg($prefix),
418 escapeshellarg($commit));
419 }
420
421}

Archive Download this file