1<?php 2 3# Copyright (c) 2012 John Reese 4# Licensed under the MIT license 5 6require_once( 'MantisSourcePlugin.class.php' ); 7 8/** 9 * General source control integration API. 10 * @author John Reese 11 */ 12 13if (!function_exists('plugin_lang_get_defaulted')) { 14 /** 15 * Get a language string for the plugin. 16 * - If found, return the appropriate string. 17 * - If not found, no default supplied, return the supplied string as is. 18 * - If not found, default supplied, return default. 19 * Automatically prepends plugin_<basename> to the string requested. 20 * @param string $p_name Language string name. 21 * @param string $p_default The default value to return. 22 * @param string $p_basename Plugin basename. 23 * @return string Language string 24 */ 25 function plugin_lang_get_defaulted( $p_name, $p_default = null, $p_basename = null ) { 26 if( !is_null( $p_basename ) ) { 27 plugin_push_current( $p_basename ); 28 } 29 $t_basename = plugin_get_current(); 30 $t_name = 'plugin_' . $t_basename . '_' . $p_name; 31 $t_string = lang_get_defaulted( $t_name, $p_default ); 32 33 if( !is_null( $p_basename ) ) { 34 plugin_pop_current(); 35 } 36 return $t_string; 37 } 38} 39 40# branch mapping strategies 41define( 'SOURCE_EXPLICIT', 1 ); 42define( 'SOURCE_NEAR', 2 ); 43define( 'SOURCE_FAR', 3 ); 44define( 'SOURCE_FIRST', 4 ); 45define( 'SOURCE_LAST', 5 ); 46 47function SourceType( $p_type ) { 48 $t_types = SourceTypes(); 49 50 if ( isset( $t_types[$p_type] ) ) { 51 return $t_types[$p_type]; 52 } 53 54 return $p_type; 55} 56 57function SourceTypes() { 58 static $s_types = null; 59 60 if ( is_null( $s_types ) ) { 61 $s_types = array(); 62 63 foreach( SourceVCS::all() as $t_type => $t_vcs ) { 64 $s_types[ $t_type ] = $t_vcs->show_type(); 65 } 66 67 asort( $s_types ); 68 } 69 70 return $s_types; 71} 72 73/** 74 * Determine if the Product Matrix integration is enabled, and trigger 75 * an error if integration is enabled but the plugin is not running. 76 * @param boolean $p_trigger_error Trigger error 77 * @return boolean Integration enabled 78 */ 79function Source_PVM( $p_trigger_error=true ) { 80 if ( config_get( 'plugin_Source_enable_product_matrix' ) ) { 81 if ( plugin_is_loaded( 'ProductMatrix' ) || !$p_trigger_error ) { 82 return true; 83 } else { 84 plugin_error( SourcePlugin::ERROR_PRODUCTMATRIX_NOT_LOADED ); 85 } 86 } 87 return false; 88} 89 90/** 91 * Parse basic bug links from a changeset commit message 92 * and return a list of referenced bug IDs. 93 * @param string $p_string Changeset commit message 94 * @return array Bug IDs 95 */ 96function Source_Parse_Buglinks( $p_string ) { 97 static $s_regex1, $s_regex2; 98 99 $t_bugs = array(); 100 101 if ( is_null( $s_regex1 ) ) { 102 $s_regex1 = config_get( 'plugin_Source_buglink_regex_1' ); 103 $s_regex2 = config_get( 'plugin_Source_buglink_regex_2' ); 104 } 105 106 preg_match_all( $s_regex1, $p_string, $t_matches_all ); 107 108 foreach( $t_matches_all[0] as $t_substring ) { 109 preg_match_all( $s_regex2, $t_substring, $t_matches ); 110 foreach ( $t_matches[1] as $t_match ) { 111 if ( 0 < (int)$t_match ) { 112 $t_bugs[$t_match] = true; 113 } 114 } 115 } 116 117 return array_keys( $t_bugs ); 118} 119 120/** 121 * Parse resolved bug fix links from a changeset commit message 122 * and return a list of referenced bug IDs. 123 * @param string $p_string Changeset commit message 124 * @return array Bug IDs 125 */ 126function Source_Parse_Bugfixes( $p_string ) { 127 static $s_regex1, $s_regex2; 128 129 $t_bugs = array(); 130 131 if ( is_null( $s_regex1 ) ) { 132 $s_regex1 = config_get( 'plugin_Source_bugfix_regex_1' ); 133 $s_regex2 = config_get( 'plugin_Source_bugfix_regex_2' ); 134 } 135 136 preg_match_all( $s_regex1, $p_string, $t_matches_all ); 137 138 foreach( $t_matches_all[0] as $t_substring ) { 139 preg_match_all( $s_regex2, $t_substring, $t_matches ); 140 foreach ( $t_matches[1] as $t_match ) { 141 if ( 0 < (int)$t_match ) { 142 $t_bugs[$t_match] = true; 143 } 144 } 145 } 146 147 return array_keys( $t_bugs ); 148} 149 150/** 151 * Sets the changeset's user id by looking up email address or name 152 * Generic code for both Author and Committer, based on the given properties 153 * @param object $p_changeset 154 * @param string $p_user_type 'author' or 'committer' 155 */ 156function Source_set_changeset_user( &$p_changeset, $p_user_type ) { 157 static $s_vcs_names; 158 static $s_names = array(); 159 static $s_emails = array(); 160 161 # Set the fields 162 switch( $p_user_type ) { 163 case 'committer': 164 list( $t_id_prop, $t_name_prop, $t_email_prop ) = explode( ' ', 'committer_id committer committer_email' ); 165 break; 166 167 case 'author': 168 default: 169 list( $t_id_prop, $t_name_prop, $t_email_prop ) = explode( ' ', 'user_id author author_email' ); 170 break; 171 } 172 173 # The user's id is already set, nothing to do 174 if( $p_changeset->$t_id_prop ) { 175 return; 176 } 177 178 # cache the vcs username mappings 179 if( is_null( $s_vcs_names ) ) { 180 $s_vcs_names = SourceUser::load_mappings(); 181 } 182 183 # Check username associations 184 if( isset( $s_vcs_names[ $p_changeset->$t_name_prop ] ) ) { 185 $p_changeset->$t_id_prop = $s_vcs_names[ $p_changeset->$t_name_prop ]; 186 return; 187 } 188 189 # Look up the email address if given 190 if( $t_email = $p_changeset->$t_email_prop ) { 191 if( isset( $s_emails[ $t_email ] ) ) { 192 $p_changeset->$t_id_prop = $s_emails[ $t_email ]; 193 return; 194 195 } else if( false !== ( $t_email_id = user_get_id_by_email( $t_email ) ) ) { 196 $s_emails[ $t_email ] = $p_changeset->$t_id_prop = $t_email_id; 197 return; 198 } 199 } 200 201 # Look up the name if the email failed 202 if( $t_name = $p_changeset->$t_name_prop ) { 203 if( isset( $s_names[ $t_name ] ) ) { 204 $p_changeset->$t_id_prop = $s_names[ $t_name ]; 205 return; 206 207 } else if( false !== ( $t_user_id = user_get_id_by_realname( $t_name ) ) ) { 208 $s_names[ $t_name ] = $p_changeset->$t_id_prop = $t_user_id; 209 return; 210 211 } else if( false !== ( $t_user_id = user_get_id_by_name( $p_changeset->$t_name_prop ) ) ) { 212 $s_names[ $t_name ] = $p_changeset->$t_id_prop = $t_user_id; 213 return; 214 } 215 } 216} 217 218/** 219 * Determine the user ID for both the author and committer. 220 * First checks the email address for a matching user, then 221 * checks the name for a matching username or realname. 222 * @param object $p_changeset Changeset object 223 * @return object updated Changeset object 224 */ 225function Source_Parse_Users( $p_changeset ) { 226 227 # Handle the changeset author 228 Source_set_changeset_user( $p_changeset, 'author' ); 229 230 # Handle the changeset committer 231 Source_set_changeset_user( $p_changeset, 'committer' ); 232 233 return $p_changeset; 234} 235 236/** 237 * Given a set of changeset objects, parse the bug links 238 * and save the changes. 239 * @param array $p_changesets Changeset objects 240 * @param object $p_repo Repository object 241 */ 242function Source_Process_Changesets( $p_changesets, $p_repo=null ) { 243 global $g_cache_current_user_id; 244 245 if ( !is_array( $p_changesets ) ) { 246 return; 247 } 248 249 if ( is_null( $p_repo ) ) { 250 $t_repos = SourceRepo::load_by_changesets( $p_changesets ); 251 } else { 252 $t_repos = array( $p_repo->id => $p_repo ); 253 } 254 255 $t_resolved_threshold = config_get('bug_resolved_status_threshold'); 256 $t_fixed_threshold = config_get('bug_resolution_fixed_threshold'); 257 $t_notfixed_threshold = config_get('bug_resolution_not_fixed_threshold'); 258 $t_handle_bug_threshold = config_get( 'handle_bug_threshold' ); 259 260 # Link author and committer name/email to user accounts 261 foreach( $p_changesets as $t_key => $t_changeset ) { 262 $p_changesets[ $t_key ] = Source_Parse_Users( $t_changeset ); 263 } 264 265 # Parse normal bug links, excluding non-existing bugs 266 foreach( $p_changesets as $t_changeset ) { 267 $t_bugs = Source_Parse_Buglinks( $t_changeset->message ); 268 foreach( $t_bugs as $t_bug_id ) { 269 if( bug_exists( $t_bug_id ) ) { 270 $t_changeset->bugs[] = $t_bug_id; 271 } 272 } 273 } 274 275 # Parse fixed bug links 276 $t_fixed_bugs = array(); 277 278 # Find and associate resolve links with the changeset 279 foreach( $p_changesets as $t_changeset ) { 280 $t_bugs = Source_Parse_Bugfixes( $t_changeset->message ); 281 282 foreach( $t_bugs as $t_key => $t_bug_id ) { 283 # Only process existing bugs 284 if( bug_exists( $t_bug_id ) ) { 285 $t_fixed_bugs[$t_bug_id] = $t_changeset; 286 } else { 287 unset( $t_bugs[$t_key] ); 288 } 289 } 290 291 # Add the link to the normal set of buglinks 292 $t_changeset->bugs = array_unique( array_merge( $t_changeset->bugs, $t_bugs ) ); 293 } 294 295 # Save changeset data before processing their consequences 296 foreach( $p_changesets as $t_changeset ) { 297 $t_changeset->repo = $p_repo; 298 $t_changeset->save(); 299 } 300 301 # Precache information for resolved bugs 302 bug_cache_array_rows( array_keys( $t_fixed_bugs ) ); 303 304 $t_current_user_id = $g_cache_current_user_id; 305 $t_enable_resolving = config_get( 'plugin_Source_enable_resolving' ); 306 $t_enable_message = config_get( 'plugin_Source_enable_message' ); 307 $t_enable_mapping = config_get( 'plugin_Source_enable_mapping' ); 308 309 $t_bugfix_status = config_get( 'plugin_Source_bugfix_status' ); 310 $t_bugfix_status_pvm = config_get( 'plugin_Source_bugfix_status_pvm' ); 311 $t_resolution = config_get( 'plugin_Source_bugfix_resolution' ); 312 $t_handler = config_get( 'plugin_Source_bugfix_handler' ); 313 $t_message_template = str_replace( 314 array( '$1', '$2', '$3', '$4', '$5', '$6' ), 315 array( '%1$s', '%2$s', '%3$s', '%4$s', '%5$s', '%6$s' ), 316 config_get( 'plugin_Source_bugfix_message' ) ); 317 318 $t_mappings = array(); 319 320 # Start fixing and/or resolving issues 321 foreach( $t_fixed_bugs as $t_bug_id => $t_changeset ) { 322 323 # Determine the Mantis user to associate with the issue referenced in 324 # the changeset: 325 # - use Author if they can handle the issue 326 # - use Committer if not 327 # - if Committer can't handle issue either, it will not be resolved. 328 # This is used to generate the history entries and set the bug handler 329 # if the changeset fixes the issue. 330 $t_user_id = null; 331 if ( $t_changeset->user_id > 0 ) { 332 $t_can_handle_bug = access_has_bug_level( $t_handle_bug_threshold, $t_bug_id, $t_changeset->user_id ); 333 if( $t_can_handle_bug ) { 334 $t_user_id = $t_changeset->user_id; 335 } 336 } 337 $t_handler_id = $t_user_id; 338 if( $t_handler_id === null && $t_changeset->committer_id > 0 ) { 339 $t_user_id = $t_changeset->committer_id; 340 $t_can_handle_bug = access_has_bug_level( $t_handle_bug_threshold, $t_bug_id, $t_user_id ); 341 if( $t_can_handle_bug ) { 342 $t_handler_id = $t_user_id; 343 } 344 } 345 346 if ( !is_null( $t_user_id ) ) { 347 $g_cache_current_user_id = $t_user_id; 348 } else if ( !is_null( $t_current_user_id ) ) { 349 $g_cache_current_user_id = $t_current_user_id; 350 } else { 351 $g_cache_current_user_id = 0; 352 } 353 354 # generate the branch mappings 355 $t_version = ''; 356 $t_pvm_version_id = 0; 357 if ( $t_enable_mapping ) { 358 $t_repo_id = $t_changeset->repo_id; 359 360 if ( !isset( $t_mappings[ $t_repo_id ] ) ) { 361 $t_mappings[ $t_repo_id ] = SourceMapping::load_by_repo( $t_repo_id ); 362 } 363 364 if ( isset( $t_mappings[ $t_repo_id ][ $t_changeset->branch ] ) ) { 365 $t_mapping = $t_mappings[ $t_repo_id ][ $t_changeset->branch ]; 366 if ( Source_PVM() ) { 367 $t_pvm_version_id = $t_mapping->apply_pvm( $t_bug_id ); 368 } else { 369 $t_version = $t_mapping->apply( $t_bug_id ); 370 } 371 } 372 } 373 374 # generate a note message 375 if ( $t_enable_message ) { 376 $t_message = sprintf( $t_message_template, 377 $t_changeset->branch, 378 $t_changeset->revision, 379 $t_changeset->timestamp, 380 $t_changeset->message, 381 $t_repos[ $t_changeset->repo_id ]->name, 382 $t_changeset->id 383 ); 384 } else { 385 $t_message = ''; 386 } 387 388 $t_bug = bug_get( $t_bug_id ); 389 390 # Update the resolution, fixed-in version, and/or add a bugnote 391 $t_update = false; 392 393 if ( Source_PVM() ) { 394 if ( $t_bugfix_status_pvm > 0 && $t_pvm_version_id > 0 ) { 395 $t_matrix = new ProductMatrix( $t_bug_id ); 396 if ( isset( $t_matrix->status[ $t_pvm_version_id ] ) ) { 397 $t_matrix->status[ $t_pvm_version_id ] = $t_bugfix_status_pvm; 398 $t_matrix->save(); 399 } 400 } 401 402 } elseif( $t_handler && $t_handler_id !== null ) { 403 # We only resolve the issue if an authorized handler has been 404 # identified; otherwise, it will remain open. 405 406 if ( $t_bugfix_status > 0 && $t_bug->status != $t_bugfix_status ) { 407 $t_bug->status = $t_bugfix_status; 408 $t_update = true; 409 } else if ( $t_enable_resolving && $t_bugfix_status == -1 && $t_bug->status < $t_resolved_threshold ) { 410 $t_bug->status = $t_resolved_threshold; 411 $t_update = true; 412 } 413 414 if( $t_bug->resolution < $t_fixed_threshold || $t_bug->resolution >= $t_notfixed_threshold 415 # With default MantisBT settings, 'reopened' is above 'fixed' 416 # but below 'not fixed' thresholds, so we need a special case 417 # to make sure the resolution is set to 'fixed'. 418 || $t_bug->resolution == REOPENED 419 ) { 420 $t_bug->resolution = $t_resolution; 421 $t_update = true; 422 } 423 if ( is_blank( $t_bug->fixed_in_version ) ) { 424 $t_bug->fixed_in_version = $t_version; 425 $t_update = true; 426 } 427 428 if( $t_bug->handler_id != $t_handler_id ) { 429 $t_bug->handler_id = $t_handler_id; 430 $t_update = true; 431 } 432 } 433 434 $t_private = plugin_config_get( 'bugfix_message_view_status' ) == VS_PRIVATE; 435 436 if ( $t_update ) { 437 if ( $t_message ) { 438 # Add a note without sending mail, since the notification will 439 # be sent by the subsequent bug update. 440 bugnote_add( $t_bug_id, $t_message, '0:00', $t_private, 0, '', null, false ); 441 } 442 $t_bug->update(); 443 444 } else if ( $t_message ) { 445 bugnote_add( $t_bug_id, $t_message, '0:00', $t_private ); 446 } 447 } 448 449 # reset the user ID 450 $g_cache_current_user_id = $t_current_user_id; 451 452 # Allow other plugins to post-process commit data 453 event_signal( 'EVENT_SOURCE_COMMITS', array( $p_changesets ) ); 454 event_signal( 'EVENT_SOURCE_FIXED', array( $t_fixed_bugs ) ); 455} 456 457/** 458 * Object for handling registration and retrieval of VCS type extension plugins. 459 */ 460class SourceVCS { 461 static private $cache = array(); 462 463 /** 464 * Initialize the extension cache. 465 */ 466 static public function init() { 467 if ( is_array( self::$cache ) && !empty( self::$cache ) ) { 468 return; 469 } 470 471 $t_raw_data = event_signal( 'EVENT_SOURCE_INTEGRATION' ); 472 foreach ( $t_raw_data as $t_plugin => $t_callbacks ) { 473 foreach ( $t_callbacks as $t_callback => $t_object ) { 474 if ( is_subclass_of( $t_object, 'MantisSourcePlugin' ) && 475 is_string( $t_object->type ) && !is_blank( $t_object->type ) ) { 476 $t_type = strtolower($t_object->type); 477 self::$cache[ $t_type ] = new SourceVCSWrapper( $t_object ); 478 } 479 } 480 } 481 482 ksort( self::$cache ); 483 } 484 485 /** 486 * Retrieve an extension plugin that can handle the requested repo's VCS type. 487 * If the requested type is not available, the "generic" type will be returned. 488 * @param object $p_repo Repository object 489 * @return object VCS plugin 490 */ 491 static public function repo( $p_repo ) { 492 return self::type( $p_repo->type ); 493 } 494 495 /** 496 * Retrieve an extension plugin that can handle the requested VCS type. 497 * If the requested type is not available, the "generic" type will be returned. 498 * @param string $p_type VCS type 499 * @return object VCS plugin 500 */ 501 static public function type( $p_type ) { 502 $p_type = strtolower( $p_type ); 503 504 if ( isset( self::$cache[ $p_type ] ) ) { 505 return self::$cache[ $p_type ]; 506 } else { 507 return self::$cache['generic']; 508 } 509 } 510 511 /** 512 * Retrieve a list of all registered VCS types. 513 * @return array VCS plugins 514 */ 515 static public function all() { 516 return self::$cache; 517 } 518} 519 520/** 521 * Class for wrapping VCS objects with plugin API calls 522 */ 523class SourceVCSWrapper { 524 private $object; 525 private $basename; 526 527 /** 528 * Build a wrapper around a VCS plugin object. 529 * @param $p_object 530 */ 531 function __construct( $p_object ) { 532 $this->object = $p_object; 533 $this->basename = $p_object->basename; 534 } 535 536 /** 537 * Wrap method calls to the target object in plugin_push/pop calls. 538 * @param $p_method 539 * @param $p_args 540 * @return mixed 541 */ 542 function __call( $p_method, $p_args ) { 543 plugin_push_current( $this->basename ); 544 $value = call_user_func_array( array( $this->object, $p_method ), $p_args ); 545 plugin_pop_current(); 546 547 return $value; 548 } 549 550 /** 551 * Wrap property reference to target object. 552 * @param $p_name 553 * @return mixed 554 */ 555 function __get( $p_name ) { 556 return $this->object->$p_name; 557 } 558 559 /** 560 * Wrap property mutation to target object. 561 * @param $p_name 562 * @param $p_value 563 * @return mixed 564 */ 565 function __set( $p_name, $p_value ) { 566 return $this->object->$p_name = $p_value; 567 } 568} 569 570/** 571 * Abstract source control repository data. 572 */ 573class SourceRepo { 574 var $id; 575 var $type; 576 var $name; 577 var $url; 578 var $info; 579 var $branches; 580 var $mappings; 581 582 /** 583 * Build a new Repo object given certain properties. 584 * @param string $p_type Repo type 585 * @param string $p_name Name 586 * @param string $p_url URL 587 * @param string $p_info Info 588 */ 589 function __construct( $p_type, $p_name, $p_url='', $p_info='' ) { 590 $this->id = 0; 591 $this->type = $p_type; 592 $this->name = $p_name; 593 $this->url = $p_url; 594 if ( is_blank( $p_info ) ) { 595 $this->info = array(); 596 } else { 597 $this->info = unserialize( $p_info ); 598 } 599 $this->branches = array(); 600 $this->mappings = array(); 601 } 602 603 /** 604 * Create or update repository data. 605 * Creates database row if $this->id is zero, updates an existing row otherwise. 606 */ 607 function save() { 608 if ( is_blank( $this->type ) || is_blank( $this->name ) ) { 609 if( is_blank( $this->type ) ) { 610 error_parameters( plugin_lang_get( 'type' ) ); 611 } else { 612 error_parameters( plugin_lang_get( 'name' ) ); 613 } 614 trigger_error( ERROR_EMPTY_FIELD, ERROR ); 615 } 616 617 $t_repo_table = plugin_table( 'repository', 'Source' ); 618 619 if ( 0 == $this->id ) { # create 620 $t_query = "INSERT INTO $t_repo_table ( type, name, url, info ) VALUES ( " . 621 db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ' )'; 622 db_query( $t_query, array( $this->type, $this->name, $this->url, serialize($this->info) ) ); 623 624 $this->id = db_insert_id( $t_repo_table ); 625 } else { # update 626 $t_query = "UPDATE $t_repo_table SET type=" . db_param() . ', name=' . db_param() . 627 ', url=' . db_param() . ', info=' . db_param() . ' WHERE id=' . db_param(); 628 db_query( $t_query, array( $this->type, $this->name, $this->url, serialize($this->info), $this->id ) ); 629 } 630 631 foreach( $this->mappings as $t_mapping ) { 632 $t_mapping->save(); 633 } 634 } 635 636 /** 637 * Load and cache the list of unique branches for the repo's changesets. 638 */ 639 function load_branches() { 640 if ( count( $this->branches ) < 1 ) { 641 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 642 643 $t_query = "SELECT DISTINCT branch FROM $t_changeset_table WHERE repo_id=" . 644 db_param() . ' ORDER BY branch ASC'; 645 $t_result = db_query( $t_query, array( $this->id ) ); 646 647 while( $t_row = db_fetch_array( $t_result ) ) { 648 $this->branches[] = $t_row['branch']; 649 } 650 } 651 652 return $this->branches; 653 } 654 655 /** 656 * Load and cache the set of branch mappings for the repository. 657 */ 658 function load_mappings() { 659 if ( count( $this->mappings ) < 1 ) { 660 $this->mappings = SourceMapping::load_by_repo( $this->id ); 661 } 662 663 return $this->mappings; 664 } 665 666 /** 667 * Get a list of repository statistics. 668 * @param bool $p_all 669 * @return array Stats 670 */ 671 function stats( $p_all=true ) { 672 $t_stats = array(); 673 674 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 675 $t_file_table = plugin_table( 'file', 'Source' ); 676 $t_bug_table = plugin_table( 'bug', 'Source' ); 677 678 $t_query = "SELECT COUNT(*) FROM $t_changeset_table WHERE repo_id=" . db_param(); 679 $t_stats['changesets'] = db_result( db_query( $t_query, array( $this->id ) ) ); 680 681 if ( $p_all ) { 682 $t_query = "SELECT COUNT(DISTINCT filename) FROM $t_file_table AS f 683 JOIN $t_changeset_table AS c 684 ON c.id=f.change_id 685 WHERE c.repo_id=" . db_param(); 686 $t_stats['files'] = db_result( db_query( $t_query, array( $this->id ) ) ); 687 688 $t_query = "SELECT COUNT(DISTINCT bug_id) FROM $t_bug_table AS b 689 JOIN $t_changeset_table AS c 690 ON c.id=b.change_id 691 WHERE c.repo_id=" . db_param(); 692 $t_stats['bugs'] = db_result( db_query( $t_query, array( $this->id ) ) ); 693 } 694 695 return $t_stats; 696 } 697 698 /** 699 * Fetch a new Repo object given an ID. 700 * @param int $p_id Repository ID 701 * @return object Repo object 702 */ 703 static function load( $p_id ) { 704 $t_repo_table = plugin_table( 'repository', 'Source' ); 705 706 $t_query = "SELECT * FROM $t_repo_table WHERE id=" . db_param(); 707 $t_result = db_query( $t_query, array( (int) $p_id ) ); 708 709 if ( db_num_rows( $t_result ) < 1 ) { 710 error_parameters( $p_id ); 711 plugin_error( SourcePlugin::ERROR_REPO_MISSING ); 712 } 713 714 $t_row = db_fetch_array( $t_result ); 715 716 $t_repo = new SourceRepo( $t_row['type'], $t_row['name'], $t_row['url'], $t_row['info'] ); 717 $t_repo->id = $t_row['id']; 718 719 return $t_repo; 720 } 721 722 /** 723 * Fetch a new Repo object given a name. 724 * @param string $p_name Repository name 725 * @return SourceRepo Repo object 726 */ 727 static function load_from_name( $p_name ) { 728 $p_name = trim($p_name); 729 $t_repo_table = plugin_table( 'repository', 'Source' ); 730 731 $t_query = "SELECT * FROM $t_repo_table WHERE name LIKE " . db_param(); 732 $t_result = db_query( $t_query, array( $p_name ) ); 733 734 if ( db_num_rows( $t_result ) < 1 ) { 735 error_parameters( $p_name ); 736 plugin_error( SourcePlugin::ERROR_REPO_MISSING ); 737 } 738 739 $t_row = db_fetch_array( $t_result ); 740 741 $t_repo = new SourceRepo( $t_row['type'], $t_row['name'], $t_row['url'], $t_row['info'] ); 742 $t_repo->id = $t_row['id']; 743 744 return $t_repo; 745 } 746 747 /** 748 * Fetch an array of all Repo objects. 749 * @return array All repo objects. 750 */ 751 static function load_all() { 752 $t_repo_table = plugin_table( 'repository', 'Source' ); 753 754 $t_query = "SELECT * FROM $t_repo_table ORDER BY name ASC"; 755 $t_result = db_query( $t_query ); 756 757 $t_repos = array(); 758 759 while ( $t_row = db_fetch_array( $t_result ) ) { 760 $t_repo = new SourceRepo( $t_row['type'], $t_row['name'], $t_row['url'], $t_row['info'] ); 761 $t_repo->id = $t_row['id']; 762 763 $t_repos[] = $t_repo; 764 } 765 766 return $t_repos; 767 } 768 769 /** 770 * Fetch a repository object with the given name. 771 * @param string $p_repo_name 772 * @return null|SourceRepo Repo object, or null if not found 773 */ 774 static function load_by_name( $p_repo_name ) { 775 $t_repo_table = plugin_table( 'repository', 'Source' ); 776 777 # Look for a repository with the exact name given 778 $t_query = "SELECT * FROM $t_repo_table WHERE name LIKE " . db_param(); 779 $t_result = db_query( $t_query, array( $p_repo_name ) ); 780 781 # If not found, look for a repo containing the name given 782 if ( db_num_rows( $t_result ) < 1 ) { 783 $t_query = "SELECT * FROM $t_repo_table WHERE name LIKE " . db_param(); 784 $t_result = db_query( $t_query, array( '%' . $p_repo_name . '%' ) ); 785 786 if ( db_num_rows( $t_result ) < 1 ) { 787 return null; 788 } 789 } 790 791 $t_row = db_fetch_array( $t_result ); 792 793 $t_repo = new SourceRepo( $t_row['type'], $t_row['name'], $t_row['url'], $t_row['info'] ); 794 $t_repo->id = $t_row['id']; 795 796 return $t_repo; 797 } 798 799 /** 800 * Fetch an array of repository objects that includes all given changesets. 801 * @param array|SourceChangeset $p_changesets Changeset objects 802 * @return array Repository objects 803 */ 804 static function load_by_changesets( $p_changesets ) { 805 if ( !is_array( $p_changesets ) ) { 806 $p_changesets = array( $p_changesets ); 807 } 808 elseif ( count( $p_changesets ) < 1 ) { 809 return array(); 810 } 811 812 $t_repo_table = plugin_table( 'repository', 'Source' ); 813 814 $t_repos = array(); 815 816 foreach ( $p_changesets as $t_changeset ) { 817 if ( !isset( $t_repos[$t_changeset->repo_id] ) ) { 818 $t_repos[$t_changeset->repo_id] = true; 819 } 820 } 821 822 $t_list = array(); 823 $t_param = array(); 824 foreach ( $t_repos as $t_repo_id => $t_repo ) { 825 $t_list[] = db_param(); 826 $t_param[] = (int)$t_repo_id; 827 } 828 $t_query = "SELECT * FROM $t_repo_table WHERE id IN (" 829 . join( ', ', $t_list ) 830 . ') ORDER BY name ASC'; 831 $t_result = db_query( $t_query, $t_param ); 832 833 while ( $t_row = db_fetch_array( $t_result ) ) { 834 $t_repo = new SourceRepo( $t_row['type'], $t_row['name'], $t_row['url'], $t_row['info'] ); 835 $t_repo->id = $t_row['id']; 836 837 $t_repos[$t_repo->id] = $t_repo; 838 } 839 840 return $t_repos; 841 } 842 843 /** 844 * Delete a repository with the given ID. 845 * @param int $p_id Repository ID 846 */ 847 static function delete( $p_id ) { 848 SourceChangeset::delete_by_repo( $p_id ); 849 850 $t_repo_table = plugin_table( 'repository', 'Source' ); 851 852 $t_query = "DELETE FROM $t_repo_table WHERE id=" . db_param(); 853 db_query( $t_query, array( (int) $p_id ) ); 854 } 855 856 /** 857 * Check to see if a repository exists with the given ID. 858 * @param int $p_id Repository ID 859 * @return boolean True if repository exists 860 */ 861 static function exists( $p_id ) { 862 $t_repo_table = plugin_table( 'repository', 'Source' ); 863 864 $t_query = "SELECT COUNT(*) FROM $t_repo_table WHERE id=" . db_param(); 865 $t_result = db_query( $t_query, array( (int) $p_id ) ); 866 867 return db_result( $t_result ) > 0; 868 } 869 870 static function ensure_exists( $p_id ) { 871 if ( !SourceRepo::exists( $p_id ) ) { 872 error_parameters( $p_id ); 873 plugin_error( SourcePlugin::ERROR_REPO_MISSING ); 874 } 875 } 876} 877 878/** 879 * Abstract source control changeset data. 880 */ 881class SourceChangeset { 882 var $id; 883 var $repo_id; 884 var $user_id; 885 var $revision; 886 var $parent; 887 var $branch; 888 var $ported; 889 var $timestamp; 890 var $author; 891 var $author_email; 892 var $committer; 893 var $committer_email; 894 var $committer_id; 895 var $message; 896 var $info; 897 898 var $files; # array of SourceFile's 899 var $bugs; 900 var $__bugs; 901 var $repo; 902 903 /** 904 * Build a new changeset object given certain properties. 905 * @param int $p_repo_id Repository ID 906 * @param string $p_revision Changeset revision 907 * @param string $p_branch 908 * @param string $p_timestamp Timestamp 909 * @param string $p_author Author 910 * @param string $p_message Commit message 911 * @param int $p_user_id 912 * @param string $p_parent 913 * @param string $p_ported 914 * @param string $p_author_email 915 */ 916 function __construct( $p_repo_id, $p_revision, $p_branch='', $p_timestamp='', 917 $p_author='', $p_message='', $p_user_id=0, $p_parent='', $p_ported='', $p_author_email='' ) { 918 919 $this->id = 0; 920 $this->user_id = $p_user_id; 921 $this->repo_id = $p_repo_id; 922 $this->revision = $p_revision; 923 $this->parent = $p_parent; 924 $this->branch = $p_branch; 925 $this->ported = $p_ported; 926 $this->timestamp = $p_timestamp; 927 $this->author = $p_author; 928 $this->author_email = $p_author_email; 929 $this->message = $p_message; 930 $this->info = ''; 931 $this->committer = ''; 932 $this->committer_email = ''; 933 $this->committer_id = 0; 934 935 $this->files = array(); 936 $this->bugs = array(); 937 $this->__bugs = array(); 938 } 939 940 /** 941 * Create or update changeset data. 942 * Creates database row if $this->id is zero, updates an existing row otherwise. 943 */ 944 function save() { 945 if ( 0 == $this->repo_id ) { 946 error_parameters( $this->id ); 947 plugin_error( SourcePlugin::ERROR_CHANGESET_INVALID_REPO ); 948 } 949 950 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 951 952 if ( 0 == $this->id ) { # create 953 $t_query = "INSERT INTO $t_changeset_table ( repo_id, revision, parent, branch, user_id, 954 timestamp, author, message, info, ported, author_email, committer, committer_email, committer_id 955 ) VALUES ( " . 956 db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . 957 db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . 958 db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . 959 db_param() . ', ' . db_param() . ' )'; 960 db_query( $t_query, array( 961 $this->repo_id, $this->revision, $this->parent, $this->branch, 962 $this->user_id, $this->timestamp, $this->author, db_mysql_fix_utf8( $this->message ), $this->info, 963 $this->ported, $this->author_email, $this->committer, $this->committer_email, 964 $this->committer_id ) ); 965 966 $this->id = db_insert_id( $t_changeset_table ); 967 968 foreach( $this->files as $t_file ) { 969 $t_file->change_id = $this->id; 970 } 971 972 } else { # update 973 $t_query = "UPDATE $t_changeset_table SET repo_id=" . db_param() . ', revision=' . db_param() . 974 ', parent=' . db_param() . ', branch=' . db_param() . ', user_id=' . db_param() . 975 ', timestamp=' . db_param() . ', author=' . db_param() . ', message=' . db_param() . 976 ', info=' . db_param() . ', ported=' . db_param() . ', author_email=' . db_param() . 977 ', committer=' . db_param() . ', committer_email=' . db_param() . ', committer_id=' . db_param() . 978 ' WHERE id=' . db_param(); 979 db_query( $t_query, array( 980 $this->repo_id, $this->revision, 981 $this->parent, $this->branch, $this->user_id, 982 $this->timestamp, $this->author, $this->message, 983 $this->info, $this->ported, $this->author_email, 984 $this->committer, $this->committer_email, 985 $this->committer_id, $this->id ) ); 986 } 987 988 foreach( $this->files as $t_file ) { 989 $t_file->save(); 990 } 991 992 $this->save_bugs(); 993 } 994 995 /** 996 * Update changeset relations to affected bugs. 997 * @param int $p_user_id 998 */ 999 function save_bugs( $p_user_id=null ) { 1000 $t_bug_table = plugin_table( 'bug', 'Source' ); 1001 1002 $this->bugs = array_unique( $this->bugs ); 1003 $this->__bugs = array_unique( $this->__bugs ); 1004 1005 $t_bugs_added = array_unique( array_diff( $this->bugs, $this->__bugs ) ); 1006 $t_bugs_deleted = array_unique( array_diff( $this->__bugs, $this->bugs ) ); 1007 1008 $this->load_repo(); 1009 $t_vcs = SourceVCS::repo( $this->repo ); 1010 1011 $t_user_id = (int)$p_user_id; 1012 if ( $t_user_id < 1 ) { 1013 if ( $this->committer_id > 0 ) { 1014 $t_user_id = $this->committer_id; 1015 } else if ( $this->user_id > 0 ) { 1016 $t_user_id = $this->user_id; 1017 } 1018 } 1019 1020 if ( count( $t_bugs_deleted ) ) { 1021 $t_bugs_deleted_str = join( ',', $t_bugs_deleted ); 1022 1023 $t_query = "DELETE FROM $t_bug_table WHERE change_id=" . $this->id . 1024 " AND bug_id IN ( $t_bugs_deleted_str )"; 1025 db_query( $t_query ); 1026 1027 foreach( $t_bugs_deleted as $t_bug_id ) { 1028 plugin_history_log( $t_bug_id, 'changeset_removed', 1029 $this->repo->name . ' ' . $t_vcs->show_changeset( $this->repo, $this ), 1030 '', $t_user_id, 'Source' ); 1031 bug_update_date( $t_bug_id ); 1032 } 1033 } 1034 1035 if ( count( $t_bugs_added ) > 0 ) { 1036 $t_query = "INSERT INTO $t_bug_table ( change_id, bug_id ) VALUES "; 1037 1038 $t_count = 0; 1039 $t_params = array(); 1040 1041 foreach( $t_bugs_added as $t_bug_id ) { 1042 $t_query .= ( $t_count == 0 ? '' : ', ' ) . 1043 '(' . db_param() . ', ' . db_param() . ')'; 1044 $t_params[] = $this->id; 1045 $t_params[] = $t_bug_id; 1046 $t_count++; 1047 } 1048 1049 db_query( $t_query, $t_params ); 1050 1051 foreach( $t_bugs_added as $t_bug_id ) { 1052 plugin_history_log( $t_bug_id, 'changeset_attached', '', 1053 $this->repo->name . ' ' . $t_vcs->show_changeset( $this->repo, $this ), 1054 $t_user_id, 'Source' ); 1055 bug_update_date( $t_bug_id ); 1056 } 1057 } 1058 } 1059 1060 /** 1061 * Load/cache repo object. 1062 */ 1063 function load_repo() { 1064 if ( is_null( $this->repo ) ) { 1065 $t_repos = SourceRepo::load_by_changesets( $this ); 1066 $this->repo = array_shift( $t_repos ); 1067 } 1068 } 1069 1070 /** 1071 * Load all file objects associated with this changeset. 1072 */ 1073 function load_files() { 1074 if ( count( $this->files ) < 1 ) { 1075 $this->files = SourceFile::load_by_changeset( $this->id ); 1076 } 1077 1078 return $this->files; 1079 } 1080 1081 /** 1082 * Load all bug numbers associated with this changeset. 1083 */ 1084 function load_bugs() { 1085 if ( count( $this->bugs ) < 1 ) { 1086 $t_bug_table = plugin_table( 'bug', 'Source' ); 1087 1088 $t_query = "SELECT bug_id FROM $t_bug_table WHERE change_id=" . db_param(); 1089 $t_result = db_query( $t_query, array( $this->id ) ); 1090 1091 $this->bugs = array(); 1092 $this->__bugs = array(); 1093 while( $t_row = db_fetch_array( $t_result ) ) { 1094 $this->bugs[] = $t_row['bug_id']; 1095 $this->__bugs[] = $t_row['bug_id']; 1096 } 1097 } 1098 1099 return $this->bugs; 1100 } 1101 1102 /** 1103 * Check if a repository's changeset already exists in the database. 1104 * @param int $p_repo_id Repo ID 1105 * @param string $p_revision Revision 1106 * @param string $p_branch Branch 1107 * @return boolean True if changeset exists 1108 */ 1109 static function exists( $p_repo_id, $p_revision, $p_branch=null ) { 1110 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 1111 1112 $t_query = "SELECT * FROM $t_changeset_table WHERE repo_id=" . db_param() . ' 1113 AND revision=' . db_param(); 1114 $t_params = array( $p_repo_id, $p_revision ); 1115 1116 if ( !is_null( $p_branch ) ) { 1117 $t_query .= ' AND branch=' . db_param(); 1118 $t_params[] = $p_branch; 1119 } 1120 1121 $t_result = db_query( $t_query, $t_params ); 1122 return db_num_rows( $t_result ) > 0; 1123 } 1124 1125 /** 1126 * Fetch a new changeset object given an ID. 1127 * @param int $p_id Changeset ID 1128 * @return mixed Changeset object 1129 */ 1130 static function load( $p_id ) { 1131 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 1132 1133 $t_query = "SELECT * FROM $t_changeset_table WHERE id=" . db_param() . ' 1134 ORDER BY timestamp DESC'; 1135 $t_result = db_query( $t_query, array( $p_id ) ); 1136 1137 if ( db_num_rows( $t_result ) < 1 ) { 1138 error_parameters( $p_id ); 1139 plugin_error( SourcePlugin::ERROR_CHANGESET_MISSING_ID ); 1140 } 1141 1142 $t_array = self::from_result( $t_result ); 1143 return array_shift( $t_array ); 1144 } 1145 1146 /** 1147 * Fetch a changeset object given a repository and revision. 1148 * @param object $p_repo Repo object 1149 * @param string $p_revision Revision 1150 * @return mixed Changeset object 1151 */ 1152 static function load_by_revision( $p_repo, $p_revision ) { 1153 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 1154 1155 $t_query = "SELECT * FROM $t_changeset_table WHERE repo_id=" . db_param() . ' 1156 AND revision=' . db_param() . ' ORDER BY timestamp DESC'; 1157 $t_result = db_query( $t_query, array( $p_repo->id, $p_revision ) ); 1158 1159 if ( db_num_rows( $t_result ) < 1 ) { 1160 error_parameters( $p_revision, $p_repo->name ); 1161 plugin_error( SourcePlugin::ERROR_CHANGESET_MISSING_REPO ); 1162 } 1163 1164 $t_array = self::from_result( $t_result ); 1165 return array_shift( $t_array ); 1166 } 1167 1168 /** 1169 * Fetch an array of changeset objects for a given repository ID. 1170 * @param int $p_repo_id Repository ID 1171 * @param bool $p_load_files 1172 * @param null $p_page 1173 * @param int $p_limit 1174 * @return array Changeset objects 1175 */ 1176 static function load_by_repo( $p_repo_id, $p_load_files=false, $p_page=null, $p_limit=25 ) { 1177 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 1178 1179 $t_query = "SELECT * FROM $t_changeset_table WHERE repo_id=" . db_param() . ' 1180 ORDER BY timestamp DESC'; 1181 if ( is_null( $p_page ) ) { 1182 $t_result = db_query( $t_query, array( $p_repo_id ) ); 1183 } else { 1184 $t_result = db_query( $t_query, array( $p_repo_id ), $p_limit, ($p_page - 1) * $p_limit ); 1185 } 1186 1187 return self::from_result( $t_result, $p_load_files ); 1188 } 1189 1190 /** 1191 * Fetch an array of changeset objects for a given bug ID. 1192 * @param int $p_bug_id Bug ID 1193 * @param bool $p_load_files 1194 * @return array Changeset objects 1195 */ 1196 static function load_by_bug( $p_bug_id, $p_load_files=false ) { 1197 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 1198 $t_bug_table = plugin_table( 'bug', 'Source' ); 1199 1200 $t_order = strtoupper( config_get( 'history_order' ) ) == 'ASC' ? 'ASC' : 'DESC'; 1201 $t_query = "SELECT c.* FROM $t_changeset_table AS c 1202 JOIN $t_bug_table AS b ON c.id=b.change_id 1203 WHERE b.bug_id=" . db_param() . " 1204 ORDER BY c.timestamp $t_order"; 1205 $t_result = db_query( $t_query, array( $p_bug_id ) ); 1206 1207 return self::from_result( $t_result, $p_load_files ); 1208 } 1209 1210 /** 1211 * Return a set of changeset objects from a database result. 1212 * Assumes selecting * from changeset_table. 1213 * @param IteratorAggregate $p_result Database result 1214 * @param bool $p_load_files 1215 * @return array Changeset objects 1216 */ 1217 static function from_result( $p_result, $p_load_files=false ) { 1218 $t_changesets = array(); 1219 1220 while ( $t_row = db_fetch_array( $p_result ) ) { 1221 $t_changeset = new SourceChangeset( $t_row['repo_id'], $t_row['revision'] ); 1222 1223 $t_changeset->id = $t_row['id']; 1224 $t_changeset->parent = $t_row['parent']; 1225 $t_changeset->branch = $t_row['branch']; 1226 $t_changeset->timestamp = $t_row['timestamp']; 1227 $t_changeset->user_id = $t_row['user_id']; 1228 $t_changeset->author = $t_row['author']; 1229 $t_changeset->author_email = $t_row['author_email']; 1230 $t_changeset->message = $t_row['message']; 1231 $t_changeset->info = $t_row['info']; 1232 $t_changeset->ported = $t_row['ported']; 1233 $t_changeset->committer = $t_row['committer']; 1234 $t_changeset->committer_email = $t_row['committer_email']; 1235 $t_changeset->committer_id = $t_row['committer_id']; 1236 1237 if ( $p_load_files ) { 1238 $t_changeset->load_files(); 1239 } 1240 1241 $t_changesets[ $t_changeset->id ] = $t_changeset; 1242 } 1243 1244 return $t_changesets; 1245 } 1246 1247 /** 1248 * Delete all changesets for a given repository ID. 1249 * @param int $p_repo_id Repository ID 1250 */ 1251 static function delete_by_repo( $p_repo_id ) { 1252 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 1253 1254 # first drop any files for the repository's changesets 1255 SourceFile::delete_by_repo( $p_repo_id ); 1256 1257 $t_query = "DELETE FROM $t_changeset_table WHERE repo_id=" . db_param(); 1258 db_query( $t_query, array( $p_repo_id ) ); 1259 } 1260 1261} 1262 1263/** 1264 * Abstract source control file data. 1265 */ 1266class SourceFile { 1267 var $id; 1268 var $change_id; 1269 var $revision; 1270 var $action; 1271 var $filename; 1272 1273 function __construct( $p_change_id, $p_revision, $p_filename, $p_action='' ) { 1274 $this->id = 0; 1275 $this->change_id = $p_change_id; 1276 $this->revision = $p_revision; 1277 $this->action = $p_action; 1278 $this->filename = $p_filename; 1279 } 1280 1281 function save() { 1282 if ( 0 == $this->change_id ) { 1283 error_parameters( $this->id ); 1284 plugin_error( SourcePlugin::ERROR_FILE_INVALID_CHANGESET ); 1285 } 1286 1287 $t_file_table = plugin_table( 'file', 'Source' ); 1288 1289 if ( 0 == $this->id ) { # create 1290 $t_query = "INSERT INTO $t_file_table ( change_id, revision, action, filename ) VALUES ( " . 1291 db_param() . ', ' . db_param() . ', ' . db_param() . ', ' . db_param() . ' )'; 1292 db_query( $t_query, array( $this->change_id, $this->revision, $this->action, $this->filename ) ); 1293 1294 $this->id = db_insert_id( $t_file_table ); 1295 } else { # update 1296 $t_query = "UPDATE $t_file_table SET change_id=" . db_param() . ', revision=' . db_param() . 1297 ', action=' . db_param() . ', filename=' . db_param() . ' WHERE id=' . db_param(); 1298 db_query( $t_query, array( $this->change_id, $this->revision, $this->action, $this->filename, $this->id ) ); 1299 } 1300 } 1301 1302 static function load( $p_id ) { 1303 $t_file_table = plugin_table( 'file', 'Source' ); 1304 1305 $t_query = "SELECT * FROM $t_file_table WHERE id=" . db_param(); 1306 $t_result = db_query( $t_query, array( $p_id ) ); 1307 1308 if ( db_num_rows( $t_result ) < 1 ) { 1309 error_parameters( $p_id ); 1310 plugin_error( SourcePlugin::ERROR_FILE_MISSING ); 1311 } 1312 1313 $t_row = db_fetch_array( $t_result ); 1314 $t_file = new SourceFile( $t_row['change_id'], $t_row['revision'], $t_row['filename'], $t_row['action'] ); 1315 $t_file->id = $t_row['id']; 1316 1317 return $t_file; 1318 } 1319 1320 static function load_by_changeset( $p_change_id ) { 1321 $t_file_table = plugin_table( 'file', 'Source' ); 1322 1323 $t_query = "SELECT * FROM $t_file_table WHERE change_id=" . db_param(); 1324 $t_result = db_query( $t_query, array( $p_change_id ) ); 1325 1326 $t_files = array(); 1327 1328 while ( $t_row = db_fetch_array( $t_result ) ) { 1329 $t_file = new SourceFile( $t_row['change_id'], $t_row['revision'], $t_row['filename'], $t_row['action'] ); 1330 $t_file->id = $t_row['id']; 1331 $t_files[] = $t_file; 1332 } 1333 1334 return $t_files; 1335 } 1336 1337 static function delete_by_changeset( $p_change_id ) { 1338 $t_file_table = plugin_table( 'file', 'Source' ); 1339 1340 $t_query = "DELETE FROM $t_file_table WHERE change_id=" . db_param(); 1341 db_query( $t_query, array( $p_change_id ) ); 1342 } 1343 1344 /** 1345 * Delete all file objects from the database for a given repository. 1346 * @param int $p_repo_id Repository ID 1347 */ 1348 static function delete_by_repo( $p_repo_id ) { 1349 $t_file_table = plugin_table( 'file', 'Source' ); 1350 $t_changeset_table = plugin_table( 'changeset', 'Source' ); 1351 1352 $t_query = "DELETE FROM $t_file_table WHERE change_id IN ( SELECT id FROM $t_changeset_table WHERE repo_id=" . db_param() . ')'; 1353 db_query( $t_query, array( $p_repo_id ) ); 1354 } 1355} 1356 1357/** 1358 * Class for handling branch version mappings on a repository. 1359 */ 1360class SourceMapping { 1361 var $_new = true; 1362 var $repo_id; 1363 var $branch; 1364 var $type; 1365 1366 var $version; 1367 var $regex; 1368 var $pvm_version_id; 1369 1370 /** 1371 * Initialize a mapping object. 1372 * @param int $p_repo_id 1373 * @param string $p_branch 1374 * @param int $p_type 1375 * @param string $p_version 1376 * @param string $p_regex 1377 * @param int $p_pvm_version_id 1378 */ 1379 function __construct( $p_repo_id, $p_branch, $p_type, $p_version='', $p_regex='', $p_pvm_version_id=0 ) { 1380 $this->repo_id = $p_repo_id; 1381 $this->branch = $p_branch; 1382 $this->type = $p_type; 1383 $this->version = $p_version; 1384 $this->regex = $p_regex; 1385 $this->pvm_version_id = $p_pvm_version_id; 1386 } 1387 1388 /** 1389 * Save the given mapping object to the database. 1390 */ 1391 function save() { 1392 $t_branch_table = plugin_table( 'branch' ); 1393 1394 if ( $this->_new ) { 1395 $t_query = "INSERT INTO $t_branch_table ( repo_id, branch, type, version, regex, pvm_version_id ) VALUES (" . 1396 db_param() . ', ' .db_param() . ', ' .db_param() . ', ' .db_param() . ', ' . db_param() . ', ' . db_param() . ')'; 1397 db_query( $t_query, array( $this->repo_id, $this->branch, $this->type, $this->version, $this->regex, $this->pvm_version_id ) ); 1398 1399 } else { 1400 $t_query = "UPDATE $t_branch_table SET branch=" . db_param() . ', type=' . db_param() . ', version=' . db_param() . 1401 ', regex=' . db_param() . ', pvm_version_id=' . db_param() . ' WHERE repo_id=' . db_param() . ' AND branch=' . db_param(); 1402 db_query( $t_query, array( $this->branch, $this->type, $this->version, 1403 $this->regex, $this->pvm_version_id, $this->repo_id, $this->branch ) ); 1404 } 1405 } 1406 1407 /** 1408 * Delete a branch mapping. 1409 */ 1410 function delete() { 1411 $t_branch_table = plugin_table( 'branch' ); 1412 1413 if ( !$this->_new ) { 1414 $t_query = "DELETE FROM $t_branch_table WHERE repo_id=" . db_param() . ' AND branch=' . db_param(); 1415 db_query( $t_query, array( $this->repo_id, $this->branch ) ); 1416 1417 $this->_new = true; 1418 } 1419 } 1420 1421 /** 1422 * Load a group of mapping objects for a given repository. 1423 * @param int $p_repo_id Repository object 1424 * @return array Mapping objects 1425 */ 1426 static function load_by_repo( $p_repo_id ) { 1427 $t_branch_table = plugin_table( 'branch' ); 1428 1429 $t_query = "SELECT * FROM $t_branch_table WHERE repo_id=" . db_param() . ' ORDER BY branch'; 1430 $t_result = db_query( $t_query, array( $p_repo_id ) ); 1431 1432 $t_mappings = array(); 1433 1434 while( $t_row = db_fetch_array( $t_result ) ) { 1435 $t_mapping = new SourceMapping( $t_row['repo_id'], $t_row['branch'], $t_row['type'], $t_row['version'], $t_row['regex'], $t_row['pvm_version_id'] ); 1436 $t_mapping->_new = false; 1437 1438 $t_mappings[$t_mapping->branch] = $t_mapping; 1439 } 1440 1441 return $t_mappings; 1442 } 1443 1444 /** 1445 * Given a bug ID, apply the appropriate branch mapping algorithm 1446 * to find and return the appropriate version ID. 1447 * @param int $p_bug_id Bug ID 1448 * @return int Version ID 1449 */ 1450 function apply( $p_bug_id ) { 1451 static $s_versions = array(); 1452 static $s_versions_sorted = array(); 1453 1454 # if it's explicit, return the version_id before doing anything else 1455 if ( $this->type == SOURCE_EXPLICIT ) { 1456 return $this->version; 1457 } 1458 1459 # cache project/version sets, and the appropriate sorting 1460 $t_project_id = bug_get_field( $p_bug_id, 'project_id' ); 1461 if ( !isset( $s_versions[ $t_project_id ] ) ) { 1462 $s_versions[ $t_project_id ] = version_get_all_rows( $t_project_id, false ); 1463 } 1464 1465 # handle empty version sets 1466 if ( count( $s_versions[ $t_project_id ] ) < 1 ) { 1467 return ''; 1468 } 1469 1470 # cache the version set based on the current algorithm 1471 if ( !isset( $s_versions_sorted[ $t_project_id ][ $this->type ] ) ) { 1472 $s_versions_sorted[ $t_project_id ][ $this->type ] = $s_versions[ $t_project_id ]; 1473 1474 switch( $this->type ) { 1475 case SOURCE_NEAR: 1476 usort( $s_versions_sorted[ $t_project_id ][ $this->type ], array( 'SourceMapping', 'cmp_near' ) ); 1477 break; 1478 case SOURCE_FAR: 1479 usort( $s_versions_sorted[ $t_project_id ][ $this->type ], array( 'SourceMapping', 'cmp_far' ) ); 1480 break; 1481 case SOURCE_FIRST: 1482 usort( $s_versions_sorted[ $t_project_id ][ $this->type ], array( 'SourceMapping', 'cmp_first' ) ); 1483 break; 1484 case SOURCE_LAST: 1485 usort( $s_versions_sorted[ $t_project_id ][ $this->type ], array( 'SourceMapping', 'cmp_last' ) ); 1486 break; 1487 } 1488 } 1489 1490 # pull the appropriate versions set from the cache 1491 $t_versions = $s_versions_sorted[ $t_project_id ][ $this->type ]; 1492 1493 # handle non-regex mappings 1494 if ( is_blank( $this->regex ) ) { 1495 return $t_versions[0]['version']; 1496 } 1497 1498 # handle regex mappings 1499 foreach( $t_versions as $t_version ) { 1500 if ( preg_match( $this->regex, $t_version['version'] ) ) { 1501 return $t_version['version']; 1502 } 1503 } 1504 1505 # no version matches the regex 1506 return ''; 1507 } 1508 1509 /** 1510 * Given a bug ID, apply the appropriate branch mapping algorithm 1511 * to find and return the appropriate product matrix version ID. 1512 * @param int $p_bug_id Bug ID 1513 * @return int Product version ID 1514 */ 1515 function apply_pvm( $p_bug_id ) { 1516 # if it's explicit, return the version_id before doing anything else 1517 if ( $this->type == SOURCE_EXPLICIT ) { 1518 return $this->pvm_version_id; 1519 } 1520 1521 # no version matches the regex 1522 return 0; 1523 } 1524 1525 function cmp_near( $a, $b ) { 1526 return strcmp( $a['date_order'], $b['date_order'] ); 1527 } 1528 function cmp_far( $a, $b ) { 1529 return strcmp( $b['date_order'], $a['date_order'] ); 1530 } 1531 function cmp_first( $a, $b ) { 1532 return version_compare( $a['version'], $b['version'] ); 1533 } 1534 function cmp_last( $a, $b ) { 1535 return version_compare( $b['version'], $a['version'] ); 1536 } 1537} 1538 1539/** 1540 * Object for handling VCS username associations. 1541 */ 1542class SourceUser { 1543 var $new = true; 1544 1545 var $user_id; 1546 var $username; 1547 1548 function __construct( $p_user_id, $p_username='' ) { 1549 $this->user_id = $p_user_id; 1550 $this->username = $p_username; 1551 } 1552 1553 /** 1554 * Load a user object from the database for a given user ID, or generate 1555 * a new object if the database entry does not exist. 1556 * @param int $p_user_id User ID 1557 * @return object User object 1558 */ 1559 static function load( $p_user_id ) { 1560 $t_user_table = plugin_table( 'user', 'Source' ); 1561 1562 $t_query = "SELECT * FROM $t_user_table WHERE user_id=" . db_param(); 1563 $t_result = db_query( $t_query, array( $p_user_id ) ); 1564 1565 if ( db_num_rows( $t_result ) > 0 ) { 1566 $t_row = db_fetch_array( $t_result ); 1567 1568 $t_user = new SourceUser( $t_row['user_id'], $t_row['username'] ); 1569 $t_user->new = false; 1570 1571 } else { 1572 $t_user = new SourceUser( $p_user_id ); 1573 } 1574 1575 return $t_user; 1576 } 1577 1578 /** 1579 * Load all user objects from the database and create an array indexed by 1580 * username, pointing to user IDs. 1581 * @return array Username mappings 1582 */ 1583 static function load_mappings() { 1584 $t_user_table = plugin_table( 'user', 'Source' ); 1585 1586 $t_query = "SELECT * FROM $t_user_table"; 1587 $t_result = db_query( $t_query ); 1588 1589 $t_usernames = array(); 1590 while( $t_row = db_fetch_array( $t_result ) ) { 1591 $t_usernames[ $t_row['username'] ] = $t_row['user_id']; 1592 } 1593 1594 return $t_usernames; 1595 } 1596 1597 /** 1598 * Persist a user object to the database. If the user object contains a blank 1599 * username, then delete any existing data from the database to minimize storage. 1600 */ 1601 function save() { 1602 $t_user_table = plugin_table( 'user', 'Source' ); 1603 1604 # handle new objects 1605 if ( $this->new ) { 1606 if ( is_blank( $this->username ) ) { # do nothing 1607 return; 1608 1609 } else { # insert new entry 1610 $t_query = "INSERT INTO $t_user_table ( user_id, username ) VALUES (" . 1611 db_param() . ', ' . db_param() . ')'; 1612 db_query( $t_query, array( $this->user_id, $this->username ) ); 1613 1614 $this->new = false; 1615 } 1616 1617 # handle loaded objects 1618 } else { 1619 if ( is_blank( $this->username ) ) { # delete existing entry 1620 $t_query = "DELETE FROM $t_user_table WHERE user_id=" . db_param(); 1621 db_query( $t_query, array( $this->user_id ) ); 1622 1623 } else { # update existing entry 1624 $t_query = "UPDATE $t_user_table SET username=" . db_param() . 1625 ' WHERE user_id=' . db_param(); 1626 db_query( $t_query, array( $this->username, $this->user_id ) ); 1627 } 1628 } 1629 } 1630} 1631