commit/galaxy-central: inithello: Tool shed functional tests for complex repository dependencies. Tool shed functional tests for reserved strings in repository and user names. Tool shed functional test enhancements.
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/cd6d105e5a5b/ changeset: cd6d105e5a5b user: inithello date: 2013-02-01 17:42:18 summary: Tool shed functional tests for complex repository dependencies. Tool shed functional tests for reserved strings in repository and user names. Tool shed functional test enhancements. affected #: 8 files diff -r ee154ef5073e9d6f8e204f893d8c24b1ed2117dd -r cd6d105e5a5ba60e317ca7e194d63e2025272edb test/tool_shed/base/common.py --- a/test/tool_shed/base/common.py +++ b/test/tool_shed/base/common.py @@ -18,6 +18,14 @@ test_user_3_email = 'test-3@bx.psu.edu' test_user_3_name = 'user3' +complex_repository_dependency_template = '''<?xml version="1.0"?> +<tool_dependency> + <package name="${package}" version="${version}"> +${dependency_lines} + </package> +</tool_dependency> +''' + new_repository_dependencies_xml = '''<?xml version="1.0"?><repositories${description}> ${dependency_lines} diff -r ee154ef5073e9d6f8e204f893d8c24b1ed2117dd -r cd6d105e5a5ba60e317ca7e194d63e2025272edb test/tool_shed/base/twilltestcase.py --- a/test/tool_shed/base/twilltestcase.py +++ b/test/tool_shed/base/twilltestcase.py @@ -203,6 +203,16 @@ return '%s=%s&%s=%s' % ( field_name, field_value, field_name, field_value ) else: return '%s=%s' % ( field_name, field_value ) + def create_repository_complex_dependency( self, repository, xml_filename, depends_on={} ): + self.generate_repository_dependency_xml( depends_on[ 'repositories' ], + xml_filename, + complex=True, + package=depends_on[ 'package' ], + version=depends_on[ 'version' ] ) + self.upload_file( repository, + 'tool_dependencies.xml', + filepath=os.path.split( xml_filename )[0], + commit_message='Uploaded dependency on %s.' % ', '.join( repo.name for repo in depends_on[ 'repositories' ] ) ) def create_repository_dependency( self, repository=None, depends_on=[], filepath=None ): dependency_description = '%s depends on %s.' % ( repository.name, ', '.join( repo.name for repo in depends_on ) ) self.generate_repository_dependency_xml( depends_on, @@ -426,7 +436,25 @@ self.visit_galaxy_url( "/user/logout" ) self.check_page_for_string( "You have been logged out" ) self.home() - def generate_repository_dependency_xml( self, repositories, xml_filename, dependency_description='' ): + def generate_invalid_dependency_xml( self, xml_filename, url, name, owner, changeset_revision, complex=True, package=None, version=None, description=None ): + file_path = os.path.split( xml_filename )[0] + dependency_entries = [] + template = string.Template( common.new_repository_dependencies_line ) + dependency_entries.append( template.safe_substitute( toolshed_url=url, + owner=owner, + repository_name=name, + changeset_revision=changeset_revision ) ) + if not os.path.exists( file_path ): + os.makedirs( file_path ) + if complex: + dependency_template = string.Template( common.complex_repository_dependency_template ) + repository_dependency_xml = dependency_template.safe_substitute( package=package, version=version, dependency_lines='\n'.join( dependency_entries ) ) + else: + template_parser = string.Template( common.new_repository_dependencies_xml ) + repository_dependency_xml = template_parser.safe_substitute( description=description, dependency_lines='\n'.join( dependency_entries ) ) + # Save the generated xml to the specified location. + file( xml_filename, 'w' ).write( repository_dependency_xml ) + def generate_repository_dependency_xml( self, repositories, xml_filename, dependency_description='', complex=False, package=None, version=None ): file_path = os.path.split( xml_filename )[0] if not os.path.exists( file_path ): os.makedirs( file_path ) @@ -442,8 +470,12 @@ description = ' description="%s"' % dependency_description else: description = dependency_description - template_parser = string.Template( common.new_repository_dependencies_xml ) - repository_dependency_xml = template_parser.safe_substitute( description=description, dependency_lines='\n'.join( dependency_entries ) ) + if complex: + dependency_template = string.Template( common.complex_repository_dependency_template ) + repository_dependency_xml = dependency_template.safe_substitute( package=package, version=version, dependency_lines='\n'.join( dependency_entries ) ) + else: + template_parser = string.Template( common.new_repository_dependencies_xml ) + repository_dependency_xml = template_parser.safe_substitute( description=description, dependency_lines='\n'.join( dependency_entries ) ) # Save the generated xml to the specified location. file( xml_filename, 'w' ).write( repository_dependency_xml ) def generate_temp_path( self, test_script_path, additional_paths=[] ): @@ -655,6 +687,11 @@ if includes_tools: self.submit_form( 1, 'select_tool_panel_section_button', **kwd ) self.check_for_strings( post_submit_strings_displayed, strings_not_displayed ) + else: + self.check_for_strings(strings_displayed=[ 'Choose the configuration file whose tool_path setting will be used for installing repositories' ] ) + args = dict( shed_tool_conf=self.shed_tool_conf ) + self.submit_form( 1, 'select_shed_tool_panel_config_button', **args ) + self.check_for_strings( post_submit_strings_displayed, strings_not_displayed ) repository_ids = self.initiate_installation_process( new_tool_panel_section=new_tool_panel_section ) self.wait_for_repository_installation( repository_ids ) def load_invalid_tool_page( self, repository, tool_xml, changeset_revision, strings_displayed=[], strings_not_displayed=[] ): diff -r ee154ef5073e9d6f8e204f893d8c24b1ed2117dd -r cd6d105e5a5ba60e317ca7e194d63e2025272edb test/tool_shed/functional/test_0000_basic_repository_features.py --- a/test/tool_shed/functional/test_0000_basic_repository_features.py +++ b/test/tool_shed/functional/test_0000_basic_repository_features.py @@ -158,3 +158,19 @@ '''Verify that resetting the metadata does not change it.''' repository = test_db_util.get_repository_by_name_and_owner( repository_name, common.test_user_1_name ) self.verify_unchanged_repository_metadata( repository ) + def test_0090_verify_reserved_repository_name_handling( self ): + '''Check that reserved repository names are handled correctly.''' + category = test_db_util.get_category_by_name( 'Test 0000 Basic Repository Features 1' ) + self.get_or_create_repository( name='repos', + description=repository_description, + long_description=repository_long_description, + owner=common.test_user_1_name, + category_id=self.security.encode_id( category.id ), + strings_displayed=[ 'The term <b>repos</b> is a reserved word in the tool shed, so it cannot be used as a repository name.' ] ) + def test_0100_verify_reserved_username_handling( self ): + '''Check that reserved usernames are handled correctly.''' + self.logout() + self.login( email='baduser@bx.psu.edu', username='repos' ) + test_user_1 = test_db_util.get_user( 'baduser@bx.psu.edu' ) + assert test_user_1 is None, 'Creating user with public name "repos" succeeded.' + self.check_for_strings( strings_displayed=[ 'The term <b>repos</b> is a reserved word in the tool shed, so it cannot be used as a public user name.' ] ) diff -r ee154ef5073e9d6f8e204f893d8c24b1ed2117dd -r cd6d105e5a5ba60e317ca7e194d63e2025272edb test/tool_shed/functional/test_0100_complex_repository_dependencies.py --- /dev/null +++ b/test/tool_shed/functional/test_0100_complex_repository_dependencies.py @@ -0,0 +1,154 @@ +from tool_shed.base.twilltestcase import ShedTwillTestCase, common, os +import tool_shed.base.test_db_util as test_db_util + +bwa_base_repository_name = 'bwa_base_repository_0100' +bwa_base_repository_description = "BWA Base" +bwa_base_repository_long_description = "BWA tool that depends on bwa 0.5.9, with a complex repository dependency pointing at bwa_tool_repository_0100" + +bwa_tool_repository_name = 'bwa_tool_repository_0100' +bwa_tool_repository_description = "BWA Tool" +bwa_tool_repository_long_description = "BWA repository with a package tool dependency defined for BWA 0.5.9." + +category_name = 'Test 0100 Complex Repository Dependencies' +category_description = 'Test 0100 Complex Repository Dependencies' + +class TestComplexRepositoryDependencies( ShedTwillTestCase ): + '''Test features related to complex repository dependencies.''' + def test_0000_initiate_users( self ): + """Create necessary user accounts.""" + self.logout() + self.login( email=common.test_user_1_email, username=common.test_user_1_name ) + test_user_1 = test_db_util.get_user( common.test_user_1_email ) + assert test_user_1 is not None, 'Problem retrieving user with email %s from the database' % test_user_1_email + test_user_1_private_role = test_db_util.get_private_role( test_user_1 ) + self.logout() + self.login( email=common.admin_email, username=common.admin_username ) + admin_user = test_db_util.get_user( common.admin_email ) + assert admin_user is not None, 'Problem retrieving user with email %s from the database' % admin_email + admin_user_private_role = test_db_util.get_private_role( admin_user ) + def test_0005_create_bwa_tool_repository( self ): + '''Create and populate bwa_tool_0100.''' + category = self.create_category( name=category_name, description=category_description ) + self.logout() + self.login( email=common.test_user_1_email, username=common.test_user_1_name ) + repository = self.get_or_create_repository( name=bwa_tool_repository_name, + description=bwa_tool_repository_description, + long_description=bwa_tool_repository_long_description, + owner=common.test_user_1_name, + category_id=self.security.encode_id( category.id ), + strings_displayed=[] ) + self.upload_file( repository, + 'bwa/complex/tool_dependencies.xml', + strings_displayed=[], + commit_message='Uploaded tool_dependencies.xml.' ) + self.display_manage_repository_page( repository, strings_displayed=[ 'Tool dependencies', 'may not be', 'in this repository' ] ) + def test_0010_create_bwa_base_repository( self ): + '''Create and populate bwa_base_0100.''' + category = self.create_category( name=category_name, description=category_description ) + self.logout() + self.login( email=common.test_user_1_email, username=common.test_user_1_name ) + repository = self.get_or_create_repository( name=bwa_base_repository_name, + description=bwa_base_repository_description, + long_description=bwa_base_repository_long_description, + owner=common.test_user_1_name, + category_id=self.security.encode_id( category.id ), + strings_displayed=[] ) + tool_repository = test_db_util.get_repository_by_name_and_owner( bwa_tool_repository_name, common.test_user_1_name ) + self.upload_file( repository, + 'bwa/complex/bwa_base.tar', + strings_displayed=[], + commit_message='Uploaded bwa_base.tar with tool wrapper XML, but without tool dependency XML.' ) + def test_0015_generate_complex_repository_dependency_invalid_shed_url( self ): + '''Generate and upload a complex repository definition that specifies an invalid tool shed URL.''' + dependency_path = self.generate_temp_path( 'test_0100', additional_paths=[ 'complex', 'shed' ] ) + xml_filename = self.get_filename( 'tool_dependencies.xml', filepath=dependency_path ) + repository = test_db_util.get_repository_by_name_and_owner( bwa_base_repository_name, common.test_user_1_name ) + url = 'http://http://this is not an url!' + name = repository.name + owner = repository.user.username + changeset_revision = self.get_repository_tip( repository ) + self.generate_invalid_dependency_xml( xml_filename, url, name, owner, changeset_revision, complex=True, package='bwa', version='0.5.9' ) + strings_displayed = [ 'Invalid tool shed %s defined for repository %s' % ( url, repository.name ) ] + self.upload_file( repository, + 'tool_dependencies.xml', + valid_tools_only=False, + filepath=dependency_path, + commit_message='Uploaded dependency on bwa_tool_0100 with invalid url.', + strings_displayed=strings_displayed ) + def test_0020_generate_complex_repository_dependency_invalid_repository_name( self ): + '''Generate and upload a complex repository definition that specifies an invalid repository name.''' + dependency_path = self.generate_temp_path( 'test_0100', additional_paths=[ 'complex', 'shed' ] ) + xml_filename = self.get_filename( 'tool_dependencies.xml', filepath=dependency_path ) + repository = test_db_util.get_repository_by_name_and_owner( bwa_base_repository_name, common.test_user_1_name ) + url = self.url + name = 'invalid_repository!?' + owner = repository.user.username + changeset_revision = self.get_repository_tip( repository ) + self.generate_invalid_dependency_xml( xml_filename, url, name, owner, changeset_revision, complex=True, package='bwa', version='0.5.9' ) + strings_displayed = 'Ignoring repository dependency definition for tool shed %s, name %s, owner %s' % ( url, name, owner ) + strings_displayed += ', changeset revision %s because the name is invalid.' % changeset_revision + self.upload_file( repository, + 'tool_dependencies.xml', + valid_tools_only=False, + filepath=dependency_path, + commit_message='Uploaded dependency on bwa_tool_0100 with invalid repository name.', + strings_displayed=[ strings_displayed ] ) + def test_0025_generate_complex_repository_dependency_invalid_owner_name( self ): + '''Generate and upload a complex repository definition that specifies an invalid owner.''' + dependency_path = self.generate_temp_path( 'test_0100', additional_paths=[ 'complex', 'shed' ] ) + xml_filename = self.get_filename( 'tool_dependencies.xml', filepath=dependency_path ) + repository = test_db_util.get_repository_by_name_and_owner( bwa_base_repository_name, common.test_user_1_name ) + url = self.url + name = repository.name + owner = 'invalid_owner!?' + changeset_revision = self.get_repository_tip( repository ) + self.generate_invalid_dependency_xml( xml_filename, url, name, owner, changeset_revision, complex=True, package='bwa', version='0.5.9' ) + strings_displayed = [ 'Invalid owner %s defined for repository %s. Repository dependencies will be ignored.' % ( owner, name ) ] + self.upload_file( repository, + 'tool_dependencies.xml', + valid_tools_only=False, + filepath=dependency_path, + commit_message='Uploaded dependency on bwa_tool_0100 with invalid owner.', + strings_displayed=strings_displayed ) + def test_0030_generate_complex_repository_dependency_invalid_changeset_revision( self ): + '''Generate and upload a complex repository definition that specifies an invalid changeset revision.''' + dependency_path = self.generate_temp_path( 'test_0100', additional_paths=[ 'complex', 'shed' ] ) + xml_filename = self.get_filename( 'tool_dependencies.xml', filepath=dependency_path ) + repository = test_db_util.get_repository_by_name_and_owner( bwa_base_repository_name, common.test_user_1_name ) + url = self.url + name = repository.name + owner = repository.user.username + changeset_revision = '1234abcd' + self.generate_invalid_dependency_xml( xml_filename, url, name, owner, changeset_revision, complex=True, package='bwa', version='0.5.9' ) + strings_displayed = 'Ignoring repository dependency definition for tool shed %s, name %s, owner %s' % ( url, name, owner ) + strings_displayed += ', changeset revision %s because the changeset revision is invalid.' % changeset_revision + self.upload_file( repository, + 'tool_dependencies.xml', + valid_tools_only=False, + filepath=dependency_path, + commit_message='Uploaded dependency on bwa_tool_0100 with invalid changeset revision.', + strings_displayed=[ strings_displayed ] ) + def test_0035_generate_complex_repository_dependency( self ): + '''Generate and upload a tool_dependencies.xml file that specifies a repository rather than a tool.''' + base_repository = test_db_util.get_repository_by_name_and_owner( bwa_base_repository_name, common.test_user_1_name ) + tool_repository = test_db_util.get_repository_by_name_and_owner( bwa_tool_repository_name, common.test_user_1_name ) + dependency_path = self.generate_temp_path( 'test_0100', additional_paths=[ 'complex' ] ) + self.create_repository_complex_dependency( base_repository, + self.get_filename( 'tool_dependencies.xml', filepath=dependency_path ), + depends_on=dict( package='bwa', version='0.5.9', repositories=[ tool_repository ] ) ) + self.check_repository_dependency( base_repository, tool_repository ) + self.display_manage_repository_page( base_repository, strings_displayed=[ 'bwa', '0.5.9', 'package' ] ) + def test_0040_update_base_repository( self ): + '''Upload a new tool_dependencies.xml to the tool repository, and verify that the base repository displays the new changeset.''' + base_repository = test_db_util.get_repository_by_name_and_owner( bwa_base_repository_name, common.test_user_1_name ) + tool_repository = test_db_util.get_repository_by_name_and_owner( bwa_tool_repository_name, common.test_user_1_name ) + previous_changeset = self.get_repository_tip( tool_repository ) + self.upload_file( tool_repository, + 'bwa/complex/readme/tool_dependencies.xml', + strings_displayed=[], + commit_message='Uploaded new tool_dependencies.xml.' ) + # Verify that the dependency display has been updated as a result of the new tool_dependencies.xml file. + self.display_manage_repository_page( base_repository, + strings_displayed=[ self.get_repository_tip( tool_repository ), 'bwa', '0.5.9', 'package' ], + strings_not_displayed=[ previous_changeset ] ) + diff -r ee154ef5073e9d6f8e204f893d8c24b1ed2117dd -r cd6d105e5a5ba60e317ca7e194d63e2025272edb test/tool_shed/functional_tests.py --- a/test/tool_shed/functional_tests.py +++ b/test/tool_shed/functional_tests.py @@ -184,27 +184,27 @@ kwargs[ 'object_store' ] = 'distributed' kwargs[ 'distributed_object_store_config_file' ] = 'distributed_object_store_conf.xml.sample' - toolshedapp = ToolshedUniverseApplication( job_queue_workers = 5, - id_secret = 'changethisinproductiontoo', - template_path = 'templates', - database_connection = toolshed_database_connection, - database_engine_option_pool_size = '10', - file_path = shed_file_path, - new_file_path = new_repos_path, - tool_path=tool_path, - datatype_converters_config_file = 'datatype_converters_conf.xml.sample', - tool_parse_help = False, - tool_data_table_config_path = galaxy_tool_data_table_conf_file, - shed_tool_data_table_config = shed_tool_data_table_conf_file, - log_destination = "stdout", - use_heartbeat = False, - allow_user_creation = True, - allow_user_deletion = True, - admin_users = 'test@bx.psu.edu', - global_conf = global_conf, - running_functional_tests = True, - hgweb_config_dir = hgweb_config_dir, - **kwargs ) + toolshedapp = ToolshedUniverseApplication( admin_users = 'test@bx.psu.edu', + allow_user_creation = True, + allow_user_deletion = True, + database_connection = toolshed_database_connection, + database_engine_option_pool_size = '10', + datatype_converters_config_file = 'datatype_converters_conf.xml.sample', + file_path = shed_file_path, + global_conf = global_conf, + hgweb_config_dir = hgweb_config_dir, + job_queue_workers = 5, + id_secret = 'changethisinproductiontoo', + log_destination = "stdout", + new_file_path = new_repos_path, + running_functional_tests = True, + shed_tool_data_table_config = shed_tool_data_table_conf_file, + template_path = 'templates', + tool_path=tool_path, + tool_parse_help = False, + tool_data_table_config_path = galaxy_tool_data_table_conf_file, + use_heartbeat = False, + **kwargs ) log.info( "Embedded Toolshed application started" ) @@ -269,32 +269,32 @@ galaxy_global_conf = { '__file__' : 'universe_wsgi.ini.sample' } if not galaxy_database_connection.startswith( 'sqlite://' ): kwargs[ 'database_engine_option_max_overflow' ] = '20' - galaxyapp = GalaxyUniverseApplication( job_queue_workers = 5, - id_secret = 'changethisinproductiontoo', - template_path = "templates", - database_connection = galaxy_database_connection, - database_engine_option_pool_size = '10', - file_path = galaxy_file_path, - new_file_path = galaxy_tempfiles, - tool_path = tool_path, - tool_data_path = tool_data_path, - shed_tool_path = galaxy_shed_tool_path, - update_integrated_tool_panel = False, - migrated_tools_config = galaxy_migrated_tool_conf_file, - tool_config_file = [ galaxy_tool_conf_file, galaxy_shed_tool_conf_file ], - tool_sheds_config_file = galaxy_tool_sheds_conf_file, - datatype_converters_config_file = "datatype_converters_conf.xml.sample", - tool_parse_help = False, - tool_data_table_config_path = galaxy_tool_data_table_conf_file, - shed_tool_data_table_config = shed_tool_data_table_conf_file, - log_destination = "stdout", - use_heartbeat = False, - allow_user_creation = True, + galaxyapp = GalaxyUniverseApplication( allow_user_creation = True, allow_user_deletion = True, admin_users = 'test@bx.psu.edu', allow_library_path_paste = True, + database_connection = galaxy_database_connection, + database_engine_option_pool_size = '10', + datatype_converters_config_file = "datatype_converters_conf.xml.sample", + file_path = galaxy_file_path, global_conf = global_conf, + id_secret = 'changethisinproductiontoo', + job_queue_workers = 5, + log_destination = "stdout", + migrated_tools_config = galaxy_migrated_tool_conf_file, + new_file_path = galaxy_tempfiles, running_functional_tests=True, + shed_tool_data_table_config = shed_tool_data_table_conf_file, + shed_tool_path = galaxy_shed_tool_path, + template_path = "templates", + tool_data_path = tool_data_path, + tool_path = tool_path, + tool_config_file = [ galaxy_tool_conf_file, galaxy_shed_tool_conf_file ], + tool_sheds_config_file = galaxy_tool_sheds_conf_file, + tool_parse_help = False, + tool_data_table_config_path = galaxy_tool_data_table_conf_file, + update_integrated_tool_panel = False, + use_heartbeat = False, **kwargs ) log.info( "Embedded Galaxy application started" ) @@ -391,14 +391,14 @@ galaxyapp.shutdown() galaxyapp = None log.info( "Embedded galaxy application stopped" ) - if 'TOOL_SHED_TEST_NO_CLEANUP' not in os.environ: - try: - for dir in [ tool_shed_test_tmp_dir ]: - if os.path.exists( dir ): - log.info( "Cleaning up temporary files in %s" % dir ) - shutil.rmtree( dir ) - except: - pass +# if 'TOOL_SHED_TEST_NO_CLEANUP' not in os.environ: +# try: +# for dir in [ tool_shed_test_tmp_dir ]: +# if os.path.exists( dir ): +# log.info( "Cleaning up temporary files in %s" % dir ) +# shutil.rmtree( dir ) +# except: +# pass if success: return 0 else: diff -r ee154ef5073e9d6f8e204f893d8c24b1ed2117dd -r cd6d105e5a5ba60e317ca7e194d63e2025272edb test/tool_shed/test_data/bwa/complex/bwa_base.tar Binary file test/tool_shed/test_data/bwa/complex/bwa_base.tar has changed diff -r ee154ef5073e9d6f8e204f893d8c24b1ed2117dd -r cd6d105e5a5ba60e317ca7e194d63e2025272edb test/tool_shed/test_data/bwa/complex/readme/tool_dependencies.xml --- /dev/null +++ b/test/tool_shed/test_data/bwa/complex/readme/tool_dependencies.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<tool_dependency> + <package name="bwa" version="0.5.9"> + <install version="1.0"> + <actions> + <action type="download_by_url">http://downloads.sourceforge.net/project/bio-bwa/bwa-0.5.9.tar.bz2</action> + <action type="shell_command">make</action> + <action type="move_file"> + <source>bwa</source> + <destination>$INSTALL_DIR/bin</destination> + </action> + <action type="set_environment"> + <environment_variable name="PATH" action="prepend_to">$INSTALL_DIR/bin</environment_variable> + </action> + </actions> + </install> + <readme> +Compiling BWA requires zlib and libpthread to be present on your system. + </readme> + </package> +</tool_dependency> \ No newline at end of file diff -r ee154ef5073e9d6f8e204f893d8c24b1ed2117dd -r cd6d105e5a5ba60e317ca7e194d63e2025272edb test/tool_shed/test_data/bwa/complex/tool_dependencies.xml --- /dev/null +++ b/test/tool_shed/test_data/bwa/complex/tool_dependencies.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<tool_dependency> + <package name="bwa" version="0.5.9"> + <install version="1.0"> + <actions> + <action type="download_by_url">http://downloads.sourceforge.net/project/bio-bwa/bwa-0.5.9.tar.bz2</action> + <action type="shell_command">make</action> + <action type="move_file"> + <source>bwa</source> + <destination>$INSTALL_DIR/bin</destination> + </action> + <action type="set_environment"> + <environment_variable name="PATH" action="prepend_to">$INSTALL_DIR/bin</environment_variable> + </action> + </actions> + </install> + </package> +</tool_dependency> \ No newline at end of file Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
Bitbucket