def test_parse_revision(self): self.assertEqual(266896, Commit._parse_revision('r266896')) self.assertEqual(266896, Commit._parse_revision('R266896')) self.assertEqual(266896, Commit._parse_revision('266896')) self.assertEqual(266896, Commit._parse_revision(266896)) self.assertEqual(None, Commit._parse_revision('c3bd784f8b88bd03')) self.assertEqual(None, Commit._parse_revision('0')) self.assertEqual(None, Commit._parse_revision('-1')) self.assertEqual(None, Commit._parse_revision('3.141592')) self.assertEqual(None, Commit._parse_revision(3.141592))
def info(self, branch=None, revision=None, tag=None): if tag and branch: raise ValueError('Cannot specify both branch and tag') if tag and revision: raise ValueError('Cannot specify both branch and tag') revision = Commit._parse_revision(revision) if branch and branch != self.default_branch and '/' not in branch: branch = 'branches/{}'.format(branch) additional_args = [ '^/{}'.format(branch) ] if branch and branch != self.default_branch else [] additional_args += ['^/tags/{}'.format(tag)] if tag else [] additional_args += ['-r', str(revision)] if revision else [] info_result = run([self.executable(), 'info'] + additional_args, cwd=self.root_path, capture_output=True, encoding='utf-8') if info_result.returncode: return {} result = {} for line in info_result.stdout.splitlines(): split = line.split(': ') result[split[0]] = ': '.join(split[1:]) return result
def commit(self, hash=None, revision=None, identifier=None, branch=None, tag=None, include_log=True, include_identifier=True): # Only git-svn checkouts can convert revisions to fully qualified commits if revision and not self.is_svn: raise self.Exception( 'This git checkout does not support SVN revisions') # Determine the hash for a provided Subversion revision elif revision: if hash: raise ValueError('Cannot define both hash and revision') revision = Commit._parse_revision(revision, do_assert=True) revision_log = run( [self.executable(), 'svn', 'find-rev', 'r{}'.format(revision)], cwd=self.root_path, capture_output=True, encoding='utf-8', timeout=3, ) if revision_log.returncode: raise self.Exception( "Failed to retrieve commit information for 'r{}'".format( revision)) hash = revision_log.stdout.rstrip() if not hash: raise self.Exception("Failed to find 'r{}'".format(revision)) default_branch = self.default_branch parsed_branch_point = None log_format = ['-1'] if include_log else ['-1', '--format=short'] # Determine the `git log` output and branch for a given identifier if identifier is not None: if revision: raise ValueError('Cannot define both revision and identifier') if hash: raise ValueError('Cannot define both hash and identifier') if tag: raise ValueError('Cannot define both tag and identifier') parsed_branch_point, identifier, parsed_branch = Commit._parse_identifier( identifier, do_assert=True) if parsed_branch: if branch and branch != parsed_branch: raise ValueError( "Caller passed both 'branch' and 'identifier', but specified different branches ({} and {})" .format( branch, parsed_branch, ), ) branch = parsed_branch baseline = branch or 'HEAD' is_default = baseline == default_branch if baseline == 'HEAD': is_default = default_branch in self._branches_for(baseline) if is_default and parsed_branch_point: raise self.Exception( 'Cannot provide a branch point for a commit on the default branch' ) base_count = self._commit_count( baseline if is_default else '{}..{}'. format(default_branch, baseline)) if identifier > base_count: raise self.Exception( 'Identifier {} cannot be found on the specified branch in the current checkout' .format(identifier)) log = run( [ self.executable(), 'log', '{}~{}'.format( branch or 'HEAD', base_count - identifier) ] + log_format, cwd=self.root_path, capture_output=True, encoding='utf-8', ) if log.returncode: raise self.Exception( "Failed to retrieve commit information for 'i{}@{}'". format(identifier, branch or 'HEAD')) # Negative identifiers are actually commits on the default branch, we will need to re-compute the identifier if identifier < 0 and is_default: raise self.Exception( 'Illegal negative identifier on the default branch') if identifier < 0: identifier = None # Determine the `git log` output for a given branch or tag elif branch or tag: if hash: raise ValueError('Cannot define both tag/branch and hash') if branch and tag: raise ValueError('Cannot define both tag and branch') log = run([self.executable(), 'log', branch or tag] + log_format, cwd=self.root_path, capture_output=True, encoding='utf-8') if log.returncode: raise self.Exception( "Failed to retrieve commit information for '{}'".format( branch or tag)) # Determine the `git log` output for a given hash else: hash = Commit._parse_hash(hash, do_assert=True) log = run([self.executable(), 'log', hash or 'HEAD'] + log_format, cwd=self.root_path, capture_output=True, encoding='utf-8') if log.returncode: raise self.Exception( "Failed to retrieve commit information for '{}'".format( hash or 'HEAD')) # Fully define the hash from the `git log` output match = self.GIT_COMMIT.match(log.stdout.splitlines()[0]) if not match: raise self.Exception('Invalid commit hash in git log') hash = match.group('hash') # A commit is often on multiple branches, the canonical branch is the one with the highest priority branch = self.prioritize_branches(self._branches_for(hash)) # Compute the identifier if the function did not receive one and we were asked to if not identifier and include_identifier: identifier = self._commit_count( hash if branch == default_branch else '{}..{}'.format(default_branch, hash)) # Only compute the branch point we're on something other than the default branch branch_point = None if not include_identifier or branch == default_branch else self._commit_count( hash) - identifier if branch_point and parsed_branch_point and branch_point != parsed_branch_point: raise ValueError( "Provided 'branch_point' does not match branch point of specified branch" ) # Check the commit log for a git-svn revision logcontent = '\n'.join(line[4:] for line in log.stdout.splitlines()[4:]) matches = self.GIT_SVN_REVISION.findall(logcontent) revision = int(matches[-1].split('@')[0]) if matches else None # We only care about when a commit was commited commit_time = run( [self.executable(), 'show', '-s', '--format=%ct', hash], cwd=self.root_path, capture_output=True, encoding='utf-8', ) if commit_time.returncode: raise self.Exception( 'Failed to retrieve commit time for {}'.format(hash)) timestamp = int(commit_time.stdout.lstrip()) # Comparing commits in different repositories involves comparing timestamps. This is problematic because it git, # it's possible for a series of commits to share a commit time. To handle this case, we assign each commit a # zero-indexed "order" within it's timestamp. order = 0 while not identifier or order + 1 < identifier + (branch_point or 0): commit_time = run( [ self.executable(), 'show', '-s', '--format=%ct', '{}~{}'.format(hash, order + 1) ], cwd=self.root_path, capture_output=True, encoding='utf-8', ) if commit_time.returncode: break if int(commit_time.stdout.lstrip()) != timestamp: break order += 1 return Commit( repository_id=self.id, hash=hash, revision=revision, identifier=identifier if include_identifier else None, branch_point=branch_point, branch=branch, timestamp=timestamp, order=order, author=Contributor.from_scm_log(log.stdout.splitlines()[1], self.contributors), message=logcontent if include_log else None, )
def commit(self, hash=None, revision=None, identifier=None, branch=None, tag=None, include_log=True): if hash: raise ValueError('SVN does not support Git hashes') parsed_branch_point = None if identifier is not None: if revision: raise ValueError('Cannot define both revision and identifier') if tag: raise ValueError('Cannot define both tag and identifier') parsed_branch_point, identifier, parsed_branch = Commit._parse_identifier(identifier, do_assert=True) if parsed_branch: if branch and branch != parsed_branch: raise ValueError( "Caller passed both 'branch' and 'identifier', but specified different branches ({} and {})".format( branch, parsed_branch, ), ) branch = parsed_branch branch = branch or self.branch if branch == self.default_branch and parsed_branch_point: raise self.Exception('Cannot provide a branch point for a commit on the default branch') if not self._metadata_cache.get(branch, []) or identifier >= len(self._metadata_cache.get(branch, [])): if branch != self.default_branch: self._cache_revisions(branch=self.default_branch) self._cache_revisions(branch=branch) if identifier > len(self._metadata_cache.get(branch, [])): raise self.Exception('Identifier {} cannot be found on the specified branch in the current checkout'.format(identifier)) if identifier <= 0: if branch == self.default_branch: raise self.Exception('Illegal negative identifier on the default branch') identifier = self._commit_count(branch=branch) + identifier if identifier < 0: raise self.Exception('Identifier does not exist on the specified branch') branch = self.default_branch revision = self._metadata_cache[branch][identifier] info = self.info(cached=True, branch=branch, revision=revision) branch = self._branch_for(revision) if not self._metadata_cache.get(branch, []) or identifier >= len(self._metadata_cache.get(branch, [])): self._cache_revisions(branch=branch) elif revision: if branch: raise ValueError('Cannot define both branch and revision') if tag: raise ValueError('Cannot define both tag and revision') revision = Commit._parse_revision(revision, do_assert=True) branch = self._branch_for(revision) info = self.info(cached=True, revision=revision) else: if branch and tag: raise ValueError('Cannot define both branch and tag') branch = None if tag else branch or self.branch info = self.info(tag=tag) if tag else self.info(branch=branch) if not info: raise self.Exception("'{}' is not a recognized {}".format( tag or branch, 'tag' if tag else 'branch', )) revision = int(info['Last Changed Rev']) if branch != self.default_branch: branch = self._branch_for(revision) date = info['Last Changed Date'].split(' (')[0] tz_diff = date.split(' ')[-1] date = datetime.strptime(date[:-len(tz_diff)], '%Y-%m-%d %H:%M:%S ') date += timedelta( hours=int(tz_diff[1:3]), minutes=int(tz_diff[3:5]), ) * (1 if tz_diff[0] == '-' else -1) if not identifier: if branch != self.default_branch and revision > self._metadata_cache.get(self.default_branch, [0])[-1]: self._cache_revisions(branch=self.default_branch) if revision not in self._metadata_cache.get(branch, []): self._cache_revisions(branch=branch) identifier = self._commit_count(revision=revision, branch=branch) branch_point = None if branch == self.default_branch else self._commit_count(branch=branch) if branch_point and parsed_branch_point and branch_point != parsed_branch_point: raise ValueError("Provided 'branch_point' does not match branch point of specified branch") if branch == self.default_branch or '/' in branch: branch_arg = '^/{}'.format(branch) else: branch_arg = '^/branches/{}'.format(branch) log = run( [self.executable(), 'log', '-l', '1', '-r', str(revision), branch_arg], cwd=self.root_path, capture_output=True, encoding='utf-8', ) if include_log else None split_log = log.stdout.splitlines() if log else [] if log and (not log.returncode or len(split_log) >= 3): author_line = split_log[1] for line in split_log[2:8]: if Contributor.SVN_PATCH_FROM_RE.match(line): author_line = line break author = Contributor.from_scm_log(author_line, self.contributors) message = '\n'.join(split_log[3:-1]) else: if include_log: self.log('Failed to connect to remote, cannot compute commit message') email = info.get('Last Changed Author') author = self.contributors.create(email, email) if '@' in email else self.contributors.create(email) message = None return Commit( revision=int(revision), branch=branch, identifier=identifier, branch_point=branch_point, timestamp=int(calendar.timegm(date.timetuple())), author=author, message=message, )
def commit(self, hash=None, revision=None, identifier=None, branch=None, tag=None, include_log=True, include_identifier=True): if hash: raise ValueError('SVN does not support Git hashes') parsed_branch_point = None if identifier is not None: if revision: raise ValueError('Cannot define both revision and identifier') if tag: raise ValueError('Cannot define both tag and identifier') parsed_branch_point, identifier, parsed_branch = Commit._parse_identifier( identifier, do_assert=True) if parsed_branch: if branch and branch != parsed_branch: raise ValueError( "Caller passed both 'branch' and 'identifier', but specified different branches ({} and {})" .format( branch, parsed_branch, ), ) branch = parsed_branch branch = branch or self.default_branch if branch == self.default_branch and parsed_branch_point: raise self.Exception( 'Cannot provide a branch point for a commit on the default branch' ) if not self._metadata_cache.get(branch, []) or identifier >= len( self._metadata_cache.get(branch, [])): if branch != self.default_branch: self._cache_revisions(branch=self.default_branch) self._cache_revisions(branch=branch) if identifier > len(self._metadata_cache.get(branch, [])): raise self.Exception( 'Identifier {} cannot be found on the specified branch in the current checkout' .format(identifier)) if identifier <= 0: if branch == self.default_branch: raise self.Exception( 'Illegal negative identifier on the default branch') identifier = self._commit_count(branch=branch) + identifier if identifier < 0: raise self.Exception( 'Identifier does not exist on the specified branch') branch = self.default_branch revision = self._metadata_cache[branch][identifier] info = self.info(cached=True, branch=branch, revision=revision) branch = self._branch_for(revision) if not self._metadata_cache.get(branch, []) or identifier >= len( self._metadata_cache.get(branch, [])): self._cache_revisions(branch=branch) elif revision: if branch: raise ValueError('Cannot define both branch and revision') if tag: raise ValueError('Cannot define both tag and revision') revision = Commit._parse_revision(revision, do_assert=True) branch = self._branch_for(revision) or self.default_branch info = self.info(cached=True, branch=branch, revision=revision) else: if branch and tag: raise ValueError('Cannot define both branch and tag') branch = None if tag else branch or self.default_branch info = self.info(tag=tag) if tag else self.info(branch=branch) if not info: raise self.Exception("'{}' is not a recognized {}".format( tag or branch, 'tag' if tag else 'branch', )) revision = int(info['Last Changed Rev']) if branch != self.default_branch: branch = self._branch_for(revision) date = datetime.strptime( info['Last Changed Date'], '%Y-%m-%d %H:%M:%S') if info.get('Last Changed Date') else None if include_identifier and not identifier: if branch != self.default_branch and revision > self._metadata_cache.get( self.default_branch, [0])[-1]: self._cache_revisions(branch=self.default_branch) if revision not in self._metadata_cache.get(branch, []): self._cache_revisions(branch=branch) identifier = self._commit_count(revision=revision, branch=branch) branch_point = None if not include_identifier or branch == self.default_branch else self._commit_count( branch=branch) if branch_point and parsed_branch_point and branch_point != parsed_branch_point: raise ValueError( "Provided 'branch_point' does not match branch point of specified branch" ) response = requests.request( method='REPORT', url='{}!svn/rvr/{}'.format(self.url, revision), headers={ 'Content-Type': 'text/xml', 'Accept-Encoding': 'gzip', 'DEPTH': '1', }, data='<S:log-report xmlns:S="svn:">\n' '<S:start-revision>{revision}</S:start-revision>\n' '<S:end-revision>{revision}</S:end-revision>\n' '<S:limit>1</S:limit>\n' '</S:log-report>\n'.format(revision=revision), ) if include_log else None if response and response.status_code == 200: response = xmltodict.parse(response.text) response = response.get('S:log-report', {}).get('S:log-item') name = response.get('D:creator-displayname') message = response.get('D:comment', None) else: if include_log: self.log( 'Failed to connect to remote, cannot compute commit message' ) message = None name = info.get('Last Changed Author') author = self.contributors.create( name, name) if name and '@' in name else self.contributors.create(name) return Commit( repository_id=self.id, revision=int(revision), branch=branch, identifier=identifier if include_identifier else None, branch_point=branch_point, timestamp=int(calendar.timegm(date.timetuple())) if date else None, author=author, message=message, )
def commit(self, hash=None, revision=None, identifier=None, branch=None, tag=None, include_log=True): if revision and not self.is_svn: raise self.Exception( 'This git checkout does not support SVN revisions') elif revision: if hash: raise ValueError('Cannot define both hash and revision') revision = Commit._parse_revision(revision, do_assert=True) revision_log = run( [self.executable(), 'svn', 'find-rev', 'r{}'.format(revision)], cwd=self.root_path, capture_output=True, encoding='utf-8', timeout=3, ) if revision_log.returncode: raise self.Exception( "Failed to retrieve commit information for 'r{}'".format( revision)) hash = revision_log.stdout.rstrip() if not hash: raise self.Exception("Failed to find 'r{}'".format(revision)) default_branch = self.default_branch parsed_branch_point = None log_format = ['-1'] if include_log else ['-1', '--format=short'] if identifier is not None: if revision: raise ValueError('Cannot define both revision and identifier') if hash: raise ValueError('Cannot define both hash and identifier') if tag: raise ValueError('Cannot define both tag and identifier') parsed_branch_point, identifier, parsed_branch = Commit._parse_identifier( identifier, do_assert=True) if parsed_branch: if branch and branch != parsed_branch: raise ValueError( "Caller passed both 'branch' and 'identifier', but specified different branches ({} and {})" .format( branch, parsed_branch, ), ) branch = parsed_branch baseline = branch or 'HEAD' is_default = baseline == default_branch if baseline == 'HEAD': is_default = default_branch in self._branches_for(baseline) if is_default and parsed_branch_point: raise self.Exception( 'Cannot provide a branch point for a commit on the default branch' ) base_count = self._commit_count( baseline if is_default else '{}..{}'. format(default_branch, baseline)) if identifier > base_count: raise self.Exception( 'Identifier {} cannot be found on the specified branch in the current checkout' .format(identifier)) log = run( [ self.executable(), 'log', '{}~{}'.format( branch or 'HEAD', base_count - identifier) ] + log_format, cwd=self.root_path, capture_output=True, encoding='utf-8', ) if log.returncode: raise self.Exception( "Failed to retrieve commit information for 'i{}@{}'". format(identifier, branch or 'HEAD')) # Negative identifiers are actually commits on the default branch, we will need to re-compute the identifier if identifier < 0 and is_default: raise self.Exception( 'Illegal negative identifier on the default branch') if identifier < 0: identifier = None elif branch or tag: if hash: raise ValueError('Cannot define both tag/branch and hash') if branch and tag: raise ValueError('Cannot define both tag and branch') log = run([self.executable(), 'log', branch or tag] + log_format, cwd=self.root_path, capture_output=True, encoding='utf-8') if log.returncode: raise self.Exception( "Failed to retrieve commit information for '{}'".format( branch or tag)) else: hash = Commit._parse_hash(hash, do_assert=True) log = run([self.executable(), 'log', hash or 'HEAD'] + log_format, cwd=self.root_path, capture_output=True, encoding='utf-8') if log.returncode: raise self.Exception( "Failed to retrieve commit information for '{}'".format( hash or 'HEAD')) match = self.GIT_COMMIT.match(log.stdout.splitlines()[0]) if not match: raise self.Exception('Invalid commit hash in git log') hash = match.group('hash') branch = self.prioritize_branches(self._branches_for(hash)) if not identifier: identifier = self._commit_count( hash if branch == default_branch else '{}..{}'.format(default_branch, hash)) branch_point = None if branch == default_branch else self._commit_count( hash) - identifier if branch_point and parsed_branch_point and branch_point != parsed_branch_point: raise ValueError( "Provided 'branch_point' does not match branch point of specified branch" ) match = self.GIT_SVN_REVISION.search(log.stdout) revision = int(match.group('revision')) if match else None commit_time = run( [self.executable(), 'show', '-s', '--format=%ct', hash], cwd=self.root_path, capture_output=True, encoding='utf-8', ) if commit_time.returncode: raise self.Exception( 'Failed to retrieve commit time for {}'.format(hash)) return Commit( hash=hash, revision=revision, identifier=identifier, branch_point=branch_point, branch=branch, timestamp=int(commit_time.stdout.lstrip()), author=Contributor.from_scm_log(log.stdout.splitlines()[1], self.contributors), message='\n'.join(line[4:] for line in log.stdout.splitlines()[4:]) if include_log else None, )