| 1 |
<?php
|
| 2 |
// $Id: versioncontrol_hg.module,v 1.14 2008/02/19 00:43:48 ezyang Exp $
|
| 3 |
/**
|
| 4 |
* @file
|
| 5 |
* Mercurial backend for Version Control API - Provides Mercurial commit
|
| 6 |
* information and account management as a pluggable backend.
|
| 7 |
*
|
| 8 |
* @note
|
| 9 |
* Docblocks for standard functions are omitted; check versioncontrol
|
| 10 |
* for the most up-to-date versions.
|
| 11 |
*
|
| 12 |
* Copyright 2008 by Edward Z. Yang (ezyang, http://drupal.org/user/211688)
|
| 13 |
*/
|
| 14 |
|
| 15 |
// Mercurial PHP interface functions
|
| 16 |
include_once(drupal_get_path('module', 'versioncontrol_hg') .'/hg/hg.inc');
|
| 17 |
|
| 18 |
/**
|
| 19 |
* Implementation of hook_versioncontrol_backends().
|
| 20 |
*/
|
| 21 |
function versioncontrol_hg_versioncontrol_backends() {
|
| 22 |
return array(
|
| 23 |
// The array key is up to 8 characters long, and used as unique identifier
|
| 24 |
// for this VCS, in functions, URLs and in the database.
|
| 25 |
'hg' => array(
|
| 26 |
// The user-visible name of the VCS.
|
| 27 |
'name' => 'Mercurial',
|
| 28 |
|
| 29 |
// A short description of the VCS, if possible not longer than one or two sentences.
|
| 30 |
'description' => t('Mercurial is a distributed revision control system written in Python.'),
|
| 31 |
|
| 32 |
// A list of optional capabilities, in addition to the required retrieval
|
| 33 |
// of detailed commit information. All allowed values are listed below.
|
| 34 |
'capabilities' => array(
|
| 35 |
// Able to retrieve a file or its revision number based on a global
|
| 36 |
// revision identifier.
|
| 37 |
VERSIONCONTROL_CAPABILITY_ATOMIC_COMMITS,
|
| 38 |
),
|
| 39 |
|
| 40 |
// An array listing which tables should be managed by Version Control API
|
| 41 |
// instead of doing it manually in the backend.
|
| 42 |
// All allowed values are listed below.
|
| 43 |
'flags' => array(
|
| 44 |
// versioncontrol_insert_repository() will automatically insert
|
| 45 |
// array elements from $repository['[xxx]_specific'] into
|
| 46 |
// {versioncontrol_[xxx]_repositories} and versioncontrol_get_repositories()
|
| 47 |
// will automatically fetch it from there.
|
| 48 |
VERSIONCONTROL_FLAG_AUTOADD_REPOSITORIES,
|
| 49 |
|
| 50 |
// versioncontrol_insert_commit() will automatically insert
|
| 51 |
// array elements from $commit['[xxx]_specific'] into
|
| 52 |
// {versioncontrol_[xxx]_commits} and versioncontrol_get_commits()
|
| 53 |
// will automatically fetch it from there.
|
| 54 |
VERSIONCONTROL_FLAG_AUTOADD_COMMITS,
|
| 55 |
),
|
| 56 |
),
|
| 57 |
);
|
| 58 |
}
|
| 59 |
|
| 60 |
/**
|
| 61 |
* Implementation of hook_cron(): Update repositories that have cron updates
|
| 62 |
* enabled.
|
| 63 |
*/
|
| 64 |
function versioncontrol_hg_cron() {
|
| 65 |
$result = db_query("SELECT repo_id FROM {versioncontrol_hg_repositories}");
|
| 66 |
// We have purposely omitted set_time_limit, as this is something that
|
| 67 |
// should be set by cron itself and not us!
|
| 68 |
while ($repo = db_fetch_object($result)) {
|
| 69 |
$repository = versioncontrol_get_repository($repo->repo_id);
|
| 70 |
if (isset($repository)) {
|
| 71 |
_versioncontrol_hg_update_repository($repository);
|
| 72 |
}
|
| 73 |
}
|
| 74 |
}
|
| 75 |
|
| 76 |
/**
|
| 77 |
* Performs all updates on a repository (currently just logs).
|
| 78 |
*/
|
| 79 |
function _versioncontrol_hg_update_repository(&$repository) {
|
| 80 |
include_once(drupal_get_path('module', 'versioncontrol_hg') .'/versioncontrol_hg.log.inc');
|
| 81 |
_versioncontrol_hg_log_update_repository($repository);
|
| 82 |
}
|
| 83 |
|
| 84 |
/**
|
| 85 |
* Implementation of [versioncontrol_backend]_get_directory_item().
|
| 86 |
*
|
| 87 |
* @warning
|
| 88 |
* I don't understand why this can't have a generic implementation.
|
| 89 |
*/
|
| 90 |
function versioncontrol_hg_get_directory_item($operation) {
|
| 91 |
$item = array(
|
| 92 |
'type' => VERSIONCONTROL_ITEM_DIRECTORY,
|
| 93 |
'path' => $operation['directory'],
|
| 94 |
'revision' => '',
|
| 95 |
// We removed selected_op; might need to readd it later
|
| 96 |
);
|
| 97 |
if (isset($operation['hg_specific']['branch_id'])) {
|
| 98 |
$item['hg_specific']['selected_branch_id'] = $operation['hg_specific']['branch_id'];
|
| 99 |
}
|
| 100 |
return $item;
|
| 101 |
}
|
| 102 |
|
| 103 |
/**
|
| 104 |
* Implementation of [versioncontrol_backend]_get_commit_branches().
|
| 105 |
*
|
| 106 |
* @warning
|
| 107 |
* This one could have a generic implementation too! The biggest problem
|
| 108 |
* is that versioncontrol doesn't directly handle branches, but if
|
| 109 |
* the module is going to call this function, it really ought to.
|
| 110 |
*/
|
| 111 |
function versioncontrol_hg_get_commit_branches($commit) {
|
| 112 |
if (!isset($commit['hg_specific']['branch_id'])) {
|
| 113 |
return array();
|
| 114 |
}
|
| 115 |
$branch = versioncontrol_get_branch($commit['hg_specific']['branch_id']);
|
| 116 |
if (!isset($branch)) {
|
| 117 |
// Only for database inconsistencies:
|
| 118 |
return array();
|
| 119 |
}
|
| 120 |
return array($branch['branch_name']);
|
| 121 |
}
|
| 122 |
|
| 123 |
/**
|
| 124 |
* Implementation of [versioncontrol_backend]_commit():
|
| 125 |
*
|
| 126 |
* @warning
|
| 127 |
* An important precondition for our particular implementation is that
|
| 128 |
* all earlier revisions must be in the database.
|
| 129 |
*/
|
| 130 |
function versioncontrol_hg_commit($op, $commit, $commit_actions) {
|
| 131 |
switch ($op) {
|
| 132 |
case 'insert':
|
| 133 |
foreach ($commit_actions as $path => $action) {
|
| 134 |
|
| 135 |
if ($path === '/.hgtags') {
|
| 136 |
$repo_id = $commit['repository']['repo_id'];
|
| 137 |
// We've got to handle the tags!
|
| 138 |
// This has nothing to do with what's going on below.
|
| 139 |
db_query("DELETE FROM {versioncontrol_hg_tags} WHERE repo_id = %d", $repo_id);
|
| 140 |
$hgtags = hg_cat($commit['repository']['root'], '.hgtags', $commit['hg_specific']['rev']);
|
| 141 |
$tags = _hg_parse_hgtags($hgtags);
|
| 142 |
foreach ($tags as $name => $nodeid) {
|
| 143 |
$vc_op_id = db_result(db_query(
|
| 144 |
"SELECT vc_op_id FROM {versioncontrol_commits} WHERE revision = '%s';",
|
| 145 |
$nodeid
|
| 146 |
));
|
| 147 |
$tag_id = db_next_id('{versioncontrol_hg_tags}_tag_id');
|
| 148 |
db_query("INSERT INTO {versioncontrol_hg_tags}
|
| 149 |
(tag_id, vc_op_id, repo_id, name) VALUES (%d, %d, %d, '%s');",
|
| 150 |
$tag_id, $vc_op_id, $repo_id, $name
|
| 151 |
);
|
| 152 |
}
|
| 153 |
}
|
| 154 |
|
| 155 |
// Determine source information for this particular path
|
| 156 |
// based on both the commit's parent information and this
|
| 157 |
// filename. This is *very* Mercurial-specific.
|
| 158 |
|
| 159 |
$source = array();
|
| 160 |
foreach (array(1, 2) as $k) {
|
| 161 |
$rev = (string) $commit['hg_specific']["parent$k"];
|
| 162 |
$source[$k] = array();
|
| 163 |
if ($rev === '-1') {
|
| 164 |
// No parent, so our job is really easy.
|
| 165 |
$source[$k]['path'] = null;
|
| 166 |
$source[$k]['vc_op_id'] = null;
|
| 167 |
continue;
|
| 168 |
}
|
| 169 |
// Determine the branch of the parent
|
| 170 |
$branch_id = db_result(db_query(
|
| 171 |
"SELECT branch_id FROM {versioncontrol_hg_commits}
|
| 172 |
WHERE rev = %d",
|
| 173 |
$rev)
|
| 174 |
);
|
| 175 |
if ($branch_id === false) {
|
| 176 |
// Database inconsistency!
|
| 177 |
$source[$k]['path'] = null;
|
| 178 |
$source[$k]['vc_op_id'] = null;
|
| 179 |
continue;
|
| 180 |
}
|
| 181 |
|
| 182 |
// This step may need to be done multiple times
|
| 183 |
do {
|
| 184 |
|
| 185 |
// Now, let's determine the latest commit_action for this
|
| 186 |
// path on this branch: this will be the parent revision.
|
| 187 |
$result = db_fetch_array(db_query(
|
| 188 |
"SELECT actions.vc_op_id, actions.type, commits.rev
|
| 189 |
FROM {versioncontrol_hg_commit_actions} AS actions
|
| 190 |
JOIN {versioncontrol_hg_commits} AS commits
|
| 191 |
ON actions.vc_op_id = commits.vc_op_id
|
| 192 |
WHERE
|
| 193 |
actions.path = '%s' AND
|
| 194 |
commits.branch_id = %d AND
|
| 195 |
commits.rev <= %d
|
| 196 |
ORDER BY commits.rev DESC
|
| 197 |
LIMIT 1
|
| 198 |
",
|
| 199 |
$path, $branch_id, $rev));
|
| 200 |
|
| 201 |
if (!$result) {
|
| 202 |
// Ok, the last item with this name either doesn't exist,
|
| 203 |
// so we're barking up the wrong
|
| 204 |
// tree: a branch had taken place, and the source file
|
| 205 |
// is in a different branch. (Note we don't have to worry
|
| 206 |
// about merges because they show up in commit actions.)
|
| 207 |
// We need to go to the top of the current branch and
|
| 208 |
// see what source it used.
|
| 209 |
$previous_branch = db_fetch_array(db_query(
|
| 210 |
"SELECT a.branch_id, a.rev FROM {versioncontrol_hg_commits} a
|
| 211 |
WHERE a.rev = (
|
| 212 |
SELECT parent1 FROM {versioncontrol_hg_commits}
|
| 213 |
WHERE branch_id = %d
|
| 214 |
ORDER BY rev ASC
|
| 215 |
LIMIT 1
|
| 216 |
)
|
| 217 |
ORDER BY a.rev DESC
|
| 218 |
LIMIT 1
|
| 219 |
",
|
| 220 |
$branch_id
|
| 221 |
));
|
| 222 |
|
| 223 |
if (!$previous_branch) {
|
| 224 |
// Database inconsistency!
|
| 225 |
$source_path = null;
|
| 226 |
$last_vc_op_id = null;
|
| 227 |
break;
|
| 228 |
}
|
| 229 |
|
| 230 |
$branch_id = $previous_branch['branch_id'];
|
| 231 |
$rev = $previous_branch['rev'];
|
| 232 |
|
| 233 |
continue;
|
| 234 |
}
|
| 235 |
elseif ($result['type'] == VERSIONCONTROL_ITEM_FILE_DELETED &&
|
| 236 |
$action['current item']['type'] != VERSIONCONTROL_ITEM_FILE_DELETED) {
|
| 237 |
// At some point earlier in the branch, this file was
|
| 238 |
// deleted, and now has been "rised from the dead" via
|
| 239 |
// a merge. Don't attempt to search any further.
|
| 240 |
// Note that if our action is a delete, this indicates
|
| 241 |
// a delete was merged in, and the parent IS valid (so
|
| 242 |
// we don't handle it here.)
|
| 243 |
$source_path = null;
|
| 244 |
$last_vc_op_id = null;
|
| 245 |
break;
|
| 246 |
}
|
| 247 |
|
| 248 |
// Note that this variable indicates, for merges, the
|
| 249 |
// operation of the other delete that is being merged in
|
| 250 |
$last_vc_op_id = $result['vc_op_id'];
|
| 251 |
|
| 252 |
// We currently have no way of telling if a file was copied.
|
| 253 |
$source_path = $path;
|
| 254 |
|
| 255 |
break;
|
| 256 |
|
| 257 |
} while (1);
|
| 258 |
|
| 259 |
$source[$k]['path'] = $source_path;
|
| 260 |
$source[$k]['vc_op_id'] = $last_vc_op_id;
|
| 261 |
|
| 262 |
}
|
| 263 |
|
| 264 |
$commit_action_id = db_next_id('{versioncontrol_hg_commit_actions}_commit_action_id');
|
| 265 |
|
| 266 |
// Use this to support nulls. Call it a nasty cludge... I think it's art. :-)
|
| 267 |
// Commented placeholders are used to maintain parameter order.
|
| 268 |
$s1p = ($source[1]['path'] === null) ? 'NULL /*%s*/' : "'%s'";
|
| 269 |
$s2p = ($source[2]['path'] === null) ? 'NULL /*%s*/' : "'%s'";
|
| 270 |
$s1v = ($source[1]['vc_op_id'] === null) ? 'NULL /*%d*/' : '%d';
|
| 271 |
$s2v = ($source[2]['vc_op_id'] === null) ? 'NULL /*%d*/' : '%d';
|
| 272 |
|
| 273 |
db_query(
|
| 274 |
"INSERT INTO {versioncontrol_hg_commit_actions}
|
| 275 |
(commit_action_id, vc_op_id, type, path, action,
|
| 276 |
source1_path, source2_path, source1_vc_op_id, source2_vc_op_id)
|
| 277 |
VALUES (%d, %d, %d, '%s', %d,
|
| 278 |
$s1p, $s2p, $s1v, $s2v)",
|
| 279 |
// Primary key:
|
| 280 |
$commit_action_id,
|
| 281 |
// Foreign key:
|
| 282 |
$commit['vc_op_id'],
|
| 283 |
$action['current item']['type'],
|
| 284 |
$path,
|
| 285 |
$action['action'],
|
| 286 |
// [Newline]
|
| 287 |
$source[1]['path'],
|
| 288 |
$source[2]['path'],
|
| 289 |
$source[1]['vc_op_id'],
|
| 290 |
$source[2]['vc_op_id']
|
| 291 |
);
|
| 292 |
}
|
| 293 |
break;
|
| 294 |
|
| 295 |
case 'delete':
|
| 296 |
$result = db_query('SELECT commit_action_id
|
| 297 |
FROM {versioncontrol_hg_commit_actions}
|
| 298 |
WHERE vc_op_id = %d', $commit['vc_op_id']);
|
| 299 |
|
| 300 |
// We have not implemented tags yet, but this is fairly sensible
|
| 301 |
// "unhooking" of tags from the deleted revisions.
|
| 302 |
|
| 303 |
//while ($revision = db_fetch_object($result)) {
|
| 304 |
// db_query('DELETE FROM {versioncontrol_hg_item_tags}
|
| 305 |
// WHERE item_revision_id = %d',
|
| 306 |
// $revision->item_revision_id);
|
| 307 |
//}
|
| 308 |
db_query('DELETE FROM {versioncontrol_hg_commit_actions}
|
| 309 |
WHERE vc_op_id = %d', $commit['vc_op_id']);
|
| 310 |
break;
|
| 311 |
}
|
| 312 |
}
|
| 313 |
|
| 314 |
/**
|
| 315 |
* Implementation of [versioncontrol_backend]_get_commit_actions().
|
| 316 |
*/
|
| 317 |
function versioncontrol_hg_get_commit_actions($commit) {
|
| 318 |
$commit_actions = array();
|
| 319 |
|
| 320 |
$result = db_query('SELECT commit_action_id, action, type, path,
|
| 321 |
source1_path, source2_path, source1_vc_op_id, source2_vc_op_id
|
| 322 |
FROM {versioncontrol_hg_commit_actions}
|
| 323 |
WHERE vc_op_id = %d', $commit['vc_op_id']);
|
| 324 |
|
| 325 |
while ($raw_commit_action = db_fetch_array($result)) {
|
| 326 |
$commit_action = array(
|
| 327 |
'action' => $raw_commit_action['action'],
|
| 328 |
'modified' => FALSE,
|
| 329 |
'hg_specific' => array(
|
| 330 |
// Some housekeeping values:
|
| 331 |
// Not sure who would need this, but we'll keep it anyway:
|
| 332 |
'commit_action_id' => $raw_commit_action['commit_action_id'],
|
| 333 |
// This is for versioncontrol_hg_get_current_item_branch():
|
| 334 |
'selected_branch_id' => $commit['hg_specific']['branch_id'],
|
| 335 |
// Don't know who will need this yet:
|
| 336 |
// 'selected_op' => $commit,
|
| 337 |
),
|
| 338 |
);
|
| 339 |
|
| 340 |
if ($raw_commit_action['action'] !== VERSIONCONTROL_ACTION_DELETED) {
|
| 341 |
$commit_action['current item'] = array(
|
| 342 |
'type' => $raw_commit_action['type'],
|
| 343 |
'path' => $raw_commit_action['path'],
|
| 344 |
'revision' => $commit['revision'],
|
| 345 |
);
|
| 346 |
}
|
| 347 |
|
| 348 |
// Most implementations check action, but since there are only
|
| 349 |
// two to check, we won't bother:
|
| 350 |
$commit_action['source items'] = array();
|
| 351 |
foreach (array(1, 2) as $k) {
|
| 352 |
if ($raw_commit_action["source{$k}_vc_op_id"] === null) {
|
| 353 |
continue;
|
| 354 |
}
|
| 355 |
// Pull the changeset node for the operation
|
| 356 |
// We could use versioncontrol_get_commits(), but that's total
|
| 357 |
// overkill for our needs. We could also have done a JOIN.
|
| 358 |
$revision = db_result(db_query(
|
| 359 |
"SELECT revision FROM {versioncontrol_commits}
|
| 360 |
WHERE vc_op_id = %d",
|
| 361 |
$raw_commit_action["source{$k}_vc_op_id"])
|
| 362 |
);
|
| 363 |
if (!$revision) {
|
| 364 |
// Database error, abort!
|
| 365 |
continue;
|
| 366 |
}
|
| 367 |
$commit_action['source items'][] = array(
|
| 368 |
'type' => $raw_commit_action['type'],
|
| 369 |
'path' => $raw_commit_action["source{$k}_path"],
|
| 370 |
'revision' => $revision,
|
| 371 |
);
|
| 372 |
}
|
| 373 |
|
| 374 |
$commit_actions[$raw_commit_action['path']] = $commit_action;
|
| 375 |
}
|
| 376 |
|
| 377 |
return $commit_actions;
|
| 378 |
}
|
| 379 |
|
| 380 |
/**
|
| 381 |
* Implementation of [versioncontrol_backend]_get_short_revision_identifier():
|
| 382 |
* Return a shortened version of the revision identifier, as plaintext.
|
| 383 |
* This is used by versioncontrol_format_commit_identifier()
|
| 384 |
* and versioncontrol_format_item_revision().
|
| 385 |
*/
|
| 386 |
function versioncontrol_hg_format_short_revision_identifier($revision) {
|
| 387 |
// Let's return only the first 12 characters of the commit identifier,
|
| 388 |
// like Mercurial (including hgweb) does by default. This might result
|
| 389 |
// in collisions.
|
| 390 |
return substr($revision, 0, 12);
|
| 391 |
}
|
| 392 |
|
| 393 |
/**
|
| 394 |
* Implementation of [versioncontrol_backend]_get_current_item_branch().
|
| 395 |
*/
|
| 396 |
function versioncontrol_hg_get_current_item_branch($repository, $item) {
|
| 397 |
if (!isset($item['hg_specific']['selected_branch_id'])) {
|
| 398 |
return NULL;
|
| 399 |
}
|
| 400 |
|
| 401 |
$branch = versioncontrol_get_branch($item['hg_specific']['selected_branch_id']);
|
| 402 |
if (!isset($branch)) {
|
| 403 |
return NULL;
|
| 404 |
}
|
| 405 |
return $branch['branch_name'];
|
| 406 |
}
|
| 407 |
|
| 408 |
/**
|
| 409 |
* Retrieve the set of items that were affected by a branch operation.
|
| 410 |
*
|
| 411 |
* @warning
|
| 412 |
* This function returns an empty array because all branches are
|
| 413 |
* repository-wide, so all files are affected.
|
| 414 |
*/
|
| 415 |
function versioncontrol_hg_get_branched_items($branch) {
|
| 416 |
return array();
|
| 417 |
}
|
| 418 |
|
| 419 |
/**
|
| 420 |
* Retrieve the set of items that were affected by a tag operation.
|
| 421 |
*
|
| 422 |
* @warning
|
| 423 |
* This function returns an empty array because all tags are
|
| 424 |
* repository-wide, so all files are affected.
|
| 425 |
*/
|
| 426 |
function versioncontrol_hg_get_tagged_items($tag) {
|
| 427 |
return array();
|
| 428 |
}
|
| 429 |
|
| 430 |
/**
|
| 431 |
* Implementation of [vcs_backend]_get_parent_item().
|
| 432 |
*/
|
| 433 |
function versioncontrol_hg_get_parent_item($repository, $item, $parent_path = NULL) {
|
| 434 |
if (!isset($parent_path)) {
|
| 435 |
$item['path'] = dirname($item['path']);
|
| 436 |
return $item;
|
| 437 |
}
|
| 438 |
else if (strpos($item['path'] .'/', $parent_path .'/') !== FALSE) {
|
| 439 |
$item['path'] = $parent_path;
|
| 440 |
return $item;
|
| 441 |
}
|
| 442 |
return NULL;
|
| 443 |
}
|
| 444 |
|
| 445 |
/**
|
| 446 |
* Implementation of [versioncontrol_backend]_get_current_item_tag():
|
| 447 |
*
|
| 448 |
* @warning
|
| 449 |
* Because there are no tagged items, this function should not be called.
|
| 450 |
*/
|
| 451 |
function versioncontrol_hg_get_current_item_tag() {
|
| 452 |
return NULL;
|
| 453 |
}
|
| 454 |
|