class Svn(Scm): LOG_RE = re.compile(r'r(?P<revision>\d+) \| (?P<email>.*) \| (?P<date>.*)') CACHE_VERSION = Version(1) @classmethod @decorators.Memoize() def executable(cls): return Scm.executable('svn') @classmethod def is_checkout(cls, path): return run([cls.executable(), 'info'], cwd=path, capture_output=True).returncode == 0 def __init__(self, path, dev_branches=None, prod_branches=None, contributors=None): super(Svn, self).__init__(path, dev_branches=dev_branches, prod_branches=prod_branches, contributors=contributors) self._root_path = self.path self._root_path = self.info(cached=False).get('Working Copy Root Path') if not self.root_path: raise OSError('Provided path {} is not a svn repository'.format(path)) if os.path.exists(self._cache_path): try: with open(self._cache_path) as file: self._metadata_cache = json.load(file) except BaseException: self._metadata_cache = dict(version=str(self.CACHE_VERSION)) raise else: self._metadata_cache = dict(version=str(self.CACHE_VERSION)) @decorators.Memoize(cached=False) 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 @property def is_svn(self): return True @property def root_path(self): return self._root_path @property def default_branch(self): return 'trunk' @property def branch(self): local_path = self.path[len(self.root_path):] if local_path: return self.info()['Relative URL'][2:-len(local_path)] return self.info()['Relative URL'][2:] def list(self, category): list_result = run([self.executable(), 'list', '^/{}'.format(category)], cwd=self.root_path, capture_output=True, encoding='utf-8') if list_result.returncode: return [] return [element.rstrip('/') for element in list_result.stdout.splitlines()] @property def branches(self): return ['trunk'] + self.list('branches') @property def tags(self): return self.list('tags') @property def _cache_path(self): return os.path.join(self.root_path, '.svn', 'webkitscmpy-cache.json') def _cache_revisions(self, branch=None): branch = branch or self.default_branch is_default_branch = branch == self.default_branch if branch not in self._metadata_cache: self._metadata_cache[branch] = [0] if is_default_branch else [] pos = len(self._metadata_cache[branch]) # If we aren't on the default branch, we will need the default branch to determine when # our branch intersects with the default branch. if not is_default_branch: self._cache_revisions(branch=self.default_branch) try: did_warn = False count = 0 log = None if is_default_branch or '/' in branch: branch_arg = '^/{}'.format(branch) else: branch_arg = '^/branches/{}'.format(branch) kwargs = dict() if sys.version_info >= (3, 0): kwargs = dict(encoding='utf-8') log = subprocess.Popen( [self.executable(), 'log', '-q', branch_arg], cwd=self.root_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs ) if log.poll(): raise self.Exception("Failed to construct branch history for '{}'".format(branch)) line = log.stdout.readline() while line: match = self.LOG_RE.match(line) if match: if not did_warn: count += 1 if count > 1000: self.log('Caching commit data for {}, this will take a few minutes...'.format(branch)) did_warn = True revision = int(match.group('revision')) if pos > 0 and self._metadata_cache[branch][pos - 1] == revision: break if not is_default_branch: if revision in self._metadata_cache[self.default_branch]: self._metadata_cache[branch].insert(pos, revision) break self._metadata_cache[branch].insert(pos, revision) line = log.stdout.readline() finally: if log: log.kill() if self._metadata_cache[self.default_branch][0] == [0]: self._metadata_cache['identifier'] = len(self._metadata_cache[branch]) try: with open(self._cache_path, 'w') as file: json.dump(self._metadata_cache, file, indent=4) except (IOError, OSError): self.log("Failed to write SVN cache to '{}'".format(self._cache_path)) return self._metadata_cache[branch] def _commit_count(self, revision=None, branch=None): branch = branch or self.default_branch if revision: if revision not in self._metadata_cache[branch]: raise self.Exception("Failed to find '{}' on '{}'".format(revision, branch)) return bisect.bisect_left(self._metadata_cache[branch], int(revision)) if branch == self.default_branch: return len(self._metadata_cache[branch]) return self._commit_count(revision=self._metadata_cache[branch][0], branch=self.default_branch) def remote(self, name=None): return self.info(cached=True)['Repository Root'] def _branch_for(self, revision): candidates = [branch for branch, revisions in self._metadata_cache.items() if branch != 'version' and revision in revisions] candidate = self.prioritize_branches(candidates) if candidates else None # In the default branch case, we don't even need to ask the remote if candidate == self.default_branch: return candidate process = run( [self.executable(), 'log', '-v', '-q', self.remote(), '-r', str(revision), '-l', '1'], cwd=self.root_path, capture_output=True, encoding='utf-8', ) # If we didn't get a valid answer from the remote, but we found a matching candidate, we return that. # This is a bit risky because there is a chance the branch we have cached is not the canonical branch # for a revision, but this is pretty unlikely because it would require the n + 1 level branch to be cached # but not the n level branch. if process.returncode or not process.stdout: if candidate: return candidate raise self.Exception("Failed to retrieve branch for '{}'".format(revision)) partial = None for line in process.stdout.splitlines(): if partial is None and line == 'Changed paths:': partial = '' elif partial == '': partial = line.lstrip()[2:] elif partial: line = line.lstrip() while line.startswith(('A ', 'D ', 'M ')) and not line[2:].startswith(partial): partial = partial[:-1] if len(partial) <= 3: raise self.Exception('Malformed set of edited files') partial = partial.split(' ')[0] candidate = partial.split('/')[2 if partial.startswith('/branches') else 1] # Tags are a unique case for SVN, because they're treated as branches in native SVN if candidate == 'tags': return partial[1:].rstrip('/') return candidate 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 checkout(self, argument): commit = self.find(argument) if not commit: return None command = [self.executable(), 'up', '-r', str(commit.revision)] if log.level > logging.WARNING: command.append('-q') return None if run(command, cwd=self.root_path).returncode else commit
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import sys from webkitscmpy import AutoInstall, Package, Version if sys.version_info[0] >= 3: AutoInstall.register(Package('autobahn', Version(20, 7, 1))) AutoInstall.register(Package('buildbot', Version(2, 8, 4))) AutoInstall.register( Package('dateutil', Version(2, 8, 1), pypi_name='python-dateutil')) AutoInstall.register( Package('jinja2', Version(2, 11, 2), pypi_name='Jinja2')) AutoInstall.register(Package('jwt', Version(1, 7, 1), pypi_name='PyJWT')) AutoInstall.register( Package('pyyaml', Version(5, 3, 1), pypi_name='PyYAML')) AutoInstall.register( Package('sqlalchemy', Version(1, 3, 20), pypi_name='SQLAlchemy')) AutoInstall.register(Package('sqlalchemy-migrate', Version(0, 13, 0))) AutoInstall.register( Package('twisted', Version(20, 3, 0), pypi_name='Twisted')) AutoInstall.register(Package('txaio', Version(20, 4, 1)))
class Svn(Scm): URL_RE = re.compile(r'\Ahttps?://svn.(?P<host>\S+)/repository/\S+\Z') DATA_RE = re.compile(br'<[SD]:(?P<tag>\S+)>(?P<content>.*)</[SD]:.+>') CACHE_VERSION = Version(1) @classmethod def is_webserver(cls, url): return True if cls.URL_RE.match(url) else False def __init__(self, url, dev_branches=None, prod_branches=None, contributors=None, id=None): if url[-1] != '/': url += '/' if not self.is_webserver(url): raise self.Exception( "'{}' is not a valid SVN webserver".format(url)) super(Svn, self).__init__( url, dev_branches=dev_branches, prod_branches=prod_branches, contributors=contributors, id=id or url.split('/')[-2].lower(), ) if os.path.exists(self._cache_path): try: with self._cache_lock(), open(self._cache_path) as file: self._metadata_cache = json.load(file) except BaseException: self._metadata_cache = dict(version=str(self.CACHE_VERSION)) else: self._metadata_cache = dict(version=str(self.CACHE_VERSION)) @property def is_svn(self): return True @decorators.Memoize(timeout=60) def _latest(self): response = requests.request( method='OPTIONS', url=self.url, headers={ 'Content-Type': 'text/xml', 'Accept-Encoding': 'gzip', 'DEPTH': '1', }, data='<?xml version="1.0" encoding="utf-8"?>\n' '<D:options xmlns:D="DAV:">\n' ' <D:activity-collection-set></D:activity-collection-set>\n' '</D:options>\n', ) if response.status_code != 200: return None return int(response.headers.get('SVN-Youngest-Rev')) @decorators.Memoize(cached=False) 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') if not revision: branch = branch or self.default_branch revision = self._latest() if not revision: return None if not revision: raise ValueError('Failed to find the latest revision') url = '{}!svn/rvr/{}'.format(self.url, revision) if branch and branch != self.default_branch and '/' not in branch: url = '{}/branches/{}'.format(url, branch) elif tag: url = '{}/tags/{}'.format(url, tag) elif branch: url = '{}/{}'.format(url, branch or self.default_branch) response = requests.request( method='PROPFIND', url=url, headers={ 'Content-Type': 'text/xml', 'Accept-Encoding': 'gzip', 'DEPTH': '1', }, data='<propfind xmlns="DAV:">\n' ' <prop>\n' ' <resourcetype xmlns="DAV:"/>\n' ' <getcontentlength xmlns="DAV:"/>\n' ' <deadprop-count xmlns="http://subversion.tigris.org/xmlns/dav/"/>\n' ' <version-name xmlns="DAV:"/>\n' ' <creationdate xmlns="DAV:"/>\n' ' <creator-displayname xmlns="DAV:"/>\n' ' </prop>\n' '</propfind>\n', ) if response.status_code not in [200, 207]: return {} response = xmltodict.parse(response.text) response = response.get('D:multistatus', response).get('D:response', []) if not response: return {} response = response[0] if isinstance(response, list) else response response = response['D:propstat'][0]['D:prop'] return { 'Last Changed Rev': response['lp1:version-name'], 'Last Changed Author': response.get('lp1:creator-displayname'), 'Last Changed Date': ' '.join(response['lp1:creationdate'].split('T')).split('.')[0], 'Revision': revision, } @property def default_branch(self): return 'trunk' def list(self, category): revision = self._latest() if not revision: return [] response = requests.request( method='PROPFIND', url='{}!svn/rvr/{}/{}'.format(self.url, revision, category), headers={ 'Content-Type': 'text/xml', 'Accept-Encoding': 'gzip', 'DEPTH': '1', }, data='<?xml version="1.0" encoding="utf-8"?>\n' '<propfind xmlns="DAV:">\n' ' <prop><resourcetype xmlns="DAV:"/></prop>\n' '</propfind>\n', ) if response.status_code not in [200, 207]: return [] responses = xmltodict.parse(response.text) responses = responses.get('D:multistatus', responses).get('D:response', []) results = [] for response in responses: candidate = response['D:href'].split('!svn/rvr/{}/{}/'.format( revision, category))[-1].rstrip('/') if not candidate: continue results.append(candidate) return results @property def branches(self): return [self.default_branch] + self.list('branches') @property def tags(self): return self.list('tags') @property @decorators.Memoize() def _cache_path(self): from webkitscmpy.mocks import remote host = 'svn.{}'.format(self.URL_RE.match(self.url).group('host')) if host in remote.Svn.remotes: host = 'mock-{}'.format(host) return os.path.join(tempfile.gettempdir(), host, 'webkitscmpy-cache.json') def _cache_lock(self): return fasteners.InterProcessLock( os.path.join(os.path.dirname(self._cache_path), 'cache.lock')) def _cache_revisions(self, branch=None): branch = branch or self.default_branch is_default_branch = branch == self.default_branch if branch not in self._metadata_cache: self._metadata_cache[branch] = [0] if is_default_branch else [] pos = len(self._metadata_cache[branch]) # If we aren't on the default branch, we will need the default branch to determine when # our branch intersects with the default branch. if not is_default_branch: self._cache_revisions(branch=self.default_branch) did_warn = False count = 0 latest = self._latest() with requests.request( method='REPORT', url='{}!svn/rvr/{}/{}'.format( self.url, latest, branch if is_default_branch or '/' in branch else 'branches/{}'.format(branch), ), stream=True, 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>0</S:end-revision>\n' '<S:path></S:path>\n' '</S:log-report>\n'.format(revision=latest), ) as response: if response.status_code != 200: raise self.Exception( "Failed to construct branch history for '{}'".format( branch)) default_count = 0 for line in response.iter_lines(): match = self.DATA_RE.match(line) if not match or match.group('tag') != b'version-name': continue if not did_warn: count += 1 if count > 1000: self.log( 'Caching commit data for {}, this will take a few minutes...' .format(branch)) did_warn = True revision = int(match.group('content')) if pos > 0 and self._metadata_cache[branch][pos - 1] == revision: break if not is_default_branch: if revision in self._metadata_cache[self.default_branch]: # Only handle 2 sequential cross-branch commits if default_count > 2: break default_count += 1 else: default_count = 0 self._metadata_cache[branch].insert(pos, revision) if default_count: self._metadata_cache[branch] = self._metadata_cache[branch][ default_count - 1:] if self._metadata_cache[self.default_branch][0] == [0]: self._metadata_cache['identifier'] = len( self._metadata_cache[branch]) try: if not os.path.isdir(os.path.dirname(self._cache_path)): os.makedirs(os.path.dirname(self._cache_path)) with self._cache_lock(), open(self._cache_path, 'w') as file: json.dump(self._metadata_cache, file, indent=4) except (IOError, OSError): self.log("Failed to write SVN cache to '{}'".format( self._cache_path)) return self._metadata_cache[branch] def _branch_for(self, revision): 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:discover-changed-paths/>\n' '</S:log-report>\n'.format(revision=revision), ) # If we didn't get a valid answer from the remote, but we found a matching candidate, we return that. # This is a bit risky because there is a chance the branch we have cached is not the canonical branch # for a revision, but this is pretty unlikely because it would require the n + 1 level branch to be cached # but not the n level branch. if response.status_code != 200: raise self.Exception( "Failed to retrieve branch for '{}'".format(revision)) partial = None items = xmltodict.parse(response.text)['S:log-report']['S:log-item'] for group in (items.get('S:modified-path', []), items.get('S:added-path', []), items.get('S:deleted-path', [])): for item in group if isinstance(group, list) else [group]: if not partial: partial = item['#text'] while not item['#text'].startswith(partial): partial = partial[:-1] candidate = partial.split( '/')[2 if partial.startswith('/branches') else 1] # Tags are a unique case for SVN, because they're treated as branches in native SVN if candidate == 'tags': return partial[1:].rstrip('/') return candidate def _commit_count(self, revision=None, branch=None): branch = branch or self.default_branch if revision: if revision not in self._metadata_cache[branch]: raise self.Exception("Failed to find '{}' on '{}'".format( revision, branch)) return bisect.bisect_left(self._metadata_cache[branch], int(revision)) if branch == self.default_branch: return len(self._metadata_cache[branch]) return self._commit_count(revision=self._metadata_cache[branch][0], branch=self.default_branch) 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 _args_from_content(self, content, include_log=True): xml = xmltodict.parse(content) date = datetime.strptime( string_utils.decode(xml['S:log-item']['S:date']).split('.')[0], '%Y-%m-%dT%H:%M:%S') name = string_utils.decode(xml['S:log-item']['D:creator-displayname']) return dict( revision=int(xml['S:log-item']['D:version-name']), author=self.contributors.create(name, name) if name and '@' in name else self.contributors.create(name), timestamp=int(calendar.timegm(date.timetuple())), message=string_utils.decode(xml['S:log-item']['D:comment']) if include_log else None, ) def commits(self, begin=None, end=None, include_log=True, include_identifier=True): begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier) previous = end content = b'' with requests.request( method='REPORT', url='{}!svn/rvr/{}/{}'.format( self.url, end.revision, end.branch if end.branch == self.default_branch or '/' in end.branch else 'branches/{}'.format(end.branch), ), stream=True, headers={ 'Content-Type': 'text/xml', 'Accept-Encoding': 'gzip', 'DEPTH': '1', }, data='<S:log-report xmlns:S="svn:">\n' '<S:start-revision>{end}</S:start-revision>\n' '<S:end-revision>{begin}</S:end-revision>\n' '<S:path></S:path>\n' '</S:log-report>\n'.format(end=end.revision, begin=begin.revision), ) as response: if response.status_code != 200: raise self.Exception( "Failed to construct branch history for '{}'".format( branch)) for line in response.iter_lines(): if line == b'<S:log-item>': content = line + b'\n' else: content += line + b'\n' if line != b'</S:log-item>': continue args = self._args_from_content(content, include_log=include_log) branch_point = previous.branch_point if include_identifier else None identifier = previous.identifier if include_identifier else None if args['revision'] != previous.revision: identifier -= 1 if not identifier: identifier = branch_point branch_point = None previous = Commit( repository_id=self.id, branch=end.branch if branch_point else self.default_branch, identifier=identifier, branch_point=branch_point, **args) yield previous content = b''
for asset in candidate.get('assets', []): url = asset.get('browser_download_url') if not url: continue if os not in url: continue extension = 'zip' if url.endswith('.zip') else 'tar.gz' if not url.endswith(extension): continue if self.version and version not in self.version: continue archives.append( Package.Archive( self.name, link=url, version=version, extension=extension, ) ) return archives package = GeckoDriverPackage(version=Version(0, 27, 0)) package.install() executable = package.location
return [] if response.status_code != 200: return [] version = Version.from_string(response.text.rstrip()) else: version = self.version os_name = platform.system() if os_name == 'Linux': os = 'linux{}'.format('64' if platform.machine()[-2:] == '64' else '') elif os_name == 'Darwin': os = 'mac64' else: return [] return [ Package.Archive( self.name, link='{}{}/chromedriver_{}.zip'.format(self.URL, version, os), version=version, extension='zip', ) ] package = ChromeDriverPackage(version=Version(86, 0, 4240, 22)) package.install() executable = package.location
# Copyright (C) 2020 Apple Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import sys from webkitscmpy import AutoInstall, Package, Version AutoInstall.register(Package('twisted', Version(15, 5, 0), pypi_name='Twisted')) sys.modules[__name__] = __import__('twisted')