1#    Licensed to the Apache Software Foundation (ASF) under one
2#    or more contributor license agreements.  See the NOTICE file
3#    distributed with this work for additional information
4#    regarding copyright ownership.  The ASF licenses this file
5#    to you under the Apache License, Version 2.0 (the
6#    "License"); you may not use this file except in compliance
7#    with the License.  You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11#    Unless required by applicable law or agreed to in writing,
12#    software distributed under the License is distributed on an
13#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14#    KIND, either express or implied.  See the License for the
15#    specific language governing permissions and limitations
16#    under the License.
17
18
19import csvn.core as svn
20from csvn.core import *
21import os
22
23class Txn(object):
24    def __init__(self, session):
25        self.pool = Pool()
26        self.iterpool = Pool()
27        self.session = session
28        self.root = _txn_operation(None, "OPEN", svn_node_dir)
29        self.commit_callback = None
30        self.ignore_func = None
31        self.autoprop_func = None
32
33    def ignore(self, ignore_func):
34        """Setup a callback function which decides whether a
35           new directory or path should be added to the repository.
36
37           IGNORE_FUNC must be a function which accepts two arguments:
38           (path, kind)
39
40           PATH is the path which is about to be added to the repository.
41           KIND is either svn_node_file or svn_node_dir, depending
42           on whether the proposed path is a file or a directory.
43
44           If IGNORE_FUNC returns True, the path will be ignored. Otherwise,
45           the path will be added.
46
47           Note that IGNORE_FUNC is only called when new files or
48           directories are added to the repository. It is not called,
49           for example, when directories within the repository are moved
50           or copied, since these copies are not new."""
51
52        self.ignore_func = ignore_func
53
54    def autoprop(self, autoprop_func):
55        """Setup a callback function which automatically sets up
56           properties on new files or directories added to the
57           repository.
58
59           AUTOPROP_FUNC must be a function which accepts three
60           arguments: (txn, path, kind)
61
62           TXN is this transaction object.
63           PATH is the path which was just added to the repository.
64           KIND is either svn_node_file or svn_node_dir, depending
65           on whether the newly added path is a file or a directory.
66
67           If AUTOPROP_FUNC wants to set properties on PATH, it should
68           call TXN.propset with the appropriate arguments.
69
70           Note that AUTOPROP_FUNC is only called when new files or
71           directories are added to the repository. It is not called,
72           for example, when directories within the repository are moved
73           or copied, since these copies are not new."""
74
75        self.autoprop_func = autoprop_func
76
77    def check_path(self, path, rev=None):
78        """Check the status of PATH@REV. If PATH or any of its
79           parents have been modified in this transaction, take this
80           into consideration."""
81        path = self.session._relative_path(path)
82        return self._check_path(path, rev)[0]
83
84    def delete(self, path, base_rev=None):
85        """Delete PATH from the repository as of base_rev"""
86
87        path = self.session._relative_path(path)
88
89        kind, parent = self._check_path(path, base_rev)
90
91        if kind == svn_node_none:
92            if base_rev:
93                message = "'%s' not found in r%d" % (path, base_rev)
94            else:
95                message = "'%s' not found" % (path)
96            raise SubversionException(SVN_ERR_BAD_URL, message)
97
98        parent.open(path, "DELETE", kind)
99
100    def mkdir(self, path):
101        """Create a directory at PATH."""
102
103        path = self.session._relative_path(path)
104
105        if self.ignore_func and self.ignore_func(path, svn_node_dir):
106            return
107
108        kind, parent = self._check_path(path)
109
110        if kind != svn_node_none:
111            if kind == svn_node_dir:
112                message = ("Can't create directory '%s': "
113                           "Directory already exists" % path)
114            else:
115                message = ("Can't create directory '%s': "
116                           "Path obstructed by file" % path)
117            raise SubversionException(SVN_ERR_BAD_URL, message)
118
119        parent.open(path, "ADD", svn_node_dir)
120
121        # Trigger autoprop_func on new directory adds
122        if self.autoprop_func:
123            self.autoprop_func(self, path, svn_node_dir)
124
125    def propset(self, path, key, value):
126        """Set the property named KEY to VALUE on the specified PATH"""
127
128        path = self.session._relative_path(path)
129
130        kind, parent = self._check_path(path)
131
132        if kind == svn_node_none:
133            message = ("Can't set property on '%s': "
134                       "No such file or directory" % path)
135            raise SubversionException(SVN_ERR_BAD_URL, message)
136
137        node = parent.open(path, "OPEN", kind)
138        node.propset(key, value)
139
140    def propdel(self, path, key):
141        """Delete the property named KEY on the specified PATH"""
142
143        path = self.session._relative_path(path)
144
145        kind, parent = self._check_path(path)
146
147        if kind == svn_node_none:
148            message = ("Can't delete property on '%s': "
149                       "No such file or directory" % path)
150            raise SubversionException(SVN_ERR_BAD_URL, message)
151
152        node = parent.open(path, "OPEN", kind)
153        node.propdel(key)
154
155
156    def copy(self, src_path, dest_path, src_rev=None, local_path=None):
157        """Copy a file or directory from SRC_PATH@SRC_REV to DEST_PATH.
158           If SRC_REV is not supplied, use the latest revision of SRC_PATH.
159           If LOCAL_PATH is supplied, update the new copy to match
160           LOCAL_PATH."""
161
162        src_path = self.session._relative_path(src_path)
163        dest_path = self.session._relative_path(dest_path)
164
165        if not src_rev:
166            src_rev = self.session.latest_revnum()
167
168        kind = self.session.check_path(src_path, src_rev, encoded=False)
169        _, parent = self._check_path(dest_path)
170
171        if kind == svn_node_none:
172            message = ("Can't copy '%s': "
173                       "No such file or directory" % src_path)
174            raise SubversionException(SVN_ERR_BAD_URL, message)
175
176        if kind == svn_node_file or local_path is None:
177            # Mark the file or directory as copied
178            parent.open(dest_path, "ADD",
179                        kind, copyfrom_path=src_path,
180                        copyfrom_rev=src_rev,
181                        local_path=local_path)
182        else:
183            # Mark the directory as copied
184            parent.open(dest_path, "ADD",
185                        kind, copyfrom_path=src_path,
186                        copyfrom_rev=src_rev)
187
188            # Upload any changes from the supplied local path
189            # to the remote repository
190            self.upload(dest_path, local_path)
191
192    def upload(self, remote_path, local_path):
193        """Upload a local file or directory into the remote repository.
194           If the given file or directory already exists in the
195           repository, overwrite it.
196
197           This function does not add or update ignored files or
198           directories."""
199
200        remote_path = self.session._relative_path(remote_path)
201
202        kind = svn_node_none
203        if os.path.isdir(local_path):
204            kind = svn_node_dir
205        elif os.path.exists(local_path):
206            kind = svn_node_file
207
208        # Don't add ignored files or directories
209        if self.ignore_func and self.ignore_func(remote_path, kind):
210            return
211
212        if (os.path.isdir(local_path) and
213              self.check_path(remote_path) != svn_node_dir):
214            self.mkdir(remote_path)
215        elif not os.path.isdir(local_path) and os.path.exists(local_path):
216            self._upload_file(remote_path, local_path)
217
218        ignores = []
219
220        for root, dirs, files in os.walk(local_path):
221
222            # Convert the local root into a remote root
223            remote_root = root.replace(local_path.rstrip(os.path.sep),
224                                       remote_path.rstrip("/"))
225            remote_root = remote_root.replace(os.path.sep, "/").rstrip("/")
226
227            # Don't process ignored subdirectories
228            if (self.ignore_func and self.ignore_func(root, svn_node_dir)
229                or root in ignores):
230
231                # Ignore children too
232                for name in dirs:
233                    ignores.append("%s/%s" % (remote_root, name))
234
235                # Skip to the next tuple
236                continue
237
238            # Add all subdirectories
239            for name in dirs:
240                remote_dir = "%s/%s" % (remote_root, name)
241                self.mkdir(remote_dir)
242
243            # Add all files in this directory
244            for name in files:
245                remote_file = "%s/%s" % (remote_root, name)
246                local_file = os.path.join(root, name)
247                self._upload_file(remote_file, local_file)
248
249    def _txn_commit_callback(self, info, baton, pool):
250        self._txn_committed(info[0])
251
252    def commit(self, message, base_rev = None):
253        """Commit all changes to the remote repository"""
254
255        if base_rev is None:
256            base_rev = self.session.latest_revnum()
257
258        commit_baton = c_void_p()
259
260        self.commit_callback = svn_commit_callback2_t(self._txn_commit_callback)
261        (editor, editor_baton) = self.session._get_commit_editor(message,
262            self.commit_callback, commit_baton, self.pool)
263
264        child_baton = c_void_p()
265        try:
266            self.root.replay(editor[0], self.session, base_rev, editor_baton)
267        except SubversionException:
268            try:
269                SVN_ERR(editor[0].abort_edit(editor_baton, self.pool))
270            except SubversionException:
271                pass
272            raise
273
274        return self.committed_rev
275
276    # This private function handles commits and saves
277    # information about them in this object
278    def _txn_committed(self, info):
279        self.committed_rev = info.revision
280        self.committed_date = info.date
281        self.committed_author = info.author
282        self.post_commit_err = info.post_commit_err
283
284    # This private function uploads a single file to the
285    # remote repository. Don't use this function directly.
286    # Use 'upload' instead.
287    def _upload_file(self, remote_path, local_path):
288
289        if self.ignore_func and self.ignore_func(remote_path, svn_node_file):
290            return
291
292        kind, parent = self._check_path(remote_path)
293        if svn_node_none == kind:
294            mode = "ADD"
295        else:
296            mode = "OPEN"
297
298        parent.open(remote_path, mode, svn_node_file,
299                    local_path=local_path)
300
301        # Trigger autoprop_func on new file adds
302        if mode == "ADD" and self.autoprop_func:
303            self.autoprop_func(self, remote_path, svn_node_file)
304
305    # Calculate the kind of the specified file, and open a handle
306    # to its parent operation.
307    def _check_path(self, path, rev=None):
308        path_components = path.split("/")
309        parent = self.root
310        copyfrom_path = None
311        total_path = path_components[0]
312        for path_component in path_components[1:]:
313            parent = parent.open(total_path, "OPEN")
314            if parent.copyfrom_path:
315                copyfrom_path = parent.copyfrom_path
316                rev = parent.copyfrom_rev
317
318            total_path = "%s/%s" % (total_path, path_component)
319            if copyfrom_path:
320                copyfrom_path = "%s/%s" % (copyfrom_path, path_component)
321
322        if path in parent.ops:
323            node = parent.open(path)
324            if node.action == "DELETE":
325                kind = svn_node_none
326            else:
327                kind = node.kind
328        else:
329            kind = self.session.check_path(copyfrom_path or total_path, rev,
330                                           encoded=False)
331
332        return (kind, parent)
333
334
335
336class _txn_operation(object):
337    def __init__(self, path, action, kind, copyfrom_path = None,
338                 copyfrom_rev = -1, local_path = None):
339        self.path = path
340        self.action = action
341        self.kind = kind
342        self.copyfrom_path = copyfrom_path
343        self.copyfrom_rev = copyfrom_rev
344        self.local_path = local_path
345        self.ops = {}
346        self.properties = {}
347
348    def propset(self, key, value):
349        """Set the property named KEY to VALUE on this file/dir"""
350        self.properties[key] = value
351
352    def propdel(self, key):
353        """Delete the property named KEY on this file/dir"""
354        self.properties[key] = None
355
356    def open(self, path, action="OPEN", kind=svn_node_dir,
357             copyfrom_path = None, copyfrom_rev = -1, local_path = None):
358        if path in self.ops:
359            op = self.ops[path]
360            if action == "OPEN" and op.kind in (svn_node_dir, svn_node_file):
361                return op
362            elif action == "ADD" and op.action == "DELETE":
363                op.action = "REPLACE"
364                op.local_path = local_path
365                op.copyfrom_path = copyfrom_path
366                op.copyfrom_rev = copyfrom_rev
367                op.kind = kind
368                return op
369            elif (action == "DELETE" and op.action == "OPEN" and
370                  kind == svn_node_dir):
371                op.action = action
372                return op
373            else:
374                # throw error
375                pass
376        else:
377            self.ops[path] = _txn_operation(path, action, kind,
378                                            copyfrom_path = copyfrom_path,
379                                            copyfrom_rev = copyfrom_rev,
380                                            local_path = local_path)
381            return self.ops[path]
382
383    def replay(self, editor, session, base_rev, baton):
384        subpool = Pool()
385        child_baton = c_void_p()
386        file_baton = c_void_p()
387        if self.path is None:
388            SVN_ERR(editor.open_root(baton, svn_revnum_t(base_rev), subpool,
389                                     byref(child_baton)))
390        else:
391            if self.action == "DELETE" or self.action == "REPLACE":
392                SVN_ERR(editor.delete_entry(self.path, base_rev, baton,
393                                            subpool))
394            elif self.action == "OPEN":
395                if self.kind == svn_node_dir:
396                    SVN_ERR(editor.open_directory(self.path, baton,
397                            svn_revnum_t(base_rev), subpool,
398                            byref(child_baton)))
399                else:
400                    SVN_ERR(editor.open_file(self.path, baton,
401                                svn_revnum_t(base_rev), subpool,
402                                byref(file_baton)))
403
404            if self.action in ("ADD", "REPLACE"):
405                copyfrom_path = None
406                if self.copyfrom_path is not None:
407                    copyfrom_path = session._abs_copyfrom_path(
408                        self.copyfrom_path)
409                if self.kind == svn_node_dir:
410                    SVN_ERR(editor.add_directory(
411                        self.path, baton, copyfrom_path,
412                        svn_revnum_t(self.copyfrom_rev), subpool,
413                        byref(child_baton)))
414                else:
415                    SVN_ERR(editor.add_file(self.path, baton,
416                        copyfrom_path, svn_revnum_t(self.copyfrom_rev),
417                        subpool, byref(file_baton)))
418
419            # Write out changes to properties
420            for (name, value) in self.properties.items():
421                if value is None:
422                    svn_value = POINTER(svn_string_t)()
423                else:
424                    svn_value = svn_string_ncreate(value, len(value),
425                                                   subpool)
426                if file_baton:
427                    SVN_ERR(editor.change_file_prop(file_baton, name,
428                            svn_value, subpool))
429                elif child_baton:
430                    SVN_ERR(editor.change_dir_prop(child_baton, name,
431                            svn_value, subpool))
432
433            # If there's a source file, and we opened a file to write,
434            # write out the contents
435            if self.local_path and file_baton:
436                handler = svn_txdelta_window_handler_t()
437                handler_baton = c_void_p()
438                f = POINTER(apr_file_t)()
439                SVN_ERR(editor.apply_textdelta(file_baton, NULL, subpool,
440                        byref(handler), byref(handler_baton)))
441
442                svn_io_file_open(byref(f), self.local_path, APR_READ,
443                                 APR_OS_DEFAULT, subpool)
444                contents = svn_stream_from_aprfile(f, subpool)
445                svn_txdelta_send_stream(contents, handler, handler_baton,
446                                        NULL, subpool)
447                svn_io_file_close(f, subpool)
448
449            # If we opened a file, we need to close it
450            if file_baton:
451                SVN_ERR(editor.close_file(file_baton, NULL, subpool))
452
453        if self.kind == svn_node_dir and self.action != "DELETE":
454            assert(child_baton)
455
456            # Look at the children
457            for op in self.ops.values():
458                op.replay(editor, session, base_rev, child_baton)
459
460            if self.path:
461                # Close the directory
462                SVN_ERR(editor.close_directory(child_baton, subpool))
463            else:
464                # Close the editor
465                SVN_ERR(editor.close_edit(baton, subpool))
466
467