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, include_identifier=True): if revision: raise self.Exception('Cannot map revisions to commits on GitHub') # Determine the commit data 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 branch = branch or self.default_branch is_default = branch == self.default_branch if is_default and parsed_branch_point: raise self.Exception('Cannot provide a branch point for a commit on the default branch') if is_default: base_count, base_ref = self._count_for_ref(ref=self.default_branch) else: _, base_ref = self._count_for_ref(ref=branch) base_count = self._difference(self.default_branch, base_ref) if identifier > base_count: raise self.Exception('Identifier {} cannot be found on {}'.format(identifier, branch)) # 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') commit_data = self.request('commits/{}~{}'.format(base_ref, base_count - identifier)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}@{}'".format(identifier, branch or 'HEAD')) # If an identifier is negative, unset it so we re-compute before constructing the commit. if identifier <= 0: identifier = None # Determine the commit data 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') commit_data = self.request('commits/{}'.format(branch or tag)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}'".format(branch or tag)) # Determine the commit data for a given hash else: hash = Commit._parse_hash(hash, do_assert=True) commit_data = self.request('commits/{}'.format(hash or self.default_branch)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}'".format(hash or 'HEAD')) # A commit is often on multiple branches, the canonical branch is the one with the highest priority branches = self._branches_for(commit_data['sha']) if branches: branch = self.prioritize_branches(branches) else: # A commit not on any branches cannot have an identifier identifier = None branch = None # Define identifiers on default branch branch_point = None if include_identifier and branch and branch == self.default_branch: if not identifier: result = self._count_for_ref(ref=commit_data['sha']) if not result: raise Exception('{} {}'.format(result, commit_data['sha'])) identifier, _ = result # Define identifiers on branches diverged from the default branch elif include_identifier and branch: if not identifier: identifier = self._difference(self.default_branch, commit_data['sha']) branch_point = self._count_for_ref(ref=commit_data['sha'])[0] - identifier # Check the commit log for a git-svn revision matches = self.GIT_SVN_REVISION.findall(commit_data['commit']['message']) revision = int(matches[-1].split('@')[0]) if matches else None email_match = self.EMAIL_RE.match(commit_data['commit']['author']['email']) timestamp = int(calendar.timegm(datetime.strptime( commit_data['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ', ).timetuple())) # 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 lhash = commit_data['sha'] while lhash: response = self.request('commits', paginate=False, params=dict(sha=lhash, per_page=20)) if len(response) <= 1: break for c in response: if lhash == c['sha']: continue parent_timestamp = int(calendar.timegm(datetime.strptime( c['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ', ).timetuple())) if parent_timestamp != timestamp: lhash = None break lhash = c['sha'] order += 1 return Commit( repository_id=self.id, hash=commit_data['sha'], revision=revision, branch_point=branch_point, identifier=identifier if include_identifier else None, branch=branch, timestamp=timestamp, order=order, author=self.contributors.create( commit_data['commit']['author']['name'], email_match.group('email') if email_match else None, ), message=commit_data['commit']['message'] 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 revision: raise self.Exception('Cannot map revisions to commits on BitBucket') # Determine the commit data 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 branch = branch or self.default_branch is_default = branch == self.default_branch if is_default and parsed_branch_point: raise self.Exception('Cannot provide a branch point for a commit on the default branch') commit_data = self.request('commits/{}'.format(branch), params=dict(limit=1)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}'".format(branch)) base_ref = commit_data['id'] if is_default: base_count = self._distance(base_ref) else: base_count = self._distance(base_ref, magnitude=256, condition=lambda val: self.default_branch not in val) if identifier > base_count: raise self.Exception('Identifier {} cannot be found on {}'.format(identifier, branch)) # 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') commit_data = self.request('commits/{}~{}'.format(base_ref, base_count - identifier)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}@{}'".format(identifier, branch or 'HEAD')) # If an identifier is negative, unset it so we re-compute before constructing the commit. if identifier <= 0: identifier = None # Determine the commit data 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') commit_data = self.request('commits/{}'.format(branch or tag)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}'".format(branch or tag)) # Determine the commit data for a given hash else: hash = Commit._parse_hash(hash, do_assert=True) commit_data = self.request('commits/{}'.format(hash or self.default_branch)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}'".format(hash or 'HEAD')) # A commit is often on multiple branches, the canonical branch is the one with the highest priority branches = self._branches_for(commit_data['id']) if branches: branch = self.prioritize_branches(branches) else: # A commit not on any branches cannot have an identifier identifier = None branch = None # Define identifiers on default branch branch_point = None if include_identifier and branch and branch == self.default_branch: if not identifier: identifier = self._distance(commit_data['id']) # Define identifiers on branches diverged from the default branch elif include_identifier and branch: if not identifier: identifier = self._distance(commit_data['id'], magnitude=256, condition=lambda val: self.default_branch not in val) branch_point = self._distance(commit_data['id']) - identifier # Check the commit log for a git-svn revision matches = self.GIT_SVN_REVISION.findall(commit_data['message']) revision = int(matches[-1].split('@')[0]) if matches else None # 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. timestamp = int(commit_data['committerTimestamp'] / 1000) order = 0 while not identifier or order + 1 < identifier + (branch_point or 0): response = self.request('commits/{}'.format('{}~{}'.format(commit_data['id'], order + 1))) if not response: break parent_timestamp = int(response['committerTimestamp'] / 1000) if parent_timestamp != timestamp: break order += 1 return Commit( repository_id=self.id, hash=commit_data['id'], revision=revision, branch_point=branch_point, identifier=identifier if include_identifier else None, branch=branch, timestamp=timestamp, order=order, author=self.contributors.create( commit_data.get('committer', {}).get('displayName', None), commit_data.get('committer', {}).get('emailAddress', None), ), message=commit_data['message'] if include_log else None, )
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, include_identifier=True): if revision: raise self.Exception('Cannot map revisions to commits on GitHub') 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 branch = branch or self.default_branch is_default = branch == self.default_branch if is_default and parsed_branch_point: raise self.Exception('Cannot provide a branch point for a commit on the default branch') if is_default: base_count, base_ref = self._count_for_ref(ref=self.default_branch) else: _, base_ref = self._count_for_ref(ref=branch) base_count = self._difference(self.default_branch, base_ref) if identifier > base_count: raise self.Exception('Identifier {} cannot be found on {}'.format(identifier, branch)) # 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') commit_data = self.request('commits/{}~{}'.format(base_ref, base_count - identifier)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}@{}'".format(identifier, branch or 'HEAD')) # If an identifier is negative, unset it so we re-compute before constructing the commit. 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') commit_data = self.request('commits/{}'.format(branch or tag)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}'".format(branch or tag)) else: hash = Commit._parse_hash(hash, do_assert=True) commit_data = self.request('commits/{}'.format(hash or self.default_branch)) if not commit_data: raise self.Exception("Failed to retrieve commit information for '{}'".format(hash or 'HEAD')) branches = self._branches_for(commit_data['sha']) if branches: branch = self.prioritize_branches(branches) else: # A commit not on any branches cannot have an identifier identifier = None branch = None branch_point = None if include_identifier and branch and branch == self.default_branch: if not identifier: result = self._count_for_ref(ref=commit_data['sha']) if not result: raise Exception('{} {}'.format(result, commit_data['sha'])) identifier, _ = result elif include_identifier and branch: if not identifier: identifier = self._difference(self.default_branch, commit_data['sha']) branch_point = self._count_for_ref(ref=commit_data['sha'])[0] - identifier matches = self.GIT_SVN_REVISION.findall(commit_data['commit']['message']) revision = int(matches[-1].split('@')[0]) if matches else None email_match = self.EMAIL_RE.match(commit_data['commit']['author']['email']) timestamp = int(calendar.timegm(datetime.strptime( commit_data['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ', ).timetuple())) order = 0 while not identifier or order + 1 < identifier + (branch_point or 0): response = self.request('commits/{}'.format('{}~{}'.format(commit_data['sha'], order + 1))) if not response: break parent_timestamp = int(calendar.timegm(datetime.strptime( response['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ', ).timetuple())) if parent_timestamp != timestamp: break order += 1 return Commit( repository_id=self.id, hash=commit_data['sha'], revision=revision, branch_point=branch_point, identifier=identifier if include_identifier else None, branch=branch, timestamp=timestamp, order=order, author=self.contributors.create( commit_data['commit']['author']['name'], email_match.group('email') if email_match else None, ), message=commit_data['commit']['message'] if include_log else None, )
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, )
def test_parse_identifier(self): self.assertEqual((None, 1234, None), Commit._parse_identifier('1234')) self.assertEqual((None, 1234, None), Commit._parse_identifier(1234)) self.assertEqual((None, 1234, None), Commit._parse_identifier('1234@')) self.assertEqual((None, 1234, 'main'), Commit._parse_identifier('1234@main')) self.assertEqual((None, 1234, 'eng/bug'), Commit._parse_identifier('1234@eng/bug')) self.assertEqual((1234, 1, 'eng/bug'), Commit._parse_identifier('1234.1@eng/bug')) self.assertEqual((None, 0, None), Commit._parse_identifier('0')) self.assertEqual((None, -1, None), Commit._parse_identifier('-1')) self.assertEqual((None, 0, 'eng/bug'), Commit._parse_identifier('0@eng/bug')) self.assertEqual((None, -1, 'eng/bug'), Commit._parse_identifier('-1@eng/bug')) self.assertEqual(None, Commit._parse_identifier('1234-invalid')) self.assertEqual(None, Commit._parse_identifier('r266896')) self.assertEqual(None, Commit._parse_identifier('c3bd784f8b88bd03')) self.assertEqual(None, Commit._parse_identifier(3.141592))