5 * A few generic functions for interfacing with Subversion via command line.
6 * The API of these functions has been inspired by the SVN PECL extension
7 * (http://www.php.net/manual/en/ref.svn.php), but works differently in places.
8 * On the one hand, this is due to the incompleteness of the functions in here,
9 * and on the other hand there are a few artificial restrictions and
10 * complications in the PECL extension's API that we can really do without.
13 * These functions can be run *without* Drupal.
15 * Copyright 2008 by Jakob Petsovits ("jpetso", http://drupal.org/user/56020)
16 * Copyright 2006-2008 by Gavin Mogan ("halkeye", http://drupal.org/user/56779)
19 // file_directory_temp() is normally provided by Drupal, but as this file is
20 // supposed to be independent from Drupal code, here's a fallback definition.
21 if (!function_exists('file_directory_temp')) {
23 * Determine the default temporary directory.
25 * @return A string containing a temp directory.
27 function file_directory_temp() {
28 return sys_get_temp_dir();
33 * If subsequent function calls from this file act on a private repository
34 * that requires authentication, this function will store username and password
35 * for the duration of the current process (in a static variable, that is).
36 * Other functions in this file make use of this login information.
38 function svnlib_set_authentication_info($username, $password) {
39 _svnlib_authentication_info(
40 array('username' => $username, 'password' => $password)
45 * Unset any username and password that was previously passed
46 * to subversion_set_authentication_info(), so that subsequent repository
47 * access will happen anonymously again.
49 function svnlib_unset_authentication_info() {
50 _svnlib_authentication_info(FALSE);
54 * Append the option for our custom config dir to a $cmd array,
55 * and also username and password if those have been set before.
57 function _svnlib_add_common_options(&$cmd) {
58 $auth_info = _svnlib_authentication_info();
59 if (isset($auth_info)) {
60 $cmd = array_merge($cmd, array(
61 '--username', escapeshellarg($auth_info['username']),
62 '--password', escapeshellarg($auth_info['password']),
65 $cmd[] = '--config-dir '. escapeshellarg(dirname(__FILE__) .'/configdir');
69 * Write or retrieve the authentication info state, stored in a static variable.
72 * NULL to retrieve the info, FALSE to unset it, or an array with array keys
73 * 'username' and 'password' to remember it for later retrieval.
75 function _svnlib_authentication_info($info = NULL) {
76 static $auth_info = NULL;
82 $auth_info = ($info === FALSE) ? NULL : $info;
88 * By default, Subversion will be invoked with the 'svn' binary which is
89 * alright as long as the binary is in the PATH. If it's not, you can call
90 * this function to set a different path to the binary (which will be used
91 * until this process finishes, or until a new path is set).
93 function svnlib_set_svn_binary($svn_binary) {
94 _svnlib_svn_binary($svn_binary);
98 * Write or retrieve the path of the svn binary, stored in a static variable.
101 * NULL to retrieve the info, or the path to the binary to remember it
102 * for later retrieval.
104 function _svnlib_svn_binary($svn_binary = NULL) {
105 static $binary = 'svn';
107 if (!isset($svn_binary)) {
110 $binary = $svn_binary;
115 * Retrieve the version of the svn binary, and return an array with the keys
116 * 'major', 'minor' and 'patch', each containing the integer for the respective
117 * part of the version number. If invoking the SVN executable fails, an empty
120 function svnlib_version() {
123 if (isset($version)) {
127 exec(_svnlib_svn_binary() .' --version', $output, $return_code);
129 if ($return_code != 0) {
133 $line = reset($output); // The first line contains the version number.
135 if (!preg_match('/\b([\d]+)\.([\d]+)(?:\.([\d]+))?/', $line, $matches)) {
140 'major' => (int) $matches[1],
141 'minor' => (int) $matches[2],
142 'patch' => empty($matches[3]) ? 0 : (int) $matches[3],
148 * Append an appropriate output pipe to a $cmd array, which causes STDERR
149 * to be written to a random file.
152 * An array with the temporary files that will be created when $cmd
153 * is executed. In its current form, the return array only contains
154 * the filename for STDERR output as 'stderr' array element.
156 function _svnlib_add_output_pipes(&$cmd) {
157 $tempdir = file_directory_temp();
159 'stderr' => $tempdir .'/drupal_versioncontrol_svn.stderr.'. mt_rand() .'.txt',
161 $cmd[] = '2> '. $tempfiles['stderr'];
166 * Delete temporary files that have been created by a command which included
167 * output pipes from _svnlib_add_output_pipes().
169 function _svnlib_delete_temporary_files($tempfiles) {
170 @unlink($tempfiles['stderr']);
174 * Read the STDERR output for a command that was executed.
175 * The output must have been written to a temporary file which was given
176 * by _svnlib_add_output_pipes(). The temporary file is deleted after it
177 * has been read. After calling the function, the error message can be
178 * retrieved by calling svnlib_last_error_message() or discarded by calling
179 * svnlib_unset_error_message().
181 function _svnlib_set_error_message($tempfiles) {
182 _svnlib_error_message(file_get_contents($tempfiles['stderr']));
183 @unlink($tempfiles['stderr']);
187 * Retrieve the STDERR output from the last invocation of 'svn' that exited
188 * with a non-zero status code. After fetching the error message, it will be
189 * unset again until a subsequent 'svn' invocation fails as well. If no message
190 * is set, this function returns NULL.
192 * For better security, it is advisable to run the returned error message
193 * through check_plain() or similar string checker functions.
195 function svnlib_last_error_message() {
196 $message = _svnlib_error_message();
197 _svnlib_error_message(FALSE);
202 * Write or retrieve an error message, stored in a static variable.
205 * NULL to retrieve the message, FALSE to unset it, or a string containing
206 * the new message to remember it for later retrieval.
208 function _svnlib_error_message($message = NULL) {
209 static $error_message = NULL;
211 if (!isset($message)) {
212 return $error_message;
215 $error_message = ($message === FALSE) ? NULL : $message;
216 return $error_message;
221 * Return commit log messages of a repository URL. This function is equivalent
222 * to 'svn log -v -r $revision_range $repository_url'.
224 * @param $repository_url
225 * The URL of the repository (e.g. 'file:///svnroot/my-repo') or an item
226 * inside that repository (e.g. 'file:///svnroot/my-repo/subdir/hello.php').
227 * @param $revision_range
228 * The revision specification that will be passed to 'svn log' as the
229 * '-r' parameter. Examples: '35' for a specific revision, 'HEAD:35' for all
230 * revisions since (and including) r35, or the default parameter 'HEAD:1'
231 * for all revisions of the given URL. If you specify the more recent
232 * revision first (e.g. 'HEAD:1') then it will also be first in the
233 * result array, whereas if you specify the older revision first ('1:HEAD')
234 * then you'll get a result array with an ascending sort, the most
235 * recent revision being the last array element.
236 * @param $url_revision
237 * The revision of the URL that should be listed.
238 * This needs to be a single revision, e.g. '35' or 'HEAD'.
239 * For example, if a file was deleted in revision 36, you need to pass '35'
240 * as parameter to get its log, otherwise Subversion won't find the file.
243 * An array of detailed information about the revisions that exist
244 * in the given URL at the specified revision or revision range.
245 * Each revision detail array has the revision number as array key.
246 * If the 'svn log' invocation exited with an error, this function
247 * returns NULL and the error message can be retrieved by calling
248 * svnlib_last_error_message().
250 function svnlib_log($repository_url, $revision_range = 'HEAD:1', $url_revision = 'HEAD') {
252 escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
254 '-r', $revision_range,
259 _svnlib_add_common_options($cmd);
260 $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
261 $tempfiles = _svnlib_add_output_pipes($cmd);
264 exec(implode(' ', $cmd), $output, $return_code);
265 if ($return_code != 0) {
266 _svnlib_set_error_message($tempfiles);
267 return NULL; // no such revision(s) found
269 $log = implode("\n", $output);
270 _svnlib_delete_temporary_files($tempfiles);
272 return _svnlib_parse_log($log);
276 * Parse the output of 'svn log' into an array of log entries.
277 * The output looks something like this (0 to N possible "logentry" elements):
278 <?xml version="1.0"?>
280 <logentry revision="272">
281 <author>jpetso</author>
282 <date>2007-04-12T15:01:00.247137Z</date>
284 <path action="M">/trunk/lila/kde/scalable/apps/ktorrent.svg</path>
285 <path action="A">/trunk/lila/kde/scalable/devices/laptop.svg</path>
286 <path copyfrom-path="/trunk/lila/kde/scalable/devices/pda_black.svg"
288 action="A">/trunk/lila/kde/scalable/devices/pda_blue.svg</path>
289 <path action="R">/trunk/lila/kde/scalable/devices/ipod_unmount.svg</path>
290 <path action="D">/trunk/lila/kde/ChangeLog</path>
292 <msg>New laptop icon from the GNOME set, more moderate
293 colors in ktorrent.svg, and bits of devices stuff.
298 function _svnlib_parse_log($log) {
299 $revisions = array();
300 $xml = new SimpleXMLElement($log);
302 foreach ($xml->logentry as $logentry) {
304 $revision['rev'] = intval((string) $logentry['revision']);
305 $revision['author'] = (string) $logentry->author;
306 $revision['msg'] = rtrim((string) $logentry->msg); // no trailing linebreaks
307 $revision['time_t'] = strtotime((string) $logentry->date);
310 foreach ($logentry->paths->path as $logpath) {
312 'path' => (string) $logpath,
313 'action' => (string) $logpath['action'],
315 if (!empty($logpath['copyfrom-path'])) {
316 $path['copyfrom'] = array(
317 'path' => (string) $logpath['copyfrom-path'],
318 'rev' => (string) $logpath['copyfrom-rev'],
321 $paths[$path['path']] = $path;
323 $revision['paths'] = $paths;
324 $revisions[$revision['rev']] = $revision;
330 * Return the contents of a directory (specified as repository URL,
331 * optionally at a certain revision) as an array of items. This function
332 * is equivalent to 'svn ls $repository_url@$revision'.
334 * @param $repository_url
335 * The URL of the repository (e.g. 'file:///svnroot/my-repo') or an item
336 * inside that repository (e.g. 'file:///svnroot/my-repo/subdir').
337 * @param $url_revision
338 * The revision of the URL that should be listed.
339 * This needs to be a single revision, e.g. '35' or 'HEAD'.
340 * For example, if a file was deleted in revision 36, you need to pass '35'
341 * as parameter to get its listing, otherwise Subversion won't find the file.
343 * FALSE to retrieve just the direct child items of the current directory,
344 * or TRUE to descend into each subdirectory and retrieve all descendant
345 * items recursively. If $recursive is true then each directory item
346 * in the result array will have an additional array element 'children'
347 * which contains the list entries below this directory, as array keys
348 * in the result array.
350 * If @p $repository_url refers to a file then the @p $recursive parameter
351 * has no effect on the 'svn ls' output and, by consequence, on the
355 * A array of items. If @p $repository_url refers to a file then the array
356 * contains a single entry with this file, whereas if @p $repository_url
357 * refers to a directory then the array contains all items inside this
358 * directory (but not the directory itself).
359 * If the 'svn ls' invocation exited with an error, this function
360 * returns NULL and the error message can be retrieved by calling
361 * svnlib_last_error_message().
363 function svnlib_ls($repository_url, $url_revision = 'HEAD', $recursive = FALSE) {
365 escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
373 _svnlib_add_common_options($cmd);
374 $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
375 $tempfiles = _svnlib_add_output_pipes($cmd);
378 exec(implode(' ', $cmd), $output, $return_code);
379 if ($return_code != 0) {
380 _svnlib_set_error_message($tempfiles);
381 return NULL; // no such item or revision found
383 $lists = implode("\n", $output);
384 _svnlib_delete_temporary_files($tempfiles);
386 return _svnlib_parse_ls($lists, $recursive);
390 * Parse the output of 'svn ls' into an array of item entries.
391 * The output looks something like this (0 to N possible "entry" elements):
392 <?xml version="1.0"?>
394 <list path="file:///home/jakob/repos/svn/lila-theme/tags/svg-utils-0-1/utils/svg-utils/svgcolor-xml">
397 <commit revision="257">
398 <author>jpetso</author>
399 <date>2006-11-29T01:27:47.192716Z</date>
403 <name>lila/lila-blue.xml</name>
405 <commit revision="9">
406 <author>dgt84</author>
407 <date>2004-05-04T21:32:13.000000Z</date>
413 function _svnlib_parse_ls($lists, $recursive) {
415 $current_item_stack = array(); // will help us determine hierarchical structures
416 $xml = new SimpleXMLElement($lists);
418 foreach ($xml->list->entry as $entry) {
420 $item['created_rev'] = intval((string) $entry->commit['revision']);
421 $item['last_author'] = (string) $entry->commit->author;
422 $item['time_t'] = strtotime((string) $entry->commit->date);
423 $relative_path = (string) $entry->name;
424 $item['name'] = basename($relative_path);
425 $item['type'] = (string) $entry['kind'];
427 if ($item['type'] == 'file') {
428 $item['size'] = intval((string) $entry->size);
431 // When listing recursively, we want to capture the item hierarchy.
433 if ($item['type'] == 'dir') {
434 $item['children'] = array();
436 if (strpos($relative_path, '/') !== FALSE) { // don't regard top-level items
437 $parent_path = dirname($relative_path);
438 if (isset($items[$parent_path]) && !in_array($relative_path, $items[$parent_path]['children'])) {
439 $items[$parent_path]['children'][] = $relative_path;
443 $items[$relative_path] = $item;
449 * Returns detail information about a directory or file item in the repository.
450 * In most cases, svnlib_info() is the better svnlib_ls(), as it retrieves not
451 * only item names but also repository root and the path of each item
452 * inside the repository.
454 * You can also use svnlib_info() to retrieve a former item path if the item
455 * has been moved or copied: just pass the current URL and revision together
456 * with a past or future revision number as @p $target_revision, and you get
457 * the path of the item at that time.
459 * This function is equivalent to
460 * 'svn info -r $target_revision $repository_url@$url_revision'.
462 * @param $repository_urls
463 * The URL of the item (e.g. 'file:///svnroot/my-repo/subdir/hello.php')
464 * or the repository itself (e.g. 'file:///svnroot/my-repo'), as string.
465 * Alternatively, you can also pass an array of multiple URLs.
466 * @param $url_revision
467 * The revision of the URL that should be listed.
468 * This needs to be a single revision, e.g. '35' or 'HEAD'.
469 * For example, if a file was deleted in revision 36, you need to pass '35'
470 * as parameter to get its info, otherwise Subversion won't find the file.
471 * In case multiple URLs are passed, this revision applies to each of them.
473 * Specifies if info for descendant items should be retrieved as well, and
474 * if so, which of those. The default 'empty' will not retrieve any children,
475 * 'files' will retrieve all immediate file children, 'immediates' will
476 * retrieve file and directory children, and 'infinity' will retrieve all
477 * descendant items there are, recursively. If $depth is 'infinity' then each
478 * directory item in the result array will have an additional array element
479 * named 'children' which contains the paths below this directory, the paths
480 * corresponding to array keys in the result array.
482 * If @p $repository_url refers to a file then the @p $depth parameter
483 * has no effect on the 'svn info' output and, by consequence, on the
486 * @param $target_revision
487 * The revision specification that will be passed to 'svn info' as the
488 * '-r' parameter. This needs to be a single revision, e.g. '35' or 'HEAD'.
489 * This is handy to track item copies and renames, see the general function
490 * description on how to do that. If you leave this at NULL, the info will be
491 * retrieved at the state of the $url_revision.
494 * A array of items that contain information about the items that correspond
495 * the specified URL(s). If @p $repository_url refers to a directory and
496 * @p $depth is 'infinity', the array also includes information about all
497 * descendants of the items that correspond to the specified URL(s).
498 * If the 'svn info' invocation exited with an error, this function
499 * returns NULL and the error message can be retrieved by calling
500 * svnlib_last_error_message().
502 function svnlib_info($repository_urls, $url_revision = 'HEAD', $depth = 'empty', $target_revision = NULL) {
503 if (!is_array($repository_urls)) { // it's a single URL as a string!
504 $repository_urls = array($repository_urls);
508 escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
514 if ($depth == 'infinity') {
515 $cmd[] = '-R'; // "--depth infinity" is not in 1.4, but '-R' (recursive) is
517 elseif ($depth != 'empty') {
518 $version = svnlib_version();
519 if ($version['major'] >= 1 && $version['minor'] >= 5) {
520 $cmd[] = '--depth '. $depth;
522 else { // 1.4 and earlier compatibility workaround
523 foreach ($repository_urls as $repository_url) {
524 // Make sure the item is a directory, otherwise it has no children
525 // anyways (and the relative path fetched by ls will lead to incorrect
526 // results as it duplicates the basename that is already in the URL).
527 $repository_url_items = svnlib_info($repository_url, $url_revision, 'empty', $target_revision);
528 $repository_url_item = reset($repository_url_items);
529 if ($repository_url_item['type'] != 'dir') {
532 // Fetch child items with svn ls, that's what 1.4 can actually do.
533 $items = svnlib_ls($repository_url, $url_revision);
534 foreach ($items as $relative_path => $item) {
535 if ($depth == 'files' && $item['type'] = 'dir') {
536 continue; // 'immediates' fetches all children, 'files' only files
538 $repository_urls[] = $repository_url .'/'. $relative_path;
544 // "--depth empty" is the default, leave it out for svn <= 1.4 compatibility
547 if (isset($target_revision)) {
549 $cmd[] = $target_revision;
551 _svnlib_add_common_options($cmd);
552 foreach ($repository_urls as $repository_url) {
553 $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
555 $tempfiles = _svnlib_add_output_pipes($cmd);
558 exec(implode(' ', $cmd), $output, $return_code);
559 if ($return_code != 0) {
560 _svnlib_set_error_message($tempfiles);
561 return NULL; // no such item or revision found
563 $info = implode("\n", $output);
564 _svnlib_delete_temporary_files($tempfiles);
566 $recursive = ($depth == 'infinity');
567 return _svnlib_parse_info($info, $recursive);
571 * Parse the output of 'svn info' into an array of item entries.
572 * The output looks something like this (same URL as in the 'svn ls' example,
573 * also 0 to N possible "entry" elements):
574 <?xml version="1.0"?>
576 <entry kind="dir" path="svgcolor-xml" revision="275">
577 <url>file:///home/jakob/repos/svn/lila-theme/tags/svg-utils-0-1/utils/svg-utils/svgcolor-xml</url>
579 <root>file:///home/jakob/repos/svn/lila-theme</root>
580 <uuid>fd53868f-e4f1-0310-84ca-8663aff3ef64</uuid>
582 <commit revision="257">
583 <author>jpetso</author>
584 <date>2006-11-29T01:27:47.192716Z</date>
587 <entry kind="dir" path="lila" revision="275">
588 <url>file:///home/jakob/repos/svn/lila-theme/tags/svg-utils-0-1/utils/svg-utils/svgcolor-xml/lila</url>
590 <root>file:///home/jakob/repos/svn/lila-theme</root>
591 <uuid>fd53868f-e4f1-0310-84ca-8663aff3ef64</uuid>
593 <commit revision="257">
594 <author>jpetso</author>
595 <date>2006-11-29T01:27:47.192716Z</date>
598 <entry kind="file" path="lila/lila-blue.xml" revision="275">
599 <url>file:///home/jakob/repos/svn/lila-theme/tags/svg-utils-0-1/utils/svg-utils/svgcolor-xml/lila/lila-blue.xml</url>
601 <root>file:///home/jakob/repos/svn/lila-theme</root>
602 <uuid>fd53868f-e4f1-0310-84ca-8663aff3ef64</uuid>
604 <commit revision="9">
605 <author>dgt84</author>
606 <date>2004-05-04T21:32:13.000000Z</date>
611 function _svnlib_parse_info($info, $recursive) {
613 $xml = new SimpleXMLElement($info);
615 foreach ($xml->entry as $entry) {
617 $item['url'] = (string) $entry->url;
618 $item['repository_root'] = (string) $entry->repository->root;
619 $item['repository_uuid'] = (string) $entry->repository->uuid;
621 if ($item['url'] == $item['repository_root']) {
625 $item['path'] = substr($item['url'], strlen($item['repository_root']));
628 if (isset($items[$item['path']])) {
629 // Duplicate item, we had this one before already. Nevertheless, we can
630 // perhaps make use of it in order to enhance the hierarchical structure.
631 $item = $items[$item['path']];
634 $item['type'] = (string) $entry['kind'];
635 $relative_path = (string) $entry['path'];
636 $item['rev'] = intval((string) $entry['revision']); // current state of the item
637 $item['created_rev'] = intval((string) $entry->commit['revision']); // last edit
638 $item['last_author'] = (string) $entry->commit->author;
639 $item['time_t'] = strtotime((string) $entry->commit->date);
641 if ($recursive && $item['type'] == 'dir') {
642 $item['children'] = array();
646 // For "--depth infinity", provide the caller with further hierarchy info.
647 if ($recursive && $item['path'] != '/') {
648 $parent_path = dirname($item['path']);
649 if (isset($items[$parent_path]) && !in_array($item['path'], $items[$parent_path]['children'])) {
650 $items[$parent_path]['children'][] = $item['path'];
653 $items[$item['path']] = $item;
660 * Copy the contents of a file in a repository to a given destination.
661 * This function is equivalent to
662 * 'svn cat $repository_url@$url_revision > $destination'.
664 * @param $destination
665 * The path of the file that should afterwards contain the file contents.
666 * @param $repository_url
667 * The URL of the file, e.g. 'file:///svnroot/my-repo/subdir/hello.php'.
668 * @param $url_revision
669 * The revision of the URL that should be queried for the property.
670 * This needs to be a single revision, e.g. '35' or 'HEAD'.
673 * TRUE if the file was created successfully. If the 'svn cat' invocation
674 * exited with an error, this function returns FALSE and the error message
675 * can be retrieved by calling svnlib_last_error_message().
677 function svnlib_cat($destination, $repository_url, $url_revision = 'HEAD') {
679 escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
683 _svnlib_add_common_options($cmd);
684 $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
685 $cmd[] = '> '. $destination;
686 $tempfiles = _svnlib_add_output_pipes($cmd);
689 exec(implode(' ', $cmd), $output, $return_code);
690 if ($return_code != 0) {
691 @unlink($destination);
692 _svnlib_set_error_message($tempfiles);
693 return FALSE; // no such item or revision found
695 _svnlib_delete_temporary_files($tempfiles);
700 * Return a specific SVN property of the given file or directory in the
701 * repository. This function is equivalent to
702 * 'svn propget $property_name $repository_url@$url_revision'.
704 * @param $property_name
705 * The name of the property, e.g. 'svn:mime-type' or 'svn:executable'.
706 * @param $repository_url
707 * The URL of the item (e.g. 'file:///svnroot/my-repo/subdir/hello.php')
708 * or the repository itself (e.g. 'file:///svnroot/my-repo'), as string.
709 * @param $url_revision
710 * The revision of the URL that should be queried for the property.
711 * This needs to be a single revision, e.g. '35' or 'HEAD'.
714 * A string containing the specified property for the item in the given
715 * revision, an empty string if this property is not set. If the
716 * 'svn propget' invocation exited with an error, this function
717 * returns NULL and the error message can be retrieved by calling
718 * svnlib_last_error_message().
720 function svnlib_propget($property_name, $repository_url, $url_revision = 'HEAD') {
722 escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
727 _svnlib_add_common_options($cmd);
728 $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
729 $tempfiles = _svnlib_add_output_pipes($cmd);
732 exec(implode(' ', $cmd), $output, $return_code);
733 if ($return_code != 0) {
734 _svnlib_set_error_message($tempfiles);
735 return NULL; // no such item or revision found
737 $property = trim(implode('', $output));
738 _svnlib_delete_temporary_files($tempfiles);
740 if (empty($property)) {