class PerforceRepository(object): """A Perforce repository implementation. Built on top of the PyPerforce API. http://pyperforce.sourceforge.net/ """ def __init__(self, connection, log, jobPrefixLength): self.authz = None # length of the job prefix used with PerforceJobScript self._job_prefix_length = jobPrefixLength # Log object for logging output self._log = log # The connection to the Perforce server self._connection = connection # The Repository object that we query for Perforce info from p4trac.repos import Repository self._repos = Repository(connection) def get_name(self): return 'p4://%s:%s@%s' % (self._connection.user, self._connection.password, self._connection.port) name = property(get_name) def close(self): self._connection.disconnect() def get_tags(self, rev): results = self._connection.run('labels') if results.errors: from p4trac.repos import PerforceError raise PerforceError(results.errors) for rec in results.records: name = self._repos.toUnicode(rec['label']) yield (name, u'@%s' % name) def get_branches(self, rev): # TODO: Generate a list of branches return [] def get_changeset(self, rev): self._log.debug('get_changeset(%r)' % rev) if isinstance(rev, int): change = rev else: from p4trac.util import toUnicode rev = toUnicode(rev) if rev.startswith(u'@'): rev = rev[1:] try: change = int(rev) except ValueError: raise TracError(u"Invalid changeset number '%s'" % rev) return PerforceChangeset(change, self._repos, self._log, self._job_prefix_length) def get_changesets(self, start, stop): self._log.debug('PerforceRepository.get_changesets(%r,%r)' % (start, stop)) import datetime start = datetime.datetime.fromtimestamp(start) stop = datetime.datetime.fromtimestamp(stop) startDate = start.strftime('%Y/%m/%d:%H:%M:%S') stopDate = stop.strftime('%Y/%m/%d:%H:%M:%S') from p4trac.repos import _P4ChangesOutputConsumer output = _P4ChangesOutputConsumer(self._repos) self._connection.run('changes', '-l', '-s', 'submitted', '@>=%s,@<=%s' % (startDate, stopDate), output=output) if output.errors: from p4trac.repos import PerforceError raise PerforceError(output.errors) for change in output.changes: yield self.get_changeset(change) def has_node(self, path, rev=None): from p4trac.repos import NodePath path = NodePath.normalisePath(path) return self._repos.getNode(NodePath(path, rev)).exists def get_node(self, path, rev=None): self._log.debug('get_node(%s, %s) called' % (path, rev)) from p4trac.repos import NodePath nodePath = NodePath(NodePath.normalisePath(path), rev) return PerforceNode(nodePath, self._repos, self._log) def get_oldest_rev(self): return self.next_rev(0) oldest_rev = property(fget=get_oldest_rev) def get_youngest_rev(self): return self._repos.getLatestChange() youngest_rev = property(fget=get_youngest_rev) def previous_rev(self, rev): self._log.debug('previous_rev(%r)' % rev) if not isinstance(rev, int): rev = self.short_rev(rev) if not isinstance(rev, int): raise NoSuchChangeset(rev) from p4trac.repos import _P4ChangesOutputConsumer output = _P4ChangesOutputConsumer(self._repos) self._connection.run('changes', '-l', '-s', 'submitted', '-m', '1', '@<%i' % rev, output=output) if output.errors: from p4trac.repos import PerforcError raise PerforcError(output.errors) if output.changes: return max(output.changes) else: return None def next_rev(self, rev, path=''): # Finding the next revision is a little more difficult in Perforce # as we can only ask for the n most recent changes according to a # given criteria. We query batches of changes using a binary search # technique so that the number of changes queried is of the order of # log N where N is the number of changes greater than rev. This way # it is still fairly efficient if the next change is 1 or 1000 changes # later. self._log.debug('next_rev(%r,%r)' % (rev, path)) from p4trac.repos import NodePath if not path: path = u'//' else: path = NodePath.normalisePath(path) node = self._repos.getNode(NodePath(path, rev)) if node.isDirectory: if node.nodePath.isRoot: # Handle the root path specially since it encompasses all # changes and so can use the repository's internal cache. return self._repos.getNextChange(int(rev)) else: queryPath = u'%s/...' % node.nodePath.path else: queryPath = node.nodePath.path queryPath = self._repos.fromUnicode(queryPath) self._log.debug( u'Looing for next_rev after change %i for %s' % (rev, path)) # Perform a binary-search of sorts for the next revision batchSize = 50 lowerBound = rev + 1 upperBound = self.youngest_rev while lowerBound <= upperBound: if lowerBound + batchSize > upperBound: batchUpperBound = upperBound else: middle = (upperBound + lowerBound) / 2 if middle - lowerBound < batchSize: batchUpperBound = lowerBound + batchSize else: batchUpperBound = middle self._log.debug( 'Looking for changes in range [%i, %i]' % (lowerBound, batchUpperBound)) from p4trac.repos import _P4ChangesOutputConsumer output = _P4ChangesOutputConsumer(self._repos) self._connection.run('changes', '-l', '-s', 'submitted', '-m', str(batchSize), '%s@>=%i,@<=%i' % (queryPath, lowerBound, batchUpperBound), output=output) if output.errors: from p4trac.repos import PerforcError raise PerforcError(output.errors) if output.changes: lowest = min(output.changes) assert lowest >= lowerBound assert lowest <= batchUpperBound if lowerBound + batchSize >= batchUpperBound: # There are no earlier changes self._log.debug('next_rev is %i' % lowest) return lowest else: # There may be another earlier changes but we know it # can't be any later than lowest. upperBound = lowest else: # Didn't find any changes in (lowerBound, batchUpperBound) # Try searching from batchUpperBound + 1 onwards lowerBound = batchUpperBound + 1 return None def rev_older_than(self, rev1, rev2): self._log.debug('PerforceRepository.rev_older_than(%r,%r)' % (rev1, rev2)) rev1 = self.short_rev(rev1) rev2 = self.short_rev(rev2) # Can compare equal revisions directly if rev1 == rev2: return False # Can compare change revisions directly if isinstance(rev1, int) and isinstance(rev2, int): return rev1 < rev2 def parseDateRevision(rev): if not isinstance(rev, unicode): raise ValueError if not rev.startswith(u'@'): raise ValueError # @YYYY/MM/DD[:HH:MM:SS] if len(rev) not in [11, 20]: raise ValueError year = int(rev[1:5]) month = int(rev[6:8]) day = int(rev[9:11]) if rev[5] != u'/' or rev[8] != u'/': raise ValueError if len(rev) == 20: hour = int(rev[12:14]) minute = int(rev[15:17]) second = int(rev[18:20]) if rev[11] != u':' or rev[14] != u':' or rev[17] != u':': raise ValueError else: hour = 0 minute = 0 second = 0 return (year, month, day, hour, minute, second) # Can compare date revisions directly try: return parseDateRevision(rev1) < parseDateRevision(rev2) except ValueError: pass # Can't compare these revisions directly, # Compare based on the latest change number that affects this revision. from p4trac.repos import NodePath if not isinstance(rev1, int): rootAtRev1 = NodePath(u'//', rev1) rev1 = self._repos.getNode(rootAtRev1).change if not isinstance(rev2, int): rootAtRev2 = NodePath(u'//', rev2) rev2 = self._repos.getNode(rootAtRev2).change self._log.debug('Comparing by change rev1=%i, rev2=%i' % (rev1, rev2)) return rev1 < rev2 def get_youngest_rev_in_cache(self, db): cursor = db.cursor() cursor.execute("SELECT r.rev " "FROM revision r " "ORDER BY r.time DESC " "LIMIT 1") row = cursor.fetchone() return row and row[0] or None def get_path_history(self, path, rev=None, limit=None): # TODO: This doesn't handle the case where the head node has been # deleted or a file has changed to a directory or vica versa. from p4trac.repos import NodePath nodePath = NodePath(NodePath.normalisePath(path), rev) node = PerforceNode(nodePath, self._repos, self._log) return node.get_history(limit) def normalize_path(self, path): self._log.debug('normalize_path(%r)' % path) return normalisePath(path) def normalize_rev(self, rev): self._log.debug('normalize_rev(%r)' % rev) rev = normaliseRev(rev) if rev is None: return self.youngest_rev else: return rev def short_rev(self, rev): self._log.debug('short_rev(%r)' % rev) return self.normalize_rev(rev) def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): self._log.debug('PerforceRepository.get_changes(%r,%r,%r,%r)' % ( old_path, old_rev, new_path, new_rev)) from p4trac.repos import NodePath oldNodePath = NodePath(NodePath.normalisePath(old_path), old_rev) oldNode = self._repos.getNode(oldNodePath) newNodePath = NodePath(NodePath.normalisePath(new_path), new_rev) newNode = self._repos.getNode(newNodePath) if (newNode.isFile and oldNode.isDirectory) or \ (newNode.isDirectory and oldNode.isFile): raise TracError("Cannot view changes between directory and file") if newNode.isDirectory or oldNode.isDirectory: if oldNodePath.isRoot: oldQueryPath = u'//...%s' % oldNodePath.rev else: oldQueryPath = u'%s/...%s' % (oldNodePath.path, oldNodePath.rev) if newNodePath.isRoot: newQueryPath = u'//...%s' % newNodePath.rev else: newQueryPath = u'%s/...%s' % (newNodePath.path, newNodePath.rev) elif newNode.isFile or oldNode.isFile: oldQueryPath = oldNodePath.fullPath newQueryPath = newNodePath.fullPath else: raise TracError("Cannot diff two non-existant nodes") from p4trac.repos import _P4Diff2OutputConsumer output = _P4Diff2OutputConsumer(self._repos) self._connection.run( 'diff2', '-ds', self._repos.fromUnicode(oldQueryPath), self._repos.fromUnicode(newQueryPath), output=output) if output.errors: from p4trac.repos import PerforceError raise PerforceError(output.errors) for change in output.changes: oldFileNodePath, newFileNodePath = change if oldFileNodePath is not None: oldFileNode = PerforceNode(oldFileNodePath, self._repos, self._log) else: oldFileNode = None if newFileNodePath is not None: newFileNode = PerforceNode(newFileNodePath, self._repos, self._log) else: newFileNode = None if newFileNode and oldFileNode: yield (oldFileNode, newFileNode, Node.FILE, Changeset.EDIT) elif newFileNode: yield (oldFileNode, newFileNode, Node.FILE, Changeset.ADD) elif oldFileNode: yield (oldFileNode, newFileNode, Node.FILE, Changeset.DELETE)
class PerforceRepository(object): """A Perforce repository implementation. Built on top of the PyPerforce API. http://pyperforce.sourceforge.net/ """ def __init__(self, connection, log): self.authz = None # Log object for logging output self._log = log # The connection to the Perforce server self._connection = connection # The Repository object that we query for Perforce info from p4trac.repos import Repository self._repos = Repository(connection) def get_name(self): return 'p4://%s' % self._connection.port name = property(get_name) def close(self): self._connection.disconnect() def get_tags(self, rev): results = self._connection.run('labels') if results.errors: from p4trac.repos import PerforceError raise PerforceError(results.errors) for rec in results.records: name = self._repos.toUnicode(rec['label']) yield (name, u'@%s' % name) def get_branches(self, rev): # TODO: Generate a list of branches return [] def get_changeset(self, rev): self._log.debug('get_changeset(%r)' % rev) if isinstance(rev, int): change = rev else: from p4trac.util import toUnicode rev = toUnicode(rev) if rev.startswith(u'@'): rev = rev[1:] try: change = int(rev) except ValueError: raise TracError(u"Invalid changeset number '%s'" % rev) return PerforceChangeset(change, self._repos, self._log) def get_changesets(self, start, stop): self._log.debug('PerforceRepository.get_changesets(%r,%r)' % (start, stop)) import datetime start = datetime.datetime.fromtimestamp(start) stop = datetime.datetime.fromtimestamp(stop) startDate = start.strftime('%Y/%m/%d:%H:%M:%S') stopDate = stop.strftime('%Y/%m/%d:%H:%M:%S') from p4trac.repos import _P4ChangesOutputConsumer output = _P4ChangesOutputConsumer(self._repos) self._connection.run('changes', '-l', '-s', 'submitted', '@>=%s,@<=%s' % (startDate, stopDate), output=output) if output.errors: from p4trac.repos import PerforceError raise PerforceError(output.errors) for change in output.changes: yield self.get_changeset(change) def has_node(self, path, rev=None): from p4trac.repos import NodePath path = NodePath.normalisePath(path) return self._repos.getNode(NodePath(path, rev)).exists def get_node(self, path, rev=None): self._log.debug('get_node(%s, %s) called' % (path, rev)) from p4trac.repos import NodePath nodePath = NodePath(NodePath.normalisePath(path), rev) return PerforceNode(nodePath, self._repos, self._log) def get_oldest_rev(self): return self.next_rev(0) oldest_rev = property(fget=get_oldest_rev) def get_youngest_rev(self): return self._repos.getLatestChange() youngest_rev = property(fget=get_youngest_rev) def previous_rev(self, rev): self._log.debug('previous_rev(%r)' % rev) if not isinstance(rev, int): rev = self.short_rev(rev) if not isinstance(rev, int): raise NoSuchChangeset(rev) from p4trac.repos import _P4ChangesOutputConsumer output = _P4ChangesOutputConsumer(self._repos) self._connection.run('changes', '-l', '-s', 'submitted', '-m', '1', '@<%i' % rev, output=output) if output.errors: from p4trac.repos import PerforcError raise PerforcError(output.errors) if output.changes: return max(output.changes) else: return None def next_rev(self, rev, path=''): # Finding the next revision is a little more difficult in Perforce # as we can only ask for the n most recent changes according to a # given criteria. We query batches of changes using a binary search # technique so that the number of changes queried is of the order of # log N where N is the number of changes greater than rev. This way # it is still fairly efficient if the next change is 1 or 1000 changes # later. self._log.debug('next_rev(%r,%r)' % (rev, path)) from p4trac.repos import NodePath if not path: path = u'//' else: path = NodePath.normalisePath(path) node = self._repos.getNode(NodePath(path, rev)) if node.isDirectory: if node.nodePath.isRoot: # Handle the root path specially since it encompasses all # changes and so can use the repository's internal cache. return self._repos.getNextChange(int(rev)) else: queryPath = u'%s/...' % node.nodePath.path else: queryPath = node.nodePath.path queryPath = self._repos.fromUnicode(queryPath) self._log.debug( u'Looing for next_rev after change %i for %s' % (rev, path)) # Perform a binary-search of sorts for the next revision batchSize = 50 lowerBound = rev + 1 upperBound = self.youngest_rev while lowerBound <= upperBound: if lowerBound + batchSize > upperBound: batchUpperBound = upperBound else: middle = (upperBound + lowerBound) / 2 if middle - lowerBound < batchSize: batchUpperBound = lowerBound + batchSize else: batchUpperBound = middle self._log.debug( 'Looking for changes in range [%i, %i]' % (lowerBound, batchUpperBound)) from p4trac.repos import _P4ChangesOutputConsumer output = _P4ChangesOutputConsumer(self._repos) self._connection.run('changes', '-l', '-s', 'submitted', '-m', str(batchSize), '%s@>=%i,@<=%i' % (queryPath, lowerBound, batchUpperBound), output=output) if output.errors: from p4trac.repos import PerforcError raise PerforcError(output.errors) if output.changes: lowest = min(output.changes) assert lowest >= lowerBound assert lowest <= batchUpperBound if lowerBound + batchSize >= batchUpperBound: # There are no earlier changes self._log.debug('next_rev is %i' % lowest) return lowest else: # There may be another earlier changes but we know it # can't be any later than lowest. upperBound = lowest else: # Didn't find any changes in (lowerBound, batchUpperBound) # Try searching from batchUpperBound + 1 onwards lowerBound = batchUpperBound + 1 return None def rev_older_than(self, rev1, rev2): self._log.debug('PerforceRepository.rev_older_than(%r,%r)' % (rev1, rev2)) rev1 = self.short_rev(rev1) rev2 = self.short_rev(rev2) # Can compare equal revisions directly if rev1 == rev2: return False # Can compare change revisions directly if isinstance(rev1, int) and isinstance(rev2, int): return rev1 < rev2 def parseDateRevision(rev): if not isinstance(rev, unicode): raise ValueError if not rev.startswith(u'@'): raise ValueError # @YYYY/MM/DD[:HH:MM:SS] if len(rev) not in [11, 20]: raise ValueError year = int(rev[1:5]) month = int(rev[6:8]) day = int(rev[9:11]) if rev[5] != u'/' or rev[8] != u'/': raise ValueError if len(rev) == 20: hour = int(rev[12:14]) minute = int(rev[15:17]) second = int(rev[18:20]) if rev[11] != u':' or rev[14] != u':' or rev[17] != u':': raise ValueError else: hour = 0 minute = 0 second = 0 return (year, month, day, hour, minute, second) # Can compare date revisions directly try: return parseDateRevision(rev1) < parseDateRevision(rev2) except ValueError: pass # Can't compare these revisions directly, # Compare based on the latest change number that affects this revision. from p4trac.repos import NodePath if not isinstance(rev1, int): rootAtRev1 = NodePath(u'//', rev1) rev1 = self._repos.getNode(rootAtRev1).change if not isinstance(rev2, int): rootAtRev2 = NodePath(u'//', rev2) rev2 = self._repos.getNode(rootAtRev2).change self._log.debug('Comparing by change rev1=%i, rev2=%i' % (rev1, rev2)) return rev1 < rev2 def get_youngest_rev_in_cache(self, db): cursor = db.cursor() cursor.execute("SELECT r.rev " "FROM revision r " "ORDER BY r.time DESC " "LIMIT 1") row = cursor.fetchone() return row and row[0] or None def get_path_history(self, path, rev=None, limit=None): # TODO: This doesn't handle the case where the head node has been # deleted or a file has changed to a directory or vica versa. from p4trac.repos import NodePath nodePath = NodePath(NodePath.normalisePath(path), rev) node = PerforceNode(nodePath, self._repos, self._log) return node.get_history(limit) def normalize_path(self, path): self._log.debug('normalize_path(%r)' % path) return normalisePath(path) def normalize_rev(self, rev): self._log.debug('normalize_rev(%r)' % rev) rev = normaliseRev(rev) if rev is None: return self.youngest_rev else: return rev def short_rev(self, rev): self._log.debug('short_rev(%r)' % rev) return self.normalize_rev(rev) def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): self._log.debug('PerforceRepository.get_changes(%r,%r,%r,%r)' % ( old_path, old_rev, new_path, new_rev)) from p4trac.repos import NodePath oldNodePath = NodePath(NodePath.normalisePath(old_path), old_rev) oldNode = self._repos.getNode(oldNodePath) newNodePath = NodePath(NodePath.normalisePath(new_path), new_rev) newNode = self._repos.getNode(newNodePath) if (newNode.isFile and oldNode.isDirectory) or \ (newNode.isDirectory and oldNode.isFile): raise TracError("Cannot view changes between directory and file") if newNode.isDirectory or oldNode.isDirectory: if oldNodePath.isRoot: oldQueryPath = u'//...%s' % oldNodePath.rev else: oldQueryPath = u'%s/...%s' % (oldNodePath.path, oldNodePath.rev) if newNodePath.isRoot: newQueryPath = u'//...%s' % newNodePath.rev else: newQueryPath = u'%s/...%s' % (newNodePath.path, newNodePath.rev) elif newNode.isFile or oldNode.isFile: oldQueryPath = oldNodePath.fullPath newQueryPath = newNodePath.fullPath else: raise TracError("Cannot diff two non-existant nodes") from p4trac.repos import _P4Diff2OutputConsumer output = _P4Diff2OutputConsumer(self._repos) self._connection.run( 'diff2', '-ds', self._repos.fromUnicode(oldQueryPath), self._repos.fromUnicode(newQueryPath), output=output) if output.errors: from p4trac.repos import PerforceError raise PerforceError(output.errors) for change in output.changes: oldFileNodePath, newFileNodePath = change if oldFileNodePath is not None: oldFileNode = PerforceNode(oldFileNodePath, self._repos, self._log) else: oldFileNode = None if newFileNodePath is not None: newFileNode = PerforceNode(newFileNodePath, self._repos, self._log) else: newFileNode = None if newFileNode and oldFileNode: yield (oldFileNode, newFileNode, Node.FILE, Changeset.EDIT) elif newFileNode: yield (oldFileNode, newFileNode, Node.FILE, Changeset.ADD) elif oldFileNode: yield (oldFileNode, newFileNode, Node.FILE, Changeset.DELETE)