Merge branch 'mnt-support'

This commit is contained in:
Loïc d'Anterroches 2010-08-27 08:59:09 +02:00
commit 14d07a22e2
39 changed files with 2691 additions and 266 deletions

View File

@ -0,0 +1,63 @@
# monotone implementation notes
## general
This version of indefero contains an implementation of the monotone
automation interface. It needs at least monotone version 0.47
(interface version 12.0) or newer, but as development continues, its
likely that this dependency has to be raised.
To set up a new IDF project with monotone quickly, all you need to do
is to create a new monotone database with
$ mtn db init -d project.mtn
in the configured repository path `$cfg['mtn_repositories']` and
configure `$cfg['mtn_db_access']` to "local".
To have a really workable setup, this database needs an initial commit
on the configured master branch of the project. This can be done easily
with
$ mkdir tmp && touch tmp/remove_me
$ mtn import -d project.mtn -b master.branch.name \
-m "initial commit" tmp
$ rm -rf tmp
Its expected that more scripts arrive soon to automate this and other
tasks in the future for (multi)forge setups.
## current state / internals
The implementation should be fairly stable and fast, though some
information, such as individual file sizes or last change information,
won't scale well with the tree size. Its expected that the mtn
automation interface improves in this area in the future and that
these parts can then be rewritten with speed in mind.
As the idf.conf-dist explains more in detail, different access patterns
are possible to retrieve changeset data from monotone. Please refer
to the documentation there for more information.
## indefero critique:
It was not always 100% clear what some of the abstract SCM API method
wanted in return. While it helped a lot to have prior art in form of the
SVN and git implementation, the documentation of the abstract IDF_Scm
should probably still be improved.
Since branch and tag names can be of arbitrary size, it was not possible
to display them completely in the default layout. This might be a problem
in other SCMs as well, in particular for the monotone implementation I
introduced a special filter, called "IDF_Views_Source_ShortenString".
The API methods getPathInfo() and getTree() return similar VCS "objects"
which unfortunately do not have a well-defined structure - this should
probably addressed in future indefero releases.
While the returned objects from getTree() contain all the needed
information, indefero doesn't seem to use them to sort the output
f.e. alphabetically or in such a way that directories are outputted
before files. It was unclear if the SCM implementor should do this
task or not and what the admired default sorting should be.

View File

@ -71,6 +71,25 @@ class IDF_Diff
$current_chunk = 0;
$indiff = true;
continue;
} else if (0 === strpos($line, '=========')) {
// by default always use the new name of a possibly renamed file
$current_file = self::getMtnFile($this->lines[$i+1]);
// mtn 0.48 and newer set /dev/null as file path for dropped files
// so we display the old name here
if ($current_file == "/dev/null") {
$current_file = self::getMtnFile($this->lines[$i]);
}
if ($current_file == "/dev/null") {
throw new Exception(
"could not determine path from diff"
);
}
$files[$current_file] = array();
$files[$current_file]['chunks'] = array();
$files[$current_file]['chunks_def'] = array();
$current_chunk = 0;
$indiff = true;
continue;
} else if (0 === strpos($line, 'Index: ')) {
$current_file = self::getSvnFile($line);
$files[$current_file] = array();
@ -133,6 +152,12 @@ class IDF_Diff
return substr(trim($line), 7);
}
public static function getMtnFile($line)
{
preg_match("/^[+-]{3} ([^\t]+)/", $line, $m);
return $m[1];
}
/**
* Return the html version of a parsed diff.
*/

View File

@ -38,6 +38,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'git' => __('git'),
'svn' => __('Subversion'),
'mercurial' => __('mercurial'),
'mtn' => __('monotone'),
);
foreach (Pluf::f('allowed_scm', array()) as $key => $class) {
$choices[$options[$key]] = $key;
@ -92,6 +93,13 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'widget' => 'Pluf_Form_Widget_PasswordInput',
));
$this->fields['mtn_master_branch'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Master branch'),
'initial' => '',
'help_text' => __('This should be a world-wide unique identifier for your project. A reverse DNS notation like "com.my-domain.my-project" is a good idea.'),
));
$this->fields['owners'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Project owners'),
@ -170,6 +178,34 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
return $url;
}
public function clean_mtn_master_branch()
{
// do not validate, but empty the field if a different
// SCM should be used
if ($this->cleaned_data['scm'] != 'mtn')
return '';
$mtn_master_branch = mb_strtolower($this->cleaned_data['mtn_master_branch']);
if (!preg_match('/^([\w\d]+([-][\w\d]+)*)(\.[\w\d]+([-][\w\d]+)*)*$/',
$mtn_master_branch)) {
throw new Pluf_Form_Invalid(__(
'The master branch is empty or contains illegal characters, '.
'please use only letters, digits, dashs and dots as separators.'
));
}
$sql = new Pluf_SQL('vkey=%s AND vdesc=%s',
array("mtn_master_branch", $mtn_master_branch));
$l = Pluf::factory('IDF_Conf')->getList(array('filter'=>$sql->gen()));
if ($l->count() > 0) {
throw new Pluf_Form_Invalid(__(
'This master branch is already used. Please select another one.'
));
}
return $mtn_master_branch;
}
public function clean_shortname()
{
$shortname = mb_strtolower($this->cleaned_data['shortname']);
@ -198,6 +234,11 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
$this->cleaned_data[$key] = '';
}
}
if ($this->cleaned_data['scm'] != 'mtn') {
$this->cleaned_data['mtn_master_branch'] = '';
}
/**
* [signal]
*
@ -246,8 +287,8 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
$project->create();
$conf = new IDF_Conf();
$conf->setProject($project);
$keys = array('scm', 'svn_remote_url',
'svn_username', 'svn_password');
$keys = array('scm', 'svn_remote_url', 'svn_username',
'svn_password', 'mtn_master_branch');
foreach ($keys as $key) {
$this->cleaned_data[$key] = (!empty($this->cleaned_data[$key])) ?
$this->cleaned_data[$key] : '';
@ -284,6 +325,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
}
}
$project->created();
if ($this->cleaned_data['template'] == '--') {
IDF_Form_MembersConf::updateMemberships($project,
$this->cleaned_data);

View File

@ -82,17 +82,15 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
),
));
$this->fields['ssh_key'] = new Pluf_Form_Field_Varchar(
$this->fields['public_key'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Add a public SSH key'),
'label' => __('Add a public key'),
'initial' => '',
'widget_attrs' => array('rows' => 3,
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
'help_text' => __('Be careful to provide the public key and not the private key!')
'help_text' => __('Paste a SSH or monotone public key. Be careful to not provide your private key here!')
));
}
/**
@ -137,11 +135,11 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
$params = array('user' => $user);
Pluf_Signal::send('Pluf_User::passwordUpdated',
'IDF_Form_Admin_UserCreate', $params);
// Create the ssh key as needed
if ('' !== $this->cleaned_data['ssh_key']) {
// Create the public key as needed
if ('' !== $this->cleaned_data['public_key']) {
$key = new IDF_Key();
$key->user = $user;
$key->content = $this->cleaned_data['ssh_key'];
$key->content = $this->cleaned_data['public_key'];
$key->create();
}
// Send an email to the user with the password
@ -162,11 +160,6 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
return $user;
}
function clean_ssh_key()
{
return IDF_Form_UserAccount::checkSshKey($this->cleaned_data['ssh_key']);
}
function clean_last_name()
{
$last_name = trim($this->cleaned_data['last_name']);
@ -211,4 +204,12 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
}
return $this->cleaned_data['login'];
}
public function clean_public_key()
{
$this->cleaned_data['public_key'] =
IDF_Form_UserAccount::checkPublicKey($this->cleaned_data['public_key']);
return $this->cleaned_data['public_key'];
}
}

View File

@ -92,17 +92,15 @@ class IDF_Form_UserAccount extends Pluf_Form
),
));
$this->fields['ssh_key'] = new Pluf_Form_Field_Varchar(
$this->fields['public_key'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Add a public SSH key'),
'label' => __('Add a public key'),
'initial' => '',
'widget_attrs' => array('rows' => 3,
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
'help_text' => __('Be careful to provide your public key and not your private key!')
'help_text' => __('Paste a SSH or monotone public key. Be careful to not provide your private key here!')
));
}
/**
@ -151,10 +149,10 @@ class IDF_Form_UserAccount extends Pluf_Form
}
$this->user->setFromFormData($this->cleaned_data);
// Add key as needed.
if ('' !== $this->cleaned_data['ssh_key']) {
if ('' !== $this->cleaned_data['public_key']) {
$key = new IDF_Key();
$key->user = $this->user;
$key->content = $this->cleaned_data['ssh_key'];
$key->content = $this->cleaned_data['public_key'];
if ($commit) {
$key->create();
}
@ -190,7 +188,7 @@ class IDF_Form_UserAccount extends Pluf_Form
}
/**
* Check an ssh key.
* Check arbitrary public keys.
*
* It will throw a Pluf_Form_Invalid exception if it cannot
* validate the key.
@ -199,27 +197,59 @@ class IDF_Form_UserAccount extends Pluf_Form
* @param $user int The user id of the user of the key (0)
* @return string The clean key
*/
public static function checkSshKey($key, $user=0)
public static function checkPublicKey($key, $user=0)
{
$key = trim($key);
if (strlen($key) == 0) {
return '';
}
if (preg_match('#^ssh\-[a-z]{3}\s\S+\s\S+$#', $key)) {
$key = str_replace(array("\n", "\r"), '', $key);
if (!preg_match('#^ssh\-[a-z]{3}\s(\S+)\s\S+$#', $key, $matches)) {
throw new Pluf_Form_Invalid(__('The format of the key is not valid. It must start with ssh-dss or ssh-rsa, a long string on a single line and at the end a comment.'));
}
if (Pluf::f('idf_strong_key_check', false)) {
$tmpfile = Pluf::f('tmp_folder', '/tmp').'/'.$user.'-key';
file_put_contents($tmpfile, $key, LOCK_EX);
$cmd = Pluf::f('idf_exec_cmd_prefix', '').
'ssh-keygen -l -f '.escapeshellarg($tmpfile).' > /dev/null 2>&1';
exec($cmd, $out, $return);
unlink($tmpfile);
if ($return != 0) {
throw new Pluf_Form_Invalid(__('Please check the key as it does not appears to be a valid key.'));
throw new Pluf_Form_Invalid(
__('Please check the key as it does not appear '.
'to be a valid SSH public key.')
);
}
}
}
else if (preg_match('#^\[pubkey [^\]]+\]\s*\S+\s*\[end\]$#', $key)) {
if (Pluf::f('idf_strong_key_check', false)) {
// if monotone can read it, it should be valid
$mtn_opts = implode(' ', Pluf::f('mtn_opts', array()));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').
sprintf('%s %s -d :memory: read >/tmp/php-out 2>&1',
Pluf::f('mtn_path', 'mtn'), $mtn_opts);
$fp = popen($cmd, 'w');
fwrite($fp, $key);
$return = pclose($fp);
if ($return != 0) {
throw new Pluf_Form_Invalid(
__('Please check the key as it does not appear '.
'to be a valid monotone public key.')
);
}
}
}
else {
throw new Pluf_Form_Invalid(
__('Public key looks neither like a SSH '.
'nor monotone public key.'));
}
// If $user, then check if not the same key stored
if ($user) {
$ruser = Pluf::factory('Pluf_User', $user);
@ -227,19 +257,15 @@ class IDF_Form_UserAccount extends Pluf_Form
$sql = new Pluf_SQL('content=%s', array($key));
$keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen()));
if (count($keys) > 0) {
throw new Pluf_Form_Invalid(__('You already have uploaded this SSH key.'));
throw new Pluf_Form_Invalid(
__('You already have uploaded this key.')
);
}
}
}
return $key;
}
function clean_ssh_key()
{
return self::checkSshKey($this->cleaned_data['ssh_key'],
$this->user->id);
}
function clean_last_name()
{
$last_name = trim($this->cleaned_data['last_name']);
@ -272,8 +298,16 @@ class IDF_Form_UserAccount extends Pluf_Form
return $this->cleaned_data['email'];
}
function clean_public_key()
{
$this->cleaned_data['public_key'] =
self::checkPublicKey($this->cleaned_data['public_key'],
$this->user->id);
return $this->cleaned_data['public_key'];
}
/**
* Check to see if the 2 passwords are the same.
* Check to see if the 2 passwords are the same
*/
public function clean()
{
@ -285,6 +319,7 @@ class IDF_Form_UserAccount extends Pluf_Form
throw new Pluf_Form_Invalid(__('The passwords do not match. Please give them again.'));
}
}
return $this->cleaned_data;
}
}

View File

@ -22,7 +22,7 @@
# ***** END LICENSE BLOCK ***** */
/**
* Storage of the SSH keys.
* Storage of the public keys (ssh or monotone).
*
*/
class IDF_Key extends Pluf_Model
@ -52,7 +52,7 @@ class IDF_Key extends Pluf_Model
array(
'type' => 'Pluf_DB_Field_Text',
'blank' => false,
'verbose' => __('ssh key'),
'verbose' => __('public key'),
),
);
// WARNING: Not using getSqlTable on the Pluf_User object to
@ -75,6 +75,58 @@ class IDF_Key extends Pluf_Model
return Pluf_Template::markSafe(Pluf_esc(substr($this->content, 0, 25)).' [...] '.Pluf_esc(substr($this->content, -55)));
}
private function parseContent()
{
if (preg_match('#^\[pubkey ([^\]]+)\]\s*(\S+)\s*\[end\]$#', $this->content, $m)) {
return array('mtn', $m[1], $m[2]);
}
else if (preg_match('#^ssh\-[a-z]{3}\s(\S+)\s(\S+)$#', $this->content, $m)) {
return array('ssh', $m[2], $m[1]);
}
throw new IDF_Exception('invalid or unknown key data detected');
}
/**
* Returns the type of the public key
*
* @return string 'ssh' or 'mtn'
*/
function getType()
{
list($type, , ) = $this->parseContent();
return $type;
}
/**
* Returns the key name of the key
*
* @return string
*/
function getName()
{
list(, $keyName, ) = $this->parseContent();
return $keyName;
}
/**
* This function should be used to calculate the key id from the
* public key hash for authentication purposes. This avoids clashes
* in case the key name is not unique across the project
*
* And yes, this is actually how monotone itself calculates the key
* id...
*
* @return string
*/
function getMtnId()
{
list($type, $keyName, $keyData) = $this->parseContent();
if ($type != 'mtn')
throw new IDF_Exception('key is not a monotone public key');
return sha1($keyName.":".$keyData);
}
function postSave($create=false)
{
/**
@ -89,7 +141,7 @@ class IDF_Key extends Pluf_Model
* [description]
*
* This signal allows an application to perform special
* operations after the saving of a SSH Key.
* operations after the saving of a public Key.
*
* [parameters]
*
@ -127,5 +179,4 @@ class IDF_Key extends Pluf_Model
Pluf_Signal::send('IDF_Key::preDelete',
'IDF_Key', $params);
}
}

View File

@ -92,6 +92,7 @@ class IDF_Middleware
array(
'size' => 'IDF_Views_Source_PrettySize',
'ssize' => 'IDF_Views_Source_PrettySizeSimple',
'shorten' => 'IDF_Views_Source_ShortenString',
));
}
}
@ -110,6 +111,7 @@ function IDF_Middleware_ContextPreProcessor($request)
$request->project);
$c = array_merge($c, $request->rights);
}
$c['usherConfigured'] = Pluf::f("mtn_usher", null) !== null;
return $c;
}

View File

@ -48,8 +48,7 @@ class IDF_Plugin_SyncGit_Cron
$out = '';
$keys = Pluf::factory('IDF_Key')->getList(array('view'=>'join_user'));
foreach ($keys as $key) {
if (strlen($key->content) > 40 // minimal check
and preg_match('/^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$/', $key->login)) {
if ($key->getType() == 'ssh' and preg_match('/^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$/', $key->login)) {
$content = trim(str_replace(array("\n", "\r"), '', $key->content));
$out .= sprintf($template, $cmd, $key->login, $content)."\n";
}

View File

@ -382,15 +382,16 @@ class IDF_Project extends Pluf_Model
* This will return the right url based on the user.
*
* @param Pluf_User The user (null)
* @param string A specific commit to access
*/
public function getSourceAccessUrl($user=null)
public function getSourceAccessUrl($user=null, $commit=null)
{
$right = $this->getConf()->getVal('source_access_rights', 'all');
if (($user == null or $user->isAnonymous())
and $right == 'all' and !$this->private) {
return $this->getRemoteAccessUrl();
return $this->getRemoteAccessUrl($commit);
}
return $this->getWriteRemoteAccessUrl($user);
return $this->getWriteRemoteAccessUrl($user, $commit);
}
@ -398,15 +399,17 @@ class IDF_Project extends Pluf_Model
* Get the remote access url to the repository.
*
* This will always return the anonymous access url.
*
* @param string A specific commit to access
*/
public function getRemoteAccessUrl()
public function getRemoteAccessUrl($commit=null)
{
$conf = $this->getConf();
$scm = $conf->getVal('scm', 'git');
$scms = Pluf::f('allowed_scm');
Pluf::loadClass($scms[$scm]);
return call_user_func(array($scms[$scm], 'getAnonymousAccessUrl'),
$this);
$this, $commit);
}
/**
@ -415,14 +418,16 @@ class IDF_Project extends Pluf_Model
* Some SCM have a remote access URL to write which is not the
* same as the one to read. For example, you do a checkout with
* git-daemon and push with SSH.
*
* @param string A specific commit to access
*/
public function getWriteRemoteAccessUrl($user)
public function getWriteRemoteAccessUrl($user,$commit=null)
{
$conf = $this->getConf();
$scm = $conf->getVal('scm', 'git');
$scms = Pluf::f('allowed_scm');
return call_user_func(array($scms[$scm], 'getAuthAccessUrl'),
$this, $user);
$this, $user, $commit);
}
/**
@ -447,7 +452,8 @@ class IDF_Project extends Pluf_Model
$roots = array(
'git' => 'master',
'svn' => 'HEAD',
'mercurial' => 'tip'
'mercurial' => 'tip',
'mtn' => 'h:'.$conf->getVal('mtn_master_branch', '*'),
);
$scm = $conf->getVal('scm', 'git');
return $roots[$scm];

View File

@ -273,12 +273,12 @@ class IDF_Scm_Git extends IDF_Scm
return null;
}
public static function getAnonymousAccessUrl($project)
public static function getAnonymousAccessUrl($project, $commit=null)
{
return sprintf(Pluf::f('git_remote_url'), $project->shortname);
}
public static function getAuthAccessUrl($project, $user)
public static function getAuthAccessUrl($project, $user, $commit=null)
{
return sprintf(Pluf::f('git_write_remote_url'), $project->shortname);
}

View File

@ -77,12 +77,12 @@ class IDF_Scm_Mercurial extends IDF_Scm
return 'tip';
}
public static function getAnonymousAccessUrl($project)
public static function getAnonymousAccessUrl($project, $commit=null)
{
return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname);
}
public static function getAuthAccessUrl($project, $user)
public static function getAuthAccessUrl($project, $user, $commit=null)
{
return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname);
}

731
src/IDF/Scm/Monotone.php Normal file
View File

@ -0,0 +1,731 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
require_once(dirname(__FILE__) . "/Monotone/Stdio.php");
/**
* Monotone scm class
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
class IDF_Scm_Monotone extends IDF_Scm
{
/** the minimum supported interface version */
public static $MIN_INTERFACE_VERSION = 12.0;
private $stdio;
private static $instances = array();
/**
* @see IDF_Scm::__construct()
*/
public function __construct($project)
{
$this->project = $project;
$this->stdio = new IDF_Scm_Monotone_Stdio($project);
}
/**
* @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 = "*";
}
if (count($this->_resolveSelector("h:$branch")) == 0) {
throw new IDF_Scm_Exception(
"Branch $branch is empty"
);
}
return $branch;
}
/**
* 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);
}
/**
* Parses monotone's basic_io format
*
* @param string $in
* @return array of arrays
*/
private static function _parseBasicIO($in)
{
$pos = 0;
$stanzas = array();
while ($pos < strlen($in)) {
$stanza = array();
while ($pos < strlen($in)) {
if ($in[$pos] == "\n") break;
$stanzaLine = array('key' => '', 'values' => array(), 'hash' => null);
while ($pos < strlen($in)) {
$ch = $in[$pos];
if ($ch == '"' || $ch == '[') break;
++$pos;
if ($ch == ' ') continue;
$stanzaLine['key'] .= $ch;
}
if ($in[$pos] == '[') {
++$pos; // opening square bracket
$stanzaLine['hash'] = substr($in, $pos, 40);
$pos += 40;
++$pos; // closing square bracket
}
else
{
$valCount = 0;
while ($in[$pos] == '"') {
++$pos; // opening quote
$stanzaLine['values'][$valCount] = '';
while ($pos < strlen($in)) {
$ch = $in[$pos]; $pr = $in[$pos-1];
if ($ch == '"' && $pr != '\\') break;
++$pos;
$stanzaLine['values'][$valCount] .= $ch;
}
++$pos; // closing quote
if ($in[$pos] == ' ') {
++$pos; // space
++$valCount;
}
}
for ($i = 0; $i <= $valCount; $i++) {
$stanzaLine['values'][$i] = str_replace(
array("\\\\", "\\\""),
array("\\", "\""),
$stanzaLine['values'][$i]
);
}
}
$stanza[] = $stanzaLine;
++$pos; // newline
}
$stanzas[] = $stanza;
++$pos; // newline
}
return $stanzas;
}
/**
* 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)
{
static $certCache = array();
if (!array_key_exists($rev, $certCache)) {
$out = $this->stdio->exec(array('certs', $rev));
$stanzas = self::_parseBasicIO($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;
}
}
}
$certCache[$rev] = $certs;
}
return $certCache[$rev];
}
/**
* 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);
}
/**
* Returns the revision in which the file has been last changed,
* starting from the start rev
*
* @param string
* @param string
* @return string
*/
private function _getLastChangeFor($file, $startrev)
{
$out = $this->stdio->exec(array(
'get_content_changed', $startrev, $file
));
$stanzas = self::_parseBasicIO($out);
// FIXME: we only care about the first returned content mark
// everything else seem to be very, very rare cases
foreach ($stanzas as $stanza) {
foreach ($stanza as $stanzaline) {
if ($stanzaline['key'] == 'content_mark') {
return $stanzaline['hash'];
}
}
}
return null;
}
/**
* @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 = self::_parseBasicIO($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:');
}
/**
* @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_manifest_of', $revs[0]
));
$files = array();
$stanzas = self::_parseBasicIO($out);
$folder = $folder == '/' || empty($folder) ? '' : $folder.'/';
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'format_version')
continue;
$path = $stanza[0]['values'][0];
if (!preg_match('#^'.$folder.'([^/]+)$#', $path, $m))
continue;
$file = array();
$file['file'] = $m[1];
$file['fullpath'] = $path;
$file['efullpath'] = self::smartEncode($path);
if ($stanza[0]['key'] == 'dir') {
$file['type'] = 'tree';
$file['size'] = 0;
}
else
{
$file['type'] = 'blob';
$file['hash'] = $stanza[1]['hash'];
$file['size'] = strlen($this->getFile((object)$file));
}
$rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]);
if ($rev !== null) {
$file['rev'] = $rev;
$certs = $this->_getCerts($rev);
// 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);
$file['log'] = implode("\n---\n", $certs['changelog']);
}
$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;
}
foreach (array('email', 'login') as $what) {
$sql = new Pluf_SQL($what.'=%s', array($match[1]));
$users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
if ($users->count() > 0) {
return $users[0];
}
}
return null;
}
/**
* @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 (count($certs['branch']) == 0) {
$branch = '*';
}
else
{
$branch = $certs['branch'][0];
}
}
}
$remote_url = Pluf::f('mtn_remote_url', '');
if (empty($remote_url)) {
return '';
}
return sprintf($remote_url, $project->shortname).'?'.$branch;
}
/**
* @see IDF_Scm::getAuthAccessUrl()
*/
public static function getAuthAccessUrl($project, $user, $commit = null)
{
$url = self::getAnonymousAccessUrl($project, $commit);
return preg_replace("#^ssh://#", "ssh://$user@", $url);
}
/**
* Returns this object correctly initialized for the project.
*
* @param IDF_Project
* @return IDF_Scm_Monotone
*/
public static function factory($project)
{
if (!array_key_exists($project->shortname, self::$instances)) {
self::$instances[$project->shortname] =
new IDF_Scm_Monotone($project);
}
return self::$instances[$project->shortname];
}
/**
* @see IDF_Scm::isValidRevision()
*/
public function isValidRevision($commit)
{
$revs = $this->_resolveSelector($commit);
return count($revs) == 1;
}
/**
* @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_manifest_of', $revs[0]
));
$files = array();
$stanzas = self::_parseBasicIO($out);
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'format_version')
continue;
$path = $stanza[0]['values'][0];
if (!preg_match('#^'.$file.'$#', $path, $m))
continue;
$file = array();
$file['fullpath'] = $path;
if ($stanza[0]['key'] == "dir") {
$file['type'] = "tree";
$file['hash'] = null;
$file['size'] = 0;
}
else
{
$file['type'] = 'blob';
$file['hash'] = $stanza[1]['hash'];
$file['size'] = strlen($this->getFile((object)$file));
}
$pathinfo = pathinfo($file['fullpath']);
$file['file'] = $pathinfo['basename'];
$rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]);
if ($rev !== null) {
$file['rev'] = $rev;
$certs = $this->_getCerts($rev);
// 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);
$file['log'] = implode("\n---\n", $certs['changelog']);
}
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::getCommit()
*/
public function getCommit($commit, $getdiff=false)
{
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0)
return array();
$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);
$res['title'] = implode("\n---\n", $certs['changelog']);
$res['commit'] = $revs[0];
$res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : '';
return (object) $res;
}
/**
* @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 = self::_parseBasicIO($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) {
$initialBranches = $certs['branch'];
}
// only add it to our log if it is on one of the initial branches
if (count(array_intersect($initialBranches, $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;
}
}

View File

@ -0,0 +1,360 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Monotone 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
{
/** 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();
}
/**
* 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', '') .
Pluf::f('mtn_path', 'mtn') . ' ';
$opts = Pluf::f('mtn_opts', array());
foreach ($opts as $opt) {
$cmd .= sprintf('%s ', escapeshellarg($opt));
}
// FIXME: we might want to add an option for anonymous / no key
// access, but upstream bug #30237 prevents that for now
if ($remote_db_access) {
$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]);
$streamsChanged = stream_select(
$read, $write = null, $except = null, 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;
}
}

View File

@ -0,0 +1,241 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* 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);
}
/**
* 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);
$ret = array();
foreach ($single_conns as $conn) {
preg_match('/\(\w+\)([^:]):(\d+)/', $conn, $matches);
$ret[$matches[1]][] = (object)array(
'server' => $matches[1],
'address' => $matches[2],
'port' => $matches[3],
);
}
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');
if (empty($uc['host'])) {
throw new IDF_Scm_Exception('usher host is empty');
}
if (!preg_match('/^\d+$/', $uc['port']) ||
$uc['port'] == 0)
{
throw new IDF_Scm_Exception('usher port is invalid');
}
if (empty($uc['user'])) {
throw new IDF_Scm_Exception('usher user is empty');
}
if (empty($uc['pass'])) {
throw new IDF_Scm_Exception('usher pass is empty');
}
$sock = @fsockopen($uc['host'], $uc['port'], $errno, $errstr);
if (!$sock) {
throw new IDF_Scm_Exception(
"could not connect to usher: $errstr ($errno)"
);
}
fwrite($sock, 'USERPASS '.$uc['user'].' '.$uc['pass'].'\n');
if (feof($sock)) {
throw new IDF_Scm_Exception(
'usher closed the connection - probably wrong admin '.
'username or password'
);
}
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;
}
}

View File

@ -80,9 +80,10 @@ class IDF_Scm_Svn extends IDF_Scm
* Returns the URL of the subversion repository.
*
* @param IDF_Project
* @param string
* @return string URL
*/
public static function getAnonymousAccessUrl($project)
public static function getAnonymousAccessUrl($project,$commit=null)
{
$conf = $project->getConf();
if (false !== ($url=$conf->getVal('svn_remote_url', false))
@ -97,9 +98,10 @@ class IDF_Scm_Svn extends IDF_Scm
* Returns the URL of the subversion repository.
*
* @param IDF_Project
* @param string
* @return string URL
*/
public static function getAuthAccessUrl($project, $user)
public static function getAuthAccessUrl($project, $user, $commit=null)
{
$conf = $project->getConf();
if (false !== ($url=$conf->getVal('svn_remote_url', false))

View File

@ -0,0 +1,239 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
require_once("simpletest/autorun.php");
/**
* Test the monotone class.
*/
class IDF_Tests_TestMonotone extends UnitTestCase
{
private $tmpdir, $dbfile, $mtnInstance;
private function mtnCall($args, $stdin = null, $dir = null)
{
// if you have an SSH agent running for key caching,
// please disable it
$cmdline = array("mtn",
"--confdir", $this->tmpdir,
"--db", $this->dbfile,
"--norc",
"--timestamps");
$cmdline = array_merge($cmdline, $args);
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "{$this->tmpdir}/mtn-errors", "a")
);
$pipes = array();
$dir = !empty($dir) ? $dir : $this->tmpdir;
$process = proc_open(implode(" ", $cmdline),
$descriptorspec,
$pipes,
$dir);
if (!is_resource($process)) {
throw new Exception("could not create process");
}
if (!empty($stdin)) {
fwrite($pipes[0], $stdin);
fclose($pipes[0]);
}
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$ret = proc_close($process);
if ($ret != 0) {
throw new Exception(
"call ended with a non-zero error code (complete cmdline was: ".
implode(" ", $cmdline).")"
);
}
return $stdout;
}
public function __construct()
{
parent::__construct("Test the monotone class.");
$this->tmpdir = sys_get_temp_dir() . "/mtn-test";
$this->dbfile = "{$this->tmpdir}/test.mtn";
set_include_path(get_include_path() . ":../../../pluf-master/src");
require_once("Pluf.php");
Pluf::start(dirname(__FILE__)."/../conf/idf.php");
// Pluf::f() mocking
$GLOBALS['_PX_config']['mtn_repositories'] = "{$this->tmpdir}/%s.mtn";
}
private static function deleteRecursive($dirname)
{
if (is_dir($dirname))
$dir_handle=opendir($dirname);
while ($file = readdir($dir_handle)) {
if ($file!="." && $file!="..") {
if (!is_dir($dirname."/".$file)) {
unlink ($dirname."/".$file);
continue;
}
self::deleteRecursive($dirname."/".$file);
}
}
closedir($dir_handle);
rmdir($dirname);
return true;
}
public function setUp()
{
if (is_dir($this->tmpdir)) {
self::deleteRecursive($this->tmpdir);
}
mkdir($this->tmpdir);
$this->mtnCall(array("db", "init"));
$this->mtnCall(array("genkey", "test@test.de"), "\n\n");
$workspaceRoot = "{$this->tmpdir}/test-workspace";
mkdir($workspaceRoot);
$this->mtnCall(array("setup", "-b", "testbranch", "."), null, $workspaceRoot);
file_put_contents("$workspaceRoot/foo", "blubber");
$this->mtnCall(array("add", "foo"), null, $workspaceRoot);
$this->mtnCall(array("commit", "-m", "initial"), null, $workspaceRoot);
file_put_contents("$workspaceRoot/bar", "blafoo");
mkdir("$workspaceRoot/subdir");
file_put_contents("$workspaceRoot/subdir/bla", "blabla");
$this->mtnCall(array("add", "-R", "--unknown"), null, $workspaceRoot);
$this->mtnCall(array("commit", "-m", "second"), null, $workspaceRoot);
$rev = $this->mtnCall(array("au", "get_base_revision_id"), null, $workspaceRoot);
$this->mtnCall(array("tag", rtrim($rev), "release-1.0"));
$project = new IDF_Project();
$project->shortname = "test";
$this->mtnInstance = new IDF_Scm_Monotone($project);
}
public function testIsAvailable()
{
$this->assertTrue($this->mtnInstance->isAvailable());
}
public function testGetBranches()
{
$branches = $this->mtnInstance->getBranches();
$this->assertEqual(1, count($branches));
list($key, $value) = each($branches);
$this->assertEqual("h:testbranch", $key);
$this->assertEqual("testbranch", $value);
}
public function testGetTags()
{
$tags = $this->mtnInstance->getTags();
$this->assertEqual(1, count($tags));
list($key, $value) = each($tags);
$this->assertEqual("t:release-1.0", $key);
$this->assertEqual("release-1.0", $value);
}
public function testInBranches()
{
$revOut = $this->mtnCall(array("au", "select", "b:testbranch"));
$revs = preg_split('/\n/', $revOut, -1, PREG_SPLIT_NO_EMPTY);
$branches = $this->mtnInstance->inBranches($revs[0], null);
$this->assertEqual(1, count($branches));
$this->assertEqual("h:testbranch", $branches[0]);
$branches = $this->mtnInstance->inBranches("t:release-1.0", null);
$this->assertEqual(1, count($branches));
$this->assertEqual("h:testbranch", $branches[0]);
}
public function testInTags()
{
$rev = $this->mtnCall(array("au", "select", "t:release-1.0"));
$tags = $this->mtnInstance->inTags(rtrim($rev), null);
$this->assertEqual(1, count($tags));
$this->assertEqual("t:release-1.0", $tags[0]);
// pick the first (root) revisions in this database
$rev = $this->mtnCall(array("au", "roots"));
$tags = $this->mtnInstance->inTags(rtrim($rev), null);
$this->assertEqual(0, count($tags));
}
public function testGetTree()
{
$files = $this->mtnInstance->getTree("t:release-1.0");
$this->assertEqual(3, count($files));
$this->assertEqual("bar", $files[0]->file);
$this->assertEqual("blob", $files[0]->type);
$this->assertEqual(6, $files[0]->size); // "blafoo"
$this->assertEqual("second\n", $files[0]->log);
$this->assertEqual("foo", $files[1]->file);
$this->assertEqual("blob", $files[1]->type);
$this->assertEqual(7, $files[1]->size); // "blubber"
$this->assertEqual("initial\n", $files[1]->log);
$this->assertEqual("subdir", $files[2]->file);
$this->assertEqual("tree", $files[2]->type);
$this->assertEqual(0, $files[2]->size);
$files = $this->mtnInstance->getTree("t:release-1.0", "subdir");
$this->assertEqual(1, count($files));
$this->assertEqual("bla", $files[0]->file);
$this->assertEqual("subdir/bla", $files[0]->fullpath);
$this->assertEqual("blob", $files[0]->type);
$this->assertEqual(6, $files[0]->size); // "blabla"
$this->assertEqual("second\n", $files[0]->log);
}
public function testIsValidRevision()
{
$this->assertTrue($this->mtnInstance->isValidRevision("t:release-1.0"));
$this->assertFalse($this->mtnInstance->isValidRevision("abcdef12345"));
}
}

View File

@ -317,6 +317,148 @@ class IDF_Views_Admin
),
$request);
}
/**
* Usher servers overview
*
*/
public $usher_precond = array('Pluf_Precondition::staffRequired');
public function usher($request, $match)
{
$title = __('Usher management');
$servers = array();
foreach (IDF_Scm_Monotone_Usher::getServerList() as $server) {
$servers[] = (object)array(
"name" => $server,
"status" => IDF_Scm_Monotone_Usher::getStatus($server),
);
}
return Pluf_Shortcuts_RenderToResponse(
'idf/gadmin/usher/index.html',
array(
'page_title' => $title,
'servers' => $servers,
),
$request
);
}
/**
* Usher control
*
*/
public $usherControl_precond = array('Pluf_Precondition::staffRequired');
public function usherControl($request, $match)
{
$title = __('Usher control');
$action = $match[1];
if (!empty($action)) {
if (!in_array($action, array('reload', 'shutdown', 'startup'))) {
throw new Pluf_HTTP_Error404();
}
$msg = null;
if ($action == 'reload') {
IDF_Scm_Monotone_Usher::reload();
$msg = __('Usher configuration has been reloaded');
}
else if ($action == 'shutdown') {
IDF_Scm_Monotone_Usher::shutDown();
$msg = __('Usher has been shut down');
}
else
{
IDF_Scm_Monotone_Usher::startUp();
$msg = __('Usher has been started up');
}
$request->user->setMessage($msg);
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usherControl', array(''));
return new Pluf_HTTP_Response_Redirect($url);
}
return Pluf_Shortcuts_RenderToResponse(
'idf/gadmin/usher/control.html',
array(
'page_title' => $title,
'status' => IDF_Scm_Monotone_Usher::getStatus(),
),
$request
);
}
/**
* Usher control
*
*/
public $usherServerControl_precond = array('Pluf_Precondition::staffRequired');
public function usherServerControl($request, $match)
{
$server = $match[1];
if (!in_array($server, IDF_Scm_Monotone_Usher::getServerList())) {
throw new Pluf_HTTP_Error404();
}
$action = $match[2];
if (!in_array($action, array('start', 'stop', 'kill'))) {
throw new Pluf_HTTP_Error404();
}
$msg = null;
if ($action == 'start') {
IDF_Scm_Monotone_Usher::startServer($server);
$msg = sprintf(__('The server "%s" has been started'), $server);
}
else if ($action == 'stop') {
IDF_Scm_Monotone_Usher::stopServer($server);
$msg = sprintf(__('The server "%s" has been stopped'), $server);
}
else
{
IDF_Scm_Monotone_Usher::killServer($server);
$msg = sprintf(__('The server "%s" has been killed'), $server);
}
$request->user->setMessage($msg);
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usher');
return new Pluf_HTTP_Response_Redirect($url);
}
/**
* Open connections for a configured server
*
*/
public $usherServerConnections_precond = array('Pluf_Precondition::staffRequired');
public function usherServerConnections($request, $match)
{
$server = $match[1];
if (!in_array($server, IDF_Scm_Monotone_Usher::getServerList())) {
throw new Pluf_HTTP_Error404();
}
$title = sprintf(__('Open connections for "%s"'), $server);
$connections = IDF_Scm_Monotone_Usher::getConnectionList($server);
if (count($connections) == 0) {
$request->user->setMessage(sprintf(
__('no connections for server "%s"'), $server
));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usher');
return new Pluf_HTTP_Response_Redirect($url);
}
return Pluf_Shortcuts_RenderToResponse(
'idf/gadmin/usher/connections.html',
array(
'page_title' => $title,
'server' => $server,
'connections' => $connections,
),
$request
);
}
}
function IDF_Views_Admin_bool($field, $item)

View File

@ -520,6 +520,7 @@ class IDF_Views_Project
'git' => __('git'),
'svn' => __('Subversion'),
'mercurial' => __('mercurial'),
'mtn' => __('monotone'),
);
$repository_type = $options[$scm];
return Pluf_Shortcuts_RenderToResponse('idf/admin/source.html',

View File

@ -37,10 +37,9 @@ class IDF_Views_Source
public static $supportedExtenstions = array(
'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cc',
'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc',
'html', 'html', 'java', 'js', 'm', 'master', 'pch', 'perl', 'php',
'pl', 'plist', 'pm', 'py', 'rb', 'sh', 'sitemap', 'skin', 'sln',
'svc', 'vala', 'vb', 'vbproj', 'wsdl', 'xhtml', 'xml', 'xsd',
'xsl', 'xslt');
'html', 'html', 'java', 'js', 'master', 'perl', 'php', 'pl',
'pm', 'py', 'rb', 'sh', 'sitemap', 'skin', 'sln', 'svc', 'vala',
'vb', 'vbproj', 'wsdl', 'xhtml', 'xml', 'xsd', 'xsl', 'xslt');
/**
* Display help on how to checkout etc.
@ -309,7 +308,7 @@ class IDF_Views_Source
$in_branches = $scm->inBranches($cobject->commit, '');
$tags = $scm->getTags();
$in_tags = $scm->inTags($cobject->commit, '');
return Pluf_Shortcuts_RenderToResponse('idf/source/commit.html',
return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/commit.html',
array(
'page_title' => $page_title,
'title' => $title,
@ -598,3 +597,16 @@ function IDF_Views_Source_PrettySizeSimple($size)
return Pluf_Utils::prettySize($size);
}
function IDF_Views_Source_ShortenString($string, $length)
{
$ellipse = "...";
$length = max(strlen($ellipse) + 2, $length);
$preflen = ceil($length / 10);
if (mb_strlen($string) < $length)
return $string;
return substr($string, 0, $preflen).$ellipse.
substr($string, -($length - $preflen - mb_strlen($ellipse)));
}

View File

@ -134,7 +134,7 @@ class IDF_Views_User
}
/**
* Delete a SSH key.
* Delete a public key.
*
* This is redirecting to the preferences
*/
@ -148,7 +148,7 @@ class IDF_Views_User
return new Pluf_HTTP_Response_Forbidden($request);
}
$key->delete();
$request->user->setMessage(__('The SSH key has been deleted.'));
$request->user->setMessage(__('The public key has been deleted.'));
}
return new Pluf_HTTP_Response_Redirect($url);
}

View File

@ -73,6 +73,90 @@ $cfg['git_write_remote_url'] = 'git@localhost:%s.git';
$cfg['svn_repositories'] = 'file:///home/svn/repositories/%s';
$cfg['svn_remote_url'] = 'http://localhost/svn/%s';
# Path to the monotone binary
$cfg['mtn_path'] = 'mtn';
# Additional options for the started monotone process
$cfg['mtn_opts'] = array('--no-workspace', '--norc');
#
# You can setup monotone for use with indefero in two ways:
#
# 1) One database for everything:
# Set 'mtn_repositories' below to a fixed database path, such as
# '/home/mtn/repositories/all_projects.mtn'
#
# Pro: - easy to setup and to manage
# Con: - while read access can be configured per-branch,
# granting write access rights to a user means that
# he can write anything in the global database
# - database lock problem: the database from which
# indefero reads its data cannot be used to serve the
# contents to the users, as the serve process locks
# the database
#
# 2) One database for every project with 'usher':
# Set 'mtn_remote_url' below to a string which matches your setup.
# Again, the '%s' placeholder will be expanded to the project's
# short name. Note that 'mtn_remote_url' is used as internal
# URI (to access the data for indefero) as well as external URI
# (for end users) at the same time.
#
# Then download and configure 'usher'
# (mtn clone mtn://monotone.ca?net.venge.monotone.contrib.usher)
# which acts as proxy in front of all single project databases.
# Usher's server names should be mapped to the project's short names,
# so you end up with something like this for every project:
#
# server "project"
# local "-d" "/home/mtn/repositories/project.mtn" "*"
#
# Alternatively if you assign every project a unique DNS such as
# 'project.my-hosting.biz', you can also configure it like this:
#
# host "project.my-hosting.biz"
# local "-d" "/home/mtn/repositories/project.mtn" "*"
#
# Pro: - read and write access can be granted per project
# - no database locking issues
# - one public server running on the one well-known port
# Con: - harder to setup
#
# Usher can also be used to forward sync requests to remote servers,
# please consult its README file for more information.
#
# monotone also allows to use SSH as transport protocol, so if you do not plan
# to setup a netsync server as described above, then just enter a URI like
# 'ssh://my-host.biz/home/mtn/repositories/%s.mtn' in 'mtn_remote_url'.
#
$cfg['mtn_repositories'] = '/home/mtn/repositories/%s.mtn';
$cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s';
#
# Whether the particular database(s) are accessed locally (via automate stdio)
# or remotely (via automate remote_stdio). 'remote' is the default for
# netsync setups, while 'local' access should be choosed for ssh access.
#
# Note that you need to setup the hook 'get_remote_automate_permitted' for
# each remotely accessible database. A full HOWTO set this up is beyond this
# scope, please refer to the documentation of monotone and / or ask on the
# mailing list (monotone-users@nongnu.org) or IRC channel
# (irc.oftc.net/#monotone)
#
$cfg['mtn_db_access'] = 'remote';
#
# If configured, this allows basic control of a running usher process
# via the forge administration
#
# 'host' and 'port' must be set to the specific bits from usher's
# configured 'adminaddr', 'user' and 'pass' must match the values set for
# the configured 'userpass' combination
#
#$cfg['mtn_usher'] = array(
# 'host' => 'localhost',
# 'port' => 12345,
# 'user' => 'admin',
# 'pass' => 'admin',
#);
#
# Mercurial repositories path
#$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s';
#$cfg['mercurial_remote_url'] = 'http://projects.ceondo.com/hg/%s';
@ -209,6 +293,7 @@ $cfg['languages'] = array('en', 'fr');
$cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git',
'svn' => 'IDF_Scm_Svn',
'mercurial' => 'IDF_Scm_Mercurial',
'mtn' => 'IDF_Scm_Monotone',
);
# If you want to use another memtypes database

View File

@ -386,6 +386,29 @@ $ctl[] = array('regex' => '#^/admin/users/(\d+)/$#',
'model' => 'IDF_Views_Admin',
'method' => 'userUpdate');
if (Pluf::f("mtn_usher", null) !== null)
{
$ctl[] = array('regex' => '#^/admin/usher/$#',
'base' => $base,
'model' => 'IDF_Views_Admin',
'method' => 'usher');
$ctl[] = array('regex' => '#^/admin/usher/control/(.*)$#',
'base' => $base,
'model' => 'IDF_Views_Admin',
'method' => 'usherControl');
$ctl[] = array('regex' => '#^/admin/usher/server/(.+)/control/(.+)$#',
'base' => $base,
'model' => 'IDF_Views_Admin',
'method' => 'usherServerControl');
$ctl[] = array('regex' => '#^/admin/usher/server/(.+)/connections/$#',
'base' => $base,
'model' => 'IDF_Views_Admin',
'method' => 'usherServerConnections');
}
// ---------- UTILITY VIEWS -------------------------------
$ctl[] = array('regex' => '#^/register/$#',

View File

@ -43,6 +43,9 @@
<div id="main-tabs">
<a href="{url 'IDF_Views_Admin::projects'}"{block tabprojects}{/block}>{trans 'Projects'}</a>
<a href="{url 'IDF_Views_Admin::users'}"{block tabusers}{/block}>{trans 'People'}</a>
{if $usherConfigured}
<a href="{url 'IDF_Views_Admin::usher'}"{block tabusher}{/block}>{trans 'Usher'}</a>
{/if}
</div>
<div id="sub-tabs">{block subtabs}{/block}</div>
</div>

View File

@ -52,6 +52,13 @@
{$form.f.svn_password|unsafe}
</td>
</tr>
<tr class="mtn-form">
<th><strong>{$form.f.mtn_master_branch.labelTag}:</strong></th>
<td>{if $form.f.mtn_master_branch.errors}{$form.f.mtn_master_branch.fieldErrors}{/if}
{$form.f.mtn_master_branch|unsafe}<br />
<span class="helptext">{$form.f.mtn_master_branch.help_text}</span>
</td>
</tr>
<tr>
<th>{$form.f.template.labelTag}</th>
<td>{if $form.f.template.errors}{$form.f.template.fieldErrors}{/if}
@ -119,12 +126,22 @@ $(document).ready(function() {
if ($("#id_scm option:selected").val() != "svn") {
$(".svn-form").hide();
}
// Hide if not mtn
if ($("#id_scm option:selected").val() != "mtn") {
$(".mtn-form").hide();
}
$("#id_scm").change(function () {
if ($("#id_scm option:selected").val() == "svn") {
$(".svn-form").show();
} else {
$(".svn-form").hide();
}
if ($("#id_scm option:selected").val() == "mtn") {
$(".mtn-form").show();
} else {
$(".mtn-form").hide();
}
});
// Hide if not svn
if ($("#id_template option:selected").val() == "--") {

View File

@ -43,10 +43,10 @@
</td>
</tr>
<tr>
<th>{$form.f.ssh_key.labelTag}:</th>
<td>{if $form.f.ssh_key.errors}{$form.f.ssh_key.fieldErrors}{/if}
{$form.f.ssh_key|unsafe}<br />
<span class="helptext">{$form.f.ssh_key.help_text}</span>
<th>{$form.f.public_key.labelTag}:</th>
<td>{if $form.f.public_key.errors}{$form.f.public_key.fieldErrors}{/if}
{$form.f.public_key|unsafe}<br />
<span class="helptext">{$form.f.public_key.help_text}</span>
</td>
</tr>
<tr>

View File

@ -0,0 +1,6 @@
{extends "idf/gadmin/base.html"}
{block tabusher} class="active"{/block}
{block subtabs}
<a {if $inUsher}class="active" {/if}href="{url 'IDF_Views_Admin::usher'}">{trans 'Configured servers'}</a> |
<a {if $inUsherControl}class="active" {/if}href="{url 'IDF_Views_Admin::usherControl', array('')}">{trans 'Usher control'}</a>
{/block}

View File

@ -0,0 +1,19 @@
{extends "idf/gadmin/usher/base.html"}
{block docclass}yui-t3{assign $inUsherServerConnections=true}{/block}
{block body}
<table class="recent-issues">
<tr>
<th>{trans "address"}</th>
<th>{trans "port"}</th>
</tr>
{foreach $connections as $connection}
<tr>
<td>{$connection.address}</td>
<td>{$connection.port}</td>
</tr>
{/foreach}
</table>
{/block}

View File

@ -0,0 +1,32 @@
{extends "idf/gadmin/usher/base.html"}
{block docclass}yui-t3{assign $inUsherControl=true}{/block}
{block body}
<p>
{trans 'current server status:'} {$status} |
{if $status == "SHUTDOWN"}
<a href="{url 'IDF_Views_Admin::usherControl', array('startup')}">{trans 'startup'}</a>
{else}
<a href="{url 'IDF_Views_Admin::usherControl', array('shutdown')}">{trans 'shutdown'}</a>
{/if}
</p>
<p>{trans 'reload server configuration:'}
<a href="{url 'IDF_Views_Admin::usherControl', array('reload')}">{trans 'reload'}</a>
</p>
{/block}
{block context}
<div class="issue-submit-info">
<p><strong>{trans 'Status explanation'}</strong></p>
<ul>
<li>ACTIVE n: {trans 'active with n total open connections'}</li>
<li>WAITING: {trans 'waiting for new connections'}</li>
<li>SHUTTINGDOWN: {trans 'usher is being shut down, not accepting connections'}</li>
<li>SHUTDOWN: {trans 'usher is shut down, all local servers are stopped and not accepting connections'}</li>
</ul>
</div>
{/block}

View File

@ -0,0 +1,52 @@
{extends "idf/gadmin/usher/base.html"}
{block docclass}yui-t3{assign $inUsher=true}{/block}
{block body}
<table class="recent-issues">
<tr>
<th>{trans "server name"}</th>
<th>{trans "status"}</th>
<th>{trans "action"}</th>
</tr>
{foreach $servers as $server}
<tr>
<td>{$server.name}</td>
<td>{$server.status}</td>
<td>
{if preg_match("/ACTIVE|RUNNING|SLEEPING/", $server.status)}
<a href="{url 'IDF_Views_Admin::usherServerControl', array($server.name, 'stop')}">
{trans 'stop'}</a>
{elseif $server.status == "STOPPED"}
<a href="{url 'IDF_Views_Admin::usherServerControl', array($server.name, 'start')}">
{trans 'start'}</a>
{/if}
{if preg_match("/ACTIVE|WAITING|SLEEPING|STOPPING/", $server.status)}
| <a href="{url 'IDF_Views_Admin::usherServerControl', array($server.name, 'kill')}">
{trans 'kill'}</a>
{/if}
{if preg_match("/STOPPING|ACTIVE/", $server.status)}
| <a href="{url 'IDF_Views_Admin::usherServerConnections', array($server.name)}">
{trans 'active connections'}</a>
{/if}
</tr>
{/foreach}
</table>
{/block}
{block context}
<div class="issue-submit-info">
<p><strong>{trans 'Status explanation'}</strong></p>
<ul>
<li>REMOTE: {trans 'remote server without open connections'}</li>
<li>ACTIVE n: {trans 'server with n open connections'}</li>
<li>WAITING: {trans 'local server running, without open connections'}</li>
<li>SLEEPING: {trans 'local server not running, waiting for connections'}</li>
<li>STOPPING n: {trans 'local server is about to stop, n connections still open'}</li>
<li>STOPPED: {trans 'local server not running, not accepting connections'}</li>
<li>SHUTDOWN: {trans 'usher is shut down, not running and not accepting connections'}</li>
</ul>
</div>
{/block}

View File

@ -37,33 +37,6 @@
{/if}
{/block}
{block context}
{if $scm != 'svn'}
<p><strong>{trans 'Branches:'}</strong><br/>
{foreach $branches as $branch => $path}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)}
<span class="label{if in_array($branch, $tree_in)} active{/if}"><a href="{$url}" class="label">{$branch}</a></span><br/>
{/foreach}
</p>
{if $tags}
<p><strong>{trans 'Tags:'}</strong><br/>
{foreach $tags as $tag => $path}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $tag)}
<span class="label{if in_array($tag, $tags_in)} active{/if}"><a href="{$url}" class="label">{if $path}{$path}{else}{$tag}{/if}</a></span><br/>
{/foreach}
</p>
{/if}
{else}
<form class="star" action="{url 'IDF_Views_Source_Svn::changelogRev', array($project.shortname)}" method="get">
<p><strong>{trans 'Revision:'}</strong> {$commit}</p>
<p>
<input accesskey="4" type="text" value="{$commit}" name="rev" size="5"/>
<input type="submit" name="s" value="{trans 'Go to revision'}"/>
</p>
</form>
{/if}
{/block}
{block javascript}
<script type="text/javascript" src="{media '/idf/js/prettify.js'}"></script>
<script type="text/javascript">

View File

@ -0,0 +1,18 @@
{extends "idf/source/commit.html"}
{block context}
<p><strong>{trans 'Branches:'}</strong><br/>
{foreach $branches as $branch => $path}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)}
<span class="label{if in_array($branch, $tree_in)} active{/if}"><a href="{$url}" class="label">{$branch}</a></span><br/>
{/foreach}
</p>
{if $tags}
<p><strong>{trans 'Tags:'}</strong><br/>
{foreach $tags as $tag => $path}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $tag)}
<span class="label{if in_array($tag, $tags_in)} active{/if}"><a href="{$url}" class="label">{if $path}{$path}{else}{$tag}{/if}</a></span><br/>
{/foreach}
</p>
{/if}
{/block}

View File

@ -0,0 +1,18 @@
{extends "idf/source/commit.html"}
{block context}
<p><strong>{trans 'Branches:'}</strong><br/>
{foreach $branches as $branch => $path}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)}
<span class="label{if in_array($branch, $tree_in)} active{/if}"><a href="{$url}" class="label">{$branch}</a></span><br/>
{/foreach}
</p>
{if $tags}
<p><strong>{trans 'Tags:'}</strong><br/>
{foreach $tags as $tag => $path}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $tag)}
<span class="label{if in_array($tag, $tags_in)} active{/if}"><a href="{$url}" class="label">{if $path}{$path}{else}{$tag}{/if}</a></span><br/>
{/foreach}
</p>
{/if}
{/block}

View File

@ -0,0 +1,25 @@
{extends "idf/source/changelog.html"}
{block context}
<p><strong>{trans 'Branches:'}</strong><br/>
{foreach $branches as $selector => $branch}
{aurl 'url', 'IDF_Views_Source::changeLog', array($project.shortname, $selector)}
<span class="label{if in_array($selector, $tree_in)} active{/if}">
<a href="{$url}" class="label" title="{$branch}">
{$branch|shorten:24}
</a>
</span><br/>
{/foreach}
</p>
{if $tags}
<p><strong>{trans 'Tags:'}</strong><br/>
{foreach $tags as $selector => $tag}
{aurl 'url', 'IDF_Views_Source::changeLog', array($project.shortname, $selector)}
<span class="label{if in_array($selector, $tags_in)} active{/if}">
<a href="{$url}" class="label" title="{$tag}">
{$tag|shorten:24}
</a>
</span><br/>
{/foreach}
</p>
{/if}
{/block}

View File

@ -0,0 +1,26 @@
{extends "idf/source/commit.html"}
{block context}
<p><strong>{trans 'Branches:'}</strong><br/>
{foreach $branches as $selector => $branch}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)}
<span class="label{if in_array($branch, $tree_in)} active{/if}">
<a href="{$url}" class="label" title="{$branch}">
{$branch|shorten:25}
</a>
</span><br/>
{/foreach}
</p>
{if $tags}
<p><strong>{trans 'Tags:'}</strong><br/>
{foreach $tags as $selector => $tag}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)}
<span class="label{if in_array($tag, $tags_in)} active{/if}">
<a href="{$url}" class="label" title="{$tag}">
{$tag|shorten:25}
</a>
</span><br/>
{/foreach}
</p>
{/if}
{/block}

View File

@ -0,0 +1,51 @@
{extends "idf/source/base.html"}
{block extraheader}<link rel="stylesheet" type="text/css" href="{media '/idf/css/prettify.css'}" />{/block}
{block docclass}yui-t1{assign $inSourceTree=true}{/block}
{block body}
<h2 class="top"><a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $commit)}">{trans 'Root'}</a><span class="sep">/</span>{if $breadcrumb}{$breadcrumb|safe}{/if}</h2>
<table class="code" summary=" ">
{if !$tree_in and !$tags_in}
{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)}
<tfoot>
<tr><th colspan="2">{blocktrans}Source at commit <a class="mono" href="{$url}">{$commit}</a> created {$cobject.date|dateago}.{/blocktrans}<br/>
<span class="smaller">{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans}</span>
</th></tr>
</tfoot>
{/if}
<tbody>
{$file}
</tbody>
</table>
{aurl 'url', 'IDF_Views_Source::getFile', array($project.shortname, $commit, $fullpath)}
<p class="right soft"><a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}">{trans 'Download this file'}</a></p>
{/block}
{block context}
<p><strong>{trans 'Branches:'}</strong><br/>
{foreach $branches as $selector => $branch}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)}
<span class="label{if in_array($branch, $tree_in)} active{/if}">
<a href="{$url}" class="label" title="{$branch}">
{$branch|shorten:25}
</a>
</span><br/>
{/foreach}
</p>
{if $tags}
<p><strong>{trans 'Tags:'}</strong><br/>
{foreach $tags as $selector => $tag}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)}
<span class="label{if in_array($tag, $tags_in)} active{/if}">
<a href="{$url}" class="label" title="{$tag}">
{$tag|shorten:25}
</a>
</span><br/>
{/foreach}
</p>
{/if}
{/block}
{block javascript}
<script type="text/javascript" src="{media '/idf/js/prettify.js'}"></script>
<script type="text/javascript">prettyPrint();</script>
{/block}

View File

@ -0,0 +1,34 @@
{extends "idf/source/base.html"}
{block docclass}yui-t2{assign $inHelp=true}{/block}
{block body}
<p>{blocktrans}The team behind {$project} is using
the <strong>monotone</strong> software to manage the source
code.{/blocktrans}</p>
<h3>{trans 'Command-Line Access'}</h3>
<p><kbd>mtn clone {$project.getSourceAccessUrl()}</kbd></p>
{if $isOwner or $isMember}
<h3>{trans 'First Commit'}</h3>
<p>{blocktrans}To make a first commit in the repository, perform the following steps:{/blocktrans}</p>
<pre>
mtn setup -b {$project.getConf().getVal('mtn_master_branch', 'your-branch')} .
mtn add -R .
mtn commit -m "initial import"
mtn push {$project.getSourceAccessUrl()}
</pre>
{/if}
{/block}
{block context}
<div class="issue-submit-info">
<p>{blocktrans}Find here more details on how to access {$project} source code.{/blocktrans}</p>
</div>
{/block}

View File

@ -0,0 +1,80 @@
{extends "idf/source/base.html"}
{block docclass}yui-t1{assign $inSourceTree=true}{/block}
{block body}
<h2 class="top"><a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $commit)}">{trans 'Root'}</a><span class="sep">/</span>{if $breadcrumb}{$breadcrumb|safe}{/if}</h2>
<table summary="" class="tree-list">
<thead>
<tr>
<th colspan="2">{trans 'File'}</th>
<th>{trans 'Age'}</th>
<th>{trans 'Message'}</th>
<th>{trans 'Size'}</th>
</tr>
</thead>{if !$tree_in and !$tags_in}
{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)}
<tfoot>
<tr><th colspan="5">{blocktrans}Source at commit <a class="mono" href="{$url}">{$commit}</a> created {$cobject.date|dateago}.{/blocktrans}<br/>
<span class="smaller">{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans}</span>
</th></tr>
</tfoot>
{/if}<tbody>
{if $base}
<tr>
<td>&nbsp;</td>
<td>
<a href="{url 'IDF_Views_Source::tree', array($project.shortname, $commit, $prev)}">..</a></td>
<td colspan="3"></td>
</tr>
{/if}
{foreach $files as $file}
{aurl 'url', 'IDF_Views_Source::tree', array($project.shortname, $commit, $file.efullpath)}
<tr>
<td class="fileicon"><img src="{media '/idf/img/'~$file.type~'.png'}" alt="{$file.type}" /></td>
{if $file.type != 'extern'}
<td{if $file.type == 'tree'} colspan="4"{/if}><a href="{$url}">{$file.file}</a></td>{else}<td><a href="#" title="{$file.hash}">{$file.file}</a></td>{/if}
{if $file.type == 'blob'}
{if isset($file.date) and $file.log != '----'}
<td><span class="smaller">{$file.date|dateago:"without"}</span></td>
<td><span class="smaller">{$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false}</span></td>
{else}<td colspan="2"></td>{/if}
<td>{$file.size|size}</td>{/if}
{if $file.type == 'extern'}
<td colspan="3">{$file.extern}</td>
{/if}
</tr>
{/foreach}
</tbody>
</table>
{aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)}
<p class="right soft">
{* <a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}">{trans 'Download this version'}</a> {trans 'or'} *}
<kbd>mtn clone {$project.getSourceAccessUrl($user, $commit)}</kbd> <a href="{url 'IDF_Views_Source::help', array($project.shortname)}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/help.png'}" alt="{trans 'Help'}" /></a>
</p>
{/block}
{block context}
<p><strong>{trans 'Branches:'}</strong><br/>
{foreach $branches as $selector => $branch}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)}
<span class="label{if in_array($selector, $tree_in)} active{/if}">
<a href="{$url}" class="label" title="{$branch}">
{$branch|shorten:24}
</a>
</span><br/>
{/foreach}
</p>
{if $tags}
<p><strong>{trans 'Tags:'}</strong><br/>
{foreach $tags as $selector => $tag}
{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)}
<span class="label{if in_array($selector, $tags_in)} active{/if}">
<a href="{$url}" class="label" title="{$tag}">
{$tag|shorten:24}
</a>
</span><br/>
{/foreach}
</p>
{/if}
{/block}

View File

@ -0,0 +1,11 @@
{extends "idf/source/commit.html"}
{block context}
<form class="star" action="{url 'IDF_Views_Source_Svn::changelogRev', array($project.shortname)}" method="get">
<p><strong>{trans 'Revision:'}</strong> {$commit}</p>
<p>
<input accesskey="4" type="text" value="{$commit}" name="rev" size="5"/>
<input type="submit" name="s" value="{trans 'Go to revision'}"/>
</p>
</form>
{/block}

View File

@ -54,10 +54,10 @@
</td>
</tr>
<tr>
<th>{$form.f.ssh_key.labelTag}:</th>
<td>{if $form.f.ssh_key.errors}{$form.f.ssh_key.fieldErrors}{/if}
{$form.f.ssh_key|unsafe}<br />
<span class="helptext">{$form.f.ssh_key.help_text}</span>
<th>{$form.f.public_key.labelTag}:</th>
<td>{if $form.f.public_key.errors}{$form.f.public_key.fieldErrors}{/if}
{$form.f.public_key|unsafe}<br />
<span class="helptext">{$form.f.public_key.help_text}</span>
</td>
</tr>
<tr class="pass-info" id="extra-password">
@ -82,7 +82,7 @@
{if count($keys)}
<table summary=" " class="recent-issues">
<tr><th colspan="2">{trans 'Your Current SSH Keys'}</th></tr>
<tr><th colspan="2">{trans 'Your Current Public Keys'}</th></tr>
{foreach $keys as $key}<tr><td>
<span class="mono">{$key.showCompact()}</span></td><td> <form class="star" method="post" action="{url 'IDF_Views_User::deleteKey', array($key.id)}"><input type="image" src="{media '/idf/img/trash.png'}" name="submit" value="{trans 'Delete this key'}" /></form>
</td>