2 * update.c: handle the update-report request and response
4 * ====================================================================
5 * Copyright (c) 2000-2007 CollabNet. All rights reserved.
7 * This software is licensed as described in the file COPYING, which
8 * you should have received as part of this distribution. The terms
9 * are also available at http://subversion.tigris.org/license-1.html.
10 * If newer versions of this license are posted there, you may use a
11 * newer version instead, at your option.
13 * This software consists of voluntary contributions made by many
14 * individuals. For exact contribution history, see the revision
15 * history and logs, available at http://subversion.tigris.org/.
16 * ====================================================================
19 #include <apr_pools.h>
20 #include <apr_strings.h>
24 #include <http_request.h>
28 #include "svn_pools.h"
29 #include "svn_repos.h"
32 #include "svn_base64.h"
36 #include "svn_props.h"
37 #include "private/svn_log.h"
39 #include "../dav_svn.h"
43 const dav_resource
*resource
;
45 /* the revision we are updating to. used to generated IDs. */
46 svn_fs_root_t
*rev_root
;
51 /* if doing a regular update, then dst_path == anchor. if this is a
52 'switch' operation, then this field is the fs path that is being
53 switched to. This path needs to telescope in the update-editor
54 just like 'anchor' above; it's used for retrieving CR's and
55 vsn-url's during the edit. */
58 /* this buffers the output for a bit and is automatically flushed,
59 at appropriate times, by the Apache filter system. */
60 apr_bucket_brigade
*bb
;
62 /* where to deliver the output */
65 /* where do these editor paths *really* point to? */
68 /* are we doing a resource walk? */
69 svn_boolean_t resource_walk
;
71 /* True iff we've already sent the open tag for the update. */
72 svn_boolean_t started_update
;
74 /* True iff client requested all data inline in the report. */
75 svn_boolean_t send_all
;
77 /* SVNDIFF version to send to client. */
81 typedef struct item_baton_t
{
84 struct item_baton_t
*parent
; /* the parent of this item. */
85 const char *name
; /* the single-component name of this item */
86 const char *path
; /* a telescoping extension of uc->anchor */
87 const char *path2
; /* a telescoping extension of uc->dst_path */
88 const char *path3
; /* a telescoping extension of uc->dst_path
89 without dst_path as prefix. */
91 const char *base_checksum
; /* base_checksum (from apply_textdelta) */
92 const char *text_checksum
; /* text_checksum (from close_file) */
94 svn_boolean_t text_changed
; /* Did the file's contents change? */
95 svn_boolean_t added
; /* File added? (Implies text_changed.) */
96 svn_boolean_t copyfrom
; /* File copied? */
97 apr_array_header_t
*changed_props
; /* array of const char * prop names */
98 apr_array_header_t
*removed_props
; /* array of const char * prop names */
101 const char *committed_rev
;
102 const char *committed_date
;
103 const char *last_author
;
108 #define DIR_OR_FILE(is_dir) ((is_dir) ? "directory" : "file")
111 /* add PATH to the pathmap HASH with a repository path of LINKPATH.
112 if LINKPATH is NULL, PATH will map to itself. */
114 add_to_path_map(apr_hash_t
*hash
, const char *path
, const char *linkpath
)
116 /* normalize 'root paths' to have a slash */
117 const char *norm_path
= strcmp(path
, "") ? path
: "/";
119 /* if there is an actual linkpath given, it is the repos path, else
120 our path maps to itself. */
121 const char *repos_path
= linkpath
? linkpath
: norm_path
;
123 /* now, geez, put the path in the map already! */
124 apr_hash_set(hash
, path
, APR_HASH_KEY_STRING
, repos_path
);
128 /* return the actual repository path referred to by the editor's PATH,
129 allocated in POOL, determined by examining the pathmap HASH. */
131 get_from_path_map(apr_hash_t
*hash
, const char *path
, apr_pool_t
*pool
)
133 const char *repos_path
;
134 svn_stringbuf_t
*my_path
;
136 /* no hash means no map. that's easy enough. */
138 return apr_pstrdup(pool
, path
);
140 if ((repos_path
= apr_hash_get(hash
, path
, APR_HASH_KEY_STRING
)))
142 /* what luck! this path is a hash key! if there is a linkpath,
143 use that, else return the path itself. */
144 return apr_pstrdup(pool
, repos_path
);
147 /* bummer. PATH wasn't a key in path map, so we get to start
148 hacking off components and looking for a parent from which to
149 derive a repos_path. use a stringbuf for convenience. */
150 my_path
= svn_stringbuf_create(path
, pool
);
153 svn_path_remove_component(my_path
);
154 if ((repos_path
= apr_hash_get(hash
, my_path
->data
, my_path
->len
)))
156 /* we found a mapping ... but of one of PATH's parents.
157 soooo, we get to re-append the chunks of PATH that we
158 broke off to the REPOS_PATH we found. */
159 return svn_path_join(repos_path
, path
+ my_path
->len
+ 1, pool
);
162 while (! svn_path_is_empty(my_path
->data
)
163 && strcmp(my_path
->data
, "/") != 0);
165 /* well, we simply never found anything worth mentioning the map.
166 PATH is its own default finding, then. */
167 return apr_pstrdup(pool
, path
);
171 static item_baton_t
*
172 make_child_baton(item_baton_t
*parent
, const char *path
, apr_pool_t
*pool
)
176 baton
= apr_pcalloc(pool
, sizeof(*baton
));
178 baton
->uc
= parent
->uc
;
179 baton
->name
= svn_path_basename(path
, pool
);
180 baton
->parent
= parent
;
182 /* Telescope the path based on uc->anchor. */
183 baton
->path
= svn_path_join(parent
->path
, baton
->name
, pool
);
185 /* Telescope the path based on uc->dst_path in the exact same way. */
186 baton
->path2
= svn_path_join(parent
->path2
, baton
->name
, pool
);
188 /* Telescope the third path: it's relative, not absolute, to
189 dst_path. Now, we gotta be careful here, because if this
190 operation had a target, and we're it, then we have to use the
191 basename of our source reflection instead of our own. */
192 if ((*baton
->uc
->target
) && (! parent
->parent
))
193 baton
->path3
= svn_path_join(parent
->path3
, baton
->uc
->target
, pool
);
195 baton
->path3
= svn_path_join(parent
->path3
, baton
->name
, pool
);
201 /* Get the real filesystem PATH for BATON, and return the value
202 allocated from POOL. This function juggles the craziness of
203 updates, switches, and updates of switched things. */
205 get_real_fs_path(item_baton_t
*baton
, apr_pool_t
*pool
)
207 const char *path
= get_from_path_map(baton
->uc
->pathmap
, baton
->path
, pool
);
208 return strcmp(path
, baton
->path
) ? path
: baton
->path2
;
213 send_vsn_url(item_baton_t
*baton
, apr_pool_t
*pool
)
217 svn_revnum_t revision
;
219 /* Try to use the CR, assuming the path exists in CR. */
220 path
= get_real_fs_path(baton
, pool
);
221 revision
= dav_svn__get_safe_cr(baton
->uc
->rev_root
, path
, pool
);
223 href
= dav_svn__build_uri(baton
->uc
->resource
->info
->repos
,
224 DAV_SVN__BUILD_URI_VERSION
,
225 revision
, path
, 0 /* add_href */, pool
);
227 return dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
228 "<D:checked-in><D:href>%s</D:href></D:checked-in>"
229 DEBUG_CR
, apr_xml_quote_string(pool
, href
, 1));
234 absent_helper(svn_boolean_t is_dir
,
236 item_baton_t
*parent
,
239 update_ctx_t
*uc
= parent
->uc
;
241 if (! uc
->resource_walk
)
243 const char *elt
= apr_psprintf(pool
,
244 "<S:absent-%s name=\"%s\"/>" DEBUG_CR
,
248 svn_path_basename(path
, pool
),
250 SVN_ERR(dav_svn__send_xml(uc
->bb
, uc
->output
, "%s", elt
));
258 upd_absent_directory(const char *path
, void *parent_baton
, apr_pool_t
*pool
)
260 return absent_helper(TRUE
, path
, parent_baton
, pool
);
265 upd_absent_file(const char *path
, void *parent_baton
, apr_pool_t
*pool
)
267 return absent_helper(FALSE
, path
, parent_baton
, pool
);
272 add_helper(svn_boolean_t is_dir
,
274 item_baton_t
*parent
,
275 const char *copyfrom_path
,
276 svn_revnum_t copyfrom_revision
,
281 update_ctx_t
*uc
= parent
->uc
;
282 const char *bc_url
= NULL
;
284 child
= make_child_baton(parent
, path
, pool
);
287 if (uc
->resource_walk
)
289 SVN_ERR(dav_svn__send_xml(child
->uc
->bb
, child
->uc
->output
,
290 "<S:resource path=\"%s\">" DEBUG_CR
,
291 apr_xml_quote_string(pool
, child
->path3
, 1)));
295 const char *qname
= apr_xml_quote_string(pool
, child
->name
, 1);
297 const char *real_path
= get_real_fs_path(child
, pool
);
301 /* files have checksums */
302 unsigned char digest
[APR_MD5_DIGESTSIZE
];
303 SVN_ERR(svn_fs_file_md5_checksum
304 (digest
, uc
->rev_root
, real_path
, pool
));
306 child
->text_checksum
= svn_md5_digest_to_cstring(digest
, pool
);
310 /* we send baseline-collection urls when we add a directory */
311 svn_revnum_t revision
;
312 revision
= dav_svn__get_safe_cr(child
->uc
->rev_root
, real_path
,
314 bc_url
= dav_svn__build_uri(child
->uc
->resource
->info
->repos
,
315 DAV_SVN__BUILD_URI_BC
,
317 0 /* add_href */, pool
);
319 /* ugh, build_uri ignores the path and just builds the root
320 of the baseline collection. we have to tack the
321 real_path on manually, ignoring its leading slash. */
322 if (real_path
&& (! svn_path_is_empty(real_path
)))
323 bc_url
= svn_path_url_add_component(bc_url
, real_path
+1, pool
);
325 /* make sure that the BC_URL is xml attribute safe. */
326 bc_url
= apr_xml_quote_string(pool
, bc_url
, 1);
330 if (copyfrom_path
== NULL
)
333 elt
= apr_psprintf(pool
, "<S:add-%s name=\"%s\" "
334 "bc-url=\"%s\">" DEBUG_CR
,
335 DIR_OR_FILE(is_dir
), qname
, bc_url
);
337 elt
= apr_psprintf(pool
, "<S:add-%s name=\"%s\">" DEBUG_CR
,
338 DIR_OR_FILE(is_dir
), qname
);
342 const char *qcopy
= apr_xml_quote_string(pool
, copyfrom_path
, 1);
345 elt
= apr_psprintf(pool
, "<S:add-%s name=\"%s\" "
346 "copyfrom-path=\"%s\" copyfrom-rev=\"%ld\" "
347 "bc-url=\"%s\">" DEBUG_CR
,
349 qname
, qcopy
, copyfrom_revision
,
352 elt
= apr_psprintf(pool
, "<S:add-%s name=\"%s\" "
353 "copyfrom-path=\"%s\""
354 " copyfrom-rev=\"%ld\">" DEBUG_CR
,
356 qname
, qcopy
, copyfrom_revision
);
358 child
->copyfrom
= TRUE
;
361 /* Resist the temptation to pass 'elt' as the format string.
362 Because it contains URIs, it might have sequences that look
363 like format string insert placeholders. For example,
364 "this%20dir" is a valid printf() format string that means
365 "this[insert an integer of width 20 here]ir". */
366 SVN_ERR(dav_svn__send_xml(child
->uc
->bb
, child
->uc
->output
, "%s", elt
));
369 SVN_ERR(send_vsn_url(child
, pool
));
371 if (uc
->resource_walk
)
372 SVN_ERR(dav_svn__send_xml(child
->uc
->bb
, child
->uc
->output
,
373 "</S:resource>" DEBUG_CR
));
375 *child_baton
= child
;
382 open_helper(svn_boolean_t is_dir
,
384 item_baton_t
*parent
,
385 svn_revnum_t base_revision
,
389 item_baton_t
*child
= make_child_baton(parent
, path
, pool
);
390 const char *qname
= apr_xml_quote_string(pool
, child
->name
, 1);
392 SVN_ERR(dav_svn__send_xml(child
->uc
->bb
, child
->uc
->output
,
393 "<S:open-%s name=\"%s\""
394 " rev=\"%ld\">" DEBUG_CR
,
395 DIR_OR_FILE(is_dir
), qname
, base_revision
));
396 SVN_ERR(send_vsn_url(child
, pool
));
397 *child_baton
= child
;
403 close_helper(svn_boolean_t is_dir
, item_baton_t
*baton
)
407 if (baton
->uc
->resource_walk
)
410 /* ### ack! binary names won't float here! */
411 /* If this is a copied file/dir, we can have removed props. */
412 if (baton
->removed_props
&& (! baton
->added
|| baton
->copyfrom
))
416 for (i
= 0; i
< baton
->removed_props
->nelts
; i
++)
418 /* We already XML-escaped the property name in change_xxx_prop. */
419 qname
= APR_ARRAY_IDX(baton
->removed_props
, i
, const char *);
420 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
421 "<S:remove-prop name=\"%s\"/>"
426 if ((! baton
->uc
->send_all
) && baton
->changed_props
&& (! baton
->added
))
428 /* Tell the client to fetch all the props */
429 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
430 "<S:fetch-props/>" DEBUG_CR
));
433 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
, "<S:prop>"));
435 /* Both modern and non-modern clients need the checksum... */
436 if (baton
->text_checksum
)
438 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
439 "<V:md5-checksum>%s</V:md5-checksum>",
440 baton
->text_checksum
));
443 /* ...but only non-modern clients want the 3 CR-related properties
444 sent like here, because they can't handle receiving these special
445 props inline like any other prop.
446 ### later on, compress via the 'scattered table' solution as
447 discussed with gstein. -bmcs */
448 if (! baton
->uc
->send_all
)
450 /* ### grrr, these DAV: property names are already #defined in
451 ra_dav.h, and statically defined in liveprops.c. And now
452 they're hardcoded here. Isn't there some header file that both
453 sides of the network can share?? */
455 /* ### special knowledge: svn_repos_dir_delta2 will never send
456 *removals* of the commit-info "entry props". */
457 if (baton
->committed_rev
)
458 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
459 "<D:version-name>%s</D:version-name>",
460 baton
->committed_rev
));
462 if (baton
->committed_date
)
463 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
464 "<D:creationdate>%s</D:creationdate>",
465 baton
->committed_date
));
467 if (baton
->last_author
)
468 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
469 "<D:creator-displayname>%s"
470 "</D:creator-displayname>",
471 apr_xml_quote_string(baton
->pool
,
477 /* Close unconditionally, because we sent checksum unconditionally. */
478 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
, "</S:prop>\n"));
481 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
482 "</S:add-%s>" DEBUG_CR
,
483 DIR_OR_FILE(is_dir
)));
485 SVN_ERR(dav_svn__send_xml(baton
->uc
->bb
, baton
->uc
->output
,
486 "</S:open-%s>" DEBUG_CR
,
487 DIR_OR_FILE(is_dir
)));
492 /* Send the opening tag of the update-report if it hasn't been sent
495 maybe_start_update_report(update_ctx_t
*uc
)
497 if ((! uc
->resource_walk
) && (! uc
->started_update
))
499 SVN_ERR(dav_svn__send_xml(uc
->bb
, uc
->output
,
500 DAV_XML_HEADER DEBUG_CR
501 "<S:update-report xmlns:S=\""
502 SVN_XML_NAMESPACE
"\" "
503 "xmlns:V=\"" SVN_DAV_PROP_NS_DAV
"\" "
504 "xmlns:D=\"DAV:\" %s>" DEBUG_CR
,
505 uc
->send_all
? "send-all=\"true\"" : ""));
507 uc
->started_update
= TRUE
;
515 upd_set_target_revision(void *edit_baton
,
516 svn_revnum_t target_revision
,
519 update_ctx_t
*uc
= edit_baton
;
521 SVN_ERR(maybe_start_update_report(uc
));
523 if (! uc
->resource_walk
)
524 SVN_ERR(dav_svn__send_xml(uc
->bb
, uc
->output
,
525 "<S:target-revision rev=\"%ld"
526 "\"/>" DEBUG_CR
, target_revision
));
533 upd_open_root(void *edit_baton
,
534 svn_revnum_t base_revision
,
538 update_ctx_t
*uc
= edit_baton
;
539 item_baton_t
*b
= apr_pcalloc(pool
, sizeof(*b
));
541 /* note that we create a subpool; the root_baton is passed to the
542 close_directory callback, where we will destroy the pool. */
546 b
->path
= uc
->anchor
;
547 b
->path2
= uc
->dst_path
;
552 SVN_ERR(maybe_start_update_report(uc
));
554 if (uc
->resource_walk
)
556 const char *qpath
= apr_xml_quote_string(pool
, b
->path3
, 1);
557 SVN_ERR(dav_svn__send_xml(uc
->bb
, uc
->output
,
558 "<S:resource path=\"%s\">" DEBUG_CR
, qpath
));
562 SVN_ERR(dav_svn__send_xml(uc
->bb
, uc
->output
,
563 "<S:open-directory rev=\"%ld\">"
564 DEBUG_CR
, base_revision
));
567 /* Only transmit the root directory's Version Resource URL if
568 there's no target. */
570 SVN_ERR(send_vsn_url(b
, pool
));
572 if (uc
->resource_walk
)
573 SVN_ERR(dav_svn__send_xml(uc
->bb
, uc
->output
,
574 "</S:resource>" DEBUG_CR
));
581 upd_delete_entry(const char *path
,
582 svn_revnum_t revision
,
586 item_baton_t
*parent
= parent_baton
;
587 const char *qname
= apr_xml_quote_string(pool
,
588 svn_path_basename(path
, pool
), 1);
589 return dav_svn__send_xml(parent
->uc
->bb
, parent
->uc
->output
,
590 "<S:delete-entry name=\"%s\"/>" DEBUG_CR
, qname
);
595 upd_add_directory(const char *path
,
597 const char *copyfrom_path
,
598 svn_revnum_t copyfrom_revision
,
602 return add_helper(TRUE
/* is_dir */,
603 path
, parent_baton
, copyfrom_path
, copyfrom_revision
, pool
,
609 upd_open_directory(const char *path
,
611 svn_revnum_t base_revision
,
615 return open_helper(TRUE
/* is_dir */,
616 path
, parent_baton
, base_revision
, pool
, child_baton
);
621 upd_change_xxx_prop(void *baton
,
623 const svn_string_t
*value
,
626 item_baton_t
*b
= baton
;
629 /* Resource walks say nothing about props. */
630 if (b
->uc
->resource_walk
)
633 /* Else this not a resource walk, so either send props or cache them
634 to send later, depending on whether this is a modern report
637 qname
= apr_xml_quote_string(b
->pool
, name
, 1);
639 /* apr_xml_quote_string doesn't realloc if there is nothing to
640 quote, so dup the name, but only if necessary. */
642 qname
= apr_pstrdup(b
->pool
, name
);
651 if (svn_xml_is_xml_safe(value
->data
, value
->len
))
653 svn_stringbuf_t
*tmp
= NULL
;
654 svn_xml_escape_cdata_string(&tmp
, value
, pool
);
656 SVN_ERR(dav_svn__send_xml(b
->uc
->bb
, b
->uc
->output
,
657 "<S:set-prop name=\"%s\">", qname
));
661 qval
= svn_base64_encode_string(value
, pool
)->data
;
662 SVN_ERR(dav_svn__send_xml(b
->uc
->bb
, b
->uc
->output
,
663 "<S:set-prop name=\"%s\" "
664 "encoding=\"base64\">" DEBUG_CR
,
668 SVN_ERR(dav_svn__send_xml(b
->uc
->bb
, b
->uc
->output
, "%s", qval
));
669 SVN_ERR(dav_svn__send_xml(b
->uc
->bb
, b
->uc
->output
,
670 "</S:set-prop>" DEBUG_CR
));
672 else /* value is null, so this is a prop removal */
674 SVN_ERR(dav_svn__send_xml(b
->uc
->bb
, b
->uc
->output
,
675 "<S:remove-prop name=\"%s\"/>" DEBUG_CR
,
679 else /* don't do inline response, just cache prop names for close_helper */
681 /* For now, store certain entry props, because we'll need to send
682 them later as standard DAV ("D:") props. ### this should go
683 away and we should just tunnel those props on through for the
684 client to deal with. */
685 #define NSLEN (sizeof(SVN_PROP_ENTRY_PREFIX) - 1)
686 if (! strncmp(name
, SVN_PROP_ENTRY_PREFIX
, NSLEN
))
688 if (strcmp(name
, SVN_PROP_ENTRY_COMMITTED_REV
) == 0)
690 b
->committed_rev
= value
?
691 apr_pstrdup(b
->pool
, value
->data
) : NULL
;
693 else if (strcmp(name
, SVN_PROP_ENTRY_COMMITTED_DATE
) == 0)
695 b
->committed_date
= value
?
696 apr_pstrdup(b
->pool
, value
->data
) : NULL
;
698 else if (strcmp(name
, SVN_PROP_ENTRY_LAST_AUTHOR
) == 0)
700 b
->last_author
= value
?
701 apr_pstrdup(b
->pool
, value
->data
) : NULL
;
703 else if ((strcmp(name
, SVN_PROP_ENTRY_LOCK_TOKEN
) == 0)
706 /* We only support delete of lock tokens, not add/modify. */
707 if (! b
->removed_props
)
708 b
->removed_props
= apr_array_make(b
->pool
, 1, sizeof(name
));
709 APR_ARRAY_PUSH(b
->removed_props
, const char *) = qname
;
717 if (! b
->changed_props
)
718 b
->changed_props
= apr_array_make(b
->pool
, 1, sizeof(name
));
720 APR_ARRAY_PUSH(b
->changed_props
, const char *) = qname
;
724 if (! b
->removed_props
)
725 b
->removed_props
= apr_array_make(b
->pool
, 1, sizeof(name
));
727 APR_ARRAY_PUSH(b
->removed_props
, const char *) = qname
;
736 upd_close_directory(void *dir_baton
, apr_pool_t
*pool
)
738 return close_helper(TRUE
/* is_dir */, dir_baton
);
743 upd_add_file(const char *path
,
745 const char *copyfrom_path
,
746 svn_revnum_t copyfrom_revision
,
750 return add_helper(FALSE
/* is_dir */,
751 path
, parent_baton
, copyfrom_path
, copyfrom_revision
, pool
,
757 upd_open_file(const char *path
,
759 svn_revnum_t base_revision
,
763 return open_helper(FALSE
/* is_dir */,
764 path
, parent_baton
, base_revision
, pool
, file_baton
);
768 /* We have our own window handler and baton as a simple wrapper around
769 the real handler (which converts txdelta windows to base64-encoded
770 svndiff data). The wrapper is responsible for sending the opening
771 and closing XML tags around the svndiff data. */
772 struct window_handler_baton
774 svn_boolean_t seen_first_window
; /* False until first window seen. */
777 /* The _real_ window handler and baton. */
778 svn_txdelta_window_handler_t handler
;
783 /* This implements 'svn_txdelta_window_handler_t'. */
785 window_handler(svn_txdelta_window_t
*window
, void *baton
)
787 struct window_handler_baton
*wb
= baton
;
789 if (! wb
->seen_first_window
)
791 wb
->seen_first_window
= TRUE
;
792 SVN_ERR(dav_svn__send_xml(wb
->uc
->bb
, wb
->uc
->output
, "<S:txdelta>"));
795 SVN_ERR(wb
->handler(window
, wb
->handler_baton
));
798 SVN_ERR(dav_svn__send_xml(wb
->uc
->bb
, wb
->uc
->output
, "</S:txdelta>"));
804 /* This implements 'svn_txdelta_window_handler_t'.
805 During a resource walk, the driver sends an empty window as a
806 boolean indicating that a change happened to this file, but we
807 don't want to send anything over the wire as a result. */
809 dummy_window_handler(svn_txdelta_window_t
*window
, void *baton
)
816 upd_apply_textdelta(void *file_baton
,
817 const char *base_checksum
,
819 svn_txdelta_window_handler_t
*handler
,
820 void **handler_baton
)
822 item_baton_t
*file
= file_baton
;
823 struct window_handler_baton
*wb
;
824 svn_stream_t
*base64_stream
;
826 /* Store the base checksum and the fact the file's text changed. */
827 file
->base_checksum
= apr_pstrdup(file
->pool
, base_checksum
);
828 file
->text_changed
= TRUE
;
830 /* If this is a resource walk, or if we're not in "send-all" mode,
831 we don't actually want to transmit text-deltas. */
832 if (file
->uc
->resource_walk
|| (! file
->uc
->send_all
))
834 *handler
= dummy_window_handler
;
835 *handler_baton
= NULL
;
839 wb
= apr_palloc(file
->pool
, sizeof(*wb
));
840 wb
->seen_first_window
= FALSE
;
842 base64_stream
= dav_svn__make_base64_output_stream(wb
->uc
->bb
,
846 svn_txdelta_to_svndiff2(&(wb
->handler
), &(wb
->handler_baton
),
847 base64_stream
, file
->uc
->svndiff_version
,
850 *handler
= window_handler
;
858 upd_close_file(void *file_baton
, const char *text_checksum
, apr_pool_t
*pool
)
860 item_baton_t
*file
= file_baton
;
862 file
->text_checksum
= text_checksum
?
863 apr_pstrdup(file
->pool
, text_checksum
) : NULL
;
865 /* If we are not in "send all" mode, and this file is not a new
866 addition or didn't otherwise have changed text, tell the client
868 if ((! file
->uc
->send_all
) && (! file
->added
) && file
->text_changed
)
871 elt
= apr_psprintf(pool
, "<S:fetch-file%s%s%s/>" DEBUG_CR
,
872 file
->base_checksum
? " base-checksum=\"" : "",
873 file
->base_checksum
? file
->base_checksum
: "",
874 file
->base_checksum
? "\"" : "");
875 SVN_ERR(dav_svn__send_xml(file
->uc
->bb
, file
->uc
->output
, "%s", elt
));
878 return close_helper(FALSE
/* is_dir */, file
);
883 upd_close_edit(void *edit_baton
, apr_pool_t
*pool
)
885 update_ctx_t
*uc
= edit_baton
;
887 /* Our driver will unconditionally close the update report... So if
888 the report hasn't even been started yet, start it now. */
889 return maybe_start_update_report(uc
);
893 /* Return a specific error associated with the contents of TAGNAME
894 being malformed. Use pool for allocations. */
896 malformed_element_error(const char *tagname
, apr_pool_t
*pool
)
898 const char *errstr
= apr_pstrcat(pool
, "The request's '", tagname
,
899 "' element is malformed; there "
900 "is a problem with the client.", NULL
);
901 return dav_svn__new_error_tag(pool
, HTTP_BAD_REQUEST
, 0, errstr
,
902 SVN_DAV_ERROR_NAMESPACE
, SVN_DAV_ERROR_TAG
);
907 dav_svn__update_report(const dav_resource
*resource
,
908 const apr_xml_doc
*doc
,
911 svn_delta_editor_t
*editor
;
914 update_ctx_t uc
= { 0 };
915 svn_revnum_t revnum
= SVN_INVALID_REVNUM
;
916 svn_revnum_t from_revnum
= SVN_INVALID_REVNUM
;
918 /* entry_counter and entry_is_empty are for operational logging. */
919 int entry_counter
= 0;
920 svn_boolean_t entry_is_empty
= FALSE
;
922 dav_error
*derr
= NULL
;
923 apr_status_t apr_err
;
924 const char *src_path
= NULL
;
925 const char *dst_path
= NULL
;
926 const dav_svn_repos
*repos
= resource
->info
->repos
;
927 const char *target
= "";
928 svn_boolean_t text_deltas
= TRUE
;
929 svn_depth_t requested_depth
= svn_depth_unknown
;
930 svn_boolean_t saw_depth
= FALSE
;
931 svn_boolean_t saw_recursive
= FALSE
;
932 svn_boolean_t resource_walk
= FALSE
;
933 svn_boolean_t ignore_ancestry
= FALSE
;
934 svn_boolean_t send_copyfrom_args
= FALSE
;
935 dav_svn__authz_read_baton arb
;
936 apr_pool_t
*subpool
= svn_pool_create(resource
->pool
);
938 /* Construct the authz read check baton. */
939 arb
.r
= resource
->info
->r
;
942 if (resource
->info
->restype
!= DAV_SVN_RESTYPE_VCC
)
944 return dav_svn__new_error_tag(resource
->pool
, HTTP_CONFLICT
, 0,
945 "This report can only be run against "
947 SVN_DAV_ERROR_NAMESPACE
,
951 ns
= dav_svn__find_ns(doc
->namespaces
, SVN_XML_NAMESPACE
);
954 return dav_svn__new_error_tag(resource
->pool
, HTTP_BAD_REQUEST
, 0,
955 "The request does not contain the 'svn:' "
956 "namespace, so it is not going to have an "
957 "svn:target-revision element. That element "
959 SVN_DAV_ERROR_NAMESPACE
,
963 /* If server configuration permits bulk updates (a report with props
964 and textdeltas inline, rather than placeholder tags that tell the
965 client to do further fetches), look to see if client requested as
967 if (repos
->bulk_updates
)
969 apr_xml_attr
*this_attr
;
971 for (this_attr
= doc
->root
->attr
; this_attr
; this_attr
= this_attr
->next
)
973 if ((strcmp(this_attr
->name
, "send-all") == 0)
974 && (strcmp(this_attr
->value
, "true") == 0))
982 for (child
= doc
->root
->first_child
; child
!= NULL
; child
= child
->next
)
984 /* Note that child->name might not match any of the cases below.
985 Thus, the check for non-empty cdata in each of these cases
986 cannot be moved to the top of the loop, because then it would
987 wrongly catch other elements that do allow empty cdata. */
990 if (child
->ns
== ns
&& strcmp(child
->name
, "target-revision") == 0)
992 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 1);
994 return malformed_element_error(child
->name
, resource
->pool
);
995 revnum
= SVN_STR_TO_REV(cdata
);
997 if (child
->ns
== ns
&& strcmp(child
->name
, "src-path") == 0)
999 dav_svn__uri_info this_info
;
1000 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 0);
1002 return malformed_element_error(child
->name
, resource
->pool
);
1003 if ((derr
= dav_svn__test_canonical(cdata
, resource
->pool
)))
1005 if ((serr
= dav_svn__simple_parse_uri(&this_info
, resource
,
1006 cdata
, resource
->pool
)))
1007 return dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1008 "Could not parse 'src-path' URL.",
1010 src_path
= this_info
.repos_path
;
1012 if (child
->ns
== ns
&& strcmp(child
->name
, "dst-path") == 0)
1014 dav_svn__uri_info this_info
;
1015 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 0);
1017 return malformed_element_error(child
->name
, resource
->pool
);
1018 if ((derr
= dav_svn__test_canonical(cdata
, resource
->pool
)))
1020 if ((serr
= dav_svn__simple_parse_uri(&this_info
, resource
,
1021 cdata
, resource
->pool
)))
1022 return dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1023 "Could not parse 'dst-path' URL.",
1025 dst_path
= this_info
.repos_path
;
1027 if (child
->ns
== ns
&& strcmp(child
->name
, "update-target") == 0)
1029 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 0);
1030 if ((derr
= dav_svn__test_canonical(cdata
, resource
->pool
)))
1034 if (child
->ns
== ns
&& strcmp(child
->name
, "depth") == 0)
1036 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 1);
1038 return malformed_element_error(child
->name
, resource
->pool
);
1039 requested_depth
= svn_depth_from_word(cdata
);
1042 if ((child
->ns
== ns
&& strcmp(child
->name
, "recursive") == 0)
1045 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 1);
1047 return malformed_element_error(child
->name
, resource
->pool
);
1048 if (strcmp(cdata
, "no") == 0)
1049 requested_depth
= svn_depth_files
;
1051 requested_depth
= svn_depth_infinity
;
1052 /* Note that even modern, depth-aware clients still transmit
1053 "no" for "recursive" (along with "files" for "depth") in
1054 the svn_depth_files case, and transmit "no" in the
1055 svn_depth_empty case. This is because they don't know if
1056 they're talking to a depth-aware server or not, and they
1057 don't need to know -- all they have to do is transmit
1058 both, and the server will DTRT either way (although in
1059 the svn_depth_empty case, the client will still have some
1060 work to do in ignoring the files that come down).
1062 When both "depth" and "recursive" are sent, we don't
1063 bother to check if they're mutually consistent, we just
1064 let depth dominate. */
1065 saw_recursive
= TRUE
;
1067 if (child
->ns
== ns
&& strcmp(child
->name
, "ignore-ancestry") == 0)
1069 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 1);
1071 return malformed_element_error(child
->name
, resource
->pool
);
1072 if (strcmp(cdata
, "no") != 0)
1073 ignore_ancestry
= TRUE
;
1075 if (child
->ns
== ns
&& strcmp(child
->name
, "send-copyfrom-args") == 0)
1077 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 1);
1079 return malformed_element_error(child
->name
, resource
->pool
);
1080 if (strcmp(cdata
, "no") != 0)
1081 send_copyfrom_args
= TRUE
;
1083 if (child
->ns
== ns
&& strcmp(child
->name
, "resource-walk") == 0)
1085 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 1);
1087 return malformed_element_error(child
->name
, resource
->pool
);
1088 if (strcmp(cdata
, "no") != 0)
1089 resource_walk
= TRUE
;
1091 if (child
->ns
== ns
&& strcmp(child
->name
, "text-deltas") == 0)
1093 cdata
= dav_xml_get_cdata(child
, resource
->pool
, 1);
1095 return malformed_element_error(child
->name
, resource
->pool
);
1096 if (strcmp(cdata
, "no") == 0)
1097 text_deltas
= FALSE
;
1101 if (!saw_depth
&& !saw_recursive
&& (requested_depth
== svn_depth_unknown
))
1102 requested_depth
= svn_depth_infinity
;
1104 /* If the client never sent a <src-path> element, it's old and
1105 sending a style of report that we no longer allow. */
1108 return dav_svn__new_error_tag
1109 (resource
->pool
, HTTP_BAD_REQUEST
, 0,
1110 "The request did not contain the '<src-path>' element.\n"
1111 "This may indicate that your client is too old.",
1112 SVN_DAV_ERROR_NAMESPACE
,
1116 /* If a revision for this operation was not dictated to us, this
1117 means "update to whatever the current HEAD is now". */
1118 if (revnum
== SVN_INVALID_REVNUM
)
1120 if ((serr
= svn_fs_youngest_rev(&revnum
, repos
->fs
, resource
->pool
)))
1121 return dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1122 "Could not determine the youngest "
1123 "revision for the update process.",
1127 uc
.svndiff_version
= resource
->info
->svndiff_version
;
1128 uc
.resource
= resource
;
1130 uc
.anchor
= src_path
;
1132 uc
.bb
= apr_brigade_create(resource
->pool
, output
->c
->bucket_alloc
);
1134 if (dst_path
) /* we're doing a 'switch' */
1138 /* if the src is split into anchor/target, so must the
1139 telescoping dst_path be. */
1140 uc
.dst_path
= svn_path_dirname(dst_path
, resource
->pool
);
1142 /* Also, the svn_repos_dir_delta2() is going to preserve our
1143 target's name, so we need a pathmap entry for that. */
1145 uc
.pathmap
= apr_hash_make(resource
->pool
);
1146 add_to_path_map(uc
.pathmap
,
1147 svn_path_join(src_path
, target
, resource
->pool
),
1152 uc
.dst_path
= dst_path
;
1155 else /* we're doing an update, so src and dst are the same. */
1156 uc
.dst_path
= uc
.anchor
;
1158 /* Get the root of the revision we want to update to. This will be used
1159 to generated stable id values. */
1160 if ((serr
= svn_fs_revision_root(&uc
.rev_root
, repos
->fs
,
1161 revnum
, resource
->pool
)))
1163 return dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1164 "The revision root could not be created.",
1168 /* If the client did *not* request 'send-all' mode, then we will be
1169 sending only a "skelta" of the difference, which will not need to
1170 contain actual text deltas. */
1172 text_deltas
= FALSE
;
1174 /* When we call svn_repos_finish_report, it will ultimately run
1175 dir_delta() between REPOS_PATH/TARGET and TARGET_PATH. In the
1176 case of an update or status, these paths should be identical. In
1177 the case of a switch, they should be different. */
1178 editor
= svn_delta_default_editor(resource
->pool
);
1179 editor
->set_target_revision
= upd_set_target_revision
;
1180 editor
->open_root
= upd_open_root
;
1181 editor
->delete_entry
= upd_delete_entry
;
1182 editor
->add_directory
= upd_add_directory
;
1183 editor
->open_directory
= upd_open_directory
;
1184 editor
->change_dir_prop
= upd_change_xxx_prop
;
1185 editor
->close_directory
= upd_close_directory
;
1186 editor
->absent_directory
= upd_absent_directory
;
1187 editor
->add_file
= upd_add_file
;
1188 editor
->open_file
= upd_open_file
;
1189 editor
->apply_textdelta
= upd_apply_textdelta
;
1190 editor
->change_file_prop
= upd_change_xxx_prop
;
1191 editor
->close_file
= upd_close_file
;
1192 editor
->absent_file
= upd_absent_file
;
1193 editor
->close_edit
= upd_close_edit
;
1194 if ((serr
= svn_repos_begin_report2(&rbaton
, revnum
,
1203 dav_svn__authz_read_func(&arb
),
1207 return dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1208 "The state report gatherer could not be "
1213 /* scan the XML doc for state information */
1214 for (child
= doc
->root
->first_child
; child
!= NULL
; child
= child
->next
)
1215 if (child
->ns
== ns
)
1217 /* Clear our subpool. */
1218 svn_pool_clear(subpool
);
1220 if (strcmp(child
->name
, "entry") == 0)
1223 svn_revnum_t rev
= SVN_INVALID_REVNUM
;
1224 svn_boolean_t saw_rev
= FALSE
;
1225 const char *linkpath
= NULL
;
1226 const char *locktoken
= NULL
;
1227 svn_boolean_t start_empty
= FALSE
;
1228 apr_xml_attr
*this_attr
= child
->attr
;
1229 /* Default to infinity, for old clients that don't send depth. */
1230 svn_depth_t depth
= svn_depth_infinity
;
1236 if (strcmp(this_attr
->name
, "rev") == 0)
1238 rev
= SVN_STR_TO_REV(this_attr
->value
);
1241 else if (strcmp(this_attr
->name
, "depth") == 0)
1242 depth
= svn_depth_from_word(this_attr
->value
);
1243 else if (strcmp(this_attr
->name
, "linkpath") == 0)
1244 linkpath
= this_attr
->value
;
1245 else if (strcmp(this_attr
->name
, "start-empty") == 0)
1246 start_empty
= entry_is_empty
= TRUE
;
1247 else if (strcmp(this_attr
->name
, "lock-token") == 0)
1248 locktoken
= this_attr
->value
;
1250 this_attr
= this_attr
->next
;
1253 /* we require the `rev' attribute for this to make sense */
1256 serr
= svn_error_create(SVN_ERR_XML_ATTRIB_NOT_FOUND
,
1257 NULL
, "Missing XML attribute: rev");
1258 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1259 "A failure occurred while "
1260 "recording one of the items of "
1261 "working copy state.",
1266 /* get cdata, stripping whitespace */
1267 path
= dav_xml_get_cdata(child
, subpool
, 0);
1269 /* determine the "from rev" for revision range ops */
1270 if (strcmp(path
, "") == 0)
1274 serr
= svn_repos_set_path3(rbaton
, path
, rev
, depth
,
1275 start_empty
, locktoken
, subpool
);
1277 serr
= svn_repos_link_path3(rbaton
, path
, linkpath
, rev
, depth
,
1278 start_empty
, locktoken
, subpool
);
1281 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1282 "A failure occurred while "
1283 "recording one of the items of "
1284 "working copy state.",
1289 /* now, add this path to our path map, but only if we are
1290 doing a regular update (not a `switch') */
1291 if (linkpath
&& (! dst_path
))
1293 const char *this_path
;
1295 uc
.pathmap
= apr_hash_make(resource
->pool
);
1296 this_path
= svn_path_join_many(apr_hash_pool_get(uc
.pathmap
),
1297 src_path
, target
, path
, NULL
);
1298 add_to_path_map(uc
.pathmap
, this_path
, linkpath
);
1301 else if (strcmp(child
->name
, "missing") == 0)
1303 /* get cdata, stripping whitespace */
1304 const char *path
= dav_xml_get_cdata(child
, subpool
, 0);
1305 serr
= svn_repos_delete_path(rbaton
, path
, subpool
);
1308 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1309 "A failure occurred while "
1310 "recording one of the (missing) "
1311 "items of working copy state.",
1318 /* Try to deduce what sort of client command is being run, then
1319 make this guess available to apache's logging subsystem. */
1321 const char *action
, *spath
, *log_depth
;
1323 if (requested_depth
== svn_depth_unknown
)
1326 log_depth
= apr_pstrcat(resource
->pool
, " depth=",
1327 svn_depth_to_word(requested_depth
), NULL
);
1330 spath
= svn_path_join(src_path
, target
, resource
->pool
);
1334 /* If a second path was passed to svn_repos_dir_delta2(), then it
1335 must have been switch, diff, or merge. */
1338 /* diff/merge don't ask for inline text-deltas. */
1340 action
= svn_log__switch(spath
, dst_path
, revnum
,
1341 requested_depth
, resource
->pool
);
1343 action
= svn_log__diff(spath
, from_revnum
, dst_path
, revnum
,
1344 requested_depth
, ignore_ancestry
,
1348 /* Otherwise, it must be checkout, export, update, or status -u. */
1351 /* svn_client_checkout() creates a single root directory, then
1352 reports it (and it alone) to the server as being empty. */
1353 if (entry_counter
== 1 && entry_is_empty
)
1354 action
= svn_log__checkout(spath
, revnum
, requested_depth
,
1359 action
= svn_log__update(spath
, revnum
, requested_depth
,
1363 action
= svn_log__status(spath
, revnum
, requested_depth
,
1368 dav_svn__operational_log(resource
->info
, action
);
1371 /* this will complete the report, and then drive our editor to generate
1372 the response to the client. */
1373 serr
= svn_repos_finish_report(rbaton
, resource
->pool
);
1376 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1377 "A failure occurred while "
1378 "driving the update report editor",
1383 /* We're finished with the report baton. Note that so we don't try
1384 to abort this report later. */
1387 /* ### Temporarily disable resource_walks for single-file switch
1388 operations. It isn't strictly necessary. */
1389 if (dst_path
&& resource_walk
)
1391 /* Sanity check: if we switched a file, we can't do a resource
1392 walk. dir_delta would choke if we pass a filepath as the
1393 'target'. Also, there's no need to do the walk, since the
1394 new vsn-rsc-url was already in the earlier part of the report. */
1395 svn_node_kind_t dst_kind
;
1396 if ((serr
= svn_fs_check_path(&dst_kind
, uc
.rev_root
, dst_path
,
1399 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1400 "Failed checking destination path kind",
1404 if (dst_kind
!= svn_node_dir
)
1405 resource_walk
= FALSE
;
1408 /* The potential "resource walk" part of the update-report. */
1409 if (dst_path
&& resource_walk
) /* this was a 'switch' operation */
1411 /* send a second embedded <S:resource-walk> tree that contains
1412 the new vsn-rsc-urls for the switched dir. this walk
1413 contains essentially nothing but <add> tags. */
1414 svn_fs_root_t
*zero_root
;
1415 serr
= svn_fs_revision_root(&zero_root
, repos
->fs
, 0,
1419 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1420 "Failed to find the revision root",
1425 serr
= dav_svn__send_xml(uc
.bb
, uc
.output
,
1426 "<S:resource-walk>" DEBUG_CR
);
1429 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1430 "Unable to begin resource walk",
1435 uc
.resource_walk
= TRUE
;
1437 /* Compare subtree DST_PATH within a pristine revision to
1438 revision 0. This should result in nothing but 'add' calls
1440 serr
= svn_repos_dir_delta2(zero_root
, "", target
,
1441 uc
.rev_root
, dst_path
,
1442 /* re-use the editor */
1444 dav_svn__authz_read_func(&arb
),
1445 &arb
, FALSE
/* text-deltas */,
1447 TRUE
/* entryprops */,
1448 FALSE
/* ignore-ancestry */,
1453 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1454 "Resource walk failed.",
1459 serr
= dav_svn__send_xml(uc
.bb
, uc
.output
,
1460 "</S:resource-walk>" DEBUG_CR
);
1463 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1464 "Unable to complete resource walk.",
1470 /* Close the report body, unless some error prevented it from being
1471 started in the first place. */
1472 if (uc
.started_update
)
1474 if ((serr
= dav_svn__send_xml(uc
.bb
, uc
.output
,
1475 "</S:update-report>" DEBUG_CR
)))
1477 derr
= dav_svn__convert_err(serr
, HTTP_INTERNAL_SERVER_ERROR
,
1478 "Unable to complete update report.",
1486 /* Flush the contents of the brigade (returning an error only if we
1487 don't already have one). */
1488 if ((! derr
) && ((apr_err
= ap_fflush(output
, uc
.bb
))))
1489 derr
= dav_svn__convert_err(svn_error_create(apr_err
, 0, NULL
),
1490 HTTP_INTERNAL_SERVER_ERROR
,
1491 "Error flushing brigade.",
1494 /* if an error was produced EITHER by the dir_delta drive or the
1495 resource-walker... */
1499 svn_error_clear(svn_repos_abort_report(rbaton
, resource
->pool
));
1503 /* Destroy our subpool. */
1504 svn_pool_destroy(subpool
);