#! /usr/bin/env python3.3 """RevRange.""" import logging import p4gf_gitmirror from p4gf_l10n import _, NTR import p4gf_pygit2 LOG = logging.getLogger('p4gf_copy_to_git').getChild('rev_range') class RevRange: """Which Perforce changelists should we copy from Perforce to Git? If this is the first copy from Perforce to Git, identify snapshot(s) of history prior to our copy that we'll use as starting point(s): the "graft" commit before our real copied commits start. """ def __init__(self): # string. Perforce revision specifier for first thing to copy from # Perforce to Git. If a changelist number "@NNN", NNN might not # actually BE a real changelist number or a changelist that touches # anything in our view of the depot, and that's okay. Someone else # will run 'p4 describe //view/...{@begin},{end} to figure out the # TRUE changelist numbers. self.begin_rev_spec = None # Integer changelist number of the first changelist we'll copy. # Set in _new_repo_from_perforce_range() via 'p4 changes'. # Left as 0 for all other creation paths. Used only when grafting. self.begin_change_num = 0 # string. Perforce revision specifier for last thing to copy from # Perforce to Git. Usually "#head" to copy everything up to current # time. self.end_rev_spec = None # boolean. Is this the first copy into a new git repo? If so, then # caller must honor branch_id_to_graft_num if set. self.new_repo = False # branch_id ==> integer # Last Perforce changelist before this branch's history starts. # Can be None for some branches and defined for others: not every # branch needs a graft. # Defined only if new_repo is True AND begin_rev_spec points to a # second-or-later changelist within our view. def __str__(self): return ("b,e={begin_rev_spec},{end_rev_spec} new_repo={new_repo}" .format( begin_rev_spec = self.begin_rev_spec , end_rev_spec = self.end_rev_spec , new_repo = self.new_repo)) def as_range_string(self): """Return 'begin,end'.""" return NTR('{begin},{end}').format(begin=self.begin_rev_spec, end=self.end_rev_spec) @staticmethod def from_start_stop(ctx, start_at="@1", stop_at="#head"): """Factory: create and return a new RevRange object. start_at: Accepts either Perforce revision specifier OR a git sha1 for an existing git commit, which is then mapped to a Perforce changelist number, and then we add 1 to start copying ONE AFTER that sha1's corresponding Perforce changelist. stop_at: Usually "#head". """ if start_at.startswith("@"): return RevRange._new_repo_from_perforce_range(start_at, stop_at) else: return RevRange._existing_repo_after_commit(ctx, start_at, stop_at) @staticmethod def _new_repo_from_perforce_range(start_at, # "@NNN" Perforce changelist num stop_at): """We're seeding a brand new repo that has no git commits yet.""" result = RevRange() result.begin_rev_spec = start_at result.end_rev_spec = stop_at result.new_repo = True result.begin_change_num = int(start_at[1:]) return result @staticmethod def _existing_repo_after_commit(ctx, start_at, # some git sha1, maybe partial stop_at): """We're adding to an existing git repo with an existing head. Find the Perforce submitted changelist that goes with start_at's Git commit sha1, then start at one after that. """ last_commit = _expand_sha1(ctx, start_at) last_changelist_number = p4gf_gitmirror.get_last_change_for_commit(last_commit, ctx) if not last_changelist_number: raise RuntimeError(_('Invalid startAt={start_at}: no commit sha1 with a' ' corresponding Perforce changelist number.') .format(start_at=start_at)) result = RevRange() result.begin_rev_spec = "@{}".format(1 + int(last_changelist_number)) result.end_rev_spec = stop_at result.new_repo = False return result def _expand_sha1(ctx, partial_sha1): """Given partial SHA1 of a git object, return complete SHA1. If there is no match, returns None. """ try: obj = ctx.repo.git_object_lookup_prefix(partial_sha1) return p4gf_pygit2.object_to_sha1(obj) if obj else None except ValueError: return None