Overview

Packages

  • None
  • Vcs

Classes

  • Horde_Vcs
  • Horde_Vcs_Base
  • Horde_Vcs_Cvs
  • Horde_Vcs_Directory_Base
  • Horde_Vcs_Directory_Cvs
  • Horde_Vcs_Directory_Git
  • Horde_Vcs_Directory_Rcs
  • Horde_Vcs_Directory_Svn
  • Horde_Vcs_File_Base
  • Horde_Vcs_File_Cvs
  • Horde_Vcs_File_Git
  • Horde_Vcs_File_Rcs
  • Horde_Vcs_File_Svn
  • Horde_Vcs_Git
  • Horde_Vcs_Log_Base
  • Horde_Vcs_Log_Cvs
  • Horde_Vcs_Log_Git
  • Horde_Vcs_Log_Rcs
  • Horde_Vcs_Log_Svn
  • Horde_Vcs_Patchset
  • Horde_Vcs_Patchset_Base
  • Horde_Vcs_Patchset_Cvs
  • Horde_Vcs_Patchset_Git
  • Horde_Vcs_Patchset_Svn
  • Horde_Vcs_QuickLog_Base
  • Horde_Vcs_QuickLog_Cvs
  • Horde_Vcs_QuickLog_Git
  • Horde_Vcs_QuickLog_Rcs
  • Horde_Vcs_QuickLog_Svn
  • Horde_Vcs_Rcs
  • Horde_Vcs_Svn
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Horde_Vcs_Git implementation.
  4:  *
  5:  * Constructor args:
  6:  * <pre>
  7:  * 'sourceroot': The source root for this repository
  8:  * 'paths': Hash with the locations of all necessary binaries: 'git'
  9:  * </pre>
 10:  *
 11:  * @TODO find bad output earlier - use proc_open, check stderr or result codes?
 12:  *
 13:  * Copyright 2008-2012 Horde LLC (http://www.horde.org/)
 14:  *
 15:  * See the enclosed file COPYING for license information (LGPL). If you
 16:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 17:  *
 18:  * @author  Chuck Hagenbuch <chuck@horde.org>
 19:  * @author  Michael Slusarz <slusarz@horde.org>
 20:  * @package Vcs
 21:  */
 22: class Horde_Vcs_Git extends Horde_Vcs_Base
 23: {
 24:     /**
 25:      * The current driver.
 26:      *
 27:      * @var string
 28:      */
 29:     protected $_driver = 'Git';
 30: 
 31:     /**
 32:      * Driver features.
 33:      *
 34:      * @var array
 35:      */
 36:     protected $_features = array(
 37:         'deleted'   => false,
 38:         'patchsets' => true,
 39:         'branches'  => true,
 40:         'snapshots' => true);
 41: 
 42:     /**
 43:      * The available diff types.
 44:      *
 45:      * @var array
 46:      */
 47:     protected $_diffTypes = array('unified');
 48: 
 49:     /**
 50:      * The list of branches for the repo.
 51:      *
 52:      * @var array
 53:      */
 54:     protected $_branchlist;
 55: 
 56:     /**
 57:      * The git version
 58:      *
 59:      * @var string
 60:      */
 61:     public $version;
 62: 
 63:     /**
 64:      * @throws Horde_Vcs_Exception
 65:      */
 66:     public function __construct($params = array())
 67:     {
 68:         parent::__construct($params);
 69: 
 70:         if (!is_executable($this->getPath('git'))) {
 71:             throw new Horde_Vcs_Exception('Missing git binary (' . $this->getPath('git') . ' is missing or not executable)');
 72:         }
 73: 
 74:         $v = trim(shell_exec($this->getPath('git') . ' --version'));
 75:         $this->version = preg_replace('/[^\d\.]/', '', $v);
 76: 
 77:         // Try to find the repository if we don't have the exact path. @TODO put
 78:         // this into a builder method/object and cache the results.
 79:         if (!file_exists($this->sourceroot . '/HEAD')) {
 80:             if (file_exists($this->sourceroot . '.git/HEAD')) {
 81:                 $this->_sourceroot .= '.git';
 82:             } elseif (file_exists($this->sourceroot . '/.git/HEAD')) {
 83:                 $this->_sourceroot .= '/.git';
 84:             } else {
 85:                 throw new Horde_Vcs_Exception('Can not find git repository.');
 86:             }
 87:         }
 88:     }
 89: 
 90:     /**
 91:      * TODO
 92:      */
 93:     public function isValidRevision($rev)
 94:     {
 95:         return $rev && preg_match('/^[a-f0-9]+$/i', $rev);
 96:     }
 97: 
 98:     /**
 99:      * TODO
100:      */
101:     public function isFile($where, $branch = null)
102:     {
103:         if (!$branch) {
104:             $branch = $this->getDefaultBranch();
105:         }
106: 
107:         $where = str_replace($this->sourceroot . '/', '', $where);
108:         $command = $this->getCommand() . ' ls-tree ' . escapeshellarg($branch) . ' ' . escapeshellarg($where) . ' 2>&1';
109:         exec($command, $entry, $retval);
110: 
111:         if (!count($entry)) { return false; }
112: 
113:         $data = explode(' ', $entry[0]);
114:         return ($data[1] == 'blob');
115:     }
116: 
117:     /**
118:      * TODO
119:      */
120:     public function getCommand()
121:     {
122:         return escapeshellcmd($this->getPath('git'))
123:             . ' --git-dir=' . escapeshellarg($this->sourceroot);
124:     }
125: 
126:     /**
127:      * Runs a git commands.
128:      *
129:      * Uses proc_open() to properly catch errors and returns a stream with the
130:      * command result. fclose() must be called manually on the returned stream
131:      * and proc_close() on the resource, once the output stream has been
132:      * finished reading.
133:      *
134:      * @param string $args  Any arguments for the git command. Must be escaped.
135:      *
136:      * @return array(resource, stream)  The process resource and the command
137:      *                                  output.
138:      * @throws Horde_Vcs_Exception if command cannot be executed or returns an
139:      *                             error.
140:      */
141:     public function runCommand($args)
142:     {
143:         $cmd = $this->getCommand() . ' ' . $args;
144:         $stream = proc_open(
145:             $cmd,
146:             array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')),
147:             $pipes);
148:         if (!$stream || !is_resource($stream)) {
149:             throw new Horde_Vcs_Exception('Failed to execute git: ' . $cmd);
150:         }
151:         stream_set_blocking($pipes[2], 0);
152:         if ($error = stream_get_contents($pipes[2])) {
153:             fclose($pipes[2]);
154:             proc_close($stream);
155:             throw new Horde_Vcs_Exception($error);
156:         }
157:         fclose($pipes[2]);
158:         return array($stream, $pipes[1]);
159:     }
160: 
161:     /**
162:      * TODO
163:      *
164:      * @throws Horde_Vcs_Exception
165:      */
166:     public function annotate($fileob, $rev)
167:     {
168:         $this->assertValidRevision($rev);
169: 
170:         $command = $this->getCommand() . ' blame -p ' . escapeshellarg($rev) . ' -- ' . escapeshellarg($fileob->getSourcerootPath()) . ' 2>&1';
171:         $pipe = popen($command, 'r');
172:         if (!$pipe) {
173:             throw new Horde_Vcs_Exception('Failed to execute git annotate: ' . $command);
174:         }
175: 
176:         $curr_rev = null;
177:         $db = $lines = array();
178:         $lines_group = $line_num = 0;
179: 
180:         while (!feof($pipe)) {
181:             $line = rtrim(fgets($pipe, 4096));
182: 
183:             if (!$line || ($line[0] == "\t")) {
184:                 if ($lines_group) {
185:                     $lines[] = array(
186:                         'author' => $db[$curr_rev]['author'] . ' ' . $db[$curr_rev]['author-mail'],
187:                         'date' => $db[$curr_rev]['author-time'],
188:                         'line' => $line ? substr($line, 1) : '',
189:                         'lineno' => $line_num++,
190:                         'rev' => $curr_rev
191:                     );
192:                     --$lines_group;
193:                 }
194:             } elseif ($line != 'boundary') {
195:                 if ($lines_group) {
196:                     list($prefix, $linedata) = explode(' ', $line, 2);
197:                     switch ($prefix) {
198:                     case 'author':
199:                     case 'author-mail':
200:                     case 'author-time':
201:                     //case 'author-tz':
202:                         $db[$curr_rev][$prefix] = trim($linedata);
203:                         break;
204:                     }
205:                 } else {
206:                     $curr_line = explode(' ', $line);
207:                     $curr_rev = $curr_line[0];
208:                     $line_num = $curr_line[2];
209:                     $lines_group = isset($curr_line[3]) ? $curr_line[3] : 1;
210:                 }
211:             }
212:         }
213: 
214:         pclose($pipe);
215: 
216:         return $lines;
217:     }
218: 
219:     /**
220:      * Function which returns a file pointing to the head of the requested
221:      * revision of a file.
222:      *
223:      * @param string $fullname  Fully qualified pathname of the desired file
224:      *                          to checkout
225:      * @param string $rev       Revision number to check out
226:      *
227:      * @return resource  A stream pointer to the head of the checkout.
228:      * @throws Horde_Vcs_Exception
229:      */
230:     public function checkout($file, $rev)
231:     {
232:         $this->assertValidRevision($rev);
233: 
234:         $file_ob = $this->getFile($file);
235:         $hash = $file_ob->getHashForRevision($rev);
236:         if ($hash == '0000000000000000000000000000000000000000') {
237:             throw new Horde_Vcs_Exception($file . ' is deleted in commit ' . $rev);
238:         }
239: 
240:         if ($pipe = popen($this->getCommand() . ' cat-file blob ' . $hash . ' 2>&1', VC_WINDOWS ? 'rb' : 'r')) {
241:             return $pipe;
242:         }
243: 
244:         throw new Horde_Vcs_Exception('Couldn\'t perform checkout of the requested file');
245:     }
246: 
247:     /**
248:      * Create a range of revisions between two revision numbers.
249:      *
250:      * @param Horde_Vcs_File_Git $file  The desired file.
251:      * @param string $r1                The initial revision.
252:      * @param string $r2                The ending revision.
253:      *
254:      * @return array  The revision range, or empty if there is no straight
255:      *                line path between the revisions.
256:      */
257:     public function getRevisionRange(Horde_Vcs_File_Base $file, $r1, $r2)
258:     {
259:         $revs = $this->_getRevisionRange($file, $r1, $r2);
260:         return empty($revs)
261:             ? array_reverse($this->_getRevisionRange($file, $r2, $r1))
262:             : $revs;
263:     }
264: 
265:     /**
266:      * TODO
267:      */
268:     protected function _getRevisionRange(Horde_Vcs_File_Git $file, $r1, $r2)
269:     {
270:         $cmd = $this->getCommand() . ' rev-list ' . escapeshellarg($r1 . '..' . $r2) . ' -- ' . escapeshellarg($file->getSourcerootPath());
271:         $revs = array();
272: 
273:         exec($cmd, $revs);
274:         return array_map('trim', $revs);
275:     }
276: 
277:     /**
278:      * Obtain the differences between two revisions of a file.
279:      *
280:      * @param Horde_Vcs_File_Git $file  The desired file.
281:      * @param string $rev1              Original revision number to compare
282:      *                                  from.
283:      * @param string $rev2              New revision number to compare against.
284:      * @param array $opts               The following optional options:
285:      *                                  - 'num': (integer) DEFAULT: 3
286:      *                                  - 'type': (string) DEFAULT: 'unified'
287:      *                                  - 'ws': (boolean) DEFAULT: true
288:      *
289:      * @return string  The diff text.
290:      */
291:     protected function _diff(Horde_Vcs_File_Base $file, $rev1, $rev2, $opts)
292:     {
293:         $diff = array();
294:         $flags = '';
295: 
296:         if (!$opts['ws']) {
297:             $flags .= ' -b -w ';
298:         }
299: 
300:         if (!$rev1) {
301:             $command = $this->getCommand() . ' show --oneline ' . escapeshellarg($rev2) . ' -- ' . escapeshellarg($file->getSourcerootPath()) . ' 2>&1';
302:         } else {
303:             switch ($opts['type']) {
304:             case 'unified':
305:                 $flags .= '--unified=' . escapeshellarg((int)$opts['num']);
306:                 break;
307:             }
308: 
309:             // @TODO: add options for $hr options - however these may not
310:             // be compatible with some diffs.
311:             $command = $this->getCommand() . ' diff -M -C ' . $flags . ' --no-color ' . escapeshellarg($rev1 . '..' . $rev2) . ' -- ' . escapeshellarg($file->getSourcerootPath()) . ' 2>&1';
312:         }
313: 
314:         exec($command, $diff, $retval);
315:         return $diff;
316:     }
317: 
318:     /**
319:      * Returns an abbreviated form of the revision, for display.
320:      *
321:      * @param string $rev  The revision string.
322:      *
323:      * @return string  The abbreviated string.
324:      */
325:     public function abbrev($rev)
326:     {
327:         return substr($rev, 0, 7) . '[...]';
328:     }
329: 
330:     /**
331:      * TODO
332:      */
333:     public function getBranchList()
334:     {
335:         if (!isset($this->_branchlist)) {
336:             $this->_branchlist = array();
337:             exec($this->getCommand() . ' show-ref --heads', $branch_list);
338: 
339:             foreach ($branch_list as $val) {
340:                 $line = explode(' ', trim($val), 2);
341:                 $this->_branchlist[substr($line[1], strrpos($line[1], '/') + 1)] = $line[0];
342:             }
343:         }
344: 
345:         return $this->_branchlist;
346:     }
347: 
348:     /**
349:      * @TODO ?
350:      */
351:     public function getDefaultBranch()
352:     {
353:         return 'master';
354:     }
355: }
356: 
API documentation generated by ApiGen