Unverified Commit 96f48063 authored by Matt Sturgeon's avatar Matt Sturgeon Committed by GitHub
Browse files

nixpkgs-plugin-update: updated plugin versioning; vimPlugins: update on 2025-12-15 (#470251)

parents 780e81e5 4a660f85
Loading
Loading
Loading
Loading
+1884 −1884

File changed.

Preview size limit exceeded, changes collapsed.

+1 −1
Original line number Diff line number Diff line
@@ -303,7 +303,7 @@ https://github.com/Koalhack/darcubox-nvim/,HEAD,
https://github.com/ptdewey/darkearth-nvim/,HEAD,
https://github.com/dart-lang/dart-vim-plugin/,,
https://github.com/iofq/dart.nvim/,HEAD,
https://github.com/rizzatti/dash.vim/,HEAD,
https://github.com/joseotaviorf/dash.vim/,HEAD,
https://github.com/nvimdev/dashboard-nvim/,,
https://github.com/Shougo/ddc-filter-matcher_head/,HEAD,
https://github.com/Shougo/ddc-filter-sorter_rank/,HEAD,
+2 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
  buildPythonPackage,
  uv-build,
  gitpython,
  packaging,
  ruff,
  mypy,
}:
@@ -18,6 +19,7 @@ buildPythonPackage {

  dependencies = [
    gitpython
    packaging
  ];

  nativeCheckInputs = [
+243 −15
Original line number Diff line number Diff line
@@ -29,11 +29,17 @@ from typing import Any, Callable
from urllib.parse import urljoin, urlparse

import git
from packaging.version import InvalidVersion, parse as parse_version

ATOM_ENTRY = "{http://www.w3.org/2005/Atom}entry"  # " vim gets confused here
ATOM_LINK = "{http://www.w3.org/2005/Atom}link"  # "
ATOM_UPDATED = "{http://www.w3.org/2005/Atom}updated"  # "

GIT_TAGS_PREFIX = "refs/tags/"

VERSION_DATE_PATTERN = re.compile(r"(\d{4}-\d{2}-\d{2})$")
VERSION_TAG_PATTERN = re.compile(r"^(.+?)-unstable-")

LOG_LEVELS = {
    logging.getLevelName(level): level
    for level in [logging.DEBUG, logging.INFO, logging.WARN, logging.ERROR]
@@ -125,6 +131,58 @@ class Repo:

        return loaded["rev"], updated

    @retry(urllib.error.URLError, tries=4, delay=3, backoff=2)
    def get_latest_tag(self) -> str | None:
        try:
            # FIXME: This fetches all tags. We need to find a way to check if a tag exists in
            # an ancestor of the default branch.
            cmd = ["git", "ls-remote", "--tags", "--refs", self.uri]
            log.debug("Fetching tags with: %s", cmd)
            output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=10)
            lines = output.decode("utf-8").strip().split("\n")

            if not lines or lines[0] == "":
                log.debug("No tags found for %s", self.uri)
                return None

            tags = []
            for line in lines:
                if "\t" in line:
                    tag_ref = line.split("\t")[1]
                    if tag_ref.startswith(GIT_TAGS_PREFIX):
                        tag_name = tag_ref[len(GIT_TAGS_PREFIX) :]
                        tags.append(tag_name)

            if not tags:
                return None

            valid_versions = []
            invalid_tags = []

            for tag in tags:
                try:
                    version = parse_version(tag)
                    valid_versions.append((tag, version))
                except InvalidVersion:
                    invalid_tags.append(tag)

            if valid_versions:
                latest_tag = max(valid_versions, key=lambda x: x[1])[0]
            elif invalid_tags:
                latest_tag = max(invalid_tags)
            else:
                log.debug("No tags found for %s", self.uri)
                return None

            log.debug("Found latest tag: %s", latest_tag)
            return latest_tag
        except subprocess.CalledProcessError as e:
            log.debug("Failed to fetch tags for %s: %s", self.uri, e)
            return None
        except Exception as e:
            log.warning("Unexpected error fetching tags for %s: %s", self.uri, e)
            return None

    def _prefetch(self, ref: str | None):
        cmd = ["nix-prefetch-git", "--quiet", "--fetch-submodules", self.uri]
        if ref is not None:
@@ -198,12 +256,145 @@ class RepoGitHub(Repo):
            assert commit_link is not None, f"No link tag found feed entry {xml!r}"
            url = urlparse(commit_link.get("href"))
            updated_tag = latest_entry.find(ATOM_UPDATED)
            assert (
                updated_tag is not None and updated_tag.text is not None
            ), f"No updated tag found feed entry {xml!r}"
            assert updated_tag is not None and updated_tag.text is not None, (
                f"No updated tag found feed entry {xml!r}"
            )
            updated = datetime.strptime(updated_tag.text, "%Y-%m-%dT%H:%M:%SZ")
            return Path(str(url.path)).name, updated

    def _execute_graphql(self, query: str, variables: dict) -> dict:
        graphql_url = "https://api.github.com/graphql"

        payload = json.dumps({"query": query, "variables": variables}).encode("utf-8")

        req = make_request(graphql_url, self.token)
        req.add_header("Content-Type", "application/json")
        req.data = payload

        with urllib.request.urlopen(req, timeout=10) as response:
            return json.load(response)

    def _extract_commit_date(self, target: dict) -> datetime | None:
        commit_date_str = None
        if "committedDate" in target:
            commit_date_str = target["committedDate"]
        elif "target" in target and "committedDate" in target["target"]:
            commit_date_str = target["target"]["committedDate"]

        if commit_date_str:
            return datetime.fromisoformat(commit_date_str.replace("Z", "+00:00"))
        return None

    @retry(urllib.error.URLError, tries=4, delay=3, backoff=2)
    def get_latest_tag(self) -> str | None:
        try:
            # FIXME: This fetches all tags. We need to find a way to check if a tag exists in
            # an ancestor of the default branch.
            query = """
            query GetLatestVersionInfo($owner: String!, $name: String!) {
              repository(owner: $owner, name: $name) {
                refs(refPrefix: "refs/tags/", first: 5, orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) {
                  nodes {
                    name
                    target {
                      ... on Commit {
                        committedDate
                      }
                      ... on Tag {
                        target {
                          ... on Commit {
                            committedDate
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
            """

            data = self._execute_graphql(
                query, {"owner": self.owner, "name": self.repo}
            )

            if "errors" in data:
                log.warning(
                    "GraphQL errors for %s/%s: %s",
                    self.owner,
                    self.repo,
                    data["errors"],
                )
                return None

            if "data" not in data or not data["data"]:
                log.warning(
                    "No data in GraphQL response for %s/%s", self.owner, self.repo
                )
                return None

            repo = data["data"]["repository"]
            if not repo:
                log.debug(
                    "Repository %s/%s not found or inaccessible", self.owner, self.repo
                )
                return None

            valid_versions = []
            invalid_tags = []
            for ref_node in repo["refs"]["nodes"]:
                tag_name = ref_node["name"]
                commit_date = self._extract_commit_date(ref_node["target"])
                if not commit_date:
                    continue

                try:
                    version = parse_version(tag_name)
                    valid_versions.append((tag_name, version, commit_date))
                except InvalidVersion:
                    invalid_tags.append((tag_name, None, commit_date))

            def get_version(tag_tuple):
                _, version, _ = tag_tuple
                return version

            def get_date(tag_tuple):
                _, _, date = tag_tuple
                return date or datetime.min

            def get_max_versions(versions, sort_key):
                return max(versions, key=sort_key, default=(None, None, None))

            max_valid_tag, _, max_valid_date = get_max_versions(
                valid_versions, get_version
            )
            max_invalid_tag, _, max_invalid_date = get_max_versions(
                invalid_tags, get_date
            )
            if max_valid_tag and max_invalid_tag:
                return (
                    max_invalid_tag
                    if (max_invalid_date or datetime.min)
                    > (max_valid_date or datetime.min)
                    else max_valid_tag
                )
            elif max_valid_tag:
                return max_valid_tag
            elif max_invalid_tag:
                return max_invalid_tag
            else:
                return None

        except Exception as e:
            log.warning(
                "Error fetching version info for %s/%s: %s",
                self.owner,
                self.repo,
                e,
                exc_info=True,
            )
            return None

    def _check_for_redirect(self, url: str, req: http.client.HTTPResponse):
        response_url = req.geturl()
        if url != response_url:
@@ -286,6 +477,7 @@ class Plugin:
    has_submodules: bool
    sha256: str
    date: datetime | None = None
    last_tag: str | None = None

    @property
    def normalized_name(self) -> str:
@@ -294,7 +486,33 @@ class Plugin:
    @property
    def version(self) -> str:
        assert self.date is not None
        return self.date.strftime("%Y-%m-%d")
        date_str = self.date.strftime("%Y-%m-%d")

        tag_part = "0"
        if self.last_tag:
            tag = (
                self.last_tag[1:]
                if self.last_tag.startswith(("v", "V"))
                else self.last_tag
            )
            if tag and tag[0].isdigit():
                tag_part = tag

        return f"{tag_part}-unstable-{date_str}"

    @staticmethod
    def parse_version_string(version_str: str) -> tuple[datetime, str | None]:
        date_match = VERSION_DATE_PATTERN.search(version_str)
        if not date_match:
            raise ValueError(f"Cannot parse date from version: {version_str}")
        date = datetime.fromisoformat(date_match.group(1))

        tag_match = VERSION_TAG_PATTERN.search(version_str)
        last_tag = (
            tag_match.group(1) if tag_match and tag_match.group(1) != "0" else None
        )

        return date, last_tag

    def as_json(self) -> dict[str, str]:
        copy = self.__dict__.copy()
@@ -419,21 +637,18 @@ class Editor:
        plugins = []
        for name, attr in data.items():
            checksum = attr["checksum"]
            version_str = attr["version"]

            # https://github.com/NixOS/nixpkgs/blob/8a335419/pkgs/applications/editors/neovim/build-neovim-plugin.nix#L36
            # https://github.com/NixOS/nixpkgs/pull/344478#discussion_r1786646055
            version = re.search(r"\d\d\d\d-\d\d?-\d\d?", attr["version"])
            if version is None:
                raise ValueError(f"Cannot parse version: {attr['version']}")
            date = datetime.strptime(version.group(), "%Y-%m-%d")
            date, last_tag = Plugin.parse_version_string(version_str)

            pdesc = PluginDesc.load_from_string(config, f'{attr["homePage"]} as {name}')
            pdesc = PluginDesc.load_from_string(config, f"{attr['homePage']} as {name}")
            p = Plugin(
                attr["pname"],
                checksum["rev"],
                checksum["submodules"],
                checksum["sha256"],
                date,
                last_tag=last_tag,
            )

            plugins.append((pdesc, p))
@@ -554,7 +769,7 @@ class Editor:

        for plugin_desc, plugin, redirect in fetched:
            # Check if plugin is a Plugin object and has normalized_name attribute
            if isinstance(plugin, Plugin) and hasattr(plugin, 'normalized_name'):
            if isinstance(plugin, Plugin) and hasattr(plugin, "normalized_name"):
                result[plugin.normalized_name] = (plugin_desc, plugin, redirect)
            elif isinstance(plugin, Exception):
                # For exceptions, we can't determine the normalized_name
@@ -562,7 +777,9 @@ class Editor:
                log.error(f"Error fetching plugin {plugin_desc.name}: {plugin!r}")
            else:
                # For unexpected types, log the issue
                log.error(f"Unexpected plugin type for {plugin_desc.name}: {type(plugin)}")
                log.error(
                    f"Unexpected plugin type for {plugin_desc.name}: {type(plugin)}"
                )

        return list(result.values())

@@ -722,11 +939,18 @@ def prefetch_plugin(
    log.info(f"Fetching last commit for plugin {p.name} from {p.repo.uri}@{p.branch}")
    commit, date = p.repo.latest_commit()

    latest_tag = p.repo.get_latest_tag()
    if latest_tag:
        log.debug("Latest tag for %s: %s", p.name, latest_tag)
    else:
        log.debug("No tags found for %s, will use '0' prefix", p.name)

    cached_plugin = cache[commit] if cache else None
    if cached_plugin is not None:
        log.debug(f"Cache hit for {p.name}!")
        cached_plugin.name = p.name
        cached_plugin.date = date
        cached_plugin.last_tag = latest_tag
        return cached_plugin, p.repo.redirect

    has_submodules = p.repo.has_submodules()
@@ -734,7 +958,7 @@ def prefetch_plugin(
    sha256 = p.repo.prefetch(commit)

    return (
        Plugin(p.name, commit, has_submodules, sha256, date=date),
        Plugin(p.name, commit, has_submodules, sha256, date=date, last_tag=latest_tag),
        p.repo.redirect,
    )

@@ -818,7 +1042,11 @@ class Cache:
            data = json.load(f)
            for attr in data.values():
                p = Plugin(
                    attr["name"], attr["commit"], attr["has_submodules"], attr["sha256"]
                    attr["name"],
                    attr["commit"],
                    attr["has_submodules"],
                    attr["sha256"],
                    last_tag=attr.get("last_tag"),
                )
                downloads[attr["commit"]] = p
        return downloads