1<?php 2 3# Copyright (c) 2012 John Reese 4# Licensed under the MIT license 5 6require_once( 'MantisSourceBase.class.php' ); 7 8/** 9 * Creates an extensible API for integrating source control applications 10 * with the Mantis bug tracker software. 11 */ 12class SourcePlugin extends MantisSourceBase { 13 14 static $cache = array(); 15 16 const PLUGIN_VERSION = self::FRAMEWORK_VERSION; 17 18 /** 19 * Error constants 20 */ 21 const ERROR_CHANGESET_MISSING_ID = 'changeset_missing_id'; 22 const ERROR_CHANGESET_MISSING_REPO = 'changeset_missing_repo'; 23 const ERROR_CHANGESET_INVALID_REPO = 'changeset_invalid_repo'; 24 const ERROR_FILE_MISSING = 'file_missing'; 25 const ERROR_FILE_INVALID_CHANGESET = 'file_invalid_changeset'; 26 const ERROR_PRODUCTMATRIX_NOT_LOADED = 'productmatrix_not_loaded'; 27 const ERROR_REPO_MISSING = 'repo_missing'; 28 const ERROR_REPO_MISSING_CHANGESET = 'repo_missing_changeset'; 29 30 /** 31 * Changeset link matching pattern. 32 * format: '<type>:<reponame>:<revision>:', where 33 * <type> = link type, 'c' or 's' for changeset details, 'd' or 'v' for diff 34 * the type may be omitted; if unspecified, defaults to 'c' 35 * <repo> = repository name 36 * <rev> = changeset revision ID (e.g. SVN rev number, GIT SHA, etc.) 37 * The match is not case-sensitive. 38 */ 39 const CHANGESET_LINKING_REGEX = '/(?:(?<=^|[^\w])([cdsvp]?):([^:\s][^:\n\t]*):([^:\s]+):)/i'; 40 41 function register() { 42 $this->name = plugin_lang_get( 'title' ); 43 $this->description = plugin_lang_get( 'description' ); 44 45 $this->version = self::PLUGIN_VERSION; 46 $this->requires = array( 47 'MantisCore' => self::MANTIS_VERSION, 48 ); 49 $this->page = 'manage_config_page'; 50 51 $this->author = 'John Reese'; 52 $this->contact = 'john@noswap.com'; 53 $this->url = 'https://github.com/mantisbt-plugins/source-integration/'; 54 } 55 56 function config() { 57 return array( 58 'show_repo_link' => ON, 59 'show_search_link' => OFF, 60 'show_repo_stats' => ON, 61 62 'view_threshold' => VIEWER, 63 'update_threshold' => UPDATER, 64 'manage_threshold' => ADMINISTRATOR, 65 'username_threshold' => DEVELOPER, 66 67 'enable_linking' => ON, 68 'enable_mapping' => OFF, 69 'enable_porting' => OFF, 70 'enable_resolving' => OFF, 71 'enable_message' => OFF, 72 'enable_product_matrix' => OFF, 73 74 'buglink_regex_1' => '/(?:bugs?|issues?|reports?)+\s*:?\s+(?:#(?:\d+)[,\.\s]*)+/i', 75 'buglink_regex_2' => '/#?(\d+)/', 76 77 'bugfix_regex_1' => '/(?:fixe?d?s?|resolved?s?)+\s*:?\s+(?:#(?:\d+)[,\.\s]*)+/i', 78 'bugfix_regex_2' => '/#?(\d+)/', 79 'bugfix_status' => -1, 80 'bugfix_resolution' => FIXED, 81 'bugfix_status_pvm' => 0, 82 'bugfix_handler' => ON, 83 'bugfix_message' => 'Fix committed to $1 branch.', 84 'bugfix_message_view_status' => VS_PUBLIC, 85 86 'default_primary_branch' => 'master', 87 88 'remote_checkin' => OFF, 89 'checkin_urls' => serialize( array( 'localhost' ) ), 90 91 'remote_imports' => OFF, 92 'import_urls' => serialize( array( 'localhost' ) ), 93 94 'api_key' => '', 95 ); 96 } 97 98 function events() { 99 return array( 100 # Allow source integration plugins to announce themselves 101 'EVENT_SOURCE_INTEGRATION' => EVENT_TYPE_DEFAULT, 102 103 # Allow vcs plugins to pre-process commit data 104 'EVENT_SOURCE_PRECOMMIT' => EVENT_TYPE_FIRST, 105 106 # Allow other plugins to post-process commit data 107 'EVENT_SOURCE_COMMITS' => EVENT_TYPE_EXECUTE, 108 'EVENT_SOURCE_FIXED' => EVENT_TYPE_EXECUTE, 109 ); 110 } 111 112 function hooks() { 113 return array( 114 'EVENT_CORE_READY' => 'core_ready', 115 'EVENT_MENU_MAIN' => 'menu_main', 116 'EVENT_FILTER_COLUMNS' => 'filter_columns', 117 ); 118 } 119 120 function init() { 121 require_once( 'Source.API.php' ); 122 123 require_once( 'SourceIntegration.php' ); 124 plugin_child( 'SourceIntegration' ); 125 } 126 127 function errors() { 128 $t_errors_list = array( 129 self::ERROR_CHANGESET_MISSING_ID, 130 self::ERROR_CHANGESET_MISSING_REPO, 131 self::ERROR_CHANGESET_INVALID_REPO, 132 self::ERROR_FILE_MISSING, 133 self::ERROR_FILE_INVALID_CHANGESET, 134 self::ERROR_PRODUCTMATRIX_NOT_LOADED, 135 self::ERROR_REPO_MISSING, 136 self::ERROR_REPO_MISSING_CHANGESET, 137 ); 138 139 foreach( $t_errors_list as $t_error ) { 140 $t_errors[$t_error] = plugin_lang_get( 'error_' . $t_error ); 141 } 142 143 return array_merge( parent::errors(), $t_errors ); 144 } 145 146 /** 147 * Register source integration plugins with the framework. 148 */ 149 function core_ready() { 150 # register the generic vcs type 151 plugin_child( 'SourceGeneric' ); 152 153 # initialize the vcs type cache 154 SourceVCS::init(); 155 156 if ( plugin_config_get( 'enable_linking' ) ) { 157 plugin_event_hook( 'EVENT_DISPLAY_FORMATTED', 'display_formatted' ); 158 } 159 } 160 161 function filter_columns() 162 { 163 require_once( 'classes/RelatedChangesetsColumn.class.php' ); 164 return array( 165 'SourceRelatedChangesetsColumn', 166 ); 167 } 168 169 function menu_main() { 170 $t_menu_options = array(); 171 172 if ( plugin_config_get( 'show_repo_link' ) ) { 173 $t_page = plugin_page( 'index', false, 'Source' ); 174 $t_lang = plugin_lang_get( 'repositories', 'Source' ); 175 176 $t_menu_option = array( 177 'title' => $t_lang, 178 'url' => $t_page, 179 'access_level' => plugin_config_get( 'view_threshold' ), 180 'icon' => 'fa-code-fork' 181 ); 182 183 $t_menu_options[] = $t_menu_option; 184 } 185 186 if ( plugin_config_get( 'show_search_link' ) ) { 187 $t_page = plugin_page( 'search_page', false, 'Source' ); 188 $t_lang = plugin_lang_get( 'search', 'Source' ); 189 190 $t_menu_option = array( 191 'title' => $t_lang, 192 'url' => $t_page, 193 'access_level' => plugin_config_get( 'view_threshold' ), 194 'icon' => 'fa-search' 195 ); 196 197 $t_menu_options[] = $t_menu_option; 198 } 199 200 return $t_menu_options; 201 } 202 203 function display_formatted( $p_event, $p_text, $p_multiline ) { 204 $p_text = preg_replace_callback( 205 self::CHANGESET_LINKING_REGEX, 206 array( $this, 'Changeset_Link_Callback' ), 207 $p_text 208 ); 209 return $p_text; 210 } 211 212 function schema() { 213 return array( 214 array( 'CreateTableSQL', array( plugin_table( 'repository' ), " 215 id I NOTNULL UNSIGNED AUTOINCREMENT PRIMARY, 216 type C(8) NOTNULL DEFAULT \" '' \" PRIMARY, 217 name C(128) NOTNULL DEFAULT \" '' \" PRIMARY, 218 url C(250) DEFAULT \" '' \", 219 info XL NOTNULL 220 ", 221 array( 'mysql' => 'DEFAULT CHARSET=utf8' ) ) ), 222 array( 'CreateTableSQL', array( plugin_table( 'changeset' ), " 223 id I NOTNULL UNSIGNED AUTOINCREMENT PRIMARY, 224 repo_id I NOTNULL UNSIGNED PRIMARY, 225 revision C(250) NOTNULL PRIMARY, 226 branch C(250) NOTNULL DEFAULT \" '' \", 227 user_id I NOTNULL UNSIGNED DEFAULT '0', 228 timestamp T NOTNULL, 229 author C(250) NOTNULL DEFAULT \" '' \", 230 message XL NOTNULL, 231 info XL NOTNULL 232 ", 233 array( 'mysql' => 'DEFAULT CHARSET=utf8' ) ) ), 234 array( 'CreateIndexSQL', array( 'idx_changeset_stamp_author', plugin_table( 'changeset' ), 'timestamp, author' ) ), 235 array( 'CreateTableSQL', array( plugin_table( 'file' ), " 236 id I NOTNULL UNSIGNED AUTOINCREMENT PRIMARY, 237 change_id I NOTNULL UNSIGNED, 238 revision C(250) NOTNULL, 239 filename XL NOTNULL 240 ", 241 array( 'mysql' => 'DEFAULT CHARSET=utf8' ) ) ), 242 array( 'CreateIndexSQL', array( 'idx_file_change_revision', plugin_table( 'file' ), 'change_id, revision' ) ), 243 array( 'CreateTableSQL', array( plugin_table( 'bug' ), " 244 change_id I NOTNULL UNSIGNED PRIMARY, 245 bug_id I NOTNULL UNSIGNED PRIMARY 246 ", 247 array( 'mysql' => 'DEFAULT CHARSET=utf8' ) ) ), 248 array( 'AddColumnSQL', array( plugin_table( 'file' ), " 249 action C(8) NOTNULL DEFAULT \" '' \" 250 " ) ), 251 array( 'AddColumnSQL', array( plugin_table( 'changeset' ), " 252 parent C(250) NOTNULL DEFAULT \" '' \" 253 " ) ), 254 # 2008-10-02 255 array( 'AddColumnSQL', array( plugin_table( 'changeset' ), " 256 ported C(250) NOTNULL DEFAULT \" '' \" 257 " ) ), 258 array( 'AddColumnSQL', array( plugin_table( 'changeset' ), " 259 author_email C(250) NOTNULL DEFAULT \" '' \" 260 " ) ), 261 # 2009-04-03 - Add committer information properties to changesets 262 array( 'AddColumnSQL', array( plugin_table( 'changeset' ), " 263 committer C(250) NOTNULL DEFAULT \" '' \", 264 committer_email C(250) NOTNULL DEFAULT \" '' \", 265 committer_id I NOTNULL UNSIGNED DEFAULT '0' 266 " ) ), 267 # 2009-03-03 - Add mappings from repository branches to project versions 268 array( 'CreateTableSQL', array( plugin_table( 'branch' ), " 269 repo_id I NOTNULL UNSIGNED PRIMARY, 270 branch C(128) NOTNULL PRIMARY, 271 type I NOTNULL UNSIGNED DEFAULT '0', 272 version C(64) NOTNULL DEFAULT \" '' \", 273 regex C(128) NOTNULL DEFAULT \" '' \" 274 ", 275 array( 'mysql' => 'DEFAULT CHARSET=utf8' ) ) ), 276 # 2009-04-15 - Allow a user/admin to specify a user's VCS username 277 array( 'CreateTableSQL', array( plugin_table( 'user' ), " 278 user_id I NOTNULL UNSIGNED PRIMARY, 279 username C(64) NOTNULL DEFAULT \" '' \" 280 ", 281 array( 'mysql' => 'DEFAULT CHARSET=utf8' ) ) ), 282 array( 'CreateIndexSQL', array( 'idx_source_user_username', plugin_table( 'user' ), 'username', array( 'UNIQUE' ) ) ), 283 # 2010-02-11 - Update repo types from svn->websvn 284 array( 'UpdateSQL', array( plugin_table( 'repository' ), " SET type='websvn' WHERE type='svn'" ) ), 285 # 2010-07-29 - Integrate with the Product Matrix plugin 286 array( 'AddColumnSQL', array( plugin_table( 'branch' ), " 287 pvm_version_id I NOTNULL UNSIGNED DEFAULT '0' 288 " ) ), 289 ); 290 } 291 292 /** 293 * preg_replace callback to generate VCS links to changesets and pull requests. 294 * @param string $p_matches 295 * @return string 296 */ 297 protected function Changeset_Link_Callback( $p_matches ) { 298 $t_url_type = strtolower($p_matches[1]); 299 $t_repo_name = $p_matches[2]; 300 $t_revision = $p_matches[3]; 301 302 // Pull request links 303 if( $t_url_type == 'p' ) { 304 $t_repo = SourceRepo::load_by_name( $t_repo_name ); 305 if( $t_repo !== null ) { 306 $t_vcs = SourceVCS::repo( $t_repo ); 307 if( $t_vcs->linkPullRequest ) { 308 $t_url = $t_vcs->url_repo( $t_repo ) 309 . sprintf( $t_vcs->linkPullRequest, $t_revision ); 310 $t_name = string_display_line( 311 $t_repo->name . ' ' . 312 plugin_lang_get ( 'pullrequest' ) . ' ' . 313 $t_revision 314 ); 315 return '<a href="' . $t_url . '">' . $t_name . '</a>'; 316 } 317 } 318 return $p_matches[0]; 319 } 320 321 // Changeset links 322 $t_repo_table = plugin_table( 'repository', 'Source' ); 323 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 324 325 $t_query = "SELECT c.* FROM $t_changeset_table AS c 326 JOIN $t_repo_table AS r ON r.id=c.repo_id 327 WHERE c.revision LIKE " . db_param() . ' 328 AND r.name LIKE ' . db_param(); 329 $t_result = db_query( $t_query, array( $t_revision . '%', $t_repo_name . '%' ), 1 ); 330 331 if( db_num_rows( $t_result ) > 0 ) { 332 $t_row = db_fetch_array( $t_result ); 333 334 $t_changeset = new SourceChangeset( 335 $t_row['repo_id'], $t_row['revision'], $t_row['branch'], 336 $t_row['timestamp'], $t_row['author'], $t_row['message'], 337 $t_row['user_id'] 338 ); 339 $t_changeset->id = $t_row['id']; 340 341 $t_repo = SourceRepo::load( $t_changeset->repo_id ); 342 $t_vcs = SourceVCS::repo( $t_repo ); 343 344 switch( $t_url_type ) { 345 case 'v': 346 case 'd': 347 $t_url = $t_vcs->url_changeset( $t_repo, $t_changeset ); 348 break; 349 case 'c': 350 case 's': 351 default: 352 $t_url = plugin_page( 'view' ) . '&id=' . $t_changeset->id; 353 } 354 355 $t_name = string_display_line( $t_repo->name . ' ' . $t_vcs->show_changeset( $t_repo, $t_changeset ) ); 356 357 return '<a href="' . $t_url . '">' . $t_name . '</a>'; 358 } 359 360 return $p_matches[0]; 361 } 362 363} 364 365