diff --git a/doc/readme-monotone.mdtext b/doc/readme-monotone.mdtext new file mode 100644 index 0000000..2e54367 --- /dev/null +++ b/doc/readme-monotone.mdtext @@ -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. + diff --git a/src/IDF/Diff.php b/src/IDF/Diff.php index 817885d..c51e883 100644 --- a/src/IDF/Diff.php +++ b/src/IDF/Diff.php @@ -51,7 +51,7 @@ class IDF_Diff $i = 0; // Used to skip the end of a git patch with --\nversion number foreach ($this->lines as $line) { $i++; - if (0 === strpos($line, '--') and isset($this->lines[$i]) + if (0 === strpos($line, '--') and isset($this->lines[$i]) and preg_match('/^\d+\.\d+\.\d+\.\d+$/', $this->lines[$i])) { break; } @@ -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. */ @@ -215,14 +240,14 @@ class IDF_Diff * @param int Number of lines before/after the chunk to be displayed (10) * @return Pluf_Template_SafeString The table body */ - public function fileCompare($orig, $chunks, $filename, $context=10) + public function fileCompare($orig, $chunks, $filename, $context=10) { $orig_lines = preg_split("/\015\012|\015|\012/", $orig); $new_chunks = $this->mergeChunks($orig_lines, $chunks, $context); return $this->renderCompared($new_chunks, $filename); } - public function mergeChunks($orig_lines, $chunks, $context=10) + public function mergeChunks($orig_lines, $chunks, $context=10) { $spans = array(); $new_chunks = array(); @@ -250,7 +275,7 @@ class IDF_Diff for ($lc=$spans[$i][0];$lc<$chunk[0][0];$lc++) { $exists = false; foreach ($chunk_lines as $line) { - if ($lc == $line[0] + if ($lc == $line[0] or ($chunk[0][1]-$chunk[0][0]+$lc) == $line[1]) { $exists = true; break; @@ -259,7 +284,7 @@ class IDF_Diff if (!$exists) { $orig = isset($orig_lines[$lc-1]) ? $orig_lines[$lc-1] : ''; $n_chunk[] = array( - $lc, + $lc, $chunk[0][1]-$chunk[0][0]+$lc, $orig ); @@ -283,7 +308,7 @@ class IDF_Diff } if (!$exists) { $n_chunk[] = array( - $lc, + $lc, $lline[1]-$lline[0]+$lc, $orig_lines[$lc-1] ); @@ -305,7 +330,7 @@ class IDF_Diff foreach ($chunk as $line) { if ($line[0] > $lline[0] or empty($line[0])) { $nnew_chunks[$i-1][] = $line; - } + } } } else { $nnew_chunks[] = $chunk; diff --git a/src/IDF/Form/Admin/ProjectCreate.php b/src/IDF/Form/Admin/ProjectCreate.php index a8017b8..e2414e9 100644 --- a/src/IDF/Form/Admin/ProjectCreate.php +++ b/src/IDF/Form/Admin/ProjectCreate.php @@ -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] * @@ -234,8 +275,8 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form if ($this->cleaned_data['template'] != '--') { // Find the template project - $sql = new Pluf_SQL('shortname=%s', - array($this->cleaned_data['template'])); + $sql = new Pluf_SQL('shortname=%s', + array($this->cleaned_data['template'])); $tmpl = Pluf::factory('IDF_Project')->getOne(array('filter' => $sql->gen())); $project->private = $tmpl->private; $project->description = $tmpl->description; @@ -246,10 +287,10 @@ 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] = (!empty($this->cleaned_data[$key])) ? $this->cleaned_data[$key] : ''; $conf->setVal($key, $this->cleaned_data[$key]); } @@ -284,12 +325,13 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form } } $project->created(); + if ($this->cleaned_data['template'] == '--') { - IDF_Form_MembersConf::updateMemberships($project, + IDF_Form_MembersConf::updateMemberships($project, $this->cleaned_data); } else { // Get the membership of the template $tmpl - IDF_Form_MembersConf::updateMemberships($project, + IDF_Form_MembersConf::updateMemberships($project, $tmpl->getMembershipData('string')); } $project->membershipsUpdated(); @@ -304,7 +346,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form if ($this->cleaned_data['template'] == '--') { return $this->cleaned_data['template']; } - $sql = new Pluf_SQL('shortname=%s', array($this->cleaned_data['template'])); + $sql = new Pluf_SQL('shortname=%s', array($this->cleaned_data['template'])); if (Pluf::factory('IDF_Project')->getOne(array('filter' => $sql->gen())) == null) { throw new Pluf_Form_Invalid(__('This project is not available.')); } diff --git a/src/IDF/Form/Admin/UserCreate.php b/src/IDF/Form/Admin/UserCreate.php index f7665e4..e39ceb7 100644 --- a/src/IDF/Form/Admin/UserCreate.php +++ b/src/IDF/Form/Admin/UserCreate.php @@ -77,22 +77,20 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form 'initial' => '', 'widget' => 'Pluf_Form_Widget_SelectInput', 'widget_attrs' => array( - 'choices' => + 'choices' => Pluf_L10n::getInstalledLanguages() ), )); - $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,16 +160,11 @@ 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']); if ($last_name == mb_strtoupper($last_name)) { - return mb_convert_case(mb_strtolower($last_name), + return mb_convert_case(mb_strtolower($last_name), MB_CASE_TITLE, 'UTF-8'); } return $last_name; @@ -181,7 +174,7 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form { $first_name = trim($this->cleaned_data['first_name']); if ($first_name == mb_strtoupper($first_name)) { - return mb_convert_case(mb_strtolower($first_name), + return mb_convert_case(mb_strtolower($first_name), MB_CASE_TITLE, 'UTF-8'); } return $first_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']; + } } diff --git a/src/IDF/Form/UserAccount.php b/src/IDF/Form/UserAccount.php index 7954f7e..2c26cca 100644 --- a/src/IDF/Form/UserAccount.php +++ b/src/IDF/Form/UserAccount.php @@ -65,7 +65,7 @@ class IDF_Form_UserAccount extends Pluf_Form 'initial' => $this->user->language, 'widget' => 'Pluf_Form_Widget_SelectInput', 'widget_attrs' => array( - 'choices' => + 'choices' => Pluf_L10n::getInstalledLanguages() ), )); @@ -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,24 +257,20 @@ 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']); if ($last_name == mb_strtoupper($last_name)) { - return mb_convert_case(mb_strtolower($last_name), + return mb_convert_case(mb_strtolower($last_name), MB_CASE_TITLE, 'UTF-8'); } return $last_name; @@ -254,7 +280,7 @@ class IDF_Form_UserAccount extends Pluf_Form { $first_name = trim($this->cleaned_data['first_name']); if ($first_name == mb_strtoupper($first_name)) { - return mb_convert_case(mb_strtolower($first_name), + return mb_convert_case(mb_strtolower($first_name), MB_CASE_TITLE, 'UTF-8'); } return $first_name; @@ -264,7 +290,7 @@ class IDF_Form_UserAccount extends Pluf_Form { $this->cleaned_data['email'] = mb_strtolower(trim($this->cleaned_data['email'])); $guser = new Pluf_User(); - $sql = new Pluf_SQL('email=%s AND id!=%s', + $sql = new Pluf_SQL('email=%s AND id!=%s', array($this->cleaned_data['email'], $this->user->id)); if ($guser->getCount(array('filter' => $sql->gen())) > 0) { throw new Pluf_Form_Invalid(sprintf(__('The email "%s" is already used.'), $this->cleaned_data['email'])); @@ -272,12 +298,20 @@ 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() { - if (!isset($this->errors['password']) + if (!isset($this->errors['password']) && !isset($this->errors['password2'])) { $password1 = $this->cleaned_data['password']; $password2 = $this->cleaned_data['password2']; @@ -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; } } diff --git a/src/IDF/Key.php b/src/IDF/Key.php index 5cc80c3..0d31ba3 100644 --- a/src/IDF/Key.php +++ b/src/IDF/Key.php @@ -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 @@ -39,9 +39,9 @@ class IDF_Key extends Pluf_Model array( 'type' => 'Pluf_DB_Field_Sequence', //It is automatically added. - 'blank' => true, + 'blank' => true, ), - 'user' => + 'user' => array( 'type' => 'Pluf_DB_Field_Foreignkey', 'model' => 'Pluf_User', @@ -52,14 +52,14 @@ 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 // avoid recursion. - $t_users = $this->_con->pfx.'users'; + $t_users = $this->_con->pfx.'users'; $this->_a['views'] = array( - 'join_user' => + 'join_user' => array( 'join' => 'LEFT JOIN '.$t_users .' ON '.$t_users.'.id='.$this->_con->qn('user'), @@ -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); } - } diff --git a/src/IDF/Middleware.php b/src/IDF/Middleware.php index a4060af..cee2270 100644 --- a/src/IDF/Middleware.php +++ b/src/IDF/Middleware.php @@ -92,6 +92,7 @@ class IDF_Middleware array( 'size' => 'IDF_Views_Source_PrettySize', 'ssize' => 'IDF_Views_Source_PrettySizeSimple', + 'shorten' => 'IDF_Views_Source_ShortenString', )); } } @@ -104,12 +105,13 @@ function IDF_Middleware_ContextPreProcessor($request) $c['isAdmin'] = ($request->user->administrator or $request->user->staff); if (isset($request->project)) { $c['project'] = $request->project; - $c['isOwner'] = $request->user->hasPerm('IDF.project-owner', + $c['isOwner'] = $request->user->hasPerm('IDF.project-owner', $request->project); - $c['isMember'] = $request->user->hasPerm('IDF.project-member', + $c['isMember'] = $request->user->hasPerm('IDF.project-member', $request->project); $c = array_merge($c, $request->rights); } + $c['usherConfigured'] = Pluf::f("mtn_usher", null) !== null; return $c; } diff --git a/src/IDF/Plugin/SyncGit/Cron.php b/src/IDF/Plugin/SyncGit/Cron.php index a60b60a..cbcf8f6 100644 --- a/src/IDF/Plugin/SyncGit/Cron.php +++ b/src/IDF/Plugin/SyncGit/Cron.php @@ -48,13 +48,12 @@ 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"; } } - file_put_contents($authorized_keys, $out, LOCK_EX); + file_put_contents($authorized_keys, $out, LOCK_EX); } /** diff --git a/src/IDF/Project.php b/src/IDF/Project.php index afabd13..7532cfe 100644 --- a/src/IDF/Project.php +++ b/src/IDF/Project.php @@ -39,7 +39,7 @@ class IDF_Project extends Pluf_Model * * @see self::isRestricted */ - protected $_isRestricted = null; + protected $_isRestricted = null; function init() { @@ -52,7 +52,7 @@ class IDF_Project extends Pluf_Model 'id' => array( 'type' => 'Pluf_DB_Field_Sequence', - 'blank' => true, + 'blank' => true, ), 'name' => array( @@ -113,7 +113,7 @@ class IDF_Project extends Pluf_Model return ''; } - + function preSave($create=false) { if ($this->id == '') { @@ -181,7 +181,7 @@ class IDF_Project extends Pluf_Model */ public function getTagIdsByStatus($status='open', $cache_refresh=false) { - if (!$cache_refresh + if (!$cache_refresh and isset($this->_extra_cache['getTagIdsByStatus-'.$status])) { return $this->_extra_cache['getTagIdsByStatus-'.$status]; } @@ -197,7 +197,7 @@ class IDF_Project extends Pluf_Model break; } $tags = array(); - foreach ($this->getTagsFromConfig($key, $default, 'Status') as $tag) { + foreach ($this->getTagsFromConfig($key, $default, 'Status') as $tag) { $tags[] = (int) $tag->id; } $this->_extra_cache['getTagIdsByStatus-'.$status] = $tags; @@ -289,9 +289,9 @@ class IDF_Project extends Pluf_Model if ($fmt == 'objects') { return new Pluf_Template_ContextVars(array('members' => $members, 'owners' => $owners, 'authorized' => $authorized)); } else { - return array('members' => implode("\n", (array) $members), + return array('members' => implode("\n", (array) $members), 'owners' => implode("\n", (array) $owners), - 'authorized' => implode("\n", (array) $authorized), + 'authorized' => implode("\n", (array) $authorized), ); } } @@ -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()) + 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); } /** @@ -445,9 +450,10 @@ class IDF_Project extends Pluf_Model { $conf = $this->getConf(); $roots = array( - 'git' => 'master', - 'svn' => 'HEAD', - 'mercurial' => 'tip' + 'git' => 'master', + 'svn' => 'HEAD', + 'mercurial' => 'tip', + 'mtn' => 'h:'.$conf->getVal('mtn_master_branch', '*'), ); $scm = $conf->getVal('scm', 'git'); return $roots[$scm]; @@ -460,7 +466,7 @@ class IDF_Project extends Pluf_Model * By convention, all the objects belonging to a project have the * 'project' property set, so this is easy to check. * - * @param Pluf_Model + * @param Pluf_Model */ public function inOr404($obj) { @@ -517,7 +523,7 @@ class IDF_Project extends Pluf_Model * * [description] * - * This signal allows an application to update the statistics + * This signal allows an application to update the statistics * array of a project. For example to add the on disk size * of the repository if available. * @@ -661,7 +667,7 @@ class IDF_Project extends Pluf_Model ); $conf = $this->getConf(); foreach ($tabs as $tab) { - if (!in_array($conf->getVal($tab, 'all'), + if (!in_array($conf->getVal($tab, 'all'), array('all', 'none'))) { $this->_isRestricted = true; return true; diff --git a/src/IDF/Scm/Git.php b/src/IDF/Scm/Git.php index 5a9c2ee..77bea39 100644 --- a/src/IDF/Scm/Git.php +++ b/src/IDF/Scm/Git.php @@ -28,12 +28,12 @@ class IDF_Scm_Git extends IDF_Scm { public $mediumtree_fmt = 'commit %H%nAuthor: %an <%ae>%nTree: %T%nDate: %ai%n%n%s%n%n%b'; - + /* ============================================== * * * * Common Methods Implemented By All The SCMs * * * - * ============================================== */ + * ============================================== */ public function __construct($repo, $project=null) { @@ -48,7 +48,7 @@ class IDF_Scm_Git extends IDF_Scm } $cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk ' .escapeshellarg($this->repo); - $out = explode(' ', + $out = explode(' ', self::shell_exec('IDF_Scm_Git::getRepositorySize', $cmd), 2); return (int) $out[0]*1024; @@ -70,13 +70,13 @@ class IDF_Scm_Git extends IDF_Scm return $this->cache['branches']; } $cmd = Pluf::f('idf_exec_cmd_prefix', '') - .sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' branch', + .sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' branch', escapeshellarg($this->repo)); - self::exec('IDF_Scm_Git::getBranches', + self::exec('IDF_Scm_Git::getBranches', $cmd, $out, $return); if ($return != 0) { throw new IDF_Scm_Exception(sprintf($this->error_tpl, - $cmd, $return, + $cmd, $return, implode("\n", $out))); } $res = array(); @@ -219,7 +219,7 @@ class IDF_Scm_Git extends IDF_Scm if ($folder) { // As we are limiting to a given folder, we need to find // the tree corresponding to this folder. - $tinfo = $this->getTreeInfo($commit, $folder); + $tinfo = $this->getTreeInfo($commit, $folder); if (isset($tinfo[0]) and $tinfo[0]->type == 'tree') { $tree = $tinfo[0]->hash; } else { @@ -234,7 +234,7 @@ class IDF_Scm_Git extends IDF_Scm // information as possible. if ($file->type == 'blob') { $file->date = $co->date; - $file->log = '----'; + $file->log = '----'; $file->author = 'Unknown'; } $file->fullpath = ($folder) ? $folder.'/'.$file->file : $file->file; @@ -273,12 +273,12 @@ class IDF_Scm_Git extends IDF_Scm return null; } - public static function getAnonymousAccessUrl($project) + public static function getAnonymousAccessUrl($project, $commit=null) { return sprintf(Pluf::f('git_remote_url'), $project->shortname); } - public static function getAuthAccessUrl($project, $user) + public static function getAuthAccessUrl($project, $user, $commit=null) { return sprintf(Pluf::f('git_write_remote_url'), $project->shortname); } @@ -323,7 +323,7 @@ class IDF_Scm_Git extends IDF_Scm /** * Get the tree info. * - * @param string Tree hash + * @param string Tree hash * @param bool Do we recurse in subtrees (true) * @param string Folder in which we want to get the info ('') * @return array Array of file information. @@ -335,15 +335,15 @@ class IDF_Scm_Git extends IDF_Scm } $cmd_tmpl = 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' ls-tree -l %s %s'; $cmd = Pluf::f('idf_exec_cmd_prefix', '') - .sprintf($cmd_tmpl, escapeshellarg($this->repo), + .sprintf($cmd_tmpl, escapeshellarg($this->repo), escapeshellarg($tree), escapeshellarg($folder)); $out = array(); $res = array(); self::exec('IDF_Scm_Git::getTreeInfo', $cmd, $out); foreach ($out as $line) { list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY); - $res[] = (object) array('perm' => $perm, 'type' => $type, - 'size' => $size, 'hash' => $hash, + $res[] = (object) array('perm' => $perm, 'type' => $type, + 'size' => $size, 'hash' => $hash, 'file' => $file); } return $res; @@ -359,8 +359,8 @@ class IDF_Scm_Git extends IDF_Scm public function getPathInfo($totest, $commit='HEAD') { $cmd_tmpl = 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' ls-tree -r -t -l %s'; - $cmd = sprintf($cmd_tmpl, - escapeshellarg($this->repo), + $cmd = sprintf($cmd_tmpl, + escapeshellarg($this->repo), escapeshellarg($commit)); $out = array(); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; @@ -369,8 +369,8 @@ class IDF_Scm_Git extends IDF_Scm list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY); if ($totest == $file) { $pathinfo = pathinfo($file); - return (object) array('perm' => $perm, 'type' => $type, - 'size' => $size, 'hash' => $hash, + return (object) array('perm' => $perm, 'type' => $type, + 'size' => $size, 'hash' => $hash, 'fullpath' => $file, 'file' => $pathinfo['basename']); } @@ -382,9 +382,9 @@ class IDF_Scm_Git extends IDF_Scm { $cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', ''). 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' cat-file blob %s', - escapeshellarg($this->repo), + escapeshellarg($this->repo), escapeshellarg($def->hash)); - return ($cmd_only) + return ($cmd_only) ? $cmd : self::shell_exec('IDF_Scm_Git::getFile', $cmd); } @@ -399,13 +399,13 @@ class IDF_Scm_Git extends IDF_Scm { if ($getdiff) { $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' show --date=iso --pretty=format:%s %s', - escapeshellarg($this->repo), - "'".$this->mediumtree_fmt."'", + escapeshellarg($this->repo), + "'".$this->mediumtree_fmt."'", escapeshellarg($commit)); } else { $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log -1 --date=iso --pretty=format:%s %s', - escapeshellarg($this->repo), - "'".$this->mediumtree_fmt."'", + escapeshellarg($this->repo), + "'".$this->mediumtree_fmt."'", escapeshellarg($commit)); } $out = array(); @@ -446,8 +446,8 @@ class IDF_Scm_Git extends IDF_Scm public function isCommitLarge($commit='HEAD') { $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log --numstat -1 --pretty=format:%s %s', - escapeshellarg($this->repo), - "'commit %H%n'", + escapeshellarg($this->repo), + "'commit %H%n'", escapeshellarg($commit)); $out = array(); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; @@ -480,7 +480,7 @@ class IDF_Scm_Git extends IDF_Scm if ($n === null) $n = ''; else $n = ' -'.$n; $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log%s --date=iso --pretty=format:\'%s\' %s', - escapeshellarg($this->repo), $n, $this->mediumtree_fmt, + escapeshellarg($this->repo), $n, $this->mediumtree_fmt, escapeshellarg($commit)); $out = array(); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; @@ -686,7 +686,7 @@ class IDF_Scm_Git extends IDF_Scm /** * Build the blob info cache. * - * We build the blob info cache 500 commits at a time. + * We build the blob info cache 500 commits at a time. */ public function buildBlobInfoCache() { @@ -740,7 +740,7 @@ class IDF_Scm_Git extends IDF_Scm /** * Cache blob info. - * + * * Given a series of blob info, cache them. * * @param array Blob info @@ -768,7 +768,7 @@ class IDF_Scm_Git extends IDF_Scm foreach ($data as $rec) { if (isset($hashes[substr($rec, 0, 40)])) { $tmp = explode(chr(31), substr($rec, 40), 3); - $res[substr($rec, 0, 40)] = + $res[substr($rec, 0, 40)] = (object) array('hash' => substr($rec, 0, 40), 'date' => $tmp[0], 'title' => $tmp[2], @@ -780,7 +780,7 @@ class IDF_Scm_Git extends IDF_Scm /** * File cache blob info. - * + * * Given a series of blob info, cache them. * * @param array Blob info @@ -795,9 +795,9 @@ class IDF_Scm_Git extends IDF_Scm } $data = implode(chr(30), $data).chr(30); $cache = Pluf::f('tmp_folder').'/IDF_Scm_Git-'.md5($this->repo).'.cache.db'; - $fp = fopen($cache, 'ab'); + $fp = fopen($cache, 'ab'); if ($fp) { - flock($fp, LOCK_EX); + flock($fp, LOCK_EX); fwrite($fp, $data, strlen($data)); fclose($fp); // releases the lock too return true; diff --git a/src/IDF/Scm/Mercurial.php b/src/IDF/Scm/Mercurial.php index f0ef715..9f34ab7 100644 --- a/src/IDF/Scm/Mercurial.php +++ b/src/IDF/Scm/Mercurial.php @@ -37,9 +37,9 @@ class IDF_Scm_Mercurial extends IDF_Scm { $cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk ' .escapeshellarg($this->repo); - $out = explode(' ', + $out = explode(' ', self::shell_exec('IDF_Scm_Mercurial::getRepositorySize', - $cmd), + $cmd), 2); return (int) $out[0]*1024; } @@ -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); } @@ -109,11 +109,11 @@ class IDF_Scm_Mercurial extends IDF_Scm $cmd = sprintf(Pluf::f('hg_path', 'hg').' log -R %s -r %s', escapeshellarg($this->repo), escapeshellarg($hash)); - $ret = 0; + $ret = 0; $out = array(); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; self::exec('IDF_Scm_Mercurial::testHash', $cmd, $out, $ret); - return ($ret != 0) ? false : 'commit'; + return ($ret != 0) ? false : 'commit'; } public function getTree($commit, $folder='/', $branch=null) @@ -130,7 +130,7 @@ class IDF_Scm_Mercurial extends IDF_Scm $found = true; break; } - } + } if (!$found) { throw new Exception(sprintf(__('Folder %1$s not found in commit %2$s.'), $folder, $commit)); } @@ -142,7 +142,7 @@ class IDF_Scm_Mercurial extends IDF_Scm /** * Get the tree info. * - * @param string Tree hash + * @param string Tree hash * @param bool Do we recurse in subtrees (true) * @return array Array of file information. */ @@ -152,7 +152,7 @@ class IDF_Scm_Mercurial extends IDF_Scm throw new Exception(sprintf(__('Not a valid tree: %s.'), $tree)); } $cmd_tmpl = Pluf::f('hg_path', 'hg').' manifest -R %s --debug -r %s'; - $cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $tree, ($recurse) ? '' : ''); + $cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $tree, ($recurse) ? '' : ''); $out = array(); $res = array(); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; @@ -192,7 +192,7 @@ class IDF_Scm_Mercurial extends IDF_Scm } $fullpath = ($folder) ? $folder.'/'.$file : $file; $efullpath = self::smartEncode($fullpath); - $res[] = (object) array('perm' => $perm, 'type' => $type, + $res[] = (object) array('perm' => $perm, 'type' => $type, 'hash' => $hash, 'fullpath' => $fullpath, 'efullpath' => $efullpath, 'file' => $file); } @@ -202,7 +202,7 @@ class IDF_Scm_Mercurial extends IDF_Scm public function getPathInfo($totest, $commit='tip') { $cmd_tmpl = Pluf::f('hg_path', 'hg').' manifest -R %s --debug -r %s'; - $cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $commit); + $cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $commit); $out = array(); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; self::exec('IDF_Scm_Mercurial::getPathInfo', $cmd, $out); @@ -219,8 +219,8 @@ class IDF_Scm_Mercurial extends IDF_Scm $tmp .= $dir[$i]; if ($tmp == $totest) { $pathinfo = pathinfo($totest); - return (object) array('perm' => '000', 'type' => 'tree', - 'hash' => $hash, + return (object) array('perm' => '000', 'type' => 'tree', + 'hash' => $hash, 'fullpath' => $totest, 'file' => $pathinfo['basename'], 'commit' => $commit @@ -239,8 +239,8 @@ class IDF_Scm_Mercurial extends IDF_Scm } if ($totest == $file) { $pathinfo = pathinfo($totest); - return (object) array('perm' => $perm, 'type' => $type, - 'hash' => $hash, + return (object) array('perm' => $perm, 'type' => $type, + 'hash' => $hash, 'fullpath' => $totest, 'file' => $pathinfo['basename'], 'commit' => $commit @@ -249,15 +249,15 @@ class IDF_Scm_Mercurial extends IDF_Scm } return false; } - + public function getFile($def, $cmd_only=false) { $cmd = sprintf(Pluf::f('hg_path', 'hg').' cat -R %s -r %s %s', - escapeshellarg($this->repo), - escapeshellarg($def->commit), + escapeshellarg($this->repo), + escapeshellarg($def->commit), escapeshellarg($this->repo.'/'.$def->fullpath)); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; - return ($cmd_only) ? + return ($cmd_only) ? $cmd : self::shell_exec('IDF_Scm_Mercurial::getFile', $cmd); } @@ -272,7 +272,7 @@ class IDF_Scm_Mercurial extends IDF_Scm return $this->cache['branches']; } $out = array(); - $cmd = sprintf(Pluf::f('hg_path', 'hg').' branches -R %s', + $cmd = sprintf(Pluf::f('hg_path', 'hg').' branches -R %s', escapeshellarg($this->repo)); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; self::exec('IDF_Scm_Mercurial::getBranches', $cmd, $out); @@ -296,7 +296,7 @@ class IDF_Scm_Mercurial extends IDF_Scm return $this->cache['tags']; } $out = array(); - $cmd = sprintf(Pluf::f('hg_path', 'hg').' tags -R %s', + $cmd = sprintf(Pluf::f('hg_path', 'hg').' tags -R %s', escapeshellarg($this->repo)); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; self::exec('IDF_Scm_Mercurial::getTags', $cmd, $out); @@ -311,13 +311,13 @@ class IDF_Scm_Mercurial extends IDF_Scm public function inBranches($commit, $path) { - return (in_array($commit, array_keys($this->getBranches()))) + return (in_array($commit, array_keys($this->getBranches()))) ? array($commit) : array(); } public function inTags($commit, $path) { - return (in_array($commit, array_keys($this->getTags()))) + return (in_array($commit, array_keys($this->getTags()))) ? array($commit) : array(); } @@ -333,9 +333,9 @@ class IDF_Scm_Mercurial extends IDF_Scm if (!$this->isValidRevision($commit)) { return false; } - $tmpl = ($getdiff) ? + $tmpl = ($getdiff) ? Pluf::f('hg_path', 'hg').' log -p -r %s -R %s' : Pluf::f('hg_path', 'hg').' log -r %s -R %s'; - $cmd = sprintf($tmpl, + $cmd = sprintf($tmpl, escapeshellarg($commit), escapeshellarg($this->repo)); $out = array(); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; @@ -411,7 +411,7 @@ class IDF_Scm_Mercurial extends IDF_Scm $c['full_message'] = ''; $i=1; continue; - + } if ($i == $hdrs) { $c['title'] = trim($line); diff --git a/src/IDF/Scm/Monotone.php b/src/IDF/Scm/Monotone.php new file mode 100644 index 0000000..dd3679f --- /dev/null +++ b/src/IDF/Scm/Monotone.php @@ -0,0 +1,731 @@ + + */ +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; + } +} + diff --git a/src/IDF/Scm/Monotone/Stdio.php b/src/IDF/Scm/Monotone/Stdio.php new file mode 100644 index 0000000..391133a --- /dev/null +++ b/src/IDF/Scm/Monotone/Stdio.php @@ -0,0 +1,360 @@ + + */ +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) ? '' : $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; + } +} + diff --git a/src/IDF/Scm/Monotone/Usher.php b/src/IDF/Scm/Monotone/Usher.php new file mode 100644 index 0000000..9891555 --- /dev/null +++ b/src/IDF/Scm/Monotone/Usher.php @@ -0,0 +1,241 @@ + + */ +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; + } +} + diff --git a/src/IDF/Scm/Svn.php b/src/IDF/Scm/Svn.php index fbdcb4b..6527bbf 100644 --- a/src/IDF/Scm/Svn.php +++ b/src/IDF/Scm/Svn.php @@ -24,7 +24,7 @@ /** * Subversion backend. * When a branch is not a branch. - * + * * Contrary to most other SCMs, Subversion is using folders to manage * the branches and so what is either the commit or the branch in * other SCMs is the revision number with Subversion. So, do not be @@ -80,12 +80,13 @@ 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)) + if (false !== ($url=$conf->getVal('svn_remote_url', false)) && !empty($url)) { // Remote repository return $url; @@ -97,12 +98,13 @@ 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)) + if (false !== ($url=$conf->getVal('svn_remote_url', false)) && !empty($url)) { // Remote repository return $url; @@ -120,7 +122,7 @@ class IDF_Scm_Svn extends IDF_Scm { $conf = $project->getConf(); // Find the repository - if (false !== ($rep=$conf->getVal('svn_remote_url', false)) + if (false !== ($rep=$conf->getVal('svn_remote_url', false)) && !empty($rep)) { // Remote repository $scm = new IDF_Scm_Svn($rep, $project); @@ -268,7 +270,7 @@ class IDF_Scm_Svn extends IDF_Scm $file['type'] = $this->assoc[(string) $entry['kind']]; $pathinfo = pathinfo($filename); $file['file'] = $pathinfo['basename']; - $file['rev'] = $rev; + $file['rev'] = $rev; $file['author'] = (string) $entry->author; $file['date'] = gmdate('Y-m-d H:i:s', strtotime((string) $entry->commit->date)); $file['size'] = (string) $entry->size; @@ -284,12 +286,12 @@ class IDF_Scm_Svn extends IDF_Scm escapeshellarg($this->repo.'/'.self::smartEncode($def->fullpath)), escapeshellarg($def->rev)); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; - return ($cmd_only) ? + return ($cmd_only) ? $cmd : self::shell_exec('IDF_Scm_Svn::getFile', $cmd); } /** - * Subversion branches are folder based. + * Subversion branches are folder based. * * One need to list the folder to know them. */ @@ -328,7 +330,7 @@ class IDF_Scm_Svn extends IDF_Scm } /** - * Subversion tags are folder based. + * Subversion tags are folder based. * * One need to list the folder to know them. */ diff --git a/src/IDF/Tests/TestMonotone.php b/src/IDF/Tests/TestMonotone.php new file mode 100644 index 0000000..f151122 --- /dev/null +++ b/src/IDF/Tests/TestMonotone.php @@ -0,0 +1,239 @@ +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")); + } +} diff --git a/src/IDF/Views/Admin.php b/src/IDF/Views/Admin.php index 23085a3..2b1db96 100644 --- a/src/IDF/Views/Admin.php +++ b/src/IDF/Views/Admin.php @@ -66,7 +66,7 @@ class IDF_Views_Admin 'name' => __('Name'), array('id', 'IDF_Views_Admin_projectSize', __('Repository Size')), ); - $pag->configure($list_display, array(), + $pag->configure($list_display, array(), array('shortname')); $pag->extra_classes = array('', '', 'right'); $pag->items_per_page = 25; @@ -214,8 +214,8 @@ class IDF_Views_Admin array('last_login', 'Pluf_Paginator_DateYMDHM', __('Last Login')), ); $pag->extra_classes = array('', '', 'a-c', 'a-c', 'a-c', 'a-c'); - $pag->configure($list_display, - array('login', 'last_name', 'email'), + $pag->configure($list_display, + array('login', 'last_name', 'email'), array('login', 'last_login')); $pag->items_per_page = 50; $pag->no_results_text = __('No users were found.'); @@ -228,7 +228,7 @@ class IDF_Views_Admin ), $request); } - + /** * Not validated users. */ @@ -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) @@ -329,7 +471,7 @@ function IDF_Views_Admin_bool($field, $item) /** * Display the size of the project. * - * @param string Field + * @param string Field * @param IDF_Project * @return string */ @@ -409,8 +551,8 @@ function IDF_Views_Admin_getForgeDbSize() } switch (Pluf::f('db_engine')) { case 'PostgreSQL': - $sql = 'SELECT relname, pg_total_relation_size(CAST(relname AS -TEXT)) AS size FROM pg_class AS pgc, pg_namespace AS pgn + $sql = 'SELECT relname, pg_total_relation_size(CAST(relname AS +TEXT)) AS size FROM pg_class AS pgc, pg_namespace AS pgn WHERE pg_table_is_visible(pgc.oid) IS TRUE AND relkind = \'r\' AND pgc.relnamespace = pgn.oid AND pgn.nspname NOT IN (\'information_schema\', \'pg_catalog\')'; diff --git a/src/IDF/Views/Project.php b/src/IDF/Views/Project.php index d68b4ff..48ed153 100644 --- a/src/IDF/Views/Project.php +++ b/src/IDF/Views/Project.php @@ -44,12 +44,12 @@ class IDF_Views_Project if ($request->rights['hasDownloadsAccess']) { $tags = IDF_Views_Download::getDownloadTags($prj); // the first tag is the featured, the last is the deprecated. - $downloads = $tags[0]->get_idf_upload_list(); + $downloads = $tags[0]->get_idf_upload_list(); } $pages = array(); if ($request->rights['hasWikiAccess']) { $tags = IDF_Views_Wiki::getWikiTags($prj); - $pages = $tags[0]->get_idf_wikipage_list(); + $pages = $tags[0]->get_idf_wikipage_list(); } return Pluf_Shortcuts_RenderToResponse('idf/project/home.html', array( @@ -100,7 +100,7 @@ class IDF_Views_Project $rights[] = '\'IDF_Dummy\''; } $sql = sprintf('model_class IN (%s)', implode(', ', $rights)); - $pag->forced_where = new Pluf_SQL('project=%s AND '.$sql, + $pag->forced_where = new Pluf_SQL('project=%s AND '.$sql, array($prj->id)); $pag->sort_order = array('creation_dtime', 'ASC'); $pag->sort_reverse_order = array('creation_dtime'); @@ -117,16 +117,16 @@ class IDF_Views_Project if ($request->rights['hasDownloadsAccess']) { $tags = IDF_Views_Download::getDownloadTags($prj); // the first tag is the featured, the last is the deprecated. - $downloads = $tags[0]->get_idf_upload_list(); + $downloads = $tags[0]->get_idf_upload_list(); } $pages = array(); if ($request->rights['hasWikiAccess']) { $tags = IDF_Views_Wiki::getWikiTags($prj); - $pages = $tags[0]->get_idf_wikipage_list(); + $pages = $tags[0]->get_idf_wikipage_list(); } if (!$request->user->isAnonymous() and $prj->isRestricted()) { $feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed_auth', - array($prj->shortname, + array($prj->shortname, IDF_Precondition::genFeedToken($prj, $request->user))); } else { $feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed', @@ -188,7 +188,7 @@ class IDF_Views_Project 'nb' => 20, ); $items = Pluf::factory('IDF_Timeline')->getList($params); - $set = new Pluf_Model_Set($items, + $set = new Pluf_Model_Set($items, array('public_dtime' => 'public_dtime')); $out = array(); foreach ($set as $item) { @@ -207,7 +207,7 @@ class IDF_Views_Project $feedurl = Pluf::f('url_base').Pluf::f('idf_base').$request->query; $viewurl = Pluf_HTTP_URL_urlForView('IDF_Views_Project::timeline', array($prj->shortname)); - $context = new Pluf_Template_Context_Request($request, + $context = new Pluf_Template_Context_Request($request, array('body' => $out, 'date' => $date, 'title' => $title, @@ -235,7 +235,7 @@ class IDF_Views_Project if ($form->isValid()) { $prj = $form->save(); $request->user->setMessage(__('The project has been updated.')); - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::admin', + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::admin', array($prj->shortname)); return new Pluf_HTTP_Response_Redirect($url); } @@ -445,7 +445,7 @@ class IDF_Views_Project } else { $params = array(); $keys = array('downloads_access_rights', 'source_access_rights', - 'issues_access_rights', 'review_access_rights', + 'issues_access_rights', 'review_access_rights', 'wiki_access_rights', 'downloads_notification_email', 'review_notification_email', @@ -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', diff --git a/src/IDF/Views/Source.php b/src/IDF/Views/Source.php index 0db7387..37d9ca9 100644 --- a/src/IDF/Views/Source.php +++ b/src/IDF/Views/Source.php @@ -35,12 +35,11 @@ class IDF_Views_Source * Extension supported by the syntax highlighter. */ 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'); + 'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cc', + 'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc', + '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, @@ -416,7 +415,7 @@ class IDF_Views_Source $scm->getMainBranch())); return new Pluf_HTTP_Response_Redirect($url); } - $info = self::getRequestedFileMimeType($request_file_info, + $info = self::getRequestedFileMimeType($request_file_info, $commit, $scm); $rep = new Pluf_HTTP_Response($scm->getFile($request_file_info), $info[0]); @@ -477,7 +476,7 @@ class IDF_Views_Source public static function getMimeTypeFromContent($file, $filedata) { $info = pathinfo($file); - $res = array('application/octet-stream', + $res = array('application/octet-stream', $info['basename'], isset($info['extension']) ? $info['extension'] : 'bin'); if (function_exists('finfo_open')) { @@ -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))); +} + diff --git a/src/IDF/Views/User.php b/src/IDF/Views/User.php index fa301ec..50f1579 100644 --- a/src/IDF/Views/User.php +++ b/src/IDF/Views/User.php @@ -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); } diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index e927fce..390ce36 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -27,14 +27,14 @@ $cfg = array(); # You must set them to false once everything is running ok. # $cfg['debug'] = true; -# It will help you catch errors at beginning when configuring your +# It will help you catch errors at beginning when configuring your # SCM backend. It must be turned off in production. -$cfg['debug_scm'] = false; +$cfg['debug_scm'] = false; # -# Note: By default, InDefero will not manage the repositories for -# you, you can enable the repositories management with the -# built-in plugins. The documentation of the plugins is available +# Note: By default, InDefero will not manage the repositories for +# you, you can enable the repositories management with the +# built-in plugins. The documentation of the plugins is available # in the `doc/` folder. # @@ -44,9 +44,9 @@ $cfg['debug_scm'] = false; # For example: '/path/to/my/project/.git' # # If you have multiple repositories, you need to put %s where you -# want the shortname of the project to be replaced. +# want the shortname of the project to be replaced. # For example: -# - You have many projects on your local computer and want to use +# - You have many projects on your local computer and want to use # InDefero to see them. Put: '/home/yourlogin/Projects/%s/.git' # - You have many projects on a remote server with only "bare" git # repositories. Put: '/home/git/repositories/%s.git' @@ -64,7 +64,7 @@ $cfg['git_remote_url'] = 'git://localhost/%s.git'; $cfg['git_write_remote_url'] = 'git@localhost:%s.git'; # Same as for git, you can have multiple repositories, one for each -# project or a single one for all the projects. +# project or a single one for all the projects. # # In the case of subversion, the admin of a project can also select a # remote repository from the web interface. From the web interface @@ -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'; @@ -90,15 +174,15 @@ $cfg['mail_host'] = 'localhost'; $cfg['mail_port'] = 25; # Paths/Url configuration. -# +# # Examples: -# You have: +# You have: # http://www.mydomain.com/myfolder/index.php # Put: # $cfg['idf_base'] = '/myfolder/index.php'; # $cfg['url_base'] = 'http://www.mydomain.com'; # -# You have mod_rewrite: +# You have mod_rewrite: # http://www.mydomain.com/ # Put: # $cfg['idf_base'] = ''; @@ -109,7 +193,7 @@ $cfg['mail_port'] = 25; $cfg['idf_base'] = '/index.php'; $cfg['url_base'] = 'http://localhost'; -# Url to access the media folder which is in the www folder +# Url to access the media folder which is in the www folder # of the archive $cfg['url_media'] = 'http://localhost/media'; @@ -120,9 +204,9 @@ $cfg['url_upload'] = 'http://localhost/media/upload'; $cfg['upload_path'] = '/home/www/indefero/www/media/upload'; # -# The following path *MUST NOT* be accessible through a web browser -# as user will be able to upload .html, .php files and this can -# create *TERRIBLE* security issues. In this folder, the attachments +# The following path *MUST NOT* be accessible through a web browser +# as user will be able to upload .html, .php files and this can +# create *TERRIBLE* security issues. In this folder, the attachments # to the issues will be uploaded and we do not restrict the content type. # $cfg['upload_issue_path'] = '/home/www/indefero/attachments'; @@ -130,10 +214,10 @@ $cfg['upload_issue_path'] = '/home/www/indefero/attachments'; # # write here a long random string unique for this installation. This # is critical to put a long string, with at least 40 characters. -$cfg['secret_key'] = ''; +$cfg['secret_key'] = ''; # the sender of all the emails. -$cfg['from_email'] = 'sender@example.com'; +$cfg['from_email'] = 'sender@example.com'; # Email address for the bounced messages. $cfg['bounce_email'] = 'no-reply@example.com'; @@ -150,22 +234,22 @@ $cfg['db_password'] = ''; $cfg['db_server'] = ''; $cfg['db_version'] = '5.1'; # Only needed for MySQL # If you want to have different installations with the same DB -$cfg['db_table_prefix'] = 'indefero_'; -# ** DO NOT USE SQLITE IN PRODUCTION ** +$cfg['db_table_prefix'] = 'indefero_'; +# ** DO NOT USE SQLITE IN PRODUCTION ** # This is not because of problems with the quality of the SQLite # driver or with SQLite itself, this is due to the lack of migration # support in Pluf for SQLite, this means we cannot modify the DB # easily once it is loaded with data. $cfg['db_engine'] = 'PostgreSQL'; # SQLite is also well tested or MySQL $cfg['db_database'] = 'website'; # put absolute path to the db if you - # are using SQLite. + # are using SQLite. # # The extension of the downloads are limited. You can add extra # extensions here. The list must start with a space. # $cfg['idf_extra_upload_ext'] = ' ext1 ext2'; # # By default, the size of the downloads is limited to 2MB. -# The php.ini upload_max_filesize configuration setting will +# The php.ini upload_max_filesize configuration setting will # always have precedence. # $cfg['max_upload_size'] = 2097152; // Size in bytes @@ -203,12 +287,13 @@ $cfg['template_context_processors'] = array('IDF_Middleware_ContextPreProcessor' $cfg['idf_views'] = dirname(__FILE__).'/urls.php'; # available languages -$cfg['languages'] = array('en', 'fr'); +$cfg['languages'] = array('en', 'fr'); # SCM base configuration $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 @@ -218,8 +303,8 @@ $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git', # $cfg['idf_extra_text_ext'] = 'ext1 ext2 ext3'; # If you can execute the shell commands executed to get info -# from the scm with the user of your PHP process but it is -# not working from within PHP, this can be due to the environment +# from the scm with the user of your PHP process but it is +# not working from within PHP, this can be due to the environment # variables not being set correctly. Note the trailing space. # $cfg['idf_exec_cmd_prefix'] = '/usr/bin/env -i '; @@ -229,11 +314,11 @@ $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git', # To know which path you need to provide, just run: # $ which git # from the command line. This will give you the path to git. -# $cfg['svn_path'] = 'svn'; -# $cfg['svnlook_path'] = 'svnlook'; +# $cfg['svn_path'] = 'svn'; +# $cfg['svnlook_path'] = 'svnlook'; # $cfg['svnadmin_path'] = 'svnadmin'; # $cfg['hg_path'] = 'hg'; -# $cfg['git_path'] = 'git'; +# $cfg['git_path'] = 'git'; # If you do not want to have calculations of the repositories, attachments # and downloads size, set it to true. You can set to false some diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index f6fef8b..1d9b56f 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -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/$#', diff --git a/src/IDF/templates/idf/gadmin/base.html b/src/IDF/templates/idf/gadmin/base.html index 827b9ef..1858682 100644 --- a/src/IDF/templates/idf/gadmin/base.html +++ b/src/IDF/templates/idf/gadmin/base.html @@ -43,18 +43,21 @@
{trans 'Projects'} {trans 'People'} + {if $usherConfigured} + {trans 'Usher'} + {/if}
{block subtabs}{/block}
-

{block title}{$page_title}{/block}

+

{block title}{$page_title}{/block}

-
+
-
+
{if $user and $user.id}{getmsgs $user}{/if} -
{block body}{/block}
-
+
{block body}{/block}
+
{block context}{/block}
diff --git a/src/IDF/templates/idf/gadmin/projects/create.html b/src/IDF/templates/idf/gadmin/projects/create.html index f2765fb..1c1cddf 100644 --- a/src/IDF/templates/idf/gadmin/projects/create.html +++ b/src/IDF/templates/idf/gadmin/projects/create.html @@ -52,6 +52,13 @@ {$form.f.svn_password|unsafe} + +{$form.f.mtn_master_branch.labelTag}: +{if $form.f.mtn_master_branch.errors}{$form.f.mtn_master_branch.fieldErrors}{/if} +{$form.f.mtn_master_branch|unsafe}
+{$form.f.mtn_master_branch.help_text} + + {$form.f.template.labelTag} {if $form.f.template.errors}{$form.f.template.fieldErrors}{/if} @@ -83,7 +90,7 @@   - + @@ -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() == "--") { diff --git a/src/IDF/templates/idf/gadmin/users/create.html b/src/IDF/templates/idf/gadmin/users/create.html index 0132bed..16a00de 100644 --- a/src/IDF/templates/idf/gadmin/users/create.html +++ b/src/IDF/templates/idf/gadmin/users/create.html @@ -43,10 +43,10 @@ -{$form.f.ssh_key.labelTag}: -{if $form.f.ssh_key.errors}{$form.f.ssh_key.fieldErrors}{/if} -{$form.f.ssh_key|unsafe}
-{$form.f.ssh_key.help_text} +{$form.f.public_key.labelTag}: +{if $form.f.public_key.errors}{$form.f.public_key.fieldErrors}{/if} +{$form.f.public_key|unsafe}
+{$form.f.public_key.help_text} diff --git a/src/IDF/templates/idf/gadmin/usher/base.html b/src/IDF/templates/idf/gadmin/usher/base.html new file mode 100644 index 0000000..8f8a8c5 --- /dev/null +++ b/src/IDF/templates/idf/gadmin/usher/base.html @@ -0,0 +1,6 @@ +{extends "idf/gadmin/base.html"} +{block tabusher} class="active"{/block} +{block subtabs} +{trans 'Configured servers'} | +{trans 'Usher control'} +{/block} diff --git a/src/IDF/templates/idf/gadmin/usher/connections.html b/src/IDF/templates/idf/gadmin/usher/connections.html new file mode 100644 index 0000000..bde9e2f --- /dev/null +++ b/src/IDF/templates/idf/gadmin/usher/connections.html @@ -0,0 +1,19 @@ +{extends "idf/gadmin/usher/base.html"} + +{block docclass}yui-t3{assign $inUsherServerConnections=true}{/block} + +{block body} + + + + + +{foreach $connections as $connection} + + + + +{/foreach} +
{trans "address"}{trans "port"}
{$connection.address}{$connection.port}
+{/block} + diff --git a/src/IDF/templates/idf/gadmin/usher/control.html b/src/IDF/templates/idf/gadmin/usher/control.html new file mode 100644 index 0000000..e7d6802 --- /dev/null +++ b/src/IDF/templates/idf/gadmin/usher/control.html @@ -0,0 +1,32 @@ +{extends "idf/gadmin/usher/base.html"} + +{block docclass}yui-t3{assign $inUsherControl=true}{/block} + +{block body} +

+{trans 'current server status:'} {$status} | +{if $status == "SHUTDOWN"} + {trans 'startup'} +{else} + {trans 'shutdown'} +{/if} +

+ +

{trans 'reload server configuration:'} + {trans 'reload'} +

+{/block} + +{block context} +
+

{trans 'Status explanation'}

+
    +
  • ACTIVE n: {trans 'active with n total open connections'}
  • +
  • WAITING: {trans 'waiting for new connections'}
  • +
  • SHUTTINGDOWN: {trans 'usher is being shut down, not accepting connections'}
  • +
  • SHUTDOWN: {trans 'usher is shut down, all local servers are stopped and not accepting connections'}
  • +
+ +
+{/block} + diff --git a/src/IDF/templates/idf/gadmin/usher/index.html b/src/IDF/templates/idf/gadmin/usher/index.html new file mode 100644 index 0000000..c81c134 --- /dev/null +++ b/src/IDF/templates/idf/gadmin/usher/index.html @@ -0,0 +1,52 @@ +{extends "idf/gadmin/usher/base.html"} + +{block docclass}yui-t3{assign $inUsher=true}{/block} + +{block body} + + + + + + +{foreach $servers as $server} + + + + +{/foreach} +
{trans "server name"}{trans "status"}{trans "action"}
{$server.name}{$server.status} + {if preg_match("/ACTIVE|RUNNING|SLEEPING/", $server.status)} + + {trans 'stop'} + {elseif $server.status == "STOPPED"} + + {trans 'start'} + {/if} + {if preg_match("/ACTIVE|WAITING|SLEEPING|STOPPING/", $server.status)} + | + {trans 'kill'} + {/if} + {if preg_match("/STOPPING|ACTIVE/", $server.status)} + | + {trans 'active connections'} + {/if} +
+{/block} + +{block context} +
+

{trans 'Status explanation'}

+
    +
  • REMOTE: {trans 'remote server without open connections'}
  • +
  • ACTIVE n: {trans 'server with n open connections'}
  • +
  • WAITING: {trans 'local server running, without open connections'}
  • +
  • SLEEPING: {trans 'local server not running, waiting for connections'}
  • +
  • STOPPING n: {trans 'local server is about to stop, n connections still open'}
  • +
  • STOPPED: {trans 'local server not running, not accepting connections'}
  • +
  • SHUTDOWN: {trans 'usher is shut down, not running and not accepting connections'}
  • +
+ +
+{/block} + diff --git a/src/IDF/templates/idf/source/commit.html b/src/IDF/templates/idf/source/commit.html index a3c0f51..c9c1051 100644 --- a/src/IDF/templates/idf/source/commit.html +++ b/src/IDF/templates/idf/source/commit.html @@ -37,33 +37,6 @@ {/if} {/block} -{block context} -{if $scm != 'svn'} -

{trans 'Branches:'}
-{foreach $branches as $branch => $path} -{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)} -{$branch}
-{/foreach} -

-{if $tags} -

{trans 'Tags:'}
-{foreach $tags as $tag => $path} -{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $tag)} -{if $path}{$path}{else}{$tag}{/if}
-{/foreach} -

-{/if} -{else} -
-

{trans 'Revision:'} {$commit}

-

- - -

-
-{/if} -{/block} - {block javascript} + +{/block} diff --git a/src/IDF/templates/idf/source/mtn/help.html b/src/IDF/templates/idf/source/mtn/help.html new file mode 100644 index 0000000..6b2fd0c --- /dev/null +++ b/src/IDF/templates/idf/source/mtn/help.html @@ -0,0 +1,34 @@ +{extends "idf/source/base.html"} +{block docclass}yui-t2{assign $inHelp=true}{/block} +{block body} + +

{blocktrans}The team behind {$project} is using +the monotone software to manage the source +code.{/blocktrans}

+ +

{trans 'Command-Line Access'}

+ +

mtn clone {$project.getSourceAccessUrl()}

+ +{if $isOwner or $isMember} +

{trans 'First Commit'}

+ +

{blocktrans}To make a first commit in the repository, perform the following steps:{/blocktrans}

+ +
+mtn setup -b {$project.getConf().getVal('mtn_master_branch', 'your-branch')} .
+mtn add -R .
+mtn commit -m "initial import"
+mtn push {$project.getSourceAccessUrl()}
+
+ +{/if} + +{/block} +{block context} +
+

{blocktrans}Find here more details on how to access {$project} source code.{/blocktrans}

+
+{/block} + + diff --git a/src/IDF/templates/idf/source/mtn/tree.html b/src/IDF/templates/idf/source/mtn/tree.html new file mode 100644 index 0000000..20b7622 --- /dev/null +++ b/src/IDF/templates/idf/source/mtn/tree.html @@ -0,0 +1,80 @@ +{extends "idf/source/base.html"} +{block docclass}yui-t1{assign $inSourceTree=true}{/block} +{block body} +

{trans 'Root'}/{if $breadcrumb}{$breadcrumb|safe}{/if}

+ + + + + + + + + +{if !$tree_in and !$tags_in} +{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)} + + + +{/if} +{if $base} + + + + + +{/if} +{foreach $files as $file} +{aurl 'url', 'IDF_Views_Source::tree', array($project.shortname, $commit, $file.efullpath)} + + +{if $file.type != 'extern'} +{$file.file}{else}{/if} +{if $file.type == 'blob'} +{if isset($file.date) and $file.log != '----'} + + +{else}{/if} +{/if} +{if $file.type == 'extern'} + +{/if} + +{/foreach} + +
{trans 'File'}{trans 'Age'}{trans 'Message'}{trans 'Size'}
{blocktrans}Source at commit {$commit} created {$cobject.date|dateago}.{/blocktrans}
+{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans} +
  +..
{$file.type}{$file.file}{$file.date|dateago:"without"}{$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false}{$file.size|size}{$file.extern}
+{aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)} +

+{* {trans 'Archive'} {trans 'Download this version'} {trans 'or'} *} +mtn clone {$project.getSourceAccessUrl($user, $commit)} {trans 'Help'} +

+ + +{/block} +{block context} +

{trans 'Branches:'}
+{foreach $branches as $selector => $branch} +{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)} + + + {$branch|shorten:24} + +
+{/foreach} +

+{if $tags} +

{trans 'Tags:'}
+{foreach $tags as $selector => $tag} +{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)} + + + {$tag|shorten:24} + +
+{/foreach} +

+{/if} +{/block} diff --git a/src/IDF/templates/idf/source/svn/commit.html b/src/IDF/templates/idf/source/svn/commit.html new file mode 100644 index 0000000..830cf60 --- /dev/null +++ b/src/IDF/templates/idf/source/svn/commit.html @@ -0,0 +1,11 @@ +{extends "idf/source/commit.html"} +{block context} +
+

{trans 'Revision:'} {$commit}

+

+ + +

+
+{/block} + diff --git a/src/IDF/templates/idf/user/myaccount.html b/src/IDF/templates/idf/user/myaccount.html index 4c1e328..feda4ae 100644 --- a/src/IDF/templates/idf/user/myaccount.html +++ b/src/IDF/templates/idf/user/myaccount.html @@ -54,10 +54,10 @@ -{$form.f.ssh_key.labelTag}: -{if $form.f.ssh_key.errors}{$form.f.ssh_key.fieldErrors}{/if} -{$form.f.ssh_key|unsafe}
-{$form.f.ssh_key.help_text} +{$form.f.public_key.labelTag}: +{if $form.f.public_key.errors}{$form.f.public_key.fieldErrors}{/if} +{$form.f.public_key|unsafe}
+{$form.f.public_key.help_text} @@ -82,7 +82,7 @@ {if count($keys)} - + {foreach $keys as $key}
{trans 'Your Current SSH Keys'}
{trans 'Your Current Public Keys'}
{$key.showCompact()}