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