/[drupal]/contributions/modules/versioncontrol_git/versioncontrol_git.log.inc
ViewVC logotype

Contents of /contributions/modules/versioncontrol_git/versioncontrol_git.log.inc

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


Revision 1.40 - (show annotations) (download) (as text)
Mon Oct 19 15:25:34 2009 UTC (5 weeks, 3 days ago) by marvil07
Branch: MAIN
CVS Tags: HEAD
Changes since 1.39: +6 -6 lines
File MIME type: text/x-php
update to the new commits at vc api

mainly operation loader and item with one abstract method
1 <?php
2 // $Id: versioncontrol_git.log.inc,v 1.39 2009/10/19 15:24:04 marvil07 Exp $
3 /**
4 * @file
5 * Git backend for Version Control API - Provides Git commit information and
6 * account management as a pluggable backend.
7 *
8 * Copyright 2008 by Jimmy Berry ("boombatower", http://drupal.org/user/214218)
9 * Copyright 2009 by Cornelius Riemenschneider ("CorniI", http://drupal.org/user/136353)
10 */
11
12 include_once(drupal_get_path('module', 'versioncontrol') .'/includes/VersioncontrolBranch.php');
13 include_once(drupal_get_path('module', 'versioncontrol') .'/includes/VersioncontrolOperation.php');
14 include_once(drupal_get_path('module', 'versioncontrol') .'/includes/VersioncontrolItem.php');
15
16 /**
17 * Actually update the repository by fetching commits and other stuff
18 * directly from the repository, invoking the git executable.
19 * @param $repository
20 * @return
21 * TRUE if the logs were updated, or FALSE if fetching and updating the logs
22 * failed for whatever reason.
23 */
24 function _versioncontrol_git_log_update_repository(&$repository) {
25 $root = escapeshellcmd($repository->root);
26 $chdir_ok = @chdir($root); // Set working directory to root.
27 if ($chdir_ok === FALSE) {
28 return FALSE;
29 }
30 if ($repository->data['versioncontrol_git']['locked'] == TRUE) {
31 drupal_set_message(t('This repository is locked, there is already a fetch in progress. If this is not the case, press the clear lock button.'), 'error');
32 return FALSE;
33 }
34 $repository->data['versioncontrol_git']['locked'] = 1;
35 $repository->update();
36
37 // Get the list of current branches from Git.
38 $branch_list = _versioncontrol_git_log_get_branches();
39 $branches = array();
40 foreach ($branch_list as $branch_name) {
41 $label = new VersioncontrolBranch($branch_name,
42 VERSIONCONTROL_ACTION_MODIFIED, NULL, $repository);
43 $label->ensure();
44 $branches[$branch_name] = $label;
45 }
46 //jpetso and me (corni) came to the conclusion that we will not delete branches.
47 // TODO: revisit this!
48 // Record new commits.
49 $constraints = array(
50 'vcs' => array('git'),
51 'repo_ids' => array($repository->repo_id),
52 'types' => array(VERSIONCONTROL_OPERATION_COMMIT),
53 'branches' => array() // used in the loop
54 );
55 $branches_per_commit = array();
56 $existing_revs = array();
57 // Get the existing revisions from the cache.
58 $cache_object = cache_get('versioncontrol_git_rev_cache');
59 // Check wether the cache object exists or not.
60 if (is_object($cache_object)) {
61 $existing_revs = $cache_object->data;
62 }
63 // Get the list of current branches from Git.
64 // Generate the range per branch with which git shall be called.
65 foreach ($branches as $branch_name => $label) {
66 if (is_object($cache_object)) {
67 // We get all commits we have in this branch to not process them later.
68 $constraints['branches'] = array($branch_name);
69 $latest_commit_date = 0;
70 $commit_op = VersioncontrolOperationCache::getInstance()->getOperations($constraints);
71 $latest_commit = FALSE;
72 foreach ($commit_op as $vc_op_id => $c_op) {
73 if ($latest_commit_date < $c_op['date']) {
74 $latest_commit = $c_op['revision'];
75 $latest_commit_date = $c_op['date'];
76 $existing_revs[$branch_name][$latest_commit] = TRUE;
77 }
78 }
79 }
80 // No way to free the damned mysql result!!
81 unset($commit_op);
82
83 $commits_in_branch = _versioncontrol_git_log_get_commits_in_branch($repository, escapeshellarg($branch_name));
84 foreach ($commits_in_branch as $commit) {
85 if (!isset($existing_revs[$branch_name][$commit])) {
86 if (!isset($branches_per_commit[$commit]) || !is_array($branches_per_commit[$commit])) {
87 $branches_per_commit[$commit] = array($label);
88 }
89 else {
90 $branches_per_commit[$commit][] = $label;
91 }
92 }
93 }
94 }
95 // This uses an extra loop on purpose!
96 // Process all commits on a per-branch base.
97 foreach ($branches_per_commit as $revision => $branch) {
98 // Update commits from Git.
99 _versioncontrol_git_process_commits($repository, $revision, $branches_per_commit, $existing_revs);
100 }
101 // Check tags.
102 $tags = _versioncontrol_git_log_get_tags(); //Now we have the current list of tags as array of strings.
103 $constraints = array(
104 'vcs' => array('git'),
105 'repo_ids' => array($repository->repo_id),
106 'types' => array(VERSIONCONTROL_OPERATION_TAG)
107 );
108 $existing_tag_ops = VersioncontrolOperationCache::getInstance()->getOperations($constraints);
109 $existing_tags = array();
110 foreach ($existing_tag_ops as $tag_op) {
111 if (!in_array($tag_op->labels[0]->name, $existing_tags)) {
112 $existing_tags[] = $tag_op->labels[0]->name;
113 }
114 }
115 // Deleting tags is *not* supported. Read the manual if you want to know why...
116 // Check for new tags.
117 $new_tags = array_diff($tags, $existing_tags);
118 if (!empty($new_tags)) {
119 _versioncontrol_git_process_tags($repository, $new_tags);
120 }
121
122 // Update repository updated field. Displayed on administration interface for documentation purposes.
123 $repository->data['versioncontrol_git']['updated'] = time();
124 $repository->data['versioncontrol_git']['locked'] = 0;
125 $repository->update();
126
127 // Write back the cache.
128 cache_set('versioncontrol_git_rev_cache', $existing_revs);
129 return TRUE;
130 }
131
132 /**
133 * Execute a Git command using the root context and the command to be executed.
134 * @param string $command Command to execute.
135 * @return mixed Logged output from the command in either array of file pointer form.
136 */
137 function _versioncontrol_git_log_exec($command) {
138 $logs = array();
139 exec($command, $logs);
140 array_unshift($logs, '');
141 reset($logs); // Reset the array pointer, so that we can use next().
142 return $logs;
143 }
144
145 /**
146 * Get branches from Git using 'branch -l' command.
147 * @return array List of branches.
148 */
149 function _versioncontrol_git_log_get_branches() {
150 $logs = _versioncontrol_git_log_exec('git show-ref --heads'); // Query branches.
151 $branches = _versioncontrol_git_log_parse_branches($logs); // Parse output.
152 return $branches;
153 }
154
155 /**
156 * Parse the branch list output from Git.
157 */
158 function _versioncontrol_git_log_parse_branches(&$logs) {
159 $branches = array();
160 while (($line = next($logs)) !== FALSE) {
161 $branches[] = substr(trim($line), 52);
162 }
163 return $branches;
164 }
165
166 /**
167 * Get tags from Git using 'tag -l' command.
168 */
169 function _versioncontrol_git_log_get_tags() {
170 //TODO: incorporate a --dereference and parse better, saves one git call for tags
171 $logs = _versioncontrol_git_log_exec('git show-ref --tags'); // Query tags.
172 $tags = _versioncontrol_git_log_parse_tags($logs); // Parse output.
173 return $tags;
174 }
175
176 /**
177 * Parse the tag list output from Git.
178 */
179 function _versioncontrol_git_log_parse_tags(&$logs) {
180 $tags = array();
181 while (($line = next($logs)) !== FALSE) {
182 $tags[] = $branches[] = substr(trim($line), 51);
183 }
184 return $tags;
185 }
186
187 /**
188 * Parses output of git show $tag_name provided by _versioncontrol_git_get_tag_operation() to retrieve an $operation for inserting a tag.
189 * @param $repository
190 * @param sring $tag_name The name of the parsed tag
191 * @param $logs The output of git show
192 * @return array An $operation array which contains the info for the tag.
193 */
194 function _versioncontrol_git_log_parse_tag_info($repository, &$logs, $tag_commits) {
195 $line = next($logs); // Get op type
196 if ($line === FALSE) {
197 return FALSE;
198 }
199 if ($line == 'commit') {
200 //let's get the author and the date from the tagged commit, better than nothing.
201 $tagged_commit = next($logs); // Get the tagged commit
202 $tag_name = substr(strrchr(next($logs), '/'), 1 ); // Get the name of the tag based on %(refname)
203 next($logs); // Skip these two lines
204 next($logs);
205 // Get the tag/commit message
206 $message = '';
207 $i = 0;
208 while (($line = next($logs)) !== FALSE) {
209 if ($line == 'ENDOFGITTAGOUTPUTMESAGEHERE') {
210 break;
211 }
212 if ($i == 1) {
213 $message .= "\n";
214 }
215 $message .= $line ."\n";
216 $i++;
217 }
218 $constraints = array(
219 'vcs' => array('git'),
220 'repo_ids' => array($repository->repo_id),
221 'types' => array(VERSIONCONTROL_OPERATION_COMMIT),
222 'revisions' => array($tagged_commit)
223 );
224 $op = VersioncontrolOperationCache::getInstance()->getOperations($constraints);
225 $op = array_pop($op);
226 //FIXME get author/commiter
227 $o_operation = new VersioncontrolGitOperation(
228 VERSIONCONTROL_OPERATION_TAG,
229 $op->author,
230 $op->date+1, // We want to be displayed *after* the tagged commit.
231 $tagged_commit,
232 $message,
233 $op->author,
234 $repository
235 );
236 $o_operation->labels = array(
237 0 => new VersioncontrolTag($tag_name, VERSIONCONTROL_ACTION_ADDED, null, $repository)
238 );
239 return $o_operation;
240 }
241 $line = next($logs); // Skip op sha1
242 $tag_name = substr(strrchr(next($logs), '/'), 1 ); // Get the name of the tag based on %(refname)
243 $tagger = next($logs); // Get tagger
244 $date = strtotime(next($logs)); // Get date
245 // Get the tag message
246 $message = '';
247 $i = 0;
248 while (($line = next($logs)) !== FALSE) {
249 if ($line == 'ENDOFGITTAGOUTPUTMESAGEHERE') {
250 break;
251 }
252 if ($i == 1) {
253 $message .= "\n";
254 }
255 $message .= $line ."\n";
256 $i++;
257 }
258 $tagged_commit = $tag_commits[$tag_name];
259 // By now, we're done with the parsing, construct the op array
260 $o_operation = new VersioncontrolGitOperation(
261 VERSIONCONTROL_OPERATION_TAG,
262 $tagger,
263 $date,
264 $tagged_commit,
265 $message,
266 $tagger,
267 $repository
268 );
269 $o_operation->labels = array(
270 0 => new VersioncontrolTag($tag_name, VERSIONCONTROL_ACTION_ADDED, null, $repository)
271 );
272 return $o_operation;
273 }
274
275 /**
276 * Invokes 'git-show tag' to get information about a tag.
277 * It's output is later parsed by _versioncontrol_git_log_parse_tag_info().
278 * @param $repository
279 * @param string $tag The name of the tag.
280 * @return An $operation array which contains the info for the tag.
281 */
282 function _versioncontrol_git_get_tag_operations($repository, $tags) {
283 $tag_ops = array();
284 $tag_string = '';
285 if (empty($tags)) {
286 return array();
287 }
288 foreach ($tags as $tag) {
289 $tag_string .= escapeshellarg("refs/tags/$tag") .' ';
290 }
291 $format = "%(objecttype)\n%(objectname)\n%(refname)\n%(taggername) %(taggeremail)\n%(taggerdate)\n%(contents)\nENDOFGITTAGOUTPUTMESAGEHERE";
292 $exec = "git for-each-ref --format=\"$format\" $tag_string";
293 $logs_tag_msg = _versioncontrol_git_log_exec($exec);
294 $exec = "git show-ref -d $tag_string";
295 $logs_tag_commits = _versioncontrol_git_log_exec($exec);
296 $tag_commits = array();
297 foreach ($logs_tag_commits as $line) {
298 if (substr($line, -3, 3) == '^{}') {
299 $commit = substr($line, 0, 40);
300 $tag = substr($line, 41);
301 $tag = substr(substr($line, 41, strlen($tag) -3), 10);
302 $tag_commits[$tag] = $commit;
303 }
304 }
305 do {
306 $ret = _versioncontrol_git_log_parse_tag_info($repository, $logs_tag_msg, $tag_commits);
307 if ($ret !== FALSE) {
308 $tag_ops[] = $ret;
309 }
310 }while ($ret !== FALSE);
311 return $tag_ops;
312 }
313
314 /**
315 * Does all the processing for all new tags.
316 * @param $repository
317 * @param array $new_tags An array of strings for all new tags which shall be processed
318 */
319 function _versioncontrol_git_process_tags($repository, $new_tags) {
320 $tag_ops = _versioncontrol_git_get_tag_operations($repository, $new_tags);
321 foreach ($tag_ops as $tag_op) {
322 $op_items = array();
323 $tag_op->insert($op_items);
324 $constraints = array(
325 'vcs' => array('git'),
326 'repo_ids' => array($repository->repo_id),
327 'types' => array(VERSIONCONTROL_OPERATION_COMMIT),
328 'revisions' => array($tag_op->revision)
329 );
330 $tag_commits = VersioncontrolOperationCache::getInstance()->getOperations($constraints);
331 foreach ($tag_commits as $vc_op_id => $tag_commit_op) {
332 $tag_commit_op->labels[] = new VersioncontrolTag(
333 $tag_op['labels'][0]['name'],
334 VERSIONCONTROL_ACTION_MODIFIED,
335 null,
336 $repository
337 );
338 $tag_commit_op->updateLabels($tag_commit_op->labels);
339 }
340 }
341 }
342
343 /**
344 * Get all commits from Git using 'git log' command.
345 * @param $repository
346 * @param string $range the computed range for the branch we check
347 * @param array $branches_per_commit An array of all commits we will encounter with a list of branches they are in.
348 */
349 function _versioncontrol_git_process_commits($repository, $revision, &$branches_per_commit, &$existing_revs) {
350 $rev_shell = escapeshellarg($revision);
351 $command = "git log $rev_shell --numstat --summary --pretty=format:\"%H%n%P%n%aN <%ae>%n%ct%n%s%n%b%nENDOFOUTPUTGITMESSAGEHERE\" -n 1 --";
352 $logs = _versioncontrol_git_log_exec($command);
353 _versioncontrol_git_log_parse_commits($repository, $logs, $branches_per_commit, $existing_revs); // Parse the info from the raw output.
354 }
355
356 /**
357 * This function returns all commits in the given range.
358 * It is used to get all new commits in a branch, which is specified by @p $range
359 * @param $repository
360 * @param string $range The range of the commits to retrieve
361 * @return array An array of strings with all commit id's in it
362 */
363 function _versioncontrol_git_log_get_commits_in_branch($repository, $range) {
364 $logs = _versioncontrol_git_log_exec("git rev-list $range --reverse --"); // Query tags.
365 $commits = array();
366 while (($line = next($logs)) !== FALSE) {
367 $commits[] = trim($line);
368 }
369 return $commits;
370 }
371
372 /**
373 * A helper function to get the source_items.
374 * @param $repository
375 * @param $revision The revision of the parent item.
376 * @param $filename The filename of the parent item.
377 * @return array An $item array ready to use for $operation['source_items']
378 */
379 function _versioncontrol_git_get_source_item_helper($repository, $revision, $filename, $branches) {
380 $branch_names = array();
381 foreach ($branches as $branch) {
382 $branch_names[] = $branch['name'];
383 }
384 $constraints = array(
385 'vcs' => array('git'),
386 'repo_ids' => array($repository->repo_id),
387 'types' => array(VERSIONCONTROL_OPERATION_COMMIT),
388 'paths' => array($filename),
389 'branches' => $branch_names
390 );
391 $commit_op = versioncontrol_get_operations($constraints);
392 ksort($commit_op);
393 $commit_op = array_pop($commit_op);
394 $op_items = versioncontrol_get_operation_items($commit_op);
395 $type = $op_items[$filename]['type'] ? $op_items[$filename]['type'] : VERSIONCONTROL_ITEM_FILE;
396 // ['action'] not needed for source items :)
397 return array(
398 'path' => $filename,
399 'type' => $type,
400 'revision' => $op_items[$filename]['revision'],
401 );
402 }
403
404 /**
405 * A function to a source_item for a specific file.
406 * @param $repository as we get it from the API
407 * @param string $filename the revision of the current item we shall get it's source from
408 * @return array $source_items array for use in an $operation.
409 */
410 function _versioncontrol_git_get_source_item($repository, $filename, $parents, $branches, $commit_rev) {
411 $ret = array();
412 if (count($parents) == 1) {
413 $filenameg = substr($filename, 1);
414 $filenameg = escapeshellarg($filenameg);
415 $commit_rev = escapeshellarg($commit_rev);
416 $exec = "git rev-list -n 1 ". $commit_rev ."^ -- $filenameg";
417 $logs = _versioncontrol_git_log_exec($exec); // Query tags.
418 $revision = next($logs);
419 $ret = array(
420 new VersioncontrolGitItem(
421 VERSIONCONTROL_ITEM_FILE,
422 $filename,
423 $revision,
424 null,
425 $repository
426 )
427 );
428 }
429 else {
430 foreach ($parents as $rev) {
431 $ret[] = _versioncontrol_git_get_source_item_helper($repository, $rev, $filename, $branches);
432 }
433 }
434 return $ret;
435 }
436
437 function _versioncontrol_git_insert_commit($repository, $date, $username, $message, $revision, $branches, $op_items) {
438 //FIXME: get author and commiter
439 $op = new VersioncontrolGitOperation(VERSIONCONTROL_OPERATION_COMMIT, $username, $date, $revision, $message, $username, $repository);
440 $op->labels = $branches;
441 $op->insert($op_items);
442 }
443
444 function _versioncontrol_git_parse_items($repository, &$logs, &$line, $revision, &$branches_per_commit, $parents, $merge) {
445 $op_items = array();
446 $read = FALSE;
447
448 // Read file line revisions.
449 do {
450 if (preg_match('/^(\S+)'."\t".'(\S+)'."\t".'(.+)$/', $line, $matches)) { // Begins with num lines added and matches expression.
451 $read = TRUE;
452 $path = '/'. $matches[3];
453 $op_items[$path] = new VersioncontrolGitItem(
454 VERSIONCONTROL_ITEM_FILE,
455 $path,
456 $revision,
457 //'source_items' => array(),//filled later
458 ($merge ? VERSIONCONTROL_ACTION_MERGED : VERSIONCONTROL_ACTION_MODIFIED),
459 $repository
460 );
461 if (is_numeric($matches[1]) && is_numeric($matches[2])) {
462 $op_items[$path]->line_changes = array(
463 'added' => $matches[1],
464 'removed' => $matches[2]
465 );
466 }
467 }
468 else {
469 break;
470 }
471 } while (($line = next($logs)) !== FALSE);
472 // Read file actions.
473 do {
474 if (preg_match('/^ (\S+) (\S+) (\S+) (.+)$/', $line, $matches)) { // Ensure that same file, they should be in same order.
475 $read = TRUE;
476 // We also can get 'mode' here if someone changes the file permissions.
477 if ($matches[1] == 'create') {
478 $op_items['/'. $matches[4]]->action = VERSIONCONTROL_ACTION_ADDED;
479 }
480 else if ($matches[1] == 'delete') {
481 $op_items['/'. $matches[4]]->action = VERSIONCONTROL_ACTION_DELETED;
482 }
483 }
484 else {
485 break;
486 }
487 }
488 while (($line = next($logs)) !== FALSE);
489
490 //This is an inconsistency in git log output...
491 if ($read) {
492 $line = next($logs);
493 }
494 foreach ($op_items as $path => $item) {
495 if ($item->action != VERSIONCONTROL_ACTION_ADDED) {
496 $op_items[$path]->source_items = _versioncontrol_git_get_source_item($repository, $path, $parents, $branches_per_commit[$revision], $revision);
497 }
498 }
499 return $op_items;
500 }
501
502 function _versioncontrol_git_check_already_parsed_commits($repository, $revision, &$branches_per_commit, &$line, &$logs) {
503 $constraints = array(
504 'types' => array(VERSIONCONTROL_OPERATION_COMMIT),
505 'revisions' => array($revision),
506 'vcs' => array('git'),
507 'repo_ids' => array($repository->repo_id)
508 );
509 $same_rev_commit = VersioncontrolOperationCache::getInstance()->getOperations($constraints);
510 $adjusted_commit = FALSE;
511 foreach ($same_rev_commit as $vc_op_id => $rev_commit) {
512 // We already have a commit with this revision recorded, so use a faster parser then.
513 $adjusted_commit = TRUE;
514 $labels = array();
515 foreach ( $rev_commit->labels as $label) {
516 if ($label['type'] == VERSIONCONTROL_OPERATION_TAG) {
517 $labels[] = $label;
518 }
519 }
520 $labels = array_merge($labels, $branches_per_commit[$revision]);
521 $rev_commit->updateLabels($labels);
522 $line = next($logs); // Get $parents
523 $line = next($logs); // Get Author
524 $line = next($logs); // Get Date as Timestamp
525 // Pretend message parsing
526 while (($line = next($logs)) !== FALSE) {
527 if (trim($line) == 'ENDOFOUTPUTGITMESSAGEHERE') {
528 break;
529 }
530 }
531 $line = next($logs);
532 // Skip everything --summary or --numstat related output
533 while (!(preg_match("/^([a-f0-9]{40})$/", trim($line))) && $line !== FALSE) {
534 $line = next($logs);
535 }
536 //$branches_per_commit[$revision] = TRUE;
537 }
538 return $adjusted_commit;
539 }
540
541 /**
542 * Parse the output of 'git log' and insert commits based on it's data.
543 *
544 * @param $repository
545 * The repository array, as given by the Version Control API.
546 * @param $logs The output of 'git log' to parse
547 * @param array $branches_per_commit An array which has all branches for all commits in it.
548 * It is used to construct $operation['labels'].
549 */
550 function _versioncontrol_git_log_parse_commits($repository, &$logs, &$branches_per_commit, &$existing_revs) {
551 // If the log was retrieved by taking the return value of exec(), we've
552 // got an array and navigate it via next(). If we stored the log in a
553 // temporary file, $logs is a file handle that we need to fgets() instead.
554 $root_path = $repository['root'];
555 $line = next($logs); // Get Revision
556 $merge = FALSE;
557 // $line already points to the revision
558 $revision = trim($line);
559 foreach ($branches_per_commit[$revision] as $label) {
560 $existing_revs[$label['name']][$revision] = TRUE;
561 }
562
563 $adjusted_commit = _versioncontrol_git_check_already_parsed_commits($repository,
564 $revision, $branches_per_commit, $line, $logs);
565 if ($adjusted_commit) {
566 return;
567 }
568
569 $line = next($logs); // Get $parents
570 $parents = explode(" ", trim($line));
571 if ($parents[0] == '') {
572 $parents = array();
573 }
574 if (isset($parents[1])) {
575 $merge = TRUE;
576 }
577 $line = next($logs); // Get Author
578 $username = trim($line);
579 $line = next($logs); // Get Date as Timestamp
580 $date = trim($line);
581 // Get revision message.
582 $message = '';
583 $i = 0;
584 while (($line = next($logs)) !== FALSE) {
585 $line = trim($line);
586 if ($line == 'ENDOFOUTPUTGITMESSAGEHERE') {
587 if (substr($message, -2) === "\n\n") {
588 $message = substr($message, 0, strlen($message) - 1);
589 }
590 break;
591 }
592 if ($i == 1) {
593 $message .= "\n";
594 }
595 $message .= $line ."\n";
596 $i++;
597 }
598 $line = next($logs); // Points to either the next entry or the first items modified or to the file actions
599 // Get the items
600 $op_items = _versioncontrol_git_parse_items($repository, $logs, $line, $revision,
601 $branches_per_commit, $parents, $merge);
602
603 _versioncontrol_git_insert_commit($repository, $date, $username,
604 $message, $revision, $branches_per_commit[$revision], $op_items);
605 }

  ViewVC Help
Powered by ViewVC 1.1.2