def parse_revision_spec(self, revisions=[]): """Parses the given revision spec. The 'revisions' argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". SCMTool-specific overrides of this method are expected to deal with such syntaxes. This will return a dictionary with the following keys: 'base': Always None. 'tip': A revision string representing either a changeset or a branch. These will be used to generate the diffs to upload to Review Board (or print). The Plastic implementation requires that one and only one revision is passed in. The diff for review will include the changes in the given changeset or branch. """ n_revisions = len(revisions) if n_revisions == 0: raise InvalidRevisionSpecError( 'Either a changeset or a branch must be specified') elif n_revisions == 1: return { 'base': None, 'tip': revisions[0], } else: raise TooManyRevisionsError
def parse_revision_spec(self, revisions): """Parse the given revision spec. The ``revisions`` argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use the TFS-native syntax of "r1~r2". Versions passed in can be any versionspec, such as a changeset number, ``L``-prefixed label name, ``W`` (latest workspace version), or ``T`` (latest upstream version). This will return a dictionary with the following keys: ``base``: A revision to use as the base of the resulting diff. ``tip``: A revision to use as the tip of the resulting diff. ``parent_base`` (optional): The revision to use as the base of a parent diff. These will be used to generate the diffs to upload to Review Board (or print). The diff for review will include the changes in (base, tip], and the parent diff (if necessary) will include (parent, base]. If a single revision is passed in, this will return the parent of that revision for 'base' and the passed-in revision for 'tip'. If zero revisions are passed in, this will return revisions relevant for the "current change" (changes in the work folder which have not yet been checked in). Args: revisions (list of unicode): The revision spec to parse. Returns: dict: A dictionary with ``base`` and ``tip`` keys, each of which is a string describing the revision. These may be special internal values. Raises: rbtools.clients.errors.TooManyRevisionsError: Too many revisions were specified. rbtools.clients.errors.InvalidRevisionSpecError: The given revision spec could not be parsed. """ if len(revisions) > 2: raise TooManyRevisionsError rc, revisions, errors = self._run_helper(['parse-revision'] + revisions, split_lines=True) if rc == 0: return {'base': revisions[0].strip(), 'tip': revisions[1].strip()} else: raise InvalidRevisionSpecError('\n'.join(errors))
def _convert_symbolic_revision(self, revision): """Convert a symbolic revision into a numeric changeset.""" args = ['history', '-stopafter:1', '-recursive', '-format:xml'] # 'tf history -version:W' doesn't seem to work (even though it's # supposed to). Luckily, W is the default when -version isn't passed, # so just elide it. if revision != 'W': args.append('-version:%s' % revision) args.append(os.getcwd()) data = self._run_tf(args) try: root = ET.fromstring(data) item = root.find('./changeset') if item is not None: return int(item.attrib['id']) else: raise Exception('No changesets found') except Exception as e: logging.debug('Failed to parse output from "tf history": %s', e, exc_info=True) logging.debug(data) raise InvalidRevisionSpecError( '"%s" does not appear to be a valid versionspec' % revision)
def _convert_symbolic_revision(self, revision, path=None): """Convert a symbolic revision into a numeric changeset. Args: revision (unicode): The TFS versionspec to convert. path (unicode, optional): The itemspec that the revision applies to. Returns: int: The changeset number corresponding to the versionspec. """ # We pass results_unicode=False because that uses the filesystem # encoding to decode the output, but the XML results we get should # always be UTF-8, and are well-formed with the encoding specified. We # can therefore let ElementTree determine how to decode it. data = self._run_tf([ 'vc', 'history', '/stopafter:1', '/recursive', '/format:detailed', '/version:%s' % revision, path or os.getcwd() ]) m = re.search('^Changeset: (\d+)$', data, re.MULTILINE) if not m: logging.debug('Failed to parse output from "tf vc history":\n%s', data) raise InvalidRevisionSpecError( '"%s" does not appear to be a valid versionspec' % revision)
def _identify_revision(self, revision): """Identify the given revision. Args: revision (unicode): The revision. Raises: rbtools.clients.errors.InvalidRevisionSpecError: The specified revision could not be identified. Returns: unicode: The global revision ID of the commit. """ identify = self._execute( [self._exe, 'identify', '-i', '--hidden', '-r', str(revision)], ignore_errors=True, none_on_ignored_error=True) if identify is None: raise InvalidRevisionSpecError( '"%s" does not appear to be a valid revision' % revision) else: return identify.split()[0]
def _convert_symbolic_revision(self, revision, path=None): """Convert a symbolic revision into a numeric changeset.""" args = ['history', '-stopafter:1', '-recursive', '-format:xml'] # 'tf history -version:W' doesn't seem to work (even though it's # supposed to). Luckily, W is the default when -version isn't passed, # so just elide it. if revision != 'W': args.append('-version:%s' % revision) args.append(path or os.getcwd()) # We pass results_unicode=False because that uses the filesystem # encoding, but the XML results we get should always be UTF-8, and are # well-formed with the encoding specified. We can therefore let # ElementTree determine how to decode it. data = self._run_tf(args, results_unicode=False) try: root = ET.fromstring(data) item = root.find('./changeset') if item is not None: return int(item.attrib['id']) else: raise Exception('No changesets found') except Exception as e: logging.debug('Failed to parse output from "tf history": %s', e, exc_info=True) logging.debug(data) raise InvalidRevisionSpecError( '"%s" does not appear to be a valid versionspec' % revision)
def parse_revision_spec(self, revisions=[]): """Parses the given revision spec. The 'revisions' argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". SCMTool-specific overrides of this method are expected to deal with such syntaxes. This will return a dictionary with the following keys: 'base': A revision to use as the base of the resulting diff. 'tip': A revision to use as the tip of the resulting diff. These will be used to generate the diffs to upload to Review Board (or print). The diff for review will include the changes in (base, tip]. If a single revision is passed in, this will raise an exception, because CVS doesn't have a repository-wide concept of "revision", so selecting an individual "revision" doesn't make sense. With two revisions, this will treat those revisions as tags and do a diff between those tags. If zero revisions are passed in, this will return revisions relevant for the "current change". The exact definition of what "current" means is specific to each SCMTool backend, and documented in the implementation classes. The CVS SCMClient never fills in the 'parent_base' key. Users who are using other patch-stack tools who want to use parent diffs with CVS will have to generate their diffs by hand. Because `cvs diff` uses multiple arguments to define multiple tags, there's no single-argument/multiple-revision syntax available. """ n_revs = len(revisions) if n_revs == 0: return { 'base': 'BASE', 'tip': self.REVISION_WORKING_COPY, } elif n_revs == 1: raise InvalidRevisionSpecError( 'CVS does not support passing in a single revision.') elif n_revs == 2: return { 'base': revisions[0], 'tip': revisions[1], } else: raise TooManyRevisionsError return { 'base': None, 'tip': None, }
def _identify_revision(self, revision): identify = self._execute( ['hg', 'identify', '-i', '--hidden', '-r', str(revision)], ignore_errors=True, none_on_ignored_error=True) if identify is None: raise InvalidRevisionSpecError( '"%s" does not appear to be a valid revision' % revision) else: return identify.split()[0]
def parse_revision_spec(self, revisions=[]): """Parse the given revision spec. Args: revisions (list of unicode, optional): A list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as ``r1..r2`` or ``r1:r2``. SCMTool-specific overrides of this method are expected to deal with such syntaxes. Raises: rbtools.clients.errors.InvalidRevisionSpecError: The given revisions could not be parsed. rbtools.clients.errors.TooManyRevisionsError: The specified revisions list contained too many revisions. Returns: dict: A dictionary with the following keys: ``base`` (:py:class:`NoneType`): Always None. ``tip`` (:py:class:`unicode`): A revision to use as the tip of the resulting diff. These will be used to generate the diffs to upload to Review Board (or print). The Plastic implementation requires that one and only one revision is passed in. The diff for review will include the changes in the given changeset or branch. """ n_revisions = len(revisions) if n_revisions == 0: raise InvalidRevisionSpecError( 'Either a changeset or a branch must be specified') elif n_revisions == 1: return { 'base': None, 'tip': revisions[0], } else: raise TooManyRevisionsError
def parse_revision_spec(self, revisions=[]): """Parses the given revision spec. The 'revisions' argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". SCMTool-specific overrides of this method are expected to deal with such syntaxes. This will return a dictionary with the following keys: 'base': A revision to use as the base of the resulting diff. 'tip': A revision to use as the tip of the resulting diff. 'parent_base': (optional) The revision to use as the base of a parent diff. 'commit_id': (optional) The ID of the single commit being posted, if not using a range. These will be used to generate the diffs to upload to Review Board (or print). The diff for review will include the changes in (base, tip], and the parent diff (if necessary) will include (parent_base, base]. If a single revision is passed in, this will return the parent of that revision for 'base' and the passed-in revision for 'tip'. If zero revisions are passed in, this will return the current HEAD as 'tip', and the upstream branch as 'base', taking into account parent branches explicitly specified via --parent. """ n_revs = len(revisions) result = {} if n_revs == 0: # No revisions were passed in--start with HEAD, and find the # tracking branch automatically. parent_branch = self.get_parent_branch() head_ref = self._rev_parse(self.get_head_ref())[0] merge_base = self._rev_parse( self._get_merge_base(head_ref, self.upstream_branch))[0] result = { 'tip': head_ref, 'commit_id': head_ref, } if parent_branch: result['base'] = self._rev_parse(parent_branch)[0] result['parent_base'] = merge_base else: result['base'] = merge_base # Since the user asked us to operate on HEAD, warn them about a # dirty working directory if self.has_pending_changes(): logging.warning('Your working directory is not clean. Any ' 'changes which have not been committed ' 'to a branch will not be included in your ' 'review request.') elif n_revs == 1 or n_revs == 2: # Let `git rev-parse` sort things out. parsed = self._rev_parse(revisions) n_parsed_revs = len(parsed) assert n_parsed_revs <= 3 if n_parsed_revs == 1: # Single revision. Extract the parent of that revision to use # as the base. parent = self._rev_parse('%s^' % parsed[0])[0] result = { 'base': parent, 'tip': parsed[0], 'commit_id': parsed[0], } elif n_parsed_revs == 2: if parsed[1].startswith('^'): # Passed in revisions were probably formatted as # "base..tip". The rev-parse output includes all ancestors # of the first part, and none of the ancestors of the # second. Basically, the second part is the base (after # stripping the ^ prefix) and the first is the tip. result = { 'base': parsed[1][1:], 'tip': parsed[0], } else: # First revision is base, second is tip result = { 'base': parsed[0], 'tip': parsed[1], } elif n_parsed_revs == 3 and parsed[2].startswith('^'): # Revision spec is diff-since-merge. Find the merge-base of the # two revs to use as base. merge_base = execute([self.git, 'merge-base', parsed[0], parsed[1]]).strip() result = { 'base': merge_base, 'tip': parsed[0], } else: raise InvalidRevisionSpecError( 'Unexpected result while parsing revision spec') parent_base = self._get_merge_base(result['base'], self.upstream_branch) if parent_base != result['base']: result['parent_base'] = parent_base else: raise TooManyRevisionsError return result
def parse_revision_spec(self, revisions): """Parse the given revision spec. Args: revisions (list of unicode, optional): A list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as ``r1..r2`` or ``r1:r2``. SCMTool-specific overrides of this method are expected to deal with such syntaxes. Raises: rbtools.clients.errors.InvalidRevisionSpecError: The given revisions could not be parsed. rbtools.clients.errors.TooManyRevisionsError: The specified revisions list contained too many revisions. Returns: dict: A dictionary with the following keys: ``base`` (:py:class:`unicode`): A revision to use as the base of the resulting diff. ``tip`` (:py:class:`unicode`): A revision to use as the tip of the resulting diff. These will be used to generate the diffs to upload to Review Board (or print). There are many different ways to generate diffs for clearcase, because there are so many different workflows. This method serves more as a way to validate the passed-in arguments than actually parsing them in the way that other clients do. """ n_revs = len(revisions) if n_revs == 0: return { 'base': self.REVISION_CHECKEDOUT_BASE, 'tip': self.REVISION_CHECKEDOUT_CHANGESET, } elif n_revs == 1: if revisions[0].startswith(self.REVISION_ACTIVITY_PREFIX): return { 'base': self.REVISION_ACTIVITY_BASE, 'tip': revisions[0][len(self.REVISION_ACTIVITY_PREFIX):], } if revisions[0].startswith(self.REVISION_BRANCH_PREFIX): return { 'base': self.REVISION_BRANCH_BASE, 'tip': revisions[0][len(self.REVISION_BRANCH_PREFIX):], } if revisions[0].startswith(self.REVISION_LABEL_PREFIX): return { 'base': self.REVISION_LABEL_BASE, 'tip': [revisions[0][len(self.REVISION_BRANCH_PREFIX):]], } # TODO: # stream:streamname[@pvob] => review changes in this UCM stream # (UCM "branch") # baseline:baseline[@pvob] => review changes between this baseline # and the working directory elif n_revs == 2: if self.viewtype != 'dynamic': raise SCMError('To generate a diff using multiple revisions, ' 'you must use a dynamic view.') if (revisions[0].startswith(self.REVISION_LABEL_PREFIX) and revisions[1].startswith(self.REVISION_LABEL_PREFIX)): return { 'base': self.REVISION_LABEL_BASE, 'tip': [x[len(self.REVISION_BRANCH_PREFIX):] for x in revisions], } # TODO: # baseline:baseline1[@pvob] baseline:baseline2[@pvob] # => review changes between these two # baselines pass pairs = [] for r in revisions: p = r.split(':') if len(p) != 2: raise InvalidRevisionSpecError( '"%s" is not a valid file@revision pair' % r) pairs.append(p) return { 'base': self.REVISION_FILES, 'tip': pairs, }
def parse_revision_spec(self, revisions=[]): """Parses the given revision spec. The 'revisions' argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". SCMTool-specific overrides of this method are expected to deal with such syntaxes. This will return a dictionary with the following keys: 'base': A revision to use as the base of the resulting diff. 'tip': A revision to use as the tip of the resulting diff. 'parent_base': (optional) The revision to use as the base of a parent diff. 'commit_id': (optional) The ID of the single commit being posted, if not using a range. These will be used to generate the diffs to upload to Review Board (or print). The diff for review will include the changes in (base, tip], and the parent diff (if necessary) will include (parent, base]. If zero revisions are passed in, this will return the outgoing changes from the parent of the working directory. If a single revision is passed in, this will return the parent of that revision for 'base' and the passed-in revision for 'tip'. This will result in generating a diff for the changeset specified. If two revisions are passed in, they will be used for the 'base' and 'tip' revisions, respectively. In all cases, a parent base will be calculated automatically from changesets not present on the remote. """ self._init() n_revisions = len(revisions) if n_revisions == 1: # If there's a single revision, try splitting it based on hg's # revision range syntax (either :: or ..). If this splits, then # it's handled as two revisions below. revisions = re.split(r'\.\.|::', revisions[0]) n_revisions = len(revisions) result = {} if n_revisions == 0: # No revisions: Find the outgoing changes. Only consider the # working copy revision and ancestors because that makes sense. # If a user wishes to include other changesets, they can run # `hg up` or specify explicit revisions as command arguments. if self._type == 'svn': result['base'] = self._get_parent_for_hgsubversion() result['tip'] = '.' else: # Ideally, generating a diff for outgoing changes would be as # simple as just running `hg outgoing --patch <remote>`, but # there are a couple problems with this. For one, the # server-side diff parser isn't equipped to filter out diff # headers such as "comparing with..." and # "changeset: <rev>:<hash>". Another problem is that the output # of `hg outgoing` potentially includes changesets across # multiple branches. # # In order to provide the most accurate comparison between # one's local clone and a given remote (something akin to git's # diff command syntax `git diff <treeish>..<treeish>`), we have # to do the following: # # - Get the name of the current branch # - Get a list of outgoing changesets, specifying a custom # format # - Filter outgoing changesets by the current branch name # - Get the "top" and "bottom" outgoing changesets # # These changesets are then used as arguments to # `hg diff -r <rev> -r <rev>`. # # Future modifications may need to be made to account for odd # cases like having multiple diverged branches which share # partial history--or we can just punish developers for doing # such nonsense :) outgoing = \ self._get_bottom_and_top_outgoing_revs_for_remote(rev='.') if outgoing[0] is None or outgoing[1] is None: raise InvalidRevisionSpecError( 'There are no outgoing changes') result['base'] = self._identify_revision(outgoing[0]) result['tip'] = self._identify_revision(outgoing[1]) result['commit_id'] = result['tip'] # Since the user asked us to operate on tip, warn them about a # dirty working directory if self.has_pending_changes(): logging.warning('Your working directory is not clean. Any ' 'changes which have not been committed ' 'to a branch will not be included in your ' 'review request.') if self.options.parent_branch: result['parent_base'] = result['base'] result['base'] = self._identify_revision( self.options.parent_branch) elif n_revisions == 1: # One revision: Use the given revision for tip, and find its parent # for base. result['tip'] = self._identify_revision(revisions[0]) result['commit_id'] = result['tip'] result['base'] = self._execute([ 'hg', 'parents', '--hidden', '-r', result['tip'], '--template', '{node|short}' ]).split()[0] if len(result['base']) != 12: raise InvalidRevisionSpecError( "Can't determine parent revision") elif n_revisions == 2: # Two revisions: Just use the given revisions result['base'] = self._identify_revision(revisions[0]) result['tip'] = self._identify_revision(revisions[1]) else: raise TooManyRevisionsError if 'base' not in result or 'tip' not in result: raise InvalidRevisionSpecError( '"%s" does not appear to be a valid revision spec' % revisions) if self._type == 'hg' and 'parent_base' not in result: # If there are missing changesets between base and the remote, we # need to generate a parent diff. outgoing = self._get_outgoing_changesets(self._get_remote_branch(), rev=result['base']) logging.debug('%d outgoing changesets between remote and base.', len(outgoing)) if not outgoing: return result parent_base = self._execute([ 'hg', 'parents', '--hidden', '-r', outgoing[0][1], '--template', '{node|short}' ]).split() if len(parent_base) == 0: raise Exception( 'Could not find parent base revision. Ensure upstream ' 'repository is not empty.') result['parent_base'] = parent_base[0] logging.debug('Identified %s as parent base', result['parent_base']) return result
def parse_revision_spec(self, revisions=[]): """Parses the given revision spec. The 'revisions' argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". SCMTool-specific overrides of this method are expected to deal with such syntaxes. This will return a dictionary with the following keys: 'base': A revision to use as the base of the resulting diff. 'tip': A revision to use as the tip of the resulting diff. These will be used to generate the diffs to upload to Review Board (or print). The diff for review will include the changes in (base, tip]. If a single revision is passed in, this will return the parent of that revision for 'base' and the passed-in revision for 'tip'. If zero revisions are passed in, this will return the most recently checked-out revision for 'base' and a special string indicating the working copy for 'tip'. The SVN SCMClient never fills in the 'parent_base' key. Users who are using other patch-stack tools who want to use parent diffs with SVN will have to generate their diffs by hand. """ n_revisions = len(revisions) if n_revisions == 1 and ':' in revisions[0]: revisions = revisions[0].split(':') n_revisions = len(revisions) if n_revisions == 0: # Most recent checked-out revision -- working copy # TODO: this should warn about mixed-revision working copies that # affect the list of files changed (see bug 2392). return { 'base': 'BASE', 'tip': self.REVISION_WORKING_COPY, } elif n_revisions == 1: # Either a numeric revision (n:n+1) or a changelist revision = revisions[0] revision = self._convert_symbolic_revision( re.sub(' ', ':', revision)) return { 'base': revision, 'tip': revision + 1, } elif n_revisions == 2: # Diff between two numeric revisions try: return { 'base': self._convert_symbolic_revision(revisions[0]), 'tip': self._convert_symbolic_revision(revisions[1]), } except ValueError: raise InvalidRevisionSpecError( 'Could not parse specified revisions: %s' % revisions) else: raise TooManyRevisionsError
def parse_revision_spec(self, revisions=[]): """Parses the given revision spec. The 'revisions' argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". SCMTool-specific overrides of this method are expected to deal with such syntaxes. This will return a dictionary with the following keys: 'base': A revision to use as the base of the resulting diff. 'tip': A revision to use as the tip of the resulting diff. These will be used to generate the diffs to upload to Review Board (or print). The diff for review will include the changes in (base, tip]. If zero revisions are passed in, this will return the 'default' changelist. If a single revision is passed in, this will return the parent of that revision for 'base' and the passed-in revision for 'tip'. The result may have special internal revisions or prefixes based on whether the changeset is submitted, pending, or shelved. If two revisions are passed in, they need to both be submitted changesets. """ n_revs = len(revisions) if n_revs == 0: return { 'base': self.REVISION_CURRENT_SYNC, 'tip': self.REVISION_PENDING_CLN_PREFIX + 'default', } elif n_revs == 1: # A single specified CLN can be any of submitted, pending, or # shelved. These are stored with special prefixes and/or names # because the way that we get the contents of the files changes # based on which of these is in effect. status = self._get_changelist_status(revisions[0]) # Both pending and shelved changes are treated as "pending", # through the same code path. This is because the documentation for # 'p4 change' tells a filthy lie, saying that shelved changes will # have their status listed as shelved. In fact, when you shelve # changes, it sticks the data up on the server, but leaves your # working copy intact, and the change is still marked as pending. # Even after reverting the working copy, the change won't have its # status as "shelved". That said, there's perhaps a way that it # could (perhaps from other clients?), so it's still handled in # this conditional. # # The diff routine will first look for opened files in the client, # and if that fails, it will then do the diff against the shelved # copy. if status in ('pending', 'shelved'): return { 'base': self.REVISION_CURRENT_SYNC, 'tip': self.REVISION_PENDING_CLN_PREFIX + revisions[0], } elif status == 'submitted': try: cln = int(revisions[0]) return { 'base': str(cln - 1), 'tip': str(cln), } except ValueError: raise InvalidRevisionSpecError( '%s does not appear to be a valid changelist' % revisions[0]) else: raise InvalidRevisionSpecError( '%s does not appear to be a valid changelist' % revisions[0]) elif n_revs == 2: result = {} # The base revision must be a submitted CLN status = self._get_changelist_status(revisions[0]) if status == 'submitted': result['base'] = revisions[0] elif status in ('pending', 'shelved'): raise InvalidRevisionSpecError( '%s cannot be used as the base CLN for a diff because ' 'it is %s.' % (revisions[0], status)) else: raise InvalidRevisionSpecError( '%s does not appear to be a valid changelist' % revisions[0]) # Tip revision can be any of submitted, pending, or shelved CLNs status = self._get_changelist_status(revisions[1]) if status == 'submitted': result['tip'] = revisions[1] elif status in ('pending', 'shelved'): raise InvalidRevisionSpecError( '%s cannot be used for a revision range diff because it ' 'is %s' % (revisions[1], status)) else: raise InvalidRevisionSpecError( '%s does not appear to be a valid changelist' % revisions[1]) return result else: raise TooManyRevisionsError
def parse_revision_spec(self, revisions=[]): """Parse the given revision spec. Args: revisions (list of unicode, optional): A list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as ``r1..r2`` or ``r1:r2``. SCMTool-specific overrides of this method are expected to deal with such syntaxes. Raises: rbtools.clients.errors.InvalidRevisionSpecError: The given revisions could not be parsed. rbtools.clients.errors.TooManyRevisionsError: The specified revisions list contained too many revisions. Returns: dict: A dictionary with the following keys: ``base`` (:py:class:`unicode`): A revision to use as the base of the resulting diff. ``tip`` (:py:class:`unicode`): A revision to use as the tip of the resulting diff. ``parent_base`` (:py:class:`unicode`, optional): The revision to use as the base of a parent diff. ``commit_id`` (:py:class:`unicode`, optional): The ID of the single commit being posted, if not using a range. These will be used to generate the diffs to upload to Review Board (or print). The diff for review will include the changes in (base, tip], and the parent diff (if necessary) will include (parent_base, base]. If a single revision is passed in, this will return the parent of that revision for "base" and the passed-in revision for "tip". If zero revisions are passed in, this will return the current HEAD as "tip", and the upstream branch as "base", taking into account parent branches explicitly specified via --parent. """ n_revs = len(revisions) result = {} if n_revs == 0: # No revisions were passed in. Start with HEAD, and find the # tracking branch automatically. head_ref = self._rev_parse(self.get_head_ref())[0] parent_ref = self._rev_parse(self._get_parent_branch())[0] merge_base = self._rev_list_youngest_remote_ancestor( parent_ref, 'origin') result = { 'base': parent_ref, 'tip': head_ref, 'commit_id': head_ref, } if parent_ref != merge_base: result['parent_base'] = merge_base # Since the user asked us to operate on HEAD, warn them about a # dirty working directory. if (self.has_pending_changes() and not self.config.get('SUPPRESS_CLIENT_WARNINGS', False)): logging.warning('Your working directory is not clean. Any ' 'changes which have not been committed ' 'to a branch will not be included in your ' 'review request.') elif n_revs == 1 or n_revs == 2: # Let `git rev-parse` sort things out. parsed = self._rev_parse(revisions) n_parsed_revs = len(parsed) assert n_parsed_revs <= 3 if n_parsed_revs == 1: # Single revision. Extract the parent of that revision to use # as the base. parent = self._rev_parse('%s^' % parsed[0])[0] result = { 'base': parent, 'tip': parsed[0], 'commit_id': parsed[0], } elif n_parsed_revs == 2: if parsed[1].startswith('^'): # Passed in revisions were probably formatted as # "base..tip". The rev-parse output includes all ancestors # of the first part, and none of the ancestors of the # second. Basically, the second part is the base (after # stripping the ^ prefix) and the first is the tip. result = { 'base': parsed[1][1:], 'tip': parsed[0], } else: # First revision is base, second is tip result = { 'base': parsed[0], 'tip': parsed[1], } elif n_parsed_revs == 3 and parsed[2].startswith('^'): # Revision spec is diff-since-merge. Find the merge-base of the # two revs to use as base. merge_base = self._execute([self.git, 'merge-base', parsed[0], parsed[1]]).strip() result = { 'base': merge_base, 'tip': parsed[0], } else: raise InvalidRevisionSpecError( 'Unexpected result while parsing revision spec') parent_base = self._rev_list_youngest_remote_ancestor( result['base'], 'origin') if parent_base != result['base']: result['parent_base'] = parent_base else: raise TooManyRevisionsError return result
def parse_revision_spec(self, revisions): """Parses the given revision spec. The 'revisions' argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". SCMTool-specific overrides of this method are expected to deal with such syntaxes. This will return a dictionary with the following keys: 'base': A revision to use as the base of the resulting diff. 'tip': A revision to use as the tip of the resulting diff. These will be used to generate the diffs to upload to Review Board (or print). There are many different ways to generate diffs for clearcase, because there are so many different workflows. This method serves more as a way to validate the passed-in arguments than actually parsing them in the way that other clients do. """ n_revs = len(revisions) if n_revs == 0: return { 'base': self.REVISION_CHECKEDOUT_BASE, 'tip': self.REVISION_CHECKEDOUT_CHANGESET, } elif n_revs == 1: if revisions[0].startswith(self.REVISION_ACTIVITY_PREFIX): return { 'base': self.REVISION_ACTIVITY_BASE, 'tip': revisions[0][len(self.REVISION_ACTIVITY_PREFIX):], } if revisions[0].startswith(self.REVISION_BRANCH_PREFIX): return { 'base': self.REVISION_BRANCH_BASE, 'tip': revisions[0][len(self.REVISION_BRANCH_PREFIX):], } # TODO: # lbtype:label1 => review changes between this label # and the working directory # stream:streamname[@pvob] => review changes in this UCM stream # (UCM "branch") # baseline:baseline[@pvob] => review changes between this baseline # and the working directory elif n_revs == 2: # TODO: # lbtype:label1 lbtype:label2 => review changes between these two # labels # baseline:baseline1[@pvob] baseline:baseline2[@pvob] # => review changes between these two # baselines pass pairs = [] for r in revisions: p = r.split(':') if len(p) != 2: raise InvalidRevisionSpecError( '"%s" is not a valid file@revision pair' % r) pairs.append(p) return { 'base': self.REVISION_FILES, 'tip': pairs, }
def parse_revision_spec(self, revisions=[]): """Parses the given revision spec. The 'revisions' argument is a list of revisions as specified by the user. Items in the list do not necessarily represent a single revision, since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". SCMTool-specific overrides of this method are expected to deal with such syntaxes. This will return a dictionary with the following keys: 'base': A revision to use as the base of the resulting diff. 'tip': A revision to use as the tip of the resulting diff. These will be used to generate the diffs to upload to Review Board (or print). The diff for review will include the changes in (base, tip]. If a single revision is passed in, this will return the parent of that revision for 'base' and the passed-in revision for 'tip'. If zero revisions are passed in, this will return the most recently checked-out revision for 'base' and a special string indicating the working copy for 'tip'. The SVN SCMClient never fills in the 'parent_base' key. Users who are using other patch-stack tools who want to use parent diffs with SVN will have to generate their diffs by hand. """ n_revisions = len(revisions) if n_revisions == 1 and ':' in revisions[0]: revisions = revisions[0].split(':') n_revisions = len(revisions) if n_revisions == 0: # Most recent checked-out revision -- working copy # TODO: this should warn about mixed-revision working copies that # affect the list of files changed (see bug 2392). return { 'base': 'BASE', 'tip': self.REVISION_WORKING_COPY, } elif n_revisions == 1: # Either a numeric revision (n-1:n) or a changelist revision = revisions[0] try: revision = self._convert_symbolic_revision(revision) return { 'base': revision - 1, 'tip': revision, } except ValueError: # It's not a revision--let's try a changelist. This only makes # sense if we have a working copy. if not self.options.repository_url: status = self._run_svn([ 'status', '--cl', str(revision), '--ignore-externals', '--xml' ], results_unicode=False) cl = ElementTree.fromstring(status).find('changelist') if cl is not None: # TODO: this should warn about mixed-revision working # copies that affect the list of files changed (see # bug 2392). return { 'base': 'BASE', 'tip': self.REVISION_CHANGELIST_PREFIX + revision } raise InvalidRevisionSpecError( '"%s" does not appear to be a valid revision or ' 'changelist name' % revision) elif n_revisions == 2: # Diff between two numeric revisions try: return { 'base': self._convert_symbolic_revision(revisions[0]), 'tip': self._convert_symbolic_revision(revisions[1]), } except ValueError: raise InvalidRevisionSpecError( 'Could not parse specified revisions: %s' % revisions) else: raise TooManyRevisionsError