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 |
|
19 |
import csvn.core as svn |
20 |
from csvn.core import * |
21 |
import os |
22 |
|
23 |
class 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 |
|
336 |
class _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 |
|