options.c revision 289166
1/*
2 * options.c :  entry point for OPTIONS RA functions for ra_serf
3 *
4 * ====================================================================
5 *    Licensed to the Apache Software Foundation (ASF) under one
6 *    or more contributor license agreements.  See the NOTICE file
7 *    distributed with this work for additional information
8 *    regarding copyright ownership.  The ASF licenses this file
9 *    to you under the Apache License, Version 2.0 (the
10 *    "License"); you may not use this file except in compliance
11 *    with the License.  You may obtain a copy of the License at
12 *
13 *      http://www.apache.org/licenses/LICENSE-2.0
14 *
15 *    Unless required by applicable law or agreed to in writing,
16 *    software distributed under the License is distributed on an
17 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 *    KIND, either express or implied.  See the License for the
19 *    specific language governing permissions and limitations
20 *    under the License.
21 * ====================================================================
22 */
23
24
25
26#include <apr_uri.h>
27
28#include <serf.h>
29
30#include "svn_dirent_uri.h"
31#include "svn_hash.h"
32#include "svn_pools.h"
33#include "svn_ra.h"
34#include "svn_dav.h"
35#include "svn_xml.h"
36#include "svn_ctype.h"
37
38#include "../libsvn_ra/ra_loader.h"
39#include "svn_private_config.h"
40#include "private/svn_fspath.h"
41
42#include "ra_serf.h"
43
44
45/* In a debug build, setting this environment variable to "yes" will force
46   the client to speak v1, even if the server is capable of speaking v2. */
47#define SVN_IGNORE_V2_ENV_VAR "SVN_I_LIKE_LATENCY_SO_IGNORE_HTTPV2"
48
49
50/*
51 * This enum represents the current state of our XML parsing for an OPTIONS.
52 */
53enum options_state_e {
54  INITIAL = 0,
55  OPTIONS,
56  ACTIVITY_COLLECTION,
57  HREF
58};
59
60typedef struct options_context_t {
61  /* pool to allocate memory from */
62  apr_pool_t *pool;
63
64  /* Have we extracted options values from the headers already?  */
65  svn_boolean_t headers_processed;
66
67  svn_ra_serf__session_t *session;
68  svn_ra_serf__connection_t *conn;
69  svn_ra_serf__handler_t *handler;
70
71  svn_ra_serf__response_handler_t inner_handler;
72  void *inner_baton;
73
74  const char *activity_collection;
75  svn_revnum_t youngest_rev;
76
77} options_context_t;
78
79#define D_ "DAV:"
80#define S_ SVN_XML_NAMESPACE
81static const svn_ra_serf__xml_transition_t options_ttable[] = {
82  { INITIAL, D_, "options-response", OPTIONS,
83    FALSE, { NULL }, FALSE },
84
85  { OPTIONS, D_, "activity-collection-set", ACTIVITY_COLLECTION,
86    FALSE, { NULL }, FALSE },
87
88  { ACTIVITY_COLLECTION, D_, "href", HREF,
89    TRUE, { NULL }, TRUE },
90
91  { 0 }
92};
93
94
95/* Conforms to svn_ra_serf__xml_closed_t  */
96static svn_error_t *
97options_closed(svn_ra_serf__xml_estate_t *xes,
98               void *baton,
99               int leaving_state,
100               const svn_string_t *cdata,
101               apr_hash_t *attrs,
102               apr_pool_t *scratch_pool)
103{
104  options_context_t *opt_ctx = baton;
105
106  SVN_ERR_ASSERT(leaving_state == HREF);
107  SVN_ERR_ASSERT(cdata != NULL);
108
109  opt_ctx->activity_collection = svn_urlpath__canonicalize(cdata->data,
110                                                           opt_ctx->pool);
111
112  return SVN_NO_ERROR;
113}
114
115
116static svn_error_t *
117create_options_body(serf_bucket_t **body_bkt,
118                    void *baton,
119                    serf_bucket_alloc_t *alloc,
120                    apr_pool_t *pool)
121{
122  serf_bucket_t *body;
123  body = serf_bucket_aggregate_create(alloc);
124  svn_ra_serf__add_xml_header_buckets(body, alloc);
125  svn_ra_serf__add_open_tag_buckets(body, alloc, "D:options",
126                                    "xmlns:D", "DAV:",
127                                    NULL);
128  svn_ra_serf__add_tag_buckets(body, "D:activity-collection-set", NULL, alloc);
129  svn_ra_serf__add_close_tag_buckets(body, alloc, "D:options");
130
131  *body_bkt = body;
132  return SVN_NO_ERROR;
133}
134
135
136/* We use these static pointers so we can employ pointer comparison
137 * of our capabilities hash members instead of strcmp()ing all over
138 * the place.
139 */
140/* Both server and repository support the capability. */
141static const char *const capability_yes = "yes";
142/* Either server or repository does not support the capability. */
143static const char *const capability_no = "no";
144/* Server supports the capability, but don't yet know if repository does. */
145static const char *const capability_server_yes = "server-yes";
146
147
148/* This implements serf_bucket_headers_do_callback_fn_t.
149 */
150static int
151capabilities_headers_iterator_callback(void *baton,
152                                       const char *key,
153                                       const char *val)
154{
155  options_context_t *opt_ctx = baton;
156  svn_ra_serf__session_t *session = opt_ctx->session;
157
158  if (svn_cstring_casecmp(key, "dav") == 0)
159    {
160      /* Each header may contain multiple values, separated by commas, e.g.:
161           DAV: version-control,checkout,working-resource
162           DAV: merge,baseline,activity,version-controlled-collection
163           DAV: http://subversion.tigris.org/xmlns/dav/svn/depth */
164      apr_array_header_t *vals = svn_cstring_split(val, ",", TRUE,
165                                                   opt_ctx->pool);
166
167      /* Right now we only have a few capabilities to detect, so just
168         seek for them directly.  This could be written slightly more
169         efficiently, but that wouldn't be worth it until we have many
170         more capabilities. */
171
172      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_DEPTH, vals))
173        {
174          svn_hash_sets(session->capabilities,
175                        SVN_RA_CAPABILITY_DEPTH, capability_yes);
176        }
177      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_MERGEINFO, vals))
178        {
179          /* The server doesn't know what repository we're referring
180             to, so it can't just say capability_yes. */
181          if (!svn_hash_gets(session->capabilities,
182                             SVN_RA_CAPABILITY_MERGEINFO))
183            {
184              svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO,
185                            capability_server_yes);
186            }
187        }
188      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_LOG_REVPROPS, vals))
189        {
190          svn_hash_sets(session->capabilities,
191                        SVN_RA_CAPABILITY_LOG_REVPROPS, capability_yes);
192        }
193      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_ATOMIC_REVPROPS, vals))
194        {
195          svn_hash_sets(session->capabilities,
196                        SVN_RA_CAPABILITY_ATOMIC_REVPROPS, capability_yes);
197        }
198      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_PARTIAL_REPLAY, vals))
199        {
200          svn_hash_sets(session->capabilities,
201                        SVN_RA_CAPABILITY_PARTIAL_REPLAY, capability_yes);
202        }
203      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_INHERITED_PROPS, vals))
204        {
205          svn_hash_sets(session->capabilities,
206                        SVN_RA_CAPABILITY_INHERITED_PROPS, capability_yes);
207        }
208      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_REVERSE_FILE_REVS,
209                                 vals))
210        {
211          svn_hash_sets(session->capabilities,
212                        SVN_RA_CAPABILITY_GET_FILE_REVS_REVERSE,
213                        capability_yes);
214        }
215      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_EPHEMERAL_TXNPROPS, vals))
216        {
217          svn_hash_sets(session->capabilities,
218                        SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS, capability_yes);
219        }
220      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_INLINE_PROPS, vals))
221        {
222          session->supports_inline_props = TRUE;
223        }
224      if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_REPLAY_REV_RESOURCE, vals))
225        {
226          session->supports_rev_rsrc_replay = TRUE;
227        }
228    }
229
230  /* SVN-specific headers -- if present, server supports HTTP protocol v2 */
231  else if (!svn_ctype_casecmp(key[0], 'S')
232           && !svn_ctype_casecmp(key[1], 'V')
233           && !svn_ctype_casecmp(key[2], 'N'))
234    {
235      /* If we've not yet seen any information about supported POST
236         requests, we'll initialize the list/hash with "create-txn"
237         (which we know is supported by virtue of the server speaking
238         HTTPv2 at all. */
239      if (! session->supported_posts)
240        {
241          session->supported_posts = apr_hash_make(session->pool);
242          apr_hash_set(session->supported_posts, "create-txn", 10, (void *)1);
243        }
244
245      if (svn_cstring_casecmp(key, SVN_DAV_ROOT_URI_HEADER) == 0)
246        {
247          session->repos_root = session->session_url;
248          session->repos_root.path =
249            (char *)svn_fspath__canonicalize(val, session->pool);
250          session->repos_root_str =
251            svn_urlpath__canonicalize(
252                apr_uri_unparse(session->pool, &session->repos_root, 0),
253                session->pool);
254        }
255      else if (svn_cstring_casecmp(key, SVN_DAV_ME_RESOURCE_HEADER) == 0)
256        {
257#ifdef SVN_DEBUG
258          char *ignore_v2_env_var = getenv(SVN_IGNORE_V2_ENV_VAR);
259
260          if (!(ignore_v2_env_var
261                && apr_strnatcasecmp(ignore_v2_env_var, "yes") == 0))
262            session->me_resource = apr_pstrdup(session->pool, val);
263#else
264          session->me_resource = apr_pstrdup(session->pool, val);
265#endif
266        }
267      else if (svn_cstring_casecmp(key, SVN_DAV_REV_STUB_HEADER) == 0)
268        {
269          session->rev_stub = apr_pstrdup(session->pool, val);
270        }
271      else if (svn_cstring_casecmp(key, SVN_DAV_REV_ROOT_STUB_HEADER) == 0)
272        {
273          session->rev_root_stub = apr_pstrdup(session->pool, val);
274        }
275      else if (svn_cstring_casecmp(key, SVN_DAV_TXN_STUB_HEADER) == 0)
276        {
277          session->txn_stub = apr_pstrdup(session->pool, val);
278        }
279      else if (svn_cstring_casecmp(key, SVN_DAV_TXN_ROOT_STUB_HEADER) == 0)
280        {
281          session->txn_root_stub = apr_pstrdup(session->pool, val);
282        }
283      else if (svn_cstring_casecmp(key, SVN_DAV_VTXN_STUB_HEADER) == 0)
284        {
285          session->vtxn_stub = apr_pstrdup(session->pool, val);
286        }
287      else if (svn_cstring_casecmp(key, SVN_DAV_VTXN_ROOT_STUB_HEADER) == 0)
288        {
289          session->vtxn_root_stub = apr_pstrdup(session->pool, val);
290        }
291      else if (svn_cstring_casecmp(key, SVN_DAV_REPOS_UUID_HEADER) == 0)
292        {
293          session->uuid = apr_pstrdup(session->pool, val);
294        }
295      else if (svn_cstring_casecmp(key, SVN_DAV_YOUNGEST_REV_HEADER) == 0)
296        {
297          opt_ctx->youngest_rev = SVN_STR_TO_REV(val);
298        }
299      else if (svn_cstring_casecmp(key, SVN_DAV_ALLOW_BULK_UPDATES) == 0)
300        {
301          session->server_allows_bulk = apr_pstrdup(session->pool, val);
302        }
303      else if (svn_cstring_casecmp(key, SVN_DAV_SUPPORTED_POSTS_HEADER) == 0)
304        {
305          /* May contain multiple values, separated by commas. */
306          int i;
307          apr_array_header_t *vals = svn_cstring_split(val, ",", TRUE,
308                                                       session->pool);
309
310          for (i = 0; i < vals->nelts; i++)
311            {
312              const char *post_val = APR_ARRAY_IDX(vals, i, const char *);
313
314              svn_hash_sets(session->supported_posts, post_val, (void *)1);
315            }
316        }
317      else if (svn_cstring_casecmp(key, SVN_DAV_REPOSITORY_MERGEINFO) == 0)
318        {
319          if (svn_cstring_casecmp(val, "yes") == 0)
320            {
321              svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO,
322                            capability_yes);
323            }
324          else if (svn_cstring_casecmp(val, "no") == 0)
325            {
326              svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO,
327                            capability_no);
328            }
329        }
330    }
331
332  return 0;
333}
334
335
336/* A custom serf_response_handler_t which is mostly a wrapper around
337   the expat-based response handler -- it just notices OPTIONS response
338   headers first, before handing off to the xml parser.
339   Implements svn_ra_serf__response_handler_t */
340static svn_error_t *
341options_response_handler(serf_request_t *request,
342                         serf_bucket_t *response,
343                         void *baton,
344                         apr_pool_t *pool)
345{
346  options_context_t *opt_ctx = baton;
347
348  if (!opt_ctx->headers_processed)
349    {
350      svn_ra_serf__session_t *session = opt_ctx->session;
351      serf_bucket_t *hdrs = serf_bucket_response_get_headers(response);
352
353      /* Start out assuming all capabilities are unsupported. */
354      svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_PARTIAL_REPLAY,
355                    capability_no);
356      svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_DEPTH,
357                    capability_no);
358      svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO,
359                    NULL);
360      svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_LOG_REVPROPS,
361                    capability_no);
362      svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_ATOMIC_REVPROPS,
363                    capability_no);
364      svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_INHERITED_PROPS,
365                    capability_no);
366      svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS,
367                    capability_no);
368      svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_GET_FILE_REVS_REVERSE,
369                    capability_no);
370
371      /* Then see which ones we can discover. */
372      serf_bucket_headers_do(hdrs, capabilities_headers_iterator_callback,
373                             opt_ctx);
374
375      /* Assume mergeinfo capability unsupported, if didn't recieve information
376         about server or repository mergeinfo capability. */
377      if (!svn_hash_gets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO))
378        svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO,
379                      capability_no);
380
381      opt_ctx->headers_processed = TRUE;
382    }
383
384  /* Execute the 'real' response handler to XML-parse the response body. */
385  return opt_ctx->inner_handler(request, response, opt_ctx->inner_baton, pool);
386}
387
388
389static svn_error_t *
390create_options_req(options_context_t **opt_ctx,
391                   svn_ra_serf__session_t *session,
392                   svn_ra_serf__connection_t *conn,
393                   apr_pool_t *pool)
394{
395  options_context_t *new_ctx;
396  svn_ra_serf__xml_context_t *xmlctx;
397  svn_ra_serf__handler_t *handler;
398
399  new_ctx = apr_pcalloc(pool, sizeof(*new_ctx));
400  new_ctx->pool = pool;
401  new_ctx->session = session;
402  new_ctx->conn = conn;
403
404  new_ctx->youngest_rev = SVN_INVALID_REVNUM;
405
406  xmlctx = svn_ra_serf__xml_context_create(options_ttable,
407                                           NULL, options_closed, NULL,
408                                           new_ctx,
409                                           pool);
410  handler = svn_ra_serf__create_expat_handler(xmlctx, pool);
411
412  handler->method = "OPTIONS";
413  handler->path = session->session_url.path;
414  handler->body_delegate = create_options_body;
415  handler->body_type = "text/xml";
416  handler->conn = conn;
417  handler->session = session;
418
419  new_ctx->handler = handler;
420
421  new_ctx->inner_handler = handler->response_handler;
422  new_ctx->inner_baton = handler->response_baton;
423  handler->response_handler = options_response_handler;
424  handler->response_baton = new_ctx;
425
426  *opt_ctx = new_ctx;
427
428  return SVN_NO_ERROR;
429}
430
431
432svn_error_t *
433svn_ra_serf__v2_get_youngest_revnum(svn_revnum_t *youngest,
434                                    svn_ra_serf__connection_t *conn,
435                                    apr_pool_t *scratch_pool)
436{
437  svn_ra_serf__session_t *session = conn->session;
438  options_context_t *opt_ctx;
439
440  SVN_ERR_ASSERT(SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session));
441
442  SVN_ERR(create_options_req(&opt_ctx, session, conn, scratch_pool));
443  SVN_ERR(svn_ra_serf__context_run_one(opt_ctx->handler, scratch_pool));
444  SVN_ERR(svn_ra_serf__error_on_status(opt_ctx->handler->sline,
445                                       opt_ctx->handler->path,
446                                       opt_ctx->handler->location));
447
448  *youngest = opt_ctx->youngest_rev;
449  SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(*youngest));
450
451  return SVN_NO_ERROR;
452}
453
454
455svn_error_t *
456svn_ra_serf__v1_get_activity_collection(const char **activity_url,
457                                        svn_ra_serf__connection_t *conn,
458                                        apr_pool_t *result_pool,
459                                        apr_pool_t *scratch_pool)
460{
461  svn_ra_serf__session_t *session = conn->session;
462  options_context_t *opt_ctx;
463
464  SVN_ERR_ASSERT(!SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session));
465
466  SVN_ERR(create_options_req(&opt_ctx, session, conn, scratch_pool));
467  SVN_ERR(svn_ra_serf__context_run_one(opt_ctx->handler, scratch_pool));
468
469  SVN_ERR(svn_ra_serf__error_on_status(opt_ctx->handler->sline,
470                                       opt_ctx->handler->path,
471                                       opt_ctx->handler->location));
472
473  *activity_url = apr_pstrdup(result_pool, opt_ctx->activity_collection);
474
475  return SVN_NO_ERROR;
476
477}
478
479
480
481/** Capabilities exchange. */
482
483svn_error_t *
484svn_ra_serf__exchange_capabilities(svn_ra_serf__session_t *serf_sess,
485                                   const char **corrected_url,
486                                   apr_pool_t *pool)
487{
488  options_context_t *opt_ctx;
489  svn_error_t *err;
490
491  /* This routine automatically fills in serf_sess->capabilities */
492  SVN_ERR(create_options_req(&opt_ctx, serf_sess, serf_sess->conns[0], pool));
493
494  err = svn_ra_serf__context_run_one(opt_ctx->handler, pool);
495
496  /* If our caller cares about server redirections, and our response
497     carries such a thing, report as much.  We'll disregard ERR --
498     it's most likely just a complaint about the response body not
499     successfully parsing as XML or somesuch. */
500  if (corrected_url && (opt_ctx->handler->sline.code == 301))
501    {
502      svn_error_clear(err);
503      *corrected_url = opt_ctx->handler->location;
504      return SVN_NO_ERROR;
505    }
506
507  SVN_ERR(svn_error_compose_create(
508              svn_ra_serf__error_on_status(opt_ctx->handler->sline,
509                                           serf_sess->session_url.path,
510                                           opt_ctx->handler->location),
511              err));
512
513  /* Opportunistically cache any reported activity URL.  (We don't
514     want to have to ask for this again later, potentially against an
515     unreadable commit anchor URL.)  */
516  if (opt_ctx->activity_collection)
517    {
518      serf_sess->activity_collection_url =
519        apr_pstrdup(serf_sess->pool, opt_ctx->activity_collection);
520    }
521
522  return SVN_NO_ERROR;
523}
524
525
526static svn_error_t *
527create_simple_options_body(serf_bucket_t **body_bkt,
528                           void *baton,
529                           serf_bucket_alloc_t *alloc,
530                           apr_pool_t *pool)
531{
532  serf_bucket_t *body;
533  serf_bucket_t *s;
534
535  body = serf_bucket_aggregate_create(alloc);
536  svn_ra_serf__add_xml_header_buckets(body, alloc);
537
538  s = SERF_BUCKET_SIMPLE_STRING("<D:options xmlns:D=\"DAV:\" />", alloc);
539  serf_bucket_aggregate_append(body, s);
540
541  *body_bkt = body;
542  return SVN_NO_ERROR;
543}
544
545
546svn_error_t *
547svn_ra_serf__probe_proxy(svn_ra_serf__session_t *serf_sess,
548                         apr_pool_t *scratch_pool)
549{
550  svn_ra_serf__handler_t *handler;
551
552  handler = apr_pcalloc(scratch_pool, sizeof(*handler));
553  handler->handler_pool = scratch_pool;
554  handler->method = "OPTIONS";
555  handler->path = serf_sess->session_url.path;
556  handler->conn = serf_sess->conns[0];
557  handler->session = serf_sess;
558
559  /* We don't care about the response body, so discard it.  */
560  handler->response_handler = svn_ra_serf__handle_discard_body;
561
562  /* We need a simple body, in order to send it in chunked format.  */
563  handler->body_delegate = create_simple_options_body;
564
565  /* No special headers.  */
566
567  SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool));
568  /* Some versions of nginx in reverse proxy mode will return 411. They want
569     a Content-Length header, rather than chunked requests. We can keep other
570     HTTP/1.1 features, but will disable the chunking.  */
571  if (handler->sline.code == 411)
572    {
573      serf_sess->using_chunked_requests = FALSE;
574
575      return SVN_NO_ERROR;
576    }
577  SVN_ERR(svn_ra_serf__error_on_status(handler->sline,
578                                       handler->path,
579                                       handler->location));
580
581  return SVN_NO_ERROR;
582}
583
584
585svn_error_t *
586svn_ra_serf__has_capability(svn_ra_session_t *ra_session,
587                            svn_boolean_t *has,
588                            const char *capability,
589                            apr_pool_t *pool)
590{
591  svn_ra_serf__session_t *serf_sess = ra_session->priv;
592  const char *cap_result;
593
594  /* This capability doesn't rely on anything server side. */
595  if (strcmp(capability, SVN_RA_CAPABILITY_COMMIT_REVPROPS) == 0)
596    {
597      *has = TRUE;
598      return SVN_NO_ERROR;
599    }
600
601  cap_result = svn_hash_gets(serf_sess->capabilities, capability);
602
603  /* If any capability is unknown, they're all unknown, so ask. */
604  if (cap_result == NULL)
605    SVN_ERR(svn_ra_serf__exchange_capabilities(serf_sess, NULL, pool));
606
607  /* Try again, now that we've fetched the capabilities. */
608  cap_result = svn_hash_gets(serf_sess->capabilities, capability);
609
610  /* Some capabilities depend on the repository as well as the server. */
611  if (cap_result == capability_server_yes)
612    {
613      if (strcmp(capability, SVN_RA_CAPABILITY_MERGEINFO) == 0)
614        {
615          /* Handle mergeinfo specially.  Mergeinfo depends on the
616             repository as well as the server, but the server routine
617             that answered our svn_ra_serf__exchange_capabilities() call above
618             didn't even know which repository we were interested in
619             -- it just told us whether the server supports mergeinfo.
620             If the answer was 'no', there's no point checking the
621             particular repository; but if it was 'yes', we still must
622             change it to 'no' iff the repository itself doesn't
623             support mergeinfo. */
624          svn_mergeinfo_catalog_t ignored;
625          svn_error_t *err;
626          apr_array_header_t *paths = apr_array_make(pool, 1,
627                                                     sizeof(char *));
628          APR_ARRAY_PUSH(paths, const char *) = "";
629
630          err = svn_ra_serf__get_mergeinfo(ra_session, &ignored, paths, 0,
631                                           FALSE, FALSE, pool);
632
633          if (err)
634            {
635              if (err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE)
636                {
637                  svn_error_clear(err);
638                  cap_result = capability_no;
639                }
640              else if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
641                {
642                  /* Mergeinfo requests use relative paths, and
643                     anyway we're in r0, so this is a likely error,
644                     but it means the repository supports mergeinfo! */
645                  svn_error_clear(err);
646                  cap_result = capability_yes;
647                }
648              else
649                return err;
650            }
651          else
652            cap_result = capability_yes;
653
654          svn_hash_sets(serf_sess->capabilities,
655                        SVN_RA_CAPABILITY_MERGEINFO,  cap_result);
656        }
657      else
658        {
659          return svn_error_createf
660            (SVN_ERR_UNKNOWN_CAPABILITY, NULL,
661             _("Don't know how to handle '%s' for capability '%s'"),
662             capability_server_yes, capability);
663        }
664    }
665
666  if (cap_result == capability_yes)
667    {
668      *has = TRUE;
669    }
670  else if (cap_result == capability_no)
671    {
672      *has = FALSE;
673    }
674  else if (cap_result == NULL)
675    {
676      return svn_error_createf
677        (SVN_ERR_UNKNOWN_CAPABILITY, NULL,
678         _("Don't know anything about capability '%s'"), capability);
679    }
680  else  /* "can't happen" */
681    {
682      /* Well, let's hope it's a string. */
683      return svn_error_createf
684        (SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL,
685         _("Attempt to fetch capability '%s' resulted in '%s'"),
686         capability, cap_result);
687    }
688
689  return SVN_NO_ERROR;
690}
691