delete.c revision 269847
1/*
2 * delete.c:  wrappers around wc delete functionality.
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_file_io.h>
31#include "svn_hash.h"
32#include "svn_types.h"
33#include "svn_pools.h"
34#include "svn_wc.h"
35#include "svn_client.h"
36#include "svn_error.h"
37#include "svn_dirent_uri.h"
38#include "svn_path.h"
39#include "client.h"
40
41#include "private/svn_client_private.h"
42#include "private/svn_wc_private.h"
43#include "private/svn_ra_private.h"
44
45#include "svn_private_config.h"
46
47
48/*** Code. ***/
49
50/* Baton for find_undeletables */
51struct can_delete_baton_t
52{
53  const char *root_abspath;
54  svn_boolean_t target_missing;
55};
56
57/* An svn_wc_status_func4_t callback function for finding
58   status structures which are not safely deletable. */
59static svn_error_t *
60find_undeletables(void *baton,
61                  const char *local_abspath,
62                  const svn_wc_status3_t *status,
63                  apr_pool_t *pool)
64{
65  if (status->node_status == svn_wc_status_missing)
66    {
67      struct can_delete_baton_t *cdt = baton;
68
69      if (strcmp(cdt->root_abspath, local_abspath) == 0)
70        cdt->target_missing = TRUE;
71    }
72
73  /* Check for error-ful states. */
74  if (status->node_status == svn_wc_status_obstructed)
75    return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
76                             _("'%s' is in the way of the resource "
77                               "actually under version control"),
78                             svn_dirent_local_style(local_abspath, pool));
79  else if (! status->versioned)
80    return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL,
81                             _("'%s' is not under version control"),
82                             svn_dirent_local_style(local_abspath, pool));
83  else if ((status->node_status == svn_wc_status_added
84            || status->node_status == svn_wc_status_replaced)
85           && status->text_status == svn_wc_status_normal
86           && (status->prop_status == svn_wc_status_normal
87               || status->prop_status == svn_wc_status_none))
88    {
89      /* Unmodified copy. Go ahead, remove it */
90    }
91  else if (status->node_status != svn_wc_status_normal
92           && status->node_status != svn_wc_status_deleted
93           && status->node_status != svn_wc_status_missing)
94    return svn_error_createf(SVN_ERR_CLIENT_MODIFIED, NULL,
95                             _("'%s' has local modifications -- commit or "
96                               "revert them first"),
97                             svn_dirent_local_style(local_abspath, pool));
98
99  return SVN_NO_ERROR;
100}
101
102/* Check whether LOCAL_ABSPATH is an external and raise an error if it is.
103
104   A file external should not be deleted since the file external is
105   implemented as a switched file and it would delete the file the
106   file external is switched to, which is not the behavior the user
107   would probably want.
108
109   A directory external should not be deleted since it is the root
110   of a different working copy. */
111static svn_error_t *
112check_external(const char *local_abspath,
113               svn_client_ctx_t *ctx,
114               apr_pool_t *scratch_pool)
115{
116  svn_node_kind_t external_kind;
117  const char *defining_abspath;
118
119  SVN_ERR(svn_wc__read_external_info(&external_kind, &defining_abspath, NULL,
120                                     NULL, NULL,
121                                     ctx->wc_ctx, local_abspath,
122                                     local_abspath, TRUE,
123                                     scratch_pool, scratch_pool));
124
125  if (external_kind != svn_node_none)
126    return svn_error_createf(SVN_ERR_WC_CANNOT_DELETE_FILE_EXTERNAL, NULL,
127                             _("Cannot remove the external at '%s'; "
128                               "please edit or delete the svn:externals "
129                               "property on '%s'"),
130                             svn_dirent_local_style(local_abspath,
131                                                    scratch_pool),
132                             svn_dirent_local_style(defining_abspath,
133                                                    scratch_pool));
134
135  return SVN_NO_ERROR;
136}
137
138/* Verify that the path can be deleted without losing stuff,
139   i.e. ensure that there are no modified or unversioned resources
140   under PATH.  This is similar to checking the output of the status
141   command.  CTX is used for the client's config options.  POOL is
142   used for all temporary allocations. */
143static svn_error_t *
144can_delete_node(svn_boolean_t *target_missing,
145                const char *local_abspath,
146                svn_client_ctx_t *ctx,
147                apr_pool_t *scratch_pool)
148{
149  apr_array_header_t *ignores;
150  struct can_delete_baton_t cdt;
151
152  /* Use an infinite-depth status check to see if there's anything in
153     or under PATH which would make it unsafe for deletion.  The
154     status callback function find_undeletables() makes the
155     determination, returning an error if it finds anything that shouldn't
156     be deleted. */
157
158  SVN_ERR(svn_wc_get_default_ignores(&ignores, ctx->config, scratch_pool));
159
160  cdt.root_abspath = local_abspath;
161  cdt.target_missing = FALSE;
162
163  SVN_ERR(svn_wc_walk_status(ctx->wc_ctx,
164                             local_abspath,
165                             svn_depth_infinity,
166                             FALSE /* get_all */,
167                             FALSE /* no_ignore */,
168                             FALSE /* ignore_text_mod */,
169                             ignores,
170                             find_undeletables, &cdt,
171                             ctx->cancel_func,
172                             ctx->cancel_baton,
173                             scratch_pool));
174
175  if (target_missing)
176    *target_missing = cdt.target_missing;
177
178  return SVN_NO_ERROR;
179}
180
181
182static svn_error_t *
183path_driver_cb_func(void **dir_baton,
184                    void *parent_baton,
185                    void *callback_baton,
186                    const char *path,
187                    apr_pool_t *pool)
188{
189  const svn_delta_editor_t *editor = callback_baton;
190  *dir_baton = NULL;
191  return editor->delete_entry(path, SVN_INVALID_REVNUM, parent_baton, pool);
192}
193
194static svn_error_t *
195single_repos_delete(svn_ra_session_t *ra_session,
196                    const char *base_uri,
197                    const apr_array_header_t *relpaths,
198                    const apr_hash_t *revprop_table,
199                    svn_commit_callback2_t commit_callback,
200                    void *commit_baton,
201                    svn_client_ctx_t *ctx,
202                    apr_pool_t *pool)
203{
204  const svn_delta_editor_t *editor;
205  apr_hash_t *commit_revprops;
206  void *edit_baton;
207  const char *log_msg;
208  int i;
209  svn_error_t *err;
210
211  /* Create new commit items and add them to the array. */
212  if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx))
213    {
214      svn_client_commit_item3_t *item;
215      const char *tmp_file;
216      apr_array_header_t *commit_items
217        = apr_array_make(pool, relpaths->nelts, sizeof(item));
218
219      for (i = 0; i < relpaths->nelts; i++)
220        {
221          const char *relpath = APR_ARRAY_IDX(relpaths, i, const char *);
222
223          item = svn_client_commit_item3_create(pool);
224          item->url = svn_path_url_add_component2(base_uri, relpath, pool);
225          item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE;
226          APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
227        }
228      SVN_ERR(svn_client__get_log_msg(&log_msg, &tmp_file, commit_items,
229                                      ctx, pool));
230      if (! log_msg)
231        return SVN_NO_ERROR;
232    }
233  else
234    log_msg = "";
235
236  SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
237                                           log_msg, ctx, pool));
238
239  /* Fetch RA commit editor */
240  SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session,
241                        svn_client__get_shim_callbacks(ctx->wc_ctx,
242                                                       NULL, pool)));
243  SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton,
244                                    commit_revprops,
245                                    commit_callback,
246                                    commit_baton,
247                                    NULL, TRUE, /* No lock tokens */
248                                    pool));
249
250  /* Call the path-based editor driver. */
251  err = svn_delta_path_driver2(editor, edit_baton, relpaths, TRUE,
252                               path_driver_cb_func, (void *)editor, pool);
253
254  if (err)
255    {
256      return svn_error_trace(
257               svn_error_compose_create(err,
258                                        editor->abort_edit(edit_baton, pool)));
259    }
260
261  /* Close the edit. */
262  return svn_error_trace(editor->close_edit(edit_baton, pool));
263}
264
265
266/* Structure for tracking remote delete targets associated with a
267   specific repository. */
268struct repos_deletables_t
269{
270  svn_ra_session_t *ra_session;
271  apr_array_header_t *target_uris;
272};
273
274
275static svn_error_t *
276delete_urls_multi_repos(const apr_array_header_t *uris,
277                        const apr_hash_t *revprop_table,
278                        svn_commit_callback2_t commit_callback,
279                        void *commit_baton,
280                        svn_client_ctx_t *ctx,
281                        apr_pool_t *pool)
282{
283  apr_hash_t *deletables = apr_hash_make(pool);
284  apr_pool_t *iterpool;
285  apr_hash_index_t *hi;
286  int i;
287
288  /* Create a hash mapping repository root URLs -> repos_deletables_t *
289     structures.  */
290  for (i = 0; i < uris->nelts; i++)
291    {
292      const char *uri = APR_ARRAY_IDX(uris, i, const char *);
293      struct repos_deletables_t *repos_deletables = NULL;
294      const char *repos_relpath;
295      svn_node_kind_t kind;
296
297      for (hi = apr_hash_first(pool, deletables); hi; hi = apr_hash_next(hi))
298        {
299          const char *repos_root = svn__apr_hash_index_key(hi);
300
301          repos_relpath = svn_uri_skip_ancestor(repos_root, uri, pool);
302          if (repos_relpath)
303            {
304              /* Great!  We've found another URI underneath this
305                 session.  We'll pick out the related RA session for
306                 use later, store the new target, and move on.  */
307              repos_deletables = svn__apr_hash_index_val(hi);
308              APR_ARRAY_PUSH(repos_deletables->target_uris, const char *) =
309                apr_pstrdup(pool, uri);
310              break;
311            }
312        }
313
314      /* If we haven't created a repos_deletable structure for this
315         delete target, we need to do.  That means opening up an RA
316         session and initializing its targets list.  */
317      if (!repos_deletables)
318        {
319          svn_ra_session_t *ra_session = NULL;
320          const char *repos_root;
321          apr_array_header_t *target_uris;
322
323          /* Open an RA session to (ultimately) the root of the
324             repository in which URI is found.  */
325          SVN_ERR(svn_client_open_ra_session2(&ra_session, uri, NULL,
326                                              ctx, pool, pool));
327          SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root, pool));
328          SVN_ERR(svn_ra_reparent(ra_session, repos_root, pool));
329          repos_relpath = svn_uri_skip_ancestor(repos_root, uri, pool);
330
331          /* Make a new relpaths list for this repository, and add
332             this URI's relpath to it. */
333          target_uris = apr_array_make(pool, 1, sizeof(const char *));
334          APR_ARRAY_PUSH(target_uris, const char *) = apr_pstrdup(pool, uri);
335
336          /* Build our repos_deletables_t item and stash it in the
337             hash. */
338          repos_deletables = apr_pcalloc(pool, sizeof(*repos_deletables));
339          repos_deletables->ra_session = ra_session;
340          repos_deletables->target_uris = target_uris;
341          svn_hash_sets(deletables, repos_root, repos_deletables);
342        }
343
344      /* If we get here, we should have been able to calculate a
345         repos_relpath for this URI.  Let's make sure.  (We return an
346         RA error code otherwise for 1.6 compatibility.)  */
347      if (!repos_relpath || !*repos_relpath)
348        return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
349                                 "URL '%s' not within a repository", uri);
350
351      /* Now, test to see if the thing actually exists in HEAD. */
352      SVN_ERR(svn_ra_check_path(repos_deletables->ra_session, repos_relpath,
353                                SVN_INVALID_REVNUM, &kind, pool));
354      if (kind == svn_node_none)
355        return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
356                                 "URL '%s' does not exist", uri);
357    }
358
359  /* Now we iterate over the DELETABLES hash, issuing a commit for
360     each repository with its associated collected targets. */
361  iterpool = svn_pool_create(pool);
362  for (hi = apr_hash_first(pool, deletables); hi; hi = apr_hash_next(hi))
363    {
364      struct repos_deletables_t *repos_deletables = svn__apr_hash_index_val(hi);
365      const char *base_uri;
366      apr_array_header_t *target_relpaths;
367
368      svn_pool_clear(iterpool);
369
370      /* We want to anchor the commit on the longest common path
371         across the targets for this one repository.  If, however, one
372         of our targets is that longest common path, we need instead
373         anchor the commit on that path's immediate parent.  Because
374         we're asking svn_uri_condense_targets() to remove
375         redundancies, this situation should be detectable by their
376         being returned either a) only a single, empty-path, target
377         relpath, or b) no target relpaths at all.  */
378      SVN_ERR(svn_uri_condense_targets(&base_uri, &target_relpaths,
379                                       repos_deletables->target_uris,
380                                       TRUE, iterpool, iterpool));
381      SVN_ERR_ASSERT(!svn_path_is_empty(base_uri));
382      if (target_relpaths->nelts == 0)
383        {
384          const char *target_relpath;
385
386          svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool);
387          APR_ARRAY_PUSH(target_relpaths, const char *) = target_relpath;
388        }
389      else if ((target_relpaths->nelts == 1)
390               && (svn_path_is_empty(APR_ARRAY_IDX(target_relpaths, 0,
391                                                   const char *))))
392        {
393          const char *target_relpath;
394
395          svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool);
396          APR_ARRAY_IDX(target_relpaths, 0, const char *) = target_relpath;
397        }
398
399      SVN_ERR(svn_ra_reparent(repos_deletables->ra_session, base_uri, pool));
400      SVN_ERR(single_repos_delete(repos_deletables->ra_session, base_uri,
401                                  target_relpaths,
402                                  revprop_table, commit_callback,
403                                  commit_baton, ctx, iterpool));
404    }
405  svn_pool_destroy(iterpool);
406
407  return SVN_NO_ERROR;
408}
409
410svn_error_t *
411svn_client__wc_delete(const char *local_abspath,
412                      svn_boolean_t force,
413                      svn_boolean_t dry_run,
414                      svn_boolean_t keep_local,
415                      svn_wc_notify_func2_t notify_func,
416                      void *notify_baton,
417                      svn_client_ctx_t *ctx,
418                      apr_pool_t *pool)
419{
420  svn_boolean_t target_missing = FALSE;
421
422  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
423
424  SVN_ERR(check_external(local_abspath, ctx, pool));
425
426  if (!force && !keep_local)
427    /* Verify that there are no "awkward" files */
428    SVN_ERR(can_delete_node(&target_missing, local_abspath, ctx, pool));
429
430  if (!dry_run)
431    /* Mark the entry for commit deletion and perform wc deletion */
432    return svn_error_trace(svn_wc_delete4(ctx->wc_ctx, local_abspath,
433                                          keep_local || target_missing
434                                                            /*keep_local */,
435                                          TRUE /* delete_unversioned */,
436                                          ctx->cancel_func, ctx->cancel_baton,
437                                          notify_func, notify_baton, pool));
438
439  return SVN_NO_ERROR;
440}
441
442svn_error_t *
443svn_client__wc_delete_many(const apr_array_header_t *targets,
444                           svn_boolean_t force,
445                           svn_boolean_t dry_run,
446                           svn_boolean_t keep_local,
447                           svn_wc_notify_func2_t notify_func,
448                           void *notify_baton,
449                           svn_client_ctx_t *ctx,
450                           apr_pool_t *pool)
451{
452  int i;
453  svn_boolean_t has_non_missing = FALSE;
454
455  for (i = 0; i < targets->nelts; i++)
456    {
457      const char *local_abspath = APR_ARRAY_IDX(targets, i, const char *);
458
459      SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
460
461      SVN_ERR(check_external(local_abspath, ctx, pool));
462
463      if (!force && !keep_local)
464        {
465          svn_boolean_t missing;
466          /* Verify that there are no "awkward" files */
467
468          SVN_ERR(can_delete_node(&missing, local_abspath, ctx, pool));
469
470          if (! missing)
471            has_non_missing = TRUE;
472        }
473      else
474        has_non_missing = TRUE;
475    }
476
477  if (!dry_run)
478    {
479      /* Mark the entry for commit deletion and perform wc deletion */
480
481      /* If none of the targets exists, pass keep local TRUE, to avoid
482         deleting case-different files. Detecting this in the generic case
483         from the delete code is expensive */
484      return svn_error_trace(svn_wc__delete_many(ctx->wc_ctx, targets,
485                                                 keep_local || !has_non_missing,
486                                                 TRUE /* delete_unversioned_target */,
487                                                 ctx->cancel_func,
488                                                 ctx->cancel_baton,
489                                                 notify_func, notify_baton,
490                                                 pool));
491    }
492
493  return SVN_NO_ERROR;
494}
495
496svn_error_t *
497svn_client_delete4(const apr_array_header_t *paths,
498                   svn_boolean_t force,
499                   svn_boolean_t keep_local,
500                   const apr_hash_t *revprop_table,
501                   svn_commit_callback2_t commit_callback,
502                   void *commit_baton,
503                   svn_client_ctx_t *ctx,
504                   apr_pool_t *pool)
505{
506  svn_boolean_t is_url;
507
508  if (! paths->nelts)
509    return SVN_NO_ERROR;
510
511  SVN_ERR(svn_client__assert_homogeneous_target_type(paths));
512  is_url = svn_path_is_url(APR_ARRAY_IDX(paths, 0, const char *));
513
514  if (is_url)
515    {
516      SVN_ERR(delete_urls_multi_repos(paths, revprop_table, commit_callback,
517                                      commit_baton, ctx, pool));
518    }
519  else
520    {
521      const char *local_abspath;
522      apr_hash_t *wcroots;
523      apr_hash_index_t *hi;
524      int i;
525      int j;
526      apr_pool_t *iterpool;
527      svn_boolean_t is_new_target;
528
529      /* Build a map of wcroots and targets within them. */
530      wcroots = apr_hash_make(pool);
531      iterpool = svn_pool_create(pool);
532      for (i = 0; i < paths->nelts; i++)
533        {
534          const char *wcroot_abspath;
535          apr_array_header_t *targets;
536
537          svn_pool_clear(iterpool);
538
539          /* See if the user wants us to stop. */
540          if (ctx->cancel_func)
541            SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
542
543          SVN_ERR(svn_dirent_get_absolute(&local_abspath,
544                                          APR_ARRAY_IDX(paths, i,
545                                                        const char *),
546                                          pool));
547          SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
548                                     local_abspath, pool, iterpool));
549          targets = svn_hash_gets(wcroots, wcroot_abspath);
550          if (targets == NULL)
551            {
552              targets = apr_array_make(pool, 1, sizeof(const char *));
553              svn_hash_sets(wcroots, wcroot_abspath, targets);
554             }
555
556          /* Make sure targets are unique. */
557          is_new_target = TRUE;
558          for (j = 0; j < targets->nelts; j++)
559            {
560              if (strcmp(APR_ARRAY_IDX(targets, j, const char *),
561                         local_abspath) == 0)
562                {
563                  is_new_target = FALSE;
564                  break;
565                }
566            }
567
568          if (is_new_target)
569            APR_ARRAY_PUSH(targets, const char *) = local_abspath;
570        }
571
572      /* Delete the targets from each working copy in turn. */
573      for (hi = apr_hash_first(pool, wcroots); hi; hi = apr_hash_next(hi))
574        {
575          const char *root_abspath;
576          const apr_array_header_t *targets = svn__apr_hash_index_val(hi);
577
578          svn_pool_clear(iterpool);
579
580          SVN_ERR(svn_dirent_condense_targets(&root_abspath, NULL, targets,
581                                              FALSE, iterpool, iterpool));
582
583          SVN_WC__CALL_WITH_WRITE_LOCK(
584            svn_client__wc_delete_many(targets, force, FALSE, keep_local,
585                                       ctx->notify_func2, ctx->notify_baton2,
586                                       ctx, iterpool),
587            ctx->wc_ctx, root_abspath, TRUE /* lock_anchor */,
588            iterpool);
589        }
590      svn_pool_destroy(iterpool);
591    }
592
593  return SVN_NO_ERROR;
594}
595