Merge branch 'mnt-support'
This commit is contained in:
commit
14d07a22e2
63
doc/readme-monotone.mdtext
Normal file
63
doc/readme-monotone.mdtext
Normal 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.
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
@ -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 '';
|
||||
}
|
||||
$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.'));
|
||||
|
||||
if (preg_match('#^ssh\-[a-z]{3}\s\S+\s\S+$#', $key)) {
|
||||
$key = str_replace(array("\n", "\r"), '', $key);
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
731
src/IDF/Scm/Monotone.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
360
src/IDF/Scm/Monotone/Stdio.php
Normal file
360
src/IDF/Scm/Monotone/Stdio.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
241
src/IDF/Scm/Monotone/Usher.php
Normal file
241
src/IDF/Scm/Monotone/Usher.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
239
src/IDF/Tests/TestMonotone.php
Normal file
239
src/IDF/Tests/TestMonotone.php
Normal 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"));
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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)));
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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/$#',
|
||||
|
@ -43,18 +43,21 @@
|
||||
<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>
|
||||
<h1 id="title" class="title">{block title}{$page_title}{/block}</h1>
|
||||
<h1 id="title" class="title">{block title}{$page_title}{/block}</h1>
|
||||
</div>
|
||||
<div id="bd">
|
||||
<div id="yui-main">
|
||||
<div class="yui-b">
|
||||
<div class="yui-g">
|
||||
<div class="yui-g">
|
||||
{if $user and $user.id}{getmsgs $user}{/if}
|
||||
<div class="content">{block body}{/block}</div>
|
||||
</div>
|
||||
<div class="content">{block body}{/block}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yui-b context">{block context}{/block}</div>
|
||||
|
@ -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() == "--") {
|
||||
|
@ -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>
|
||||
|
6
src/IDF/templates/idf/gadmin/usher/base.html
Normal file
6
src/IDF/templates/idf/gadmin/usher/base.html
Normal 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}
|
19
src/IDF/templates/idf/gadmin/usher/connections.html
Normal file
19
src/IDF/templates/idf/gadmin/usher/connections.html
Normal 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}
|
||||
|
32
src/IDF/templates/idf/gadmin/usher/control.html
Normal file
32
src/IDF/templates/idf/gadmin/usher/control.html
Normal 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}
|
||||
|
52
src/IDF/templates/idf/gadmin/usher/index.html
Normal file
52
src/IDF/templates/idf/gadmin/usher/index.html
Normal 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}
|
||||
|
@ -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">
|
||||
|
18
src/IDF/templates/idf/source/git/commit.html
Normal file
18
src/IDF/templates/idf/source/git/commit.html
Normal 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}
|
||||
|
18
src/IDF/templates/idf/source/mercurial/commit.html
Normal file
18
src/IDF/templates/idf/source/mercurial/commit.html
Normal 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}
|
||||
|
25
src/IDF/templates/idf/source/mtn/changelog.html
Normal file
25
src/IDF/templates/idf/source/mtn/changelog.html
Normal 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}
|
26
src/IDF/templates/idf/source/mtn/commit.html
Normal file
26
src/IDF/templates/idf/source/mtn/commit.html
Normal 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}
|
||||
|
51
src/IDF/templates/idf/source/mtn/file.html
Normal file
51
src/IDF/templates/idf/source/mtn/file.html
Normal 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}
|
34
src/IDF/templates/idf/source/mtn/help.html
Normal file
34
src/IDF/templates/idf/source/mtn/help.html
Normal 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}
|
||||
|
||||
|
80
src/IDF/templates/idf/source/mtn/tree.html
Normal file
80
src/IDF/templates/idf/source/mtn/tree.html
Normal 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> </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}
|
11
src/IDF/templates/idf/source/svn/commit.html
Normal file
11
src/IDF/templates/idf/source/svn/commit.html
Normal 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}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user