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