|
| 1 | +"""*Dfetch* can add projects through the cli to the manifest. |
| 2 | +
|
| 3 | +Sometimes you want to add a project to your manifest, but you don't want to |
| 4 | +edit the manifest by hand. With ``dfetch add`` you can add a project to your manifest |
| 5 | +through the command line. This will add the project to your manifest and fetch it |
| 6 | +to your disk. You can also specify a version to add, or it will be added with the |
| 7 | +latest version available. |
| 8 | +
|
| 9 | +""" |
| 10 | + |
| 11 | +import argparse |
| 12 | +import os |
| 13 | +from collections.abc import Sequence |
| 14 | +from pathlib import Path |
| 15 | + |
| 16 | +from rich.prompt import Prompt |
| 17 | + |
| 18 | +import dfetch.commands.command |
| 19 | +import dfetch.manifest.project |
| 20 | +import dfetch.project |
| 21 | +from dfetch.log import get_logger |
| 22 | +from dfetch.manifest.manifest import append_entry_manifest_file |
| 23 | +from dfetch.manifest.project import ProjectEntry, ProjectEntryDict |
| 24 | +from dfetch.manifest.remote import Remote |
| 25 | +from dfetch.project import create_sub_project, create_super_project |
| 26 | +from dfetch.util.purl import remote_url_to_purl |
| 27 | + |
| 28 | +logger = get_logger(__name__) |
| 29 | + |
| 30 | + |
| 31 | +class Add(dfetch.commands.command.Command): |
| 32 | + """Add a new project to the manifest. |
| 33 | +
|
| 34 | + Add a new project to the manifest. |
| 35 | + """ |
| 36 | + |
| 37 | + @staticmethod |
| 38 | + def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None: |
| 39 | + """Add the parser menu for this action.""" |
| 40 | + parser = dfetch.commands.command.Command.parser(subparsers, Add) |
| 41 | + |
| 42 | + parser.add_argument( |
| 43 | + "remote_url", |
| 44 | + metavar="<remote_url>", |
| 45 | + type=str, |
| 46 | + nargs=1, |
| 47 | + help="Remote URL of project to add", |
| 48 | + ) |
| 49 | + |
| 50 | + parser.add_argument( |
| 51 | + "-f", |
| 52 | + "--force", |
| 53 | + action="store_true", |
| 54 | + help="Always perform addition.", |
| 55 | + ) |
| 56 | + |
| 57 | + def __call__(self, args: argparse.Namespace) -> None: |
| 58 | + """Perform the add.""" |
| 59 | + superproject = create_super_project() |
| 60 | + |
| 61 | + purl = remote_url_to_purl(args.remote_url[0]) |
| 62 | + project_entry = ProjectEntry( |
| 63 | + ProjectEntryDict(name=purl.name, url=args.remote_url[0]) |
| 64 | + ) |
| 65 | + |
| 66 | + # Determines VCS type tries to reach remote |
| 67 | + subproject = create_sub_project(project_entry) |
| 68 | + |
| 69 | + if project_entry.name in [ |
| 70 | + project.name for project in superproject.manifest.projects |
| 71 | + ]: |
| 72 | + raise RuntimeError( |
| 73 | + f"Project with name {project_entry.name} already exists in manifest!" |
| 74 | + ) |
| 75 | + |
| 76 | + destination = _guess_destination( |
| 77 | + project_entry.name, superproject.manifest.projects |
| 78 | + ) |
| 79 | + |
| 80 | + if remote_to_use := _determine_remote( |
| 81 | + superproject.manifest.remotes, project_entry.remote_url |
| 82 | + ): |
| 83 | + logger.debug( |
| 84 | + f"Remote URL {project_entry.remote_url} matches remote {remote_to_use.name}" |
| 85 | + ) |
| 86 | + |
| 87 | + project_entry = ProjectEntry( |
| 88 | + ProjectEntryDict( |
| 89 | + name=project_entry.name, |
| 90 | + url=(project_entry.remote_url), |
| 91 | + branch=subproject.get_default_branch(), |
| 92 | + dst=destination, |
| 93 | + ), |
| 94 | + ) |
| 95 | + if remote_to_use: |
| 96 | + project_entry.set_remote(remote_to_use) |
| 97 | + |
| 98 | + logger.print_overview( |
| 99 | + project_entry.name, |
| 100 | + "Will add following entry to manifest:", |
| 101 | + project_entry.as_yaml(), |
| 102 | + ) |
| 103 | + |
| 104 | + if not args.force and not confirm(): |
| 105 | + logger.print_warning_line(project_entry.name, "Aborting add of project") |
| 106 | + return |
| 107 | + |
| 108 | + append_entry_manifest_file( |
| 109 | + (superproject.root_directory / superproject.manifest.path).absolute(), |
| 110 | + project_entry, |
| 111 | + ) |
| 112 | + |
| 113 | + logger.print_info_line(project_entry.name, "Added project to manifest") |
| 114 | + |
| 115 | + |
| 116 | +def confirm() -> bool: |
| 117 | + """Show a confirmation prompt to the user before adding the project.""" |
| 118 | + return ( |
| 119 | + Prompt.ask("Add project to manifest?", choices=["y", "n"], default="y") == "y" |
| 120 | + ) |
| 121 | + |
| 122 | + |
| 123 | +def _check_name_uniqueness( |
| 124 | + project_name: str, manifest_projects: Sequence[ProjectEntry] |
| 125 | +) -> None: |
| 126 | + """Validate that the project name is not already used in the manifest.""" |
| 127 | + if project_name in [project.name for project in manifest_projects]: |
| 128 | + raise RuntimeError( |
| 129 | + f"Project with name {project_name} already exists in manifest!" |
| 130 | + ) |
| 131 | + |
| 132 | + |
| 133 | +def _guess_destination( |
| 134 | + project_name: str, manifest_projects: Sequence[ProjectEntry] |
| 135 | +) -> str: |
| 136 | + """Guess the destination of the project based on the remote URL and existing projects.""" |
| 137 | + if len(manifest_projects) <= 1: |
| 138 | + return "" |
| 139 | + |
| 140 | + common_path = os.path.commonpath( |
| 141 | + [project.destination for project in manifest_projects] |
| 142 | + ) |
| 143 | + |
| 144 | + if common_path and common_path != os.path.sep: |
| 145 | + return (Path(common_path) / project_name).as_posix() |
| 146 | + return "" |
| 147 | + |
| 148 | + |
| 149 | +def _determine_remote(remotes: Sequence[Remote], remote_url: str) -> Remote | None: |
| 150 | + """Determine if the remote URL matches any of the remotes in the manifest.""" |
| 151 | + for remote in remotes: |
| 152 | + if remote_url.startswith(remote.url): |
| 153 | + return remote |
| 154 | + return None |
0 commit comments