diff --git a/lib/galaxy/dependencies/pinned-requirements.txt b/lib/galaxy/dependencies/pinned-requirements.txt index 2f9a0dc0321..83fe9855306 100644 --- a/lib/galaxy/dependencies/pinned-requirements.txt +++ b/lib/galaxy/dependencies/pinned-requirements.txt @@ -170,7 +170,8 @@ markdown-it-py==4.0.0 ; python_full_version >= '3.11' markupsafe==3.0.3 mcp==1.25.0 ; python_full_version >= '3.10' mdurl==0.1.2 -mercurial==7.1.2 +mercurial==7.1.2 ; python_version < '3.14' +mercurial==7.2rc0 ; python_version >= '3.14' mistralai==1.9.11 mistune==3.1.4 more-itertools==10.8.0 diff --git a/lib/galaxy/tool_shed/metadata/metadata_generator.py b/lib/galaxy/tool_shed/metadata/metadata_generator.py index b0b7a6123a8..6422639bc3f 100644 --- a/lib/galaxy/tool_shed/metadata/metadata_generator.py +++ b/lib/galaxy/tool_shed/metadata/metadata_generator.py @@ -392,7 +392,9 @@ class BaseMetadataGenerator: if invalid_tool_configs: metadata_dict["invalid_tools"] = invalid_tool_configs self.metadata_dict = metadata_dict - remove_dir(work_dir) + # Only remove work_dir if not resetting all metadata - in that case the caller handles cleanup + if not self.resetting_all_metadata_on_repository: + remove_dir(work_dir) def generate_package_dependency_metadata(self, elem, valid_tool_dependencies_dict, invalid_tool_dependencies_dict): """ diff --git a/lib/tool_shed/metadata/repository_metadata_manager.py b/lib/tool_shed/metadata/repository_metadata_manager.py index 13031959ff7..5c26eb70f10 100644 --- a/lib/tool_shed/metadata/repository_metadata_manager.py +++ b/lib/tool_shed/metadata/repository_metadata_manager.py @@ -799,14 +799,24 @@ class RepositoryMetadataManager(ToolShedMetadataGenerator): metadata_dict = None ancestor_changeset_revision = None ancestor_metadata_dict = None - for changeset in self.repository.get_changesets_for_setting_metadata(self.app): - work_dir = tempfile.mkdtemp(prefix="tmp-toolshed-ramorits") - ctx = repo[changeset] - log.debug("Cloning repository changeset revision: %s", str(ctx.rev())) - assert self.repository_clone_url - repository_clone_url = repository_clone_url or self.repository_clone_url - cloned_ok, error_message = hg_util.clone_repository(repository_clone_url, work_dir, str(ctx.rev())) - if cloned_ok: + + # Clone repository once, then update for each changeset revision. + work_dir = tempfile.mkdtemp(prefix="tmp-toolshed-ramorits") + assert self.repository_clone_url + repository_clone_url = repository_clone_url or self.repository_clone_url + log.debug("Cloning repository for metadata reset") + cloned_ok, error_message = hg_util.clone_repository(repository_clone_url, work_dir) + if not cloned_ok: + log.error(f"Failed to clone repository: {error_message}") + basic_util.remove_dir(work_dir) + self._clean_repository_metadata(changeset_revisions) + return + + try: + for changeset in self.repository.get_changesets_for_setting_metadata(self.app): + ctx = repo[changeset] + log.debug("Updating to changeset revision: %s", str(ctx.rev())) + hg_util.update_repository(work_dir, str(ctx.rev())) log.debug("Generating metadata for changeset revision: %s", str(ctx.rev())) self.set_changeset_revision(str(ctx)) self.set_repository_files_dir(work_dir) @@ -823,11 +833,11 @@ class RepositoryMetadataManager(ToolShedMetadataGenerator): # self.SUBSET - ancestor metadata is a subset of current metadata, so continue from current # self.NOT_EQUAL_AND_NOT_SUBSET - ancestor metadata is neither equal to nor a subset of current # metadata, so persist ancestor metadata. - log.info(f"amd {ancestor_metadata_dict}") + log.debug(f"amd {ancestor_metadata_dict}") comparison = self.compare_changeset_revisions( ancestor_changeset_revision, ancestor_metadata_dict ) - log.info(f"comparison {comparison}") + log.debug(f"comparison {comparison}") if comparison in [self.NO_METADATA, self.EQUAL, self.SUBSET]: ancestor_changeset_revision = self.changeset_revision ancestor_metadata_dict = self.metadata_dict @@ -858,6 +868,7 @@ class RepositoryMetadataManager(ToolShedMetadataGenerator): changeset_revisions.append(metadata_changeset_revision) ancestor_changeset_revision = None ancestor_metadata_dict = None + finally: basic_util.remove_dir(work_dir) # Delete all repository_metadata records for this repository that do not have a changeset_revision # value in changeset_revisions. diff --git a/lib/tool_shed/webapp/controllers/repository.py b/lib/tool_shed/webapp/controllers/repository.py index 4d8f2a4c612..c4bbb712266 100644 --- a/lib/tool_shed/webapp/controllers/repository.py +++ b/lib/tool_shed/webapp/controllers/repository.py @@ -4,10 +4,9 @@ import os import string import tempfile from datetime import date +from typing import Any from mercurial import ( - cmdutil, - commands, mdiff, patch, ) @@ -74,15 +73,15 @@ malicious_error = " This changeset cannot be downloaded because it potentially malicious_error_can_push = " Correct this changeset as soon as possible, it potentially produces malicious behavior or contains inappropriate content." -def get_mercurial_default_options_dict(command): - """Borrowed from repoman - get default parameters for a mercurial command.""" - possible = cmdutil.findpossible(command, commands.table) - # Mercurial >= 3.4 returns a tuple whose first element is the old return dict - if type(possible) is tuple: - possible = possible[0] - if len(possible) != 1: - raise Exception(f'unable to find mercurial command "{command}"') - return {r[1].replace(b"-", b"_"): r[2] for r in next(iter(possible.values()))[1][1]} +def get_mercurial_default_options_dict(command: str) -> dict[str, Any]: + """Get default parameters for a mercurial command.""" + # Use mdiff.diffopts defaults directly instead of introspecting command table + # (the old cmdutil.findpossible API was removed in Mercurial 7.2) + if command == "diff": + # Convert byte keys to strings but preserve value types (int, bool) + # as mdiff.diffopts expects properly typed values + return {(k.decode("utf-8") if isinstance(k, bytes) else k): v for k, v in mdiff.diffopts.defaults.items()} + raise Exception(f'unable to find mercurial command "{command}"') class RepositoryController(BaseUIController, ratings_util.ItemRatings): @@ -2366,10 +2365,8 @@ class RepositoryController(BaseUIController, ratings_util.ItemRatings): else: ctx_child = None diffs = [] - options_dict = get_mercurial_default_options_dict(b"diff") - # Not quite sure if the following settings make any difference, but with a combination of them and the size check on each - # diff, we don't run out of memory when viewing the changelog of the cisortho2 repository on the test tool shed. - options_dict = {util.unicodify(k): util.unicodify(v) for k, v in options_dict.items()} + # Get default diff options with string keys and properly typed values + options_dict = get_mercurial_default_options_dict("diff") options_dict["maxfile"] = basic_util.MAXDIFFSIZE options_dict["maxtotal"] = basic_util.MAXDIFFSIZE diffopts = mdiff.diffopts(**options_dict) diff --git a/pyproject.toml b/pyproject.toml index 688156fd7d3..4b32d1a3485 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,8 @@ dependencies = [ "Mako", "Markdown", "MarkupSafe", - "mercurial>=6.8.2", # Python 3.13 support + "mercurial>=6.8.2 ; python_version<'3.14'", # Python 3.13 support + "mercurial>=7.2rc0 ; python_version>='3.14'", # Python 3.14 support "mrcfile", "more-itertools", "msal",