async def _metadata_folder(self, folder: BitbucketPath, **kwargs) -> list: this_dir = await self._fetch_dir_listing(folder) if folder.commit_sha is None: folder.set_commit_sha(this_dir['node']) ret = [] for name in this_dir['directories']: ret.append( BitbucketFolderMetadata( {'name': name}, folder.child(name, folder=True), owner=self.owner, repo=self.repo, )) for item in this_dir['files']: name = self.bitbucket_path_to_name(item['path'], this_dir['path']) # TODO: mypy doesn't like the mix of File & Folder Metadata objects ret.append( BitbucketFileMetadata( # type: ignore item, folder.child(name, folder=False), owner=self.owner, repo=self.repo, )) return ret
async def validate_path(self, path: str, **kwargs) -> BitbucketPath: commit_sha = kwargs.get('commitSha') branch_name = kwargs.get('branch') # revision query param could be commit sha OR branch # take a guess which one it will be. revision = kwargs.get('revision', None) if revision is not None: try: int(revision, 16) # is revision valid hex? except (TypeError, ValueError): branch_name = revision else: commit_sha = revision if not commit_sha and not branch_name: branch_name = await self._fetch_default_branch() if path == '/': return BitbucketPath(path, _ids=[(commit_sha, branch_name)]) path_obj = BitbucketPath(path) for part in path_obj.parts: part._id = (commit_sha, branch_name) return path_obj
async def _metadata_folder(self, folder: BitbucketPath, **kwargs) -> list: this_dir = await self._fetch_dir_listing(folder) if folder.commit_sha is None: folder.set_commit_sha(this_dir['node']) ret = [] for name in this_dir['directories']: ret.append(BitbucketFolderMetadata( {'name': name}, folder.child(name, folder=True), owner=self.owner, repo=self.repo, )) for item in this_dir['files']: name = self.bitbucket_path_to_name(item['path'], this_dir['path']) # TODO: mypy doesn't like the mix of File & Folder Metadata objects ret.append(BitbucketFileMetadata( # type: ignore item, folder.child(name, folder=False), owner=self.owner, repo=self.repo, )) return ret
def test_child_inherits_id(self): bb_parent = BitbucketPath('/foo/', _ids=[(COMMIT_SHA, BRANCH), (COMMIT_SHA, BRANCH)]) bb_child = bb_parent.child('foo') assert bb_child.commit_sha == COMMIT_SHA assert bb_child.branch_name == BRANCH assert bb_child.ref == COMMIT_SHA
async def validate_v1_path(self, path: str, **kwargs) -> BitbucketPath: commit_sha = kwargs.get('commitSha') branch_name = kwargs.get('branch') # revision query param could be commit sha OR branch # take a guess which one it will be. revision = kwargs.get('revision', None) if revision is not None: try: int(revision, 16) # is revision valid hex? except (TypeError, ValueError): branch_name = revision else: commit_sha = revision if not commit_sha and not branch_name: branch_name = await self._fetch_default_branch() if path == '/': return BitbucketPath(path, _ids=[(commit_sha, branch_name)]) path_obj = BitbucketPath(path) for part in path_obj.parts: part._id = (commit_sha, branch_name) # Cache parent directory listing (a WB API V1 feature) # Note: Property ``_parent_dir`` has been re-structured for Bitbucket API 2.0. Please refer # to ``_fetch_path_metadata()`` and ``_fetch_dir_listing()`` for detailed information. self._parent_dir = { 'metadata': await self._fetch_path_metadata(path_obj.parent), 'contents': await self._fetch_dir_listing(path_obj.parent) } # Tweak dir_commit_sha and dir_path for Bitbucket API 2.0 parent_dir_commit_sha = self._parent_dir['metadata']['commit']['hash'][:12] parent_dir_path = '{}/'.format(self._parent_dir['metadata']['path']) # Check file or folder existence path_obj_type = 'commit_directory' if path_obj.is_dir else 'commit_file' if path_obj.name not in [ self.bitbucket_path_to_name(x['path'], parent_dir_path) for x in self._parent_dir['contents'] if x['type'] == path_obj_type ]: raise exceptions.NotFoundError(str(path)) # _fetch_dir_listing() will tell us the commit sha used to look up the listing # if not set in path_obj or if the lookup sha is shorter than the returned sha, update it if not commit_sha or (len(commit_sha) < len(parent_dir_commit_sha)): path_obj.set_commit_sha(parent_dir_commit_sha) return path_obj
def test_child_given_explicit_branch(self): """This one is weird. An explicit child id should override the id from the parent, but I don't know if this is sensible behavior.""" bb_parent = BitbucketPath('/foo/', _ids=[(COMMIT_SHA, BRANCH), (COMMIT_SHA, BRANCH)]) bb_child = bb_parent.child('foo', _id=('413006763', 'master')) assert bb_child.commit_sha == '413006763' assert bb_child.branch_name == 'master' assert bb_child.ref == '413006763' assert bb_parent.commit_sha == COMMIT_SHA assert bb_parent.branch_name == BRANCH assert bb_parent.ref == COMMIT_SHA
def test_child_given_explicit_branch(self): """This one is weird. An explicit child id should override the id from the parent, but I don't know if this is sensible behavior.""" bb_parent = BitbucketPath('/foo/', _ids=[(COMMIT_SHA, BRANCH), (COMMIT_SHA, BRANCH)]) bb_child = bb_parent.child('foo', _id=('413006763', 'master')) assert bb_child.commit_sha == '413006763' assert bb_child.branch_name == 'master' assert bb_child.ref == '413006763' assert bb_parent.commit_sha == COMMIT_SHA assert bb_parent.branch_name == BRANCH assert bb_parent.ref == COMMIT_SHA
def test_id_accessors(self): bb_path = BitbucketPath('/foo', _ids=[(COMMIT_SHA, BRANCH), (COMMIT_SHA, BRANCH)]) assert bb_path.commit_sha == COMMIT_SHA assert bb_path.branch_name == BRANCH assert bb_path.ref == COMMIT_SHA
def test_build_file_metadata(self, file_metadata, owner, repo): name = 'file0002.20bytes.txt' subdir = 'folder2-lvl1/folder1-lvl2/folder1-lvl3' full_path = '/{}/{}'.format(subdir, name) # Note: When building Bitbucket Path, the length of ``_ids`` array must be equal to the # number of path segments, including the root. path = BitbucketPath(full_path, _ids=[(COMMIT_SHA, BRANCH) for _ in full_path.split('/')]) try: metadata = BitbucketFileMetadata(file_metadata, path, owner=owner, repo=repo) except Exception as exc: pytest.fail(str(exc)) return assert metadata.name == name assert metadata.path == full_path assert metadata.kind == 'file' assert metadata.modified == '2019-04-26T15:13:12+00:00' assert metadata.modified_utc == '2019-04-26T15:13:12+00:00' assert metadata.created_utc == '2019-04-25T06:18:21+00:00' assert metadata.content_type is None assert metadata.size == 20 assert metadata.size_as_int == 20 assert metadata.etag == '{}::{}'.format(full_path, COMMIT_SHA) assert metadata.provider == 'bitbucket' assert metadata.last_commit_sha == 'dd8c7b642e32' assert metadata.commit_sha == COMMIT_SHA assert metadata.branch_name == BRANCH web_view = ('https://bitbucket.org/{}/{}/src/{}{}?' 'fileviewer=file-view-default'.format( owner, repo, COMMIT_SHA, full_path)) assert metadata.web_view == web_view assert metadata.extra == { 'commitSha': COMMIT_SHA, 'branch': BRANCH, 'webView': web_view, 'lastCommitSha': 'dd8c7b642e32', } resource = 'mst3k' assert metadata._json_api_links(resource) == { 'delete': None, 'upload': None, 'move': 'http://localhost:7777/v1/resources/{}/providers/bitbucket{}?' 'commitSha={}'.format(resource, full_path, COMMIT_SHA), 'download': 'http://localhost:7777/v1/resources/{}/providers/bitbucket{}?' 'commitSha={}'.format(resource, full_path, COMMIT_SHA), }
async def download(self, path: BitbucketPath, # type: ignore range: Tuple[int, int]=None, **kwargs) -> streams.ResponseStreamReader: """Get the stream to the specified file on Bitbucket In BB API 2.0, the ``repo/username/repo_slug/src/node/path`` endpoint is used for download. Please note that same endpoint has several different usages / behaviors depending on the type of the path and the query params. 1) File download: type is file, no query param``format=meta`` 2) File metadata: type is file, with ``format=meta`` as query param 3) Folder contents: type is folder, no query param``format=meta`` 4) Folder metadata: type is folder, with ``format=meta`` as query param API Doc: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/src/%7Bnode%7D/%7Bpath%7D :param path: the BitbucketPath object of the file to be downloaded :param range: the range header """ metadata = await self.metadata(path) logger.debug('requested-range:: {}'.format(range)) resp = await self.make_request( 'GET', self._build_v2_repo_url('src', path.commit_sha, *path.path_tuple()), range=range, expects=(200, ), throws=exceptions.DownloadError, ) logger.debug('download-headers:: {}'.format([(x, resp.headers[x]) for x in resp.headers])) return streams.ResponseStreamReader(resp, size=metadata.size)
async def _fetch_path_metadata(self, path: BitbucketPath) -> dict: """Get the metadata for folder and file itself. Bitbucket API 2.0 provides an easy way to fetch metadata for files and folders by simply appending ``?format=meta`` to the path endpoint. Quirk 1: This new feature no longer returns several WB required attributes out of the box: ``node`` and ``path`` for folder, ``revision``, ``timestamp`` and ``created_utc`` for file. 1) The ``path`` for folders no longer has an ending slash. 2) The ``node`` for folders and ``revision`` for files are gone. They have always been the first 12 chars of the commit hash in both 1.0 and 2.0. 3) ``timestamp`` and ``created_utc`` for files are gone and must be obtained using the file history endpoint indicated by ``links.history.href``. See ``_metadata_file()`` and ``_fetch_commit_history_by_url()`` for details. Quirk 2: This PATH endpoint ``/2.0/repositories/{username}/{repo_slug}/src/{node}/{path}`` returns HTTP 404 if the ``node`` segment is a branch of which the name contains a slash. This is a either a limitation or a bug on several BB API 2.0 endpoints. It has nothing to do with encoding. More specifically, neither encoding / with %2F nor enclosing ``node`` with curly braces %7B%7D works. Here is the closest reference to the issue we can find as of May 2019: https://bitbucket.org/site/master/issues/9969/get-commit-revision-api-does-not-accept. The fix is simple, just make an extra request to fetch the commit sha of the branch. See ``_fetch_branch_commit_sha()`` for details. In addition, this will happen on all branches, no matter if the name contains a slash or not. API Doc: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/src/%7Bnode%7D/%7Bpath%7D :param path: the file or folder of which the metadata is requested :return: the file metadata dict """ query_params = { 'format': 'meta', 'fields': 'commit.hash,commit.date,path,size,links.history.href' } if not path.commit_sha: path.set_commit_sha(await self._fetch_branch_commit_sha(path.branch_name)) path_meta_url = self._build_v2_repo_url('src', path.ref, *path.path_tuple()) resp = await self.make_request( 'GET', '{}/?{}'.format(path_meta_url, urlencode(query_params)), expects=(200,), throws=exceptions.ProviderError, ) return await resp.json()
def test_build_file_metadata(self, file_metadata, owner, repo): name = 'aaa-01-2.txt' subdir = 'plaster' full_path = '/{}/{}'.format(subdir, name) branch = 'master' path = BitbucketPath(full_path, _ids=[(COMMIT_SHA, branch), (COMMIT_SHA, branch), (COMMIT_SHA, branch)]) try: metadata = BitbucketFileMetadata(file_metadata, path, owner=owner, repo=repo) except Exception as exc: pytest.fail(str(exc)) assert metadata.name == name assert metadata.path == full_path assert metadata.kind == 'file' assert metadata.modified == '2016-10-14T00:37:55Z' assert metadata.modified_utc == '2016-10-14T00:37:55+00:00' assert metadata.created_utc is None assert metadata.content_type is None assert metadata.size == 13 assert metadata.size_as_int == 13 assert metadata.etag == '{}::{}'.format(full_path, COMMIT_SHA) assert metadata.provider == 'bitbucket' assert metadata.last_commit_sha == '90c8f7eef948' assert metadata.commit_sha == COMMIT_SHA assert metadata.branch_name == branch web_view = ('https://bitbucket.org/{}/{}/src/{}{}?' 'fileviewer=file-view-default'.format( owner, repo, COMMIT_SHA, full_path)) assert metadata.web_view == web_view assert metadata.extra == { 'commitSha': COMMIT_SHA, 'branch': 'master', 'webView': web_view, 'lastCommitSha': '90c8f7eef948', } resource = 'mst3k' assert metadata._json_api_links(resource) == { 'delete': None, 'upload': None, 'move': 'http://localhost:7777/v1/resources/{}/providers/bitbucket{}?commitSha={}' .format(resource, full_path, COMMIT_SHA), 'download': 'http://localhost:7777/v1/resources/{}/providers/bitbucket{}?commitSha={}' .format(resource, full_path, COMMIT_SHA), }
async def validate_v1_path(self, path: str, **kwargs) -> BitbucketPath: commit_sha = kwargs.get('commitSha') branch_name = kwargs.get('branch') # revision query param could be commit sha OR branch # take a guess which one it will be. revision = kwargs.get('revision', None) if revision is not None: try: int(revision, 16) # is revision valid hex? except (TypeError, ValueError): branch_name = revision else: commit_sha = revision if not commit_sha and not branch_name: branch_name = await self._fetch_default_branch() if path == '/': return BitbucketPath(path, _ids=[(commit_sha, branch_name)]) path_obj = BitbucketPath(path) for part in path_obj.parts: part._id = (commit_sha, branch_name) self._parent_dir = await self._fetch_dir_listing(path_obj.parent) if path_obj.is_dir: if path_obj.name not in self._parent_dir['directories']: raise exceptions.NotFoundError(str(path)) else: if path_obj.name not in [ self.bitbucket_path_to_name(x['path'], self._parent_dir['path']) for x in self._parent_dir['files'] ]: raise exceptions.NotFoundError(str(path)) # _fetch_dir_listing will tell us the commit sha used to look up the listing # if not set in path_obj or if the lookup sha is shorter than the returned sha, update it if not commit_sha or (len(commit_sha) < len(self._parent_dir['node'])): path_obj.set_commit_sha(self._parent_dir['node']) return path_obj
async def validate_v1_path(self, path: str, **kwargs) -> BitbucketPath: commit_sha = kwargs.get('commitSha') branch_name = kwargs.get('branch') # revision query param could be commit sha OR branch # take a guess which one it will be. revision = kwargs.get('revision', None) if revision is not None: try: int(revision, 16) # is revision valid hex? except (TypeError, ValueError): branch_name = revision else: commit_sha = revision if not commit_sha and not branch_name: branch_name = await self._fetch_default_branch() if path == '/': return BitbucketPath(path, _ids=[(commit_sha, branch_name)]) path_obj = BitbucketPath(path) for part in path_obj.parts: part._id = (commit_sha, branch_name) self._parent_dir = await self._fetch_dir_listing(path_obj.parent) if path_obj.is_dir: if path_obj.name not in self._parent_dir['directories']: raise exceptions.NotFoundError(str(path)) else: if path_obj.name not in [ self.bitbucket_path_to_name(x['path'], self._parent_dir['path']) for x in self._parent_dir['files'] ]: raise exceptions.NotFoundError(str(path)) # _fetch_dir_listing will tell us the commit sha used to look up the listing # if not set in path_obj or if the lookup sha is shorter than the returned sha, update it if not commit_sha or (len(commit_sha) < len(self._parent_dir['node'])): path_obj.set_commit_sha(self._parent_dir['node']) return path_obj
def test_update_commit_sha(self): bb_child = BitbucketPath('/foo/bar', _ids=[(None, BRANCH), (None, BRANCH), (None, BRANCH)]) assert bb_child.commit_sha == None assert bb_child.branch_name == BRANCH assert bb_child.ref == BRANCH bb_child.set_commit_sha(COMMIT_SHA) assert bb_child.commit_sha == COMMIT_SHA assert bb_child.branch_name == BRANCH assert bb_child.ref == COMMIT_SHA bb_parent = bb_child.parent assert bb_parent.commit_sha == COMMIT_SHA assert bb_parent.branch_name == BRANCH assert bb_parent.ref == COMMIT_SHA bb_grandparent = bb_parent.parent assert bb_grandparent.commit_sha == COMMIT_SHA assert bb_grandparent.branch_name == BRANCH assert bb_grandparent.ref == COMMIT_SHA
def test_update_commit_sha(self): bb_child = BitbucketPath('/foo/bar', _ids=[(None, BRANCH), (None, BRANCH), (None, BRANCH)]) assert bb_child.commit_sha == None assert bb_child.branch_name == BRANCH assert bb_child.ref == BRANCH bb_child.set_commit_sha(COMMIT_SHA) assert bb_child.commit_sha == COMMIT_SHA assert bb_child.branch_name == BRANCH assert bb_child.ref == COMMIT_SHA bb_parent = bb_child.parent assert bb_parent.commit_sha == COMMIT_SHA assert bb_parent.branch_name == BRANCH assert bb_parent.ref == COMMIT_SHA bb_grandparent = bb_parent.parent assert bb_grandparent.commit_sha == COMMIT_SHA assert bb_grandparent.branch_name == BRANCH assert bb_grandparent.ref == COMMIT_SHA
async def _fetch_dir_listing(self, folder: BitbucketPath) -> list: """Get a list of the folder's full contents (upto the max limit setting if there is one). Bitbucket API 2.0 refactored the response structure for listing folder contents. 1) The response is paginated. If ``resp_dict`` contains the key ``next``, the contents are partial. The caller must use the URL provided by ``dict['next']`` to fetch the next page after this method returns. 2) The response no longer provides the metadata about the folder itself. In order to obtain the ``node`` and ``path`` attributes, please use ``_fetch_path_metadata()`` instead. API Doc: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/src/%7Bnode%7D/%7Bpath%7D :param folder: the folder of which the contents should be listed :returns: a list of the folder's full contents """ query_params = { 'pagelen': self.RESP_PAGE_LEN, 'fields': 'values.path,values.size,values.type,next', } if not folder.commit_sha: folder.set_commit_sha(await self._fetch_branch_commit_sha(folder.branch_name)) next_url = '{}/?{}'.format(self._build_v2_repo_url('src', folder.ref, *folder.path_tuple()), urlencode(query_params)) dir_list = [] # type: ignore while next_url: resp = await self.make_request( 'GET', next_url, expects=(200,), throws=exceptions.ProviderError, ) content = await resp.json() next_url = content.get('next', None) dir_list.extend(content['values']) return dir_list
async def download(self, path: BitbucketPath, **kwargs): # type: ignore '''Get the stream to the specified file on bitbucket :param str path: The path to the file on bitbucket ''' metadata = await self.metadata(path) resp = await self.make_request( 'GET', self._build_v1_repo_url('raw', path.commit_sha, *path.path_tuple()), expects=(200, ), throws=exceptions.DownloadError, ) return streams.ResponseStreamReader(resp, size=metadata.size)
async def download(self, path: BitbucketPath, # type: ignore range: Tuple[int, int]=None, **kwargs) -> streams.ResponseStreamReader: """Get the stream to the specified file on Bitbucket :param path: The path to the file on Bitbucket :param range: the range header """ metadata = await self.metadata(path) logger.debug('requested-range:: {}'.format(range)) resp = await self.make_request( 'GET', self._build_v1_repo_url('raw', path.commit_sha, *path.path_tuple()), range=range, expects=(200, ), throws=exceptions.DownloadError, ) logger.debug('download-headers:: {}'.format([(x, resp.headers[x]) for x in resp.headers])) return streams.ResponseStreamReader(resp, size=metadata.size)
def test_build_folder_metadata(self, folder_metadata, owner, repo): name = 'folder1-lvl3' subdir = 'folder2-lvl1/folder1-lvl2' full_path = '/{}/{}'.format(subdir, name) path = BitbucketPath(full_path, _ids=[(None, BRANCH) for _ in full_path.split('/')]) try: metadata = BitbucketFolderMetadata(folder_metadata, path, owner=owner, repo=repo) except Exception as exc: pytest.fail(str(exc)) return assert metadata.name == name assert metadata.path == '{}/'.format(full_path) assert metadata.kind == 'folder' assert metadata.children is None assert metadata.extra == { 'commitSha': None, 'branch': BRANCH, } assert metadata.provider == 'bitbucket' assert metadata.commit_sha is None assert metadata.branch_name == BRANCH assert metadata._json_api_links('mst3k') == { 'delete': None, 'upload': None, 'move': 'http://localhost:7777/v1/resources/mst3k/providers/bitbucket{}/?' 'branch={}'.format(full_path, BRANCH), 'new_folder': None, }
async def _fetch_dir_listing(self, folder: BitbucketPath) -> dict: """Get listing of contents within a BitbucketPath folder object. https://confluence.atlassian.com/bitbucket/src-resources-296095214.html#srcResources-GETalistofreposource Note:: Using this endpoint for a file will return the file contents. :param BitbucketPath folder: the folder whose contents should be listed :rtype dict: :returns: a directory listing of the contents of the folder """ assert folder.is_dir # don't use this method on files resp = await self.make_request( 'GET', self._build_v1_repo_url('src', folder.ref, *folder.path_tuple()) + '/', expects=(200, ), throws=exceptions.ProviderError, ) return await resp.json()
def test_build_folder_metadata(self, folder_metadata, owner, repo): branch = 'master' name = 'plaster' path = BitbucketPath('/{}/'.format(name), _ids=[(None, branch), (None, branch)]) try: metadata = BitbucketFolderMetadata(folder_metadata, path, owner=owner, repo=repo) except Exception as exc: pytest.fail(str(exc)) assert metadata.name == name assert metadata.path == '/{}/'.format(name) assert metadata.kind == 'folder' assert metadata.children is None assert metadata.extra == { 'commitSha': None, 'branch': branch, } assert metadata.provider == 'bitbucket' assert metadata.commit_sha is None assert metadata.branch_name == branch assert metadata._json_api_links('mst3k') == { 'delete': None, 'upload': None, 'move': 'http://localhost:7777/v1/resources/mst3k/providers/bitbucket/{}/?branch={}' .format(name, branch), 'new_folder': None, }
def path_from_metadata(self, # type: ignore parent_path: BitbucketPath, metadata) -> BitbucketPath: return parent_path.child(metadata.name, folder=metadata.is_folder)
def path_from_metadata(self, # type: ignore parent_path: BitbucketPath, metadata) -> BitbucketPath: return parent_path.child(metadata.name, folder=metadata.is_folder)
async def _fetch_commit_history_by_path(self, path: BitbucketPath) -> list: if not path.commit_sha: path.set_commit_sha(await self._fetch_branch_commit_sha(path.branch_name)) return await self._fetch_commit_history_by_url( self._build_v2_repo_url('filehistory', path.ref, path.path) )
def test_child_inherits_id(self): bb_parent = BitbucketPath('/foo/', _ids=[(COMMIT_SHA, BRANCH), (COMMIT_SHA, BRANCH)]) bb_child = bb_parent.child('foo') assert bb_child.commit_sha == COMMIT_SHA assert bb_child.branch_name == BRANCH assert bb_child.ref == COMMIT_SHA
def test_id_accessors_no_sha(self): bb_path = BitbucketPath('/foo', _ids=[(None, BRANCH), (None, BRANCH)]) assert bb_path.commit_sha == None assert bb_path.branch_name == BRANCH assert bb_path.ref == BRANCH
def test_id_accessors_no_branch(self): bb_path = BitbucketPath('/foo', _ids=[(COMMIT_SHA, None), (COMMIT_SHA, None)]) assert bb_path.commit_sha == COMMIT_SHA assert bb_path.branch_name == None assert bb_path.ref == COMMIT_SHA
async def _metadata_folder(self, folder: BitbucketPath, **kwargs) -> list: """Get a list of the folder contents, each item of which is a BitbucketPath object. :param folder: the folder of which the contents should be listed :return: a list of BitbucketFileMetadata and BitbucketFolderMetadata objects """ # Fetch metadata itself dir_meta = await self._fetch_path_metadata(folder) # Quirk: ``node`` attribute is no longer available for folder metadata in BB API 1.0. The # value of ``node`` can still be obtained from the commit hash of which the first 12 # chars turn out to be the value we need. dir_commit_sha = dir_meta['commit']['hash'][:12] # Quirk: the ``path`` attribute in folder metadata no longer has an trailing slash in BB API # 2.0. To keep ``bitbucket_path_to_name()`` intact, a trailing slash is added. dir_path = '{}/'.format(dir_meta['path']) # Fetch content list dir_list = await self._fetch_dir_listing(folder) # Set the commit hash if folder.commit_sha is None: folder.set_commit_sha(dir_commit_sha) # Build the metadata to return # Quirks: # 1) BB API 2.0 treats both files and folders the same way.``path`` for both is a full or # absolute path. ``bitbucket_path_to_name()`` must be called to get the correct name. # 2) Both files and folders share the same list and use the same dict/json structure. Use # the ``type`` field to check whether a path is a folder or not. # 3) ``revision`` for files is gone but can be replaced with part of the commit hash. # However, it is tricky for files. The ``commit`` field of each file item in the # returned content list is the latest branch commit. In order to obtain the correct # time when the file was last modified, WB needs to fetch the file history. This adds # lots of requests and significantly hits performance due to folder listing being called # very frequently. The decision is to remove them. # 4) Similar to ``revision``, ``timestamp``, and ``created_utc`` are removed. ret = [] for value in dir_list: if value['type'] == 'commit_file': name = self.bitbucket_path_to_name(value['path'], dir_path) # TODO: existing issue - find out why timestamp doesn't show up on the files page item = { 'size': value['size'], 'path': value['path'], } ret.append(BitbucketFileMetadata( # type: ignore item, folder.child(name, folder=False), owner=self.owner, repo=self.repo, )) if value['type'] == 'commit_directory': name = self.bitbucket_path_to_name(value['path'], dir_path) ret.append(BitbucketFolderMetadata( # type: ignore {'name': name}, folder.child(name, folder=True), owner=self.owner, repo=self.repo, )) return ret