1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
22: class Horde_Vcs_Git extends Horde_Vcs_Base
23: {
24: 25: 26: 27: 28:
29: protected $_driver = 'Git';
30:
31: 32: 33: 34: 35:
36: protected $_features = array(
37: 'deleted' => false,
38: 'patchsets' => true,
39: 'branches' => true,
40: 'snapshots' => true);
41:
42: 43: 44: 45: 46:
47: protected $_diffTypes = array('unified');
48:
49: 50: 51: 52: 53:
54: protected $_branchlist;
55:
56: 57: 58: 59: 60:
61: public $version;
62:
63: 64: 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:
78:
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: 92:
93: public function isValidRevision($rev)
94: {
95: return $rev && preg_match('/^[a-f0-9]+$/i', $rev);
96: }
97:
98: 99: 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: 119:
120: public function getCommand()
121: {
122: return escapeshellcmd($this->getPath('git'))
123: . ' --git-dir=' . escapeshellarg($this->sourceroot);
124: }
125:
126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 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: 163: 164: 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:
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: 221: 222: 223: 224: 225: 226: 227: 228: 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: 249: 250: 251: 252: 253: 254: 255: 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: 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: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 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:
310:
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: 320: 321: 322: 323: 324:
325: public function abbrev($rev)
326: {
327: return substr($rev, 0, 7) . '[...]';
328: }
329:
330: 331: 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: 350:
351: public function getDefaultBranch()
352: {
353: return 'master';
354: }
355: }
356: