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 *repos_root,
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(repos_root, 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      const char *repos_root = svn__apr_hash_index_key(hi);
365      struct repos_deletables_t *repos_deletables = svn__apr_hash_index_val(hi);
366      const char *base_uri;
367      apr_array_header_t *target_relpaths;
368
369      svn_pool_clear(iterpool);
370
371      /* We want to anchor the commit on the longest common path
372         across the targets for this one repository.  If, however, one
373         of our targets is that longest common path, we need instead
374         anchor the commit on that path's immediate parent.  Because
375         we're asking svn_uri_condense_targets() to remove
376         redundancies, this situation should be detectable by their
377         being returned either a) only a single, empty-path, target
378         relpath, or b) no target relpaths at all.  */
379      SVN_ERR(svn_uri_condense_targets(&base_uri, &target_relpaths,
380                                       repos_deletables->target_uris,
381                                       TRUE, iterpool, iterpool));
382      SVN_ERR_ASSERT(!svn_path_is_empty(base_uri));
383      if (target_relpaths->nelts == 0)
384        {
385          const char *target_relpath;
386
387          svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool);
388          APR_ARRAY_PUSH(target_relpaths, const char *) = target_relpath;
389        }
390      else if ((target_relpaths->nelts == 1)
391               && (svn_path_is_empty(APR_ARRAY_IDX(target_relpaths, 0,
392                                                   const char *))))
393        {
394          const char *target_relpath;
395
396          svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool);
397          APR_ARRAY_IDX(target_relpaths, 0, const char *) = target_relpath;
398        }
399
400      SVN_ERR(svn_ra_reparent(repos_deletables->ra_session, base_uri, pool));
401      SVN_ERR(single_repos_delete(repos_deletables->ra_session, repos_root,
402                                  target_relpaths,
403                                  revprop_table, commit_callback,
404                                  commit_baton, ctx, iterpool));
405    }
406  svn_pool_destroy(iterpool);
407
408  return SVN_NO_ERROR;
409}
410
411svn_error_t *
412svn_client__wc_delete(const char *local_abspath,
413                      svn_boolean_t force,
414                      svn_boolean_t dry_run,
415                      svn_boolean_t keep_local,
416                      svn_wc_notify_func2_t notify_func,
417                      void *notify_baton,
418                      svn_client_ctx_t *ctx,
419                      apr_pool_t *pool)
420{
421  svn_boolean_t target_missing = FALSE;
422
423  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
424
425  SVN_ERR(check_external(local_abspath, ctx, pool));
426
427  if (!force && !keep_local)
428    /* Verify that there are no "awkward" files */
429    SVN_ERR(can_delete_node(&target_missing, local_abspath, ctx, pool));
430
431  if (!dry_run)
432    /* Mark the entry for commit deletion and perform wc deletion */
433    return svn_error_trace(svn_wc_delete4(ctx->wc_ctx, local_abspath,
434                                          keep_local || target_missing
435                                                            /*keep_local */,
436                                          TRUE /* delete_unversioned */,
437                                          ctx->cancel_func, ctx->cancel_baton,
438                                          notify_func, notify_baton, pool));
439
440  return SVN_NO_ERROR;
441}
442
443svn_error_t *
444svn_client__wc_delete_many(const apr_array_header_t *targets,
445                           svn_boolean_t force,
446                           svn_boolean_t dry_run,
447                           svn_boolean_t keep_local,
448                           svn_wc_notify_func2_t notify_func,
449                           void *notify_baton,
450                           svn_client_ctx_t *ctx,
451                           apr_pool_t *pool)
452{
453  int i;
454  svn_boolean_t has_non_missing = FALSE;
455
456  for (i = 0; i < targets->nelts; i++)
457    {
458      const char *local_abspath = APR_ARRAY_IDX(targets, i, const char *);
459
460      SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
461
462      SVN_ERR(check_external(local_abspath, ctx, pool));
463
464      if (!force && !keep_local)
465        {
466          svn_boolean_t missing;
467          /* Verify that there are no "awkward" files */
468
469          SVN_ERR(can_delete_node(&missing, local_abspath, ctx, pool));
470
471          if (! missing)
472            has_non_missing = TRUE;
473        }
474      else
475        has_non_missing = TRUE;
476    }
477
478  if (!dry_run)
479    {
480      /* Mark the entry for commit deletion and perform wc deletion */
481
482      /* If none of the targets exists, pass keep local TRUE, to avoid
483         deleting case-different files. Detecting this in the generic case
484         from the delete code is expensive */
485      return svn_error_trace(svn_wc__delete_many(ctx->wc_ctx, targets,
486                                                 keep_local || !has_non_missing,
487                                                 TRUE /* delete_unversioned_target */,
488                                                 ctx->cancel_func,
489                                                 ctx->cancel_baton,
490                                                 notify_func, notify_baton,
491                                                 pool));
492    }
493
494  return SVN_NO_ERROR;
495}
496
497svn_error_t *
498svn_client_delete4(const apr_array_header_t *paths,
499                   svn_boolean_t force,
500                   svn_boolean_t keep_local,
501                   const apr_hash_t *revprop_table,
502                   svn_commit_callback2_t commit_callback,
503                   void *commit_baton,
504                   svn_client_ctx_t *ctx,
505                   apr_pool_t *pool)
506{
507  svn_boolean_t is_url;
508
509  if (! paths->nelts)
510    return SVN_NO_ERROR;
511
512  SVN_ERR(svn_client__assert_homogeneous_target_type(paths));
513  is_url = svn_path_is_url(APR_ARRAY_IDX(paths, 0, const char *));
514
515  if (is_url)
516    {
517      SVN_ERR(delete_urls_multi_repos(paths, revprop_table, commit_callback,
518                                      commit_baton, ctx, pool));
519    }
520  else
521    {
522      const char *local_abspath;
523      apr_hash_t *wcroots;
524      apr_hash_index_t *hi;
525      int i;
526      int j;
527      apr_pool_t *iterpool;
528      svn_boolean_t is_new_target;
529
530      /* Build a map of wcroots and targets within them. */
531      wcroots = apr_hash_make(pool);
532      iterpool = svn_pool_create(pool);
533      for (i = 0; i < paths->nelts; i++)
534        {
535          const char *wcroot_abspath;
536          apr_array_header_t *targets;
537
538          svn_pool_clear(iterpool);
539
540          /* See if the user wants us to stop. */
541          if (ctx->cancel_func)
542            SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
543
544          SVN_ERR(svn_dirent_get_absolute(&local_abspath,
545                                          APR_ARRAY_IDX(paths, i,
546                                                        const char *),
547                                          pool));
548          SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
549                                     local_abspath, pool, iterpool));
550          targets = svn_hash_gets(wcroots, wcroot_abspath);
551          if (targets == NULL)
552            {
553              targets = apr_array_make(pool, 1, sizeof(const char *));
554              svn_hash_sets(wcroots, wcroot_abspath, targets);
555             }
556
557          /* Make sure targets are unique. */
558          is_new_target = TRUE;
559          for (j = 0; j < targets->nelts; j++)
560            {
561              if (strcmp(APR_ARRAY_IDX(targets, j, const char *),
562                         local_abspath) == 0)
563                {
564                  is_new_target = FALSE;
565                  break;
566                }
567            }
568
569          if (is_new_target)
570            APR_ARRAY_PUSH(targets, const char *) = local_abspath;
571        }
572
573      /* Delete the targets from each working copy in turn. */
574      for (hi = apr_hash_first(pool, wcroots); hi; hi = apr_hash_next(hi))
575        {
576          const char *root_abspath;
577          const apr_array_header_t *targets = svn__apr_hash_index_val(hi);
578
579          svn_pool_clear(iterpool);
580
581          SVN_ERR(svn_dirent_condense_targets(&root_abspath, NULL, targets,
582                                              FALSE, iterpool, iterpool));
583
584          SVN_WC__CALL_WITH_WRITE_LOCK(
585            svn_client__wc_delete_many(targets, force, FALSE, keep_local,
586                                       ctx->notify_func2, ctx->notify_baton2,
587                                       ctx, iterpool),
588            ctx->wc_ctx, root_abspath, TRUE /* lock_anchor */,
589            iterpool);
590        }
591      svn_pool_destroy(iterpool);
592    }
593
594  return SVN_NO_ERROR;
595}
596