externals.c revision 269847
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 svn_opt_revision_t *peg_revision,
150                    const svn_opt_revision_t *revision,
151                    const char *defining_abspath,
152                    svn_boolean_t *timestamp_sleep,
153                    svn_client_ctx_t *ctx,
154                    apr_pool_t *pool)
155{
156  svn_node_kind_t kind;
157  svn_error_t *err;
158  svn_revnum_t external_peg_rev = SVN_INVALID_REVNUM;
159  svn_revnum_t external_rev = SVN_INVALID_REVNUM;
160  apr_pool_t *subpool = svn_pool_create(pool);
161  const char *repos_root_url;
162  const char *repos_uuid;
163
164  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
165
166  if (peg_revision->kind == svn_opt_revision_number)
167    external_peg_rev = peg_revision->value.number;
168
169  if (revision->kind == svn_opt_revision_number)
170    external_rev = revision->value.number;
171
172  /* If path is a directory, try to update/switch to the correct URL
173     and revision. */
174  SVN_ERR(svn_io_check_path(local_abspath, &kind, pool));
175  if (kind == svn_node_dir)
176    {
177      const char *node_url;
178
179      /* Doubles as an "is versioned" check. */
180      err = svn_wc__node_get_url(&node_url, ctx->wc_ctx, local_abspath,
181                                 pool, subpool);
182      if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
183        {
184          svn_error_clear(err);
185          err = SVN_NO_ERROR;
186          goto relegate;
187        }
188      else if (err)
189        return svn_error_trace(err);
190
191      if (node_url)
192        {
193          /* If we have what appears to be a version controlled
194             subdir, and its top-level URL matches that of our
195             externals definition, perform an update. */
196          if (strcmp(node_url, url) == 0)
197            {
198              SVN_ERR(svn_client__update_internal(NULL, local_abspath,
199                                                  revision, svn_depth_unknown,
200                                                  FALSE, FALSE, FALSE, TRUE,
201                                                  FALSE, TRUE,
202                                                  timestamp_sleep,
203                                                  ctx, subpool));
204              svn_pool_destroy(subpool);
205              goto cleanup;
206            }
207
208          /* We'd really prefer not to have to do a brute-force
209             relegation -- blowing away the current external working
210             copy and checking it out anew -- so we'll first see if we
211             can get away with a generally cheaper relocation (if
212             required) and switch-style update.
213
214             To do so, we need to know the repository root URL of the
215             external working copy as it currently sits. */
216          err = svn_wc__node_get_repos_info(NULL, NULL,
217                                            &repos_root_url, &repos_uuid,
218                                            ctx->wc_ctx, local_abspath,
219                                            pool, subpool);
220          if (err)
221            {
222              if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
223                  && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
224                return svn_error_trace(err);
225
226              svn_error_clear(err);
227              repos_root_url = NULL;
228              repos_uuid = NULL;
229            }
230
231          if (repos_root_url)
232            {
233              /* If the new external target URL is not obviously a
234                 child of the external working copy's current
235                 repository root URL... */
236              if (! svn_uri__is_ancestor(repos_root_url, url))
237                {
238                  const char *repos_root;
239
240                  /* ... then figure out precisely which repository
241                      root URL that target URL *is* a child of ... */
242                  SVN_ERR(svn_client_get_repos_root(&repos_root, NULL, url,
243                                                    ctx, subpool, subpool));
244
245                  /* ... and use that to try to relocate the external
246                     working copy to the target location.  */
247                  err = svn_client_relocate2(local_abspath, repos_root_url,
248                                             repos_root, FALSE, ctx, subpool);
249
250                  /* If the relocation failed because the new URL
251                     points to a totally different repository, we've
252                     no choice but to relegate and check out a new WC. */
253                  if (err
254                      && (err->apr_err == SVN_ERR_WC_INVALID_RELOCATION
255                          || (err->apr_err
256                              == SVN_ERR_CLIENT_INVALID_RELOCATION)))
257                    {
258                      svn_error_clear(err);
259                      goto relegate;
260                    }
261                  else if (err)
262                    return svn_error_trace(err);
263
264                  /* If the relocation went without a hitch, we should
265                     have a new repository root URL. */
266                  repos_root_url = repos_root;
267                }
268
269              SVN_ERR(svn_client__switch_internal(NULL, local_abspath, url,
270                                                  peg_revision, revision,
271                                                  svn_depth_infinity,
272                                                  TRUE, FALSE, FALSE,
273                                                  TRUE /* ignore_ancestry */,
274                                                  timestamp_sleep,
275                                                  ctx, subpool));
276
277              SVN_ERR(svn_wc__external_register(ctx->wc_ctx,
278                                                defining_abspath,
279                                                local_abspath, svn_node_dir,
280                                                repos_root_url, repos_uuid,
281                                                svn_uri_skip_ancestor(
282                                                            repos_root_url,
283                                                            url, subpool),
284                                                external_peg_rev,
285                                                external_rev,
286                                                subpool));
287
288              svn_pool_destroy(subpool);
289              goto cleanup;
290            }
291        }
292    }
293
294 relegate:
295
296  /* Fall back on removing the WC and checking out a new one. */
297
298  /* Ensure that we don't have any RA sessions or WC locks from failed
299     operations above. */
300  svn_pool_destroy(subpool);
301
302  if (kind == svn_node_dir)
303    {
304      /* Buh-bye, old and busted ... */
305      SVN_ERR(relegate_dir_external(ctx->wc_ctx, defining_abspath,
306                                    local_abspath,
307                                    ctx->cancel_func, ctx->cancel_baton,
308                                    ctx->notify_func2, ctx->notify_baton2,
309                                    pool));
310    }
311  else
312    {
313      /* The target dir might have multiple components.  Guarantee
314         the path leading down to the last component. */
315      const char *parent = svn_dirent_dirname(local_abspath, pool);
316      SVN_ERR(svn_io_make_dir_recursively(parent, pool));
317    }
318
319  /* ... Hello, new hotness. */
320  SVN_ERR(svn_client__checkout_internal(NULL, url, local_abspath, peg_revision,
321                                        revision, svn_depth_infinity,
322                                        FALSE, FALSE, timestamp_sleep,
323                                        ctx, pool));
324
325  SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL,
326                                      &repos_root_url,
327                                      &repos_uuid,
328                                      ctx->wc_ctx, local_abspath,
329                                      pool, pool));
330
331  SVN_ERR(svn_wc__external_register(ctx->wc_ctx,
332                                    defining_abspath,
333                                    local_abspath, svn_node_dir,
334                                    repos_root_url, repos_uuid,
335                                    svn_uri_skip_ancestor(repos_root_url,
336                                                          url, pool),
337                                    external_peg_rev,
338                                    external_rev,
339                                    pool));
340
341 cleanup:
342  /* Issues #4123 and #4130: We don't need to keep the newly checked
343     out external's DB open. */
344  SVN_ERR(svn_wc__close_db(local_abspath, ctx->wc_ctx, pool));
345
346  return SVN_NO_ERROR;
347}
348
349/* Try to update a file external at LOCAL_ABSPATH to URL at REVISION using a
350   access baton that has a write lock.  Use SCRATCH_POOL for temporary
351   allocations, and use the client context CTX. */
352static svn_error_t *
353switch_file_external(const char *local_abspath,
354                     const char *url,
355                     const svn_opt_revision_t *peg_revision,
356                     const svn_opt_revision_t *revision,
357                     const char *def_dir_abspath,
358                     svn_ra_session_t *ra_session,
359                     svn_client_ctx_t *ctx,
360                     apr_pool_t *scratch_pool)
361{
362  svn_config_t *cfg = ctx->config
363                      ? svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG)
364                      : NULL;
365  svn_boolean_t use_commit_times;
366  const char *diff3_cmd;
367  const char *preserved_exts_str;
368  const apr_array_header_t *preserved_exts;
369  svn_node_kind_t kind, external_kind;
370
371  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
372
373  /* See if the user wants last-commit timestamps instead of current ones. */
374  SVN_ERR(svn_config_get_bool(cfg, &use_commit_times,
375                              SVN_CONFIG_SECTION_MISCELLANY,
376                              SVN_CONFIG_OPTION_USE_COMMIT_TIMES, FALSE));
377
378  /* Get the external diff3, if any. */
379  svn_config_get(cfg, &diff3_cmd, SVN_CONFIG_SECTION_HELPERS,
380                 SVN_CONFIG_OPTION_DIFF3_CMD, NULL);
381
382  if (diff3_cmd != NULL)
383    SVN_ERR(svn_path_cstring_to_utf8(&diff3_cmd, diff3_cmd, scratch_pool));
384
385  /* See which files the user wants to preserve the extension of when
386     conflict files are made. */
387  svn_config_get(cfg, &preserved_exts_str, SVN_CONFIG_SECTION_MISCELLANY,
388                 SVN_CONFIG_OPTION_PRESERVED_CF_EXTS, "");
389  preserved_exts = *preserved_exts_str
390    ? svn_cstring_split(preserved_exts_str, "\n\r\t\v ", FALSE, scratch_pool)
391    : NULL;
392
393  {
394    const char *wcroot_abspath;
395
396    SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, local_abspath,
397                               scratch_pool, scratch_pool));
398
399    /* File externals can only be installed inside the current working copy.
400       So verify if the working copy that contains/will contain the target
401       is the defining abspath, or one of its ancestors */
402
403    if (!svn_dirent_is_ancestor(wcroot_abspath, def_dir_abspath))
404        return svn_error_createf(
405                        SVN_ERR_WC_BAD_PATH, NULL,
406                        _("Cannot insert a file external defined on '%s' "
407                          "into the working copy '%s'."),
408                        svn_dirent_local_style(def_dir_abspath,
409                                               scratch_pool),
410                        svn_dirent_local_style(wcroot_abspath,
411                                               scratch_pool));
412  }
413
414  SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath,
415                            TRUE, FALSE, scratch_pool));
416
417  SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL,
418                                     ctx->wc_ctx, local_abspath, local_abspath,
419                                     TRUE, scratch_pool, scratch_pool));
420
421  /* If there is a versioned item with this name, ensure it's a file
422     external before working with it.  If there is no entry in the
423     working copy, then create an empty file and add it to the working
424     copy. */
425  if (kind != svn_node_none && kind != svn_node_unknown)
426    {
427      if (external_kind != svn_node_file)
428        {
429          return svn_error_createf(
430              SVN_ERR_CLIENT_FILE_EXTERNAL_OVERWRITE_VERSIONED, 0,
431             _("The file external from '%s' cannot overwrite the existing "
432               "versioned item at '%s'"),
433             url, svn_dirent_local_style(local_abspath, scratch_pool));
434        }
435    }
436  else
437    {
438      svn_node_kind_t disk_kind;
439
440      SVN_ERR(svn_io_check_path(local_abspath, &disk_kind, scratch_pool));
441
442      if (kind == svn_node_file || kind == svn_node_dir)
443        return svn_error_createf(SVN_ERR_WC_PATH_FOUND, NULL,
444                                 _("The file external '%s' can not be "
445                                   "created because the node exists."),
446                                 svn_dirent_local_style(local_abspath,
447                                                        scratch_pool));
448    }
449
450  {
451    const svn_ra_reporter3_t *reporter;
452    void *report_baton;
453    const svn_delta_editor_t *switch_editor;
454    void *switch_baton;
455    svn_client__pathrev_t *switch_loc;
456    svn_revnum_t revnum;
457    apr_array_header_t *inherited_props;
458    const char *dir_abspath;
459    const char *target;
460
461    svn_dirent_split(&dir_abspath, &target, local_abspath, scratch_pool);
462
463    /* Open an RA session to 'source' URL */
464    SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &switch_loc,
465                                              url, dir_abspath,
466                                              peg_revision, revision,
467                                              ctx, scratch_pool));
468    /* Get the external file's iprops. */
469    SVN_ERR(svn_ra_get_inherited_props(ra_session, &inherited_props, "",
470                                       switch_loc->rev,
471                                       scratch_pool, scratch_pool));
472
473    SVN_ERR(svn_ra_reparent(ra_session, svn_uri_dirname(url, scratch_pool),
474                            scratch_pool));
475
476    SVN_ERR(svn_wc__get_file_external_editor(&switch_editor, &switch_baton,
477                                             &revnum, ctx->wc_ctx,
478                                             local_abspath,
479                                             def_dir_abspath,
480                                             switch_loc->url,
481                                             switch_loc->repos_root_url,
482                                             switch_loc->repos_uuid,
483                                             inherited_props,
484                                             use_commit_times,
485                                             diff3_cmd, preserved_exts,
486                                             def_dir_abspath,
487                                             url, peg_revision, revision,
488                                             ctx->conflict_func2,
489                                             ctx->conflict_baton2,
490                                             ctx->cancel_func,
491                                             ctx->cancel_baton,
492                                             ctx->notify_func2,
493                                             ctx->notify_baton2,
494                                             scratch_pool, scratch_pool));
495
496    /* Tell RA to do an update of URL+TARGET to REVISION; if we pass an
497     invalid revnum, that means RA will use the latest revision. */
498    SVN_ERR(svn_ra_do_switch3(ra_session, &reporter, &report_baton,
499                              switch_loc->rev,
500                              target, svn_depth_unknown, url,
501                              FALSE /* send_copyfrom */,
502                              TRUE /* ignore_ancestry */,
503                              switch_editor, switch_baton,
504                              scratch_pool, scratch_pool));
505
506    SVN_ERR(svn_wc__crawl_file_external(ctx->wc_ctx, local_abspath,
507                                        reporter, report_baton,
508                                        TRUE,  use_commit_times,
509                                        ctx->cancel_func, ctx->cancel_baton,
510                                        ctx->notify_func2, ctx->notify_baton2,
511                                        scratch_pool));
512
513    if (ctx->notify_func2)
514      {
515        svn_wc_notify_t *notify
516          = svn_wc_create_notify(local_abspath, svn_wc_notify_update_completed,
517                                 scratch_pool);
518        notify->kind = svn_node_none;
519        notify->content_state = notify->prop_state
520          = svn_wc_notify_state_inapplicable;
521        notify->lock_state = svn_wc_notify_lock_state_inapplicable;
522        notify->revision = revnum;
523        (*ctx->notify_func2)(ctx->notify_baton2, notify, scratch_pool);
524      }
525  }
526
527  return SVN_NO_ERROR;
528}
529
530/* Wrappers around svn_wc__external_remove, obtaining and releasing a lock for
531   directory externals */
532static svn_error_t *
533remove_external2(svn_boolean_t *removed,
534                svn_wc_context_t *wc_ctx,
535                const char *wri_abspath,
536                const char *local_abspath,
537                svn_node_kind_t external_kind,
538                svn_cancel_func_t cancel_func,
539                void *cancel_baton,
540                apr_pool_t *scratch_pool)
541{
542  SVN_ERR(svn_wc__external_remove(wc_ctx, wri_abspath,
543                                  local_abspath,
544                                  (external_kind == svn_node_none),
545                                  cancel_func, cancel_baton,
546                                  scratch_pool));
547
548  *removed = TRUE;
549  return SVN_NO_ERROR;
550}
551
552
553static svn_error_t *
554remove_external(svn_boolean_t *removed,
555                svn_wc_context_t *wc_ctx,
556                const char *wri_abspath,
557                const char *local_abspath,
558                svn_node_kind_t external_kind,
559                svn_cancel_func_t cancel_func,
560                void *cancel_baton,
561                apr_pool_t *scratch_pool)
562{
563  *removed = FALSE;
564  switch (external_kind)
565    {
566      case svn_node_dir:
567        SVN_WC__CALL_WITH_WRITE_LOCK(
568            remove_external2(removed,
569                             wc_ctx, wri_abspath,
570                             local_abspath, external_kind,
571                             cancel_func, cancel_baton,
572                             scratch_pool),
573            wc_ctx, local_abspath, FALSE, scratch_pool);
574        break;
575      case svn_node_file:
576      default:
577        SVN_ERR(remove_external2(removed,
578                                 wc_ctx, wri_abspath,
579                                 local_abspath, external_kind,
580                                 cancel_func, cancel_baton,
581                                 scratch_pool));
582        break;
583    }
584
585  return SVN_NO_ERROR;
586}
587
588/* Called when an external that is in the EXTERNALS table is no longer
589   referenced from an svn:externals property */
590static svn_error_t *
591handle_external_item_removal(const svn_client_ctx_t *ctx,
592                             const char *defining_abspath,
593                             const char *local_abspath,
594                             apr_pool_t *scratch_pool)
595{
596  svn_error_t *err;
597  svn_node_kind_t external_kind;
598  svn_node_kind_t kind;
599  svn_boolean_t removed = FALSE;
600
601  /* local_abspath should be a wcroot or a file external */
602  SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL,
603                                     ctx->wc_ctx, defining_abspath,
604                                     local_abspath, FALSE,
605                                     scratch_pool, scratch_pool));
606
607  SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath, TRUE, FALSE,
608                           scratch_pool));
609
610  if (external_kind != kind)
611    external_kind = svn_node_none; /* Only remove the registration */
612
613  err = remove_external(&removed,
614                        ctx->wc_ctx, defining_abspath, local_abspath,
615                        external_kind,
616                        ctx->cancel_func, ctx->cancel_baton,
617                        scratch_pool);
618
619  if (err && err->apr_err == SVN_ERR_WC_NOT_LOCKED && removed)
620    {
621      svn_error_clear(err);
622      err = NULL; /* We removed the working copy, so we can't release the
623                     lock that was stored inside */
624    }
625
626  if (ctx->notify_func2)
627    {
628      svn_wc_notify_t *notify =
629          svn_wc_create_notify(local_abspath,
630                               svn_wc_notify_update_external_removed,
631                               scratch_pool);
632
633      notify->kind = kind;
634      notify->err = err;
635
636      (ctx->notify_func2)(ctx->notify_baton2, notify, scratch_pool);
637
638      if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)
639        {
640          notify = svn_wc_create_notify(local_abspath,
641                                      svn_wc_notify_left_local_modifications,
642                                      scratch_pool);
643          notify->kind = svn_node_dir;
644          notify->err = err;
645
646          (ctx->notify_func2)(ctx->notify_baton2, notify, scratch_pool);
647        }
648    }
649
650  if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)
651    {
652      svn_error_clear(err);
653      err = NULL;
654    }
655
656  return svn_error_trace(err);
657}
658
659static svn_error_t *
660handle_external_item_change(svn_client_ctx_t *ctx,
661                            const char *repos_root_url,
662                            const char *parent_dir_abspath,
663                            const char *parent_dir_url,
664                            const char *local_abspath,
665                            const char *old_defining_abspath,
666                            const svn_wc_external_item2_t *new_item,
667                            svn_boolean_t *timestamp_sleep,
668                            apr_pool_t *scratch_pool)
669{
670  svn_ra_session_t *ra_session;
671  svn_client__pathrev_t *new_loc;
672  const char *new_url;
673  svn_node_kind_t ext_kind;
674
675  SVN_ERR_ASSERT(repos_root_url && parent_dir_url);
676  SVN_ERR_ASSERT(new_item != NULL);
677
678  /* Don't bother to check status, since we'll get that for free by
679     attempting to retrieve the hash values anyway.  */
680
681  /* When creating the absolute URL, use the pool and not the
682     iterpool, since the hash table values outlive the iterpool and
683     any pointers they have should also outlive the iterpool.  */
684
685  SVN_ERR(svn_wc__resolve_relative_external_url(&new_url,
686                                                new_item, repos_root_url,
687                                                parent_dir_url,
688                                                scratch_pool, scratch_pool));
689
690  /* Determine if the external is a file or directory. */
691  /* Get the RA connection. */
692  SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc,
693                                            new_url, NULL,
694                                            &(new_item->peg_revision),
695                                            &(new_item->revision), ctx,
696                                            scratch_pool));
697
698  SVN_ERR(svn_ra_check_path(ra_session, "", new_loc->rev, &ext_kind,
699                            scratch_pool));
700
701  if (svn_node_none == ext_kind)
702    return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
703                             _("URL '%s' at revision %ld doesn't exist"),
704                             new_loc->url, new_loc->rev);
705
706  if (svn_node_dir != ext_kind && svn_node_file != ext_kind)
707    return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
708                             _("URL '%s' at revision %ld is not a file "
709                               "or a directory"),
710                             new_loc->url, new_loc->rev);
711
712
713  /* Not protecting against recursive externals.  Detecting them in
714     the global case is hard, and it should be pretty obvious to a
715     user when it happens.  Worst case: your disk fills up :-). */
716
717  /* First notify that we're about to handle an external. */
718  if (ctx->notify_func2)
719    {
720      (*ctx->notify_func2)(
721         ctx->notify_baton2,
722         svn_wc_create_notify(local_abspath,
723                              svn_wc_notify_update_external,
724                              scratch_pool),
725         scratch_pool);
726    }
727
728  if (! old_defining_abspath)
729    {
730      /* The target dir might have multiple components.  Guarantee the path
731         leading down to the last component. */
732      SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(local_abspath,
733                                                             scratch_pool),
734                                          scratch_pool));
735    }
736
737  switch (ext_kind)
738    {
739      case svn_node_dir:
740        SVN_ERR(switch_dir_external(local_abspath, new_loc->url,
741                                    &(new_item->peg_revision),
742                                    &(new_item->revision),
743                                    parent_dir_abspath,
744                                    timestamp_sleep, ctx,
745                                    scratch_pool));
746        break;
747      case svn_node_file:
748        if (strcmp(repos_root_url, new_loc->repos_root_url))
749          {
750            const char *local_repos_root_url;
751            const char *local_repos_uuid;
752            const char *ext_repos_relpath;
753            svn_error_t *err;
754
755            /*
756             * The working copy library currently requires that all files
757             * in the working copy have the same repository root URL.
758             * The URL from the file external's definition differs from the
759             * one used by the working copy. As a workaround, replace the
760             * root URL portion of the file external's URL, after making
761             * sure both URLs point to the same repository. See issue #4087.
762             */
763
764            err = svn_wc__node_get_repos_info(NULL, NULL,
765                                              &local_repos_root_url,
766                                              &local_repos_uuid,
767                                              ctx->wc_ctx, parent_dir_abspath,
768                                              scratch_pool, scratch_pool);
769            if (err)
770              {
771                if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
772                    && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
773                  return svn_error_trace(err);
774
775                svn_error_clear(err);
776                local_repos_root_url = NULL;
777                local_repos_uuid = NULL;
778              }
779
780            ext_repos_relpath = svn_uri_skip_ancestor(new_loc->repos_root_url,
781                                                      new_url, scratch_pool);
782            if (local_repos_uuid == NULL || local_repos_root_url == NULL ||
783                ext_repos_relpath == NULL ||
784                strcmp(local_repos_uuid, new_loc->repos_uuid) != 0)
785              return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
786                        _("Unsupported external: URL of file external '%s' "
787                          "is not in repository '%s'"),
788                        new_url, repos_root_url);
789
790            new_url = svn_path_url_add_component2(local_repos_root_url,
791                                                  ext_repos_relpath,
792                                                  scratch_pool);
793            SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc,
794                                                      new_url,
795                                                      NULL,
796                                                      &(new_item->peg_revision),
797                                                      &(new_item->revision),
798                                                      ctx, scratch_pool));
799          }
800
801        SVN_ERR(switch_file_external(local_abspath,
802                                     new_url,
803                                     &new_item->peg_revision,
804                                     &new_item->revision,
805                                     parent_dir_abspath,
806                                     ra_session,
807                                     ctx,
808                                     scratch_pool));
809        break;
810
811      default:
812        SVN_ERR_MALFUNCTION();
813        break;
814    }
815
816  return SVN_NO_ERROR;
817}
818
819static svn_error_t *
820wrap_external_error(const svn_client_ctx_t *ctx,
821                    const char *target_abspath,
822                    svn_error_t *err,
823                    apr_pool_t *scratch_pool)
824{
825  if (err && err->apr_err != SVN_ERR_CANCELLED)
826    {
827      if (ctx->notify_func2)
828        {
829          svn_wc_notify_t *notifier = svn_wc_create_notify(
830                                            target_abspath,
831                                            svn_wc_notify_failed_external,
832                                            scratch_pool);
833          notifier->err = err;
834          ctx->notify_func2(ctx->notify_baton2, notifier, scratch_pool);
835        }
836      svn_error_clear(err);
837      return SVN_NO_ERROR;
838    }
839
840  return err;
841}
842
843static svn_error_t *
844handle_externals_change(svn_client_ctx_t *ctx,
845                        const char *repos_root_url,
846                        svn_boolean_t *timestamp_sleep,
847                        const char *local_abspath,
848                        const char *new_desc_text,
849                        apr_hash_t *old_externals,
850                        svn_depth_t ambient_depth,
851                        svn_depth_t requested_depth,
852                        apr_pool_t *scratch_pool)
853{
854  apr_array_header_t *new_desc;
855  int i;
856  apr_pool_t *iterpool;
857  const char *url;
858
859  iterpool = svn_pool_create(scratch_pool);
860
861  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
862
863  /* Bag out if the depth here is too shallow for externals action. */
864  if ((requested_depth < svn_depth_infinity
865       && requested_depth != svn_depth_unknown)
866      || (ambient_depth < svn_depth_infinity
867          && requested_depth < svn_depth_infinity))
868    return SVN_NO_ERROR;
869
870  if (new_desc_text)
871    SVN_ERR(svn_wc_parse_externals_description3(&new_desc, local_abspath,
872                                                new_desc_text,
873                                                FALSE, scratch_pool));
874  else
875    new_desc = NULL;
876
877  SVN_ERR(svn_wc__node_get_url(&url, ctx->wc_ctx, local_abspath,
878                               scratch_pool, iterpool));
879
880  SVN_ERR_ASSERT(url);
881
882  for (i = 0; new_desc && (i < new_desc->nelts); i++)
883    {
884      const char *old_defining_abspath;
885      svn_wc_external_item2_t *new_item;
886      const char *target_abspath;
887      svn_boolean_t under_root;
888
889      new_item = APR_ARRAY_IDX(new_desc, i, svn_wc_external_item2_t *);
890
891      svn_pool_clear(iterpool);
892
893      if (ctx->cancel_func)
894        SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
895
896      SVN_ERR(svn_dirent_is_under_root(&under_root, &target_abspath,
897                                       local_abspath, new_item->target_dir,
898                                       iterpool));
899
900      if (! under_root)
901        {
902          return svn_error_createf(
903                    SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
904                    _("Path '%s' is not in the working copy"),
905                    svn_dirent_local_style(
906                        svn_dirent_join(local_abspath, new_item->target_dir,
907                                        iterpool),
908                        iterpool));
909        }
910
911      old_defining_abspath = svn_hash_gets(old_externals, target_abspath);
912
913      SVN_ERR(wrap_external_error(
914                      ctx, target_abspath,
915                      handle_external_item_change(ctx,
916                                                  repos_root_url,
917                                                  local_abspath, url,
918                                                  target_abspath,
919                                                  old_defining_abspath,
920                                                  new_item,
921                                                  timestamp_sleep,
922                                                  iterpool),
923                      iterpool));
924
925      /* And remove already processed items from the to-remove hash */
926      if (old_defining_abspath)
927        svn_hash_sets(old_externals, target_abspath, NULL);
928    }
929
930  svn_pool_destroy(iterpool);
931
932  return SVN_NO_ERROR;
933}
934
935
936svn_error_t *
937svn_client__handle_externals(apr_hash_t *externals_new,
938                             apr_hash_t *ambient_depths,
939                             const char *repos_root_url,
940                             const char *target_abspath,
941                             svn_depth_t requested_depth,
942                             svn_boolean_t *timestamp_sleep,
943                             svn_client_ctx_t *ctx,
944                             apr_pool_t *scratch_pool)
945{
946  apr_hash_t *old_external_defs;
947  apr_hash_index_t *hi;
948  apr_pool_t *iterpool;
949
950  SVN_ERR_ASSERT(repos_root_url);
951
952  iterpool = svn_pool_create(scratch_pool);
953
954  SVN_ERR(svn_wc__externals_defined_below(&old_external_defs,
955                                          ctx->wc_ctx, target_abspath,
956                                          scratch_pool, iterpool));
957
958  for (hi = apr_hash_first(scratch_pool, externals_new);
959       hi;
960       hi = apr_hash_next(hi))
961    {
962      const char *local_abspath = svn__apr_hash_index_key(hi);
963      const char *desc_text = svn__apr_hash_index_val(hi);
964      svn_depth_t ambient_depth = svn_depth_infinity;
965
966      svn_pool_clear(iterpool);
967
968      if (ambient_depths)
969        {
970          const char *ambient_depth_w;
971
972          ambient_depth_w = apr_hash_get(ambient_depths, local_abspath,
973                                         svn__apr_hash_index_klen(hi));
974
975          if (ambient_depth_w == NULL)
976            {
977              return svn_error_createf(
978                        SVN_ERR_WC_CORRUPT, NULL,
979                        _("Traversal of '%s' found no ambient depth"),
980                        svn_dirent_local_style(local_abspath, scratch_pool));
981            }
982          else
983            {
984              ambient_depth = svn_depth_from_word(ambient_depth_w);
985            }
986        }
987
988      SVN_ERR(handle_externals_change(ctx, repos_root_url, timestamp_sleep,
989                                      local_abspath,
990                                      desc_text, old_external_defs,
991                                      ambient_depth, requested_depth,
992                                      iterpool));
993    }
994
995  /* Remove the remaining externals */
996  for (hi = apr_hash_first(scratch_pool, old_external_defs);
997       hi;
998       hi = apr_hash_next(hi))
999    {
1000      const char *item_abspath = svn__apr_hash_index_key(hi);
1001      const char *defining_abspath = svn__apr_hash_index_val(hi);
1002      const char *parent_abspath;
1003
1004      svn_pool_clear(iterpool);
1005
1006      SVN_ERR(wrap_external_error(
1007                          ctx, item_abspath,
1008                          handle_external_item_removal(ctx, defining_abspath,
1009                                                       item_abspath, iterpool),
1010                          iterpool));
1011
1012      /* Are there any unversioned directories between the removed
1013       * external and the DEFINING_ABSPATH which we can remove? */
1014      parent_abspath = item_abspath;
1015      do {
1016        svn_node_kind_t kind;
1017
1018        parent_abspath = svn_dirent_dirname(parent_abspath, iterpool);
1019        SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, parent_abspath,
1020                                  FALSE /* show_deleted*/,
1021                                  FALSE /* show_hidden */,
1022                                  iterpool));
1023        if (kind == svn_node_none)
1024          {
1025            svn_error_t *err;
1026
1027            err = svn_io_dir_remove_nonrecursive(parent_abspath, iterpool);
1028            if (err)
1029              {
1030                if (APR_STATUS_IS_ENOTEMPTY(err->apr_err))
1031                  {
1032                    svn_error_clear(err);
1033                    break; /* No parents to delete */
1034                  }
1035                else if (APR_STATUS_IS_ENOENT(err->apr_err)
1036                         || APR_STATUS_IS_ENOTDIR(err->apr_err))
1037                  {
1038                    svn_error_clear(err);
1039                    /* Fall through; parent dir might be unversioned */
1040                  }
1041                else
1042                  return svn_error_trace(err);
1043              }
1044          }
1045      } while (strcmp(parent_abspath, defining_abspath) != 0);
1046    }
1047
1048
1049  svn_pool_destroy(iterpool);
1050  return SVN_NO_ERROR;
1051}
1052
1053
1054svn_error_t *
1055svn_client__export_externals(apr_hash_t *externals,
1056                             const char *from_url,
1057                             const char *to_abspath,
1058                             const char *repos_root_url,
1059                             svn_depth_t requested_depth,
1060                             const char *native_eol,
1061                             svn_boolean_t ignore_keywords,
1062                             svn_client_ctx_t *ctx,
1063                             apr_pool_t *scratch_pool)
1064{
1065  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
1066  apr_pool_t *sub_iterpool = svn_pool_create(scratch_pool);
1067  apr_hash_index_t *hi;
1068
1069  SVN_ERR_ASSERT(svn_dirent_is_absolute(to_abspath));
1070
1071  for (hi = apr_hash_first(scratch_pool, externals);
1072       hi;
1073       hi = apr_hash_next(hi))
1074    {
1075      const char *local_abspath = svn__apr_hash_index_key(hi);
1076      const char *desc_text = svn__apr_hash_index_val(hi);
1077      const char *local_relpath;
1078      const char *dir_url;
1079      apr_array_header_t *items;
1080      int i;
1081
1082      svn_pool_clear(iterpool);
1083
1084      SVN_ERR(svn_wc_parse_externals_description3(&items, local_abspath,
1085                                                  desc_text, FALSE,
1086                                                  iterpool));
1087
1088      if (! items->nelts)
1089        continue;
1090
1091      local_relpath = svn_dirent_skip_ancestor(to_abspath, local_abspath);
1092
1093      dir_url = svn_path_url_add_component2(from_url, local_relpath,
1094                                            scratch_pool);
1095
1096      for (i = 0; i < items->nelts; i++)
1097        {
1098          const char *item_abspath;
1099          const char *new_url;
1100          svn_boolean_t under_root;
1101          svn_wc_external_item2_t *item = APR_ARRAY_IDX(items, i,
1102                                                svn_wc_external_item2_t *);
1103
1104          svn_pool_clear(sub_iterpool);
1105
1106          SVN_ERR(svn_dirent_is_under_root(&under_root, &item_abspath,
1107                                           local_abspath, item->target_dir,
1108                                           sub_iterpool));
1109
1110          if (! under_root)
1111            {
1112              return svn_error_createf(
1113                        SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
1114                        _("Path '%s' is not in the working copy"),
1115                        svn_dirent_local_style(
1116                            svn_dirent_join(local_abspath, item->target_dir,
1117                                            sub_iterpool),
1118                            sub_iterpool));
1119            }
1120
1121          SVN_ERR(svn_wc__resolve_relative_external_url(&new_url, item,
1122                                                        repos_root_url,
1123                                                        dir_url, sub_iterpool,
1124                                                        sub_iterpool));
1125
1126          /* The target dir might have multiple components.  Guarantee
1127             the path leading down to the last component. */
1128          SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(item_abspath,
1129                                                                 sub_iterpool),
1130                                              sub_iterpool));
1131
1132          SVN_ERR(wrap_external_error(
1133                          ctx, item_abspath,
1134                          svn_client_export5(NULL, new_url, item_abspath,
1135                                             &item->peg_revision,
1136                                             &item->revision,
1137                                             TRUE, FALSE, ignore_keywords,
1138                                             svn_depth_infinity,
1139                                             native_eol,
1140                                             ctx, sub_iterpool),
1141                          sub_iterpool));
1142        }
1143    }
1144
1145  svn_pool_destroy(sub_iterpool);
1146  svn_pool_destroy(iterpool);
1147
1148  return SVN_NO_ERROR;
1149}
1150
1151