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;
50  svn_client_ctx_t *ctx;
51  apr_pool_t *pool;
52};
53
54
55/* This callback is called by the ra_layer for each path locked.
56 * BATON is a 'struct lock_baton *', PATH is the path being locked,
57 * and LOCK is the lock itself.
58 *
59 * If BATON->base_dir_abspath is not null, then this function either
60 * stores the LOCK on REL_URL or removes any lock tokens from REL_URL
61 * (depending on whether DO_LOCK is true or false respectively), but
62 * only if RA_ERR is null, or (in the unlock case) is something other
63 * than SVN_ERR_FS_LOCK_OWNER_MISMATCH.
64 *
65 * Implements svn_ra_lock_callback_t.
66 */
67static svn_error_t *
68store_locks_callback(void *baton,
69                     const char *rel_url,
70                     svn_boolean_t do_lock,
71                     const svn_lock_t *lock,
72                     svn_error_t *ra_err, apr_pool_t *pool)
73{
74  struct lock_baton *lb = baton;
75  svn_wc_notify_t *notify;
76
77  /* Create the notify struct first, so we can tweak it below. */
78  notify = svn_wc_create_notify(rel_url,
79                                do_lock
80                                ? (ra_err
81                                   ? svn_wc_notify_failed_lock
82                                   : svn_wc_notify_locked)
83                                : (ra_err
84                                   ? svn_wc_notify_failed_unlock
85                                   : svn_wc_notify_unlocked),
86                                pool);
87  notify->lock = lock;
88  notify->err = ra_err;
89
90  if (lb->base_dir_abspath)
91    {
92      char *path = svn_hash_gets(lb->urls_to_paths, rel_url);
93      const char *local_abspath;
94
95      local_abspath = svn_dirent_join(lb->base_dir_abspath, path, pool);
96
97      /* Notify a valid working copy path */
98      notify->path = local_abspath;
99      notify->path_prefix = lb->base_dir_abspath;
100
101      if (do_lock)
102        {
103          if (!ra_err)
104            {
105              SVN_ERR(svn_wc_add_lock2(lb->ctx->wc_ctx, local_abspath, lock,
106                                       lb->pool));
107              notify->lock_state = svn_wc_notify_lock_state_locked;
108            }
109          else
110            notify->lock_state = svn_wc_notify_lock_state_unchanged;
111        }
112      else /* unlocking */
113        {
114          /* Remove our wc lock token either a) if we got no error, or b) if
115             we got any error except for owner mismatch.  Note that the only
116             errors that are handed to this callback will be locking-related
117             errors. */
118
119          if (!ra_err ||
120              (ra_err && (ra_err->apr_err != SVN_ERR_FS_LOCK_OWNER_MISMATCH)))
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 = rel_url; /* Notify that path is actually a url  */
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};
199
200/* Set *COMMON_PARENT_URL to the nearest common parent URL of all TARGETS.
201 * If TARGETS are local paths, then the entry for each path is examined
202 * and *COMMON_PARENT is set to the common parent URL for all the
203 * targets (as opposed to the common local path).
204 *
205 * If there is no common parent, either because the targets are a
206 * mixture of URLs and local paths, or because they simply do not
207 * share a common parent, then return SVN_ERR_UNSUPPORTED_FEATURE.
208 *
209 * DO_LOCK is TRUE for locking TARGETS, and FALSE for unlocking them.
210 * FORCE is TRUE for breaking or stealing locks, and FALSE otherwise.
211 *
212 * Each key stored in *REL_TARGETS_P is a path relative to
213 * *COMMON_PARENT.  If TARGETS are local paths, then: if DO_LOCK is
214 * true, the value is a pointer to the corresponding base_revision
215 * (allocated in POOL) for the path, else the value is the lock token
216 * (or "" if no token found in the wc).
217 *
218 * If TARGETS is an array of urls, REL_FS_PATHS_P is set to NULL.
219 * Otherwise each key in REL_FS_PATHS_P is an repository path (relative to
220 * COMMON_PARENT) mapped to the target path for TARGET (relative to
221 * the common parent WC path). working copy targets that they "belong" to.
222 *
223 * If *COMMON_PARENT is a URL, then the values are a pointer to
224 * SVN_INVALID_REVNUM (allocated in pool) if DO_LOCK, else "".
225 *
226 * TARGETS may not be empty.
227 */
228static svn_error_t *
229organize_lock_targets(const char **common_parent_url,
230                      const char **base_dir,
231                      apr_hash_t **rel_targets_p,
232                      apr_hash_t **rel_fs_paths_p,
233                      const apr_array_header_t *targets,
234                      svn_boolean_t do_lock,
235                      svn_boolean_t force,
236                      svn_wc_context_t *wc_ctx,
237                      apr_pool_t *result_pool,
238                      apr_pool_t *scratch_pool)
239{
240  const char *common_url = NULL;
241  const char *common_dirent = NULL;
242  apr_hash_t *rel_targets_ret = apr_hash_make(result_pool);
243  apr_hash_t *rel_fs_paths = NULL;
244  apr_array_header_t *rel_targets;
245  apr_hash_t *wc_info = apr_hash_make(scratch_pool);
246  svn_boolean_t url_mode;
247  int i;
248
249  SVN_ERR_ASSERT(targets->nelts);
250  SVN_ERR(svn_client__assert_homogeneous_target_type(targets));
251
252  url_mode = svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *));
253
254  if (url_mode)
255    {
256      svn_revnum_t *invalid_revnum =
257        apr_palloc(result_pool, sizeof(*invalid_revnum));
258
259      *invalid_revnum = SVN_INVALID_REVNUM;
260
261      /* Get the common parent URL and a bunch of relpaths, one per target. */
262      SVN_ERR(condense_targets(&common_url, &rel_targets, targets,
263                               TRUE, TRUE, result_pool, scratch_pool));
264      if (! (common_url && *common_url))
265        return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
266                                _("No common parent found, unable to operate "
267                                  "on disjoint arguments"));
268
269      /* Create mapping of the target relpaths to either
270         SVN_INVALID_REVNUM (if our caller is locking) or to an empty
271         lock token string (if the caller is unlocking). */
272      for (i = 0; i < rel_targets->nelts; i++)
273        {
274          svn_hash_sets(rel_targets_ret,
275                        APR_ARRAY_IDX(rel_targets, i, const char *),
276                        do_lock
277                        ? (const void *)invalid_revnum
278                        : (const void *)"");
279        }
280    }
281  else
282    {
283      apr_array_header_t *rel_urls, *target_urls;
284      apr_pool_t *iterpool = svn_pool_create(scratch_pool);
285
286      /* Get the common parent dirent and a bunch of relpaths, one per
287         target. */
288      SVN_ERR(condense_targets(&common_dirent, &rel_targets, targets,
289                               FALSE, TRUE, result_pool, scratch_pool));
290      if (! (common_dirent && *common_dirent))
291        return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
292                                _("No common parent found, unable to operate "
293                                  "on disjoint arguments"));
294
295      /* Get the URL for each target (which also serves to verify that
296         the dirent targets are sane).  */
297      target_urls = apr_array_make(scratch_pool, rel_targets->nelts,
298                                   sizeof(const char *));
299      for (i = 0; i < rel_targets->nelts; i++)
300        {
301          const char *rel_target;
302          const char *repos_relpath;
303          const char *repos_root_url;
304          const char *target_url;
305          struct wc_lock_item_t *wli;
306          const char *local_abspath;
307          svn_node_kind_t kind;
308
309          svn_pool_clear(iterpool);
310
311          rel_target = APR_ARRAY_IDX(rel_targets, i, const char *);
312          local_abspath = svn_dirent_join(common_dirent, rel_target, scratch_pool);
313          wli = apr_pcalloc(scratch_pool, sizeof(*wli));
314
315          SVN_ERR(svn_wc__node_get_base(&kind, &wli->revision, &repos_relpath,
316                                        &repos_root_url, NULL,
317                                        &wli->lock_token,
318                                        wc_ctx, local_abspath,
319                                        FALSE /* ignore_enoent */,
320                                        FALSE /* show_hidden */,
321                                        result_pool, iterpool));
322
323          if (kind != svn_node_file)
324            return svn_error_createf(SVN_ERR_WC_NOT_FILE, NULL,
325                                     _("The node '%s' is not a file"),
326                                     svn_dirent_local_style(local_abspath,
327                                                            iterpool));
328
329          svn_hash_sets(wc_info, local_abspath, wli);
330
331          target_url = svn_path_url_add_component2(repos_root_url,
332                                                   repos_relpath,
333                                                   scratch_pool);
334
335          APR_ARRAY_PUSH(target_urls, const char *) = target_url;
336        }
337
338      /* Now that we have a bunch of URLs for our dirent targets,
339         condense those into a single common parent URL and a bunch of
340         paths relative to that. */
341      SVN_ERR(condense_targets(&common_url, &rel_urls, target_urls,
342                               TRUE, FALSE, result_pool, scratch_pool));
343      if (! (common_url && *common_url))
344        return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
345                                _("Unable to lock/unlock across multiple "
346                                  "repositories"));
347
348      /* Now we need to create a couple of different hash mappings. */
349      rel_fs_paths = apr_hash_make(result_pool);
350      for (i = 0; i < rel_targets->nelts; i++)
351        {
352          const char *rel_target, *rel_url;
353          const char *local_abspath;
354
355          svn_pool_clear(iterpool);
356
357          /* First, we need to map our REL_URL (which is relative to
358             COMMON_URL) to our REL_TARGET (which is relative to
359             COMMON_DIRENT). */
360          rel_target = APR_ARRAY_IDX(rel_targets, i, const char *);
361          rel_url = APR_ARRAY_IDX(rel_urls, i, const char *);
362          svn_hash_sets(rel_fs_paths, rel_url,
363                        apr_pstrdup(result_pool, rel_target));
364
365          /* Then, we map our REL_URL (again) to either the base
366             revision of the dirent target with which it is associated
367             (if our caller is locking) or to a (possible empty) lock
368             token string (if the caller is unlocking). */
369          local_abspath = svn_dirent_join(common_dirent, rel_target, iterpool);
370
371          if (do_lock) /* Lock. */
372            {
373              svn_revnum_t *revnum;
374              struct wc_lock_item_t *wli;
375              revnum = apr_palloc(result_pool, sizeof(* revnum));
376
377              wli = svn_hash_gets(wc_info, local_abspath);
378
379              SVN_ERR_ASSERT(wli != NULL);
380
381              *revnum = wli->revision;
382
383              svn_hash_sets(rel_targets_ret, rel_url, revnum);
384            }
385          else /* Unlock. */
386            {
387              const char *lock_token;
388              struct wc_lock_item_t *wli;
389
390              /* If not forcing the unlock, get the lock token. */
391              if (! force)
392                {
393                  wli = svn_hash_gets(wc_info, local_abspath);
394
395                  SVN_ERR_ASSERT(wli != NULL);
396
397                  if (! wli->lock_token)
398                    return svn_error_createf(
399                               SVN_ERR_CLIENT_MISSING_LOCK_TOKEN, NULL,
400                               _("'%s' is not locked in this working copy"),
401                               svn_dirent_local_style(local_abspath,
402                                                      scratch_pool));
403
404                  lock_token = wli->lock_token
405                                ? apr_pstrdup(result_pool, wli->lock_token)
406                                : NULL;
407                }
408              else
409                lock_token = NULL;
410
411              /* If breaking a lock, we shouldn't pass any lock token. */
412              svn_hash_sets(rel_targets_ret, rel_url,
413                            lock_token ? lock_token : "");
414            }
415        }
416
417      svn_pool_destroy(iterpool);
418    }
419
420  /* Set our return variables. */
421  *common_parent_url = common_url;
422  *base_dir = common_dirent;
423  *rel_targets_p = rel_targets_ret;
424  *rel_fs_paths_p = rel_fs_paths;
425
426  return SVN_NO_ERROR;
427}
428
429/* Fetch lock tokens from the repository for the paths in PATH_TOKENS,
430   setting the values to the fetched tokens, allocated in pool. */
431static svn_error_t *
432fetch_tokens(svn_ra_session_t *ra_session, apr_hash_t *path_tokens,
433             apr_pool_t *pool)
434{
435  apr_hash_index_t *hi;
436  apr_pool_t *iterpool = svn_pool_create(pool);
437
438  for (hi = apr_hash_first(pool, path_tokens); hi; hi = apr_hash_next(hi))
439    {
440      const char *path = svn__apr_hash_index_key(hi);
441      svn_lock_t *lock;
442
443      svn_pool_clear(iterpool);
444
445      SVN_ERR(svn_ra_get_lock(ra_session, &lock, path, iterpool));
446
447      if (! lock)
448        return svn_error_createf
449          (SVN_ERR_CLIENT_MISSING_LOCK_TOKEN, NULL,
450           _("'%s' is not locked"), path);
451
452      svn_hash_sets(path_tokens, path, apr_pstrdup(pool, lock->token));
453    }
454
455  svn_pool_destroy(iterpool);
456  return SVN_NO_ERROR;
457}
458
459
460svn_error_t *
461svn_client_lock(const apr_array_header_t *targets,
462                const char *comment,
463                svn_boolean_t steal_lock,
464                svn_client_ctx_t *ctx,
465                apr_pool_t *pool)
466{
467  const char *base_dir;
468  const char *base_dir_abspath = NULL;
469  const char *common_parent_url;
470  svn_ra_session_t *ra_session;
471  apr_hash_t *path_revs, *urls_to_paths;
472  struct lock_baton cb;
473
474  if (apr_is_empty_array(targets))
475    return SVN_NO_ERROR;
476
477  /* Enforce that the comment be xml-escapable. */
478  if (comment)
479    {
480      if (! svn_xml_is_xml_safe(comment, strlen(comment)))
481        return svn_error_create
482          (SVN_ERR_XML_UNESCAPABLE_DATA, NULL,
483           _("Lock comment contains illegal characters"));
484    }
485
486  SVN_ERR(organize_lock_targets(&common_parent_url, &base_dir, &path_revs,
487                                &urls_to_paths, targets, TRUE, steal_lock,
488                                ctx->wc_ctx, pool, pool));
489
490  /* Open an RA session to the common parent of TARGETS. */
491  if (base_dir)
492    SVN_ERR(svn_dirent_get_absolute(&base_dir_abspath, base_dir, pool));
493  SVN_ERR(svn_client_open_ra_session2(&ra_session, common_parent_url,
494                                      base_dir_abspath, ctx, pool, pool));
495
496  cb.base_dir_abspath = base_dir_abspath;
497  cb.urls_to_paths = urls_to_paths;
498  cb.ctx = ctx;
499  cb.pool = pool;
500
501  /* Lock the paths. */
502  SVN_ERR(svn_ra_lock(ra_session, path_revs, comment,
503                      steal_lock, store_locks_callback, &cb, pool));
504
505  return SVN_NO_ERROR;
506}
507
508svn_error_t *
509svn_client_unlock(const apr_array_header_t *targets,
510                  svn_boolean_t break_lock,
511                  svn_client_ctx_t *ctx,
512                  apr_pool_t *pool)
513{
514  const char *base_dir;
515  const char *base_dir_abspath = NULL;
516  const char *common_parent_url;
517  svn_ra_session_t *ra_session;
518  apr_hash_t *path_tokens, *urls_to_paths;
519  struct lock_baton cb;
520
521  if (apr_is_empty_array(targets))
522    return SVN_NO_ERROR;
523
524  SVN_ERR(organize_lock_targets(&common_parent_url, &base_dir, &path_tokens,
525                                &urls_to_paths, targets, FALSE, break_lock,
526                                ctx->wc_ctx, pool, pool));
527
528  /* Open an RA session. */
529  if (base_dir)
530    SVN_ERR(svn_dirent_get_absolute(&base_dir_abspath, base_dir, pool));
531  SVN_ERR(svn_client_open_ra_session2(&ra_session, common_parent_url,
532                                      base_dir_abspath, ctx, pool, pool));
533
534  /* If break_lock is not set, lock tokens are required by the server.
535     If the targets were all URLs, ensure that we provide lock tokens,
536     so the repository will only check that the user owns the
537     locks. */
538  if (! base_dir && !break_lock)
539    SVN_ERR(fetch_tokens(ra_session, path_tokens, pool));
540
541  cb.base_dir_abspath = base_dir_abspath;
542  cb.urls_to_paths = urls_to_paths;
543  cb.ctx = ctx;
544  cb.pool = pool;
545
546  /* Unlock the paths. */
547  SVN_ERR(svn_ra_unlock(ra_session, path_tokens, break_lock,
548                        store_locks_callback, &cb, pool));
549
550  return SVN_NO_ERROR;
551}
552
553