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