Root/
<?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) 2008-2011 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 = 13.0; private static $instances = array (); private $stdio ; /** * Constructor */ public function __construct(IDF_Project $project , IDF_Scm_Monotone_IStdio $stdio ) { $this ->project = $project ; $this ->stdio = $stdio ; } /** * 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 = null) { $revs = $this ->_resolveSelector( $commit ); // sanity: this should actually not happen, because the // revision is validated before already if ( count ( $revs ) == 0) { throw new IDF_Scm_Exception( "$commit is not a valid revision" ); } 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 ); } /** * 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 )) { $stdio = new IDF_Scm_Monotone_Stdio( $project ); self:: $instances [ $project ->shortname] = new IDF_Scm_Monotone( $project , $stdio ); } 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][ '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 false; $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 (), 'copies' => 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 false; $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::getProperties() */ public function getProperties( $rev , $path = '' ) { $out = $this ->stdio-> exec ( array ( 'interface_version' )); // support for querying file attributes of committed revisions // was added for mtn 1.1 (interface version 13.1) if ( floatval ( $out ) < 13.1) return array (); $out = $this ->stdio-> exec ( array ( 'get_attributes' , $path ), array ( 'r' => $rev )); $stanzas = IDF_Scm_Monotone_BasicIO::parse( $out ); $res = array (); foreach ( $stanzas as $stanza ) { $line = $stanza [0]; $res [ $line [ 'values' ][0]] = $line [ 'values' ][1]; } return $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 ; } } |