commit.c revision 289166
1/* commit.c --- editor for committing changes to a filesystem.
2 *
3 * ====================================================================
4 *    Licensed to the Apache Software Foundation (ASF) under one
5 *    or more contributor license agreements.  See the NOTICE file
6 *    distributed with this work for additional information
7 *    regarding copyright ownership.  The ASF licenses this file
8 *    to you under the Apache License, Version 2.0 (the
9 *    "License"); you may not use this file except in compliance
10 *    with the License.  You may obtain a copy of the License at
11 *
12 *      http://www.apache.org/licenses/LICENSE-2.0
13 *
14 *    Unless required by applicable law or agreed to in writing,
15 *    software distributed under the License is distributed on an
16 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 *    KIND, either express or implied.  See the License for the
18 *    specific language governing permissions and limitations
19 *    under the License.
20 * ====================================================================
21 */
22
23
24#include <string.h>
25
26#include <apr_pools.h>
27#include <apr_file_io.h>
28
29#include "svn_hash.h"
30#include "svn_compat.h"
31#include "svn_pools.h"
32#include "svn_error.h"
33#include "svn_dirent_uri.h"
34#include "svn_path.h"
35#include "svn_delta.h"
36#include "svn_fs.h"
37#include "svn_repos.h"
38#include "svn_checksum.h"
39#include "svn_ctype.h"
40#include "svn_props.h"
41#include "svn_mergeinfo.h"
42#include "svn_private_config.h"
43
44#include "repos.h"
45
46#include "private/svn_fspath.h"
47#include "private/svn_fs_private.h"
48#include "private/svn_repos_private.h"
49#include "private/svn_editor.h"
50
51
52
53/*** Editor batons. ***/
54
55struct edit_baton
56{
57  apr_pool_t *pool;
58
59  /** Supplied when the editor is created: **/
60
61  /* Revision properties to set for this commit. */
62  apr_hash_t *revprop_table;
63
64  /* Callback to run when the commit is done. */
65  svn_commit_callback2_t commit_callback;
66  void *commit_callback_baton;
67
68  /* Callback to check authorizations on paths. */
69  svn_repos_authz_callback_t authz_callback;
70  void *authz_baton;
71
72  /* The already-open svn repository to commit to. */
73  svn_repos_t *repos;
74
75  /* URL to the root of the open repository. */
76  const char *repos_url;
77
78  /* The name of the repository (here for convenience). */
79  const char *repos_name;
80
81  /* The filesystem associated with the REPOS above (here for
82     convenience). */
83  svn_fs_t *fs;
84
85  /* Location in fs where the edit will begin. */
86  const char *base_path;
87
88  /* Does this set of interfaces 'own' the commit transaction? */
89  svn_boolean_t txn_owner;
90
91  /* svn transaction associated with this edit (created in
92     open_root, or supplied by the public API caller). */
93  svn_fs_txn_t *txn;
94
95  /** Filled in during open_root: **/
96
97  /* The name of the transaction. */
98  const char *txn_name;
99
100  /* The object representing the root directory of the svn txn. */
101  svn_fs_root_t *txn_root;
102
103  /* Avoid aborting an fs transaction more than once */
104  svn_boolean_t txn_aborted;
105
106  /** Filled in when the edit is closed: **/
107
108  /* The new revision created by this commit. */
109  svn_revnum_t *new_rev;
110
111  /* The date (according to the repository) of this commit. */
112  const char **committed_date;
113
114  /* The author (also according to the repository) of this commit. */
115  const char **committed_author;
116};
117
118
119struct dir_baton
120{
121  struct edit_baton *edit_baton;
122  struct dir_baton *parent;
123  const char *path; /* the -absolute- path to this dir in the fs */
124  svn_revnum_t base_rev;        /* the revision I'm based on  */
125  svn_boolean_t was_copied; /* was this directory added with history? */
126  apr_pool_t *pool; /* my personal pool, in which I am allocated. */
127};
128
129
130struct file_baton
131{
132  struct edit_baton *edit_baton;
133  const char *path; /* the -absolute- path to this file in the fs */
134};
135
136
137struct ev2_baton
138{
139  /* The repository we are editing.  */
140  svn_repos_t *repos;
141
142  /* The authz baton for checks; NULL to skip authz.  */
143  svn_authz_t *authz;
144
145  /* The repository name and user for performing authz checks.  */
146  const char *authz_repos_name;
147  const char *authz_user;
148
149  /* Callback to provide info about the committed revision.  */
150  svn_commit_callback2_t commit_cb;
151  void *commit_baton;
152
153  /* The FS txn editor  */
154  svn_editor_t *inner;
155
156  /* The name of the open transaction (so we know what to commit)  */
157  const char *txn_name;
158};
159
160
161/* Create and return a generic out-of-dateness error. */
162static svn_error_t *
163out_of_date(const char *path, svn_node_kind_t kind)
164{
165  return svn_error_createf(SVN_ERR_FS_TXN_OUT_OF_DATE, NULL,
166                           (kind == svn_node_dir
167                            ? _("Directory '%s' is out of date")
168                            : kind == svn_node_file
169                            ? _("File '%s' is out of date")
170                            : _("'%s' is out of date")),
171                           path);
172}
173
174
175static svn_error_t *
176invoke_commit_cb(svn_commit_callback2_t commit_cb,
177                 void *commit_baton,
178                 svn_fs_t *fs,
179                 svn_revnum_t revision,
180                 const char *post_commit_errstr,
181                 apr_pool_t *scratch_pool)
182{
183  /* FS interface returns non-const values.  */
184  /* const */ svn_string_t *date;
185  /* const */ svn_string_t *author;
186  svn_commit_info_t *commit_info;
187
188  if (commit_cb == NULL)
189    return SVN_NO_ERROR;
190
191  SVN_ERR(svn_fs_revision_prop(&date, fs, revision, SVN_PROP_REVISION_DATE,
192                               scratch_pool));
193  SVN_ERR(svn_fs_revision_prop(&author, fs, revision,
194                               SVN_PROP_REVISION_AUTHOR,
195                               scratch_pool));
196
197  commit_info = svn_create_commit_info(scratch_pool);
198
199  /* fill up the svn_commit_info structure */
200  commit_info->revision = revision;
201  commit_info->date = date ? date->data : NULL;
202  commit_info->author = author ? author->data : NULL;
203  commit_info->post_commit_err = post_commit_errstr;
204
205  return svn_error_trace(commit_cb(commit_info, commit_baton, scratch_pool));
206}
207
208
209
210/* If EDITOR_BATON contains a valid authz callback, verify that the
211   REQUIRED access to PATH in ROOT is authorized.  Return an error
212   appropriate for throwing out of the commit editor with SVN_ERR.  If
213   no authz callback is present in EDITOR_BATON, then authorize all
214   paths.  Use POOL for temporary allocation only. */
215static svn_error_t *
216check_authz(struct edit_baton *editor_baton, const char *path,
217            svn_fs_root_t *root, svn_repos_authz_access_t required,
218            apr_pool_t *pool)
219{
220  if (editor_baton->authz_callback)
221    {
222      svn_boolean_t allowed;
223
224      SVN_ERR(editor_baton->authz_callback(required, &allowed, root, path,
225                                           editor_baton->authz_baton, pool));
226      if (!allowed)
227        return svn_error_create(required & svn_authz_write ?
228                                SVN_ERR_AUTHZ_UNWRITABLE :
229                                SVN_ERR_AUTHZ_UNREADABLE,
230                                NULL, "Access denied");
231    }
232
233  return SVN_NO_ERROR;
234}
235
236
237/* Return a directory baton allocated in POOL which represents
238   FULL_PATH, which is the immediate directory child of the directory
239   represented by PARENT_BATON.  EDIT_BATON is the commit editor
240   baton.  WAS_COPIED reveals whether or not this directory is the
241   result of a copy operation.  BASE_REVISION is the base revision of
242   the directory. */
243static struct dir_baton *
244make_dir_baton(struct edit_baton *edit_baton,
245               struct dir_baton *parent_baton,
246               const char *full_path,
247               svn_boolean_t was_copied,
248               svn_revnum_t base_revision,
249               apr_pool_t *pool)
250{
251  struct dir_baton *db;
252  db = apr_pcalloc(pool, sizeof(*db));
253  db->edit_baton = edit_baton;
254  db->parent = parent_baton;
255  db->pool = pool;
256  db->path = full_path;
257  db->was_copied = was_copied;
258  db->base_rev = base_revision;
259  return db;
260}
261
262/* This function is the shared guts of add_file() and add_directory(),
263   which see for the meanings of the parameters.  The only extra
264   parameter here is IS_DIR, which is TRUE when adding a directory,
265   and FALSE when adding a file.  */
266static svn_error_t *
267add_file_or_directory(const char *path,
268                      void *parent_baton,
269                      const char *copy_path,
270                      svn_revnum_t copy_revision,
271                      svn_boolean_t is_dir,
272                      apr_pool_t *pool,
273                      void **return_baton)
274{
275  struct dir_baton *pb = parent_baton;
276  struct edit_baton *eb = pb->edit_baton;
277  apr_pool_t *subpool = svn_pool_create(pool);
278  svn_boolean_t was_copied = FALSE;
279  const char *full_path;
280
281  /* Reject paths which contain control characters (related to issue #4340). */
282  SVN_ERR(svn_path_check_valid(path, pool));
283
284  full_path = svn_fspath__join(eb->base_path,
285                               svn_relpath_canonicalize(path, pool), pool);
286
287  /* Sanity check. */
288  if (copy_path && (! SVN_IS_VALID_REVNUM(copy_revision)))
289    return svn_error_createf
290      (SVN_ERR_FS_GENERAL, NULL,
291       _("Got source path but no source revision for '%s'"), full_path);
292
293  if (copy_path)
294    {
295      const char *fs_path;
296      svn_fs_root_t *copy_root;
297      svn_node_kind_t kind;
298      size_t repos_url_len;
299      svn_repos_authz_access_t required;
300
301      /* Copy requires recursive write access to the destination path
302         and write access to the parent path. */
303      required = svn_authz_write | (is_dir ? svn_authz_recursive : 0);
304      SVN_ERR(check_authz(eb, full_path, eb->txn_root,
305                          required, subpool));
306      SVN_ERR(check_authz(eb, pb->path, eb->txn_root,
307                          svn_authz_write, subpool));
308
309      /* Check PATH in our transaction.  Make sure it does not exist
310         unless its parent directory was copied (in which case, the
311         thing might have been copied in as well), else return an
312         out-of-dateness error. */
313      SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, subpool));
314      if ((kind != svn_node_none) && (! pb->was_copied))
315        return svn_error_trace(out_of_date(full_path, kind));
316
317      /* For now, require that the url come from the same repository
318         that this commit is operating on. */
319      copy_path = svn_path_uri_decode(copy_path, subpool);
320      repos_url_len = strlen(eb->repos_url);
321      if (strncmp(copy_path, eb->repos_url, repos_url_len) != 0)
322        return svn_error_createf
323          (SVN_ERR_FS_GENERAL, NULL,
324           _("Source url '%s' is from different repository"), copy_path);
325
326      fs_path = apr_pstrdup(subpool, copy_path + repos_url_len);
327
328      /* Now use the "fs_path" as an absolute path within the
329         repository to make the copy from. */
330      SVN_ERR(svn_fs_revision_root(&copy_root, eb->fs,
331                                   copy_revision, subpool));
332
333      /* Copy also requires (recursive) read access to the source */
334      required = svn_authz_read | (is_dir ? svn_authz_recursive : 0);
335      SVN_ERR(check_authz(eb, fs_path, copy_root, required, subpool));
336
337      SVN_ERR(svn_fs_copy(copy_root, fs_path,
338                          eb->txn_root, full_path, subpool));
339      was_copied = TRUE;
340    }
341  else
342    {
343      /* No ancestry given, just make a new directory or empty file.
344         Note that we don't perform an existence check here like the
345         copy-from case does -- that's because svn_fs_make_*()
346         already errors out if the file already exists.  Verify write
347         access to the full path and to the parent. */
348      SVN_ERR(check_authz(eb, full_path, eb->txn_root,
349                          svn_authz_write, subpool));
350      SVN_ERR(check_authz(eb, pb->path, eb->txn_root,
351                          svn_authz_write, subpool));
352      if (is_dir)
353        SVN_ERR(svn_fs_make_dir(eb->txn_root, full_path, subpool));
354      else
355        SVN_ERR(svn_fs_make_file(eb->txn_root, full_path, subpool));
356    }
357
358  /* Cleanup our temporary subpool. */
359  svn_pool_destroy(subpool);
360
361  /* Build a new child baton. */
362  if (is_dir)
363    {
364      *return_baton = make_dir_baton(eb, pb, full_path, was_copied,
365                                     SVN_INVALID_REVNUM, pool);
366    }
367  else
368    {
369      struct file_baton *new_fb = apr_pcalloc(pool, sizeof(*new_fb));
370      new_fb->edit_baton = eb;
371      new_fb->path = full_path;
372      *return_baton = new_fb;
373    }
374
375  return SVN_NO_ERROR;
376}
377
378
379
380/*** Editor functions ***/
381
382static svn_error_t *
383open_root(void *edit_baton,
384          svn_revnum_t base_revision,
385          apr_pool_t *pool,
386          void **root_baton)
387{
388  struct dir_baton *dirb;
389  struct edit_baton *eb = edit_baton;
390  svn_revnum_t youngest;
391
392  /* Ignore BASE_REVISION.  We always build our transaction against
393     HEAD.  However, we will keep it in our dir baton for out of
394     dateness checks.  */
395  SVN_ERR(svn_fs_youngest_rev(&youngest, eb->fs, eb->pool));
396
397  /* Unless we've been instructed to use a specific transaction, we'll
398     make our own. */
399  if (eb->txn_owner)
400    {
401      SVN_ERR(svn_repos_fs_begin_txn_for_commit2(&(eb->txn),
402                                                 eb->repos,
403                                                 youngest,
404                                                 eb->revprop_table,
405                                                 eb->pool));
406    }
407  else /* Even if we aren't the owner of the transaction, we might
408          have been instructed to set some properties. */
409    {
410      apr_array_header_t *props = svn_prop_hash_to_array(eb->revprop_table,
411                                                         pool);
412      SVN_ERR(svn_repos_fs_change_txn_props(eb->txn, props, pool));
413    }
414  SVN_ERR(svn_fs_txn_name(&(eb->txn_name), eb->txn, eb->pool));
415  SVN_ERR(svn_fs_txn_root(&(eb->txn_root), eb->txn, eb->pool));
416
417  /* Create a root dir baton.  The `base_path' field is an -absolute-
418     path in the filesystem, upon which all further editor paths are
419     based. */
420  dirb = apr_pcalloc(pool, sizeof(*dirb));
421  dirb->edit_baton = edit_baton;
422  dirb->parent = NULL;
423  dirb->pool = pool;
424  dirb->was_copied = FALSE;
425  dirb->path = apr_pstrdup(pool, eb->base_path);
426  dirb->base_rev = base_revision;
427
428  *root_baton = dirb;
429  return SVN_NO_ERROR;
430}
431
432
433
434static svn_error_t *
435delete_entry(const char *path,
436             svn_revnum_t revision,
437             void *parent_baton,
438             apr_pool_t *pool)
439{
440  struct dir_baton *parent = parent_baton;
441  struct edit_baton *eb = parent->edit_baton;
442  svn_node_kind_t kind;
443  svn_revnum_t cr_rev;
444  svn_repos_authz_access_t required = svn_authz_write;
445  const char *full_path;
446
447  full_path = svn_fspath__join(eb->base_path,
448                               svn_relpath_canonicalize(path, pool), pool);
449
450  /* Check PATH in our transaction.  */
451  SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, pool));
452
453  /* Deletion requires a recursive write access, as well as write
454     access to the parent directory. */
455  if (kind == svn_node_dir)
456    required |= svn_authz_recursive;
457  SVN_ERR(check_authz(eb, full_path, eb->txn_root,
458                      required, pool));
459  SVN_ERR(check_authz(eb, parent->path, eb->txn_root,
460                      svn_authz_write, pool));
461
462  /* If PATH doesn't exist in the txn, the working copy is out of date. */
463  if (kind == svn_node_none)
464    return svn_error_trace(out_of_date(full_path, kind));
465
466  /* Now, make sure we're deleting the node we *think* we're
467     deleting, else return an out-of-dateness error. */
468  SVN_ERR(svn_fs_node_created_rev(&cr_rev, eb->txn_root, full_path, pool));
469  if (SVN_IS_VALID_REVNUM(revision) && (revision < cr_rev))
470    return svn_error_trace(out_of_date(full_path, kind));
471
472  /* This routine is a mindless wrapper.  We call svn_fs_delete()
473     because that will delete files and recursively delete
474     directories.  */
475  return svn_fs_delete(eb->txn_root, full_path, pool);
476}
477
478
479static svn_error_t *
480add_directory(const char *path,
481              void *parent_baton,
482              const char *copy_path,
483              svn_revnum_t copy_revision,
484              apr_pool_t *pool,
485              void **child_baton)
486{
487  return add_file_or_directory(path, parent_baton, copy_path, copy_revision,
488                               TRUE /* is_dir */, pool, child_baton);
489}
490
491
492static svn_error_t *
493open_directory(const char *path,
494               void *parent_baton,
495               svn_revnum_t base_revision,
496               apr_pool_t *pool,
497               void **child_baton)
498{
499  struct dir_baton *pb = parent_baton;
500  struct edit_baton *eb = pb->edit_baton;
501  svn_node_kind_t kind;
502  const char *full_path;
503
504  full_path = svn_fspath__join(eb->base_path,
505                               svn_relpath_canonicalize(path, pool), pool);
506
507  /* Check PATH in our transaction.  If it does not exist,
508     return a 'Path not present' error. */
509  SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, pool));
510  if (kind == svn_node_none)
511    return svn_error_createf(SVN_ERR_FS_NOT_DIRECTORY, NULL,
512                             _("Path '%s' not present"),
513                             path);
514
515  /* Build a new dir baton for this directory. */
516  *child_baton = make_dir_baton(eb, pb, full_path, pb->was_copied,
517                                base_revision, pool);
518  return SVN_NO_ERROR;
519}
520
521
522static svn_error_t *
523apply_textdelta(void *file_baton,
524                const char *base_checksum,
525                apr_pool_t *pool,
526                svn_txdelta_window_handler_t *handler,
527                void **handler_baton)
528{
529  struct file_baton *fb = file_baton;
530
531  /* Check for write authorization. */
532  SVN_ERR(check_authz(fb->edit_baton, fb->path,
533                      fb->edit_baton->txn_root,
534                      svn_authz_write, pool));
535
536  return svn_fs_apply_textdelta(handler, handler_baton,
537                                fb->edit_baton->txn_root,
538                                fb->path,
539                                base_checksum,
540                                NULL,
541                                pool);
542}
543
544
545static svn_error_t *
546add_file(const char *path,
547         void *parent_baton,
548         const char *copy_path,
549         svn_revnum_t copy_revision,
550         apr_pool_t *pool,
551         void **file_baton)
552{
553  return add_file_or_directory(path, parent_baton, copy_path, copy_revision,
554                               FALSE /* is_dir */, pool, file_baton);
555}
556
557
558static svn_error_t *
559open_file(const char *path,
560          void *parent_baton,
561          svn_revnum_t base_revision,
562          apr_pool_t *pool,
563          void **file_baton)
564{
565  struct file_baton *new_fb;
566  struct dir_baton *pb = parent_baton;
567  struct edit_baton *eb = pb->edit_baton;
568  svn_revnum_t cr_rev;
569  apr_pool_t *subpool = svn_pool_create(pool);
570  const char *full_path;
571
572  full_path = svn_fspath__join(eb->base_path,
573                               svn_relpath_canonicalize(path, pool), pool);
574
575  /* Check for read authorization. */
576  SVN_ERR(check_authz(eb, full_path, eb->txn_root,
577                      svn_authz_read, subpool));
578
579  /* Get this node's creation revision (doubles as an existence check). */
580  SVN_ERR(svn_fs_node_created_rev(&cr_rev, eb->txn_root, full_path,
581                                  subpool));
582
583  /* If the node our caller has is an older revision number than the
584     one in our transaction, return an out-of-dateness error. */
585  if (SVN_IS_VALID_REVNUM(base_revision) && (base_revision < cr_rev))
586    return svn_error_trace(out_of_date(full_path, svn_node_file));
587
588  /* Build a new file baton */
589  new_fb = apr_pcalloc(pool, sizeof(*new_fb));
590  new_fb->edit_baton = eb;
591  new_fb->path = full_path;
592
593  *file_baton = new_fb;
594
595  /* Destory the work subpool. */
596  svn_pool_destroy(subpool);
597
598  return SVN_NO_ERROR;
599}
600
601
602static svn_error_t *
603change_file_prop(void *file_baton,
604                 const char *name,
605                 const svn_string_t *value,
606                 apr_pool_t *pool)
607{
608  struct file_baton *fb = file_baton;
609  struct edit_baton *eb = fb->edit_baton;
610
611  /* Check for write authorization. */
612  SVN_ERR(check_authz(eb, fb->path, eb->txn_root,
613                      svn_authz_write, pool));
614
615  return svn_repos_fs_change_node_prop(eb->txn_root, fb->path,
616                                       name, value, pool);
617}
618
619
620static svn_error_t *
621close_file(void *file_baton,
622           const char *text_digest,
623           apr_pool_t *pool)
624{
625  struct file_baton *fb = file_baton;
626
627  if (text_digest)
628    {
629      svn_checksum_t *checksum;
630      svn_checksum_t *text_checksum;
631
632      SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5,
633                                   fb->edit_baton->txn_root, fb->path,
634                                   TRUE, pool));
635      SVN_ERR(svn_checksum_parse_hex(&text_checksum, svn_checksum_md5,
636                                     text_digest, pool));
637
638      if (!svn_checksum_match(text_checksum, checksum))
639        return svn_checksum_mismatch_err(text_checksum, checksum, pool,
640                            _("Checksum mismatch for resulting fulltext\n(%s)"),
641                            fb->path);
642    }
643
644  return SVN_NO_ERROR;
645}
646
647
648static svn_error_t *
649change_dir_prop(void *dir_baton,
650                const char *name,
651                const svn_string_t *value,
652                apr_pool_t *pool)
653{
654  struct dir_baton *db = dir_baton;
655  struct edit_baton *eb = db->edit_baton;
656
657  /* Check for write authorization. */
658  SVN_ERR(check_authz(eb, db->path, eb->txn_root,
659                      svn_authz_write, pool));
660
661  if (SVN_IS_VALID_REVNUM(db->base_rev))
662    {
663      /* Subversion rule:  propchanges can only happen on a directory
664         which is up-to-date. */
665      svn_revnum_t created_rev;
666      SVN_ERR(svn_fs_node_created_rev(&created_rev,
667                                      eb->txn_root, db->path, pool));
668
669      if (db->base_rev < created_rev)
670        return svn_error_trace(out_of_date(db->path, svn_node_dir));
671    }
672
673  return svn_repos_fs_change_node_prop(eb->txn_root, db->path,
674                                       name, value, pool);
675}
676
677const char *
678svn_repos__post_commit_error_str(svn_error_t *err,
679                                 apr_pool_t *pool)
680{
681  svn_error_t *hook_err1, *hook_err2;
682  const char *msg;
683
684  if (! err)
685    return _("(no error)");
686
687  err = svn_error_purge_tracing(err);
688
689  /* hook_err1 is the SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED wrapped
690     error from the post-commit script, if any, and hook_err2 should
691     be the original error, but be defensive and handle a case where
692     SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED doesn't wrap an error. */
693  hook_err1 = svn_error_find_cause(err, SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED);
694  if (hook_err1 && hook_err1->child)
695    hook_err2 = hook_err1->child;
696  else
697    hook_err2 = hook_err1;
698
699  /* This implementation counts on svn_repos_fs_commit_txn() and
700     libsvn_repos/commit.c:complete_cb() returning
701     svn_fs_commit_txn() as the parent error with a child
702     SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED error.  If the parent error
703     is SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED then there was no error
704     in svn_fs_commit_txn().
705
706     The post-commit hook error message is already self describing, so
707     it can be dropped into an error message without any additional
708     text. */
709  if (hook_err1)
710    {
711      if (err == hook_err1)
712        {
713          if (hook_err2->message)
714            msg = apr_pstrdup(pool, hook_err2->message);
715          else
716            msg = _("post-commit hook failed with no error message.");
717        }
718      else
719        {
720          msg = hook_err2->message
721                  ? apr_pstrdup(pool, hook_err2->message)
722                  : _("post-commit hook failed with no error message.");
723          msg = apr_psprintf(
724                  pool,
725                  _("post commit FS processing had error:\n%s\n%s"),
726                  err->message ? err->message : _("(no error message)"),
727                  msg);
728        }
729    }
730  else
731    {
732      msg = apr_psprintf(pool,
733                         _("post commit FS processing had error:\n%s"),
734                         err->message ? err->message
735                                      : _("(no error message)"));
736    }
737
738  return msg;
739}
740
741static svn_error_t *
742close_edit(void *edit_baton,
743           apr_pool_t *pool)
744{
745  struct edit_baton *eb = edit_baton;
746  svn_revnum_t new_revision = SVN_INVALID_REVNUM;
747  svn_error_t *err;
748  const char *conflict;
749  const char *post_commit_err = NULL;
750
751  /* If no transaction has been created (ie. if open_root wasn't
752     called before close_edit), abort the operation here with an
753     error. */
754  if (! eb->txn)
755    return svn_error_create(SVN_ERR_REPOS_BAD_ARGS, NULL,
756                            "No valid transaction supplied to close_edit");
757
758  /* Commit. */
759  err = svn_repos_fs_commit_txn(&conflict, eb->repos,
760                                &new_revision, eb->txn, pool);
761
762  if (SVN_IS_VALID_REVNUM(new_revision))
763    {
764      /* The actual commit succeeded, i.e. the transaction does no longer
765         exist and we can't use txn_root for conflict resolution etc.
766
767         Since close_edit is supposed to release resources, do it now. */
768      if (eb->txn_root)
769        svn_fs_close_root(eb->txn_root);
770
771      if (err)
772        {
773          /* If the error was in post-commit, then the commit itself
774             succeeded.  In which case, save the post-commit warning
775             (to be reported back to the client, who will probably
776             display it as a warning) and clear the error. */
777          post_commit_err = svn_repos__post_commit_error_str(err, pool);
778          svn_error_clear(err);
779        }
780
781      /* Make sure a future abort doesn't perform
782         any work. This may occur if the commit
783         callback returns an error! */
784
785      eb->txn = NULL;
786      eb->txn_root = NULL;
787    }
788  else
789    {
790      /* ### todo: we should check whether it really was a conflict,
791         and return the conflict info if so? */
792
793      /* If the commit failed, it's *probably* due to a conflict --
794         that is, the txn being out-of-date.  The filesystem gives us
795         the ability to continue diddling the transaction and try
796         again; but let's face it: that's not how the cvs or svn works
797         from a user interface standpoint.  Thus we don't make use of
798         this fs feature (for now, at least.)
799
800         So, in a nutshell: svn commits are an all-or-nothing deal.
801         Each commit creates a new fs txn which either succeeds or is
802         aborted completely.  No second chances;  the user simply
803         needs to update and commit again  :) */
804
805      eb->txn_aborted = TRUE;
806
807      return svn_error_trace(
808                svn_error_compose_create(err,
809                                         svn_fs_abort_txn(eb->txn, pool)));
810    }
811
812  /* At this point, the post-commit error has been converted to a string.
813     That information will be passed to a callback, if provided. If the
814     callback invocation fails in some way, that failure is returned here.
815     IOW, the post-commit error information is low priority compared to
816     other gunk here.  */
817
818  /* Pass new revision information to the caller's callback. */
819  return svn_error_trace(invoke_commit_cb(eb->commit_callback,
820                                          eb->commit_callback_baton,
821                                          eb->repos->fs,
822                                          new_revision,
823                                          post_commit_err,
824                                          pool));
825}
826
827
828static svn_error_t *
829abort_edit(void *edit_baton,
830           apr_pool_t *pool)
831{
832  struct edit_baton *eb = edit_baton;
833  if ((! eb->txn) || (! eb->txn_owner) || eb->txn_aborted)
834    return SVN_NO_ERROR;
835
836  eb->txn_aborted = TRUE;
837
838  /* Since abort_edit is supposed to release resources, do it now. */
839  if (eb->txn_root)
840    svn_fs_close_root(eb->txn_root);
841
842  return svn_error_trace(svn_fs_abort_txn(eb->txn, pool));
843}
844
845
846static svn_error_t *
847fetch_props_func(apr_hash_t **props,
848                 void *baton,
849                 const char *path,
850                 svn_revnum_t base_revision,
851                 apr_pool_t *result_pool,
852                 apr_pool_t *scratch_pool)
853{
854  struct edit_baton *eb = baton;
855  svn_fs_root_t *fs_root;
856  svn_error_t *err;
857
858  SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs,
859                               svn_fs_txn_base_revision(eb->txn),
860                               scratch_pool));
861  err = svn_fs_node_proplist(props, fs_root, path, result_pool);
862  if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
863    {
864      svn_error_clear(err);
865      *props = apr_hash_make(result_pool);
866      return SVN_NO_ERROR;
867    }
868  else if (err)
869    return svn_error_trace(err);
870
871  return SVN_NO_ERROR;
872}
873
874static svn_error_t *
875fetch_kind_func(svn_node_kind_t *kind,
876                void *baton,
877                const char *path,
878                svn_revnum_t base_revision,
879                apr_pool_t *scratch_pool)
880{
881  struct edit_baton *eb = baton;
882  svn_fs_root_t *fs_root;
883
884  if (!SVN_IS_VALID_REVNUM(base_revision))
885    base_revision = svn_fs_txn_base_revision(eb->txn);
886
887  SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool));
888
889  SVN_ERR(svn_fs_check_path(kind, fs_root, path, scratch_pool));
890
891  return SVN_NO_ERROR;
892}
893
894static svn_error_t *
895fetch_base_func(const char **filename,
896                void *baton,
897                const char *path,
898                svn_revnum_t base_revision,
899                apr_pool_t *result_pool,
900                apr_pool_t *scratch_pool)
901{
902  struct edit_baton *eb = baton;
903  svn_stream_t *contents;
904  svn_stream_t *file_stream;
905  const char *tmp_filename;
906  svn_fs_root_t *fs_root;
907  svn_error_t *err;
908
909  if (!SVN_IS_VALID_REVNUM(base_revision))
910    base_revision = svn_fs_txn_base_revision(eb->txn);
911
912  SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool));
913
914  err = svn_fs_file_contents(&contents, fs_root, path, scratch_pool);
915  if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
916    {
917      svn_error_clear(err);
918      *filename = NULL;
919      return SVN_NO_ERROR;
920    }
921  else if (err)
922    return svn_error_trace(err);
923  SVN_ERR(svn_stream_open_unique(&file_stream, &tmp_filename, NULL,
924                                 svn_io_file_del_on_pool_cleanup,
925                                 scratch_pool, scratch_pool));
926  SVN_ERR(svn_stream_copy3(contents, file_stream, NULL, NULL, scratch_pool));
927
928  *filename = apr_pstrdup(result_pool, tmp_filename);
929
930  return SVN_NO_ERROR;
931}
932
933
934
935/*** Public interfaces. ***/
936
937svn_error_t *
938svn_repos_get_commit_editor5(const svn_delta_editor_t **editor,
939                             void **edit_baton,
940                             svn_repos_t *repos,
941                             svn_fs_txn_t *txn,
942                             const char *repos_url,
943                             const char *base_path,
944                             apr_hash_t *revprop_table,
945                             svn_commit_callback2_t commit_callback,
946                             void *commit_baton,
947                             svn_repos_authz_callback_t authz_callback,
948                             void *authz_baton,
949                             apr_pool_t *pool)
950{
951  svn_delta_editor_t *e;
952  apr_pool_t *subpool = svn_pool_create(pool);
953  struct edit_baton *eb;
954  svn_delta_shim_callbacks_t *shim_callbacks =
955                                    svn_delta_shim_callbacks_default(pool);
956
957  /* Do a global authz access lookup.  Users with no write access
958     whatsoever to the repository don't get a commit editor. */
959  if (authz_callback)
960    {
961      svn_boolean_t allowed;
962
963      SVN_ERR(authz_callback(svn_authz_write, &allowed, NULL, NULL,
964                             authz_baton, pool));
965      if (!allowed)
966        return svn_error_create(SVN_ERR_AUTHZ_UNWRITABLE, NULL,
967                                "Not authorized to open a commit editor.");
968    }
969
970  /* Allocate the structures. */
971  e = svn_delta_default_editor(pool);
972  eb = apr_pcalloc(subpool, sizeof(*eb));
973
974  /* Set up the editor. */
975  e->open_root         = open_root;
976  e->delete_entry      = delete_entry;
977  e->add_directory     = add_directory;
978  e->open_directory    = open_directory;
979  e->change_dir_prop   = change_dir_prop;
980  e->add_file          = add_file;
981  e->open_file         = open_file;
982  e->close_file        = close_file;
983  e->apply_textdelta   = apply_textdelta;
984  e->change_file_prop  = change_file_prop;
985  e->close_edit        = close_edit;
986  e->abort_edit        = abort_edit;
987
988  /* Set up the edit baton. */
989  eb->pool = subpool;
990  eb->revprop_table = svn_prop_hash_dup(revprop_table, subpool);
991  eb->commit_callback = commit_callback;
992  eb->commit_callback_baton = commit_baton;
993  eb->authz_callback = authz_callback;
994  eb->authz_baton = authz_baton;
995  eb->base_path = svn_fspath__canonicalize(base_path, subpool);
996  eb->repos = repos;
997  eb->repos_url = repos_url;
998  eb->repos_name = svn_dirent_basename(svn_repos_path(repos, subpool),
999                                       subpool);
1000  eb->fs = svn_repos_fs(repos);
1001  eb->txn = txn;
1002  eb->txn_owner = txn == NULL;
1003
1004  *edit_baton = eb;
1005  *editor = e;
1006
1007  shim_callbacks->fetch_props_func = fetch_props_func;
1008  shim_callbacks->fetch_kind_func = fetch_kind_func;
1009  shim_callbacks->fetch_base_func = fetch_base_func;
1010  shim_callbacks->fetch_baton = eb;
1011
1012  SVN_ERR(svn_editor__insert_shims(editor, edit_baton, *editor, *edit_baton,
1013                                   eb->repos_url, eb->base_path,
1014                                   shim_callbacks, pool, pool));
1015
1016  return SVN_NO_ERROR;
1017}
1018
1019
1020#if 0
1021static svn_error_t *
1022ev2_check_authz(const struct ev2_baton *eb,
1023                const char *relpath,
1024                svn_repos_authz_access_t required,
1025                apr_pool_t *scratch_pool)
1026{
1027  const char *fspath;
1028  svn_boolean_t allowed;
1029
1030  if (eb->authz == NULL)
1031    return SVN_NO_ERROR;
1032
1033  if (relpath)
1034    fspath = apr_pstrcat(scratch_pool, "/", relpath, NULL);
1035  else
1036    fspath = NULL;
1037
1038  SVN_ERR(svn_repos_authz_check_access(eb->authz, eb->authz_repos_name, fspath,
1039                                       eb->authz_user, required,
1040                                       &allowed, scratch_pool));
1041  if (!allowed)
1042    return svn_error_create(required & svn_authz_write
1043                            ? SVN_ERR_AUTHZ_UNWRITABLE
1044                            : SVN_ERR_AUTHZ_UNREADABLE,
1045                            NULL, "Access denied");
1046
1047  return SVN_NO_ERROR;
1048}
1049#endif
1050
1051
1052/* This implements svn_editor_cb_add_directory_t */
1053static svn_error_t *
1054add_directory_cb(void *baton,
1055                 const char *relpath,
1056                 const apr_array_header_t *children,
1057                 apr_hash_t *props,
1058                 svn_revnum_t replaces_rev,
1059                 apr_pool_t *scratch_pool)
1060{
1061  struct ev2_baton *eb = baton;
1062
1063  SVN_ERR(svn_editor_add_directory(eb->inner, relpath, children, props,
1064                                   replaces_rev));
1065  return SVN_NO_ERROR;
1066}
1067
1068
1069/* This implements svn_editor_cb_add_file_t */
1070static svn_error_t *
1071add_file_cb(void *baton,
1072            const char *relpath,
1073            const svn_checksum_t *checksum,
1074            svn_stream_t *contents,
1075            apr_hash_t *props,
1076            svn_revnum_t replaces_rev,
1077            apr_pool_t *scratch_pool)
1078{
1079  struct ev2_baton *eb = baton;
1080
1081  SVN_ERR(svn_editor_add_file(eb->inner, relpath, checksum, contents, props,
1082                              replaces_rev));
1083  return SVN_NO_ERROR;
1084}
1085
1086
1087/* This implements svn_editor_cb_add_symlink_t */
1088static svn_error_t *
1089add_symlink_cb(void *baton,
1090               const char *relpath,
1091               const char *target,
1092               apr_hash_t *props,
1093               svn_revnum_t replaces_rev,
1094               apr_pool_t *scratch_pool)
1095{
1096  struct ev2_baton *eb = baton;
1097
1098  SVN_ERR(svn_editor_add_symlink(eb->inner, relpath, target, props,
1099                                 replaces_rev));
1100  return SVN_NO_ERROR;
1101}
1102
1103
1104/* This implements svn_editor_cb_add_absent_t */
1105static svn_error_t *
1106add_absent_cb(void *baton,
1107              const char *relpath,
1108              svn_node_kind_t kind,
1109              svn_revnum_t replaces_rev,
1110              apr_pool_t *scratch_pool)
1111{
1112  struct ev2_baton *eb = baton;
1113
1114  SVN_ERR(svn_editor_add_absent(eb->inner, relpath, kind, replaces_rev));
1115  return SVN_NO_ERROR;
1116}
1117
1118
1119/* This implements svn_editor_cb_alter_directory_t */
1120static svn_error_t *
1121alter_directory_cb(void *baton,
1122                   const char *relpath,
1123                   svn_revnum_t revision,
1124                   const apr_array_header_t *children,
1125                   apr_hash_t *props,
1126                   apr_pool_t *scratch_pool)
1127{
1128  struct ev2_baton *eb = baton;
1129
1130  SVN_ERR(svn_editor_alter_directory(eb->inner, relpath, revision,
1131                                     children, props));
1132  return SVN_NO_ERROR;
1133}
1134
1135
1136/* This implements svn_editor_cb_alter_file_t */
1137static svn_error_t *
1138alter_file_cb(void *baton,
1139              const char *relpath,
1140              svn_revnum_t revision,
1141              apr_hash_t *props,
1142              const svn_checksum_t *checksum,
1143              svn_stream_t *contents,
1144              apr_pool_t *scratch_pool)
1145{
1146  struct ev2_baton *eb = baton;
1147
1148  SVN_ERR(svn_editor_alter_file(eb->inner, relpath, revision, props,
1149                                checksum, contents));
1150  return SVN_NO_ERROR;
1151}
1152
1153
1154/* This implements svn_editor_cb_alter_symlink_t */
1155static svn_error_t *
1156alter_symlink_cb(void *baton,
1157                 const char *relpath,
1158                 svn_revnum_t revision,
1159                 apr_hash_t *props,
1160                 const char *target,
1161                 apr_pool_t *scratch_pool)
1162{
1163  struct ev2_baton *eb = baton;
1164
1165  SVN_ERR(svn_editor_alter_symlink(eb->inner, relpath, revision, props,
1166                                   target));
1167  return SVN_NO_ERROR;
1168}
1169
1170
1171/* This implements svn_editor_cb_delete_t */
1172static svn_error_t *
1173delete_cb(void *baton,
1174          const char *relpath,
1175          svn_revnum_t revision,
1176          apr_pool_t *scratch_pool)
1177{
1178  struct ev2_baton *eb = baton;
1179
1180  SVN_ERR(svn_editor_delete(eb->inner, relpath, revision));
1181  return SVN_NO_ERROR;
1182}
1183
1184
1185/* This implements svn_editor_cb_copy_t */
1186static svn_error_t *
1187copy_cb(void *baton,
1188        const char *src_relpath,
1189        svn_revnum_t src_revision,
1190        const char *dst_relpath,
1191        svn_revnum_t replaces_rev,
1192        apr_pool_t *scratch_pool)
1193{
1194  struct ev2_baton *eb = baton;
1195
1196  SVN_ERR(svn_editor_copy(eb->inner, src_relpath, src_revision, dst_relpath,
1197                          replaces_rev));
1198  return SVN_NO_ERROR;
1199}
1200
1201
1202/* This implements svn_editor_cb_move_t */
1203static svn_error_t *
1204move_cb(void *baton,
1205        const char *src_relpath,
1206        svn_revnum_t src_revision,
1207        const char *dst_relpath,
1208        svn_revnum_t replaces_rev,
1209        apr_pool_t *scratch_pool)
1210{
1211  struct ev2_baton *eb = baton;
1212
1213  SVN_ERR(svn_editor_move(eb->inner, src_relpath, src_revision, dst_relpath,
1214                          replaces_rev));
1215  return SVN_NO_ERROR;
1216}
1217
1218
1219/* This implements svn_editor_cb_rotate_t */
1220static svn_error_t *
1221rotate_cb(void *baton,
1222          const apr_array_header_t *relpaths,
1223          const apr_array_header_t *revisions,
1224          apr_pool_t *scratch_pool)
1225{
1226  struct ev2_baton *eb = baton;
1227
1228  SVN_ERR(svn_editor_rotate(eb->inner, relpaths, revisions));
1229  return SVN_NO_ERROR;
1230}
1231
1232
1233/* This implements svn_editor_cb_complete_t */
1234static svn_error_t *
1235complete_cb(void *baton,
1236            apr_pool_t *scratch_pool)
1237{
1238  struct ev2_baton *eb = baton;
1239  svn_revnum_t revision;
1240  svn_error_t *post_commit_err;
1241  const char *conflict_path;
1242  svn_error_t *err;
1243  const char *post_commit_errstr;
1244  apr_hash_t *hooks_env;
1245
1246  /* Parse the hooks-env file (if any). */
1247  SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, eb->repos->hooks_env_path,
1248                                     scratch_pool, scratch_pool));
1249
1250  /* The transaction has been fully edited. Let the pre-commit hook
1251     have a look at the thing.  */
1252  SVN_ERR(svn_repos__hooks_pre_commit(eb->repos, hooks_env,
1253                                      eb->txn_name, scratch_pool));
1254
1255  /* Hook is done. Let's do the actual commit.  */
1256  SVN_ERR(svn_fs__editor_commit(&revision, &post_commit_err, &conflict_path,
1257                                eb->inner, scratch_pool, scratch_pool));
1258
1259  /* Did a conflict occur during the commit process?  */
1260  if (conflict_path != NULL)
1261    return svn_error_createf(SVN_ERR_FS_CONFLICT, NULL,
1262                             _("Conflict at '%s'"), conflict_path);
1263
1264  /* Since did not receive an error during the commit process, and no
1265     conflict was specified... we committed a revision. Run the hooks.
1266     Other errors may have occurred within the FS (specified by the
1267     POST_COMMIT_ERR localvar), but we need to run the hooks.  */
1268  SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(revision));
1269  err = svn_repos__hooks_post_commit(eb->repos, hooks_env, revision,
1270                                     eb->txn_name, scratch_pool);
1271  if (err)
1272    err = svn_error_create(SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED, err,
1273                           _("Commit succeeded, but post-commit hook failed"));
1274
1275  /* Combine the FS errors with the hook errors, and stringify.  */
1276  err = svn_error_compose_create(post_commit_err, err);
1277  if (err)
1278    {
1279      post_commit_errstr = svn_repos__post_commit_error_str(err, scratch_pool);
1280      svn_error_clear(err);
1281    }
1282  else
1283    {
1284      post_commit_errstr = NULL;
1285    }
1286
1287  return svn_error_trace(invoke_commit_cb(eb->commit_cb, eb->commit_baton,
1288                                          eb->repos->fs, revision,
1289                                          post_commit_errstr,
1290                                          scratch_pool));
1291}
1292
1293
1294/* This implements svn_editor_cb_abort_t */
1295static svn_error_t *
1296abort_cb(void *baton,
1297         apr_pool_t *scratch_pool)
1298{
1299  struct ev2_baton *eb = baton;
1300
1301  SVN_ERR(svn_editor_abort(eb->inner));
1302  return SVN_NO_ERROR;
1303}
1304
1305
1306static svn_error_t *
1307apply_revprops(svn_fs_t *fs,
1308               const char *txn_name,
1309               apr_hash_t *revprops,
1310               apr_pool_t *scratch_pool)
1311{
1312  svn_fs_txn_t *txn;
1313  const apr_array_header_t *revprops_array;
1314
1315  /* The FS editor has a TXN inside it, but we can't access it. Open another
1316     based on the TXN_NAME.  */
1317  SVN_ERR(svn_fs_open_txn(&txn, fs, txn_name, scratch_pool));
1318
1319  /* Validate and apply the revision properties.  */
1320  revprops_array = svn_prop_hash_to_array(revprops, scratch_pool);
1321  SVN_ERR(svn_repos_fs_change_txn_props(txn, revprops_array, scratch_pool));
1322
1323  /* ### do we need to force the txn to close, or is it enough to wait
1324     ### for the pool to be cleared?  */
1325  return SVN_NO_ERROR;
1326}
1327
1328
1329svn_error_t *
1330svn_repos__get_commit_ev2(svn_editor_t **editor,
1331                          svn_repos_t *repos,
1332                          svn_authz_t *authz,
1333                          const char *authz_repos_name,
1334                          const char *authz_user,
1335                          apr_hash_t *revprops,
1336                          svn_commit_callback2_t commit_cb,
1337                          void *commit_baton,
1338                          svn_cancel_func_t cancel_func,
1339                          void *cancel_baton,
1340                          apr_pool_t *result_pool,
1341                          apr_pool_t *scratch_pool)
1342{
1343  static const svn_editor_cb_many_t editor_cbs = {
1344    add_directory_cb,
1345    add_file_cb,
1346    add_symlink_cb,
1347    add_absent_cb,
1348    alter_directory_cb,
1349    alter_file_cb,
1350    alter_symlink_cb,
1351    delete_cb,
1352    copy_cb,
1353    move_cb,
1354    rotate_cb,
1355    complete_cb,
1356    abort_cb
1357  };
1358  struct ev2_baton *eb;
1359  const svn_string_t *author;
1360  apr_hash_t *hooks_env;
1361
1362  /* Parse the hooks-env file (if any). */
1363  SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path,
1364                                     scratch_pool, scratch_pool));
1365
1366  /* Can the user modify the repository at all?  */
1367  /* ### check against AUTHZ.  */
1368
1369  author = svn_hash_gets(revprops, SVN_PROP_REVISION_AUTHOR);
1370
1371  eb = apr_palloc(result_pool, sizeof(*eb));
1372  eb->repos = repos;
1373  eb->authz = authz;
1374  eb->authz_repos_name = authz_repos_name;
1375  eb->authz_user = authz_user;
1376  eb->commit_cb = commit_cb;
1377  eb->commit_baton = commit_baton;
1378
1379  SVN_ERR(svn_fs__editor_create(&eb->inner, &eb->txn_name,
1380                                repos->fs, SVN_FS_TXN_CHECK_LOCKS,
1381                                cancel_func, cancel_baton,
1382                                result_pool, scratch_pool));
1383
1384  /* The TXN has been created. Go ahead and apply all revision properties.  */
1385  SVN_ERR(apply_revprops(repos->fs, eb->txn_name, revprops, scratch_pool));
1386
1387  /* Okay... some access is allowed. Let's run the start-commit hook.  */
1388  SVN_ERR(svn_repos__hooks_start_commit(repos, hooks_env,
1389                                        author ? author->data : NULL,
1390                                        repos->client_capabilities,
1391                                        eb->txn_name, scratch_pool));
1392
1393  /* Wrap the FS editor within our editor.  */
1394  SVN_ERR(svn_editor_create(editor, eb, cancel_func, cancel_baton,
1395                            result_pool, scratch_pool));
1396  SVN_ERR(svn_editor_setcb_many(*editor, &editor_cbs, scratch_pool));
1397
1398  return SVN_NO_ERROR;
1399}
1400