1/*
2 * path_driver.c -- drive an editor across a set of paths
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#include <apr_pools.h>
26#include <apr_strings.h>
27
28#include "svn_types.h"
29#include "svn_delta.h"
30#include "svn_pools.h"
31#include "svn_dirent_uri.h"
32#include "svn_path.h"
33#include "svn_sorts.h"
34#include "private/svn_sorts_private.h"
35
36
37/*** Helper functions. ***/
38
39typedef struct dir_stack_t
40{
41  void *dir_baton;   /* the dir baton. */
42  apr_pool_t *pool;  /* the pool associated with the dir baton. */
43
44} dir_stack_t;
45
46
47/* Push onto dir_stack a new item allocated in POOL and containing
48 * DIR_BATON and POOL.
49 */
50static void
51push_dir_stack_item(apr_array_header_t *db_stack,
52                    void *dir_baton,
53                    apr_pool_t *pool)
54{
55  dir_stack_t *item = apr_pcalloc(pool, sizeof(*item));
56
57  item->dir_baton = dir_baton;
58  item->pool = pool;
59  APR_ARRAY_PUSH(db_stack, dir_stack_t *) = item;
60}
61
62
63/* Call EDITOR's open_directory() function with the PATH argument, then
64 * add the resulting dir baton to the dir baton stack.
65 */
66static svn_error_t *
67open_dir(apr_array_header_t *db_stack,
68         const svn_delta_editor_t *editor,
69         const char *path,
70         apr_pool_t *pool)
71{
72  void *parent_db, *db;
73  dir_stack_t *item;
74  apr_pool_t *subpool;
75
76  /* Assert that we are in a stable state. */
77  SVN_ERR_ASSERT(db_stack && db_stack->nelts);
78
79  /* Get the parent dir baton. */
80  item = APR_ARRAY_IDX(db_stack, db_stack->nelts - 1, void *);
81  parent_db = item->dir_baton;
82
83  /* Call the EDITOR's open_directory function to get a new directory
84     baton. */
85  subpool = svn_pool_create(pool);
86  SVN_ERR(editor->open_directory(path, parent_db, SVN_INVALID_REVNUM, subpool,
87                                 &db));
88
89  /* Now add the dir baton to the stack. */
90  push_dir_stack_item(db_stack, db, subpool);
91
92  return SVN_NO_ERROR;
93}
94
95
96/* Pop a directory from the dir baton stack and update the stack
97 * pointer.
98 *
99 * This function calls the EDITOR's close_directory() function.
100 */
101static svn_error_t *
102pop_stack(apr_array_header_t *db_stack,
103          const svn_delta_editor_t *editor)
104{
105  dir_stack_t *item;
106
107  /* Assert that we are in a stable state. */
108  SVN_ERR_ASSERT(db_stack && db_stack->nelts);
109
110  /* Close the most recent directory pushed to the stack. */
111  item = APR_ARRAY_IDX(db_stack, db_stack->nelts - 1, dir_stack_t *);
112  (void) apr_array_pop(db_stack);
113  SVN_ERR(editor->close_directory(item->dir_baton, item->pool));
114  svn_pool_destroy(item->pool);
115
116  return SVN_NO_ERROR;
117}
118
119
120/* Count the number of path components in PATH. */
121static int
122count_components(const char *path)
123{
124  int count = 1;
125  const char *instance = path;
126
127  if ((strlen(path) == 1) && (path[0] == '/'))
128    return 0;
129
130  do
131    {
132      instance++;
133      instance = strchr(instance, '/');
134      if (instance)
135        count++;
136    }
137  while (instance);
138
139  return count;
140}
141
142
143
144/*** Public interfaces ***/
145svn_error_t *
146svn_delta_path_driver3(const svn_delta_editor_t *editor,
147                       void *edit_baton,
148                       const apr_array_header_t *relpaths,
149                       svn_boolean_t sort_paths,
150                       svn_delta_path_driver_cb_func2_t callback_func,
151                       void *callback_baton,
152                       apr_pool_t *pool)
153{
154  svn_delta_path_driver_state_t *state;
155  int i;
156  apr_pool_t *subpool, *iterpool;
157
158  /* Do nothing if there are no paths. */
159  if (! relpaths->nelts)
160    return SVN_NO_ERROR;
161
162  subpool = svn_pool_create(pool);
163  iterpool = svn_pool_create(pool);
164
165  /* sort paths if necessary */
166  if (sort_paths && relpaths->nelts > 1)
167    {
168      apr_array_header_t *sorted = apr_array_copy(subpool, relpaths);
169      svn_sort__array(sorted, svn_sort_compare_paths);
170      relpaths = sorted;
171    }
172
173  SVN_ERR(svn_delta_path_driver_start(&state,
174                                      editor, edit_baton,
175                                      callback_func, callback_baton,
176                                      pool));
177
178  /* Now, loop over the commit items, traversing the URL tree and
179     driving the editor. */
180  for (i = 0; i < relpaths->nelts; i++)
181    {
182      const char *relpath;
183
184      /* Clear the iteration pool. */
185      svn_pool_clear(iterpool);
186
187      /* Get the next path. */
188      relpath = APR_ARRAY_IDX(relpaths, i, const char *);
189
190      SVN_ERR(svn_delta_path_driver_step(state, relpath, iterpool));
191    }
192
193  /* Destroy the iteration subpool. */
194  svn_pool_destroy(iterpool);
195
196  SVN_ERR(svn_delta_path_driver_finish(state, pool));
197
198  return SVN_NO_ERROR;
199}
200
201struct svn_delta_path_driver_state_t
202{
203  const svn_delta_editor_t *editor;
204  void *edit_baton;
205  svn_delta_path_driver_cb_func2_t callback_func;
206  void *callback_baton;
207  apr_array_header_t *db_stack;
208  const char *last_path;
209  apr_pool_t *pool;  /* at least the lifetime of the entire drive */
210};
211
212svn_error_t *
213svn_delta_path_driver_start(svn_delta_path_driver_state_t **state_p,
214                            const svn_delta_editor_t *editor,
215                            void *edit_baton,
216                            svn_delta_path_driver_cb_func2_t callback_func,
217                            void *callback_baton,
218                            apr_pool_t *pool)
219{
220  svn_delta_path_driver_state_t *state = apr_pcalloc(pool, sizeof(*state));
221
222  state->editor = editor;
223  state->edit_baton = edit_baton;
224  state->callback_func = callback_func;
225  state->callback_baton = callback_baton;
226  state->db_stack = apr_array_make(pool, 4, sizeof(void *));
227  state->last_path = NULL;
228  state->pool = pool;
229
230  *state_p = state;
231  return SVN_NO_ERROR;
232}
233
234svn_error_t *
235svn_delta_path_driver_step(svn_delta_path_driver_state_t *state,
236                           const char *relpath,
237                           apr_pool_t *scratch_pool)
238{
239  const char *pdir;
240  const char *common = "";
241  size_t common_len;
242  apr_pool_t *subpool;
243  dir_stack_t *item;
244  void *parent_db, *db;
245
246  SVN_ERR_ASSERT(svn_relpath_is_canonical(relpath));
247
248  /* If the first target path is not the root of the edit, we must first
249     call open_root() ourselves. (If the first target path is the root of
250     the edit, then we expect the user's callback to do so.) */
251  if (!state->last_path && !svn_path_is_empty(relpath))
252    {
253      subpool = svn_pool_create(state->pool);
254      SVN_ERR(state->editor->open_root(state->edit_baton, SVN_INVALID_REVNUM,
255                                       subpool, &db));
256      push_dir_stack_item(state->db_stack, db, subpool);
257    }
258
259  /*** Step A - Find the common ancestor of the last path and the
260       current one.  For the first iteration, this is just the
261       empty string. ***/
262  if (state->last_path)
263    common = svn_relpath_get_longest_ancestor(state->last_path, relpath,
264                                              scratch_pool);
265  common_len = strlen(common);
266
267  /*** Step B - Close any directories between the last path and
268       the new common ancestor, if any need to be closed.
269       Sometimes there is nothing to do here (like, for the first
270       iteration, or when the last path was an ancestor of the
271       current one). ***/
272  if ((state->last_path) && (strlen(state->last_path) > common_len))
273    {
274      const char *rel = state->last_path + (common_len ? (common_len + 1) : 0);
275      int count = count_components(rel);
276      while (count--)
277        {
278          SVN_ERR(pop_stack(state->db_stack, state->editor));
279        }
280    }
281
282  /*** Step C - Open any directories between the common ancestor
283       and the parent of the current path. ***/
284  pdir = svn_relpath_dirname(relpath, scratch_pool);
285
286  if (strlen(pdir) > common_len)
287    {
288      const char *piece = pdir + common_len + 1;
289
290      while (1)
291        {
292          const char *rel = pdir;
293
294          /* Find the first separator. */
295          piece = strchr(piece, '/');
296
297          /* Calculate REL as the portion of PDIR up to (but not
298             including) the location to which PIECE is pointing. */
299          if (piece)
300            rel = apr_pstrmemdup(scratch_pool, pdir, piece - pdir);
301
302          /* Open the subdirectory. */
303          SVN_ERR(open_dir(state->db_stack, state->editor, rel, state->pool));
304
305          /* If we found a '/', advance our PIECE pointer to
306             character just after that '/'.  Otherwise, we're
307             done.  */
308          if (piece)
309            piece++;
310          else
311            break;
312        }
313    }
314
315  /*** Step D - Tell our caller to handle the current path. ***/
316  if (state->db_stack->nelts)
317    {
318      item = APR_ARRAY_IDX(state->db_stack, state->db_stack->nelts - 1, void *);
319      parent_db = item->dir_baton;
320    }
321  else
322    parent_db = NULL;
323  db = NULL;  /* predictable behaviour for callbacks that don't set it */
324  subpool = svn_pool_create(state->pool);
325  SVN_ERR(state->callback_func(&db,
326                               state->editor, state->edit_baton, parent_db,
327                               state->callback_baton,
328                               relpath, subpool));
329  if (db)
330    {
331      push_dir_stack_item(state->db_stack, db, subpool);
332    }
333  else
334    {
335      svn_pool_destroy(subpool);
336    }
337
338  /*** Step E - Save our state for the next iteration.  If our
339       caller opened or added PATH as a directory, that becomes
340       our LAST_PATH.  Otherwise, we use PATH's parent
341       directory. ***/
342  state->last_path = apr_pstrdup(state->pool, db ? relpath : pdir);
343
344  return SVN_NO_ERROR;
345}
346
347svn_error_t *
348svn_delta_path_driver_finish(svn_delta_path_driver_state_t *state,
349                             apr_pool_t *scratch_pool)
350{
351  /* Close down any remaining open directory batons. */
352  while (state->db_stack->nelts)
353    {
354      SVN_ERR(pop_stack(state->db_stack, state->editor));
355    }
356
357  return SVN_NO_ERROR;
358}
359