aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/up/update-vim-plugins/update_vim_plugins
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/up/update-vim-plugins/update_vim_plugins')
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__init__.py0
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__main__.py15
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/cleanup.py100
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/helpers.py61
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/nix.py121
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/plugin.py182
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/spec.py143
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/__init__.py0
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/fixtures.py44
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_nix.py32
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_plugin.py144
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_spec.py136
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/update.py212
13 files changed, 1190 insertions, 0 deletions
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__init__.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__init__.py
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__main__.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__main__.py
new file mode 100644
index 00000000..a8d9e06f
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__main__.py
@@ -0,0 +1,15 @@
+from cleo.application import Application
+
+from .update import UpdateCommand
+from .cleanup import CleanUpCommand
+
+
+def main():
+ application = Application()
+ application.add(UpdateCommand())
+ application.add(CleanUpCommand())
+ application.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/cleanup.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/cleanup.py
new file mode 100644
index 00000000..fd313ed0
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/cleanup.py
@@ -0,0 +1,100 @@
+from cleo.commands.command import Command
+from cleo.helpers import argument
+
+from .helpers import read_manifest_to_spec, read_blacklist_to_spec, write_manifest_from_spec
+
+
+class CleanUpCommand(Command):
+ name = "cleanup"
+ description = "Clean up manifest"
+ arguments = [argument("plug_dir", description="Path to the plugin directory", optional=False)]
+
+ def handle(self):
+ """Main command function"""
+
+ plug_dir = self.argument("plug_dir")
+ self.line("<comment>Checking manifest file</comment>")
+ # all cleaning up will be done during reading and writing automatically
+ manifest = read_manifest_to_spec(plug_dir)
+ blacklist = read_blacklist_to_spec(plug_dir)
+
+ new_manifest = [spec for spec in manifest if spec not in blacklist]
+
+ new_manifest_filterd = self.filter_renamed(new_manifest)
+
+ write_manifest_from_spec(new_manifest_filterd, plug_dir)
+
+ self.line("<comment>Done</comment>")
+
+ def filter_renamed(self, specs):
+ """Filter specs that define the same plugin (same owner and same repo) but with different properties.
+ This could be a different name, source, or branch
+ """
+
+ error = False
+ for i, p in enumerate(specs):
+ for p2 in specs:
+ same_owner = p.owner.lower() == p2.owner.lower()
+ same_repo = p.repo.lower() == p2.repo.lower()
+ different_specs = p != p2
+ marked_duplicate = p.marked_duplicate or p2.marked_duplicate
+
+ if same_owner and same_repo and different_specs and not marked_duplicate:
+ self.line("<info>The following lines appear to define the same plugin</info>")
+
+ p_props_defined = p.branch is not None or p.custom_name is not None
+ p2_props_defined = p2.branch is not None or p2.custom_name is not None
+ p_is_lower_case = p.owner == p.owner.lower() and p.name == p.name.lower()
+ p2_is_lower_case = p2.owner == p2.owner.lower() and p2.name == p2.name.lower()
+
+ # list of conditions for selecting p
+ select_p = p_props_defined and not p2_props_defined or p2_is_lower_case and not p_is_lower_case
+ # list of conditions for selecting p2
+ select_p2 = p2_props_defined and not p_props_defined or p_is_lower_case and not p2_is_lower_case
+
+ # one is more defined and is all lower, but the other is not all lower
+ # (we assume the not all lower case is the correct naming)
+ error_props_lower = (
+ p_props_defined and p_is_lower_case and not p2_props_defined and not p2_is_lower_case
+ )
+ error_props_lower2 = (
+ p2_props_defined and p2_is_lower_case and not p_props_defined and not p_is_lower_case
+ )
+
+ # both props are defined
+ error_props = p_props_defined and p2_props_defined
+
+ # the sources are different
+ error_source = p.repository_host != p2.repository_host
+
+ if error_props_lower or error_props_lower2 or error_props or error_source:
+ self.line(" • <error>Cannot determine which is the correct plugin</error>")
+ self.line(f" - {p.line}")
+ self.line(f" - {p2.line}")
+ error = True
+ # remove second spec to not encounter the error twice
+ # this will not be written to the manifest.txt because we set
+ # the error flag and will exit after the loop
+ specs.remove(p2)
+ elif select_p:
+ self.line(f" - <comment>{p.line}</comment>")
+ self.line(f" - {p2.line}")
+ specs.remove(p2)
+ elif select_p2:
+ self.line(f" - {p.line}")
+ self.line(f" - <comment>{p2.line}</comment>")
+ specs.remove(p)
+ else:
+ self.line(" • <error>Logic error in correct spec determination</error>")
+ self.line(f" - {p.line}")
+ self.line(f" - {p2.line}")
+ error = True
+ # remove second spec to not encounter the error twice
+ # this will not be written to the manifest.txt because we set
+ # the error flag and will exit after the loop
+ specs.remove(p)
+ if error:
+ # exit after all errors have been found
+ exit(1)
+
+ return specs
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/helpers.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/helpers.py
new file mode 100644
index 00000000..8a28b0e8
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/helpers.py
@@ -0,0 +1,61 @@
+from .spec import PluginSpec
+
+MANIFEST_FILE = "manifest.txt"
+BLACKLIST_FILE = "blacklist.txt"
+PKGS_FILE = "default.nix"
+JSON_FILE = ".plugins.json"
+PLUGINS_LIST_FILE = "plugins.md"
+
+
+def get_const(const: str, plug_dir: str) -> str:
+ out = plug_dir + "/" + const
+ return out
+
+
+def read_manifest(plug_dir: str) -> list[str]:
+ with open(get_const(MANIFEST_FILE, plug_dir), "r") as file:
+ specs = set([spec.strip() for spec in file.readlines()])
+
+ return sorted(specs)
+
+
+def read_manifest_to_spec(plug_dir: str) -> list[PluginSpec]:
+ manifest = read_manifest(plug_dir)
+ specs = [PluginSpec.from_spec(spec.strip()) for spec in manifest]
+
+ return sorted(specs)
+
+
+def read_blacklist(plug_dir: str) -> list[str]:
+ with open(get_const(BLACKLIST_FILE, plug_dir), "r") as file:
+ if len(file.readlines()) == 0:
+ return [""]
+ else:
+ blacklisted_specs = set([spec.strip() for spec in file.readlines()])
+
+ return sorted(blacklisted_specs)
+
+
+def read_blacklist_to_spec(plug_dir: str) -> list[PluginSpec]:
+ blacklist = read_blacklist(plug_dir)
+ specs = [PluginSpec.from_spec(spec.strip()) for spec in blacklist]
+
+ return sorted(specs)
+
+
+def write_manifest(specs: list[str] | set[str], plug_dir: str):
+ """write specs to manifest file. Does some cleaning up"""
+
+ with open(get_const(MANIFEST_FILE, plug_dir), "w") as file:
+ specs = sorted(set(specs), key=lambda x: x.lower())
+ specs = [p for p in specs]
+
+ for s in specs:
+ file.write(f"{s}\n")
+
+
+def write_manifest_from_spec(specs: list[PluginSpec], plug_dir: str):
+ """write specs to manifest file. Does some cleaning up"""
+
+ strings = [f"{spec}" for spec in specs]
+ write_manifest(strings, plug_dir)
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/nix.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/nix.py
new file mode 100644
index 00000000..66a8df4c
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/nix.py
@@ -0,0 +1,121 @@
+import abc
+import enum
+import json
+import subprocess
+
+
+def nix_prefetch_url(url):
+ """Return the sha256 hash of the given url."""
+ subprocess_output = subprocess.check_output(
+ ["nix-prefetch-url", "--type", "sha256", url],
+ stderr=subprocess.DEVNULL,
+ )
+ sha256 = subprocess_output.decode("utf-8").strip()
+ return sha256
+
+
+def nix_prefetch_git(url):
+ """Return the sha256 hash of the given git url."""
+ subprocess_output = subprocess.check_output(["nix-prefetch-git", url], stderr=subprocess.DEVNULL)
+ sha256 = json.loads(subprocess_output)["sha256"]
+ return sha256
+
+
+class Source(abc.ABC):
+ """Abstract base class for sources."""
+
+ url: str
+ sha256: str
+
+ @abc.abstractmethod
+ def __init__(self, url: str) -> None:
+ """Initialize a Source."""
+
+ @abc.abstractmethod
+ def get_nix_expression(self):
+ """Return the nix expression for this source."""
+
+ def __repr__(self):
+ """Return the representation of this source."""
+ return self.get_nix_expression()
+
+
+class UrlSource(Source):
+ """A source that is a url."""
+
+ def __init__(self, url: str) -> None:
+ """Initialize a UrlSource."""
+ self.url = url
+ self.sha256 = nix_prefetch_url(url)
+
+ def get_nix_expression(self):
+ """Return the nix expression for this source."""
+ return f'fetchurl {{ url = "{self.url}"; sha256 = "{self.sha256}"; }}'
+
+
+class GitSource(Source):
+ """A source that is a git repository."""
+
+ def __init__(self, url: str, rev: str) -> None:
+ """Initialize a GitSource."""
+ self.url = url
+ self.rev = rev
+ self.sha256 = nix_prefetch_git(url)
+
+ def get_nix_expression(self):
+ """Return the nix expression for this source."""
+ return f'fetchgit {{ url = "{self.url}"; rev = "{self.rev}"; sha256 = "{self.sha256}"; }}'
+
+
+class License(enum.Enum):
+ """An enumeration of licenses."""
+
+ AGPL_3_0 = "agpl3Only"
+ APACHE_2_0 = "asf20"
+ BSD_2_CLAUSE = "bsd2"
+ BSD_3_CLAUSE = "bsd3"
+ BSL_1_0 = "bsl1_0"
+ CC0_1_0 = "cc0"
+ EPL_2_0 = "epl20"
+ GPL_2_0 = "gpl2Only"
+ GPL_3_0 = "gpl3Only"
+ ISCLGPL_2_1 = "lgpl21Only"
+ MIT = "mit"
+ MPL_2_0 = "mpl20"
+ UNLUNLICENSE = "unlicense"
+ WTFPL = "wtfpl"
+ UNFREE = "unfree"
+ UNKNOWN = ""
+
+ @classmethod
+ def from_spdx_id(cls, spdx_id: str | None) -> "License":
+ """Return the License from the given spdx_id."""
+ mapping = {
+ "AGPL-3.0": cls.AGPL_3_0,
+ "AGPL-3.0-only": cls.AGPL_3_0,
+ "Apache-2.0": cls.APACHE_2_0,
+ "BSD-2-Clause": cls.BSD_2_CLAUSE,
+ "BSD-3-Clause": cls.BSD_3_CLAUSE,
+ "BSL-1.0": cls.BSL_1_0,
+ "CC0-1.0": cls.CC0_1_0,
+ "EPL-2.0": cls.EPL_2_0,
+ "GPL-2.0": cls.GPL_2_0,
+ "GPL-2.0-only": cls.GPL_2_0,
+ "GPL-3.0": cls.GPL_3_0,
+ "GPL-3.0-only": cls.GPL_3_0,
+ "LGPL-2.1-only": cls.ISCLGPL_2_1,
+ "MIT": cls.MIT,
+ "MPL-2.0": cls.MPL_2_0,
+ "Unlicense": cls.UNLUNLICENSE,
+ "WTFPL": cls.WTFPL,
+ }
+
+ if spdx_id is None:
+ return cls.UNKNOWN
+
+ spdx_id = spdx_id.upper()
+ return mapping.get(spdx_id, cls.UNKNOWN)
+
+ def __str__(self):
+ """Return the string representation of this license."""
+ return self.value
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/plugin.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/plugin.py
new file mode 100644
index 00000000..8334ad53
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/plugin.py
@@ -0,0 +1,182 @@
+import logging
+import os
+import urllib
+
+import requests
+import jsonpickle
+from datetime import datetime, date
+from dateparser import parse
+
+from .nix import GitSource, License, Source, UrlSource
+from .spec import PluginSpec, RepositoryHost
+
+
+logger = logging.getLogger(__name__)
+
+
+class VimPlugin:
+ """Abstract base class for vim plugins."""
+
+ name: str
+ owner: str
+ repo: str
+ version: date
+ source: Source
+ description: str = "No description"
+ homepage: str
+ license: License
+ source_line: str
+ checked: date = datetime.now().date()
+
+ def to_nix(self):
+ """Return the nix expression for this plugin."""
+ meta = f'with lib; {{ description = "{self.description}"; homepage = "{self.homepage}"; license = with licenses; [ {self.license.value} ]; }}'
+ return f'/* Generated from: {self.source_line} */ {self.name} = buildVimPlugin {{ pname = "{self.name}"; version = "{self.version}"; src = {self.source.get_nix_expression()}; meta = {meta}; }};'
+
+ def to_json(self):
+ """Serizalize the plugin to json"""
+ return jsonpickle.encode(self)
+
+ def to_markdown(self):
+ link = f"[{self.source_line}]({self.homepage})"
+ version = f"{self.version}"
+ package_name = f"{self.name}"
+ checked = f"{self.checked}"
+
+ return f"| {link} | {version} | `{package_name}` | {checked} |"
+
+ def __lt__(self, o: object) -> bool:
+ if not isinstance(o, VimPlugin):
+ return False
+
+ return self.name.lower() < o.name.lower()
+
+ def __repr__(self):
+ """Return the representation of this plugin."""
+ return f"VimPlugin({self.name!r}, {self.version.strftime('%Y-%m-%d')})"
+
+
+def _get_github_token():
+ token = os.environ.get("GITHUB_TOKEN")
+ if token is None:
+ # NOTE: This should never use more than the free api requests <2023-12-09>
+ pass
+ # logger.warning("GITHUB_TOKEN environment variable not set")
+ return token
+
+
+class GitHubPlugin(VimPlugin):
+ def __init__(self, plugin_spec: PluginSpec) -> None:
+ """Initialize a GitHubPlugin."""
+
+ full_name = f"{plugin_spec.owner}/{plugin_spec.repo}"
+ repo_info = self._api_call(f"repos/{full_name}")
+ default_branch = plugin_spec.branch or repo_info["default_branch"]
+ api_callback = self._api_call(f"repos/{full_name}/commits/{default_branch}")
+ latest_commit = api_callback["commit"]
+ sha = api_callback["sha"]
+
+ self.name = plugin_spec.name
+ self.owner = plugin_spec.owner
+ self.version = parse(latest_commit["committer"]["date"]).date()
+ self.source = UrlSource(f"https://github.com/{full_name}/archive/{sha}.tar.gz")
+ self.description = (repo_info.get("description") or "").replace('"', '\\"')
+ self.homepage = repo_info["html_url"]
+ self.license = plugin_spec.license or License.from_spdx_id((repo_info.get("license") or {}).get("spdx_id"))
+ self.source_line = plugin_spec.line
+
+ def _api_call(self, path: str, token: str | None = _get_github_token()):
+ """Call the GitHub API."""
+ url = f"https://api.github.com/{path}"
+ headers = {"Content-Type": "application/json"}
+ if token is not None:
+ headers["Authorization"] = f"token {token}"
+ response = requests.get(url, headers=headers)
+ if response.status_code != 200:
+ raise RuntimeError(f"GitHub API call failed: {response.text}")
+ return response.json()
+
+
+class GitlabPlugin(VimPlugin):
+ def __init__(self, plugin_spec: PluginSpec) -> None:
+ """Initialize a GitlabPlugin."""
+
+ full_name = urllib.parse.quote(f"{plugin_spec.owner}/{plugin_spec.repo}", safe="")
+ repo_info = self._api_call(f"projects/{full_name}")
+ default_branch = plugin_spec.branch or repo_info["default_branch"]
+ api_callback = self._api_call(f"projects/{full_name}/repository/branches/{default_branch}")
+ latest_commit = api_callback["commit"]
+ sha = latest_commit["id"]
+
+ self.name = plugin_spec.name
+ self.owner = plugin_spec.owner
+ self.version = parse(latest_commit["created_at"]).date()
+ self.source = UrlSource(f"https://gitlab.com/api/v4/projects/{full_name}/repository/archive.tar.gz?sha={sha}")
+ self.description = (repo_info.get("description") or "").replace('"', '\\"')
+ self.homepage = repo_info["web_url"]
+ self.license = plugin_spec.license or License.from_spdx_id(repo_info.get("license", {}).get("key"))
+ self.source_line = plugin_spec.line
+
+ def _api_call(self, path: str) -> dict:
+ """Call the Gitlab API."""
+ url = f"https://gitlab.com/api/v4/{path}"
+ response = requests.get(url)
+ if response.status_code != 200:
+ raise RuntimeError(f"Gitlab API call failed: {response.text}")
+ return response.json()
+
+
+def _get_sourcehut_token():
+ token = os.environ.get("SOURCEHUT_TOKEN")
+ if token is None:
+ # NOTE: This should never use more than the free requests <2023-12-09>
+ pass
+ # logger.warning("SOURCEHUT_TOKEN environment variable not set")
+ return token
+
+
+class SourceHutPlugin(VimPlugin):
+ def __init__(self, plugin_spec: PluginSpec) -> None:
+ """Initialize a SourceHutPlugin."""
+
+ repo_info = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}")
+ if plugin_spec.branch is None:
+ commits = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}/log")
+ else:
+ commits = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}/log/{plugin_spec.branch}")
+ latest_commit = commits["results"][0]
+ sha = latest_commit["id"]
+
+ self.name = plugin_spec.name
+ self.owner = plugin_spec.owner
+ self.version = parse(latest_commit["timestamp"]).date()
+ self.description = (repo_info.get("description") or "").replace('"', '\\"')
+ self.homepage = f"https://git.sr.ht/~{plugin_spec.owner}/{plugin_spec.repo}"
+ self.source = GitSource(self.homepage, sha)
+ self.license = plugin_spec.license or License.UNKNOWN # cannot be determined via API
+ self.source_line = plugin_spec.line
+
+ def _api_call(self, path: str, token: str | None = _get_sourcehut_token()):
+ """Call the SourceHut API."""
+
+ url = f"https://git.sr.ht/api/{path}"
+ headers = {"Content-Type": "application/json"}
+ if token is not None:
+ headers["Authorization"] = f"token {token}"
+ response = requests.get(url, headers=headers)
+ if response.status_code != 200:
+ raise RuntimeError(f"SourceHut API call failed: {response.json()}")
+ return response.json()
+
+
+def plugin_from_spec(plugin_spec: PluginSpec) -> VimPlugin:
+ """Initialize a VimPlugin."""
+
+ if plugin_spec.repository_host == RepositoryHost.GITHUB:
+ return GitHubPlugin(plugin_spec)
+ elif plugin_spec.repository_host == RepositoryHost.GITLAB:
+ return GitlabPlugin(plugin_spec)
+ elif plugin_spec.repository_host == RepositoryHost.SOURCEHUT:
+ return SourceHutPlugin(plugin_spec)
+ else:
+ raise NotImplementedError(f"Unsupported source: {plugin_spec.repository_host}")
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/spec.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/spec.py
new file mode 100644
index 00000000..0f2fb29c
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/spec.py
@@ -0,0 +1,143 @@
+import enum
+import re
+
+from .nix import License
+
+
+class RepositoryHost(enum.Enum):
+ """A repository host."""
+
+ GITHUB = "github"
+ GITLAB = "gitlab"
+ SOURCEHUT = "sourcehut"
+
+
+class PluginSpec:
+ """A Vim plugin Spec."""
+
+ @classmethod
+ def from_spec(cls, spec):
+ """The spec line must be in the format:
+ [<repository_host>:]<owner>/<repo>[:<branch>][:name].
+
+ repository_host is one of github (default), gitlab, or sourcehut.
+ owner is the repository owner.
+ repo is the repository name.
+ branch is the git branch.
+ name is the name to use for the plugin (default is value of repo).
+ """
+ repository_host = RepositoryHost.GITHUB
+ # gitref = "master"
+
+ repository_host_regex = r"((?P<repository_host>[^:]+):)"
+ owner_regex = r"(?P<owner>[^/:]+)"
+ repo_regex = r"(?P<repo>[^:]+)"
+ branch_regex = r"(:(?P<branch>[^:]+)?)"
+ name_regex = r"(:(?P<name>[^:]+)?)"
+ license_regex = r"(:(?P<license>[^:]+)?)"
+ marked_duplicate_regex = r"(:(?P<duplicate>duplicate))"
+
+ spec_regex = re.compile(
+ f"^{repository_host_regex}?{owner_regex}/{repo_regex}{branch_regex}?{name_regex}?{license_regex}?{marked_duplicate_regex}?$",
+ )
+
+ match = spec_regex.match(spec)
+ if match is None:
+ raise ValueError(f"Invalid spec: {spec}")
+
+ group_dict = match.groupdict()
+
+ repository_host = RepositoryHost(group_dict.get("repository_host") or "github")
+
+ owner = group_dict.get("owner")
+ if owner is None:
+ raise RuntimeError("Could not get owner")
+
+ repo = group_dict.get("repo")
+ if repo is None:
+ raise RuntimeError("Could not get repo")
+
+ branch = group_dict.get("branch")
+ name = group_dict.get("name")
+ license = group_dict.get("license")
+ marked_duplicate = bool(group_dict.get("duplicate")) # True if 'duplicate', False if None
+
+ line = spec
+
+ return cls(repository_host, owner, repo, line, branch, name, license, marked_duplicate)
+
+ def __init__(
+ self,
+ repository_host: RepositoryHost,
+ owner: str,
+ repo: str,
+ line: str,
+ branch: str | None = None,
+ name: str | None = None,
+ license: str | None = None,
+ marked_duplicate: bool = False,
+ ) -> None:
+ """Initialize a VimPluginSpec."""
+ self.repository_host = repository_host
+ self.owner = owner
+ self.repo = repo
+ self.branch = branch
+ self.custom_name = name
+ self.name = name or repo.replace(".", "-").replace("_", "-")
+ self.license = License(license) if license else None
+ self.line = line
+ self.marked_duplicate = marked_duplicate
+
+ def __str__(self) -> str:
+ """Return a string representation of a VimPluginSpec."""
+ spec = ""
+
+ if self.repository_host != RepositoryHost.GITHUB:
+ spec += f"{self.repository_host.value}:"
+
+ spec += f"{self.owner}/{self.repo}"
+
+ spec += ":"
+ if self.branch is not None:
+ spec += self.branch
+
+ spec += ":"
+ if self.custom_name is not None:
+ spec += self.custom_name
+
+ spec += ":"
+ if self.license is not None:
+ spec += str(self.license)
+
+ spec += ":"
+ if self.marked_duplicate:
+ spec += "duplicate"
+
+ return spec.rstrip(":")
+
+ def __repr__(self):
+ """Return the representation of the specs"""
+ return f"PluginSpec({self.owner}/{self.repo}, {self.name})"
+
+ def to_spec(self):
+ """Return a spec line for a VimPluginSpec."""
+ return str(self)
+
+ def __lt__(self, o: object) -> bool:
+ if not isinstance(o, PluginSpec):
+ return False
+
+ return self.name.lower() < o.name.lower()
+
+ def __eq__(self, o: object) -> bool:
+ """Return True if the two specs are equal."""
+ if not isinstance(o, PluginSpec):
+ return False
+
+ return (
+ self.repository_host == o.repository_host
+ and self.owner == o.owner
+ and self.repo == o.repo
+ and self.branch == o.branch
+ and self.name == o.name
+ )
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/__init__.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/__init__.py
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/fixtures.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/fixtures.py
new file mode 100644
index 00000000..75dd251a
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/fixtures.py
@@ -0,0 +1,44 @@
+import json
+
+import pytest
+from pytest_mock import MockerFixture
+
+from update_vim_plugins.nix import GitSource, UrlSource
+
+
+@pytest.fixture()
+def url():
+ return "https://example.com"
+
+
+@pytest.fixture()
+def rev():
+ return "1234567890abcdef"
+
+
+@pytest.fixture()
+def sha256():
+ return "sha256-1234567890abcdef"
+
+
+@pytest.fixture()
+def url_source(mocker: MockerFixture, url: str, sha256: str):
+ mocker.patch("subprocess.check_output", return_value=bytes(sha256, "utf-8"))
+ return UrlSource(url)
+
+
+@pytest.fixture()
+def git_source(mocker: MockerFixture, url: str, rev: str, sha256: str):
+ return_value = {
+ "url": url,
+ "rev": rev,
+ "date": "1970-01-01T00:00:00+00:00",
+ "path": "",
+ "sha256": sha256,
+ "fetchLFS": False,
+ "fetchSubmodules": False,
+ "deepClone": False,
+ "leaveDotGit": False,
+ }
+ mocker.patch("subprocess.check_output", return_value=json.dumps(return_value).encode("utf-8"))
+ return GitSource(url, rev)
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_nix.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_nix.py
new file mode 100644
index 00000000..46e59f76
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_nix.py
@@ -0,0 +1,32 @@
+from update_vim_plugins.nix import GitSource, License, UrlSource
+
+
+def test_url_source(url_source: UrlSource, url: str, sha256: str):
+ assert url_source.url == url
+ assert url_source.sha256 == sha256
+
+
+def test_url_source_nix_expression(url_source: UrlSource, url: str, sha256: str):
+ assert url_source.get_nix_expression() == f'fetchurl {{ url = "{url}"; sha256 = "{sha256}"; }}'
+
+
+def test_git_source(git_source: GitSource, url: str, rev: str, sha256: str):
+ assert git_source.url == url
+ assert git_source.sha256 == sha256
+ assert git_source.rev == rev
+
+
+def test_git_source_nix_expression(git_source: GitSource, url: str, rev: str, sha256: str):
+ assert git_source.get_nix_expression() == f'fetchgit {{ url = "{url}"; rev = "{rev}"; sha256 = "{sha256}"; }}'
+
+
+def test_license_github():
+ github_license = "MIT"
+ license = License.from_spdx_id(github_license)
+ assert license == License.MIT
+
+
+def test_license_gitlab():
+ gitlab_license = "mit"
+ license = License.from_spdx_id(gitlab_license)
+ assert license == License.MIT
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_plugin.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_plugin.py
new file mode 100644
index 00000000..32377e24
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_plugin.py
@@ -0,0 +1,144 @@
+import json
+from typing import Callable
+
+import pytest
+from pytest_mock import MockFixture
+
+from update_vim_plugins.nix import License, UrlSource
+from update_vim_plugins.plugin import GitHubPlugin, VimPlugin
+from update_vim_plugins.spec import PluginSpec
+
+
+@pytest.fixture()
+def mock_source(sha256: str):
+ class MockSource:
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def get_nix_expression(self):
+ return "src"
+
+ return MockSource()
+
+
+@pytest.fixture()
+def mock_plugin(mock_source):
+ class MockVimPlugin(VimPlugin):
+ def __init__(self):
+ self.name = "test"
+ self.version = "1.0.0"
+ self.source = mock_source
+ self.description = "No description"
+ self.homepage = "https://example.com"
+ self.license = License.UNKNOWN
+
+ return MockVimPlugin()
+
+
+def test_vim_plugin_nix_expression(mock_plugin):
+ assert (
+ mock_plugin.get_nix_expression()
+ == 'test = buildVimPluginFrom2Nix { pname = "test"; version = "1.0.0"; src = src; meta = with lib; { description = "No description"; homepage = "https://example.com"; license = with licenses; [ ]; }; };'
+ )
+
+
+class MockResponse:
+ def __init__(self, status_code: int, content: bytes):
+ self.status_code = status_code
+ self.content = content
+
+ def json(self):
+ return json.loads(self.content)
+
+
+def mock_request_get(repsonses: dict[str, MockResponse]):
+ respones_not_found = MockResponse(404, b'{"message": "Not Found"}')
+
+ def mock_get(url: str, *args, **kwargs):
+ return repsonses.get(url, respones_not_found)
+
+ return mock_get
+
+
+@pytest.fixture()
+def github_commits_response():
+ return MockResponse(
+ 200,
+ json.dumps(
+ {
+ "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
+ "commit": {
+ "committer": {
+ "date": "2011-04-14T16:00:49Z",
+ },
+ },
+ }
+ ),
+ )
+
+
+@pytest.fixture()
+def github_get(github_commits_response: MockResponse):
+ repos_response = MockResponse(
+ 200,
+ json.dumps(
+ {
+ "html_url": "https://github.com/octocat/Hello-World",
+ "description": "This your first repo!",
+ "fork": False,
+ "default_branch": "master",
+ "license": {
+ "spdx_id": "MIT",
+ },
+ }
+ ),
+ )
+ responses = {
+ "https://api.github.com/repos/octocat/Hello-World": repos_response,
+ "https://api.github.com/repos/octocat/Hello-World/commits/master": github_commits_response,
+ }
+ return mock_request_get(responses)
+
+
+@pytest.fixture()
+def github_get_no_license(github_commits_response: MockResponse):
+ repos_response = MockResponse(
+ 200,
+ json.dumps(
+ {
+ "html_url": "https://github.com/octocat/Hello-World",
+ "description": "This your first repo!",
+ "fork": False,
+ "default_branch": "master",
+ }
+ ),
+ )
+ responses = {
+ "https://api.github.com/repos/octocat/Hello-World": repos_response,
+ "https://api.github.com/repos/octocat/Hello-World/commits/master": github_commits_response,
+ }
+ return mock_request_get(responses)
+
+
+def test_github_plugin(mocker: MockFixture, github_get: Callable, url_source: UrlSource):
+ mocker.patch("requests.get", github_get)
+ url_source = mocker.patch("update_vim_plugins.nix.UrlSource", url_source)
+
+ spec = PluginSpec.from_spec("octocat/Hello-World")
+ plugin = GitHubPlugin(spec)
+
+ assert plugin.name == "Hello-World"
+ assert plugin.version == "2011-04-14"
+ assert plugin.description == "This your first repo!"
+ assert plugin.homepage == "https://github.com/octocat/Hello-World"
+ assert plugin.license == License.MIT
+
+
+def test_github_plugin_no_license(mocker: MockFixture, github_get_no_license: Callable, url_source: UrlSource):
+ mocker.patch("requests.get", github_get_no_license)
+ url_source = mocker.patch("update_vim_plugins.nix.UrlSource", url_source)
+
+ spec = PluginSpec.from_spec("octocat/Hello-World")
+ plugin = GitHubPlugin(spec)
+
+ assert plugin.license == License.UNKNOWN
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_spec.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_spec.py
new file mode 100644
index 00000000..2b9a1d24
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_spec.py
@@ -0,0 +1,136 @@
+import pytest
+
+from update_vim_plugins.spec import PluginSpec, RepositoryHost
+
+
+@pytest.fixture()
+def owner():
+ return "owner"
+
+
+@pytest.fixture()
+def repo():
+ return "repo.nvim"
+
+
+@pytest.fixture()
+def branch():
+ return "main"
+
+
+@pytest.fixture()
+def name():
+ return "repo-nvim"
+
+
+@pytest.fixture()
+def license():
+ return "mit"
+
+
+def test_from_spec_simple(owner: str, repo: str):
+ vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}")
+
+ assert vim_plugin.owner == owner
+ assert vim_plugin.repo == repo
+
+
+def test_from_spec_with_gitref(owner: str, repo: str, branch: str):
+ vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}:{branch}")
+
+ assert vim_plugin.branch == branch
+
+
+def test_from_spec_with_name(owner: str, repo: str, name: str):
+ vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}::{name}")
+
+ assert vim_plugin.name == name
+
+
+@pytest.mark.parametrize("host", RepositoryHost)
+def test_from_spec_with_repository_host(owner: str, repo: str, host: RepositoryHost):
+ vim_plugin = PluginSpec.from_spec(f"{host.value}:{owner}/{repo}")
+
+ assert vim_plugin.repository_host == host
+
+
+def test_from_spec_without_repository_host(owner: str, repo: str):
+ vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}")
+
+ assert vim_plugin.repository_host == RepositoryHost.GITHUB
+
+
+def test_from_spec_complex(owner: str, repo: str, branch: str, name: str):
+ vim_plugin = PluginSpec.from_spec(f"gitlab:{owner}/{repo}:{branch}:{name}")
+
+ assert vim_plugin.repository_host == RepositoryHost.GITLAB
+ assert vim_plugin.owner == owner
+ assert vim_plugin.repo == repo
+ assert vim_plugin.branch == branch
+ assert vim_plugin.name == name
+
+
+def test_from_spec_invalid_spec():
+ with pytest.raises(ValueError):
+ PluginSpec.from_spec("invalid_spec")
+
+
+def test_to_spec_simple(owner: str, repo: str):
+ vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+
+ assert vim_plugin.to_spec() == f"{owner}/{repo}"
+
+
+def test_to_spec_with_branch(owner: str, repo: str, branch: str):
+ vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo, branch=branch)
+ assert vim_plugin.to_spec() == f"{owner}/{repo}:{branch}"
+
+
+def test_to_spec_with_name(owner: str, repo: str, name: str):
+ vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo, name=name)
+
+ assert vim_plugin.to_spec() == f"{owner}/{repo}::{name}"
+
+
+@pytest.mark.parametrize("host", [RepositoryHost.GITLAB, RepositoryHost.SOURCEHUT])
+def test_to_spec_with_repository_host(host: RepositoryHost, owner: str, repo: str):
+ vim_plugin = PluginSpec(host, owner, repo)
+
+ assert vim_plugin.to_spec() == f"{host.value}:{owner}/{repo}"
+
+
+def test_to_spec_complex(owner: str, repo: str, branch: str, name: str, license: str):
+ vim_plugin = PluginSpec(RepositoryHost.GITLAB, owner, repo, branch=branch, name=name, license=license)
+
+ assert vim_plugin.to_spec() == f"gitlab:{owner}/{repo}:{branch}:{name}:{license}"
+
+
+def test_spec_equal(owner: str, repo: str):
+ vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+ vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+
+ assert vim_plugin == vim_plugin2
+
+
+def test_spec_not_equal_different_branch(owner: str, repo: str):
+ vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+ vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, branch="main")
+
+ assert vim_plugin != vim_plugin2
+
+
+def test_spec_not_equal_different_name(owner: str, repo: str):
+ vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+ vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, name="renamed")
+
+ assert vim_plugin != vim_plugin2
+
+
+def test_spec_equal_same_normalized_name(owner: str):
+ repo = "repo.nvim"
+ name = "repo-nvim"
+
+ vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+ vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, name=name)
+
+ assert vim_plugin == vim_plugin2
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/update.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/update.py
new file mode 100644
index 00000000..7eb3eeb4
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/update.py
@@ -0,0 +1,212 @@
+import subprocess
+from random import shuffle
+from cleo.helpers import argument, option
+from cleo.commands.command import Command
+from concurrent.futures import ThreadPoolExecutor, as_completed
+
+from pprint import pprint
+
+from .plugin import plugin_from_spec
+
+from .helpers import read_manifest_to_spec, get_const
+from .helpers import JSON_FILE, PLUGINS_LIST_FILE, PKGS_FILE
+
+import json
+import jsonpickle
+
+jsonpickle.set_encoder_options("json", sort_keys=True)
+
+
+class UpdateCommand(Command):
+ name = "update"
+ description = "Generate nix module from input file"
+ arguments = [argument("plug_dir", description="Path to the plugin directory", optional=False)]
+ options = [
+ option("all", "a", description="Update all plugins. Else only update new plugins", flag=True),
+ option("dry-run", "d", description="Show which plugins would be updated", flag=True),
+ ]
+
+ def handle(self):
+ """Main command function"""
+
+ plug_dir = self.argument("plug_dir")
+ self.specs = read_manifest_to_spec(plug_dir)
+
+ if self.option("all"):
+ # update all plugins
+ spec_list = self.specs
+ known_plugins = []
+ else:
+ # filter plugins we already know
+ spec_list = self.specs
+
+ with open(get_const(JSON_FILE, plug_dir), "r") as json_file:
+ data = json.load(json_file)
+
+ known_specs = list(filter(lambda x: x.line in data, spec_list))
+ known_plugins = [jsonpickle.decode(data[x.line]) for x in known_specs]
+
+ spec_list = list(filter(lambda x: x.line not in data, spec_list))
+
+ if self.option("dry-run"):
+ self.line("<comment>These plugins would be updated</comment>")
+ pprint(spec_list)
+ self.line(f"<info>Total:</info> {len(spec_list)}")
+ exit(0)
+
+ processed_plugins, failed_plugins, failed_but_known = self.process_manifest(spec_list, plug_dir)
+
+ processed_plugins += known_plugins # add plugins from .plugins.json
+ processed_plugins: list = sorted(set(processed_plugins)) # remove duplicates based only on source line
+
+ self.check_duplicates(processed_plugins)
+
+ if failed_plugins != []:
+ self.line("<error>Not processed:</error> The following plugins could not be updated")
+ for s, e in failed_plugins:
+ self.line(f" - {s!r} - {e}")
+
+ if failed_but_known != []:
+ self.line(
+ "<error>Not updated:</error> The following plugins could not be updated but an older version is known"
+ )
+ for s, e in failed_but_known:
+ self.line(f" - {s!r} - {e}")
+
+ # update plugin "database"
+ self.write_plugins_json(processed_plugins, plug_dir)
+
+ # generate output
+ self.write_plugins_nix(processed_plugins, plug_dir)
+
+ self.write_plugins_markdown(processed_plugins, plug_dir)
+
+ self.line("<comment>Done</comment>")
+
+ def write_plugins_markdown(self, plugins, plug_dir):
+ """Write the list of all plugins to PLUGINS_LIST_FILE in markdown"""
+
+ plugins.sort()
+
+ self.line("<info>Updating plugins.md</info>")
+
+ header = f" - Plugin count: {len(plugins)}\n\n| Repo | Last Update | Nix package name | Last checked |\n|:---|:---|:---|:---|\n"
+
+ with open(get_const(PLUGINS_LIST_FILE, plug_dir), "w") as file:
+ file.write(header)
+ for plugin in plugins:
+ file.write(f"{plugin.to_markdown()}\n")
+
+ def write_plugins_nix(self, plugins, plug_dir):
+ self.line("<info>Generating nix output</info>")
+
+ plugins.sort()
+
+ header = "{ lib, buildVimPlugin, fetchurl, fetchgit }: {"
+ footer = "}"
+
+ with open(get_const(PKGS_FILE, plug_dir), "w") as file:
+ file.write(header)
+ for plugin in plugins:
+ file.write(f"{plugin.to_nix()}\n")
+ file.write(footer)
+
+ self.line("<info>Formatting nix output</info>")
+
+ subprocess.run(
+ ["alejandra", get_const(PKGS_FILE, plug_dir)],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+
+ def write_plugins_json(self, plugins, plug_dir):
+ self.line("<info>Storing results in .plugins.json</info>")
+
+ plugins.sort()
+
+ with open(get_const(JSON_FILE, plug_dir), "r+") as json_file:
+ data = json.load(json_file)
+
+ for plugin in plugins:
+ data.update({f"{plugin.source_line}": plugin.to_json()})
+
+ json_file.seek(0)
+ json_file.write(json.dumps(data, indent=2, sort_keys=True))
+ json_file.truncate()
+
+ def check_duplicates(self, plugins):
+ """check for duplicates in proccesed_plugins"""
+ error = False
+ for i, plugin in enumerate(plugins):
+ for p in plugins[i + 1 :]:
+ if plugin.name == p.name:
+ self.line(
+ f"<error>Error:</error> The following two lines produce the same plugin name:\n - {plugin.source_line}\n - {p.source_line}\n -> {p.name}"
+ )
+ error = True
+
+ # We want to exit if the resulting nix file would be broken
+ # But we want to go through all plugins before we do so
+ if error:
+ exit(1)
+
+ def generate_plugin(self, spec, i, size, plug_dir):
+ debug_string = ""
+
+ processed_plugin = None
+ failed_but_known = None
+ failed_plugin = None
+ try:
+ debug_string += f" - <info>({i+1}/{size}) Processing</info> {spec!r}\n"
+ vim_plugin = plugin_from_spec(spec)
+ debug_string += f" • <comment>Success</comment> {vim_plugin!r}\n"
+ processed_plugin = vim_plugin
+ except Exception as e:
+ debug_string += f" • <error>Error:</error> Could not update <info>{spec.name}</info>. Keeping old values. Reason: {e}\n"
+ with open(get_const(JSON_FILE, plug_dir), "r") as json_file:
+ data = json.load(json_file)
+
+ plugin_json = data.get(spec.line)
+ if plugin_json:
+ vim_plugin = jsonpickle.decode(plugin_json)
+ processed_plugin = vim_plugin
+ failed_but_known = (vim_plugin, e)
+ else:
+ debug_string += f" • <error>Error:</error> No entries for <info>{spec.name}</info> in '.plugins.json'. Skipping...\n"
+ failed_plugin = (spec, e)
+
+ self.line(debug_string.strip())
+
+ return processed_plugin, failed_plugin, failed_but_known
+
+ def process_manifest(self, spec_list, plug_dir):
+ """Read specs in 'spec_list' and generate plugins"""
+
+ size = len(spec_list)
+
+ # We have to assume that we will reach an api limit. Therefore
+ # we randomize the spec list to give every entry the same change to be updated and
+ # not favor those at the start of the list
+ shuffle(spec_list)
+
+ with ThreadPoolExecutor() as executor:
+ futures = [
+ executor.submit(self.generate_plugin, spec, i, size, plug_dir) for i, spec in enumerate(spec_list)
+ ]
+ results = [future.result() for future in as_completed(futures)]
+
+ processed_plugins = [r[0] for r in results]
+ failed_plugins = [r[1] for r in results]
+ failed_but_known = [r[2] for r in results]
+
+ processed_plugins = list(filter(lambda x: x is not None, processed_plugins))
+ failed_plugins = list(filter(lambda x: x is not None, failed_plugins))
+ failed_but_known = list(filter(lambda x: x is not None, failed_but_known))
+
+ processed_plugins.sort()
+ failed_plugins.sort()
+ failed_but_known.sort()
+
+ assert len(processed_plugins) == len(spec_list) - len(failed_plugins)
+
+ return processed_plugins, failed_plugins, failed_but_known