Indefero

Indefero Git Source Tree


Root/src/IDF/Scm/Monotone.php

<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 CĂ©ondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
# ***** END LICENSE BLOCK ***** */

/**
 * Monotone scm class
 *
 * @author Thomas Keller <me@thomaskeller.biz>
 */
class IDF_Scm_Monotone extends IDF_Scm
{
    /** the minimum supported interface version */
    public static $MIN_INTERFACE_VERSION = 12.0;

    private $stdio;

    private static $instances = array();

    /**
     * @see IDF_Scm::__construct()
     */
    public function __construct($project)
    {
        $this->project = $project;
        $this->stdio = new IDF_Scm_Monotone_Stdio($project);
    }

    /**
     * Returns the stdio instance in use
     *
     * @return IDF_Scm_Monotone_Stdio
     */
    public function getStdio()
    {
        return $this->stdio;
    }

    /**
     * @see IDF_Scm::getRepositorySize()
     */
    public function getRepositorySize()
    {
        // FIXME: this obviously won't work with remote databases - upstream
        // needs to implement mtn db info in automate at first
        $repo = sprintf(Pluf::f('mtn_repositories'), $this->project->shortname);
        if (!file_exists($repo)) {
            return 0;
        }

        $cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
            .escapeshellarg($repo);
        $out = explode(' ',
                       self::shell_exec('IDF_Scm_Monotone::getRepositorySize', $cmd),
                       2);
        return (int) $out[0]*1024;
    }

    /**
     * @see IDF_Scm::isAvailable()
     */
    public function isAvailable()
    {
        try
        {
            $out = $this->stdio->exec(array('interface_version'));
            return floatval($out) >= self::$MIN_INTERFACE_VERSION;
        }
        catch (IDF_Scm_Exception $e) {}

        return false;
    }

    /**
     * @see IDF_Scm::getBranches()
     */
    public function getBranches()
    {
        if (isset($this->cache['branches'])) {
            return $this->cache['branches'];
        }
        // FIXME: we could / should introduce handling of suspended
        // (i.e. dead) branches here by hiding them from the user's eye...
        $out = $this->stdio->exec(array('branches'));

        // note: we could expand each branch with one of its head revisions
        // here, but these would soon become bogus anyway and we cannot
        // map multiple head revisions here either, so we just use the
        // selector as placeholder
        $res = array();
        foreach (preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY) as $b) {
            $res["h:$b"] = $b;
        }

        $this->cache['branches'] = $res;
        return $res;
    }

    /**
     * monotone has no concept of a "main" branch, so just return
     * the configured one. Ensure however that we can select revisions
     * with it at all.
     *
     * @see IDF_Scm::getMainBranch()
     */
    public function getMainBranch()
    {
        $conf = $this->project->getConf();
        if (false === ($branch = $conf->getVal('mtn_master_branch', false))
            || empty($branch)) {
            $branch = "*";
        }

        return $branch;
    }

    /**
     * @see IDF_Scm::getArchiveStream
     */
    public function getArchiveStream($commit, $prefix='repository/')
    {
        $revs = $this->_resolveSelector($commit);
        // sanity: this should actually not happen, because the
        // revision is validated before already
        if (count($revs) == 0) {
            return new Pluf_HTTP_Response_NotFound();
        }
        return new IDF_Scm_Monotone_ZipRender($this->stdio, $revs[0]);
    }

    /**
     * expands a selector or a partial revision id to zero, one or
     * multiple 40 byte revision ids
     *
     * @param string $selector
     * @return array
     */
    private function _resolveSelector($selector)
    {
        $out = $this->stdio->exec(array('select', $selector));
        return preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY);
    }

    /**
     * Queries the certs for a given revision and returns them in an
     * associative array array("branch" => array("branch1", ...), ...)
     *
     * @param string
     * @param array
     */
    private function _getCerts($rev)
    {
        $cache = Pluf_Cache::factory();
        $cachekey = 'mtn-plugin-certs-for-rev-' . $rev;
        $certs = $cache->get($cachekey);

        if ($certs === null) {
            $out = $this->stdio->exec(array('certs', $rev));

            $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
            $certs = array();
            foreach ($stanzas as $stanza) {
                $certname = null;
                foreach ($stanza as $stanzaline) {
                    // luckily, name always comes before value
                    if ($stanzaline['key'] == 'name') {
                        $certname = $stanzaline['values'][0];
                        continue;
                    }

                    if ($stanzaline['key'] == 'value') {
                        if (!array_key_exists($certname, $certs)) {
                            $certs[$certname] = array();
                        }

                        $certs[$certname][] = $stanzaline['values'][0];
                        break;
                    }
                }
            }
            $cache->set($cachekey, $certs);
        }

        return $certs;
    }

    /**
     * Returns unique certificate values for the given revs and the specific
     * cert name, optionally prefixed with $prefix
     *
     * @param array
     * @param string
     * @param string
     * @return array
     */
    private function _getUniqueCertValuesFor($revs, $certName, $prefix)
    {
        $certValues = array();
        foreach ($revs as $rev) {
            $certs = $this->_getCerts($rev);
            if (!array_key_exists($certName, $certs))
                continue;
            foreach ($certs[$certName] as $certValue) {
                $certValues[] = "$prefix$certValue";
            }
        }
        return array_unique($certValues);
    }

    /**
     * @see IDF_Scm::inBranches()
     */
    public function inBranches($commit, $path)
    {
        $revs = $this->_resolveSelector($commit);
        if (count($revs) == 0) return array();
        return $this->_getUniqueCertValuesFor($revs, 'branch', 'h:');
    }

    /**
     * @see IDF_Scm::getTags()
     */
    public function getTags()
    {
        if (isset($this->cache['tags'])) {
            return $this->cache['tags'];
        }

        $out = $this->stdio->exec(array('tags'));

        $tags = array();
        $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
        foreach ($stanzas as $stanza) {
            $tagname = null;
            foreach ($stanza as $stanzaline) {
                // revision comes directly after the tag stanza
                if ($stanzaline['key'] == 'tag') {
                    $tagname = $stanzaline['values'][0];
                    continue;
                }
                if ($stanzaline['key'] == 'revision') {
                    // FIXME: warn if multiple revisions have
                    // equally named tags
                    if (!array_key_exists("t:$tagname", $tags)) {
                        $tags["t:$tagname"] = $tagname;
                    }
                    break;
                }
            }
        }

        $this->cache['tags'] = $tags;
        return $tags;
    }

    /**
     * @see IDF_Scm::inTags()
     */
    public function inTags($commit, $path)
    {
        $revs = $this->_resolveSelector($commit);
        if (count($revs) == 0) return array();
        return $this->_getUniqueCertValuesFor($revs, 'tag', 't:');
    }

    /**
     * Takes a single stanza coming from an extended manifest output
     * and converts it into a file structure used by IDF
     *
     * @param string $forceBasedir  If given then the element's path is checked
     *                              to be directly beneath the given directory.
     *                              If not, null is returned and the parsing is
     *                              aborted.
     * @return array | null
     */
    private function _fillFileEntry(array $manifestEntry, $forceBasedir = null)
    {
        $fullpath = $manifestEntry[0]['values'][0];
        $filename = basename($fullpath);
        $dirname = dirname($fullpath);
        $dirname = $dirname == '.' ? '' : $dirname;

        if ($forceBasedir !== null && $forceBasedir != $dirname) {
            return null;
        }

        $file = array();
        $file['file'] = $filename;
        $file['fullpath'] = $fullpath;
        $file['efullpath'] = self::smartEncode($fullpath);

        $wanted_mark = '';
        if ($manifestEntry[0]['key'] == 'dir') {
            $file['type'] = 'tree';
            $file['size'] = 0;
            $wanted_mark = 'path_mark';
        }
        else {
            $file['type'] = 'blob';
            $file['hash'] = $manifestEntry[1]['hash'];
            $size = 0;
            foreach ($manifestEntry as $line) {
                if ($line['key'] == 'size') {
                    $size = $line['values'][0];
                    break;
                }
            }
            $file['size'] = $size;
            $wanted_mark = 'content_mark';
        }

        $rev_mark = null;
        foreach ($manifestEntry as $line) {
            if ($line['key'] == $wanted_mark) {
                $rev_mark = $line['hash'];
                break;
            }
        }

        if ($rev_mark !== null) {
            $file['rev'] = $rev_mark;
            $certs = $this->_getCerts($rev_mark);

            // FIXME: this assumes that author, date and changelog are always given
            $file['author'] = implode(", ", $certs['author']);

            $dates = array();
            foreach ($certs['date'] as $date)
                $dates[] = date('Y-m-d H:i:s', strtotime($date));
            $file['date'] = implode(', ', $dates);
            $combinedChangelog = implode("\n---\n", $certs['changelog']);
            $split = preg_split("/[\n\r]/", $combinedChangelog, 2);
            // FIXME: the complete log message is currently not used in the
            // tree view (the same is true for the other SCM implementations)
            // but we _should_ really use or at least return that here
            // in case we want to do fancy stuff like described in
            // issue 492
            $file['log'] =  $split[0];
        }

        return $file;
    }

    /**
     * @see IDF_Scm::getTree()
     */
    public function getTree($commit, $folder='/', $branch=null)
    {
        $revs = $this->_resolveSelector($commit);
        if (count($revs) == 0) {
            return array();
        }

        $out = $this->stdio->exec(array(
            'get_extended_manifest_of', $revs[0]
        ));

        $files = array();
        $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
        $folder = $folder == '/' || empty($folder) ? '' : $folder;

        foreach ($stanzas as $stanza) {
            if ($stanza[0]['key'] == 'format_version')
                continue;

            $file = $this->_fillFileEntry($stanza, $folder);
            if ($file === null)
                continue;

            $files[] = (object) $file;
        }
        return $files;
    }

    /**
     * @see IDF_Scm::findAuthor()
     */
    public function findAuthor($author)
    {
        // We extract anything which looks like an email.
        $match = array();
        if (!preg_match('/([^ ]+@[^ ]+)/', $author, $match)) {
            return null;
        }
        $sql = new Pluf_SQL('login=%s', array($match[1]));
        $users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
        if ($users->count() > 0) {
            return $users[0];
        }
        return Pluf::factory('IDF_EmailAddress')->get_user_for_email_address($match[1]);
    }

    /**
     * @see IDF_Scm::getAnonymousAccessUrl()
     */
    public static function getAnonymousAccessUrl($project, $commit = null)
    {
        $scm = IDF_Scm::get($project);
        $branch = $scm->getMainBranch();

        if (!empty($commit)) {
            $revs = $scm->_resolveSelector($commit);
            if (count($revs) > 0) {
                $certs = $scm->_getCerts($revs[0]);
                // for the very seldom case that a revision
                // has no branch certificate
                if (!array_key_exists('branch', $certs)) {
                    $branch = '*';
                }
                else
                {
                    $branch = $certs['branch'][0];
                }
            }
        }

        $remote_url = Pluf::f('mtn_remote_url', '');
        if (empty($remote_url)) {
            return '';
        }

        return sprintf($remote_url, $project->shortname).'?'.$branch;
    }

    /**
     * @see IDF_Scm::getAuthAccessUrl()
     */
    public static function getAuthAccessUrl($project, $user, $commit = null)
    {
        $url = self::getAnonymousAccessUrl($project, $commit);
        return preg_replace("#^ssh://#", "ssh://$user@", $url);
    }

    /**
     * Returns this object correctly initialized for the project.
     *
     * @param IDF_Project
     * @return IDF_Scm_Monotone
     */
    public static function factory($project)
    {
        if (!array_key_exists($project->shortname, self::$instances)) {
            self::$instances[$project->shortname] =
                new IDF_Scm_Monotone($project);
        }
        return self::$instances[$project->shortname];
    }

    /**
     * @see IDF_Scm::validateRevision()
     */
    public function validateRevision($commit)
    {
        $revs = $this->_resolveSelector($commit);
        if (count($revs) == 0)
            return IDF_Scm::REVISION_INVALID;

        if (count($revs) > 1)
            return IDF_Scm::REVISION_AMBIGUOUS;

        return IDF_Scm::REVISION_VALID;
    }

    /**
     * @see IDF_Scm::disambiguateRevision
     */
    public function disambiguateRevision($commit)
    {
        $revs = $this->_resolveSelector($commit);

        $out = array();
        foreach ($revs as $rev)
        {
            $certs = $this->_getCerts($rev);

            $log = array();
            $log['author'] = implode(', ', $certs['author']);

            $log['branch'] = implode(', ', $certs['branch']);

            $dates = array();
            foreach ($certs['date'] as $date)
                $dates[] = date('Y-m-d H:i:s', strtotime($date));
            $log['date'] = implode(', ', $dates);

            $combinedChangelog = implode("\n---\n", $certs['changelog']);
            $split = preg_split("/[\n\r]/", $combinedChangelog, 2);
            $log['title'] = $split[0];
            $log['full_message'] = (isset($split[1])) ? trim($split[1]) : '';

            $log['commit'] = $rev;

            $out[] = (object)$log;
        }

        return $out;
    }

    /**
     * @see IDF_Scm::getPathInfo()
     */
    public function getPathInfo($file, $commit = null)
    {
        if ($commit === null) {
            $commit = 'h:' . $this->getMainBranch();
        }

        $revs = $this->_resolveSelector($commit);
        if (count($revs) == 0)
            return false;

        $out = $this->stdio->exec(array(
            'get_extended_manifest_of', $revs[0]
        ));

        $files = array();
        $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);

        foreach ($stanzas as $stanza) {
            if ($stanza[0]['key'] == 'format_version')
                continue;

            if ($stanza[0]['values'][0] != $file)
                continue;

            $file = $this->_fillFileEntry($stanza);
            return (object) $file;
        }
        return false;
    }

    /**
     * @see IDF_Scm::getFile()
     */
    public function getFile($def, $cmd_only=false)
    {
        // this won't work with remote databases
        if ($cmd_only) {
            throw new Pluf_Exception_NotImplemented();
        }

        return $this->stdio->exec(array('get_file', $def->hash));
    }

    /**
     * Returns the differences between two revisions as unified diff
     *
     * @param string    The target of the diff
     * @param string    The source of the diff, if not given, the first
     *                  parent of the target is used
     * @return string
     */
    private function _getDiff($target, $source = null)
    {
        if (empty($source)) {
            $source = "p:$target";
        }

        // FIXME: add real support for merge revisions here which have
        // two distinct diff sets
        $targets = $this->_resolveSelector($target);
        $sources = $this->_resolveSelector($source);

        if (count($targets) == 0 || count($sources) == 0) {
            return '';
        }

        // if target contains a root revision, we cannot produce a diff
        if (empty($sources[0])) {
            return '';
        }

        return $this->stdio->exec(
            array('content_diff'),
            array('r' => array($sources[0], $targets[0]))
        );
    }

    /**
     * @see IDF_Scm::getChanges()
     */
    public function getChanges($commit)
    {
        $revs = $this->_resolveSelector($commit);
        if (count($revs) == 0)
            return null;

        $revision = $revs[0];
        $out = $this->stdio->exec(array('get_revision', $revision));
        $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);

        $return = (object) array(
            'additions'  => array(),
            'deletions'  => array(),
            'renames'    => array(),
            'patches'    => array(),
            'properties' => array(),
        );

        foreach ($stanzas as $stanza) {
            if ($stanza[0]['key'] == 'format_version' ||
                $stanza[0]['key'] == 'old_revision' ||
                $stanza[0]['key'] == 'new_manifest')
                continue;

            if ($stanza[0]['key'] == 'add_file' ||
                $stanza[0]['key'] == 'add_dir') {
                $return->additions[] = $stanza[0]['values'][0];
                continue;
            }

            if ($stanza[0]['key'] == 'delete') {
                $return->deletions[] = $stanza[0]['values'][0];
                continue;
            }

            if ($stanza[0]['key'] == 'rename') {
                $return->renames[$stanza[0]['values'][0]] =
                    $stanza[1]['values'][0];
                continue;
            }

            if ($stanza[0]['key'] == 'patch') {
                $return->patches[] = $stanza[0]['values'][0];
                continue;
            }

            if ($stanza[0]['key'] == 'clear' ||
                $stanza[0]['key'] == 'set') {

                $filename = $stanza[0]['values'][0];
                if (!array_key_exists($filename, $return->properties)) {
                    $return->properties[$filename] = array();
                }
                $key = $stanza[1]['values'][0];
                $value = null;
                if (isset($stanza[2])) {
                    $value = $stanza[2]['values'][0];
                }
                $return->properties[$filename][$key] = $value;
                continue;
            }
        }

        return $return;
    }

    /**
     * @see IDF_Scm::getCommit()
     */
    public function getCommit($commit, $getdiff=false)
    {
        $revs = $this->_resolveSelector($commit);
        if (count($revs) == 0)
            return array();

        $res = array();

        $parents = $this->stdio->exec(array('parents', $revs[0]));
        $res['parents'] = preg_split("/\n/", $parents, -1, PREG_SPLIT_NO_EMPTY);
        
        $certs = $this->_getCerts($revs[0]);        
        // FIXME: this assumes that author, date and changelog are always given
        $res['author'] = implode(', ', $certs['author']);

        $dates = array();
        foreach ($certs['date'] as $date)
            $dates[] = date('Y-m-d H:i:s', strtotime($date));
        $res['date'] = implode(', ', $dates);

        $combinedChangelog = implode("\n---\n", $certs['changelog']);
        $split = preg_split("/[\n\r]/", $combinedChangelog, 2);
        $res['title'] = $split[0];
        $res['full_message'] = (isset($split[1])) ? trim($split[1]) : '';

        $res['branch'] = implode(', ', $certs['branch']);
        $res['commit'] = $revs[0];

        $res['diff'] = ($getdiff) ? $this->_getDiff($revs[0]) : '';

        return (object) $res;
    }

    /**
     * @see IDF_Scm::getExtraProperties
     */
    public function getExtraProperties($obj)
    {
        return (isset($obj->parents)) ? array('parents' => $obj->parents) : array();
    }

    /**
     * @see IDF_Scm::isCommitLarge()
     */
    public function isCommitLarge($commit=null)
    {
        if (empty($commit)) {
            $commit = 'h:'.$this->getMainBranch();
        }

        $revs = $this->_resolveSelector($commit);
        if (count($revs) == 0)
            return false;

        $out = $this->stdio->exec(array(
            'get_revision', $revs[0]
        ));

        $newAndPatchedFiles = 0;
        $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);

        foreach ($stanzas as $stanza) {
            if ($stanza[0]['key'] == 'patch' || $stanza[0]['key'] == 'add_file')
                $newAndPatchedFiles++;
        }

        return $newAndPatchedFiles > 100;
    }

    /**
     * @see IDF_Scm::getChangeLog()
     */
    public function getChangeLog($commit=null, $n=10)
    {
        $horizont = $this->_resolveSelector($commit);
        $initialBranches = array();
        $logs = array();

        while (!empty($horizont) && $n > 0) {
            if (count($horizont) > 1) {
                $out = $this->stdio->exec(array('toposort') + $horizont);
                $horizont = preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY);
            }

            $rev = array_shift($horizont);
            $certs = $this->_getCerts($rev);

            // read in the initial branches we should follow
            if (count($initialBranches) == 0) {
                if (!isset($certs['branch'])) {
                    // this revision has no branch cert, we cannot start logging
                    // from this revision
                    continue;
                }
                $initialBranches = $certs['branch'];
            }

            // only add it to our log if it is on one of the initial branches
            // ignore revisions without any branch certificate
            if (count(array_intersect($initialBranches, (array)@$certs['branch'])) > 0) {
                --$n;

                $log = array();
                $log['author'] = implode(', ', $certs['author']);

                $dates = array();
                foreach ($certs['date'] as $date)
                    $dates[] = date('Y-m-d H:i:s', strtotime($date));
                $log['date'] = implode(', ', $dates);

                $combinedChangelog = implode("\n---\n", $certs['changelog']);
                $split = preg_split("/[\n\r]/", $combinedChangelog, 2);
                $log['title'] = $split[0];
                $log['full_message'] = (isset($split[1])) ? trim($split[1]) : '';

                $log['commit'] = $rev;

                $logs[] = (object)$log;

                $out = $this->stdio->exec(array('parents', $rev));
                $horizont += preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY);
            }
        }

        return $logs;
    }
}

Archive Download this file

Page rendered in 0.09974s using 11 queries.