merge.c revision 362181
1/*
2 * merge.c :  MERGE response parsing 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_hash.h"
31#include "svn_pools.h"
32#include "svn_ra.h"
33#include "svn_dav.h"
34#include "svn_xml.h"
35#include "svn_config.h"
36#include "svn_dirent_uri.h"
37#include "svn_props.h"
38
39#include "private/svn_dav_protocol.h"
40#include "private/svn_fspath.h"
41#include "svn_private_config.h"
42
43#include "ra_serf.h"
44#include "../libsvn_ra/ra_loader.h"
45
46
47/*
48 * This enum represents the current state of our XML parsing for a MERGE.
49 */
50typedef enum merge_state_e {
51  INITIAL = XML_STATE_INITIAL,
52  MERGE_RESPONSE,
53  UPDATED_SET,
54  RESPONSE,
55  HREF,
56  PROPSTAT,
57  PROP,
58  RESOURCE_TYPE,
59  BASELINE,
60  COLLECTION,
61  SKIP_HREF,
62  CHECKED_IN,
63  VERSION_NAME,
64  DATE,
65  AUTHOR,
66  POST_COMMIT_ERR,
67
68  STATUS
69} merge_state_e;
70
71
72/* Structure associated with a MERGE request. */
73typedef struct merge_context_t
74{
75  apr_pool_t *pool;
76
77  svn_ra_serf__session_t *session;
78  svn_ra_serf__handler_t *handler;
79
80  apr_hash_t *lock_tokens;
81  svn_boolean_t keep_locks;
82  svn_boolean_t disable_merge_response;
83
84  const char *merge_resource_url; /* URL of resource to be merged. */
85  const char *merge_url; /* URL at which the MERGE request is aimed. */
86
87  svn_commit_info_t *commit_info;
88
89} merge_context_t;
90
91
92#define D_ "DAV:"
93#define S_ SVN_XML_NAMESPACE
94static const svn_ra_serf__xml_transition_t merge_ttable[] = {
95  { INITIAL, D_, "merge-response", MERGE_RESPONSE,
96    FALSE, { NULL }, FALSE },
97
98  { MERGE_RESPONSE, D_, "updated-set", UPDATED_SET,
99    FALSE, { NULL }, FALSE },
100
101  { UPDATED_SET, D_, "response", RESPONSE,
102    FALSE, { NULL }, TRUE },
103
104  { RESPONSE, D_, "href", HREF,
105    TRUE, { NULL }, TRUE },
106
107  { RESPONSE, D_, "propstat", PROPSTAT,
108    FALSE, { NULL }, FALSE },
109
110#if 0
111  /* Not needed.  */
112  { PROPSTAT, D_, "status", STATUS,
113    FALSE, { NULL }, FALSE },
114#endif
115
116  { PROPSTAT, D_, "prop", PROP,
117    FALSE, { NULL }, FALSE },
118
119  { PROP, D_, "resourcetype", RESOURCE_TYPE,
120    FALSE, { NULL }, FALSE },
121
122  { RESOURCE_TYPE, D_, "baseline", BASELINE,
123    FALSE, { NULL }, TRUE },
124
125  { RESOURCE_TYPE, D_, "collection", COLLECTION,
126    FALSE, { NULL }, TRUE },
127
128  { PROP, D_, "checked-in", SKIP_HREF,
129    FALSE, { NULL }, FALSE },
130
131  { SKIP_HREF, D_, "href", CHECKED_IN,
132    TRUE, { NULL }, TRUE },
133
134  { PROP, D_, SVN_DAV__VERSION_NAME, VERSION_NAME,
135    TRUE, { NULL }, TRUE },
136
137  { PROP, D_, SVN_DAV__CREATIONDATE, DATE,
138    TRUE, { NULL }, TRUE },
139
140  { PROP, D_, "creator-displayname", AUTHOR,
141    TRUE, { NULL }, TRUE },
142
143  { PROP, S_, "post-commit-err", POST_COMMIT_ERR,
144    TRUE, { NULL }, TRUE },
145
146  { 0 }
147};
148
149
150/* Conforms to svn_ra_serf__xml_closed_t  */
151static svn_error_t *
152merge_closed(svn_ra_serf__xml_estate_t *xes,
153             void *baton,
154             int leaving_state,
155             const svn_string_t *cdata,
156             apr_hash_t *attrs,
157             apr_pool_t *scratch_pool)
158{
159  merge_context_t *merge_ctx = baton;
160
161  if (leaving_state == RESPONSE)
162    {
163      const char *rtype;
164
165      rtype = svn_hash_gets(attrs, "resourcetype");
166
167      /* rtype can only be "baseline" or "collection" (or NULL). We can
168         keep this check simple.  */
169      if (rtype && *rtype == 'b')
170        {
171          const char *rev_str;
172
173          rev_str = svn_hash_gets(attrs, "revision");
174          if (rev_str)
175            {
176              apr_int64_t rev;
177
178              SVN_ERR(svn_cstring_atoi64(&rev, rev_str));
179              merge_ctx->commit_info->revision = (svn_revnum_t)rev;
180            }
181          else
182            merge_ctx->commit_info->revision = SVN_INVALID_REVNUM;
183
184          merge_ctx->commit_info->date =
185              apr_pstrdup(merge_ctx->pool,
186                          svn_hash_gets(attrs, "date"));
187
188          merge_ctx->commit_info->author =
189              apr_pstrdup(merge_ctx->pool,
190                          svn_hash_gets(attrs, "author"));
191
192          merge_ctx->commit_info->post_commit_err =
193             apr_pstrdup(merge_ctx->pool,
194                         svn_hash_gets(attrs, "post-commit-err"));
195        }
196      else
197        {
198          const char *href;
199
200          href = svn_urlpath__skip_ancestor(
201                   merge_ctx->merge_url,
202                   svn_hash_gets(attrs, "href"));
203
204          if (href == NULL)
205            return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL,
206                                     _("A MERGE response for '%s' is not "
207                                       "a child of the destination ('%s')"),
208                                     href, merge_ctx->merge_url);
209
210          /* We now need to dive all the way into the WC to update the
211             base VCC url.  */
212          if (!SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(merge_ctx->session)
213              && merge_ctx->session->wc_callbacks->push_wc_prop)
214            {
215              const char *checked_in;
216              svn_string_t checked_in_str;
217
218              checked_in = svn_hash_gets(attrs, "checked-in");
219              checked_in_str.data = checked_in;
220              checked_in_str.len = strlen(checked_in);
221
222              SVN_ERR(merge_ctx->session->wc_callbacks->push_wc_prop(
223                        merge_ctx->session->wc_callback_baton,
224                        href,
225                        SVN_RA_SERF__WC_CHECKED_IN_URL,
226                        &checked_in_str,
227                        scratch_pool));
228            }
229        }
230    }
231  else if (leaving_state == BASELINE)
232    {
233      svn_ra_serf__xml_note(xes, RESPONSE, "resourcetype", "baseline");
234    }
235  else if (leaving_state == COLLECTION)
236    {
237      svn_ra_serf__xml_note(xes, RESPONSE, "resourcetype", "collection");
238    }
239  else
240    {
241      const char *name;
242      const char *value = cdata->data;
243
244      if (leaving_state == HREF)
245        {
246          name = "href";
247          value = svn_urlpath__canonicalize(value, scratch_pool);
248        }
249      else if (leaving_state == CHECKED_IN)
250        {
251          name = "checked-in";
252          value = svn_urlpath__canonicalize(value, scratch_pool);
253        }
254      else if (leaving_state == VERSION_NAME)
255        name = "revision";
256      else if (leaving_state == DATE)
257        name = "date";
258      else if (leaving_state == AUTHOR)
259        name = "author";
260      else if (leaving_state == POST_COMMIT_ERR)
261        name = "post-commit-err";
262      else
263        SVN_ERR_MALFUNCTION();
264
265      svn_ra_serf__xml_note(xes, RESPONSE, name, value);
266    }
267
268  return SVN_NO_ERROR;
269}
270
271
272static svn_error_t *
273setup_merge_headers(serf_bucket_t *headers,
274                    void *baton,
275                    apr_pool_t *pool /* request pool */,
276                    apr_pool_t *scratch_pool)
277{
278  merge_context_t *ctx = baton;
279  apr_array_header_t *vals = apr_array_make(scratch_pool, 2,
280                                            sizeof(const char *));
281
282  if (!ctx->keep_locks)
283    APR_ARRAY_PUSH(vals, const char *) = SVN_DAV_OPTION_RELEASE_LOCKS;
284  if (ctx->disable_merge_response)
285    APR_ARRAY_PUSH(vals, const char *) = SVN_DAV_OPTION_NO_MERGE_RESPONSE;
286
287  if (vals->nelts > 0)
288    serf_bucket_headers_set(headers, SVN_DAV_OPTIONS_HEADER,
289                            svn_cstring_join2(vals, " ", FALSE, scratch_pool));
290
291  return SVN_NO_ERROR;
292}
293
294void
295svn_ra_serf__merge_lock_token_list(apr_hash_t *lock_tokens,
296                                   const char *parent,
297                                   serf_bucket_t *body,
298                                   serf_bucket_alloc_t *alloc,
299                                   apr_pool_t *pool)
300{
301  apr_hash_index_t *hi;
302
303  if (!lock_tokens || apr_hash_count(lock_tokens) == 0)
304    return;
305
306  svn_ra_serf__add_open_tag_buckets(body, alloc,
307                                    "S:lock-token-list",
308                                    "xmlns:S", SVN_XML_NAMESPACE,
309                                    SVN_VA_NULL);
310
311  for (hi = apr_hash_first(pool, lock_tokens);
312       hi;
313       hi = apr_hash_next(hi))
314    {
315      const void *key;
316      apr_ssize_t klen;
317      void *val;
318      svn_string_t path;
319
320      apr_hash_this(hi, &key, &klen, &val);
321
322      path.data = key;
323      path.len = klen;
324
325      if (parent && !svn_relpath_skip_ancestor(parent, key))
326        continue;
327
328      svn_ra_serf__add_open_tag_buckets(body, alloc, "S:lock", SVN_VA_NULL);
329
330      svn_ra_serf__add_open_tag_buckets(body, alloc, "lock-path", SVN_VA_NULL);
331      svn_ra_serf__add_cdata_len_buckets(body, alloc, path.data, path.len);
332      svn_ra_serf__add_close_tag_buckets(body, alloc, "lock-path");
333
334      svn_ra_serf__add_tag_buckets(body, "lock-token", val, alloc);
335
336      svn_ra_serf__add_close_tag_buckets(body, alloc, "S:lock");
337    }
338
339  svn_ra_serf__add_close_tag_buckets(body, alloc, "S:lock-token-list");
340}
341
342/* Implements svn_ra_serf__request_body_delegate_t */
343static svn_error_t*
344create_merge_body(serf_bucket_t **bkt,
345                  void *baton,
346                  serf_bucket_alloc_t *alloc,
347                  apr_pool_t *pool /* request pool */,
348                  apr_pool_t *scratch_pool)
349{
350  merge_context_t *ctx = baton;
351  serf_bucket_t *body_bkt;
352
353  body_bkt = serf_bucket_aggregate_create(alloc);
354
355  svn_ra_serf__add_xml_header_buckets(body_bkt, alloc);
356  svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:merge",
357                                    "xmlns:D", "DAV:",
358                                    SVN_VA_NULL);
359  svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:source", SVN_VA_NULL);
360  svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:href", SVN_VA_NULL);
361
362  svn_ra_serf__add_cdata_len_buckets(body_bkt, alloc,
363                                     ctx->merge_resource_url,
364                                     strlen(ctx->merge_resource_url));
365
366  svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:href");
367  svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:source");
368
369  svn_ra_serf__add_empty_tag_buckets(body_bkt, alloc,
370                                     "D:no-auto-merge", SVN_VA_NULL);
371  svn_ra_serf__add_empty_tag_buckets(body_bkt, alloc,
372                                     "D:no-checkout", SVN_VA_NULL);
373
374  svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", SVN_VA_NULL);
375  svn_ra_serf__add_empty_tag_buckets(body_bkt, alloc,
376                                     "D:checked-in", SVN_VA_NULL);
377  svn_ra_serf__add_empty_tag_buckets(body_bkt, alloc,
378                                     "D:" SVN_DAV__VERSION_NAME, SVN_VA_NULL);
379  svn_ra_serf__add_empty_tag_buckets(body_bkt, alloc,
380                                     "D:resourcetype", SVN_VA_NULL);
381  svn_ra_serf__add_empty_tag_buckets(body_bkt, alloc,
382                                     "D:" SVN_DAV__CREATIONDATE, SVN_VA_NULL);
383  svn_ra_serf__add_empty_tag_buckets(body_bkt, alloc,
384                                     "D:creator-displayname", SVN_VA_NULL);
385  svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop");
386
387  svn_ra_serf__merge_lock_token_list(ctx->lock_tokens, NULL, body_bkt,
388                                     alloc, pool);
389
390  svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:merge");
391
392  *bkt = body_bkt;
393
394  return SVN_NO_ERROR;
395}
396
397
398svn_error_t *
399svn_ra_serf__run_merge(const svn_commit_info_t **commit_info,
400                       svn_ra_serf__session_t *session,
401                       const char *merge_resource_url,
402                       apr_hash_t *lock_tokens,
403                       svn_boolean_t keep_locks,
404                       apr_pool_t *result_pool,
405                       apr_pool_t *scratch_pool)
406{
407  merge_context_t *merge_ctx;
408  svn_ra_serf__handler_t *handler;
409  svn_ra_serf__xml_context_t *xmlctx;
410
411  merge_ctx = apr_pcalloc(scratch_pool, sizeof(*merge_ctx));
412
413  merge_ctx->pool = result_pool;
414  merge_ctx->session = session;
415
416  merge_ctx->merge_resource_url = merge_resource_url;
417
418  merge_ctx->lock_tokens = lock_tokens;
419  merge_ctx->keep_locks = keep_locks;
420
421  /* We don't need the full merge response when working over HTTPv2.
422   * Over HTTPv1, this response is only required with a non-null
423   * svn_ra_push_wc_prop_func_t callback. */
424  merge_ctx->disable_merge_response =
425    SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session) ||
426    session->wc_callbacks->push_wc_prop == NULL;
427
428  merge_ctx->commit_info = svn_create_commit_info(result_pool);
429
430  merge_ctx->merge_url = session->session_url.path;
431
432  xmlctx = svn_ra_serf__xml_context_create(merge_ttable,
433                                           NULL, merge_closed, NULL,
434                                           merge_ctx,
435                                           scratch_pool);
436  handler = svn_ra_serf__create_expat_handler(session, xmlctx, NULL,
437                                              scratch_pool);
438
439  handler->method = "MERGE";
440  handler->path = merge_ctx->merge_url;
441  handler->body_delegate = create_merge_body;
442  handler->body_delegate_baton = merge_ctx;
443  handler->body_type = "text/xml";
444
445  handler->header_delegate = setup_merge_headers;
446  handler->header_delegate_baton = merge_ctx;
447
448  merge_ctx->handler = handler;
449
450  SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool));
451
452  if (handler->sline.code != 200)
453    return svn_error_trace(svn_ra_serf__unexpected_status(handler));
454
455  *commit_info = merge_ctx->commit_info;
456
457  /* Sanity check (Reported to be triggered by CodePlex's svnbridge) */
458  if (! SVN_IS_VALID_REVNUM(merge_ctx->commit_info->revision))
459    {
460      return svn_error_create(SVN_ERR_RA_DAV_PROPS_NOT_FOUND, NULL,
461                              _("The MERGE response did not include "
462                                "a new revision"));
463    }
464
465  merge_ctx->commit_info->repos_root = apr_pstrdup(result_pool,
466                                                   session->repos_root_str);
467
468  return SVN_NO_ERROR;
469}
470