In the command-line client, forbid
[svn.git] / subversion / libsvn_repos / replay.c
blobd5a738fc3bb48a6ecae680d98945d652caa766be
1 /*
2 * replay.c: an editor driver for changes made in a given revision
3 * or transaction
5 * ====================================================================
6 * Copyright (c) 2000-2006 CollabNet. All rights reserved.
8 * This software is licensed as described in the file COPYING, which
9 * you should have received as part of this distribution. The terms
10 * are also available at http://subversion.tigris.org/license-1.html.
11 * If newer versions of this license are posted there, you may use a
12 * newer version instead, at your option.
14 * This software consists of voluntary contributions made by many
15 * individuals. For exact contribution history, see the revision
16 * history and logs, available at http://subversion.tigris.org/.
17 * ====================================================================
21 #include <assert.h>
22 #include <apr_hash.h>
23 #include <apr_md5.h>
25 #include "svn_types.h"
26 #include "svn_delta.h"
27 #include "svn_fs.h"
28 #include "svn_md5.h"
29 #include "svn_repos.h"
30 #include "svn_props.h"
31 #include "svn_pools.h"
32 #include "svn_path.h"
33 #include "svn_private_config.h"
36 /*** Backstory ***/
38 /* The year was 2003. Subversion usage was rampant in the world, and
39 there was a rapidly growing issues database to prove it. To make
40 matters worse, svn_repos_dir_delta() had simply outgrown itself.
41 No longer content to simply describe the differences between two
42 trees, the function had been slowly bearing the added
43 responsibility of representing the actions that had been taken to
44 cause those differences -- a burden it was never meant to bear.
45 Now grown into a twisted mess of razor-sharp metal and glass, and
46 trembling with a sort of momentarily stayed spring force,
47 svn_repos_dir_delta was a timebomb poised for total annihilation of
48 the American Midwest.
50 Subversion needed a change.
52 Changes, in fact. And not just in the literary segue sense. What
53 Subversion desperately needed was a new mechanism solely
54 responsible for replaying repository actions back to some
55 interested party -- to translate and retransmit the contents of the
56 Berkeley 'changes' database file. */
58 /*** Overview ***/
60 /* The filesystem keeps a record of high-level actions that affect the
61 files and directories in itself. The 'changes' table records
62 additions, deletions, textual and property modifications, and so
63 on. The goal of the functions in this file is to examine those
64 change records, and use them to drive an editor interface in such a
65 way as to effectively replay those actions.
67 This is critically different than what svn_repos_dir_delta() was
68 designed to do. That function describes, in the simplest way it
69 can, how to transform one tree into another. It doesn't care
70 whether or not this was the same way a user might have done this
71 transformation. More to the point, it doesn't care if this is how
72 those differences *did* come into being. And it is for this reason
73 that it cannot be relied upon for tasks such as the repository
74 dumpfile-generation code, which is supposed to represent not
75 changes, but actions that cause changes.
77 So, what's the plan here?
79 First, we fetch the changes for a particular revision or
80 transaction. We get these as an array, sorted chronologically.
81 From this array we will build a hash, keyed on the path associated
82 with each change item, and whose values are arrays of changes made
83 to that path, again preserving the chronological ordering.
85 Once our hash is built, we then sort all the keys of the hash (the
86 paths) using a depth-first directory sort routine.
88 Finally, we drive an editor, moving down our list of sorted paths,
89 and manufacturing any intermediate editor calls (directory openings
90 and closures) needed to navigate between each successive path. For
91 each path, we replay the sorted actions that occurred at that path.
93 When we've finished the editor drive, we should have fully replayed
94 the filesystem events that occurred in that revision or transaction
95 (though not necessarily in the same order in which they
96 occurred). */
100 /*** Helper functions. ***/
103 /* Information for an active copy, that is a directory which we are currently
104 working on and which was added with history. */
105 struct copy_info
107 /* Destination path. */
108 const char *path;
109 /* Copy source. NULL/invalid if this is an add without history,
110 nested inside an add with history. */
111 const char *copyfrom_path;
112 svn_revnum_t copyfrom_rev;
115 struct path_driver_cb_baton
117 const svn_delta_editor_t *editor;
118 void *edit_baton;
120 /* The root of the revision we're replaying. */
121 svn_fs_root_t *root;
123 /* The root of the previous revision. If this is non-NULL it means that
124 we are supposed to generate props and text deltas relative to it. */
125 svn_fs_root_t *compare_root;
127 apr_hash_t *changed_paths;
129 svn_repos_authz_func_t authz_read_func;
130 void *authz_read_baton;
132 const char *base_path;
133 int base_path_len;
135 svn_revnum_t low_water_mark;
136 /* Stack of active copy operations. */
137 apr_array_header_t *copies;
139 /* The global pool for this replay operation. */
140 apr_pool_t *pool;
143 /* Recursively traverse PATH (as it exists under SOURCE_ROOT) emitting
144 the appropriate editor calls to add it and its children without any
145 history. This is meant to be used when either a subset of the tree
146 has been ignored and we need to copy something from that subset to
147 the part of the tree we do care about, or if a subset of the tree is
148 unavailable because of authz and we need to use it as the source of
149 a copy. */
150 static svn_error_t *
151 add_subdir(svn_fs_root_t *source_root,
152 svn_fs_root_t *target_root,
153 const svn_delta_editor_t *editor,
154 void *edit_baton,
155 const char *path,
156 void *parent_baton,
157 const char *source_path,
158 svn_repos_authz_func_t authz_read_func,
159 void *authz_read_baton,
160 apr_hash_t *changed_paths,
161 apr_pool_t *pool,
162 void **dir_baton)
164 apr_pool_t *subpool = svn_pool_create(pool);
165 apr_hash_index_t *hi, *phi;
166 apr_hash_t *dirents;
167 apr_hash_t *props;
169 SVN_ERR(editor->add_directory(path, parent_baton, NULL,
170 SVN_INVALID_REVNUM, pool, dir_baton));
172 SVN_ERR(svn_fs_node_proplist(&props, target_root, path, pool));
174 for (phi = apr_hash_first(pool, props); phi; phi = apr_hash_next(phi))
176 const void *key;
177 void *val;
179 svn_pool_clear(subpool);
181 apr_hash_this(phi, &key, NULL, &val);
183 SVN_ERR(editor->change_dir_prop(*dir_baton,
184 key,
185 val,
186 subpool));
189 /* We have to get the dirents from the source path, not the target,
190 because we want nested copies from *readable* paths to be handled by
191 path_driver_cb_func, not add_subdir (in order to preserve history). */
192 SVN_ERR(svn_fs_dir_entries(&dirents, source_root, source_path, pool));
194 for (hi = apr_hash_first(pool, dirents); hi; hi = apr_hash_next(hi))
196 svn_fs_path_change_t *change;
197 svn_boolean_t readable = TRUE;
198 svn_fs_dirent_t *dent;
199 const char *new_path;
200 void *val;
202 svn_pool_clear(subpool);
204 apr_hash_this(hi, NULL, NULL, &val);
206 dent = val;
208 new_path = svn_path_join(path, dent->name, subpool);
210 /* If a file or subdirectory of the copied directory is listed as a
211 changed path (because it was modified after the copy but before the
212 commit), we remove it from the changed_paths hash so that future
213 calls to path_driver_cb_func will ignore it. */
214 change = apr_hash_get(changed_paths, new_path, APR_HASH_KEY_STRING);
215 if (change)
217 apr_hash_set(changed_paths, new_path, APR_HASH_KEY_STRING, NULL);
218 /* If it's a delete, skip this entry. */
219 if (change->change_kind == svn_fs_path_change_delete)
220 continue;
223 if (authz_read_func)
224 SVN_ERR(authz_read_func(&readable, target_root, new_path,
225 authz_read_baton, pool));
227 if (! readable)
228 continue;
230 if (dent->kind == svn_node_dir)
232 void *new_dir_baton;
234 SVN_ERR(add_subdir(source_root, target_root, editor, edit_baton,
235 new_path, *dir_baton,
236 svn_path_join(source_path, dent->name,
237 subpool),
238 authz_read_func, authz_read_baton,
239 changed_paths, subpool, &new_dir_baton));
241 SVN_ERR(editor->close_directory(new_dir_baton, subpool));
243 else if (dent->kind == svn_node_file)
245 svn_txdelta_window_handler_t delta_handler;
246 void *delta_handler_baton, *file_baton;
247 svn_txdelta_stream_t *delta_stream;
248 unsigned char digest[APR_MD5_DIGESTSIZE];
250 SVN_ERR(editor->add_file(new_path, *dir_baton, NULL,
251 SVN_INVALID_REVNUM, pool, &file_baton));
253 SVN_ERR(svn_fs_node_proplist(&props, target_root, new_path, subpool));
255 for (phi = apr_hash_first(pool, props);
256 phi;
257 phi = apr_hash_next(phi))
259 const void *key;
261 apr_hash_this(phi, &key, NULL, &val);
263 SVN_ERR(editor->change_file_prop(file_baton,
264 key,
265 val,
266 subpool));
269 SVN_ERR(editor->apply_textdelta(file_baton, NULL, pool,
270 &delta_handler,
271 &delta_handler_baton));
273 SVN_ERR(svn_fs_get_file_delta_stream
274 (&delta_stream, NULL, NULL, target_root, new_path,
275 pool));
277 SVN_ERR(svn_txdelta_send_txstream(delta_stream,
278 delta_handler,
279 delta_handler_baton,
280 pool));
282 SVN_ERR(svn_fs_file_md5_checksum(digest,
283 target_root,
284 new_path,
285 pool));
286 SVN_ERR(editor->close_file(file_baton,
287 svn_md5_digest_to_cstring(digest, pool),
288 pool));
290 else
291 abort();
294 svn_pool_destroy(subpool);
296 return SVN_NO_ERROR;
299 static svn_boolean_t
300 is_within_base_path(const char *path, const char *base_path, int base_len)
302 if (base_path[0] == '\0')
303 return TRUE;
305 if (strncmp(base_path, path, base_len) == 0
306 && (path[base_len] == '/' || path[base_len] == '\0'))
307 return TRUE;
309 return FALSE;
312 static svn_error_t *
313 path_driver_cb_func(void **dir_baton,
314 void *parent_baton,
315 void *callback_baton,
316 const char *path,
317 apr_pool_t *pool)
319 struct path_driver_cb_baton *cb = callback_baton;
320 const svn_delta_editor_t *editor = cb->editor;
321 void *edit_baton = cb->edit_baton;
322 svn_fs_root_t *root = cb->root;
323 svn_fs_path_change_t *change;
324 svn_boolean_t do_add = FALSE, do_delete = FALSE;
325 svn_node_kind_t kind;
326 void *file_baton = NULL;
327 const char *copyfrom_path = NULL;
328 const char *real_copyfrom_path = NULL;
329 svn_revnum_t copyfrom_rev;
330 svn_boolean_t src_readable = TRUE;
331 svn_fs_root_t *source_root = cb->compare_root;
332 const char *source_path = source_root ? path : NULL;
333 const char *base_path = cb->base_path;
334 int base_path_len = cb->base_path_len;
336 *dir_baton = NULL;
338 /* First, flush the copies stack so it only contains ancestors of path. */
339 while (cb->copies->nelts > 0
340 && ! svn_path_is_ancestor(APR_ARRAY_IDX(cb->copies,
341 cb->copies->nelts - 1,
342 struct copy_info).path,
343 path))
344 cb->copies->nelts--;
346 change = apr_hash_get(cb->changed_paths, path, APR_HASH_KEY_STRING);
347 if (! change)
349 /* This can only happen if the path was removed from cb->changed_paths
350 by an earlier call to add_subdir, which means the path was already
351 handled and we should simply ignore it. */
352 return SVN_NO_ERROR;
354 switch (change->change_kind)
356 case svn_fs_path_change_add:
357 do_add = TRUE;
358 break;
360 case svn_fs_path_change_delete:
361 do_delete = TRUE;
362 break;
364 case svn_fs_path_change_replace:
365 do_add = TRUE;
366 do_delete = TRUE;
367 break;
369 case svn_fs_path_change_modify:
370 default:
371 /* do nothing */
372 break;
375 /* Handle any deletions. */
376 if (do_delete)
377 SVN_ERR(editor->delete_entry(path, SVN_INVALID_REVNUM,
378 parent_baton, pool));
380 /* Fetch the node kind if it makes sense to do so. */
381 if (! do_delete || do_add)
383 SVN_ERR(svn_fs_check_path(&kind, root, path, pool));
384 if ((kind != svn_node_dir) && (kind != svn_node_file))
385 return svn_error_createf
386 (SVN_ERR_FS_NOT_FOUND, NULL,
387 _("Filesystem path '%s' is neither a file nor a directory"), path);
390 /* Handle any adds/opens. */
391 if (do_add)
393 svn_fs_root_t *copyfrom_root = NULL;
394 /* Was this node copied? */
395 SVN_ERR(svn_fs_copied_from(&copyfrom_rev, &copyfrom_path,
396 root, path, pool));
398 if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_rev))
400 SVN_ERR(svn_fs_revision_root(&copyfrom_root,
401 svn_fs_root_fs(root),
402 copyfrom_rev, pool));
404 if (cb->authz_read_func)
406 SVN_ERR(cb->authz_read_func(&src_readable, copyfrom_root,
407 copyfrom_path,
408 cb->authz_read_baton, pool));
412 /* Save away the copyfrom path in case we null it out below. */
413 real_copyfrom_path = copyfrom_path;
414 /* If we have a copyfrom path, and we can't read it or we're just
415 ignoring it, or the copyfrom rev is prior to the low water mark
416 then we just null them out and do a raw add with no history at
417 all. */
418 if (copyfrom_path
419 && (! src_readable
420 || ! is_within_base_path(copyfrom_path + 1, base_path,
421 base_path_len)
422 || cb->low_water_mark > copyfrom_rev))
424 copyfrom_path = NULL;
425 copyfrom_rev = SVN_INVALID_REVNUM;
428 /* Do the right thing based on the path KIND. */
429 if (kind == svn_node_dir)
431 /* If this is a copy, but we can't represent it as such,
432 then we just do a recursive add of the source path
433 contents. */
434 if (real_copyfrom_path && ! copyfrom_path)
436 SVN_ERR(add_subdir(copyfrom_root, root, editor, edit_baton,
437 path, parent_baton, real_copyfrom_path,
438 cb->authz_read_func, cb->authz_read_baton,
439 cb->changed_paths, pool, dir_baton));
441 else
443 SVN_ERR(editor->add_directory(path, parent_baton,
444 copyfrom_path, copyfrom_rev,
445 pool, dir_baton));
448 else
450 SVN_ERR(editor->add_file(path, parent_baton, copyfrom_path,
451 copyfrom_rev, pool, &file_baton));
454 /* If we represent this as a copy... */
455 if (copyfrom_path)
457 /* If it is a directory, make sure descendants get the correct
458 delta source by remembering that we are operating inside a
459 (possibly nested) copy operation. */
460 if (kind == svn_node_dir)
462 struct copy_info *info = &APR_ARRAY_PUSH(cb->copies,
463 struct copy_info);
464 info->path = apr_pstrdup(cb->pool, path);
465 info->copyfrom_path = apr_pstrdup(cb->pool, copyfrom_path);
466 info->copyfrom_rev = copyfrom_rev;
469 /* Save the source so that we can use it later, when we
470 need to generate text and prop deltas. */
471 source_root = copyfrom_root;
472 source_path = copyfrom_path;
474 else
475 /* Else, we are an add without history... */
477 /* If an ancestor is added with history, we need to forget about
478 that here, go on with life and repeat all the mistakes of our
479 past... */
480 if (kind == svn_node_dir && cb->copies->nelts > 0)
482 struct copy_info *info = &APR_ARRAY_PUSH(cb->copies,
483 struct copy_info);
484 info->path = apr_pstrdup(cb->pool, path);
485 info->copyfrom_path = NULL;
486 info->copyfrom_rev = SVN_INVALID_REVNUM;
488 source_root = NULL;
489 source_path = NULL;
492 else if (! do_delete)
494 /* Do the right thing based on the path KIND (and the presence
495 of a PARENT_BATON). */
496 if (kind == svn_node_dir)
498 if (parent_baton)
500 SVN_ERR(editor->open_directory(path, parent_baton,
501 SVN_INVALID_REVNUM,
502 pool, dir_baton));
504 else
506 SVN_ERR(editor->open_root(edit_baton, SVN_INVALID_REVNUM,
507 pool, dir_baton));
510 else
512 SVN_ERR(editor->open_file(path, parent_baton, SVN_INVALID_REVNUM,
513 pool, &file_baton));
515 /* If we are inside an add with history, we need to adjust the
516 delta source. */
517 if (cb->copies->nelts > 0)
519 struct copy_info *info = &APR_ARRAY_IDX(cb->copies,
520 cb->copies->nelts - 1,
521 struct copy_info);
522 if (info->copyfrom_path)
524 SVN_ERR(svn_fs_revision_root(&source_root,
525 svn_fs_root_fs(root),
526 info->copyfrom_rev, pool));
527 source_path = svn_path_join(info->copyfrom_path,
528 svn_path_is_child(info->path, path,
529 pool), pool);
531 else
533 /* This is an add without history, nested inside an
534 add with history. We have no delta source in this case. */
535 source_root = NULL;
536 source_path = NULL;
541 /* Handle property modifications. */
542 if (! do_delete || do_add)
544 if (change->prop_mod)
546 if (cb->compare_root)
548 apr_array_header_t *prop_diffs;
549 apr_hash_t *old_props;
550 apr_hash_t *new_props;
551 int i;
553 if (source_root)
554 SVN_ERR(svn_fs_node_proplist
555 (&old_props, source_root, source_path, pool));
556 else
557 old_props = apr_hash_make(pool);
559 SVN_ERR(svn_fs_node_proplist(&new_props, root, path, pool));
561 SVN_ERR(svn_prop_diffs(&prop_diffs, new_props, old_props,
562 pool));
564 for (i = 0; i < prop_diffs->nelts; ++i)
566 svn_prop_t *pc = &APR_ARRAY_IDX(prop_diffs, i, svn_prop_t);
567 if (kind == svn_node_dir)
568 SVN_ERR(editor->change_dir_prop(*dir_baton, pc->name,
569 pc->value, pool));
570 else if (kind == svn_node_file)
571 SVN_ERR(editor->change_file_prop(file_baton, pc->name,
572 pc->value, pool));
575 else
577 /* Just do a dummy prop change to signal that there are *any*
578 propmods. */
579 if (kind == svn_node_dir)
580 SVN_ERR(editor->change_dir_prop(*dir_baton, "", NULL,
581 pool));
582 else if (kind == svn_node_file)
583 SVN_ERR(editor->change_file_prop(file_baton, "", NULL,
584 pool));
588 /* Handle textual modifications.
590 Note that this needs to happen in the "copy from a file we
591 aren't allowed to see" case since otherwise the caller will
592 have no way to actually get the new file's contents, which
593 they are apparently allowed to see. */
594 if (kind == svn_node_file
595 && (change->text_mod || (real_copyfrom_path && ! copyfrom_path)))
597 svn_txdelta_window_handler_t delta_handler;
598 void *delta_handler_baton;
599 const char *checksum = NULL;
601 if (cb->compare_root && source_root && source_path)
603 unsigned char digest[APR_MD5_DIGESTSIZE];
604 SVN_ERR(svn_fs_file_md5_checksum(digest,
605 source_root,
606 source_path,
607 pool));
608 checksum = svn_md5_digest_to_cstring(digest, pool);
611 SVN_ERR(editor->apply_textdelta(file_baton, checksum, pool,
612 &delta_handler,
613 &delta_handler_baton));
614 if (cb->compare_root)
616 svn_txdelta_stream_t *delta_stream;
618 SVN_ERR(svn_fs_get_file_delta_stream
619 (&delta_stream, source_root, source_path,
620 root, path, pool));
622 SVN_ERR(svn_txdelta_send_txstream(delta_stream,
623 delta_handler,
624 delta_handler_baton,
625 pool));
627 else
628 SVN_ERR(delta_handler(NULL, delta_handler_baton));
632 /* Close the file baton if we opened it. */
633 if (file_baton)
635 unsigned char digest[APR_MD5_DIGESTSIZE];
636 SVN_ERR(svn_fs_file_md5_checksum(digest, root, path, pool));
637 SVN_ERR(editor->close_file(file_baton,
638 svn_md5_digest_to_cstring(digest, pool),
639 pool));
642 return SVN_NO_ERROR;
648 svn_error_t *
649 svn_repos_replay2(svn_fs_root_t *root,
650 const char *base_path,
651 svn_revnum_t low_water_mark,
652 svn_boolean_t send_deltas,
653 const svn_delta_editor_t *editor,
654 void *edit_baton,
655 svn_repos_authz_func_t authz_read_func,
656 void *authz_read_baton,
657 apr_pool_t *pool)
659 apr_hash_t *fs_changes;
660 apr_hash_t *changed_paths;
661 apr_hash_index_t *hi;
662 apr_array_header_t *paths;
663 struct path_driver_cb_baton cb_baton;
664 int base_path_len;
666 /* Fetch the paths changed under ROOT. */
667 SVN_ERR(svn_fs_paths_changed(&fs_changes, root, pool));
669 if (! base_path)
670 base_path = "";
671 else if (base_path[0] == '/')
672 ++base_path;
674 base_path_len = strlen(base_path);
676 /* Make an array from the keys of our CHANGED_PATHS hash, and copy
677 the values into a new hash whose keys have no leading slashes. */
678 paths = apr_array_make(pool, apr_hash_count(fs_changes),
679 sizeof(const char *));
680 changed_paths = apr_hash_make(pool);
681 for (hi = apr_hash_first(pool, fs_changes); hi; hi = apr_hash_next(hi))
683 const void *key;
684 void *val;
685 apr_ssize_t keylen;
686 const char *path;
687 svn_fs_path_change_t *change;
688 svn_boolean_t allowed = TRUE;
690 apr_hash_this(hi, &key, &keylen, &val);
691 path = key;
692 change = val;
694 if (authz_read_func)
695 SVN_ERR(authz_read_func(&allowed, root, path, authz_read_baton,
696 pool));
698 if (allowed)
700 if (path[0] == '/')
702 path++;
703 keylen--;
706 /* If the base_path doesn't match the top directory of this path
707 we don't want anything to do with it... */
708 if (is_within_base_path(path, base_path, base_path_len))
710 APR_ARRAY_PUSH(paths, const char *) = path;
711 apr_hash_set(changed_paths, path, keylen, change);
716 /* If we were not given a low water mark, assume that everything is there,
717 all the way back to revision 0. */
718 if (! SVN_IS_VALID_REVNUM(low_water_mark))
719 low_water_mark = 0;
721 /* Initialize our callback baton. */
722 cb_baton.editor = editor;
723 cb_baton.edit_baton = edit_baton;
724 cb_baton.root = root;
725 cb_baton.changed_paths = changed_paths;
726 cb_baton.authz_read_func = authz_read_func;
727 cb_baton.authz_read_baton = authz_read_baton;
728 cb_baton.base_path = base_path;
729 cb_baton.base_path_len = base_path_len;
730 cb_baton.low_water_mark = low_water_mark;
731 cb_baton.compare_root = NULL;
733 if (send_deltas)
735 SVN_ERR(svn_fs_revision_root(&cb_baton.compare_root,
736 svn_fs_root_fs(root),
737 svn_fs_is_revision_root(root)
738 ? svn_fs_revision_root_revision(root) - 1
739 : svn_fs_txn_root_base_revision(root),
740 pool));
743 cb_baton.copies = apr_array_make(pool, 4, sizeof(struct copy_info));
744 cb_baton.pool = pool;
746 /* Determine the revision to use throughout the edit, and call
747 EDITOR's set_target_revision() function. */
748 if (svn_fs_is_revision_root(root))
750 svn_revnum_t revision = svn_fs_revision_root_revision(root);
751 SVN_ERR(editor->set_target_revision(edit_baton, revision, pool));
754 /* Call the path-based editor driver. */
755 SVN_ERR(svn_delta_path_driver(editor, edit_baton,
756 SVN_INVALID_REVNUM, paths,
757 path_driver_cb_func, &cb_baton, pool));
759 return SVN_NO_ERROR;
762 svn_error_t *
763 svn_repos_replay(svn_fs_root_t *root,
764 const svn_delta_editor_t *editor,
765 void *edit_baton,
766 apr_pool_t *pool)
768 return svn_repos_replay2(root,
769 "" /* the whole tree */,
770 SVN_INVALID_REVNUM, /* no low water mark */
771 FALSE /* no text deltas */,
772 editor, edit_baton,
773 NULL /* no authz func */,
774 NULL /* no authz baton */,
775 pool);