1/*
2 * locking_commands.c:  Implementation of lock and unlock.
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 "svn_client.h"
31#include "svn_hash.h"
32#include "client.h"
33#include "svn_dirent_uri.h"
34#include "svn_path.h"
35#include "svn_xml.h"
36#include "svn_pools.h"
37
38#include "svn_private_config.h"
39#include "private/svn_client_private.h"
40#include "private/svn_wc_private.h"
41
42
43/*** Code. ***/
44
45/* For use with store_locks_callback, below. */
46struct lock_baton
47{
48  const char *base_dir_abspath;
49  apr_hash_t *urls_to_paths; /* url -> abspath */
50  const char *base_url;
51  svn_client_ctx_t *ctx;
52  apr_pool_t *pool;
53};
54
55
56/* This callback is called by the ra_layer for each path locked.
57 * BATON is a 'struct lock_baton *', PATH is the path being locked,
58 * and LOCK is the lock itself.
59 *
60 * If BATON->urls_to_paths is not null, then this function either
61 * stores the LOCK on REL_URL or removes any lock tokens from REL_URL
62 * (depending on whether DO_LOCK is true or false respectively), but
63 * only if RA_ERR is null, or (in the unlock case) is something other
64 * than SVN_ERR_FS_LOCK_OWNER_MISMATCH.
65 *
66 * Implements svn_ra_lock_callback_t.
67 */
68static svn_error_t *
69store_locks_callback(void *baton,
70                     const char *rel_url,
71                     svn_boolean_t do_lock,
72                     const svn_lock_t *lock,
73                     svn_error_t *ra_err, apr_pool_t *pool)
74{
75  struct lock_baton *lb = baton;
76  svn_wc_notify_t *notify;
77  const char *local_abspath = lb->urls_to_paths
78                                  ? svn_hash_gets(lb->urls_to_paths, rel_url)
79                                  : NULL;
80
81  /* Create the notify struct first, so we can tweak it below. */
82  notify = svn_wc_create_notify(local_abspath ? local_abspath : rel_url,
83                                do_lock
84                                ? (ra_err
85                                   ? svn_wc_notify_failed_lock
86                                   : svn_wc_notify_locked)
87                                : (ra_err
88                                   ? svn_wc_notify_failed_unlock
89                                   : svn_wc_notify_unlocked),
90                                pool);
91  notify->lock = lock;
92  notify->err = ra_err;
93
94  if (local_abspath)
95    {
96      /* Notify a valid working copy path */
97      notify->path_prefix = lb->base_dir_abspath;
98
99      if (do_lock)
100        {
101          if (!ra_err && lock)
102            {
103              SVN_ERR(svn_wc_add_lock2(lb->ctx->wc_ctx, local_abspath, lock,
104                                       lb->pool));
105              notify->lock_state = svn_wc_notify_lock_state_locked;
106            }
107          else
108            notify->lock_state = svn_wc_notify_lock_state_unchanged;
109        }
110      else /* unlocking */
111        {
112          /* Remove our wc lock token either a) if we got no error, or b) if
113             we got any error except for owner mismatch or hook failure (the
114             hook would be pre-unlock rather than post-unlock). Note that the
115             only errors that are handed to this callback will be
116             locking-related errors. */
117
118          if (!ra_err ||
119              (ra_err && (ra_err->apr_err != SVN_ERR_FS_LOCK_OWNER_MISMATCH
120                          && ra_err->apr_err != SVN_ERR_REPOS_HOOK_FAILURE)))
121            {
122              SVN_ERR(svn_wc_remove_lock2(lb->ctx->wc_ctx, local_abspath,
123                                          lb->pool));
124              notify->lock_state = svn_wc_notify_lock_state_unlocked;
125            }
126          else
127            notify->lock_state = svn_wc_notify_lock_state_unchanged;
128        }
129    }
130  else
131    notify->url = svn_path_url_add_component2(lb->base_url, rel_url, pool);
132
133  if (lb->ctx->notify_func2)
134    lb->ctx->notify_func2(lb->ctx->notify_baton2, notify, pool);
135
136  return SVN_NO_ERROR;
137}
138
139
140/* This is a wrapper around svn_uri_condense_targets() and
141 * svn_dirent_condense_targets() (the choice of which is made based on
142 * the value of TARGETS_ARE_URIS) which takes care of the
143 * single-target special case.
144 *
145 * Callers are expected to check for an empty *COMMON_PARENT (which
146 * means, "there was nothing common") for themselves.
147 */
148static svn_error_t *
149condense_targets(const char **common_parent,
150                 apr_array_header_t **target_relpaths,
151                 const apr_array_header_t *targets,
152                 svn_boolean_t targets_are_uris,
153                 svn_boolean_t remove_redundancies,
154                 apr_pool_t *result_pool,
155                 apr_pool_t *scratch_pool)
156{
157  if (targets_are_uris)
158    {
159      SVN_ERR(svn_uri_condense_targets(common_parent, target_relpaths,
160                                       targets, remove_redundancies,
161                                       result_pool, scratch_pool));
162    }
163  else
164    {
165      SVN_ERR(svn_dirent_condense_targets(common_parent, target_relpaths,
166                                          targets, remove_redundancies,
167                                          result_pool, scratch_pool));
168    }
169
170  /* svn_*_condense_targets leaves *TARGET_RELPATHS empty if TARGETS only
171     had 1 member, so we special case that. */
172  if (apr_is_empty_array(*target_relpaths))
173    {
174      const char *base_name;
175
176      if (targets_are_uris)
177        {
178          svn_uri_split(common_parent, &base_name,
179                        *common_parent, result_pool);
180        }
181      else
182        {
183          svn_dirent_split(common_parent, &base_name,
184                           *common_parent, result_pool);
185        }
186      APR_ARRAY_PUSH(*target_relpaths, const char *) = base_name;
187    }
188
189  return SVN_NO_ERROR;
190}
191
192/* Lock info. Used in organize_lock_targets.
193   ### Maybe return this instead of the ugly hashes? */
194struct wc_lock_item_t
195{
196  svn_revnum_t revision;
197  const char *lock_token;
198  const char *url;
199};
200
201/*
202 * Sets LOCK_PATHS to an array of working copy paths that this function
203 * has obtained lock on. The caller is responsible to release the locks
204 * EVEN WHEN THIS FUNCTION RETURNS AN ERROR.
205 *
206 * Set *COMMON_PARENT_URL to the nearest common parent URL of all TARGETS.
207 * If TARGETS are local paths, then the entry for each path is examined
208 * and *COMMON_PARENT is set to the common parent URL for all the
209 * targets (as opposed to the common local path).
210 *
211 * If there is no common parent, either because the targets are a
212 * mixture of URLs and local paths, or because they simply do not
213 * share a common parent, then return SVN_ERR_UNSUPPORTED_FEATURE.
214 *
215 * DO_LOCK is TRUE for locking TARGETS, and FALSE for unlocking them.
216 * FORCE is TRUE for breaking or stealing locks, and FALSE otherwise.
217 *
218 * Each key stored in *REL_TARGETS_P is a path relative to
219 * *COMMON_PARENT.  If TARGETS are local paths, then: if DO_LOCK is
220 * true, the value is a pointer to the corresponding base_revision
221 * (allocated in POOL) for the path, else the value is the lock token
222 * (or "" if no token found in the wc).
223 *
224 * If TARGETS is an array of urls, REL_FS_PATHS_P is set to NULL.
225 * Otherwise each key in REL_FS_PATHS_P is an repository path (relative to
226 * COMMON_PARENT) mapped to the absolute path for TARGET.
227 *
228 * If *COMMON_PARENT is a URL, then the values are a pointer to
229 * SVN_INVALID_REVNUM (allocated in pool) if DO_LOCK, else "".
230 *
231 * TARGETS may not be empty.
232 */
233static svn_error_t *
234organize_lock_targets(apr_array_header_t **lock_paths,
235                      const char **common_parent_url,
236                      const char **base_dir_abspath,
237                      apr_hash_t **rel_targets_p,
238                      apr_hash_t **rel_fs_paths_p,
239                      const apr_array_header_t *targets,
240                      svn_boolean_t do_lock,
241                      svn_boolean_t force,
242                      svn_wc_context_t *wc_ctx,
243                      apr_pool_t *result_pool,
244                      apr_pool_t *scratch_pool)
245{
246  const char *common_url = NULL;
247  apr_hash_t *rel_targets_ret = apr_hash_make(result_pool);
248  apr_hash_t *rel_fs_paths = NULL;
249  apr_hash_t *wc_info = apr_hash_make(scratch_pool);
250  svn_boolean_t url_mode;
251
252  *lock_paths = NULL;
253
254  SVN_ERR_ASSERT(targets->nelts);
255  SVN_ERR(svn_client__assert_homogeneous_target_type(targets));
256
257  url_mode = svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *));
258
259  if (url_mode)
260    {
261      apr_array_header_t *rel_targets;
262      int i;
263      svn_revnum_t *invalid_revnum =
264        apr_palloc(result_pool, sizeof(*invalid_revnum));
265
266      *invalid_revnum = SVN_INVALID_REVNUM;
267
268      /* Get the common parent URL and a bunch of relpaths, one per target. */
269      SVN_ERR(condense_targets(&common_url, &rel_targets, targets,
270                               TRUE, TRUE, result_pool, scratch_pool));
271      if (! (common_url && *common_url))
272        return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
273                                _("No common parent found, unable to operate "
274                                  "on disjoint arguments"));
275
276      /* Create mapping of the target relpaths to either
277         SVN_INVALID_REVNUM (if our caller is locking) or to an empty
278         lock token string (if the caller is unlocking). */
279      for (i = 0; i < rel_targets->nelts; i++)
280        {
281          svn_hash_sets(rel_targets_ret,
282                        APR_ARRAY_IDX(rel_targets, i, const char *),
283                        do_lock
284                        ? (const void *)invalid_revnum
285                        : (const void *)"");
286        }
287    }
288  else
289    {
290      apr_array_header_t *rel_urls, *target_urls;
291      apr_hash_t *wcroot_target = apr_hash_make(scratch_pool);
292      apr_pool_t *iterpool = svn_pool_create(scratch_pool);
293      apr_hash_index_t *hi;
294      int i;
295
296      *lock_paths = apr_array_make(result_pool, 1, sizeof(const char *));
297
298      for (i = 0; i < targets->nelts; i++)
299        {
300          const char *target_abspath;
301          const char *wcroot_abspath;
302          apr_array_header_t *wc_targets;
303
304          svn_pool_clear(iterpool);
305
306          SVN_ERR(svn_dirent_get_absolute(&target_abspath,
307                                          APR_ARRAY_IDX(targets, i, const char*),
308                                          result_pool));
309
310          SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, wc_ctx, target_abspath,
311                                     iterpool, iterpool));
312
313          wc_targets = svn_hash_gets(wcroot_target, wcroot_abspath);
314
315          if (!wc_targets)
316            {
317              wc_targets = apr_array_make(scratch_pool, 1, sizeof(const char*));
318              svn_hash_sets(wcroot_target, apr_pstrdup(scratch_pool, wcroot_abspath),
319                            wc_targets);
320            }
321
322          APR_ARRAY_PUSH(wc_targets, const char *) = target_abspath;
323        }
324
325      for (hi = apr_hash_first(scratch_pool, wcroot_target);
326           hi;
327           hi = apr_hash_next(hi))
328        {
329          const char *lock_abspath;
330          apr_array_header_t *paths = apr_hash_this_val(hi);
331
332          /* Use parent dir of a single file target */
333          if (paths->nelts == 1)
334            lock_abspath = svn_dirent_dirname(
335                                APR_ARRAY_IDX(paths, 0, const char *),
336                                result_pool);
337          else
338            SVN_ERR(svn_dirent_condense_targets(&lock_abspath, NULL, paths,
339                                                FALSE, result_pool,
340                                                scratch_pool));
341
342          SVN_ERR(svn_wc__acquire_write_lock(&lock_abspath,
343                                             wc_ctx, lock_abspath, FALSE,
344                                             result_pool, scratch_pool));
345
346          APR_ARRAY_PUSH(*lock_paths, const char *) = lock_abspath;
347        }
348
349      /* Get the URL for each target (which also serves to verify that
350         the dirent targets are sane).  */
351      target_urls = apr_array_make(scratch_pool, targets->nelts,
352                                   sizeof(const char *));
353      for (hi = apr_hash_first(scratch_pool, wcroot_target);
354           hi;
355           hi = apr_hash_next(hi))
356        {
357          apr_array_header_t *wc_targets = apr_hash_this_val(hi);
358
359          for (i = 0; i < wc_targets->nelts; i++)
360            {
361              const char *repos_relpath;
362              const char *repos_root_url;
363              struct wc_lock_item_t *wli;
364              const char *local_abspath;
365              svn_node_kind_t kind;
366
367              svn_pool_clear(iterpool);
368
369              local_abspath = APR_ARRAY_IDX(wc_targets, i, const char *);
370              wli = apr_pcalloc(scratch_pool, sizeof(*wli));
371
372              SVN_ERR(svn_wc__node_get_base(&kind, &wli->revision,
373                                            &repos_relpath,
374                                            &repos_root_url, NULL,
375                                            &wli->lock_token,
376                                            wc_ctx, local_abspath,
377                                            FALSE /* ignore_enoent */,
378                                            result_pool, iterpool));
379
380              if (kind != svn_node_file)
381                return svn_error_createf(SVN_ERR_WC_NOT_FILE, NULL,
382                                         _("The node '%s' is not a file"),
383                                         svn_dirent_local_style(local_abspath,
384                                                                iterpool));
385
386              wli->url = svn_path_url_add_component2(repos_root_url,
387                                                     repos_relpath,
388                                                     scratch_pool);
389              svn_hash_sets(wc_info, local_abspath, wli);
390
391              APR_ARRAY_PUSH(target_urls, const char *) = wli->url;
392            }
393        }
394
395      /* Now that we have a bunch of URLs for our dirent targets,
396         condense those into a single common parent URL and a bunch of
397         paths relative to that. */
398      SVN_ERR(condense_targets(&common_url, &rel_urls, target_urls,
399                               TRUE, FALSE, result_pool, scratch_pool));
400      if (! (common_url && *common_url))
401        return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
402                                _("Unable to lock/unlock across multiple "
403                                  "repositories"));
404
405      /* Now we need to create our mapping. */
406      rel_fs_paths = apr_hash_make(result_pool);
407
408      for (hi = apr_hash_first(scratch_pool, wc_info);
409           hi;
410           hi = apr_hash_next(hi))
411        {
412          const char *local_abspath = apr_hash_this_key(hi);
413          struct wc_lock_item_t *wli = apr_hash_this_val(hi);
414          const char *rel_url;
415
416          svn_pool_clear(iterpool);
417
418          rel_url = svn_uri_skip_ancestor(common_url, wli->url, result_pool);
419
420          svn_hash_sets(rel_fs_paths, rel_url,
421                        apr_pstrdup(result_pool, local_abspath));
422
423          if (do_lock) /* Lock. */
424            {
425              svn_revnum_t *revnum;
426              revnum = apr_palloc(result_pool, sizeof(* revnum));
427
428              *revnum = wli->revision;
429
430              svn_hash_sets(rel_targets_ret, rel_url, revnum);
431            }
432          else /* Unlock. */
433            {
434              const char *lock_token;
435
436              /* If not forcing the unlock, get the lock token. */
437              if (! force)
438                {
439                  if (! wli->lock_token)
440                    return svn_error_createf(
441                               SVN_ERR_CLIENT_MISSING_LOCK_TOKEN, NULL,
442                               _("'%s' is not locked in this working copy"),
443                               svn_dirent_local_style(local_abspath,
444                                                      scratch_pool));
445
446                  lock_token = wli->lock_token
447                                ? apr_pstrdup(result_pool, wli->lock_token)
448                                : NULL;
449                }
450              else
451                lock_token = NULL;
452
453              /* If breaking a lock, we shouldn't pass any lock token. */
454              svn_hash_sets(rel_targets_ret, rel_url,
455                            lock_token ? lock_token : "");
456            }
457        }
458
459      svn_pool_destroy(iterpool);
460    }
461
462  /* Set our return variables. */
463  *common_parent_url = common_url;
464  if (*lock_paths && (*lock_paths)->nelts == 1)
465    *base_dir_abspath = APR_ARRAY_IDX(*lock_paths, 0, const char*);
466  else
467    *base_dir_abspath = NULL;
468  *rel_targets_p = rel_targets_ret;
469  *rel_fs_paths_p = rel_fs_paths;
470
471  return SVN_NO_ERROR;
472}
473
474/* Fetch lock tokens from the repository for the paths in PATH_TOKENS,
475   setting the values to the fetched tokens, allocated in pool. */
476static svn_error_t *
477fetch_tokens(svn_ra_session_t *ra_session, apr_hash_t *path_tokens,
478             apr_pool_t *pool)
479{
480  apr_hash_index_t *hi;
481  apr_pool_t *iterpool = svn_pool_create(pool);
482
483  for (hi = apr_hash_first(pool, path_tokens); hi; hi = apr_hash_next(hi))
484    {
485      const char *path = apr_hash_this_key(hi);
486      svn_lock_t *lock;
487
488      svn_pool_clear(iterpool);
489
490      SVN_ERR(svn_ra_get_lock(ra_session, &lock, path, iterpool));
491
492      if (! lock)
493        return svn_error_createf
494          (SVN_ERR_CLIENT_MISSING_LOCK_TOKEN, NULL,
495           _("'%s' is not locked"), path);
496
497      svn_hash_sets(path_tokens, path, apr_pstrdup(pool, lock->token));
498    }
499
500  svn_pool_destroy(iterpool);
501  return SVN_NO_ERROR;
502}
503
504
505svn_error_t *
506svn_client_lock(const apr_array_header_t *targets,
507                const char *comment,
508                svn_boolean_t steal_lock,
509                svn_client_ctx_t *ctx,
510                apr_pool_t *pool)
511{
512  const char *base_dir_abspath = NULL;
513  const char *common_parent_url;
514  svn_ra_session_t *ra_session;
515  apr_hash_t *path_revs, *urls_to_paths;
516  struct lock_baton cb;
517  apr_array_header_t *lock_abspaths;
518  svn_error_t *err;
519
520  if (apr_is_empty_array(targets))
521    return SVN_NO_ERROR;
522
523  /* Enforce that the comment be xml-escapable. */
524  if (comment)
525    {
526      if (! svn_xml_is_xml_safe(comment, strlen(comment)))
527        return svn_error_create
528          (SVN_ERR_XML_UNESCAPABLE_DATA, NULL,
529           _("Lock comment contains illegal characters"));
530    }
531
532  err = organize_lock_targets(&lock_abspaths, &common_parent_url,
533                              &base_dir_abspath, &path_revs,
534                              &urls_to_paths,
535                              targets, TRUE, steal_lock,
536                              ctx->wc_ctx, pool, pool);
537
538  if (err)
539    goto release_locks;
540
541  /* Open an RA session to the common parent URL of TARGETS. */
542  err = svn_client_open_ra_session2(&ra_session, common_parent_url,
543                                    base_dir_abspath, ctx, pool, pool);
544
545  if (err)
546    goto release_locks;
547
548  cb.base_dir_abspath = base_dir_abspath;
549  cb.base_url = common_parent_url;
550  cb.urls_to_paths = urls_to_paths;
551  cb.ctx = ctx;
552  cb.pool = pool;
553
554  /* Lock the paths. */
555  err = svn_ra_lock(ra_session, path_revs, comment,
556                    steal_lock, store_locks_callback, &cb, pool);
557
558release_locks:
559  if (lock_abspaths)
560    {
561      int i;
562
563      for (i = 0; i < lock_abspaths->nelts; i++)
564        {
565          err = svn_error_compose_create(
566                  err,
567                  svn_wc__release_write_lock(ctx->wc_ctx,
568                                             APR_ARRAY_IDX(lock_abspaths, i,
569                                                           const char *),
570                                             pool));
571        }
572    }
573
574  return svn_error_trace(err);
575}
576
577svn_error_t *
578svn_client_unlock(const apr_array_header_t *targets,
579                  svn_boolean_t break_lock,
580                  svn_client_ctx_t *ctx,
581                  apr_pool_t *pool)
582{
583  const char *base_dir_abspath = NULL;
584  const char *common_parent_url;
585  svn_ra_session_t *ra_session;
586  apr_hash_t *path_tokens, *urls_to_paths;
587  apr_array_header_t *lock_abspaths;
588  struct lock_baton cb;
589  svn_error_t *err;
590
591  if (apr_is_empty_array(targets))
592    return SVN_NO_ERROR;
593
594  err = organize_lock_targets(&lock_abspaths, &common_parent_url,
595                              &base_dir_abspath, &path_tokens, &urls_to_paths,
596                              targets, FALSE, break_lock,
597                              ctx->wc_ctx, pool, pool);
598
599  if (err)
600    goto release_locks;
601
602  /* Open an RA session to the common parent URL of TARGETS. */
603  err = svn_client_open_ra_session2(&ra_session, common_parent_url,
604                                    base_dir_abspath, ctx, pool, pool);
605
606  if (err)
607    goto release_locks;
608
609  /* If break_lock is not set, lock tokens are required by the server.
610     If the targets were all URLs, ensure that we provide lock tokens,
611     so the repository will only check that the user owns the
612     locks. */
613  if (! lock_abspaths && !break_lock)
614    {
615      err = fetch_tokens(ra_session, path_tokens, pool);
616
617      if (err)
618        goto release_locks;
619    }
620
621  cb.base_dir_abspath = base_dir_abspath;
622  cb.base_url = common_parent_url;
623  cb.urls_to_paths = urls_to_paths;
624  cb.ctx = ctx;
625  cb.pool = pool;
626
627  /* Unlock the paths. */
628  err = svn_ra_unlock(ra_session, path_tokens, break_lock,
629                      store_locks_callback, &cb, pool);
630
631release_locks:
632  if (lock_abspaths)
633    {
634      int i;
635
636      for (i = 0; i < lock_abspaths->nelts; i++)
637        {
638          err = svn_error_compose_create(
639                  err,
640                  svn_wc__release_write_lock(ctx->wc_ctx,
641                                             APR_ARRAY_IDX(lock_abspaths, i,
642                                                           const char *),
643                                             pool));
644        }
645    }
646
647  return svn_error_trace(err);
648}
649
650