/[drupal]/contributions/modules/versioncontrol_project/versioncontrol_project.module
ViewVC logotype

Contents of /contributions/modules/versioncontrol_project/versioncontrol_project.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.77 - (show annotations) (download) (as text)
Mon Jun 1 13:23:33 2009 UTC (5 months, 3 weeks ago) by jpetso
Branch: MAIN
CVS Tags: DRUPAL-6--1-0-RC2, HEAD
Changes since 1.76: +192 -5 lines
File MIME type: text/x-php
Port of #371969 by dww, adapted and extended for cross-VCS purposes:
Provide blocks for maintainer and developer commit information.
1 <?php
2 // $Id: versioncontrol_project.module,v 1.76 2009/05/31 17:32:33 jpetso Exp $
3 /**
4 * @file
5 * Version Control / Project Node integration - Integrates project nodes
6 * (provided by the Project module) with version control systems supported
7 * by the Version Control API.
8 *
9 * Copyright 2006, 2007, 2009 by Derek Wright ("dww", http://drupal.org/user/46549)
10 * Copyright 2007, 2008, 2009 by Jakob Petsovits ("jpetso", http://drupal.org/user/56020)
11 */
12
13 /**
14 * The nid used for project/item associations in {versioncontrol_project_items}
15 * that have been checked, but are not associated to any project.
16 * Useful for keeping track of which items have been checked and which haven't.
17 */
18 define('VERSIONCONTROL_PROJECT_NID_NONE', 0);
19
20 /**
21 * @name Project relation constraints
22 * Allowed values for use with the 'project_relation' constraint in
23 * versioncontrol_get_operations() queries.
24 */
25 //@{
26 define('VERSIONCONTROL_PROJECT_ASSOCIATED', 1);
27 define('VERSIONCONTROL_PROJECT_ASSOCIATED_PUBLISHED', 2);
28 //@}
29
30
31 /**
32 * Implementation of hook_boot():
33 * Code that is run on every page request (even on cached ones).
34 */
35 function versioncontrol_project_boot() {
36 if (module_exists('metrics')) {
37 include_once(drupal_get_path('module', 'versioncontrol_project') .'/versioncontrol_project.metrics.inc');
38 }
39 }
40
41 /**
42 * Check if any project/item associations have been missed, and if so,
43 * determine their status and write it to the database.
44 */
45 function _versioncontrol_project_write_missing_item_associations() {
46 // SELECT item_revision_id WHERE ir NOT IN projitem
47 // ...only a tad more portable:
48 $result = db_query('
49 SELECT ir.item_revision_id, ir.repo_id, ir.path
50 FROM {versioncontrol_item_revisions} ir
51 LEFT JOIN {versioncontrol_project_items} projitem
52 ON ir.item_revision_id = projitem.item_revision_id
53 WHERE projitem.item_revision_id IS NULL');
54
55 while ($item = db_fetch_object($result)) {
56 $project = versioncontrol_project_get_project_for_item(
57 $item->repo_id, $item->path, TRUE // include unpublished projects
58 );
59 _versioncontrol_project_add_item_association($item->item_revision_id,
60 (empty($project) ? VERSIONCONTROL_PROJECT_NID_NONE : $project['nid'])
61 );
62 }
63 }
64
65 /**
66 * Implementation of hook_menu().
67 */
68 function versioncontrol_project_menu() {
69 $items = array();
70
71 $items['admin/project/versioncontrol-settings/project'] = array(
72 'title' => 'Project node integration',
73 'description' => 'Configure how project nodes will be integrated with version control systems.',
74 'page callback' => 'drupal_get_form',
75 'page arguments' => array('versioncontrol_project_admin_form'),
76 'access callback' => 'versioncontrol_admin_access',
77 'type' => MENU_LOCAL_TASK,
78 );
79
80 // Two publicly viewable extensions to the project node.
81 $items['node/%versioncontrol_project_node/commitlog'] = array(
82 'title' => 'Commit messages',
83 'page callback' => 'versioncontrol_project_commitlog',
84 'page arguments' => array(1),
85 'access callback' => 'node_access',
86 'access arguments' => array('view', 1),
87 'type' => MENU_CALLBACK,
88 'weight' => 4,
89 );
90 $items['node/%versioncontrol_project_node/developers'] = array(
91 'title' => 'Developers',
92 'page callback' => 'versioncontrol_project_developers',
93 'page arguments' => array(1),
94 'access callback' => 'node_access',
95 'access arguments' => array('view', 1),
96 'type' => MENU_CALLBACK,
97 'weight' => 6,
98 );
99
100 // If a user is viewing a project node that they own (or the user has
101 // the 'administer nodes' permission), add the 'Commit access' tab,
102 // but only if the project is associated with a repository.
103 $items['node/%versioncontrol_project_node/commit-access'] = array(
104 'title' => 'Commit access',
105 'page callback' => 'drupal_get_form',
106 'page arguments' => array('versioncontrol_project_commit_access_form', 1),
107 'access callback' => 'versioncontrol_project_commit_access_edit_access',
108 'access arguments' => array(1),
109 'type' => MENU_LOCAL_TASK,
110 'weight' => 5,
111 );
112 $items['node/%versioncontrol_project_node/commit-access/%user/delete'] = array(
113 'title' => 'Commit access',
114 'page callback' => 'drupal_get_form',
115 'page arguments' => array('versioncontrol_project_commit_access_delete_confirm', 1, 3),
116 'access callback' => 'versioncontrol_project_commit_access_edit_access',
117 'access arguments' => array(1),
118 'type' => MENU_CALLBACK,
119 );
120
121 return $items;
122 }
123
124 /**
125 * Return TRUE if a given (project) node has a repository location associated,
126 * or FALSE otherwise.
127 */
128 function versioncontrol_project_node_uses_versioncontrol($node) {
129 if (empty($node->versioncontrol_project) || empty($node->versioncontrol_project['repo_id'])) {
130 return FALSE;
131 }
132 return TRUE;
133 }
134
135 /**
136 * Menu wildcard loader for version control enabled project nodes
137 * ('%versioncontrol_project_node'). Returns FALSE if the project is not
138 * associated with any repository location, or the node object otherwise.
139 */
140 function versioncontrol_project_node_load($nid) {
141 $node = node_load($nid);
142 if (versioncontrol_project_node_uses_versioncontrol($node)) {
143 return $node;
144 }
145 return FALSE;
146 }
147
148 /**
149 * Get the version-control-integrated project node from the currently active
150 * menu context, if possible.
151 *
152 * @return
153 * A fully loaded project $node object if the currently active menu has a
154 * project node context, or FALSE if the menu isn't pointing to a
155 * project node or the node does not use version control functionality.
156 */
157 function versioncontrol_project_node_from_menu() {
158 if ($node = menu_get_object('versioncontrol_project_node')) {
159 return $node;
160 }
161 $node = menu_get_object();
162 if (is_object($node) && versioncontrol_project_node_uses_versioncontrol($node)) {
163 return $node;
164 }
165 return FALSE;
166 }
167
168 /**
169 * Custom access callback, ensuring that the current user (or the one given
170 * in @p $account, if set) is permitted to view and edit the list of people
171 * with commit access for a given project node.
172 *
173 * @param $project_node
174 * A project node associated to a repository location, ideally loaded
175 * with versioncontrol_project_node_load().
176 */
177 function versioncontrol_project_commit_access_edit_access($project_node, $account = NULL) {
178 if (!isset($account)) {
179 global $user;
180 $account = clone $user;
181 }
182 if (!node_access('view', $project_node, $account)) {
183 return FALSE;
184 }
185 $repo_id = $project_node->versioncontrol_project['repo_id'];
186 $repository = versioncontrol_get_repository($repo_id);
187
188 // Grant access to the node owner.
189 if ($project_node->uid == $account->uid
190 && versioncontrol_is_account_authorized($repository, $account->uid)) {
191 return TRUE;
192 }
193 // Grant access to version control and node admins.
194 if (versioncontrol_admin_access($account) || user_access('administer nodes', $account)) {
195 return TRUE;
196 }
197 return FALSE;
198 }
199
200 /**
201 * Implementation of project.module's hook_project_page_link_alter():
202 * Add a link to the project's commit log to the resources section.
203 */
204 function versioncontrol_project_project_page_link_alter(&$links, $node) {
205 if (versioncontrol_project_node_uses_versioncontrol($node)) {
206 $links['development']['links']['view_commitlog'] = l(t('View commit messages'), 'node/'. $node->nid .'/commitlog');
207 }
208 }
209
210
211 /**
212 * Form callback for 'admin/project/versioncontrol-settings/project':
213 * Global settings for this module.
214 */
215 function versioncontrol_project_admin_form(&$form_state) {
216 $form = array();
217 $repositories = versioncontrol_get_repositories();
218 $repository_options = array();
219
220 foreach ($repositories as $repo_id => $repository) {
221 if (empty($repository_options)) {
222 $first_repo_id = $repo_id;
223 }
224 $repository_options[$repo_id] = check_plain($repository['name']);
225 }
226
227 $form['versioncontrol_project_restrict_commits'] = array(
228 '#type' => 'checkbox',
229 '#title' => t('Enable project-based commit restrictions'),
230 '#description' => t('Restrict commit access to projects that the user maintains. (This feature requires pre-commit hook scripts that integrate with the Version Control API.)'),
231 '#default_value' => variable_get('versioncontrol_project_restrict_commits', 1),
232 '#weight' => -20,
233 );
234 $form['versioncontrol_project_restrict_creation'] = array(
235 '#title' => t('Restrict project creation to users with VCS accounts'),
236 '#type' => 'checkbox',
237 '#default_value' => variable_get('versioncontrol_project_restrict_creation', 1),
238 '#description' => t('If this box is checked, only users with VCS accounts will be allowed to create project nodes.'),
239 '#weight' => -18,
240 );
241
242 if (module_exists('project') && project_use_taxonomy()) {
243 $form['type_validation'] = array(
244 '#title' => t('Validate directory by project type'),
245 '#type' => 'fieldset',
246 '#collapsible' => TRUE,
247 '#collapsed' => FALSE,
248 '#weight' => -1,
249 );
250 $form['type_validation']['versioncontrol_project_dir_validate_by_type'] = array(
251 '#title' => t('Validate directory by project type'),
252 '#type' => 'checkbox',
253 '#default_value' => variable_get('versioncontrol_project_dir_validate_by_type', 1),
254 '#description' => t('If this box is checked, the path specified in the project directory field must match the path given for the respective project type, as defined below. For each type, you can specify a PHP regular expression for allowed project directories. Example: "@^/contributions/modules/(.+)$@". If empty, all directories will be allowed for the respective project type.'),
255 );
256 $terms = taxonomy_get_tree(_project_get_vid());
257 foreach ($terms as $term) {
258 // Only use the first-level terms.
259 if ($term->depth == 0) {
260 $form['type_validation']['versioncontrol_project_directory_tid_'. $term->tid] = array(
261 '#title' => t('Directory for %term_name', array('%term_name' => $term->name)),
262 '#type' => 'textfield',
263 '#default_value' => variable_get('versioncontrol_project_directory_tid_'. $term->tid, ''),
264 );
265 }
266 }
267 }
268
269 return system_settings_form($form);
270 }
271
272
273 /**
274 * Implementation of hook_nodeapi():
275 * Load the project array into $node->versioncontrol_project if there is
276 * a project for this node, and update/delete the project when the node
277 * is being deleted.
278 */
279 function versioncontrol_project_nodeapi(&$node, $op, $arg = NULL) {
280 if ($node->type == 'project_project') {
281 switch ($op) {
282 case 'load':
283 $project = versioncontrol_project_get_project($node->nid, TRUE);
284 if (isset($project)) {
285 $node->versioncontrol_project = $project;
286 }
287 return;
288
289 case 'validate':
290 // Fail validation if the user doesn't have an account.
291 // This is ugly to do in the validation phase, but the only clean
292 // solution so far as node_access() doesn't provide a general hook.
293 // So, the best thing is probably to keep this as fallback and insert
294 // conditional checks in Project and similar modules.
295 global $user;
296 if (!versioncontrol_project_creation_is_allowed($user->uid)) {
297 form_set_error('nid', t('You cannot create projects without having a version control system account assigned.'));
298 }
299 return;
300
301 case 'insert':
302 case 'update':
303 // The node array possibly contains repo_id and project_directory
304 // as the submit values of the (form_altered) node edit form.
305 if (!isset($node->repo_id)) {
306 versioncontrol_project_delete_project($node->nid);
307 unset($node->versioncontrol_project);
308 unset($node->repo_id);
309 unset($node->project_directory);
310 }
311 else {
312 $directory = _versioncontrol_project_remove_trailing_slashes($node->project_directory);
313 $project = array(
314 'nid' => $node->nid,
315 'owner_uid' => $node->uid,
316 'repo_id' => $node->repo_id,
317 'directory' => $node->project_directory,
318 );
319 $node->versioncontrol_project = versioncontrol_project_set_project($project);
320 }
321 return;
322
323 case 'delete':
324 versioncontrol_project_delete_project($node->nid);
325 return;
326
327 default:
328 return;
329 }
330 }
331 }
332
333 /**
334 * Implementation of hook_form_alter():
335 * Add a fieldset to the add/edit project form where the user can specify
336 * the project's repository and path.
337 */
338 function versioncontrol_project_form_alter(&$form, $form_state, $form_id) {
339 if (isset($form['#id']) && $form['#id'] == 'node-form' && $form['#node']->type == 'project_project') {
340 $node = $form['#node'];
341 $project = isset($node->versioncontrol_project) ? $node->versioncontrol_project : NULL;
342 $accounts = versioncontrol_get_accounts(array('uids' => array($node->uid)));
343
344 // Default setting: no version control integration at all
345 $form['repo_id'] = array(
346 '#type' => 'value',
347 '#value' => 0,
348 );
349
350 // If the user doesn't have commit access to at least one repository,
351 // it makes no sense to present version control integration options.
352 if (empty($accounts)) {
353 return;
354 }
355
356 // Retrieve the possible repository options.
357 $user_repo_ids = array(); // repositories where the user has an account
358 foreach ($accounts as $uid => $usernames_by_repository) {
359 foreach ($usernames_by_repository as $repo_id => $username) {
360 $user_repo_ids[] = $repo_id;
361 }
362 }
363 $repositories = versioncontrol_get_repositories(array('repo_ids' => $user_repo_ids));
364
365 $repository_options = array(0 => t('<none>'));
366 foreach ($repositories as $repo_id => $repository) {
367 if (empty($repository_options)) {
368 $first_repo_id = $repo_id;
369 }
370 $repository_options[$repo_id] = check_plain($repository['name']);
371 }
372
373 if (empty($repository_options)) {
374 return;
375 }
376
377 // We're ready to go, add the form elements now.
378 $form['#validate'][] = 'versioncontrol_project_form_validate';
379
380 $form['versioncontrol_project'] = array(
381 '#type' => 'fieldset',
382 '#title' => t('Version control integration'),
383 '#collapsible' => TRUE,
384 '#collapsed' => isset($project),
385 );
386 $form['versioncontrol_project']['new_repository'] = array(); // currently unused
387 $form['versioncontrol_project']['existing_repository'] = array();
388
389 if (count($repository_options) > 1) {
390 unset($form['repo_id']);
391 $form['versioncontrol_project']['existing_repository']['repo_id'] = array(
392 '#type' => 'select',
393 '#title' => t('Repository'),
394 '#description' => t('The version control repository where this project is located.'),
395 '#default_value' => isset($project) ? $project['repo_id'] : $first_repo_id,
396 '#options' => $repository_options,
397 );
398 $form['versioncontrol_project']['project_directory'] = array(
399 '#type' => 'textfield',
400 '#title' => t('Project directory'),
401 '#description' => t("The project's directory within the selected repository. Directory names should start with a leading slash and must be unique for each project. For example: <code>/modules/foo</code>, <code>/themes/foo</code> or <code>/translations/foo</code>. If there is no repository associated with the project, this setting should be left blank."),
402 '#default_value' => isset($project) ? $project['directory'] : '',
403 '#size' => 40,
404 '#maxlength' => 255,
405 );
406 }
407 }
408 }
409
410 /**
411 * Validate the add/edit project form before it is submitted.
412 */
413 function versioncontrol_project_form_validate($form, &$form_state) {
414 $repo_id = $form_state['values']['repo_id'];
415 $admin_access = versioncontrol_admin_access();
416
417 // Don't allow changing the repository after the project has been created.
418 if (is_numeric($form_state['values']['nid'])) {
419 $project = versioncontrol_project_get_project($form_state['values']['nid'], TRUE);
420
421 if (!$admin_access && isset($project) && $repo_id != $project['repo_id']) {
422 form_error($form['versioncontrol_project']['existing_repository']['repo_id'],
423 t('You do not have permission to modify the repository for this project. (The repository can\'t be changed after the project has been created.)')
424 );
425 return;
426 }
427 }
428
429 if (!$repo_id) {
430 // No version control integration, we don't have to validate.
431 return;
432 }
433
434 $repository = versioncontrol_get_repository($repo_id);
435
436 if (!isset($repository)) {
437 form_set_error($form['versioncontrol_project']['existing_repository']['repo_id'],
438 t('You must select a valid repository.')
439 );
440 return;
441 }
442 if (empty($form_state['values']['project_directory'])) {
443 form_error($form['versioncontrol_project']['project_directory'],
444 t('You can not specify a directory if there is no repository for this project.')
445 );
446 return;
447 }
448
449 $directory = _versioncontrol_project_remove_trailing_slashes(
450 $form_state['values']['project_directory']
451 );
452 if (!preg_match('/^[a-zA-Z0-9\/_-]+$/', $directory)) {
453 form_error($form['versioncontrol_project']['project_directory'],
454 t("The path of the project directory can only contain letters, numbers, slashes ('/'), hyphens ('-') and underscores ('_').")
455 );
456 return;
457 }
458
459 // Don't do this if another project with the same directory already exists.
460 $existing_project = versioncontrol_project_get_project_for_item($repository, $directory, TRUE);
461 if (isset($existing_project) && $existing_project['nid'] != $form_state['values']['nid']) {
462 form_error($form['versioncontrol_project']['project_directory'],
463 t('The specified project directory conflicts with that of an existing project.')
464 );
465 return;
466 }
467
468 // This one is especially project.module specific. Please leave the
469 // module test, I still hope that we might go back to supporting any kind
470 // of content type sometime again.
471 if (module_exists('project')) {
472 if (project_use_taxonomy() && isset($form_state['values']['project_type'])
473 && variable_get('versioncontrol_project_dir_validate_by_type', 1)) {
474 $project_type_tid = $form_state['values']['project_type'];
475 $tree = taxonomy_get_term($project_type_tid);
476
477 $dir_regexp = variable_get('versioncontrol_project_directory_tid_'. $project_type_tid, '');
478
479 if (!empty($dir_regexp)) { // empty means "all directories allowed"
480 $directory_with_slash = ($directory == '/') ? '/' : $directory .'/';
481 if (!preg_match($dir_regexp, $directory_with_slash)) {
482 form_error($form['versioncontrol_project']['project_directory'],
483 t('The root of the project directory does not match the selected project type (%type). Given the current project type, the directory path should match the following regular expression: %goal.', array('%type' => $tree->name, '%goal' => $dir_regexp))
484 );
485 return;
486 }
487 }
488 }
489
490 if (variable_get('versioncontrol_project_validate_by_short_name', 1)) {
491 $last_element = array_pop(explode('/', $directory));
492 if ($last_element != $form_state['values']['project']['uri']) {
493 form_error($form['versioncontrol_project']['project_directory'],
494 t('The last part of the project directory (%last) does not match the short project name (%short).', array(
495 '%last' => $last_element,
496 '%short' => $form_state['values']['project']['uri'],
497 ))
498 );
499 return;
500 }
501 }
502 }
503 }
504
505 /**
506 * Make sure the directory starts with, but does not end with a slash.
507 */
508 function _versioncontrol_project_remove_trailing_slashes($directory) {
509 $directory = trim($directory); // remove whitespace
510 if (!empty($directory) && $directory != '/') {
511 $directory = '/'. trim($directory, '/');
512 }
513 return $directory;
514 }
515
516
517 /**
518 * Form callback for 'node/%versioncontrol_project_node/commit-access':
519 * Overview / management of project maintainers. This is just the minimal
520 * version, the real form is done in the theming function further down,
521 * as we don't want to drupal_render() the "new user" textfield and the
522 * "Grant access" button at this point already.
523 */
524 function versioncontrol_project_commit_access_form(&$form_state, $node) {
525 $form = array();
526 $project = $node->versioncontrol_project;
527 $repository = versioncontrol_get_repository($project['repo_id']);
528
529 if ($node->type == 'project_project') {
530 project_project_set_breadcrumb($node);
531 }
532
533 $form['#nid'] = $node->nid;
534 $form['#repo_id'] = $project['repo_id'];
535 $form['#maintainer_uids'] = $project['maintainer_uids'];
536
537 // The user id for a new project co-maintainer,
538 // we'll fill this in with a real value during the validation hook.
539 $form['uid'] = array(
540 '#type' => 'hidden',
541 '#value' => 0,
542 );
543
544 $form['introduction'] = array(
545 '#type' => 'markup',
546 '#value' => t(
547 "This page controls commit access for the %title project. Unless otherwise indicated, all users listed in this table have permission to commit and tag files in this project's directory in @repository (%directory). The project owner is listed first and always has full access.", array(
548 '%title' => $node->title,
549 '%directory' => $project['directory'],
550 '@repository' => $repository['name'],
551 )),
552 '#prefix' => '<p>',
553 '#suffix' => '</p>',
554 );
555
556 // The actual table is created inside the '#after_build' function.
557 $form['table'] = array(
558 '#type' => 'markup',
559 '#value' => '',
560 '#node' => $node, // remember that one for the '#after_build' function
561 '#repository' => $repository, // and this one as well
562 '#after_build' => array('versioncontrol_project_commit_access_table'),
563 );
564 $form['table']['username'] = array(
565 '#type' => 'textfield',
566 //'#required' => TRUE, // already checked by the validation hook
567 '#size' => 30,
568 '#maxlength' => 60,
569 '#autocomplete_path' => 'versioncontrol/user/autocomplete/'. $project['repo_id'],
570 );
571 $form['table']['submit'] = array(
572 '#type' => 'submit',
573 '#value' => t('Grant access'),
574 );
575
576 return $form;
577 }
578
579 /**
580 * The maintainers table for the above commit access form,
581 * constructed by means of an '#after_build' callback function.
582 */
583 function versioncontrol_project_commit_access_table($form, $form_state) {
584 $node = $form['#node'];
585 $project = $node->versioncontrol_project;
586 $project_repository = $form['#repository'];
587
588 $rows = array();
589 $header = array();
590 $header[] = array('data' => t('Drupal username'), 'field' => 'name');
591 $header[] = array('data' => t('Actions'));
592
593 // The owner of the node automatically gets commit access, list this user
594 // as the first row in the table, and don't allow any operations.
595 $rows[] = array(
596 theme('username', $node), // theme_username() only needs ->uid and ->name.
597 '<span class="disabled">'. t('locked') .'</span>'
598 );
599
600 // ...and now the co-maintainers...
601 foreach ($project['comaintainer_uids'] as $uid) {
602 $user = user_load(array('uid' => $uid));
603 if (!$user) {
604 continue; // safety check, should not happen
605 }
606
607 // Indicate any maintainers whose account is no longer approved.
608 if (!versioncontrol_is_account_authorized($project_repository, $uid)) {
609 $username = '<del>'. theme('username', $user) .'</del> <em>('. t('repository access disabled') .')</em>';
610 }
611 else {
612 $username = theme('username', $user);
613 }
614 $rows[] = array($username, l(
615 t('Delete'), 'node/'. $node->nid .'/commit-access/'. $uid .'/delete'
616 ));
617 }
618
619 // The "new user" and "Grant access" controls from the original form.
620 $rows[] = array(drupal_render($form['username']), drupal_render($form['submit']));
621
622 // The table is done, rejoice.
623 $form['#value'] = theme('table', $header, $rows);
624
625 return $form;
626 }
627
628 /**
629 * The validation hook for the commit access form:
630 * check the validity of the new project maintainer that should be added.
631 */
632 function versioncontrol_project_commit_access_form_validate($form, &$form_state) {
633 $username = $form_state['values']['username'];
634 $repo_id = $form['#repo_id'];
635 $maintainer_uids = $form['#maintainer_uids'];
636
637 if (empty($username)) {
638 form_error($form['table']['username'], t('You must specify a valid user name.'));
639 return;
640 }
641
642 $result = db_fetch_object(db_query("SELECT name, uid FROM {users}
643 WHERE name = '%s'", $username));
644
645 if (!isset($result)) {
646 form_error($form['table']['username'],
647 t('%user is not a valid user on this site.', array('%user' => $username))
648 );
649 return;
650 }
651
652 $uid = $result->uid;
653 $vcs_username = versioncontrol_get_account_username_for_uid($repo_id, $uid);
654
655 if (!isset($vcs_username)) {
656 $repository = versioncontrol_get_repository($repo_id);
657 form_error($form['table']['username'], t('%user does not have an account in @repository.', array('%user' => $username, '@repository' => $repository['name'])));
658 return;
659 }
660 if (in_array($uid, $maintainer_uids)) {
661 form_error($form['table']['username'], t('%user already has commit access for this project.', array('%user' => $username)));
662 return;
663 }
664
665 // Save the uid in the form so we don't have to look it up again in
666 // submit(). We also stash user, since it's not set directly when
667 // using the special theme function to generate the form.
668 form_set_value($form['uid'], $uid, $form_state);
669 }
670
671 /**
672 * The submit hook for the commit access form: add a new project maintainer.
673 */
674 function versioncontrol_project_commit_access_form_submit($form, &$form_state) {
675 $nid = $form['#nid'];
676 $uid = $form_state['values']['uid'];
677 db_query("INSERT INTO {versioncontrol_project_comaintainers} (nid, uid)
678 VALUES (%d, %d)", $nid, $uid);
679
680 $user = new stdClass();
681 $user->uid = $form_state['values']['uid'];
682 $user->name = $form_state['values']['username'];
683 drupal_set_message(t('Commit access has been granted to !user.', array(
684 '!user' => theme('username', $user),
685 )));
686 }
687
688 /**
689 * Form callback for 'node/%versioncontrol_project_node/commit-access/%user/delete':
690 * Provide a form to confirm deletion of a user's commit access.
691 */
692 function versioncontrol_project_commit_access_delete_confirm(&$form_state, $node, $deleted_user) {
693 $form = array();
694 $project = $node->versioncontrol_project;
695
696 if ($node->type == 'project_project') {
697 project_project_set_breadcrumb($node, array(l($node->title, 'node/'. $node->nid)));
698 }
699
700 if ($deleted_user->uid == $node->uid) {
701 drupal_set_title(t('User is locked'));
702 $form['nodelete'] = array(
703 '#type' => 'markup',
704 '#value' => t('You cannot revoke commit access for !user because this user is the owner of the project.', array('!user' => theme('username', $node))),
705 );
706 return $form;
707 }
708
709 if (!in_array($deleted_user->uid, $project['comaintainer_uids'])) {
710 drupal_set_title(t('User is not a maintainer'));
711 $form['nodelete'] = array(
712 '#type' => 'markup',
713 '#value' => t('!user does not have commit access so you cannot delete that.',
714 array('!user' => theme('username', $deleted_user))),
715 );
716 return $form;
717 }
718
719 $form['#nid'] = $node->nid;
720 $form['#uid'] = $deleted_user->uid;
721
722 return confirm_form($form,
723 t('Are you sure you want to revoke commit access for !user?',
724 array('!user' => theme('username', $deleted_user))),
725 'node/'. $node->nid .'/commit-access',
726 t('This action cannot be undone.'),
727 t('Delete'),
728 t('Cancel')
729 );
730 }
731
732 /**
733 * Delete the repository when the confirmation form is submitted.
734 */
735 function versioncontrol_project_commit_access_delete_confirm_submit($form, &$form_state) {
736 $nid = $form['#nid'];
737 $uid = $form['#uid'];
738 $user = user_load($uid);
739 db_query('DELETE FROM {versioncontrol_project_comaintainers}
740 WHERE nid = %d AND uid = %d', $nid, $uid);
741 drupal_set_message(t('Commit access for !user has been revoked.', array('!user' => theme('username', $user))));
742 $form_state['redirect'] = 'node/'. $nid .'/commit-access';
743 }
744
745
746 /**
747 * Page callback for 'node/%versioncontrol_project_node/commitlog'.
748 */
749 function versioncontrol_project_commitlog($node) {
750 drupal_set_title(t('Commits for @title', array('@title' => $node->title)));
751
752 if ($node->type == 'project_project') {
753 project_project_set_breadcrumb($node, array(l($title, 'node/'. $node->nid)));
754 }
755 return commitlog_operations_page(array('nids' => array($node->nid)));
756 }
757
758 /**
759 * Page callback for 'node/%versioncontrol_project_node/developers'.
760 */
761 function versioncontrol_project_developers($node) {
762 $title = check_plain($node->title);
763 drupal_set_title(t('Developers for !title', array('!title' => $title)));
764
765 if (module_exists('project')) {
766 project_project_set_breadcrumb($node, array(l($title, 'node/'. $node->nid)));
767 }
768
769 $project = $node->versioncontrol_project;
770 $constraints = array(
771 'types' => array(VERSIONCONTROL_OPERATION_COMMIT),
772 'nids' => array($project['nid']),
773 );
774 $statistics = versioncontrol_get_operation_statistics($constraints, array(
775 'group_by' => array('uid', 'repo_id', 'username'),
776 'order_by' => array('last_operation_date'),
777 ));
778
779 return theme('versioncontrol_user_statistics_table', $statistics, array(
780 'constraints' => $constraints,
781 ));
782 }
783
784 /**
785 * Implementation of hook_user():
786 * Add a list of projects to the user page that the user has committed to.
787 */
788 function versioncontrol_project_user($type, &$edit, &$account, $category = NULL) {
789 if ($type == 'view') {
790 $result = db_query('
791 SELECT n.nid, n.title, COUNT(projop.vc_op_id) as number_commits
792 FROM {versioncontrol_operations} op
793 INNER JOIN {versioncontrol_project_operations} projop
794 ON op.vc_op_id = projop.vc_op_id
795 INNER JOIN {node} n
796 ON projop.nid = n.nid
797 WHERE op.uid = %d AND op.type = %d AND n.status = 1
798 GROUP BY n.nid, n.title
799 ORDER BY number_commits DESC',
800 array($account->uid, VERSIONCONTROL_OPERATION_COMMIT)
801 );
802 $project_titles = array();
803 $total_commits = 0;
804
805 while ($project_stats = db_fetch_object($result)) {
806 $total_commits += $project_stats->number_commits;
807 $project_name = l($project_stats->title, 'node/'. $project_stats->nid);
808 $project_titles[] = format_plural($project_stats->number_commits,
809 '!project-name (1 commit)', '!project-name (@count commits)',
810 array('!project-name' => $project_name)
811 );
812 }
813 if ($total_commits > 1) {
814 $project_titles[] = format_plural($total_commits,
815 '1 commit total', '@count commits total'
816 );
817 }
818 if (!empty($project_titles)) {
819 $account->content['versioncontrol_project'] = array(
820 '#type' => 'user_profile_category',
821 '#title' => t('Projects'),
822 '#attributes' => array('class' => 'versioncontrol-project-user-commits'),
823 );
824 $account->content['versioncontrol_project']['items'] = array(
825 '#type' => 'user_profile_item',
826 '#value' => theme('item_list', $project_titles),
827 );
828 }
829 }
830 }
831
832 /**
833 * Implementation of hook_block(): Present a list of the most active projects.
834 */
835 function versioncontrol_project_block($op = 'list', $delta = 0, $edit = array()) {
836 if ($op == 'list') {
837 $blocks = array();
838 $blocks['site_active_projects'] = array(
839 'info' => t('Version Control API: Most active projects'),
840 'cache' => BLOCK_CACHE_GLOBAL,
841 );
842 // We roll our own caching for these blocks, since the existing block cache
843 // is cleared on every comment or node added on the site, which isn't at
844 // all what we need for this. There are expensive queries for this block,
845 // and it only changes when an operation is made to a given project.
846 $blocks['project_developers'] = array(
847 'info' => t('Version Control API: Project developers'),
848 'cache' => BLOCK_NO_CACHE,
849 );
850 $blocks['project_maintainers'] = array(
851 'info' => t('Version Control API: Project maintainers'),
852 'cache' => BLOCK_NO_CACHE,
853 );
854 return $blocks;
855 }
856 else if ($op == 'view') {
857 if ($delta == 'site_active_projects') {
858 return versioncontrol_project_block_site_active_projects();
859 }
860 if ($delta == 'project_developers' || $delta == 'project_maintainers') {
861 return versioncontrol_project_block_project_committers($delta);
862 }
863 }
864 else if ($op == 'configure') {
865 if ($delta == 'project_developers' || $delta == 'project_maintainers') {
866 return versioncontrol_project_block_project_committers_configure($delta);
867 }
868 }
869 else if ($op == 'save') {
870 if ($delta == 'project_developers' || $delta == 'project_maintainers') {
871 return versioncontrol_project_block_project_committers_save($delta, $edit);
872 }
873 }
874 }
875
876 /**
877 * Implementation of hook_block($op='view') for the "active projects" block.
878 */
879 function versioncontrol_project_block_site_active_projects() {
880 $block = array();
881 $interval = 7 * 24 * 60 * 60;
882 $limit = 15;
883
884 $result = db_query_range('
885 SELECT n.nid, n.title, COUNT(projop.vc_op_id) as number_commits
886 FROM {versioncontrol_operations} op
887 INNER JOIN {versioncontrol_project_operations} projop
888 ON op.vc_op_id = projop.vc_op_id
889 INNER JOIN {node} n
890 ON projop.nid = n.nid
891 WHERE op.date >= %d AND op.type = %d AND n.status = 1
892 GROUP BY n.nid, n.title
893 ORDER BY number_commits DESC',
894 array(time() - $interval, VERSIONCONTROL_OPERATION_COMMIT),
895 0, $limit
896 );
897 $project_titles = array();
898
899 while ($project_stats = db_fetch_object($result)) {
900 $nid = $project_stats->nid;
901 $project_titles[] = l($project_stats->title, 'node/'. $nid);
902 }
903 if (!empty($project_titles)) {
904 $block = array(
905 'subject' => t('Most active projects'),
906 'content' => theme('item_list', $project_titles),
907 );
908 }
909 return $block;
910 }
911
912 /**
913 * Implementation of hook_block($op='configure') for the project developers
914 * or maintainers block.
915 *
916 * @param $delta
917 * The block delta: either 'project_developers' or 'project_maintainers'.
918 */
919 function versioncontrol_project_block_project_committers_configure($delta) {
920 $options = array();
921 for ($i = 1; $i <= 10; $i++) {
922 $options[$i] = $i;
923 }
924 $options['all'] = t('Unlimited');
925
926 $form = array();
927 $form['versioncontrol_'. $delta .'_block_length'] = array(
928 '#type' => 'select',
929 '#options' => $options,
930 '#title' => t('Number of developers to display'),
931 '#default_value' => variable_get('versioncontrol_'. $delta .'_block_length', 5),
932 );
933 return $form;
934 }
935
936 /**
937 * Implementation of hook_block($op='save') for the project developers or
938 * maintainers block.
939 *
940 * @param $delta
941 * The block delta: either 'project_developers' or 'project_maintainers'.
942 */
943 function versioncontrol_project_block_project_committers_save($delta, $edit) {
944 variable_set('versioncontrol_'. $delta .'_block_length',
945 $edit['versioncontrol_'. $delta .'_block_length']
946 );
947 // Clear the cache because it might now contain statistics for a too limited
948 // set of committers, and as an overall easy way to regenerate the values.
949 _versioncontrol_project_block_cache_clear();
950 }
951
952 /**
953 * Implementation of hook_block($op='view') for the project developers or
954 * maintainers block.
955 *
956 * @param $delta
957 * The block delta: either 'project_developers' or 'project_maintainers'.
958 */
959 function versioncontrol_project_block_project_committers($delta) {
960 $block = array();
961 $project_node = versioncontrol_project_node_from_menu();
962
963 if (!$project_node) {
964 return $block;
965 }
966 if (!user_access('access commit messages') || !node_access('view', $project_node)) {
967 return $block;
968 }
969 $cid = 'versioncontrol_'. $delta .'_statistics:'. $project_node->nid;
970 $statistics = _versioncontrol_project_block_cache_get($cid);
971
972 if (empty($statistics)) {
973 $constraints = array(
974 'types' => array(VERSIONCONTROL_OPERATION_COMMIT),
975 'nids' => array($project_node->nid),
976 );
977 if ($delta == 'project_maintainers') {
978 $constraints['uids'] = $project_node->versioncontrol_project['maintainer_uids'];
979 $group_by = array('uid'); // per Drupal user
980 }
981 else { // $delta == 'project_developers'
982 // Group per account, because no associated Drupal user might be involved.
983 $group_by = array('uid', 'repo_id', 'username');
984 }
985 $statistics = versioncontrol_get_operation_statistics($constraints, array(
986 'group_by' => $group_by,
987 'order_by' => array('last_operation_date'),
988 'query_type' => 'range',
989 'count' => variable_get('versioncontrol_'. $delta .'_block_length', 5),
990 'from' => 0,
991 ));
992 _versioncontrol_project_block_cache_set($cid, $statistics);
993 }
994
995 $title = ($delta == 'project_maintainers')
996 ? t('Maintainers for @project', array('@project' => $project_node->title))
997 : t('Developers for @project', array('@project' => $project_node->title));
998 $more_link = l(t('View all developers'), 'node/'. $project_node->nid .'/developers');
999
1000 $block = array(
1001 'subject' => $title,
1002 'content' => theme('versioncontrol_user_statistics_item_list', $statistics, $more_link),
1003 );
1004 return $block;
1005 }
1006
1007 function _versioncontrol_project_block_cache_get($cid) {
1008 $data = db_result(db_query("SELECT data FROM {versioncontrol_project_cache_block}
1009 WHERE cid = '%s'", $cid));
1010 if (!empty($data)) {
1011 return unserialize($data);
1012 }
1013 }
1014
1015 function _versioncontrol_project_block_cache_set($cid, $data) {
1016 if (empty($data)) {
1017 db_query("DELETE FROM {versioncontrol_project_cache_block}
1018 WHERE cid = '%s'", $cid);
1019 }
1020 else {
1021 $serialized = serialize($data);
1022 db_query("UPDATE {versioncontrol_project_cache_block}
1023 SET data = '%s' WHERE cid = '%s'", $serialized, $cid);
1024 if (!db_affected_rows()) {
1025 db_query("INSERT INTO {versioncontrol_project_cache_block} (cid, data)
1026 VALUES ('%s', '%s')", $cid, $serialized);
1027 }
1028 }
1029 }
1030
1031 function _versioncontrol_project_block_cache_clear() {
1032 db_query("DELETE FROM {versioncontrol_project_cache_block}");
1033 }
1034
1035
1036 /**
1037 * Implementation of hook_commitlog_constraints():
1038 * Provide a list of supported constraints and corresponding request attributes.
1039 */
1040 function versioncontrol_project_commitlog_constraints() {
1041 return array(
1042 'maintainer_uids' => array('single' => 'maintainer', 'multiple' => 'maintainers'),
1043 'nids' => array('single' => 'nid', 'multiple' => 'nids'),
1044 'project_relation' => array('single' => 'project-relation'),
1045 );
1046 }
1047
1048 /**
1049 * Implementation of hook_versioncontrol_operation_constraint_info().
1050 * This module adds the native operation constraint 'nids', which filters
1051 * by the nid of project nodes associated to the operations.
1052 */
1053 function versioncontrol_project_versioncontrol_operation_constraint_info() {
1054 return array(
1055 'nids' => array('join callback' => 'versioncontrol_project_table_project_operations_join'),
1056 'project_relation' => array(
1057 'join callback' => 'versioncontrol_project_table_project_operations_join',
1058 'cardinality' => VERSIONCONTROL_CONSTRAINT_SINGLE,
1059 ),
1060 );
1061 }
1062
1063 /**
1064 * Implementation of versioncontrol_alter_operation_constraints():
1065 * Return a modified version of the given operation constraints that includes
1066 * the given project specific constraints in a way that the Version Control API
1067 * can understand it.
1068 *
1069 * @param $constraints
1070 * The constraints array that were passed to versioncontrol_get_operations().
1071 * The following constraints will be removed and replaced by constraints that
1072 * the Version Control API can process natively:
1073 *
1074 * - 'nids': An array of project node ids.
1075 * If given, only operations for these projects will be returned.
1076 * - 'maintainer_uids': An array of Drupal user ids. If given, the result set
1077 * will only contain operations that correspond to one of the projects
1078 * that any of the specified users maintain.
1079 */
1080 function versioncontrol_project_versioncontrol_operation_constraints_alter(&$constraints) {
1081 if (isset($constraints['maintainer_uids'])) {
1082 $projects = versioncontrol_project_get_projects(array(
1083 'maintainer_uids' => $constraints['maintainer_uids'],
1084 ), TRUE);
1085 $constraints['nids'] = array();
1086
1087 foreach ($projects as $nid => $project) {
1088 $constraints['nids'][] = $nid;
1089 }
1090 unset($constraints['maintainer_uids']);
1091 }
1092 }
1093
1094 /**
1095 * Filter operations by associated project nodes.
1096 */
1097 function versioncontrol_project_operation_constraint_nids($constraint, &$tables, &$and_constraints, &$params) {
1098 $placeholders = array();
1099 foreach ($constraint as $nid) {
1100 $placeholders[] = '%d';
1101 $params[] = $nid;
1102 }
1103 $and_constraints[] = $tables['versioncontrol_project_operations']['alias'] .'.nid
1104 IN ('. implode(',', $placeholders) .')';
1105 }
1106
1107 /**
1108 * Filter operations by associated project node status.
1109 */
1110 function versioncontrol_project_operation_constraint_project_relation($constraint, &$tables, &$and_constraints, &$params) {
1111 $and_constraints[] = $tables['versioncontrol_project_operations']['alias'] .'.nid <> %d';
1112 $params[] = VERSIONCONTROL_PROJECT_NID_NONE;
1113
1114 if ($constraint == VERSIONCONTROL_PROJECT_ASSOCIATED_PUBLISHED) {
1115 versioncontrol_project_table_project_node_join($tables);
1116 $and_constraints[] = $tables['versioncontrol_project_node']['alias'] .'.status = 1';
1117 }
1118 }
1119
1120 /**
1121 * Take an existing @p $tables array and add the table join for
1122 * {versioncontrol_project_operations}. Only meant to be used within a
1123 * constraint construction callback.
1124 */
1125 function versioncontrol_project_table_project_operations_join(&$tables) {
1126 if (!isset($tables['versioncontrol_project_operations'])) {
1127 $tables['versioncontrol_project_operations'] = array(
1128 'alias' => 'projop',
1129 'join_on' => $tables['versioncontrol_operations']['alias'] .'.vc_op_id = projop.vc_op_id',
1130 );
1131 }
1132 }
1133
1134 /**
1135 * Take an existing @p $tables array and add the table joins for
1136 * {versioncontrol_project_operations} and {node}. Only meant to be used within
1137 * a constraint construction callback.
1138 */
1139 function versioncontrol_project_table_project_node_join(&$tables) {
1140 if (!isset($tables['versioncontrol_project_node'])) {
1141 versioncontrol_project_table_project_operations_join($tables);
1142
1143 $tables['versioncontrol_project_node'] = array(
1144 'real_table' => 'node',
1145 'alias' => 'projnode',
1146 'join_on' => $tables['versioncontrol_project_operations']['alias'] .'.nid = projnode.nid',
1147 );
1148 }
1149 }
1150
1151
1152 /**
1153 * Convenience function to retrieve one single project by project node id.
1154 *
1155 * @param $nid
1156 * The node id of the project node.
1157 * @param $include_unpublished
1158 * If FALSE (which is the default), this function returns NULL
1159 * when the project node is unpublished, even if the project exists.
1160 * If TRUE, the function doesn't care whether the node is published or not.
1161 *
1162 * @return
1163 * The project array for the given project node,
1164 * which consists of the following elements:
1165 *
1166 * - 'nid': The node id of the project node.
1167 * - 'owner_uid': The Drupal user id of the project owner.
1168 * - 'repo_id': The repository id of the repository where the project resides.
1169 * - 'directory': The project directory inside the repository.
1170 * - 'maintainer_uids': An array containing all maintainer uids, that is,
1171 * the project owner and the co-maintainers.
1172 * - 'comaintainer_uids': An array containing all co-maintainer uids.
1173 *
1174 * If there is no version control data for that node, NULL is returned.
1175 */
1176 function versioncontrol_project_get_project($nid, $include_unpublished = FALSE) {
1177 $projects = versioncontrol_project_get_projects(array('nids' => array($nid)), $include_unpublished);
1178 foreach ($projects as $nid => $project) {
1179 return $project;
1180 }
1181 return NULL;
1182 }
1183
1184 /**
1185 * Convenience function to retrieve the projects maintained by a given user.
1186 *
1187 * @param $uid
1188 * The user whose projects will be retrieved.
1189 * @param $include_unpublished
1190 * If FALSE (which is the default), this function does not return projects
1191 * where the corresponding node is unpublished.
1192 * If TRUE, all projects are returned, regardless of their status.
1193 *
1194 * @return
1195 * An array of projects with the project node id as key, where each element
1196 * is again a structured array that consists of the following elements:
1197 *
1198 * - 'nid': The node id of the project node.
1199 * - 'owner_uid': The Drupal user id of the project owner.
1200 * - 'repo_id': The repository id of the repository where the project resides.
1201 * - 'directory': The project directory inside the repository.
1202 * - 'maintainer_uids': An array containing all maintainer uids, that is,
1203 * the project owner and the co-maintainers.
1204 * - 'comaintainer_uids': An array containing all co-maintainer uids.
1205 *
1206 * If the given user doesn't maintain at least one project,
1207 * an empty