PageRenderTime 59ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/Source/Source.API.php

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