From 398fc8f29b6d5ca29f72a75f240d10531a7676da Mon Sep 17 00:00:00 2001 From: Nitai Heeb Date: Thu, 25 Jul 2024 12:32:10 +0200 Subject: [PATCH] Add init command --- src/hermes/commands/__init__.py | 1 + src/hermes/commands/cli.py | 3 +- src/hermes/commands/init/__init__.py | 3 + src/hermes/commands/init/base.py | 201 +++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/hermes/commands/init/__init__.py create mode 100644 src/hermes/commands/init/base.py diff --git a/src/hermes/commands/__init__.py b/src/hermes/commands/__init__.py index 6739fdfa..2dd6032a 100644 --- a/src/hermes/commands/__init__.py +++ b/src/hermes/commands/__init__.py @@ -10,6 +10,7 @@ from hermes.commands.base import HermesHelpCommand from hermes.commands.clean.base import HermesCleanCommand +from hermes.commands.init.base import HermesInitCommand from hermes.commands.curate.base import HermesCurateCommand from hermes.commands.harvest.base import HermesHarvestCommand from hermes.commands.process.base import HermesProcessCommand diff --git a/src/hermes/commands/cli.py b/src/hermes/commands/cli.py index e13cfcf0..44bef30e 100644 --- a/src/hermes/commands/cli.py +++ b/src/hermes/commands/cli.py @@ -11,7 +11,7 @@ import argparse from hermes.commands import HermesHelpCommand, HermesCleanCommand, HermesHarvestCommand, HermesProcessCommand, \ - HermesCurateCommand, HermesDepositCommand, HermesPostprocessCommand + HermesCurateCommand, HermesDepositCommand, HermesPostprocessCommand, HermesInitCommand from hermes.commands.base import HermesCommand @@ -34,6 +34,7 @@ def main() -> None: for command in ( HermesHelpCommand(parser), + HermesInitCommand(parser), HermesCleanCommand(parser), HermesHarvestCommand(parser), HermesProcessCommand(parser), diff --git a/src/hermes/commands/init/__init__.py b/src/hermes/commands/init/__init__.py new file mode 100644 index 00000000..faf5a2f5 --- /dev/null +++ b/src/hermes/commands/init/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR) +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/hermes/commands/init/base.py b/src/hermes/commands/init/base.py new file mode 100644 index 00000000..407a09c1 --- /dev/null +++ b/src/hermes/commands/init/base.py @@ -0,0 +1,201 @@ +# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR) +# +# SPDX-License-Identifier: Apache-2.0 + +# SPDX-FileContributor: Michael Meinel + +import argparse +import shutil +import os +import subprocess +import requests +import click +from pydantic import BaseModel + +from hermes.commands.base import HermesCommand + + +class HermesInitFolderInfo: + def __init__(self): + self.absolute_path: str = "" + self.has_git: bool = False + self.git_remote_url: str = "" + self.uses_github: bool = False + self.uses_gitlab: bool = False + self.has_hermes_toml: bool = False + self.has_gitignore: bool = False + self.has_citation_cff = False + self.current_branch: str = "" + + +def scout_current_folder() -> HermesInitFolderInfo: + info = HermesInitFolderInfo() + current_dir = os.getcwd() + info.absolute_path = str(current_dir) + info.has_git = os.path.isdir(os.path.join(current_dir, ".git")) + if info.has_git: + remote_info = subprocess.run(['git', 'remote', '-v'], capture_output=True, text=True, check=True).stdout + for line in remote_info.splitlines(): + if line.startswith("origin"): + whitespace_split = line.split() + if len(whitespace_split) > 1: + info.git_remote_url = whitespace_split[1] + break + branch_info = subprocess.run(['git', 'branch'], capture_output=True, text=True, check=True).stdout + for line in remote_info.splitlines(): + if line.startswith("*"): + info.current_branch = line.split()[1].strip() + break + info.uses_github = "github" in info.git_remote_url + info.uses_gitlab = "gitlab" in info.git_remote_url + info.has_hermes_toml = os.path.isfile(os.path.join(current_dir, "hermes.toml")) + info.has_gitignore = os.path.isfile(os.path.join(current_dir, ".gitignore")) + info.has_citation_cff = os.path.isfile(os.path.join(current_dir, "CITATION.cff")) + return info + + +def wait_until_the_user_is_done(): + if not click.confirm("Are you done?", default=False): + while not click.confirm("Are you done now?", default=True): + pass + + +def create_console_hyperlink(url: str, word: str) -> str: + return f"\033]8;;{url}\033\\{word}\033]8;;\033\\" + + +def download_file_from_url(url, filepath): + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(filepath, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + +class HermesInitSettings(BaseModel): + """Configuration of the ``init`` command.""" + pass + + +class HermesInitCommand(HermesCommand): + """ Install HERMES onto a project. """ + + command_name = "init" + settings_class = HermesInitSettings + + def __init__(self, parser: argparse.ArgumentParser): + super().__init__(parser) + self.folder_info: HermesInitFolderInfo = HermesInitFolderInfo() + + def load_settings(self, args: argparse.Namespace): + pass + + def refresh_folder_info(self): + self.folder_info = scout_current_folder() + + def __call__(self, args: argparse.Namespace) -> None: + self.refresh_folder_info() + click.echo(f"Starting to initialize HERMES in {self.folder_info.absolute_path}") + + # Abort if there is already a hermes.toml + if self.folder_info.has_hermes_toml: + click.echo("The current directory already has a `hermes.toml`. " + "It seems like HERMES was already initialized for this project.") + return + + # Abort if there is no git + if not self.folder_info.has_git: + click.echo("The current directory already has no `.git` subdirectory. " + "Please execute `hermes init` in the root directory of your git project.") + return + + # Abort if neither GitHub nor gitlab is used + if not (self.folder_info.uses_github or self.folder_info.uses_gitlab): + click.echo("Your git project ({}) is not connected to github or gitlab. It is mandatory for HERMES to " + "use one of those hosting services.".format(self.folder_info.git_remote_url)) + return + + # Creating the citation File + if not self.folder_info.has_citation_cff: + citation_cff_url = "https://citation-file-format.github.io/cff-initializer-javascript/#/" + click.echo("Your project does not contain a `CITATION.cff` file (yet). It would be very helpful for " + "saving important metadata which is necessary for publishing.") + create_cff_now = click.confirm("Do you want to create a `CITATION.cff` file now?", default=True) + if create_cff_now: + click.echo("{} to create the file. Then move it into the project folder before you continue.".format( + create_console_hyperlink(citation_cff_url, "Click here"))) + done_creating_cff = click.confirm("Are you done?", default=False) + while not done_creating_cff: + done_creating_cff = click.confirm("Are you done now?", default=True) + self.refresh_folder_info() + if self.folder_info.has_citation_cff: + click.echo("Good job!") + else: + click.echo("Hey you lied to me :( Don't forget to add the `CITATION.cff` file later!") + else: + click.echo("You better do it later or HERMES won't work properly.") + click.echo("You can {} to create the file. Then move it into the project folder.".format( + create_console_hyperlink(citation_cff_url, "click here"))) + else: + click.echo("Your project already contains a `CITATION.cff` file. Nice!") + + # Creating the hermes.toml file + hermes_toml_raw_url = "https://raw.githubusercontent.com/nheeb/zenodo-test/main/hermes.toml" + download_file_from_url(hermes_toml_raw_url, os.path.join(os.getcwd(), "hermes.toml")) + click.echo("hermes.toml was created.") + + # Adding .hermes to the .gitignore + if not self.folder_info.has_gitignore: + with open(".gitignore", 'w') as file: + pass + click.echo("A new `.gitignore` file was created.") + self.refresh_folder_info() + if self.folder_info.has_gitignore: + with open(".gitignore", "r") as file: + gitignore_lines = file.readlines() + if any([line.startswith(".hermes") for line in gitignore_lines]): + click.echo("The `.gitignore` file already contains `.hermes/`") + else: + with open(".gitignore", "a") as file: + file.write("# Ignoring all HERMES cache files") + file.write(".hermes/") + click.echo("Added `.hermes/` to the `.gitignore` file.") + + # Creating the ci file + if self.folder_info.uses_github: + github_ci_template_raw_url = ("https://raw.githubusercontent.com/softwarepub/ci-templates/main" + "/TEMPLATE_hermes_github_to_zenodo.yml") + github_folder_path = os.path.join(os.getcwd(), ".github") + if not os.path.isdir(github_folder_path): + os.mkdir(github_folder_path) + workflows_folder_path = os.path.join(github_folder_path, "workflows") + if not os.path.isdir(workflows_folder_path): + os.mkdir(workflows_folder_path) + ci_file_path = os.path.join(workflows_folder_path, "hermes_github_to_zenodo.yml") + download_file_from_url(github_ci_template_raw_url, ci_file_path) + click.echo(f"GitHub CI yml file was created at {ci_file_path}") + + # Zenodo access token -> Github Secrets + zenodo_token_url = "https://sandbox.zenodo.org/account/settings/applications/tokens/new/" + click.echo("If you haven't already, {}. You might have to create an account first.".format( + create_console_hyperlink(zenodo_token_url, "create a access token for zenodo") + )) + wait_until_the_user_is_done() + if self.folder_info.uses_github: + click.echo("Now add this token to your {} under the name ZENODO_SANDBOX.".format( + create_console_hyperlink(self.folder_info.git_remote_url.replace(".git", "/settings/secrets/actions"), + "project's GitHub secrets") + )) + wait_until_the_user_is_done() + click.echo("Next go to your {} and check the checkbox which reads:".format( + create_console_hyperlink(self.folder_info.git_remote_url.replace(".git", "/settings/actions"), + "project settings") + )) + click.echo(click.style("Allow GitHub Actions to create and approve pull requests", bold=True)) + wait_until_the_user_is_done() + click.echo("Good job!") + elif self.folder_info.uses_gitlab: + print("GITLAB INIT NOT IMPLEMENTED YET") + + click.echo("HERMES was initialized.") +