initial commit
[emacs-init.git] / nxhtml / tests / in / bug381979-svnlib.inc
1 <?php
2 // $Id$
3 /**
4  * @file
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.
11  *
12  * @note
13  *    These functions can be run *without* Drupal.
14  *
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)
17  */
18
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')) {
22   /**
23   * Determine the default temporary directory.
24   *
25   * @return A string containing a temp directory.
26   */
27   function file_directory_temp() {
28     return sys_get_temp_dir();
29   }
30 }
31
32 /**
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.
37  */
38 function svnlib_set_authentication_info($username, $password) {
39   _svnlib_authentication_info(
40     array('username' => $username, 'password' => $password)
41   );
42 }
43
44 /**
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.
48  */
49 function svnlib_unset_authentication_info() {
50   _svnlib_authentication_info(FALSE);
51 }
52
53 /**
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.
56  */
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']),
63     ));
64   }
65   $cmd[] = '--config-dir '. escapeshellarg(dirname(__FILE__) .'/configdir');
66 }
67
68 /**
69  * Write or retrieve the authentication info state, stored in a static variable.
70  *
71  * @param $info
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.
74  */
75 function _svnlib_authentication_info($info = NULL) {
76   static $auth_info = NULL;
77
78   if (!isset($info)) {
79     return $auth_info;
80   }
81   else {
82     $auth_info = ($info === FALSE) ? NULL : $info;
83     return $auth_info;
84   }
85 }
86
87 /**
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).
92  */
93 function svnlib_set_svn_binary($svn_binary) {
94   _svnlib_svn_binary($svn_binary);
95 }
96
97 /**
98  * Write or retrieve the path of the svn binary, stored in a static variable.
99  *
100  * @param $svn_binary
101  *   NULL to retrieve the info, or the path to the binary to remember it
102  *   for later retrieval.
103  */
104 function _svnlib_svn_binary($svn_binary = NULL) {
105   static $binary = 'svn';
106
107   if (!isset($svn_binary)) {
108     return $binary;
109   }
110   $binary = $svn_binary;
111   return $binary;
112 }
113
114 /**
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
118  * array is returned.
119  */
120 function svnlib_version() {
121   static $version;
122
123   if (isset($version)) {
124     return $version;
125   }
126   $return_code = 0;
127   exec(_svnlib_svn_binary() .' --version', $output, $return_code);
128
129   if ($return_code != 0) {
130     $version = array();
131     return $version;
132   }
133   $line = reset($output); // The first line contains the version number.
134
135   if (!preg_match('/\b([\d]+)\.([\d]+)(?:\.([\d]+))?/', $line, $matches)) {
136     $version = array();
137     return $version;
138   }
139   $version = array(
140     'major' => (int) $matches[1],
141     'minor' => (int) $matches[2],
142     'patch' => empty($matches[3]) ? 0 : (int) $matches[3],
143   );
144   return $version;
145 }
146
147 /**
148  * Append an appropriate output pipe to a $cmd array, which causes STDERR
149  * to be written to a random file.
150  *
151  * @return
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.
155  */
156 function _svnlib_add_output_pipes(&$cmd) {
157   $tempdir = file_directory_temp();
158   $tempfiles = array(
159     'stderr' => $tempdir .'/drupal_versioncontrol_svn.stderr.'. mt_rand() .'.txt',
160   );
161   $cmd[] = '2> '. $tempfiles['stderr'];
162   return $tempfiles;
163 }
164
165 /**
166  * Delete temporary files that have been created by a command which included
167  * output pipes from _svnlib_add_output_pipes().
168  */
169 function _svnlib_delete_temporary_files($tempfiles) {
170   @unlink($tempfiles['stderr']);
171 }
172
173 /**
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().
180  */
181 function _svnlib_set_error_message($tempfiles) {
182   _svnlib_error_message(file_get_contents($tempfiles['stderr']));
183   @unlink($tempfiles['stderr']);
184 }
185
186 /**
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.
191  *
192  * For better security, it is advisable to run the returned error message
193  * through check_plain() or similar string checker functions.
194  */
195 function svnlib_last_error_message() {
196   $message = _svnlib_error_message();
197   _svnlib_error_message(FALSE);
198   return $message;
199 }
200
201 /**
202  * Write or retrieve an error message, stored in a static variable.
203  *
204  * @param $info
205  *   NULL to retrieve the message, FALSE to unset it, or a string containing
206  *   the new message to remember it for later retrieval.
207  */
208 function _svnlib_error_message($message = NULL) {
209   static $error_message = NULL;
210
211   if (!isset($message)) {
212     return $error_message;
213   }
214   else {
215     $error_message = ($message === FALSE) ? NULL : $message;
216     return $error_message;
217   }
218 }
219
220 /**
221  * Return commit log messages of a repository URL. This function is equivalent
222  * to 'svn log -v -r $revision_range $repository_url'.
223  *
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.
241  *
242  * @return
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().
249  */
250 function svnlib_log($repository_url, $revision_range = 'HEAD:1', $url_revision = 'HEAD') {
251   $cmd = array(
252     escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
253     'log',
254     '-r', $revision_range,
255     '--non-interactive',
256     '--xml',
257     '-v',
258   );
259   _svnlib_add_common_options($cmd);
260   $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
261   $tempfiles = _svnlib_add_output_pipes($cmd);
262
263   $return_code = 0;
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
268   }
269   $log = implode("\n", $output);
270   _svnlib_delete_temporary_files($tempfiles);
271
272   return _svnlib_parse_log($log);
273 }
274
275 /*
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"?>
279 <log>
280   <logentry revision="272">
281     <author>jpetso</author>
282     <date>2007-04-12T15:01:00.247137Z</date>
283     <paths>
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"
287             copyfrom-rev="270"
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>
291     </paths>
292     <msg>New laptop icon from the GNOME set, more moderate
293     colors in ktorrent.svg, and bits of devices stuff.
294     </msg>
295   </logentry>
296 </log>
297 */
298 function _svnlib_parse_log($log) {
299   $revisions = array();
300   $xml = new SimpleXMLElement($log);
301
302   foreach ($xml->logentry as $logentry) {
303     $revision = array();
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);
308     $paths = array();
309
310     foreach ($logentry->paths->path as $logpath) {
311       $path = array(
312         'path' => (string) $logpath,
313         'action' => (string) $logpath['action'],
314       );
315       if (!empty($logpath['copyfrom-path'])) {
316         $path['copyfrom'] = array(
317           'path' => (string) $logpath['copyfrom-path'],
318           'rev' => (string) $logpath['copyfrom-rev'],
319         );
320       }
321       $paths[$path['path']] = $path;
322     }
323     $revision['paths'] = $paths;
324     $revisions[$revision['rev']] = $revision;
325   }
326   return $revisions;
327 }
328
329 /**
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'.
333  *
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.
342  * @param $recursive
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.
349  *
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
352  *   return value.
353  *
354  * @return
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().
362  */
363 function svnlib_ls($repository_url, $url_revision = 'HEAD', $recursive = FALSE) {
364   $cmd = array(
365     escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
366     'ls',
367     '--non-interactive',
368     '--xml',
369   );
370   if ($recursive) {
371     $cmd[] = '-R';
372   }
373   _svnlib_add_common_options($cmd);
374   $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
375   $tempfiles = _svnlib_add_output_pipes($cmd);
376
377   $return_code = 0;
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
382   }
383   $lists = implode("\n", $output);
384   _svnlib_delete_temporary_files($tempfiles);
385
386   return _svnlib_parse_ls($lists, $recursive);
387 }
388
389 /*
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"?>
393 <lists>
394   <list path="file:///home/jakob/repos/svn/lila-theme/tags/svg-utils-0-1/utils/svg-utils/svgcolor-xml">
395   <entry kind="dir">
396     <name>lila</name>
397     <commit revision="257">
398       <author>jpetso</author>
399       <date>2006-11-29T01:27:47.192716Z</date>
400     </commit>
401   </entry>
402   <entry kind="file">
403     <name>lila/lila-blue.xml</name>
404     <size>918</size>
405     <commit revision="9">
406       <author>dgt84</author>
407       <date>2004-05-04T21:32:13.000000Z</date>
408     </commit>
409   </entry>
410   </list>
411 </lists>
412 */
413 function _svnlib_parse_ls($lists, $recursive) {
414   $items = array();
415   $current_item_stack = array(); // will help us determine hierarchical structures
416   $xml = new SimpleXMLElement($lists);
417
418   foreach ($xml->list->entry as $entry) {
419     $item = array();
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'];
426
427     if ($item['type'] == 'file') {
428       $item['size'] = intval((string) $entry->size);
429     }
430
431     // When listing recursively, we want to capture the item hierarchy.
432     if ($recursive) {
433       if ($item['type'] == 'dir') {
434         $item['children'] = array();
435       }
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;
440         }
441       }
442     }
443     $items[$relative_path] = $item;
444   }
445   return $items;
446 }
447
448 /**
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.
453  *
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.
458  *
459  * This function is equivalent to
460  * 'svn info -r $target_revision $repository_url@$url_revision'.
461  *
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.
472  * @param $depth
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.
481  *
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
484  *   return value.
485  *
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.
492  *
493  * @return
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().
501  */
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);
505   }
506
507   $cmd = array(
508     escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
509     'info',
510     '--non-interactive',
511     '--xml',
512   );
513
514   if ($depth == 'infinity') {
515     $cmd[] = '-R'; // "--depth infinity" is not in 1.4, but '-R' (recursive) is
516   }
517   elseif ($depth != 'empty') {
518     $version = svnlib_version();
519     if ($version['major'] >= 1 && $version['minor'] >= 5) {
520       $cmd[] = '--depth '. $depth;
521     }
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') {
530           continue;
531         }
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
537           }
538           $repository_urls[] = $repository_url .'/'. $relative_path;
539         }
540       }
541     }
542   }
543   // else {
544   //   "--depth empty" is the default, leave it out for svn <= 1.4 compatibility
545   // }
546
547   if (isset($target_revision)) {
548     $cmd[] = '-r';
549     $cmd[] = $target_revision;
550   }
551   _svnlib_add_common_options($cmd);
552   foreach ($repository_urls as $repository_url) {
553     $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
554   }
555   $tempfiles = _svnlib_add_output_pipes($cmd);
556
557   $return_code = 0;
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
562   }
563   $info = implode("\n", $output);
564   _svnlib_delete_temporary_files($tempfiles);
565
566   $recursive = ($depth == 'infinity');
567   return _svnlib_parse_info($info, $recursive);
568 }
569
570 /*
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"?>
575 <info>
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>
578     <repository>
579       <root>file:///home/jakob/repos/svn/lila-theme</root>
580       <uuid>fd53868f-e4f1-0310-84ca-8663aff3ef64</uuid>
581     </repository>
582     <commit revision="257">
583       <author>jpetso</author>
584       <date>2006-11-29T01:27:47.192716Z</date>
585     </commit>
586   </entry>
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>
589     <repository>
590       <root>file:///home/jakob/repos/svn/lila-theme</root>
591       <uuid>fd53868f-e4f1-0310-84ca-8663aff3ef64</uuid>
592     </repository>
593     <commit revision="257">
594       <author>jpetso</author>
595       <date>2006-11-29T01:27:47.192716Z</date>
596     </commit>
597   </entry>
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>
600     <repository>
601       <root>file:///home/jakob/repos/svn/lila-theme</root>
602       <uuid>fd53868f-e4f1-0310-84ca-8663aff3ef64</uuid>
603     </repository>
604     <commit revision="9">
605       <author>dgt84</author>
606       <date>2004-05-04T21:32:13.000000Z</date>
607     </commit>
608   </entry>
609 </info>
610 */
611 function _svnlib_parse_info($info, $recursive) {
612   $items = array();
613   $xml = new SimpleXMLElement($info);
614
615   foreach ($xml->entry as $entry) {
616     $item = array();
617     $item['url'] = (string) $entry->url;
618     $item['repository_root'] = (string) $entry->repository->root;
619     $item['repository_uuid'] = (string) $entry->repository->uuid;
620
621     if ($item['url'] == $item['repository_root']) {
622       $item['path'] = '/';
623     }
624     else {
625       $item['path'] = substr($item['url'], strlen($item['repository_root']));
626     }
627
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']];
632     }
633     else {
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);
640
641       if ($recursive && $item['type'] == 'dir') {
642         $item['children'] = array();
643       }
644     }
645
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'];
651       }
652     }
653     $items[$item['path']] = $item;
654   }
655   return $items;
656 }
657
658
659 /**
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'.
663  *
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'.
671  *
672  * @return
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().
676  */
677 function svnlib_cat($destination, $repository_url, $url_revision = 'HEAD') {
678   $cmd = array(
679     escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
680     'cat',
681     '--non-interactive',
682   );
683   _svnlib_add_common_options($cmd);
684   $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
685   $cmd[] = '> '. $destination;
686   $tempfiles = _svnlib_add_output_pipes($cmd);
687
688   $return_code = 0;
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
694   }
695   _svnlib_delete_temporary_files($tempfiles);
696   return TRUE;
697 }
698
699 /**
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'.
703  *
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'.
712  *
713  * @return
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().
719  */
720 function svnlib_propget($property_name, $repository_url, $url_revision = 'HEAD') {
721   $cmd = array(
722     escapeshellarg(escapeshellcmd(_svnlib_svn_binary())),
723     'propget',
724     $property_name,
725     '--non-interactive',
726   );
727   _svnlib_add_common_options($cmd);
728   $cmd[] = escapeshellarg($repository_url .'@'. $url_revision);
729   $tempfiles = _svnlib_add_output_pipes($cmd);
730
731   $return_code = 0;
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
736   }
737   $property = trim(implode('', $output));
738   _svnlib_delete_temporary_files($tempfiles);
739
740   if (empty($property)) {
741     return '';
742   }
743   return $property;
744 }