Break out Horde/Vcs classes into autoloadable files
authorMichael J. Rubinsky <mrubinsk@horde.org>
Fri, 15 Oct 2010 18:05:02 +0000 (14:05 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Fri, 15 Oct 2010 18:05:02 +0000 (14:05 -0400)
16 files changed:
framework/Vcs/lib/Horde/Vcs.php
framework/Vcs/lib/Horde/Vcs/Cvs.php
framework/Vcs/lib/Horde/Vcs/Directory.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/Directory/Cvs.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/Directory/Git.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/File.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/File/Cvs.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/File/Git.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/Git.php
framework/Vcs/lib/Horde/Vcs/Log.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/Log/Cvs.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/Log/Git.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/Patchset.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/Patchset/Cvs.php [new file with mode: 0644]
framework/Vcs/lib/Horde/Vcs/Patchset/Git.php [new file with mode: 0644]
framework/Vcs/package.xml

index 5acf612..de63c3a 100644 (file)
@@ -641,681 +641,3 @@ class Horde_Vcs
     }
 
 }
-
-/**
- * Horde_Vcs_Cvs directory class.
- *
- * @package Horde_Vcs
- */
-abstract class Horde_Vcs_Directory
-{
-    /**
-     * @var Horde_Vcs
-     */
-    protected $_rep;
-
-    /**
-     * @var string
-     */
-    protected $_dirName;
-
-    /**
-     * @var array
-     */
-    protected $_files = array();
-
-    /**
-     * @var array
-     */
-    protected $_atticFiles = array();
-
-    /**
-     * @var array
-     */
-    protected $_mergedFiles = array();
-
-    /**
-     * @var string
-     */
-    protected $_dirs = array();
-
-    /**
-     * @var string
-     */
-    protected $_moduleName;
-
-    /**
-     * Create a Directory object to store information about the files in a
-     * single directory in the repository.
-     *
-     * @param Horde_Vcs $rep  The Repository object this directory is part of.
-     * @param string $dn      Path to the directory.
-     * @param array $opts     TODO
-     */
-    public function __construct($rep, $dn, $opts = array())
-    {
-        $this->_rep = $rep;
-        $this->_moduleName = $dn;
-        $this->_dirName = '/' . $dn;
-    }
-
-    /**
-     * Return fully qualified pathname to this directory with no trailing /.
-     *
-     * @return string Pathname of this directory.
-     */
-    public function queryDir()
-    {
-        return $this->_dirName;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryDirList()
-    {
-        return $this->_dirs;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryFileList($showattic = false)
-    {
-        return ($showattic && isset($this->_mergedFiles))
-            ? $this->_mergedFiles
-            : $this->_files;
-    }
-
-    /**
-     * Sort the contents of the directory in a given fashion and
-     * order.
-     *
-     * @param integer $how  Of the form Horde_Vcs::SORT_[*] where * can be:
-     *                      NONE, NAME, AGE, REV for sorting by name, age or
-     *                      revision.
-     * @param integer $dir  Of the form Horde_Vcs::SORT_[*] where * can be:
-     *                      ASCENDING, DESCENDING for the order of the sort.
-     */
-    public function applySort($how = Horde_Vcs::SORT_NONE,
-                              $dir = Horde_Vcs::SORT_ASCENDING)
-    {
-        // Always sort directories by name.
-        natcasesort($this->_dirs);
-
-        $this->_doFileSort($this->_files, $how);
-
-        if (isset($this->_atticFiles)) {
-            $this->_doFileSort($this->_atticFiles, $how);
-        }
-
-        if (isset($this->_mergedFiles)) {
-            $this->_doFileSort($this->_mergedFiles, $how);
-        }
-
-        if ($dir == Horde_Vcs::SORT_DESCENDING) {
-            $this->_dirs = array_reverse($this->_dirs);
-            $this->_files = array_reverse($this->_files);
-            if (isset($this->_mergedFiles)) {
-                $this->_mergedFiles = array_reverse($this->_mergedFiles);
-            }
-        }
-    }
-
-    /**
-     * TODO
-     */
-    protected function _doFileSort(&$fileList, $how = Horde_Vcs::SORT_NONE)
-    {
-        switch ($how) {
-        case Horde_Vcs::SORT_AGE:
-            usort($fileList, array($this, '_fileAgeSort'));
-            break;
-
-        case Horde_Vcs::SORT_NAME:
-            usort($fileList, array($this, '_fileNameSort'));
-            break;
-
-        case Horde_Vcs::SORT_AUTHOR:
-            usort($fileList, array($this, '_fileAuthorSort'));
-            break;
-
-        case Horde_Vcs::SORT_REV:
-            usort($fileList, array($this, '_fileRevSort'));
-            break;
-
-        case Horde_Vcs::SORT_NONE:
-        default:
-            break;
-        }
-    }
-
-    /**
-     * Sort function for ascending age.
-     */
-    public function _fileAgeSort($a, $b)
-    {
-        $aa = $a->queryLastLog();
-        $bb = $b->queryLastLog();
-        return ($aa->queryDate() == $bb->queryDate())
-            ? 0
-            : (($aa->queryDate() < $bb->queryDate()) ? 1 : -1);
-    }
-
-    /**
-     * Sort function by author name.
-     */
-    public function _fileAuthorSort($a, $b)
-    {
-        $aa = $a->queryLastLog();
-        $bb = $b->queryLastLog();
-        return ($aa->queryAuthor() == $bb->queryAuthor())
-            ? 0
-            : (($aa->queryAuthor() > $bb->queryAuthor()) ? 1 : -1);
-    }
-
-    /**
-     * Sort function for ascending filename.
-     */
-    public function _fileNameSort($a, $b)
-    {
-        return strcasecmp($a->queryName(), $b->queryName());
-    }
-
-    /**
-     * Sort function for ascending revision.
-     */
-    public function _fileRevSort($a, $b)
-    {
-        return $this->_rep->cmp($a->queryRevision(), $b->queryRevision());
-    }
-
-    /**
-     * TODO
-     */
-    public function getBranches()
-    {
-        return array();
-    }
-}
-
-/**
- * Horde_Vcs file class.
- *
- * @package Horde_Vcs
- */
-abstract class Horde_Vcs_File
-{
-    /**
-     * TODO
-     */
-    protected $_dir;
-
-    /**
-     * TODO
-     */
-    protected $_name;
-
-    /**
-     * TODO
-     */
-    public $logs = array();
-
-    /**
-     * TODO
-     */
-    protected $_revs = array();
-
-    /**
-     * TODO
-     */
-    protected $_rep;
-
-    /**
-     * TODO
-     */
-    protected $_quicklog;
-
-    /**
-     * TODO
-     */
-    protected $_branch = null;
-
-    /**
-     * Create a repository file object, and give it information about
-     * what its parent directory and repository objects are.
-     *
-     * @param string $filename  Full path to this file.
-     * @param array  $opts      TODO
-     */
-    public function __construct($filename, $opts = array())
-    {
-        $this->_name = basename($filename);
-        $this->_dir = dirname($filename);
-
-        $this->_quicklog = !empty($opts['quicklog']);
-        if (!empty($opts['branch'])) {
-            $this->_branch = $opts['branch'];
-        }
-    }
-
-    protected function _ensureRevisionsInitialized()
-    {
-    }
-
-    protected function _ensureLogsInitialized()
-    {
-    }
-
-    /**
-     * When serializing, don't return the repository object
-     */
-    public function __sleep()
-    {
-        return array_diff(array_keys(get_object_vars($this)), array('_rep'));
-    }
-
-    /**
-     * TODO
-     */
-    public function setRepository($rep)
-    {
-        $this->_rep = $rep;
-    }
-
-    /**
-     * TODO - better name, wrap an object around this?
-     */
-    public function getBlob($revision)
-    {
-        return $this->_rep->checkout($this->queryPath(), $revision);
-    }
-
-    /**
-     * Has the file been deleted?
-     *
-     * @return boolean  Is this file deleted?
-     */
-    public function isDeleted()
-    {
-        return false;
-    }
-
-    /**
-     * Returns name of the current file without the repository extensions.
-     *
-     * @return string  Filename without repository extension
-     */
-    function queryName()
-    {
-        return $this->_name;
-    }
-
-    /**
-     * Returns the name of the current file as in the repository.
-     *
-     * @return string  Filename (without the path).
-     */
-    public function queryRepositoryName()
-    {
-        return $this->_name;
-    }
-
-    /**
-     * Return the last revision of the current file on the HEAD branch.
-     *
-     * @return string  Last revision of the current file.
-     * @throws Horde_Vcs_Exception
-     */
-    public function queryRevision()
-    {
-        $this->_ensureRevisionsInitialized();
-        if (!isset($this->_revs[0])) {
-            throw new Horde_Vcs_Exception('No revisions');
-        }
-        return $this->_revs[0];
-    }
-
-    /**
-     * TODO
-     */
-    public function queryPreviousRevision($rev)
-    {
-        $this->_ensureRevisionsInitialized();
-        $key = array_search($rev, $this->_revs);
-        return (($key !== false) && isset($this->_revs[$key + 1]))
-            ? $this->_revs[$key + 1]
-            : null;
-    }
-
-   /**
-     * Return the last Horde_Vcs_Log object in the file.
-     *
-     * @return Horde_Vcs_Log  Log object of the last entry in the file.
-     * @throws Horde_Vcs_Exception
-     */
-    public function queryLastLog()
-    {
-        $this->_ensureRevisionsInitialized();
-        $this->_ensureLogsInitialized();
-        if (!isset($this->_revs[0]) || !isset($this->logs[$this->_revs[0]])) {
-            throw new Horde_Vcs_Exception('No revisions');
-        }
-        return $this->logs[$this->_revs[0]];
-    }
-
-    /**
-     * Sort the list of Horde_Vcs_Log objects that this file contains.
-     *
-     * @param integer $how  Horde_Vcs::SORT_REV (sort by revision),
-     *                      Horde_Vcs::SORT_NAME (sort by author name), or
-     *                      Horde_Vcs::SORT_AGE (sort by commit date).
-     */
-    public function applySort($how = Horde_Vcs::SORT_REV)
-    {
-        $this->_ensureLogsInitialized();
-
-        switch ($how) {
-        case Horde_Vcs::SORT_NAME:
-            $func = 'Name';
-            break;
-
-        case Horde_Vcs::SORT_AGE:
-            $func = 'Age';
-            break;
-
-        case Horde_Vcs::SORT_REV:
-        default:
-            $func = 'Revision';
-            break;
-        }
-
-        uasort($this->logs, array($this, 'sortBy' . $func));
-        return true;
-    }
-
-    /**
-     * The sortBy*() functions are internally used by applySort.
-     */
-    public function sortByRevision($a, $b)
-    {
-        return $this->_rep->cmp($b->queryRevision(), $a->queryRevision());
-    }
-
-    public function sortByAge($a, $b)
-    {
-        return $b->queryDate() - $a->queryDate();
-    }
-
-    public function sortByName($a, $b)
-    {
-        return strcmp($a->queryAuthor(), $b->queryAuthor());
-    }
-
-    /**
-     * Return the fully qualified filename of this object.
-     *
-     * @return string  Fully qualified filename of this object.
-     */
-    public function queryFullPath()
-    {
-        return $this->_rep->sourceroot() . '/' . $this->queryModulePath();
-    }
-
-    /**
-     * Return the filename relative to its sourceroot.
-     *
-     * @return string  Pathname relative to the sourceroot.
-     */
-    public function queryModulePath()
-    {
-        return $this->_dir . '/' . $this->_name;
-    }
-
-    /**
-     * Return the "base" filename (i.e. the filename needed by the various
-     * command line utilities).
-     *
-     * @return string  A filename.
-     */
-    public function queryPath()
-    {
-        return $this->queryFullPath();
-    }
-
-    /**
-     * TODO
-     */
-    public function queryBranches()
-    {
-        return array();
-    }
-
-    /**
-     * TODO
-     */
-    public function queryLogs($rev = null)
-    {
-        $this->_ensureLogsInitialized();
-        return is_null($rev)
-            ? $this->logs
-            : (isset($this->logs[$rev]) ? $this->logs[$rev] : null);
-    }
-
-    /**
-     * TODO
-     */
-    public function revisionCount()
-    {
-        $this->_ensureRevisionsInitialized();
-        return count($this->_revs);
-    }
-
-    /**
-     * TODO
-     */
-    public function querySymbolicRevisions()
-    {
-        return array();
-    }
-}
-
-/**
- * Horde_Vcs log class.
- *
- * @package Horde_Vcs
- */
-abstract class Horde_Vcs_Log
-{
-    protected $_rep;
-    protected $_file;
-    protected $_files = array();
-    protected $_rev;
-    protected $_author;
-    protected $_tags = array();
-    protected $_date;
-    protected $_log;
-    protected $_state;
-    protected $_lines = '';
-    protected $_branches = array();
-
-    /**
-     * Constructor.
-     */
-    public function __construct($rev)
-    {
-        $this->_rev = $rev;
-    }
-
-    protected function _ensureInitialized()
-    {
-    }
-
-    /**
-     * When serializing, don't return the repository object
-     */
-    public function __sleep()
-    {
-        return array_diff(array_keys(get_object_vars($this)), array('_file', '_rep'));
-    }
-
-    /**
-     * TODO
-     */
-    public function setRepository($rep)
-    {
-        $this->_rep = $rep;
-    }
-
-    public function setFile(Horde_Vcs_File $file)
-    {
-        $this->_file = $file;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryRevision()
-    {
-        $this->_ensureInitialized();
-        return $this->_rev;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryDate()
-    {
-        $this->_ensureInitialized();
-        return $this->_date;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryAuthor()
-    {
-        $this->_ensureInitialized();
-        return $this->_author;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryLog()
-    {
-        $this->_ensureInitialized();
-        return $this->_log;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryBranch()
-    {
-        $this->_ensureInitialized();
-        return array();
-    }
-
-    /**
-     * TODO
-     */
-    public function queryChangedLines()
-    {
-        $this->_ensureInitialized();
-        return $this->_lines;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryTags()
-    {
-        $this->_ensureInitialized();
-        return $this->_tags;
-    }
-
-    /**
-     * Given a branch revision number, this function remaps it
-     * accordingly, and performs a lookup on the file object to
-     * return the symbolic name(s) of that branch in the tree.
-     *
-     * @return array  Hash of symbolic names => branch numbers.
-     */
-    public function querySymbolicBranches()
-    {
-        $this->_ensureInitialized();
-
-        $symBranches = array();
-        $branches = $this->_file->queryBranches();
-
-        foreach ($this->_branches as $branch) {
-            if (($key = array_search($branch, $branches)) !== false) {
-                $symBranches[$key] = $branch;
-            }
-        }
-
-        return $symBranches;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryFiles($file = null)
-    {
-        $this->_ensureInitialized();
-        return is_null($file)
-            ? $this->_files
-            : (isset($this->_files[$file]) ? $this->_files[$file] : array());
-    }
-}
-
-/**
- * Horde_Vcs patchset class.
- *
- * @package Horde_Vcs
- */
-abstract class Horde_Vcs_Patchset
-{
-    const MODIFIED = 0;
-    const ADDED = 1;
-    const DELETED = 2;
-
-    /**
-     * @var array
-     */
-    protected $_patchsets = array();
-
-    /**
-     * Constructor
-     *
-     * @param Horde_Vcs $rep  A Horde_Vcs repository object.
-     * @param string $file    The filename to create patchsets for.
-     * @param array $opts     Additional options:
-     * <pre>
-     * 'range' - (array) The patchsets to process.
-     *           DEFAULT: None (all patchsets are processed).
-     * </pre>
-     */
-    abstract public function __construct($rep, $file, $opts = array());
-
-    /**
-     * TODO
-     *
-     * @return array  TODO
-     * 'date'
-     * 'author'
-     * 'branches'
-     * 'tags'
-     * 'log'
-     * 'members' - array:
-     *     'file'
-     *     'from'
-     *     'to'
-     *     'status'
-     */
-    public function getPatchsets()
-    {
-        return $this->_patchsets;
-    }
-}
index 93cec5e..72a4544 100644 (file)
@@ -257,572 +257,3 @@ class Horde_Vcs_Cvs extends Horde_Vcs_Rcs
     }
 
 }
-
-/**
- * Horde_Vcs_Cvs directory class.
- *
- * @author  Anil Madhavapeddy <anil@recoil.org>
- * @author  Michael Slusarz <slusarz@horde.org>
- * @package Horde_Vcs
- */
-class Horde_Vcs_Directory_Cvs extends Horde_Vcs_Directory
-{
-    /**
-     * Create a Directory object to store information about the files in a
-     * single directory in the repository
-     *
-     * @param Horde_Vcs $rep  The Repository object this directory is part of.
-     * @param string $dn      Path to the directory.
-     * @param array $opts     TODO
-     *
-     * @throws Horde_Vcs_Exception
-     */
-    public function __construct($rep, $dn, $opts = array())
-    {
-        parent::__construct($rep, $dn, $opts);
-        $this->_dirName = $rep->sourceroot() . '/' . $dn;
-
-        /* Make sure we are trying to list a directory */
-        if (!@is_dir($this->_dirName)) {
-            throw new Horde_Vcs_Exception('Unable to find directory: ' . $this->_dirName);
-        }
-
-        /* Open the directory for reading its contents */
-        if (!($DIR = @opendir($this->_dirName))) {
-            throw new Horde_Vcs_Exception(empty($php_errormsg) ? 'Permission denied' : $php_errormsg);
-        }
-
-        /* Create two arrays - one of all the files, and the other of
-         * all the directories. */
-        while (($name = readdir($DIR)) !== false) {
-            if (($name == '.') || ($name == '..')) {
-                continue;
-            }
-
-            $path = $this->_dirName . '/' . $name;
-            if (@is_dir($path)) {
-                /* Skip Attic directory. */
-                if ($name != 'Attic') {
-                    $this->_dirs[] = $name;
-                }
-            } elseif (@is_file($path) && (substr($name, -2) == ',v')) {
-                /* Spawn a new file object to represent this file. */
-                $this->_files[] = $rep->getFileObject(substr($path, strlen($rep->sourceroot()), -2), array('quicklog' => !empty($opts['quicklog'])));
-            }
-        }
-
-        /* Close the filehandle; we've now got a list of dirs and files. */
-        closedir($DIR);
-
-        /* If we want to merge the attic, add it in here. */
-        if (!empty($opts['showattic'])) {
-            try {
-                $atticDir = new Horde_Vcs_Directory_Cvs($rep, $this->_moduleName . '/Attic', $opts, $this);
-                $this->_atticFiles = $atticDir->queryFileList();
-                $this->_mergedFiles = array_merge($this->_files, $this->_atticFiles);
-            } catch (Horde_Vcs_Exception $e) {}
-        }
-
-        return true;
-    }
-
-    /**
-     * TODO
-     */
-    public function getBranches()
-    {
-        return array('HEAD');
-    }
-
-}
-
-/**
- * Horde_Vcs_Cvs file class.
- *
- * @author  Anil Madhavapeddy <anil@recoil.org>
- * @author  Michael Slusarz <slusarz@horde.org>
- * @package Horde_Vcs
- */
-class Horde_Vcs_File_Cvs extends Horde_Vcs_File
-{
-    /**
-     * TODO
-     *
-     * @var string
-     */
-    protected $_accum;
-
-    /**
-     * TODO
-     *
-     * @var array
-     */
-    protected $_revsym = array();
-
-    /**
-     * TODO
-     *
-     * @var array
-     */
-    protected $_symrev = array();
-
-    /**
-     * TODO
-     *
-     * @var array
-     */
-    protected $_revlist = array();
-
-    /**
-     * TODO
-     *
-     * @var array
-     */
-    protected $_branches = array();
-    
-    private $_initialized;
-
-    protected function _init()
-    {
-        /* Check that we are actually in the filesystem. */
-        $file = $this->_dir . '/' . $this->_name;
-        if (!is_file($file)) {
-            throw new Horde_Vcs_Exception('File Not Found: ' . $file);
-        }
-
-        $ret_array = array();
-        $cmd = escapeshellcmd($this->_rep->getPath('rlog')) . ($this->_quicklog ? ' -r' : '') . ' ' . escapeshellarg($file);
-        exec($cmd, $ret_array, $retval);
-
-        if ($retval) {
-            throw new Horde_Vcs_Exception('Failed to spawn rlog to retrieve file log information for ' . $file);
-        }
-
-        $branches = array();
-        $state = 'init';
-
-        foreach ($ret_array as $line) {
-            switch ($state) {
-            case 'init':
-                if (strpos($line, 'head: ') === 0) {
-                    $this->_branches['HEAD'] = substr($line, 6);
-                    $this->_revlist['HEAD'] = $this->_rep->getRevisionRange($this, '1.1', $this->_branches['HEAD']);
-                } elseif (strpos($line, 'branch:') === 0) {
-                    $state = 'rev';
-                }
-                break;
-
-            case 'rev':
-                if (strpos($line, '----------') === 0) {
-                    $state = 'info';
-                } elseif (preg_match("/^\s+([^:]+):\s+([\d\.]+)/", $line, $regs)) {
-                    // Check to see if this is a branch.
-                    if (preg_match('/^(\d+(\.\d+)+)\.0\.(\d+)$/', $regs[2])) {
-                        $rev = $regs[2];
-                        $end = strrpos($rev, '.');
-                        $rev[$end] = 0;
-                        $branchRev = (($end2 = strrpos($rev, '.')) === false)
-                            ? substr($rev, ++$end)
-                            : substr_replace($rev, '.', $end2, ($end - $end2 + 1));
-
-                        /* $branchRev is only the branching point, NOT the
-                         * HEAD of the branch. To determine the HEAD, we need
-                         * to parse all of the log data first. Yuck. */
-                        $branches[$regs[1]] = $branchRev . '.';
-                    } else {
-                        $this->_symrev[$regs[1]] = $regs[2];
-                        if (empty($this->_revsym[$regs[2]])) {
-                            $this->_revsym[$regs[2]] = array();
-                        }
-                        $this->_revsym[$regs[2]][] = $regs[1];
-                    }
-                }
-                break;
-
-            case 'info':
-                if ((strpos($line, '==============================') === false) &&
-                    (strpos($line, '----------------------------') === false)) {
-                    $this->_accum[] = $line;
-                } elseif (count($this->_accum)) {
-                    $log = $this->_rep->getLogObject($this, null);
-                    $rev = $log->queryRevision();
-                    $onbranch = false;
-                    $onhead = (substr_count($rev, '.') == 1);
-
-                    // Determine branch information.
-                    if ($onhead) {
-                        $onbranch = (empty($this->_branch) || $this->_branch == 'HEAD') ||
-                            ($this->_rep->cmp($branches[$this->_branch], $rev) === 1);
-                    } elseif ($this->_branch != 'HEAD') {
-                        foreach ($branches as $key => $val) {
-                            if (strpos($rev, $val) === 0) {
-                                $onbranch = true;
-                                $log->setBranch($key);
-                                if (!isset($this->_branches[$key])) {
-                                    $this->_branches[$key] = $rev;
-                                    $this->_revlist[$key] = $this->_rep->getRevisionRange($this, '1.1', $rev);
-                                }
-                                break;
-                            }
-                        }
-                    }
-
-                    if ($onbranch) {
-                        $this->_revs[] = $rev;
-                        $this->logs[$rev] = $log;
-                    }
-
-                    $this->_accum = array();
-                }
-                break;
-            }
-        }
-    }
-
-    protected function _ensureRevisionsInitialized()
-    {
-        if(!$this->_initialized) {
-            $this->_init();
-            $this->_initialized = true;
-        }
-    }
-
-    protected function _ensureLogsInitialized()
-    {
-        if(!$this->_initialized) {
-            $this->_init();
-            $this->_initialized = true;
-        }
-    }
-
-    /**
-     * If this file is present in an Attic directory, this indicates it.
-     *
-     * @return boolean  True if file is in the Attic, and false otherwise
-     */
-    public function isDeleted()
-    {
-        return (substr($this->_dir, -5) == 'Attic');
-    }
-
-    /**
-     * Returns name of the current file without the repository
-     * extensions (usually ,v)
-     *
-     * @return string  Filename without repository extension
-     */
-    public function queryName()
-    {
-        return preg_replace('/,v$/', '', $this->_name);
-    }
-
-    /**
-     * Return the fully qualified filename of this object.
-     *
-     * @return Fully qualified filename of this object
-     */
-    public function queryFullPath()
-    {
-        return parent::queryModulePath();
-    }
-
-    /**
-     * Return the name of this file relative to its sourceroot.
-     *
-     * @return string  Pathname relative to the sourceroot.
-     */
-    public function queryModulePath()
-    {
-        return preg_replace('|^'. $this->_rep->sourceroot() . '/?(.*),v$|', '\1', $this->queryFullPath());
-    }
-
-    /**
-     * TODO
-     */
-    public function getBranchList()
-    {
-        return $this->_revlist();
-    }
-
-    /**
-     * TODO
-     */
-    public function queryRevsym($rev)
-    {
-        return isset($this->_revsym[$rev])
-            ? $this->_revsym[$rev]
-            : array();
-    }
-
-    /**
-     * TODO
-     */
-    public function querySymbolicRevisions()
-    {
-        return $this->_symrev;
-    }
-
-    /**
-     * TODO
-     */
-    public function getAccum()
-    {
-        return $this->_accum;
-    }
-
-    /**
-     * TODO
-     */
-     public function queryBranches()
-     {
-         return $this->_branches;
-     }
-
-}
-
-/**
- * Horde_Vcs_cvs Log class.
- *
- * @author  Anil Madhavapeddy <anil@recoil.org>
- * @author  Michael Slusarz <slusarz@horde.org>
- * @package Horde_Vcs
- */
-class Horde_Vcs_Log_Cvs extends Horde_Vcs_Log
-{
-    /**
-     * Cached branch info.
-     *
-     * @var string
-     */
-    protected $_branch;
-
-    private $_initialized;
-
-    protected function _init()
-    {
-       $raw = $this->_file->getAccum();
-
-        /* Initialise a simple state machine to parse the output of rlog */
-        $state = 'init';
-        while (!empty($raw) && $state != 'done') {
-            switch ($state) {
-            /* Found filename, now looking for the revision number */
-            case 'init':
-                $line = array_shift($raw);
-                if (preg_match("/revision (.+)$/", $line, $parts)) {
-                    $this->_rev = $parts[1];
-                    $state = 'date';
-                }
-                break;
-
-            /* Found revision and filename, now looking for date */
-            case 'date':
-                $line = array_shift($raw);
-                if (preg_match("|^date:\s+(\d+)[-/](\d+)[-/](\d+)\s+(\d+):(\d+):(\d+).*?;\s+author:\s+(.+);\s+state:\s+(\S+);(\s+lines:\s+([0-9\s+-]+))?|", $line, $parts)) {
-                    $this->_date = gmmktime($parts[4], $parts[5], $parts[6], $parts[2], $parts[3], $parts[1]);
-                    $this->_author = $parts[7];
-                    $this->_state = $parts[8];
-                    $this->_lines = isset($parts[10]) ? $parts[10] : '';
-                    $state = 'branches';
-                }
-                break;
-
-            /* Look for a branch point here - format is 'branches:
-             * x.y.z; a.b.c;' */
-            case 'branches':
-                /* If we find a branch tag, process and pop it,
-                   otherwise leave input stream untouched */
-                if (!empty($raw) &&
-                    preg_match("/^branches:\s+(.*)/", $raw[0], $br)) {
-                    /* Get the list of branches from the string, and
-                     * push valid revisions into the branches array */
-                    $brs = preg_split('/;\s*/', $br[1]);
-                    foreach ($brs as $brpoint) {
-                        if ($this->_rep->isValidRevision($brpoint)) {
-                            $this->_branches[] = $brpoint;
-                        }
-                    }
-                    array_shift($raw);
-                }
-
-                $state = 'done';
-                break;
-            }
-        }
-
-        /* Assume the rest of the lines are the log message */
-        $this->_log = implode("\n", $raw);
-        $this->_tags = $this->_file->queryRevsym($this->_rev);
-    }
-
-    protected function _ensureInitialized()
-    {
-        if (!$this->_initialized) {
-            $this->_init();
-            $this->_initialized = true;
-        }
-    }
-
-    /**
-     * TODO
-     */
-    public function setBranch($branch)
-    {
-        $this->_branch = array($branch);
-    }
-
-    /**
-     * TODO
-     */
-    public function queryBranch()
-    {
-        if (!empty($this->_branch)) {
-            return $this->_branch;
-        }
-
-        $branches = $this->_file->queryBranches();
-        $key = array_keys($branches, $this->_rev);
-        return empty($key)
-            ? array_keys($branches, $this->_rep->strip($this->_rev, 1))
-            : $key;
-    }
-
-}
-
-/**
- * Horde_Vcs_Cvs Patchset class.
- *
- * @author  Anil Madhavapeddy <anil@recoil.org>
- * @author  Michael Slusarz <slusarz@horde.org>
- * @package Horde_Vcs
- */
-class Horde_Vcs_Patchset_Cvs extends Horde_Vcs_Patchset
-{
-    /**
-     * Constructor
-     *
-     * @param Horde_Vcs $rep  A Horde_Vcs repository object.
-     * @param string $file    The filename to create a patchset for.
-     * @param array $opts     Additional options.
-     * <pre>
-     * 'file' - (string) The filename to process.
-     *          REQUIRED for this driver.
-     * 'range' - (array) The patchsets to process.
-     *           DEFAULT: None (all patchsets are processed).
-     * </pre>
-     *
-     * @throws Horde_Vcs_Exception
-     */
-    public function __construct($rep, $opts = array())
-    {
-        $file = $rep->sourceroot() . '/' . $opts['file'];
-
-        /* Check that we are actually in the filesystem. */
-        if (!$rep->isFile($file)) {
-            throw new Horde_Vcs_Exception('File Not Found');
-        }
-
-        /* Call cvsps to retrieve all patchsets for this file. */
-        $cvsps_home = $rep->getPath('cvsps_home');
-        $HOME = !empty($cvsps_home) ?
-            'HOME=' . escapeshellarg($cvsps_home) . ' ' :
-            '';
-
-        $rangecmd = empty($opts['range'])
-            ? ''
-            : ' -s ' . escapeshellarg(implode(',', $opts['range']));
-
-        $ret_array = array();
-        $cmd = $HOME . escapeshellcmd($rep->getPath('cvsps')) . $rangecmd . ' -u --cvs-direct --root ' . escapeshellarg($rep->sourceroot()) . ' -f ' . escapeshellarg(basename($file)) . ' ' . escapeshellarg(dirname($file));
-        exec($cmd, $ret_array, $retval);
-        if ($retval) {
-            throw new Horde_Vcs_Exception('Failed to spawn cvsps to retrieve patchset information.');
-        }
-
-        $state = 'begin';
-        reset($ret_array);
-        while (list(,$line) = each($ret_array)) {
-            $line = trim($line);
-
-            if ($line == '---------------------') {
-                $state = 'begin';
-                continue;
-            }
-
-            switch ($state) {
-            case 'begin':
-                $id = str_replace('PatchSet ', '', $line);
-                $this->_patchsets[$id] = array();
-                $state = 'info';
-                break;
-
-            case 'info':
-                $info = explode(':', $line, 2);
-                $info[1] = ltrim($info[1]);
-
-                switch ($info[0]) {
-                case 'Date':
-                    $d = new DateTime($info[1]);
-                    $this->_patchsets[$id]['date'] = $d->format('U');
-                    break;
-
-                case 'Author':
-                    $this->_patchsets[$id]['author'] = $info[1];
-                    break;
-
-                case 'Branch':
-                    $this->_patchsets[$id]['branches'] = ($info[1] == 'HEAD')
-                        ? array()
-                        : array($info[1]);
-                    break;
-
-                case 'Tag':
-                    $this->_patchsets[$id]['tags'] = ($info[1] == '(none)')
-                        ? array()
-                        : array($info[1]);
-                    break;
-
-                case 'Log':
-                    $state = 'log';
-                    $this->_patchsets[$id]['log'] = '';
-                    break;
-                }
-                break;
-
-            case 'log':
-                if ($line == 'Members:') {
-                    $state = 'members';
-                    $this->_patchsets[$id]['log'] = rtrim($this->_patchsets[$id]['log']);
-                    $this->_patchsets[$id]['members'] = array();
-                } else {
-                    $this->_patchsets[$id]['log'] .= $line . "\n";
-                }
-                break;
-
-            case 'members':
-                if (!empty($line)) {
-                    $parts = explode(':', $line);
-                    list($from, $to) = explode('->', $parts[1], 2);
-                    $status = self::MODIFIED;
-
-                    if ($from == 'INITIAL') {
-                        $from = null;
-                        $status = self::ADDED;
-                    } elseif (substr($to, -6) == '(DEAD)') {
-                        $to = null;
-                        $status = self::DELETED;
-                    }
-
-                    $this->_patchsets[$id]['members'][] = array(
-                        'file' => $parts[0],
-                        'from' => $from,
-                        'status' => $status,
-                        'to' => $to
-                    );
-                }
-                break;
-            }
-        }
-    }
-
-}
diff --git a/framework/Vcs/lib/Horde/Vcs/Directory.php b/framework/Vcs/lib/Horde/Vcs/Directory.php
new file mode 100644 (file)
index 0000000..8a0d696
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+/**
+ * Horde_Vcs_Cvs directory class.
+ *
+ * Copyright 2008-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package Horde_Vcs
+ */
+abstract class Horde_Vcs_Directory
+{
+    /**
+     * @var Horde_Vcs
+     */
+    protected $_rep;
+
+    /**
+     * @var string
+     */
+    protected $_dirName;
+
+    /**
+     * @var array
+     */
+    protected $_files = array();
+
+    /**
+     * @var array
+     */
+    protected $_atticFiles = array();
+
+    /**
+     * @var array
+     */
+    protected $_mergedFiles = array();
+
+    /**
+     * @var string
+     */
+    protected $_dirs = array();
+
+    /**
+     * @var string
+     */
+    protected $_moduleName;
+
+    /**
+     * Create a Directory object to store information about the files in a
+     * single directory in the repository.
+     *
+     * @param Horde_Vcs $rep  The Repository object this directory is part of.
+     * @param string $dn      Path to the directory.
+     * @param array $opts     TODO
+     */
+    public function __construct($rep, $dn, $opts = array())
+    {
+        $this->_rep = $rep;
+        $this->_moduleName = $dn;
+        $this->_dirName = '/' . $dn;
+    }
+
+    /**
+     * Return fully qualified pathname to this directory with no trailing /.
+     *
+     * @return string Pathname of this directory.
+     */
+    public function queryDir()
+    {
+        return $this->_dirName;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryDirList()
+    {
+        return $this->_dirs;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryFileList($showattic = false)
+    {
+        return ($showattic && isset($this->_mergedFiles))
+            ? $this->_mergedFiles
+            : $this->_files;
+    }
+
+    /**
+     * Sort the contents of the directory in a given fashion and
+     * order.
+     *
+     * @param integer $how  Of the form Horde_Vcs::SORT_[*] where * can be:
+     *                      NONE, NAME, AGE, REV for sorting by name, age or
+     *                      revision.
+     * @param integer $dir  Of the form Horde_Vcs::SORT_[*] where * can be:
+     *                      ASCENDING, DESCENDING for the order of the sort.
+     */
+    public function applySort($how = Horde_Vcs::SORT_NONE,
+                              $dir = Horde_Vcs::SORT_ASCENDING)
+    {
+        // Always sort directories by name.
+        natcasesort($this->_dirs);
+
+        $this->_doFileSort($this->_files, $how);
+
+        if (isset($this->_atticFiles)) {
+            $this->_doFileSort($this->_atticFiles, $how);
+        }
+
+        if (isset($this->_mergedFiles)) {
+            $this->_doFileSort($this->_mergedFiles, $how);
+        }
+
+        if ($dir == Horde_Vcs::SORT_DESCENDING) {
+            $this->_dirs = array_reverse($this->_dirs);
+            $this->_files = array_reverse($this->_files);
+            if (isset($this->_mergedFiles)) {
+                $this->_mergedFiles = array_reverse($this->_mergedFiles);
+            }
+        }
+    }
+
+    /**
+     * TODO
+     */
+    protected function _doFileSort(&$fileList, $how = Horde_Vcs::SORT_NONE)
+    {
+        switch ($how) {
+        case Horde_Vcs::SORT_AGE:
+            usort($fileList, array($this, '_fileAgeSort'));
+            break;
+
+        case Horde_Vcs::SORT_NAME:
+            usort($fileList, array($this, '_fileNameSort'));
+            break;
+
+        case Horde_Vcs::SORT_AUTHOR:
+            usort($fileList, array($this, '_fileAuthorSort'));
+            break;
+
+        case Horde_Vcs::SORT_REV:
+            usort($fileList, array($this, '_fileRevSort'));
+            break;
+
+        case Horde_Vcs::SORT_NONE:
+        default:
+            break;
+        }
+    }
+
+    /**
+     * Sort function for ascending age.
+     */
+    public function _fileAgeSort($a, $b)
+    {
+        $aa = $a->queryLastLog();
+        $bb = $b->queryLastLog();
+        return ($aa->queryDate() == $bb->queryDate())
+            ? 0
+            : (($aa->queryDate() < $bb->queryDate()) ? 1 : -1);
+    }
+
+    /**
+     * Sort function by author name.
+     */
+    public function _fileAuthorSort($a, $b)
+    {
+        $aa = $a->queryLastLog();
+        $bb = $b->queryLastLog();
+        return ($aa->queryAuthor() == $bb->queryAuthor())
+            ? 0
+            : (($aa->queryAuthor() > $bb->queryAuthor()) ? 1 : -1);
+    }
+
+    /**
+     * Sort function for ascending filename.
+     */
+    public function _fileNameSort($a, $b)
+    {
+        return strcasecmp($a->queryName(), $b->queryName());
+    }
+
+    /**
+     * Sort function for ascending revision.
+     */
+    public function _fileRevSort($a, $b)
+    {
+        return $this->_rep->cmp($a->queryRevision(), $b->queryRevision());
+    }
+
+    /**
+     * TODO
+     */
+    public function getBranches()
+    {
+        return array();
+    }
+}
diff --git a/framework/Vcs/lib/Horde/Vcs/Directory/Cvs.php b/framework/Vcs/lib/Horde/Vcs/Directory/Cvs.php
new file mode 100644 (file)
index 0000000..acabe9a
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Horde_Vcs_Cvs directory class.
+ *
+ * Copyright 2000-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anil Madhavapeddy <anil@recoil.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Vcs
+ */
+class Horde_Vcs_Directory_Cvs extends Horde_Vcs_Directory_Base
+{
+    /**
+     * Create a Directory object to store information about the files in a
+     * single directory in the repository
+     *
+     * @param Horde_Vcs $rep  The Repository object this directory is part of.
+     * @param string $dn      Path to the directory.
+     * @param array $opts     TODO
+     *
+     * @throws Horde_Vcs_Exception
+     */
+    public function __construct($rep, $dn, $opts = array())
+    {
+        parent::__construct($rep, $dn, $opts);
+        $this->_dirName = $rep->sourceroot() . '/' . $dn;
+
+        /* Make sure we are trying to list a directory */
+        if (!@is_dir($this->_dirName)) {
+            throw new Horde_Vcs_Exception('Unable to find directory: ' . $this->_dirName);
+        }
+
+        /* Open the directory for reading its contents */
+        if (!($DIR = @opendir($this->_dirName))) {
+            throw new Horde_Vcs_Exception(empty($php_errormsg) ? 'Permission denied' : $php_errormsg);
+        }
+
+        /* Create two arrays - one of all the files, and the other of
+         * all the directories. */
+        while (($name = readdir($DIR)) !== false) {
+            if (($name == '.') || ($name == '..')) {
+                continue;
+            }
+
+            $path = $this->_dirName . '/' . $name;
+            if (@is_dir($path)) {
+                /* Skip Attic directory. */
+                if ($name != 'Attic') {
+                    $this->_dirs[] = $name;
+                }
+            } elseif (@is_file($path) && (substr($name, -2) == ',v')) {
+                /* Spawn a new file object to represent this file. */
+                $this->_files[] = $rep->getFileObject(substr($path, strlen($rep->sourceroot()), -2), array('quicklog' => !empty($opts['quicklog'])));
+            }
+        }
+
+        /* Close the filehandle; we've now got a list of dirs and files. */
+        closedir($DIR);
+
+        /* If we want to merge the attic, add it in here. */
+        if (!empty($opts['showattic'])) {
+            try {
+                $atticDir = new Horde_Vcs_Directory_Cvs($rep, $this->_moduleName . '/Attic', $opts, $this);
+                $this->_atticFiles = $atticDir->queryFileList();
+                $this->_mergedFiles = array_merge($this->_files, $this->_atticFiles);
+            } catch (Horde_Vcs_Exception $e) {}
+        }
+
+        return true;
+    }
+
+    /**
+     * TODO
+     */
+    public function getBranches()
+    {
+        return array('HEAD');
+    }
+
+}
diff --git a/framework/Vcs/lib/Horde/Vcs/Directory/Git.php b/framework/Vcs/lib/Horde/Vcs/Directory/Git.php
new file mode 100644 (file)
index 0000000..9201413
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Horde_Vcs_Git directory class.
+ *
+ * Copyright 2008-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Vcs
+ */
+class Horde_Vcs_Directory_Git extends Horde_Vcs_Directory
+{
+    /**
+     * The current branch.
+     *
+     * @var string
+     */
+    protected $_branch;
+
+    /**
+     * Create a Directory object to store information about the files in a
+     * single directory in the repository.
+     *
+     * @param Horde_Vcs $rep  The Repository object this directory is part of.
+     * @param string $dn      Path to the directory.
+     * @param array $opts     TODO
+     *
+     * @throws Horde_Vcs_Exception
+     */
+    public function __construct($rep, $dn, $opts = array())
+    {
+        parent::__construct($rep, $dn, $opts);
+
+        $this->_branch = empty($opts['rev'])
+            ? $rep->getDefaultBranch()
+            : $opts['rev'];
+
+        // @TODO See if we have a valid cache of the tree at this revision
+
+        $dir = $this->queryDir();
+        if (substr($dir, 0, 1) == '/') {
+            $dir = (string)substr($dir, 1);
+        }
+        if (strlen($dir) && substr($dir, -1) != '/') {
+            $dir .= '/';
+        }
+
+        $cmd = $rep->getCommand() . ' ls-tree --full-name ' . escapeshellarg($this->_branch) . ' ' . escapeshellarg($dir) . ' 2>&1';
+        $stream = popen($cmd, 'r');
+        if (!$stream) {
+            throw new Horde_Vcs_Exception('Failed to execute git ls-tree: ' . $cmd);
+        }
+
+        // Create two arrays - one of all the files, and the other of
+        // all the dirs.
+        while (!feof($stream)) {
+            $line = fgets($stream);
+            if ($line === false) { break; }
+
+            $line = rtrim($line);
+            if (!strlen($line))  { continue; }
+
+            list(, $type, , $file) = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
+            if ($type == 'tree') {
+                $this->_dirs[] = basename($file);
+            } else {
+                $this->_files[] = $rep->getFileObject($file, array('branch' => $this->_branch, 'quicklog' => !empty($opts['quicklog'])));
+            }
+        }
+
+        pclose($stream);
+    }
+
+    /**
+     * TODO
+     */
+    public function getBranches()
+    {
+        $blist = array_keys($this->_rep->getBranchList());
+        if (!in_array($this->_branch, $blist)) {
+            $blist[] = $this->_branch;
+        }
+        return $blist;
+    }
+
+}
\ No newline at end of file
diff --git a/framework/Vcs/lib/Horde/Vcs/File.php b/framework/Vcs/lib/Horde/Vcs/File.php
new file mode 100644 (file)
index 0000000..5e918a2
--- /dev/null
@@ -0,0 +1,286 @@
+<?php
+/**
+ * Horde_Vcs file class.
+ *
+ * Copyright 2008-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package Horde_Vcs
+ */
+abstract class Horde_Vcs_File
+{
+    /**
+     * TODO
+     */
+    protected $_dir;
+
+    /**
+     * TODO
+     */
+    protected $_name;
+
+    /**
+     * TODO
+     */
+    public $logs = array();
+
+    /**
+     * TODO
+     */
+    protected $_revs = array();
+
+    /**
+     * TODO
+     */
+    protected $_rep;
+
+    /**
+     * TODO
+     */
+    protected $_quicklog;
+
+    /**
+     * TODO
+     */
+    protected $_branch = null;
+
+    /**
+     * Create a repository file object, and give it information about
+     * what its parent directory and repository objects are.
+     *
+     * @param string $filename  Full path to this file.
+     * @param array  $opts      TODO
+     */
+    public function __construct($filename, $opts = array())
+    {
+        $this->_name = basename($filename);
+        $this->_dir = dirname($filename);
+
+        $this->_quicklog = !empty($opts['quicklog']);
+        if (!empty($opts['branch'])) {
+            $this->_branch = $opts['branch'];
+        }
+    }
+
+    protected function _ensureRevisionsInitialized()
+    {
+    }
+
+    protected function _ensureLogsInitialized()
+    {
+    }
+
+    /**
+     * When serializing, don't return the repository object
+     */
+    public function __sleep()
+    {
+        return array_diff(array_keys(get_object_vars($this)), array('_rep'));
+    }
+
+    /**
+     * TODO
+     */
+    public function setRepository($rep)
+    {
+        $this->_rep = $rep;
+    }
+
+    /**
+     * TODO - better name, wrap an object around this?
+     */
+    public function getBlob($revision)
+    {
+        return $this->_rep->checkout($this->queryPath(), $revision);
+    }
+
+    /**
+     * Has the file been deleted?
+     *
+     * @return boolean  Is this file deleted?
+     */
+    public function isDeleted()
+    {
+        return false;
+    }
+
+    /**
+     * Returns name of the current file without the repository extensions.
+     *
+     * @return string  Filename without repository extension
+     */
+    function queryName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Returns the name of the current file as in the repository.
+     *
+     * @return string  Filename (without the path).
+     */
+    public function queryRepositoryName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Return the last revision of the current file on the HEAD branch.
+     *
+     * @return string  Last revision of the current file.
+     * @throws Horde_Vcs_Exception
+     */
+    public function queryRevision()
+    {
+        $this->_ensureRevisionsInitialized();
+        if (!isset($this->_revs[0])) {
+            throw new Horde_Vcs_Exception('No revisions');
+        }
+        return $this->_revs[0];
+    }
+
+    /**
+     * TODO
+     */
+    public function queryPreviousRevision($rev)
+    {
+        $this->_ensureRevisionsInitialized();
+        $key = array_search($rev, $this->_revs);
+        return (($key !== false) && isset($this->_revs[$key + 1]))
+            ? $this->_revs[$key + 1]
+            : null;
+    }
+
+   /**
+     * Return the last Horde_Vcs_Log object in the file.
+     *
+     * @return Horde_Vcs_Log  Log object of the last entry in the file.
+     * @throws Horde_Vcs_Exception
+     */
+    public function queryLastLog()
+    {
+        $this->_ensureRevisionsInitialized();
+        $this->_ensureLogsInitialized();
+        if (!isset($this->_revs[0]) || !isset($this->logs[$this->_revs[0]])) {
+            throw new Horde_Vcs_Exception('No revisions');
+        }
+        return $this->logs[$this->_revs[0]];
+    }
+
+    /**
+     * Sort the list of Horde_Vcs_Log objects that this file contains.
+     *
+     * @param integer $how  Horde_Vcs::SORT_REV (sort by revision),
+     *                      Horde_Vcs::SORT_NAME (sort by author name), or
+     *                      Horde_Vcs::SORT_AGE (sort by commit date).
+     */
+    public function applySort($how = Horde_Vcs::SORT_REV)
+    {
+        $this->_ensureLogsInitialized();
+
+        switch ($how) {
+        case Horde_Vcs::SORT_NAME:
+            $func = 'Name';
+            break;
+
+        case Horde_Vcs::SORT_AGE:
+            $func = 'Age';
+            break;
+
+        case Horde_Vcs::SORT_REV:
+        default:
+            $func = 'Revision';
+            break;
+        }
+
+        uasort($this->logs, array($this, 'sortBy' . $func));
+        return true;
+    }
+
+    /**
+     * The sortBy*() functions are internally used by applySort.
+     */
+    public function sortByRevision($a, $b)
+    {
+        return $this->_rep->cmp($b->queryRevision(), $a->queryRevision());
+    }
+
+    public function sortByAge($a, $b)
+    {
+        return $b->queryDate() - $a->queryDate();
+    }
+
+    public function sortByName($a, $b)
+    {
+        return strcmp($a->queryAuthor(), $b->queryAuthor());
+    }
+
+    /**
+     * Return the fully qualified filename of this object.
+     *
+     * @return string  Fully qualified filename of this object.
+     */
+    public function queryFullPath()
+    {
+        return $this->_rep->sourceroot() . '/' . $this->queryModulePath();
+    }
+
+    /**
+     * Return the filename relative to its sourceroot.
+     *
+     * @return string  Pathname relative to the sourceroot.
+     */
+    public function queryModulePath()
+    {
+        return $this->_dir . '/' . $this->_name;
+    }
+
+    /**
+     * Return the "base" filename (i.e. the filename needed by the various
+     * command line utilities).
+     *
+     * @return string  A filename.
+     */
+    public function queryPath()
+    {
+        return $this->queryFullPath();
+    }
+
+    /**
+     * TODO
+     */
+    public function queryBranches()
+    {
+        return array();
+    }
+
+    /**
+     * TODO
+     */
+    public function queryLogs($rev = null)
+    {
+        $this->_ensureLogsInitialized();
+        return is_null($rev)
+            ? $this->logs
+            : (isset($this->logs[$rev]) ? $this->logs[$rev] : null);
+    }
+
+    /**
+     * TODO
+     */
+    public function revisionCount()
+    {
+        $this->_ensureRevisionsInitialized();
+        return count($this->_revs);
+    }
+
+    /**
+     * TODO
+     */
+    public function querySymbolicRevisions()
+    {
+        return array();
+    }
+}
diff --git a/framework/Vcs/lib/Horde/Vcs/File/Cvs.php b/framework/Vcs/lib/Horde/Vcs/File/Cvs.php
new file mode 100644 (file)
index 0000000..0996a42
--- /dev/null
@@ -0,0 +1,249 @@
+<?php
+/**
+ * Horde_Vcs_Cvs file class.
+ *
+ * Copyright 2000-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anil Madhavapeddy <anil@recoil.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Vcs
+ */
+class Horde_Vcs_File_Cvs extends Horde_Vcs_File
+{
+    /**
+     * TODO
+     *
+     * @var string
+     */
+    protected $_accum;
+
+    /**
+     * TODO
+     *
+     * @var array
+     */
+    protected $_revsym = array();
+
+    /**
+     * TODO
+     *
+     * @var array
+     */
+    protected $_symrev = array();
+
+    /**
+     * TODO
+     *
+     * @var array
+     */
+    protected $_revlist = array();
+
+    /**
+     * TODO
+     *
+     * @var array
+     */
+    protected $_branches = array();
+
+    private $_initialized;
+
+    protected function _init()
+    {
+        /* Check that we are actually in the filesystem. */
+        $file = $this->_dir . '/' . $this->_name;
+        if (!is_file($file)) {
+            throw new Horde_Vcs_Exception('File Not Found: ' . $file);
+        }
+
+        $ret_array = array();
+        $cmd = escapeshellcmd($this->_rep->getPath('rlog')) . ($this->_quicklog ? ' -r' : '') . ' ' . escapeshellarg($file);
+        exec($cmd, $ret_array, $retval);
+
+        if ($retval) {
+            throw new Horde_Vcs_Exception('Failed to spawn rlog to retrieve file log information for ' . $file);
+        }
+
+        $branches = array();
+        $state = 'init';
+
+        foreach ($ret_array as $line) {
+            switch ($state) {
+            case 'init':
+                if (strpos($line, 'head: ') === 0) {
+                    $this->_branches['HEAD'] = substr($line, 6);
+                    $this->_revlist['HEAD'] = $this->_rep->getRevisionRange($this, '1.1', $this->_branches['HEAD']);
+                } elseif (strpos($line, 'branch:') === 0) {
+                    $state = 'rev';
+                }
+                break;
+
+            case 'rev':
+                if (strpos($line, '----------') === 0) {
+                    $state = 'info';
+                } elseif (preg_match("/^\s+([^:]+):\s+([\d\.]+)/", $line, $regs)) {
+                    // Check to see if this is a branch.
+                    if (preg_match('/^(\d+(\.\d+)+)\.0\.(\d+)$/', $regs[2])) {
+                        $rev = $regs[2];
+                        $end = strrpos($rev, '.');
+                        $rev[$end] = 0;
+                        $branchRev = (($end2 = strrpos($rev, '.')) === false)
+                            ? substr($rev, ++$end)
+                            : substr_replace($rev, '.', $end2, ($end - $end2 + 1));
+
+                        /* $branchRev is only the branching point, NOT the
+                         * HEAD of the branch. To determine the HEAD, we need
+                         * to parse all of the log data first. Yuck. */
+                        $branches[$regs[1]] = $branchRev . '.';
+                    } else {
+                        $this->_symrev[$regs[1]] = $regs[2];
+                        if (empty($this->_revsym[$regs[2]])) {
+                            $this->_revsym[$regs[2]] = array();
+                        }
+                        $this->_revsym[$regs[2]][] = $regs[1];
+                    }
+                }
+                break;
+
+            case 'info':
+                if ((strpos($line, '==============================') === false) &&
+                    (strpos($line, '----------------------------') === false)) {
+                    $this->_accum[] = $line;
+                } elseif (count($this->_accum)) {
+                    $log = $this->_rep->getLogObject($this, null);
+                    $rev = $log->queryRevision();
+                    $onbranch = false;
+                    $onhead = (substr_count($rev, '.') == 1);
+
+                    // Determine branch information.
+                    if ($onhead) {
+                        $onbranch = (empty($this->_branch) || $this->_branch == 'HEAD') ||
+                            ($this->_rep->cmp($branches[$this->_branch], $rev) === 1);
+                    } elseif ($this->_branch != 'HEAD') {
+                        foreach ($branches as $key => $val) {
+                            if (strpos($rev, $val) === 0) {
+                                $onbranch = true;
+                                $log->setBranch($key);
+                                if (!isset($this->_branches[$key])) {
+                                    $this->_branches[$key] = $rev;
+                                    $this->_revlist[$key] = $this->_rep->getRevisionRange($this, '1.1', $rev);
+                                }
+                                break;
+                            }
+                        }
+                    }
+
+                    if ($onbranch) {
+                        $this->_revs[] = $rev;
+                        $this->logs[$rev] = $log;
+                    }
+
+                    $this->_accum = array();
+                }
+                break;
+            }
+        }
+    }
+
+    protected function _ensureRevisionsInitialized()
+    {
+        if(!$this->_initialized) {
+            $this->_init();
+            $this->_initialized = true;
+        }
+    }
+
+    protected function _ensureLogsInitialized()
+    {
+        if(!$this->_initialized) {
+            $this->_init();
+            $this->_initialized = true;
+        }
+    }
+
+    /**
+     * If this file is present in an Attic directory, this indicates it.
+     *
+     * @return boolean  True if file is in the Attic, and false otherwise
+     */
+    public function isDeleted()
+    {
+        return (substr($this->_dir, -5) == 'Attic');
+    }
+
+    /**
+     * Returns name of the current file without the repository
+     * extensions (usually ,v)
+     *
+     * @return string  Filename without repository extension
+     */
+    public function queryName()
+    {
+        return preg_replace('/,v$/', '', $this->_name);
+    }
+
+    /**
+     * Return the fully qualified filename of this object.
+     *
+     * @return Fully qualified filename of this object
+     */
+    public function queryFullPath()
+    {
+        return parent::queryModulePath();
+    }
+
+    /**
+     * Return the name of this file relative to its sourceroot.
+     *
+     * @return string  Pathname relative to the sourceroot.
+     */
+    public function queryModulePath()
+    {
+        return preg_replace('|^'. $this->_rep->sourceroot() . '/?(.*),v$|', '\1', $this->queryFullPath());
+    }
+
+    /**
+     * TODO
+     */
+    public function getBranchList()
+    {
+        return $this->_revlist();
+    }
+
+    /**
+     * TODO
+     */
+    public function queryRevsym($rev)
+    {
+        return isset($this->_revsym[$rev])
+            ? $this->_revsym[$rev]
+            : array();
+    }
+
+    /**
+     * TODO
+     */
+    public function querySymbolicRevisions()
+    {
+        return $this->_symrev;
+    }
+
+    /**
+     * TODO
+     */
+    public function getAccum()
+    {
+        return $this->_accum;
+    }
+
+    /**
+     * TODO
+     */
+     public function queryBranches()
+     {
+         return $this->_branches;
+     }
+
+}
\ No newline at end of file
diff --git a/framework/Vcs/lib/Horde/Vcs/File/Git.php b/framework/Vcs/lib/Horde/Vcs/File/Git.php
new file mode 100644 (file)
index 0000000..9eb46c0
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Horde_Vcs_Git file class.
+ *
+ * Copyright 2008-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Vcs
+ */
+class Horde_Vcs_File_Git extends Horde_Vcs_File
+{
+    /**
+     * The master list of revisions for this file.
+     *
+     * @var array
+     */
+    protected $_revlist = array();
+
+    /**
+     * Have we initalized logs and revisions?
+     *
+     * @var boolean
+     */
+    private $_initialized = false;
+
+    protected function _ensureRevisionsInitialized()
+    {
+        if (!$this->_initialized) { $this->_init(); }
+        $this->_initialized = true;
+    }
+
+    protected function _ensureLogsInitialized()
+    {
+        if (!$this->_initialized) { $this->_init(); }
+        $this->_initialized = true;
+    }
+
+    protected function _init()
+    {
+        $log_list = null;
+
+        /* First, grab the master list of revisions. If quicklog is specified,
+         * we don't need this master list - we are only concerned about the
+         * most recent revision for the given branch. */
+        if ($this->_quicklog) {
+            $branchlist = empty($this->_branch)
+                ? array($this->_rep->getDefaultBranch())
+                : array($this->_branch);
+        } else {
+            if (version_compare($this->_rep->version, '1.6.0', '>=')) {
+                $cmd = $this->_rep->getCommand() . ' rev-list --branches -- ' . escapeshellarg($this->queryModulePath()) . ' 2>&1';
+            } else {
+                $cmd = $this->_rep->getCommand() . ' branch -v --no-abbrev';
+                exec($cmd, $branch_heads);
+                if (stripos($branch_heads[0], 'fatal') === 0) {
+                    throw new Horde_Vcs_Exception(implode(', ', $branch_heads));
+                }
+                foreach ($branch_heads as &$hd) {
+                    $line = explode(' ', substr($hd, 2));
+                    $hd = $line[1];
+                }
+
+                $cmd = $this->_rep->getCommand() . ' rev-list ' . implode(' ', $branch_heads) . ' -- ' . escapeshellarg($this->queryModulePath()) . ' 2>&1';
+            }
+
+            exec($cmd, $revs);
+            if (count($revs) == 0) {
+                if (!$this->_rep->isFile($this->queryModulePath(), isset($opts['branch']) ? $opts['branch'] : null)) {
+                    throw new Horde_Vcs_Exception('No such file: ' . $this->queryModulePath());
+                } else {
+                    throw new Horde_Vcs_Exception('No revisions found');
+                }
+            }
+
+            if (stripos($revs[0], 'fatal') === 0) {
+                throw new Horde_Vcs_Exception(implode(', ', $revs));
+            }
+
+            $this->_revs = $revs;
+
+            $branchlist = array_keys($this->queryBranches());
+        }
+
+        /* Get the list of revisions. Need to get all revisions, not just
+         * those on $this->_branch, for branch determination reasons. */
+        foreach ($branchlist as $key) {
+            $revs = array();
+            $cmd = $this->_rep->getCommand() . ' rev-list ' . ($this->_quicklog ? '-n 1' : '') . ' ' . escapeshellarg($key) . ' -- ' . escapeshellarg($this->queryModulePath()) . ' 2>&1';
+            exec($cmd, $revs);
+
+            if (!empty($revs)) {
+                if (stripos($revs[0], 'fatal') === 0) {
+                    throw new Horde_Vcs_Exception(implode(', ', $revs));
+                }
+
+                $this->_revlist[$key] = $revs;
+
+                if (!empty($this->_branch) && ($this->_branch == $key)) {
+                    $log_list = $revs;
+                }
+
+                if ($this->_quicklog) {
+                    $this->_revs[] = reset($revs);
+                }
+            }
+        }
+
+        if (is_null($log_list)) {
+            $log_list = ($this->_quicklog || empty($this->_branch))
+                ? $this->_revs
+                : array();
+        }
+
+        foreach ($log_list as $val) {
+            $this->logs[$val] = $this->_rep->getLogObject($this, $val);
+        }
+    }
+
+    /**
+     * Get the hash name for this file at a specific revision.
+     *
+     * @param string $rev  Revision string.
+     *
+     * @return string  Commit hash.
+     */
+    public function getHashForRevision($rev)
+    {
+        if (!isset($this->logs[$rev])) {
+            throw new Horde_Vcs_Exception('This file doesn\'t exist at that revision');
+        }
+        return $this->logs[$rev]->getHashForPath($this->queryModulePath());
+    }
+
+    /**
+     * Return the name of this file relative to its sourceroot.
+     *
+     * @return string  Pathname relative to the sourceroot.
+     */
+    public function queryModulePath()
+    {
+        return ($this->_dir == '.')
+            ? $this->_name
+            : parent::queryModulePath();
+    }
+
+    /**
+     * TODO
+     */
+    public function getBranchList()
+    {
+        return $this->_revlist;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryBranch($rev)
+    {
+        $branches = array();
+
+        foreach (array_keys($this->_revlist) as $val) {
+            if (array_search($rev, $this->_revlist[$val]) !== false) {
+                $branches[] = $val;
+            }
+        }
+
+        return $branches;
+    }
+
+    /**
+     * Return the "base" filename (i.e. the filename needed by the various
+     * command line utilities).
+     *
+     * @return string  A filename.
+     */
+    public function queryPath()
+    {
+        return $this->queryModulePath();
+    }
+
+    /**
+     * TODO
+     */
+    public function queryBranches()
+    {
+        /* If dealing with a branch that is not explicitly named (i.e. an
+         * implicit branch for a given tree-ish commit ID), we need to add
+         * that information to the branch list. */
+        $revlist = $this->_rep->getBranchList();
+        if (!empty($this->_branch) &&
+            !in_array($this->_branch, $revlist)) {
+            $revlist[$this->_branch] = $this->_branch;
+        }
+        return $revlist;
+    }
+
+   /**
+     * Return the last Horde_Vcs_Log object in the file.
+     *
+     * @return Horde_Vcs_Log  Log object of the last entry in the file.
+     * @throws Horde_Vcs_Exception
+     */
+    public function queryLastLog()
+    {
+        if (empty($this->_branch)) {
+            return parent::queryLastLog();
+        }
+
+        $rev = reset($this->_revlist[$this->_branch]);
+        if (!is_null($rev)) {
+            if (isset($this->logs[$rev])) {
+                return $this->logs[$rev];
+            }
+        }
+
+        throw new Horde_Vcs_Exception('No revisions');
+    }
+}
\ No newline at end of file
index 1930e3e..7a3c2ec 100644 (file)
@@ -313,553 +313,4 @@ class Horde_Vcs_Git extends Horde_Vcs
         return 'master';
     }
 
-}
-
-/**
- * Horde_Vcs_Git directory class.
- *
- * @author  Chuck Hagenbuch <chuck@horde.org>
- * @author  Michael Slusarz <slusarz@horde.org>
- * @package Horde_Vcs
- */
-class Horde_Vcs_Directory_Git extends Horde_Vcs_Directory
-{
-    /**
-     * The current branch.
-     *
-     * @var string
-     */
-    protected $_branch;
-
-    /**
-     * Create a Directory object to store information about the files in a
-     * single directory in the repository.
-     *
-     * @param Horde_Vcs $rep  The Repository object this directory is part of.
-     * @param string $dn      Path to the directory.
-     * @param array $opts     TODO
-     *
-     * @throws Horde_Vcs_Exception
-     */
-    public function __construct($rep, $dn, $opts = array())
-    {
-        parent::__construct($rep, $dn, $opts);
-
-        $this->_branch = empty($opts['rev'])
-            ? $rep->getDefaultBranch()
-            : $opts['rev'];
-
-        // @TODO See if we have a valid cache of the tree at this revision
-
-        $dir = $this->queryDir();
-        if (substr($dir, 0, 1) == '/') {
-            $dir = (string)substr($dir, 1);
-        }
-        if (strlen($dir) && substr($dir, -1) != '/') {
-            $dir .= '/';
-        }
-
-        $cmd = $rep->getCommand() . ' ls-tree --full-name ' . escapeshellarg($this->_branch) . ' ' . escapeshellarg($dir) . ' 2>&1';
-        $stream = popen($cmd, 'r');
-        if (!$stream) {
-            throw new Horde_Vcs_Exception('Failed to execute git ls-tree: ' . $cmd);
-        }
-
-        // Create two arrays - one of all the files, and the other of
-        // all the dirs.
-        while (!feof($stream)) {
-            $line = fgets($stream);
-            if ($line === false) { break; }
-
-            $line = rtrim($line);
-            if (!strlen($line))  { continue; }
-
-            list(, $type, , $file) = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
-            if ($type == 'tree') {
-                $this->_dirs[] = basename($file);
-            } else {
-                $this->_files[] = $rep->getFileObject($file, array('branch' => $this->_branch, 'quicklog' => !empty($opts['quicklog'])));
-            }
-        }
-
-        pclose($stream);
-    }
-
-    /**
-     * TODO
-     */
-    public function getBranches()
-    {
-        $blist = array_keys($this->_rep->getBranchList());
-        if (!in_array($this->_branch, $blist)) {
-            $blist[] = $this->_branch;
-        }
-        return $blist;
-    }
-
-}
-
-/**
- * Horde_Vcs_Git file class.
- *
- * @author  Chuck Hagenbuch <chuck@horde.org>
- * @author  Michael Slusarz <slusarz@horde.org>
- * @package Horde_Vcs
- */
-class Horde_Vcs_File_Git extends Horde_Vcs_File
-{
-    /**
-     * The master list of revisions for this file.
-     *
-     * @var array
-     */
-    protected $_revlist = array();
-
-    /**
-     * Have we initalized logs and revisions?
-     *
-     * @var boolean
-     */
-    private $_initialized = false;
-
-    protected function _ensureRevisionsInitialized()
-    {
-        if (!$this->_initialized) { $this->_init(); }
-        $this->_initialized = true;
-    }
-
-    protected function _ensureLogsInitialized()
-    {
-        if (!$this->_initialized) { $this->_init(); }
-        $this->_initialized = true;
-    }
-
-    protected function _init()
-    {
-        $log_list = null;
-
-        /* First, grab the master list of revisions. If quicklog is specified,
-         * we don't need this master list - we are only concerned about the
-         * most recent revision for the given branch. */
-        if ($this->_quicklog) {
-            $branchlist = empty($this->_branch)
-                ? array($this->_rep->getDefaultBranch())
-                : array($this->_branch);
-        } else {
-            if (version_compare($this->_rep->version, '1.6.0', '>=')) {
-                $cmd = $this->_rep->getCommand() . ' rev-list --branches -- ' . escapeshellarg($this->queryModulePath()) . ' 2>&1';
-            } else {
-                $cmd = $this->_rep->getCommand() . ' branch -v --no-abbrev';
-                exec($cmd, $branch_heads);
-                if (stripos($branch_heads[0], 'fatal') === 0) {
-                    throw new Horde_Vcs_Exception(implode(', ', $branch_heads));
-                }
-                foreach ($branch_heads as &$hd) {
-                    $line = explode(' ', substr($hd, 2));
-                    $hd = $line[1];
-                }
-
-                $cmd = $this->_rep->getCommand() . ' rev-list ' . implode(' ', $branch_heads) . ' -- ' . escapeshellarg($this->queryModulePath()) . ' 2>&1';
-            }
-
-            exec($cmd, $revs);
-            if (count($revs) == 0) {
-                if (!$this->_rep->isFile($this->queryModulePath(), isset($opts['branch']) ? $opts['branch'] : null)) {
-                    throw new Horde_Vcs_Exception('No such file: ' . $this->queryModulePath());
-                } else {
-                    throw new Horde_Vcs_Exception('No revisions found');
-                }
-            }
-
-            if (stripos($revs[0], 'fatal') === 0) {
-                throw new Horde_Vcs_Exception(implode(', ', $revs));
-            }
-
-            $this->_revs = $revs;
-
-            $branchlist = array_keys($this->queryBranches());
-        }
-
-        /* Get the list of revisions. Need to get all revisions, not just
-         * those on $this->_branch, for branch determination reasons. */
-        foreach ($branchlist as $key) {
-            $revs = array();
-            $cmd = $this->_rep->getCommand() . ' rev-list ' . ($this->_quicklog ? '-n 1' : '') . ' ' . escapeshellarg($key) . ' -- ' . escapeshellarg($this->queryModulePath()) . ' 2>&1';
-            exec($cmd, $revs);
-
-            if (!empty($revs)) {
-                if (stripos($revs[0], 'fatal') === 0) {
-                    throw new Horde_Vcs_Exception(implode(', ', $revs));
-                }
-
-                $this->_revlist[$key] = $revs;
-
-                if (!empty($this->_branch) && ($this->_branch == $key)) {
-                    $log_list = $revs;
-                }
-
-                if ($this->_quicklog) {
-                    $this->_revs[] = reset($revs);
-                }
-            }
-        }
-
-        if (is_null($log_list)) {
-            $log_list = ($this->_quicklog || empty($this->_branch))
-                ? $this->_revs
-                : array();
-        }
-
-        foreach ($log_list as $val) {
-            $this->logs[$val] = $this->_rep->getLogObject($this, $val);
-        }
-    }
-
-    /**
-     * Get the hash name for this file at a specific revision.
-     *
-     * @param string $rev  Revision string.
-     *
-     * @return string  Commit hash.
-     */
-    public function getHashForRevision($rev)
-    {
-        if (!isset($this->logs[$rev])) {
-            throw new Horde_Vcs_Exception('This file doesn\'t exist at that revision');
-        }
-        return $this->logs[$rev]->getHashForPath($this->queryModulePath());
-    }
-
-    /**
-     * Return the name of this file relative to its sourceroot.
-     *
-     * @return string  Pathname relative to the sourceroot.
-     */
-    public function queryModulePath()
-    {
-        return ($this->_dir == '.')
-            ? $this->_name
-            : parent::queryModulePath();
-    }
-
-    /**
-     * TODO
-     */
-    public function getBranchList()
-    {
-        return $this->_revlist;
-    }
-
-    /**
-     * TODO
-     */
-    public function queryBranch($rev)
-    {
-        $branches = array();
-
-        foreach (array_keys($this->_revlist) as $val) {
-            if (array_search($rev, $this->_revlist[$val]) !== false) {
-                $branches[] = $val;
-            }
-        }
-
-        return $branches;
-    }
-
-    /**
-     * Return the "base" filename (i.e. the filename needed by the various
-     * command line utilities).
-     *
-     * @return string  A filename.
-     */
-    public function queryPath()
-    {
-        return $this->queryModulePath();
-    }
-
-    /**
-     * TODO
-     */
-    public function queryBranches()
-    {
-        /* If dealing with a branch that is not explicitly named (i.e. an
-         * implicit branch for a given tree-ish commit ID), we need to add
-         * that information to the branch list. */
-        $revlist = $this->_rep->getBranchList();
-        if (!empty($this->_branch) &&
-            !in_array($this->_branch, $revlist)) {
-            $revlist[$this->_branch] = $this->_branch;
-        }
-        return $revlist;
-    }
-
-   /**
-     * Return the last Horde_Vcs_Log object in the file.
-     *
-     * @return Horde_Vcs_Log  Log object of the last entry in the file.
-     * @throws Horde_Vcs_Exception
-     */
-    public function queryLastLog()
-    {
-        if (empty($this->_branch)) {
-            return parent::queryLastLog();
-        }
-
-        $rev = reset($this->_revlist[$this->_branch]);
-        if (!is_null($rev)) {
-            if (isset($this->logs[$rev])) {
-                return $this->logs[$rev];
-            }
-        }
-
-        throw new Horde_Vcs_Exception('No revisions');
-    }
-}
-
-/**
- * Horde_Vcs_Git log class.
- *
- * @author  Chuck Hagenbuch <chuck@horde.org>
- * @author  Michael Slusarz <slusarz@horde.org>
- * @package Horde_Vcs
- */
-class Horde_Vcs_Log_Git extends Horde_Vcs_Log
-{
-    /**
-     * @var string
-     */
-    protected $_parent = null;
-
-    /**
-     * @var boolean
-     */
-    private $_initialized;
-
-    protected function _ensureInitialized()
-    {
-        if (!$this->_initialized) {
-            $this->_init();
-            $this->_initialized = true;
-        }
-    }
-
-    protected function _init()
-    {
-        /* Get diff statistics. */
-        $stats = array();
-        $cmd = $this->_rep->getCommand() . ' diff-tree --numstat ' . escapeshellarg($this->_rev);
-        exec($cmd, $output);
-
-        reset($output);
-        // Skip the first entry (it is the revision number)
-        next($output);
-        while (list(,$v) = each($output)) {
-            $tmp = explode("\t", $v);
-            $stats[$tmp[2]] = array_slice($tmp, 0, 2);
-        }
-
-        // @TODO use Commit, CommitDate, and Merge properties
-        $cmd = $this->_rep->getCommand() . ' whatchanged --no-color --pretty=format:"Rev:%H%nParents:%P%nAuthor:%an <%ae>%nAuthorDate:%at%nRefs:%d%n%n%s%n%b" --no-abbrev -n 1 ' . escapeshellarg($this->_rev);
-        $pipe = popen($cmd, 'r');
-        if (!is_resource($pipe)) {
-            throw new Horde_Vcs_Exception('Unable to run ' . $cmd . ': ' . error_get_last());
-        }
-
-        while (true) {
-            $line = trim(fgets($pipe));
-            if (!strlen($line)) { break; }
-            if (strpos($line, ':') === false) {
-                throw new Horde_Vcs_Exception('Malformed log line: ' . $line);
-            }
-
-            list($key, $value) = explode(':', $line, 2);
-            $value = trim($value);
-
-            switch (trim($key)) {
-            case 'Rev':
-                if ($this->_rev != $value) {
-                    fclose($pipe);
-                    throw new Horde_Vcs_Exception('Expected ' . $this->_rev . ', got ' . $value);
-                }
-                break;
-
-            case 'Parents':
-                // @TODO: More than 1 parent?
-                $this->_parent = $value;
-                break;
-
-            case 'Author':
-                $this->_author = $value;
-                break;
-
-            case 'AuthorDate':
-                $this->_date = $value;
-                break;
-
-            case 'Refs':
-                if ($value) {
-                    $value = substr($value, 1, -1);
-                    foreach (explode(',', $value) as $val) {
-                        $val = trim($val);
-                        if (strpos($val, 'refs/tags/') === 0) {
-                            $this->_tags[] = substr($val, 10);
-                        }
-                    }
-                    if (!empty($this->_tags)) {
-                        sort($this->_tags);
-                    }
-                }
-                break;
-            }
-        }
-
-        $log = '';
-        $line = fgets($pipe);
-        while ($line !== false && substr($line, 0, 1) != ':') {
-            $log .= $line;
-            $line = fgets($pipe);
-        }
-        $this->_log = trim($log);
-
-        // Build list of files in this revision. The format of these lines is
-        // documented in the git diff-tree documentation:
-        // http://www.kernel.org/pub/software/scm/git/docs/git-diff-tree.html
-        while ($line) {
-            preg_match('/:(\d+) (\d+) (\w+) (\w+) (.+)\t(.+)(\t(.+))?/', $line, $matches);
-
-            $statinfo = isset($stats[$matches[6]])
-                ? array('added' => $stats[$matches[6]][0], 'deleted' => $stats[$matches[6]][1])
-                : array();
-
-            $this->_files[$matches[6]] = array_merge(array(
-                'srcMode' => $matches[1],
-                'dstMode' => $matches[2],
-                'srcSha1' => $matches[3],
-                'dstSha1' => $matches[4],
-                'status' => $matches[5],
-                'srcPath' => $matches[6],
-                'dstPath' => isset($matches[7]) ? $matches[7] : ''
-            ), $statinfo);
-
-            $line = fgets($pipe);
-        }
-
-        fclose($pipe);
-    }
-
-    /**
-     * TODO
-     */
-    public function getHashForPath($path)
-    {
-        $this->_ensureInitialized();
-        return $this->_files[$path]['dstSha1'];
-    }
-
-    /**
-     * TODO
-     */
-    public function queryBranch()
-    {
-        return $this->_file->queryBranch($this->_rev);
-    }
-
-    /**
-     * TODO
-     */
-    public function queryParent()
-    {
-        return $this->_parent;
-    }
-
-}
-
-/**
- * Horde_Vcs_Git Patchset class.
- *
- * @author  Chuck Hagenbuch <chuck@horde.org>
- * @author  Michael Slusarz <slusarz@horde.org>
- * @package Horde_Vcs
- */
-class Horde_Vcs_Patchset_Git extends Horde_Vcs_Patchset
-{
-    /**
-     * Constructor
-     *
-     * @param Horde_Vcs $rep  A Horde_Vcs repository object.
-     * @param array $opts     Additional options.
-     * <pre>
-     * 'file' - (string) The filename to produce patchsets for.
-     * 'range' - (array) The patchsets to process.
-     *           DEFAULT: None (all patchsets are processed).
-     * </pre>
-     */
-    public function __construct($rep, $opts = array())
-    {
-        $revs = array();
-
-        if (isset($opts['file'])) {
-            $ob = $rep->getFileObject($opts['file']);
-            $revs = $ob->queryLogs();
-        } elseif (!empty($opts['range'])) {
-            foreach ($opts['range'] as $val) {
-                /* Grab a filename in the patchset to get log info. */
-                $cmd = $rep->getCommand() . ' diff-tree --name-only -r ' . escapeshellarg($val);
-                exec($cmd, $output);
-
-                /* The first line is the SHA1 hash. */
-                $ob = $rep->getFileObject($output[1]);
-                $revs[$val] = $ob->queryLogs($val);
-            }
-        }
-
-        reset($revs);
-        while (list($rev, $log) = each($revs)) {
-            if (empty($log)) {
-                continue;
-            }
-
-            $this->_patchsets[$rev] = array(
-                'date' => $log->queryDate(),
-                'author' => $log->queryAuthor(),
-                'branches' => $log->queryBranch(),
-                'tags' => $log->queryTags(),
-                'log' => $log->queryLog(),
-                'members' => array(),
-            );
-
-            foreach ($log->queryFiles() as $file) {
-                $to = $rev;
-                $status = self::MODIFIED;
-
-                switch ($file['status']) {
-                case 'A':
-                    $from = null;
-                    $status = self::ADDED;
-                    break;
-
-                case 'D':
-                    $from = $to;
-                    $to = null;
-                    $status = self::DELETED;
-                    break;
-
-                default:
-                    $from = $log->queryParent();
-                }
-
-                $statinfo = isset($file['added'])
-                    ? array('added' => $file['added'], 'deleted' => $file['deleted'])
-                    : array();
-
-                $this->_patchsets[$rev]['members'][] = array_merge(array(
-                    'file' => $file['srcPath'],
-                    'from' => $from,
-                    'status' => $status,
-                    'to' => $to,
-                ), $statinfo);
-            }
-        }
-    }
-
-}
+}
\ No newline at end of file
diff --git a/framework/Vcs/lib/Horde/Vcs/Log.php b/framework/Vcs/lib/Horde/Vcs/Log.php
new file mode 100644 (file)
index 0000000..6645db7
--- /dev/null
@@ -0,0 +1,155 @@
+<?php
+/**
+ * Horde_Vcs log class.
+ *
+ * Copyright 2008-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package Horde_Vcs
+ */
+abstract class Horde_Vcs_Log
+{
+    protected $_rep;
+    protected $_file;
+    protected $_files = array();
+    protected $_rev;
+    protected $_author;
+    protected $_tags = array();
+    protected $_date;
+    protected $_log;
+    protected $_state;
+    protected $_lines = '';
+    protected $_branches = array();
+
+    /**
+     * Constructor.
+     */
+    public function __construct($rev)
+    {
+        $this->_rev = $rev;
+    }
+
+    protected function _ensureInitialized()
+    {
+    }
+
+    /**
+     * When serializing, don't return the repository object
+     */
+    public function __sleep()
+    {
+        return array_diff(array_keys(get_object_vars($this)), array('_file', '_rep'));
+    }
+
+    /**
+     * TODO
+     */
+    public function setRepository($rep)
+    {
+        $this->_rep = $rep;
+    }
+
+    public function setFile(Horde_Vcs_File $file)
+    {
+        $this->_file = $file;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryRevision()
+    {
+        $this->_ensureInitialized();
+        return $this->_rev;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryDate()
+    {
+        $this->_ensureInitialized();
+        return $this->_date;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryAuthor()
+    {
+        $this->_ensureInitialized();
+        return $this->_author;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryLog()
+    {
+        $this->_ensureInitialized();
+        return $this->_log;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryBranch()
+    {
+        $this->_ensureInitialized();
+        return array();
+    }
+
+    /**
+     * TODO
+     */
+    public function queryChangedLines()
+    {
+        $this->_ensureInitialized();
+        return $this->_lines;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryTags()
+    {
+        $this->_ensureInitialized();
+        return $this->_tags;
+    }
+
+    /**
+     * Given a branch revision number, this function remaps it
+     * accordingly, and performs a lookup on the file object to
+     * return the symbolic name(s) of that branch in the tree.
+     *
+     * @return array  Hash of symbolic names => branch numbers.
+     */
+    public function querySymbolicBranches()
+    {
+        $this->_ensureInitialized();
+
+        $symBranches = array();
+        $branches = $this->_file->queryBranches();
+
+        foreach ($this->_branches as $branch) {
+            if (($key = array_search($branch, $branches)) !== false) {
+                $symBranches[$key] = $branch;
+            }
+        }
+
+        return $symBranches;
+    }
+
+    /**
+     * TODO
+     */
+    public function queryFiles($file = null)
+    {
+        $this->_ensureInitialized();
+        return is_null($file)
+            ? $this->_files
+            : (isset($this->_files[$file]) ? $this->_files[$file] : array());
+    }
+}
diff --git a/framework/Vcs/lib/Horde/Vcs/Log/Cvs.php b/framework/Vcs/lib/Horde/Vcs/Log/Cvs.php
new file mode 100644 (file)
index 0000000..17aa849
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+/**
+ * Horde_Vcs_cvs Log class.
+ *
+ * Copyright 2000-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anil Madhavapeddy <anil@recoil.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Vcs
+ */
+class Horde_Vcs_Log_Cvs extends Horde_Vcs_Log
+{
+    /**
+     * Cached branch info.
+     *
+     * @var string
+     */
+    protected $_branch;
+
+    private $_initialized;
+
+    protected function _init()
+    {
+       $raw = $this->_file->getAccum();
+
+        /* Initialise a simple state machine to parse the output of rlog */
+        $state = 'init';
+        while (!empty($raw) && $state != 'done') {
+            switch ($state) {
+            /* Found filename, now looking for the revision number */
+            case 'init':
+                $line = array_shift($raw);
+                if (preg_match("/revision (.+)$/", $line, $parts)) {
+                    $this->_rev = $parts[1];
+                    $state = 'date';
+                }
+                break;
+
+            /* Found revision and filename, now looking for date */
+            case 'date':
+                $line = array_shift($raw);
+                if (preg_match("|^date:\s+(\d+)[-/](\d+)[-/](\d+)\s+(\d+):(\d+):(\d+).*?;\s+author:\s+(.+);\s+state:\s+(\S+);(\s+lines:\s+([0-9\s+-]+))?|", $line, $parts)) {
+                    $this->_date = gmmktime($parts[4], $parts[5], $parts[6], $parts[2], $parts[3], $parts[1]);
+                    $this->_author = $parts[7];
+                    $this->_state = $parts[8];
+                    $this->_lines = isset($parts[10]) ? $parts[10] : '';
+                    $state = 'branches';
+                }
+                break;
+
+            /* Look for a branch point here - format is 'branches:
+             * x.y.z; a.b.c;' */
+            case 'branches':
+                /* If we find a branch tag, process and pop it,
+                   otherwise leave input stream untouched */
+                if (!empty($raw) &&
+                    preg_match("/^branches:\s+(.*)/", $raw[0], $br)) {
+                    /* Get the list of branches from the string, and
+                     * push valid revisions into the branches array */
+                    $brs = preg_split('/;\s*/', $br[1]);
+                    foreach ($brs as $brpoint) {
+                        if ($this->_rep->isValidRevision($brpoint)) {
+                            $this->_branches[] = $brpoint;
+                        }
+                    }
+                    array_shift($raw);
+                }
+
+                $state = 'done';
+                break;
+            }
+        }
+
+        /* Assume the rest of the lines are the log message */
+        $this->_log = implode("\n", $raw);
+        $this->_tags = $this->_file->queryRevsym($this->_rev);
+    }
+
+    protected function _ensureInitialized()
+    {
+        if (!$this->_initialized) {
+            $this->_init();
+            $this->_initialized = true;
+        }
+    }
+
+    /**
+     * TODO
+     */
+    public function setBranch($branch)
+    {
+        $this->_branch = array($branch);
+    }
+
+    /**
+     * TODO
+     */
+    public function queryBranch()
+    {
+        if (!empty($this->_branch)) {
+            return $this->_branch;
+        }
+
+        $branches = $this->_file->queryBranches();
+        $key = array_keys($branches, $this->_rev);
+        return empty($key)
+            ? array_keys($branches, $this->_rep->strip($this->_rev, 1))
+            : $key;
+    }
+
+}
\ No newline at end of file
diff --git a/framework/Vcs/lib/Horde/Vcs/Log/Git.php b/framework/Vcs/lib/Horde/Vcs/Log/Git.php
new file mode 100644 (file)
index 0000000..4f95193
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+/**
+ * Horde_Vcs_Git log class.
+ *
+ * Copyright 2008-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Vcs
+ */
+class Horde_Vcs_Log_Git extends Horde_Vcs_Log
+{
+    /**
+     * @var string
+     */
+    protected $_parent = null;
+
+    /**
+     * @var boolean
+     */
+    private $_initialized;
+
+    protected function _ensureInitialized()
+    {
+        if (!$this->_initialized) {
+            $this->_init();
+            $this->_initialized = true;
+        }
+    }
+
+    protected function _init()
+    {
+        /* Get diff statistics. */
+        $stats = array();
+        $cmd = $this->_rep->getCommand() . ' diff-tree --numstat ' . escapeshellarg($this->_rev);
+        exec($cmd, $output);
+
+        reset($output);
+        // Skip the first entry (it is the revision number)
+        next($output);
+        while (list(,$v) = each($output)) {
+            $tmp = explode("\t", $v);
+            $stats[$tmp[2]] = array_slice($tmp, 0, 2);
+        }
+
+        // @TODO use Commit, CommitDate, and Merge properties
+        $cmd = $this->_rep->getCommand() . ' whatchanged --no-color --pretty=format:"Rev:%H%nParents:%P%nAuthor:%an <%ae>%nAuthorDate:%at%nRefs:%d%n%n%s%n%b" --no-abbrev -n 1 ' . escapeshellarg($this->_rev);
+        $pipe = popen($cmd, 'r');
+        if (!is_resource($pipe)) {
+            throw new Horde_Vcs_Exception('Unable to run ' . $cmd . ': ' . error_get_last());
+        }
+
+        while (true) {
+            $line = trim(fgets($pipe));
+            if (!strlen($line)) { break; }
+            if (strpos($line, ':') === false) {
+                throw new Horde_Vcs_Exception('Malformed log line: ' . $line);
+            }
+
+            list($key, $value) = explode(':', $line, 2);
+            $value = trim($value);
+
+            switch (trim($key)) {
+            case 'Rev':
+                if ($this->_rev != $value) {
+                    fclose($pipe);
+                    throw new Horde_Vcs_Exception('Expected ' . $this->_rev . ', got ' . $value);
+                }
+                break;
+
+            case 'Parents':
+                // @TODO: More than 1 parent?
+                $this->_parent = $value;
+                break;
+
+            case 'Author':
+                $this->_author = $value;
+                break;
+
+            case 'AuthorDate':
+                $this->_date = $value;
+                break;
+
+            case 'Refs':
+                if ($value) {
+                    $value = substr($value, 1, -1);
+                    foreach (explode(',', $value) as $val) {
+                        $val = trim($val);
+                        if (strpos($val, 'refs/tags/') === 0) {
+                            $this->_tags[] = substr($val, 10);
+                        }
+                    }
+                    if (!empty($this->_tags)) {
+                        sort($this->_tags);
+                    }
+                }
+                break;
+            }
+        }
+
+        $log = '';
+        $line = fgets($pipe);
+        while ($line !== false && substr($line, 0, 1) != ':') {
+            $log .= $line;
+            $line = fgets($pipe);
+        }
+        $this->_log = trim($log);
+
+        // Build list of files in this revision. The format of these lines is
+        // documented in the git diff-tree documentation:
+        // http://www.kernel.org/pub/software/scm/git/docs/git-diff-tree.html
+        while ($line) {
+            preg_match('/:(\d+) (\d+) (\w+) (\w+) (.+)\t(.+)(\t(.+))?/', $line, $matches);
+
+            $statinfo = isset($stats[$matches[6]])
+                ? array('added' => $stats[$matches[6]][0], 'deleted' => $stats[$matches[6]][1])
+                : array();
+
+            $this->_files[$matches[6]] = array_merge(array(
+                'srcMode' => $matches[1],
+                'dstMode' => $matches[2],
+                'srcSha1' => $matches[3],
+                'dstSha1' => $matches[4],
+                'status' => $matches[5],
+                'srcPath' => $matches[6],
+                'dstPath' => isset($matches[7]) ? $matches[7] : ''
+            ), $statinfo);
+
+            $line = fgets($pipe);
+        }
+
+        fclose($pipe);
+    }
+
+    /**
+     * TODO
+     */
+    public function getHashForPath($path)
+    {
+        $this->_ensureInitialized();
+        return $this->_files[$path]['dstSha1'];
+    }
+
+    /**
+     * TODO
+     */
+    public function queryBranch()
+    {
+        return $this->_file->queryBranch($this->_rev);
+    }
+
+    /**
+     * TODO
+     */
+    public function queryParent()
+    {
+        return $this->_parent;
+    }
+
+}
\ No newline at end of file
diff --git a/framework/Vcs/lib/Horde/Vcs/Patchset.php b/framework/Vcs/lib/Horde/Vcs/Patchset.php
new file mode 100644 (file)
index 0000000..c0984ea
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Horde_Vcs patchset class.
+ *
+ * @package Horde_Vcs
+ */
+abstract class Horde_Vcs_Patchset
+{
+    const MODIFIED = 0;
+    const ADDED = 1;
+    const DELETED = 2;
+
+    /**
+     * @var array
+     */
+    protected $_patchsets = array();
+
+    /**
+     * Constructor
+     *
+     * @param Horde_Vcs $rep  A Horde_Vcs repository object.
+     * @param string $file    The filename to create patchsets for.
+     * @param array $opts     Additional options:
+     * <pre>
+     * 'range' - (array) The patchsets to process.
+     *           DEFAULT: None (all patchsets are processed).
+     * </pre>
+     */
+    abstract public function __construct($rep, $file, $opts = array());
+
+    /**
+     * TODO
+     *
+     * @return array  TODO
+     * 'date'
+     * 'author'
+     * 'branches'
+     * 'tags'
+     * 'log'
+     * 'members' - array:
+     *     'file'
+     *     'from'
+     *     'to'
+     *     'status'
+     */
+    public function getPatchsets()
+    {
+        return $this->_patchsets;
+    }
+}
diff --git a/framework/Vcs/lib/Horde/Vcs/Patchset/Cvs.php b/framework/Vcs/lib/Horde/Vcs/Patchset/Cvs.php
new file mode 100644 (file)
index 0000000..4802e3f
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+/**
+ * Horde_Vcs_Cvs Patchset class.
+ *
+ * Copyright 2000-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Anil Madhavapeddy <anil@recoil.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Vcs
+ */
+class Horde_Vcs_Patchset_Cvs extends Horde_Vcs_Patchset
+{
+    /**
+     * Constructor
+     *
+     * @param Horde_Vcs $rep  A Horde_Vcs repository object.
+     * @param string $file    The filename to create a patchset for.
+     * @param array $opts     Additional options.
+     * <pre>
+     * 'file' - (string) The filename to process.
+     *          REQUIRED for this driver.
+     * 'range' - (array) The patchsets to process.
+     *           DEFAULT: None (all patchsets are processed).
+     * </pre>
+     *
+     * @throws Horde_Vcs_Exception
+     */
+    public function __construct($rep, $opts = array())
+    {
+        $file = $rep->sourceroot() . '/' . $opts['file'];
+
+        /* Check that we are actually in the filesystem. */
+        if (!$rep->isFile($file)) {
+            throw new Horde_Vcs_Exception('File Not Found');
+        }
+
+        /* Call cvsps to retrieve all patchsets for this file. */
+        $cvsps_home = $rep->getPath('cvsps_home');
+        $HOME = !empty($cvsps_home) ?
+            'HOME=' . escapeshellarg($cvsps_home) . ' ' :
+            '';
+
+        $rangecmd = empty($opts['range'])
+            ? ''
+            : ' -s ' . escapeshellarg(implode(',', $opts['range']));
+
+        $ret_array = array();
+        $cmd = $HOME . escapeshellcmd($rep->getPath('cvsps')) . $rangecmd . ' -u --cvs-direct --root ' . escapeshellarg($rep->sourceroot()) . ' -f ' . escapeshellarg(basename($file)) . ' ' . escapeshellarg(dirname($file));
+        exec($cmd, $ret_array, $retval);
+        if ($retval) {
+            throw new Horde_Vcs_Exception('Failed to spawn cvsps to retrieve patchset information.');
+        }
+
+        $state = 'begin';
+        reset($ret_array);
+        while (list(,$line) = each($ret_array)) {
+            $line = trim($line);
+
+            if ($line == '---------------------') {
+                $state = 'begin';
+                continue;
+            }
+
+            switch ($state) {
+            case 'begin':
+                $id = str_replace('PatchSet ', '', $line);
+                $this->_patchsets[$id] = array();
+                $state = 'info';
+                break;
+
+            case 'info':
+                $info = explode(':', $line, 2);
+                $info[1] = ltrim($info[1]);
+
+                switch ($info[0]) {
+                case 'Date':
+                    $d = new DateTime($info[1]);
+                    $this->_patchsets[$id]['date'] = $d->format('U');
+                    break;
+
+                case 'Author':
+                    $this->_patchsets[$id]['author'] = $info[1];
+                    break;
+
+                case 'Branch':
+                    $this->_patchsets[$id]['branches'] = ($info[1] == 'HEAD')
+                        ? array()
+                        : array($info[1]);
+                    break;
+
+                case 'Tag':
+                    $this->_patchsets[$id]['tags'] = ($info[1] == '(none)')
+                        ? array()
+                        : array($info[1]);
+                    break;
+
+                case 'Log':
+                    $state = 'log';
+                    $this->_patchsets[$id]['log'] = '';
+                    break;
+                }
+                break;
+
+            case 'log':
+                if ($line == 'Members:') {
+                    $state = 'members';
+                    $this->_patchsets[$id]['log'] = rtrim($this->_patchsets[$id]['log']);
+                    $this->_patchsets[$id]['members'] = array();
+                } else {
+                    $this->_patchsets[$id]['log'] .= $line . "\n";
+                }
+                break;
+
+            case 'members':
+                if (!empty($line)) {
+                    $parts = explode(':', $line);
+                    list($from, $to) = explode('->', $parts[1], 2);
+                    $status = self::MODIFIED;
+
+                    if ($from == 'INITIAL') {
+                        $from = null;
+                        $status = self::ADDED;
+                    } elseif (substr($to, -6) == '(DEAD)') {
+                        $to = null;
+                        $status = self::DELETED;
+                    }
+
+                    $this->_patchsets[$id]['members'][] = array(
+                        'file' => $parts[0],
+                        'from' => $from,
+                        'status' => $status,
+                        'to' => $to
+                    );
+                }
+                break;
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/framework/Vcs/lib/Horde/Vcs/Patchset/Git.php b/framework/Vcs/lib/Horde/Vcs/Patchset/Git.php
new file mode 100644 (file)
index 0000000..e7a8d96
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Horde_Vcs_Git Patchset class.
+ *
+ * Copyright 2008-2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Vcs
+ */
+class Horde_Vcs_Patchset_Git extends Horde_Vcs_Patchset
+{
+    /**
+     * Constructor
+     *
+     * @param Horde_Vcs $rep  A Horde_Vcs repository object.
+     * @param array $opts     Additional options.
+     * <pre>
+     * 'file' - (string) The filename to produce patchsets for.
+     * 'range' - (array) The patchsets to process.
+     *           DEFAULT: None (all patchsets are processed).
+     * </pre>
+     */
+    public function __construct($rep, $opts = array())
+    {
+        $revs = array();
+
+        if (isset($opts['file'])) {
+            $ob = $rep->getFileObject($opts['file']);
+            $revs = $ob->queryLogs();
+        } elseif (!empty($opts['range'])) {
+            foreach ($opts['range'] as $val) {
+                /* Grab a filename in the patchset to get log info. */
+                $cmd = $rep->getCommand() . ' diff-tree --name-only -r ' . escapeshellarg($val);
+                exec($cmd, $output);
+
+                /* The first line is the SHA1 hash. */
+                $ob = $rep->getFileObject($output[1]);
+                $revs[$val] = $ob->queryLogs($val);
+            }
+        }
+
+        reset($revs);
+        while (list($rev, $log) = each($revs)) {
+            if (empty($log)) {
+                continue;
+            }
+
+            $this->_patchsets[$rev] = array(
+                'date' => $log->queryDate(),
+                'author' => $log->queryAuthor(),
+                'branches' => $log->queryBranch(),
+                'tags' => $log->queryTags(),
+                'log' => $log->queryLog(),
+                'members' => array(),
+            );
+
+            foreach ($log->queryFiles() as $file) {
+                $to = $rev;
+                $status = self::MODIFIED;
+
+                switch ($file['status']) {
+                case 'A':
+                    $from = null;
+                    $status = self::ADDED;
+                    break;
+
+                case 'D':
+                    $from = $to;
+                    $to = null;
+                    $status = self::DELETED;
+                    break;
+
+                default:
+                    $from = $log->queryParent();
+                }
+
+                $statinfo = isset($file['added'])
+                    ? array('added' => $file['added'], 'deleted' => $file['deleted'])
+                    : array();
+
+                $this->_patchsets[$rev]['members'][] = array_merge(array(
+                    'file' => $file['srcPath'],
+                    'from' => $from,
+                    'status' => $status,
+                    'to' => $to,
+                ), $statinfo);
+            }
+        }
+    }
+
+}
\ No newline at end of file
index f62e402..b79720b 100644 (file)
@@ -1,14 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
-http://pear.php.net/dtd/tasks-1.0.xsd
-http://pear.php.net/dtd/package-2.0
-http://pear.php.net/dtd/package-2.0.xsd">
+<package packagerversion="1.9.1" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
  <name>Vcs</name>
  <channel>pear.horde.org</channel>
  <summary>Version Control API</summary>
  <description>The Horde_VC package provides a generalized API to multiple
- version control systems.
- </description>
+ version control systems.</description>
  <lead>
   <name>Chuck Hagenbuch</name>
   <user>chuck</user>
@@ -27,7 +23,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
   <email>slusarz@horde.org</email>
   <active>yes</active>
  </lead>
- <date>2008-12-20</date>
+ <date>2010-10-15</date>
+ <time>14:02:52</time>
  <version>
   <release>0.1.0</release>
   <api>0.1.0</api>
@@ -37,21 +34,43 @@ http://pear.php.net/dtd/package-2.0.xsd">
   <api>beta</api>
  </stability>
  <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
- <notes>* Initial Horde 4 package.</notes>
+ <notes>
+* Initial Horde 4 package.
+ </notes>
  <contents>
-  <dir name="/">
+  <dir baseinstalldir="/" name="/">
    <dir name="lib">
     <dir name="Horde">
      <dir name="Vcs">
+      <dir name="Directory">
+       <file name="Cvs.php" role="php" />
+       <file name="Git.php" role="php" />
+      </dir> <!-- //lib/Horde/Vcs/Directory -->
+      <dir name="File">
+       <file name="Cvs.php" role="php" />
+       <file name="Git.php" role="php" />
+      </dir> <!-- //lib/Horde/Vcs/File -->
+      <dir name="Log">
+       <file name="Cvs.php" role="php" />
+       <file name="Git.php" role="php" />
+      </dir> <!-- //lib/Horde/Vcs/Log -->
+      <dir name="Patchset">
+       <file name="Cvs.php" role="php" />
+       <file name="Git.php" role="php" />
+      </dir> <!-- //lib/Horde/Vcs/Patchset -->
       <file name="Cvs.php" role="php" />
+      <file name="Directory.php" role="php" />
       <file name="Exception.php" role="php" />
+      <file name="File.php" role="php" />
       <file name="Git.php" role="php" />
+      <file name="Log.php" role="php" />
+      <file name="Patchset.php" role="php" />
       <file name="Rcs.php" role="php" />
       <file name="Svn.php" role="php" />
-     </dir> <!-- /lib/Horde/VC -->
+     </dir> <!-- //lib/Horde/Vcs -->
      <file name="Vcs.php" role="php" />
-    </dir> <!-- /lib/Horde -->
-   </dir> <!-- /lib -->
+    </dir> <!-- //lib/Horde -->
+   </dir> <!-- //lib -->
   </dir> <!-- / -->
  </contents>
  <dependencies>
@@ -79,17 +98,45 @@ http://pear.php.net/dtd/package-2.0.xsd">
  </dependencies>
  <phprelease>
   <filelist>
-   <install name="lib/Horde/Vcs/Cvs.php" as="Horde/Vcs/Cvs.php" />
-   <install name="lib/Horde/Vcs/Exception.php" as="Horde/Vcs/Exception.php" />
-   <install name="lib/Horde/Vcs/Git.php" as="Horde/Vcs/Git.php" />
-   <install name="lib/Horde/Vcs/Rcs.php" as="Horde/Vcs/Rcs.php" />
-   <install name="lib/Horde/Vcs/Svn.php" as="Horde/Vcs/Svn.php" />
-   <install name="lib/Horde/Vcs.php" as="Horde/Vcs.php" />
+   <install as="Horde/Vcs.php" name="lib/Horde/Vcs.php" />
+   <install as="Horde/Vcs/Cvs.php" name="lib/Horde/Vcs/Cvs.php" />
+   <install as="Horde/Vcs/Directory.php" name="lib/Horde/Vcs/Directory.php" />
+   <install as="Horde/Vcs/Exception.php" name="lib/Horde/Vcs/Exception.php" />
+   <install as="Horde/Vcs/File.php" name="lib/Horde/Vcs/File.php" />
+   <install as="Horde/Vcs/Git.php" name="lib/Horde/Vcs/Git.php" />
+   <install as="Horde/Vcs/Log.php" name="lib/Horde/Vcs/Log.php" />
+   <install as="Horde/Vcs/Patchset.php" name="lib/Horde/Vcs/Patchset.php" />
+   <install as="Horde/Vcs/Rcs.php" name="lib/Horde/Vcs/Rcs.php" />
+   <install as="Horde/Vcs/Svn.php" name="lib/Horde/Vcs/Svn.php" />
+   <install as="Horde/Vcs/Directory/Cvs.php" name="lib/Horde/Vcs/Directory/Cvs.php" />
+   <install as="Horde/Vcs/Directory/Git.php" name="lib/Horde/Vcs/Directory/Git.php" />
+   <install as="Horde/Vcs/File/Cvs.php" name="lib/Horde/Vcs/File/Cvs.php" />
+   <install as="Horde/Vcs/File/Git.php" name="lib/Horde/Vcs/File/Git.php" />
+   <install as="Horde/Vcs/Log/Cvs.php" name="lib/Horde/Vcs/Log/Cvs.php" />
+   <install as="Horde/Vcs/Log/Git.php" name="lib/Horde/Vcs/Log/Git.php" />
+   <install as="Horde/Vcs/Patchset/Cvs.php" name="lib/Horde/Vcs/Patchset/Cvs.php" />
+   <install as="Horde/Vcs/Patchset/Git.php" name="lib/Horde/Vcs/Patchset/Git.php" />
   </filelist>
  </phprelease>
  <changelog>
   <release>
    <version>
+    <release>0.0.3</release>
+    <api>0.0.3</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2004-01-01</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+- Fix caching of Patchset objects.
+- Add a VC_Patchset parent class for common Patchset functionality.
+   </notes>
+  </release>
+  <release>
+   <version>
     <release>0.0.4</release>
     <api>0.0.4</api>
    </version>
@@ -99,24 +146,25 @@ http://pear.php.net/dtd/package-2.0.xsd">
    </stability>
    <date>2006-05-08</date>
    <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
-   <notes>* Converted to package.xml 2.0 for pear.horde.org
+   <notes>
+* Converted to package.xml 2.0 for pear.horde.org
 * Fix subversion diffs on files with spaces (helly@php.net, Bug #5244)
 * Pass username and password to the SVN client if set (duck@obala.net, Request #5958)
    </notes>
   </release>
   <release>
    <version>
-    <release>0.0.3</release>
-    <api>0.0.3</api>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
    </version>
    <stability>
     <release>beta</release>
     <api>beta</api>
    </stability>
-   <date>2004-01-01</date>
+   <date>2010-10-15</date>
    <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
-   <notes>- Fix caching of Patchset objects.
-- Add a VC_Patchset parent class for common Patchset functionality.
+   <notes>
+* Initial Horde 4 package.
    </notes>
   </release>
  </changelog>