Changeset 2973 for perforceplugin/branches/trac-0.10
- Timestamp:
- 01/03/08 09:39:12 (11 months ago)
- Files:
-
- perforceplugin/branches/trac-0.10/p4trac/api.py (modified) (37 diffs)
- perforceplugin/branches/trac-0.10/p4trac/repos.py (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
perforceplugin/branches/trac-0.10/p4trac/api.py
r2503 r2973 1 1 from trac.core import Component, implements, TracError 2 from trac.versioncontrol.api import IRepositoryConnector, Repository, Node, \ 3 Changeset, Authorizer, NoSuchChangeset, NoSuchNode, PermissionDenied 4 from trac.versioncontrol.cache import CachedRepository, _kindmap, _actionmap 2 from trac.versioncontrol import Changeset, Repository, Node, \ 3 IRepositoryConnector, \ 4 Authorizer, NoSuchChangeset, NoSuchNode 5 from trac.versioncontrol.cache import CachedRepository 6 from trac.util.text import to_unicode 7 5 8 6 9 def normalisePath(path): … … 36 39 else: 37 40 return rev 41 38 42 39 43 class PerforceConnector(Component): … … 155 159 p4.client = '' 156 160 157 repos = PerforceRepository(p4, self.log, jobPrefixLength) 158 159 from trac.versioncontrol.cache import CachedRepository 160 return PerforceCachedRepository(self.env.get_db_cnx(), 161 repos, 162 None, 163 self.log) 164 165 class PerforceCachedRepository(CachedRepository): 166 167 def __init__(self, db, repos, authz, log): 168 CachedRepository.__init__(self, db, repos, authz, log) 169 self.synced = 0 170 171 def checkRepositoryDir(self): 172 """Check that the underlying repository_dir hasn't changed.""" 173 cursor = self.db.cursor() 174 cursor.execute("SELECT value " 175 "FROM system " 176 "WHERE name='repository_dir'") 177 row = cursor.fetchone() 178 if row and row[0] != self.name: 179 raise TracError("The 'repository_dir' has changed " 180 "a 'trac-admin resync' operation is needed") 181 182 def storeChangesInDB(self, changes): 183 """Store the specified changes in the Trac database. 184 185 @param changes: List of integers that specifies the changes to store 186 in the Trac database. 187 """ 188 kindmap = dict(zip(_kindmap.values(), _kindmap.keys())) 189 actionmap = dict(zip(_actionmap.values(), _actionmap.keys())) 190 191 cursor = self.db.cursor() 192 for change in changes: 193 cs = self.repos.get_changeset(change) 194 cursor.execute("INSERT INTO revision (rev,time,author,message) " 195 "VALUES (%s,%s,%s,%s)", (str(change), 196 cs.date, 197 cs.author, 198 cs.message)) 199 for path, kind, action, base_path, base_rev in cs.get_changes(): 200 kind = kindmap[kind] 201 action = actionmap[action] 202 cursor.execute("INSERT INTO node_change (rev,path, node_type, " 203 "change_type, base_path, base_rev) " 204 "VALUES (%s,%s,%s,%s,%s,%s)", 205 (str(change), 206 path, kind, action, base_path, base_rev)) 207 self.db.commit() 208 209 def updateCache(self, fromChange): 210 211 from perforce import ConnectionDropped 212 213 # Update the database in batches of 1000 changes so that we don't 214 # overload the virtual memory system by trying to store information 215 # about every change in the repository at once during the initial 216 # cache population. 217 218 batchSize = 1000 219 lowerBound = fromChange 220 upperBound = self.repos.youngest_rev + 1 221 222 self.log.debug("Updating cache with changes [%i,%i]" % (lowerBound, 223 upperBound)) 224 225 try: 226 while lowerBound < upperBound: 227 batchUpperBound = min(lowerBound + batchSize, upperBound) 228 229 # Get the next batch of changes to cache 230 from p4trac.repos import _P4ChangesOutputConsumer 231 output = _P4ChangesOutputConsumer(self.repos._repos) 232 self.repos._connection.run('changes', '-l', '-s', 'submitted', 233 '@>=%i,@<%i' % (lowerBound, 234 batchUpperBound), 235 output=output) 236 237 if output.errors: 238 from p4trac.repos import PerforceError 239 raise PerforceError(output.errors) 240 241 changes = output.changes 242 changes.reverse() 243 244 # Pre-cache all information about these changes in memory 245 # before caching in the database. Clear the in-memory cache 246 # afterwards to save on memory usage. 247 self.repos._repos.precacheFileInformationForChanges(changes) 248 self.storeChangesInDB(changes) 249 self.repos._repos.clearFileInformationCache() 250 251 lowerBound += batchSize 252 253 except ConnectionDropped, e: 254 self.log.debug('Rolling back uncommitted cache updates') 255 self.db.rollback() 256 raise TracError('Connection to Perforce server lost') 257 258 def sync(self): 259 260 self.log.debug("Checking whether sync with repository is needed") 261 262 self.checkRepositoryDir() 263 264 youngestStored = self.repos.get_youngest_rev_in_cache(self.db) 265 if youngestStored is None: 266 youngestStored = 0 267 else: 268 youngestStored = int(youngestStored) 269 270 if youngestStored != self.repos.youngest_rev: 271 # Cache is out of date. 272 273 # Remove permissions checking while populating the cache 274 authz = self.repos.authz 275 self.repos.authz = Authorizer() 276 try: 277 self.updateCache(fromChange=youngestStored+1) 278 finally: 279 self.repos.authz = authz 280 281 self.youngest = youngestStored 282 283 def get_changesets(self, start, stop): 284 if not self.synced: 285 self.sync() 286 self.synced = 1 287 cursor = self.db.cursor() 288 cursor.execute("SELECT rev " 289 "FROM revision " 290 "WHERE time >= %i AND time <= %i " 291 "ORDER BY time DESC" % (int(start), int(stop))) 292 for row in cursor: 293 yield self.get_changeset(row[0]) 294 295 # HACK: This method should be in the base class. 296 def get_tags(self, rev): 297 return self.repos.get_tags(rev) 298 299 class PerforceRepository(object): 161 repos = PerforceRepository(p4, None, self.log, jobPrefixLength) 162 crepos = CachedRepository(self.env.get_db_cnx(), repos, None, self.log) 163 return crepos 164 165 166 class PerforceRepository(Repository): 300 167 """A Perforce repository implementation. 301 168 … … 304 171 """ 305 172 306 def __init__(self, connection, log, jobPrefixLength): 307 308 self.authz = None 309 310 # length of the job prefix used with PerforceJobScript 173 def __init__(self, connection, authz, log, jobPrefixLength): 311 174 self._job_prefix_length = jobPrefixLength 312 313 # Log object for logging output314 self._log = log315 316 # The connection to the Perforce server317 175 self._connection = connection 318 176 name = 'p4://%s:%s@%s' % (self._connection.user, self._connection.password, self._connection.port) 177 Repository.__init__(self, name, None, log) 319 178 # The Repository object that we query for Perforce info 320 from p4trac.repos import Repository 321 self._repos = Repository(connection) 322 323 def get_name(self): 324 return 'p4://%s:%s@%s' % (self._connection.user, self._connection.password, self._connection.port) 325 name = property(get_name) 179 from p4trac.repos import P4Repository 180 self._repos = P4Repository(connection) 181 182 def __del__(self): 183 self.close() 184 185 def clear(self, youngest_rev=None): 186 self.youngest = None 187 if youngest_rev is not None: 188 self.youngest = self.normalize_rev(youngest_rev) 189 self.oldest = None 326 190 327 191 def close(self): … … 329 193 330 194 def get_tags(self, rev): 331 332 195 results = self._connection.run('labels') 333 334 196 if results.errors: 335 197 from p4trac.repos import PerforceError 336 198 raise PerforceError(results.errors) 337 338 199 for rec in results.records: 339 200 name = self._repos.toUnicode(rec['label']) 340 201 yield (name, u'@%s' % name) 341 202 342 343 203 def get_branches(self, rev): 344 204 # TODO: Generate a list of branches … … 346 206 347 207 def get_changeset(self, rev): 348 349 self._log.debug('get_changeset(%r)' % rev) 350 208 self.log.debug('get_changeset(%r)' % rev) 351 209 if isinstance(rev, int): 352 210 change = rev … … 354 212 from p4trac.util import toUnicode 355 213 rev = toUnicode(rev) 356 357 214 if rev.startswith(u'@'): 358 215 rev = rev[1:] 359 216 360 217 try: 361 218 change = int(rev) 362 219 except ValueError: 363 220 raise TracError(u"Invalid changeset number '%s'" % rev) 364 365 return PerforceChangeset(change, self._repos, self. _log, self._job_prefix_length)221 222 return PerforceChangeset(change, self._repos, self.log, self._job_prefix_length) 366 223 367 224 def get_changesets(self, start, stop): 368 369 self._log.debug('PerforceRepository.get_changesets(%r,%r)' % (start, 370 stop)) 225 self.log.debug('PerforceRepository.get_changesets(%r,%r)' % (start, stop)) 371 226 372 227 import datetime … … 382 237 '@>=%s,@<=%s' % (startDate, stopDate), 383 238 output=output) 384 385 239 if output.errors: 386 240 from p4trac.repos import PerforceError … … 396 250 397 251 def get_node(self, path, rev=None): 398 self._log.debug('get_node(%s, %s) called' % (path, rev)) 399 252 self.log.debug('get_node(%s, %s) called' % (path, rev)) 400 253 from p4trac.repos import NodePath 401 254 nodePath = NodePath(NodePath.normalisePath(path), rev) 402 403 return PerforceNode(nodePath, self._repos, self._log) 255 return PerforceNode(nodePath, self._repos, self.log) 404 256 405 257 def get_oldest_rev(self): 406 258 return self.next_rev(0) 407 oldest_rev = property(fget=get_oldest_rev)408 259 409 260 def get_youngest_rev(self): 410 261 return self._repos.getLatestChange() 411 youngest_rev = property(fget=get_youngest_rev)412 262 413 263 def previous_rev(self, rev): 414 415 self._log.debug('previous_rev(%r)' % rev) 416 264 self.log.debug('previous_rev(%r)' % rev) 417 265 if not isinstance(rev, int): 418 266 rev = self.short_rev(rev) … … 423 271 output = _P4ChangesOutputConsumer(self._repos) 424 272 self._connection.run('changes', '-l', '-s', 'submitted', 425 '-m', '1', 426 '@<%i' % rev, 427 output=output) 428 273 '-m', '1', '@<%i' % rev, output=output) 429 274 if output.errors: 430 275 from p4trac.repos import PerforcError 431 276 raise PerforcError(output.errors) 432 433 277 if output.changes: 434 278 return max(output.changes) … … 437 281 438 282 def next_rev(self, rev, path=''): 439 440 283 # Finding the next revision is a little more difficult in Perforce 441 284 # as we can only ask for the n most recent changes according to a … … 445 288 # it is still fairly efficient if the next change is 1 or 1000 changes 446 289 # later. 447 448 self._log.debug('next_rev(%r,%r)' % (rev, path)) 290 self.log.debug('next_rev(%r,%r)' % (rev, path)) 449 291 450 292 from p4trac.repos import NodePath … … 466 308 467 309 queryPath = self._repos.fromUnicode(queryPath) 468 469 self._log.debug( 470 u'Looing for next_rev after change %i for %s' % (rev, path)) 310 self.log.debug(u'Looing for next_rev after change %i for %s' % (rev, path)) 471 311 472 312 # Perform a binary-search of sorts for the next revision … … 476 316 477 317 while lowerBound <= upperBound: 478 479 318 if lowerBound + batchSize > upperBound: 480 319 batchUpperBound = upperBound … … 485 324 else: 486 325 batchUpperBound = middle 487 488 self._log.debug( 326 self.log.debug( 489 327 'Looking for changes in range [%i, %i]' % (lowerBound, 490 328 batchUpperBound)) … … 498 336 batchUpperBound), 499 337 output=output) 500 501 338 if output.errors: 502 339 from p4trac.repos import PerforcError … … 510 347 if lowerBound + batchSize >= batchUpperBound: 511 348 # There are no earlier changes 512 self. _log.debug('next_rev is %i' % lowest)349 self.log.debug('next_rev is %i' % lowest) 513 350 return lowest 514 351 else: … … 520 357 # Try searching from batchUpperBound + 1 onwards 521 358 lowerBound = batchUpperBound + 1 522 523 359 return None 524 360 525 361 def rev_older_than(self, rev1, rev2): 526 527 self._log.debug('PerforceRepository.rev_older_than(%r,%r)' % (rev1, 528 rev2)) 529 362 self.log.debug('PerforceRepository.rev_older_than(%r,%r)' % (rev1, rev2)) 363 530 364 rev1 = self.short_rev(rev1) 531 365 rev2 = self.short_rev(rev2) 532 533 366 # Can compare equal revisions directly 534 367 if rev1 == rev2: … … 540 373 541 374 def parseDateRevision(rev): 542 543 375 if not isinstance(rev, unicode): 544 376 raise ValueError 545 546 377 if not rev.startswith(u'@'): 547 378 raise ValueError 548 549 379 # @YYYY/MM/DD[:HH:MM:SS] 550 380 if len(rev) not in [11, 20]: 551 381 raise ValueError 552 553 382 year = int(rev[1:5]) 554 383 month = int(rev[6:8]) … … 569 398 minute = 0 570 399 second = 0 571 572 400 return (year, month, day, hour, minute, second) 573 401 … … 581 409 # Compare based on the latest change number that affects this revision. 582 410 from p4trac.repos import NodePath 583 584 411 if not isinstance(rev1, int): 585 412 rootAtRev1 = NodePath(u'//', rev1) 586 413 rev1 = self._repos.getNode(rootAtRev1).change 587 588 414 if not isinstance(rev2, int): 589 415 rootAtRev2 = NodePath(u'//', rev2) 590 416 rev2 = self._repos.getNode(rootAtRev2).change 591 592 self._log.debug('Comparing by change rev1=%i, rev2=%i' % (rev1, rev2)) 593 417 self.log.debug('Comparing by change rev1=%i, rev2=%i' % (rev1, rev2)) 594 418 return rev1 < rev2 595 596 def get_youngest_rev_in_cache(self, db):597 598 cursor = db.cursor()599 cursor.execute("SELECT r.rev "600 "FROM revision r "601 "ORDER BY r.time DESC "602 "LIMIT 1")603 row = cursor.fetchone()604 return row and row[0] or None605 419 606 420 def get_path_history(self, path, rev=None, limit=None): … … 609 423 from p4trac.repos import NodePath 610 424 nodePath = NodePath(NodePath.normalisePath(path), rev) 611 node = PerforceNode(nodePath, self._repos, self. _log)425 node = PerforceNode(nodePath, self._repos, self.log) 612 426 return node.get_history(limit) 613 427 614 428 def normalize_path(self, path): 615 self. _log.debug('normalize_path(%r)' % path)429 self.log.debug('normalize_path(%r)' % path) 616 430 return normalisePath(path) 617 431 618 432 def normalize_rev(self, rev): 619 self. _log.debug('normalize_rev(%r)' % rev)433 self.log.debug('normalize_rev(%r)' % rev) 620 434 rev = normaliseRev(rev) 621 435 if rev is None: … … 625 439 626 440 def short_rev(self, rev): 627 self. _log.debug('short_rev(%r)' % rev)441 self.log.debug('short_rev(%r)' % rev) 628 442 return self.normalize_rev(rev) 629 443 630 def get_changes(self, old_path, old_rev, new_path, new_rev, 631 ignore_ancestry=1): 632 633 self._log.debug('PerforceRepository.get_changes(%r,%r,%r,%r)' % ( 444 def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 445 self.log.debug('PerforceRepository.get_changes(%r,%r,%r,%r)' % ( 634 446 old_path, old_rev, new_path, new_rev)) 635 447 … … 641 453 newNode = self._repos.getNode(newNodePath) 642 454 643 644 455 if (newNode.isFile and oldNode.isDirectory) or \ 645 456 (newNode.isDirectory and oldNode.isFile): … … 647 458 648 459 if newNode.isDirectory or oldNode.isDirectory: 649 650 460 if oldNodePath.isRoot: 651 461 oldQueryPath = u'//...%s' % oldNodePath.rev … … 659 469 newQueryPath = u'%s/...%s' % (newNodePath.path, 660 470 newNodePath.rev) 661 662 471 elif newNode.isFile or oldNode.isFile: 663 664 472 oldQueryPath = oldNodePath.fullPath 665 473 newQueryPath = newNodePath.fullPath 666 667 474 else: 668 475 raise TracError("Cannot diff two non-existant nodes") … … 671 478 output = _P4Diff2OutputConsumer(self._repos) 672 479 673 self._connection.run( 674 'diff2', '-ds', 675 self._repos.fromUnicode(oldQueryPath), 676 self._repos.fromUnicode(newQueryPath), 677 output=output) 678 480 self._connection.run('diff2', '-ds', 481 self._repos.fromUnicode(oldQueryPath), 482 self._repos.fromUnicode(newQueryPath), 483 output=output) 679 484 if output.errors: 680 485 from p4trac.repos import PerforceError … … 682 487 683 488 for change in output.changes: 684 685 489 oldFileNodePath, newFileNodePath = change 686 687 490 if oldFileNodePath is not None: 688 491 oldFileNode = PerforceNode(oldFileNodePath, 689 492 self._repos, 690 self. _log)493 self.log) 691 494 else: 692 495 oldFileNode = None … … 695 498 newFileNode = PerforceNode(newFileNodePath, 696 499 self._repos, 697 self. _log)500 self.log) 698 501 else: 699 502 newFileNode = None 700 503 701 504 if newFileNode and oldFileNode: 702 yield (oldFileNode, 703 newFileNode, 704 Node.FILE, 705 Changeset.EDIT) 505 yield (oldFileNode, newFileNode, Node.FILE, Changeset.EDIT) 706 506 elif newFileNode: 707 yield (oldFileNode, 708 newFileNode, 709 Node.FILE, 710 Changeset.ADD) 507 yield (oldFileNode, newFileNode, Node.FILE, Changeset.ADD) 711 508 elif oldFileNode: 712 yield (oldFileNode, 713 newFileNode, 714 Node.FILE, 715 Changeset.DELETE) 716 717 class PerforceNode(object): 509 yield (oldFileNode, newFileNode, Node.FILE, Changeset.DELETE) 510 511 512 class PerforceNode(Node): 718 513 """A Perforce repository node (depot, directory or file)""" 719 514 720 515 def __init__(self, nodePath, repos, log): 721 722 516 log.debug('Created PerforceNode for %r' % nodePath) 723 517 self._log = log 724 518 self._repos = repos 725 519 self._nodePath = nodePath 726 self._log = log727 520 self._node = self._repos.getNode(nodePath) 521 node_type = self._get_kind() 522 self.created_rev = self._node.change 523 self.created_path = normalisePath(self._nodePath.path) 524 self.rev = self.created_rev 525 Node.__init__(self, normalisePath(self._nodePath.path), self.rev, node_type) 728 526 729 527 def _get_kind(self): 730 731 528 if self._node.isDirectory: 732 529 return Node.DIRECTORY … … 737 534 self._nodePath.rev) 738 535 739 kind = property(fget=_get_kind)740 isdir = property(fget=lambda self: self.kind == Node.DIRECTORY)741 742 def _get_path(self):743 self._log.debug('PerforceNode.path')744 return normalisePath(self._nodePath.path)745 path = property(_get_path)746 747 def _get_rev(self):748 self._log.debug('PerforceNode.rev')749 return self._node.change750 rev = property(fget=_get_rev)751 752 def _get_created_path(self):753 self._log.debug('PerforceNode.created_path')754 # HACK: When should this be different to self.path?755 return self.path756 created_path = property(fget=_get_created_path)757 758 def _get_created_rev(self):759 self._log.debug('PerforceNode.created_rev')760 # HACK: When should this be different to self.rev?761 return self._node.change762 created_rev = property(_get_created_rev)763 764 536 def get_content(self): 765 537 self._log.debug('PerforceNode.get_content()') … … 821 593 if currentNode.fileRevision > 1: 822 594 # Get the previous revision 823 nodePath = NodePath( 824 currentNode.nodePath.path, 825 '#%i' % (currentNode.fileRevision - 1)) 595 nodePath = NodePath(currentNode.nodePath.path, 596 '#%i' % (currentNode.fileRevision - 1)) 826 597 currentNode = self._repos.getNode(nodePath) 827 598 else: … … 882 653 queryPath = '//...%s' % self._nodePath.rev 883 654 else: 884 queryPath = '%s/...%s' % (self._nodePath.path, 885 self._nodePath.rev) 655 queryPath = '%s/...%s' % (self._nodePath.path, self._nodePath.rev) 886 656 887 657 if limit is None: … … 943 713 else: 944 714 raise NoSuchNode(self._nodePath.path, self._nodePath.rev) 945 946 def get_previous(self):947 self._log.debug('PerforceNode.get_previous')948 skip = True949 for p in self.get_history(2):950 if skip:951 skip = False952 else:953 return p954 715 955 716 def get_properties(self): … … 971 732 else: 972 733 return self._node.fileSize 973 content_length = property(fget=get_content_length)974 734 975 735 def get_content_type(self): … … 978 738 return self._node.attributes[u'mime-type'] 979 739 return None 980 content_type = property(fget=get_content_type)981 982 def get_name(self):983 return self._nodePath.leaf984 name = property(fget=get_name)985 740 986 741 def get_last_modified(self): 987 742 return self._repos.getChangelist(self._node.change).time 988 last_modified = property(fget=get_last_modified) 989 990 class PerforceChangeset( object):743 744 745 class PerforceChangeset(Changeset): 991 746 """A Perforce repository changelist""" 992 747 … … 999 754 self._log = log 1000 755 self._changelist = self._repos.getChangelist(self._change) 1001 1002 def _get_message(self): 1003 import p4trac.repos 1004 try: 1005 return self._changelist.description 1006 except p4trac.repos.NoSuchChangelist, e: 1007 raise NoSuchChangeset(e.change) 1008 message = property(fget=_get_message) 1009 1010 def _get_date(self): 1011 import p4trac.repos 1012 try: 1013 return self._changelist.time 1014 except p4trac.repos.NoSuchChangelist, e: 1015 raise NoSuchChangeset(e.change) 1016 date = property(fget=_get_date) 1017 1018 def _get_author(self): 1019 import p4trac.repos 1020 try: 1021 return self._changelist.user 1022 except p4trac.repos.NoSuchChangelist, e: 1023 raise NoSuchChangeset(e.change) 1024 author = property(fget=_get_author) 1025 1026 def _get_rev(self): 1027 import p4trac.repos 1028 try: 1029 return self._change 1030 except p4trac.repos.NoSuchChangelist, e: 1031 raise NoSuchChangeset(e.change) 1032 rev = property(_get_rev) 756 Changeset.__init__(self, self._change, self._changelist.description, 757 self._changelist.user, self._changelist.time) 1033 758 1034 759 def get_properties(self): 760 self._log.debug('PerforceChangeset.get_properties()') 1035 761 import p4trac.repos 1036 762 try: … … 1052 778 1053 779 def get_changes(self): 1054 1055 780 self._log.debug('PerforceChangeset.get_changes()') 1056 1057 781 # Force population of the file history for the files modified in this 1058 782 # changelist. 1059 1060 self._log.debug("PerforceChangeset(%i).get_changes()" % 1061 self._change) 1062 783 self._log.debug("PerforceChangeset(%i).get_changes()" % self._change) 1063 784 self._repos.precacheFileInformationForChanges([self._change]) 1064 785 1065 786 for node in self._changelist.nodes: 1066 1067 787 nodePath = node.nodePath 1068 1069 788 self._log.debug('Change %i contains %s%s [%s]' % (self._change, 1070 789 nodePath.path, perforceplugin/branches/trac-0.10/p4trac/repos.py
r2226 r2973 7 7 from perforce.results import IOutputConsumer 8 8 from p4trac.util import AutoAttributesMeta 9 9 10 10 11 class _ChangeInfo(object): … … 31 32 self.status = None 32 33 self.files = None 34 33 35 34 36 class _FileInfo(object): … … 61 63 self.attributes = None 62 64 65 63 66 class _DirectoryInfo(object): 64 67 """A data structure for recording info about a directory. … … 81 84 self.change = None 82 85 86 83 87 class PerforceError(Exception): 84 88 … … 89 93 return '\n'.join([e.format() for e in self.errors]) 90 94 95 91 96 class NoSuchChangelist(Exception): 92 97 93 98 def __init__(self, change): 94 99 self.change = change 100 95 101 96 102 class NoSuchNode(Exception): … … 100 106 self.rev = rev 101 107 108 102 109 class NoSuchFile(NoSuchNode): 103 110 pass 111 104 112 105 113 class NoSuchDirectory(NoSuchNode): … … 110 118 _dirSlashDotDotRE = re.compile(ur'[^/]+/..(/|$)') 111 119 _trailingSlashesRE = re.compile(ur'(?<=[^/])/*$') 120 112 121 113 122 class NodePath(object): … … 350 359 return not (self == other) 351 360 361 352 362 class Changelist(object): 353 363 """A proxy object that gives access to details about a particular … … 490 500 491 501 return nodes 502 492 503 493 504 class Node(object): … … 847 858 return fileNodes 848 859 849 class Repository(object): 860 861 class P4Repository(object): 850 862 """The repository object. 851 863
