diff --git a/CONTRIBUTE.mdtext b/CONTRIBUTE.mdtext new file mode 100644 index 0000000..2aaa0e9 --- /dev/null +++ b/CONTRIBUTE.mdtext @@ -0,0 +1,115 @@ +[Indefero][idf] is not only a software you can use either hosted for +you or hosted by you, but also a free software you can contribute to. + +Here you will get how to contribute and what to contribute. + +[idf]: http://www.indefero.net + +# Quick Way on How to Contribute + +Simple contribution: + +1. Open a ticket with your idea. You can directly propose a patch if + you have it. + +2. Wait for it to be checked by the devs or meet us on the #indefero + channel on [FreeNode][freenode]. + +Bigger contribution: + +1. Fork Indefero where you want (fork from the develop branch). + +2. Code your change and document it. + +3. Open a ticket with a pull request and talk about it on IRC. + +# The General Contribution Workflow for Regular Contributors + +1. Fork Indefero from the **develop** branch. +2. Request a pull request if you do not have write access on the repository. +3. Merge your changes without fast forward in develop. This keeps track of + the history of the changes and makes understanding what is going on easy. +4. Merge your changes with fast forward **only if a single commit**. + +Indefero is composed of two main branches: + +1. **master**: this is the shipped branch, only a select number of people + can push into it. +2. **develop**: this is the development branch, all the people having write + access to the repository are welcomed to push in. + +**Note:** The branching model we use is [explained in details here][bmi]. You +**must** understand it to really contribute to the code base in an +efficient way. + +[bmi]: http://nvie.com/git-model "A successful Git branching model" + +# What to Contribute + +Contribution is easy, you can contribute in a lot of different fields, +contributions small or big are always appreciated. Here is an example +list of what you can do: + +- Install InDefero on your system and report the problem you had. +- Find the bad English and typos and propose corrections. +- Help with the translation effort. +- Find little bugs or usability problems and provide ideas on how to fix them. +- Register to the [discussion group][group] and help new users. +- Come and chat on IRC #indefero on the [FreeNode][freenode] servers. +- Find ways to improve the design while keeping it **beautifully simple**. +- Write a blog post about the project, what you think is good or bad. +- Translate InDefero for the sake of the community. +- Or maybe really hack into the code. + +As you can see, the real hacking into the code is just a small part of the work, so even if you are not a coder you can do a lot. + +[group]: http://groups.google.com/group/indefero-users +[freenode]: http://freenode.net/ + +## I am a simple user + +Thanks a lot! Really! As a project leader, I consider **you** as +**the most important person in the success of the project**. So do not +worry, I will really listen to your needs and make you love this +project. + +What you can do to help: + +- Use the software and each time you find something a bit annoying in your daily use, report a bug. Usability issues are high priority issues. +- Find typos, grammar mistakes, etc. and report a bug. +- Write about InDefero on your blog/website. +- Read the issues submitted by the users and provide answers if you have them. +- ... + +## I am a designer + +A lot of things to do for you: + +- Check the design and find the flaws in it. Is the space well used, does it look really nice and is it also functional for the first users? +- Do we have good support of all the major browsers? +- ... + +## I am a coder + +Checkout the code and have fun, but keep in mind that your results +must be simple to use. Do not worry about the beautiful part, the +designers can work on that. + +## I am a security guy + +Please, do try to break it, if you find a problem, come on IRC or +contact the developers to get the issue fixed as soon as +possible. Please, be nice, do not release the issue in the wild +without first talking to us. + +## I am a translator + +We currently use (transifex)[http://trac.transifex.org] to help our +users to translate indefero. You don't have to use it, but it's an +easy way to do the job. You can visit the indefero page at transifex +here : http://www.transifex.net/projects/p/indefero/c/indefero/ + +Please understand that your changes will not be commited instantly, +but are sent to the maintainers e-mails before. Then, your changes +will not be in the main repository until da-loic push the changes. In +that way, try to do big changes with less submissions. \ No newline at end of file diff --git a/INSTALL.mdtext b/INSTALL.mdtext index 947323d..d0f6c01 100644 --- a/INSTALL.mdtext +++ b/INSTALL.mdtext @@ -212,4 +212,14 @@ If you access a Subversion server with a self-signed certificate, you may have problems as your certificate is not trusted, check the [procedure provided here][svnfix] to solve the problem. -[svnfix]: http://projects.ceondo.com/p/indefero/issues/319/#ic1358 \ No newline at end of file +[svnfix]: http://projects.ceondo.com/p/indefero/issues/319/#ic1358 + +## If the registration links are not working + +If You have standard instalaction of PHP ie in Debian, php.ini sets +mbstring.func_overload to value "2" for overloading str* +functions. You need to prevent the overload as it does not make sense +anyway (magic in the background is bad!). +See the [corresponding ticket][reglink]. + +[reglink]: http://projects.ceondo.com/p/indefero/issues/481/ \ No newline at end of file diff --git a/doc/readme-monotone.mdtext b/doc/readme-monotone.mdtext index 2e54367..cbaf8ba 100644 --- a/doc/readme-monotone.mdtext +++ b/doc/readme-monotone.mdtext @@ -3,9 +3,8 @@ ## 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. + automation interface. It needs at least monotone version 0.99 + (interface version 13.0) or newer. To set up a new IDF project with monotone quickly, all you need to do is to create a new monotone database with diff --git a/scripts/mtn-post-push b/scripts/mtn-post-push new file mode 100755 index 0000000..46f5d3b --- /dev/null +++ b/scripts/mtn-post-push @@ -0,0 +1,23 @@ +#!/bin/sh +# +# This hook informs IDF that new revisions arrived in the database +# of the specified project. +# +# This hook is normally installed automatically at the creation of your +# repository if you have everything configured correctly. If you want +# to enable it later, you need to call it into your monotonerc file +# from the hook "note_netsync_end". (See chapter "Event Notifications +# and Triggers" on .) +# + +dir=$(dirname "$0") +res=$(cd "$dir" && /bin/pwd || "$dir") +SCRIPTDIR="$res/$(readlink $0)" +PHP_POST_PUSH=$SCRIPTDIR/mtnpostpush.php + +base=$(basename "$0") +TMPFILE=$(mktemp /tmp/${tempfoo}.XXXXXX) || exit 1 +while read rev; do echo $rev >> $TMPFILE; done + +echo php $PHP_POST_PUSH "$1" \< $TMPFILE \&\& rm -f $TMPFILE |\ + at now > /dev/null 2>&1 diff --git a/scripts/mtnpostpush.php b/scripts/mtnpostpush.php new file mode 100644 index 0000000..0fc9e02 --- /dev/null +++ b/scripts/mtnpostpush.php @@ -0,0 +1,63 @@ + 'name-of-the-project', + * 'revisions' => array('123abc...', '456def...', ...)); + * + */ + +fwrite(STDERR, "waiting for revisions on STDIN...\n"); +$stdin = file_get_contents('php://stdin'); + +$params = array('project' => $argv[1], + 'revisions' => explode("\n", chop($stdin))); +Pluf_Signal::send('mtnpostpush.php::run', 'mtnpostpush.php', $params); + + + diff --git a/src/IDF/Form/Admin/ProjectCreate.php b/src/IDF/Form/Admin/ProjectCreate.php index e2414e9..d297f14 100644 --- a/src/IDF/Form/Admin/ProjectCreate.php +++ b/src/IDF/Form/Admin/ProjectCreate.php @@ -64,6 +64,14 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form 'help_text' => __('It must be unique for each project and composed only of letters, digits and dash (-) like "my-project".'), )); + $this->fields['shortdesc'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('short description'), + 'help_text' => __('A one line description of the project.'), + 'initial' => '', + 'widget_attrs' => array('size' => '35'), + )); + $this->fields['scm'] = new Pluf_Form_Field_Varchar( array('required' => true, 'label' => __('Repository type'), @@ -272,6 +280,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form $project = new IDF_Project(); $project->name = $this->cleaned_data['name']; $project->shortname = $this->cleaned_data['shortname']; + $project->shortdesc = $this->cleaned_data['shortdesc']; if ($this->cleaned_data['template'] != '--') { // Find the template project diff --git a/src/IDF/Form/Admin/ProjectUpdate.php b/src/IDF/Form/Admin/ProjectUpdate.php index cd9995e..4157851 100644 --- a/src/IDF/Form/Admin/ProjectUpdate.php +++ b/src/IDF/Form/Admin/ProjectUpdate.php @@ -43,6 +43,14 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form 'initial' => $this->project->name, )); + $this->fields['shortdesc'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('short description'), + 'help_text' => __('A one line description of the project.'), + 'initial' => $this->project->shortdesc, + 'widget_attrs' => array('size' => '35'), + )); + $this->fields['owners'] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => __('Project owners'), @@ -80,6 +88,7 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form $this->cleaned_data); $this->project->membershipsUpdated(); $this->project->name = $this->cleaned_data['name']; + $this->project->shortdesc = $this->cleaned_data['shortdesc']; $this->project->update(); } } diff --git a/src/IDF/Key.php b/src/IDF/Key.php index 0d31ba3..c9d7eff 100644 --- a/src/IDF/Key.php +++ b/src/IDF/Key.php @@ -84,7 +84,7 @@ class IDF_Key extends Pluf_Model return array('ssh', $m[2], $m[1]); } - throw new IDF_Exception('invalid or unknown key data detected'); + throw new Exception('invalid or unknown key data detected'); } /** @@ -123,7 +123,7 @@ class IDF_Key extends Pluf_Model { list($type, $keyName, $keyData) = $this->parseContent(); if ($type != 'mtn') - throw new IDF_Exception('key is not a monotone public key'); + throw new Exception('key is not a monotone public key'); return sha1($keyName.":".$keyData); } diff --git a/src/IDF/Plugin/SyncMonotone.php b/src/IDF/Plugin/SyncMonotone.php index 4819db3..b347d37 100644 --- a/src/IDF/Plugin/SyncMonotone.php +++ b/src/IDF/Plugin/SyncMonotone.php @@ -3,7 +3,7 @@ /* # ***** BEGIN LICENSE BLOCK ***** # This file is part of InDefero, an open source project management application. -# Copyright (C) 2008 Céondo Ltd and contributors. +# 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 @@ -35,25 +35,39 @@ class IDF_Plugin_SyncMonotone $plug = new IDF_Plugin_SyncMonotone(); switch ($signal) { case 'IDF_Project::created': - $plug->processMonotoneCreate($params['project']); + $plug->processProjectCreate($params['project']); + break; + case 'IDF_Project::preDelete': + $plug->processProjectDelete($params['project']); + break; + case 'IDF_Key::postSave': + $plug->processKeyCreate($params['key']); + break; + case 'IDF_Key::preDelete': + $plug->processKeyDelete($params['key']); + break; + case 'mtnpostpush.php::run': + $plug->processSyncTimeline($params['project']); break; } } /** - * Four steps to setup a new monotone project: + * Initial steps to setup a new monotone project: * * 1) run mtn db init to initialize a new database underknees * 'mtn_repositories' * 2) create a new server key in the same directory - * 3) write monotonerc for access control - * 4) add the database as new local server in the usher configuration - * 5) reload the running usher instance so it acknowledges the new - * server + * 3) create a new client key for IDF and store it in the project conf + * 4) write monotonerc + * 5) add the database as new local server in the usher configuration + * 6) reload the running usher instance so it acknowledges the new server + * 7) create read-/write-permissions for the project and add all public + * keys to the project * * @param IDF_Project */ - function processMonotoneCreate($project) + function processProjectCreate($project) { if ($project->getConf()->getVal('scm') != 'mtn') { return; @@ -73,6 +87,13 @@ class IDF_Plugin_SyncMonotone ); } + $mtnpostpush = realpath(dirname(__FILE__) . "/../../../scripts/mtn-post-push"); + if (!file_exists($mtnpostpush)) { + throw new IDF_Scm_Exception(sprintf( + __('Could not find mtn-post-push script "%s".'), $mtnpostpush + )); + } + $shortname = $project->shortname; $projectpath = sprintf($projecttempl, $shortname); if (file_exists($projectpath)) { @@ -91,17 +112,8 @@ class IDF_Plugin_SyncMonotone // step 1) create a new database // $dbfile = $projectpath.'/database.mtn'; - $cmd = sprintf( - Pluf::f('mtn_path', 'mtn').' db init -d %s', - escapeshellarg($dbfile) - ); - $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; - $ll = exec($cmd, $output = array(), $return = 0); - if ($return != 0) { - throw new IDF_Scm_Exception(sprintf( - __('The database file %s could not be created.'), $dbfile - )); - } + $cmd = sprintf('db init -d %s', escapeshellarg($dbfile)); + self::_mtn_exec($cmd); // // step 2) create a server key @@ -115,56 +127,91 @@ class IDF_Plugin_SyncMonotone $server = $parsed['host']; } - $keyname = $shortname.'-server@'.$server; - $cmd = sprintf( - Pluf::f('mtn_path', 'mtn').' au genkey --confdir=%s %s ""', + $serverkey = $shortname.'-server@'.$server; + $cmd = sprintf('au generate_key --confdir=%s %s ""', escapeshellarg($projectpath), - escapeshellarg($keyname) + escapeshellarg($serverkey) ); - $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; - $ll = exec($cmd, $output = array(), $return = 0); - if ($return != 0) { - throw new IDF_Scm_Exception(sprintf( - __('The server key %s could not be created.'), $keyname - )); + self::_mtn_exec($cmd); + + // + // step 3) create a client key, and save it in IDF + // + $clientkey_hash = ''; + $monotonerc_tpl = 'monotonerc-noauth.tpl'; + + if (Pluf::f('mtn_remote_auth', true)) { + $monotonerc_tpl = 'monotonerc-auth.tpl'; + $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; + if (!file_exists($keydir)) { + if (!mkdir($keydir)) { + throw new IDF_Scm_Exception(sprintf( + __('The key directory %s could not be created.'), $keydir + )); + } + } + + $clientkey_name = $shortname.'-client@'.$server; + $cmd = sprintf('au generate_key --keydir=%s %s ""', + escapeshellarg($keydir), + escapeshellarg($clientkey_name) + ); + $keyinfo = self::_mtn_exec($cmd); + + $parsed_keyinfo = array(); + try { + $parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo); + } + catch (Exception $e) { + throw new IDF_Scm_Exception(sprintf( + __('Could not parse key information: %s'), $e->getMessage() + )); + } + + $clientkey_hash = $parsed_keyinfo[0][1]['hash']; + $clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash; + $clientkey_data = file_get_contents($clientkey_file); + + $project->getConf()->setVal('mtn_client_key_name', $clientkey_name); + $project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash); + $project->getConf()->setVal('mtn_client_key_data', $clientkey_data); + + // add the public client key to the server + $cmd = sprintf('au get_public_key --keydir=%s %s', + escapeshellarg($keydir), + escapeshellarg($clientkey_hash) + ); + $clientkey_pubdata = self::_mtn_exec($cmd); + + $cmd = sprintf('au put_public_key --db=%s %s', + escapeshellarg($dbfile), + escapeshellarg($clientkey_pubdata) + ); + self::_mtn_exec($cmd); } // - // step 3) write monotonerc for access control - // FIXME: netsync access control is still missing! + // step 4) write monotonerc // - $monotonerc =<<getStdio(); + + $auth_ids = self::getAuthorizedUserIds($project); + $key_ids = array(); + foreach ($auth_ids as $auth_id) { + $sql = new Pluf_SQL('user=%s', array($auth_id)); + $keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen())); + foreach ($keys as $key) { + if ($key->getType() != 'mtn') + continue; + $stdio->exec(array('put_public_key', $key->content)); + $key_ids[] = $key->getMtnId(); + } + } + + $write_permissions = implode("\n", $key_ids); + $rcfile = $projectpath.'/write-permissions'; + if (file_put_contents($rcfile, $write_permissions, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write write-permissions file "%s"'), $rcfile + )); + } + + if ($project->private) { + $stanza = array( + array('key' => 'pattern', 'values' => array('*')), + ); + foreach ($key_ids as $key_id) + { + $stanza[] = array('key' => 'allow', 'values' => array($key_id)); + } + } + else { + $stanza = array( + array('key' => 'pattern', 'values' => array('*')), + array('key' => 'allow', 'values' => array('*')), + ); + } + $read_permissions = IDF_Scm_Monotone_BasicIO::compile(array($stanza)); + $rcfile = $projectpath.'/read-permissions'; + if (file_put_contents($rcfile, $read_permissions, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write read-permissions file "%s"'), $rcfile + )); + } + } + + /** + * Clean up after a mtn project was deleted + * + * @param IDF_Project + */ + public function processProjectDelete($project) + { + if ($project->getConf()->getVal('scm') != 'mtn') { + return; + } + + $usher_config = Pluf::f('mtn_usher_conf', false); + if (!$usher_config || !is_writable($usher_config)) { + throw new IDF_Scm_Exception( + '"mtn_usher_conf" does not exist or is not writable.' + ); + } + + $shortname = $project->shortname; + IDF_Scm_Monotone_Usher::killServer($shortname); + + $projecttempl = Pluf::f('mtn_repositories', false); + if ($projecttempl === false) { + throw new IDF_Scm_Exception( + '"mtn_repositories" must be defined in your configuration file.' + ); + } + + $usher_config = Pluf::f('mtn_usher_conf', false); + if (!$usher_config || !is_writable($usher_config)) { + throw new IDF_Scm_Exception( + '"mtn_usher_conf" does not exist or is not writable.' + ); + } + + $projectpath = sprintf($projecttempl, $shortname); + if (file_exists($projectpath)) { + if (!self::_delete_recursive($projectpath)) { + throw new IDF_Scm_Exception(sprintf( + __('One or more paths underknees %s could not be deleted.'), $projectpath + )); + } + } + + if (Pluf::f('mtn_remote_auth', true)) { + $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; + $keyname = $project->getConf()->getVal('mtn_client_key_name', false); + $keyhash = $project->getConf()->getVal('mtn_client_key_hash', false); + if ($keyname && $keyhash && + file_exists($keydir .'/'. $keyname . '.' . $keyhash)) { + if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) { + throw new IDF_Scm_Exception(sprintf( + __('Could not delete client private key %s'), $keyname + )); + } + } + } + + $usher_rc = file_get_contents($usher_config); + $parsed_config = array(); + try { + $parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc); + } + catch (Exception $e) { + throw new IDF_Scm_Exception(sprintf( + __('Could not parse usher configuration in "%s": %s'), + $usher_config, $e->getMessage() + )); + } + + foreach ($parsed_config as $idx => $stanzas) { + foreach ($stanzas as $stanza_line) { + if ($stanza_line['key'] == 'server' && + $stanza_line['values'][0] == $shortname) { + unset($parsed_config[$idx]); + break; + } + } + } + + $usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config); + + // FIXME: more sanity - what happens on failing writes? we do not + // have a backup copy of usher.conf around... + if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write usher configuration file "%s"'), $usher_config + )); + } + + IDF_Scm_Monotone_Usher::reload(); + } + + /** + * Adds the (monotone) key to all monotone projects of this forge + * where the user of the key has write access to + */ + public function processKeyCreate($key) + { + if ($key->getType() != 'mtn') + return; + + $projecttempl = Pluf::f('mtn_repositories', false); + if ($projecttempl === false) { + throw new IDF_Scm_Exception( + '"mtn_repositories" must be defined in your configuration file.' + ); + } + + foreach (Pluf::factory('IDF_Project')->getList() as $project) { + $conf = new IDF_Conf(); + $conf->setProject($project); + $scm = $conf->getVal('scm', 'mtn'); + if ($scm != 'mtn') + continue; + + $shortname = $project->shortname; + $projectpath = sprintf($projecttempl, $shortname); + if (!file_exists($projectpath)) { + throw new IDF_Scm_Exception(sprintf( + __('The project path %s does not exists.'), $projectpath + )); + } + + $auth_ids = self::getAuthorizedUserIds($project); + if (!in_array($key->user, $auth_ids)) + continue; + + $mtn_key_id = $key->getMtnId(); + + // if the project is not defined as private, all people have + // read access already, so we don't need to write anything + // and we currently do not check if read-permissions really + // contains + // pattern "*" + // allow "*" + // which is the default for non-private projects + if ($project->private == true) { + $read_perms = file_get_contents($projectpath.'/read-permissions'); + $parsed_read_perms = array(); + try { + $parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms); + } + catch (Exception $e) { + throw new IDF_Scm_Exception(sprintf( + __('Could not parse read-permissions for project "%s": %s'), + $shortname, $e->getMessage() + )); + } + + $wildcard_section = null; + foreach ($parsed_read_perms as $stanzas) { + foreach ($stanzas as $stanza_line) { + if ($stanza_line['key'] == 'pattern' && + $stanza_line['values'][0] == '*') { + $wildcard_section =& $stanzas; + break; + } + } + } + + if ($wildcard_section == null) + { + $wildcard_section = array( + array('key' => 'pattern', 'values' => array('*')) + ); + $parsed_read_perms[] =& $wildcard_section; + } + + $key_found = false; + foreach ($wildcard_section as $line) + { + if ($line['key'] == 'allow' && $line['values'][0] == $mtn_key_id) { + $key_found = true; + break; + } + } + + if (!$key_found) { + $wildcard_section[] = array( + 'key' => 'allow', 'values' => array($mtn_key_id) + ); + } + + $read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms); + + if (file_put_contents($projectpath.'/read-permissions', + $read_perms, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write read-permissions for project "%s"'), $shortname + )); + } + } + + $write_perms = file_get_contents($projectpath.'/write-permissions'); + $lines = preg_split("/(\n|\r\n)/", $write_perms); + if (!in_array('*', $lines) && !in_array($mtn_key_id, $lines)) { + $lines[] = $mtn_key_id; + } + if (file_put_contents($projectpath.'/write-permissions', + implode("\n", $lines), LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write write-permissions file for project "%s"'), + $shortname + )); + } + + $mtn = IDF_Scm_Monotone::factory($project); + $stdio = $mtn->getStdio(); + $stdio->exec(array('put_public_key', $key->content)); + } + } + + /** + * Removes the (monotone) key from all monotone projects of this forge + * where the user of the key has write access to + */ + public function processKeyDelete($key) + { + if ($key->getType() != 'mtn') + return; + + $projecttempl = Pluf::f('mtn_repositories', false); + if ($projecttempl === false) { + throw new IDF_Scm_Exception( + '"mtn_repositories" must be defined in your configuration file.' + ); + } + + foreach (Pluf::factory('IDF_Project')->getList() as $project) { + $conf = new IDF_Conf(); + $conf->setProject($project); + $scm = $conf->getVal('scm', 'mtn'); + if ($scm != 'mtn') + continue; + + $shortname = $project->shortname; + $projectpath = sprintf($projecttempl, $shortname); + if (!file_exists($projectpath)) { + throw new IDF_Scm_Exception(sprintf( + __('The project path %s does not exists.'), $projectpath + )); + } + + $auth_ids = self::getAuthorizedUserIds($project); + if (!in_array($key->user, $auth_ids)) + continue; + + $mtn_key_id = $key->getMtnId(); + + // if the project is not defined as private, all people have + // read access already, so we don't need to write anything + // and we currently do not check if read-permissions really + // contains + // pattern "*" + // allow "*" + // which is the default for non-private projects + if ($project->private === true) { + $read_perms = file_get_contents($projectpath.'/read-permissions'); + $parsed_read_perms = array(); + try { + $parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms); + } + catch (Exception $e) { + throw new IDF_Scm_Exception(sprintf( + __('Could not parse read-permissions for project "%s": %s'), + $shortname, $e->getMessage() + )); + } + + // while we add new keys only to an existing wild-card entry + // we remove dropped keys from all sections since the key + // should be simply unavailable for all of them + foreach ($parsed_read_perms as $stanzas) { + for ($i=0; $igetStdio(); + // if the public key did not sign any revisions, drop it from + // the database as well + try { + if (strlen($stdio->exec(array('select', 'k:' . $mtn_key_id))) == 0) { + $stdio->exec(array('drop_public_key', $mtn_key_id)); + } + } catch (IDF_Scm_Exception $e) { + if (strpos($e->getMessage(), 'there is no key named') === false) + throw $e; + } + } + } + + private static function getAuthorizedUserIds($project) + { + $mem = $project->getMembershipData(); + $members = array_merge((array)$mem['members'], + (array)$mem['owners'], + (array)$mem['authorized']); + $userids = array(); + foreach ($members as $member) { + $userids[] = $member->id; + } + return $userids; + } + + /** + * Update the timeline after a push + * + */ + public function processSyncTimeline($project_name) + { + try { + $project = IDF_Project::getOr404($project_name); + } catch (Pluf_HTTP_Error404 $e) { + Pluf_Log::event(array( + 'IDF_Plugin_SyncMonotone::processSyncTimeline', + 'Project not found.', + array($project_name, $params) + )); + return false; // Project not found + } + + Pluf_Log::debug(array( + 'IDF_Plugin_SyncMonotone::processSyncTimeline', + 'Project found', $project_name, $project->id + )); + IDF_Scm::syncTimeline($project, true); + Pluf_Log::event(array( + 'IDF_Plugin_SyncMonotone::processSyncTimeline', + 'sync', array($project_name, $project->id) + )); + } + + private static function _mtn_exec($cmd) + { + $fullcmd = sprintf('%s %s %s', + Pluf::f('idf_exec_cmd_prefix', ''), + Pluf::f('mtn_path', 'mtn'), + $cmd + ); + + $output = $return = null; + exec($fullcmd, $output, $return); + if ($return != 0) { + throw new IDF_Scm_Exception(sprintf( + __('The command "%s" could not be executed.'), $cmd + )); + } + return implode("\n", $output); + } + + private static function _delete_recursive($path) + { + if (is_file($path)) { + return @unlink($path); + } + + if (is_dir($path)) { + $scan = glob(rtrim($path, '/') . '/*'); + $status = 0; + foreach ($scan as $subpath) { + $status |= self::_delete_recursive($subpath); + } + $status |= rmdir($path); + return $status; + } } } diff --git a/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl b/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl new file mode 100644 index 0000000..daf3307 --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl @@ -0,0 +1,63 @@ +-- ***** 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 ***** + +-- +-- controls the access rights for remote_stdio which is used by IDFs frontend +-- and other interested parties +-- +function get_remote_automate_permitted(key_identity, command, options) + if (key_identity.id == "%%MTNCLIENTKEY%%") then + return true + end + + return false +end + +-- +-- let IDF know of new arriving revisions to fill its timeline +-- +_idf_revs = {} +function note_netsync_start(session_id) + _idf_revs[session_id] = {} +end + +function note_netsync_revision_received(new_id, revision, certs, session_id) + table.insert(_idf_revs[session_id], new_id) +end + +function note_netsync_end (session_id, ...) + if table.getn(_idf_revs[session_id]) == 0 then + return + end + + local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%"); + if pid == -1 then + print("could execute %%MTNPOSTPUSH%%") + return + end + + for _,r in ipairs(_idf_revs[session_id]) do + pin:write(r .. "\n") + end + pin:close() + + wait(pid) +end + diff --git a/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl b/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl new file mode 100644 index 0000000..c0c0050 --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl @@ -0,0 +1,75 @@ +-- ***** 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 ***** + +-- +-- controls the access rights for remote_stdio which is used by IDFs frontend +-- and other interested parties +-- +function get_remote_automate_permitted(key_identity, command, options) + local read_only_commands = { + "get_corresponding_path", "get_content_changed", "tags", "branches", + "common_ancestors", "packet_for_fdelta", "packet_for_fdata", + "packets_for_certs", "packet_for_rdata", "get_manifest_of", + "get_revision", "select", "graph", "children", "parents", "roots", + "leaves", "ancestry_difference", "toposort", "erase_ancestors", + "descendents", "ancestors", "heads", "get_file_of", "get_file", + "interface_version", "get_attributes", "content_diff", + "file_merge", "show_conflicts", "certs", "keys" + } + + for _,v in ipairs(read_only_commands) do + if (v == command[1]) then + return true + end + end + + return false +end + +-- +-- let IDF know of new arriving revisions to fill its timeline +-- +_idf_revs = {} +function note_netsync_start(session_id) + _idf_revs[session_id] = {} +end + +function note_netsync_revision_received(new_id, revision, certs, session_id) + table.insert(_idf_revs[session_id], new_id) +end + +function note_netsync_end (session_id, ...) + if table.getn(_idf_revs[session_id]) == 0 then + return + end + + local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%"); + if pid == -1 then + print("could execute %%MTNPOSTPUSH%%") + return + end + + for _,r in ipairs(_idf_revs[session_id]) do + pin:write(r .. "\n") + end + pin:close() + + wait(pid) +end diff --git a/src/IDF/Scm.php b/src/IDF/Scm.php index 94cf5ec..1496974 100644 --- a/src/IDF/Scm.php +++ b/src/IDF/Scm.php @@ -63,7 +63,7 @@ class IDF_Scm public $project = null; /** - * Cache storage. + * Cache storage. * * It must only be used to store data for the lifetime of the * object. For example if you need to get the list of branches in @@ -166,13 +166,28 @@ class IDF_Scm throw new Pluf_Exception_NotImplemented(); } + const REVISION_VALID = 0; + const REVISION_INVALID = 1; + const REVISION_AMBIGUOUS = 2; + /** - * Check if a revision or commit is valid. + * Check if a revision or commit is valid, invalid or ambiguous. * * @param string Revision or commit - * @return bool + * @return int One of REVISION_VALID, REVISION_INVALID or REVISION_AMBIGIOUS */ - public function isValidRevision($rev) + public function validateRevision($rev) + { + throw new Pluf_Exception_NotImplemented(); + } + + /** + * Returns an array of single commit objects for ambiguous commit identifiers + * + * @param string Ambiguous commit identifier + * @return array of objects + */ + public function disambiguateRevision($commit) { throw new Pluf_Exception_NotImplemented(); } @@ -217,7 +232,7 @@ class IDF_Scm * 'foo-branch' => 'branches/foo-branch',) * * - * @return array Branches + * @return array Branches */ public function getBranches() { @@ -282,7 +297,7 @@ class IDF_Scm * @param string Revision or commit * @param string Folder ('/') * @param string Branch (null) - * @return array + * @return array */ public function getTree($rev, $folder='/', $branch=null) { @@ -396,7 +411,7 @@ class IDF_Scm public static function syncTimeline($project, $force=false) { $cache = Pluf_Cache::factory(); - $key = 'IDF_Scm:'.$project->shortname.':lastsync'; + $key = 'IDF_Scm:'.$project->shortname.':lastsync'; if ($force or null === ($res=$cache->get($key))) { $scm = IDF_Scm::get($project); if ($scm->isAvailable()) { diff --git a/src/IDF/Scm/Git.php b/src/IDF/Scm/Git.php index 77bea39..4a3b308 100644 --- a/src/IDF/Scm/Git.php +++ b/src/IDF/Scm/Git.php @@ -296,10 +296,12 @@ class IDF_Scm_Git extends IDF_Scm } - public function isValidRevision($commit) + public function validateRevision($commit) { $type = $this->testHash($commit); - return ('commit' == $type || 'tag' == $type); + if ('commit' == $type || 'tag' == $type) + return IDF_Scm::REVISION_VALID; + return IDF_Scm::REVISION_INVALID; } /** diff --git a/src/IDF/Scm/Mercurial.php b/src/IDF/Scm/Mercurial.php index 9f34ab7..ac66464 100644 --- a/src/IDF/Scm/Mercurial.php +++ b/src/IDF/Scm/Mercurial.php @@ -87,14 +87,19 @@ class IDF_Scm_Mercurial extends IDF_Scm return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname); } - public function isValidRevision($rev) + public function validateRevision($rev) { $cmd = sprintf(Pluf::f('hg_path', 'hg').' log -R %s -r %s', escapeshellarg($this->repo), escapeshellarg($rev)); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; self::exec('IDF_Scm_Mercurial::isValidRevision', $cmd, $out, $ret); - return ($ret == 0) && (count($out) > 0); + + // FIXME: apparently a given hg revision can also be ambigious - + // handle this case here sometime + if ($ret == 0 && count($out) > 0) + return IDF_Scm::REVISION_VALID; + return IDF_Scm::REVISION_INVALID; } /** diff --git a/src/IDF/Scm/Monotone.php b/src/IDF/Scm/Monotone.php index f8c898f..a0b01c6 100644 --- a/src/IDF/Scm/Monotone.php +++ b/src/IDF/Scm/Monotone.php @@ -47,6 +47,16 @@ class IDF_Scm_Monotone extends IDF_Scm $this->stdio = new IDF_Scm_Monotone_Stdio($project); } + /** + * Returns the stdio instance in use + * + * @return IDF_Scm_Monotone_Stdio + */ + public function getStdio() + { + return $this->stdio; + } + /** * @see IDF_Scm::getRepositorySize() */ @@ -340,7 +350,14 @@ class IDF_Scm_Monotone extends IDF_Scm 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']); + $combinedChangelog = implode("\n---\n", $certs['changelog']); + $split = preg_split("/[\n\r]/", $combinedChangelog, 2); + // FIXME: the complete log message is currently not used in the + // tree view (the same is true for the other SCM implementations) + // but we _should_ really use or at least return that here + // in case we want to do fancy stuff like described in + // issue 492 + $file['log'] = $split[0]; } $files[] = (object) $file; @@ -425,12 +442,53 @@ class IDF_Scm_Monotone extends IDF_Scm } /** - * @see IDF_Scm::isValidRevision() + * @see IDF_Scm::validateRevision() */ - public function isValidRevision($commit) + public function validateRevision($commit) { $revs = $this->_resolveSelector($commit); - return count($revs) == 1; + if (count($revs) == 0) + return IDF_Scm::REVISION_INVALID; + + if (count($revs) > 1) + return IDF_Scm::REVISION_AMBIGUOUS; + + return IDF_Scm::REVISION_VALID; + } + + /** + * @see IDF_Scm::disambiguateRevision + */ + public function disambiguateRevision($commit) + { + $revs = $this->_resolveSelector($commit); + + $out = array(); + foreach ($revs as $rev) + { + $certs = $this->_getCerts($rev); + + $log = array(); + $log['author'] = implode(', ', $certs['author']); + + $log['branch'] = implode(', ', $certs['branch']); + + $dates = array(); + foreach ($certs['date'] as $date) + $dates[] = date('Y-m-d H:i:s', strtotime($date)); + $log['date'] = implode(', ', $dates); + + $combinedChangelog = implode("\n---\n", $certs['changelog']); + $split = preg_split("/[\n\r]/", $combinedChangelog, 2); + $log['title'] = $split[0]; + $log['full_message'] = (isset($split[1])) ? trim($split[1]) : ''; + + $log['commit'] = $rev; + + $out[] = (object)$log; + } + + return $out; } /** @@ -630,7 +688,7 @@ class IDF_Scm_Monotone extends IDF_Scm --$n; $log = array(); - $log['author'] = implode(", ", $certs['author']); + $log['author'] = implode(', ', $certs['author']); $dates = array(); foreach ($certs['date'] as $date) diff --git a/src/IDF/Scm/Monotone/BasicIO.php b/src/IDF/Scm/Monotone/BasicIO.php index 0562c9b..78707c5 100644 --- a/src/IDF/Scm/Monotone/BasicIO.php +++ b/src/IDF/Scm/Monotone/BasicIO.php @@ -75,6 +75,9 @@ class IDF_Scm_Monotone_BasicIO } ++$pos; // closing quote + if ($pos >= strlen($in)) + break; + if ($in[$pos] == ' ') { ++$pos; // space ++$valCount; diff --git a/src/IDF/Scm/Monotone/Stdio.php b/src/IDF/Scm/Monotone/Stdio.php index 391133a..fa243d8 100644 --- a/src/IDF/Scm/Monotone/Stdio.php +++ b/src/IDF/Scm/Monotone/Stdio.php @@ -62,6 +62,55 @@ class IDF_Scm_Monotone_Stdio $this->stop(); } + /** + * Returns a string with additional options which are passed to + * an mtn instance connecting to remote databases + * + * @return string + */ + public function _getAuthOptions() + { + // no remote authentication - the simple case + if (!Pluf::f('mtn_remote_auth', true)) { + return '--key= '; + } + + $prjconf = $this->project->getConf(); + $name = $prjconf->getVal('mtn_client_key_name', false); + $hash = $prjconf->getVal('mtn_client_key_hash', false); + + if (!$name || !$hash) { + throw new IDF_Scm_Exception(sprintf( + __('Monotone client key name or hash not in project conf.') + )); + } + + $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; + if (!file_exists($keydir)) { + if (!mkdir($keydir)) { + throw new IDF_Scm_Exception(sprintf( + __('The key directory %s could not be created.'), $keydir + )); + } + } + + // in case somebody cleaned out the cache, we restore the key here + $keyfile = $keydir . '/' . $name .'.'. $hash; + if (!file_exists($keyfile)) { + $data = $prjconf->getVal('mtn_client_key_data'); + if (!file_put_contents($keyfile, $data, LOCK_EX)) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write client key "%s"'), $keyfile + )); + } + } + + return sprintf('--keydir=%s --key=%s ', + escapeshellarg($keydir), + escapeshellarg($hash) + ); + } + /** * Starts the stdio process and resets the command counter */ @@ -80,9 +129,8 @@ class IDF_Scm_Monotone_Stdio $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) { + $cmd .= $this->_getAuthOptions(); $host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname); $cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host)); } @@ -104,7 +152,6 @@ class IDF_Scm_Monotone_Stdio ); $env = array('LANG' => 'en_US.UTF-8'); - $this->proc = proc_open($cmd, $descriptors, $this->pipes, null, $env); @@ -146,8 +193,9 @@ class IDF_Scm_Monotone_Stdio return false; $read = array($this->pipes[1], $this->pipes[2]); + $write = $except = null; $streamsChanged = stream_select( - $read, $write = null, $except = null, 0, 20000 + $read, $write, $except, 0, 20000 ); if ($streamsChanged === false) { diff --git a/src/IDF/Scm/Svn.php b/src/IDF/Scm/Svn.php index 6527bbf..fdbc340 100644 --- a/src/IDF/Scm/Svn.php +++ b/src/IDF/Scm/Svn.php @@ -138,7 +138,7 @@ class IDF_Scm_Svn extends IDF_Scm /** * Subversion revisions are either a number or 'HEAD'. */ - public function isValidRevision($rev) + public function validateRevision($rev) { if ($rev == 'HEAD') { return true; @@ -149,8 +149,11 @@ class IDF_Scm_Svn extends IDF_Scm escapeshellarg($this->repo), escapeshellarg($rev)); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; - self::exec('IDF_Scm_Svn::isValidRevision', $cmd, $out, $ret); - return (0 == $ret); + self::exec('IDF_Scm_Svn::validateRevision', $cmd, $out, $ret); + + if ($ret == 0) + return IDF_Scm::REVISION_VALID; + return IDF_Scm::REVISION_INVALID; } diff --git a/src/IDF/Template/Markdown.php b/src/IDF/Template/Markdown.php index feb8512..3054ca8 100644 --- a/src/IDF/Template/Markdown.php +++ b/src/IDF/Template/Markdown.php @@ -46,25 +46,32 @@ class IDF_Template_Markdown extends Pluf_Template_Tag array($this, 'callbackEmbeddedDoc'), $text); } + // Replace [Page]([[PageName]]) with corresponding link to the page, with link text being Page. + $text = preg_replace_callback('#\[([^\]]+)\]\(\[\[([A-Za-z0-9\-]+)\]\]\)#im', + array($this, 'callbackWikiPage'), + $text); // Replace [[PageName]] with corresponding link to the page. $text = preg_replace_callback('#\[\[([A-Za-z0-9\-]+)\]\]#im', - array($this, 'callbackWikiPage'), + array($this, 'callbackWikiPageNoName'), $text); $filter = new IDF_Template_MarkdownPrefilter(); echo $filter->go(Pluf_Text_MarkDown_parse($text)); } + function callbackWikiPageNoName($m) + { + $m[2] = $m[1]; //Set the link text to be the same as the page name. + return $this->callbackWikiPage($m); + } + function callbackWikiPage($m) { $sql = new Pluf_SQL('project=%s AND title=%s', - array($this->project->id, $m[1])); + array($this->project->id, $m[2])); $pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen())); - if ($pages->count() != 1 and !$this->request->rights['hasWikiAccess']) { - return $m[0]; - } if ($pages->count() != 1 and $this->request->rights['hasWikiAccess'] and !$this->request->user->isAnonymous()) { - return ' '.$m[1].''; + return ' '.$m[1].''; } if (!$this->request->rights['hasWikiAccess'] or $pages->count() == 0) { return $m[1]; diff --git a/src/IDF/Views/Source.php b/src/IDF/Views/Source.php index 37d9ca9..2d1b3f8 100644 --- a/src/IDF/Views/Source.php +++ b/src/IDF/Views/Source.php @@ -59,30 +59,56 @@ class IDF_Views_Source $params, $request); } - public $changeLog_precond = array('IDF_Precondition::accessSource'); + /** + * Is displayed in case an invalid revision is requested + */ + public $invalidRevision_precond = array('IDF_Precondition::accessSource'); + public function invalidRevision($request, $match) + { + $title = sprintf(__('%s Invalid Revision'), (string) $request->project); + $commit = $match[2]; + $params = array( + 'page_title' => $title, + 'title' => $title, + 'commit' => $commit, + ); + return Pluf_Shortcuts_RenderToResponse('idf/source/invalid_revision.html', + $params, $request); + } + + /** + * Is displayed in case a revision identifier cannot be uniquely resolved + * to one single revision + */ + public $disambiguateRevision_precond = array('IDF_Precondition::accessSource', + 'IDF_Views_Source_Precondition::scmAvailable'); + public function disambiguateRevision($request, $match) + { + $title = sprintf(__('%s Ambiguous Revision'), (string) $request->project); + $commit = $match[2]; + $redirect = $match[3]; + $scm = IDF_Scm::get($request->project); + $revisions = $scm->disambiguateRevision($commit); + $params = array( + 'page_title' => $title, + 'title' => $title, + 'commit' => $commit, + 'revisions' => $revisions, + 'redirect' => $redirect, + ); + return Pluf_Shortcuts_RenderToResponse('idf/source/disambiguate_revision.html', + $params, $request); + } + + public $changeLog_precond = array('IDF_Precondition::accessSource', + 'IDF_Views_Source_Precondition::scmAvailable', + 'IDF_Views_Source_Precondition::revisionValid'); public function changeLog($request, $match) { $scm = IDF_Scm::get($request->project); - if (!$scm->isAvailable()) { - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help', - array($request->project->shortname)); - return new Pluf_HTTP_Response_Redirect($url); - } $branches = $scm->getBranches(); $commit = $match[2]; - if (!$scm->isValidRevision($commit)) { - if (count($branches) == 0) { - // Redirect to the project source help - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help', - array($request->project->shortname)); - return new Pluf_HTTP_Response_Redirect($url); - } - // Redirect to the first branch - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::changeLog', - array($request->project->shortname, - $scm->getMainBranch())); - return new Pluf_HTTP_Response_Redirect($url); - } + $title = sprintf(__('%1$s %2$s Change Log'), (string) $request->project, $this->getScmType($request)); $changes = $scm->getChangeLog($commit, 25); @@ -111,22 +137,17 @@ class IDF_Views_Source $request); } - public $treeBase_precond = array('IDF_Precondition::accessSource'); + public $treeBase_precond = array('IDF_Precondition::accessSource', + 'IDF_Views_Source_Precondition::scmAvailable', + 'IDF_Views_Source_Precondition::revisionValid'); public function treeBase($request, $match) { $scm = IDF_Scm::get($request->project); - if (!$scm->isAvailable()) { - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help', - array($request->project->shortname)); - return new Pluf_HTTP_Response_Redirect($url); - } $commit = $match[2]; + $cobject = $scm->getCommit($commit); if (!$cobject) { - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', - array($request->project->shortname, - $scm->getMainBranch())); - return new Pluf_HTTP_Response_Redirect($url); + throw new Exception('could not retrieve commit object for '. $commit); } $title = sprintf(__('%1$s %2$s Source Tree'), $request->project, $this->getScmType($request)); @@ -159,20 +180,14 @@ class IDF_Views_Source $request); } - public $tree_precond = array('IDF_Precondition::accessSource'); + public $tree_precond = array('IDF_Precondition::accessSource', + 'IDF_Views_Source_Precondition::scmAvailable', + 'IDF_Views_Source_Precondition::revisionValid'); public function tree($request, $match) { $scm = IDF_Scm::get($request->project); $commit = $match[2]; - if (!$scm->isAvailable()) { - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help', - array($request->project->shortname)); - return new Pluf_HTTP_Response_Redirect($url); - } - $fburl = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', - array($request->project->shortname, - $scm->getMainBranch())); $request_file = $match[3]; if (substr($request_file, -1) == '/') { $request_file = substr($request_file, 0, -1); @@ -181,13 +196,13 @@ class IDF_Views_Source $request_file)); return new Pluf_HTTP_Response_Redirect($url, 301); } - if (!$scm->isValidRevision($commit, $request_file)) { - // Redirect to the first branch - return new Pluf_HTTP_Response_Redirect($fburl); - } + $request_file_info = $scm->getPathInfo($request_file, $commit); if (!$request_file_info) { - // Redirect to the first branch + // Redirect to the main branch + $fburl = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', + array($request->project->shortname, + $scm->getMainBranch())); return new Pluf_HTTP_Response_Redirect($fburl); } $branches = $scm->getBranches(); @@ -277,26 +292,17 @@ class IDF_Views_Source return ''.implode(''.$sep.'', $out).''; } - public $commit_precond = array('IDF_Precondition::accessSource'); + public $commit_precond = array('IDF_Precondition::accessSource', + 'IDF_Views_Source_Precondition::scmAvailable', + 'IDF_Views_Source_Precondition::revisionValid'); public function commit($request, $match) { $scm = IDF_Scm::get($request->project); $commit = $match[2]; - if (!$scm->isValidRevision($commit)) { - // Redirect to the first branch - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', - array($request->project->shortname, - $scm->getMainBranch())); - return new Pluf_HTTP_Response_Redirect($url); - } $large = $scm->isCommitLarge($commit); $cobject = $scm->getCommit($commit, !$large); if (!$cobject) { - // Redirect to the first branch - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', - array($request->project->shortname, - $scm->getMainBranch())); - return new Pluf_HTTP_Response_Redirect($url); + throw new Exception('could not retrieve commit object for '. $commit); } $title = sprintf(__('%s Commit Details'), (string) $request->project); $page_title = sprintf(__('%s Commit Details - %s'), (string) $request->project, $commit); @@ -326,19 +332,17 @@ class IDF_Views_Source $request); } - public $downloadDiff_precond = array('IDF_Precondition::accessSource'); + public $downloadDiff_precond = array('IDF_Precondition::accessSource', + 'IDF_Views_Source_Precondition::scmAvailable', + 'IDF_Views_Source_Precondition::revisionValid'); public function downloadDiff($request, $match) { $scm = IDF_Scm::get($request->project); $commit = $match[2]; - if (!$scm->isValidRevision($commit)) { - // Redirect to the first branch - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', - array($request->project->shortname, - $scm->getMainBranch())); - return new Pluf_HTTP_Response_Redirect($url); - } $cobject = $scm->getCommit($commit, true); + if (!$cobject) { + throw new Exception('could not retrieve commit object for '. $commit); + } $rep = new Pluf_HTTP_Response($cobject->changes, 'text/plain'); $rep->headers['Content-Disposition'] = 'attachment; filename="'.$commit.'.diff"'; return $rep; @@ -394,19 +398,14 @@ class IDF_Views_Source * Get a given file at a given commit. * */ - public $getFile_precond = array('IDF_Precondition::accessSource'); + public $getFile_precond = array('IDF_Precondition::accessSource', + 'IDF_Views_Source_Precondition::scmAvailable', + 'IDF_Views_Source_Precondition::revisionValid'); public function getFile($request, $match) { $scm = IDF_Scm::get($request->project); $commit = $match[2]; $request_file = $match[3]; - if (!$scm->isValidRevision($commit)) { - // Redirect to the first branch - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', - array($request->project->shortname, - $scm->getMainBranch())); - return new Pluf_HTTP_Response_Redirect($url); - } $request_file_info = $scm->getPathInfo($request_file, $commit); if (!$request_file_info or $request_file_info->type == 'tree') { // Redirect to the first branch @@ -427,18 +426,13 @@ class IDF_Views_Source * Get a zip archive of the current commit. * */ - public $download_precond = array('IDF_Precondition::accessSource'); + public $download_precond = array('IDF_Precondition::accessSource', + 'IDF_Views_Source_Precondition::scmAvailable', + 'IDF_Views_Source_Precondition::revisionValid'); public function download($request, $match) { $commit = trim($match[2]); $scm = IDF_Scm::get($request->project); - if (!$scm->isValidRevision($commit)) { - // Redirect to the first branch - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', - array($request->project->shortname, - $scm->getMainBranch())); - return new Pluf_HTTP_Response_Redirect($url); - } $base = $request->project->shortname.'-'.$commit; $cmd = $scm->getArchiveCommand($commit, $base.'/'); $rep = new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip'); @@ -447,7 +441,6 @@ class IDF_Views_Source return $rep; } - /** * Find the mime type of a requested file. * @@ -495,7 +488,6 @@ class IDF_Views_Source return $res; } - /** * Find the mime type of a file. * @@ -609,4 +601,3 @@ function IDF_Views_Source_ShortenString($string, $length) return substr($string, 0, $preflen).$ellipse. substr($string, -($length - $preflen - mb_strlen($ellipse))); } - diff --git a/src/IDF/Views/Source/Precondition.php b/src/IDF/Views/Source/Precondition.php new file mode 100644 index 0000000..a3a0172 --- /dev/null +++ b/src/IDF/Views/Source/Precondition.php @@ -0,0 +1,74 @@ +project); + if (!$scm->isAvailable()) { + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help', + array($request->project->shortname)); + return new Pluf_HTTP_Response_Redirect($url); + } + return true; + } + + /** + * Validates the revision given in the URL path and acts accordingly + * + * @param $request + * @return true | Pluf_HTTP_Response_Redirect + * @throws Exception + */ + static public function revisionValid($request) + { + list($url_info, $url_matches) = $request->view; + list(, $project, $commit) = $url_matches; + + $scm = IDF_Scm::get($request->project); + $res = $scm->validateRevision($commit); + switch ($res) { + case IDF_Scm::REVISION_VALID: + return true; + case IDF_Scm::REVISION_INVALID: + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::invalidRevision', + array($request->project->shortname, $commit)); + return new Pluf_HTTP_Response_Redirect($url); + case IDF_Scm::REVISION_AMBIGUOUS: + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::disambiguateRevision', + array($request->project->shortname, + $commit, + $url_info['model'].'::'.$url_info['method'])); + return new Pluf_HTTP_Response_Redirect($url); + default: + throw new Exception('unknown validation result: '. $res); + } + } +} diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index 57d3e95..d4bcd5c 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -73,10 +73,10 @@ $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 +# Path to the monotone binary (you need mtn 0.99 or newer) $cfg['mtn_path'] = 'mtn'; # Additional options for the started monotone process -$cfg['mtn_opts'] = array('--no-workspace', '--norc'); +$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles'); # # You can setup monotone for use with indefero in several ways. The # two most-used should be: @@ -157,6 +157,19 @@ $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s'; # $cfg['mtn_db_access'] = 'remote'; # +# If true, each access to the database is authenticated with an auto-generated +# project key which is stored in the IDF project configuration +# ('mtn_client_key_*') and written out to $cfg['tmp_folder']/mtn-client-keys +# for its actual use. This key is then configured on the server to have +# full read / write access to all functions, while anonymous access can be +# completely disabled. +# If false, IDF tries to connect anonymously, without authentication, to +# the remote monotone server instance. In this case no project-specific +# keys are generated and the server must be configured to allow at least +# anonymous read access to the main functions. +# +$cfg['mtn_remote_auth'] = true; +# # If configured, this allows basic control of a running usher process # via the forge administration. The variable must point to the full (writable) # path of the usher configuration file which gets updated when new projects diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index b4f6a4c..c01e565 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -148,6 +148,16 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#', 'model' => 'IDF_Views_Source', 'method' => 'help'); +$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/invalid/([^/]+)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Source', + 'method' => 'invalidRevision'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/disambiguate/([^/]+)/from/([^/]+)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Source', + 'method' => 'disambiguateRevision'); + $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/tree/([^/]+)/$#', 'base' => $base, 'model' => 'IDF_Views_Source', diff --git a/src/IDF/locale/cs/idf.po b/src/IDF/locale/cs/idf.po index 8e9f055..28cead4 100644 --- a/src/IDF/locale/cs/idf.po +++ b/src/IDF/locale/cs/idf.po @@ -970,7 +970,7 @@ msgid "" "
    \n" "
  • A commit or revision of the current code in the repository from which you started your work.
  • \n" "
  • A patch describing your changes with respect to the reference commit.
  • \n" -"
  • Check your patch to not provide any password or confidential information!
  • \n" +"
  • Ensure your patch does not contain any passwords or confidential information!
  • \n" "
" msgstr "" diff --git a/src/IDF/locale/de/idf.po b/src/IDF/locale/de/idf.po index 0547f12..c558fad 100644 --- a/src/IDF/locale/de/idf.po +++ b/src/IDF/locale/de/idf.po @@ -970,7 +970,7 @@ msgid "" "
    \n" "
  • A commit or revision of the current code in the repository from which you started your work.
  • \n" "
  • A patch describing your changes with respect to the reference commit.
  • \n" -"
  • Check your patch to not provide any password or confidential information!
  • \n" +"
  • Ensure your patch does not contain any passwords or confidential information!
  • \n" "
" msgstr "" diff --git a/src/IDF/locale/fr/idf.po b/src/IDF/locale/fr/idf.po index 491fb6b..4be8ba9 100644 --- a/src/IDF/locale/fr/idf.po +++ b/src/IDF/locale/fr/idf.po @@ -1026,7 +1026,7 @@ msgid "" "
    \n" "
  • A commit or revision of the current code in the repository from which you started your work.
  • \n" "
  • A patch describing your changes with respect to the reference commit.
  • \n" -"
  • Check your patch to not provide any password or confidential information!
  • \n" +"
  • Ensure your patch does not contain any passwords or confidential information!
  • \n" "
" msgstr "" "

Pour démarrer une revue de code vous devez fournir :

\n" diff --git a/src/IDF/locale/ru/idf.po b/src/IDF/locale/ru/idf.po index 40de7a9..5d83baf 100644 --- a/src/IDF/locale/ru/idf.po +++ b/src/IDF/locale/ru/idf.po @@ -1019,7 +1019,7 @@ msgid "" "
    \n" "
  • A commit or revision of the current code in the repository from which you started your work.
  • \n" "
  • A patch describing your changes with respect to the reference commit.
  • \n" -"
  • Check your patch to not provide any password or confidential information!
  • \n" +"
  • Ensure your patch does not contain any passwords or confidential information!
  • \n" "
" msgstr "" diff --git a/src/IDF/locale/sl/idf.po b/src/IDF/locale/sl/idf.po index f826710..3d0fe69 100644 --- a/src/IDF/locale/sl/idf.po +++ b/src/IDF/locale/sl/idf.po @@ -997,7 +997,7 @@ msgid "" "
    \n" "
  • A commit or revision of the current code in the repository from which you started your work.
  • \n" "
  • A patch describing your changes with respect to the reference commit.
  • \n" -"
  • Check your patch to not provide any password or confidential information!
  • \n" +"
  • Ensure your patch does not contain any passwords or confidential information!
  • \n" "
" msgstr "" diff --git a/src/IDF/relations.php b/src/IDF/relations.php index bf4e32e..16dd8b4 100644 --- a/src/IDF/relations.php +++ b/src/IDF/relations.php @@ -88,6 +88,15 @@ Pluf_Signal::connect('gitpostupdate.php::run', # monotone synchronization Pluf_Signal::connect('IDF_Project::created', array('IDF_Plugin_SyncMonotone', 'entry')); +Pluf_Signal::connect('IDF_Project::preDelete', + array('IDF_Plugin_SyncMonotone', 'entry')); +Pluf_Signal::connect('IDF_Key::postSave', + array('IDF_Plugin_SyncMonotone', 'entry')); +Pluf_Signal::connect('IDF_Key::preDelete', + array('IDF_Plugin_SyncMonotone', 'entry')); +Pluf_Signal::connect('phppostpush.php::run', + array('IDF_Plugin_SyncMonotone', 'entry')); + # # -- Processing of the webhook queue -- Pluf_Signal::connect('queuecron.php::run', diff --git a/src/IDF/templates/idf/gadmin/projects/create.html b/src/IDF/templates/idf/gadmin/projects/create.html index 1c1cddf..a2f70b8 100644 --- a/src/IDF/templates/idf/gadmin/projects/create.html +++ b/src/IDF/templates/idf/gadmin/projects/create.html @@ -29,6 +29,14 @@ +{$form.f.shortdesc.labelTag}: + +{if $form.f.shortdesc.errors}{$form.f.shortdesc.fieldErrors}{/if} +{$form.f.shortdesc|unsafe}
+{$form.f.shortdesc.help_text} + + + {$form.f.scm.labelTag}: {if $form.f.scm.errors}{$form.f.scm.fieldErrors}{/if} {$form.f.scm|unsafe} diff --git a/src/IDF/templates/idf/gadmin/projects/update.html b/src/IDF/templates/idf/gadmin/projects/update.html index 5442e72..e6a40eb 100644 --- a/src/IDF/templates/idf/gadmin/projects/update.html +++ b/src/IDF/templates/idf/gadmin/projects/update.html @@ -18,6 +18,14 @@ +{$form.f.shortdesc.labelTag}: + +{if $form.f.shortdesc.errors}{$form.f.shortdesc.fieldErrors}{/if} +{$form.f.shortdesc|unsafe}
+{$form.f.shortdesc.help_text} + + + {$form.f.owners.labelTag}: {if $form.f.owners.errors}{$form.f.owners.fieldErrors}{/if} diff --git a/src/IDF/templates/idf/gadmin/usher/index.html b/src/IDF/templates/idf/gadmin/usher/index.html index c81c134..fa6041b 100644 --- a/src/IDF/templates/idf/gadmin/usher/index.html +++ b/src/IDF/templates/idf/gadmin/usher/index.html @@ -14,7 +14,7 @@ {$server.name} {$server.status} - {if preg_match("/ACTIVE|RUNNING|SLEEPING/", $server.status)} + {if preg_match("/ACTIVE|WAITING|RUNNING|SLEEPING/", $server.status)} {trans 'stop'} {elseif $server.status == "STOPPED"} diff --git a/src/IDF/templates/idf/review/create.html b/src/IDF/templates/idf/review/create.html index 7275bbe..1a5a74e 100644 --- a/src/IDF/templates/idf/review/create.html +++ b/src/IDF/templates/idf/review/create.html @@ -57,7 +57,7 @@
  • A commit or revision of the current code in the repository from which you started your work.
  • A patch describing your changes with respect to the reference commit.
  • -
  • Check your patch to not provide any password or confidential information!
  • +
  • Ensure your patch does not contain any passwords or confidential information!
{/blocktrans} {/block} diff --git a/src/IDF/templates/idf/source/base.html b/src/IDF/templates/idf/source/base.html index 322f6ce..0526095 100644 --- a/src/IDF/templates/idf/source/base.html +++ b/src/IDF/templates/idf/source/base.html @@ -1,7 +1,7 @@ {extends "idf/base.html"} {block tabsource} class="active"{/block} {block subtabs} -{if !$inHelp and (in_array($commit, $tree_in) or (in_array($commit, $tags_in)))}{assign $currentCommit = $commit}{else}{assign $currentCommit = $project.getScmRoot()}{/if} +{if !$inHelp and !$inError and (in_array($commit, $tree_in) or (in_array($commit, $tags_in)))}{assign $currentCommit = $commit}{else}{assign $currentCommit = $project.getScmRoot()}{/if}
{trans 'Source Tree'} | {trans 'Change Log'} diff --git a/src/IDF/templates/idf/source/changelog.html b/src/IDF/templates/idf/source/changelog.html index 971d3ba..8c56074 100644 --- a/src/IDF/templates/idf/source/changelog.html +++ b/src/IDF/templates/idf/source/changelog.html @@ -12,12 +12,12 @@ {foreach $changes as $change} {aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $change.scm_id)} -{$change.creation_dtime|dateago:"wihtout"} +{$change.creation_dtime|dateago:"without"} {issuetext $change.summary, $request}{if $change.fullmessage}

{issuetext $change.fullmessage, $request, true, false, true, true, true}{/if} -
{trans 'Commit'} {$change.scm_id}, +
{trans 'Commit'} {$change.scm_id}, {trans 'by'} {showuser $change.get_author(), $request, $change.origauthor}
diff --git a/src/IDF/templates/idf/source/disambiguate_revision.html b/src/IDF/templates/idf/source/disambiguate_revision.html new file mode 100644 index 0000000..85c3d61 --- /dev/null +++ b/src/IDF/templates/idf/source/disambiguate_revision.html @@ -0,0 +1,33 @@ +{extends "idf/source/base.html"} +{block docclass}yui-t2{assign $inError=true}{/block} +{block body} + +

{blocktrans}The revision identifier {$commit} is ambiguous and can be +expanded to multiple valid revisions - please choose one:{/blocktrans}

+ + + + + + + + + + + + +{foreach $revisions as $revision} +{aurl 'url', $redirect, array($project.shortname, $revision.commit)} + + + + + + + + +{/foreach} + +
{trans 'Title'}{trans 'Author'}{trans 'Date'}{trans 'Branch'}{trans 'Revision'}
{$revision.title}{$revision.author}{$revision.date}{$revision.branch}{$revision.commit}
+{/block} + diff --git a/src/IDF/templates/idf/source/git/tree.html b/src/IDF/templates/idf/source/git/tree.html index 859a249..0f5d0eb 100644 --- a/src/IDF/templates/idf/source/git/tree.html +++ b/src/IDF/templates/idf/source/git/tree.html @@ -12,7 +12,7 @@ {trans 'Size'} {if !$tree_in and !$tags_in} -{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)} +{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)} {blocktrans}Source at commit {$commit} created {$cobject.date|dateago}.{/blocktrans}
{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans} @@ -35,7 +35,7 @@ {$file.file}{else}{$file.file}{/if} {if $file.type == 'blob'} {if isset($file.date) and $file.log != '----'} -{$file.date|dateago:"wihtout"} +{$file.date|dateago:"without"} {$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false} {else}{/if} {$file.size|size}{/if} diff --git a/src/IDF/templates/idf/source/invalid_revision.html b/src/IDF/templates/idf/source/invalid_revision.html new file mode 100644 index 0000000..4c0e966 --- /dev/null +++ b/src/IDF/templates/idf/source/invalid_revision.html @@ -0,0 +1,17 @@ +{extends "idf/source/base.html"} +{block docclass}yui-t2{assign $inError=true}{/block} +{block body} + +

{blocktrans}The revision {$commit} is not valid or does not exist +in this repository.{/blocktrans}

+ +{if $isOwner or $isMember} +{aurl 'url', 'IDF_Views_Source::help', array($project.shortname)} +

{blocktrans}If this is a new repository, the reason for this error +could be that you have not committed and / or pushed any change so far. +In this case please take a look at the Help page +how to access your repository.{/blocktrans}

+{/if} + +{/block} + diff --git a/src/IDF/templates/idf/source/mercurial/tree.html b/src/IDF/templates/idf/source/mercurial/tree.html index b5880cf..373e6a0 100644 --- a/src/IDF/templates/idf/source/mercurial/tree.html +++ b/src/IDF/templates/idf/source/mercurial/tree.html @@ -34,7 +34,7 @@ {$file.file} {if $file.type == 'blob'} {if isset($file.date)} -{$file.date|dateago:"wihtout"} +{$file.date|dateago:"without"} {$file.author|strip_tags|trim}{trans ':'} {$file.log} {else}{/if} {/if} diff --git a/src/IDF/templates/idf/source/svn/tree.html b/src/IDF/templates/idf/source/svn/tree.html index d9a1e29..dbb1713 100644 --- a/src/IDF/templates/idf/source/svn/tree.html +++ b/src/IDF/templates/idf/source/svn/tree.html @@ -45,7 +45,7 @@ {$file.type} {$file.file} - {$file.date|dateago:"wihtout"} + {$file.date|dateago:"without"} {$file.rev} {$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false} {if $file.type == 'blob'} diff --git a/src/IDF/templates/idf/wiki/edit-info.html b/src/IDF/templates/idf/wiki/edit-info.html index 98abdc7..088421c 100644 --- a/src/IDF/templates/idf/wiki/edit-info.html +++ b/src/IDF/templates/idf/wiki/edit-info.html @@ -1,6 +1,8 @@ -{assign $url = 'http://michelf.com/projects/php-markdown/extra/'} +{assign $eurl = 'http://michelf.com/projects/php-markdown/extra/'} +{assign $burl = 'http://daringfireball.net/projects/markdown/syntax'} {blocktrans}

Instructions:

-

The content of the page can use the Markdown syntax.

+

The content of the page can use the Markdown syntax with the Extra extension.

Website addresses are automatically linked and you can link to another page in the documentation using double square brackets like that [[AnotherPage]].

+

To directly include a file content from the repository, embrace its path with triple square brackets: [[[path/to/file.txt]]].

{/blocktrans}