#! /usr/bin/env python3.3 """Code to recall every revision of every file we've created.""" import bisect import logging import p4gf_config from p4gf_l10n import _ import p4gf_squee_value LOG = logging.getLogger("p4gf_fast_push.rev_history") class RevHistoryStore: """BigStore of depot_path -> RevHistory. """ def __init__(self, ctx, file_path, store_how): # use a dict until we have a BigStore. # pylint:disable=invalid-name self.file_path = file_path if store_how == p4gf_config.VALUE_FAST_PUSH_WORKING_STORAGE_DICT: self._d = {} elif store_how == p4gf_config.VALUE_FAST_PUSH_WORKING_STORAGE_SQLITE_MEMORY: self._d = p4gf_squee_value.SqueeValueDict(file_path) elif store_how == p4gf_config.VALUE_FAST_PUSH_WORKING_STORAGE_SQLITE_SINGLE_TABLE: self._d = p4gf_squee_value.SqueeValue(file_path) elif store_how == p4gf_config.VALUE_FAST_PUSH_WORKING_STORAGE_SQLITE_MULTIPLE_TABLES: self._d = p4gf_squee_value.SqueeValueMultiTable(file_path) else: raise RuntimeError(_("Unsupported store_how {store_how}") .format(store_how=store_how)) self._is_contextdata_written = set() self.ctx = ctx def head_db_rev(self, depot_path): """Return the DbRev record most recently record_head()ed for this depot_path. """ depot_path_key = self.ctx.path_to_key(depot_path) rh = self._d.get(depot_path_key) if rh: return rh.head_db_rev() return None def record_head(self, db_rev, is_delete): """Record a new head revision.""" depot_path_key = self.ctx.path_to_key(db_rev.depot_path) rh = self._d.get(depot_path_key) if not rh: rh = RevHistory() rh.record_head(db_rev, is_delete) self._d[depot_path_key] = rh self._is_contextdata_written.add(depot_path_key) def next_rev_num(self, depot_path): """Return the integer rev number to use for the next file action on depot_path. """ depot_path_key = self.ctx.path_to_key(depot_path) rh = self._d.get(depot_path_key) if rh: return rh.next_rev_num() return 1 def is_contextdata_written(self, depot_path): """Have we already written this depot path to the current commit_gfunzip's contextdata.jnl? """ depot_path_key = self.ctx.path_to_key(depot_path) return depot_path_key in self._is_contextdata_written def clear_all_contextdata_written(self): """Clear is_contextdata_written() for all depot paths. Call this when you open a new commit_gfunzip. """ self._is_contextdata_written = set() def exists_at_head(self, depot_path): """Does this depot file current exist, undeleted, at head? Used when deciding whether to treat git-fast-export 'M' as either 'p4 add' or 'p4 delete' """ depot_path_key = self.ctx.path_to_key(depot_path) rh = self._d.get(depot_path_key) if not rh: return False return not rh.is_deleted_at_head() def src_range(self, depot_path, change_num): """Return a (#startRev,endRev) integer pair to use as an integ source for 'p4 integ -f src@change_num. Return (None, None) if no revision at that change_num. """ if depot_path not in self._d: return (None, None) rh = self._d.get(depot_path) if not rh: rh = RevHistory() return rh.src_range(change_num) # ---------------------------------------------------------------------------- class RevHistory: """Every revision of a single depot file. Keep this struct tiny. We retain millions of these in a BigStore. Do NOT use gfmarks here. Integer changelist numbers only. Internal compact storage: a single list of integers, with element [0] special [1:] = list of integer changelist numbers, one per revision. index into self._l is rev#1, value at that location is change_num. [0] = None for most files list of integer revision numbers that "add" this file after a delete for those few files deleted then re-added. Note for future second-and-later push: Assumes changelists monotonically increase. For the possible future where we use this code for pushes into existing Perforce history, choose your starting gfmark to be greater than the current 'p4 counter change' value so that when you fill us with existing file history, those changelists sort numerically before anything you append later. """ def __init__(self): self._l = [None] # pylint:disable=invalid-name self._deleted_at_head = False self._head_db_rev = None def head_db_rev(self): """Return the DbRev record most recently record_head()ed.""" return self._head_db_rev def record_head(self, db_rev, is_delete): """Record a new head revision.""" self.append_rev( change_num = db_rev.change_num , is_delete = is_delete , depot_path = db_rev.depot_path ) self._head_db_rev = db_rev def next_rev_num(self): """Return the integer number to use for the next rev of this file.""" if not self._head_db_rev: return 1 return 1 + self._head_db_rev.depot_rev def append_rev(self, change_num, is_delete, depot_path): """Record a new revision. Returns the integer revision number for this new action. depot_path used only for reporting bugs. """ if is_delete: if (not 1 < len(self._l)) or self._deleted_at_head: LOG.warning("RevHistory.append_rev()" " attempt to delete a depot file that" " does not exist, undeleted at head: {}" .format(depot_path)) is_add = self._deleted_at_head and not is_delete # Record "add" revisions so that they can # act as #startRev integ sources later. rev_num = len(self._l) if is_add and 1 < rev_num: if self._l[0] is None: self._l[0] = [rev_num] else: self._l[0].append(rev_num) self._l.append(int(change_num)) self._deleted_at_head = is_delete return rev_num def src_range(self, change_num): """Return a (#startRev,endRev) integer pair to use as an integ source for 'p4 integ -f src@change_num. Return (None, None) if no revision at that change_num. """ # Find highest #rev number at or before @change_num # # +1 here because index_le() is called with a slice # that starts at index 1. # i = _index_le(self._l[1:], int(change_num)) + 1 if i < 1: # change_num is before first rev#1 change_num return (None, None) end_rev = i # Find highest #rev number at or before #end_rev # that 'p4 add'ed this file. start_rev = 1 if self._l[0]: i = _index_le(self._l[0], end_rev) if 0 <= i: start_rev = self._l[0][i] return (start_rev, end_rev) def is_deleted_at_head(self): """Was the most recent action one that deleted this file?""" return self._deleted_at_head and 1 < len(self._l) # -- module-wide ------------------------------------------------------------- def _index_le(lisst, val): """Return index of element in lisst that is <= val. Return -1 if no such element. Do this using O(lg n) binary search rather than O(n) scan. """ i = bisect.bisect_right(lisst, val) if i: return i - 1 return -1