Initial commit
This commit is contained in:
138
indefero/src/IDF/Scm/Cache/Git.php
Normal file
138
indefero/src/IDF/Scm/Cache/Git.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?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 ***** */
|
||||
|
||||
/**
|
||||
* This class implements the cache storage for the Git commits.
|
||||
*
|
||||
* The storage is simple. Each commit is linked to a project to drop
|
||||
* the cache when the project is dropped. The key is the commit hash
|
||||
* and the data is the date, author and one line title of information.
|
||||
*
|
||||
* A clean interface is available to bulk set/get a series of commit
|
||||
* info with the minimum of SQL queries. The goal is to be fast.
|
||||
*/
|
||||
class IDF_Scm_Cache_Git extends Pluf_Model
|
||||
{
|
||||
public $_model = __CLASS__;
|
||||
|
||||
/**
|
||||
* The current project to limit the search to.
|
||||
*/
|
||||
public $_project = null;
|
||||
|
||||
/**
|
||||
* Store in the cache blob infos.
|
||||
*
|
||||
* The info is an array of stdClasses, with hash, date, title and
|
||||
* author properties.
|
||||
*
|
||||
* @param array Blob infos
|
||||
*/
|
||||
public function store($infos)
|
||||
{
|
||||
foreach ($infos as $blob) {
|
||||
$cache = new IDF_Scm_Cache_Git();
|
||||
$cache->project = $this->_project;
|
||||
$cache->githash = $blob->hash;
|
||||
$blob->title = IDF_Commit::toUTF8($blob->title);
|
||||
$cache->content = IDF_Commit::toUTF8($blob->date) . chr(31)
|
||||
. IDF_Commit::toUTF8($blob->author) . chr(31)
|
||||
. IDF_Commit::toUTF8($blob->title);
|
||||
$sql = new Pluf_SQL('project=%s AND githash=%s',
|
||||
array($this->_project->id, $blob->hash));
|
||||
if (0 == Pluf::factory(__CLASS__)->getCount(array('filter' => $sql->gen()))) {
|
||||
$cache->create();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get for the given hashes the corresponding date, title and
|
||||
* author.
|
||||
*
|
||||
* It returns an hash indexed array with the info. If an hash is
|
||||
* not in the db, the key is not set.
|
||||
*
|
||||
* Note that the hashes must always come from internal tools.
|
||||
*
|
||||
* @param array Hashes to get info
|
||||
* @return array Blob infos
|
||||
*/
|
||||
public function retrieve($hashes)
|
||||
{
|
||||
$res = array();
|
||||
$db = $this->getDbConnection();
|
||||
$hashes = array_map(array($db, 'esc'), $hashes);
|
||||
$sql = new Pluf_SQL('project=%s AND githash IN ('.implode(', ', $hashes).')',
|
||||
array($this->_project->id));
|
||||
foreach (Pluf::factory(__CLASS__)->getList(array('filter' => $sql->gen())) as $blob) {
|
||||
$tmp = explode(chr(31), $blob->content, 3);
|
||||
// sometimes the title might be empty
|
||||
if (!isset($tmp[2])) $tmp[2] = '';
|
||||
|
||||
$res[$blob->githash] = (object) array(
|
||||
'hash' => $blob->githash,
|
||||
'date' => $tmp[0],
|
||||
'title' => $tmp[2],
|
||||
'author' => $tmp[1],
|
||||
);
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* The storage is composed of 4 columns, id, project, hash and the
|
||||
* raw data.
|
||||
*/
|
||||
function init()
|
||||
{
|
||||
$this->_a['table'] = 'idf_scm_cache_git';
|
||||
$this->_a['model'] = __CLASS__;
|
||||
$this->_a['cols'] = array(
|
||||
// It is mandatory to have an "id" column.
|
||||
'id' =>
|
||||
array(
|
||||
'type' => 'Pluf_DB_Field_Sequence',
|
||||
'blank' => true,
|
||||
),
|
||||
'project' =>
|
||||
array(
|
||||
'type' => 'Pluf_DB_Field_Foreignkey',
|
||||
'model' => 'IDF_Project',
|
||||
'blank' => false,
|
||||
),
|
||||
'githash' =>
|
||||
array(
|
||||
'type' => 'Pluf_DB_Field_Varchar',
|
||||
'blank' => false,
|
||||
'size' => 40,
|
||||
'index' => true,
|
||||
),
|
||||
'content' =>
|
||||
array(
|
||||
'type' => 'Pluf_DB_Field_Text',
|
||||
'blank' => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
26
indefero/src/IDF/Scm/Exception.php
Normal file
26
indefero/src/IDF/Scm/Exception.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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 ***** */
|
||||
|
||||
class IDF_Scm_Exception extends Exception
|
||||
{
|
||||
}
|
1002
indefero/src/IDF/Scm/Git.php
Normal file
1002
indefero/src/IDF/Scm/Git.php
Normal file
File diff suppressed because it is too large
Load Diff
611
indefero/src/IDF/Scm/Mercurial.php
Normal file
611
indefero/src/IDF/Scm/Mercurial.php
Normal file
@@ -0,0 +1,611 @@
|
||||
<?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 ***** */
|
||||
|
||||
/**
|
||||
* A simple RAII helper that manages style files to format hg's log output
|
||||
*/
|
||||
class IDF_Scm_Mercurial_LogStyle
|
||||
{
|
||||
const FULL_LOG = 1;
|
||||
const CHANGES = 2;
|
||||
|
||||
public function __construct($type)
|
||||
{
|
||||
$this->file = tempnam(Pluf::f('tmp_folder'), 'hg-log-style-');
|
||||
|
||||
if ($type == self::FULL_LOG) {
|
||||
$style = 'changeset = "'
|
||||
. 'changeset: {node|short}\n'
|
||||
. 'branch: {branch}\n'
|
||||
. 'author: {author}\n'
|
||||
. 'date: {date|isodate}\n'
|
||||
. 'parents: {parents}\n\n'
|
||||
. '{desc}\n'
|
||||
. '\0\n"'
|
||||
. "\n"
|
||||
. 'parent = "{node|short} "'
|
||||
. "\n";
|
||||
} elseif ($type == self::CHANGES) {
|
||||
$style = 'changeset = "'
|
||||
. 'file_mods: {file_mods}\n'
|
||||
. 'file_adds: {file_adds}\n'
|
||||
. 'file_dels: {file_dels}\n'
|
||||
. 'file_copies: {file_copies}\n\n'
|
||||
. '\0\n"'
|
||||
. "\n"
|
||||
. 'file_mod = "{file_mod}\0"'
|
||||
. "\n"
|
||||
. 'file_add = "{file_add}\0"'
|
||||
. "\n"
|
||||
. 'file_del = "{file_del}\0"'
|
||||
. "\n"
|
||||
. 'file_copy = "{source}\0{name}\0"'
|
||||
. "\n";
|
||||
} else {
|
||||
throw new IDF_Scm_Exception('invalid type ' . $type);
|
||||
}
|
||||
|
||||
file_put_contents($this->file, $style);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
@unlink($this->file);
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main SCM class for Mercurial
|
||||
*
|
||||
* Note: Some commands take a --debug option, this is not lousy coding, but
|
||||
* totally wanted, as hg returns additional / different data in this
|
||||
* mode on which this largely depends.
|
||||
*/
|
||||
class IDF_Scm_Mercurial extends IDF_Scm
|
||||
{
|
||||
public function __construct($repo, $project=null)
|
||||
{
|
||||
$this->repo = $repo;
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getRepositorySize()
|
||||
{
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
|
||||
.escapeshellarg($this->repo);
|
||||
$out = explode(' ',
|
||||
self::shell_exec('IDF_Scm_Mercurial::getRepositorySize',
|
||||
$cmd),
|
||||
2);
|
||||
return (int) $out[0]*1024;
|
||||
}
|
||||
|
||||
public static function factory($project)
|
||||
{
|
||||
$rep = sprintf(Pluf::f('mercurial_repositories'), $project->shortname);
|
||||
return new IDF_Scm_Mercurial($rep, $project);
|
||||
}
|
||||
|
||||
public function isAvailable()
|
||||
{
|
||||
try {
|
||||
$branches = $this->getBranches();
|
||||
} catch (IDF_Scm_Exception $e) {
|
||||
return false;
|
||||
}
|
||||
return (count($branches) > 0);
|
||||
}
|
||||
|
||||
public function findAuthor($author)
|
||||
{
|
||||
// We extract the email.
|
||||
$match = array();
|
||||
if (!preg_match('/<(.*)>/', $author, $match)) {
|
||||
return null;
|
||||
}
|
||||
return Pluf::factory('IDF_EmailAddress')->get_user_for_email_address($match[1]);
|
||||
}
|
||||
|
||||
public function getMainBranch()
|
||||
{
|
||||
return 'tip';
|
||||
}
|
||||
|
||||
public static function getAnonymousAccessUrl($project, $commit=null)
|
||||
{
|
||||
return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname);
|
||||
}
|
||||
|
||||
public static function getAuthAccessUrl($project, $user, $commit=null)
|
||||
{
|
||||
return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname);
|
||||
}
|
||||
|
||||
public function validateRevision($rev)
|
||||
{
|
||||
$cmd = sprintf(Pluf::f('hg_path', 'hg').' log -R %s -r %s',
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($rev));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Mercurial::validateRevision', $cmd, $out, $ret);
|
||||
|
||||
// FIXME: apparently a given hg revision can also be ambigious -
|
||||
// handle this case here sometime
|
||||
if ($ret == 0 && count($out) > 0)
|
||||
return IDF_Scm::REVISION_VALID;
|
||||
return IDF_Scm::REVISION_INVALID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a given object hash.
|
||||
*
|
||||
* @param string Object hash.
|
||||
* @param null to be svn client compatible
|
||||
* @return mixed false if not valid or 'blob', 'tree', 'commit'
|
||||
*/
|
||||
public function testHash($hash, $dummy=null)
|
||||
{
|
||||
$cmd = sprintf(Pluf::f('hg_path', 'hg').' log -R %s -r %s',
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($hash));
|
||||
$ret = 0;
|
||||
$out = array();
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Mercurial::testHash', $cmd, $out, $ret);
|
||||
return ($ret != 0) ? false : 'commit';
|
||||
}
|
||||
|
||||
public function getTree($commit, $folder='/', $branch=null)
|
||||
{
|
||||
// now we grab the info about this commit including its tree.
|
||||
$folder = ($folder == '/') ? '' : $folder;
|
||||
$co = $this->getCommit($commit);
|
||||
if ($folder) {
|
||||
// As we are limiting to a given folder, we need to find
|
||||
// the tree corresponding to this folder.
|
||||
$found = false;
|
||||
foreach ($this->getTreeInfo($co->tree, true, '', true) as $file) {
|
||||
if ($file->type == 'tree' and $file->file == $folder) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
throw new Exception(sprintf(__('Folder %1$s not found in commit %2$s.'), $folder, $commit));
|
||||
}
|
||||
}
|
||||
$res = $this->getTreeInfo($commit, $recurse=true, $folder);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tree info.
|
||||
*
|
||||
* @param string Tree hash
|
||||
* @param bool Do we recurse in subtrees (true)
|
||||
* @return array Array of file information.
|
||||
*/
|
||||
public function getTreeInfo($tree, $recurse=true, $folder='', $root=false)
|
||||
{
|
||||
if ('commit' != $this->testHash($tree)) {
|
||||
throw new Exception(sprintf(__('Not a valid tree: %s.'), $tree));
|
||||
}
|
||||
$cmd_tmpl = Pluf::f('hg_path', 'hg').' manifest -R %s --debug -r %s';
|
||||
$cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo),
|
||||
escapeshellarg($tree));
|
||||
$out = array();
|
||||
$res = array();
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Mercurial::getTreeInfo', $cmd, $out);
|
||||
$tmp_hack = array();
|
||||
while (null !== ($line = array_pop($out))) {
|
||||
list($hash, $perm, $exec, $file) = preg_split('/ |\t/', $line, 4);
|
||||
$file = trim($file);
|
||||
$dir = explode('/', $file, -1);
|
||||
$tmp = '';
|
||||
for ($i=0, $n=count($dir); $i<$n; $i++) {
|
||||
if ($i > 0) {
|
||||
$tmp .= '/';
|
||||
}
|
||||
$tmp .= $dir[$i];
|
||||
if (!isset($tmp_hack["empty\t000\t\t$tmp/"])) {
|
||||
$out[] = "empty\t000\t\t$tmp/";
|
||||
$tmp_hack["empty\t000\t\t$tmp/"] = 1;
|
||||
}
|
||||
}
|
||||
if (preg_match('/^(.*)\/$/', $file, $match)) {
|
||||
$type = 'tree';
|
||||
$file = $match[1];
|
||||
} else {
|
||||
$type = 'blob';
|
||||
}
|
||||
if (!$root and !$folder and preg_match('/^.*\/.*$/', $file)) {
|
||||
continue;
|
||||
}
|
||||
if ($folder) {
|
||||
preg_match('|^'.$folder.'[/]?([^/]+)?$|', $file,$match);
|
||||
if (count($match) > 1) {
|
||||
$file = $match[1];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$fullpath = ($folder) ? $folder.'/'.$file : $file;
|
||||
$efullpath = self::smartEncode($fullpath);
|
||||
$res[] = (object) array('perm' => $perm, 'type' => $type,
|
||||
'hash' => $hash, 'fullpath' => $fullpath,
|
||||
'efullpath' => $efullpath, 'file' => $file);
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function getPathInfo($totest, $commit='tip')
|
||||
{
|
||||
$cmd_tmpl = Pluf::f('hg_path', 'hg').' manifest -R %s --debug -r %s';
|
||||
$cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo),
|
||||
escapeshellarg($commit));
|
||||
$out = array();
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Mercurial::getPathInfo', $cmd, $out);
|
||||
$tmp_hack = array();
|
||||
while (null !== ($line = array_pop($out))) {
|
||||
list($hash, $perm, $exec, $file) = preg_split('/ |\t/', $line, 4);
|
||||
$file = trim($file);
|
||||
$dir = explode('/', $file, -1);
|
||||
$tmp = '';
|
||||
for ($i=0, $n=count($dir); $i<$n; $i++) {
|
||||
if ($i > 0) {
|
||||
$tmp .= '/';
|
||||
}
|
||||
$tmp .= $dir[$i];
|
||||
if ($tmp == $totest) {
|
||||
$pathinfo = pathinfo($totest);
|
||||
return (object) array('perm' => '000', 'type' => 'tree',
|
||||
'hash' => $hash,
|
||||
'fullpath' => $totest,
|
||||
'file' => $pathinfo['basename'],
|
||||
'commit' => $commit
|
||||
);
|
||||
}
|
||||
if (!isset($tmp_hack["empty\t000\t\t$tmp/"])) {
|
||||
$out[] = "empty\t000\t\t$tmp/";
|
||||
$tmp_hack["empty\t000\t\t$tmp/"] = 1;
|
||||
}
|
||||
}
|
||||
if (preg_match('/^(.*)\/$/', $file, $match)) {
|
||||
$type = 'tree';
|
||||
$file = $match[1];
|
||||
} else {
|
||||
$type = 'blob';
|
||||
}
|
||||
if ($totest == $file) {
|
||||
$pathinfo = pathinfo($totest);
|
||||
return (object) array('perm' => $perm, 'type' => $type,
|
||||
'hash' => $hash,
|
||||
'fullpath' => $totest,
|
||||
'file' => $pathinfo['basename'],
|
||||
'commit' => $commit
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getFile($def, $cmd_only=false)
|
||||
{
|
||||
$cmd = sprintf(Pluf::f('hg_path', 'hg').' cat -R %s -r %s %s',
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($def->commit),
|
||||
escapeshellarg($this->repo.'/'.$def->fullpath));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
return ($cmd_only) ?
|
||||
$cmd : self::shell_exec('IDF_Scm_Mercurial::getFile', $cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the branches.
|
||||
*
|
||||
* @return array Branches.
|
||||
*/
|
||||
public function getBranches()
|
||||
{
|
||||
if (isset($this->cache['branches'])) {
|
||||
return $this->cache['branches'];
|
||||
}
|
||||
$out = array();
|
||||
$cmd = sprintf(Pluf::f('hg_path', 'hg').' branches -R %s',
|
||||
escapeshellarg($this->repo));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Mercurial::getBranches', $cmd, $out);
|
||||
$res = array();
|
||||
foreach ($out as $b) {
|
||||
preg_match('/(.+?)\s+\S+:(\S+)/', $b, $match);
|
||||
$res[$match[1]] = '';
|
||||
}
|
||||
$this->cache['branches'] = $res;
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tags.
|
||||
*
|
||||
* @return array Tags.
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
if (isset($this->cache['tags'])) {
|
||||
return $this->cache['tags'];
|
||||
}
|
||||
$out = array();
|
||||
$cmd = sprintf(Pluf::f('hg_path', 'hg').' tags -R %s',
|
||||
escapeshellarg($this->repo));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Mercurial::getTags', $cmd, $out);
|
||||
$res = array();
|
||||
foreach ($out as $b) {
|
||||
preg_match('/(.+?)\s+\S+:(\S+)/', $b, $match);
|
||||
$res[$match[1]] = '';
|
||||
}
|
||||
$this->cache['tags'] = $res;
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function inBranches($commit, $path)
|
||||
{
|
||||
return (in_array($commit, array_keys($this->getBranches())))
|
||||
? array($commit) : array();
|
||||
}
|
||||
|
||||
public function inTags($commit, $path)
|
||||
{
|
||||
return (in_array($commit, array_keys($this->getTags())))
|
||||
? array($commit) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit details.
|
||||
*
|
||||
* @param string Commit ('HEAD')
|
||||
* @param bool Get commit diff (false)
|
||||
* @return array Changes
|
||||
*/
|
||||
public function getCommit($commit, $getdiff=false)
|
||||
{
|
||||
if ($this->validateRevision($commit) != IDF_Scm::REVISION_VALID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$logStyle = new IDF_Scm_Mercurial_LogStyle(IDF_Scm_Mercurial_LogStyle::FULL_LOG);
|
||||
$tmpl = ($getdiff)
|
||||
? Pluf::f('hg_path', 'hg').' log --debug -p -r %s -R %s --style %s'
|
||||
: Pluf::f('hg_path', 'hg').' log --debug -r %s -R %s --style %s';
|
||||
$cmd = sprintf($tmpl,
|
||||
escapeshellarg($commit),
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($logStyle->get()));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
$out = self::shell_exec('IDF_Scm_Mercurial::getCommit', $cmd);
|
||||
if (strlen($out) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$diffStart = strpos($out, 'diff -r');
|
||||
$diff = '';
|
||||
if ($diffStart !== false) {
|
||||
$log = substr($out, 0, $diffStart);
|
||||
$diff = substr($out, $diffStart);
|
||||
} else {
|
||||
$log = $out;
|
||||
}
|
||||
|
||||
$out = self::parseLog(preg_split('/\r\n|\n/', $log));
|
||||
$out[0]->diff = $diff;
|
||||
return $out[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see IDF_Scm::getChanges()
|
||||
*/
|
||||
public function getChanges($commit)
|
||||
{
|
||||
if ($this->validateRevision($commit) != IDF_Scm::REVISION_VALID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$logStyle = new IDF_Scm_Mercurial_LogStyle(IDF_Scm_Mercurial_LogStyle::CHANGES);
|
||||
$tmpl = Pluf::f('hg_path', 'hg').' log --debug -r %s -R %s --style %s';
|
||||
$cmd = sprintf($tmpl,
|
||||
escapeshellarg($commit),
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($logStyle->get()));
|
||||
$out = array();
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Mercurial::getChanges', $cmd, $out);
|
||||
$log = self::parseLog($out);
|
||||
// we expect only one log entry that contains all the needed information
|
||||
$log = $log[0];
|
||||
|
||||
$return = (object) array(
|
||||
'additions' => preg_split('/\0/', $log->file_adds, -1, PREG_SPLIT_NO_EMPTY),
|
||||
'deletions' => preg_split('/\0/', $log->file_dels, -1, PREG_SPLIT_NO_EMPTY),
|
||||
'patches' => preg_split('/\0/', $log->file_mods, -1, PREG_SPLIT_NO_EMPTY),
|
||||
// hg has no support for built-in attributes, so this keeps empty
|
||||
'properties' => array(),
|
||||
// these two are filled below
|
||||
'copies' => array(),
|
||||
'renames' => array(),
|
||||
);
|
||||
|
||||
$file_copies = preg_split('/\0/', $log->file_copies, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
// copies are treated as renames if they have an add _and_ a drop;
|
||||
// only if they only have an add, but no drop, they're treated as copies
|
||||
for ($i=0; $i<count($file_copies); $i+=2) {
|
||||
$src = $file_copies[$i];
|
||||
$trg = $file_copies[$i+1];
|
||||
$srcidx = array_search($src, $return->deletions);
|
||||
$trgidx = array_search($trg, $return->additions);
|
||||
if ($srcidx !== false && $trgidx !== false) {
|
||||
$return->renames[$src] = $trg;
|
||||
unset($return->deletions[$srcidx]);
|
||||
unset($return->additions[$trgidx]);
|
||||
continue;
|
||||
}
|
||||
if ($srcidx === false && $trgidx !== false) {
|
||||
$return->copies[$src] = $trg;
|
||||
unset($return->additions[$trgidx]);
|
||||
continue;
|
||||
}
|
||||
// file sutures (counter-operation to copy) not supported
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a commit is big.
|
||||
*
|
||||
* @param string Commit ('HEAD')
|
||||
* @return bool The commit is big
|
||||
*/
|
||||
public function isCommitLarge($commit='HEAD')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest changes.
|
||||
*
|
||||
* @param string Commit ('HEAD').
|
||||
* @param int Number of changes (10).
|
||||
* @return array Changes.
|
||||
*/
|
||||
public function getChangeLog($commit='tip', $n=10)
|
||||
{
|
||||
$logStyle = new IDF_Scm_Mercurial_LogStyle(IDF_Scm_Mercurial_LogStyle::FULL_LOG);
|
||||
|
||||
// hg accepts revision IDs as arguments to --branch / -b as well and
|
||||
// uses the branch of the revision in question to filter the other
|
||||
// revisions
|
||||
$cmd = sprintf(Pluf::f('hg_path', 'hg').' log --debug -R %s -l%s --style %s -b %s',
|
||||
escapeshellarg($this->repo),
|
||||
$n,
|
||||
escapeshellarg($logStyle->get()),
|
||||
escapeshellarg($commit));
|
||||
$out = array();
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Mercurial::getChangeLog', $cmd, $out);
|
||||
return self::parseLog($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the log lines of our custom style format.
|
||||
*
|
||||
* @param array Lines.
|
||||
* @return array Change log.
|
||||
*/
|
||||
public static function parseLog($lines)
|
||||
{
|
||||
$res = array();
|
||||
$c = array();
|
||||
$headers_processed = false;
|
||||
foreach ($lines as $line) {
|
||||
if ($line == "\0") {
|
||||
$headers_processed = false;
|
||||
if (count($c) > 0) {
|
||||
if (array_key_exists('full_message', $c))
|
||||
$c['full_message'] = trim($c['full_message']);
|
||||
$res[] = (object) $c;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!$headers_processed && empty($line)) {
|
||||
$headers_processed = true;
|
||||
continue;
|
||||
}
|
||||
if (!$headers_processed && preg_match('/^(\S+):\s*(.*)/', $line, $match)) {
|
||||
$match[1] = strtolower($match[1]);
|
||||
if ($match[1] == 'changeset') {
|
||||
$c = array();
|
||||
$c['commit'] = $match[2];
|
||||
$c['tree'] = $c['commit'];
|
||||
$c['full_message'] = '';
|
||||
} elseif ($match[1] == 'author') {
|
||||
$c['author'] = $match[2];
|
||||
} elseif ($match[1] == 'branch') {
|
||||
$c['branch'] = empty($match[2]) ? 'default' : $match[2];
|
||||
} elseif ($match[1] == 'parents') {
|
||||
$parents = preg_split('/\s+/', $match[2], -1, PREG_SPLIT_NO_EMPTY);
|
||||
for ($i=0, $j=count($parents); $i<$j; ++$i) {
|
||||
if ($parents[$i] == '000000000000')
|
||||
unset($parents[$i]);
|
||||
}
|
||||
$c['parents'] = $parents;
|
||||
} else {
|
||||
$c[$match[1]] = trim($match[2]);
|
||||
}
|
||||
if ($match[1] == 'date') {
|
||||
$c['date'] = gmdate('Y-m-d H:i:s', strtotime($match[2]));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($headers_processed) {
|
||||
if (empty($c['title']))
|
||||
$c['title'] = trim($line);
|
||||
else
|
||||
$c['full_message'] .= trim($line)."\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a zip archive at a given commit.
|
||||
*
|
||||
* @param string Commit
|
||||
* @param string Prefix ('git-repo-dump')
|
||||
* @return Pluf_HTTP_Response The HTTP response containing the zip archive
|
||||
*/
|
||||
public function getArchiveStream($commit, $prefix='')
|
||||
{
|
||||
$cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', '').
|
||||
Pluf::f('hg_path', 'hg').' archive --type=zip -R %s -r %s -',
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($commit));
|
||||
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
|
||||
}
|
||||
|
||||
/**
|
||||
* @see IDF_Scm::getDiffPathStripLevel()
|
||||
*/
|
||||
public function getDiffPathStripLevel()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
819
indefero/src/IDF/Scm/Monotone.php
Normal file
819
indefero/src/IDF/Scm/Monotone.php
Normal file
@@ -0,0 +1,819 @@
|
||||
<?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);
|
||||
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)) {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
186
indefero/src/IDF/Scm/Monotone/BasicIO.php
Normal file
186
indefero/src/IDF/Scm/Monotone/BasicIO.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?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 ***** */
|
||||
|
||||
require_once 'IDF/Scm/Exception.php';
|
||||
|
||||
/**
|
||||
* Utility class to parse and compile basic_io stanzas
|
||||
*
|
||||
* @author Thomas Keller <me@thomaskeller.biz>
|
||||
*/
|
||||
class IDF_Scm_Monotone_BasicIO
|
||||
{
|
||||
/**
|
||||
* Parses monotone's basic_io format
|
||||
*
|
||||
* Known quirks:
|
||||
* - does not handle multi-values starting with a hash '[]' (no known output)
|
||||
* - does not validate hashes (should be /[0-9a-f]{40}/i)
|
||||
* - does not handle forbidden \0
|
||||
*
|
||||
* @param string $in
|
||||
* @return array of arrays
|
||||
*/
|
||||
public static function parse($in)
|
||||
{
|
||||
$pos = 0;
|
||||
$stanzas = array();
|
||||
$length = strlen($in);
|
||||
|
||||
while ($pos < $length) {
|
||||
$stanza = array();
|
||||
while ($pos < $length) {
|
||||
if ($in[$pos] == "\n") break;
|
||||
|
||||
$stanzaLine = array('key' => '', 'values' => array(), 'hash' => null);
|
||||
while ($pos < $length) {
|
||||
$ch = $in[$pos];
|
||||
if ($ch == '"' || $ch == '[') break;
|
||||
++$pos;
|
||||
if ($ch == ' ') continue;
|
||||
$stanzaLine['key'] .= $ch;
|
||||
}
|
||||
|
||||
// ensure we don't look at a symbol w/o a value list
|
||||
if ($pos >= $length || $in[$pos] == "\n") {
|
||||
unset($stanzaLine['values']);
|
||||
unset($stanzaLine['hash']);
|
||||
}
|
||||
else {
|
||||
if ($in[$pos] == '[') {
|
||||
unset($stanzaLine['values']);
|
||||
++$pos; // opening square bracket
|
||||
while ($pos < $length && $in[$pos] != ']') {
|
||||
$stanzaLine['hash'] .= $in[$pos];
|
||||
++$pos;
|
||||
}
|
||||
++$pos; // closing square bracket
|
||||
}
|
||||
else
|
||||
{
|
||||
unset($stanzaLine['hash']);
|
||||
$valCount = 0;
|
||||
// if hashs and plain values are encountered in the same
|
||||
// value list, we add the hash values as simple values as well
|
||||
while ($in[$pos] == '"' || $in[$pos] == '[') {
|
||||
$isHashValue = $in[$pos] == '[';
|
||||
++$pos; // opening quote / bracket
|
||||
$stanzaLine['values'][$valCount] = '';
|
||||
while ($pos < $length) {
|
||||
$ch = $in[$pos]; $pr = $in[$pos-1];
|
||||
if (($isHashValue && $ch == ']')
|
||||
||(!$isHashValue && $ch == '"' && $pr != '\\'))
|
||||
break;
|
||||
++$pos;
|
||||
$stanzaLine['values'][$valCount] .= $ch;
|
||||
}
|
||||
++$pos; // closing quote
|
||||
|
||||
if (!$isHashValue) {
|
||||
$stanzaLine['values'][$valCount] = str_replace(
|
||||
array("\\\\", "\\\""),
|
||||
array("\\", "\""),
|
||||
$stanzaLine['values'][$valCount]
|
||||
);
|
||||
}
|
||||
|
||||
if ($pos >= $length)
|
||||
break;
|
||||
|
||||
if ($in[$pos] == ' ') {
|
||||
++$pos; // space
|
||||
++$valCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stanza[] = $stanzaLine;
|
||||
++$pos; // newline
|
||||
}
|
||||
$stanzas[] = $stanza;
|
||||
++$pos; // newline
|
||||
}
|
||||
return $stanzas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles monotone's basicio format
|
||||
*
|
||||
* Known quirks:
|
||||
* - does not validate keys for /[a-z_]+/
|
||||
* - does not validate hashes (should be /[0-9a-f]{40}/i)
|
||||
* - does not support intermixed value / hash formats
|
||||
* - does not handle forbidden \0
|
||||
*
|
||||
* @param array $in Array of arrays
|
||||
* @return string
|
||||
*/
|
||||
public static function compile($in)
|
||||
{
|
||||
$out = "";
|
||||
$first = true;
|
||||
foreach ((array)$in as $sx => $stanza) {
|
||||
if ($first)
|
||||
$first = false;
|
||||
else
|
||||
$out .= "\n";
|
||||
|
||||
$maxkeylength = 0;
|
||||
foreach ((array)$stanza as $lx => $line) {
|
||||
if (!array_key_exists('key', $line) || empty($line['key'])) {
|
||||
throw new IDF_Scm_Exception(
|
||||
'"key" not found in basicio stanza '.$sx.', line '.$lx
|
||||
);
|
||||
}
|
||||
$maxkeylength = max($maxkeylength, strlen($line['key']));
|
||||
}
|
||||
|
||||
foreach ((array)$stanza as $lx => $line) {
|
||||
$out .= str_pad($line['key'], $maxkeylength, ' ', STR_PAD_LEFT);
|
||||
|
||||
if (array_key_exists('hash', $line)) {
|
||||
$out .= ' ['.$line['hash'].']';
|
||||
} else
|
||||
if (array_key_exists('values', $line)) {
|
||||
if (!is_array($line['values']) || count($line['values']) == 0) {
|
||||
throw new IDF_Scm_Exception(
|
||||
'"values" must be an array of a size >= 1 '.
|
||||
'in basicio stanza '.$sx.', line '.$lx
|
||||
);
|
||||
}
|
||||
foreach ($line['values'] as $value) {
|
||||
$out .= ' "'.str_replace(
|
||||
array("\\", "\""),
|
||||
array("\\\\", "\\\""),
|
||||
$value).'"';
|
||||
}
|
||||
}
|
||||
|
||||
$out .= "\n";
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
66
indefero/src/IDF/Scm/Monotone/IStdio.php
Normal file
66
indefero/src/IDF/Scm/Monotone/IStdio.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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 stdio interface
|
||||
*
|
||||
* @author Thomas Keller <me@thomaskeller.biz>
|
||||
*/
|
||||
interface IDF_Scm_Monotone_IStdio
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(IDF_Project $project);
|
||||
|
||||
/**
|
||||
* Starts the stdio process and resets the command counter
|
||||
*/
|
||||
public function start();
|
||||
|
||||
/**
|
||||
* Stops the stdio process and closes all pipes
|
||||
*/
|
||||
public function stop();
|
||||
|
||||
/**
|
||||
* Executes a command over stdio and returns its result
|
||||
*
|
||||
* @param array Array of arguments
|
||||
* @param array Array of options as key-value pairs. Multiple options
|
||||
* can be defined in sub-arrays, like
|
||||
* "r" => array("123...", "456...")
|
||||
* @return string
|
||||
*/
|
||||
public function exec(array $args, array $options = array());
|
||||
|
||||
/**
|
||||
* Returns the last out-of-band output for a previously executed
|
||||
* command as associative array with 'e' (error), 'w' (warning),
|
||||
* 'p' (progress) and 't' (ticker, unparsed) as keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLastOutOfBandOutput();
|
||||
}
|
||||
|
405
indefero/src/IDF/Scm/Monotone/Stdio.php
Normal file
405
indefero/src/IDF/Scm/Monotone/Stdio.php
Normal file
@@ -0,0 +1,405 @@
|
||||
<?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 ***** */
|
||||
|
||||
require_once 'IDF/Scm/Monotone/IStdio.php';
|
||||
|
||||
/**
|
||||
* Monotone stdio class
|
||||
*
|
||||
* Connects to a monotone process and executes commands via its
|
||||
* stdio interface
|
||||
*
|
||||
* @author Thomas Keller <me@thomaskeller.biz>
|
||||
*/
|
||||
class IDF_Scm_Monotone_Stdio implements IDF_Scm_Monotone_IStdio
|
||||
{
|
||||
/** this is the most recent STDIO version. The number is output
|
||||
at the protocol start. Older versions of monotone (prior 0.47)
|
||||
do not output it and are therefor incompatible */
|
||||
public static $SUPPORTED_STDIO_VERSION = 2;
|
||||
|
||||
private $project;
|
||||
private $proc;
|
||||
private $pipes;
|
||||
private $oob;
|
||||
private $cmdnum;
|
||||
private $lastcmd;
|
||||
|
||||
/**
|
||||
* Constructor - starts the stdio process
|
||||
*
|
||||
* @param IDF_Project
|
||||
*/
|
||||
public function __construct(IDF_Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
$this->start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor - stops the stdio process
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with additional options which are passed to
|
||||
* an mtn instance connecting to remote databases
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function _getAuthOptions()
|
||||
{
|
||||
$prjconf = $this->project->getConf();
|
||||
$name = $prjconf->getVal('mtn_client_key_name', false);
|
||||
$hash = $prjconf->getVal('mtn_client_key_hash', false);
|
||||
|
||||
if (!$name || !$hash) {
|
||||
throw new IDF_Scm_Exception(sprintf(
|
||||
__('Monotone client key name or hash not in project conf.')
|
||||
));
|
||||
}
|
||||
|
||||
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
|
||||
if (!file_exists($keydir)) {
|
||||
if (!mkdir($keydir)) {
|
||||
throw new IDF_Scm_Exception(sprintf(
|
||||
__('The key directory %s could not be created.'), $keydir
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// in case somebody cleaned out the cache, we restore the key here
|
||||
$keyfile = $keydir . '/' . $name .'.'. $hash;
|
||||
if (!file_exists($keyfile)) {
|
||||
$data = $prjconf->getVal('mtn_client_key_data');
|
||||
if (!file_put_contents($keyfile, $data, LOCK_EX)) {
|
||||
throw new IDF_Scm_Exception(sprintf(
|
||||
__('Could not write client key "%s"'), $keyfile
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf('--keydir=%s --key=%s ',
|
||||
escapeshellarg($keydir),
|
||||
escapeshellarg($hash)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the stdio process and resets the command counter
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
if (is_resource($this->proc))
|
||||
$this->stop();
|
||||
|
||||
$remote_db_access = Pluf::f('mtn_db_access', 'remote') == 'remote';
|
||||
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '') .
|
||||
escapeshellarg(Pluf::f('mtn_path', 'mtn')) . ' ';
|
||||
|
||||
$opts = Pluf::f('mtn_opts', array());
|
||||
foreach ($opts as $opt) {
|
||||
$cmd .= sprintf('%s ', escapeshellarg($opt));
|
||||
}
|
||||
|
||||
if ($remote_db_access) {
|
||||
$cmd .= $this->_getAuthOptions();
|
||||
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
|
||||
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
|
||||
}
|
||||
else
|
||||
{
|
||||
$repo = sprintf(Pluf::f('mtn_repositories'), $this->project->shortname);
|
||||
if (!file_exists($repo)) {
|
||||
throw new IDF_Scm_Exception(
|
||||
"repository file '$repo' does not exist"
|
||||
);
|
||||
}
|
||||
$cmd .= sprintf('--db %s automate stdio', escapeshellarg($repo));
|
||||
}
|
||||
|
||||
$descriptors = array(
|
||||
0 => array('pipe', 'r'),
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
);
|
||||
|
||||
$env = array('LANG' => 'en_US.UTF-8');
|
||||
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
|
||||
null, $env);
|
||||
|
||||
if (!is_resource($this->proc)) {
|
||||
throw new IDF_Scm_Exception('could not start stdio process');
|
||||
}
|
||||
|
||||
$this->_checkVersion();
|
||||
|
||||
$this->cmdnum = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the stdio process and closes all pipes
|
||||
*/
|
||||
public function stop()
|
||||
{
|
||||
if (!is_resource($this->proc))
|
||||
return;
|
||||
|
||||
fclose($this->pipes[0]);
|
||||
fclose($this->pipes[1]);
|
||||
fclose($this->pipes[2]);
|
||||
|
||||
proc_close($this->proc);
|
||||
$this->proc = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* select()'s on stdout and returns true as soon as we got new
|
||||
* data to read, false if the select() timed out
|
||||
*
|
||||
* @return boolean
|
||||
* @throws IDF_Scm_Exception
|
||||
*/
|
||||
private function _waitForReadyRead()
|
||||
{
|
||||
if (!is_resource($this->pipes[1]))
|
||||
return false;
|
||||
|
||||
$read = array($this->pipes[1], $this->pipes[2]);
|
||||
$write = $except = null;
|
||||
$streamsChanged = stream_select(
|
||||
$read, $write, $except, 0, 20000
|
||||
);
|
||||
|
||||
if ($streamsChanged === false) {
|
||||
throw new IDF_Scm_Exception(
|
||||
'Could not select() on read pipe'
|
||||
);
|
||||
}
|
||||
|
||||
if ($streamsChanged == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the version of the used stdio protocol
|
||||
*
|
||||
* @throws IDF_Scm_Exception
|
||||
*/
|
||||
private function _checkVersion()
|
||||
{
|
||||
$this->_waitForReadyRead();
|
||||
|
||||
$version = fgets($this->pipes[1]);
|
||||
if ($version === false) {
|
||||
throw new IDF_Scm_Exception(
|
||||
"Could not determine stdio version, stderr is:\n".
|
||||
$this->_readStderr()
|
||||
);
|
||||
}
|
||||
|
||||
if (!preg_match('/^format-version: (\d+)$/', $version, $m) ||
|
||||
$m[1] != self::$SUPPORTED_STDIO_VERSION)
|
||||
{
|
||||
throw new IDF_Scm_Exception(
|
||||
'stdio format version mismatch, expected "'.
|
||||
self::$SUPPORTED_STDIO_VERSION.'", got "'.@$m[1].'"'
|
||||
);
|
||||
}
|
||||
|
||||
fgets($this->pipes[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a command to stdio
|
||||
*
|
||||
* @param array
|
||||
* @param array
|
||||
* @throws IDF_Scm_Exception
|
||||
*/
|
||||
private function _write(array $args, array $options = array())
|
||||
{
|
||||
$cmd = '';
|
||||
if (count($options) > 0) {
|
||||
$cmd = 'o';
|
||||
foreach ($options as $k => $vals) {
|
||||
if (!is_array($vals))
|
||||
$vals = array($vals);
|
||||
|
||||
foreach ($vals as $v) {
|
||||
$cmd .= strlen((string)$k) . ':' . (string)$k;
|
||||
$cmd .= strlen((string)$v) . ':' . (string)$v;
|
||||
}
|
||||
}
|
||||
$cmd .= 'e ';
|
||||
}
|
||||
|
||||
$cmd .= 'l';
|
||||
foreach ($args as $arg) {
|
||||
$cmd .= strlen((string)$arg) . ':' . (string)$arg;
|
||||
}
|
||||
$cmd .= "e\n";
|
||||
|
||||
if (!fwrite($this->pipes[0], $cmd)) {
|
||||
throw new IDF_Scm_Exception("could not write '$cmd' to process");
|
||||
}
|
||||
|
||||
$this->lastcmd = $cmd;
|
||||
$this->cmdnum++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all output from stderr and returns it
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function _readStderr()
|
||||
{
|
||||
$err = "";
|
||||
while (($line = fgets($this->pipes[2])) !== false) {
|
||||
$err .= $line;
|
||||
}
|
||||
return empty($err) ? '<empty>' : $err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the last output from the stdio process, parses and returns it
|
||||
*
|
||||
* @return string
|
||||
* @throws IDF_Scm_Exception
|
||||
*/
|
||||
private function _readStdout()
|
||||
{
|
||||
$this->oob = array('w' => array(),
|
||||
'p' => array(),
|
||||
't' => array(),
|
||||
'e' => array());
|
||||
|
||||
$output = "";
|
||||
$errcode = 0;
|
||||
|
||||
while (true) {
|
||||
if (!$this->_waitForReadyRead())
|
||||
continue;
|
||||
|
||||
$data = array(0,"",0);
|
||||
$idx = 0;
|
||||
while (true) {
|
||||
$c = fgetc($this->pipes[1]);
|
||||
if ($c === false) {
|
||||
throw new IDF_Scm_Exception(
|
||||
"No data on stdin, stderr is:\n".
|
||||
$this->_readStderr()
|
||||
);
|
||||
}
|
||||
|
||||
if ($c == ':') {
|
||||
if ($idx == 2)
|
||||
break;
|
||||
|
||||
++$idx;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_numeric($c))
|
||||
$data[$idx] = $data[$idx] * 10 + $c;
|
||||
else
|
||||
$data[$idx] .= $c;
|
||||
}
|
||||
|
||||
// sanity
|
||||
if ($this->cmdnum != $data[0]) {
|
||||
throw new IDF_Scm_Exception(
|
||||
'command numbers out of sync; expected '.
|
||||
$this->cmdnum .', got '. $data[0]
|
||||
);
|
||||
}
|
||||
|
||||
$toRead = $data[2];
|
||||
$buffer = "";
|
||||
while ($toRead > 0) {
|
||||
$buffer .= fread($this->pipes[1], $toRead);
|
||||
$toRead = $data[2] - strlen($buffer);
|
||||
}
|
||||
|
||||
switch ($data[1]) {
|
||||
case 'w':
|
||||
case 'p':
|
||||
case 't':
|
||||
case 'e':
|
||||
$this->oob[$data[1]][] = $buffer;
|
||||
continue;
|
||||
case 'm':
|
||||
$output .= $buffer;
|
||||
continue;
|
||||
case 'l':
|
||||
$errcode = $buffer;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($errcode != 0) {
|
||||
throw new IDF_Scm_Exception(
|
||||
"command '{$this->lastcmd}' returned error code $errcode: ".
|
||||
implode(' ', $this->oob['e'])
|
||||
);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command over stdio and returns its result
|
||||
*
|
||||
* @param array Array of arguments
|
||||
* @param array Array of options as key-value pairs. Multiple options
|
||||
* can be defined in sub-arrays, like
|
||||
* "r" => array("123...", "456...")
|
||||
* @return string
|
||||
*/
|
||||
public function exec(array $args, array $options = array())
|
||||
{
|
||||
$this->_write($args, $options);
|
||||
return $this->_readStdout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last out-of-band output for a previously executed
|
||||
* command as associative array with 'e' (error), 'w' (warning),
|
||||
* 'p' (progress) and 't' (ticker, unparsed) as keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLastOutOfBandOutput()
|
||||
{
|
||||
return $this->oob;
|
||||
}
|
||||
}
|
||||
|
269
indefero/src/IDF/Scm/Monotone/Usher.php
Normal file
269
indefero/src/IDF/Scm/Monotone/Usher.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?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 ***** */
|
||||
|
||||
require_once(dirname(__FILE__) . "/BasicIO.php");
|
||||
|
||||
/**
|
||||
* Connects with the admininistrative interface of usher,
|
||||
* the monotone proxy. This class contains only static methods because
|
||||
* there is really no state to keep between each invocation, as usher
|
||||
* closes the connection after every command.
|
||||
*
|
||||
* @author Thomas Keller <me@thomaskeller.biz>
|
||||
*/
|
||||
class IDF_Scm_Monotone_Usher
|
||||
{
|
||||
/**
|
||||
* Without giving a specific state, returns an array of all servers.
|
||||
* When a state is given, the array contains only servers which are
|
||||
* in the given state.
|
||||
*
|
||||
* @param string $state One of REMOTE, ACTIVE, WAITING, SLEEPING,
|
||||
* STOPPING, STOPPED, SHUTTINGDOWN or SHUTDOWN
|
||||
* @return array
|
||||
*/
|
||||
public static function getServerList($state = null)
|
||||
{
|
||||
$conn = self::_triggerCommand('LIST '.$state);
|
||||
if ($conn == 'none')
|
||||
return array();
|
||||
|
||||
return preg_split('/[ ]/', $conn, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all open connections to the given server, or to
|
||||
* any server if no server is specified.
|
||||
* If there are no connections to list, an empty array is returned.
|
||||
*
|
||||
* Example:
|
||||
* array("server1" => array(
|
||||
* array("address" => "192.168.1.0", "port" => "13456"),
|
||||
* ...
|
||||
* ),
|
||||
* "server2" => ...
|
||||
* )
|
||||
*
|
||||
* @param string $server
|
||||
* @return array
|
||||
*/
|
||||
public static function getConnectionList($server = null)
|
||||
{
|
||||
$conn = self::_triggerCommand('LISTCONNECTIONS '.$server);
|
||||
if ($conn == 'none')
|
||||
return array();
|
||||
|
||||
$single_conns = preg_split('/[ ]/', $conn, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$ret = array();
|
||||
foreach ($single_conns as $conn) {
|
||||
preg_match('/\(([^)]+)\)([^:]+):(\d+)/', $conn, $matches);
|
||||
$ret[$matches[1]][] = (object)array(
|
||||
'server' => $matches[1],
|
||||
'address' => $matches[2],
|
||||
'port' => $matches[3],
|
||||
);
|
||||
}
|
||||
|
||||
if ($server !== null) {
|
||||
if (array_key_exists($server, $ret))
|
||||
return $ret[$server];
|
||||
return array();
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of a particular server, or of the usher as a whole if
|
||||
* no server is specified.
|
||||
*
|
||||
* @param string $server
|
||||
* @return One of REMOTE, SLEEPING, STOPPING, STOPPED for servers or
|
||||
* ACTIVE, WAITING, SHUTTINGDOWN or SHUTDOWN for usher itself
|
||||
*/
|
||||
public static function getStatus($server = null)
|
||||
{
|
||||
return self::_triggerCommand('STATUS '.$server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the name of the server that would be used for an incoming
|
||||
* connection having the given host and pattern.
|
||||
*
|
||||
* @param string $host Host
|
||||
* @param string $pattern Branch pattern
|
||||
* @return server name
|
||||
* @throws IDF_Scm_Exception
|
||||
*/
|
||||
public static function matchServer($host, $pattern)
|
||||
{
|
||||
$ret = self::_triggerCommand('MATCH '.$host.' '.$pattern);
|
||||
if (preg_match('/^OK: (.+)/', $ret, $m))
|
||||
return $m[1];
|
||||
preg_match('/^ERROR: (.+)/', $ret, $m);
|
||||
throw new IDF_Scm_Exception('could not match server: '.$m[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the given local server from receiving further connections,
|
||||
* and stop it once all connections are closed. The return value will
|
||||
* be the new status of that server: ACTIVE local servers will become
|
||||
* STOPPING, and WAITING and SLEEPING serveres become STOPPED.
|
||||
* Servers in other states are not affected.
|
||||
*
|
||||
* @param string $server
|
||||
* @return string State of the server after the command
|
||||
*/
|
||||
public static function stopServer($server)
|
||||
{
|
||||
return self::_triggerCommand("STOP $server");
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow a STOPPED or STOPPING server to receive connections again.
|
||||
* The return value is the new status of that server: STOPPING servers
|
||||
* become ACTIVE, and STOPPED servers become SLEEPING. Servers in other
|
||||
* states are not affected.
|
||||
*
|
||||
* @param string $server
|
||||
* @return string State of the server after the command
|
||||
*/
|
||||
public static function startServer($server)
|
||||
{
|
||||
return self::_triggerCommand('START '.$server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately kill the given local server, dropping any open connections,
|
||||
* and prevent is from receiving new connections and restarting. The named
|
||||
* server will immediately change to state STOPPED.
|
||||
*
|
||||
* @param string $server
|
||||
* @return bool True if successful
|
||||
*/
|
||||
public static function killServer($server)
|
||||
{
|
||||
return self::_triggerCommand('KILL_NOW '.$server) == 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not accept new connections for any servers, local or remote.
|
||||
*
|
||||
* @return bool True if successful
|
||||
*/
|
||||
public static function shutDown()
|
||||
{
|
||||
return self::_triggerCommand('SHUTDOWN') == 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin accepting connections after a SHUTDOWN.
|
||||
*
|
||||
* @return bool True if successful
|
||||
*/
|
||||
public static function startUp()
|
||||
{
|
||||
return self::_triggerCommand('STARTUP') == 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the config file, the same as sending SIGHUP.
|
||||
*
|
||||
* @return bool True if successful (after the configuration was reloaded)
|
||||
*/
|
||||
public static function reload()
|
||||
{
|
||||
return self::_triggerCommand('RELOAD') == 'ok';
|
||||
}
|
||||
|
||||
private static function _triggerCommand($cmd)
|
||||
{
|
||||
$uc = Pluf::f('mtn_usher_conf', false);
|
||||
if (!$uc || !is_readable($uc)) {
|
||||
throw new IDF_Scm_Exception(
|
||||
'"mtn_usher_conf" is not configured or not readable'
|
||||
);
|
||||
}
|
||||
|
||||
$parsed_config =
|
||||
IDF_Scm_Monotone_BasicIO::parse(file_get_contents($uc));
|
||||
$host = $port = $user = $pass = null;
|
||||
foreach ($parsed_config as $stanza) {
|
||||
foreach ($stanza as $line) {
|
||||
if ($line['key'] == 'adminaddr') {
|
||||
list($host, $port) = explode(":", @$line['values'][0]);
|
||||
break;
|
||||
}
|
||||
if ($line['key'] == 'userpass') {
|
||||
$user = @$line['values'][0];
|
||||
$pass = @$line['values'][1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($host)) {
|
||||
throw new IDF_Scm_Exception('usher host is empty');
|
||||
}
|
||||
if (!preg_match('/^\d+$/', $port))
|
||||
{
|
||||
throw new IDF_Scm_Exception('usher port is invalid');
|
||||
}
|
||||
|
||||
if (empty($user)) {
|
||||
throw new IDF_Scm_Exception('usher user is empty');
|
||||
}
|
||||
|
||||
if (empty($pass)) {
|
||||
throw new IDF_Scm_Exception('usher pass is empty');
|
||||
}
|
||||
|
||||
$sock = @fsockopen($host, $port, $errno, $errstr);
|
||||
if (!$sock) {
|
||||
throw new IDF_Scm_Exception(
|
||||
"could not connect to usher: $errstr ($errno)"
|
||||
);
|
||||
}
|
||||
|
||||
fwrite($sock, 'USERPASS '.$user.' '.$pass."\n");
|
||||
if (feof($sock)) {
|
||||
throw new IDF_Scm_Exception(
|
||||
'usher closed the connection - this should not happen'
|
||||
);
|
||||
}
|
||||
|
||||
fwrite($sock, $cmd."\n");
|
||||
$out = '';
|
||||
while (!feof($sock)) {
|
||||
$out .= fgets($sock);
|
||||
}
|
||||
fclose($sock);
|
||||
$out = rtrim($out);
|
||||
|
||||
if ($out == 'unknown command') {
|
||||
throw new IDF_Scm_Exception('unknown command: '.$cmd);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
102
indefero/src/IDF/Scm/Monotone/ZipRender.php
Normal file
102
indefero/src/IDF/Scm/Monotone/ZipRender.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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 ***** */
|
||||
|
||||
require_once(IDF_PATH.'/../contrib/zipstream-php-0.2.2/zipstream.php');
|
||||
|
||||
/**
|
||||
* Special response object to output
|
||||
*
|
||||
* The Content-Length will not be set as it is not possible to predict it.
|
||||
*
|
||||
* Note: The ZipArchive version 0.2.2 has been patched in-tree with this
|
||||
* patch http://pastebin.ca/1977584 to avoid a couple of PHP notices
|
||||
*
|
||||
*/
|
||||
class IDF_Scm_Monotone_ZipRender extends Pluf_HTTP_Response
|
||||
{
|
||||
/**
|
||||
* The revision argument must be a safe string!
|
||||
*
|
||||
* @param Object stdio context
|
||||
* @param string revision
|
||||
* @param string Mimetype (null)
|
||||
*/
|
||||
|
||||
private $stdio = null;
|
||||
private $revision = null;
|
||||
|
||||
function __construct(IDF_Scm_Monotone_IStdio $stdio, $revision)
|
||||
{
|
||||
parent::__construct($revision, 'application/x-zip');
|
||||
$this->stdio = $stdio;
|
||||
$this->revision = $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a response object.
|
||||
*/
|
||||
function render($output_body=true)
|
||||
{
|
||||
$this->outputHeaders();
|
||||
|
||||
if ($output_body) {
|
||||
$certs = $this->stdio->exec(array('certs', $this->revision));
|
||||
$stanzas = IDF_Scm_Monotone_BasicIO::parse($certs);
|
||||
|
||||
// use the revision's date (if there is one) as timestamp
|
||||
// for all file entries
|
||||
$timestamp = time();
|
||||
foreach ($stanzas as $stanza) {
|
||||
$next_is_date = false;
|
||||
foreach ($stanza as $line) {
|
||||
if ($line['key'] == 'name' && $line['values'][0] == 'date') {
|
||||
$next_is_date = true;
|
||||
continue;
|
||||
}
|
||||
if ($next_is_date && $line['key'] == 'value') {
|
||||
$timestamp = strtotime($line['values'][0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$manifest = $this->stdio->exec(array('get_manifest_of', $this->revision));
|
||||
$stanzas = IDF_Scm_Monotone_BasicIO::parse($manifest);
|
||||
|
||||
$zip = new ZipStream();
|
||||
|
||||
foreach ($stanzas as $stanza) {
|
||||
if ($stanza[0]['key'] != 'file')
|
||||
continue;
|
||||
$content = $this->stdio->exec(array('get_file', $stanza[1]['hash']));
|
||||
$zip->add_file(
|
||||
$stanza[0]['values'][0],
|
||||
$content,
|
||||
array('time' => $timestamp)
|
||||
);
|
||||
}
|
||||
|
||||
$zip->finish();
|
||||
}
|
||||
}
|
||||
}
|
636
indefero/src/IDF/Scm/Svn.php
Normal file
636
indefero/src/IDF/Scm/Svn.php
Normal file
@@ -0,0 +1,636 @@
|
||||
<?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 ***** */
|
||||
|
||||
/**
|
||||
* Subversion backend.
|
||||
* When a branch is not a branch.
|
||||
*
|
||||
* Contrary to most other SCMs, Subversion is using folders to manage
|
||||
* the branches and so what is either the commit or the branch in
|
||||
* other SCMs is the revision number with Subversion. So, do not be
|
||||
* surprised if you have the feeling that the methods are not really
|
||||
* returning what could be expected from their names.
|
||||
*/
|
||||
class IDF_Scm_Svn extends IDF_Scm
|
||||
{
|
||||
public $username = '';
|
||||
public $password = '';
|
||||
private $assoc = array('dir' => 'tree',
|
||||
'file' => 'blob');
|
||||
|
||||
public function __construct($repo, $project=null)
|
||||
{
|
||||
$this->repo = $repo;
|
||||
$this->project = $project;
|
||||
$this->cache['commitmess'] = array();
|
||||
}
|
||||
|
||||
public function isAvailable()
|
||||
{
|
||||
$cmd = $this->svnCmd(array('info', '--xml'), $this->repo);
|
||||
$xmlInfo = self::shell_exec('IDF_Scm_Svn::isAvailable', $cmd);
|
||||
|
||||
try {
|
||||
$xml = simplexml_load_string($xmlInfo);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($xml->entry->commit['revision'])) {
|
||||
return false;
|
||||
}
|
||||
if (0 == (int)$xml->entry->commit['revision']) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getRepositorySize()
|
||||
{
|
||||
if (strpos($this->repo, 'file://') !== 0) {
|
||||
return -1;
|
||||
}
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
|
||||
.escapeshellarg(substr($this->repo, 7));
|
||||
$out = explode(' ', self::shell_exec('IDF_Scm_Svn::getRepositorySize', $cmd), 2);
|
||||
return (int) $out[0]*1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the string describing the author from the log find the
|
||||
* author in the database.
|
||||
*
|
||||
* @param string Author
|
||||
* @return mixed Pluf_User or null
|
||||
*/
|
||||
public function findAuthor($author)
|
||||
{
|
||||
$sql = new Pluf_SQL('login=%s', array(trim($author)));
|
||||
$users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
|
||||
return ($users->count() > 0) ? $users[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the subversion repository.
|
||||
*
|
||||
* @param IDF_Project
|
||||
* @param string
|
||||
* @return string URL
|
||||
*/
|
||||
public static function getAnonymousAccessUrl($project,$commit=null)
|
||||
{
|
||||
$conf = $project->getConf();
|
||||
if (false !== ($url=$conf->getVal('svn_remote_url', false))
|
||||
&& !empty($url)) {
|
||||
// Remote repository
|
||||
return $url;
|
||||
}
|
||||
return sprintf(Pluf::f('svn_remote_url'), $project->shortname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the subversion repository.
|
||||
*
|
||||
* @param IDF_Project
|
||||
* @param string
|
||||
* @return string URL
|
||||
*/
|
||||
public static function getAuthAccessUrl($project, $user, $commit=null)
|
||||
{
|
||||
$conf = $project->getConf();
|
||||
if (false !== ($url=$conf->getVal('svn_remote_url', false))
|
||||
&& !empty($url)) {
|
||||
// Remote repository
|
||||
return $url;
|
||||
}
|
||||
return sprintf(Pluf::f('svn_remote_url'), $project->shortname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this object correctly initialized for the project.
|
||||
*
|
||||
* @param IDF_Project
|
||||
* @return IDF_Scm_Svn
|
||||
*/
|
||||
public static function factory($project)
|
||||
{
|
||||
$conf = $project->getConf();
|
||||
// Find the repository
|
||||
if (false !== ($rep=$conf->getVal('svn_remote_url', false))
|
||||
&& !empty($rep)) {
|
||||
// Remote repository
|
||||
$scm = new IDF_Scm_Svn($rep, $project);
|
||||
$scm->username = $conf->getVal('svn_username');
|
||||
$scm->password = $conf->getVal('svn_password');
|
||||
return $scm;
|
||||
} else {
|
||||
$rep = sprintf(Pluf::f('svn_repositories'), $project->shortname);
|
||||
return new IDF_Scm_Svn($rep, $project);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subversion revisions are either a number or 'HEAD'.
|
||||
*/
|
||||
public function validateRevision($rev)
|
||||
{
|
||||
if ($rev == 'HEAD') {
|
||||
return IDF_Scm::REVISION_VALID;
|
||||
}
|
||||
|
||||
$cmd = $this->svnCmd(array('info'), $this->repo, $rev);
|
||||
self::exec('IDF_Scm_Svn::validateRevision', $cmd, $out, $ret);
|
||||
|
||||
if ($ret == 0)
|
||||
return IDF_Scm::REVISION_VALID;
|
||||
return IDF_Scm::REVISION_INVALID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a given object hash.
|
||||
*
|
||||
* @param string Object hash.
|
||||
* @return mixed false if not valid or 'blob', 'tree', 'commit'
|
||||
*/
|
||||
public function testHash($rev, $path='')
|
||||
{
|
||||
// OK if HEAD on /
|
||||
if ($rev === 'HEAD' && $path === '') {
|
||||
return 'commit';
|
||||
}
|
||||
|
||||
// Else, test the path on revision
|
||||
$cmd = $this->svnCmd(array('info', '--xml'),
|
||||
$this->repo.'/'.self::smartEncode($path),
|
||||
$rev);
|
||||
$xmlInfo = self::shell_exec('IDF_Scm_Svn::testHash', $cmd);
|
||||
|
||||
// If exception is thrown, return false
|
||||
try {
|
||||
$xml = simplexml_load_string($xmlInfo);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the entry node does exists, params are wrong
|
||||
if (!isset($xml->entry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Else, enjoy it :)
|
||||
return 'commit';
|
||||
}
|
||||
|
||||
public function getTree($commit, $folder='/', $branch=null)
|
||||
{
|
||||
$cmd = $this->svnCmd(array('ls', '--xml'),
|
||||
$this->repo.'/'.self::smartEncode($folder),
|
||||
$commit);
|
||||
$xml = simplexml_load_string(self::shell_exec('IDF_Scm_Svn::getTree', $cmd));
|
||||
$res = array();
|
||||
$folder = (strlen($folder) and ($folder != '/')) ? $folder.'/' : '';
|
||||
foreach ($xml->list->entry as $entry) {
|
||||
$file = array();
|
||||
$file['type'] = $this->assoc[(string) $entry['kind']];
|
||||
$file['file'] = (string) $entry->name;
|
||||
$file['fullpath'] = $folder.((string) $entry->name);
|
||||
$file['efullpath'] = self::smartEncode($file['fullpath']);
|
||||
$file['date'] = gmdate('Y-m-d H:i:s',
|
||||
strtotime((string) $entry->commit->date));
|
||||
$file['rev'] = (string) $entry->commit['revision'];
|
||||
$file['log'] = $this->getCommitMessage($file['rev']);
|
||||
// Get the size if the type is blob
|
||||
if ($file['type'] == 'blob') {
|
||||
$file['size'] = (string) $entry->size;
|
||||
}
|
||||
$file['author'] = (string) $entry->commit->author;
|
||||
$file['perm'] = '';
|
||||
$res[] = (object) $file;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the commit message of a revision revision.
|
||||
*
|
||||
* @param string Commit ('HEAD')
|
||||
* @return String commit message
|
||||
*/
|
||||
private function getCommitMessage($rev='HEAD')
|
||||
{
|
||||
if (isset($this->cache['commitmess'][$rev])) {
|
||||
return $this->cache['commitmess'][$rev];
|
||||
}
|
||||
$cmd = $this->svnCmd(array('log', '--xml', '--limit', '1'), $this->repo, $rev);
|
||||
try {
|
||||
$xml = simplexml_load_string(self::shell_exec('IDF_Scm_Svn::getCommitMessage', $cmd));
|
||||
$this->cache['commitmess'][$rev] = (string) $xml->logentry->msg;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->cache['commitmess'][$rev] = '';
|
||||
}
|
||||
return $this->cache['commitmess'][$rev];
|
||||
}
|
||||
|
||||
public function getPathInfo($filename, $rev=null)
|
||||
{
|
||||
if ($rev == null) {
|
||||
$rev = 'HEAD';
|
||||
}
|
||||
$cmd = $this->svnCmd(array('info', '--xml'),
|
||||
$this->repo.'/'.self::smartEncode($filename), $rev);
|
||||
$xml = simplexml_load_string(self::shell_exec('IDF_Scm_Svn::getPathInfo', $cmd));
|
||||
if (!isset($xml->entry)) {
|
||||
return false;
|
||||
}
|
||||
$entry = $xml->entry;
|
||||
$file = array();
|
||||
$file['fullpath'] = $filename;
|
||||
$file['hash'] = (string) $entry->repository->uuid;
|
||||
$file['type'] = $this->assoc[(string) $entry['kind']];
|
||||
$pathinfo = pathinfo($filename);
|
||||
$file['file'] = $pathinfo['basename'];
|
||||
$file['rev'] = $rev;
|
||||
$file['author'] = (string) $entry->author;
|
||||
$file['date'] = gmdate('Y-m-d H:i:s', strtotime((string) $entry->commit->date));
|
||||
$file['size'] = (string) $entry->size;
|
||||
$file['log'] = '';
|
||||
return (object) $file;
|
||||
}
|
||||
|
||||
public function getFile($def, $cmd_only=false)
|
||||
{
|
||||
$cmd = $this->svnCmd(array('cat'),
|
||||
$this->repo.'/'.self::smartEncode($def->fullpath),
|
||||
$def->rev);
|
||||
return ($cmd_only) ?
|
||||
$cmd : self::shell_exec('IDF_Scm_Svn::getFile', $cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subversion branches are folder based.
|
||||
*
|
||||
* One need to list the folder to know them.
|
||||
*/
|
||||
public function getBranches()
|
||||
{
|
||||
if (isset($this->cache['branches'])) {
|
||||
return $this->cache['branches'];
|
||||
}
|
||||
$res = array();
|
||||
$cmd = $this->svnCmd(array('ls'), $this->repo.'/branches', 'HEAD');
|
||||
self::exec('IDF_Scm_Svn::getBranches', $cmd, $out, $ret);
|
||||
if ($ret == 0) {
|
||||
foreach ($out as $entry) {
|
||||
if (substr(trim($entry), -1) == '/') {
|
||||
$branch = substr(trim($entry), 0, -1);
|
||||
$res[$branch] = 'branches/'.$branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($res);
|
||||
|
||||
$cmd = $this->svnCmd(array('info'), $this->repo.'/trunk', 'HEAD');
|
||||
self::exec('IDF_Scm_Svn::getBranches', $cmd, $out, $ret);
|
||||
if ($ret == 0) {
|
||||
$res = array('trunk' => 'trunk') + $res;
|
||||
}
|
||||
$this->cache['branches'] = $res;
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subversion tags are folder based.
|
||||
*
|
||||
* One need to list the folder to know them.
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
if (isset($this->cache['tags'])) {
|
||||
return $this->cache['tags'];
|
||||
}
|
||||
$res = array();
|
||||
$cmd = $this->svnCmd(array('ls'), $this->repo.'/tags', 'HEAD');
|
||||
self::exec('IDF_Scm_Svn::getTags', $cmd, $out, $ret);
|
||||
if ($ret == 0) {
|
||||
foreach ($out as $entry) {
|
||||
if (substr(trim($entry), -1) == '/') {
|
||||
$tag = substr(trim($entry), 0, -1);
|
||||
$res[$tag] = 'tags/'.$tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($res);
|
||||
$this->cache['tags'] = $res;
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function getMainBranch()
|
||||
{
|
||||
return 'HEAD';
|
||||
}
|
||||
|
||||
public function inBranches($commit, $path)
|
||||
{
|
||||
foreach ($this->getBranches() as $branch => $bpath) {
|
||||
if ($bpath and 0 === strpos($path, $bpath)) {
|
||||
return array($branch);
|
||||
}
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
public function inTags($commit, $path)
|
||||
{
|
||||
foreach ($this->getTags() as $tag => $tpath) {
|
||||
if ($tpath and 0 === strpos($path, $tpath)) {
|
||||
return array($tag);
|
||||
}
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit details.
|
||||
*
|
||||
* @param string Commit
|
||||
* @param bool Get commit diff (false)
|
||||
* @return array Changes
|
||||
*/
|
||||
public function getCommit($commit, $getdiff=false)
|
||||
{
|
||||
if ($this->validateRevision($commit) != IDF_Scm::REVISION_VALID) {
|
||||
return false;
|
||||
}
|
||||
$res = array();
|
||||
$cmd = $this->svnCmd(array('log', '--xml', '--limit', '1', '-v'),
|
||||
$this->repo, $commit);
|
||||
$xmlRes = self::shell_exec('IDF_Scm_Svn::getCommit', $cmd);
|
||||
$xml = simplexml_load_string($xmlRes);
|
||||
$res['author'] = (string) $xml->logentry->author;
|
||||
$res['date'] = gmdate('Y-m-d H:i:s', strtotime((string) $xml->logentry->date));
|
||||
$res['title'] = (string) $xml->logentry->msg;
|
||||
$res['commit'] = (string) $xml->logentry['revision'];
|
||||
$res['parents'] = $xml->logentry['revision'] > 1
|
||||
? array((string) $xml->logentry['revision'] - 1)
|
||||
: array();
|
||||
$res['diff'] = ($getdiff) ? $this->getDiff($commit) : '';
|
||||
$res['tree'] = '';
|
||||
$res['branch'] = '';
|
||||
return (object) $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a commit is big.
|
||||
*
|
||||
* @param string Commit ('HEAD')
|
||||
* @return bool The commit is big
|
||||
*/
|
||||
public function isCommitLarge($commit='HEAD')
|
||||
{
|
||||
if (substr($this->repo, 0, 7) != 'file://') {
|
||||
return false;
|
||||
}
|
||||
// We have a locally hosted repository, we can query it with
|
||||
// svnlook
|
||||
$repo = substr($this->repo, 7);
|
||||
$cmd = sprintf(Pluf::f('svnlook_path', 'svnlook').' changed -r %s %s',
|
||||
escapeshellarg($commit),
|
||||
escapeshellarg($repo));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
$out = self::shell_exec('IDF_Scm_Svn::isCommitLarge', $cmd);
|
||||
$lines = preg_split("/\015\012|\015|\012/", $out);
|
||||
return (count($lines) > 100);
|
||||
}
|
||||
|
||||
private function getDiff($rev='HEAD')
|
||||
{
|
||||
$res = array();
|
||||
$cmd = $this->svnCmd(array('diff', '-c', $rev), $this->repo);
|
||||
return self::shell_exec('IDF_Scm_Svn::getDiff', $cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see IDF_Scm::getChanges()
|
||||
*/
|
||||
public function getChanges($commit)
|
||||
{
|
||||
if ($this->validateRevision($commit) != IDF_Scm::REVISION_VALID) {
|
||||
return null;
|
||||
}
|
||||
$cmd = $this->svnCmd(array('log', '--xml', '-v'), $this->repo, $commit);
|
||||
$out = array();
|
||||
$out = self::shell_exec('IDF_Scm_Svn::getChanges', $cmd);
|
||||
$xml = simplexml_load_string($out);
|
||||
if (count($xml) == 0) {
|
||||
return null;
|
||||
}
|
||||
$entry = current($xml);
|
||||
|
||||
$return = (object) array(
|
||||
'additions' => array(),
|
||||
'deletions' => array(),
|
||||
'patches' => array(),
|
||||
// while SVN has support for attributes, we cannot see their changes
|
||||
// in the log's XML unfortunately
|
||||
'properties' => array(),
|
||||
'copies' => array(),
|
||||
'renames' => array(),
|
||||
);
|
||||
|
||||
foreach ($entry->paths->path as $p) {
|
||||
$path = (string) $p;
|
||||
foreach ($p->attributes() as $k => $v) {
|
||||
$key = (string) $k;
|
||||
$val = (string) $v;
|
||||
if ($key != 'action')
|
||||
continue;
|
||||
if ($val == 'M')
|
||||
$return->patches[] = $path;
|
||||
else if ($val == 'A')
|
||||
$return->additions[] = $path;
|
||||
else if ($val == 'D')
|
||||
$return->deletions[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
// copies are treated as renames if they have an add _and_ a drop;
|
||||
// only if they only have an add, but no drop, they're treated as copies
|
||||
foreach ($entry->paths->path as $p) {
|
||||
$trg = (string) $p;
|
||||
$src = null;
|
||||
foreach ($p->attributes() as $k => $v) {
|
||||
if ((string) $k == 'copyfrom-path') {
|
||||
$src = (string) $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($src == null)
|
||||
continue;
|
||||
|
||||
$srcidx = array_search($src, $return->deletions);
|
||||
$trgidx = array_search($trg, $return->additions);
|
||||
if ($srcidx !== false && $trgidx !== false) {
|
||||
$return->renames[$src] = $trg;
|
||||
unset($return->deletions[$srcidx]);
|
||||
unset($return->additions[$trgidx]);
|
||||
continue;
|
||||
}
|
||||
if ($srcidx === false && $trgidx !== false) {
|
||||
$return->copies[$src] = $trg;
|
||||
unset($return->additions[$trgidx]);
|
||||
continue;
|
||||
}
|
||||
// file sutures (counter-operation to copy) not supported
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest changes.
|
||||
*
|
||||
* @param string Revision or ('HEAD').
|
||||
* @param int Number of changes (10).
|
||||
*
|
||||
* @return array Changes.
|
||||
*/
|
||||
public function getChangeLog($rev=null, $n=10)
|
||||
{
|
||||
if ($rev != 'HEAD' and !preg_match('/^\d+$/', $rev)) {
|
||||
// we accept only revisions or HEAD
|
||||
$rev = 'HEAD';
|
||||
}
|
||||
$res = array();
|
||||
$cmd = $this->svnCmd(array('log', '--xml', '-v', '--limit', $n),
|
||||
$this->repo.'@'.$rev);
|
||||
$xmlRes = self::shell_exec('IDF_Scm_Svn::getChangeLog', $cmd);
|
||||
$xml = simplexml_load_string($xmlRes);
|
||||
foreach ($xml->logentry as $entry) {
|
||||
$log = array();
|
||||
$log['author'] = (string) $entry->author;
|
||||
$log['date'] = gmdate('Y-m-d H:i:s', strtotime((string) $entry->date));
|
||||
$split = preg_split("[\n\r]", (string) $entry->msg, 2);
|
||||
$log['title'] = $split[0];
|
||||
$log['commit'] = (string) $entry['revision'];
|
||||
$log['full_message'] = (isset($split[1])) ? trim($split[1]) : '';
|
||||
$res[] = (object) $log;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additionnals properties on path and revision
|
||||
*
|
||||
* @param string File
|
||||
* @param string Commit ('HEAD')
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties($rev, $path='')
|
||||
{
|
||||
$res = array();
|
||||
$cmd = $this->svnCmd(array('proplist', '--xml'),
|
||||
$this->repo.'/'.self::smartEncode($path), $rev);
|
||||
$xmlProps = self::shell_exec('IDF_Scm_Svn::getProperties', $cmd);
|
||||
$props = simplexml_load_string($xmlProps);
|
||||
|
||||
// No properties, returns an empty array
|
||||
if (!isset($props->target)) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
// Get the value of each property
|
||||
foreach ($props->target->property as $prop) {
|
||||
$key = (string) $prop['name'];
|
||||
$res[$key] = $this->getProperty($key, $rev, $path);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific additionnal property on path and revision
|
||||
*
|
||||
* @param string Property
|
||||
* @param string File
|
||||
* @param string Commit ('HEAD')
|
||||
* @return string the property value
|
||||
*/
|
||||
private function getProperty($property, $rev, $path='')
|
||||
{
|
||||
$res = array();
|
||||
$cmd = $this->svnCmd(array('propget', $property, '--xml'),
|
||||
$this->repo.'/'.self::smartEncode($path), $rev);
|
||||
$xmlProp = self::shell_exec('IDF_Scm_Svn::getProperty', $cmd);
|
||||
$prop = simplexml_load_string($xmlProp);
|
||||
|
||||
return (string) $prop->target->property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of the last commit in the repository.
|
||||
*
|
||||
* @param string Commit ('HEAD').
|
||||
*
|
||||
* @return String last number commit
|
||||
*/
|
||||
public function getLastCommit($rev='HEAD')
|
||||
{
|
||||
$xmlInfo = '';
|
||||
$cmd = $this->svnCmd(array('info', '--xml'), $this->repo, $rev);
|
||||
$xmlInfo = self::shell_exec('IDF_Scm_Svn::getLastCommit', $cmd);
|
||||
|
||||
$xml = simplexml_load_string($xmlInfo);
|
||||
return (string) $xml->entry->commit['revision'];
|
||||
}
|
||||
|
||||
private function svnCmd($args = array(), $repoarg = null, $revarg = null)
|
||||
{
|
||||
$cmdline = array();
|
||||
$cmdline[] = Pluf::f('idf_exec_cmd_prefix', '');
|
||||
$cmdline[] = Pluf::f('svn_path', 'svn');
|
||||
$cmdline[] = '--no-auth-cache';
|
||||
$cmdline[] = '--username='.escapeshellarg($this->username);
|
||||
$cmdline[] = '--password='.escapeshellarg($this->password);
|
||||
|
||||
foreach ($args as $arg) {
|
||||
$cmdline[] = escapeshellarg($arg);
|
||||
}
|
||||
|
||||
if ($repoarg != null) {
|
||||
if ($revarg != null) {
|
||||
$repoarg .= '@'.$revarg;
|
||||
}
|
||||
$cmdline[] = escapeshellarg($repoarg);
|
||||
}
|
||||
|
||||
if ($revarg != null) {
|
||||
$cmdline[] = '--revision='.escapeshellarg($revarg);
|
||||
}
|
||||
|
||||
return implode(' ', $cmdline);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user