1: <?php
2: /**
3: * Version Control generalized library.
4: *
5: * Copyright 2008-2012 Horde LLC (http://www.horde.org/)
6: *
7: * See the enclosed file COPYING for license information (LGPL). If you
8: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
9: *
10: * @package Vcs
11: */
12: abstract class Horde_Vcs_Base
13: {
14: /**
15: * The source root of the repository.
16: *
17: * @var string
18: */
19: public $sourceroot;
20:
21: /**
22: * Hash with the locations of all necessary binaries.
23: *
24: * @var array
25: */
26: protected $_paths = array();
27:
28: /**
29: * Hash caching the parsed users file.
30: *
31: * @var array
32: */
33: protected $_users = array();
34:
35: /**
36: * The current driver.
37: *
38: * @var string
39: */
40: protected $_driver;
41:
42: /**
43: * If caching is desired, a Horde_Cache object.
44: *
45: * @var Horde_Cache
46: */
47: protected $_cache;
48:
49: /**
50: * Driver features.
51: *
52: * @var array
53: */
54: protected $_features = array(
55: 'deleted' => false,
56: 'patchsets' => false,
57: 'branches' => false,
58: 'snapshots' => false);
59:
60: /**
61: * Current cache version.
62: *
63: * @var integer
64: */
65: protected $_cacheVersion = 5;
66:
67: /**
68: * The available diff types.
69: *
70: * @var array
71: */
72: protected $_diffTypes = array('column', 'context', 'ed', 'unified');
73:
74: /**
75: * Constructor.
76: */
77: public function __construct($params = array())
78: {
79: $this->_cache = empty($params['cache']) ? null : $params['cache'];
80: $this->sourceroot = $params['sourceroot'];
81: $this->_paths = $params['paths'];
82: }
83:
84: /**
85: * Does this driver support the given feature?
86: *
87: * @return boolean True if driver supports the given feature.
88: */
89: public function hasFeature($feature)
90: {
91: return !empty($this->_features[$feature]);
92: }
93:
94: /**
95: * Validation function to ensure that a revision string is of the right
96: * form.
97: *
98: * @param mixed $rev The purported revision string.
99: *
100: * @return boolean True if a valid revision string.
101: */
102: abstract public function isValidRevision($rev);
103:
104: /**
105: * Throw an exception if the revision number isn't valid.
106: *
107: * @param mixed $rev The revision number.
108: *
109: * @throws Horde_Vcs_Exception
110: */
111: public function assertValidRevision($rev)
112: {
113: if (!$this->isValidRevision($rev)) {
114: throw new Horde_Vcs_Exception('Invalid revision number "' . $rev . '"');
115: }
116: }
117:
118: /**
119: * Given two revisions, this figures out which one is greater than the
120: * the other.
121: *
122: * @param string $rev1 Revision string.
123: * @param string $rev2 Second revision string.
124: *
125: * @return integer 1 if the first is greater, -1 if the second if greater,
126: * and 0 if they are equal
127: */
128: public function cmp($rev1, $rev2)
129: {
130: return strcasecmp($rev1, $rev2);
131: }
132:
133: /**
134: * TODO
135: */
136: public function isFile($where)
137: {
138: return true;
139: }
140:
141: /**
142: * Create a range of revisions between two revision numbers.
143: *
144: * @param Horde_Vcs_File_Base $file The desired file.
145: * @param string $r1 The initial revision.
146: * @param string $r2 The ending revision.
147: *
148: * @return array The revision range, or empty if there is no straight
149: * line path between the revisions.
150: */
151: public function getRevisionRange(Horde_Vcs_File_Base $file, $r1, $r2)
152: {
153: return array();
154: }
155:
156: /**
157: * Obtain the differences between two revisions of a file.
158: *
159: * @param Horde_Vcs_File_Base $file The desired file.
160: * @param string $rev1 Original revision number to compare
161: * from.
162: * @param string $rev2 New revision number to compare against.
163: * @param array $opts The following optional options:
164: * - 'human': (boolean) DEFAULT: false
165: * - 'num': (integer) DEFAULT: 3
166: * - 'type': (string) DEFAULT: 'unified'
167: * - 'ws': (boolean) DEFAULT: true
168: *
169: * @return string The diff string.
170: * @throws Horde_Vcs_Exception
171: */
172: public function diff(Horde_Vcs_File_Base $file, $rev1, $rev2,
173: $opts = array())
174: {
175: $opts = array_merge(array(
176: 'num' => 3,
177: 'type' => 'unified',
178: 'ws' => true
179: ), $opts);
180:
181: if ($rev1) {
182: $this->assertValidRevision($rev1);
183: }
184:
185: $diff = $this->_diff($file, $rev1, $rev2, $opts);
186: return empty($opts['human'])
187: ? $diff
188: : $this->_humanReadableDiff($diff);
189: }
190:
191: /**
192: * Obtain the differences between two revisions of a file.
193: *
194: * @param Horde_Vcs_File_Base $file The desired file.
195: * @param string $rev1 Original revision number to compare
196: * from.
197: * @param string $rev2 New revision number to compare against.
198: * @param array $opts The following optional options:
199: * - 'num': (integer) DEFAULT: 3
200: * - 'type': (string) DEFAULT: 'unified'
201: * - 'ws': (boolean) DEFAULT: true
202: *
203: * @return string|boolean False on failure, or a string containing the
204: * diff on success.
205: */
206: protected function _diff(Horde_Vcs_File_Base $file, $rev1, $rev2, $opts)
207: {
208: return false;
209: }
210:
211: /**
212: * Obtain a tree containing information about the changes between
213: * two revisions.
214: *
215: * @param array $raw An array of lines of the raw unified diff,
216: * normally obtained through Horde_Vcs_Diff::get().
217: *
218: * @return array @TODO
219: */
220: protected function _humanReadableDiff($raw)
221: {
222: $ret = array();
223:
224: /* Hold the left and right columns of lines for change
225: * blocks. */
226: $cols = array(array(), array());
227: $state = 'empty';
228:
229: /* Iterate through every line of the diff. */
230: foreach ($raw as $line) {
231: /* Look for a header which indicates the start of a diff
232: * chunk. */
233: if (preg_match('/^@@ \-([0-9]+).*\+([0-9]+).*@@(.*)/', $line, $regs)) {
234: /* Push any previous header information to the return
235: * stack. */
236: if (isset($data)) {
237: $ret[] = $data;
238: }
239: $data = array('type' => 'header', 'oldline' => $regs[1],
240: 'newline' => $regs[2], 'contents'> array());
241: $data['function'] = isset($regs[3]) ? $regs[3] : '';
242: $state = 'dump';
243: } elseif ($state != 'empty') {
244: /* We are in a chunk, so split out the action (+/-)
245: * and the line. */
246: preg_match('/^([\+\- ])(.*)/', $line, $regs);
247: if (count($regs) > 2) {
248: $action = $regs[1];
249: $content = $regs[2];
250: } else {
251: $action = ' ';
252: $content = '';
253: }
254:
255: if ($action == '+') {
256: /* This is just an addition line. */
257: if ($state == 'dump' || $state == 'add') {
258: /* Start adding to the addition stack. */
259: $cols[0][] = $content;
260: $state = 'add';
261: } else {
262: /* This is inside a change block, so start
263: * accumulating lines. */
264: $state = 'change';
265: $cols[1][] = $content;
266: }
267: } elseif ($action == '-') {
268: /* This is a removal line. */
269: $state = 'remove';
270: $cols[0][] = $content;
271: } else {
272: /* An empty block with no action. */
273: switch ($state) {
274: case 'add':
275: $data['contents'][] = array('type' => 'add', 'lines' => $cols[0]);
276: break;
277:
278: case 'remove':
279: /* We have some removal lines pending in our
280: * stack, so flush them. */
281: $data['contents'][] = array('type' => 'remove', 'lines' => $cols[0]);
282: break;
283:
284: case 'change':
285: /* We have both remove and addition lines, so
286: * this is a change block. */
287: $data['contents'][] = array('type' => 'change', 'old' => $cols[0], 'new' => $cols[1]);
288: break;
289: }
290: $cols = array(array(), array());
291: $data['contents'][] = array('type' => 'empty', 'line' => $content);
292: $state = 'dump';
293: }
294: }
295: }
296:
297: /* Just flush any remaining entries in the columns stack. */
298: switch ($state) {
299: case 'add':
300: $data['contents'][] = array('type' => 'add', 'lines' => $cols[0]);
301: break;
302:
303: case 'remove':
304: /* We have some removal lines pending in our stack, so
305: * flush them. */
306: $data['contents'][] = array('type' => 'remove', 'lines' => $cols[0]);
307: break;
308:
309: case 'change':
310: /* We have both remove and addition lines, so this is a
311: * change block. */
312: $data['contents'][] = array('type' => 'change', 'old' => $cols[0], 'new' => $cols[1]);
313: break;
314: }
315:
316: if (isset($data)) {
317: $ret[] = $data;
318: }
319:
320: return $ret;
321: }
322:
323: /**
324: * Return the list of available diff types.
325: *
326: * @return array The list of available diff types for use with get().
327: */
328: public function availableDiffTypes()
329: {
330: return $this->_diffTypes;
331: }
332:
333: /**
334: * Returns the location of the specified binary.
335: *
336: * @param string $binary An external program name.
337: *
338: * @return boolean|string The location of the external program or false
339: * if it wasn't specified.
340: */
341: public function getPath($binary)
342: {
343: if (isset($this->_paths[$binary])) {
344: return $this->_paths[$binary];
345: }
346:
347: return false;
348: }
349:
350: /**
351: * Parse the users file, if present in the sourceroot, and return
352: * a hash containing the requisite information, keyed on the
353: * username, and with the 'desc', 'name', and 'mail' values inside.
354: *
355: * @return array User data.
356: * @throws Horde_Vcs_Exception
357: */
358: public function getUsers($usersfile)
359: {
360: /* Check that we haven't already parsed users. */
361: if (isset($this->_users[$usersfile])) {
362: return $this->_users[$usersfile];
363: }
364:
365: if (!@is_file($usersfile) ||
366: !($fl = @fopen($usersfile, VC_WINDOWS ? 'rb' : 'r'))) {
367: throw new Horde_Vcs_Exception('Invalid users file: ' . $usersfile);
368: }
369:
370: /* Discard the first line, since it'll be the header info. */
371: fgets($fl, 4096);
372:
373: /* Parse the rest of the lines into a hash, keyed on username. */
374: $users = array();
375: while ($line = fgets($fl, 4096)) {
376: if (!preg_match('/^\s*$/', $line) &&
377: preg_match('/^(\w+)\s+(.+)\s+([\w\.\-\_]+@[\w\.\-\_]+)\s+(.*)$/', $line, $regs)) {
378: $users[$regs[1]] = array(
379: 'name' => trim($regs[2]),
380: 'mail' => trim($regs[3]),
381: 'desc' => trim($regs[4])
382: );
383: }
384: }
385:
386: $this->_users[$usersfile] = $users;
387:
388: return $users;
389: }
390:
391: /**
392: * Returns a directory object.
393: *
394: * @param string $where Path to the directory.
395: * @param array $opts Any additional options (depends on driver):
396: * - 'showattic': (boolean) Parse any Attic/
397: * sub-directory contents too.
398: * - 'rev': (string) Generate directory list for
399: * a certain branch or revision.
400: *
401: * @return Horde_Vcs_Directory_Base A directory object.
402: */
403: public function getDirectory($where, $opts = array())
404: {
405: $class = 'Horde_Vcs_Directory_' . $this->_driver;
406: return new $class($this, $where, $opts);
407: }
408:
409: /**
410: * Function which returns a file pointing to the head of the requested
411: * revision of a file.
412: *
413: * @param string $fullname Fully qualified pathname of the desired file
414: * to checkout.
415: * @param string $rev Revision number to check out.
416: *
417: * @return resource A stream pointer to the head of the checkout.
418: */
419: public function checkout($fullname, $rev)
420: {
421: return null;
422: }
423:
424: /**
425: * TODO
426: *
427: * $opts:
428: * 'branch' - (string)
429: */
430: public function getFile($filename, $opts = array())
431: {
432: $class = 'Horde_Vcs_File_' . $this->_driver;
433:
434: ksort($opts);
435: $cacheId = implode('|', array($class, $this->sourceroot, $filename, serialize($opts), $this->_cacheVersion));
436: $fetchedFromCache = false;
437:
438: if (!empty($this->_cache)) {
439: // TODO: Can't use filemtime() - Git bare repos contain no files
440: if (file_exists($filename)) {
441: $ctime = time() - filemtime($filename);
442: } else {
443: $ctime = 60;
444: }
445: if ($this->_cache->exists($cacheId, $ctime)) {
446: $ob = unserialize($this->_cache->get($cacheId, $ctime));
447: $fetchedFromCache = true;
448: }
449: }
450:
451: if (empty($ob) || !$ob) {
452: $ob = new $class($filename, $opts);
453:
454: }
455: $ob->setRepository($this);
456:
457: if (!empty($this->_cache) && !$fetchedFromCache) {
458: $this->_cache->set($cacheId, serialize($ob));
459: }
460:
461: return $ob;
462: }
463:
464: /**
465: * TODO
466: *
467: * @param array $opts Options:
468: * <pre>
469: * 'file' - (string) TODO
470: * 'range' - (array) TODO
471: * </pre>
472: *
473: * @return Horde_Vcs_Patchset Patchset object.
474: */
475: public function getPatchset($opts = array())
476: {
477: $class = 'Horde_Vcs_Patchset_' . $this->_driver;
478:
479: if (!is_array($opts)) { $opts = array(); }
480: ksort($opts);
481: $cacheId = implode('|', array($class, $this->sourceroot, serialize($opts), $this->_cacheVersion));
482:
483: if (!empty($this->_cache)) {
484: if (isset($opts['file']) && file_exists($opts['file'])) {
485: $ctime = time() - filemtime($opts['file']);
486: } else {
487: $ctime = 60;
488: }
489:
490: if ($this->_cache->exists($cacheId, $ctime)) {
491: return unserialize($this->_cache->get($cacheId, $ctime));
492: }
493: }
494:
495: $ob = new $class($this, $opts);
496:
497: if (!empty($this->_cache)) {
498: $this->_cache->set($cacheId, serialize($ob));
499: }
500:
501: return $ob;
502: }
503:
504: /**
505: * TODO
506: */
507: public function annotate($fileob, $rev)
508: {
509: return array();
510: }
511:
512: /**
513: * Returns an abbreviated form of the revision, for display.
514: *
515: * @param string $rev The revision string.
516: *
517: * @return string The abbreviated string.
518: */
519: public function abbrev($rev)
520: {
521: return $rev;
522: }
523:
524: /**
525: * @TODO ?
526: */
527: public function getDefaultBranch()
528: {
529: return 'HEAD';
530: }
531: }
532: