| /* |
| * editorp.c : Driving and consuming an editor across an svn connection |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| |
| |
| #define APR_WANT_STRFUNC |
| #include <apr_want.h> |
| #include <apr_general.h> |
| #include <apr_strings.h> |
| |
| #include "svn_hash.h" |
| #include "svn_types.h" |
| #include "svn_string.h" |
| #include "svn_error.h" |
| #include "svn_delta.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_ra_svn.h" |
| #include "svn_path.h" |
| #include "svn_pools.h" |
| #include "svn_private_config.h" |
| |
| #include "private/svn_atomic.h" |
| #include "private/svn_fspath.h" |
| #include "private/svn_editor.h" |
| #include "private/svn_string_private.h" |
| #include "private/svn_subr_private.h" |
| |
| #include "ra_svn.h" |
| |
| /* |
| * Both the client and server in the svn protocol need to drive and |
| * consume editors. For a commit, the client drives and the server |
| * consumes; for an update/switch/status/diff, the server drives and |
| * the client consumes. This file provides a generic framework for |
| * marshalling and unmarshalling editor operations over an svn |
| * connection; both ends are useful for both server and client. |
| */ |
| |
| typedef struct ra_svn_edit_baton_t { |
| svn_ra_svn_conn_t *conn; |
| svn_ra_svn_edit_callback callback; /* Called on successful completion. */ |
| void *callback_baton; |
| apr_uint64_t next_token; |
| svn_boolean_t got_status; |
| } ra_svn_edit_baton_t; |
| |
| /* Works for both directories and files. */ |
| typedef struct ra_svn_baton_t { |
| svn_ra_svn_conn_t *conn; |
| apr_pool_t *pool; |
| ra_svn_edit_baton_t *eb; |
| svn_string_t *token; |
| } ra_svn_baton_t; |
| |
| /* Forward declaration. */ |
| typedef struct ra_svn_token_entry_t ra_svn_token_entry_t; |
| |
| typedef struct ra_svn_driver_state_t { |
| const svn_delta_editor_t *editor; |
| void *edit_baton; |
| apr_hash_t *tokens; |
| |
| /* Entry for the last token seen. May be NULL. */ |
| ra_svn_token_entry_t *last_token; |
| svn_boolean_t *aborted; |
| svn_boolean_t done; |
| apr_pool_t *pool; |
| apr_pool_t *file_pool; |
| int file_refs; |
| svn_boolean_t for_replay; |
| } ra_svn_driver_state_t; |
| |
| /* Works for both directories and files; however, the pool handling is |
| different for files. To save space during commits (where file |
| batons generally last until the end of the commit), token entries |
| for files are all created in a single reference-counted pool (the |
| file_pool member of the driver state structure), which is cleared |
| at close_file time when the reference count hits zero. So the pool |
| field in this structure is vestigial for files, and we use it for a |
| different purpose instead: at apply-textdelta time, we set it to a |
| subpool of the file pool, which is destroyed in textdelta-end. */ |
| struct ra_svn_token_entry_t { |
| svn_string_t *token; |
| void *baton; |
| svn_boolean_t is_file; |
| svn_stream_t *dstream; /* svndiff stream for apply_textdelta */ |
| apr_pool_t *pool; |
| }; |
| |
| /* --- CONSUMING AN EDITOR BY PASSING EDIT OPERATIONS OVER THE NET --- */ |
| |
| static svn_string_t * |
| make_token(char type, |
| ra_svn_edit_baton_t *eb, |
| apr_pool_t *pool) |
| { |
| apr_size_t len; |
| char buffer[1 + SVN_INT64_BUFFER_SIZE]; |
| buffer[0] = type; |
| len = 1 + svn__ui64toa(&buffer[1], eb->next_token++); |
| |
| return svn_string_ncreate(buffer, len, pool); |
| } |
| |
| static ra_svn_baton_t *ra_svn_make_baton(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| ra_svn_edit_baton_t *eb, |
| svn_string_t *token) |
| { |
| ra_svn_baton_t *b; |
| |
| b = apr_palloc(pool, sizeof(*b)); |
| b->conn = conn; |
| b->pool = pool; |
| b->eb = eb; |
| b->token = token; |
| return b; |
| } |
| |
| /* Check for an early error status report from the consumer. If we |
| * get one, abort the edit and return the error. */ |
| static svn_error_t * |
| check_for_error_internal(ra_svn_edit_baton_t *eb, apr_pool_t *pool) |
| { |
| svn_boolean_t available; |
| SVN_ERR_ASSERT(!eb->got_status); |
| |
| /* reset TX counter */ |
| eb->conn->written_since_error_check = 0; |
| |
| /* if we weren't asked to always check, wait for at least the next TX */ |
| eb->conn->may_check_for_error = eb->conn->error_check_interval == 0; |
| |
| /* any incoming data? */ |
| SVN_ERR(svn_ra_svn__data_available(eb->conn, &available)); |
| if (available) |
| { |
| eb->got_status = TRUE; |
| SVN_ERR(svn_ra_svn__write_cmd_abort_edit(eb->conn, pool)); |
| SVN_ERR(svn_ra_svn__read_cmd_response(eb->conn, pool, "")); |
| /* We shouldn't get here if the consumer is doing its job. */ |
| return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, |
| _("Successful edit status returned too soon")); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| check_for_error(ra_svn_edit_baton_t *eb, apr_pool_t *pool) |
| { |
| return eb->conn->may_check_for_error |
| ? check_for_error_internal(eb, pool) |
| : SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_target_rev(void *edit_baton, svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| ra_svn_edit_baton_t *eb = edit_baton; |
| |
| SVN_ERR(check_for_error(eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_target_rev(eb->conn, pool, rev)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_open_root(void *edit_baton, svn_revnum_t rev, |
| apr_pool_t *pool, void **root_baton) |
| { |
| ra_svn_edit_baton_t *eb = edit_baton; |
| svn_string_t *token = make_token('d', eb, pool); |
| |
| SVN_ERR(check_for_error(eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_open_root(eb->conn, pool, rev, token)); |
| *root_baton = ra_svn_make_baton(eb->conn, pool, eb, token); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_delete_entry(const char *path, svn_revnum_t rev, |
| void *parent_baton, apr_pool_t *pool) |
| { |
| ra_svn_baton_t *b = parent_baton; |
| |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_delete_entry(b->conn, pool, |
| path, rev, b->token)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_add_dir(const char *path, void *parent_baton, |
| const char *copy_path, |
| svn_revnum_t copy_rev, |
| apr_pool_t *pool, void **child_baton) |
| { |
| ra_svn_baton_t *b = parent_baton; |
| svn_string_t *token = make_token('d', b->eb, pool); |
| |
| SVN_ERR_ASSERT((copy_path && SVN_IS_VALID_REVNUM(copy_rev)) |
| || (!copy_path && !SVN_IS_VALID_REVNUM(copy_rev))); |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_add_dir(b->conn, pool, path, b->token, |
| token, copy_path, copy_rev)); |
| *child_baton = ra_svn_make_baton(b->conn, pool, b->eb, token); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_open_dir(const char *path, void *parent_baton, |
| svn_revnum_t rev, apr_pool_t *pool, |
| void **child_baton) |
| { |
| ra_svn_baton_t *b = parent_baton; |
| svn_string_t *token = make_token('d', b->eb, pool); |
| |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_open_dir(b->conn, pool, path, b->token, |
| token, rev)); |
| *child_baton = ra_svn_make_baton(b->conn, pool, b->eb, token); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_change_dir_prop(void *dir_baton, const char *name, |
| const svn_string_t *value, |
| apr_pool_t *pool) |
| { |
| ra_svn_baton_t *b = dir_baton; |
| |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_change_dir_prop(b->conn, pool, b->token, |
| name, value)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_close_dir(void *dir_baton, apr_pool_t *pool) |
| { |
| ra_svn_baton_t *b = dir_baton; |
| |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_close_dir(b->conn, pool, b->token)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_absent_dir(const char *path, |
| void *parent_baton, apr_pool_t *pool) |
| { |
| ra_svn_baton_t *b = parent_baton; |
| |
| /* Avoid sending an unknown command if the other end doesn't support |
| absent-dir. */ |
| if (! svn_ra_svn_has_capability(b->conn, SVN_RA_SVN_CAP_ABSENT_ENTRIES)) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_absent_dir(b->conn, pool, path, b->token)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_add_file(const char *path, |
| void *parent_baton, |
| const char *copy_path, |
| svn_revnum_t copy_rev, |
| apr_pool_t *pool, |
| void **file_baton) |
| { |
| ra_svn_baton_t *b = parent_baton; |
| svn_string_t *token = make_token('c', b->eb, pool); |
| |
| SVN_ERR_ASSERT((copy_path && SVN_IS_VALID_REVNUM(copy_rev)) |
| || (!copy_path && !SVN_IS_VALID_REVNUM(copy_rev))); |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_add_file(b->conn, pool, path, b->token, |
| token, copy_path, copy_rev)); |
| *file_baton = ra_svn_make_baton(b->conn, pool, b->eb, token); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_open_file(const char *path, |
| void *parent_baton, |
| svn_revnum_t rev, |
| apr_pool_t *pool, |
| void **file_baton) |
| { |
| ra_svn_baton_t *b = parent_baton; |
| svn_string_t *token = make_token('c', b->eb, pool); |
| |
| SVN_ERR(check_for_error(b->eb, b->pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_open_file(b->conn, pool, path, b->token, |
| token, rev)); |
| *file_baton = ra_svn_make_baton(b->conn, pool, b->eb, token); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_svndiff_handler(void *baton, const char *data, |
| apr_size_t *len) |
| { |
| ra_svn_baton_t *b = baton; |
| svn_string_t str; |
| |
| SVN_ERR(check_for_error(b->eb, b->pool)); |
| str.data = data; |
| str.len = *len; |
| return svn_ra_svn__write_cmd_textdelta_chunk(b->conn, b->pool, |
| b->token, &str); |
| } |
| |
| static svn_error_t *ra_svn_svndiff_close_handler(void *baton) |
| { |
| ra_svn_baton_t *b = baton; |
| |
| SVN_ERR(check_for_error(b->eb, b->pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_textdelta_end(b->conn, b->pool, b->token)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_apply_textdelta(void *file_baton, |
| const char *base_checksum, |
| apr_pool_t *pool, |
| svn_txdelta_window_handler_t *wh, |
| void **wh_baton) |
| { |
| ra_svn_baton_t *b = file_baton; |
| svn_stream_t *diff_stream; |
| |
| /* Tell the other side we're starting a text delta. */ |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_apply_textdelta(b->conn, pool, b->token, |
| base_checksum)); |
| |
| /* Transform the window stream to an svndiff stream. Reuse the |
| * file baton for the stream handler, since it has all the |
| * needed information. */ |
| diff_stream = svn_stream_create(b, pool); |
| svn_stream_set_write(diff_stream, ra_svn_svndiff_handler); |
| svn_stream_set_close(diff_stream, ra_svn_svndiff_close_handler); |
| |
| svn_txdelta_to_svndiff3(wh, wh_baton, diff_stream, |
| svn_ra_svn__svndiff_version(b->conn), |
| b->conn->compression_level, pool); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_change_file_prop(void *file_baton, |
| const char *name, |
| const svn_string_t *value, |
| apr_pool_t *pool) |
| { |
| ra_svn_baton_t *b = file_baton; |
| |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_change_file_prop(b->conn, pool, |
| b->token, name, value)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_close_file(void *file_baton, |
| const char *text_checksum, |
| apr_pool_t *pool) |
| { |
| ra_svn_baton_t *b = file_baton; |
| |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_close_file(b->conn, pool, |
| b->token, text_checksum)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_absent_file(const char *path, |
| void *parent_baton, apr_pool_t *pool) |
| { |
| ra_svn_baton_t *b = parent_baton; |
| |
| /* Avoid sending an unknown command if the other end doesn't support |
| absent-file. */ |
| if (! svn_ra_svn_has_capability(b->conn, SVN_RA_SVN_CAP_ABSENT_ENTRIES)) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(check_for_error(b->eb, pool)); |
| SVN_ERR(svn_ra_svn__write_cmd_absent_file(b->conn, pool, path, b->token)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_close_edit(void *edit_baton, apr_pool_t *pool) |
| { |
| ra_svn_edit_baton_t *eb = edit_baton; |
| svn_error_t *err; |
| |
| SVN_ERR_ASSERT(!eb->got_status); |
| eb->got_status = TRUE; |
| SVN_ERR(svn_ra_svn__write_cmd_close_edit(eb->conn, pool)); |
| err = svn_error_trace(svn_ra_svn__read_cmd_response(eb->conn, pool, "")); |
| if (err) |
| { |
| return svn_error_compose_create( |
| err, |
| svn_error_trace( |
| svn_ra_svn__write_cmd_abort_edit(eb->conn, pool))); |
| } |
| if (eb->callback) |
| SVN_ERR(eb->callback(eb->callback_baton)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t *ra_svn_abort_edit(void *edit_baton, apr_pool_t *pool) |
| { |
| ra_svn_edit_baton_t *eb = edit_baton; |
| |
| if (eb->got_status) |
| return SVN_NO_ERROR; |
| SVN_ERR(svn_ra_svn__write_cmd_abort_edit(eb->conn, pool)); |
| SVN_ERR(svn_ra_svn__read_cmd_response(eb->conn, pool, "")); |
| return SVN_NO_ERROR; |
| } |
| |
| void svn_ra_svn_get_editor(const svn_delta_editor_t **editor, |
| void **edit_baton, svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| svn_ra_svn_edit_callback callback, |
| void *callback_baton) |
| { |
| svn_delta_editor_t *ra_svn_editor = svn_delta_default_editor(pool); |
| ra_svn_edit_baton_t *eb; |
| |
| eb = apr_palloc(pool, sizeof(*eb)); |
| eb->conn = conn; |
| eb->callback = callback; |
| eb->callback_baton = callback_baton; |
| eb->next_token = 0; |
| eb->got_status = FALSE; |
| |
| ra_svn_editor->set_target_revision = ra_svn_target_rev; |
| ra_svn_editor->open_root = ra_svn_open_root; |
| ra_svn_editor->delete_entry = ra_svn_delete_entry; |
| ra_svn_editor->add_directory = ra_svn_add_dir; |
| ra_svn_editor->open_directory = ra_svn_open_dir; |
| ra_svn_editor->change_dir_prop = ra_svn_change_dir_prop; |
| ra_svn_editor->close_directory = ra_svn_close_dir; |
| ra_svn_editor->absent_directory = ra_svn_absent_dir; |
| ra_svn_editor->add_file = ra_svn_add_file; |
| ra_svn_editor->open_file = ra_svn_open_file; |
| ra_svn_editor->apply_textdelta = ra_svn_apply_textdelta; |
| ra_svn_editor->change_file_prop = ra_svn_change_file_prop; |
| ra_svn_editor->close_file = ra_svn_close_file; |
| ra_svn_editor->absent_file = ra_svn_absent_file; |
| ra_svn_editor->close_edit = ra_svn_close_edit; |
| ra_svn_editor->abort_edit = ra_svn_abort_edit; |
| |
| *editor = ra_svn_editor; |
| *edit_baton = eb; |
| |
| svn_error_clear(svn_editor__insert_shims(editor, edit_baton, *editor, |
| *edit_baton, NULL, NULL, |
| conn->shim_callbacks, |
| pool, pool)); |
| } |
| |
| /* --- DRIVING AN EDITOR --- */ |
| |
| /* Store a token entry. The token string will be copied into pool. */ |
| static ra_svn_token_entry_t *store_token(ra_svn_driver_state_t *ds, |
| void *baton, |
| svn_string_t *token, |
| svn_boolean_t is_file, |
| apr_pool_t *pool) |
| { |
| ra_svn_token_entry_t *entry; |
| |
| entry = apr_palloc(pool, sizeof(*entry)); |
| entry->token = svn_string_dup(token, pool); |
| entry->baton = baton; |
| entry->is_file = is_file; |
| entry->dstream = NULL; |
| entry->pool = pool; |
| |
| apr_hash_set(ds->tokens, entry->token->data, entry->token->len, entry); |
| ds->last_token = entry; |
| |
| return entry; |
| } |
| |
| static svn_error_t *lookup_token(ra_svn_driver_state_t *ds, |
| svn_string_t *token, |
| svn_boolean_t is_file, |
| ra_svn_token_entry_t **entry) |
| { |
| if (ds->last_token && svn_string_compare(ds->last_token->token, token)) |
| { |
| *entry = ds->last_token; |
| } |
| else |
| { |
| *entry = apr_hash_get(ds->tokens, token->data, token->len); |
| ds->last_token = *entry; |
| } |
| |
| if (!*entry || (*entry)->is_file != is_file) |
| return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, |
| _("Invalid file or dir token during edit")); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Remove a TOKEN entry from DS. */ |
| static void remove_token(ra_svn_driver_state_t *ds, |
| svn_string_t *token) |
| { |
| apr_hash_set(ds->tokens, token->data, token->len, NULL); |
| |
| /* Reset this unconditionally. In most cases, LAST_TOKEN->TOKEN will |
| match TOKEN anyway and if it doesn't, lookup_token() will suffer only |
| a minor performance hit. */ |
| ds->last_token = NULL; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_target_rev(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| svn_revnum_t rev; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "r", &rev)); |
| SVN_CMD_ERR(ds->editor->set_target_revision(ds->edit_baton, rev, pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_open_root(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| svn_revnum_t rev; |
| apr_pool_t *subpool; |
| svn_string_t *token; |
| void *root_baton; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "(?r)s", &rev, &token)); |
| subpool = svn_pool_create(ds->pool); |
| SVN_CMD_ERR(ds->editor->open_root(ds->edit_baton, rev, subpool, |
| &root_baton)); |
| store_token(ds, root_baton, token, FALSE, subpool); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_delete_entry(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| const char *path; |
| svn_string_t *token; |
| svn_revnum_t rev; |
| ra_svn_token_entry_t *entry; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "c(?r)s", |
| &path, &rev, &token)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| path = svn_relpath_canonicalize(path, pool); |
| SVN_CMD_ERR(ds->editor->delete_entry(path, rev, entry->baton, pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_add_dir(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| const char *path, *copy_path; |
| svn_string_t *token, *child_token; |
| svn_revnum_t copy_rev; |
| ra_svn_token_entry_t *entry; |
| apr_pool_t *subpool; |
| void *child_baton; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "css(?cr)", &path, &token, |
| &child_token, ©_path, ©_rev)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| subpool = svn_pool_create(entry->pool); |
| path = svn_relpath_canonicalize(path, pool); |
| |
| /* Some operations pass COPY_PATH as a full URL (commits, etc.). |
| Others (replay, e.g.) deliver an fspath. That's ... annoying. */ |
| if (copy_path) |
| { |
| if (svn_path_is_url(copy_path)) |
| copy_path = svn_uri_canonicalize(copy_path, pool); |
| else |
| copy_path = svn_fspath__canonicalize(copy_path, pool); |
| } |
| |
| SVN_CMD_ERR(ds->editor->add_directory(path, entry->baton, copy_path, |
| copy_rev, subpool, &child_baton)); |
| store_token(ds, child_baton, child_token, FALSE, subpool); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_open_dir(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| const char *path; |
| svn_string_t *token, *child_token; |
| svn_revnum_t rev; |
| ra_svn_token_entry_t *entry; |
| apr_pool_t *subpool; |
| void *child_baton; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "css(?r)", &path, &token, |
| &child_token, &rev)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| subpool = svn_pool_create(entry->pool); |
| path = svn_relpath_canonicalize(path, pool); |
| SVN_CMD_ERR(ds->editor->open_directory(path, entry->baton, rev, subpool, |
| &child_baton)); |
| store_token(ds, child_baton, child_token, FALSE, subpool); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_change_dir_prop(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| svn_string_t *token; |
| const char *name; |
| svn_string_t *value; |
| ra_svn_token_entry_t *entry; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "sc(?s)", &token, &name, |
| &value)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| SVN_CMD_ERR(ds->editor->change_dir_prop(entry->baton, name, value, |
| entry->pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_close_dir(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| svn_string_t *token; |
| ra_svn_token_entry_t *entry; |
| |
| /* Parse and look up the directory token. */ |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "s", &token)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| |
| /* Close the directory and destroy the baton. */ |
| SVN_CMD_ERR(ds->editor->close_directory(entry->baton, pool)); |
| remove_token(ds, token); |
| svn_pool_destroy(entry->pool); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_absent_dir(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| const char *path; |
| svn_string_t *token; |
| ra_svn_token_entry_t *entry; |
| |
| /* Parse parameters and look up the directory token. */ |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "cs", &path, &token)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| |
| /* Call the editor. */ |
| SVN_CMD_ERR(ds->editor->absent_directory(path, entry->baton, pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_add_file(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| const char *path, *copy_path; |
| svn_string_t *token, *file_token; |
| svn_revnum_t copy_rev; |
| ra_svn_token_entry_t *entry, *file_entry; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "css(?cr)", &path, &token, |
| &file_token, ©_path, ©_rev)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| ds->file_refs++; |
| |
| /* The PATH should be canonical .. but never trust incoming data. */ |
| if (!svn_relpath_is_canonical(path)) |
| path = svn_relpath_canonicalize(path, pool); |
| |
| /* Some operations pass COPY_PATH as a full URL (commits, etc.). |
| Others (replay, e.g.) deliver an fspath. That's ... annoying. */ |
| if (copy_path) |
| { |
| if (svn_path_is_url(copy_path)) |
| copy_path = svn_uri_canonicalize(copy_path, pool); |
| else |
| copy_path = svn_fspath__canonicalize(copy_path, pool); |
| } |
| |
| file_entry = store_token(ds, NULL, file_token, TRUE, ds->file_pool); |
| SVN_CMD_ERR(ds->editor->add_file(path, entry->baton, copy_path, copy_rev, |
| ds->file_pool, &file_entry->baton)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_open_file(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| const char *path; |
| svn_string_t *token, *file_token; |
| svn_revnum_t rev; |
| ra_svn_token_entry_t *entry, *file_entry; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "css(?r)", &path, &token, |
| &file_token, &rev)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| ds->file_refs++; |
| |
| /* The PATH should be canonical .. but never trust incoming data. */ |
| if (!svn_relpath_is_canonical(path)) |
| path = svn_relpath_canonicalize(path, pool); |
| |
| file_entry = store_token(ds, NULL, file_token, TRUE, ds->file_pool); |
| SVN_CMD_ERR(ds->editor->open_file(path, entry->baton, rev, ds->file_pool, |
| &file_entry->baton)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_apply_textdelta(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| svn_string_t *token; |
| ra_svn_token_entry_t *entry; |
| svn_txdelta_window_handler_t wh; |
| void *wh_baton; |
| char *base_checksum; |
| |
| /* Parse arguments and look up the token. */ |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "s(?c)", |
| &token, &base_checksum)); |
| SVN_ERR(lookup_token(ds, token, TRUE, &entry)); |
| if (entry->dstream) |
| return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, |
| _("Apply-textdelta already active")); |
| entry->pool = svn_pool_create(ds->file_pool); |
| SVN_CMD_ERR(ds->editor->apply_textdelta(entry->baton, base_checksum, |
| entry->pool, &wh, &wh_baton)); |
| entry->dstream = svn_txdelta_parse_svndiff(wh, wh_baton, TRUE, entry->pool); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_textdelta_chunk(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| svn_string_t *token; |
| ra_svn_token_entry_t *entry; |
| svn_string_t *str; |
| |
| /* Parse arguments and look up the token. */ |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "ss", &token, &str)); |
| SVN_ERR(lookup_token(ds, token, TRUE, &entry)); |
| if (!entry->dstream) |
| return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, |
| _("Apply-textdelta not active")); |
| SVN_CMD_ERR(svn_stream_write(entry->dstream, str->data, &str->len)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_textdelta_end(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| svn_string_t *token; |
| ra_svn_token_entry_t *entry; |
| |
| /* Parse arguments and look up the token. */ |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "s", &token)); |
| SVN_ERR(lookup_token(ds, token, TRUE, &entry)); |
| if (!entry->dstream) |
| return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, |
| _("Apply-textdelta not active")); |
| SVN_CMD_ERR(svn_stream_close(entry->dstream)); |
| entry->dstream = NULL; |
| svn_pool_destroy(entry->pool); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_change_file_prop(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| const char *name; |
| svn_string_t *token, *value; |
| ra_svn_token_entry_t *entry; |
| |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "sc(?s)", &token, &name, |
| &value)); |
| SVN_ERR(lookup_token(ds, token, TRUE, &entry)); |
| SVN_CMD_ERR(ds->editor->change_file_prop(entry->baton, name, value, pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_close_file(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| svn_string_t *token; |
| ra_svn_token_entry_t *entry; |
| const char *text_checksum; |
| |
| /* Parse arguments and look up the file token. */ |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "s(?c)", |
| &token, &text_checksum)); |
| SVN_ERR(lookup_token(ds, token, TRUE, &entry)); |
| |
| /* Close the file and destroy the baton. */ |
| SVN_CMD_ERR(ds->editor->close_file(entry->baton, text_checksum, pool)); |
| remove_token(ds, token); |
| if (--ds->file_refs == 0) |
| svn_pool_clear(ds->file_pool); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_absent_file(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| const char *path; |
| svn_string_t *token; |
| ra_svn_token_entry_t *entry; |
| |
| /* Parse parameters and look up the parent directory token. */ |
| SVN_ERR(svn_ra_svn__parse_tuple(params, "cs", &path, &token)); |
| SVN_ERR(lookup_token(ds, token, FALSE, &entry)); |
| |
| /* Call the editor. */ |
| SVN_CMD_ERR(ds->editor->absent_file(path, entry->baton, pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_close_edit(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| SVN_CMD_ERR(ds->editor->close_edit(ds->edit_baton, pool)); |
| ds->done = TRUE; |
| #ifdef SVN_DEBUG |
| /* Before enabling this in non-maintainer mode: |
| * Note that this code is used on both client *and* server */ |
| if (apr_hash_count(ds->tokens) != 0) |
| return svn_error_create( |
| SVN_ERR_FS_INCORRECT_EDITOR_COMPLETION, NULL, |
| _("Closing editor with directories or files open")); |
| #endif |
| if (ds->aborted) |
| *ds->aborted = FALSE; |
| return svn_ra_svn__write_cmd_response(conn, pool, ""); |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_abort_edit(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| ds->done = TRUE; |
| if (ds->aborted) |
| *ds->aborted = TRUE; |
| SVN_CMD_ERR(ds->editor->abort_edit(ds->edit_baton, pool)); |
| return svn_ra_svn__write_cmd_response(conn, pool, ""); |
| } |
| |
| static svn_error_t * |
| ra_svn_handle_finish_replay(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds) |
| { |
| if (!ds->for_replay) |
| return svn_error_createf |
| (SVN_ERR_RA_SVN_UNKNOWN_CMD, NULL, |
| _("Command 'finish-replay' invalid outside of replays")); |
| ds->done = TRUE; |
| if (ds->aborted) |
| *ds->aborted = FALSE; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Common function signature for all editor command handlers. */ |
| typedef svn_error_t *(*cmd_handler_t)(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_ra_svn__list_t *params, |
| ra_svn_driver_state_t *ds); |
| |
| static const struct { |
| const char *cmd; |
| cmd_handler_t handler; |
| } ra_svn_edit_cmds[] = { |
| { "change-file-prop", ra_svn_handle_change_file_prop }, |
| { "open-file", ra_svn_handle_open_file }, |
| { "apply-textdelta", ra_svn_handle_apply_textdelta }, |
| { "textdelta-chunk", ra_svn_handle_textdelta_chunk }, |
| { "close-file", ra_svn_handle_close_file }, |
| { "add-dir", ra_svn_handle_add_dir }, |
| { "open-dir", ra_svn_handle_open_dir }, |
| { "change-dir-prop", ra_svn_handle_change_dir_prop }, |
| { "delete-entry", ra_svn_handle_delete_entry }, |
| { "close-dir", ra_svn_handle_close_dir }, |
| { "absent-dir", ra_svn_handle_absent_dir }, |
| { "add-file", ra_svn_handle_add_file }, |
| { "textdelta-end", ra_svn_handle_textdelta_end }, |
| { "absent-file", ra_svn_handle_absent_file }, |
| { "abort-edit", ra_svn_handle_abort_edit }, |
| { "finish-replay", ra_svn_handle_finish_replay }, |
| { "target-rev", ra_svn_handle_target_rev }, |
| { "open-root", ra_svn_handle_open_root }, |
| { "close-edit", ra_svn_handle_close_edit }, |
| { NULL } |
| }; |
| |
| /* All editor commands are kept in a collision-free hash table. */ |
| |
| /* Hash table entry. |
| It is similar to ra_svn_edit_cmds but uses our SVN string type. */ |
| typedef struct cmd_t { |
| svn_string_t cmd; |
| cmd_handler_t handler; |
| } cmd_t; |
| |
| /* The actual hash table. It will be filled once before first usage. |
| |
| If you add more commands, you may have to tweak the table size to |
| eliminate collisions. Alternatively, you may modify the hash function. |
| |
| Be sure to initialize all elements with 0 as the has conflict detection |
| will rely on it (see init_cmd_hash). |
| */ |
| #define CMD_HASH_SIZE 67 |
| static cmd_t cmd_hash[CMD_HASH_SIZE] = { { { NULL } } }; |
| |
| /* Init flag that controls CMD_HASH's atomic initialization. */ |
| static volatile svn_atomic_t cmd_hash_initialized = FALSE; |
| |
| /* Super-fast hash function that works very well with the structure of our |
| command words. It produces no conflicts for them. |
| |
| Return the index within CMD_HASH that a command NAME of LEN chars would |
| be found. LEN > 0. |
| */ |
| static apr_size_t |
| cmd_hash_func(const char *name, |
| apr_size_t len) |
| { |
| apr_size_t value = (apr_byte_t)(name[0] - 'a') % 8 |
| + 1 * (apr_byte_t)(name[len - 1] - 'a') % 8 |
| + 10 * (len - 7); |
| return value % CMD_HASH_SIZE; |
| } |
| |
| /* svn_atomic__init_once callback that fills the CMD_HASH table. It will |
| error out on hash collisions. BATON and POOL are not used. */ |
| static svn_error_t * |
| init_cmd_hash(void *baton, |
| apr_pool_t *pool) |
| { |
| int i; |
| for (i = 0; ra_svn_edit_cmds[i].cmd; i++) |
| { |
| apr_size_t len = strlen(ra_svn_edit_cmds[i].cmd); |
| apr_size_t value = cmd_hash_func(ra_svn_edit_cmds[i].cmd, len); |
| SVN_ERR_ASSERT(cmd_hash[value].cmd.data == NULL); |
| |
| cmd_hash[value].cmd.data = ra_svn_edit_cmds[i].cmd; |
| cmd_hash[value].cmd.len = len; |
| cmd_hash[value].handler = ra_svn_edit_cmds[i].handler; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return the command handler function for the command name CMD. |
| Return NULL if no such handler exists */ |
| static cmd_handler_t |
| cmd_lookup(const char *cmd) |
| { |
| apr_size_t value; |
| apr_size_t len = strlen(cmd); |
| |
| /* Malicious data that our hash function may not like? */ |
| if (len == 0) |
| return NULL; |
| |
| /* Hash lookup. */ |
| value = cmd_hash_func(cmd, len); |
| |
| /* Hit? */ |
| if (cmd_hash[value].cmd.len != len) |
| return NULL; |
| |
| if (memcmp(cmd_hash[value].cmd.data, cmd, len)) |
| return NULL; |
| |
| /* Yes! */ |
| return cmd_hash[value].handler; |
| } |
| |
| static svn_error_t *blocked_write(svn_ra_svn_conn_t *conn, apr_pool_t *pool, |
| void *baton) |
| { |
| ra_svn_driver_state_t *ds = baton; |
| const char *cmd; |
| svn_ra_svn__list_t *params; |
| |
| /* We blocked trying to send an error. Read and discard an editing |
| * command in order to avoid deadlock. */ |
| SVN_ERR(svn_ra_svn__read_tuple(conn, pool, "wl", &cmd, ¶ms)); |
| if (strcmp(cmd, "abort-edit") == 0) |
| { |
| ds->done = TRUE; |
| svn_ra_svn__set_block_handler(conn, NULL, NULL); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t *svn_ra_svn_drive_editor2(svn_ra_svn_conn_t *conn, |
| apr_pool_t *pool, |
| const svn_delta_editor_t *editor, |
| void *edit_baton, |
| svn_boolean_t *aborted, |
| svn_boolean_t for_replay) |
| { |
| ra_svn_driver_state_t state; |
| apr_pool_t *subpool = svn_pool_create(pool); |
| const char *cmd; |
| svn_error_t *err, *write_err; |
| svn_ra_svn__list_t *params; |
| |
| SVN_ERR(svn_atomic__init_once(&cmd_hash_initialized, init_cmd_hash, NULL, |
| pool)); |
| |
| state.editor = editor; |
| state.edit_baton = edit_baton; |
| state.tokens = svn_hash__make(pool); |
| state.last_token = NULL; |
| state.aborted = aborted; |
| state.done = FALSE; |
| state.pool = pool; |
| state.file_pool = svn_pool_create(pool); |
| state.file_refs = 0; |
| state.for_replay = for_replay; |
| |
| while (!state.done) |
| { |
| svn_pool_clear(subpool); |
| |
| /* WRT to applying I/O limits, treat each editor command as a separate |
| * protocol command. */ |
| svn_ra_svn__reset_command_io_counters(conn); |
| if (editor) |
| { |
| cmd_handler_t handler; |
| SVN_ERR(svn_ra_svn__read_tuple(conn, subpool, "wl", &cmd, ¶ms)); |
| handler = cmd_lookup(cmd); |
| |
| if (handler) |
| err = (*handler)(conn, subpool, params, &state); |
| else if (strcmp(cmd, "failure") == 0) |
| { |
| /* While not really an editor command this can occur when |
| reporter->finish_report() fails before the first editor |
| command */ |
| if (aborted) |
| *aborted = TRUE; |
| err = svn_ra_svn__handle_failure_status(params); |
| return svn_error_compose_create( |
| err, |
| editor->abort_edit(edit_baton, subpool)); |
| } |
| else |
| { |
| err = svn_error_createf(SVN_ERR_RA_SVN_UNKNOWN_CMD, NULL, |
| _("Unknown editor command '%s'"), cmd); |
| err = svn_error_create(SVN_ERR_RA_SVN_CMD_ERR, err, NULL); |
| } |
| } |
| else |
| { |
| const char* command = NULL; |
| SVN_ERR(svn_ra_svn__read_command_only(conn, subpool, &command)); |
| if (strcmp(command, "close-edit") == 0) |
| { |
| state.done = TRUE; |
| if (aborted) |
| *aborted = FALSE; |
| err = svn_ra_svn__write_cmd_response(conn, pool, ""); |
| } |
| else |
| err = NULL; |
| } |
| |
| if (err && err->apr_err == SVN_ERR_RA_SVN_CMD_ERR) |
| { |
| if (aborted) |
| *aborted = TRUE; |
| if (!state.done) |
| { |
| /* Abort the edit and use non-blocking I/O to write the error. */ |
| if (editor) |
| { |
| err = svn_error_compose_create( |
| err, |
| svn_error_trace(editor->abort_edit(edit_baton, |
| subpool))); |
| } |
| svn_ra_svn__set_block_handler(conn, blocked_write, &state); |
| } |
| write_err = svn_ra_svn__write_cmd_failure( |
| conn, subpool, |
| svn_ra_svn__locate_real_error_child(err)); |
| if (!write_err) |
| write_err = svn_ra_svn__flush(conn, subpool); |
| svn_ra_svn__set_block_handler(conn, NULL, NULL); |
| svn_error_clear(err); /* We just sent this error */ |
| SVN_ERR(write_err); |
| break; |
| } |
| SVN_ERR(err); |
| } |
| |
| /* Read and discard editing commands until the edit is complete. |
| Hopefully, the other side will call another editor command, run |
| check_for_error, notice the error, write "abort-edit" at us, and |
| throw the error up a few levels on its side (possibly even |
| tossing it right back at us, which is why we can return |
| SVN_NO_ERROR below). |
| |
| However, if the other side is way ahead of us, it might |
| completely finish the edit (or sequence of edit/revprops, for |
| "replay-range") before we send over our "failure". So we should |
| also stop if we see "success". (Then the other side will try to |
| interpret our "failure" as a command, which will itself fail... |
| The net effect is that whatever error we wrote to the other side |
| will be replaced with SVN_ERR_RA_SVN_UNKNOWN_CMD.) |
| */ |
| while (!state.done) |
| { |
| svn_pool_clear(subpool); |
| err = svn_ra_svn__read_tuple(conn, subpool, "wl", &cmd, ¶ms); |
| if (err && err->apr_err == SVN_ERR_RA_SVN_CONNECTION_CLOSED) |
| { |
| /* Other side disconnected; that's no error. */ |
| svn_error_clear(err); |
| svn_pool_destroy(subpool); |
| return SVN_NO_ERROR; |
| } |
| svn_error_clear(err); |
| if (strcmp(cmd, "abort-edit") == 0 |
| || strcmp(cmd, "success") == 0) |
| state.done = TRUE; |
| } |
| |
| svn_pool_destroy(subpool); |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t *svn_ra_svn_drive_editor(svn_ra_svn_conn_t *conn, apr_pool_t *pool, |
| const svn_delta_editor_t *editor, |
| void *edit_baton, |
| svn_boolean_t *aborted) |
| { |
| return svn_ra_svn_drive_editor2(conn, |
| pool, |
| editor, |
| edit_baton, |
| aborted, |
| FALSE); |
| } |