1# -*-python-*- 2# 3# Copyright (C) 1999-2020 The ViewCVS Group. All Rights Reserved. 4# 5# By using this file, you agree to the terms and conditions set forth in 6# the LICENSE.html file which can be found at the top level of the ViewVC 7# distribution or at http://viewvc.org/license-1.html. 8# 9# For more information, visit http://viewvc.org/ 10# 11# ----------------------------------------------------------------------- 12 13"Version Control lib driver for remotely accessible Subversion repositories." 14 15import vclib 16import sys 17import os 18import re 19import tempfile 20import time 21import functools 22from urllib.parse import quote as _quote 23 24from .svn_repos import Revision, SVNChangedPath, _datestr_to_date, _to_str, \ 25 _compare_paths, _path_parts, _cleanup_path, \ 26 _rev2optrev, _normalize_property_value, \ 27 _normalize_property, _split_revprops 28from svn import core, delta, client, wc, ra 29 30 31### Verify that we have an acceptable version of Subversion. 32MIN_SUBVERSION_VERSION = (1, 14, 0) 33HAS_SUBVERSION_VERSION = (core.SVN_VER_MAJOR, 34 core.SVN_VER_MINOR, 35 core.SVN_VER_PATCH) 36if HAS_SUBVERSION_VERSION < MIN_SUBVERSION_VERSION: 37 found_ver = '.'.join([str(x) for x in HAS_SUBVERSION_VERSION]) 38 needs_ver = '.'.join([str(x) for x in MIN_SUBVERSION_VERSION]) 39 raise Exception("Subversion version %s is required (%s found)" 40 % (needs_ver, found_ver)) 41 42 43def client_log(url, start_rev, end_rev, log_limit, include_changes, 44 cross_copies, cb_func, ctx): 45 include_changes = include_changes and 1 or 0 46 cross_copies = cross_copies and 1 or 0 47 client.svn_client_log4([url], start_rev, start_rev, end_rev, 48 log_limit, include_changes, not cross_copies, 49 0, None, cb_func, ctx) 50 51 52def setup_client_ctx(config_dir): 53 # Ensure that the configuration directory exists. 54 core.svn_config_ensure(config_dir) 55 56 # Fetch the configuration (and 'config' bit thereof). 57 cfg = core.svn_config_get_config(config_dir) 58 config = cfg.get(core.SVN_CONFIG_CATEGORY_CONFIG) 59 60 auth_baton = core.svn_cmdline_create_auth_baton(1, None, None, config_dir, 61 1, 1, config, None) 62 63 # Create, setup, and return the client context baton. 64 ctx = client.svn_client_create_context() 65 ctx.config = cfg 66 ctx.auth_baton = auth_baton 67 return ctx 68 69class LogCollector: 70 71 def __init__(self, path, show_all_logs, lockinfo, access_check_func, 72 encoding='utf-8'): 73 # This class uses leading slashes for paths internally 74 if not path: 75 self.path = '/' 76 else: 77 self.path = path[0] == '/' and path or '/' + path 78 self.logs = [] 79 self.show_all_logs = show_all_logs 80 self.lockinfo = lockinfo 81 self.access_check_func = access_check_func 82 self.done = False 83 self.encoding = encoding 84 85 def add_log(self, log_entry, pool): 86 if self.done: 87 return 88 paths = log_entry.changed_paths 89 revision = log_entry.revision 90 msg, author, date, revprops = _split_revprops(log_entry.revprops) 91 92 # Changed paths have leading slashes 93 changed_paths = [_to_str(p) for p in paths.keys()] 94 changed_paths.sort(key=functools.cmp_to_key( 95 lambda a, b: _compare_paths(a, b))) 96 this_path = None 97 if self.path in changed_paths: 98 this_path = self.path 99 change = paths[self.path.encode('utf-8', 'surrogateescape')] 100 if change.copyfrom_path: 101 this_path = _to_str(change.copyfrom_path) 102 for changed_path in changed_paths: 103 if changed_path != self.path: 104 # If a parent of our path was copied, our "next previous" 105 # (huh?) path will exist elsewhere (under the copy source). 106 if (self.path.rfind(changed_path) == 0) and \ 107 self.path[len(changed_path)] == '/': 108 change = paths[changed_path.encode('utf-8', 'surrogateescape')] 109 if change.copyfrom_path: 110 this_path = ( _to_str(change.copyfrom_path) 111 + self.path[len(changed_path):]) 112 if self.show_all_logs or this_path: 113 if self.access_check_func is None \ 114 or self.access_check_func(self.path[1:], revision): 115 entry = Revision(revision, date, author, msg, None, self.lockinfo, 116 self.path[1:], None, None) 117 self.logs.append(entry) 118 else: 119 self.done = True 120 if this_path: 121 self.path = this_path 122 123def cat_to_tempfile(svnrepos, path, rev): 124 """Check out file revision to temporary file""" 125 fd, temp = tempfile.mkstemp() 126 fp = os.fdopen(fd, 'wb') 127 url = svnrepos._geturl(path) 128 client.svn_client_cat(fp, url, _rev2optrev(rev), 129 svnrepos.ctx) 130 fp.close() 131 return temp 132 133class SelfCleanFP: 134 def __init__(self, path): 135 self.readable = True 136 self._fp = open(path, 'rb') 137 self._path = path 138 self._eof = 0 139 140 def read(self, len=None): 141 if len: 142 chunk = self._fp.read(len) 143 else: 144 chunk = self._fp.read() 145 if chunk == b'': 146 self._eof = 1 147 return chunk 148 149 def readline(self): 150 chunk = self._fp.readline() 151 if chunk == b'': 152 self._eof = 1 153 return chunk 154 155 def readlines(self): 156 lines = self._fp.readlines() 157 self._eof = 1 158 return lines 159 160 def close(self): 161 self._fp.close() 162 if self._path: 163 try: 164 os.remove(self._path) 165 self._path = None 166 except OSError: 167 pass 168 169 def __del__(self): 170 self.close() 171 172 def eof(self): 173 return self._eof 174 175 176class RemoteSubversionRepository(vclib.Repository): 177 def __init__(self, name, rootpath, authorizer, utilities, config_dir, 178 encoding): 179 self.name = name 180 self.rootpath = rootpath 181 self.auth = authorizer 182 self.diff_cmd = utilities.diff or 'diff' 183 self.config_dir = config_dir or None 184 self.encoding = encoding 185 186 # See if this repository is even viewable, authz-wise. 187 if not vclib.check_root_access(self): 188 raise vclib.ReposNotFound(name) 189 190 def open(self): 191 # Setup the client context baton, complete with non-prompting authstuffs. 192 self.ctx = setup_client_ctx(self.config_dir) 193 194 ra_callbacks = ra.svn_ra_callbacks_t() 195 ra_callbacks.auth_baton = self.ctx.auth_baton 196 self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None, 197 self.ctx.config) 198 self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session) 199 self._dirent_cache = { } 200 self._revinfo_cache = { } 201 202 # See if a universal read access determination can be made. 203 if self.auth and self.auth.check_universal_access(self.name) == 1: 204 self.auth = None 205 206 def rootname(self): 207 return self.name 208 209 def rootpath(self): 210 return self.rootpath 211 212 def roottype(self): 213 return vclib.SVN 214 215 def authorizer(self): 216 return self.auth 217 218 def itemtype(self, path_parts, rev): 219 pathtype = None 220 if not len(path_parts): 221 pathtype = vclib.DIR 222 else: 223 path = self._getpath(path_parts) 224 rev = self._getrev(rev) 225 try: 226 kind = ra.svn_ra_check_path(self.ra_session, path, rev) 227 if kind == core.svn_node_file: 228 pathtype = vclib.FILE 229 elif kind == core.svn_node_dir: 230 pathtype = vclib.DIR 231 except: 232 pass 233 if pathtype is None: 234 raise vclib.ItemNotFound(path_parts) 235 if not vclib.check_path_access(self, path_parts, pathtype, rev): 236 raise vclib.ItemNotFound(path_parts) 237 return pathtype 238 239 def openfile(self, path_parts, rev, options): 240 path = self._getpath(path_parts) 241 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check 242 raise vclib.Error("Path '%s' is not a file." % path) 243 rev = self._getrev(rev) 244 url = self._geturl(path) 245 ### rev here should be the last history revision of the URL 246 fp = SelfCleanFP(cat_to_tempfile(self, path, rev)) 247 lh_rev, c_rev = self._get_last_history_rev(path_parts, rev) 248 return fp, lh_rev 249 250 def listdir(self, path_parts, rev, options): 251 path = self._getpath(path_parts) 252 if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check 253 raise vclib.Error("Path '%s' is not a directory." % path) 254 rev = self._getrev(rev) 255 entries = [] 256 dirents, locks = self._get_dirents(path, rev) 257 for name in dirents.keys(): 258 entry = dirents[name] 259 if entry.kind == core.svn_node_dir: 260 kind = vclib.DIR 261 elif entry.kind == core.svn_node_file: 262 kind = vclib.FILE 263 else: 264 kind = None 265 entries.append(vclib.DirEntry(name, kind)) 266 return entries 267 268 def dirlogs(self, path_parts, rev, entries, options): 269 path = self._getpath(path_parts) 270 if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check 271 raise vclib.Error("Path '%s' is not a directory." % path) 272 rev = self._getrev(rev) 273 dirents, locks = self._get_dirents(path, rev) 274 for entry in entries: 275 entry_path_parts = path_parts + [entry.name] 276 dirent = dirents.get(entry.name, None) 277 # dirents is authz-sanitized, so ensure the entry is found therein. 278 if dirent is None: 279 continue 280 # Get authz-sanitized revision metadata. 281 entry.date, entry.author, entry.log, revprops, changes = \ 282 self._revinfo(dirent.created_rev) 283 entry.rev = str(dirent.created_rev) 284 entry.size = dirent.size 285 entry.lockinfo = None 286 if entry.name in locks: 287 entry.lockinfo = locks[entry.name].owner 288 289 def itemlog(self, path_parts, rev, sortby, first, limit, options): 290 assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV 291 path_type = self.itemtype(path_parts, rev) # does auth-check 292 path = self._getpath(path_parts) 293 rev = self._getrev(rev) 294 url = self._geturl(path) 295 296 # If this is a file, fetch the lock status and size (as of REV) 297 # for this item. 298 lockinfo = size_in_rev = None 299 if path_type == vclib.FILE: 300 basename = path_parts[-1].encode(self.encoding, 'surrogateescape') 301 list_url = self._geturl(self._getpath(path_parts[:-1])) 302 dirents, locks = client.svn_client_ls3(list_url, _rev2optrev(rev), 303 _rev2optrev(rev), 0, self.ctx) 304 if basename in locks: 305 lockinfo = locks[basename].owner 306 if basename in dirents: 307 size_in_rev = dirents[basename].size 308 309 # Special handling for the 'svn_latest_log' scenario. 310 ### FIXME: Don't like this hack. We should just introduce 311 ### something more direct in the vclib API. 312 if options.get('svn_latest_log', 0): 313 dir_lh_rev, dir_c_rev = self._get_last_history_rev(path_parts, rev) 314 date, author, log, revprops, changes = self._revinfo(dir_lh_rev) 315 return [vclib.Revision(dir_lh_rev, str(dir_lh_rev), date, 316 author, None, log, size_in_rev, lockinfo)] 317 318 def _access_checker(check_path, check_rev): 319 return vclib.check_path_access(self, _path_parts(check_path), 320 path_type, check_rev) 321 322 # It's okay if we're told to not show all logs on a file -- all 323 # the revisions should match correctly anyway. 324 lc = LogCollector(path, options.get('svn_show_all_dir_logs', 0), 325 lockinfo, _access_checker) 326 327 cross_copies = options.get('svn_cross_copies', 0) 328 log_limit = 0 329 if limit: 330 log_limit = first + limit 331 client_log(url, _rev2optrev(rev), _rev2optrev(1), log_limit, 1, 332 cross_copies, lc.add_log, self.ctx) 333 revs = lc.logs 334 revs.sort() 335 prev = None 336 for rev in revs: 337 # Swap out revision info with stuff from the cache (which is 338 # authz-sanitized). 339 rev.date, rev.author, rev.log, revprops, changes \ 340 = self._revinfo(rev.number) 341 rev.prev = prev 342 prev = rev 343 revs.reverse() 344 345 if len(revs) < first: 346 return [] 347 if limit: 348 return revs[first:first+limit] 349 return revs 350 351 def itemprops(self, path_parts, rev): 352 path = self._getpath(path_parts) 353 path_type = self.itemtype(path_parts, rev) # does auth-check 354 rev = self._getrev(rev) 355 url = self._geturl(path) 356 pairs = client.svn_client_proplist2(url, _rev2optrev(rev), 357 _rev2optrev(rev), 0, self.ctx) 358 propdict = {} 359 if pairs: 360 for pname in pairs[0][1].keys(): 361 pvalue = pairs[0][1][pname] 362 pname, pvalue = _normalize_property(pname, pvalue, self.encoding) 363 if pname: 364 propdict[pname] = pvalue 365 return propdict 366 367 def annotate(self, path_parts, rev, include_text=False): 368 path = self._getpath(path_parts) 369 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check 370 raise vclib.Error("Path '%s' is not a file." % path) 371 rev = self._getrev(rev) 372 url = self._geturl(path) 373 374 # Examine logs for the file to determine the oldest revision we are 375 # permitted to see. 376 log_options = { 377 'svn_cross_copies' : 1, 378 'svn_show_all_dir_logs' : 1, 379 } 380 revs = self.itemlog(path_parts, rev, vclib.SORTBY_REV, 0, 0, log_options) 381 oldest_rev = revs[-1].number 382 383 # Now calculate the annotation data. Note that we'll not 384 # inherently trust the provided author and date, because authz 385 # rules might necessitate that we strip that information out. 386 blame_data = [] 387 388 def _blame_cb(line_no, revision, author, date, 389 line, pool, blame_data=blame_data): 390 prev_rev = None 391 if revision > 1: 392 prev_rev = revision - 1 393 394 # If we have an invalid revision, clear the date and author 395 # values. Otherwise, if we have authz filtering to do, use the 396 # revinfo cache to do so. 397 if revision < 0: 398 date = author = None 399 elif self.auth: 400 date, author, msg, revprops, changes = self._revinfo(revision) 401 else: 402 author = _normalize_property_value(author, self.encoding) 403 404 # Strip text if the caller doesn't want it. 405 if not include_text: 406 line = None 407 blame_data.append(vclib.Annotation(line, line_no + 1, revision, prev_rev, 408 author, date)) 409 410 client.blame2(url, _rev2optrev(rev), _rev2optrev(oldest_rev), 411 _rev2optrev(rev), _blame_cb, self.ctx) 412 return blame_data, rev 413 414 def revinfo(self, rev): 415 return self._revinfo(rev, 1) 416 417 def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): 418 p1 = self._getpath(path_parts1) 419 p2 = self._getpath(path_parts2) 420 r1 = self._getrev(rev1) 421 r2 = self._getrev(rev2) 422 if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1): 423 raise vclib.ItemNotFound(path_parts1) 424 if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2): 425 raise vclib.ItemNotFound(path_parts2) 426 427 args = vclib._diff_args(type, options) 428 429 def _date_from_rev(rev): 430 date, author, msg, revprops, changes = self._revinfo(rev) 431 return date 432 433 try: 434 temp1 = cat_to_tempfile(self, p1, r1) 435 temp2 = cat_to_tempfile(self, p2, r2) 436 info1 = p1, _date_from_rev(r1), r1 437 info2 = p2, _date_from_rev(r2), r2 438 return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args) 439 except core.SubversionException as e: 440 if e.apr_err == vclib.svn.core.SVN_ERR_FS_NOT_FOUND: 441 raise vclib.InvalidRevision 442 raise 443 444 def isexecutable(self, path_parts, rev): 445 props = self.itemprops(path_parts, rev) # does authz-check 446 return core.SVN_PROP_EXECUTABLE in props 447 448 def filesize(self, path_parts, rev): 449 path = self._getpath(path_parts) 450 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check 451 raise vclib.Error("Path '%s' is not a file." % path) 452 rev = self._getrev(rev) 453 dirents, locks = self._get_dirents(self._getpath(path_parts[:-1]), rev) 454 dirent = dirents.get(path_parts[-1], None) 455 return dirent.size 456 457 def _getpath(self, path_parts): 458 return '/'.join(path_parts) 459 460 def _getrev(self, rev): 461 if rev is None or rev == 'HEAD': 462 return self.youngest 463 try: 464 if type(rev) == type(''): 465 while rev[0] == 'r': 466 rev = rev[1:] 467 rev = int(rev) 468 except: 469 raise vclib.InvalidRevision(rev) 470 if (rev < 0) or (rev > self.youngest): 471 raise vclib.InvalidRevision(rev) 472 return rev 473 474 def _geturl(self, path=None): 475 if not path: 476 return self.rootpath 477 path = self.rootpath + '/' + _quote(path) 478 return core.svn_path_canonicalize(path) 479 480 def _get_dirents(self, path, rev): 481 """Return a 2-type of dirents and locks, possibly reading/writing 482 from a local cache of that information. This functions performs 483 authz checks, stripping out unreadable dirents.""" 484 485 dir_url = self._geturl(path) 486 path_parts = _path_parts(path) 487 if path: 488 key = str(rev) + '/' + path 489 else: 490 key = str(rev) 491 492 # Ensure that the cache gets filled... 493 dirents_locks = self._dirent_cache.get(key) 494 if not dirents_locks: 495 tmp_dirents, locks = client.svn_client_ls3(dir_url, _rev2optrev(rev), 496 _rev2optrev(rev), 0, self.ctx) 497 dirents = {} 498 for name, dirent in tmp_dirents.items(): 499 dirent_parts = path_parts + [_to_str(name)] 500 kind = dirent.kind 501 if (kind == core.svn_node_dir or kind == core.svn_node_file) \ 502 and vclib.check_path_access(self, dirent_parts, 503 kind == core.svn_node_dir \ 504 and vclib.DIR or vclib.FILE, rev): 505 lh_rev, c_rev = self._get_last_history_rev(dirent_parts, rev) 506 dirent.created_rev = lh_rev 507 dirents[_to_str(name)] = dirent 508 dirents_locks = [dirents, locks] 509 self._dirent_cache[key] = dirents_locks 510 511 # ...then return the goodies from the cache. 512 return dirents_locks[0], dirents_locks[1] 513 514 def _get_last_history_rev(self, path_parts, rev): 515 """Return the a 2-tuple which contains: 516 - the last interesting revision equal to or older than REV in 517 the history of PATH_PARTS. 518 - the created_rev of of PATH_PARTS as of REV.""" 519 520 path = self._getpath(path_parts) 521 url = self._geturl(self._getpath(path_parts)) 522 optrev = _rev2optrev(rev) 523 524 # Get the last-changed-rev. 525 revisions = [] 526 def _info_cb(path, info, pool, retval=revisions): 527 revisions.append(info.last_changed_rev) 528 client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx) 529 last_changed_rev = revisions[0] 530 531 # Now, this object might not have been directly edited since the 532 # last-changed-rev, but it might have been the child of a copy. 533 # To determine this, we'll run a potentially no-op log between 534 # LAST_CHANGED_REV and REV. 535 lc = LogCollector(path, 1, None, None) 536 client_log(url, optrev, _rev2optrev(last_changed_rev), 1, 1, 0, 537 lc.add_log, self.ctx) 538 revs = lc.logs 539 if revs: 540 revs.sort() 541 return revs[0].number, last_changed_rev 542 else: 543 return last_changed_rev, last_changed_rev 544 545 def _revinfo_fetch(self, rev, include_changed_paths=0): 546 need_changes = include_changed_paths or self.auth 547 revs = [] 548 549 def _log_cb(log_entry, pool, retval=revs): 550 # If Subversion happens to call us more than once, we choose not 551 # to care. 552 if retval: 553 return 554 555 revision = log_entry.revision 556 msg, author, date, revprops = _split_revprops(log_entry.revprops) 557 action_map = { 'D' : vclib.DELETED, 558 'A' : vclib.ADDED, 559 'R' : vclib.REPLACED, 560 'M' : vclib.MODIFIED, 561 } 562 563 # Easy out: if we won't use the changed-path info, just return a 564 # changes-less tuple. 565 if not need_changes: 566 return revs.append([date, author, msg, revprops, None]) 567 568 # Subversion 1.5 and earlier didn't offer the 'changed_paths2' 569 # hash, and in Subversion 1.6, it's offered but broken. 570 try: 571 changed_paths = log_entry.changed_paths2 572 paths = list((changed_paths or {}).keys()) 573 except: 574 changed_paths = log_entry.changed_paths 575 paths = list((changed_paths or {}).keys()) 576 paths.sort(key=functools.cmp_to_key(lambda a, b: _compare_paths(_to_str(a), _to_str(b)))) 577 578 # If we get this far, our caller needs changed-paths, or we need 579 # them for authz-related sanitization. 580 changes = [] 581 found_readable = found_unreadable = 0 582 for path in paths: 583 change = changed_paths[path] 584 585 # svn_log_changed_path_t (which we might get instead of the 586 # svn_log_changed_path2_t we'd prefer) doesn't have the 587 # 'node_kind' member. 588 pathtype = None 589 if hasattr(change, 'node_kind'): 590 if change.node_kind == core.svn_node_dir: 591 pathtype = vclib.DIR 592 elif change.node_kind == core.svn_node_file: 593 pathtype = vclib.FILE 594 595 # svn_log_changed_path2_t only has the 'text_modified' and 596 # 'props_modified' bits in Subversion 1.7 and beyond. And 597 # svn_log_changed_path_t is without. 598 text_modified = props_modified = 0 599 if hasattr(change, 'text_modified'): 600 if change.text_modified == core.svn_tristate_true: 601 text_modified = 1 602 if hasattr(change, 'props_modified'): 603 if change.props_modified == core.svn_tristate_true: 604 props_modified = 1 605 606 # Wrong, diddily wrong wrong wrong. Can you say, 607 # "Manufacturing data left and right because it hurts to 608 # figure out the right stuff?" 609 action = action_map.get(change.action, vclib.MODIFIED) 610 if change.copyfrom_path and change.copyfrom_rev: 611 is_copy = 1 612 base_path = change.copyfrom_path 613 base_rev = change.copyfrom_rev 614 elif action == vclib.ADDED or action == vclib.REPLACED: 615 is_copy = 0 616 base_path = base_rev = None 617 else: 618 is_copy = 0 619 base_path = path 620 base_rev = revision - 1 621 622 # Check authz rules (sadly, we have to lie about the path type) 623 parts = _path_parts(_to_str(path)) 624 if vclib.check_path_access(self, parts, vclib.FILE, revision): 625 if is_copy and base_path and (base_path != path): 626 parts = _path_parts(_to_str(base_path)) 627 if not vclib.check_path_access(self, parts, vclib.FILE, base_rev): 628 is_copy = 0 629 base_path = None 630 base_rev = None 631 found_unreadable = 1 632 changes.append(SVNChangedPath(_to_str(path), revision, pathtype, 633 _to_str(base_path), base_rev, 634 action, is_copy, 635 text_modified, props_modified)) 636 found_readable = 1 637 else: 638 found_unreadable = 1 639 640 # If our caller doesn't want changed-path stuff, and we have 641 # the info we need to make an authz determination already, 642 # quit this loop and get on with it. 643 if (not include_changed_paths) and found_unreadable and found_readable: 644 break 645 646 # Filter unreadable information. 647 if found_unreadable: 648 msg = None 649 if not found_readable: 650 author = None 651 date = None 652 653 # Drop unrequested changes. 654 if not include_changed_paths: 655 changes = None 656 657 # Add this revision information to the "return" array. 658 retval.append([date, author, msg, revprops, changes]) 659 660 optrev = _rev2optrev(rev) 661 client_log(self.rootpath, optrev, optrev, 1, need_changes, 0, 662 _log_cb, self.ctx) 663 return tuple(revs[0]) 664 665 def _revinfo(self, rev, include_changed_paths=0): 666 """Internal-use, cache-friendly revision information harvester.""" 667 668 # Consult the revinfo cache first. If we don't have cached info, 669 # or our caller wants changed paths and we don't have those for 670 # this revision, go do the real work. 671 rev = self._getrev(rev) 672 cached_info = self._revinfo_cache.get(rev) 673 if not cached_info \ 674 or (include_changed_paths and cached_info[4] is None): 675 cached_info = self._revinfo_fetch(rev, include_changed_paths) 676 self._revinfo_cache[rev] = cached_info 677 return cached_info 678 679 ##--- custom --## 680 681 def get_youngest_revision(self): 682 return self.youngest 683 684 def get_location(self, path, rev, old_rev): 685 try: 686 results = ra.get_locations(self.ra_session, path, rev, [old_rev]) 687 except core.SubversionException as e: 688 if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: 689 raise vclib.ItemNotFound(path) 690 raise 691 try: 692 old_path = results[old_rev] 693 except KeyError: 694 raise vclib.ItemNotFound(path) 695 old_path = _cleanup_path(_to_str(old_path)) 696 old_path_parts = _path_parts(old_path) 697 # Check access (lying about path types) 698 if not vclib.check_path_access(self, old_path_parts, vclib.FILE, old_rev): 699 raise vclib.ItemNotFound(path) 700 return old_path 701 702 def created_rev(self, path, rev): 703 lh_rev, c_rev = self._get_last_history_rev(_path_parts(path), rev) 704 return lh_rev 705 706 def last_rev(self, path, peg_revision, limit_revision=None): 707 """Given PATH, known to exist in PEG_REVISION, find the youngest 708 revision older than, or equal to, LIMIT_REVISION in which path 709 exists. Return that revision, and the path at which PATH exists in 710 that revision.""" 711 712 # Here's the plan, man. In the trivial case (where PEG_REVISION is 713 # the same as LIMIT_REVISION), this is a no-brainer. If 714 # LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's 715 # history tracing code to find the right location. If, however, 716 # LIMIT_REVISION is younger than PEG_REVISION, we suffer from 717 # Subversion's lack of forward history searching. Our workaround, 718 # ugly as it may be, involves a binary search through the revisions 719 # between PEG_REVISION and LIMIT_REVISION to find our last live 720 # revision. 721 peg_revision = self._getrev(peg_revision) 722 limit_revision = self._getrev(limit_revision) 723 if peg_revision == limit_revision: 724 return peg_revision, path 725 elif peg_revision > limit_revision: 726 path = self.get_location(path, peg_revision, limit_revision) 727 return limit_revision, path 728 else: 729 direction = 1 730 while peg_revision != limit_revision: 731 mid = (peg_revision + 1 + limit_revision) // 2 732 try: 733 path = self.get_location(path, peg_revision, mid) 734 except vclib.ItemNotFound: 735 limit_revision = mid - 1 736 else: 737 peg_revision = mid 738 return peg_revision, path 739 740 def get_symlink_target(self, path_parts, rev): 741 """Return the target of the symbolic link versioned at PATH_PARTS 742 in REV, or None if that object is not a symlink.""" 743 744 path = self._getpath(path_parts) 745 path_type = self.itemtype(path_parts, rev) # does auth-check 746 rev = self._getrev(rev) 747 url = self._geturl(path) 748 749 # Symlinks must be files with the svn:special property set on them 750 # and with file contents which read "link SOME_PATH". 751 if path_type != vclib.FILE: 752 return None 753 pairs = client.svn_client_proplist2(url, _rev2optrev(rev), 754 _rev2optrev(rev), 0, self.ctx) 755 props = pairs and pairs[0][1] or {} 756 if core.SVN_PROP_SPECIAL not in props: 757 return None 758 pathspec = '' 759 ### FIXME: We're being a touch sloppy here, first by grabbing the 760 ### whole file and then by checking only the first line 761 ### of it. 762 fp = SelfCleanFP(cat_to_tempfile(self, path, rev)) 763 pathspec = fp.readline() 764 fp.close() 765 if pathspec[:5] != 'link ': 766 return None 767 return pathspec[5:] 768 769