1/*
2 * externals.c:  handle the svn:externals property
3 *
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
12 *
13 *      http://www.apache.org/licenses/LICENSE-2.0
14 *
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
20 *    under the License.
21 * ====================================================================
22 */
23
24/* ==================================================================== */
25
26
27
28/*** Includes. ***/
29
30#include <apr_uri.h>
31#include "svn_hash.h"
32#include "svn_wc.h"
33#include "svn_pools.h"
34#include "svn_client.h"
35#include "svn_types.h"
36#include "svn_error.h"
37#include "svn_dirent_uri.h"
38#include "svn_path.h"
39#include "svn_props.h"
40#include "svn_config.h"
41#include "client.h"
42
43#include "svn_private_config.h"
44#include "private/svn_wc_private.h"
45
46
47/* Remove the directory at LOCAL_ABSPATH from revision control, and do the
48 * same to any revision controlled directories underneath LOCAL_ABSPATH
49 * (including directories not referred to by parent svn administrative areas);
50 * then if LOCAL_ABSPATH is empty afterwards, remove it, else rename it to a
51 * unique name in the same parent directory.
52 *
53 * Pass CANCEL_FUNC, CANCEL_BATON to svn_wc_remove_from_revision_control.
54 *
55 * Use SCRATCH_POOL for all temporary allocation.
56 */
57static svn_error_t *
58relegate_dir_external(svn_wc_context_t *wc_ctx,
59                      const char *wri_abspath,
60                      const char *local_abspath,
61                      svn_cancel_func_t cancel_func,
62                      void *cancel_baton,
63                      svn_wc_notify_func2_t notify_func,
64                      void *notify_baton,
65                      apr_pool_t *scratch_pool)
66{
67  svn_error_t *err = SVN_NO_ERROR;
68
69  SVN_ERR(svn_wc__acquire_write_lock(NULL, wc_ctx, local_abspath,
70                                     FALSE, scratch_pool, scratch_pool));
71
72  err = svn_wc__external_remove(wc_ctx, wri_abspath, local_abspath, FALSE,
73                                cancel_func, cancel_baton, scratch_pool);
74  if (err && (err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD))
75    {
76      const char *parent_dir;
77      const char *dirname;
78      const char *new_path;
79
80      svn_error_clear(err);
81      err = SVN_NO_ERROR;
82
83      svn_dirent_split(&parent_dir, &dirname, local_abspath, scratch_pool);
84
85      /* Reserve the new dir name. */
86      SVN_ERR(svn_io_open_uniquely_named(NULL, &new_path,
87                                         parent_dir, dirname, ".OLD",
88                                         svn_io_file_del_none,
89                                         scratch_pool, scratch_pool));
90
91      /* Sigh...  We must fall ever so slightly from grace.
92
93         Ideally, there would be no window, however brief, when we
94         don't have a reservation on the new name.  Unfortunately,
95         at least in the Unix (Linux?) version of apr_file_rename(),
96         you can't rename a directory over a file, because it's just
97         calling stdio rename(), which says:
98
99            ENOTDIR
100              A  component used as a directory in oldpath or newpath
101              path is not, in fact, a directory.  Or, oldpath  is
102              a directory, and newpath exists but is not a directory
103
104         So instead, we get the name, then remove the file (ugh), then
105         rename the directory, hoping that nobody has gotten that name
106         in the meantime -- which would never happen in real life, so
107         no big deal.
108      */
109      /* Do our best, but no biggy if it fails. The rename will fail. */
110      svn_error_clear(svn_io_remove_file2(new_path, TRUE, scratch_pool));
111
112      /* Rename. If this is still a working copy we should use the working
113         copy rename function (to release open handles) */
114      err = svn_wc__rename_wc(wc_ctx, local_abspath, new_path,
115                              scratch_pool);
116
117      if (err && err->apr_err == SVN_ERR_WC_PATH_UNEXPECTED_STATUS)
118        {
119          svn_error_clear(err);
120
121          /* And if it is no longer a working copy, we should just rename
122             it */
123          err = svn_io_file_rename2(local_abspath, new_path, FALSE, scratch_pool);
124        }
125
126      /* ### TODO: We should notify the user about the rename */
127      if (notify_func)
128        {
129          svn_wc_notify_t *notify;
130
131          notify = svn_wc_create_notify(err ? local_abspath : new_path,
132                                        svn_wc_notify_left_local_modifications,
133                                        scratch_pool);
134          notify->kind = svn_node_dir;
135          notify->err = err;
136
137          notify_func(notify_baton, notify, scratch_pool);
138        }
139    }
140
141  return svn_error_trace(err);
142}
143
144/* Try to update a directory external at PATH to URL at REVISION.
145   Use POOL for temporary allocations, and use the client context CTX. */
146static svn_error_t *
147switch_dir_external(const char *local_abspath,
148                    const char *url,
149                    const char *url_from_externals_definition,
150                    const svn_opt_revision_t *peg_revision,
151                    const svn_opt_revision_t *revision,
152                    const char *defining_abspath,
153                    svn_boolean_t *timestamp_sleep,
154                    svn_ra_session_t *ra_session,
155                    svn_client_ctx_t *ctx,
156                    apr_pool_t *pool)
157{
158  svn_node_kind_t kind;
159  svn_error_t *err;
160  svn_revnum_t external_peg_rev = SVN_INVALID_REVNUM;
161  svn_revnum_t external_rev = SVN_INVALID_REVNUM;
162  apr_pool_t *subpool = svn_pool_create(pool);
163  const char *repos_root_url;
164  const char *repos_uuid;
165
166  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
167
168  if (peg_revision->kind == svn_opt_revision_number)
169    external_peg_rev = peg_revision->value.number;
170
171  if (revision->kind == svn_opt_revision_number)
172    external_rev = revision->value.number;
173
174  /*
175   * The code below assumes existing versioned paths are *not* part of
176   * the external's defining working copy.
177   * The working copy library does not support registering externals
178   * on top of existing BASE nodes and will error out if we try.
179   * So if the external target is part of the defining working copy's
180   * BASE tree, don't attempt to create the external. Doing so would
181   * leave behind a switched path instead of an external (since the
182   * switch succeeds but registration of the external in the DB fails).
183   * The working copy then cannot be updated until the path is switched back.
184   * See issue #4085.
185   */
186  SVN_ERR(svn_wc__node_get_base(&kind, NULL, NULL,
187                                &repos_root_url, &repos_uuid,
188                                NULL, ctx->wc_ctx, local_abspath,
189                                TRUE, /* ignore_enoent */
190                                pool, pool));
191  if (kind != svn_node_unknown)
192    {
193      const char *wcroot_abspath;
194      const char *defining_wcroot_abspath;
195
196      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
197                                 local_abspath, pool, pool));
198      SVN_ERR(svn_wc__get_wcroot(&defining_wcroot_abspath, ctx->wc_ctx,
199                                 defining_abspath, pool, pool));
200      if (strcmp(wcroot_abspath, defining_wcroot_abspath) == 0)
201        return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL,
202                                 _("The external '%s' defined in %s at '%s' "
203                                   "cannot be checked out because '%s' is "
204                                   "already a versioned path."),
205                                   url_from_externals_definition,
206                                   SVN_PROP_EXTERNALS,
207                                   svn_dirent_local_style(defining_abspath,
208                                                          pool),
209                                   svn_dirent_local_style(local_abspath,
210                                                          pool));
211    }
212
213  /* If path is a directory, try to update/switch to the correct URL
214     and revision. */
215  SVN_ERR(svn_io_check_path(local_abspath, &kind, pool));
216  if (kind == svn_node_dir)
217    {
218      const char *node_url;
219
220      /* Doubles as an "is versioned" check. */
221      err = svn_wc__node_get_url(&node_url, ctx->wc_ctx, local_abspath,
222                                 pool, subpool);
223      if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
224        {
225          svn_error_clear(err);
226          err = SVN_NO_ERROR;
227          goto relegate;
228        }
229      else if (err)
230        return svn_error_trace(err);
231
232      if (node_url)
233        {
234          svn_boolean_t is_wcroot;
235
236          SVN_ERR(svn_wc__is_wcroot(&is_wcroot, ctx->wc_ctx, local_abspath,
237                                    pool));
238
239          if (! is_wcroot)
240          {
241            /* This can't be a directory external! */
242
243            err = svn_wc__external_remove(ctx->wc_ctx, defining_abspath,
244                                          local_abspath,
245                                          TRUE /* declaration_only */,
246                                          ctx->cancel_func, ctx->cancel_baton,
247                                          pool);
248
249            if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
250              {
251                /* New external... No problem that we can't remove it */
252                svn_error_clear(err);
253                err = NULL;
254              }
255            else if (err)
256              return svn_error_trace(err);
257
258            return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL,
259                                     _("The external '%s' defined in %s at '%s' "
260                                       "cannot be checked out because '%s' is "
261                                       "already a versioned path."),
262                                     url_from_externals_definition,
263                                     SVN_PROP_EXTERNALS,
264                                     svn_dirent_local_style(defining_abspath,
265                                                            pool),
266                                     svn_dirent_local_style(local_abspath,
267                                                            pool));
268          }
269
270          /* If we have what appears to be a version controlled
271             subdir, and its top-level URL matches that of our
272             externals definition, perform an update. */
273          if (strcmp(node_url, url) == 0)
274            {
275              SVN_ERR(svn_client__update_internal(NULL, timestamp_sleep,
276                                                  local_abspath,
277                                                  revision, svn_depth_unknown,
278                                                  FALSE, FALSE, FALSE, TRUE,
279                                                  FALSE, TRUE,
280                                                  ra_session, ctx, subpool));
281
282              /* We just decided that this existing directory is an external,
283                 so update the external registry with this information, like
284                 when checking out an external */
285              SVN_ERR(svn_wc__external_register(ctx->wc_ctx,
286                                    defining_abspath,
287                                    local_abspath, svn_node_dir,
288                                    repos_root_url, repos_uuid,
289                                    svn_uri_skip_ancestor(repos_root_url,
290                                                          url, pool),
291                                    external_peg_rev,
292                                    external_rev,
293                                    pool));
294
295              svn_pool_destroy(subpool);
296              goto cleanup;
297            }
298
299          /* We'd really prefer not to have to do a brute-force
300             relegation -- blowing away the current external working
301             copy and checking it out anew -- so we'll first see if we
302             can get away with a generally cheaper relocation (if
303             required) and switch-style update.
304
305             To do so, we need to know the repository root URL of the
306             external working copy as it currently sits. */
307          err = svn_wc__node_get_repos_info(NULL, NULL,
308                                            &repos_root_url, &repos_uuid,
309                                            ctx->wc_ctx, local_abspath,
310                                            pool, subpool);
311          if (err)
312            {
313              if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
314                  && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
315                return svn_error_trace(err);
316
317              svn_error_clear(err);
318              repos_root_url = NULL;
319              repos_uuid = NULL;
320            }
321
322          if (repos_root_url)
323            {
324              /* If the new external target URL is not obviously a
325                 child of the external working copy's current
326                 repository root URL... */
327              if (! svn_uri__is_ancestor(repos_root_url, url))
328                {
329                  const char *repos_root;
330
331                  /* ... then figure out precisely which repository
332                      root URL that target URL *is* a child of ... */
333                  SVN_ERR(svn_client_get_repos_root(&repos_root, NULL, url,
334                                                    ctx, subpool, subpool));
335
336                  /* ... and use that to try to relocate the external
337                     working copy to the target location.  */
338                  err = svn_client_relocate2(local_abspath, repos_root_url,
339                                             repos_root, FALSE, ctx, subpool);
340
341                  /* If the relocation failed because the new URL
342                     points to a totally different repository, we've
343                     no choice but to relegate and check out a new WC. */
344                  if (err
345                      && (err->apr_err == SVN_ERR_WC_INVALID_RELOCATION
346                          || (err->apr_err
347                              == SVN_ERR_CLIENT_INVALID_RELOCATION)))
348                    {
349                      svn_error_clear(err);
350                      goto relegate;
351                    }
352                  else if (err)
353                    return svn_error_trace(err);
354
355                  /* If the relocation went without a hitch, we should
356                     have a new repository root URL. */
357                  repos_root_url = repos_root;
358                }
359
360              SVN_ERR(svn_client__switch_internal(NULL, local_abspath, url,
361                                                  peg_revision, revision,
362                                                  svn_depth_infinity,
363                                                  TRUE, FALSE, FALSE,
364                                                  TRUE /* ignore_ancestry */,
365                                                  timestamp_sleep,
366                                                  ctx, subpool));
367
368              SVN_ERR(svn_wc__external_register(ctx->wc_ctx,
369                                                defining_abspath,
370                                                local_abspath, svn_node_dir,
371                                                repos_root_url, repos_uuid,
372                                                svn_uri_skip_ancestor(
373                                                            repos_root_url,
374                                                            url, subpool),
375                                                external_peg_rev,
376                                                external_rev,
377                                                subpool));
378
379              svn_pool_destroy(subpool);
380              goto cleanup;
381            }
382        }
383    }
384
385 relegate:
386
387  /* Fall back on removing the WC and checking out a new one. */
388
389  /* Ensure that we don't have any RA sessions or WC locks from failed
390     operations above. */
391  svn_pool_destroy(subpool);
392
393  if (kind == svn_node_dir)
394    {
395      /* Buh-bye, old and busted ... */
396      SVN_ERR(relegate_dir_external(ctx->wc_ctx, defining_abspath,
397                                    local_abspath,
398                                    ctx->cancel_func, ctx->cancel_baton,
399                                    ctx->notify_func2, ctx->notify_baton2,
400                                    pool));
401    }
402  else
403    {
404      /* The target dir might have multiple components.  Guarantee
405         the path leading down to the last component. */
406      const char *parent = svn_dirent_dirname(local_abspath, pool);
407      SVN_ERR(svn_io_make_dir_recursively(parent, pool));
408    }
409
410  /* ... Hello, new hotness. */
411  SVN_ERR(svn_client__checkout_internal(NULL, timestamp_sleep,
412                                        url, local_abspath, peg_revision,
413                                        revision, svn_depth_infinity,
414                                        FALSE, FALSE,
415                                        ra_session,
416                                        ctx, pool));
417
418  SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL,
419                                      &repos_root_url,
420                                      &repos_uuid,
421                                      ctx->wc_ctx, local_abspath,
422                                      pool, pool));
423
424  SVN_ERR(svn_wc__external_register(ctx->wc_ctx,
425                                    defining_abspath,
426                                    local_abspath, svn_node_dir,
427                                    repos_root_url, repos_uuid,
428                                    svn_uri_skip_ancestor(repos_root_url,
429                                                          url, pool),
430                                    external_peg_rev,
431                                    external_rev,
432                                    pool));
433
434 cleanup:
435  /* Issues #4123 and #4130: We don't need to keep the newly checked
436     out external's DB open. */
437  SVN_ERR(svn_wc__close_db(local_abspath, ctx->wc_ctx, pool));
438
439  return SVN_NO_ERROR;
440}
441
442/* Try to update a file external at LOCAL_ABSPATH to SWITCH_LOC. This function
443   assumes caller has a write lock in CTX.  Use SCRATCH_POOL for temporary
444   allocations, and use the client context CTX. */
445static svn_error_t *
446switch_file_external(const char *local_abspath,
447                     const svn_client__pathrev_t *switch_loc,
448                     const char *record_url,
449                     const svn_opt_revision_t *record_peg_revision,
450                     const svn_opt_revision_t *record_revision,
451                     const char *def_dir_abspath,
452                     svn_ra_session_t *ra_session,
453                     svn_client_ctx_t *ctx,
454                     apr_pool_t *scratch_pool)
455{
456  svn_config_t *cfg = ctx->config
457                      ? svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG)
458                      : NULL;
459  svn_boolean_t use_commit_times;
460  const char *diff3_cmd;
461  const char *preserved_exts_str;
462  const apr_array_header_t *preserved_exts;
463  svn_node_kind_t kind, external_kind;
464
465  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
466
467  /* See if the user wants last-commit timestamps instead of current ones. */
468  SVN_ERR(svn_config_get_bool(cfg, &use_commit_times,
469                              SVN_CONFIG_SECTION_MISCELLANY,
470                              SVN_CONFIG_OPTION_USE_COMMIT_TIMES, FALSE));
471
472  /* Get the external diff3, if any. */
473  svn_config_get(cfg, &diff3_cmd, SVN_CONFIG_SECTION_HELPERS,
474                 SVN_CONFIG_OPTION_DIFF3_CMD, NULL);
475
476  if (diff3_cmd != NULL)
477    SVN_ERR(svn_path_cstring_to_utf8(&diff3_cmd, diff3_cmd, scratch_pool));
478
479  /* See which files the user wants to preserve the extension of when
480     conflict files are made. */
481  svn_config_get(cfg, &preserved_exts_str, SVN_CONFIG_SECTION_MISCELLANY,
482                 SVN_CONFIG_OPTION_PRESERVED_CF_EXTS, "");
483  preserved_exts = *preserved_exts_str
484    ? svn_cstring_split(preserved_exts_str, "\n\r\t\v ", FALSE, scratch_pool)
485    : NULL;
486
487  {
488    const char *wcroot_abspath;
489
490    SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, local_abspath,
491                               scratch_pool, scratch_pool));
492
493    /* File externals can only be installed inside the current working copy.
494       So verify if the working copy that contains/will contain the target
495       is the defining abspath, or one of its ancestors */
496
497    if (!svn_dirent_is_ancestor(wcroot_abspath, def_dir_abspath))
498        return svn_error_createf(
499                        SVN_ERR_WC_BAD_PATH, NULL,
500                        _("Cannot insert a file external defined on '%s' "
501                          "into the working copy '%s'."),
502                        svn_dirent_local_style(def_dir_abspath,
503                                               scratch_pool),
504                        svn_dirent_local_style(wcroot_abspath,
505                                               scratch_pool));
506  }
507
508  SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath,
509                            TRUE, FALSE, scratch_pool));
510
511  SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL,
512                                     ctx->wc_ctx, local_abspath, local_abspath,
513                                     TRUE, scratch_pool, scratch_pool));
514
515  /* If there is a versioned item with this name, ensure it's a file
516     external before working with it.  If there is no entry in the
517     working copy, then create an empty file and add it to the working
518     copy. */
519  if (kind != svn_node_none && kind != svn_node_unknown)
520    {
521      if (external_kind != svn_node_file)
522        {
523          return svn_error_createf(
524              SVN_ERR_CLIENT_FILE_EXTERNAL_OVERWRITE_VERSIONED, 0,
525             _("The file external from '%s' cannot overwrite the existing "
526               "versioned item at '%s'"),
527             switch_loc->url,
528             svn_dirent_local_style(local_abspath, scratch_pool));
529        }
530    }
531  else
532    {
533      svn_node_kind_t disk_kind;
534
535      SVN_ERR(svn_io_check_path(local_abspath, &disk_kind, scratch_pool));
536
537      if (disk_kind == svn_node_file || disk_kind == svn_node_dir)
538        return svn_error_createf(SVN_ERR_WC_PATH_FOUND, NULL,
539                                 _("The file external '%s' can not be "
540                                   "created because the node exists."),
541                                 svn_dirent_local_style(local_abspath,
542                                                        scratch_pool));
543    }
544
545  {
546    const svn_ra_reporter3_t *reporter;
547    void *report_baton;
548    const svn_delta_editor_t *switch_editor;
549    void *switch_baton;
550    svn_revnum_t revnum;
551    apr_array_header_t *inherited_props;
552    const char *target = svn_dirent_basename(local_abspath, scratch_pool);
553
554    /* Get the external file's iprops. */
555    SVN_ERR(svn_ra_get_inherited_props(ra_session, &inherited_props, "",
556                                       switch_loc->rev,
557                                       scratch_pool, scratch_pool));
558
559    SVN_ERR(svn_ra_reparent(ra_session,
560                            svn_uri_dirname(switch_loc->url, scratch_pool),
561                            scratch_pool));
562
563    SVN_ERR(svn_wc__get_file_external_editor(&switch_editor, &switch_baton,
564                                             &revnum, ctx->wc_ctx,
565                                             local_abspath,
566                                             def_dir_abspath,
567                                             switch_loc->url,
568                                             switch_loc->repos_root_url,
569                                             switch_loc->repos_uuid,
570                                             inherited_props,
571                                             use_commit_times,
572                                             diff3_cmd, preserved_exts,
573                                             def_dir_abspath,
574                                             record_url,
575                                             record_peg_revision,
576                                             record_revision,
577                                             ctx->conflict_func2,
578                                             ctx->conflict_baton2,
579                                             ctx->cancel_func,
580                                             ctx->cancel_baton,
581                                             ctx->notify_func2,
582                                             ctx->notify_baton2,
583                                             scratch_pool, scratch_pool));
584
585    /* Tell RA to do an update of URL+TARGET to REVISION; if we pass an
586     invalid revnum, that means RA will use the latest revision. */
587    SVN_ERR(svn_ra_do_switch3(ra_session, &reporter, &report_baton,
588                              switch_loc->rev,
589                              target, svn_depth_unknown, switch_loc->url,
590                              FALSE /* send_copyfrom */,
591                              TRUE /* ignore_ancestry */,
592                              switch_editor, switch_baton,
593                              scratch_pool, scratch_pool));
594
595    SVN_ERR(svn_wc__crawl_file_external(ctx->wc_ctx, local_abspath,
596                                        reporter, report_baton,
597                                        TRUE,  use_commit_times,
598                                        ctx->cancel_func, ctx->cancel_baton,
599                                        ctx->notify_func2, ctx->notify_baton2,
600                                        scratch_pool));
601
602    if (ctx->notify_func2)
603      {
604        svn_wc_notify_t *notify
605          = svn_wc_create_notify(local_abspath, svn_wc_notify_update_completed,
606                                 scratch_pool);
607        notify->kind = svn_node_none;
608        notify->content_state = notify->prop_state
609          = svn_wc_notify_state_inapplicable;
610        notify->lock_state = svn_wc_notify_lock_state_inapplicable;
611        notify->revision = revnum;
612        ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
613      }
614  }
615
616  return SVN_NO_ERROR;
617}
618
619/* Wrappers around svn_wc__external_remove, obtaining and releasing a lock for
620   directory externals */
621static svn_error_t *
622remove_external2(svn_boolean_t *removed,
623                svn_wc_context_t *wc_ctx,
624                const char *wri_abspath,
625                const char *local_abspath,
626                svn_node_kind_t external_kind,
627                svn_cancel_func_t cancel_func,
628                void *cancel_baton,
629                apr_pool_t *scratch_pool)
630{
631  SVN_ERR(svn_wc__external_remove(wc_ctx, wri_abspath,
632                                  local_abspath,
633                                  (external_kind == svn_node_none),
634                                  cancel_func, cancel_baton,
635                                  scratch_pool));
636
637  *removed = TRUE;
638  return SVN_NO_ERROR;
639}
640
641
642static svn_error_t *
643remove_external(svn_boolean_t *removed,
644                svn_wc_context_t *wc_ctx,
645                const char *wri_abspath,
646                const char *local_abspath,
647                svn_node_kind_t external_kind,
648                svn_cancel_func_t cancel_func,
649                void *cancel_baton,
650                apr_pool_t *scratch_pool)
651{
652  *removed = FALSE;
653  switch (external_kind)
654    {
655      case svn_node_dir:
656        SVN_WC__CALL_WITH_WRITE_LOCK(
657            remove_external2(removed,
658                             wc_ctx, wri_abspath,
659                             local_abspath, external_kind,
660                             cancel_func, cancel_baton,
661                             scratch_pool),
662            wc_ctx, local_abspath, FALSE, scratch_pool);
663        break;
664      case svn_node_file:
665      default:
666        SVN_ERR(remove_external2(removed,
667                                 wc_ctx, wri_abspath,
668                                 local_abspath, external_kind,
669                                 cancel_func, cancel_baton,
670                                 scratch_pool));
671        break;
672    }
673
674  return SVN_NO_ERROR;
675}
676
677/* Called when an external that is in the EXTERNALS table is no longer
678   referenced from an svn:externals property */
679static svn_error_t *
680handle_external_item_removal(const svn_client_ctx_t *ctx,
681                             const char *defining_abspath,
682                             const char *local_abspath,
683                             apr_pool_t *scratch_pool)
684{
685  svn_error_t *err;
686  svn_node_kind_t external_kind;
687  svn_node_kind_t kind;
688  svn_boolean_t removed = FALSE;
689
690  /* local_abspath should be a wcroot or a file external */
691  SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL,
692                                     ctx->wc_ctx, defining_abspath,
693                                     local_abspath, FALSE,
694                                     scratch_pool, scratch_pool));
695
696  SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath, TRUE, FALSE,
697                           scratch_pool));
698
699  if (external_kind != kind)
700    external_kind = svn_node_none; /* Only remove the registration */
701
702  err = remove_external(&removed,
703                        ctx->wc_ctx, defining_abspath, local_abspath,
704                        external_kind,
705                        ctx->cancel_func, ctx->cancel_baton,
706                        scratch_pool);
707
708  if (err && err->apr_err == SVN_ERR_WC_NOT_LOCKED && removed)
709    {
710      svn_error_clear(err);
711      err = NULL; /* We removed the working copy, so we can't release the
712                     lock that was stored inside */
713    }
714
715  if (ctx->notify_func2)
716    {
717      svn_wc_notify_t *notify =
718          svn_wc_create_notify(local_abspath,
719                               svn_wc_notify_update_external_removed,
720                               scratch_pool);
721
722      notify->kind = kind;
723      notify->err = err;
724
725      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
726
727      if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)
728        {
729          notify = svn_wc_create_notify(local_abspath,
730                                      svn_wc_notify_left_local_modifications,
731                                      scratch_pool);
732          notify->kind = svn_node_dir;
733          notify->err = err;
734
735          ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
736        }
737    }
738
739  if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)
740    {
741      svn_error_clear(err);
742      err = NULL;
743    }
744
745  return svn_error_trace(err);
746}
747
748static svn_error_t *
749handle_external_item_change(svn_client_ctx_t *ctx,
750                            const char *repos_root_url,
751                            const char *parent_dir_abspath,
752                            const char *parent_dir_url,
753                            const char *local_abspath,
754                            const char *old_defining_abspath,
755                            const svn_wc_external_item2_t *new_item,
756                            svn_ra_session_t *ra_session,
757                            svn_boolean_t *timestamp_sleep,
758                            apr_pool_t *scratch_pool)
759{
760  svn_client__pathrev_t *new_loc;
761  const char *new_url;
762  svn_node_kind_t ext_kind;
763
764  SVN_ERR_ASSERT(repos_root_url && parent_dir_url);
765  SVN_ERR_ASSERT(new_item != NULL);
766
767  /* Don't bother to check status, since we'll get that for free by
768     attempting to retrieve the hash values anyway.  */
769
770  /* When creating the absolute URL, use the pool and not the
771     iterpool, since the hash table values outlive the iterpool and
772     any pointers they have should also outlive the iterpool.  */
773
774  SVN_ERR(svn_wc__resolve_relative_external_url(&new_url,
775                                                new_item, repos_root_url,
776                                                parent_dir_url,
777                                                scratch_pool, scratch_pool));
778
779  /* Determine if the external is a file or directory. */
780  /* Get the RA connection, if needed. */
781  if (ra_session)
782    {
783      svn_error_t *err = svn_ra_reparent(ra_session, new_url, scratch_pool);
784
785      if (err)
786        {
787          if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL)
788            {
789              svn_error_clear(err);
790              ra_session = NULL;
791            }
792          else
793            return svn_error_trace(err);
794        }
795      else
796        {
797          SVN_ERR(svn_client__resolve_rev_and_url(&new_loc,
798                                                  ra_session, new_url,
799                                                  &(new_item->peg_revision),
800                                                  &(new_item->revision), ctx,
801                                                  scratch_pool));
802
803          SVN_ERR(svn_ra_reparent(ra_session, new_loc->url, scratch_pool));
804        }
805    }
806
807  if (!ra_session)
808    SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc,
809                                              new_url, NULL,
810                                              &(new_item->peg_revision),
811                                              &(new_item->revision), ctx,
812                                              scratch_pool));
813
814  SVN_ERR(svn_ra_check_path(ra_session, "", new_loc->rev, &ext_kind,
815                            scratch_pool));
816
817  if (svn_node_none == ext_kind)
818    return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
819                             _("URL '%s' at revision %ld doesn't exist"),
820                             new_loc->url, new_loc->rev);
821
822  if (svn_node_dir != ext_kind && svn_node_file != ext_kind)
823    return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
824                             _("URL '%s' at revision %ld is not a file "
825                               "or a directory"),
826                             new_loc->url, new_loc->rev);
827
828
829  /* Not protecting against recursive externals.  Detecting them in
830     the global case is hard, and it should be pretty obvious to a
831     user when it happens.  Worst case: your disk fills up :-). */
832
833  /* First notify that we're about to handle an external. */
834  if (ctx->notify_func2)
835    {
836      ctx->notify_func2(
837         ctx->notify_baton2,
838         svn_wc_create_notify(local_abspath,
839                              svn_wc_notify_update_external,
840                              scratch_pool),
841         scratch_pool);
842    }
843
844  if (! old_defining_abspath)
845    {
846      /* The target dir might have multiple components.  Guarantee the path
847         leading down to the last component. */
848      SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(local_abspath,
849                                                             scratch_pool),
850                                          scratch_pool));
851    }
852
853  switch (ext_kind)
854    {
855      case svn_node_dir:
856        SVN_ERR(switch_dir_external(local_abspath, new_loc->url,
857                                    new_item->url,
858                                    &(new_item->peg_revision),
859                                    &(new_item->revision),
860                                    parent_dir_abspath,
861                                    timestamp_sleep, ra_session, ctx,
862                                    scratch_pool));
863        break;
864      case svn_node_file:
865        if (strcmp(repos_root_url, new_loc->repos_root_url))
866          {
867            const char *local_repos_root_url;
868            const char *local_repos_uuid;
869            const char *ext_repos_relpath;
870            svn_error_t *err;
871
872            /*
873             * The working copy library currently requires that all files
874             * in the working copy have the same repository root URL.
875             * The URL from the file external's definition differs from the
876             * one used by the working copy. As a workaround, replace the
877             * root URL portion of the file external's URL, after making
878             * sure both URLs point to the same repository. See issue #4087.
879             */
880
881            err = svn_wc__node_get_repos_info(NULL, NULL,
882                                              &local_repos_root_url,
883                                              &local_repos_uuid,
884                                              ctx->wc_ctx, parent_dir_abspath,
885                                              scratch_pool, scratch_pool);
886            if (err)
887              {
888                if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
889                    && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
890                  return svn_error_trace(err);
891
892                svn_error_clear(err);
893                local_repos_root_url = NULL;
894                local_repos_uuid = NULL;
895              }
896
897            ext_repos_relpath = svn_uri_skip_ancestor(new_loc->repos_root_url,
898                                                      new_url, scratch_pool);
899            if (local_repos_uuid == NULL || local_repos_root_url == NULL ||
900                ext_repos_relpath == NULL ||
901                strcmp(local_repos_uuid, new_loc->repos_uuid) != 0)
902              return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
903                        _("Unsupported external: URL of file external '%s' "
904                          "is not in repository '%s'"),
905                        new_url, repos_root_url);
906
907            new_url = svn_path_url_add_component2(local_repos_root_url,
908                                                  ext_repos_relpath,
909                                                  scratch_pool);
910            SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc,
911                                                      new_url,
912                                                      NULL,
913                                                      &(new_item->peg_revision),
914                                                      &(new_item->revision),
915                                                      ctx, scratch_pool));
916          }
917
918        SVN_ERR(switch_file_external(local_abspath,
919                                     new_loc,
920                                     new_url,
921                                     &new_item->peg_revision,
922                                     &new_item->revision,
923                                     parent_dir_abspath,
924                                     ra_session,
925                                     ctx,
926                                     scratch_pool));
927        break;
928
929      default:
930        SVN_ERR_MALFUNCTION();
931        break;
932    }
933
934  return SVN_NO_ERROR;
935}
936
937static svn_error_t *
938wrap_external_error(const svn_client_ctx_t *ctx,
939                    const char *target_abspath,
940                    svn_error_t *err,
941                    apr_pool_t *scratch_pool)
942{
943  if (err && err->apr_err != SVN_ERR_CANCELLED)
944    {
945      if (ctx->notify_func2)
946        {
947          svn_wc_notify_t *notifier = svn_wc_create_notify(
948                                            target_abspath,
949                                            svn_wc_notify_failed_external,
950                                            scratch_pool);
951          notifier->err = err;
952          ctx->notify_func2(ctx->notify_baton2, notifier, scratch_pool);
953        }
954      svn_error_clear(err);
955      return SVN_NO_ERROR;
956    }
957
958  return err;
959}
960
961static svn_error_t *
962handle_externals_change(svn_client_ctx_t *ctx,
963                        const char *repos_root_url,
964                        svn_boolean_t *timestamp_sleep,
965                        const char *local_abspath,
966                        const char *new_desc_text,
967                        apr_hash_t *old_externals,
968                        svn_depth_t ambient_depth,
969                        svn_depth_t requested_depth,
970                        svn_ra_session_t *ra_session,
971                        apr_pool_t *scratch_pool)
972{
973  apr_array_header_t *new_desc;
974  int i;
975  apr_pool_t *iterpool;
976  const char *url;
977
978  iterpool = svn_pool_create(scratch_pool);
979
980  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
981
982  /* Bag out if the depth here is too shallow for externals action. */
983  if ((requested_depth < svn_depth_infinity
984       && requested_depth != svn_depth_unknown)
985      || (ambient_depth < svn_depth_infinity
986          && requested_depth < svn_depth_infinity))
987    return SVN_NO_ERROR;
988
989  if (new_desc_text)
990    SVN_ERR(svn_wc_parse_externals_description3(&new_desc, local_abspath,
991                                                new_desc_text,
992                                                FALSE, scratch_pool));
993  else
994    new_desc = NULL;
995
996  SVN_ERR(svn_wc__node_get_url(&url, ctx->wc_ctx, local_abspath,
997                               scratch_pool, iterpool));
998
999  SVN_ERR_ASSERT(url);
1000
1001  for (i = 0; new_desc && (i < new_desc->nelts); i++)
1002    {
1003      const char *old_defining_abspath;
1004      svn_wc_external_item2_t *new_item;
1005      const char *target_abspath;
1006      svn_boolean_t under_root;
1007
1008      new_item = APR_ARRAY_IDX(new_desc, i, svn_wc_external_item2_t *);
1009
1010      svn_pool_clear(iterpool);
1011
1012      if (ctx->cancel_func)
1013        SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
1014
1015      SVN_ERR(svn_dirent_is_under_root(&under_root, &target_abspath,
1016                                       local_abspath, new_item->target_dir,
1017                                       iterpool));
1018
1019      if (! under_root)
1020        {
1021          return svn_error_createf(
1022                    SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
1023                    _("Path '%s' is not in the working copy"),
1024                    svn_dirent_local_style(
1025                        svn_dirent_join(local_abspath, new_item->target_dir,
1026                                        iterpool),
1027                        iterpool));
1028        }
1029
1030      old_defining_abspath = svn_hash_gets(old_externals, target_abspath);
1031
1032      SVN_ERR(wrap_external_error(
1033                      ctx, target_abspath,
1034                      handle_external_item_change(ctx,
1035                                                  repos_root_url,
1036                                                  local_abspath, url,
1037                                                  target_abspath,
1038                                                  old_defining_abspath,
1039                                                  new_item, ra_session,
1040                                                  timestamp_sleep,
1041                                                  iterpool),
1042                      iterpool));
1043
1044      /* And remove already processed items from the to-remove hash */
1045      if (old_defining_abspath)
1046        svn_hash_sets(old_externals, target_abspath, NULL);
1047    }
1048
1049  svn_pool_destroy(iterpool);
1050
1051  return SVN_NO_ERROR;
1052}
1053
1054
1055svn_error_t *
1056svn_client__handle_externals(apr_hash_t *externals_new,
1057                             apr_hash_t *ambient_depths,
1058                             const char *repos_root_url,
1059                             const char *target_abspath,
1060                             svn_depth_t requested_depth,
1061                             svn_boolean_t *timestamp_sleep,
1062                             svn_ra_session_t *ra_session,
1063                             svn_client_ctx_t *ctx,
1064                             apr_pool_t *scratch_pool)
1065{
1066  apr_hash_t *old_external_defs;
1067  apr_hash_index_t *hi;
1068  apr_pool_t *iterpool;
1069
1070  SVN_ERR_ASSERT(repos_root_url);
1071
1072  iterpool = svn_pool_create(scratch_pool);
1073
1074  SVN_ERR(svn_wc__externals_defined_below(&old_external_defs,
1075                                          ctx->wc_ctx, target_abspath,
1076                                          scratch_pool, iterpool));
1077
1078  for (hi = apr_hash_first(scratch_pool, externals_new);
1079       hi;
1080       hi = apr_hash_next(hi))
1081    {
1082      const char *local_abspath = apr_hash_this_key(hi);
1083      const char *desc_text = apr_hash_this_val(hi);
1084      svn_depth_t ambient_depth = svn_depth_infinity;
1085
1086      svn_pool_clear(iterpool);
1087
1088      if (ambient_depths)
1089        {
1090          const char *ambient_depth_w;
1091
1092          ambient_depth_w = apr_hash_get(ambient_depths, local_abspath,
1093                                         apr_hash_this_key_len(hi));
1094
1095          if (ambient_depth_w == NULL)
1096            {
1097              return svn_error_createf(
1098                        SVN_ERR_WC_CORRUPT, NULL,
1099                        _("Traversal of '%s' found no ambient depth"),
1100                        svn_dirent_local_style(local_abspath, scratch_pool));
1101            }
1102          else
1103            {
1104              ambient_depth = svn_depth_from_word(ambient_depth_w);
1105            }
1106        }
1107
1108      SVN_ERR(handle_externals_change(ctx, repos_root_url, timestamp_sleep,
1109                                      local_abspath,
1110                                      desc_text, old_external_defs,
1111                                      ambient_depth, requested_depth,
1112                                      ra_session, iterpool));
1113    }
1114
1115  /* Remove the remaining externals */
1116  for (hi = apr_hash_first(scratch_pool, old_external_defs);
1117       hi;
1118       hi = apr_hash_next(hi))
1119    {
1120      const char *item_abspath = apr_hash_this_key(hi);
1121      const char *defining_abspath = apr_hash_this_val(hi);
1122      const char *parent_abspath;
1123
1124      svn_pool_clear(iterpool);
1125
1126      SVN_ERR(wrap_external_error(
1127                          ctx, item_abspath,
1128                          handle_external_item_removal(ctx, defining_abspath,
1129                                                       item_abspath, iterpool),
1130                          iterpool));
1131
1132      /* Are there any unversioned directories between the removed
1133       * external and the DEFINING_ABSPATH which we can remove? */
1134      parent_abspath = item_abspath;
1135      do {
1136        svn_node_kind_t kind;
1137
1138        parent_abspath = svn_dirent_dirname(parent_abspath, iterpool);
1139        SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, parent_abspath,
1140                                  FALSE /* show_deleted*/,
1141                                  FALSE /* show_hidden */,
1142                                  iterpool));
1143        if (kind == svn_node_none)
1144          {
1145            svn_error_t *err;
1146
1147            err = svn_io_dir_remove_nonrecursive(parent_abspath, iterpool);
1148            if (err)
1149              {
1150                if (APR_STATUS_IS_ENOTEMPTY(err->apr_err))
1151                  {
1152                    svn_error_clear(err);
1153                    break; /* No parents to delete */
1154                  }
1155                else if (APR_STATUS_IS_ENOENT(err->apr_err)
1156                         || APR_STATUS_IS_ENOTDIR(err->apr_err))
1157                  {
1158                    svn_error_clear(err);
1159                    /* Fall through; parent dir might be unversioned */
1160                  }
1161                else
1162                  return svn_error_trace(err);
1163              }
1164          }
1165      } while (strcmp(parent_abspath, defining_abspath) != 0);
1166    }
1167
1168
1169  svn_pool_destroy(iterpool);
1170  return SVN_NO_ERROR;
1171}
1172
1173
1174svn_error_t *
1175svn_client__export_externals(apr_hash_t *externals,
1176                             const char *from_url,
1177                             const char *to_abspath,
1178                             const char *repos_root_url,
1179                             svn_depth_t requested_depth,
1180                             const char *native_eol,
1181                             svn_boolean_t ignore_keywords,
1182                             svn_client_ctx_t *ctx,
1183                             apr_pool_t *scratch_pool)
1184{
1185  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
1186  apr_pool_t *sub_iterpool = svn_pool_create(scratch_pool);
1187  apr_hash_index_t *hi;
1188
1189  SVN_ERR_ASSERT(svn_dirent_is_absolute(to_abspath));
1190
1191  for (hi = apr_hash_first(scratch_pool, externals);
1192       hi;
1193       hi = apr_hash_next(hi))
1194    {
1195      const char *local_abspath = apr_hash_this_key(hi);
1196      const char *desc_text = apr_hash_this_val(hi);
1197      const char *local_relpath;
1198      const char *dir_url;
1199      apr_array_header_t *items;
1200      int i;
1201
1202      svn_pool_clear(iterpool);
1203
1204      SVN_ERR(svn_wc_parse_externals_description3(&items, local_abspath,
1205                                                  desc_text, FALSE,
1206                                                  iterpool));
1207
1208      if (! items->nelts)
1209        continue;
1210
1211      local_relpath = svn_dirent_skip_ancestor(to_abspath, local_abspath);
1212
1213      dir_url = svn_path_url_add_component2(from_url, local_relpath,
1214                                            scratch_pool);
1215
1216      for (i = 0; i < items->nelts; i++)
1217        {
1218          const char *item_abspath;
1219          const char *new_url;
1220          svn_boolean_t under_root;
1221          svn_wc_external_item2_t *item = APR_ARRAY_IDX(items, i,
1222                                                svn_wc_external_item2_t *);
1223
1224          svn_pool_clear(sub_iterpool);
1225
1226          SVN_ERR(svn_dirent_is_under_root(&under_root, &item_abspath,
1227                                           local_abspath, item->target_dir,
1228                                           sub_iterpool));
1229
1230          if (! under_root)
1231            {
1232              return svn_error_createf(
1233                        SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
1234                        _("Path '%s' is not in the working copy"),
1235                        svn_dirent_local_style(
1236                            svn_dirent_join(local_abspath, item->target_dir,
1237                                            sub_iterpool),
1238                            sub_iterpool));
1239            }
1240
1241          SVN_ERR(svn_wc__resolve_relative_external_url(&new_url, item,
1242                                                        repos_root_url,
1243                                                        dir_url, sub_iterpool,
1244                                                        sub_iterpool));
1245
1246          /* The target dir might have multiple components.  Guarantee
1247             the path leading down to the last component. */
1248          SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(item_abspath,
1249                                                                 sub_iterpool),
1250                                              sub_iterpool));
1251
1252          /* First notify that we're about to handle an external. */
1253          if (ctx->notify_func2)
1254            {
1255              ctx->notify_func2(
1256                       ctx->notify_baton2,
1257                       svn_wc_create_notify(item_abspath,
1258                                            svn_wc_notify_update_external,
1259                                            sub_iterpool),
1260                       sub_iterpool);
1261            }
1262
1263          SVN_ERR(wrap_external_error(
1264                          ctx, item_abspath,
1265                          svn_client_export5(NULL, new_url, item_abspath,
1266                                             &item->peg_revision,
1267                                             &item->revision,
1268                                             TRUE, FALSE, ignore_keywords,
1269                                             svn_depth_infinity,
1270                                             native_eol,
1271                                             ctx, sub_iterpool),
1272                          sub_iterpool));
1273        }
1274    }
1275
1276  svn_pool_destroy(sub_iterpool);
1277  svn_pool_destroy(iterpool);
1278
1279  return SVN_NO_ERROR;
1280}
1281
1282