2 * ra_plugin.c : the main RA module for local repository access
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
13 * http://www.apache.org/licenses/LICENSE-2.0
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
21 * ====================================================================
28 #include "svn_delta.h"
29 #include "svn_repos.h"
30 #include "svn_pools.h"
32 #include "svn_props.h"
33 #include "svn_mergeinfo.h"
35 #include "svn_version.h"
36 #include "svn_cache_config.h"
38 #include "svn_private_config.h"
39 #include "../libsvn_ra/ra_loader.h"
40 #include "private/svn_mergeinfo_private.h"
41 #include "private/svn_repos_private.h"
42 #include "private/svn_fspath.h"
43 #include "private/svn_atomic.h"
45 #define APR_WANT_STRFUNC
48 /*----------------------------------------------------------------*/
50 /*** Miscellaneous helper functions ***/
53 /* Pool cleanup handler: ensure that the access descriptor of the
54 filesystem (svn_fs_t *) DATA is set to NULL. */
56 cleanup_access(void *data)
61 serr = svn_fs_set_access(fs, NULL);
65 apr_status_t apr_err = serr->apr_err;
66 svn_error_clear(serr);
74 /* Fetch a username for use with SESSION, and store it in SESSION->username.
76 * Allocate the username in SESSION->pool. Use SCRATCH_POOL for temporary
79 get_username(svn_ra_session_t *session,
80 apr_pool_t *scratch_pool)
82 svn_ra_local__session_baton_t *sess = session->priv;
84 /* If we've already found the username don't ask for it again. */
87 /* Get a username somehow, so we have some svn:author property to
88 attach to a commit. */
89 if (sess->callbacks->auth_baton)
92 svn_auth_cred_username_t *username_creds;
93 svn_auth_iterstate_t *iterstate;
95 SVN_ERR(svn_auth_first_credentials(&creds, &iterstate,
96 SVN_AUTH_CRED_USERNAME,
97 sess->uuid, /* realmstring */
98 sess->callbacks->auth_baton,
101 /* No point in calling next_creds(), since that assumes that the
102 first_creds() somehow failed to authenticate. But there's no
103 challenge going on, so we use whatever creds we get back on
105 username_creds = creds;
106 if (username_creds && username_creds->username)
108 sess->username = apr_pstrdup(session->pool,
109 username_creds->username);
110 svn_error_clear(svn_auth_save_credentials(iterstate,
120 /* If we have a real username, attach it to the filesystem so that it can
121 be used to validate locks. Even if there already is a user context
122 associated, it may contain irrelevant lock tokens, so always create a new.
126 svn_fs_access_t *access_ctx;
128 SVN_ERR(svn_fs_create_access(&access_ctx, sess->username,
130 SVN_ERR(svn_fs_set_access(sess->fs, access_ctx));
132 /* Make sure this context is disassociated when the pool gets
134 apr_pool_cleanup_register(session->pool, sess->fs, cleanup_access,
135 apr_pool_cleanup_null);
141 /* Implements an svn_atomic__init_once callback. Sets the FSFS memory
144 cache_init(void *baton, apr_pool_t *pool)
146 apr_hash_t *config_hash = baton;
147 svn_config_t *config = NULL;
148 const char *memory_cache_size_str;
151 config = svn_hash_gets(config_hash, SVN_CONFIG_CATEGORY_CONFIG);
152 svn_config_get(config, &memory_cache_size_str, SVN_CONFIG_SECTION_MISCELLANY,
153 SVN_CONFIG_OPTION_MEMORY_CACHE_SIZE, NULL);
154 if (memory_cache_size_str)
156 apr_uint64_t memory_cache_size;
157 svn_cache_config_t settings = *svn_cache_config_get();
159 SVN_ERR(svn_error_quick_wrap(svn_cstring_atoui64(&memory_cache_size,
160 memory_cache_size_str),
161 _("memory-cache-size invalid")));
162 settings.cache_size = 1024 * 1024 * memory_cache_size;
163 svn_cache_config_set(&settings);
169 /*----------------------------------------------------------------*/
171 /*** The reporter vtable needed by do_update() and friends ***/
173 typedef struct reporter_baton_t
175 svn_ra_local__session_baton_t *sess;
182 make_reporter_baton(svn_ra_local__session_baton_t *sess,
186 reporter_baton_t *rbaton = apr_palloc(pool, sizeof(*rbaton));
188 rbaton->report_baton = report_baton;
194 reporter_set_path(void *reporter_baton,
196 svn_revnum_t revision,
198 svn_boolean_t start_empty,
199 const char *lock_token,
202 reporter_baton_t *rbaton = reporter_baton;
203 return svn_repos_set_path3(rbaton->report_baton, path,
204 revision, depth, start_empty, lock_token, pool);
209 reporter_delete_path(void *reporter_baton,
213 reporter_baton_t *rbaton = reporter_baton;
214 return svn_repos_delete_path(rbaton->report_baton, path, pool);
219 reporter_link_path(void *reporter_baton,
222 svn_revnum_t revision,
224 svn_boolean_t start_empty,
225 const char *lock_token,
228 reporter_baton_t *rbaton = reporter_baton;
229 const char *repos_url = rbaton->sess->repos_url;
230 const char *relpath = svn_uri_skip_ancestor(repos_url, url, pool);
234 return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
236 "is not the same repository as\n"
237 "'%s'"), url, rbaton->sess->repos_url);
239 /* Convert the relpath to an fspath */
240 if (relpath[0] == '\0')
243 fs_path = apr_pstrcat(pool, "/", relpath, (char *)NULL);
245 return svn_repos_link_path3(rbaton->report_baton, path, fs_path, revision,
246 depth, start_empty, lock_token, pool);
251 reporter_finish_report(void *reporter_baton,
254 reporter_baton_t *rbaton = reporter_baton;
255 return svn_repos_finish_report(rbaton->report_baton, pool);
260 reporter_abort_report(void *reporter_baton,
263 reporter_baton_t *rbaton = reporter_baton;
264 return svn_repos_abort_report(rbaton->report_baton, pool);
268 static const svn_ra_reporter3_t ra_local_reporter =
271 reporter_delete_path,
273 reporter_finish_report,
274 reporter_abort_report
280 * Wrap a cancellation editor using SESSION's cancellation function around
281 * the supplied EDITOR. ### Some callers (via svn_ra_do_update2() etc.)
282 * don't appear to know that we do this, and are supplying an editor that
283 * they have already wrapped with the same cancellation editor, so it ends
286 * Allocate @a *reporter and @a *report_baton in @a result_pool. Use
287 * @a scratch_pool for temporary allocations.
290 make_reporter(svn_ra_session_t *session,
291 const svn_ra_reporter3_t **reporter,
293 svn_revnum_t revision,
295 const char *other_url,
296 svn_boolean_t text_deltas,
298 svn_boolean_t send_copyfrom_args,
299 svn_boolean_t ignore_ancestry,
300 const svn_delta_editor_t *editor,
302 apr_pool_t *result_pool,
303 apr_pool_t *scratch_pool)
305 svn_ra_local__session_baton_t *sess = session->priv;
307 const char *other_fs_path = NULL;
309 /* Get the HEAD revision if one is not supplied. */
310 if (! SVN_IS_VALID_REVNUM(revision))
311 SVN_ERR(svn_fs_youngest_rev(&revision, sess->fs, scratch_pool));
313 /* If OTHER_URL was provided, validate it and convert it into a
314 regular filesystem path. */
317 const char *other_relpath
318 = svn_uri_skip_ancestor(sess->repos_url, other_url, scratch_pool);
320 /* Sanity check: the other_url better be in the same repository as
321 the original session url! */
323 return svn_error_createf
324 (SVN_ERR_RA_ILLEGAL_URL, NULL,
326 "is not the same repository as\n"
327 "'%s'"), other_url, sess->repos_url);
329 other_fs_path = apr_pstrcat(scratch_pool, "/", other_relpath,
333 /* Pass back our reporter */
334 *reporter = &ra_local_reporter;
336 SVN_ERR(get_username(session, scratch_pool));
339 SVN_ERR(svn_delta_get_cancellation_editor(sess->callbacks->cancel_func,
340 sess->callback_baton,
347 /* Build a reporter baton. */
348 SVN_ERR(svn_repos_begin_report3(&rbaton,
362 1024 * 1024, /* process-local transfers
366 /* Wrap the report baton given us by the repos layer with our own
368 *report_baton = make_reporter_baton(sess, rbaton, result_pool);
374 /*----------------------------------------------------------------*/
376 /*** Deltification stuff for get_commit_editor() ***/
378 struct deltify_etc_baton
380 svn_fs_t *fs; /* the fs to deltify in */
381 svn_repos_t *repos; /* repos for unlocking */
382 const char *fspath_base; /* fs-path part of split session URL */
384 apr_hash_t *lock_tokens; /* tokens to unlock, if any */
386 svn_commit_callback2_t commit_cb; /* the original callback */
387 void *commit_baton; /* the original callback's baton */
390 /* This implements 'svn_commit_callback_t'. Its invokes the original
391 (wrapped) callback, but also does deltification on the new revision and
392 possibly unlocks committed paths.
393 BATON is 'struct deltify_etc_baton *'. */
395 deltify_etc(const svn_commit_info_t *commit_info,
397 apr_pool_t *scratch_pool)
399 struct deltify_etc_baton *deb = baton;
400 svn_error_t *err1 = SVN_NO_ERROR;
403 /* Invoke the original callback first, in case someone's waiting to
404 know the revision number so they can go off and annotate an
405 issue or something. */
407 err1 = deb->commit_cb(commit_info, deb->commit_baton, scratch_pool);
409 /* Maybe unlock the paths. */
410 if (deb->lock_tokens)
412 apr_pool_t *iterpool = svn_pool_create(scratch_pool);
413 apr_hash_index_t *hi;
415 for (hi = apr_hash_first(scratch_pool, deb->lock_tokens); hi;
416 hi = apr_hash_next(hi))
418 const void *relpath = svn__apr_hash_index_key(hi);
419 const char *token = svn__apr_hash_index_val(hi);
422 svn_pool_clear(iterpool);
424 fspath = svn_fspath__join(deb->fspath_base, relpath, iterpool);
426 /* We may get errors here if the lock was broken or stolen
427 after the commit succeeded. This is fine and should be
429 svn_error_clear(svn_repos_fs_unlock(deb->repos, fspath, token,
433 svn_pool_destroy(iterpool);
436 /* But, deltification shouldn't be stopped just because someone's
437 random callback failed, so proceed unconditionally on to
439 err2 = svn_fs_deltify_revision(deb->fs, commit_info->revision, scratch_pool);
441 return svn_error_compose_create(err1, err2);
445 /* If LOCK_TOKENS is not NULL, then copy all tokens into the access context
446 of FS. The tokens' paths will be prepended with FSPATH_BASE.
448 ACCESS_POOL must match (or exceed) the lifetime of the access context
449 that was associated with FS. Typically, this is the session pool.
451 Temporary allocations are made in SCRATCH_POOL. */
453 apply_lock_tokens(svn_fs_t *fs,
454 const char *fspath_base,
455 apr_hash_t *lock_tokens,
456 apr_pool_t *access_pool,
457 apr_pool_t *scratch_pool)
461 svn_fs_access_t *access_ctx;
463 SVN_ERR(svn_fs_get_access(&access_ctx, fs));
465 /* If there is no access context, the filesystem will scream if a
469 apr_hash_index_t *hi;
471 /* Note: we have no use for an iterpool here since the data
472 within the loop is copied into ACCESS_POOL. */
474 for (hi = apr_hash_first(scratch_pool, lock_tokens); hi;
475 hi = apr_hash_next(hi))
477 const void *relpath = svn__apr_hash_index_key(hi);
478 const char *token = svn__apr_hash_index_val(hi);
481 /* The path needs to live as long as ACCESS_CTX. */
482 fspath = svn_fspath__join(fspath_base, relpath, access_pool);
484 /* The token must live as long as ACCESS_CTX. */
485 token = apr_pstrdup(access_pool, token);
487 SVN_ERR(svn_fs_access_add_lock_token2(access_ctx, fspath,
497 /*----------------------------------------------------------------*/
499 /*** The RA vtable routines ***/
501 #define RA_LOCAL_DESCRIPTION \
502 N_("Module for accessing a repository on local disk.")
505 svn_ra_local__get_description(void)
507 return _(RA_LOCAL_DESCRIPTION);
510 static const char * const *
511 svn_ra_local__get_schemes(apr_pool_t *pool)
513 static const char *schemes[] = { "file", NULL };
520 * Why is this acceptable? FS warnings used to be used for only
521 * two things: failures to close BDB repositories and failures to
522 * interact with memcached in FSFS (new in 1.6). In 1.5 and earlier,
523 * we did not call svn_fs_set_warning_func in ra_local, which means
524 * that any BDB-closing failure would have led to abort()s; the fact
525 * that this hasn't led to huge hues and cries makes it seem likely
526 * that this just doesn't happen that often, at least not through
527 * ra_local. And as far as memcached goes, it seems unlikely that
528 * somebody is going to go through the trouble of setting up and
529 * running memcached servers but then use ra_local access. So we
530 * ignore errors here, so that memcached can use the FS warnings API
531 * without crashing ra_local.
534 ignore_warnings(void *baton,
538 SVN_DBG(("Ignoring FS warning %d\n", err ? err->apr_err : 0));
544 svn_ra_local__open(svn_ra_session_t *session,
545 const char **corrected_url,
546 const char *repos_URL,
547 const svn_ra_callbacks2_t *callbacks,
548 void *callback_baton,
552 svn_ra_local__session_baton_t *sess;
554 static volatile svn_atomic_t cache_init_state = 0;
556 /* Initialise the FSFS memory cache size. We can only do this once
557 so one CONFIG will win the race and all others will be ignored
559 SVN_ERR(svn_atomic__init_once(&cache_init_state, cache_init, config, pool));
561 /* We don't support redirections in ra-local. */
563 *corrected_url = NULL;
565 /* Allocate and stash the session_sess args we have already. */
566 sess = apr_pcalloc(pool, sizeof(*sess));
567 sess->callbacks = callbacks;
568 sess->callback_baton = callback_baton;
570 /* Look through the URL, figure out which part points to the
571 repository, and which part is the path *within* the
573 SVN_ERR_W(svn_ra_local__split_URL(&(sess->repos),
578 _("Unable to open an ra_local session to URL"));
579 sess->fs_path = svn_stringbuf_create(fs_path, session->pool);
581 /* Cache the filesystem object from the repos here for
583 sess->fs = svn_repos_fs(sess->repos);
585 /* Ignore FS warnings. */
586 svn_fs_set_warning_func(sess->fs, ignore_warnings, NULL);
588 /* Cache the repository UUID as well */
589 SVN_ERR(svn_fs_get_uuid(sess->fs, &sess->uuid, session->pool));
591 /* Be sure username is NULL so we know to look it up / ask for it */
592 sess->username = NULL;
594 session->priv = sess;
599 svn_ra_local__reparent(svn_ra_session_t *session,
603 svn_ra_local__session_baton_t *sess = session->priv;
604 const char *relpath = svn_uri_skip_ancestor(sess->repos_url, url, pool);
606 /* If the new URL isn't the same as our repository root URL, then
607 let's ensure that it's some child of it. */
609 return svn_error_createf
610 (SVN_ERR_RA_ILLEGAL_URL, NULL,
611 _("URL '%s' is not a child of the session's repository root "
612 "URL '%s'"), url, sess->repos_url);
614 /* Update our FS_PATH sess member to point to our new
615 relative-URL-turned-absolute-filesystem-path. */
616 svn_stringbuf_set(sess->fs_path,
617 svn_fspath__canonicalize(relpath, pool));
623 svn_ra_local__get_session_url(svn_ra_session_t *session,
627 svn_ra_local__session_baton_t *sess = session->priv;
628 *url = svn_path_url_add_component2(sess->repos_url,
629 sess->fs_path->data + 1,
635 svn_ra_local__get_latest_revnum(svn_ra_session_t *session,
636 svn_revnum_t *latest_revnum,
639 svn_ra_local__session_baton_t *sess = session->priv;
640 return svn_fs_youngest_rev(latest_revnum, sess->fs, pool);
644 svn_ra_local__get_file_revs(svn_ra_session_t *session,
648 svn_boolean_t include_merged_revisions,
649 svn_file_rev_handler_t handler,
653 svn_ra_local__session_baton_t *sess = session->priv;
654 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
655 return svn_repos_get_file_revs2(sess->repos, abs_path, start, end,
656 include_merged_revisions, NULL, NULL,
657 handler, handler_baton, pool);
661 svn_ra_local__get_dated_revision(svn_ra_session_t *session,
662 svn_revnum_t *revision,
666 svn_ra_local__session_baton_t *sess = session->priv;
667 return svn_repos_dated_revision(revision, sess->repos, tm, pool);
672 svn_ra_local__change_rev_prop(svn_ra_session_t *session,
675 const svn_string_t *const *old_value_p,
676 const svn_string_t *value,
679 svn_ra_local__session_baton_t *sess = session->priv;
681 SVN_ERR(get_username(session, pool));
682 return svn_repos_fs_change_rev_prop4(sess->repos, rev, sess->username,
683 name, old_value_p, value, TRUE, TRUE,
688 svn_ra_local__get_uuid(svn_ra_session_t *session,
692 svn_ra_local__session_baton_t *sess = session->priv;
698 svn_ra_local__get_repos_root(svn_ra_session_t *session,
702 svn_ra_local__session_baton_t *sess = session->priv;
703 *url = sess->repos_url;
708 svn_ra_local__rev_proplist(svn_ra_session_t *session,
713 svn_ra_local__session_baton_t *sess = session->priv;
714 return svn_repos_fs_revision_proplist(props, sess->repos, rev,
719 svn_ra_local__rev_prop(svn_ra_session_t *session,
722 svn_string_t **value,
725 svn_ra_local__session_baton_t *sess = session->priv;
726 return svn_repos_fs_revision_prop(value, sess->repos, rev, name,
731 svn_ra_local__get_commit_editor(svn_ra_session_t *session,
732 const svn_delta_editor_t **editor,
734 apr_hash_t *revprop_table,
735 svn_commit_callback2_t callback,
736 void *callback_baton,
737 apr_hash_t *lock_tokens,
738 svn_boolean_t keep_locks,
741 svn_ra_local__session_baton_t *sess = session->priv;
742 struct deltify_etc_baton *deb = apr_palloc(pool, sizeof(*deb));
744 /* Prepare the baton for deltify_etc() */
746 deb->repos = sess->repos;
747 deb->fspath_base = sess->fs_path->data;
749 deb->lock_tokens = lock_tokens;
751 deb->lock_tokens = NULL;
752 deb->commit_cb = callback;
753 deb->commit_baton = callback_baton;
755 SVN_ERR(get_username(session, pool));
757 /* If there are lock tokens to add, do so. */
758 SVN_ERR(apply_lock_tokens(sess->fs, sess->fs_path->data, lock_tokens,
759 session->pool, pool));
761 /* Copy the revprops table so we can add the username. */
762 revprop_table = apr_hash_copy(pool, revprop_table);
763 svn_hash_sets(revprop_table, SVN_PROP_REVISION_AUTHOR,
764 svn_string_create(sess->username, pool));
765 svn_hash_sets(revprop_table, SVN_PROP_TXN_CLIENT_COMPAT_VERSION,
766 svn_string_create(SVN_VER_NUMBER, pool));
768 /* Get the repos commit-editor */
769 return svn_repos_get_commit_editor5
770 (editor, edit_baton, sess->repos, NULL,
771 svn_path_uri_decode(sess->repos_url, pool), sess->fs_path->data,
772 revprop_table, deltify_etc, deb, NULL, NULL, pool);
777 svn_ra_local__get_mergeinfo(svn_ra_session_t *session,
778 svn_mergeinfo_catalog_t *catalog,
779 const apr_array_header_t *paths,
780 svn_revnum_t revision,
781 svn_mergeinfo_inheritance_t inherit,
782 svn_boolean_t include_descendants,
785 svn_ra_local__session_baton_t *sess = session->priv;
786 svn_mergeinfo_catalog_t tmp_catalog;
788 apr_array_header_t *abs_paths =
789 apr_array_make(pool, 0, sizeof(const char *));
791 for (i = 0; i < paths->nelts; i++)
793 const char *relative_path = APR_ARRAY_IDX(paths, i, const char *);
794 APR_ARRAY_PUSH(abs_paths, const char *) =
795 svn_fspath__join(sess->fs_path->data, relative_path, pool);
798 SVN_ERR(svn_repos_fs_get_mergeinfo(&tmp_catalog, sess->repos, abs_paths,
799 revision, inherit, include_descendants,
801 if (apr_hash_count(tmp_catalog) > 0)
802 SVN_ERR(svn_mergeinfo__remove_prefix_from_catalog(catalog,
814 svn_ra_local__do_update(svn_ra_session_t *session,
815 const svn_ra_reporter3_t **reporter,
817 svn_revnum_t update_revision,
818 const char *update_target,
820 svn_boolean_t send_copyfrom_args,
821 svn_boolean_t ignore_ancestry,
822 const svn_delta_editor_t *update_editor,
824 apr_pool_t *result_pool,
825 apr_pool_t *scratch_pool)
827 return make_reporter(session,
839 result_pool, scratch_pool);
844 svn_ra_local__do_switch(svn_ra_session_t *session,
845 const svn_ra_reporter3_t **reporter,
847 svn_revnum_t update_revision,
848 const char *update_target,
850 const char *switch_url,
851 svn_boolean_t send_copyfrom_args,
852 svn_boolean_t ignore_ancestry,
853 const svn_delta_editor_t *update_editor,
855 apr_pool_t *result_pool,
856 apr_pool_t *scratch_pool)
858 return make_reporter(session,
864 TRUE /* text_deltas */,
870 result_pool, scratch_pool);
875 svn_ra_local__do_status(svn_ra_session_t *session,
876 const svn_ra_reporter3_t **reporter,
878 const char *status_target,
879 svn_revnum_t revision,
881 const svn_delta_editor_t *status_editor,
885 return make_reporter(session,
902 svn_ra_local__do_diff(svn_ra_session_t *session,
903 const svn_ra_reporter3_t **reporter,
905 svn_revnum_t update_revision,
906 const char *update_target,
908 svn_boolean_t ignore_ancestry,
909 svn_boolean_t text_deltas,
910 const char *switch_url,
911 const svn_delta_editor_t *update_editor,
915 return make_reporter(session,
933 svn_ra_local__session_baton_t *sess;
934 svn_log_entry_receiver_t real_cb;
939 log_receiver_wrapper(void *baton,
940 svn_log_entry_t *log_entry,
943 struct log_baton *b = baton;
944 svn_ra_local__session_baton_t *sess = b->sess;
946 if (sess->callbacks->cancel_func)
947 SVN_ERR((sess->callbacks->cancel_func)(sess->callback_baton));
949 /* For consistency with the other RA layers, replace an empty
950 changed-paths hash with a NULL one.
952 ### Should this be done by svn_ra_get_log2() instead, then? */
953 if ((log_entry->changed_paths2)
954 && (apr_hash_count(log_entry->changed_paths2) == 0))
956 log_entry->changed_paths = NULL;
957 log_entry->changed_paths2 = NULL;
960 return b->real_cb(b->real_baton, log_entry, pool);
965 svn_ra_local__get_log(svn_ra_session_t *session,
966 const apr_array_header_t *paths,
970 svn_boolean_t discover_changed_paths,
971 svn_boolean_t strict_node_history,
972 svn_boolean_t include_merged_revisions,
973 const apr_array_header_t *revprops,
974 svn_log_entry_receiver_t receiver,
975 void *receiver_baton,
978 svn_ra_local__session_baton_t *sess = session->priv;
980 apr_array_header_t *abs_paths =
981 apr_array_make(pool, 0, sizeof(const char *));
987 for (i = 0; i < paths->nelts; i++)
989 const char *relative_path = APR_ARRAY_IDX(paths, i, const char *);
990 APR_ARRAY_PUSH(abs_paths, const char *) =
991 svn_fspath__join(sess->fs_path->data, relative_path, pool);
995 lb.real_cb = receiver;
996 lb.real_baton = receiver_baton;
998 receiver = log_receiver_wrapper;
999 receiver_baton = &lb;
1001 return svn_repos_get_logs4(sess->repos,
1006 discover_changed_paths,
1007 strict_node_history,
1008 include_merged_revisions,
1017 static svn_error_t *
1018 svn_ra_local__do_check_path(svn_ra_session_t *session,
1020 svn_revnum_t revision,
1021 svn_node_kind_t *kind,
1024 svn_ra_local__session_baton_t *sess = session->priv;
1025 svn_fs_root_t *root;
1026 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1028 if (! SVN_IS_VALID_REVNUM(revision))
1029 SVN_ERR(svn_fs_youngest_rev(&revision, sess->fs, pool));
1030 SVN_ERR(svn_fs_revision_root(&root, sess->fs, revision, pool));
1031 return svn_fs_check_path(kind, root, abs_path, pool);
1035 static svn_error_t *
1036 svn_ra_local__stat(svn_ra_session_t *session,
1038 svn_revnum_t revision,
1039 svn_dirent_t **dirent,
1042 svn_ra_local__session_baton_t *sess = session->priv;
1043 svn_fs_root_t *root;
1044 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1046 if (! SVN_IS_VALID_REVNUM(revision))
1047 SVN_ERR(svn_fs_youngest_rev(&revision, sess->fs, pool));
1048 SVN_ERR(svn_fs_revision_root(&root, sess->fs, revision, pool));
1050 return svn_repos_stat(dirent, root, abs_path, pool);
1056 static svn_error_t *
1057 get_node_props(apr_hash_t **props,
1058 apr_array_header_t **inherited_props,
1059 svn_ra_local__session_baton_t *sess,
1060 svn_fs_root_t *root,
1062 apr_pool_t *result_pool,
1063 apr_pool_t *scratch_pool)
1065 svn_revnum_t cmt_rev;
1066 const char *cmt_date, *cmt_author;
1068 /* Create a hash with props attached to the fs node. */
1071 SVN_ERR(svn_fs_node_proplist(props, root, path, result_pool));
1074 /* Get inherited properties if requested. */
1075 if (inherited_props)
1077 SVN_ERR(svn_repos_fs_get_inherited_props(inherited_props, root, path,
1079 result_pool, scratch_pool));
1082 /* Now add some non-tweakable metadata to the hash as well... */
1086 /* The so-called 'entryprops' with info about CR & friends. */
1087 SVN_ERR(svn_repos_get_committed_info(&cmt_rev, &cmt_date,
1088 &cmt_author, root, path,
1091 svn_hash_sets(*props, SVN_PROP_ENTRY_COMMITTED_REV,
1092 svn_string_createf(result_pool, "%ld", cmt_rev));
1093 svn_hash_sets(*props, SVN_PROP_ENTRY_COMMITTED_DATE, cmt_date ?
1094 svn_string_create(cmt_date, result_pool) :NULL);
1095 svn_hash_sets(*props, SVN_PROP_ENTRY_LAST_AUTHOR, cmt_author ?
1096 svn_string_create(cmt_author, result_pool) :NULL);
1097 svn_hash_sets(*props, SVN_PROP_ENTRY_UUID,
1098 svn_string_create(sess->uuid, result_pool));
1100 /* We have no 'wcprops' in ra_local, but might someday. */
1103 return SVN_NO_ERROR;
1107 /* Getting just one file. */
1108 static svn_error_t *
1109 svn_ra_local__get_file(svn_ra_session_t *session,
1111 svn_revnum_t revision,
1112 svn_stream_t *stream,
1113 svn_revnum_t *fetched_rev,
1117 svn_fs_root_t *root;
1118 svn_stream_t *contents;
1119 svn_revnum_t youngest_rev;
1120 svn_ra_local__session_baton_t *sess = session->priv;
1121 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1122 svn_node_kind_t node_kind;
1124 /* Open the revision's root. */
1125 if (! SVN_IS_VALID_REVNUM(revision))
1127 SVN_ERR(svn_fs_youngest_rev(&youngest_rev, sess->fs, pool));
1128 SVN_ERR(svn_fs_revision_root(&root, sess->fs, youngest_rev, pool));
1129 if (fetched_rev != NULL)
1130 *fetched_rev = youngest_rev;
1133 SVN_ERR(svn_fs_revision_root(&root, sess->fs, revision, pool));
1135 SVN_ERR(svn_fs_check_path(&node_kind, root, abs_path, pool));
1136 if (node_kind == svn_node_none)
1138 return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
1139 _("'%s' path not found"), abs_path);
1141 else if (node_kind != svn_node_file)
1143 return svn_error_createf(SVN_ERR_FS_NOT_FILE, NULL,
1144 _("'%s' is not a file"), abs_path);
1149 /* Get a stream representing the file's contents. */
1150 SVN_ERR(svn_fs_file_contents(&contents, root, abs_path, pool));
1152 /* Now push data from the fs stream back at the caller's stream.
1153 Note that this particular RA layer does not computing a
1154 checksum as we go, and confirming it against the repository's
1155 checksum when done. That's because it calls
1156 svn_fs_file_contents() directly, which already checks the
1157 stored checksum, and all we're doing here is writing bytes in
1158 a loop. Truly, Nothing Can Go Wrong :-). But RA layers that
1159 go over a network should confirm the checksum.
1161 Note: we are not supposed to close the passed-in stream, so
1164 SVN_ERR(svn_stream_copy3(contents, svn_stream_disown(stream, pool),
1166 ? sess->callbacks->cancel_func : NULL,
1167 sess->callback_baton,
1171 /* Handle props if requested. */
1173 SVN_ERR(get_node_props(props, NULL, sess, root, abs_path, pool, pool));
1175 return SVN_NO_ERROR;
1180 /* Getting a directory's entries */
1181 static svn_error_t *
1182 svn_ra_local__get_dir(svn_ra_session_t *session,
1183 apr_hash_t **dirents,
1184 svn_revnum_t *fetched_rev,
1187 svn_revnum_t revision,
1188 apr_uint32_t dirent_fields,
1191 svn_fs_root_t *root;
1192 svn_revnum_t youngest_rev;
1193 apr_hash_t *entries;
1194 apr_hash_index_t *hi;
1195 svn_ra_local__session_baton_t *sess = session->priv;
1196 apr_pool_t *subpool;
1197 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1199 /* Open the revision's root. */
1200 if (! SVN_IS_VALID_REVNUM(revision))
1202 SVN_ERR(svn_fs_youngest_rev(&youngest_rev, sess->fs, pool));
1203 SVN_ERR(svn_fs_revision_root(&root, sess->fs, youngest_rev, pool));
1204 if (fetched_rev != NULL)
1205 *fetched_rev = youngest_rev;
1208 SVN_ERR(svn_fs_revision_root(&root, sess->fs, revision, pool));
1212 /* Get the dir's entries. */
1213 SVN_ERR(svn_fs_dir_entries(&entries, root, abs_path, pool));
1215 /* Loop over the fs dirents, and build a hash of general
1217 *dirents = apr_hash_make(pool);
1218 subpool = svn_pool_create(pool);
1219 for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi))
1223 apr_hash_t *prophash;
1224 const char *datestring, *entryname, *fullpath;
1225 svn_fs_dirent_t *fs_entry;
1226 svn_dirent_t *entry = svn_dirent_create(pool);
1228 svn_pool_clear(subpool);
1230 apr_hash_this(hi, &key, NULL, &val);
1231 entryname = (const char *) key;
1232 fs_entry = (svn_fs_dirent_t *) val;
1234 fullpath = svn_dirent_join(abs_path, entryname, subpool);
1236 if (dirent_fields & SVN_DIRENT_KIND)
1239 entry->kind = fs_entry->kind;
1242 if (dirent_fields & SVN_DIRENT_SIZE)
1245 if (entry->kind == svn_node_dir)
1248 SVN_ERR(svn_fs_file_length(&(entry->size), root,
1249 fullpath, subpool));
1252 if (dirent_fields & SVN_DIRENT_HAS_PROPS)
1255 SVN_ERR(svn_fs_node_proplist(&prophash, root, fullpath,
1257 entry->has_props = (apr_hash_count(prophash) != 0);
1260 if ((dirent_fields & SVN_DIRENT_TIME)
1261 || (dirent_fields & SVN_DIRENT_LAST_AUTHOR)
1262 || (dirent_fields & SVN_DIRENT_CREATED_REV))
1264 /* created_rev & friends */
1265 SVN_ERR(svn_repos_get_committed_info(&(entry->created_rev),
1267 &(entry->last_author),
1268 root, fullpath, subpool));
1270 SVN_ERR(svn_time_from_cstring(&(entry->time), datestring,
1272 if (entry->last_author)
1273 entry->last_author = apr_pstrdup(pool, entry->last_author);
1277 svn_hash_sets(*dirents, entryname, entry);
1279 svn_pool_destroy(subpool);
1282 /* Handle props if requested. */
1284 SVN_ERR(get_node_props(props, NULL, sess, root, abs_path, pool, pool));
1286 return SVN_NO_ERROR;
1290 static svn_error_t *
1291 svn_ra_local__get_locations(svn_ra_session_t *session,
1292 apr_hash_t **locations,
1294 svn_revnum_t peg_revision,
1295 const apr_array_header_t *location_revisions,
1298 svn_ra_local__session_baton_t *sess = session->priv;
1299 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1300 return svn_repos_trace_node_locations(sess->fs, locations, abs_path,
1301 peg_revision, location_revisions,
1306 static svn_error_t *
1307 svn_ra_local__get_location_segments(svn_ra_session_t *session,
1309 svn_revnum_t peg_revision,
1310 svn_revnum_t start_rev,
1311 svn_revnum_t end_rev,
1312 svn_location_segment_receiver_t receiver,
1313 void *receiver_baton,
1316 svn_ra_local__session_baton_t *sess = session->priv;
1317 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1318 return svn_repos_node_location_segments(sess->repos, abs_path,
1319 peg_revision, start_rev, end_rev,
1320 receiver, receiver_baton,
1325 static svn_error_t *
1326 svn_ra_local__lock(svn_ra_session_t *session,
1327 apr_hash_t *path_revs,
1328 const char *comment,
1329 svn_boolean_t force,
1330 svn_ra_lock_callback_t lock_func,
1334 svn_ra_local__session_baton_t *sess = session->priv;
1335 apr_hash_index_t *hi;
1336 apr_pool_t *iterpool = svn_pool_create(pool);
1338 /* A username is absolutely required to lock a path. */
1339 SVN_ERR(get_username(session, pool));
1341 for (hi = apr_hash_first(pool, path_revs); hi; hi = apr_hash_next(hi))
1347 svn_revnum_t *revnum;
1348 const char *abs_path;
1349 svn_error_t *err, *callback_err = NULL;
1351 svn_pool_clear(iterpool);
1352 apr_hash_this(hi, &key, NULL, &val);
1356 abs_path = svn_fspath__join(sess->fs_path->data, path, iterpool);
1358 /* This wrapper will call pre- and post-lock hooks. */
1359 err = svn_repos_fs_lock(&lock, sess->repos, abs_path, NULL, comment,
1360 FALSE /* not DAV comment */,
1361 0 /* no expiration */, *revnum, force,
1364 if (err && !SVN_ERR_IS_LOCK_ERROR(err))
1368 callback_err = lock_func(lock_baton, path, TRUE, err ? NULL : lock,
1371 svn_error_clear(err);
1374 return callback_err;
1377 svn_pool_destroy(iterpool);
1379 return SVN_NO_ERROR;
1383 static svn_error_t *
1384 svn_ra_local__unlock(svn_ra_session_t *session,
1385 apr_hash_t *path_tokens,
1386 svn_boolean_t force,
1387 svn_ra_lock_callback_t lock_func,
1391 svn_ra_local__session_baton_t *sess = session->priv;
1392 apr_hash_index_t *hi;
1393 apr_pool_t *iterpool = svn_pool_create(pool);
1395 /* A username is absolutely required to unlock a path. */
1396 SVN_ERR(get_username(session, pool));
1398 for (hi = apr_hash_first(pool, path_tokens); hi; hi = apr_hash_next(hi))
1403 const char *abs_path, *token;
1404 svn_error_t *err, *callback_err = NULL;
1406 svn_pool_clear(iterpool);
1407 apr_hash_this(hi, &key, NULL, &val);
1409 /* Since we can't store NULL values in a hash, we turn "" to
1411 if (strcmp(val, "") != 0)
1416 abs_path = svn_fspath__join(sess->fs_path->data, path, iterpool);
1418 /* This wrapper will call pre- and post-unlock hooks. */
1419 err = svn_repos_fs_unlock(sess->repos, abs_path, token, force,
1422 if (err && !SVN_ERR_IS_UNLOCK_ERROR(err))
1426 callback_err = lock_func(lock_baton, path, FALSE, NULL, err, iterpool);
1428 svn_error_clear(err);
1431 return callback_err;
1434 svn_pool_destroy(iterpool);
1436 return SVN_NO_ERROR;
1441 static svn_error_t *
1442 svn_ra_local__get_lock(svn_ra_session_t *session,
1447 svn_ra_local__session_baton_t *sess = session->priv;
1448 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1449 return svn_fs_get_lock(lock, sess->fs, abs_path, pool);
1454 static svn_error_t *
1455 svn_ra_local__get_locks(svn_ra_session_t *session,
1461 svn_ra_local__session_baton_t *sess = session->priv;
1462 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1464 /* Kinda silly to call the repos wrapper, since we have no authz
1465 func to give it. But heck, why not. */
1466 return svn_repos_fs_get_locks2(locks, sess->repos, abs_path, depth,
1471 static svn_error_t *
1472 svn_ra_local__replay(svn_ra_session_t *session,
1473 svn_revnum_t revision,
1474 svn_revnum_t low_water_mark,
1475 svn_boolean_t send_deltas,
1476 const svn_delta_editor_t *editor,
1480 svn_ra_local__session_baton_t *sess = session->priv;
1481 svn_fs_root_t *root;
1483 SVN_ERR(svn_fs_revision_root(&root, svn_repos_fs(sess->repos),
1485 return svn_repos_replay2(root, sess->fs_path->data, low_water_mark,
1486 send_deltas, editor, edit_baton, NULL, NULL,
1491 static svn_error_t *
1492 svn_ra_local__replay_range(svn_ra_session_t *session,
1493 svn_revnum_t start_revision,
1494 svn_revnum_t end_revision,
1495 svn_revnum_t low_water_mark,
1496 svn_boolean_t send_deltas,
1497 svn_ra_replay_revstart_callback_t revstart_func,
1498 svn_ra_replay_revfinish_callback_t revfinish_func,
1502 return svn_error_create(SVN_ERR_RA_NOT_IMPLEMENTED, NULL, NULL);
1506 static svn_error_t *
1507 svn_ra_local__has_capability(svn_ra_session_t *session,
1509 const char *capability,
1512 svn_ra_local__session_baton_t *sess = session->priv;
1514 if (strcmp(capability, SVN_RA_CAPABILITY_DEPTH) == 0
1515 || strcmp(capability, SVN_RA_CAPABILITY_LOG_REVPROPS) == 0
1516 || strcmp(capability, SVN_RA_CAPABILITY_PARTIAL_REPLAY) == 0
1517 || strcmp(capability, SVN_RA_CAPABILITY_COMMIT_REVPROPS) == 0
1518 || strcmp(capability, SVN_RA_CAPABILITY_ATOMIC_REVPROPS) == 0
1519 || strcmp(capability, SVN_RA_CAPABILITY_INHERITED_PROPS) == 0
1520 || strcmp(capability, SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS) == 0
1521 || strcmp(capability, SVN_RA_CAPABILITY_GET_FILE_REVS_REVERSE) == 0
1526 else if (strcmp(capability, SVN_RA_CAPABILITY_MERGEINFO) == 0)
1528 /* With mergeinfo, the code's capabilities may not reflect the
1529 repository's, so inquire further. */
1530 SVN_ERR(svn_repos_has_capability(sess->repos, has,
1531 SVN_REPOS_CAPABILITY_MERGEINFO,
1534 else /* Don't know any other capabilities, so error. */
1536 return svn_error_createf
1537 (SVN_ERR_UNKNOWN_CAPABILITY, NULL,
1538 _("Don't know anything about capability '%s'"), capability);
1541 return SVN_NO_ERROR;
1544 static svn_error_t *
1545 svn_ra_local__get_deleted_rev(svn_ra_session_t *session,
1547 svn_revnum_t peg_revision,
1548 svn_revnum_t end_revision,
1549 svn_revnum_t *revision_deleted,
1552 svn_ra_local__session_baton_t *sess = session->priv;
1553 const char *abs_path = svn_fspath__join(sess->fs_path->data, path, pool);
1555 SVN_ERR(svn_repos_deleted_rev(sess->fs,
1562 return SVN_NO_ERROR;
1565 static svn_error_t *
1566 svn_ra_local__get_inherited_props(svn_ra_session_t *session,
1567 apr_array_header_t **iprops,
1569 svn_revnum_t revision,
1570 apr_pool_t *result_pool,
1571 apr_pool_t *scratch_pool)
1573 svn_fs_root_t *root;
1574 svn_revnum_t youngest_rev;
1575 svn_ra_local__session_baton_t *sess = session->priv;
1576 const char *abs_path = svn_fspath__join(sess->fs_path->data, path,
1578 svn_node_kind_t node_kind;
1580 /* Open the revision's root. */
1581 if (! SVN_IS_VALID_REVNUM(revision))
1583 SVN_ERR(svn_fs_youngest_rev(&youngest_rev, sess->fs, scratch_pool));
1584 SVN_ERR(svn_fs_revision_root(&root, sess->fs, youngest_rev,
1589 SVN_ERR(svn_fs_revision_root(&root, sess->fs, revision, scratch_pool));
1592 SVN_ERR(svn_fs_check_path(&node_kind, root, abs_path, scratch_pool));
1593 if (node_kind == svn_node_none)
1595 return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
1596 _("'%s' path not found"), abs_path);
1599 return svn_error_trace(get_node_props(NULL, iprops, sess, root, abs_path,
1600 result_pool, scratch_pool));
1603 static svn_error_t *
1604 svn_ra_local__register_editor_shim_callbacks(svn_ra_session_t *session,
1605 svn_delta_shim_callbacks_t *callbacks)
1607 /* This is currenly a no-op, since we don't provide our own editor, just
1608 use the one the libsvn_repos hands back to us. */
1609 return SVN_NO_ERROR;
1613 static svn_error_t *
1614 svn_ra_local__get_commit_ev2(svn_editor_t **editor,
1615 svn_ra_session_t *session,
1616 apr_hash_t *revprops,
1617 svn_commit_callback2_t commit_cb,
1619 apr_hash_t *lock_tokens,
1620 svn_boolean_t keep_locks,
1621 svn_ra__provide_base_cb_t provide_base_cb,
1622 svn_ra__provide_props_cb_t provide_props_cb,
1623 svn_ra__get_copysrc_kind_cb_t get_copysrc_kind_cb,
1625 svn_cancel_func_t cancel_func,
1627 apr_pool_t *result_pool,
1628 apr_pool_t *scratch_pool)
1630 svn_ra_local__session_baton_t *sess = session->priv;
1631 struct deltify_etc_baton *deb = apr_palloc(result_pool, sizeof(*deb));
1633 /* NOTE: the RA callbacks are ignored. We pass everything directly to
1634 the REPOS editor. */
1636 /* Prepare the baton for deltify_etc() */
1638 deb->repos = sess->repos;
1639 deb->fspath_base = sess->fs_path->data;
1641 deb->lock_tokens = lock_tokens;
1643 deb->lock_tokens = NULL;
1644 deb->commit_cb = commit_cb;
1645 deb->commit_baton = commit_baton;
1647 /* Ensure there is a username (and an FS access context) associated with
1648 the session and its FS handle. */
1649 SVN_ERR(get_username(session, scratch_pool));
1651 /* If there are lock tokens to add, do so. */
1652 SVN_ERR(apply_lock_tokens(sess->fs, sess->fs_path->data, lock_tokens,
1653 session->pool, scratch_pool));
1655 /* Copy the REVPROPS and insert the author/username. */
1656 revprops = apr_hash_copy(scratch_pool, revprops);
1657 svn_hash_sets(revprops, SVN_PROP_REVISION_AUTHOR,
1658 svn_string_create(sess->username, scratch_pool));
1660 return svn_error_trace(svn_repos__get_commit_ev2(
1661 editor, sess->repos, NULL /* authz */,
1662 NULL /* authz_repos_name */, NULL /* authz_user */,
1664 deltify_etc, deb, cancel_func, cancel_baton,
1665 result_pool, scratch_pool));
1668 /*----------------------------------------------------------------*/
1670 static const svn_version_t *
1671 ra_local_version(void)
1676 /** The ra_vtable **/
1678 static const svn_ra__vtable_t ra_local_vtable =
1681 svn_ra_local__get_description,
1682 svn_ra_local__get_schemes,
1684 svn_ra_local__reparent,
1685 svn_ra_local__get_session_url,
1686 svn_ra_local__get_latest_revnum,
1687 svn_ra_local__get_dated_revision,
1688 svn_ra_local__change_rev_prop,
1689 svn_ra_local__rev_proplist,
1690 svn_ra_local__rev_prop,
1691 svn_ra_local__get_commit_editor,
1692 svn_ra_local__get_file,
1693 svn_ra_local__get_dir,
1694 svn_ra_local__get_mergeinfo,
1695 svn_ra_local__do_update,
1696 svn_ra_local__do_switch,
1697 svn_ra_local__do_status,
1698 svn_ra_local__do_diff,
1699 svn_ra_local__get_log,
1700 svn_ra_local__do_check_path,
1702 svn_ra_local__get_uuid,
1703 svn_ra_local__get_repos_root,
1704 svn_ra_local__get_locations,
1705 svn_ra_local__get_location_segments,
1706 svn_ra_local__get_file_revs,
1708 svn_ra_local__unlock,
1709 svn_ra_local__get_lock,
1710 svn_ra_local__get_locks,
1711 svn_ra_local__replay,
1712 svn_ra_local__has_capability,
1713 svn_ra_local__replay_range,
1714 svn_ra_local__get_deleted_rev,
1715 svn_ra_local__register_editor_shim_callbacks,
1716 svn_ra_local__get_inherited_props,
1717 svn_ra_local__get_commit_ev2
1721 /*----------------------------------------------------------------*/
1723 /** The One Public Routine, called by libsvn_ra **/
1726 svn_ra_local__init(const svn_version_t *loader_version,
1727 const svn_ra__vtable_t **vtable,
1730 static const svn_version_checklist_t checklist[] =
1732 { "svn_subr", svn_subr_version },
1733 { "svn_delta", svn_delta_version },
1734 { "svn_repos", svn_repos_version },
1735 { "svn_fs", svn_fs_version },
1740 /* Simplified version check to make sure we can safely use the
1741 VTABLE parameter. The RA loader does a more exhaustive check. */
1742 if (loader_version->major != SVN_VER_MAJOR)
1743 return svn_error_createf(SVN_ERR_VERSION_MISMATCH, NULL,
1744 _("Unsupported RA loader version (%d) for "
1746 loader_version->major);
1748 SVN_ERR(svn_ver_check_list(ra_local_version(), checklist));
1750 #ifndef SVN_LIBSVN_CLIENT_LINKS_RA_LOCAL
1751 /* This assumes that POOL was the pool used to load the dso. */
1752 SVN_ERR(svn_fs_initialize(pool));
1755 *vtable = &ra_local_vtable;
1757 return SVN_NO_ERROR;
1760 /* Compatibility wrapper for the 1.1 and before API. */
1761 #define NAME "ra_local"
1762 #define DESCRIPTION RA_LOCAL_DESCRIPTION
1763 #define VTBL ra_local_vtable
1764 #define INITFUNC svn_ra_local__init
1765 #define COMPAT_INITFUNC svn_ra_local_init
1766 #include "../libsvn_ra/wrapper_template.h"