##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r263:2fd24df3 merge default
parent child Browse files
Show More
@@ -1,21 +1,22 b''
1 1 c6fad7d1e61f22b1f4a4863eff207a04c27e9462 v4.0.0
2 2 77b6e243b4cc5b702c15abd6d737798edbac60dc v4.0.1
3 3 a359c072337fdd8e1e71df72cc520b8a9b042f80 v4.1.0
4 4 49aa7ed030a36b7ceba149a21e587cb5d20b4946 v4.1.1
5 5 f38ed1e1a31dce3c170b4d31585ba43471cf0705 v4.1.2
6 6 21269ba7bafd8f0c77e79dd86a31eb9bce7643d2 v4.2.0
7 7 b53930c918c25b2c8f69ceddc6641e511be27fd3 v4.2.1
8 8 6627ff4119723d8b2b60918e8b1aa49e9f055aab v4.3.0
9 9 d38f2c2b861dde6c4178923f7cf15ea58b85aa92 v4.3.1
10 10 1232313f9e6adac5ce5399c2a891dc1e72b79022 v4.4.0
11 11 cbb9f1d329ae5768379cdec55a62ebdd546c4e27 v4.4.1
12 12 24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17 v4.4.2
13 13 beaeeaa440cd17471110d4621b8816506c0dff4a v4.5.0
14 14 668e5c656f61dd94595611844e1106d1361aa6a7 v4.5.1
15 15 ae0640240cb7a77e6dc8c77e64dd80d79732cb5b v4.5.2
16 16 7af06899f426813583fe60449d7517cc49c15b28 v4.6.0
17 17 8f7f4299bf341b43f94dadafa1ea73d6cea2c9ba v4.6.1
18 18 de00a831a0709ffaac57f948738ea927b97223a9 v4.7.0
19 19 57f527e0646d731768fb5e0fe742b12a35bdc63b v4.7.1
20 20 f9b09787da9845e4a105f4bffdc252099902cefb v4.7.2
21 21 0b7c790b726f08385e6ebdf4f257c905787b9244 v4.8.0
22 f4123e725b74d0e82fe89982ab8791a66062e2b3 v4.9.0
@@ -1,668 +1,679 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 from __future__ import absolute_import
19 19
20 import os
20 21 from urllib2 import URLError
21 22 import logging
22 23 import posixpath as vcspath
23 24 import StringIO
24 25 import subprocess
25 26 import urllib
26 27 import traceback
27 28
28 29 import svn.client
29 30 import svn.core
30 31 import svn.delta
31 32 import svn.diff
32 33 import svn.fs
33 34 import svn.repos
34 35
35 36 from vcsserver import svn_diff
36 37 from vcsserver import exceptions
37 38 from vcsserver.base import RepoFactory, raise_from_original
38 39
39 40
40 41 log = logging.getLogger(__name__)
41 42
42 43
43 44 # Set of svn compatible version flags.
44 45 # Compare with subversion/svnadmin/svnadmin.c
45 46 svn_compatible_versions = set([
46 47 'pre-1.4-compatible',
47 48 'pre-1.5-compatible',
48 49 'pre-1.6-compatible',
49 50 'pre-1.8-compatible',
50 51 'pre-1.9-compatible',
51 52 ])
52 53
53 54 svn_compatible_versions_map = {
54 55 'pre-1.4-compatible': '1.3',
55 56 'pre-1.5-compatible': '1.4',
56 57 'pre-1.6-compatible': '1.5',
57 58 'pre-1.8-compatible': '1.7',
58 59 'pre-1.9-compatible': '1.8',
59 60 }
60 61
61 62
62 63 def reraise_safe_exceptions(func):
63 64 """Decorator for converting svn exceptions to something neutral."""
64 65 def wrapper(*args, **kwargs):
65 66 try:
66 67 return func(*args, **kwargs)
67 68 except Exception as e:
68 69 if not hasattr(e, '_vcs_kind'):
69 70 log.exception("Unhandled exception in hg remote call")
70 71 raise_from_original(exceptions.UnhandledException)
71 72 raise
72 73 return wrapper
73 74
74 75
75 76 class SubversionFactory(RepoFactory):
76 77
77 78 def _create_repo(self, wire, create, compatible_version):
78 79 path = svn.core.svn_path_canonicalize(wire['path'])
79 80 if create:
80 81 fs_config = {'compatible-version': '1.9'}
81 82 if compatible_version:
82 83 if compatible_version not in svn_compatible_versions:
83 84 raise Exception('Unknown SVN compatible version "{}"'
84 85 .format(compatible_version))
85 86 fs_config['compatible-version'] = \
86 87 svn_compatible_versions_map[compatible_version]
87 88
88 89 log.debug('Create SVN repo with config "%s"', fs_config)
89 90 repo = svn.repos.create(path, "", "", None, fs_config)
90 91 else:
91 92 repo = svn.repos.open(path)
93
94 log.debug('Got SVN object: %s', repo)
92 95 return repo
93 96
94 97 def repo(self, wire, create=False, compatible_version=None):
95 98 def create_new_repo():
96 99 return self._create_repo(wire, create, compatible_version)
97 100
98 101 return self._repo(wire, create_new_repo)
99 102
100 103
101 104 NODE_TYPE_MAPPING = {
102 105 svn.core.svn_node_file: 'file',
103 106 svn.core.svn_node_dir: 'dir',
104 107 }
105 108
106 109
107 110 class SvnRemote(object):
108 111
109 112 def __init__(self, factory, hg_factory=None):
110 113 self._factory = factory
111 114 # TODO: Remove once we do not use internal Mercurial objects anymore
112 115 # for subversion
113 116 self._hg_factory = hg_factory
114 117
115 118 @reraise_safe_exceptions
116 119 def discover_svn_version(self):
117 120 try:
118 121 import svn.core
119 122 svn_ver = svn.core.SVN_VERSION
120 123 except ImportError:
121 124 svn_ver = None
122 125 return svn_ver
123 126
124 127 def check_url(self, url, config_items):
125 128 # this can throw exception if not installed, but we detect this
126 129 from hgsubversion import svnrepo
127 130
128 131 baseui = self._hg_factory._create_config(config_items)
129 132 # uuid function get's only valid UUID from proper repo, else
130 133 # throws exception
131 134 try:
132 135 svnrepo.svnremoterepo(baseui, url).svn.uuid
133 136 except Exception:
134 137 tb = traceback.format_exc()
135 138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
136 139 raise URLError(
137 140 '"%s" is not a valid Subversion source url.' % (url, ))
138 141 return True
139 142
140 143 def is_path_valid_repository(self, wire, path):
144
145 # NOTE(marcink): short circuit the check for SVN repo
146 # the repos.open might be expensive to check, but we have one cheap
147 # pre condition that we can use, to check for 'format' file
148
149 if not os.path.isfile(os.path.join(path, 'format')):
150 return False
151
141 152 try:
142 153 svn.repos.open(path)
143 154 except svn.core.SubversionException:
144 155 tb = traceback.format_exc()
145 156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
146 157 return False
147 158 return True
148 159
149 160 @reraise_safe_exceptions
150 161 def verify(self, wire,):
151 162 repo_path = wire['path']
152 163 if not self.is_path_valid_repository(wire, repo_path):
153 164 raise Exception(
154 165 "Path %s is not a valid Subversion repository." % repo_path)
155 166
156 167 load = subprocess.Popen(
157 168 ['svnadmin', 'info', repo_path],
158 169 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
159 170 return ''.join(load.stdout)
160 171
161 172 def lookup(self, wire, revision):
162 173 if revision not in [-1, None, 'HEAD']:
163 174 raise NotImplementedError
164 175 repo = self._factory.repo(wire)
165 176 fs_ptr = svn.repos.fs(repo)
166 177 head = svn.fs.youngest_rev(fs_ptr)
167 178 return head
168 179
169 180 def lookup_interval(self, wire, start_ts, end_ts):
170 181 repo = self._factory.repo(wire)
171 182 fsobj = svn.repos.fs(repo)
172 183 start_rev = None
173 184 end_rev = None
174 185 if start_ts:
175 186 start_ts_svn = apr_time_t(start_ts)
176 187 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
177 188 else:
178 189 start_rev = 1
179 190 if end_ts:
180 191 end_ts_svn = apr_time_t(end_ts)
181 192 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
182 193 else:
183 194 end_rev = svn.fs.youngest_rev(fsobj)
184 195 return start_rev, end_rev
185 196
186 197 def revision_properties(self, wire, revision):
187 198 repo = self._factory.repo(wire)
188 199 fs_ptr = svn.repos.fs(repo)
189 200 return svn.fs.revision_proplist(fs_ptr, revision)
190 201
191 202 def revision_changes(self, wire, revision):
192 203
193 204 repo = self._factory.repo(wire)
194 205 fsobj = svn.repos.fs(repo)
195 206 rev_root = svn.fs.revision_root(fsobj, revision)
196 207
197 208 editor = svn.repos.ChangeCollector(fsobj, rev_root)
198 209 editor_ptr, editor_baton = svn.delta.make_editor(editor)
199 210 base_dir = ""
200 211 send_deltas = False
201 212 svn.repos.replay2(
202 213 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
203 214 editor_ptr, editor_baton, None)
204 215
205 216 added = []
206 217 changed = []
207 218 removed = []
208 219
209 220 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
210 221 for path, change in editor.changes.iteritems():
211 222 # TODO: Decide what to do with directory nodes. Subversion can add
212 223 # empty directories.
213 224
214 225 if change.item_kind == svn.core.svn_node_dir:
215 226 continue
216 227 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
217 228 added.append(path)
218 229 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
219 230 svn.repos.CHANGE_ACTION_REPLACE]:
220 231 changed.append(path)
221 232 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
222 233 removed.append(path)
223 234 else:
224 235 raise NotImplementedError(
225 236 "Action %s not supported on path %s" % (
226 237 change.action, path))
227 238
228 239 changes = {
229 240 'added': added,
230 241 'changed': changed,
231 242 'removed': removed,
232 243 }
233 244 return changes
234 245
235 246 def node_history(self, wire, path, revision, limit):
236 247 cross_copies = False
237 248 repo = self._factory.repo(wire)
238 249 fsobj = svn.repos.fs(repo)
239 250 rev_root = svn.fs.revision_root(fsobj, revision)
240 251
241 252 history_revisions = []
242 253 history = svn.fs.node_history(rev_root, path)
243 254 history = svn.fs.history_prev(history, cross_copies)
244 255 while history:
245 256 __, node_revision = svn.fs.history_location(history)
246 257 history_revisions.append(node_revision)
247 258 if limit and len(history_revisions) >= limit:
248 259 break
249 260 history = svn.fs.history_prev(history, cross_copies)
250 261 return history_revisions
251 262
252 263 def node_properties(self, wire, path, revision):
253 264 repo = self._factory.repo(wire)
254 265 fsobj = svn.repos.fs(repo)
255 266 rev_root = svn.fs.revision_root(fsobj, revision)
256 267 return svn.fs.node_proplist(rev_root, path)
257 268
258 269 def file_annotate(self, wire, path, revision):
259 270 abs_path = 'file://' + urllib.pathname2url(
260 271 vcspath.join(wire['path'], path))
261 272 file_uri = svn.core.svn_path_canonicalize(abs_path)
262 273
263 274 start_rev = svn_opt_revision_value_t(0)
264 275 peg_rev = svn_opt_revision_value_t(revision)
265 276 end_rev = peg_rev
266 277
267 278 annotations = []
268 279
269 280 def receiver(line_no, revision, author, date, line, pool):
270 281 annotations.append((line_no, revision, line))
271 282
272 283 # TODO: Cannot use blame5, missing typemap function in the swig code
273 284 try:
274 285 svn.client.blame2(
275 286 file_uri, peg_rev, start_rev, end_rev,
276 287 receiver, svn.client.create_context())
277 288 except svn.core.SubversionException as exc:
278 289 log.exception("Error during blame operation.")
279 290 raise Exception(
280 291 "Blame not supported or file does not exist at path %s. "
281 292 "Error %s." % (path, exc))
282 293
283 294 return annotations
284 295
285 296 def get_node_type(self, wire, path, rev=None):
286 297 repo = self._factory.repo(wire)
287 298 fs_ptr = svn.repos.fs(repo)
288 299 if rev is None:
289 300 rev = svn.fs.youngest_rev(fs_ptr)
290 301 root = svn.fs.revision_root(fs_ptr, rev)
291 302 node = svn.fs.check_path(root, path)
292 303 return NODE_TYPE_MAPPING.get(node, None)
293 304
294 305 def get_nodes(self, wire, path, revision=None):
295 306 repo = self._factory.repo(wire)
296 307 fsobj = svn.repos.fs(repo)
297 308 if revision is None:
298 309 revision = svn.fs.youngest_rev(fsobj)
299 310 root = svn.fs.revision_root(fsobj, revision)
300 311 entries = svn.fs.dir_entries(root, path)
301 312 result = []
302 313 for entry_path, entry_info in entries.iteritems():
303 314 result.append(
304 315 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
305 316 return result
306 317
307 318 def get_file_content(self, wire, path, rev=None):
308 319 repo = self._factory.repo(wire)
309 320 fsobj = svn.repos.fs(repo)
310 321 if rev is None:
311 322 rev = svn.fs.youngest_revision(fsobj)
312 323 root = svn.fs.revision_root(fsobj, rev)
313 324 content = svn.core.Stream(svn.fs.file_contents(root, path))
314 325 return content.read()
315 326
316 327 def get_file_size(self, wire, path, revision=None):
317 328 repo = self._factory.repo(wire)
318 329 fsobj = svn.repos.fs(repo)
319 330 if revision is None:
320 331 revision = svn.fs.youngest_revision(fsobj)
321 332 root = svn.fs.revision_root(fsobj, revision)
322 333 size = svn.fs.file_length(root, path)
323 334 return size
324 335
325 336 def create_repository(self, wire, compatible_version=None):
326 337 log.info('Creating Subversion repository in path "%s"', wire['path'])
327 338 self._factory.repo(wire, create=True,
328 339 compatible_version=compatible_version)
329 340
330 341 def import_remote_repository(self, wire, src_url):
331 342 repo_path = wire['path']
332 343 if not self.is_path_valid_repository(wire, repo_path):
333 344 raise Exception(
334 345 "Path %s is not a valid Subversion repository." % repo_path)
335 346 # TODO: johbo: URL checks ?
336 347 rdump = subprocess.Popen(
337 348 ['svnrdump', 'dump', '--non-interactive', src_url],
338 349 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
339 350 load = subprocess.Popen(
340 351 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
341 352
342 353 # TODO: johbo: This can be a very long operation, might be better
343 354 # to track some kind of status and provide an api to check if the
344 355 # import is done.
345 356 rdump.wait()
346 357 load.wait()
347 358
348 359 if rdump.returncode != 0:
349 360 errors = rdump.stderr.read()
350 361 log.error('svnrdump dump failed: statuscode %s: message: %s',
351 362 rdump.returncode, errors)
352 363 reason = 'UNKNOWN'
353 364 if 'svnrdump: E230001:' in errors:
354 365 reason = 'INVALID_CERTIFICATE'
355 366 raise Exception(
356 367 'Failed to dump the remote repository from %s.' % src_url,
357 368 reason)
358 369 if load.returncode != 0:
359 370 raise Exception(
360 371 'Failed to load the dump of remote repository from %s.' %
361 372 (src_url, ))
362 373
363 374 def commit(self, wire, message, author, timestamp, updated, removed):
364 375 assert isinstance(message, str)
365 376 assert isinstance(author, str)
366 377
367 378 repo = self._factory.repo(wire)
368 379 fsobj = svn.repos.fs(repo)
369 380
370 381 rev = svn.fs.youngest_rev(fsobj)
371 382 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
372 383 txn_root = svn.fs.txn_root(txn)
373 384
374 385 for node in updated:
375 386 TxnNodeProcessor(node, txn_root).update()
376 387 for node in removed:
377 388 TxnNodeProcessor(node, txn_root).remove()
378 389
379 390 commit_id = svn.repos.fs_commit_txn(repo, txn)
380 391
381 392 if timestamp:
382 393 apr_time = apr_time_t(timestamp)
383 394 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
384 395 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
385 396
386 397 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
387 398 return commit_id
388 399
389 400 def diff(self, wire, rev1, rev2, path1=None, path2=None,
390 401 ignore_whitespace=False, context=3):
391 402
392 403 wire.update(cache=False)
393 404 repo = self._factory.repo(wire)
394 405 diff_creator = SvnDiffer(
395 406 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
396 407 try:
397 408 return diff_creator.generate_diff()
398 409 except svn.core.SubversionException as e:
399 410 log.exception(
400 411 "Error during diff operation operation. "
401 412 "Path might not exist %s, %s" % (path1, path2))
402 413 return ""
403 414
404 415 @reraise_safe_exceptions
405 416 def is_large_file(self, wire, path):
406 417 return False
407 418
408 419
409 420 class SvnDiffer(object):
410 421 """
411 422 Utility to create diffs based on difflib and the Subversion api
412 423 """
413 424
414 425 binary_content = False
415 426
416 427 def __init__(
417 428 self, repo, src_rev, src_path, tgt_rev, tgt_path,
418 429 ignore_whitespace, context):
419 430 self.repo = repo
420 431 self.ignore_whitespace = ignore_whitespace
421 432 self.context = context
422 433
423 434 fsobj = svn.repos.fs(repo)
424 435
425 436 self.tgt_rev = tgt_rev
426 437 self.tgt_path = tgt_path or ''
427 438 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
428 439 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
429 440
430 441 self.src_rev = src_rev
431 442 self.src_path = src_path or self.tgt_path
432 443 self.src_root = svn.fs.revision_root(fsobj, src_rev)
433 444 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
434 445
435 446 self._validate()
436 447
437 448 def _validate(self):
438 449 if (self.tgt_kind != svn.core.svn_node_none and
439 450 self.src_kind != svn.core.svn_node_none and
440 451 self.src_kind != self.tgt_kind):
441 452 # TODO: johbo: proper error handling
442 453 raise Exception(
443 454 "Source and target are not compatible for diff generation. "
444 455 "Source type: %s, target type: %s" %
445 456 (self.src_kind, self.tgt_kind))
446 457
447 458 def generate_diff(self):
448 459 buf = StringIO.StringIO()
449 460 if self.tgt_kind == svn.core.svn_node_dir:
450 461 self._generate_dir_diff(buf)
451 462 else:
452 463 self._generate_file_diff(buf)
453 464 return buf.getvalue()
454 465
455 466 def _generate_dir_diff(self, buf):
456 467 editor = DiffChangeEditor()
457 468 editor_ptr, editor_baton = svn.delta.make_editor(editor)
458 469 svn.repos.dir_delta2(
459 470 self.src_root,
460 471 self.src_path,
461 472 '', # src_entry
462 473 self.tgt_root,
463 474 self.tgt_path,
464 475 editor_ptr, editor_baton,
465 476 authorization_callback_allow_all,
466 477 False, # text_deltas
467 478 svn.core.svn_depth_infinity, # depth
468 479 False, # entry_props
469 480 False, # ignore_ancestry
470 481 )
471 482
472 483 for path, __, change in sorted(editor.changes):
473 484 self._generate_node_diff(
474 485 buf, change, path, self.tgt_path, path, self.src_path)
475 486
476 487 def _generate_file_diff(self, buf):
477 488 change = None
478 489 if self.src_kind == svn.core.svn_node_none:
479 490 change = "add"
480 491 elif self.tgt_kind == svn.core.svn_node_none:
481 492 change = "delete"
482 493 tgt_base, tgt_path = vcspath.split(self.tgt_path)
483 494 src_base, src_path = vcspath.split(self.src_path)
484 495 self._generate_node_diff(
485 496 buf, change, tgt_path, tgt_base, src_path, src_base)
486 497
487 498 def _generate_node_diff(
488 499 self, buf, change, tgt_path, tgt_base, src_path, src_base):
489 500
490 501 if self.src_rev == self.tgt_rev and tgt_base == src_base:
491 502 # makes consistent behaviour with git/hg to return empty diff if
492 503 # we compare same revisions
493 504 return
494 505
495 506 tgt_full_path = vcspath.join(tgt_base, tgt_path)
496 507 src_full_path = vcspath.join(src_base, src_path)
497 508
498 509 self.binary_content = False
499 510 mime_type = self._get_mime_type(tgt_full_path)
500 511
501 512 if mime_type and not mime_type.startswith('text'):
502 513 self.binary_content = True
503 514 buf.write("=" * 67 + '\n')
504 515 buf.write("Cannot display: file marked as a binary type.\n")
505 516 buf.write("svn:mime-type = %s\n" % mime_type)
506 517 buf.write("Index: %s\n" % (tgt_path, ))
507 518 buf.write("=" * 67 + '\n')
508 519 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
509 520 'tgt_path': tgt_path})
510 521
511 522 if change == 'add':
512 523 # TODO: johbo: SVN is missing a zero here compared to git
513 524 buf.write("new file mode 10644\n")
514 525
515 526 #TODO(marcink): intro to binary detection of svn patches
516 527 # if self.binary_content:
517 528 # buf.write('GIT binary patch\n')
518 529
519 530 buf.write("--- /dev/null\t(revision 0)\n")
520 531 src_lines = []
521 532 else:
522 533 if change == 'delete':
523 534 buf.write("deleted file mode 10644\n")
524 535
525 536 #TODO(marcink): intro to binary detection of svn patches
526 537 # if self.binary_content:
527 538 # buf.write('GIT binary patch\n')
528 539
529 540 buf.write("--- a/%s\t(revision %s)\n" % (
530 541 src_path, self.src_rev))
531 542 src_lines = self._svn_readlines(self.src_root, src_full_path)
532 543
533 544 if change == 'delete':
534 545 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
535 546 tgt_lines = []
536 547 else:
537 548 buf.write("+++ b/%s\t(revision %s)\n" % (
538 549 tgt_path, self.tgt_rev))
539 550 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
540 551
541 552 if not self.binary_content:
542 553 udiff = svn_diff.unified_diff(
543 554 src_lines, tgt_lines, context=self.context,
544 555 ignore_blank_lines=self.ignore_whitespace,
545 556 ignore_case=False,
546 557 ignore_space_changes=self.ignore_whitespace)
547 558 buf.writelines(udiff)
548 559
549 560 def _get_mime_type(self, path):
550 561 try:
551 562 mime_type = svn.fs.node_prop(
552 563 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
553 564 except svn.core.SubversionException:
554 565 mime_type = svn.fs.node_prop(
555 566 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
556 567 return mime_type
557 568
558 569 def _svn_readlines(self, fs_root, node_path):
559 570 if self.binary_content:
560 571 return []
561 572 node_kind = svn.fs.check_path(fs_root, node_path)
562 573 if node_kind not in (
563 574 svn.core.svn_node_file, svn.core.svn_node_symlink):
564 575 return []
565 576 content = svn.core.Stream(
566 577 svn.fs.file_contents(fs_root, node_path)).read()
567 578 return content.splitlines(True)
568 579
569 580
570 581 class DiffChangeEditor(svn.delta.Editor):
571 582 """
572 583 Records changes between two given revisions
573 584 """
574 585
575 586 def __init__(self):
576 587 self.changes = []
577 588
578 589 def delete_entry(self, path, revision, parent_baton, pool=None):
579 590 self.changes.append((path, None, 'delete'))
580 591
581 592 def add_file(
582 593 self, path, parent_baton, copyfrom_path, copyfrom_revision,
583 594 file_pool=None):
584 595 self.changes.append((path, 'file', 'add'))
585 596
586 597 def open_file(self, path, parent_baton, base_revision, file_pool=None):
587 598 self.changes.append((path, 'file', 'change'))
588 599
589 600
590 601 def authorization_callback_allow_all(root, path, pool):
591 602 return True
592 603
593 604
594 605 class TxnNodeProcessor(object):
595 606 """
596 607 Utility to process the change of one node within a transaction root.
597 608
598 609 It encapsulates the knowledge of how to add, update or remove
599 610 a node for a given transaction root. The purpose is to support the method
600 611 `SvnRemote.commit`.
601 612 """
602 613
603 614 def __init__(self, node, txn_root):
604 615 assert isinstance(node['path'], str)
605 616
606 617 self.node = node
607 618 self.txn_root = txn_root
608 619
609 620 def update(self):
610 621 self._ensure_parent_dirs()
611 622 self._add_file_if_node_does_not_exist()
612 623 self._update_file_content()
613 624 self._update_file_properties()
614 625
615 626 def remove(self):
616 627 svn.fs.delete(self.txn_root, self.node['path'])
617 628 # TODO: Clean up directory if empty
618 629
619 630 def _ensure_parent_dirs(self):
620 631 curdir = vcspath.dirname(self.node['path'])
621 632 dirs_to_create = []
622 633 while not self._svn_path_exists(curdir):
623 634 dirs_to_create.append(curdir)
624 635 curdir = vcspath.dirname(curdir)
625 636
626 637 for curdir in reversed(dirs_to_create):
627 638 log.debug('Creating missing directory "%s"', curdir)
628 639 svn.fs.make_dir(self.txn_root, curdir)
629 640
630 641 def _svn_path_exists(self, path):
631 642 path_status = svn.fs.check_path(self.txn_root, path)
632 643 return path_status != svn.core.svn_node_none
633 644
634 645 def _add_file_if_node_does_not_exist(self):
635 646 kind = svn.fs.check_path(self.txn_root, self.node['path'])
636 647 if kind == svn.core.svn_node_none:
637 648 svn.fs.make_file(self.txn_root, self.node['path'])
638 649
639 650 def _update_file_content(self):
640 651 assert isinstance(self.node['content'], str)
641 652 handler, baton = svn.fs.apply_textdelta(
642 653 self.txn_root, self.node['path'], None, None)
643 654 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
644 655
645 656 def _update_file_properties(self):
646 657 properties = self.node.get('properties', {})
647 658 for key, value in properties.iteritems():
648 659 svn.fs.change_node_prop(
649 660 self.txn_root, self.node['path'], key, value)
650 661
651 662
652 663 def apr_time_t(timestamp):
653 664 """
654 665 Convert a Python timestamp into APR timestamp type apr_time_t
655 666 """
656 667 return timestamp * 1E6
657 668
658 669
659 670 def svn_opt_revision_value_t(num):
660 671 """
661 672 Put `num` into a `svn_opt_revision_value_t` structure.
662 673 """
663 674 value = svn.core.svn_opt_revision_value_t()
664 675 value.number = num
665 676 revision = svn.core.svn_opt_revision_t()
666 677 revision.kind = svn.core.svn_opt_revision_number
667 678 revision.value = value
668 679 return revision
General Comments 0
You need to be logged in to leave comments. Login now