diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 6ee72b5f81..fcac349708 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -110,3 +110,6 @@ class InvalidAuthorizationPrefix(CSRFTokenError): pass class InvalidAuthorizationToken(CSRFTokenError): pass class InvalidDatabaseFile(ValidationError): pass class ExecutableNotFound(FileNotFoundError): pass + +class InvalidRemoteException(Exception): + pass diff --git a/frappe/installer.py b/frappe/installer.py index d10dc78286..e28a942f01 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -5,10 +5,11 @@ import json import os import sys from collections import OrderedDict -from typing import List, Dict +from typing import List, Dict, Tuple import frappe from frappe.defaults import _clear_cache +from frappe.utils import is_git_url def _new_site( @@ -34,7 +35,6 @@ def _new_site( from frappe.commands.scheduler import _is_scheduler_enabled from frappe.utils import get_site_path, scheduler, touch_file - if not force and os.path.exists(site): print("Site {0} already exists".format(site)) sys.exit(1) @@ -124,6 +124,86 @@ def install_db(root_login=None, root_password=None, db_name=None, source_sql=Non frappe.flags.in_install_db = False +def find_org(org_repo: str) -> Tuple[str, str]: + """ find the org a repo is in + + find_org() + ref -> https://github.com/frappe/bench/blob/develop/bench/utils/__init__.py#L390 + + :param org_repo: + :type org_repo: str + + :raises InvalidRemoteException: if the org is not found + + :return: organisation and repository + :rtype: Tuple[str, str] + """ + from frappe.exceptions import InvalidRemoteException + import requests + + for org in ["frappe", "erpnext"]: + res = requests.head(f"https://api.github.com/repos/{org}/{org_repo}") + if res.ok: + return org, org_repo + + raise InvalidRemoteException + + +def fetch_details_from_tag(_tag: str) -> Tuple[str, str, str]: + """ parse org, repo, tag from string + + fetch_details_from_tag() + ref -> https://github.com/frappe/bench/blob/develop/bench/utils/__init__.py#L403 + + :param _tag: input string + :type _tag: str + + :return: organisation, repostitory, tag + :rtype: Tuple[str, str, str] + """ + app_tag = _tag.split("@") + org_repo = app_tag[0].split("/") + + try: + repo, tag = app_tag + except ValueError: + repo, tag = app_tag + [None] + + try: + org, repo = org_repo + except Exception: + org, repo = find_org(org_repo[0]) + + return org, repo, tag + + +def parse_app_name(name: str) -> str: + """parse repo name from name + + __setup_details_from_git() + ref -> https://github.com/frappe/bench/blob/develop/bench/app.py#L114 + + + :param name: git tag + :type name: str + + :return: repository name + :rtype: str + """ + name = name.rstrip("/") + if os.path.exists(name): + repo = os.path.split(name)[-1] + elif is_git_url(name): + if name.startswith("git@") or name.startswith("ssh://"): + _repo = name.split(":")[1].rsplit("/", 1)[1] + else: + _repo = name.rsplit("/", 2)[2] + repo = _repo.split(".")[0] + else: + _, repo, _ = fetch_details_from_tag(name) + return repo + + def install_app(name, verbose=False, set_as_patched=True): from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.model.sync import sync_for @@ -140,7 +220,8 @@ def install_app(name, verbose=False, set_as_patched=True): # install pre-requisites if app_hooks.required_apps: for app in app_hooks.required_apps: - install_app(app, verbose=verbose) + name = parse_app_name(name) + install_app(name, verbose=verbose) frappe.flags.in_install = name frappe.clear_cache() diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 18fca9de8c..fa6b5a3820 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import io +import os import json import unittest from datetime import date, datetime, time, timedelta @@ -14,13 +15,14 @@ import pytz from PIL import Image import frappe -from frappe.utils import ceil, evaluate_filters, floor, format_timedelta +from frappe.utils import ceil, evaluate_filters, floor, format_timedelta, get_bench_path from frappe.utils import get_url, money_in_words, parse_timedelta, scrub_urls from frappe.utils import validate_email_address, validate_url from frappe.utils.data import cast, get_time, get_timedelta, nowtime, now_datetime, validate_python_code from frappe.utils.diff import _get_value_from_version, get_version_diff, version_query from frappe.utils.image import optimize_image, strip_exif_data from frappe.utils.response import json_handler +from frappe.installer import parse_app_name class TestFilters(unittest.TestCase): @@ -510,3 +512,13 @@ class TestLinkTitle(unittest.TestCase): todo.delete() user.delete() prop_setter.delete() + +class TestAppParser(unittest.TestCase): + def test_app_name_parser(self): + bench_path = get_bench_path() + frappe_app = os.path.join(bench_path, "apps", "frappe") + self.assertEqual("frappe", parse_app_name(frappe_app)) + self.assertEqual("healthcare", parse_app_name("healthcare")) + self.assertEqual("healthcare", parse_app_name("https://github.com/frappe/healthcare.git")) + self.assertEqual("healthcare", parse_app_name("git@github.com:frappe/healthcare.git")) + self.assertEqual("healthcare", parse_app_name("frappe/healthcare@develop")) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index c361b5b430..4a6d578a9c 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -918,3 +918,8 @@ def add_user_info(user, user_info): email = info.email, time_zone = info.time_zone ) + +def is_git_url(url: str) -> bool: + # modified to allow without the tailing .git from https://github.com/jonschlinkert/is-git-url.git + pattern = r"(?:git|ssh|https?|\w*@[-\w.]+):(\/\/)?(.*?)(\.git)?(\/?|\#[-\d\w._]+?)$" + return bool(re.match(pattern, url))