<?php # Copyright (c) 2012 John Reese # Licensed under the MIT license if ( false === include_once( config_get( 'plugin_path' ) . 'Source/MantisSourcePlugin.class.php' ) ) { return; } class SourceSVNPlugin extends MantisSourcePlugin { public function register() { $this->name = lang_get( 'plugin_SourceSVN_title' ); $this->description = lang_get( 'plugin_SourceSVN_description' ); $this->version = '0.16'; $this->requires = array( 'MantisCore' => '1.2.0', 'Source' => '0.16', ); $this->author = 'John Reese'; $this->contact = 'jreese@leetcode.net'; $this->url = 'http://leetcode.net'; } public function config() { return array( 'svnpath' => '', 'svnargs' => '', 'svnssl' => false, 'winstart' => false, ); } public function errors() { return array( 'SVNPathInvalid' => 'Path to Subversion binary invalid or inaccessible', ); } public $type = 'svn'; public $configuration = true; public function show_type() { return lang_get( 'plugin_SourceSVN_svn' ); } public function show_changeset( $p_repo, $p_changeset ) { return "$p_changeset->branch r$p_changeset->revision"; } public function show_file( $p_repo, $p_changeset, $p_file ) { return $p_file->action . ' - ' . $p_file->filename; } public function url_repo( $p_repo, $p_changeset=null ) {} public function url_changeset( $p_repo, $p_changeset ) {} public function url_file( $p_repo, $p_changeset, $p_file ) {} public function url_diff( $p_repo, $p_changeset, $p_file ) {} public function update_repo_form( $p_repo ) { $t_svn_username = isset( $p_repo->info['svn_username'] ) ? $p_repo->info['svn_username'] : ''; $t_svn_password = isset( $p_repo->info['svn_password'] ) ? $p_repo->info['svn_password'] : ''; $t_standard_repo = isset( $p_repo->info['standard_repo'] ) ? $p_repo->info['standard_repo'] : ''; $t_trunk_path = isset( $p_repo->info['trunk_path'] ) ? $p_repo->info['trunk_path'] : ''; $t_branch_path = isset( $p_repo->info['branch_path'] ) ? $p_repo->info['branch_path'] : ''; $t_tag_path = isset( $p_repo->info['tag_path'] ) ? $p_repo->info['tag_path'] : ''; $t_ignore_paths = isset( $p_repo->info['ignore_paths'] ) ? $p_repo->info['ignore_paths'] : ''; ?> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_svn_username' ) ?></td> <td><input name="svn_username" maxlength="250" size="40" value="<?php echo string_attribute( $t_svn_username ) ?>"/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_svn_password' ) ?></td> <td><input name="svn_password" maxlength="250" size="40" value="<?php echo string_attribute( $t_svn_password ) ?>"/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_standard_repo' ) ?></td> <td><input name="standard_repo" type="checkbox" <?php echo ($t_standard_repo ? 'checked="checked"' : '') ?>/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_trunk_path' ) ?></td> <td><input name="trunk_path" maxlength="250" size="40" value="<?php echo string_attribute( $t_trunk_path ) ?>"/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_branch_path' ) ?></td> <td><input name="branch_path" maxlength="250" size="40" value="<?php echo string_attribute( $t_branch_path ) ?>"/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_tag_path' ) ?></td> <td><input name="tag_path" maxlength="250" size="40" value="<?php echo string_attribute( $t_tag_path ) ?>"/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_ignore_paths' ) ?></td> <td><input name="ignore_paths" type="checkbox" <?php echo ($t_ignore_paths ? 'checked="checked"' : '') ?>/></td> </tr> <?php } public function update_repo( $p_repo ) { $p_repo->info['svn_username'] = gpc_get_string( 'svn_username' ); $p_repo->info['svn_password'] = gpc_get_string( 'svn_password' ); $p_repo->info['standard_repo'] = gpc_get_bool( 'standard_repo', false ); $p_repo->info['trunk_path'] = gpc_get_string( 'trunk_path' ); $p_repo->info['branch_path'] = gpc_get_string( 'branch_path' ); $p_repo->info['tag_path'] = gpc_get_string( 'tag_path' ); $p_repo->info['ignore_paths'] = gpc_get_bool( 'ignore_paths', false ); return $p_repo; } private static $config_form_handled = false; public function update_config_form() { # Prevent more than one SVN class from outputting form elements. if ( !SourceSVNPlugin::$config_form_handled ) { SourceSVNPlugin::$config_form_handled = true; $t_svnpath = config_get( 'plugin_SourceSVN_svnpath', '' ); $t_svnargs = config_get( 'plugin_SourceSVN_svnargs', '' ); $t_svnssl = config_get( 'plugin_SourceSVN_svnssl', '' ); $t_winstart = config_get( 'plugin_SourceSVN_winstart', '' ); ?> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_svnpath' ) ?></td> <td><input name="plugin_SourceSVN_svnpath" value="<?php echo string_attribute( $t_svnpath ) ?>" size="40"/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_svnargs' ) ?></td> <td><input name="plugin_SourceSVN_svnargs" value="<?php echo string_attribute( $t_svnargs ) ?>" size="40"/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_svnssl' ) ?></td> <td><input name="plugin_SourceSVN_svnssl" type="checkbox" <?php if ( $t_svnssl ) echo 'checked="checked"' ?>/></td> </tr> <tr <?php echo helper_alternate_class() ?>> <td class="category"><?php echo lang_get( 'plugin_SourceSVN_winstart' ) ?></td> <td><input name="plugin_SourceSVN_winstart" type="checkbox" <?php if ( $t_winstart ) echo 'checked="checked"' ?>/></td> </tr> <?php } } public function update_config() { # Prevent more than one SVN class from handling form elements. if ( !SourceSVNPlugin::$config_form_handled ) { SourceSVNPlugin::$config_form_handled = true; $f_svnpath = gpc_get_string( 'plugin_SourceSVN_svnpath', '' ); $t_svnpath = config_get( 'plugin_SourceSVN_svnpath', '' ); $f_svnpath = rtrim( $f_svnpath, DIRECTORY_SEPARATOR ); if ( $f_svnpath != $t_svnpath ) { if ( is_blank( $f_svnpath ) ) { config_delete( 'plugin_SourceSVN_svnpath' ); } else { # be sure that the path is valid if ( ( $t_binary = SourceSVNPlugin::svn_binary( $f_svnpath, true ) ) != 'svn' ) { config_set( 'plugin_SourceSVN_svnpath', $f_svnpath ); } else { plugin_error( 'SVNPathInvalid', ERROR ); } } } $f_svnargs = gpc_get_string( 'plugin_SourceSVN_svnargs', '' ); if ( $f_svnargs != config_get( 'plugin_SourceSVN_svnargs', '' ) ) { config_set( 'plugin_SourceSVN_svnargs', $f_svnargs ); } $f_svnssl = gpc_get_bool( 'plugin_SourceSVN_svnssl', false ); if ( $f_svnssl != config_get( 'plugin_SourceSVN_svnssl', false ) ) { config_set( 'plugin_SourceSVN_svnssl', $f_svnssl ); } $f_winstart = gpc_get_bool( 'plugin_SourceSVN_winstart', false ); if ( $f_winstart != config_get( 'plugin_SourceSVN_winstart', false ) ) { config_set( 'plugin_SourceSVN_winstart', $f_winstart ); } } } public function commit( $p_repo, $p_data ) { if ( preg_match( '/(\d+)/', $p_data, $p_matches ) ) { $svn = $this->svn_call( $p_repo ); $t_url = $p_repo->url; $t_revision = $p_matches[1]; $t_svnlog = explode( "\n", shell_exec( "$svn log -v $t_url -r$t_revision" ) ); if ( SourceChangeset::exists( $p_repo->id, $t_revision ) ) { echo "Revision $t_revision already committed!\n"; return null; } return $this->process_svn_log( $p_repo, $t_svnlog ); } } public function import_full( $p_repo ) { $this->check_svn(); $svn = $this->svn_call( $p_repo ); $t_changeset_table = plugin_table( 'changeset', 'Source' ); $t_max_query = "SELECT revision FROM $t_changeset_table WHERE repo_id=" . db_param() . ' ORDER BY CAST( revision AS DECIMAL ) DESC'; $t_db_revision = db_result( db_query_bound( $t_max_query, array( $p_repo->id ), 1 ) ); $t_url = $p_repo->url; $t_rev = ( false === $t_db_revision ? 0 : $t_db_revision + 1 ); echo "<pre>"; while( true ) { echo "Requesting svn log for {$p_repo->name} starting with revision {$t_rev}...\n"; $t_svnlog = explode( "\n", shell_exec( "$svn log -v -r $t_rev:HEAD --limit 200 $t_url" ) ); $t_changesets = $this->process_svn_log( $p_repo, $t_svnlog ); # if an array is returned, processing is done if ( is_array( $t_changesets ) ) { echo "</pre>"; return $t_changesets; # if a number is returned, repeat from given revision } else if ( is_numeric( $t_changesets ) ) { $t_rev = $t_changesets + 1; } } echo "</pre>"; } public function import_latest( $p_repo ) { return $this->import_full( $p_repo ); } private function check_svn() { $svn = self::svn_binary(); if ( is_blank( shell_exec( "$svn help" ) ) ) { trigger_error( ERROR_GENERIC, ERROR ); } } private function svn_call( $p_repo=null ) { static $s_call; # Get a full binary call, including configured parameters if ( is_null( $s_call ) ) { $s_call = self::svn_binary() . ' --non-interactive'; if ( config_get( 'plugin_SourceSVN_svnssl', false ) ) { $s_call .= ' --trust-server-cert'; } $t_svnargs = config_get( 'plugin_SourceSVN_svnargs', '' ); if ( !is_blank( $t_svnargs ) ) { $s_call .= " $t_svnargs"; } } # If not given a repo, just return the base SVN binary if ( is_null( $p_repo ) ) { return $s_call; } $t_call = $s_call; # With a repo, add arguments for repo info $t_username = escapeshellarg($p_repo->info['svn_username']); $t_password = escapeshellarg($p_repo->info['svn_password']); if ( !is_blank( $t_username ) ) { $t_call .= ' --username ' . $t_username; } if ( !is_blank( $t_password ) ) { $t_call .= ' --password ' . $t_password; } # Done return $t_call; } /** * Generate, validate, and cache the SVN binary path. * @param string Path to SVN * @param boolean Reset cached value * @return string SVN binary */ public static function svn_binary( $p_path=null, $p_reset=false ) { static $s_binary; if ( is_null( $s_binary ) || $p_reset ) { if ( is_null( $p_path ) ) { $t_path = config_get( 'plugin_SourceSVN_svnpath', '' ); } else { $t_path = $p_path; } if ( !is_blank( $t_path ) && is_dir( $t_path ) ) { # Linux / UNIX paths $t_binary = $t_path . DIRECTORY_SEPARATOR . 'svn'; if ( is_file( $t_binary ) && is_executable( $t_binary ) ) { return $s_binary = $t_binary; } # Windows paths $t_binary = $t_path . DIRECTORY_SEPARATOR . 'svn.exe'; if ( is_file( $t_binary ) && is_executable( $t_binary ) ) { if ( config_get( 'plugin_SourceSVN_winstart', '' ) ) { return "start /B /D \"{$t_path}\" svn.exe"; } else { return $s_binary = $t_binary; } } } # Generic pathless call $s_binary = 'svn'; } return $s_binary; } private function process_svn_log( $p_repo, $p_svnlog ) { $t_state = 0; $t_svnline = str_pad( '', 72, '-' ); $t_changesets = array(); $t_changeset = null; $t_comments = ''; $t_count = 0; $t_trunk_path = $p_repo->info['trunk_path']; $t_branch_path = $p_repo->info['branch_path']; $t_tag_path = $p_repo->info['tag_path']; $t_ignore_paths = $p_repo->info['ignore_paths']; $t_discarded = false; echo "Processing svn log...\n"; foreach( $p_svnlog as $t_line ) { # starting state, do nothing if ( 0 == $t_state ) { if ( $t_line == $t_svnline ) { $t_state = 1; } # Changeset info } elseif ( 1 == $t_state && preg_match( '/^r([0-9]+) \| ([^|]+) \| ([0-9\-]+) ([0-9:]+)/', $t_line, $t_matches ) ) { if ( !is_null( $t_changeset ) ) { if ( !is_blank( $t_changeset->branch ) ) { $t_changeset->save(); $t_changesets[] = $t_changeset; } else { $t_discarded = $t_changeset->revision; } } $t_changeset = new SourceChangeset( $p_repo->id, $t_matches[1], '', $t_matches[3] . ' ' . $t_matches[4], $t_matches[2], '' ); $t_state = 2; # Changed paths } elseif ( 2 == $t_state ) { if ( strlen( $t_line ) == 0 ) { $t_state = 3; } else { if ( preg_match( '/^\s+([a-zA-Z])\s+([^\(]+)(?: \(from [^\)]+\))?/', $t_line, $t_matches ) ) { switch( $t_matches[1] ) { case 'A': $t_action = 'add'; break; case 'D': $t_action = 'rm'; break; case 'M': $t_action = 'mod'; break; case 'R': $t_action = 'mv'; break; default: $t_action = $t_matches[1]; } $t_file = new SourceFile( $t_changeset->id, '', trim( $t_matches[2] ), $t_action ); $t_changeset->files[] = $t_file; # Branch-checking if ( is_blank( $t_changeset->branch) ) { # Look for standard trunk/branches/tags information if ( $p_repo->info['standard_repo'] ) { if ( preg_match( '@/(?:(trunk)|(?:branches|tags)/([^/]+))@i', $t_file->filename, $t_matches ) ) { if ( !is_blank( $t_matches[1] ) ) { $t_changeset->branch = $t_matches[1]; } else { $t_changeset->branch = $t_matches[2]; } } } else { # Look for non-standard trunk path if ( !is_blank( $t_trunk_path ) && preg_match( '@^/*(' . $t_trunk_path . ')@i', $t_file->filename, $t_matches ) ) { $t_changeset->branch = $t_matches[1]; # Look for non-standard branch path } else if ( !is_blank( $t_branch_path ) && preg_match( '@^/*(?:' . $t_branch_path . ')/([^/]+)@i', $t_file->filename, $t_matches ) ) { $t_changeset->branch = $t_matches[1]; # Look for non-standard tag path } else if ( !is_blank( $t_tag_path ) && preg_match( '@^/*(?:' . $t_tag_path . ')/([^/]+)@i', $t_file->filename, $t_matches ) ) { $t_changeset->branch = $t_matches[1]; # Fall back to just using the root folder as the branch name } else if ( !$t_ignore_paths && preg_match( '@/([^/]+)@', $t_file->filename, $t_matches ) ) { $t_changeset->branch = $t_matches[1]; } } } } } # Changeset comments } elseif ( 3 == $t_state ) { if ( $t_line == $t_svnline ) { $t_state = 1; } else { if ( !is_blank($t_changeset->message) ) { $t_changeset->message .= "\n$t_line"; } else { $t_changeset->message .= $t_line; } } # Should only happen at the end... } else { break; } } if ( !is_null( $t_changeset ) ) { echo "Parsed to revision {$t_changeset->revision}.\n"; $t_changeset->save(); $t_changesets[] = $t_changeset; } else { echo "No revisions parsed.\n"; } return $t_changesets; } }