externals.c revision 299742
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_rename(local_abspath, new_path, 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 (kind == svn_node_file || 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->cancel_func,
578                                             ctx->cancel_baton,
579                                             ctx->notify_func2,
580                                             ctx->notify_baton2,
581                                             scratch_pool, scratch_pool));
582
583    /* Tell RA to do an update of URL+TARGET to REVISION; if we pass an
584     invalid revnum, that means RA will use the latest revision. */
585    SVN_ERR(svn_ra_do_switch3(ra_session, &reporter, &report_baton,
586                              switch_loc->rev,
587                              target, svn_depth_unknown, switch_loc->url,
588                              FALSE /* send_copyfrom */,
589                              TRUE /* ignore_ancestry */,
590                              switch_editor, switch_baton,
591                              scratch_pool, scratch_pool));
592
593    SVN_ERR(svn_wc__crawl_file_external(ctx->wc_ctx, local_abspath,
594                                        reporter, report_baton,
595                                        TRUE,  use_commit_times,
596                                        ctx->cancel_func, ctx->cancel_baton,
597                                        ctx->notify_func2, ctx->notify_baton2,
598                                        scratch_pool));
599
600    if (ctx->notify_func2)
601      {
602        svn_wc_notify_t *notify
603          = svn_wc_create_notify(local_abspath, svn_wc_notify_update_completed,
604                                 scratch_pool);
605        notify->kind = svn_node_none;
606        notify->content_state = notify->prop_state
607          = svn_wc_notify_state_inapplicable;
608        notify->lock_state = svn_wc_notify_lock_state_inapplicable;
609        notify->revision = revnum;
610        ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
611      }
612  }
613
614  return SVN_NO_ERROR;
615}
616
617/* Wrappers around svn_wc__external_remove, obtaining and releasing a lock for
618   directory externals */
619static svn_error_t *
620remove_external2(svn_boolean_t *removed,
621                svn_wc_context_t *wc_ctx,
622                const char *wri_abspath,
623                const char *local_abspath,
624                svn_node_kind_t external_kind,
625                svn_cancel_func_t cancel_func,
626                void *cancel_baton,
627                apr_pool_t *scratch_pool)
628{
629  SVN_ERR(svn_wc__external_remove(wc_ctx, wri_abspath,
630                                  local_abspath,
631                                  (external_kind == svn_node_none),
632                                  cancel_func, cancel_baton,
633                                  scratch_pool));
634
635  *removed = TRUE;
636  return SVN_NO_ERROR;
637}
638
639
640static svn_error_t *
641remove_external(svn_boolean_t *removed,
642                svn_wc_context_t *wc_ctx,
643                const char *wri_abspath,
644                const char *local_abspath,
645                svn_node_kind_t external_kind,
646                svn_cancel_func_t cancel_func,
647                void *cancel_baton,
648                apr_pool_t *scratch_pool)
649{
650  *removed = FALSE;
651  switch (external_kind)
652    {
653      case svn_node_dir:
654        SVN_WC__CALL_WITH_WRITE_LOCK(
655            remove_external2(removed,
656                             wc_ctx, wri_abspath,
657                             local_abspath, external_kind,
658                             cancel_func, cancel_baton,
659                             scratch_pool),
660            wc_ctx, local_abspath, FALSE, scratch_pool);
661        break;
662      case svn_node_file:
663      default:
664        SVN_ERR(remove_external2(removed,
665                                 wc_ctx, wri_abspath,
666                                 local_abspath, external_kind,
667                                 cancel_func, cancel_baton,
668                                 scratch_pool));
669        break;
670    }
671
672  return SVN_NO_ERROR;
673}
674
675/* Called when an external that is in the EXTERNALS table is no longer
676   referenced from an svn:externals property */
677static svn_error_t *
678handle_external_item_removal(const svn_client_ctx_t *ctx,
679                             const char *defining_abspath,
680                             const char *local_abspath,
681                             apr_pool_t *scratch_pool)
682{
683  svn_error_t *err;
684  svn_node_kind_t external_kind;
685  svn_node_kind_t kind;
686  svn_boolean_t removed = FALSE;
687
688  /* local_abspath should be a wcroot or a file external */
689  SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL,
690                                     ctx->wc_ctx, defining_abspath,
691                                     local_abspath, FALSE,
692                                     scratch_pool, scratch_pool));
693
694  SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath, TRUE, FALSE,
695                           scratch_pool));
696
697  if (external_kind != kind)
698    external_kind = svn_node_none; /* Only remove the registration */
699
700  err = remove_external(&removed,
701                        ctx->wc_ctx, defining_abspath, local_abspath,
702                        external_kind,
703                        ctx->cancel_func, ctx->cancel_baton,
704                        scratch_pool);
705
706  if (err && err->apr_err == SVN_ERR_WC_NOT_LOCKED && removed)
707    {
708      svn_error_clear(err);
709      err = NULL; /* We removed the working copy, so we can't release the
710                     lock that was stored inside */
711    }
712
713  if (ctx->notify_func2)
714    {
715      svn_wc_notify_t *notify =
716          svn_wc_create_notify(local_abspath,
717                               svn_wc_notify_update_external_removed,
718                               scratch_pool);
719
720      notify->kind = kind;
721      notify->err = err;
722
723      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
724
725      if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)
726        {
727          notify = svn_wc_create_notify(local_abspath,
728                                      svn_wc_notify_left_local_modifications,
729                                      scratch_pool);
730          notify->kind = svn_node_dir;
731          notify->err = err;
732
733          ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
734        }
735    }
736
737  if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)
738    {
739      svn_error_clear(err);
740      err = NULL;
741    }
742
743  return svn_error_trace(err);
744}
745
746static svn_error_t *
747handle_external_item_change(svn_client_ctx_t *ctx,
748                            const char *repos_root_url,
749                            const char *parent_dir_abspath,
750                            const char *parent_dir_url,
751                            const char *local_abspath,
752                            const char *old_defining_abspath,
753                            const svn_wc_external_item2_t *new_item,
754                            svn_ra_session_t *ra_session,
755                            svn_boolean_t *timestamp_sleep,
756                            apr_pool_t *scratch_pool)
757{
758  svn_client__pathrev_t *new_loc;
759  const char *new_url;
760  svn_node_kind_t ext_kind;
761
762  SVN_ERR_ASSERT(repos_root_url && parent_dir_url);
763  SVN_ERR_ASSERT(new_item != NULL);
764
765  /* Don't bother to check status, since we'll get that for free by
766     attempting to retrieve the hash values anyway.  */
767
768  /* When creating the absolute URL, use the pool and not the
769     iterpool, since the hash table values outlive the iterpool and
770     any pointers they have should also outlive the iterpool.  */
771
772  SVN_ERR(svn_wc__resolve_relative_external_url(&new_url,
773                                                new_item, repos_root_url,
774                                                parent_dir_url,
775                                                scratch_pool, scratch_pool));
776
777  /* Determine if the external is a file or directory. */
778  /* Get the RA connection, if needed. */
779  if (ra_session)
780    {
781      svn_error_t *err = svn_ra_reparent(ra_session, new_url, scratch_pool);
782
783      if (err)
784        {
785          if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL)
786            {
787              svn_error_clear(err);
788              ra_session = NULL;
789            }
790          else
791            return svn_error_trace(err);
792        }
793      else
794        {
795          SVN_ERR(svn_client__resolve_rev_and_url(&new_loc,
796                                                  ra_session, new_url,
797                                                  &(new_item->peg_revision),
798                                                  &(new_item->revision), ctx,
799                                                  scratch_pool));
800
801          SVN_ERR(svn_ra_reparent(ra_session, new_loc->url, scratch_pool));
802        }
803    }
804
805  if (!ra_session)
806    SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc,
807                                              new_url, NULL,
808                                              &(new_item->peg_revision),
809                                              &(new_item->revision), ctx,
810                                              scratch_pool));
811
812  SVN_ERR(svn_ra_check_path(ra_session, "", new_loc->rev, &ext_kind,
813                            scratch_pool));
814
815  if (svn_node_none == ext_kind)
816    return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
817                             _("URL '%s' at revision %ld doesn't exist"),
818                             new_loc->url, new_loc->rev);
819
820  if (svn_node_dir != ext_kind && svn_node_file != ext_kind)
821    return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
822                             _("URL '%s' at revision %ld is not a file "
823                               "or a directory"),
824                             new_loc->url, new_loc->rev);
825
826
827  /* Not protecting against recursive externals.  Detecting them in
828     the global case is hard, and it should be pretty obvious to a
829     user when it happens.  Worst case: your disk fills up :-). */
830
831  /* First notify that we're about to handle an external. */
832  if (ctx->notify_func2)
833    {
834      ctx->notify_func2(
835         ctx->notify_baton2,
836         svn_wc_create_notify(local_abspath,
837                              svn_wc_notify_update_external,
838                              scratch_pool),
839         scratch_pool);
840    }
841
842  if (! old_defining_abspath)
843    {
844      /* The target dir might have multiple components.  Guarantee the path
845         leading down to the last component. */
846      SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(local_abspath,
847                                                             scratch_pool),
848                                          scratch_pool));
849    }
850
851  switch (ext_kind)
852    {
853      case svn_node_dir:
854        SVN_ERR(switch_dir_external(local_abspath, new_loc->url,
855                                    new_item->url,
856                                    &(new_item->peg_revision),
857                                    &(new_item->revision),
858                                    parent_dir_abspath,
859                                    timestamp_sleep, ra_session, ctx,
860                                    scratch_pool));
861        break;
862      case svn_node_file:
863        if (strcmp(repos_root_url, new_loc->repos_root_url))
864          {
865            const char *local_repos_root_url;
866            const char *local_repos_uuid;
867            const char *ext_repos_relpath;
868            svn_error_t *err;
869
870            /*
871             * The working copy library currently requires that all files
872             * in the working copy have the same repository root URL.
873             * The URL from the file external's definition differs from the
874             * one used by the working copy. As a workaround, replace the
875             * root URL portion of the file external's URL, after making
876             * sure both URLs point to the same repository. See issue #4087.
877             */
878
879            err = svn_wc__node_get_repos_info(NULL, NULL,
880                                              &local_repos_root_url,
881                                              &local_repos_uuid,
882                                              ctx->wc_ctx, parent_dir_abspath,
883                                              scratch_pool, scratch_pool);
884            if (err)
885              {
886                if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
887                    && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
888                  return svn_error_trace(err);
889
890                svn_error_clear(err);
891                local_repos_root_url = NULL;
892                local_repos_uuid = NULL;
893              }
894
895            ext_repos_relpath = svn_uri_skip_ancestor(new_loc->repos_root_url,
896                                                      new_url, scratch_pool);
897            if (local_repos_uuid == NULL || local_repos_root_url == NULL ||
898                ext_repos_relpath == NULL ||
899                strcmp(local_repos_uuid, new_loc->repos_uuid) != 0)
900              return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
901                        _("Unsupported external: URL of file external '%s' "
902                          "is not in repository '%s'"),
903                        new_url, repos_root_url);
904
905            new_url = svn_path_url_add_component2(local_repos_root_url,
906                                                  ext_repos_relpath,
907                                                  scratch_pool);
908            SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc,
909                                                      new_url,
910                                                      NULL,
911                                                      &(new_item->peg_revision),
912                                                      &(new_item->revision),
913                                                      ctx, scratch_pool));
914          }
915
916        SVN_ERR(switch_file_external(local_abspath,
917                                     new_loc,
918                                     new_url,
919                                     &new_item->peg_revision,
920                                     &new_item->revision,
921                                     parent_dir_abspath,
922                                     ra_session,
923                                     ctx,
924                                     scratch_pool));
925        break;
926
927      default:
928        SVN_ERR_MALFUNCTION();
929        break;
930    }
931
932  return SVN_NO_ERROR;
933}
934
935static svn_error_t *
936wrap_external_error(const svn_client_ctx_t *ctx,
937                    const char *target_abspath,
938                    svn_error_t *err,
939                    apr_pool_t *scratch_pool)
940{
941  if (err && err->apr_err != SVN_ERR_CANCELLED)
942    {
943      if (ctx->notify_func2)
944        {
945          svn_wc_notify_t *notifier = svn_wc_create_notify(
946                                            target_abspath,
947                                            svn_wc_notify_failed_external,
948                                            scratch_pool);
949          notifier->err = err;
950          ctx->notify_func2(ctx->notify_baton2, notifier, scratch_pool);
951        }
952      svn_error_clear(err);
953      return SVN_NO_ERROR;
954    }
955
956  return err;
957}
958
959static svn_error_t *
960handle_externals_change(svn_client_ctx_t *ctx,
961                        const char *repos_root_url,
962                        svn_boolean_t *timestamp_sleep,
963                        const char *local_abspath,
964                        const char *new_desc_text,
965                        apr_hash_t *old_externals,
966                        svn_depth_t ambient_depth,
967                        svn_depth_t requested_depth,
968                        svn_ra_session_t *ra_session,
969                        apr_pool_t *scratch_pool)
970{
971  apr_array_header_t *new_desc;
972  int i;
973  apr_pool_t *iterpool;
974  const char *url;
975
976  iterpool = svn_pool_create(scratch_pool);
977
978  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
979
980  /* Bag out if the depth here is too shallow for externals action. */
981  if ((requested_depth < svn_depth_infinity
982       && requested_depth != svn_depth_unknown)
983      || (ambient_depth < svn_depth_infinity
984          && requested_depth < svn_depth_infinity))
985    return SVN_NO_ERROR;
986
987  if (new_desc_text)
988    SVN_ERR(svn_wc_parse_externals_description3(&new_desc, local_abspath,
989                                                new_desc_text,
990                                                FALSE, scratch_pool));
991  else
992    new_desc = NULL;
993
994  SVN_ERR(svn_wc__node_get_url(&url, ctx->wc_ctx, local_abspath,
995                               scratch_pool, iterpool));
996
997  SVN_ERR_ASSERT(url);
998
999  for (i = 0; new_desc && (i < new_desc->nelts); i++)
1000    {
1001      const char *old_defining_abspath;
1002      svn_wc_external_item2_t *new_item;
1003      const char *target_abspath;
1004      svn_boolean_t under_root;
1005
1006      new_item = APR_ARRAY_IDX(new_desc, i, svn_wc_external_item2_t *);
1007
1008      svn_pool_clear(iterpool);
1009
1010      if (ctx->cancel_func)
1011        SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
1012
1013      SVN_ERR(svn_dirent_is_under_root(&under_root, &target_abspath,
1014                                       local_abspath, new_item->target_dir,
1015                                       iterpool));
1016
1017      if (! under_root)
1018        {
1019          return svn_error_createf(
1020                    SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
1021                    _("Path '%s' is not in the working copy"),
1022                    svn_dirent_local_style(
1023                        svn_dirent_join(local_abspath, new_item->target_dir,
1024                                        iterpool),
1025                        iterpool));
1026        }
1027
1028      old_defining_abspath = svn_hash_gets(old_externals, target_abspath);
1029
1030      SVN_ERR(wrap_external_error(
1031                      ctx, target_abspath,
1032                      handle_external_item_change(ctx,
1033                                                  repos_root_url,
1034                                                  local_abspath, url,
1035                                                  target_abspath,
1036                                                  old_defining_abspath,
1037                                                  new_item, ra_session,
1038                                                  timestamp_sleep,
1039                                                  iterpool),
1040                      iterpool));
1041
1042      /* And remove already processed items from the to-remove hash */
1043      if (old_defining_abspath)
1044        svn_hash_sets(old_externals, target_abspath, NULL);
1045    }
1046
1047  svn_pool_destroy(iterpool);
1048
1049  return SVN_NO_ERROR;
1050}
1051
1052
1053svn_error_t *
1054svn_client__handle_externals(apr_hash_t *externals_new,
1055                             apr_hash_t *ambient_depths,
1056                             const char *repos_root_url,
1057                             const char *target_abspath,
1058                             svn_depth_t requested_depth,
1059                             svn_boolean_t *timestamp_sleep,
1060                             svn_ra_session_t *ra_session,
1061                             svn_client_ctx_t *ctx,
1062                             apr_pool_t *scratch_pool)
1063{
1064  apr_hash_t *old_external_defs;
1065  apr_hash_index_t *hi;
1066  apr_pool_t *iterpool;
1067
1068  SVN_ERR_ASSERT(repos_root_url);
1069
1070  iterpool = svn_pool_create(scratch_pool);
1071
1072  SVN_ERR(svn_wc__externals_defined_below(&old_external_defs,
1073                                          ctx->wc_ctx, target_abspath,
1074                                          scratch_pool, iterpool));
1075
1076  for (hi = apr_hash_first(scratch_pool, externals_new);
1077       hi;
1078       hi = apr_hash_next(hi))
1079    {
1080      const char *local_abspath = apr_hash_this_key(hi);
1081      const char *desc_text = apr_hash_this_val(hi);
1082      svn_depth_t ambient_depth = svn_depth_infinity;
1083
1084      svn_pool_clear(iterpool);
1085
1086      if (ambient_depths)
1087        {
1088          const char *ambient_depth_w;
1089
1090          ambient_depth_w = apr_hash_get(ambient_depths, local_abspath,
1091                                         apr_hash_this_key_len(hi));
1092
1093          if (ambient_depth_w == NULL)
1094            {
1095              return svn_error_createf(
1096                        SVN_ERR_WC_CORRUPT, NULL,
1097                        _("Traversal of '%s' found no ambient depth"),
1098                        svn_dirent_local_style(local_abspath, scratch_pool));
1099            }
1100          else
1101            {
1102              ambient_depth = svn_depth_from_word(ambient_depth_w);
1103            }
1104        }
1105
1106      SVN_ERR(handle_externals_change(ctx, repos_root_url, timestamp_sleep,
1107                                      local_abspath,
1108                                      desc_text, old_external_defs,
1109                                      ambient_depth, requested_depth,
1110                                      ra_session, iterpool));
1111    }
1112
1113  /* Remove the remaining externals */
1114  for (hi = apr_hash_first(scratch_pool, old_external_defs);
1115       hi;
1116       hi = apr_hash_next(hi))
1117    {
1118      const char *item_abspath = apr_hash_this_key(hi);
1119      const char *defining_abspath = apr_hash_this_val(hi);
1120      const char *parent_abspath;
1121
1122      svn_pool_clear(iterpool);
1123
1124      SVN_ERR(wrap_external_error(
1125                          ctx, item_abspath,
1126                          handle_external_item_removal(ctx, defining_abspath,
1127                                                       item_abspath, iterpool),
1128                          iterpool));
1129
1130      /* Are there any unversioned directories between the removed
1131       * external and the DEFINING_ABSPATH which we can remove? */
1132      parent_abspath = item_abspath;
1133      do {
1134        svn_node_kind_t kind;
1135
1136        parent_abspath = svn_dirent_dirname(parent_abspath, iterpool);
1137        SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, parent_abspath,
1138                                  FALSE /* show_deleted*/,
1139                                  FALSE /* show_hidden */,
1140                                  iterpool));
1141        if (kind == svn_node_none)
1142          {
1143            svn_error_t *err;
1144
1145            err = svn_io_dir_remove_nonrecursive(parent_abspath, iterpool);
1146            if (err)
1147              {
1148                if (APR_STATUS_IS_ENOTEMPTY(err->apr_err))
1149                  {
1150                    svn_error_clear(err);
1151                    break; /* No parents to delete */
1152                  }
1153                else if (APR_STATUS_IS_ENOENT(err->apr_err)
1154                         || APR_STATUS_IS_ENOTDIR(err->apr_err))
1155                  {
1156                    svn_error_clear(err);
1157                    /* Fall through; parent dir might be unversioned */
1158                  }
1159                else
1160                  return svn_error_trace(err);
1161              }
1162          }
1163      } while (strcmp(parent_abspath, defining_abspath) != 0);
1164    }
1165
1166
1167  svn_pool_destroy(iterpool);
1168  return SVN_NO_ERROR;
1169}
1170
1171
1172svn_error_t *
1173svn_client__export_externals(apr_hash_t *externals,
1174                             const char *from_url,
1175                             const char *to_abspath,
1176                             const char *repos_root_url,
1177                             svn_depth_t requested_depth,
1178                             const char *native_eol,
1179                             svn_boolean_t ignore_keywords,
1180                             svn_client_ctx_t *ctx,
1181                             apr_pool_t *scratch_pool)
1182{
1183  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
1184  apr_pool_t *sub_iterpool = svn_pool_create(scratch_pool);
1185  apr_hash_index_t *hi;
1186
1187  SVN_ERR_ASSERT(svn_dirent_is_absolute(to_abspath));
1188
1189  for (hi = apr_hash_first(scratch_pool, externals);
1190       hi;
1191       hi = apr_hash_next(hi))
1192    {
1193      const char *local_abspath = apr_hash_this_key(hi);
1194      const char *desc_text = apr_hash_this_val(hi);
1195      const char *local_relpath;
1196      const char *dir_url;
1197      apr_array_header_t *items;
1198      int i;
1199
1200      svn_pool_clear(iterpool);
1201
1202      SVN_ERR(svn_wc_parse_externals_description3(&items, local_abspath,
1203                                                  desc_text, FALSE,
1204                                                  iterpool));
1205
1206      if (! items->nelts)
1207        continue;
1208
1209      local_relpath = svn_dirent_skip_ancestor(to_abspath, local_abspath);
1210
1211      dir_url = svn_path_url_add_component2(from_url, local_relpath,
1212                                            scratch_pool);
1213
1214      for (i = 0; i < items->nelts; i++)
1215        {
1216          const char *item_abspath;
1217          const char *new_url;
1218          svn_boolean_t under_root;
1219          svn_wc_external_item2_t *item = APR_ARRAY_IDX(items, i,
1220                                                svn_wc_external_item2_t *);
1221
1222          svn_pool_clear(sub_iterpool);
1223
1224          SVN_ERR(svn_dirent_is_under_root(&under_root, &item_abspath,
1225                                           local_abspath, item->target_dir,
1226                                           sub_iterpool));
1227
1228          if (! under_root)
1229            {
1230              return svn_error_createf(
1231                        SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
1232                        _("Path '%s' is not in the working copy"),
1233                        svn_dirent_local_style(
1234                            svn_dirent_join(local_abspath, item->target_dir,
1235                                            sub_iterpool),
1236                            sub_iterpool));
1237            }
1238
1239          SVN_ERR(svn_wc__resolve_relative_external_url(&new_url, item,
1240                                                        repos_root_url,
1241                                                        dir_url, sub_iterpool,
1242                                                        sub_iterpool));
1243
1244          /* The target dir might have multiple components.  Guarantee
1245             the path leading down to the last component. */
1246          SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(item_abspath,
1247                                                                 sub_iterpool),
1248                                              sub_iterpool));
1249
1250          /* First notify that we're about to handle an external. */
1251          if (ctx->notify_func2)
1252            {
1253              ctx->notify_func2(
1254                       ctx->notify_baton2,
1255                       svn_wc_create_notify(item_abspath,
1256                                            svn_wc_notify_update_external,
1257                                            sub_iterpool),
1258                       sub_iterpool);
1259            }
1260
1261          SVN_ERR(wrap_external_error(
1262                          ctx, item_abspath,
1263                          svn_client_export5(NULL, new_url, item_abspath,
1264                                             &item->peg_revision,
1265                                             &item->revision,
1266                                             TRUE, FALSE, ignore_keywords,
1267                                             svn_depth_infinity,
1268                                             native_eol,
1269                                             ctx, sub_iterpool),
1270                          sub_iterpool));
1271        }
1272    }
1273
1274  svn_pool_destroy(sub_iterpool);
1275  svn_pool_destroy(iterpool);
1276
1277  return SVN_NO_ERROR;
1278}
1279
1280