Merge branch 'mnt-support'
This commit is contained in:
		
							
								
								
									
										63
									
								
								doc/readme-monotone.mdtext
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								doc/readme-monotone.mdtext
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| # monotone implementation notes | ||||
|  | ||||
| ## general | ||||
|  | ||||
|     This version of indefero contains an implementation of the monotone  | ||||
|     automation interface. It needs at least monotone version 0.47  | ||||
|     (interface version 12.0) or newer, but as development continues, its | ||||
|     likely that this dependency has to be raised. | ||||
|      | ||||
|     To set up a new IDF project with monotone quickly, all you need to do | ||||
|     is to create a new monotone database with | ||||
|  | ||||
|         $ mtn db init -d project.mtn | ||||
|  | ||||
|     in the configured repository path `$cfg['mtn_repositories']` and | ||||
|     configure `$cfg['mtn_db_access']` to "local". | ||||
|  | ||||
|     To have a really workable setup, this database needs an initial commit | ||||
|     on the configured master branch of the project.  This can be done easily | ||||
|     with | ||||
|  | ||||
|         $ mkdir tmp && touch tmp/remove_me | ||||
|         $ mtn import -d project.mtn -b master.branch.name \ | ||||
|             -m "initial commit" tmp | ||||
|         $ rm -rf tmp | ||||
|  | ||||
|     Its expected that more scripts arrive soon to automate this and other | ||||
|     tasks in the future for (multi)forge setups. | ||||
|  | ||||
| ## current state / internals | ||||
|  | ||||
|     The implementation should be fairly stable and fast, though some | ||||
|     information, such as individual file sizes or last change information, | ||||
|     won't scale well with the tree size.  Its expected that the mtn | ||||
|     automation interface improves in this area in the future and that | ||||
|     these parts can then be rewritten with speed in mind. | ||||
|  | ||||
|     As the idf.conf-dist explains more in detail, different access patterns | ||||
|     are possible to retrieve changeset data from monotone. Please refer | ||||
|     to the documentation there for more information. | ||||
|  | ||||
| ## indefero critique: | ||||
|  | ||||
|     It was not always 100% clear what some of the abstract SCM API method | ||||
|     wanted in return.  While it helped a lot to have prior art in form of the | ||||
|     SVN and git implementation, the documentation of the abstract IDF_Scm | ||||
|     should probably still be improved. | ||||
|  | ||||
|     Since branch and tag names can be of arbitrary size, it was not possible | ||||
|     to display them completely in the default layout.  This might be a problem | ||||
|     in other SCMs as well, in particular for the monotone implementation I | ||||
|     introduced a special filter, called "IDF_Views_Source_ShortenString". | ||||
|  | ||||
|     The API methods getPathInfo() and getTree() return similar VCS "objects" | ||||
|     which unfortunately do not have a well-defined structure - this should | ||||
|     probably addressed in future indefero releases. | ||||
|  | ||||
|     While the returned objects from getTree() contain all the needed | ||||
|     information, indefero doesn't seem to use them to sort the output | ||||
|     f.e. alphabetically or in such a way that directories are outputted | ||||
|     before files.  It was unclear if the SCM implementor should do this | ||||
|     task or not and what the admired default sorting should be. | ||||
|  | ||||
| @@ -71,6 +71,25 @@ class IDF_Diff | ||||
|                 $current_chunk = 0; | ||||
|                 $indiff = true; | ||||
|                 continue; | ||||
|             } else if (0 === strpos($line, '=========')) { | ||||
|                 // by default always use the new name of a possibly renamed file | ||||
|                 $current_file = self::getMtnFile($this->lines[$i+1]); | ||||
|                 // mtn 0.48 and newer set /dev/null as file path for dropped files | ||||
|                 // so we display the old name here | ||||
|                 if ($current_file == "/dev/null") { | ||||
|                     $current_file = self::getMtnFile($this->lines[$i]); | ||||
|                 } | ||||
|                 if ($current_file == "/dev/null") { | ||||
|                     throw new Exception( | ||||
|                         "could not determine path from diff" | ||||
|                     ); | ||||
|                 } | ||||
|                 $files[$current_file] = array(); | ||||
|                 $files[$current_file]['chunks'] = array(); | ||||
|                 $files[$current_file]['chunks_def'] = array(); | ||||
|                 $current_chunk = 0; | ||||
|                 $indiff = true; | ||||
|                 continue; | ||||
|             } else if (0 === strpos($line, 'Index: ')) { | ||||
|                 $current_file = self::getSvnFile($line); | ||||
|                 $files[$current_file] = array(); | ||||
| @@ -133,6 +152,12 @@ class IDF_Diff | ||||
|         return substr(trim($line), 7); | ||||
|     } | ||||
|  | ||||
|     public static function getMtnFile($line) | ||||
|     { | ||||
|         preg_match("/^[+-]{3} ([^\t]+)/", $line, $m); | ||||
|         return $m[1]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return the html version of a parsed diff. | ||||
|      */ | ||||
|   | ||||
| @@ -38,6 +38,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form | ||||
|                          'git' => __('git'), | ||||
|                          'svn' => __('Subversion'), | ||||
|                          'mercurial' => __('mercurial'), | ||||
|                          'mtn' => __('monotone'), | ||||
|                          ); | ||||
|         foreach (Pluf::f('allowed_scm', array()) as $key => $class) { | ||||
|             $choices[$options[$key]] = $key; | ||||
| @@ -92,6 +93,13 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form | ||||
|                           'widget' => 'Pluf_Form_Widget_PasswordInput', | ||||
|                           )); | ||||
|  | ||||
|         $this->fields['mtn_master_branch'] = new Pluf_Form_Field_Varchar( | ||||
|                     array('required' => false, | ||||
|                           'label' => __('Master branch'), | ||||
|                           'initial' => '', | ||||
|                           'help_text' => __('This should be a world-wide unique identifier for your project. A reverse DNS notation like "com.my-domain.my-project" is a good idea.'), | ||||
|                           )); | ||||
|  | ||||
|         $this->fields['owners'] = new Pluf_Form_Field_Varchar( | ||||
|                                       array('required' => false, | ||||
|                                             'label' => __('Project owners'), | ||||
| @@ -170,6 +178,34 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form | ||||
|         return $url; | ||||
|     } | ||||
|  | ||||
|     public function clean_mtn_master_branch() | ||||
|     { | ||||
|         // do not validate, but empty the field if a different | ||||
|         // SCM should be used | ||||
|         if ($this->cleaned_data['scm'] != 'mtn') | ||||
|             return ''; | ||||
|  | ||||
|         $mtn_master_branch = mb_strtolower($this->cleaned_data['mtn_master_branch']); | ||||
|         if (!preg_match('/^([\w\d]+([-][\w\d]+)*)(\.[\w\d]+([-][\w\d]+)*)*$/', | ||||
|                         $mtn_master_branch)) { | ||||
|             throw new Pluf_Form_Invalid(__( | ||||
|                 'The master branch is empty or contains illegal characters, '. | ||||
|                 'please use only letters, digits, dashs and dots as separators.' | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         $sql = new Pluf_SQL('vkey=%s AND vdesc=%s', | ||||
|                             array("mtn_master_branch", $mtn_master_branch)); | ||||
|         $l = Pluf::factory('IDF_Conf')->getList(array('filter'=>$sql->gen())); | ||||
|         if ($l->count() > 0) { | ||||
|             throw new Pluf_Form_Invalid(__( | ||||
|                 'This master branch is already used. Please select another one.' | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         return $mtn_master_branch; | ||||
|     } | ||||
|  | ||||
|     public function clean_shortname() | ||||
|     { | ||||
|         $shortname = mb_strtolower($this->cleaned_data['shortname']); | ||||
| @@ -198,6 +234,11 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form | ||||
|                 $this->cleaned_data[$key] = ''; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if ($this->cleaned_data['scm'] != 'mtn') { | ||||
|             $this->cleaned_data['mtn_master_branch'] = ''; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * [signal] | ||||
|          * | ||||
| @@ -246,8 +287,8 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form | ||||
|         $project->create(); | ||||
|         $conf = new IDF_Conf(); | ||||
|         $conf->setProject($project); | ||||
|         $keys = array('scm', 'svn_remote_url',  | ||||
|                       'svn_username', 'svn_password'); | ||||
|         $keys = array('scm', 'svn_remote_url', 'svn_username', | ||||
|                       'svn_password', 'mtn_master_branch'); | ||||
|         foreach ($keys as $key) { | ||||
|             $this->cleaned_data[$key] = (!empty($this->cleaned_data[$key])) ? | ||||
|                 $this->cleaned_data[$key] : ''; | ||||
| @@ -284,6 +325,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form | ||||
|             } | ||||
|         } | ||||
|         $project->created(); | ||||
|  | ||||
|         if ($this->cleaned_data['template'] == '--') { | ||||
|             IDF_Form_MembersConf::updateMemberships($project, | ||||
|                                                     $this->cleaned_data); | ||||
|   | ||||
| @@ -82,17 +82,15 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form | ||||
|                                                                     ), | ||||
|                                             )); | ||||
|  | ||||
|         $this->fields['ssh_key'] = new Pluf_Form_Field_Varchar( | ||||
|         $this->fields['public_key'] = new Pluf_Form_Field_Varchar( | ||||
|                                       array('required' => false, | ||||
|                                             'label' => __('Add a public SSH key'), | ||||
|                                             'label' => __('Add a public key'), | ||||
|                                             'initial' => '', | ||||
|                                             'widget_attrs' => array('rows' => 3, | ||||
|                                                                     'cols' => 40), | ||||
|                                             'widget' => 'Pluf_Form_Widget_TextareaInput', | ||||
|                                             'help_text' => __('Be careful to provide the public key and not the private key!') | ||||
|                                             'help_text' => __('Paste a SSH or monotone public key. Be careful to not provide your private key here!') | ||||
|                                             )); | ||||
|          | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -137,11 +135,11 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form | ||||
|         $params = array('user' => $user); | ||||
|         Pluf_Signal::send('Pluf_User::passwordUpdated', | ||||
|                           'IDF_Form_Admin_UserCreate', $params); | ||||
|         // Create the ssh key as needed | ||||
|         if ('' !== $this->cleaned_data['ssh_key']) { | ||||
|         // Create the public key as needed | ||||
|         if ('' !== $this->cleaned_data['public_key']) { | ||||
|             $key = new IDF_Key(); | ||||
|             $key->user = $user; | ||||
|             $key->content = $this->cleaned_data['ssh_key']; | ||||
|             $key->content = $this->cleaned_data['public_key']; | ||||
|             $key->create(); | ||||
|         } | ||||
|         // Send an email to the user with the password | ||||
| @@ -162,11 +160,6 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form | ||||
|         return $user; | ||||
|     } | ||||
|  | ||||
|     function clean_ssh_key() | ||||
|     { | ||||
|         return IDF_Form_UserAccount::checkSshKey($this->cleaned_data['ssh_key']); | ||||
|     } | ||||
|  | ||||
|     function clean_last_name() | ||||
|     { | ||||
|         $last_name = trim($this->cleaned_data['last_name']); | ||||
| @@ -211,4 +204,12 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form | ||||
|         } | ||||
|         return $this->cleaned_data['login']; | ||||
|     } | ||||
|  | ||||
|     public function clean_public_key() | ||||
|     { | ||||
|         $this->cleaned_data['public_key'] = | ||||
|             IDF_Form_UserAccount::checkPublicKey($this->cleaned_data['public_key']); | ||||
|  | ||||
|         return $this->cleaned_data['public_key']; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -92,17 +92,15 @@ class IDF_Form_UserAccount  extends Pluf_Form | ||||
|                                                                     ), | ||||
|                                             )); | ||||
|  | ||||
|         $this->fields['ssh_key'] = new Pluf_Form_Field_Varchar( | ||||
|         $this->fields['public_key'] = new Pluf_Form_Field_Varchar( | ||||
|                                       array('required' => false, | ||||
|                                             'label' => __('Add a public SSH key'), | ||||
|                                             'label' => __('Add a public key'), | ||||
|                                             'initial' => '', | ||||
|                                             'widget_attrs' => array('rows' => 3, | ||||
|                                                                     'cols' => 40), | ||||
|                                             'widget' => 'Pluf_Form_Widget_TextareaInput', | ||||
|                                             'help_text' => __('Be careful to provide your public key and not your private key!') | ||||
|                                             'help_text' => __('Paste a SSH or monotone public key. Be careful to not provide your private key here!') | ||||
|                                             )); | ||||
|          | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -151,10 +149,10 @@ class IDF_Form_UserAccount  extends Pluf_Form | ||||
|         } | ||||
|         $this->user->setFromFormData($this->cleaned_data); | ||||
|         // Add key as needed. | ||||
|         if ('' !== $this->cleaned_data['ssh_key']) { | ||||
|         if ('' !== $this->cleaned_data['public_key']) { | ||||
|             $key = new IDF_Key(); | ||||
|             $key->user = $this->user; | ||||
|             $key->content = $this->cleaned_data['ssh_key']; | ||||
|             $key->content = $this->cleaned_data['public_key']; | ||||
|             if ($commit) { | ||||
|                 $key->create(); | ||||
|             } | ||||
| @@ -190,7 +188,7 @@ class IDF_Form_UserAccount  extends Pluf_Form | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check an ssh key. | ||||
|      * Check arbitrary public keys. | ||||
|      * | ||||
|      * It will throw a Pluf_Form_Invalid exception if it cannot | ||||
|      * validate the key. | ||||
| @@ -199,27 +197,59 @@ class IDF_Form_UserAccount  extends Pluf_Form | ||||
|      * @param $user int The user id of the user of the key (0) | ||||
|      * @return string The clean key | ||||
|      */ | ||||
|     public static function checkSshKey($key, $user=0) | ||||
|     public static function checkPublicKey($key, $user=0) | ||||
|     { | ||||
|         $key = trim($key); | ||||
|         if (strlen($key) == 0) { | ||||
|             return ''; | ||||
|         } | ||||
|         $key = str_replace(array("\n", "\r"), '', $key); | ||||
|         if (!preg_match('#^ssh\-[a-z]{3}\s(\S+)\s\S+$#', $key, $matches)) { | ||||
|             throw new Pluf_Form_Invalid(__('The format of the key is not valid. It must start with ssh-dss or ssh-rsa, a long string on a single line and at the end a comment.')); | ||||
|         } | ||||
|         if (Pluf::f('idf_strong_key_check', false)) { | ||||
|             $tmpfile = Pluf::f('tmp_folder', '/tmp').'/'.$user.'-key'; | ||||
|             file_put_contents($tmpfile, $key, LOCK_EX); | ||||
|             $cmd = Pluf::f('idf_exec_cmd_prefix', ''). | ||||
|                 'ssh-keygen -l -f '.escapeshellarg($tmpfile).' > /dev/null 2>&1'; | ||||
|             exec($cmd, $out, $return); | ||||
|             unlink($tmpfile); | ||||
|             if ($return != 0) { | ||||
|                 throw new Pluf_Form_Invalid(__('Please check the key as it does not appears to be a valid key.')); | ||||
|  | ||||
|         if (preg_match('#^ssh\-[a-z]{3}\s\S+\s\S+$#', $key)) { | ||||
|             $key = str_replace(array("\n", "\r"), '', $key); | ||||
|  | ||||
|             if (Pluf::f('idf_strong_key_check', false)) { | ||||
|  | ||||
|                 $tmpfile = Pluf::f('tmp_folder', '/tmp').'/'.$user.'-key'; | ||||
|                 file_put_contents($tmpfile, $key, LOCK_EX); | ||||
|                 $cmd = Pluf::f('idf_exec_cmd_prefix', ''). | ||||
|                     'ssh-keygen -l -f '.escapeshellarg($tmpfile).' > /dev/null 2>&1'; | ||||
|                 exec($cmd, $out, $return); | ||||
|                 unlink($tmpfile); | ||||
|  | ||||
|                 if ($return != 0) { | ||||
|                     throw new Pluf_Form_Invalid( | ||||
|                         __('Please check the key as it does not appear '. | ||||
|                            'to be a valid SSH public key.') | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else if (preg_match('#^\[pubkey [^\]]+\]\s*\S+\s*\[end\]$#', $key)) { | ||||
|             if (Pluf::f('idf_strong_key_check', false)) { | ||||
|  | ||||
|                 // if monotone can read it, it should be valid | ||||
|                 $mtn_opts = implode(' ', Pluf::f('mtn_opts', array())); | ||||
|                 $cmd = Pluf::f('idf_exec_cmd_prefix', ''). | ||||
|                     sprintf('%s %s -d :memory: read >/tmp/php-out 2>&1', | ||||
|                             Pluf::f('mtn_path', 'mtn'), $mtn_opts); | ||||
|                 $fp = popen($cmd, 'w'); | ||||
|                 fwrite($fp, $key); | ||||
|                 $return = pclose($fp); | ||||
|  | ||||
|                 if ($return != 0) { | ||||
|                        throw new Pluf_Form_Invalid( | ||||
|                         __('Please check the key as it does not appear '. | ||||
|                            'to be a valid monotone public key.') | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             throw new Pluf_Form_Invalid( | ||||
|                 __('Public key looks neither like a SSH '. | ||||
|                    'nor monotone public key.')); | ||||
|         } | ||||
|  | ||||
|         // If $user, then check if not the same key stored | ||||
|         if ($user) { | ||||
|             $ruser = Pluf::factory('Pluf_User', $user); | ||||
| @@ -227,19 +257,15 @@ class IDF_Form_UserAccount  extends Pluf_Form | ||||
|                 $sql = new Pluf_SQL('content=%s', array($key)); | ||||
|                 $keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen())); | ||||
|                 if (count($keys) > 0) { | ||||
|                     throw new Pluf_Form_Invalid(__('You already have uploaded this SSH key.')); | ||||
|                     throw new Pluf_Form_Invalid( | ||||
|                         __('You already have uploaded this key.') | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return $key; | ||||
|     } | ||||
|  | ||||
|     function clean_ssh_key() | ||||
|     { | ||||
|         return self::checkSshKey($this->cleaned_data['ssh_key'],  | ||||
|                                  $this->user->id); | ||||
|     } | ||||
|  | ||||
|     function clean_last_name() | ||||
|     { | ||||
|         $last_name = trim($this->cleaned_data['last_name']); | ||||
| @@ -272,8 +298,16 @@ class IDF_Form_UserAccount  extends Pluf_Form | ||||
|         return $this->cleaned_data['email']; | ||||
|     } | ||||
|  | ||||
|     function clean_public_key() | ||||
|     { | ||||
|         $this->cleaned_data['public_key'] = | ||||
|                 self::checkPublicKey($this->cleaned_data['public_key'], | ||||
|                                      $this->user->id); | ||||
|         return $this->cleaned_data['public_key']; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check to see if the 2 passwords are the same. | ||||
|      * Check to see if the 2 passwords are the same | ||||
|      */ | ||||
|     public function clean() | ||||
|     { | ||||
| @@ -285,6 +319,7 @@ class IDF_Form_UserAccount  extends Pluf_Form | ||||
|                 throw new Pluf_Form_Invalid(__('The passwords do not match. Please give them again.')); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return $this->cleaned_data; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
| # ***** END LICENSE BLOCK ***** */ | ||||
|  | ||||
| /** | ||||
|  * Storage of the SSH keys. | ||||
|  * Storage of the public keys (ssh or monotone). | ||||
|  * | ||||
|  */ | ||||
| class IDF_Key extends Pluf_Model | ||||
| @@ -52,7 +52,7 @@ class IDF_Key extends Pluf_Model | ||||
|                             array( | ||||
|                                   'type' => 'Pluf_DB_Field_Text', | ||||
|                                   'blank' => false, | ||||
|                                   'verbose' => __('ssh key'), | ||||
|                                   'verbose' => __('public key'), | ||||
|                                   ), | ||||
|                             ); | ||||
|         // WARNING: Not using getSqlTable on the Pluf_User object to | ||||
| @@ -75,6 +75,58 @@ class IDF_Key extends Pluf_Model | ||||
|         return Pluf_Template::markSafe(Pluf_esc(substr($this->content, 0, 25)).' [...] '.Pluf_esc(substr($this->content, -55))); | ||||
|     } | ||||
|  | ||||
|     private function parseContent() | ||||
|     { | ||||
|         if (preg_match('#^\[pubkey ([^\]]+)\]\s*(\S+)\s*\[end\]$#', $this->content, $m)) { | ||||
|             return array('mtn', $m[1], $m[2]); | ||||
|         } | ||||
|         else if (preg_match('#^ssh\-[a-z]{3}\s(\S+)\s(\S+)$#', $this->content, $m)) { | ||||
|             return array('ssh', $m[2], $m[1]); | ||||
|         } | ||||
|  | ||||
|         throw new IDF_Exception('invalid or unknown key data detected'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the type of the public key | ||||
|      * | ||||
|      * @return string 'ssh' or 'mtn' | ||||
|      */ | ||||
|     function getType() | ||||
|     { | ||||
|         list($type, , ) = $this->parseContent(); | ||||
|         return $type; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the key name of the key | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     function getName() | ||||
|     { | ||||
|         list(, $keyName, ) = $this->parseContent(); | ||||
|         return $keyName; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This function should be used to calculate the key id from the | ||||
|      * public key hash for authentication purposes. This avoids clashes | ||||
|      * in case the key name is not unique across the project | ||||
|      * | ||||
|      * And yes, this is actually how monotone itself calculates the key | ||||
|      * id... | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     function getMtnId() | ||||
|     { | ||||
|         list($type, $keyName, $keyData) = $this->parseContent(); | ||||
|         if ($type != 'mtn') | ||||
|             throw new IDF_Exception('key is not a monotone public key'); | ||||
|         return sha1($keyName.":".$keyData); | ||||
|     } | ||||
|  | ||||
|     function postSave($create=false) | ||||
|     { | ||||
|         /** | ||||
| @@ -89,7 +141,7 @@ class IDF_Key extends Pluf_Model | ||||
|          * [description] | ||||
|          * | ||||
|          * This signal allows an application to perform special | ||||
|          * operations after the saving of a SSH Key. | ||||
|          * operations after the saving of a public Key. | ||||
|          * | ||||
|          * [parameters] | ||||
|          * | ||||
| @@ -127,5 +179,4 @@ class IDF_Key extends Pluf_Model | ||||
|         Pluf_Signal::send('IDF_Key::preDelete', | ||||
|                           'IDF_Key', $params); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -92,6 +92,7 @@ class IDF_Middleware | ||||
|                                            array( | ||||
|                                    'size' => 'IDF_Views_Source_PrettySize', | ||||
|                                    'ssize' => 'IDF_Views_Source_PrettySizeSimple', | ||||
|                                    'shorten' => 'IDF_Views_Source_ShortenString', | ||||
|                                                  )); | ||||
|     } | ||||
| } | ||||
| @@ -110,6 +111,7 @@ function IDF_Middleware_ContextPreProcessor($request) | ||||
|                                                  $request->project); | ||||
|         $c = array_merge($c, $request->rights); | ||||
|     } | ||||
|     $c['usherConfigured'] = Pluf::f("mtn_usher", null) !== null; | ||||
|     return $c; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -48,8 +48,7 @@ class IDF_Plugin_SyncGit_Cron | ||||
|         $out = ''; | ||||
|         $keys = Pluf::factory('IDF_Key')->getList(array('view'=>'join_user')); | ||||
|         foreach ($keys as $key) { | ||||
|             if (strlen($key->content) > 40 // minimal check | ||||
|                 and preg_match('/^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$/', $key->login)) { | ||||
|             if ($key->getType() == 'ssh' and preg_match('/^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$/', $key->login)) { | ||||
|                 $content = trim(str_replace(array("\n", "\r"), '', $key->content)); | ||||
|                 $out .= sprintf($template, $cmd, $key->login, $content)."\n"; | ||||
|             } | ||||
|   | ||||
| @@ -382,15 +382,16 @@ class IDF_Project extends Pluf_Model | ||||
|      * This will return the right url based on the user. | ||||
|      * | ||||
|      * @param Pluf_User The user (null) | ||||
|      * @param string    A specific commit to access | ||||
|      */ | ||||
|     public function getSourceAccessUrl($user=null) | ||||
|     public function getSourceAccessUrl($user=null, $commit=null) | ||||
|     { | ||||
|         $right = $this->getConf()->getVal('source_access_rights', 'all'); | ||||
|         if (($user == null or $user->isAnonymous()) | ||||
|             and  $right == 'all' and !$this->private) { | ||||
|             return $this->getRemoteAccessUrl(); | ||||
|             return $this->getRemoteAccessUrl($commit); | ||||
|         } | ||||
|         return $this->getWriteRemoteAccessUrl($user); | ||||
|         return $this->getWriteRemoteAccessUrl($user, $commit); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -398,15 +399,17 @@ class IDF_Project extends Pluf_Model | ||||
|      * Get the remote access url to the repository. | ||||
|      * | ||||
|      * This will always return the anonymous access url. | ||||
|      * | ||||
|      * @param string    A specific commit to access | ||||
|      */ | ||||
|     public function getRemoteAccessUrl() | ||||
|     public function getRemoteAccessUrl($commit=null) | ||||
|     { | ||||
|         $conf = $this->getConf(); | ||||
|         $scm = $conf->getVal('scm', 'git'); | ||||
|         $scms = Pluf::f('allowed_scm'); | ||||
|         Pluf::loadClass($scms[$scm]); | ||||
|         return call_user_func(array($scms[$scm], 'getAnonymousAccessUrl'), | ||||
|                               $this); | ||||
|                               $this, $commit); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -415,14 +418,16 @@ class IDF_Project extends Pluf_Model | ||||
|      * Some SCM have a remote access URL to write which is not the | ||||
|      * same as the one to read. For example, you do a checkout with | ||||
|      * git-daemon and push with SSH. | ||||
|      * | ||||
|      * @param string    A specific commit to access | ||||
|      */ | ||||
|     public function getWriteRemoteAccessUrl($user) | ||||
|     public function getWriteRemoteAccessUrl($user,$commit=null) | ||||
|     { | ||||
|         $conf = $this->getConf(); | ||||
|         $scm = $conf->getVal('scm', 'git'); | ||||
|         $scms = Pluf::f('allowed_scm'); | ||||
|         return call_user_func(array($scms[$scm], 'getAuthAccessUrl'), | ||||
|                               $this, $user); | ||||
|                               $this, $user, $commit); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -447,7 +452,8 @@ class IDF_Project extends Pluf_Model | ||||
|         $roots = array( | ||||
|                        'git' => 'master', | ||||
|                        'svn' => 'HEAD', | ||||
|                        'mercurial' => 'tip' | ||||
|                        'mercurial' => 'tip', | ||||
|                        'mtn' => 'h:'.$conf->getVal('mtn_master_branch', '*'), | ||||
|                        ); | ||||
|         $scm = $conf->getVal('scm', 'git'); | ||||
|         return $roots[$scm]; | ||||
|   | ||||
| @@ -273,12 +273,12 @@ class IDF_Scm_Git extends IDF_Scm | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     public static function getAnonymousAccessUrl($project) | ||||
|     public static function getAnonymousAccessUrl($project, $commit=null) | ||||
|     { | ||||
|         return sprintf(Pluf::f('git_remote_url'), $project->shortname); | ||||
|     } | ||||
|  | ||||
|     public static function getAuthAccessUrl($project, $user) | ||||
|     public static function getAuthAccessUrl($project, $user, $commit=null) | ||||
|     { | ||||
|         return sprintf(Pluf::f('git_write_remote_url'), $project->shortname); | ||||
|     } | ||||
|   | ||||
| @@ -77,12 +77,12 @@ class IDF_Scm_Mercurial extends IDF_Scm | ||||
|         return 'tip'; | ||||
|     } | ||||
|  | ||||
|     public static function getAnonymousAccessUrl($project) | ||||
|     public static function getAnonymousAccessUrl($project, $commit=null) | ||||
|     { | ||||
|         return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname); | ||||
|     } | ||||
|  | ||||
|     public static function getAuthAccessUrl($project, $user) | ||||
|     public static function getAuthAccessUrl($project, $user, $commit=null) | ||||
|     { | ||||
|         return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										731
									
								
								src/IDF/Scm/Monotone.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										731
									
								
								src/IDF/Scm/Monotone.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,731 @@ | ||||
| <?php | ||||
| /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ | ||||
| /* | ||||
| # ***** BEGIN LICENSE BLOCK ***** | ||||
| # This file is part of InDefero, an open source project management application. | ||||
| # Copyright (C) 2010 Céondo Ltd and contributors. | ||||
| # | ||||
| # InDefero is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 2 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # InDefero is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program; if not, write to the Free Software | ||||
| # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA | ||||
| # | ||||
| # ***** END LICENSE BLOCK ***** */ | ||||
|  | ||||
| require_once(dirname(__FILE__) . "/Monotone/Stdio.php"); | ||||
|  | ||||
| /** | ||||
|  * Monotone scm class | ||||
|  * | ||||
|  * @author Thomas Keller <me@thomaskeller.biz> | ||||
|  */ | ||||
| class IDF_Scm_Monotone extends IDF_Scm | ||||
| { | ||||
|     /** the minimum supported interface version */ | ||||
|     public static $MIN_INTERFACE_VERSION = 12.0; | ||||
|  | ||||
|     private $stdio; | ||||
|  | ||||
|     private static $instances = array(); | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::__construct() | ||||
|      */ | ||||
|     public function __construct($project) | ||||
|     { | ||||
|         $this->project = $project; | ||||
|         $this->stdio = new IDF_Scm_Monotone_Stdio($project); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getRepositorySize() | ||||
|      */ | ||||
|     public function getRepositorySize() | ||||
|     { | ||||
|         // FIXME: this obviously won't work with remote databases - upstream | ||||
|         // needs to implement mtn db info in automate at first | ||||
|         $repo = sprintf(Pluf::f('mtn_repositories'), $this->project->shortname); | ||||
|         if (!file_exists($repo)) { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         $cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk ' | ||||
|             .escapeshellarg($repo); | ||||
|         $out = explode(' ', | ||||
|                        self::shell_exec('IDF_Scm_Monotone::getRepositorySize', $cmd), | ||||
|                        2); | ||||
|         return (int) $out[0]*1024; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::isAvailable() | ||||
|      */ | ||||
|     public function isAvailable() | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             $out = $this->stdio->exec(array('interface_version')); | ||||
|             return floatval($out) >= self::$MIN_INTERFACE_VERSION; | ||||
|         } | ||||
|         catch (IDF_Scm_Exception $e) {} | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getBranches() | ||||
|      */ | ||||
|     public function getBranches() | ||||
|     { | ||||
|         if (isset($this->cache['branches'])) { | ||||
|             return $this->cache['branches']; | ||||
|         } | ||||
|         // FIXME: we could / should introduce handling of suspended | ||||
|         // (i.e. dead) branches here by hiding them from the user's eye... | ||||
|         $out = $this->stdio->exec(array('branches')); | ||||
|  | ||||
|         // note: we could expand each branch with one of its head revisions | ||||
|         // here, but these would soon become bogus anyway and we cannot | ||||
|         // map multiple head revisions here either, so we just use the | ||||
|         // selector as placeholder | ||||
|         $res = array(); | ||||
|         foreach (preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY) as $b) { | ||||
|             $res["h:$b"] = $b; | ||||
|         } | ||||
|  | ||||
|         $this->cache['branches'] = $res; | ||||
|         return $res; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * monotone has no concept of a "main" branch, so just return | ||||
|      * the configured one. Ensure however that we can select revisions | ||||
|      * with it at all. | ||||
|      * | ||||
|      * @see IDF_Scm::getMainBranch() | ||||
|      */ | ||||
|     public function getMainBranch() | ||||
|     { | ||||
|         $conf = $this->project->getConf(); | ||||
|         if (false === ($branch = $conf->getVal('mtn_master_branch', false)) | ||||
|             || empty($branch)) { | ||||
|             $branch = "*"; | ||||
|         } | ||||
|  | ||||
|         if (count($this->_resolveSelector("h:$branch")) == 0) { | ||||
|             throw new IDF_Scm_Exception( | ||||
|                 "Branch $branch is empty" | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return $branch; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * expands a selector or a partial revision id to zero, one or | ||||
|      * multiple 40 byte revision ids | ||||
|      * | ||||
|      * @param string $selector | ||||
|      * @return array | ||||
|      */ | ||||
|     private function _resolveSelector($selector) | ||||
|     { | ||||
|         $out = $this->stdio->exec(array('select', $selector)); | ||||
|         return preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parses monotone's basic_io format | ||||
|      * | ||||
|      * @param string $in | ||||
|      * @return array of arrays | ||||
|      */ | ||||
|     private static function _parseBasicIO($in) | ||||
|     { | ||||
|         $pos = 0; | ||||
|         $stanzas = array(); | ||||
|  | ||||
|         while ($pos < strlen($in)) { | ||||
|             $stanza = array(); | ||||
|             while ($pos < strlen($in)) { | ||||
|                 if ($in[$pos] == "\n") break; | ||||
|  | ||||
|                 $stanzaLine = array('key' => '', 'values' => array(), 'hash' => null); | ||||
|                 while ($pos < strlen($in)) { | ||||
|                     $ch = $in[$pos]; | ||||
|                     if ($ch == '"' || $ch == '[') break; | ||||
|                     ++$pos; | ||||
|                     if ($ch == ' ') continue; | ||||
|                     $stanzaLine['key'] .= $ch; | ||||
|                 } | ||||
|  | ||||
|                 if ($in[$pos] == '[') { | ||||
|                     ++$pos; // opening square bracket | ||||
|                     $stanzaLine['hash'] = substr($in, $pos, 40); | ||||
|                     $pos += 40; | ||||
|                     ++$pos; // closing square bracket | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     $valCount = 0; | ||||
|                     while ($in[$pos] == '"') { | ||||
|                         ++$pos; // opening quote | ||||
|                         $stanzaLine['values'][$valCount] = ''; | ||||
|                         while ($pos < strlen($in)) { | ||||
|                             $ch = $in[$pos]; $pr = $in[$pos-1]; | ||||
|                             if ($ch == '"' && $pr != '\\') break; | ||||
|                             ++$pos; | ||||
|                             $stanzaLine['values'][$valCount] .= $ch; | ||||
|                         } | ||||
|                         ++$pos; // closing quote | ||||
|  | ||||
|                         if ($in[$pos] == ' ') { | ||||
|                             ++$pos; // space | ||||
|                             ++$valCount; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     for ($i = 0; $i <= $valCount; $i++) { | ||||
|                         $stanzaLine['values'][$i] = str_replace( | ||||
|                             array("\\\\", "\\\""), | ||||
|                             array("\\", "\""), | ||||
|                             $stanzaLine['values'][$i] | ||||
|                         ); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 $stanza[] = $stanzaLine; | ||||
|                 ++$pos; // newline | ||||
|             } | ||||
|             $stanzas[] = $stanza; | ||||
|             ++$pos; // newline | ||||
|         } | ||||
|         return $stanzas; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Queries the certs for a given revision and returns them in an | ||||
|      * associative array array("branch" => array("branch1", ...), ...) | ||||
|      * | ||||
|      * @param string | ||||
|      * @param array | ||||
|      */ | ||||
|     private function _getCerts($rev) | ||||
|     { | ||||
|         static $certCache = array(); | ||||
|  | ||||
|         if (!array_key_exists($rev, $certCache)) { | ||||
|             $out = $this->stdio->exec(array('certs', $rev)); | ||||
|  | ||||
|             $stanzas = self::_parseBasicIO($out); | ||||
|             $certs = array(); | ||||
|             foreach ($stanzas as $stanza) { | ||||
|                 $certname = null; | ||||
|                 foreach ($stanza as $stanzaline) { | ||||
|                     // luckily, name always comes before value | ||||
|                     if ($stanzaline['key'] == 'name') { | ||||
|                         $certname = $stanzaline['values'][0]; | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     if ($stanzaline['key'] == 'value') { | ||||
|                         if (!array_key_exists($certname, $certs)) { | ||||
|                             $certs[$certname] = array(); | ||||
|                         } | ||||
|  | ||||
|                         $certs[$certname][] = $stanzaline['values'][0]; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             $certCache[$rev] = $certs; | ||||
|         } | ||||
|  | ||||
|         return $certCache[$rev]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns unique certificate values for the given revs and the specific | ||||
|      * cert name, optionally prefixed with $prefix | ||||
|      * | ||||
|      * @param array | ||||
|      * @param string | ||||
|      * @param string | ||||
|      * @return array | ||||
|      */ | ||||
|     private function _getUniqueCertValuesFor($revs, $certName, $prefix) | ||||
|     { | ||||
|         $certValues = array(); | ||||
|         foreach ($revs as $rev) { | ||||
|             $certs = $this->_getCerts($rev); | ||||
|             if (!array_key_exists($certName, $certs)) | ||||
|                 continue; | ||||
|             foreach ($certs[$certName] as $certValue) { | ||||
|                 $certValues[] = "$prefix$certValue"; | ||||
|             } | ||||
|         } | ||||
|         return array_unique($certValues); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the revision in which the file has been last changed, | ||||
|      * starting from the start rev | ||||
|      * | ||||
|      * @param string | ||||
|      * @param string | ||||
|      * @return string | ||||
|      */ | ||||
|     private function _getLastChangeFor($file, $startrev) | ||||
|     { | ||||
|         $out = $this->stdio->exec(array( | ||||
|             'get_content_changed', $startrev, $file | ||||
|         )); | ||||
|  | ||||
|         $stanzas = self::_parseBasicIO($out); | ||||
|  | ||||
|         // FIXME: we only care about the first returned content mark | ||||
|         // everything else seem to be very, very rare cases | ||||
|         foreach ($stanzas as $stanza) { | ||||
|             foreach ($stanza as $stanzaline) { | ||||
|                 if ($stanzaline['key'] == 'content_mark') { | ||||
|                     return $stanzaline['hash']; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::inBranches() | ||||
|      */ | ||||
|     public function inBranches($commit, $path) | ||||
|     { | ||||
|         $revs = $this->_resolveSelector($commit); | ||||
|         if (count($revs) == 0) return array(); | ||||
|         return $this->_getUniqueCertValuesFor($revs, 'branch', 'h:'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getTags() | ||||
|      */ | ||||
|     public function getTags() | ||||
|     { | ||||
|         if (isset($this->cache['tags'])) { | ||||
|             return $this->cache['tags']; | ||||
|         } | ||||
|  | ||||
|         $out = $this->stdio->exec(array('tags')); | ||||
|  | ||||
|         $tags = array(); | ||||
|         $stanzas = self::_parseBasicIO($out); | ||||
|         foreach ($stanzas as $stanza) { | ||||
|             $tagname = null; | ||||
|             foreach ($stanza as $stanzaline) { | ||||
|                 // revision comes directly after the tag stanza | ||||
|                 if ($stanzaline['key'] == 'tag') { | ||||
|                     $tagname = $stanzaline['values'][0]; | ||||
|                     continue; | ||||
|                 } | ||||
|                 if ($stanzaline['key'] == 'revision') { | ||||
|                     // FIXME: warn if multiple revisions have | ||||
|                     // equally named tags | ||||
|                     if (!array_key_exists("t:$tagname", $tags)) { | ||||
|                         $tags["t:$tagname"] = $tagname; | ||||
|                     } | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         $this->cache['tags'] = $tags; | ||||
|         return $tags; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::inTags() | ||||
|      */ | ||||
|     public function inTags($commit, $path) | ||||
|     { | ||||
|         $revs = $this->_resolveSelector($commit); | ||||
|         if (count($revs) == 0) return array(); | ||||
|         return $this->_getUniqueCertValuesFor($revs, 'tag', 't:'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getTree() | ||||
|      */ | ||||
|     public function getTree($commit, $folder='/', $branch=null) | ||||
|     { | ||||
|         $revs = $this->_resolveSelector($commit); | ||||
|         if (count($revs) == 0) { | ||||
|             return array(); | ||||
|         } | ||||
|  | ||||
|         $out = $this->stdio->exec(array( | ||||
|             'get_manifest_of', $revs[0] | ||||
|         )); | ||||
|  | ||||
|         $files = array(); | ||||
|         $stanzas = self::_parseBasicIO($out); | ||||
|         $folder = $folder == '/' || empty($folder) ? '' : $folder.'/'; | ||||
|  | ||||
|         foreach ($stanzas as $stanza) { | ||||
|             if ($stanza[0]['key'] == 'format_version') | ||||
|                 continue; | ||||
|  | ||||
|             $path = $stanza[0]['values'][0]; | ||||
|             if (!preg_match('#^'.$folder.'([^/]+)$#', $path, $m)) | ||||
|                 continue; | ||||
|  | ||||
|             $file = array(); | ||||
|             $file['file'] = $m[1]; | ||||
|             $file['fullpath'] = $path; | ||||
|             $file['efullpath'] = self::smartEncode($path); | ||||
|  | ||||
|             if ($stanza[0]['key'] == 'dir') { | ||||
|                 $file['type'] = 'tree'; | ||||
|                 $file['size'] = 0; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 $file['type'] = 'blob'; | ||||
|                 $file['hash'] = $stanza[1]['hash']; | ||||
|                 $file['size'] = strlen($this->getFile((object)$file)); | ||||
|             } | ||||
|  | ||||
|             $rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]); | ||||
|             if ($rev !== null) { | ||||
|                 $file['rev'] = $rev; | ||||
|                 $certs = $this->_getCerts($rev); | ||||
|  | ||||
|                 // FIXME: this assumes that author, date and changelog are always given | ||||
|                 $file['author'] = implode(", ", $certs['author']); | ||||
|  | ||||
|                 $dates = array(); | ||||
|                 foreach ($certs['date'] as $date) | ||||
|                     $dates[] = date('Y-m-d H:i:s', strtotime($date)); | ||||
|                 $file['date'] = implode(', ', $dates); | ||||
|                 $file['log'] = implode("\n---\n", $certs['changelog']); | ||||
|             } | ||||
|  | ||||
|             $files[] = (object) $file; | ||||
|         } | ||||
|         return $files; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::findAuthor() | ||||
|      */ | ||||
|     public function findAuthor($author) | ||||
|     { | ||||
|         // We extract anything which looks like an email. | ||||
|         $match = array(); | ||||
|         if (!preg_match('/([^ ]+@[^ ]+)/', $author, $match)) { | ||||
|             return null; | ||||
|         } | ||||
|         foreach (array('email', 'login') as $what) { | ||||
|             $sql = new Pluf_SQL($what.'=%s', array($match[1])); | ||||
|             $users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen())); | ||||
|             if ($users->count() > 0) { | ||||
|                 return $users[0]; | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getAnonymousAccessUrl() | ||||
|      */ | ||||
|     public static function getAnonymousAccessUrl($project, $commit = null) | ||||
|     { | ||||
|         $scm = IDF_Scm::get($project); | ||||
|         $branch = $scm->getMainBranch(); | ||||
|  | ||||
|         if (!empty($commit)) { | ||||
|             $revs = $scm->_resolveSelector($commit); | ||||
|             if (count($revs) > 0) { | ||||
|                 $certs = $scm->_getCerts($revs[0]); | ||||
|                 // for the very seldom case that a revision | ||||
|                 // has no branch certificate | ||||
|                 if (count($certs['branch']) == 0) { | ||||
|                     $branch = '*'; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     $branch = $certs['branch'][0]; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         $remote_url = Pluf::f('mtn_remote_url', ''); | ||||
|         if (empty($remote_url)) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
|         return sprintf($remote_url, $project->shortname).'?'.$branch; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getAuthAccessUrl() | ||||
|      */ | ||||
|     public static function getAuthAccessUrl($project, $user, $commit = null) | ||||
|     { | ||||
|         $url = self::getAnonymousAccessUrl($project, $commit); | ||||
|         return preg_replace("#^ssh://#", "ssh://$user@", $url); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns this object correctly initialized for the project. | ||||
|      * | ||||
|      * @param IDF_Project | ||||
|      * @return IDF_Scm_Monotone | ||||
|      */ | ||||
|     public static function factory($project) | ||||
|     { | ||||
|         if (!array_key_exists($project->shortname, self::$instances)) { | ||||
|             self::$instances[$project->shortname] = | ||||
|                 new IDF_Scm_Monotone($project); | ||||
|         } | ||||
|         return self::$instances[$project->shortname]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::isValidRevision() | ||||
|      */ | ||||
|     public function isValidRevision($commit) | ||||
|     { | ||||
|         $revs = $this->_resolveSelector($commit); | ||||
|         return count($revs) == 1; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getPathInfo() | ||||
|      */ | ||||
|     public function getPathInfo($file, $commit = null) | ||||
|     { | ||||
|         if ($commit === null) { | ||||
|             $commit = 'h:' . $this->getMainBranch(); | ||||
|         } | ||||
|  | ||||
|         $revs = $this->_resolveSelector($commit); | ||||
|         if (count($revs) == 0) | ||||
|             return false; | ||||
|  | ||||
|         $out = $this->stdio->exec(array( | ||||
|             'get_manifest_of', $revs[0] | ||||
|         )); | ||||
|  | ||||
|         $files = array(); | ||||
|         $stanzas = self::_parseBasicIO($out); | ||||
|  | ||||
|         foreach ($stanzas as $stanza) { | ||||
|             if ($stanza[0]['key'] == 'format_version') | ||||
|                 continue; | ||||
|  | ||||
|             $path = $stanza[0]['values'][0]; | ||||
|             if (!preg_match('#^'.$file.'$#', $path, $m)) | ||||
|                 continue; | ||||
|  | ||||
|             $file = array(); | ||||
|             $file['fullpath'] = $path; | ||||
|  | ||||
|             if ($stanza[0]['key'] == "dir") { | ||||
|                 $file['type'] = "tree"; | ||||
|                 $file['hash'] = null; | ||||
|                 $file['size'] = 0; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 $file['type'] = 'blob'; | ||||
|                 $file['hash'] = $stanza[1]['hash']; | ||||
|                 $file['size'] = strlen($this->getFile((object)$file)); | ||||
|             } | ||||
|  | ||||
|             $pathinfo = pathinfo($file['fullpath']); | ||||
|             $file['file'] = $pathinfo['basename']; | ||||
|  | ||||
|             $rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]); | ||||
|             if ($rev !== null) { | ||||
|                 $file['rev'] = $rev; | ||||
|                 $certs = $this->_getCerts($rev); | ||||
|  | ||||
|                 // FIXME: this assumes that author, date and changelog are always given | ||||
|                 $file['author'] = implode(", ", $certs['author']); | ||||
|  | ||||
|                 $dates = array(); | ||||
|                 foreach ($certs['date'] as $date) | ||||
|                     $dates[] = date('Y-m-d H:i:s', strtotime($date)); | ||||
|                 $file['date'] = implode(', ', $dates); | ||||
|                 $file['log'] = implode("\n---\n", $certs['changelog']); | ||||
|             } | ||||
|  | ||||
|             return (object) $file; | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getFile() | ||||
|      */ | ||||
|     public function getFile($def, $cmd_only=false) | ||||
|     { | ||||
|         // this won't work with remote databases | ||||
|         if ($cmd_only) { | ||||
|             throw new Pluf_Exception_NotImplemented(); | ||||
|         } | ||||
|  | ||||
|         return $this->stdio->exec(array('get_file', $def->hash)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the differences between two revisions as unified diff | ||||
|      * | ||||
|      * @param string    The target of the diff | ||||
|      * @param string    The source of the diff, if not given, the first | ||||
|      *                  parent of the target is used | ||||
|      * @return string | ||||
|      */ | ||||
|     private function _getDiff($target, $source = null) | ||||
|     { | ||||
|         if (empty($source)) { | ||||
|             $source = "p:$target"; | ||||
|         } | ||||
|  | ||||
|         // FIXME: add real support for merge revisions here which have | ||||
|         // two distinct diff sets | ||||
|         $targets = $this->_resolveSelector($target); | ||||
|         $sources = $this->_resolveSelector($source); | ||||
|  | ||||
|         if (count($targets) == 0 || count($sources) == 0) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
|         // if target contains a root revision, we cannot produce a diff | ||||
|         if (empty($sources[0])) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
|         return $this->stdio->exec( | ||||
|             array('content_diff'), | ||||
|             array('r' => array($sources[0], $targets[0])) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getCommit() | ||||
|      */ | ||||
|     public function getCommit($commit, $getdiff=false) | ||||
|     { | ||||
|         $revs = $this->_resolveSelector($commit); | ||||
|         if (count($revs) == 0) | ||||
|             return array(); | ||||
|  | ||||
|         $certs = $this->_getCerts($revs[0]); | ||||
|  | ||||
|         // FIXME: this assumes that author, date and changelog are always given | ||||
|         $res['author'] = implode(', ', $certs['author']); | ||||
|  | ||||
|         $dates = array(); | ||||
|         foreach ($certs['date'] as $date) | ||||
|             $dates[] = date('Y-m-d H:i:s', strtotime($date)); | ||||
|         $res['date'] = implode(', ', $dates); | ||||
|  | ||||
|         $res['title'] = implode("\n---\n", $certs['changelog']); | ||||
|  | ||||
|         $res['commit'] = $revs[0]; | ||||
|  | ||||
|         $res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : ''; | ||||
|  | ||||
|         return (object) $res; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::isCommitLarge() | ||||
|      */ | ||||
|     public function isCommitLarge($commit=null) | ||||
|     { | ||||
|         if (empty($commit)) { | ||||
|             $commit = 'h:'.$this->getMainBranch(); | ||||
|         } | ||||
|  | ||||
|         $revs = $this->_resolveSelector($commit); | ||||
|         if (count($revs) == 0) | ||||
|             return false; | ||||
|  | ||||
|         $out = $this->stdio->exec(array( | ||||
|             'get_revision', $revs[0] | ||||
|         )); | ||||
|  | ||||
|         $newAndPatchedFiles = 0; | ||||
|         $stanzas = self::_parseBasicIO($out); | ||||
|  | ||||
|         foreach ($stanzas as $stanza) { | ||||
|             if ($stanza[0]['key'] == 'patch' || $stanza[0]['key'] == 'add_file') | ||||
|                 $newAndPatchedFiles++; | ||||
|         } | ||||
|  | ||||
|         return $newAndPatchedFiles > 100; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see IDF_Scm::getChangeLog() | ||||
|      */ | ||||
|     public function getChangeLog($commit=null, $n=10) | ||||
|     { | ||||
|         $horizont = $this->_resolveSelector($commit); | ||||
|         $initialBranches = array(); | ||||
|         $logs = array(); | ||||
|  | ||||
|         while (!empty($horizont) && $n > 0) { | ||||
|             if (count($horizont) > 1) { | ||||
|                 $out = $this->stdio->exec(array('toposort') + $horizont); | ||||
|                 $horizont = preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY); | ||||
|             } | ||||
|  | ||||
|             $rev = array_shift($horizont); | ||||
|             $certs = $this->_getCerts($rev); | ||||
|  | ||||
|             // read in the initial branches we should follow | ||||
|             if (count($initialBranches) == 0) { | ||||
|                 $initialBranches = $certs['branch']; | ||||
|             } | ||||
|  | ||||
|             // only add it to our log if it is on one of the initial branches | ||||
|             if (count(array_intersect($initialBranches, $certs['branch'])) > 0) { | ||||
|                 --$n; | ||||
|  | ||||
|                 $log = array(); | ||||
|                 $log['author'] = implode(", ", $certs['author']); | ||||
|  | ||||
|                 $dates = array(); | ||||
|                 foreach ($certs['date'] as $date) | ||||
|                     $dates[] = date('Y-m-d H:i:s', strtotime($date)); | ||||
|                 $log['date'] = implode(', ', $dates); | ||||
|  | ||||
|                 $combinedChangelog = implode("\n---\n", $certs['changelog']); | ||||
|                 $split = preg_split("/[\n\r]/", $combinedChangelog, 2); | ||||
|                 $log['title'] = $split[0]; | ||||
|                 $log['full_message'] = (isset($split[1])) ? trim($split[1]) : ''; | ||||
|  | ||||
|                 $log['commit'] = $rev; | ||||
|  | ||||
|                 $logs[] = (object)$log; | ||||
|             } | ||||
|  | ||||
|             $out = $this->stdio->exec(array('parents', $rev)); | ||||
|             $horizont += preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY); | ||||
|         } | ||||
|  | ||||
|         return $logs; | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										360
									
								
								src/IDF/Scm/Monotone/Stdio.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								src/IDF/Scm/Monotone/Stdio.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,360 @@ | ||||
| <?php | ||||
| /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ | ||||
| /* | ||||
| # ***** BEGIN LICENSE BLOCK ***** | ||||
| # This file is part of InDefero, an open source project management application. | ||||
| # Copyright (C) 2010 Céondo Ltd and contributors. | ||||
| # | ||||
| # InDefero is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 2 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # InDefero is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program; if not, write to the Free Software | ||||
| # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA | ||||
| # | ||||
| # ***** END LICENSE BLOCK ***** */ | ||||
|  | ||||
| /** | ||||
|  * Monotone stdio class | ||||
|  * | ||||
|  * Connects to a monotone process and executes commands via its | ||||
|  * stdio interface | ||||
|  * | ||||
|  * @author Thomas Keller <me@thomaskeller.biz> | ||||
|  */ | ||||
| class IDF_Scm_Monotone_Stdio | ||||
| { | ||||
|     /** this is the most recent STDIO version. The number is output | ||||
|         at the protocol start. Older versions of monotone (prior 0.47) | ||||
|         do not output it and are therefor incompatible */ | ||||
|     public static $SUPPORTED_STDIO_VERSION = 2; | ||||
|  | ||||
|     private $project; | ||||
|     private $proc; | ||||
|     private $pipes; | ||||
|     private $oob; | ||||
|     private $cmdnum; | ||||
|     private $lastcmd; | ||||
|  | ||||
|     /** | ||||
|      * Constructor - starts the stdio process | ||||
|      * | ||||
|      * @param IDF_Project | ||||
|      */ | ||||
|     public function __construct(IDF_Project $project) | ||||
|     { | ||||
|         $this->project = $project; | ||||
|         $this->start(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Destructor - stops the stdio process | ||||
|      */ | ||||
|     public function __destruct() | ||||
|     { | ||||
|         $this->stop(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Starts the stdio process and resets the command counter | ||||
|      */ | ||||
|     public function start() | ||||
|     { | ||||
|         if (is_resource($this->proc)) | ||||
|             $this->stop(); | ||||
|  | ||||
|         $remote_db_access = Pluf::f('mtn_db_access', 'remote') == 'remote'; | ||||
|  | ||||
|         $cmd = Pluf::f('idf_exec_cmd_prefix', '') . | ||||
|                Pluf::f('mtn_path', 'mtn') . ' '; | ||||
|  | ||||
|         $opts = Pluf::f('mtn_opts', array()); | ||||
|         foreach ($opts as $opt) { | ||||
|             $cmd .= sprintf('%s ', escapeshellarg($opt)); | ||||
|         } | ||||
|  | ||||
|         // FIXME: we might want to add an option for anonymous / no key | ||||
|         // access, but upstream bug #30237 prevents that for now | ||||
|         if ($remote_db_access) { | ||||
|             $host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname); | ||||
|             $cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host)); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             $repo = sprintf(Pluf::f('mtn_repositories'), $this->project->shortname); | ||||
|             if (!file_exists($repo)) { | ||||
|                 throw new IDF_Scm_Exception( | ||||
|                     "repository file '$repo' does not exist" | ||||
|                 ); | ||||
|             } | ||||
|             $cmd .= sprintf('--db %s automate stdio', escapeshellarg($repo)); | ||||
|         } | ||||
|  | ||||
|         $descriptors = array( | ||||
|             0 => array('pipe', 'r'), | ||||
|             1 => array('pipe', 'w'), | ||||
|             2 => array('pipe', 'w'), | ||||
|         ); | ||||
|  | ||||
|         $env = array('LANG' => 'en_US.UTF-8'); | ||||
|  | ||||
|         $this->proc = proc_open($cmd, $descriptors, $this->pipes, | ||||
|                                 null, $env); | ||||
|  | ||||
|         if (!is_resource($this->proc)) { | ||||
|             throw new IDF_Scm_Exception('could not start stdio process'); | ||||
|         } | ||||
|  | ||||
|         $this->_checkVersion(); | ||||
|  | ||||
|         $this->cmdnum = -1; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Stops the stdio process and closes all pipes | ||||
|      */ | ||||
|     public function stop() | ||||
|     { | ||||
|         if (!is_resource($this->proc)) | ||||
|             return; | ||||
|  | ||||
|         fclose($this->pipes[0]); | ||||
|         fclose($this->pipes[1]); | ||||
|         fclose($this->pipes[2]); | ||||
|  | ||||
|         proc_close($this->proc); | ||||
|         $this->proc = null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * select()'s on stdout and returns true as soon as we got new | ||||
|      * data to read, false if the select() timed out | ||||
|      * | ||||
|      * @return boolean | ||||
|      * @throws IDF_Scm_Exception | ||||
|      */ | ||||
|     private function _waitForReadyRead() | ||||
|     { | ||||
|         if (!is_resource($this->pipes[1])) | ||||
|             return false; | ||||
|  | ||||
|         $read = array($this->pipes[1], $this->pipes[2]); | ||||
|         $streamsChanged = stream_select( | ||||
|             $read, $write = null, $except = null, 0, 20000 | ||||
|         ); | ||||
|  | ||||
|         if ($streamsChanged === false) { | ||||
|             throw new IDF_Scm_Exception( | ||||
|                 'Could not select() on read pipe' | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if ($streamsChanged == 0) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks the version of the used stdio protocol | ||||
|      * | ||||
|      * @throws IDF_Scm_Exception | ||||
|      */ | ||||
|     private function _checkVersion() | ||||
|     { | ||||
|         $this->_waitForReadyRead(); | ||||
|  | ||||
|         $version = fgets($this->pipes[1]); | ||||
|         if ($version === false) { | ||||
|             throw new IDF_Scm_Exception( | ||||
|                 "Could not determine stdio version, stderr is:\n". | ||||
|                 $this->_readStderr() | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if (!preg_match('/^format-version: (\d+)$/', $version, $m) || | ||||
|             $m[1] != self::$SUPPORTED_STDIO_VERSION) | ||||
|         { | ||||
|             throw new IDF_Scm_Exception( | ||||
|                 'stdio format version mismatch, expected "'. | ||||
|                 self::$SUPPORTED_STDIO_VERSION.'", got "'.@$m[1].'"' | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         fgets($this->pipes[1]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Writes a command to stdio | ||||
|      * | ||||
|      * @param array | ||||
|      * @param array | ||||
|      * @throws IDF_Scm_Exception | ||||
|      */ | ||||
|     private function _write(array $args, array $options = array()) | ||||
|     { | ||||
|         $cmd = ''; | ||||
|         if (count($options) > 0) { | ||||
|             $cmd = 'o'; | ||||
|             foreach ($options as $k => $vals) { | ||||
|                 if (!is_array($vals)) | ||||
|                     $vals = array($vals); | ||||
|  | ||||
|                 foreach ($vals as $v) { | ||||
|                     $cmd .= strlen((string)$k) . ':' . (string)$k; | ||||
|                     $cmd .= strlen((string)$v) . ':' . (string)$v; | ||||
|                 } | ||||
|             } | ||||
|             $cmd .= 'e '; | ||||
|         } | ||||
|  | ||||
|         $cmd .= 'l'; | ||||
|         foreach ($args as $arg) { | ||||
|             $cmd .= strlen((string)$arg) . ':' . (string)$arg; | ||||
|         } | ||||
|         $cmd .= "e\n"; | ||||
|  | ||||
|         if (!fwrite($this->pipes[0], $cmd)) { | ||||
|             throw new IDF_Scm_Exception("could not write '$cmd' to process"); | ||||
|         } | ||||
|  | ||||
|         $this->lastcmd = $cmd; | ||||
|         $this->cmdnum++; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Reads all output from stderr and returns it | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     private function _readStderr() | ||||
|     { | ||||
|         $err = ""; | ||||
|         while (($line = fgets($this->pipes[2])) !== false) { | ||||
|             $err .= $line; | ||||
|         } | ||||
|         return empty($err) ? '<empty>' : $err; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Reads the last output from the stdio process, parses and returns it | ||||
|      * | ||||
|      * @return string | ||||
|      * @throws IDF_Scm_Exception | ||||
|      */ | ||||
|     private function _readStdout() | ||||
|     { | ||||
|         $this->oob = array('w' => array(), | ||||
|                            'p' => array(), | ||||
|                            't' => array(), | ||||
|                            'e' => array()); | ||||
|  | ||||
|         $output = ""; | ||||
|         $errcode = 0; | ||||
|  | ||||
|         while (true) { | ||||
|             if (!$this->_waitForReadyRead()) | ||||
|                 continue; | ||||
|  | ||||
|             $data = array(0,"",0); | ||||
|             $idx = 0; | ||||
|             while (true) { | ||||
|                 $c = fgetc($this->pipes[1]); | ||||
|                 if ($c === false) { | ||||
|                     throw new IDF_Scm_Exception( | ||||
|                         "No data on stdin, stderr is:\n". | ||||
|                         $this->_readStderr() | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 if ($c == ':') { | ||||
|                     if ($idx == 2) | ||||
|                         break; | ||||
|  | ||||
|                     ++$idx; | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 if (is_numeric($c)) | ||||
|                     $data[$idx] = $data[$idx] * 10 + $c; | ||||
|                 else | ||||
|                     $data[$idx] .= $c; | ||||
|             } | ||||
|  | ||||
|             // sanity | ||||
|             if ($this->cmdnum != $data[0]) { | ||||
|                 throw new IDF_Scm_Exception( | ||||
|                     'command numbers out of sync; expected '. | ||||
|                     $this->cmdnum .', got '. $data[0] | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             $toRead = $data[2]; | ||||
|             $buffer = ""; | ||||
|             while ($toRead > 0) { | ||||
|                 $buffer .= fread($this->pipes[1], $toRead); | ||||
|                 $toRead = $data[2] - strlen($buffer); | ||||
|             } | ||||
|  | ||||
|             switch ($data[1]) { | ||||
|                 case 'w': | ||||
|                 case 'p': | ||||
|                 case 't': | ||||
|                 case 'e': | ||||
|                     $this->oob[$data[1]][] = $buffer; | ||||
|                     continue; | ||||
|                 case 'm': | ||||
|                     $output .= $buffer; | ||||
|                     continue; | ||||
|                 case 'l': | ||||
|                     $errcode = $buffer; | ||||
|                     break 2; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if ($errcode != 0) { | ||||
|             throw new IDF_Scm_Exception( | ||||
|                 "command '{$this->lastcmd}' returned error code $errcode: ". | ||||
|                 implode(' ', $this->oob['e']) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return $output; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Executes a command over stdio and returns its result | ||||
|      * | ||||
|      * @param array Array of arguments | ||||
|      * @param array Array of options as key-value pairs. Multiple options | ||||
|      *              can be defined in sub-arrays, like | ||||
|      *              "r" => array("123...", "456...") | ||||
|      * @return string | ||||
|      */ | ||||
|     public function exec(array $args, array $options = array()) | ||||
|     { | ||||
|         $this->_write($args, $options); | ||||
|         return $this->_readStdout(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the last out-of-band output for a previously executed | ||||
|      * command as associative array with 'e' (error), 'w' (warning), | ||||
|      * 'p' (progress) and 't' (ticker, unparsed) as keys | ||||
|      * | ||||
|      * @return array | ||||
|      */ | ||||
|     public function getLastOutOfBandOutput() | ||||
|     { | ||||
|         return $this->oob; | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										241
									
								
								src/IDF/Scm/Monotone/Usher.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								src/IDF/Scm/Monotone/Usher.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| <?php | ||||
| /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ | ||||
| /* | ||||
| # ***** BEGIN LICENSE BLOCK ***** | ||||
| # This file is part of InDefero, an open source project management application. | ||||
| # Copyright (C) 2010 Céondo Ltd and contributors. | ||||
| # | ||||
| # InDefero is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 2 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # InDefero is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program; if not, write to the Free Software | ||||
| # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA | ||||
| # | ||||
| # ***** END LICENSE BLOCK ***** */ | ||||
|  | ||||
| /** | ||||
|  * Connects with the admininistrative interface of usher, | ||||
|  * the monotone proxy. This class contains only static methods because | ||||
|  * there is really no state to keep between each invocation, as usher | ||||
|  * closes the connection after every command. | ||||
|  * | ||||
|  * @author Thomas Keller <me@thomaskeller.biz> | ||||
|  */ | ||||
| class IDF_Scm_Monotone_Usher | ||||
| { | ||||
|     /** | ||||
|      * Without giving a specific state, returns an array of all servers. | ||||
|      * When a state is given, the array contains only servers which are | ||||
|      * in the given state. | ||||
|      * | ||||
|      * @param string $state One of REMOTE, ACTIVE, WAITING, SLEEPING, | ||||
|      *                      STOPPING, STOPPED, SHUTTINGDOWN or SHUTDOWN | ||||
|      * @return array | ||||
|      */ | ||||
|     public static function getServerList($state = null) | ||||
|     { | ||||
|         $conn = self::_triggerCommand('LIST '.$state); | ||||
|         if ($conn == 'none') | ||||
|             return array(); | ||||
|  | ||||
|         return preg_split('/[ ]/', $conn); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns an array of all open connections to the given server, or to | ||||
|      * any server if no server is specified. | ||||
|      * If there are no connections to list, an empty array is returned. | ||||
|      * | ||||
|      * Example: | ||||
|      *    array("server1" => array( | ||||
|      *              array("address" => "192.168.1.0", "port" => "13456"), | ||||
|      *              ... | ||||
|      *          ), | ||||
|      *          "server2" => ... | ||||
|      *    ) | ||||
|      * | ||||
|      * @param string $server | ||||
|      * @return array | ||||
|      */ | ||||
|     public static function getConnectionList($server = null) | ||||
|     { | ||||
|         $conn = self::_triggerCommand('LISTCONNECTIONS '.$server); | ||||
|         if ($conn == 'none') | ||||
|             return array(); | ||||
|  | ||||
|         $single_conns = preg_split('/[ ]/', $conn); | ||||
|         $ret = array(); | ||||
|         foreach ($single_conns as $conn) { | ||||
|             preg_match('/\(\w+\)([^:]):(\d+)/', $conn, $matches); | ||||
|             $ret[$matches[1]][] = (object)array( | ||||
|                 'server'    => $matches[1], | ||||
|                 'address'   => $matches[2], | ||||
|                 'port'      => $matches[3], | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return $ret; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the status of a particular server, or of the usher as a whole if | ||||
|      * no server is specified. | ||||
|      * | ||||
|      * @param string $server | ||||
|      * @return One of REMOTE, SLEEPING, STOPPING, STOPPED for servers or | ||||
|      *         ACTIVE, WAITING, SHUTTINGDOWN or SHUTDOWN for usher itself | ||||
|      */ | ||||
|     public static function getStatus($server = null) | ||||
|     { | ||||
|         return self::_triggerCommand('STATUS '.$server); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Looks up the name of the server that would be used for an incoming | ||||
|      * connection having the given host and pattern. | ||||
|      * | ||||
|      * @param string $host      Host | ||||
|      * @param string $pattern   Branch pattern | ||||
|      * @return server name | ||||
|      * @throws IDF_Scm_Exception | ||||
|      */ | ||||
|     public static function matchServer($host, $pattern) | ||||
|     { | ||||
|         $ret = self::_triggerCommand('MATCH '.$host.' '.$pattern); | ||||
|         if (preg_match('/^OK: (.+)/', $ret, $m)) | ||||
|             return $m[1]; | ||||
|         preg_match('/^ERROR: (.+)/', $ret, $m); | ||||
|         throw new IDF_Scm_Exception('could not match server: '.$m[1]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Prevent the given local server from receiving further connections, | ||||
|      * and stop it once all connections are closed. The return value will | ||||
|      * be the new status of that server: ACTIVE local servers will become | ||||
|      * STOPPING, and WAITING and SLEEPING serveres become STOPPED. | ||||
|      * Servers in other states are not affected. | ||||
|      * | ||||
|      * @param string $server | ||||
|      * @return string State of the server after the command | ||||
|      */ | ||||
|     public static function stopServer($server) | ||||
|     { | ||||
|         return self::_triggerCommand("STOP $server"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Allow a STOPPED or STOPPING server to receive connections again. | ||||
|      * The return value is the new status of that server: STOPPING servers | ||||
|      * become ACTIVE, and STOPPED servers become SLEEPING. Servers in other | ||||
|      * states are not affected. | ||||
|      * | ||||
|      * @param string $server | ||||
|      * @return string State of the server after the command | ||||
|      */ | ||||
|     public static function startServer($server) | ||||
|     { | ||||
|         return self::_triggerCommand('START '.$server); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Immediately kill the given local server, dropping any open connections, | ||||
|      * and prevent is from receiving new connections and restarting. The named | ||||
|      * server will immediately change to state STOPPED. | ||||
|      * | ||||
|      * @param string $server | ||||
|      * @return bool True if successful | ||||
|      */ | ||||
|     public static function killServer($server) | ||||
|     { | ||||
|         return self::_triggerCommand('KILL_NOW '.$server) == 'ok'; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Do not accept new connections for any servers, local or remote. | ||||
|      * | ||||
|      * @return bool True if successful | ||||
|      */ | ||||
|     public static function shutDown() | ||||
|     { | ||||
|         return self::_triggerCommand('SHUTDOWN') == 'ok'; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Begin accepting connections after a SHUTDOWN. | ||||
|      * | ||||
|      * @return bool True if successful | ||||
|      */ | ||||
|     public static function startUp() | ||||
|     { | ||||
|         return self::_triggerCommand('STARTUP') == 'ok'; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Reload the config file, the same as sending SIGHUP. | ||||
|      * | ||||
|      * @return bool True if successful (after the configuration was reloaded) | ||||
|      */ | ||||
|     public static function reload() | ||||
|     { | ||||
|         return self::_triggerCommand('RELOAD') == 'ok'; | ||||
|     } | ||||
|  | ||||
|     private static function _triggerCommand($cmd) | ||||
|     { | ||||
|         $uc = Pluf::f('mtn_usher'); | ||||
|         if (empty($uc['host'])) { | ||||
|             throw new IDF_Scm_Exception('usher host is empty'); | ||||
|         } | ||||
|         if (!preg_match('/^\d+$/', $uc['port']) || | ||||
|             $uc['port'] == 0) | ||||
|         { | ||||
|             throw new IDF_Scm_Exception('usher port is invalid'); | ||||
|         } | ||||
|  | ||||
|         if (empty($uc['user'])) { | ||||
|             throw new IDF_Scm_Exception('usher user is empty'); | ||||
|         } | ||||
|  | ||||
|         if (empty($uc['pass'])) { | ||||
|             throw new IDF_Scm_Exception('usher pass is empty'); | ||||
|         } | ||||
|  | ||||
|         $sock = @fsockopen($uc['host'], $uc['port'], $errno, $errstr); | ||||
|         if (!$sock) { | ||||
|             throw new IDF_Scm_Exception( | ||||
|                 "could not connect to usher: $errstr ($errno)" | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         fwrite($sock, 'USERPASS '.$uc['user'].' '.$uc['pass'].'\n'); | ||||
|         if (feof($sock)) { | ||||
|             throw new IDF_Scm_Exception( | ||||
|                 'usher closed the connection - probably wrong admin '. | ||||
|                 'username or password' | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         fwrite($sock, $cmd.'\n'); | ||||
|         $out = ''; | ||||
|         while (!feof($sock)) { | ||||
|             $out .= fgets($sock); | ||||
|         } | ||||
|         fclose($sock); | ||||
|         $out = rtrim($out); | ||||
|  | ||||
|         if ($out == 'unknown command') { | ||||
|             throw new IDF_Scm_Exception("unknown command: $cmd"); | ||||
|         } | ||||
|  | ||||
|         return $out; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -80,9 +80,10 @@ class IDF_Scm_Svn extends IDF_Scm | ||||
|      * Returns the URL of the subversion repository. | ||||
|      * | ||||
|      * @param IDF_Project | ||||
|      * @param string | ||||
|      * @return string URL | ||||
|      */ | ||||
|     public static function getAnonymousAccessUrl($project) | ||||
|     public static function getAnonymousAccessUrl($project,$commit=null) | ||||
|     { | ||||
|         $conf = $project->getConf(); | ||||
|         if (false !== ($url=$conf->getVal('svn_remote_url', false)) | ||||
| @@ -97,9 +98,10 @@ class IDF_Scm_Svn extends IDF_Scm | ||||
|      * Returns the URL of the subversion repository. | ||||
|      * | ||||
|      * @param IDF_Project | ||||
|      * @param string | ||||
|      * @return string URL | ||||
|      */ | ||||
|     public static function getAuthAccessUrl($project, $user) | ||||
|     public static function getAuthAccessUrl($project, $user, $commit=null) | ||||
|     { | ||||
|         $conf = $project->getConf(); | ||||
|         if (false !== ($url=$conf->getVal('svn_remote_url', false)) | ||||
|   | ||||
							
								
								
									
										239
									
								
								src/IDF/Tests/TestMonotone.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								src/IDF/Tests/TestMonotone.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | ||||
| <?php | ||||
| /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ | ||||
| /* | ||||
| # ***** BEGIN LICENSE BLOCK ***** | ||||
| # This file is part of InDefero, an open source project management application. | ||||
| # Copyright (C) 2010 Céondo Ltd and contributors. | ||||
| # | ||||
| # InDefero is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 2 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # InDefero is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program; if not, write to the Free Software | ||||
| # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA | ||||
| # | ||||
| # ***** END LICENSE BLOCK ***** */ | ||||
|  | ||||
| require_once("simpletest/autorun.php"); | ||||
|  | ||||
| /** | ||||
|  * Test the monotone class. | ||||
|  */ | ||||
| class IDF_Tests_TestMonotone extends UnitTestCase | ||||
| { | ||||
|     private $tmpdir, $dbfile, $mtnInstance; | ||||
|  | ||||
|     private function mtnCall($args, $stdin = null, $dir = null) | ||||
|     { | ||||
|         // if you have an SSH agent running for key caching, | ||||
|         // please disable it | ||||
|         $cmdline = array("mtn", | ||||
|                         "--confdir", $this->tmpdir, | ||||
|                         "--db", $this->dbfile, | ||||
|                         "--norc", | ||||
|                         "--timestamps"); | ||||
|  | ||||
|         $cmdline = array_merge($cmdline, $args); | ||||
|  | ||||
|         $descriptorspec = array( | ||||
|            0 => array("pipe", "r"), | ||||
|            1 => array("pipe", "w"), | ||||
|            2 => array("file", "{$this->tmpdir}/mtn-errors", "a") | ||||
|         ); | ||||
|  | ||||
|         $pipes = array(); | ||||
|         $dir = !empty($dir) ? $dir : $this->tmpdir; | ||||
|         $process = proc_open(implode(" ", $cmdline), | ||||
|                              $descriptorspec, | ||||
|                              $pipes, | ||||
|                              $dir); | ||||
|  | ||||
|         if (!is_resource($process)) { | ||||
|             throw new Exception("could not create process"); | ||||
|         } | ||||
|  | ||||
|         if (!empty($stdin)) { | ||||
|             fwrite($pipes[0], $stdin); | ||||
|             fclose($pipes[0]); | ||||
|         } | ||||
|  | ||||
|         $stdout = stream_get_contents($pipes[1]); | ||||
|         fclose($pipes[1]); | ||||
|  | ||||
|         $ret = proc_close($process); | ||||
|         if ($ret != 0) { | ||||
|             throw new Exception( | ||||
|                 "call ended with a non-zero error code (complete cmdline was: ". | ||||
|                 implode(" ", $cmdline).")" | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return $stdout; | ||||
|     } | ||||
|  | ||||
|     public function __construct() | ||||
|     { | ||||
|         parent::__construct("Test the monotone class."); | ||||
|  | ||||
|         $this->tmpdir = sys_get_temp_dir() . "/mtn-test"; | ||||
|         $this->dbfile = "{$this->tmpdir}/test.mtn"; | ||||
|  | ||||
|         set_include_path(get_include_path() . ":../../../pluf-master/src"); | ||||
|         require_once("Pluf.php"); | ||||
|  | ||||
|         Pluf::start(dirname(__FILE__)."/../conf/idf.php"); | ||||
|  | ||||
|         // Pluf::f() mocking | ||||
|         $GLOBALS['_PX_config']['mtn_repositories'] = "{$this->tmpdir}/%s.mtn"; | ||||
|     } | ||||
|  | ||||
|     private static function deleteRecursive($dirname) | ||||
|     { | ||||
|         if (is_dir($dirname)) | ||||
|             $dir_handle=opendir($dirname); | ||||
|  | ||||
|         while ($file = readdir($dir_handle)) { | ||||
|             if ($file!="." && $file!="..") { | ||||
|                 if (!is_dir($dirname."/".$file)) { | ||||
|                     unlink ($dirname."/".$file); | ||||
|                     continue; | ||||
|                 } | ||||
|                 self::deleteRecursive($dirname."/".$file); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         closedir($dir_handle); | ||||
|         rmdir($dirname); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public function setUp() | ||||
|     { | ||||
|         if (is_dir($this->tmpdir)) { | ||||
|             self::deleteRecursive($this->tmpdir); | ||||
|         } | ||||
|  | ||||
|         mkdir($this->tmpdir); | ||||
|  | ||||
|         $this->mtnCall(array("db", "init")); | ||||
|  | ||||
|         $this->mtnCall(array("genkey", "test@test.de"), "\n\n"); | ||||
|  | ||||
|         $workspaceRoot = "{$this->tmpdir}/test-workspace"; | ||||
|         mkdir($workspaceRoot); | ||||
|  | ||||
|         $this->mtnCall(array("setup", "-b", "testbranch", "."), null, $workspaceRoot); | ||||
|  | ||||
|         file_put_contents("$workspaceRoot/foo", "blubber"); | ||||
|         $this->mtnCall(array("add", "foo"), null, $workspaceRoot); | ||||
|  | ||||
|         $this->mtnCall(array("commit", "-m", "initial"), null, $workspaceRoot); | ||||
|  | ||||
|         file_put_contents("$workspaceRoot/bar", "blafoo"); | ||||
|         mkdir("$workspaceRoot/subdir"); | ||||
|         file_put_contents("$workspaceRoot/subdir/bla", "blabla"); | ||||
|         $this->mtnCall(array("add", "-R", "--unknown"), null, $workspaceRoot); | ||||
|  | ||||
|         $this->mtnCall(array("commit", "-m", "second"), null, $workspaceRoot); | ||||
|  | ||||
|         $rev = $this->mtnCall(array("au", "get_base_revision_id"), null, $workspaceRoot); | ||||
|         $this->mtnCall(array("tag", rtrim($rev), "release-1.0")); | ||||
|  | ||||
|         $project = new IDF_Project(); | ||||
|         $project->shortname = "test"; | ||||
|         $this->mtnInstance = new IDF_Scm_Monotone($project); | ||||
|     } | ||||
|  | ||||
|     public function testIsAvailable() | ||||
|     { | ||||
|         $this->assertTrue($this->mtnInstance->isAvailable()); | ||||
|     } | ||||
|  | ||||
|     public function testGetBranches() | ||||
|     { | ||||
|         $branches = $this->mtnInstance->getBranches(); | ||||
|         $this->assertEqual(1, count($branches)); | ||||
|         list($key, $value) = each($branches); | ||||
|         $this->assertEqual("h:testbranch", $key); | ||||
|         $this->assertEqual("testbranch", $value); | ||||
|     } | ||||
|  | ||||
|     public function testGetTags() | ||||
|     { | ||||
|         $tags = $this->mtnInstance->getTags(); | ||||
|         $this->assertEqual(1, count($tags)); | ||||
|         list($key, $value) = each($tags); | ||||
|         $this->assertEqual("t:release-1.0", $key); | ||||
|         $this->assertEqual("release-1.0", $value); | ||||
|     } | ||||
|  | ||||
|     public function testInBranches() | ||||
|     { | ||||
|         $revOut = $this->mtnCall(array("au", "select", "b:testbranch")); | ||||
|         $revs = preg_split('/\n/', $revOut, -1, PREG_SPLIT_NO_EMPTY); | ||||
|  | ||||
|         $branches = $this->mtnInstance->inBranches($revs[0], null); | ||||
|         $this->assertEqual(1, count($branches)); | ||||
|         $this->assertEqual("h:testbranch", $branches[0]); | ||||
|  | ||||
|         $branches = $this->mtnInstance->inBranches("t:release-1.0", null); | ||||
|         $this->assertEqual(1, count($branches)); | ||||
|         $this->assertEqual("h:testbranch", $branches[0]); | ||||
|     } | ||||
|  | ||||
|     public function testInTags() | ||||
|     { | ||||
|         $rev = $this->mtnCall(array("au", "select", "t:release-1.0")); | ||||
|         $tags = $this->mtnInstance->inTags(rtrim($rev), null); | ||||
|         $this->assertEqual(1, count($tags)); | ||||
|         $this->assertEqual("t:release-1.0", $tags[0]); | ||||
|  | ||||
|         // pick the first (root) revisions in this database | ||||
|         $rev = $this->mtnCall(array("au", "roots")); | ||||
|         $tags = $this->mtnInstance->inTags(rtrim($rev), null); | ||||
|         $this->assertEqual(0, count($tags)); | ||||
|     } | ||||
|  | ||||
|     public function testGetTree() | ||||
|     { | ||||
|         $files = $this->mtnInstance->getTree("t:release-1.0"); | ||||
|         $this->assertEqual(3, count($files)); | ||||
|  | ||||
|         $this->assertEqual("bar", $files[0]->file); | ||||
|         $this->assertEqual("blob", $files[0]->type); | ||||
|         $this->assertEqual(6, $files[0]->size); // "blafoo" | ||||
|         $this->assertEqual("second\n", $files[0]->log); | ||||
|  | ||||
|         $this->assertEqual("foo", $files[1]->file); | ||||
|         $this->assertEqual("blob", $files[1]->type); | ||||
|         $this->assertEqual(7, $files[1]->size); // "blubber" | ||||
|         $this->assertEqual("initial\n", $files[1]->log); | ||||
|  | ||||
|         $this->assertEqual("subdir", $files[2]->file); | ||||
|         $this->assertEqual("tree", $files[2]->type); | ||||
|         $this->assertEqual(0, $files[2]->size); | ||||
|  | ||||
|         $files = $this->mtnInstance->getTree("t:release-1.0", "subdir"); | ||||
|         $this->assertEqual(1, count($files)); | ||||
|  | ||||
|         $this->assertEqual("bla", $files[0]->file); | ||||
|         $this->assertEqual("subdir/bla", $files[0]->fullpath); | ||||
|         $this->assertEqual("blob", $files[0]->type); | ||||
|         $this->assertEqual(6, $files[0]->size); // "blabla" | ||||
|         $this->assertEqual("second\n", $files[0]->log); | ||||
|     } | ||||
|  | ||||
|     public function testIsValidRevision() | ||||
|     { | ||||
|         $this->assertTrue($this->mtnInstance->isValidRevision("t:release-1.0")); | ||||
|         $this->assertFalse($this->mtnInstance->isValidRevision("abcdef12345")); | ||||
|     } | ||||
| } | ||||
| @@ -317,6 +317,148 @@ class IDF_Views_Admin | ||||
|                                                      ), | ||||
|                                                $request); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Usher servers overview | ||||
|      * | ||||
|      */ | ||||
|     public $usher_precond = array('Pluf_Precondition::staffRequired'); | ||||
|     public function usher($request, $match) | ||||
|     { | ||||
|         $title = __('Usher management'); | ||||
|         $servers = array(); | ||||
|         foreach (IDF_Scm_Monotone_Usher::getServerList() as $server) { | ||||
|             $servers[] = (object)array( | ||||
|                 "name" => $server, | ||||
|                 "status" => IDF_Scm_Monotone_Usher::getStatus($server), | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return Pluf_Shortcuts_RenderToResponse( | ||||
|             'idf/gadmin/usher/index.html', | ||||
|             array( | ||||
|                  'page_title' => $title, | ||||
|                  'servers' => $servers, | ||||
|                  ), | ||||
|             $request | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Usher control | ||||
|      * | ||||
|      */ | ||||
|     public $usherControl_precond = array('Pluf_Precondition::staffRequired'); | ||||
|     public function usherControl($request, $match) | ||||
|     { | ||||
|         $title = __('Usher control'); | ||||
|         $action = $match[1]; | ||||
|  | ||||
|         if (!empty($action)) { | ||||
|             if (!in_array($action, array('reload', 'shutdown', 'startup'))) { | ||||
|                 throw new Pluf_HTTP_Error404(); | ||||
|             } | ||||
|  | ||||
|             $msg = null; | ||||
|             if ($action == 'reload') { | ||||
|                 IDF_Scm_Monotone_Usher::reload(); | ||||
|                 $msg = __('Usher configuration has been reloaded'); | ||||
|             } | ||||
|             else if ($action == 'shutdown') { | ||||
|                 IDF_Scm_Monotone_Usher::shutDown(); | ||||
|                 $msg = __('Usher has been shut down'); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 IDF_Scm_Monotone_Usher::startUp(); | ||||
|                 $msg = __('Usher has been started up'); | ||||
|             } | ||||
|  | ||||
|             $request->user->setMessage($msg); | ||||
|             $url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usherControl', array('')); | ||||
|             return new Pluf_HTTP_Response_Redirect($url); | ||||
|         } | ||||
|  | ||||
|         return Pluf_Shortcuts_RenderToResponse( | ||||
|             'idf/gadmin/usher/control.html', | ||||
|             array( | ||||
|                  'page_title' => $title, | ||||
|                  'status' => IDF_Scm_Monotone_Usher::getStatus(), | ||||
|                  ), | ||||
|             $request | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Usher control | ||||
|      * | ||||
|      */ | ||||
|     public $usherServerControl_precond = array('Pluf_Precondition::staffRequired'); | ||||
|     public function usherServerControl($request, $match) | ||||
|     { | ||||
|         $server = $match[1]; | ||||
|         if (!in_array($server, IDF_Scm_Monotone_Usher::getServerList())) { | ||||
|             throw new Pluf_HTTP_Error404(); | ||||
|         } | ||||
|  | ||||
|         $action = $match[2]; | ||||
|         if (!in_array($action, array('start', 'stop', 'kill'))) { | ||||
|             throw new Pluf_HTTP_Error404(); | ||||
|         } | ||||
|  | ||||
|         $msg = null; | ||||
|         if ($action == 'start') { | ||||
|             IDF_Scm_Monotone_Usher::startServer($server); | ||||
|             $msg = sprintf(__('The server "%s" has been started'), $server); | ||||
|         } | ||||
|         else if ($action == 'stop') { | ||||
|             IDF_Scm_Monotone_Usher::stopServer($server); | ||||
|             $msg = sprintf(__('The server "%s" has been stopped'), $server); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             IDF_Scm_Monotone_Usher::killServer($server); | ||||
|             $msg = sprintf(__('The server "%s" has been killed'), $server); | ||||
|         } | ||||
|  | ||||
|         $request->user->setMessage($msg); | ||||
|         $url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usher'); | ||||
|         return new Pluf_HTTP_Response_Redirect($url); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Open connections for a configured server | ||||
|      * | ||||
|      */ | ||||
|     public $usherServerConnections_precond = array('Pluf_Precondition::staffRequired'); | ||||
|     public function usherServerConnections($request, $match) | ||||
|     { | ||||
|         $server = $match[1]; | ||||
|         if (!in_array($server, IDF_Scm_Monotone_Usher::getServerList())) { | ||||
|             throw new Pluf_HTTP_Error404(); | ||||
|         } | ||||
|  | ||||
|         $title = sprintf(__('Open connections for "%s"'), $server); | ||||
|  | ||||
|         $connections = IDF_Scm_Monotone_Usher::getConnectionList($server); | ||||
|         if (count($connections) == 0) { | ||||
|             $request->user->setMessage(sprintf( | ||||
|                __('no connections for server "%s"'), $server | ||||
|             )); | ||||
|             $url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usher'); | ||||
|             return new Pluf_HTTP_Response_Redirect($url); | ||||
|         } | ||||
|  | ||||
|         return Pluf_Shortcuts_RenderToResponse( | ||||
|             'idf/gadmin/usher/connections.html', | ||||
|             array( | ||||
|                  'page_title' => $title, | ||||
|                  'server' => $server, | ||||
|                  'connections' => $connections, | ||||
|                  ), | ||||
|             $request | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function IDF_Views_Admin_bool($field, $item) | ||||
|   | ||||
| @@ -520,6 +520,7 @@ class IDF_Views_Project | ||||
|                          'git' => __('git'), | ||||
|                          'svn' => __('Subversion'), | ||||
|                          'mercurial' => __('mercurial'), | ||||
|                          'mtn' => __('monotone'), | ||||
|                          ); | ||||
|         $repository_type = $options[$scm]; | ||||
|         return Pluf_Shortcuts_RenderToResponse('idf/admin/source.html', | ||||
|   | ||||
| @@ -37,10 +37,9 @@ class IDF_Views_Source | ||||
|     public static $supportedExtenstions = array( | ||||
|               'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cc', | ||||
|               'config', 'cpp', 'cs', 'csh',	'csproj', 'css', 'cv', 'cyc', | ||||
|               'html', 'html', 'java', 'js', 'm', 'master', 'pch', 'perl', 'php', | ||||
|               'pl', 'plist', 'pm', 'py', 'rb', 'sh', 'sitemap', 'skin', 'sln',  | ||||
|               'svc', 'vala', 'vb', 'vbproj', 'wsdl', 'xhtml', 'xml', 'xsd',  | ||||
|               'xsl', 'xslt'); | ||||
|               'html', 'html', 'java', 'js', 'master', 'perl', 'php', 'pl', | ||||
|               'pm', 'py', 'rb', 'sh', 'sitemap', 'skin', 'sln', 'svc', 'vala', | ||||
|               'vb', 'vbproj', 'wsdl', 'xhtml', 'xml', 'xsd', 'xsl', 'xslt'); | ||||
|  | ||||
|     /** | ||||
|      * Display help on how to checkout etc. | ||||
| @@ -309,7 +308,7 @@ class IDF_Views_Source | ||||
|         $in_branches = $scm->inBranches($cobject->commit, ''); | ||||
|         $tags = $scm->getTags(); | ||||
|         $in_tags = $scm->inTags($cobject->commit, ''); | ||||
|         return Pluf_Shortcuts_RenderToResponse('idf/source/commit.html', | ||||
|         return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/commit.html', | ||||
|                                                array( | ||||
|                                                      'page_title' => $page_title, | ||||
|                                                      'title' => $title, | ||||
| @@ -598,3 +597,16 @@ function IDF_Views_Source_PrettySizeSimple($size) | ||||
|     return Pluf_Utils::prettySize($size); | ||||
| } | ||||
|  | ||||
| function IDF_Views_Source_ShortenString($string, $length) | ||||
| { | ||||
|     $ellipse = "..."; | ||||
|     $length = max(strlen($ellipse) + 2, $length); | ||||
|     $preflen = ceil($length / 10); | ||||
|  | ||||
|     if (mb_strlen($string) < $length) | ||||
|         return $string; | ||||
|  | ||||
|     return substr($string, 0, $preflen).$ellipse. | ||||
|            substr($string, -($length - $preflen - mb_strlen($ellipse))); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -134,7 +134,7 @@ class IDF_Views_User | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Delete a SSH key. | ||||
|      * Delete a public key. | ||||
|      * | ||||
|      * This is redirecting to the preferences | ||||
|      */ | ||||
| @@ -148,7 +148,7 @@ class IDF_Views_User | ||||
|                 return new Pluf_HTTP_Response_Forbidden($request); | ||||
|             } | ||||
|             $key->delete(); | ||||
|             $request->user->setMessage(__('The SSH key has been deleted.')); | ||||
|             $request->user->setMessage(__('The public key has been deleted.')); | ||||
|         } | ||||
|         return new Pluf_HTTP_Response_Redirect($url); | ||||
|     } | ||||
|   | ||||
| @@ -73,6 +73,90 @@ $cfg['git_write_remote_url'] = 'git@localhost:%s.git'; | ||||
| $cfg['svn_repositories'] = 'file:///home/svn/repositories/%s'; | ||||
| $cfg['svn_remote_url'] = 'http://localhost/svn/%s'; | ||||
|  | ||||
| # Path to the monotone binary | ||||
| $cfg['mtn_path'] = 'mtn'; | ||||
| # Additional options for the started monotone process | ||||
| $cfg['mtn_opts'] = array('--no-workspace', '--norc'); | ||||
| # | ||||
| # You can setup monotone for use with indefero in two ways: | ||||
| # | ||||
| # 1) One database for everything: | ||||
| #    Set 'mtn_repositories' below to a fixed database path, such as | ||||
| #    '/home/mtn/repositories/all_projects.mtn' | ||||
| # | ||||
| #    Pro: - easy to setup and to manage | ||||
| #    Con: - while read access can be configured per-branch, | ||||
| #           granting write access rights to a user means that | ||||
| #           he can write anything in the global database | ||||
| #         - database lock problem: the database from which | ||||
| #           indefero reads its data cannot be used to serve the | ||||
| #           contents to the users, as the serve process locks | ||||
| #           the database | ||||
| # | ||||
| # 2) One database for every project with 'usher': | ||||
| #    Set 'mtn_remote_url' below to a string which matches your setup. | ||||
| #    Again, the '%s' placeholder will be expanded to the project's | ||||
| #    short name. Note that 'mtn_remote_url' is used as internal | ||||
| #    URI (to access the data for indefero) as well as external URI | ||||
| #    (for end users) at the same time. | ||||
| # | ||||
| #    Then download and configure 'usher' | ||||
| #    (mtn clone mtn://monotone.ca?net.venge.monotone.contrib.usher) | ||||
| #    which acts as proxy in front of all single project databases. | ||||
| #    Usher's server names should be mapped to the project's short names, | ||||
| #    so you end up with something like this for every project: | ||||
| # | ||||
| #       server  "project" | ||||
| #       local   "-d" "/home/mtn/repositories/project.mtn" "*" | ||||
| # | ||||
| #    Alternatively if you assign every project a unique DNS such as | ||||
| #    'project.my-hosting.biz', you can also configure it like this: | ||||
| # | ||||
| #       host    "project.my-hosting.biz" | ||||
| #       local   "-d" "/home/mtn/repositories/project.mtn" "*" | ||||
| # | ||||
| #    Pro: - read and write access can be granted per project | ||||
| #         - no database locking issues | ||||
| #         - one public server running on the one well-known port | ||||
| #    Con: - harder to setup | ||||
| # | ||||
| # Usher can also be used to forward sync requests to remote servers, | ||||
| # please consult its README file for more information. | ||||
| # | ||||
| # monotone also allows to use SSH as transport protocol, so if you do not plan | ||||
| # to setup a netsync server as described above, then just enter a URI like | ||||
| # 'ssh://my-host.biz/home/mtn/repositories/%s.mtn' in 'mtn_remote_url'. | ||||
| # | ||||
| $cfg['mtn_repositories'] = '/home/mtn/repositories/%s.mtn'; | ||||
| $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s'; | ||||
| # | ||||
| # Whether the particular database(s) are accessed locally (via automate stdio) | ||||
| # or remotely (via automate remote_stdio). 'remote' is the default for | ||||
| # netsync setups, while 'local' access should be choosed for ssh access. | ||||
| # | ||||
| # Note that you need to setup the hook 'get_remote_automate_permitted' for | ||||
| # each remotely accessible database. A full HOWTO set this up is beyond this | ||||
| # scope, please refer to the documentation of monotone and / or ask on the | ||||
| # mailing list (monotone-users@nongnu.org) or IRC channel | ||||
| # (irc.oftc.net/#monotone) | ||||
| # | ||||
| $cfg['mtn_db_access'] = 'remote'; | ||||
| # | ||||
| # If configured, this allows basic control of a running usher process | ||||
| # via the forge administration | ||||
| # | ||||
| # 'host' and 'port' must be set to the specific bits from usher's | ||||
| # configured 'adminaddr', 'user' and 'pass' must match the values set for | ||||
| # the configured 'userpass' combination | ||||
| # | ||||
| #$cfg['mtn_usher'] = array( | ||||
| #    'host' => 'localhost', | ||||
| #    'port' => 12345, | ||||
| #    'user' => 'admin', | ||||
| #    'pass' => 'admin', | ||||
| #); | ||||
| # | ||||
|  | ||||
| # Mercurial repositories path | ||||
| #$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s'; | ||||
| #$cfg['mercurial_remote_url'] = 'http://projects.ceondo.com/hg/%s'; | ||||
| @@ -209,6 +293,7 @@ $cfg['languages'] = array('en', 'fr'); | ||||
| $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git', | ||||
|                             'svn' => 'IDF_Scm_Svn', | ||||
|                             'mercurial' => 'IDF_Scm_Mercurial', | ||||
|                             'mtn' => 'IDF_Scm_Monotone', | ||||
|                             ); | ||||
|  | ||||
| # If you want to use another memtypes database | ||||
|   | ||||
| @@ -386,6 +386,29 @@ $ctl[] = array('regex' => '#^/admin/users/(\d+)/$#', | ||||
|                'model' => 'IDF_Views_Admin', | ||||
|                'method' => 'userUpdate'); | ||||
|  | ||||
| if (Pluf::f("mtn_usher", null) !== null) | ||||
| { | ||||
|     $ctl[] = array('regex' => '#^/admin/usher/$#', | ||||
|                    'base' => $base, | ||||
|                    'model' => 'IDF_Views_Admin', | ||||
|                    'method' => 'usher'); | ||||
|  | ||||
|     $ctl[] = array('regex' => '#^/admin/usher/control/(.*)$#', | ||||
|                    'base' => $base, | ||||
|                    'model' => 'IDF_Views_Admin', | ||||
|                    'method' => 'usherControl'); | ||||
|  | ||||
|     $ctl[] = array('regex' => '#^/admin/usher/server/(.+)/control/(.+)$#', | ||||
|                    'base' => $base, | ||||
|                    'model' => 'IDF_Views_Admin', | ||||
|                    'method' => 'usherServerControl'); | ||||
|  | ||||
|     $ctl[] = array('regex' => '#^/admin/usher/server/(.+)/connections/$#', | ||||
|                    'base' => $base, | ||||
|                    'model' => 'IDF_Views_Admin', | ||||
|                    'method' => 'usherServerConnections'); | ||||
| } | ||||
|  | ||||
| // ---------- UTILITY VIEWS ------------------------------- | ||||
|  | ||||
| $ctl[] = array('regex' => '#^/register/$#', | ||||
|   | ||||
| @@ -43,18 +43,21 @@ | ||||
| <div id="main-tabs"> | ||||
|   <a href="{url 'IDF_Views_Admin::projects'}"{block tabprojects}{/block}>{trans 'Projects'}</a> | ||||
|   <a href="{url 'IDF_Views_Admin::users'}"{block tabusers}{/block}>{trans 'People'}</a> | ||||
|   {if $usherConfigured} | ||||
|   <a href="{url 'IDF_Views_Admin::usher'}"{block tabusher}{/block}>{trans 'Usher'}</a> | ||||
|   {/if} | ||||
| </div> | ||||
| <div id="sub-tabs">{block subtabs}{/block}</div> | ||||
| </div> | ||||
| 	  <h1 id="title" class="title">{block title}{$page_title}{/block}</h1> | ||||
|       <h1 id="title" class="title">{block title}{$page_title}{/block}</h1> | ||||
|   </div> | ||||
|   <div id="bd"> | ||||
|     <div id="yui-main"> | ||||
|       <div class="yui-b"> | ||||
| 	<div class="yui-g">  | ||||
|     <div class="yui-g"> | ||||
|           {if $user and $user.id}{getmsgs $user}{/if} | ||||
| 	  <div class="content">{block body}{/block}</div> | ||||
| 	</div> | ||||
|       <div class="content">{block body}{/block}</div> | ||||
|     </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="yui-b context">{block context}{/block}</div> | ||||
|   | ||||
| @@ -52,6 +52,13 @@ | ||||
| {$form.f.svn_password|unsafe} | ||||
| </td> | ||||
| </tr> | ||||
| <tr class="mtn-form"> | ||||
| <th><strong>{$form.f.mtn_master_branch.labelTag}:</strong></th> | ||||
| <td>{if $form.f.mtn_master_branch.errors}{$form.f.mtn_master_branch.fieldErrors}{/if} | ||||
| {$form.f.mtn_master_branch|unsafe}<br /> | ||||
| <span class="helptext">{$form.f.mtn_master_branch.help_text}</span> | ||||
| </td> | ||||
| </tr> | ||||
| <tr> | ||||
| <th>{$form.f.template.labelTag}</th> | ||||
| <td>{if $form.f.template.errors}{$form.f.template.fieldErrors}{/if} | ||||
| @@ -119,12 +126,22 @@ $(document).ready(function() { | ||||
|     if ($("#id_scm option:selected").val() != "svn") { | ||||
|         $(".svn-form").hide(); | ||||
|     } | ||||
|     // Hide if not mtn | ||||
|     if ($("#id_scm option:selected").val() != "mtn") { | ||||
|         $(".mtn-form").hide(); | ||||
|     } | ||||
|     $("#id_scm").change(function () { | ||||
|         if ($("#id_scm option:selected").val() == "svn") { | ||||
|             $(".svn-form").show(); | ||||
|         } else { | ||||
|             $(".svn-form").hide(); | ||||
|         } | ||||
|         if ($("#id_scm option:selected").val() == "mtn") { | ||||
|             $(".mtn-form").show(); | ||||
|         } else { | ||||
|             $(".mtn-form").hide(); | ||||
|         } | ||||
|  | ||||
|     }); | ||||
|     // Hide if not svn | ||||
|     if ($("#id_template option:selected").val() == "--") { | ||||
|   | ||||
| @@ -43,10 +43,10 @@ | ||||
| </td> | ||||
| </tr> | ||||
| <tr> | ||||
| <th>{$form.f.ssh_key.labelTag}:</th> | ||||
| <td>{if $form.f.ssh_key.errors}{$form.f.ssh_key.fieldErrors}{/if} | ||||
| {$form.f.ssh_key|unsafe}<br /> | ||||
| <span class="helptext">{$form.f.ssh_key.help_text}</span> | ||||
| <th>{$form.f.public_key.labelTag}:</th> | ||||
| <td>{if $form.f.public_key.errors}{$form.f.public_key.fieldErrors}{/if} | ||||
| {$form.f.public_key|unsafe}<br /> | ||||
| <span class="helptext">{$form.f.public_key.help_text}</span> | ||||
| </td> | ||||
| </tr> | ||||
| <tr> | ||||
|   | ||||
							
								
								
									
										6
									
								
								src/IDF/templates/idf/gadmin/usher/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/IDF/templates/idf/gadmin/usher/base.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| {extends "idf/gadmin/base.html"} | ||||
| {block tabusher} class="active"{/block} | ||||
| {block subtabs} | ||||
| <a {if $inUsher}class="active" {/if}href="{url 'IDF_Views_Admin::usher'}">{trans 'Configured servers'}</a> | | ||||
| <a {if $inUsherControl}class="active" {/if}href="{url 'IDF_Views_Admin::usherControl', array('')}">{trans 'Usher control'}</a> | ||||
| {/block} | ||||
							
								
								
									
										19
									
								
								src/IDF/templates/idf/gadmin/usher/connections.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/IDF/templates/idf/gadmin/usher/connections.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| {extends "idf/gadmin/usher/base.html"} | ||||
|  | ||||
| {block docclass}yui-t3{assign $inUsherServerConnections=true}{/block} | ||||
|  | ||||
| {block body} | ||||
| <table class="recent-issues"> | ||||
| <tr> | ||||
|     <th>{trans "address"}</th> | ||||
|     <th>{trans "port"}</th> | ||||
| </tr> | ||||
| {foreach $connections as $connection} | ||||
| <tr> | ||||
|     <td>{$connection.address}</td> | ||||
|     <td>{$connection.port}</td> | ||||
| </tr> | ||||
| {/foreach} | ||||
| </table> | ||||
| {/block} | ||||
|  | ||||
							
								
								
									
										32
									
								
								src/IDF/templates/idf/gadmin/usher/control.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/IDF/templates/idf/gadmin/usher/control.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| {extends "idf/gadmin/usher/base.html"} | ||||
|  | ||||
| {block docclass}yui-t3{assign $inUsherControl=true}{/block} | ||||
|  | ||||
| {block body} | ||||
| <p> | ||||
| {trans 'current server status:'} {$status} | | ||||
| {if $status == "SHUTDOWN"} | ||||
|    <a href="{url 'IDF_Views_Admin::usherControl', array('startup')}">{trans 'startup'}</a> | ||||
| {else} | ||||
|    <a href="{url 'IDF_Views_Admin::usherControl', array('shutdown')}">{trans 'shutdown'}</a> | ||||
| {/if} | ||||
| </p> | ||||
|  | ||||
| <p>{trans 'reload server configuration:'} | ||||
|    <a href="{url 'IDF_Views_Admin::usherControl', array('reload')}">{trans 'reload'}</a> | ||||
| </p> | ||||
| {/block} | ||||
|  | ||||
| {block context} | ||||
| <div class="issue-submit-info"> | ||||
| <p><strong>{trans 'Status explanation'}</strong></p> | ||||
| <ul> | ||||
| <li>ACTIVE n: {trans 'active with n total open connections'}</li> | ||||
| <li>WAITING: {trans 'waiting for new connections'}</li> | ||||
| <li>SHUTTINGDOWN: {trans 'usher is being shut down, not accepting connections'}</li> | ||||
| <li>SHUTDOWN: {trans 'usher is shut down, all local servers are stopped and not accepting connections'}</li> | ||||
| </ul> | ||||
|  | ||||
| </div> | ||||
| {/block} | ||||
|  | ||||
							
								
								
									
										52
									
								
								src/IDF/templates/idf/gadmin/usher/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/IDF/templates/idf/gadmin/usher/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| {extends "idf/gadmin/usher/base.html"} | ||||
|  | ||||
| {block docclass}yui-t3{assign $inUsher=true}{/block} | ||||
|  | ||||
| {block body} | ||||
| <table class="recent-issues"> | ||||
| <tr> | ||||
|     <th>{trans "server name"}</th> | ||||
|     <th>{trans "status"}</th> | ||||
|     <th>{trans "action"}</th> | ||||
| </tr> | ||||
| {foreach $servers as $server} | ||||
| <tr> | ||||
|     <td>{$server.name}</td> | ||||
|     <td>{$server.status}</td> | ||||
|     <td> | ||||
|         {if preg_match("/ACTIVE|RUNNING|SLEEPING/", $server.status)} | ||||
|            <a href="{url 'IDF_Views_Admin::usherServerControl', array($server.name, 'stop')}"> | ||||
|              {trans 'stop'}</a> | ||||
|         {elseif $server.status == "STOPPED"} | ||||
|            <a href="{url 'IDF_Views_Admin::usherServerControl', array($server.name, 'start')}"> | ||||
|              {trans 'start'}</a> | ||||
|         {/if} | ||||
|         {if preg_match("/ACTIVE|WAITING|SLEEPING|STOPPING/", $server.status)} | ||||
|           | <a href="{url 'IDF_Views_Admin::usherServerControl', array($server.name, 'kill')}"> | ||||
|               {trans 'kill'}</a> | ||||
|         {/if} | ||||
|         {if preg_match("/STOPPING|ACTIVE/", $server.status)} | ||||
|           | <a href="{url 'IDF_Views_Admin::usherServerConnections', array($server.name)}"> | ||||
|               {trans 'active connections'}</a> | ||||
|         {/if} | ||||
| </tr> | ||||
| {/foreach} | ||||
| </table> | ||||
| {/block} | ||||
|  | ||||
| {block context} | ||||
| <div class="issue-submit-info"> | ||||
| <p><strong>{trans 'Status explanation'}</strong></p> | ||||
| <ul> | ||||
| <li>REMOTE: {trans 'remote server without open connections'}</li> | ||||
| <li>ACTIVE n: {trans 'server with n open connections'}</li> | ||||
| <li>WAITING: {trans 'local server running, without open connections'}</li> | ||||
| <li>SLEEPING: {trans 'local server not running, waiting for connections'}</li> | ||||
| <li>STOPPING n: {trans 'local server is about to stop, n connections still open'}</li> | ||||
| <li>STOPPED: {trans 'local server not running, not accepting connections'}</li> | ||||
| <li>SHUTDOWN: {trans 'usher is shut down, not running and not accepting connections'}</li> | ||||
| </ul> | ||||
|  | ||||
| </div> | ||||
| {/block} | ||||
|  | ||||
| @@ -37,33 +37,6 @@ | ||||
| {/if} | ||||
|  | ||||
| {/block} | ||||
| {block context} | ||||
| {if $scm != 'svn'} | ||||
| <p><strong>{trans 'Branches:'}</strong><br/> | ||||
| {foreach $branches as $branch => $path} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)} | ||||
| <span class="label{if in_array($branch, $tree_in)} active{/if}"><a href="{$url}" class="label">{$branch}</a></span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {if $tags} | ||||
| <p><strong>{trans 'Tags:'}</strong><br/> | ||||
| {foreach $tags as $tag => $path} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $tag)} | ||||
| <span class="label{if in_array($tag, $tags_in)} active{/if}"><a href="{$url}" class="label">{if $path}{$path}{else}{$tag}{/if}</a></span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {/if} | ||||
| {else} | ||||
| <form class="star" action="{url 'IDF_Views_Source_Svn::changelogRev', array($project.shortname)}" method="get"> | ||||
| <p><strong>{trans 'Revision:'}</strong> {$commit}</p> | ||||
| <p> | ||||
|     <input accesskey="4" type="text" value="{$commit}" name="rev" size="5"/> | ||||
|     <input type="submit" name="s" value="{trans 'Go to revision'}"/> | ||||
| </p> | ||||
| </form> | ||||
| {/if} | ||||
| {/block} | ||||
|  | ||||
| {block javascript} | ||||
| <script type="text/javascript" src="{media '/idf/js/prettify.js'}"></script> | ||||
| <script type="text/javascript"> | ||||
|   | ||||
							
								
								
									
										18
									
								
								src/IDF/templates/idf/source/git/commit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/IDF/templates/idf/source/git/commit.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| {extends "idf/source/commit.html"} | ||||
| {block context} | ||||
| <p><strong>{trans 'Branches:'}</strong><br/> | ||||
| {foreach $branches as $branch => $path} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)} | ||||
| <span class="label{if in_array($branch, $tree_in)} active{/if}"><a href="{$url}" class="label">{$branch}</a></span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {if $tags} | ||||
| <p><strong>{trans 'Tags:'}</strong><br/> | ||||
| {foreach $tags as $tag => $path} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $tag)} | ||||
| <span class="label{if in_array($tag, $tags_in)} active{/if}"><a href="{$url}" class="label">{if $path}{$path}{else}{$tag}{/if}</a></span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {/if} | ||||
| {/block} | ||||
|  | ||||
							
								
								
									
										18
									
								
								src/IDF/templates/idf/source/mercurial/commit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/IDF/templates/idf/source/mercurial/commit.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| {extends "idf/source/commit.html"} | ||||
| {block context} | ||||
| <p><strong>{trans 'Branches:'}</strong><br/> | ||||
| {foreach $branches as $branch => $path} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)} | ||||
| <span class="label{if in_array($branch, $tree_in)} active{/if}"><a href="{$url}" class="label">{$branch}</a></span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {if $tags} | ||||
| <p><strong>{trans 'Tags:'}</strong><br/> | ||||
| {foreach $tags as $tag => $path} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $tag)} | ||||
| <span class="label{if in_array($tag, $tags_in)} active{/if}"><a href="{$url}" class="label">{if $path}{$path}{else}{$tag}{/if}</a></span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {/if} | ||||
| {/block} | ||||
|  | ||||
							
								
								
									
										25
									
								
								src/IDF/templates/idf/source/mtn/changelog.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/IDF/templates/idf/source/mtn/changelog.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| {extends "idf/source/changelog.html"} | ||||
| {block context} | ||||
| <p><strong>{trans 'Branches:'}</strong><br/> | ||||
| {foreach $branches as $selector => $branch} | ||||
| {aurl 'url', 'IDF_Views_Source::changeLog', array($project.shortname, $selector)} | ||||
| <span class="label{if in_array($selector, $tree_in)} active{/if}"> | ||||
|     <a href="{$url}" class="label" title="{$branch}"> | ||||
|         {$branch|shorten:24} | ||||
|     </a> | ||||
| </span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {if $tags} | ||||
| <p><strong>{trans 'Tags:'}</strong><br/> | ||||
| {foreach $tags as $selector => $tag} | ||||
| {aurl 'url', 'IDF_Views_Source::changeLog', array($project.shortname, $selector)} | ||||
| <span class="label{if in_array($selector, $tags_in)} active{/if}"> | ||||
|     <a href="{$url}" class="label" title="{$tag}"> | ||||
|         {$tag|shorten:24} | ||||
|     </a> | ||||
| </span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {/if} | ||||
| {/block} | ||||
							
								
								
									
										26
									
								
								src/IDF/templates/idf/source/mtn/commit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/IDF/templates/idf/source/mtn/commit.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| {extends "idf/source/commit.html"} | ||||
| {block context} | ||||
| <p><strong>{trans 'Branches:'}</strong><br/> | ||||
| {foreach $branches as $selector => $branch} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)} | ||||
| <span class="label{if in_array($branch, $tree_in)} active{/if}"> | ||||
|     <a href="{$url}" class="label" title="{$branch}"> | ||||
|         {$branch|shorten:25} | ||||
|     </a> | ||||
| </span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {if $tags} | ||||
| <p><strong>{trans 'Tags:'}</strong><br/> | ||||
| {foreach $tags as $selector => $tag} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)} | ||||
| <span class="label{if in_array($tag, $tags_in)} active{/if}"> | ||||
|     <a href="{$url}" class="label" title="{$tag}"> | ||||
|         {$tag|shorten:25} | ||||
|     </a> | ||||
| </span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {/if} | ||||
| {/block} | ||||
|  | ||||
							
								
								
									
										51
									
								
								src/IDF/templates/idf/source/mtn/file.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/IDF/templates/idf/source/mtn/file.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| {extends "idf/source/base.html"} | ||||
| {block extraheader}<link rel="stylesheet" type="text/css" href="{media '/idf/css/prettify.css'}" />{/block} | ||||
| {block docclass}yui-t1{assign $inSourceTree=true}{/block} | ||||
| {block body} | ||||
| <h2 class="top"><a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $commit)}">{trans 'Root'}</a><span class="sep">/</span>{if $breadcrumb}{$breadcrumb|safe}{/if}</h2> | ||||
|  | ||||
| <table class="code" summary=" "> | ||||
| {if !$tree_in and !$tags_in} | ||||
| {aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)} | ||||
| <tfoot> | ||||
| <tr><th colspan="2">{blocktrans}Source at commit <a class="mono" href="{$url}">{$commit}</a> created {$cobject.date|dateago}.{/blocktrans}<br/> | ||||
| <span class="smaller">{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans}</span> | ||||
| </th></tr> | ||||
| </tfoot> | ||||
| {/if} | ||||
| <tbody> | ||||
| {$file} | ||||
| </tbody> | ||||
| </table> | ||||
| {aurl 'url', 'IDF_Views_Source::getFile', array($project.shortname, $commit, $fullpath)} | ||||
| <p class="right soft"><a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}">{trans 'Download this file'}</a></p> | ||||
|  | ||||
| {/block} | ||||
| {block context} | ||||
| <p><strong>{trans 'Branches:'}</strong><br/> | ||||
| {foreach $branches as $selector => $branch} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)} | ||||
| <span class="label{if in_array($branch, $tree_in)} active{/if}"> | ||||
|     <a href="{$url}" class="label" title="{$branch}"> | ||||
|         {$branch|shorten:25} | ||||
|     </a> | ||||
| </span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {if $tags} | ||||
| <p><strong>{trans 'Tags:'}</strong><br/> | ||||
| {foreach $tags as $selector => $tag} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)} | ||||
| <span class="label{if in_array($tag, $tags_in)} active{/if}"> | ||||
|     <a href="{$url}" class="label" title="{$tag}"> | ||||
|         {$tag|shorten:25} | ||||
|     </a> | ||||
| </span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {/if} | ||||
| {/block} | ||||
| {block javascript} | ||||
| <script type="text/javascript" src="{media '/idf/js/prettify.js'}"></script> | ||||
| <script type="text/javascript">prettyPrint();</script> | ||||
| {/block} | ||||
							
								
								
									
										34
									
								
								src/IDF/templates/idf/source/mtn/help.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/IDF/templates/idf/source/mtn/help.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| {extends "idf/source/base.html"} | ||||
| {block docclass}yui-t2{assign $inHelp=true}{/block} | ||||
| {block body} | ||||
|  | ||||
| <p>{blocktrans}The team behind {$project} is using | ||||
| the <strong>monotone</strong> software to manage the source | ||||
| code.{/blocktrans}</p> | ||||
|  | ||||
| <h3>{trans 'Command-Line Access'}</h3> | ||||
|  | ||||
| <p><kbd>mtn clone {$project.getSourceAccessUrl()}</kbd></p> | ||||
|  | ||||
| {if $isOwner or $isMember} | ||||
| <h3>{trans 'First Commit'}</h3> | ||||
|  | ||||
| <p>{blocktrans}To make a first commit in the repository, perform the following steps:{/blocktrans}</p> | ||||
|  | ||||
| <pre> | ||||
| mtn setup -b {$project.getConf().getVal('mtn_master_branch', 'your-branch')} . | ||||
| mtn add -R . | ||||
| mtn commit -m "initial import" | ||||
| mtn push {$project.getSourceAccessUrl()} | ||||
| </pre> | ||||
|  | ||||
| {/if} | ||||
|  | ||||
| {/block} | ||||
| {block context} | ||||
| <div class="issue-submit-info"> | ||||
| <p>{blocktrans}Find here more details on how to access {$project} source code.{/blocktrans}</p> | ||||
| </div> | ||||
| {/block} | ||||
|  | ||||
|  | ||||
							
								
								
									
										80
									
								
								src/IDF/templates/idf/source/mtn/tree.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/IDF/templates/idf/source/mtn/tree.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| {extends "idf/source/base.html"} | ||||
| {block docclass}yui-t1{assign $inSourceTree=true}{/block} | ||||
| {block body} | ||||
| <h2 class="top"><a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $commit)}">{trans 'Root'}</a><span class="sep">/</span>{if $breadcrumb}{$breadcrumb|safe}{/if}</h2> | ||||
|  | ||||
| <table summary="" class="tree-list"> | ||||
| <thead> | ||||
| <tr> | ||||
| <th colspan="2">{trans 'File'}</th> | ||||
| <th>{trans 'Age'}</th> | ||||
| <th>{trans 'Message'}</th> | ||||
| <th>{trans 'Size'}</th> | ||||
| </tr> | ||||
| </thead>{if !$tree_in and !$tags_in} | ||||
| {aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)} | ||||
| <tfoot> | ||||
| <tr><th colspan="5">{blocktrans}Source at commit <a class="mono" href="{$url}">{$commit}</a> created {$cobject.date|dateago}.{/blocktrans}<br/> | ||||
| <span class="smaller">{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans}</span> | ||||
| </th></tr> | ||||
| </tfoot> | ||||
| {/if}<tbody> | ||||
| {if $base} | ||||
| <tr> | ||||
| <td> </td> | ||||
| <td> | ||||
| <a href="{url 'IDF_Views_Source::tree', array($project.shortname, $commit, $prev)}">..</a></td> | ||||
| <td colspan="3"></td> | ||||
| </tr> | ||||
| {/if} | ||||
| {foreach $files as $file} | ||||
| {aurl 'url', 'IDF_Views_Source::tree', array($project.shortname, $commit, $file.efullpath)} | ||||
| <tr> | ||||
| <td class="fileicon"><img src="{media '/idf/img/'~$file.type~'.png'}" alt="{$file.type}" /></td> | ||||
| {if $file.type != 'extern'} | ||||
| <td{if $file.type == 'tree'} colspan="4"{/if}><a href="{$url}">{$file.file}</a></td>{else}<td><a href="#" title="{$file.hash}">{$file.file}</a></td>{/if} | ||||
| {if $file.type == 'blob'} | ||||
| {if isset($file.date) and $file.log != '----'} | ||||
| <td><span class="smaller">{$file.date|dateago:"without"}</span></td> | ||||
| <td><span class="smaller">{$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false}</span></td> | ||||
| {else}<td colspan="2"></td>{/if} | ||||
| <td>{$file.size|size}</td>{/if} | ||||
| {if $file.type == 'extern'} | ||||
| <td colspan="3">{$file.extern}</td> | ||||
| {/if} | ||||
| </tr> | ||||
| {/foreach} | ||||
| </tbody> | ||||
| </table> | ||||
| {aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)} | ||||
| <p class="right soft"> | ||||
| {* <a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}">{trans 'Download this version'}</a> {trans 'or'} *} | ||||
| <kbd>mtn clone {$project.getSourceAccessUrl($user, $commit)}</kbd> <a href="{url 'IDF_Views_Source::help', array($project.shortname)}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/help.png'}" alt="{trans 'Help'}" /></a> | ||||
| </p> | ||||
|  | ||||
|  | ||||
| {/block} | ||||
| {block context} | ||||
| <p><strong>{trans 'Branches:'}</strong><br/> | ||||
| {foreach $branches as $selector => $branch} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)} | ||||
| <span class="label{if in_array($selector, $tree_in)} active{/if}"> | ||||
|     <a href="{$url}" class="label" title="{$branch}"> | ||||
|         {$branch|shorten:24} | ||||
|     </a> | ||||
| </span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {if $tags} | ||||
| <p><strong>{trans 'Tags:'}</strong><br/> | ||||
| {foreach $tags as $selector => $tag} | ||||
| {aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $selector)} | ||||
| <span class="label{if in_array($selector, $tags_in)} active{/if}"> | ||||
|     <a href="{$url}" class="label" title="{$tag}"> | ||||
|         {$tag|shorten:24} | ||||
|     </a> | ||||
| </span><br/> | ||||
| {/foreach} | ||||
| </p> | ||||
| {/if} | ||||
| {/block} | ||||
							
								
								
									
										11
									
								
								src/IDF/templates/idf/source/svn/commit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/IDF/templates/idf/source/svn/commit.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| {extends "idf/source/commit.html"} | ||||
| {block context} | ||||
| <form class="star" action="{url 'IDF_Views_Source_Svn::changelogRev', array($project.shortname)}" method="get"> | ||||
| <p><strong>{trans 'Revision:'}</strong> {$commit}</p> | ||||
| <p> | ||||
|     <input accesskey="4" type="text" value="{$commit}" name="rev" size="5"/> | ||||
|     <input type="submit" name="s" value="{trans 'Go to revision'}"/> | ||||
| </p> | ||||
| </form> | ||||
| {/block} | ||||
|  | ||||
| @@ -54,10 +54,10 @@ | ||||
| </td> | ||||
| </tr> | ||||
| <tr> | ||||
| <th>{$form.f.ssh_key.labelTag}:</th> | ||||
| <td>{if $form.f.ssh_key.errors}{$form.f.ssh_key.fieldErrors}{/if} | ||||
| {$form.f.ssh_key|unsafe}<br /> | ||||
| <span class="helptext">{$form.f.ssh_key.help_text}</span> | ||||
| <th>{$form.f.public_key.labelTag}:</th> | ||||
| <td>{if $form.f.public_key.errors}{$form.f.public_key.fieldErrors}{/if} | ||||
| {$form.f.public_key|unsafe}<br /> | ||||
| <span class="helptext">{$form.f.public_key.help_text}</span> | ||||
| </td> | ||||
| </tr> | ||||
| <tr class="pass-info" id="extra-password"> | ||||
| @@ -82,7 +82,7 @@ | ||||
|  | ||||
| {if count($keys)} | ||||
| <table summary=" " class="recent-issues"> | ||||
| <tr><th colspan="2">{trans 'Your Current SSH Keys'}</th></tr> | ||||
| <tr><th colspan="2">{trans 'Your Current Public Keys'}</th></tr> | ||||
| {foreach $keys as $key}<tr><td> | ||||
| <span class="mono">{$key.showCompact()}</span></td><td> <form class="star" method="post" action="{url 'IDF_Views_User::deleteKey', array($key.id)}"><input type="image" src="{media '/idf/img/trash.png'}" name="submit" value="{trans 'Delete this key'}" /></form> | ||||
| </td> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user