From 716b980baf6821179bc8e04da2dac9b19e2ef083 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Sun, 24 Oct 2021 13:55:45 +0530 Subject: [PATCH 1/8] fetch patch --- foo/app.py | 54 ++++++++++++++++++++++++++++++++++------------- libgit/src/lib.rs | 4 +++- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/foo/app.py b/foo/app.py index 19ebe4d..21e7a46 100644 --- a/foo/app.py +++ b/foo/app.py @@ -50,6 +50,7 @@ def get_patch(url: str) -> str: url += ".patch" resp = requests.get(url) if resp.status_code == 200: + print("patch", resp.text) return resp.text def clean_url(url: str): @@ -104,7 +105,7 @@ def init_app(app): ISSSUE="Issue" -PULL ="pull" +PULL ="Pull" COMMIT = "commit" REPOSITORY = "repository" @@ -191,6 +192,17 @@ def set_updated_at(self,date): """ set comment update time""" self.payload["updated_at"] = date + def set_upstream(self,upstream): + """ set comment update time""" + print("settings upstream", upstream) + self.payload["upstream"] = upstream + + + def set_pr_url(self,url): + """ set comment pr url""" + self.payload["pr_url"] = url + + def set_type(self,notification_type): """ set comment update time""" self.payload["type"] = notification_type @@ -357,11 +369,12 @@ def apply_patch(self, patch: forge_libgit.Patch, repository_url: str, pr_url: st return branch - def process_patch(self, patch: forge_libgit.Patch, local_url: str, upstream_url, branch_name) -> str: + def process_patch(self, patch: str, local_url: str, upstream_url, branch_name) -> str: """ process patch""" repo = forge_libgit.Repo(local_settings.BASE_DIR, local_url, upstream_url) repo.fetch_upstream() - repo.apply_patch(patch, self.admin, branch_name) + patch = repo.process_patch(patch, branch_name) + print(patch) def get_owner_repo_from_url(self, url: str) -> (str, str): """ Get (owner, repo) from repository URL""" @@ -518,6 +531,12 @@ def get_notifications(self, since: datetime.datetime) -> NotificationResp: if notification_type == REPOSITORY: print(n) + if notification_type == PULL: + rn.set_pr_url(requests.request("GET", subject["url"]).json()["html_url"]) + rn.set_upstream(n["repository"]["description"]) + print(n["repository"]["description"]) + + if notification_type == ISSSUE: comment_url = subject["latest_comment_url"] print(comment_url) @@ -776,7 +795,7 @@ def fork_foreign_repository(): repository_url = client.get_repository(repository_url) info = client.get_repository_info(repository_url) local_name = get_local_repository_from_foreign_repo(repository_url) - forge.create_repository(repo=local_name, description=info["description"]) + forge.create_repository(repo=local_name, description=repository_url) forge.git_clone(repository_url, local_name) return jsonify({}) @@ -975,7 +994,6 @@ def run(app): def background_job(app): with app.app_context(): global RUNNING - print(RUNNING) if RUNNING: scheduler.enter(local_settings.JOB_RUNNER_DELAY, 8, background_job) return @@ -987,15 +1005,22 @@ def background_job(app): forge = get_forge() notifications = forge.get_notifications(since=date_parse(last_run)).get_payload() - print(notifications) - #for n in notifications: - # import json - # print(json.dumps(n)) - -# # if all([n["type"] == PULL, n["owner"] == local_settings.ADMIN_USER]): - -# # if n["type"] == - logger.warning('hello from background_job %s', time.time()) +# print(notifications) + for n in notifications: + (owner, repo) = forge.get_owner_repo_from_url(n["repo_url"]) + if all([n["type"] == PULL, owner == local_settings.ADMIN_USER]): + print("pr_url") + patch = get_patch(n["pr_url"]) + #patch = libgit.Patch(data["message"], data["author_name"], data["author_email"]) + local = n["repo_url"] + upstream = n["upstream"] + #print(local, upstream) + patch = forge.process_patch(patch, local, upstream, get_branch_name(n["pr_url"])) +# # patch = libgit.Patch(n["title"], n["owner"], local_settings.ADMIN_EMAIL) + print(patch) + + +# if n["type"] == scheduler.enter(local_settings.JOB_RUNNER_DELAY, 8, background_job, argument=(app,)) RUNNING = False @@ -1012,7 +1037,6 @@ def create_app(test_config=None): ) init_app(app) - if test_config is None: app.config.from_pyfile("config.py", silent=True) else: diff --git a/libgit/src/lib.rs b/libgit/src/lib.rs index 5141485..0bf67ec 100644 --- a/libgit/src/lib.rs +++ b/libgit/src/lib.rs @@ -307,7 +307,9 @@ impl Repo { git2::ResetType::Hard, Some(&mut checkout_options), )?; - Ok(processed_patch.as_str().unwrap().to_owned()) + let patch = processed_patch.as_str().unwrap().to_owned(); + println!("{}", patch); + Ok(patch) } } fn rm_file(repo: &Repo, file: &DiffFile) -> FResult<()> { From 323a2faecc68b234adf2a2e2ef1e4170fad451d1 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Mon, 25 Oct 2021 13:17:55 +0530 Subject: [PATCH 2/8] clean up --- Makefile | 2 +- foo/app.py | 1062 ----------------- foo/local_settings_example.py | 14 - .../20211023_01_0W52q-event-subscriptions.py | 30 - .../20211024_01_h2IuD-interface-jobs.py | 17 - foo/yoyo.ini | 6 - interface/__init__.py | 3 + interface/{forge.py => __main__.py} | 9 +- interface/api/__init__.py | 2 - interface/api/v1/__init__.py | 7 + interface/api/v1/issues.py | 79 ++ interface/api/v1/notifications.py | 67 ++ interface/api/v1/repo.py | 258 ++-- interface/app.py | 913 +------------- interface/client.py | 72 +- interface/db.py | 6 +- interface/forges/__init__.py | 12 + interface/forges/base.py | 166 +++ interface/forges/gitea.py | 83 +- interface/forges/notifications.py | 107 ++ interface/forges/payload.py | 129 ++ interface/forges/utils.py | 43 + interface/local_settings_example.py | 6 +- interface/runner.py | 155 +-- interface/utils.py | 24 +- requirements.txt | 1 - 26 files changed, 907 insertions(+), 2366 deletions(-) delete mode 100644 foo/app.py delete mode 100644 foo/local_settings_example.py delete mode 100644 foo/migrations/20211023_01_0W52q-event-subscriptions.py delete mode 100644 foo/migrations/20211024_01_h2IuD-interface-jobs.py delete mode 100644 foo/yoyo.ini create mode 100644 interface/__init__.py rename interface/{forge.py => __main__.py} (82%) create mode 100644 interface/api/v1/issues.py create mode 100644 interface/api/v1/notifications.py create mode 100644 interface/forges/base.py create mode 100644 interface/forges/notifications.py create mode 100644 interface/forges/payload.py create mode 100644 interface/forges/utils.py diff --git a/Makefile b/Makefile index 2590407..f965dda 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ default: ## Run app cd libgit && maturin build - . ./venv/bin/activate && FLASK_APP=interface/__init__.py FLASK_ENV=development flask run + . ./venv/bin/activate && pythom -m interface docker: ## Build Docker image from source docker build -t forgedfed/interface . diff --git a/foo/app.py b/foo/app.py deleted file mode 100644 index 21e7a46..0000000 --- a/foo/app.py +++ /dev/null @@ -1,1062 +0,0 @@ -from rfc3339 import rfc3339 -import datetime -import requests -from urllib.parse import urlparse, urlunparse -from dateutil.parser import parse as date_parse -import logging -import sched -import threading -import time -import datetime - - - -from flask import Blueprint, jsonify, request, Flask -#from interface import FORGE - -#from interface.db import get_db -#from interface.client import get_client, GET_REPOSITORY, GET_REPOSITORY_INFO -#from interface.client import SUBSCRIBE, COMMENT_ON_ISSUE, CREATE_ISSUE -#from interface.client import FORK_FOREIGN, FORK_LOCAL - - -import libgit as forge_libgit - - -from flask import current_app, g - -#from interface.utils import clean_url, get_branch_name, get_patch -#from interface.forges.gitea import Gitea -#from interface import local_settings -#from interface import utils - -import local_settings - - -import sqlite3 -import os - -import click -from flask import current_app, g -from flask.cli import with_appcontext -from yoyo import read_migrations -from yoyo import get_backend - -def get_patch(url: str) -> str: - """ Get patch from pull request""" - if url.endswith('/'): - url = url[0:-1] + ".patch" - else: - url += ".patch" - resp = requests.get(url) - if resp.status_code == 200: - print("patch", resp.text) - return resp.text - -def clean_url(url: str): - """Remove paths and tracking elements from URL""" - parsed = urlparse(url) - cleaned = urlunparse((parsed.scheme, parsed.netloc, "", "", "", "")) - return cleaned - -def get_branch_name(pull_request_url: str) -> str: - """ Get branch name from pull request URL """ - parsed = urlparse(pull_request_url) - return format("%s%s" % (parsed.netloc, parsed.path.replace("/", "-"))) - -def get_local_repository_from_foreign_repo(repo_url: str) -> str: - return get_branch_name(repo_url) - -def get_db() -> sqlite3.Connection: - """Get database connection""" - if "db" not in g: - g.db = sqlite3.connect( - current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES - ) - g.db.row_factory = sqlite3.Row - return g.db - -def close_db(e=None): - db = g.pop("db", None) - - if db is not None: - db.close() - -def init_db(): - """Apply database migrations""" - db = str.format("sqlite:///%s" % (current_app.config["DATABASE"])) - backend = get_backend(db) - migrations = read_migrations("./migrations/") - with backend.lock(): - backend.apply_migrations(backend.to_apply(migrations)) - backend.commit() - -@click.command("migrate") -@with_appcontext -def migrate_db_command(): - """Apply database migrations CLI handler""" - init_db() - click.echo("Migrations applied") - - -def init_app(app): - app.teardown_appcontext(close_db) - app.cli.add_command(migrate_db_command) - - -ISSSUE="Issue" -PULL ="Pull" -COMMIT = "commit" -REPOSITORY = "repository" - -class Payload: - """ Payload base class. self.mandatory should be defined""" - def __init__(self, mandatory: [str]): - self.payload = {} - self.mandatory = [] - - def get_payload(self): - """ get payload """ - for f in self.mandatory: - if self.payload[f] is None: - raise Exception("%s can't be empty" % f) - return self.payload - -class RepositoryInfo(Payload): - """ Describes a repository""" - def __init__(self): - mandatory = ["name", "owner_name"] - super().__init__(mandatory) - - def set_name(self, name): - """ Set name of repository""" - self.payload["name"] = name - - def set_owner_name(self, name): - """ Set owner name of repository""" - self.payload["owner_name"] = name - - def set_description(self, description): - """ Is this a template repository""" - self.payload["description"] = description - -class CreateIssue(Payload): - """ Create new issue payload""" - def __init__(self): - mandatory = ["title"] - super().__init__(mandatory) - - def set_title(self,title): - """ set issue title""" - self.payload["title"] = title - - def set_body(self,body): - """ set issue body""" - self.payload["body"] = body - - def set_due_date(self, due_date): - """ set issue due date""" - self.payload["due_date"] = due_date - - def set_closed(self, closed: bool): - """ set issue open status""" - self.payload["closed"] = closed - -class Comment(Payload): - def __init__(self): - mandatory = ["body", "author", "updated_at", "url"] - super().__init__(mandatory) - - def set_updated_at(self,date): - """ set comment update time""" - self.payload["updated_at"] = date - - def set_body(self,body): - """ set issue body""" - self.payload["body"] = body - - def set_author(self, author): - """ set issue author""" - self.payload["author"] = author - - def set_url(self, url): - """ set url of comment""" - self.payload["url"] = url - -class Notification(Payload): - def __init__(self): - mandatory = ["type", "state", "updated_at", "title"] - super().__init__(mandatory) - - def set_updated_at(self,date): - """ set comment update time""" - self.payload["updated_at"] = date - - def set_upstream(self,upstream): - """ set comment update time""" - print("settings upstream", upstream) - self.payload["upstream"] = upstream - - - def set_pr_url(self,url): - """ set comment pr url""" - self.payload["pr_url"] = url - - - def set_type(self,notification_type): - """ set comment update time""" - self.payload["type"] = notification_type - - def set_state(self,state): - """ set comment update time""" - self.payload["state"] = state - - def set_comment(self,comment: Comment): - """ set comment update time""" - self.payload["status"] = comment.get_payload() - - def set_repo_url(self,repo_url: str): - """ set repository URL update time""" - self.payload["repo_url"] = repo_url - - - def set_title(self,title): - """ set issue title""" - self.payload["title"] = title - - -class NotificationResp: - def __init__(self, notifications: [Notification], last_read: datetime.datetime): - self.notifications = notifications - self.last_read = last_read - def get_payload(self): - notifications = [] - for n in self.notifications: - notifications.append(n.get_payload()) - - return notifications - -class CreatePullrequest(Payload): - # see https://docs.github.com/en/rest/reference/pulls - def __init__(self): - mandatory = ["owner", "message", "repo", "head", "base", "title"] - super().__init__(mandatory) - - def set_owner(self, name): - """ Set owner name of repository""" - self.payload["owner"] = name - - def set_repo(self, repo): - """ Set owner name of repository""" - self.payload["repo"] = repo - - def set_head(self, head): - """ - From GitHub Docs: - - The name of the branch you want the changes pulled into. - This should be an existing branch on the current repository. - You cannot submit a pull request to one repository that requests a merge to - a base of another repository. - """ - self.payload["head"] = head - - def set_base(self, base): - """ - From GitHub Docs: - The name of the branch you want the changes pulled into. - This should be an existing branch on the current repository. - You cannot submit a pull request to one repository that requests a merge to - a base of another repository. - """ - self.payload["base"] = base - - def set_title(self, title): - """ set title of the PR""" - self.payload["title"] = title - - def set_message(self, message): - """ set message of the PR""" - self.payload["message"] = message - - def set_body(self, body): - """ set title of the PR message""" - self.payload["body"] = body - - - -class Forge: - def __init__(self, base_url: str, admin_user: str, admin_email): - self.base_url = urlparse(clean_url(base_url)) - if all([self.base_url.scheme != "http", self.base_url.scheme != "https"]): - print(self.base_url.scheme) - raise Exception("scheme should be wither http or https") - self.admin = forge_libgit.InterfaceAdmin(admin_email, admin_user) - - - def _lock_repo(self, local_url): - conn = db.get_db() - cur = conn.cursor() - - res = cur.execute( - "SELECT ID, is_locked from interface_repositories WHERE html_url = ?", - (local_url,),).fetch_one() - - now = rfc3339(datetime.datetime.now()) - if len(res) == 0: - cur.execute( - "INSERT OR IGNORE INTO interface_repositories (html_url, is_locked) VALUES (?);", - (local_url, now), - ) - conn.commit() - return True - else: - if res[0]["is_locked"] is None: - cur.execute( - "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", - (now, local_url), - ) - conn.commit() - cur.execute( - "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", - (None, local_url), - ) - conn.commit() - return True - return False - - def _unlock_repo(self, local_url): - conn = db.get_db() - cur = conn.cursor() - cur.execute( - "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", - (None, local_url), - ) - conn.commit() - - def git_clone(self, upstream_url: str, local_name: str): - local_url = self.get_local_html_url(local_name) - local_push_url = self.get_local_push_url(local_name) - - if self._lock_repo(local_url): - repo = forge_libgit.Repo(local_settings.BASE_DIR, local_push_url, upstream_url) - default_branch = repo.default_branch() - repo.push_local(default_branch) - self._unlock_repo(local_url) - - def get_fetch_remote(self, url: str) -> str: - """Get fetch remote for possible forge URL""" - parsed = urlparse(url) - if all([parsed.scheme != "http", parsed.scheme != "https"]): - raise Exception("scheme should be wither http or https") - if parsed.netloc != self.base_url.netloc: - raise Exception("Unsupported forge") - repo = parsed.path.split('/')[1:3] - path = format("/%s/%s" % (repo[0], repo[1])) - return urlunparse((self.base_url.scheme, self.base_url.netloc, path, "", "", "")) - - def apply_patch(self, patch: forge_libgit.Patch, repository_url: str, pr_url: str) -> str: - """apply patch""" - (_, repo) = self.get_owner_repo_from_url(repository_url) - local_url = self.get_local_html_url(repo) - local_push_url = self.get_local_push_url(repo) - branch = get_branch_name(pr_url) - if self._lock_repo(local_url): - repo = forge_libgit.Repo(local_settings.BASE_DIR, local_push_url, repository_url) - repo.apply_patch(patch, self.admin, branch) - repo.push_loca(branch) - self._unlock_repo(local_url) - return branch - - - def process_patch(self, patch: str, local_url: str, upstream_url, branch_name) -> str: - """ process patch""" - repo = forge_libgit.Repo(local_settings.BASE_DIR, local_url, upstream_url) - repo.fetch_upstream() - patch = repo.process_patch(patch, branch_name) - print(patch) - - def get_owner_repo_from_url(self, url: str) -> (str, str): - """ Get (owner, repo) from repository URL""" - url = self.get_fetch_remote(url) - parsed = urlparse(url) - details = parsed.path.split('/')[1:3] - (owner, repo) = (details[0], details[1]) - return (owner, repo) - - def get_local_html_url(self, repo: str) -> str: - """ get local repository's HTML url""" - raise NotImplementedError - - def get_local_push_url(self, repo: str) -> str: - raise NotImplementedError - - - """ Forge characteristics. All interfaces must implement this class""" - def get_issues(self, owner: str, repo: str, *args, **kwargs): - """ Get issues on a repository. Supports pagination via 'page' optional param""" - raise NotImplementedError - - def create_issue(self, owner: str, repo: str, issue: CreateIssue) -> str: - """ Creates issue on a repository. reurns html url of the newly created issue""" - raise NotImplementedError - - def get_repository(self, owner: str, repo: str) -> RepositoryInfo: - """ Get repository details""" - raise NotImplementedError - - def create_repository(self, repo: str, description: str): - """ Create new repository """ - raise NotImplementedError - - def subscribe(self, owner: str, repo: str): - """ subscribe to events in repository""" - raise NotImplementedError - - def get_notifications(self, since: datetime.datetime) -> NotificationResp: - """ subscribe to events in repository""" - raise NotImplementedError - - def create_pull_request(self, pr: CreatePullrequest) -> str: - """ - create pull request - return value is the URL(HTML page) of the newely created PR - """ - raise NotImplementedError - - def fork(self, owner: str, repo:str): - """ Fork a repository """ - raise NotImplementedError - - def close_pr(self, owner: str, repo:str): - """ Fork a repository """ - raise NotImplementedError - - def comment_on_issue(self, owner: str, repo: str, issue_url: str, body:str): - """Add comment on an existing issue""" - raise NotImplementedError - -class Gitea(Forge): - def __init__(self, base_url: str, admin_user: str, admin_email): - super().__init__(base_url=base_url, admin_user=admin_user, admin_email=admin_email) - self.host = urlparse(clean_url(local_settings.GITEA_HOST)) - - def _auth(self): - return {'Authorization': format("token %s" % (local_settings.GITEA_API_KEY))} - - def _get_url(self, path: str) -> str: - prefix = "/api/v1/" - if path.startswith('/'): - path=path[1:] - - path = format("%s%s" % (prefix, path)) - url = urlunparse((self.host.scheme, self.host.netloc, path, "", "", "")) - return url - - def get_issues(self, owner: str, repo: str, *args, **kwargs): - """ Get issues on a repository. Supports pagination via 'page' optional param""" - query = {} - since = kwargs.get('since') - if since is not None: - query["since"] = since - - page = kwargs.get('page') - if page is not None: - query["page"] = page - - url = self._get_url(format("/repos/%s/%s/issues" % (owner, repo))) - - headers = self._auth() - response = requests.request("GET", url, params=query, headers=headers) - return response.json() - - def create_issue(self, owner: str, repo: str, issue: CreateIssue): - """ Creates issue on a repository""" - url = self._get_url(format("/repos/%s/%s/issues" % (owner, repo))) - - headers = self._auth() - payload = issue.get_payload() - response = requests.request("POST", url, json=payload, headers=headers) - data = response.json() - return data["html_url"] - - def _into_repository(self, data) -> RepositoryInfo: - info = RepositoryInfo() - info.set_description(data["description"]) - info.set_name(data["name"]) - info.set_owner_name(data["owner"]["login"]) - return info - - def get_repository(self, owner: str, repo: str) -> RepositoryInfo: - """ Get repository details""" - url = self._get_url(format("/repos/%s/%s" % (owner, repo))) - response = requests.request("GET", url) - data = response.json() - info = self._into_repository(data) - return info - - def create_repository(self, repo: str, description: str): - url = self._get_url("/user/repos/") - payload = { "name" : repo, "description": description} - headers = self._auth() - _response = requests.request("POST", url, json=payload, headers=headers) - - def subscribe(self, owner: str, repo: str): - url = self._get_url(format("/repos/%s/%s/subscription" % (owner, repo))) - headers = self._auth() - _response = requests.request("PUT", url, headers=headers) - - - def get_notifications(self, since: datetime.datetime) -> NotificationResp: - query = {} - query["since"] = rfc3339(since) - url = self._get_url("/notifications") - headers = self._auth() - response = requests.request("GET", url, params=query, headers=headers) - notifications = response.json() - last_read = "" - val = [] - for n in notifications: - # resp notification - rn = Notification() - subject = n["subject"] - notification_type = subject["type"] - - last_read = n["updated_at"] - rn.set_updated_at(last_read) - rn.set_type(notification_type) - rn.set_title(subject["title"]) - rn.set_state(subject["state"]) - rn.set_repo_url(n["repository"]["html_url"]) - - if notification_type == REPOSITORY: - print(n) - if notification_type == PULL: - rn.set_pr_url(requests.request("GET", subject["url"]).json()["html_url"]) - rn.set_upstream(n["repository"]["description"]) - print(n["repository"]["description"]) - - - if notification_type == ISSSUE: - comment_url = subject["latest_comment_url"] - print(comment_url) - if len(comment_url) != 0: - resp = requests.request("GET", comment_url) - comment = resp.json() - if date_parse(comment["updated_at"]) > since: - c = Comment() - c.set_updated_at(comment["updated_at"]) - c.set_author(comment["user"]["login"]) - c.set_body(comment["body"]) - pr_url = comment["pull_request_url"] - if len(comment["pull_request_url"]) == 0: - c.set_url(comment["issue_url"]) - else: - url = pr_url - c.set_url(comment["pull_request_url"]) - rn.set_comment(c) - val.append(rn) - return NotificationResp(val, date_parse(last_read)) - - def create_pull_request(self, pr: CreatePullrequest): - url = self._get_url(format("/repos/%s/%s/pulls" , (pr.owner, pr.repo))) - headers = self._auth() - - payload = pr.get_payload() - for key in ["repo", "owner"]: - del payload[key] - - payload["assignees"] = [] - payload["lables"] = [0] - payload["milestones"] = 0 - - response = requests.request("POST", url, json=payload, headers=headers) - return response.json()["html_url"] - - def fork(self, owner: str, repo:str): - """ Fork a repository """ - url = self._get_url(format("/repos/%s/%s/forks" % (owner, repo))) - print(url) - headers = self._auth() - payload = {"oarganization" :"bot"} - _response = requests.request("POST", url, json=payload, headers=headers) - - def get_issue_index(self, issue_url, owner: str) -> int: - parsed = urlparse(issue_url) - path = parsed.path - path.endswith('/') - if path.endswith('/'): - path=path[0:-1] - index = path.split(owner)[0].split('issue')[2] - if index.startswith('/'): - index = index[1:] - - if index.endswith('/'): - index = index[0:-1] - - return int(index) - - - def comment_on_issue(self, owner: str, repo: str, issue_url: str, body:str): - headers = self._auth() - (owner, repo) = self.get_fetch_remote(issue_url) - index = self.get_issue_index(issue_url, owner) - url = self._get_url(format("/repos/%s/%s/issues/%s" % (owner, repo, index))) - payload = {"body": body} - _response = requests.request("POST", url, json=payload, headers=headers) - - def get_local_html_url(self, repo:str) -> str: - path = format("/%s/%s", local_settings.GITEA_USERNAME, repo) - return urlunparse((self.host.scheme, self.host.netloc, path, "", "", "")) - - def get_local_push_url(self, repo:str) -> str: - return format("git@%s:%s/%s.git", self.host.netloc, local_settings.GITEA_USERNAME, repo) - -def get_forge() -> Forge: - return Gitea(base_url=local_settings.GITEA_HOST, - admin_user=local_settings.ADMIN_USER, - admin_email=local_settings.ADMIN_EMAIL) - - - -from urllib.parse import urlparse, urlunparse -import requests - -from flask import g - -#from interface import forge -#from interface import db -#from interface.api.v1.repo import GET_REPOSITORY - -GET_REPOSITORY = "/fetch" -GET_REPOSITORY_INFO = "/info" -FORK_LOCAL = "/fork/local" -FORK_FOREIGN = "/fork/foreign" -SUBSCRIBE = "/subscribe" -COMMENT_ON_ISSUE = "/issues/comment" -CREATE_ISSUE = "/issue/create" -CREATE_PULL_REQUEST = "/pull/create" - -class ForgeClient: - def __init__(self, forge: Forge): - self.forge = forge - self.interfaces = [ - { - "forge": "https://github.com", - "interface": "https://github-interface.shuttlecraft.io", - }, - { - "forge": "https://git.batsense.net", - "interface": "https://gitea-interface.shuttlecraft.io", - } - ] - def _construct_url(self, interface_url: str, path: str) -> str: - """ Get interface API routes""" - prefix = "/api/v1/" - if path.startswith('/'): - path=path[1:] - - path = format("%s%s" % (prefix, path)) - parsed = urlparse(interface_url) - url = urlunparse((parsed.scheme, parsed.netloc, path, "", "", "")) - return url - - - def find_interface(self, url: str): - parsed = urlparse(url) - for interface in self.interfaces: - if urlparse(interface["forge"]).netloc == parsed.netloc: - return interface["interface"] - - - def get_repository(self, repo_url: str): - """ Get foreign repository url """ - interface_url = self.forge.find_interface(repo_url) - interface_api_url = self._construct_url(interface_url=interface_url, path=GET_REPOSITORY) - - payload = { "url": repo_url } - response = requests.request("POST", interface_api_url, json=payload) - data = response.json() - return data["repository_url"] - - def get_repository_info(self, repo_url: str): - """ Get foreign repository url """ - interface_url = self.forge.find_interface(repo_url) - interface_api_url = self._construct_url(interface_url=interface_url, path=GET_REPOSITORY_INFO) - - payload = { "repository_url": repo_url } - response = requests.request("POST", interface_api_url, json=payload) - data = response.json() - return data - - -def get_client() -> ForgeClient: - if "client" not in g: - g.client = ForgeClient(db.get_forge()) - return g.client - - - -#from .errors import Error - -bp = Blueprint("API_V1_INTERFACE", __name__, url_prefix="/api/v1/repository") - -#F_D_EMPTY_FORGE_LIST = Error( -# errcode="F_D_EMPTY_FORGE_LIST", -# error="The forge list submitted is empty", -# status=400, -#) -# -#F_D_INTERFACE_UNREACHABLE = Error( -# errcode="F_D_INTERFACE_UNREACHABLE", -# error="The interface was unreachable with the publicly accessible URL provided", -# status=503, -#) - - -@bp.route(GET_REPOSITORY, methods=["POST"]) -def get_repository(): - """ - get repository URL - - ## Request - { - "url": string - } - - ## Response - { - "repository_url": string - } - """ - data = request.json() - payload = { "repository_url": get_forge().get_fetch_remote(data["url"]) } - return jsonify(payload) - -@bp.route(GET_REPOSITORY_INFO, methods=["POST"]) -def get_repository_info(): - """ - get repository INFO - - ## Request - { - "repository_url": string - } - - ## Response - { - "name": string - "owner": string - "description": string - } - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - resp = forge.get_repository(owner, repo).get_payload() - return jsonify(resp) - -@bp.route(FORK_LOCAL, methods=["POST"]) -def fork_local_repository(): - """ - fork local repository - - ## Request - { - "repository_url": string - } - - ## Response - { } # empty json - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - forge.fork(owner, repo) - return jsonify({}) - -@bp.route(FORK_FOREIGN, methods=["POST"]) -def fork_foreign_repository(): - """ - fork foreign repository - - ## Request - { - "repository_url": string - } - - ## Response - { } # empty json - """ - data = request.json() - forge = get_forge() - repository_url = data["repository_url"] - client = get_client() - repository_url = client.get_repository(repository_url) - info = client.get_repository_info(repository_url) - local_name = get_local_repository_from_foreign_repo(repository_url) - forge.create_repository(repo=local_name, description=repository_url) - forge.git_clone(repository_url, local_name) - return jsonify({}) - -@bp.route(SUBSCRIBE, methods=["POST"]) -def subscribe(): - """ - subscribe to repository - - ## Request - { - "repository_url": string - "interface_url": string - } - - ## Response - { } # empty json - """ - data = request.json() - forge = get_forge() - repository_url = forge.get_fetch_remote(data["repository_url"]) - interface_url = forge.get_fetch_remote(data["interface_url"]) - (owner, repo) = forge.get_owner_repo_from_url(repository_url) - forge.subscribe(owner, repo) - - conn = get_db() - cur = conn.cursor() - cur.execute( - "INSERT OR IGNORE INTO interface_repositories (html_url) VALUES (?);", - (repository_url,), - ) - cur.execute( - "INSERT OR IGNORE INTO interface_interfaces (url) VALUES (?);", - (interface_url,), - ) - conn.commit() - cur.execute( - """ - INSERT OR IGNORE INTO interface_event_subscriptsions (repository_id, interface_id) - VALUES ( - (SELECT interface_interfaces WHERE url = ?), - (SELECT interface_repositories WHERE html_url = ?) - ); - """, - (interface_url,repository_url), - ) - return jsonify({}) - -@bp.route(CREATE_ISSUE, methods=["POST"]) -def create_issue(): - """ - create new issue - - ## Request - { - "repository_url": string - "title": string - "body": string - "due_date": string - "closed": bool - } - - ## Response - { - "html_url": string // of the newly created issue - } - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - - c = CreateIssue() - c.set_title(data["title"]) - c.set_body(data["body"]) - c.set_due_date(data["due_date"]) - c.set_closed(data["closed"]) - - resp = {"html_url" : forge.create_issue(owner, repo, c) } - return jsonify(resp) - - -@bp.route(COMMENT_ON_ISSUE, methods=["POST"]) -def comment_on_issue(): - """ - get repository URL - - ## Request - { - "issue_url": string // of the target issue - "body": string // message body - } - - ## Response - { } - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - forge.comment_on_issue(owner, repo, issue_url=data["issue_url"], body=data["body"]) - return jsonify({}) - - -@bp.route(COMMENT_ON_ISSUE, methods=["POST"]) -def create_pull_request(): - """ - get repository URL - - ## Request - { - "repository_url": string // of the target issue - "pr_url": string // pull request url - "message": string // message body - "head": string - "base" string - "title": string - "patch": string - "author_name": string - "author_email": string - } - - ## Response - { } - """ - data = request.json() - forge = get_forge() - repository_url = data["repository_url"] - (owner, repo) = forge.get_owner_repo_from_url(repository_url) - try: - forge.fork(owner, repo) - except: - pass - forge.comment_on_issue(owner, repo, issue_url=data["issue_url"], body=data["body"]) - patch = libgit.Patch(data["message"], data["author_name"], data["author_email"]) - branch = forge.apply_patch(patch, repository_url, data["pr_url"]) - pr = CreatePullrequest() - pr.set_base(data["base"]) - pr.set_body(data["message"]) - pr.set_title(data["title"]) - pr.set_owner(owner) - pr.set_repo(repo) - pr.set_head(format("%s:%s", forge.admin.name, branch)) - - resp = {"html_url" : forge.create_pull_request(pr) } - return jsonify(resp) - -RUNNING = False -APP = "" - -def init(app): - # global APP - with app.app_context(): - conn = get_db() - cur = conn.cursor() - last_run = date_parse("2021-10-10T17:06:02+05:30") - cur.execute( - "INSERT OR IGNORE INTO interface_jobs_run (this_interface_url, last_run) VALUES (?, ?);", - (local_settings.INTERFACE_URL, str(last_run)), - ) - conn.commit() - -def update_time(time: datetime.datetime, app): - # global APP - with app.app_context(): - conn = get_db() - cur = conn.cursor() - cur.execute( - "UPDATE interface_jobs_run set last_run = ? WHERE this_interface_url = ?;", - (str(time), local_settings.INTERFACE_URL), - ) - conn.commit() - - -def get_last_run(app): - with app.app_context(): - conn = get_db() - cur = conn.cursor() - res = cur.execute( - "SELECT last_run FROM interface_jobs_run WHERE this_interface_url = ?;", - (local_settings.INTERFACE_URL,), - ).fetchone() - return res[0] - -#def proces_pull_request(): - - - -def run(app): - #global APP - #APP = app - with app.app_context(): - logging.getLogger('jobs').setLevel(logging.WARNING) - logger = logging.getLogger('jobs') - scheduler = sched.scheduler(time.time, time.sleep) - - init(app) - - def background_job(app): - with app.app_context(): - global RUNNING - if RUNNING: - scheduler.enter(local_settings.JOB_RUNNER_DELAY, 8, background_job) - return - else: - RUNNING = True - - last_run = get_last_run(app) - print(last_run) - - forge = get_forge() - notifications = forge.get_notifications(since=date_parse(last_run)).get_payload() -# print(notifications) - for n in notifications: - (owner, repo) = forge.get_owner_repo_from_url(n["repo_url"]) - if all([n["type"] == PULL, owner == local_settings.ADMIN_USER]): - print("pr_url") - patch = get_patch(n["pr_url"]) - #patch = libgit.Patch(data["message"], data["author_name"], data["author_email"]) - local = n["repo_url"] - upstream = n["upstream"] - #print(local, upstream) - patch = forge.process_patch(patch, local, upstream, get_branch_name(n["pr_url"])) -# # patch = libgit.Patch(n["title"], n["owner"], local_settings.ADMIN_EMAIL) - print(patch) - - -# if n["type"] == - scheduler.enter(local_settings.JOB_RUNNER_DELAY, 8, background_job, argument=(app,)) - RUNNING = False - - scheduler.enter(local_settings.JOB_RUNNER_DELAY, 8, background_job, argument=(app,)) - threading.Thread(target=scheduler.run).start() - - - -def create_app(test_config=None): - # create and configure the app - app = Flask(__name__, instance_relative_config=True) - app.config.from_mapping( - DATABASE=os.path.join(app.instance_path, "interface.db"), - ) - - init_app(app) - if test_config is None: - app.config.from_pyfile("config.py", silent=True) - else: - app.config.from_mapping(test_config) - - try: - os.makedirs(app.instance_path) - except OSError: - pass - - @app.after_request - def flock_google(response): - response.headers["Permissions-Policy"] = "interest-cohort=()" - return response - - run(app) - - app.register_blueprint(bp) - return app - -if __name__ == "__main__": - app = create_app() - app.run(threaded=True,port=7000) diff --git a/foo/local_settings_example.py b/foo/local_settings_example.py deleted file mode 100644 index 5ef6f3f..0000000 --- a/foo/local_settings_example.py +++ /dev/null @@ -1,14 +0,0 @@ -GITEA_API_KEY = "" -GITEA_USERNAME = "" -GITEA_HOST = "" -GITHUB_HOST ="" -GITHUB_API_KEY = "" - -INTERFACE_URL = "" # URL at which this interface is available - - -BASE_DIR = "" - -ADMIN_EMAIL = "" -ADMIN_USER = "" -JOB_RUNNER_DELAY = 10 ## in seconds diff --git a/foo/migrations/20211023_01_0W52q-event-subscriptions.py b/foo/migrations/20211023_01_0W52q-event-subscriptions.py deleted file mode 100644 index f900a6e..0000000 --- a/foo/migrations/20211023_01_0W52q-event-subscriptions.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -event subscriptions -""" - -from yoyo import step - -__depends__ = {} - -steps = [ - step(""" - CREATE TABLE IF NOT EXISTS interface_repositories( - is_locked VARCHAR(100) DEFAULT NULL, - html_url VARCHAR(3000) UNIQUE NOT NULL, - ID INTEGER PRIMARY KEY NOT NULL - ); - - """), - step(""" - CREATE TABLE IF NOT EXISTS interface_interfaces( - url VARCHAR(3000) UNIQUE NOT NULL, - ID INTEGER PRIMARY KEY NOT NULL - ); - """), - step(""" - CREATE TABLE IF NOT EXISTS interface_event_subscriptsions( - repository_id INTEGER NOT NULL REFERENCES interface_repositories(ID) ON DELETE CASCADE, - interface_id INTEGER NOT NULL REFERENCES interface_interfaces(ID) ON DELETE CASCADE - ); - """) -] diff --git a/foo/migrations/20211024_01_h2IuD-interface-jobs.py b/foo/migrations/20211024_01_h2IuD-interface-jobs.py deleted file mode 100644 index 921cbc5..0000000 --- a/foo/migrations/20211024_01_h2IuD-interface-jobs.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -interface jobs -""" - -from yoyo import step - -__depends__ = {'20211023_01_0W52q-event-subscriptions'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS interface_jobs_run( - this_interface_url VARCHAR(3000) NOT NULL UNIQUE PRIMARY KEY, - last_run VARCHAR(50) NOT NULL - ); - """) -] diff --git a/foo/yoyo.ini b/foo/yoyo.ini deleted file mode 100644 index 1e952fc..0000000 --- a/foo/yoyo.ini +++ /dev/null @@ -1,6 +0,0 @@ -[DEFAULT] -sources = ./migrations -migration_table = _yoyo_migration -batch_mode = off -verbosity = 0 -database = sqlite:///instance/interface.db diff --git a/interface/__init__.py b/interface/__init__.py new file mode 100644 index 0000000..6aa5aad --- /dev/null +++ b/interface/__init__.py @@ -0,0 +1,3 @@ +""" +Interface: A software forge bridge that creates a distributed software development environment +""" diff --git a/interface/forge.py b/interface/__main__.py similarity index 82% rename from interface/forge.py rename to interface/__main__.py index 40d6971..ee29c5c 100644 --- a/interface/forge.py +++ b/interface/__main__.py @@ -1,3 +1,6 @@ +""" +Run ForgeFed Interface flask application +""" # Bridges software forges to create a distributed software development environment # Copyright © 2021 Aravinth Manivannan # @@ -13,5 +16,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -""" defines basic data structuctures and interfaces used in a forge fed interface""" +from interface.app import create_app + +if __name__ == "__main__": + app = create_app() + app.run(threaded=True, port=7000) diff --git a/interface/api/__init__.py b/interface/api/__init__.py index 6e5c78b..c2db7fa 100644 --- a/interface/api/__init__.py +++ b/interface/api/__init__.py @@ -12,5 +12,3 @@ # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from .v1 import bp -from . import v1 diff --git a/interface/api/v1/__init__.py b/interface/api/v1/__init__.py index 7d208ee..5b49a6f 100644 --- a/interface/api/v1/__init__.py +++ b/interface/api/v1/__init__.py @@ -1,3 +1,6 @@ +""" +Version 1 API +""" # Bridges software forges to create a distributed software development environment # Copyright © 2021 Aravinth Manivannan # @@ -14,6 +17,10 @@ # along with this program. If not, see . from flask import Blueprint from . import repo +from . import issues +from . import notifications bp = Blueprint("API_V1", __name__, url_prefix="/api/v1") bp.register_blueprint(repo.bp) +bp.register_blueprint(notifications.bp) +bp.register_blueprint(issues.bp) diff --git a/interface/api/v1/issues.py b/interface/api/v1/issues.py new file mode 100644 index 0000000..df8326f --- /dev/null +++ b/interface/api/v1/issues.py @@ -0,0 +1,79 @@ +""" +Issues related routes +""" +# Bridges software forges to create a distributed software development environment +# Copyright © 2021 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from flask import Blueprint, jsonify, request + +from interface.forges import get_forge +from interface.client import CREATE_ISSUE, COMMENT_ON_ISSUE +from interface.forges.payload import CreateIssue + +bp = Blueprint("API_V1_ISSUES", __name__, url_prefix="/issues") + + +@bp.route(CREATE_ISSUE, methods=["POST"]) +def create_issue(): + """ + create new issue + + ## Request + { + "repository_url": string + "title": string + "body": string + "due_date": string + "closed": bool + } + + ## Response + { + "html_url": string // of the newly created issue + } + """ + data = request.json() + forge = get_forge() + (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) + + c = CreateIssue() + c.set_title(data["title"]) + c.set_body(data["body"]) + c.set_due_date(data["due_date"]) + c.set_closed(data["closed"]) + + resp = {"html_url": forge.create_issue(owner, repo, c)} + return jsonify(resp) + + +@bp.route(COMMENT_ON_ISSUE, methods=["POST"]) +def comment_on_issue(): + """ + get repository URL + + ## Request + { + "issue_url": string // of the target issue + "body": string // message body + } + + ## Response + { } + """ + data = request.json() + forge = get_forge() + (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) + forge.comment_on_issue(owner, repo, issue_url=data["issue_url"], body=data["body"]) + return jsonify({}) diff --git a/interface/api/v1/notifications.py b/interface/api/v1/notifications.py new file mode 100644 index 0000000..3f0df68 --- /dev/null +++ b/interface/api/v1/notifications.py @@ -0,0 +1,67 @@ +# Bridges software forges to create a distributed software development environment +# Copyright © 2021 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from flask import Blueprint, jsonify, request + +from interface import db +from interface.forges import get_forge +from interface.client import SUBSCRIBE + +bp = Blueprint("API_V1_NOTIFICATIONS", __name__, url_prefix="/notifications") + + +@bp.route(SUBSCRIBE, methods=["POST"]) +def subscribe(): + """ + subscribe to repository + + ## Request + { + "repository_url": string + "interface_url": string + } + + ## Response + { } # empty json + """ + data = request.json() + forge = get_forge() + repository_url = forge.get_fetch_remote(data["repository_url"]) + interface_url = forge.get_fetch_remote(data["interface_url"]) + (owner, repo) = forge.get_owner_repo_from_url(repository_url) + forge.subscribe(owner, repo) + + conn = db.get_db() + cur = conn.cursor() + cur.execute( + "INSERT OR IGNORE INTO interface_repositories (html_url) VALUES (?);", + (repository_url,), + ) + cur.execute( + "INSERT OR IGNORE INTO interface_interfaces (url) VALUES (?);", + (interface_url,), + ) + conn.commit() + cur.execute( + """ + INSERT OR IGNORE INTO interface_event_subscriptsions (repository_id, interface_id) + VALUES ( + (SELECT interface_interfaces WHERE url = ?), + (SELECT interface_repositories WHERE html_url = ?) + ); + """, + (interface_url, repository_url), + ) + return jsonify({}) diff --git a/interface/api/v1/repo.py b/interface/api/v1/repo.py index a5c7ac4..db89dd9 100644 --- a/interface/api/v1/repo.py +++ b/interface/api/v1/repo.py @@ -1,3 +1,6 @@ +""" +Repository related routes +""" # Bridges software forges to create a distributed software development environment # Copyright © 2021 Aravinth Manivannan # @@ -13,240 +16,129 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from urllib.parse import urlparse, urlunparse - from flask import Blueprint, jsonify, request -from interface.utils import clean_url, get_local_repository_from_foreign_repo -#from interface import FORGE -from interface.forge import CreateIssue, CreatePullrequest - -from interface.db import get_db -from interface.forge import get_forge -from interface.client import get_client, GET_REPOSITORY, GET_REPOSITORY_INFO -from interface.client import SUBSCRIBE, COMMENT_ON_ISSUE, CREATE_ISSUE -from interface.client import FORK_FOREIGN, FORK_LOCAL -#from .errors import Error +from libgit import Patch -bp = Blueprint("API_V1_INTERFACE", __name__, url_prefix="/repository") +from interface.forges import get_forge +from interface.forges.payload import CreatePullrequest +from interface.forges.utils import get_local_repository_from_foreign_repo +from interface.client import GET_REPOSITORY, GET_REPOSITORY_INFO, FORK_FOREIGN +from interface.client import FORK_LOCAL, CREATE_PULL_REQUEST, get_client -#F_D_EMPTY_FORGE_LIST = Error( -# errcode="F_D_EMPTY_FORGE_LIST", -# error="The forge list submitted is empty", -# status=400, -#) -# -#F_D_INTERFACE_UNREACHABLE = Error( -# errcode="F_D_INTERFACE_UNREACHABLE", -# error="The interface was unreachable with the publicly accessible URL provided", -# status=503, -#) +bp = Blueprint("API_V1_REPO", __name__, url_prefix="/repository") @bp.route(GET_REPOSITORY, methods=["POST"]) def get_repository(): """ - get repository URL + get repository URL - ## Request - { - "url": string - } + ## Request + { + "url": string + } - ## Response - { - "repository_url": string - } + ## Response + { + "repository_url": string + } """ - data = request.json() - payload = { "repository_url": get_forge().get_fetch_remote(data["url"]) } + data = request.json() + payload = {"repository_url": get_forge().get_fetch_remote(data["url"])} return jsonify(payload) + @bp.route(GET_REPOSITORY_INFO, methods=["POST"]) def get_repository_info(): """ - get repository INFO + get repository INFO - ## Request - { - "repository_url": string - } + ## Request + { + "repository_url": string + } - ## Response - { - "name": string - "owner": string - "description": string - } + ## Response + { + "name": string + "owner": string + "description": string + } """ - data = request.json() + data = request.json() forge = get_forge() (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) resp = forge.get_repository(owner, repo).get_payload() return jsonify(resp) + @bp.route(FORK_LOCAL, methods=["POST"]) def fork_local_repository(): """ - fork local repository + fork local repository - ## Request - { - "repository_url": string - } + ## Request + { + "repository_url": string + } - ## Response - { } # empty json + ## Response + { } # empty json """ - data = request.json() + data = request.json() forge = get_forge() (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) forge.fork(owner, repo) return jsonify({}) + @bp.route(FORK_FOREIGN, methods=["POST"]) def fork_foreign_repository(): """ - fork foreign repository + fork foreign repository - ## Request - { - "repository_url": string - } + ## Request + { + "repository_url": string + } - ## Response - { } # empty json + ## Response + { } # empty json """ - data = request.json() + data = request.json() forge = get_forge() repository_url = data["repository_url"] client = get_client() repository_url = client.get_repository(repository_url) info = client.get_repository_info(repository_url) local_name = get_local_repository_from_foreign_repo(repository_url) - forge.create_repository(repo=local_name, description=info["description"]) + forge.create_repository(repo=local_name, description=repository_url) forge.git_clone(repository_url, local_name) return jsonify({}) -@bp.route(SUBSCRIBE, methods=["POST"]) -def subscribe(): - """ - subscribe to repository - - ## Request - { - "repository_url": string - "interface_url": string - } - - ## Response - { } # empty json - """ - data = request.json() - forge = get_forge() - repository_url = forge.get_fetch_remote(data["repository_url"]) - interface_url = forge.get_fetch_remote(data["interface_url"]) - (owner, repo) = forge.get_owner_repo_from_url(repository_url) - forge.subscribe(owner, repo) - - conn = get_db() - cur = conn.cursor() - cur.execute( - "INSERT OR IGNORE INTO interface_repositories (html_url) VALUES (?);", - (repository_url,), - ) - cur.execute( - "INSERT OR IGNORE INTO interface_interfaces (url) VALUES (?);", - (interface_url,), - ) - conn.commit() - cur.execute( - """ - INSERT OR IGNORE INTO interface_event_subscriptsions (repository_id, interface_id) - VALUES ( - (SELECT interface_interfaces WHERE url = ?), - (SELECT interface_repositories WHERE html_url = ?) - ); - """, - (interface_url,repository_url), - ) - return jsonify({}) - -@bp.route(CREATE_ISSUE, methods=["POST"]) -def create_issue(): - """ - create new issue - - ## Request - { - "repository_url": string - "title": string - "body": string - "due_date": string - "closed": bool - } - - ## Response - { - "html_url": string // of the newly created issue - } - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - - c = CreateIssue() - c.set_title(data["title"]) - c.set_body(data["body"]) - c.set_due_date(data["due_date"]) - c.set_closed(data["closed"]) - - resp = {"html_url" : forge.create_issue(owner, repo, c) } - return jsonify(resp) - - -@bp.route(COMMENT_ON_ISSUE, methods=["POST"]) -def comment_on_issue(): - """ - get repository URL - - ## Request - { - "issue_url": string // of the target issue - "body": string // message body - } - - ## Response - { } - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - forge.comment_on_issue(owner, repo, issue_url=data["issue_url"], body=data["body"]) - return jsonify({}) - -@bp.route(COMMENT_ON_ISSUE, methods=["POST"]) +@bp.route(CREATE_PULL_REQUEST, methods=["POST"]) def create_pull_request(): """ - get repository URL - - ## Request - { - "repository_url": string // of the target issue - "pr_url": string // pull request url - "message": string // message body - "head": string - "base" string - "title": string - "patch": string - "author_name": string - "author_email": string - } - - ## Response - { } - """ - data = request.json() + get repository URL + + ## Request + { + "repository_url": string // of the target issue + "pr_url": string // pull request url + "message": string // message body + "head": string + "base" string + "title": string + "patch": string + "author_name": string + "author_email": string + } + + ## Response + { } + """ + data = request.json() forge = get_forge() repository_url = data["repository_url"] (owner, repo) = forge.get_owner_repo_from_url(repository_url) @@ -255,7 +147,7 @@ def create_pull_request(): except: pass forge.comment_on_issue(owner, repo, issue_url=data["issue_url"], body=data["body"]) - patch = libgit.Patch(data["message"], data["author_name"], data["author_email"]) + patch = Patch(data["message"], data["author_name"], data["author_email"]) branch = forge.apply_patch(patch, repository_url, data["pr_url"]) pr = CreatePullrequest() pr.set_base(data["base"]) @@ -265,5 +157,5 @@ def create_pull_request(): pr.set_repo(repo) pr.set_head(format("%s:%s", forge.admin.name, branch)) - resp = {"html_url" : forge.create_pull_request(pr) } + resp = {"html_url": forge.create_pull_request(pr)} return jsonify(resp) diff --git a/interface/app.py b/interface/app.py index 04b361d..cb00d4d 100644 --- a/interface/app.py +++ b/interface/app.py @@ -1,901 +1,39 @@ -from rfc3339 import rfc3339 -import datetime -import requests -from urllib.parse import urlparse, urlunparse -from dateutil.parser import parse as date_parse - -from flask import Blueprint, jsonify, request -#from interface import FORGE - -from interface.db import get_db -#from interface.client import get_client, GET_REPOSITORY, GET_REPOSITORY_INFO -#from interface.client import SUBSCRIBE, COMMENT_ON_ISSUE, CREATE_ISSUE -#from interface.client import FORK_FOREIGN, FORK_LOCAL - - -import libgit as forge_libgit - - -from flask import current_app, g - -from interface.utils import clean_url, get_branch_name, get_patch -from interface.forges.gitea import Gitea -from interface import local_settings -from interface import utils - -import sqlite3 +""" +Flask application +""" +# Bridges software forges to create a distributed software development environment +# Copyright © 2021 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . import os -import click -from flask import current_app, g -from flask.cli import with_appcontext -from yoyo import read_migrations -from yoyo import get_backend - -def get_db() -> sqlite3.Connection: - """Get database connection""" - if "db" not in g: - g.db = sqlite3.connect( - current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES - ) - g.db.row_factory = sqlite3.Row - return g.db - -def close_db(e=None): - db = g.pop("db", None) - - if db is not None: - db.close() - -def init_db(): - """Apply database migrations""" - db = str.format("sqlite:///%s" % (current_app.config["DATABASE"])) - backend = get_backend(db) - migrations = read_migrations("./migrations/") - with backend.lock(): - backend.apply_migrations(backend.to_apply(migrations)) - backend.commit() - -@click.command("migrate") -@with_appcontext -def migrate_db_command(): - """Apply database migrations CLI handler""" - init_db() - click.echo("Migrations applied") - - -def init_app(app): - app.teardown_appcontext(close_db) - app.cli.add_command(migrate_db_command) - - -ISSSUE="Issue" -PULL ="pull" -COMMIT = "commit" -REPOSITORY = "repository" - -class Payload: - """ Payload base class. self.mandatory should be defined""" - def __init__(self, mandatory: [str]): - self.payload = {} - self.mandatory = [] - - def get_payload(self): - """ get payload """ - for f in self.mandatory: - if self.payload[f] is None: - raise Exception("%s can't be empty" % f) - return self.payload - -class RepositoryInfo(Payload): - """ Describes a repository""" - def __init__(self): - mandatory = ["name", "owner_name"] - super().__init__(mandatory) - - def set_name(self, name): - """ Set name of repository""" - self.payload["name"] = name - - def set_owner_name(self, name): - """ Set owner name of repository""" - self.payload["owner_name"] = name - - def set_description(self, description): - """ Is this a template repository""" - self.payload["description"] = description - -class CreateIssue(Payload): - """ Create new issue payload""" - def __init__(self): - mandatory = ["title"] - super().__init__(mandatory) - - def set_title(self,title): - """ set issue title""" - self.payload["title"] = title - - def set_body(self,body): - """ set issue body""" - self.payload["body"] = body - - def set_due_date(self, due_date): - """ set issue due date""" - self.payload["due_date"] = due_date - - def set_closed(self, closed: bool): - """ set issue open status""" - self.payload["closed"] = closed - -class Comment(Payload): - def __init__(self): - mandatory = ["body", "author", "updated_at", "url"] - super().__init__(mandatory) - - def set_updated_at(self,date): - """ set comment update time""" - self.payload["updated_at"] = date - - def set_body(self,body): - """ set issue body""" - self.payload["body"] = body - - def set_author(self, author): - """ set issue author""" - self.payload["author"] = author - - def set_url(self, url): - """ set url of comment""" - self.payload["url"] = url - -class Notification(Payload): - def __init__(self): - mandatory = ["type", "state", "updated_at", "title"] - super().__init__(mandatory) - - def set_updated_at(self,date): - """ set comment update time""" - self.payload["updated_at"] = date - - def set_type(self,notification_type): - """ set comment update time""" - self.payload["type"] = notification_type - - def set_state(self,state): - """ set comment update time""" - self.payload["state"] = state - - def set_comment(self,comment: Comment): - """ set comment update time""" - self.payload["status"] = comment.get_payload() - - def set_repo_url(self,repo_url: str): - """ set repository URL update time""" - self.payload["repo_url"] = repo_url - - - def set_title(self,title): - """ set issue title""" - self.payload["title"] = title - - -class NotificationResp: - def __init__(self, notifications: [Notification], last_read: datetime.datetime): - self.notifications = notifications - self.last_read = last_read - def get_payload(self): - notifications = [] - for n in self.notifications: - notifications.append(n.get_payload()) - - return notifications - -class CreatePullrequest(Payload): - # see https://docs.github.com/en/rest/reference/pulls - def __init__(self): - mandatory = ["owner", "message", "repo", "head", "base", "title"] - super().__init__(mandatory) - - def set_owner(self, name): - """ Set owner name of repository""" - self.payload["owner"] = name - - def set_repo(self, repo): - """ Set owner name of repository""" - self.payload["repo"] = repo - - def set_head(self, head): - """ - From GitHub Docs: - - The name of the branch you want the changes pulled into. - This should be an existing branch on the current repository. - You cannot submit a pull request to one repository that requests a merge to - a base of another repository. - """ - self.payload["head"] = head - - def set_base(self, base): - """ - From GitHub Docs: - The name of the branch you want the changes pulled into. - This should be an existing branch on the current repository. - You cannot submit a pull request to one repository that requests a merge to - a base of another repository. - """ - self.payload["base"] = base - - def set_title(self, title): - """ set title of the PR""" - self.payload["title"] = title - - def set_message(self, message): - """ set message of the PR""" - self.payload["message"] = message - - def set_body(self, body): - """ set title of the PR message""" - self.payload["body"] = body - - - -class Forge: - def __init__(self, base_url: str, admin_user: str, admin_email): - self.base_url = urlparse(clean_url(base_url)) - if all([self.base_url.scheme != "http", self.base_url.scheme != "https"]): - print(self.base_url.scheme) - raise Exception("scheme should be wither http or https") - self.admin = forge_libgit.InterfaceAdmin(admin_email, admin_user) - - - def _lock_repo(self, local_url): - conn = db.get_db() - cur = conn.cursor() - - res = cur.execute( - "SELECT ID, is_locked from interface_repositories WHERE html_url = ?", - (local_url,),).fetch_one() - - now = rfc3339(datetime.datetime.now()) - if len(res) == 0: - cur.execute( - "INSERT OR IGNORE INTO interface_repositories (html_url, is_locked) VALUES (?);", - (local_url, now), - ) - conn.commit() - return True - else: - if res[0]["is_locked"] is None: - cur.execute( - "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", - (now, local_url), - ) - conn.commit() - cur.execute( - "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", - (None, local_url), - ) - conn.commit() - return True - return False - - def _unlock_repo(self, local_url): - conn = db.get_db() - cur = conn.cursor() - cur.execute( - "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", - (None, local_url), - ) - conn.commit() - - def git_clone(self, upstream_url: str, local_name: str): - local_url = self.get_local_html_url(local_name) - local_push_url = self.get_local_push_url(local_name) - - if self._lock_repo(local_url): - repo = forge_libgit.Repo(local_settings.BASE_DIR, local_push_url, upstream_url) - default_branch = repo.default_branch() - repo.push_local(default_branch) - self._unlock_repo(local_url) - - def get_fetch_remote(self, url: str) -> str: - """Get fetch remote for possible forge URL""" - parsed = urlparse(url) - if all([parsed.scheme != "http", parsed.scheme != "https"]): - raise Exception("scheme should be wither http or https") - if parsed.netloc != self.base_url.netloc: - raise Exception("Unsupported forge") - repo = parsed.path.split('/')[1:3] - path = format("/%s/%s" % (repo[0], repo[1])) - return urlunparse((self.base_url.scheme, self.base_url.netloc, path, "", "", "")) - - def apply_patch(self, patch: forge_libgit.Patch, repository_url: str, pr_url: str) -> str: - """apply patch""" - (_, repo) = self.get_owner_repo_from_url(repository_url) - local_url = self.get_local_html_url(repo) - local_push_url = self.get_local_push_url(repo) - branch = get_branch_name(pr_url) - if self._lock_repo(local_url): - repo = forge_libgit.Repo(local_settings.BASE_DIR, local_push_url, repository_url) - repo.apply_patch(patch, self.admin, branch) - repo.push_loca(branch) - self._unlock_repo(local_url) - return branch - - - def process_patch(self, patch: forge_libgit.Patch, local_url: str, upstream_url, branch_name) -> str: - """ process patch""" - repo = forge_libgit.Repo(local_settings.BASE_DIR, local_url, upstream_url) - repo.fetch_upstream() - repo.apply_patch(patch, self.admin, branch_name) - - def get_owner_repo_from_url(self, url: str) -> (str, str): - """ Get (owner, repo) from repository URL""" - url = self.get_fetch_remote(url) - parsed = urlparse(url) - details = parsed.path.split('/')[1:3] - (owner, repo) = (details[0], details[1]) - return (owner, repo) - - def get_local_html_url(self, repo: str) -> str: - """ get local repository's HTML url""" - raise NotImplementedError - - def get_local_push_url(self, repo: str) -> str: - raise NotImplementedError - - - """ Forge characteristics. All interfaces must implement this class""" - def get_issues(self, owner: str, repo: str, *args, **kwargs): - """ Get issues on a repository. Supports pagination via 'page' optional param""" - raise NotImplementedError - - def create_issue(self, owner: str, repo: str, issue: CreateIssue) -> str: - """ Creates issue on a repository. reurns html url of the newly created issue""" - raise NotImplementedError - - def get_repository(self, owner: str, repo: str) -> RepositoryInfo: - """ Get repository details""" - raise NotImplementedError - - def create_repository(self, repo: str, description: str): - """ Create new repository """ - raise NotImplementedError - - def subscribe(self, owner: str, repo: str): - """ subscribe to events in repository""" - raise NotImplementedError - - def get_notifications(self, since: datetime.datetime) -> NotificationResp: - """ subscribe to events in repository""" - raise NotImplementedError - - def create_pull_request(self, pr: CreatePullrequest) -> str: - """ - create pull request - return value is the URL(HTML page) of the newely created PR - """ - raise NotImplementedError - - def fork(self, owner: str, repo:str): - """ Fork a repository """ - raise NotImplementedError - - def close_pr(self, owner: str, repo:str): - """ Fork a repository """ - raise NotImplementedError - - def comment_on_issue(self, owner: str, repo: str, issue_url: str, body:str): - """Add comment on an existing issue""" - raise NotImplementedError - -class Gitea(Forge): - def __init__(self, base_url: str, admin_user: str, admin_email): - super().__init__(base_url=base_url, admin_user=admin_user, admin_email=admin_email) - self.host = urlparse(utils.clean_url(local_settings.GITEA_HOST)) - - def _auth(self): - return {'Authorization': format("token %s" % (local_settings.GITEA_API_KEY))} - - def _get_url(self, path: str) -> str: - prefix = "/api/v1/" - if path.startswith('/'): - path=path[1:] - - path = format("%s%s" % (prefix, path)) - url = urlunparse((self.host.scheme, self.host.netloc, path, "", "", "")) - return url +from flask import Flask - def get_issues(self, owner: str, repo: str, *args, **kwargs): - """ Get issues on a repository. Supports pagination via 'page' optional param""" - query = {} - since = kwargs.get('since') - if since is not None: - query["since"] = since - - page = kwargs.get('page') - if page is not None: - query["page"] = page - - url = self._get_url(format("/repos/%s/%s/issues" % (owner, repo))) - - headers = self._auth() - response = requests.request("GET", url, params=query, headers=headers) - return response.json() - - def create_issue(self, owner: str, repo: str, issue: CreateIssue): - """ Creates issue on a repository""" - url = self._get_url(format("/repos/%s/%s/issues" % (owner, repo))) - - headers = self._auth() - payload = issue.get_payload() - response = requests.request("POST", url, json=payload, headers=headers) - data = response.json() - return data["html_url"] - - def _into_repository(self, data) -> RepositoryInfo: - info = RepositoryInfo() - info.set_description(data["description"]) - info.set_name(data["name"]) - info.set_owner_name(data["owner"]["login"]) - return info - - def get_repository(self, owner: str, repo: str) -> RepositoryInfo: - """ Get repository details""" - url = self._get_url(format("/repos/%s/%s" % (owner, repo))) - response = requests.request("GET", url) - data = response.json() - info = self._into_repository(data) - return info - - def create_repository(self, repo: str, description: str): - url = self._get_url("/user/repos/") - payload = { "name" : repo, "description": description} - headers = self._auth() - _response = requests.request("POST", url, json=payload, headers=headers) - - def subscribe(self, owner: str, repo: str): - url = self._get_url(format("/repos/%s/%s/subscription" % (owner, repo))) - headers = self._auth() - _response = requests.request("PUT", url, headers=headers) - - - def get_notifications(self, since: datetime.datetime) -> NotificationResp: - query = {} - query["since"] = rfc3339(since) - url = self._get_url("/notifications") - headers = self._auth() - response = requests.request("GET", url, params=query, headers=headers) - notifications = response.json() - last_read = "" - val = [] - for n in notifications: - # resp notification - rn = Notification() - subject = n["subject"] - notification_type = subject["type"] - - last_read = n["updated_at"] - rn.set_updated_at(last_read) - rn.set_type(notification_type) - rn.set_title(subject["title"]) - rn.set_state(subject["state"]) - rn.set_repo_url(n["repository"]["html_url"]) - - if notification_type == REPOSITORY: - print(n) - if notification_type == ISSSUE: - comment_url = subject["latest_comment_url"] - print(comment_url) - if len(comment_url) != 0: - resp = requests.request("GET", comment_url) - comment = resp.json() - if date_parse(comment["updated_at"]) > since: - c = Comment() - c.set_updated_at(comment["updated_at"]) - c.set_author(comment["user"]["login"]) - c.set_body(comment["body"]) - pr_url = comment["pull_request_url"] - if len(comment["pull_request_url"]) == 0: - c.set_url(comment["issue_url"]) - else: - url = pr_url - c.set_url(comment["pull_request_url"]) - rn.set_comment(c) - val.append(rn) - return NotificationResp(val, date_parse(last_read)) - - def create_pull_request(self, pr: CreatePullrequest): - url = self._get_url(format("/repos/%s/%s/pulls" , (pr.owner, pr.repo))) - headers = self._auth() - - payload = pr.get_payload() - for key in ["repo", "owner"]: - del payload[key] - - payload["assignees"] = [] - payload["lables"] = [0] - payload["milestones"] = 0 - - response = requests.request("POST", url, json=payload, headers=headers) - return response.json()["html_url"] - - def fork(self, owner: str, repo:str): - """ Fork a repository """ - url = self._get_url(format("/repos/%s/%s/forks" % (owner, repo))) - print(url) - headers = self._auth() - payload = {"oarganization" :"bot"} - _response = requests.request("POST", url, json=payload, headers=headers) - - def get_issue_index(self, issue_url, owner: str) -> int: - parsed = urlparse(issue_url) - path = parsed.path - path.endswith('/') - if path.endswith('/'): - path=path[0:-1] - index = path.split(owner)[0].split('issue')[2] - if index.startswith('/'): - index = index[1:] - - if index.endswith('/'): - index = index[0:-1] - - return int(index) - - - def comment_on_issue(self, owner: str, repo: str, issue_url: str, body:str): - headers = self._auth() - (owner, repo) = self.get_fetch_remote(issue_url) - index = self.get_issue_index(issue_url, owner) - url = self._get_url(format("/repos/%s/%s/issues/%s" % (owner, repo, index))) - payload = {"body": body} - _response = requests.request("POST", url, json=payload, headers=headers) - - def get_local_html_url(self, repo:str) -> str: - path = format("/%s/%s", local_settings.GITEA_USERNAME, repo) - return urlunparse((self.host.scheme, self.host.netloc, path, "", "", "")) - - def get_local_push_url(self, repo:str) -> str: - return format("git@%s:%s/%s.git", self.host.netloc, local_settings.GITEA_USERNAME, repo) - -def get_forge() -> Forge: - return Gitea(base_url=local_settings.GITEA_HOST, - admin_user=local_settings.ADMIN_USER, - admin_email=local_settings.ADMIN_EMAIL) - - - -from urllib.parse import urlparse, urlunparse -import requests - -from flask import g - -from interface import forge from interface import db -#from interface.api.v1.repo import GET_REPOSITORY - -GET_REPOSITORY = "/fetch" -GET_REPOSITORY_INFO = "/info" -FORK_LOCAL = "/fork/local" -FORK_FOREIGN = "/fork/foreign" -SUBSCRIBE = "/subscribe" -COMMENT_ON_ISSUE = "/issues/comment" -CREATE_ISSUE = "/issue/create" -CREATE_PULL_REQUEST = "/pull/create" - -class ForgeClient: - def __init__(self, forge: forge.Forge): - self.forge = forge - self.interfaces = [ - { - "forge": "https://github.com", - "interface": "https://github-interface.shuttlecraft.io", - }, - { - "forge": "https://git.batsense.net", - "interface": "https://gitea-interface.shuttlecraft.io", - } - ] - def _construct_url(self, interface_url: str, path: str) -> str: - """ Get interface API routes""" - prefix = "/api/v1/" - if path.startswith('/'): - path=path[1:] - - path = format("%s%s" % (prefix, path)) - parsed = urlparse(interface_url) - url = urlunparse((parsed.scheme, parsed.netloc, path, "", "", "")) - return url - - - def find_interface(self, url: str): - parsed = urlparse(url) - for interface in self.interfaces: - if urlparse(interface["forge"]).netloc == parsed.netloc: - return interface["interface"] - - - def get_repository(self, repo_url: str): - """ Get foreign repository url """ - interface_url = self.forge.find_interface(repo_url) - interface_api_url = self._construct_url(interface_url=interface_url, path=GET_REPOSITORY) - - payload = { "url": repo_url } - response = requests.request("POST", interface_api_url, json=payload) - data = response.json() - return data["repository_url"] - - def get_repository_info(self, repo_url: str): - """ Get foreign repository url """ - interface_url = self.forge.find_interface(repo_url) - interface_api_url = self._construct_url(interface_url=interface_url, path=GET_REPOSITORY_INFO) - - payload = { "repository_url": repo_url } - response = requests.request("POST", interface_api_url, json=payload) - data = response.json() - return data - - -def get_client() -> ForgeClient: - if "client" not in g: - g.client = ForgeClient(db.get_forge()) - return g.client - - - -#from .errors import Error - -bp = Blueprint("API_V1_INTERFACE", __name__, url_prefix="/api/v1/repository") - -#F_D_EMPTY_FORGE_LIST = Error( -# errcode="F_D_EMPTY_FORGE_LIST", -# error="The forge list submitted is empty", -# status=400, -#) -# -#F_D_INTERFACE_UNREACHABLE = Error( -# errcode="F_D_INTERFACE_UNREACHABLE", -# error="The interface was unreachable with the publicly accessible URL provided", -# status=503, -#) - - -@bp.route(GET_REPOSITORY, methods=["POST"]) -def get_repository(): - """ - get repository URL - - ## Request - { - "url": string - } - - ## Response - { - "repository_url": string - } - """ - data = request.json() - payload = { "repository_url": get_forge().get_fetch_remote(data["url"]) } - return jsonify(payload) - -@bp.route(GET_REPOSITORY_INFO, methods=["POST"]) -def get_repository_info(): - """ - get repository INFO - - ## Request - { - "repository_url": string - } - - ## Response - { - "name": string - "owner": string - "description": string - } - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - resp = forge.get_repository(owner, repo).get_payload() - return jsonify(resp) - -@bp.route(FORK_LOCAL, methods=["POST"]) -def fork_local_repository(): - """ - fork local repository - - ## Request - { - "repository_url": string - } - - ## Response - { } # empty json - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - forge.fork(owner, repo) - return jsonify({}) - -@bp.route(FORK_FOREIGN, methods=["POST"]) -def fork_foreign_repository(): - """ - fork foreign repository - - ## Request - { - "repository_url": string - } - - ## Response - { } # empty json - """ - data = request.json() - forge = get_forge() - repository_url = data["repository_url"] - client = get_client() - repository_url = client.get_repository(repository_url) - info = client.get_repository_info(repository_url) - local_name = get_local_repository_from_foreign_repo(repository_url) - forge.create_repository(repo=local_name, description=info["description"]) - forge.git_clone(repository_url, local_name) - return jsonify({}) - -@bp.route(SUBSCRIBE, methods=["POST"]) -def subscribe(): - """ - subscribe to repository - - ## Request - { - "repository_url": string - "interface_url": string - } - - ## Response - { } # empty json - """ - data = request.json() - forge = get_forge() - repository_url = forge.get_fetch_remote(data["repository_url"]) - interface_url = forge.get_fetch_remote(data["interface_url"]) - (owner, repo) = forge.get_owner_repo_from_url(repository_url) - forge.subscribe(owner, repo) - - conn = get_db() - cur = conn.cursor() - cur.execute( - "INSERT OR IGNORE INTO interface_repositories (html_url) VALUES (?);", - (repository_url,), - ) - cur.execute( - "INSERT OR IGNORE INTO interface_interfaces (url) VALUES (?);", - (interface_url,), - ) - conn.commit() - cur.execute( - """ - INSERT OR IGNORE INTO interface_event_subscriptsions (repository_id, interface_id) - VALUES ( - (SELECT interface_interfaces WHERE url = ?), - (SELECT interface_repositories WHERE html_url = ?) - ); - """, - (interface_url,repository_url), - ) - return jsonify({}) - -@bp.route(CREATE_ISSUE, methods=["POST"]) -def create_issue(): - """ - create new issue - - ## Request - { - "repository_url": string - "title": string - "body": string - "due_date": string - "closed": bool - } - - ## Response - { - "html_url": string // of the newly created issue - } - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - - c = CreateIssue() - c.set_title(data["title"]) - c.set_body(data["body"]) - c.set_due_date(data["due_date"]) - c.set_closed(data["closed"]) - - resp = {"html_url" : forge.create_issue(owner, repo, c) } - return jsonify(resp) - - -@bp.route(COMMENT_ON_ISSUE, methods=["POST"]) -def comment_on_issue(): - """ - get repository URL - - ## Request - { - "issue_url": string // of the target issue - "body": string // message body - } - - ## Response - { } - """ - data = request.json() - forge = get_forge() - (owner, repo) = forge.get_owner_repo_from_url(data["repository_url"]) - forge.comment_on_issue(owner, repo, issue_url=data["issue_url"], body=data["body"]) - return jsonify({}) - - -@bp.route(COMMENT_ON_ISSUE, methods=["POST"]) -def create_pull_request(): - """ - get repository URL - - ## Request - { - "repository_url": string // of the target issue - "pr_url": string // pull request url - "message": string // message body - "head": string - "base" string - "title": string - "patch": string - "author_name": string - "author_email": string - } - - ## Response - { } - """ - data = request.json() - forge = get_forge() - repository_url = data["repository_url"] - (owner, repo) = forge.get_owner_repo_from_url(repository_url) - try: - forge.fork(owner, repo) - except: - pass - forge.comment_on_issue(owner, repo, issue_url=data["issue_url"], body=data["body"]) - patch = libgit.Patch(data["message"], data["author_name"], data["author_email"]) - branch = forge.apply_patch(patch, repository_url, data["pr_url"]) - pr = CreatePullrequest() - pr.set_base(data["base"]) - pr.set_body(data["message"]) - pr.set_title(data["title"]) - pr.set_owner(owner) - pr.set_repo(repo) - pr.set_head(format("%s:%s", forge.admin.name, branch)) - - resp = {"html_url" : forge.create_pull_request(pr) } - return jsonify(resp) +from interface.api.v1 import bp +from interface.runner import Runner def create_app(test_config=None): + """Create flask application""" # create and configure the app app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( DATABASE=os.path.join(app.instance_path, "interface.db"), ) - init_app(app) + db.init_app(app) if test_config is None: app.config.from_pyfile("config.py", silent=True) @@ -912,10 +50,7 @@ def flock_google(response): response.headers["Permissions-Policy"] = "interest-cohort=()" return response -# runner.run(app) + Runner(app).run() app.register_blueprint(bp) return app - - -app = create_app() diff --git a/interface/client.py b/interface/client.py index ae7fb2e..99ffac6 100644 --- a/interface/client.py +++ b/interface/client.py @@ -15,76 +15,96 @@ # along with this program. If not, see . from urllib.parse import urlparse, urlunparse import requests +from urllib.parse import urlparse, urlunparse from flask import g -from interface import forge -from interface import db -#from interface.api.v1.repo import GET_REPOSITORY +from interface.forges.base import Forge +from interface.db import get_db GET_REPOSITORY = "/fetch" GET_REPOSITORY_INFO = "/info" FORK_LOCAL = "/fork/local" FORK_FOREIGN = "/fork/foreign" SUBSCRIBE = "/subscribe" -COMMENT_ON_ISSUE = "/issues/comment" +COMMENT_ON_ISSUE = "/issues/comment" CREATE_ISSUE = "/issue/create" CREATE_PULL_REQUEST = "/pull/create" + class ForgeClient: - def __init__(self, forge: forge.Forge): + def __init__(self, forge: Forge): self.forge = forge self.interfaces = [ - { - "forge": "https://github.com", - "interface": "https://github-interface.shuttlecraft.io", - }, - { - "forge": "https://git.batsense.net", - "interface": "https://gitea-interface.shuttlecraft.io", - } - ] + { + "forge": "https://github.com", + "interface": "https://github-interface.shuttlecraft.io", + }, + { + "forge": "https://git.batsense.net", + "interface": "https://gitea-interface.shuttlecraft.io", + }, + ] + def _construct_url(self, interface_url: str, path: str) -> str: - """ Get interface API routes""" + """Get interface API routes""" prefix = "/api/v1/" - if path.startswith('/'): - path=path[1:] + if path.startswith("/"): + path = path[1:] path = format("%s%s" % (prefix, path)) parsed = urlparse(interface_url) url = urlunparse((parsed.scheme, parsed.netloc, path, "", "", "")) return url - def find_interface(self, url: str): parsed = urlparse(url) for interface in self.interfaces: if urlparse(interface["forge"]).netloc == parsed.netloc: return interface["interface"] - def get_repository(self, repo_url: str): - """ Get foreign repository url """ + """Get foreign repository url""" interface_url = self.forge.find_interface(repo_url) - interface_api_url = self._construct_url(interface_url=interface_url, path=GET_REPOSITORY) + interface_api_url = self._construct_url( + interface_url=interface_url, path=GET_REPOSITORY + ) - payload = { "url": repo_url } + payload = {"url": repo_url} response = requests.request("POST", interface_api_url, json=payload) data = response.json() return data["repository_url"] def get_repository_info(self, repo_url: str): - """ Get foreign repository url """ + """Get foreign repository url""" interface_url = self.forge.find_interface(repo_url) - interface_api_url = self._construct_url(interface_url=interface_url, path=GET_REPOSITORY_INFO) + interface_api_url = self._construct_url( + interface_url=interface_url, path=GET_REPOSITORY_INFO + ) - payload = { "repository_url": repo_url } + payload = {"repository_url": repo_url} response = requests.request("POST", interface_api_url, json=payload) data = response.json() return data +# def send_contributions(self, patch, upstream, pr_url, message): +# interface_url = self.forge.find_interface(upstream) +# interface_api_url = self._construct_url(interface_url=interface_url, path=GET_REPOSITORY) +# payload = { +# "repository_url": upstream, +# "pr_url": pr_url +# "message": string +# "head": "master" +# "base" string +# "title": string +# "patch": string +# "author_name": string +# "author_email": string +# + + def get_client() -> ForgeClient: if "client" not in g: - g.client = ForgeClient(db.get_forge()) + g.client = ForgeClient(get_forge()) return g.client diff --git a/interface/db.py b/interface/db.py index 225809c..b599ef8 100644 --- a/interface/db.py +++ b/interface/db.py @@ -21,7 +21,8 @@ from yoyo import read_migrations from yoyo import get_backend -import interface.local_settings +from interface import local_settings + def get_db() -> sqlite3.Connection: """Get database connection""" @@ -32,12 +33,14 @@ def get_db() -> sqlite3.Connection: g.db.row_factory = sqlite3.Row return g.db + def close_db(e=None): db = g.pop("db", None) if db is not None: db.close() + def init_db(): """Apply database migrations""" db = str.format("sqlite:///%s" % (current_app.config["DATABASE"])) @@ -47,6 +50,7 @@ def init_db(): backend.apply_migrations(backend.to_apply(migrations)) backend.commit() + @click.command("migrate") @with_appcontext def migrate_db_command(): diff --git a/interface/forges/__init__.py b/interface/forges/__init__.py index d74a074..4dbab8a 100644 --- a/interface/forges/__init__.py +++ b/interface/forges/__init__.py @@ -1,3 +1,6 @@ +""" +Forge behavior +""" # Bridges software forges to create a distributed software development environment # Copyright © 2021 Aravinth Manivannan # @@ -13,3 +16,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from .gitea import Gitea +from .. import local_settings + + +def get_forge(): + return Gitea( + local_settings.GITEA_HOST, + local_settings.GITEA_USERNAME, + local_settings.ADMIN_EMAIL, + ) diff --git a/interface/forges/base.py b/interface/forges/base.py new file mode 100644 index 0000000..aaa5a9a --- /dev/null +++ b/interface/forges/base.py @@ -0,0 +1,166 @@ +import datetime +from urllib.parse import urlparse, urlunparse + +from libgit import InterfaceAdmin, Repo, Patch +import rfc3339 + +from interface.db import get_db +from interface import local_settings +from .notifications import Notification, NotificationResp, Comment +from .payload import RepositoryInfo, CreatePullrequest, CreateIssue +from .utils import clean_url, get_branch_name + + +class Forge: + def __init__(self, base_url: str, admin_user: str, admin_email): + self.base_url = urlparse(clean_url(base_url)) + if all([self.base_url.scheme != "http", self.base_url.scheme != "https"]): + print(self.base_url.scheme) + raise Exception("scheme should be wither http or https") + self.admin = InterfaceAdmin(admin_email, admin_user) + + def _lock_repo(self, local_url): + conn = get_db() + cur = conn.cursor() + + res = cur.execute( + "SELECT ID, is_locked from interface_repositories WHERE html_url = ?", + (local_url,), + ).fetch_one() + + now = rfc3339.rfc3339(datetime.datetime.now()) + if len(res) == 0: + cur.execute( + "INSERT OR IGNORE INTO interface_repositories (html_url, is_locked) VALUES (?);", + (local_url, now), + ) + conn.commit() + return True + else: + if res[0]["is_locked"] is None: + cur.execute( + "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", + (now, local_url), + ) + conn.commit() + cur.execute( + "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", + (None, local_url), + ) + conn.commit() + return True + return False + + def _unlock_repo(self, local_url): + conn = get_db() + cur = conn.cursor() + cur.execute( + "UPDATE interface_repositories is_locked = ? WHERE html_url = ?;", + (None, local_url), + ) + conn.commit() + + def git_clone(self, upstream_url: str, local_name: str): + local_url = self.get_local_html_url(local_name) + local_push_url = self.get_local_push_url(local_name) + + if self._lock_repo(local_url): + repo = Repo(local_settings.BASE_DIR, local_push_url, upstream_url) + default_branch = repo.default_branch() + repo.push_local(default_branch) + self._unlock_repo(local_url) + + def get_fetch_remote(self, url: str) -> str: + """Get fetch remote for possible forge URL""" + parsed = urlparse(url) + if all([parsed.scheme != "http", parsed.scheme != "https"]): + raise Exception("scheme should be wither http or https") + if parsed.netloc != self.base_url.netloc: + raise Exception("Unsupported forge") + repo = parsed.path.split("/")[1:3] + path = format("/%s/%s" % (repo[0], repo[1])) + return urlunparse( + (self.base_url.scheme, self.base_url.netloc, path, "", "", "") + ) + + def apply_patch(self, patch: Patch, repository_url: str, pr_url: str) -> str: + """apply patch""" + (_, repo) = self.get_owner_repo_from_url(repository_url) + local_url = self.get_local_html_url(repo) + local_push_url = self.get_local_push_url(repo) + branch = get_branch_name(pr_url) + if self._lock_repo(local_url): + repo = Repo(local_settings.BASE_DIR, local_push_url, repository_url) + repo.apply_patch(patch, self.admin, branch) + repo.push_loca(branch) + self._unlock_repo(local_url) + return branch + + def process_patch( + self, patch: str, local_url: str, upstream_url, branch_name + ) -> str: + """process patch""" + repo = Repo(local_settings.BASE_DIR, local_url, upstream_url) + repo.fetch_upstream() + patch = repo.process_patch(patch, branch_name) + return patch + + def get_owner_repo_from_url(self, url: str) -> (str, str): + """Get (owner, repo) from repository URL""" + url = self.get_fetch_remote(url) + parsed = urlparse(url) + details = parsed.path.split("/")[1:3] + (owner, repo) = (details[0], details[1]) + return (owner, repo) + + def get_local_html_url(self, repo: str) -> str: + """get local repository's HTML url""" + raise NotImplementedError + + def get_local_push_url(self, repo: str) -> str: + raise NotImplementedError + + """ Forge characteristics. All interfaces must implement this class""" + + def get_issues(self, owner: str, repo: str, *args, **kwargs): + """Get issues on a repository. Supports pagination via 'page' optional param""" + raise NotImplementedError + + def create_issue(self, owner: str, repo: str, issue: CreateIssue) -> str: + """Creates issue on a repository. reurns html url of the newly created issue""" + raise NotImplementedError + + def get_repository(self, owner: str, repo: str) -> RepositoryInfo: + """Get repository details""" + raise NotImplementedError + + def create_repository(self, repo: str, description: str): + """Create new repository""" + raise NotImplementedError + + def subscribe(self, owner: str, repo: str): + """subscribe to events in repository""" + raise NotImplementedError + + def get_notifications(self, since: datetime.datetime) -> NotificationResp: + """subscribe to events in repository""" + raise NotImplementedError + + def create_pull_request(self, pr: CreatePullrequest) -> str: + """ + create pull request + return value is the URL(HTML page) of the newely created PR + """ + raise NotImplementedError + + def fork(self, owner: str, repo: str): + """Fork a repository""" + raise NotImplementedError + + def close_pr(self, owner: str, repo: str): + """Fork a repository""" + raise NotImplementedError + + def comment_on_issue(self, owner: str, repo: str, issue_url: str, body: str): + """Add comment on an existing issue""" + raise NotImplementedError diff --git a/interface/forges/gitea.py b/interface/forges/gitea.py index 254d46c..fb8aeaa 100644 --- a/interface/forges/gitea.py +++ b/interface/forges/gitea.py @@ -17,40 +17,45 @@ import datetime from urllib.parse import urlparse, urlunparse, urlencode import requests -from libgit import InterfaceAdmin -from rfc3339 import rfc3338 +import libgit +import rfc3339 from interface import local_settings, utils -from interface.forge import CreateIssue, Forge, RepositoryInfo, Comment -from interface.forge import Notification, NotificationResp, CreatePullrequest -from interface.forge import ISSSUE, PULL, COMMIT, REPOSITORY + +from .base import Forge +from .payload import CreateIssue, RepositoryInfo, CreatePullrequest +from .notifications import Notification, NotificationResp, Comment +from .notifications import ISSUE, PULL, COMMIT, REPOSITORY + class Gitea(Forge): def __init__(self, base_url: str, admin_user: str, admin_email): - super().__init__(base_url=base_url, admin_user=admin_user, admin_email=admin_email) + super().__init__( + base_url=base_url, admin_user=admin_user, admin_email=admin_email + ) self.host = urlparse(utils.clean_url(local_settings.GITEA_HOST)) def _auth(self): - return {'Authorization': format("token %s" % (local_settings.GITEA_API_KEY))} + return {"Authorization": format("token %s" % (local_settings.GITEA_API_KEY))} def _get_url(self, path: str) -> str: prefix = "/api/v1/" - if path.startswith('/'): - path=path[1:] + if path.startswith("/"): + path = path[1:] path = format("%s%s" % (prefix, path)) url = urlunparse((self.host.scheme, self.host.netloc, path, "", "", "")) return url def get_issues(self, owner: str, repo: str, *args, **kwargs): - """ Get issues on a repository. Supports pagination via 'page' optional param""" + """Get issues on a repository. Supports pagination via 'page' optional param""" query = {} - since = kwargs.get('since') + since = kwargs.get("since") if since is not None: query["since"] = since - page = kwargs.get('page') + page = kwargs.get("page") if page is not None: query["page"] = page @@ -61,7 +66,7 @@ def get_issues(self, owner: str, repo: str, *args, **kwargs): return response.json() def create_issue(self, owner: str, repo: str, issue: CreateIssue): - """ Creates issue on a repository""" + """Creates issue on a repository""" url = self._get_url(format("/repos/%s/%s/issues" % (owner, repo))) headers = self._auth() @@ -78,7 +83,7 @@ def _into_repository(self, data) -> RepositoryInfo: return info def get_repository(self, owner: str, repo: str) -> RepositoryInfo: - """ Get repository details""" + """Get repository details""" url = self._get_url(format("/repos/%s/%s" % (owner, repo))) response = requests.request("GET", url) data = response.json() @@ -87,7 +92,7 @@ def get_repository(self, owner: str, repo: str) -> RepositoryInfo: def create_repository(self, repo: str, description: str): url = self._get_url("/user/repos/") - payload = { "name" : repo, "description": description} + payload = {"name": repo, "description": description} headers = self._auth() _response = requests.request("POST", url, json=payload, headers=headers) @@ -96,10 +101,9 @@ def subscribe(self, owner: str, repo: str): headers = self._auth() _response = requests.request("PUT", url, headers=headers) - def get_notifications(self, since: datetime.datetime) -> NotificationResp: query = {} - query["since"] = rfc3339(since) + query["since"] = rfc3339.rfc3339(since) url = self._get_url("/notifications") headers = self._auth() response = requests.request("GET", url, params=query, headers=headers) @@ -121,7 +125,14 @@ def get_notifications(self, since: datetime.datetime) -> NotificationResp: if notification_type == REPOSITORY: print(n) - if notification_type == ISSSUE: + if notification_type == PULL: + rn.set_pr_url( + requests.request("GET", subject["url"]).json()["html_url"] + ) + rn.set_upstream(n["repository"]["description"]) + print(n["repository"]["description"]) + + if notification_type == ISSUE: comment_url = subject["latest_comment_url"] print(comment_url) if len(comment_url) != 0: @@ -143,10 +154,10 @@ def get_notifications(self, since: datetime.datetime) -> NotificationResp: return NotificationResp(val, date_parse(last_read)) def create_pull_request(self, pr: CreatePullrequest): - url = self._get_url(format("/repos/%s/%s/pulls" , (pr.owner, pr.repo))) + url = self._get_url(format("/repos/%s/%s/pulls", (pr.owner, pr.repo))) headers = self._auth() - payload = pr.get_payload() + payload = pr.get_payload() for key in ["repo", "owner"]: del payload[key] @@ -157,31 +168,30 @@ def create_pull_request(self, pr: CreatePullrequest): response = requests.request("POST", url, json=payload, headers=headers) return response.json()["html_url"] - def fork(self, owner: str, repo:str): - """ Fork a repository """ + def fork(self, owner: str, repo: str): + """Fork a repository""" url = self._get_url(format("/repos/%s/%s/forks" % (owner, repo))) print(url) headers = self._auth() - payload = {"oarganization" :"bot"} + payload = {"oarganization": "bot"} _response = requests.request("POST", url, json=payload, headers=headers) def get_issue_index(self, issue_url, owner: str) -> int: parsed = urlparse(issue_url) path = parsed.path - path.endswith('/') - if path.endswith('/'): - path=path[0:-1] - index = path.split(owner)[0].split('issue')[2] - if index.startswith('/'): + path.endswith("/") + if path.endswith("/"): + path = path[0:-1] + index = path.split(owner)[0].split("issue")[2] + if index.startswith("/"): index = index[1:] - if index.endswith('/'): + if index.endswith("/"): index = index[0:-1] return int(index) - - def comment_on_issue(self, owner: str, repo: str, issue_url: str, body:str): + def comment_on_issue(self, owner: str, repo: str, issue_url: str, body: str): headers = self._auth() (owner, repo) = self.get_fetch_remote(issue_url) index = self.get_issue_index(issue_url, owner) @@ -189,16 +199,17 @@ def comment_on_issue(self, owner: str, repo: str, issue_url: str, body:str): payload = {"body": body} _response = requests.request("POST", url, json=payload, headers=headers) - def get_local_html_url(self, repo:str) -> str: + def get_local_html_url(self, repo: str) -> str: path = format("/%s/%s", local_settings.GITEA_USERNAME, repo) return urlunparse((self.host.scheme, self.host.netloc, path, "", "", "")) - def get_local_push_url(self, repo:str) -> str: - return format("git@%s:%s/%s.git", self.host.netloc, local_settings.GITEA_USERNAME, repo) - + def get_local_push_url(self, repo: str) -> str: + return format( + "git@%s:%s/%s.git", self.host.netloc, local_settings.GITEA_USERNAME, repo + ) -#if __name__ == "__main__": +# if __name__ == "__main__": # owner = "realaravinth" # repo = "tmp" # g = Gitea() diff --git a/interface/forges/notifications.py b/interface/forges/notifications.py new file mode 100644 index 0000000..8595986 --- /dev/null +++ b/interface/forges/notifications.py @@ -0,0 +1,107 @@ +""" +Notifications payload +""" +# Bridges software forges to create a distributed software development environment +# Copyright © 2021 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import datetime + +from .payload import Payload + +ISSUE = "Issue" +PULL = "Pull" +COMMIT = "commit" +REPOSITORY = "repository" + + +class Comment(Payload): + """Data structure that represents a comment""" + + def __init__(self): + mandatory = ["body", "author", "updated_at", "url"] + super().__init__(mandatory) + + def set_updated_at(self, date): + """set comment last update time""" + self.payload["updated_at"] = date + + def set_body(self, body): + """set comment body""" + self.payload["body"] = body + + def set_author(self, author): + """set comment author""" + self.payload["author"] = author + + def set_url(self, url): + """set url of comment""" + self.payload["url"] = url + + +class Notification(Payload): + """Data structure that represents a notification""" + + def __init__(self): + mandatory = ["type", "state", "updated_at", "title"] + super().__init__(mandatory) + + def set_updated_at(self, date): + """set notification update time""" + self.payload["updated_at"] = date + + def set_upstream(self, upstream): + """set upstream repository URL""" + print("settings upstream", upstream) + self.payload["upstream"] = upstream + + def set_pr_url(self, url): + """set pr url""" + self.payload["pr_url"] = url + + def set_type(self, notification_type): + """set notification type""" + self.payload["type"] = notification_type + + def set_state(self, state): + """set notification state""" + self.payload["state"] = state + + def set_comment(self, comment: Comment): + """set comment""" + self.payload["status"] = comment.get_payload() + + def set_repo_url(self, repo_url: str): + """set repository URL update time""" + self.payload["repo_url"] = repo_url + + def set_title(self, title): + """set issue title""" + self.payload["title"] = title + + +class NotificationResp: + """Notification response helper type""" + + def __init__(self, notifications: [Notification], last_read: datetime.datetime): + self.notifications = notifications + self.last_read = last_read + + def get_payload(self): + """get flattened data""" + notifications = [] + for n in self.notifications: + notifications.append(n.get_payload()) + + return notifications diff --git a/interface/forges/payload.py b/interface/forges/payload.py new file mode 100644 index 0000000..fa53a8b --- /dev/null +++ b/interface/forges/payload.py @@ -0,0 +1,129 @@ +""" +Payload data structures +""" +# Bridges software forges to create a distributed software development environment +# Copyright © 2021 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +class Payload: + """Payload base class. self.mandatory should be defined""" + + def __init__(self, mandatory: [str]): + self.payload = {} + self.mandatory = [] + + def get_payload(self): + """get payload""" + for f in self.mandatory: + if self.payload[f] is None: + raise Exception("%s can't be empty" % f) + return self.payload + + +class RepositoryInfo(Payload): + """Describes a repository""" + + def __init__(self): + mandatory = ["name", "owner_name"] + super().__init__(mandatory) + + def set_name(self, name): + """Set name of repository""" + self.payload["name"] = name + + def set_owner_name(self, name): + """Set owner name of repository""" + self.payload["owner_name"] = name + + def set_description(self, description): + """Is this a template repository""" + self.payload["description"] = description + + +class CreateIssue(Payload): + """Create new issue payload""" + + def __init__(self): + mandatory = ["title"] + super().__init__(mandatory) + + def set_title(self, title): + """set issue title""" + self.payload["title"] = title + + def set_body(self, body): + """set issue body""" + self.payload["body"] = body + + def set_due_date(self, due_date): + """set issue due date""" + self.payload["due_date"] = due_date + + def set_closed(self, closed: bool): + """set issue open status""" + self.payload["closed"] = closed + + +class CreatePullrequest(Payload): + """ + Data structure that contains params to create a pull request. + See https://docs.github.com/en/rest/reference/pulls + """ + + def __init__(self): + mandatory = ["owner", "message", "repo", "head", "base", "title"] + super().__init__(mandatory) + + def set_owner(self, name): + """Set owner name of repository""" + self.payload["owner"] = name + + def set_repo(self, repo): + """Set owner name of repository""" + self.payload["repo"] = repo + + def set_head(self, head): + """ + From GitHub Docs: + + The name of the branch you want the changes pulled into. + This should be an existing branch on the current repository. + You cannot submit a pull request to one repository that requests a merge to + a base of another repository. + """ + self.payload["head"] = head + + def set_base(self, base): + """ + From GitHub Docs: + The name of the branch you want the changes pulled into. + This should be an existing branch on the current repository. + You cannot submit a pull request to one repository that requests a merge to + a base of another repository. + """ + self.payload["base"] = base + + def set_title(self, title): + """set title of the PR""" + self.payload["title"] = title + + def set_message(self, message): + """set message of the PR""" + self.payload["message"] = message + + def set_body(self, body): + """set title of the PR message""" + self.payload["body"] = body diff --git a/interface/forges/utils.py b/interface/forges/utils.py new file mode 100644 index 0000000..8149315 --- /dev/null +++ b/interface/forges/utils.py @@ -0,0 +1,43 @@ +""" +Utility functions to work with forges +""" +# Bridges software forges to create a distributed software development environment +# Copyright © 2021 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from urllib.parse import urlparse +import requests + +from interface.utils import clean_url + + +def get_patch(url: str) -> str: + """Get patch from pull request""" + if url.endswith("/"): + url = url[0:-1] + ".patch" + else: + url += ".patch" + resp = requests.get(url) + if resp.status_code == 200: + return resp.text + + +def get_branch_name(pull_request_url: str) -> str: + """Get branch name from pull request URL""" + parsed = urlparse(pull_request_url) + return format("%s%s" % (parsed.netloc, parsed.path.replace("/", "-"))) + + +def get_local_repository_from_foreign_repo(repo_url: str) -> str: + return get_branch_name(repo_url) diff --git a/interface/local_settings_example.py b/interface/local_settings_example.py index 5ef6f3f..892cfb4 100644 --- a/interface/local_settings_example.py +++ b/interface/local_settings_example.py @@ -1,14 +1,14 @@ GITEA_API_KEY = "" GITEA_USERNAME = "" GITEA_HOST = "" -GITHUB_HOST ="" +GITHUB_HOST = "" GITHUB_API_KEY = "" -INTERFACE_URL = "" # URL at which this interface is available +INTERFACE_URL = "" # URL at which this interface is available BASE_DIR = "" ADMIN_EMAIL = "" ADMIN_USER = "" -JOB_RUNNER_DELAY = 10 ## in seconds +JOB_RUNNER_DELAY = 10 ## in seconds diff --git a/interface/runner.py b/interface/runner.py index 1b83e79..ce73334 100644 --- a/interface/runner.py +++ b/interface/runner.py @@ -1,3 +1,6 @@ +""" +A job runner that receives events(notifications) and runs revelant jobs on them +""" # Bridges software forges to create a distributed software development environment # Copyright © 2021 Aravinth Manivannan # @@ -21,89 +24,95 @@ from interface import local_settings -from interface.forge import get_forge, Notification, PULL, ISSSUE +from interface.forges import get_forge +from interface.forges.notifications import PULL, ISSUE +from interface.forges.utils import get_patch, get_branch_name from interface.db import get_db from interface.forges.gitea import date_parse - - RUNNING = False -APP = "" -def init(app): - # global APP - with app.app_context(): - conn = get_db() - cur = conn.cursor() - last_run = date_parse("2021-10-10T17:06:02+05:30") - cur.execute( - "INSERT OR IGNORE INTO interface_jobs_run (this_interface_url, last_run) VALUES (?, ?);", - (local_settings.INTERFACE_URL, str(last_run)), + +class Runner: + def __init__(self, app): + self.app = app + logging.getLogger("jobs").setLevel(logging.WARNING) + self.logger = logging.getLogger("jobs") + self.scheduler = sched.scheduler(time.time, time.sleep) + self.forge = get_forge() + + with self.app.app_context(): + conn = get_db() + cur = conn.cursor() + last_run = date_parse("2021-10-10T17:06:02+05:30") + cur.execute( + """ + INSERT OR IGNORE INTO interface_jobs_run + (this_interface_url, last_run) VALUES (?, ?); + """, + (local_settings.INTERFACE_URL, str(last_run)), ) - conn.commit() + conn.commit() -def update_time(time: datetime.datetime, app): - # global APP - with app.app_context(): - conn = get_db() - cur = conn.cursor() - cur.execute( + def _update_time(self, last_run: datetime.datetime): + with self.app.app_context(): + conn = get_db() + cur = conn.cursor() + cur.execute( "UPDATE interface_jobs_run set last_run = ? WHERE this_interface_url = ?;", - (str(time), local_settings.INTERFACE_URL), + (str(last_run), local_settings.INTERFACE_URL), ) - conn.commit() - + conn.commit() -def get_last_run(app): - with app.app_context(): - conn = get_db() - cur = conn.cursor() - res = cur.execute( + def get_last_run(self): + with self.app.app_context(): + conn = get_db() + cur = conn.cursor() + res = cur.execute( "SELECT last_run FROM interface_jobs_run WHERE this_interface_url = ?;", (local_settings.INTERFACE_URL,), ).fetchone() - return res[0] - -#def proces_pull_request(): - - - -def run(app): - #global APP - #APP = app - with app.app_context(): - logging.getLogger('jobs').setLevel(logging.WARNING) - logger = logging.getLogger('jobs') - scheduler = sched.scheduler(time.time, time.sleep) - - init(app) - - def background_job(app): - with app.app_context(): - global RUNNING - print(RUNNING) - if RUNNING: - scheduler.enter(local_settings.JOB_RUNNER_DELAY, 8, background_job) - return - else: - RUNNING = True - - last_run = get_last_run(app) - print(last_run) - - forge = get_forge() - notifications = forge.get_notifications(since=last_run).get_payload() - print(notifications) - #for n in notifications: - # import json - # print(json.dumps(n)) - -# # if all([n["type"] == PULL, n["owner"] == local_settings.ADMIN_USER]): - -# # if n["type"] == - logger.warning('hello from background_job %s', time.time()) - scheduler.enter(local_settings.JOB_RUNNER_DELAY, 8, background_job, argument=(app,)) - RUNNING = False - - scheduler.enter(local_settings.JOB_RUNNER_DELAY, 8, background_job, argument=(app,)) - threading.Thread(target=scheduler.run).start() + return res[0] + + def _background_job(self): + with self.app.app_context(): + global RUNNING + if RUNNING: + self.scheduler.enter( + local_settings.JOB_RUNNER_DELAY, 8, self._background_job + ) + return + else: + RUNNING = True + + last_run = self.get_last_run() + print(last_run) + + notifications = self.forge.get_notifications( + since=date_parse(last_run) + ).get_payload() + # print(notifications) + for n in notifications: + (owner, _repo) = self.forge.get_owner_repo_from_url(n["repo_url"]) + if all([n["type"] == PULL, owner == local_settings.ADMIN_USER]): + patch = get_patch(n["pr_url"]) + local = n["repo_url"] + upstream = n["upstream"] + patch = self.forge.process_patch( + patch, local, upstream, get_branch_name(n["pr_url"]) + ) + print(patch) + + # if n["type"] == + RUNNING = False + self.scheduler.enter( + local_settings.JOB_RUNNER_DELAY, 8, self._background_job + ) + # argument=(app,)) + + def run(self): + """Start job runner""" + self.scheduler.enter( + local_settings.JOB_RUNNER_DELAY, 8, self._background_job + ) # argument=(self,)) + threading.Thread(target=self.scheduler.run).start() diff --git a/interface/utils.py b/interface/utils.py index 87915f3..b87bbd3 100644 --- a/interface/utils.py +++ b/interface/utils.py @@ -1,16 +1,16 @@ # Bridges software forges to create a distributed software development environment # Copyright © 2021 Aravinth Manivannan -# +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. -# +# # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from urllib.parse import urlparse, urlunparse @@ -19,26 +19,8 @@ from libgit import Repo, InterfaceAdmin, Patch -def get_patch(url: str) -> str: - """ Get patch from pull request""" - if url.endswith('/'): - url = url[0:-1] + ".patch" - else: - url += ".patch" - resp = requests.get(url) - if resp.status_code == 200: - return resp.text - def clean_url(url: str): """Remove paths and tracking elements from URL""" parsed = urlparse(url) cleaned = urlunparse((parsed.scheme, parsed.netloc, "", "", "", "")) return cleaned - -def get_branch_name(pull_request_url: str) -> str: - """ Get branch name from pull request URL """ - parsed = urlparse(pull_request_url) - return format("%s%s" % (parsed.netloc, parsed.path.replace("/", "-"))) - -def get_local_repository_from_foreign_repo(repo_url: str) -> str: - return get_branch_name(repo_url) diff --git a/requirements.txt b/requirements.txt index 307c30c..4d81e92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ giteapy==1.0.8 greenlet==1.1.2 gunicorn==20.1.0 idna==3.3 -./ isort==5.9.3 itsdangerous==2.0.1 jedi==0.18.0 From d3cfa794b0948c6a05e18a8eb6dd393cec19adc6 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Sat, 23 Oct 2021 14:48:51 +0530 Subject: [PATCH 3/8] rust 2018 edition --- libgit/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libgit/Cargo.toml b/libgit/Cargo.toml index 4d93a13..f12aa25 100644 --- a/libgit/Cargo.toml +++ b/libgit/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "libgit" version = "0.1.0" -edition = "2021" +edition = "2018" authors = ["Aravinth Manivannan "] license = "AGPLv3 or later version" From 626ec6f1a1a3e5928269bead381fc2ad82248962 Mon Sep 17 00:00:00 2001 From: dat-adi Date: Sat, 23 Oct 2021 20:13:52 +0530 Subject: [PATCH 4/8] Set up GitHub forge interface A simple set up. Endpoint for `get_issues` is ready. Endpoint for `create_issue` is underway. --- forges/github.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 forges/github.py diff --git a/forges/github.py b/forges/github.py new file mode 100644 index 0000000..7639c5a --- /dev/null +++ b/forges/github.py @@ -0,0 +1,67 @@ +# Bridges software forges to create a distributed software development environment +# Copyright © 2021 G V Datta Adithya +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import requests +import local_settings +from urllib.parse import urlparse, urlunparse +import interface.utils as utils +from interface.forge import CreateIssue + +class GitHub: + def __init__(self): + self.host = urlparse(utils.clean_url(local_settings.GITHUB_HOST)) + + def _get_url(self, path: str) -> str: + if path.startswith("/"): + path = path[1:] + + url = urlunparse((self.host.scheme, self.host.netloc, path, "", "", "")) + return url + + def _auth(self): + return {'Authorization': format("token %s" % (local_settings.GITHUB_API_KEY))} + + def get_issues(self, owner: str, repo: str, *args, **kwargs): + """Get the issues present in a provided repository""" + + # Defining a formatted url with the repo details + url = format("https://api.github.com/repos/%s/%s/issues" % (owner, repo)) + + # Requesting the issues present in the repo + # GitHub provides a paginated response for 30 + # issues at a time + response = requests.request("GET", url) + + # returning the responses in the form of JSON + return response.json() + + def create_issue(self, owner: str, repo: str, issue: CreateIssue): + """Creates an issue on a repository""" + url = self._get_url(format("/repos/%s/%s/issues" % (owner, repo))) + + headers = self._auth() + payload = issue.get_payload() + response = requests.request("POST", url, json=payload, headers=headers) + return response.json() + +if __name__ == "__main__": + # Testing the API primitively with a simple call + owner = "dat-adi" + repo = "tmp" + g = GitHub() + issue = CreateIssue() + issue.set_title("testing yet again") + print(g.create_issue(owner, repo, issue)) From 54532c24d503750d128e4c3d125b7afe59c4bb31 Mon Sep 17 00:00:00 2001 From: dat-adi Date: Sat, 23 Oct 2021 21:16:55 +0530 Subject: [PATCH 5/8] Testing functionality Attempting to work with the modules. Using this as a point to revert back to, if things go wrong. Implemented the get_repository method, and create_issue. --- {forges => interface}/github.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) rename {forges => interface}/github.py (75%) diff --git a/forges/github.py b/interface/github.py similarity index 75% rename from forges/github.py rename to interface/github.py index 7639c5a..f251895 100644 --- a/forges/github.py +++ b/interface/github.py @@ -17,8 +17,8 @@ import requests import local_settings from urllib.parse import urlparse, urlunparse -import interface.utils as utils -from interface.forge import CreateIssue +import utils +from forge import CreateIssue, RepositoryInfo class GitHub: def __init__(self): @@ -57,6 +57,22 @@ def create_issue(self, owner: str, repo: str, issue: CreateIssue): response = requests.request("POST", url, json=payload, headers=headers) return response.json() + def _into_repository(self, data) -> RepositoryInfo: + info = RepositoryInfo() + info.set_description(data["description"]) + info.set_name(data["name"]) + info.set_owner_name(data["owner"]["login"]) + return info + + def get_repository(self, owner: str, repo: str) -> RepositoryInfo: + """Get repository details""" + url = self._get_url(format("/repos/%s/%s" % (owner, repo))) + response = requests.request("GET", url) + data = response.json() + info = self._into_repository(data) + print("Payload deets", info.get_payload()) + return info + if __name__ == "__main__": # Testing the API primitively with a simple call owner = "dat-adi" @@ -65,3 +81,4 @@ def create_issue(self, owner: str, repo: str, issue: CreateIssue): issue = CreateIssue() issue.set_title("testing yet again") print(g.create_issue(owner, repo, issue)) + print(g._into_repository({"name": "G V Datta Adithya", "description": "Octowhat?", "owner": {"login": "userwhat?"}})) From 2385e6c0215b310714fdf3f82268785264ab4f6c Mon Sep 17 00:00:00 2001 From: dat-adi Date: Sat, 23 Oct 2021 23:03:26 +0530 Subject: [PATCH 6/8] Minor fixes to the GitHub implementation --- interface/forges/gitea.py | 4 ++-- interface/{ => forges}/github.py | 35 ++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) rename interface/{ => forges}/github.py (81%) diff --git a/interface/forges/gitea.py b/interface/forges/gitea.py index fb8aeaa..a10e0fc 100644 --- a/interface/forges/gitea.py +++ b/interface/forges/gitea.py @@ -153,8 +153,8 @@ def get_notifications(self, since: datetime.datetime) -> NotificationResp: val.append(rn) return NotificationResp(val, date_parse(last_read)) - def create_pull_request(self, pr: CreatePullrequest): - url = self._get_url(format("/repos/%s/%s/pulls", (pr.owner, pr.repo))) + def create_pull_request(self, owner: str, repo: str, pr: CreatePullrequest): + url = self._get_url(format("/repos/%s/%s/pulls" % (owner, repo))) headers = self._auth() payload = pr.get_payload() diff --git a/interface/github.py b/interface/forges/github.py similarity index 81% rename from interface/github.py rename to interface/forges/github.py index f251895..ec5e0c0 100644 --- a/interface/github.py +++ b/interface/forges/github.py @@ -14,13 +14,15 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import requests -import local_settings from urllib.parse import urlparse, urlunparse -import utils -from forge import CreateIssue, RepositoryInfo +import requests + +from interface import local_settings +from . import utils +from .base import Forge, CreateIssue, RepositoryInfo -class GitHub: + +class GitHub(Forge): def __init__(self): self.host = urlparse(utils.clean_url(local_settings.GITHUB_HOST)) @@ -32,7 +34,7 @@ def _get_url(self, path: str) -> str: return url def _auth(self): - return {'Authorization': format("token %s" % (local_settings.GITHUB_API_KEY))} + return {"Authorization": format("token %s" % (local_settings.GITHUB_API_KEY))} def get_issues(self, owner: str, repo: str, *args, **kwargs): """Get the issues present in a provided repository""" @@ -52,12 +54,19 @@ def create_issue(self, owner: str, repo: str, issue: CreateIssue): """Creates an issue on a repository""" url = self._get_url(format("/repos/%s/%s/issues" % (owner, repo))) + # Defining authorization headers and a payload headers = self._auth() payload = issue.get_payload() + + # Sending in a POST request response = requests.request("POST", url, json=payload, headers=headers) + + # Returns the response with a JSON output return response.json() def _into_repository(self, data) -> RepositoryInfo: + """Getting and setting data""" + info = RepositoryInfo() info.set_description(data["description"]) info.set_name(data["name"]) @@ -66,13 +75,15 @@ def _into_repository(self, data) -> RepositoryInfo: def get_repository(self, owner: str, repo: str) -> RepositoryInfo: """Get repository details""" + url = self._get_url(format("/repos/%s/%s" % (owner, repo))) response = requests.request("GET", url) data = response.json() info = self._into_repository(data) - print("Payload deets", info.get_payload()) + print("Payload deets", info.get_payload()) return info + if __name__ == "__main__": # Testing the API primitively with a simple call owner = "dat-adi" @@ -81,4 +92,12 @@ def get_repository(self, owner: str, repo: str) -> RepositoryInfo: issue = CreateIssue() issue.set_title("testing yet again") print(g.create_issue(owner, repo, issue)) - print(g._into_repository({"name": "G V Datta Adithya", "description": "Octowhat?", "owner": {"login": "userwhat?"}})) + print( + g._into_repository( + { + "name": "G V Datta Adithya", + "description": "Octowhat?", + "owner": {"login": "userwhat?"}, + } + ) + ) From d5fd18a7668791338cf0560e028d2cee349944c2 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Mon, 25 Oct 2021 13:21:48 +0530 Subject: [PATCH 7/8] clean up --- interface/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/utils.py b/interface/utils.py index b87bbd3..477d521 100644 --- a/interface/utils.py +++ b/interface/utils.py @@ -16,8 +16,6 @@ from urllib.parse import urlparse, urlunparse import requests -from libgit import Repo, InterfaceAdmin, Patch - def clean_url(url: str): """Remove paths and tracking elements from URL""" From a284569bc9fb0372c29a122fd5554a69328be09d Mon Sep 17 00:00:00 2001 From: realaravinth Date: Thu, 28 Oct 2021 18:11:03 +0530 Subject: [PATCH 8/8] fix typos --- Makefile | 2 +- interface/forges/base.py | 2 +- interface/forges/github.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index f965dda..85cfb33 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ default: ## Run app cd libgit && maturin build - . ./venv/bin/activate && pythom -m interface + . ./venv/bin/activate && python -m interface docker: ## Build Docker image from source docker build -t forgedfed/interface . diff --git a/interface/forges/base.py b/interface/forges/base.py index aaa5a9a..f4e4374 100644 --- a/interface/forges/base.py +++ b/interface/forges/base.py @@ -16,7 +16,7 @@ def __init__(self, base_url: str, admin_user: str, admin_email): self.base_url = urlparse(clean_url(base_url)) if all([self.base_url.scheme != "http", self.base_url.scheme != "https"]): print(self.base_url.scheme) - raise Exception("scheme should be wither http or https") + raise Exception("scheme should be either http or https") self.admin = InterfaceAdmin(admin_email, admin_user) def _lock_repo(self, local_url): diff --git a/interface/forges/github.py b/interface/forges/github.py index 18cedec..ec5e0c0 100644 --- a/interface/forges/github.py +++ b/interface/forges/github.py @@ -21,6 +21,7 @@ from . import utils from .base import Forge, CreateIssue, RepositoryInfo + class GitHub(Forge): def __init__(self): self.host = urlparse(utils.clean_url(local_settings.GITHUB_HOST)) @@ -34,7 +35,8 @@ def _get_url(self, path: str) -> str: def _auth(self): return {"Authorization": format("token %s" % (local_settings.GITHUB_API_KEY))} -def get_issues(self, owner: str, repo: str, *args, **kwargs): + + def get_issues(self, owner: str, repo: str, *args, **kwargs): """Get the issues present in a provided repository""" # Defining a formatted url with the repo details