diff --git a/.gitignore b/.gitignore
index 28982b7..bd7a118 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,4 @@ __pycache__/
node_modules
.idea
*.lock
-*.pyc
\ No newline at end of file
+*.pyc
diff --git a/Makefile b/Makefile
index bce5edb..350f780 100644
--- a/Makefile
+++ b/Makefile
@@ -3,13 +3,17 @@ SHELL := /bin/bash
.PHONY: backend
backend:
pip install -r backend/requirements.txt
- python3 backend/web_server.py
+ python3 backend/server.py
.PHONY: frontend
frontend:
yarn --cwd frontend install
yarn --cwd frontend run serve
+.PHONY: webhook
+webhook:
+ smee --url https://smee.io/UUkxjK9NwrD3pnTH --path /webhook_handler --port 5000
+
.PHONY: db
db:
mongod --config /usr/local/etc/mongod.conf
diff --git a/README.md b/README.md
index 710afe4..f1695ee 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,13 @@ To clean the database document collection, run
make db_clean
```
+### Setting the webhook redirect
+
+To set the webhook redirect to localhost:
+```bash
+make webhook
+```
+
#### Check out the app
Visit http://localhost:8080/
@@ -70,8 +77,26 @@ pip freeze > backend/requirements.txt
pip install -r backend/requirements.txt
```
+### Clone the test folder locally
+
+```bash
+git clone git@github.com:codersdoc/Test.git
+```
+
+### Change the setup of the Github app
+
+Go to this [link](https://github.com/settings/apps/tech-documentation).
+
+### Useful links
+
+* [Github enpoints available](https://developer.github.com/v3/apps/available-endpoints/).
+
+
## Miscallaneous
### Tech and library used
* pyGithub
+* mongo
+* flask
+* Vuejs
diff --git a/backend/github_interface/git/git_diff_types/git_diff_hunk.py b/backend/github_interface/git/git_diff_types/git_diff_hunk.py
index 569a32f..dc69bd4 100644
--- a/backend/github_interface/git/git_diff_types/git_diff_hunk.py
+++ b/backend/github_interface/git/git_diff_types/git_diff_hunk.py
@@ -45,6 +45,7 @@ def count_line_change_after_exclusive(self, line_number):
def __count_line_change_helper(self, line_number, is_count_before):
current_old_line_number = self.get_old_start_line() - 1
total_line_change = 0
+ counter_reseted = False
for code_line in self.code_lines:
if code_line.state == GitDiffCodeLineState.UNCHANGED:
@@ -55,10 +56,11 @@ def __count_line_change_helper(self, line_number, is_count_before):
elif code_line.state == GitDiffCodeLineState.ADDED:
total_line_change += 1
- if current_old_line_number == line_number:
+ if current_old_line_number == line_number and not counter_reseted:
if is_count_before:
break
else:
+ counter_reseted = True
total_line_change = 0
return total_line_change
diff --git a/backend/github_interface/github_types/github_commit_file.py b/backend/github_interface/github_types/github_commit_file.py
index 2a8cdee..5008558 100644
--- a/backend/github_interface/github_types/github_commit_file.py
+++ b/backend/github_interface/github_types/github_commit_file.py
@@ -3,21 +3,29 @@
class GithubCommitFile(GithubFile):
- def __init__(self, content_file, previous_path, patch):
+ def __init__(self, content_file, path, previous_path, patch, is_deleted):
GithubFile.__init__(self, content_file)
- self._previous_path = previous_path
+ self.__path = path
+ self.__has_path_changed = not (previous_path is None)
+ self.__previous_path = self.__path if previous_path is None else previous_path
self.__git_diff_parser = GitDiffParser(patch)
+ self.__is_deleted = is_deleted
@property
def previous_path(self):
- return self._previous_path
+ return self.__previous_path
+
+ @property
+ def has_path_changed(self):
+ return self.__has_path_changed
+
+ @property
+ def is_deleted(self):
+ return self.__is_deleted
def calculate_updated_line_range(self, start_line, end_line):
return self.__git_diff_parser.calculate_updated_line_range(start_line, end_line)
- def has_path_changed(self):
- return self.previous_path is not None
-
def to_json(self):
new_json = {
"previous_path": self.previous_path
diff --git a/backend/github_interface/github_types/github_repository.py b/backend/github_interface/github_types/github_repository.py
index 367e425..3ac64ca 100644
--- a/backend/github_interface/github_types/github_repository.py
+++ b/backend/github_interface/github_types/github_repository.py
@@ -45,9 +45,11 @@ def get_commit_files(self, branch_name="master", sha=None):
for file in commit.files:
if file.status == "removed":
file_object = None
+ is_deleted = True
else:
file_object = self.__get_file_object(file.filename)
- files.append(GithubCommitFile(file_object, file.previous_filename, file.patch))
+ is_deleted = False
+ files.append(GithubCommitFile(file_object, file.filename, file.previous_filename, file.patch, is_deleted))
return files
diff --git a/backend/github_interface/hooks/hook_manager.py b/backend/github_interface/hooks/hook_manager.py
deleted file mode 100644
index 0abba1a..0000000
--- a/backend/github_interface/hooks/hook_manager.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import json
-
-import requests
-
-"""
-https://developer.github.com/v3/repos/hooks/
-"""
-
-
-def get_hooks(oauth_token, owner, repo):
- return requests.get(
- "https://api.github.com/repos/{}/{}/hooks".format(owner, repo),
- headers={'Authorization': 'token {}'.format(oauth_token)}
- ).json()
-
-
-def subscribe_hooks(oauth_token, owner, repo, url_postback):
- return requests.post(
- "https://api.github.com/repos/{}/{}/hooks".format(owner, repo),
- data=json.dumps({
- "name": "web",
- "active": True,
- "events": [
- "pull_request"
- ],
- "config": {
- "url": url_postback,
- "content_type": "json"
- }
- }),
- headers={'Authorization': 'token {}'.format(oauth_token)}
- ).json()
\ No newline at end of file
diff --git a/backend/github_interface/interface.py b/backend/github_interface/interface.py
index e75c720..2547531 100644
--- a/backend/github_interface/interface.py
+++ b/backend/github_interface/interface.py
@@ -1,25 +1,84 @@
-from github import Github
+import hmac
+
+import requests
+from github import Github, GithubIntegration
+from hashlib import sha1
from github_interface.github_types.github_repository import GithubRepository
+from mongo.credentials import CredentialsManager
+from utils.constants import GITHUB_APP_IDENTIFIER
class GithubInterface:
- def __init__(self, access_token):
- self.__github_account = Github(access_token)
- self.__repo_cache = {}
+ __repo_cache = {}
+
+ # TODO: use repo id and not repo name for the cache
+ @staticmethod
+ def get_repo(repo_name, is_user_access_token=False):
+ github_account = Github(GithubInterface.__fetch_access_token_from_db(is_user_access_token))
- def get_repo(self, repo_name):
- if repo_name in self.__repo_cache:
- repo = self.__repo_cache[repo_name]
+ if repo_name in GithubInterface.__repo_cache:
+ repo = GithubInterface.__repo_cache[repo_name]
else:
- self.__repo_cache[repo_name] = GithubRepository(self.__github_account.get_repo(repo_name))
- repo = self.__repo_cache[repo_name]
+ GithubInterface.__repo_cache[repo_name] = GithubRepository(github_account.get_repo(repo_name))
+ repo = GithubInterface.__repo_cache[repo_name]
return repo
- def get_repos(self):
+ @staticmethod
+ def get_repos(is_user_access_token=False):
+ github_account = Github(GithubInterface.__fetch_access_token_from_db(is_user_access_token))
+
repos = []
- for repo in self.__github_account.get_user().get_repos():
+
+ if is_user_access_token:
+ raw_repos = github_account.get_user().get_repos()
+ else:
+ raw_repos = github_account.get_installation(-1).get_repos()
+
+ for repo in raw_repos:
repos.append(GithubRepository(repo))
return repos
+ @staticmethod
+ def get_user_access_token(client_id, client_secret, code, redirect_uri):
+ params = {
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "code": code,
+ "redirect_uri": redirect_uri
+ }
+ r = requests.get(url="https://github.com/login/oauth/access_token", params=params)
+ content = r.content.decode("utf-8")
+ user_access_token = content[content.find("access_token=") + 13: content.find("&")]
+
+ return user_access_token
+
+ @staticmethod
+ def get_user_installations():
+ access_token = GithubInterface.__fetch_access_token_from_db(True)
+ response = requests.get(url="https://api.github.com/user/installations",
+ headers={
+ "Authorization": "token " + access_token,
+ "Accept": "application/vnd.github.machine-man-preview+json"
+ })
+ return response.json()
+
+ @staticmethod
+ def get_installation_access_token(installation_id, private_key):
+ integration = GithubIntegration(str(GITHUB_APP_IDENTIFIER), private_key)
+ return integration.get_access_token(installation_id).token
+
+ @staticmethod
+ def verify_signature(signature, body, github_webhook_secret):
+ computed_signature = "sha1=" + hmac.new(str.encode(github_webhook_secret), body, sha1).hexdigest()
+ return computed_signature == signature
+
+ @staticmethod
+ def __fetch_access_token_from_db(is_user_access_token):
+ if is_user_access_token:
+ return CredentialsManager.read_credentials()["user_access_token"]
+ else:
+ return CredentialsManager.read_credentials()["installation_access_token"]
+
+
diff --git a/backend/mongo/credentials.py b/backend/mongo/credentials.py
new file mode 100644
index 0000000..35062b0
--- /dev/null
+++ b/backend/mongo/credentials.py
@@ -0,0 +1,29 @@
+import json
+
+
+class CredentialsManager:
+
+ @staticmethod
+ def write_credentials(user_access_token="", installation_access_token=""):
+ credentials = CredentialsManager.read_credentials()
+
+ user_access_token = credentials[
+ "user_access_token"] if "user_access_token" in credentials and not user_access_token else user_access_token
+ installation_access_token = credentials[
+ "installation_access_token"] if "installation_access_token" in credentials and not installation_access_token else installation_access_token
+
+ with open("backend/ressources/credentials.txt", "w") as file:
+ credentials = "{\"installation_access_token\":\"" + str(
+ installation_access_token) + "\",\"user_access_token\":\"" + str(user_access_token) + "\"}"
+ file.write(credentials)
+ file.close()
+
+ @staticmethod
+ def read_credentials():
+ with open("backend/ressources/credentials.txt", "r") as file:
+ file_content = file.read()
+ if file_content == "":
+ file_content = "{}"
+ file.close()
+
+ return json.loads(file_content)
\ No newline at end of file
diff --git a/backend/mongo/models.py b/backend/mongo/models.py
index 029438d..ccf0ac1 100644
--- a/backend/mongo/models.py
+++ b/backend/mongo/models.py
@@ -13,12 +13,13 @@ class FileReference:
A FileReference is part of a Document, and references lines of code in repositories
"""
- def __init__(self, ref_id, repo, path, start_line, end_line):
+ def __init__(self, ref_id, repo, path, start_line, end_line, is_deleted):
self.ref_id = ref_id
self.repo = repo
self.path = path
self.start_line = start_line
self.end_line = end_line
+ self.is_deleted = is_deleted
def to_json(self):
return {
@@ -27,6 +28,7 @@ def to_json(self):
'path': self.path,
'start_line': self.start_line,
'end_line': self.end_line,
+ 'is_deleted': self.is_deleted
}
@staticmethod
@@ -36,7 +38,8 @@ def from_json(file_ref):
file_ref['repo'],
file_ref['path'],
int(file_ref['start_line']),
- int(file_ref['end_line'])
+ int(file_ref['end_line']),
+ file_ref['is_deleted']
)
def __init__(self, name, content, references):
@@ -47,6 +50,45 @@ def __init__(self, name, content, references):
def insert(self):
return self.COLLECTION.insert_one(self.to_json())
+ @staticmethod
+ def __update(query, new_values):
+ return Document.COLLECTION.update_one(query, new_values)
+
+ @staticmethod
+ def update_lines_ref(ref_id, new_start_line, new_end_line):
+ query = { "refs.ref_id": ref_id }
+ new_values = {
+ "$set": {
+ "refs.$.start_line": new_start_line,
+ "refs.$.end_line": new_end_line
+ }
+ }
+
+ return Document.__update(query, new_values)
+
+ @staticmethod
+ def update_path_ref(ref_id, path):
+ query = {"refs.ref_id": ref_id}
+ new_values = {
+ "$set": {
+ "refs.$.path": path
+ }
+ }
+
+ return Document.__update(query, new_values)
+
+ staticmethod
+
+ def update_is_deleted_ref(ref_id, is_deleted):
+ query = {"refs.ref_id": ref_id}
+ new_values = {
+ "$set": {
+ "refs.$.is_deleted": is_deleted
+ }
+ }
+
+ return Document.__update(query, new_values)
+
@staticmethod
def find(name):
doc = Document.COLLECTION.find_one({
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 1b66ac9..52b73b9 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -1,17 +1,28 @@
+asn1crypto==0.24.0
certifi==2019.3.9
+cffi==1.12.3
chardet==3.0.4
Click==7.0
+cryptography==2.7
Deprecated==1.2.5
Flask==1.0.3
+GitHub-Flask==3.2.0
+github3.py==1.3.0
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10.1
+jwcrypto==0.6.0
+jwt==0.6.1
MarkupSafe==1.1.1
+pycparser==2.19
PyGithub==1.43.7
Pygments==2.4.2
PyJWT==1.7.1
pymongo==3.8.0
+python-dateutil==2.8.0
requests==2.22.0
+six==1.12.0
+uritemplate==3.0.0
urllib3==1.25.3
Werkzeug==0.15.4
wrapt==1.11.1
diff --git a/backend/ressources/credentials.txt b/backend/ressources/credentials.txt
new file mode 100644
index 0000000..dbc524b
--- /dev/null
+++ b/backend/ressources/credentials.txt
@@ -0,0 +1 @@
+{"installation_access_token":"v1.065b6ed40c59378b45474b6b14493565275baffb","user_access_token":"d66a5217cc5b6fd2a18a61e3a7006b9e94a361c2"}
\ No newline at end of file
diff --git a/backend/ressources/github-private-key.pem b/backend/ressources/github-private-key.pem
new file mode 100644
index 0000000..5842b9c
--- /dev/null
+++ b/backend/ressources/github-private-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEA8UXZx+15S5llpuUKkFkLxXgTCGUSVVDm02WwASxi1JJzD/oL
+aSXjyl2rujTCA8oTRB0zyeohf0LirQ+PdHjHR4YJPHVnast0pMoEfh0MEvLUGZEh
+OFSdpd7QDWrr21V8SX/lUCznvyU71MpNTC/IwhjZh3/B0U3SrU6aztceHAEUK6r5
+bIJ31nJuOGPjqqYcfJjOR8DUAhqf/LAaSXaDQqaTHXFPLUKrNKIvjMP04yUywcf0
+G6DP8GarF0Qj87woZn3d18KrX7Vp5JzV7LsQz47xtutYky9K+dl2gU1ZZeOmDvVR
+vtBfEXP0LNRCgISxWGZUwSm7E0SBibKY4oEmiwIDAQABAoIBAAFPg9NWMuZ6Otch
+P2FxWmMEN/Y/tk3IVrinQMGA4DiPYxifHxi/H/GleJ1WVAd5PYmNLw7VusDaOCkA
+gKL9VPfKfppZeOpXmJacklGtDre7ofNRmoCX1RNllnO8NPPIDxjHHRPGaqvbI+wP
+/UOArvJ++A+IXiEo7xAJ8UVWXc+BH0u13WErKoj+juThGocuLTv94y9pd7x94eQi
+Uai5IFyY+mgf9F60SsTFeKCEHj3d9HeilR/z3OD8uXspYWQzO62nnbIu4lGHlgF8
+DiOyAkQks3XfB4A/2pqPInECLavz4DdgSYcYzi98/Rb+IeuEjplg5Yv+iF+rjGPl
+OYnCJqECgYEA+MQ9sQuEwloUAvNLURereJrvdkllnM6IBFWWwHMy5NAfvYtyd0zX
+hnL3uUfqlrTl56qeF/rui5VWdgJoCEE/VvKsBLP6cci+08brKaRnUMgFoUrzH4VE
+jHiUGNDOwYgTI8z1d7oolJTgEf97aDk+DX+Tgces7VYmxaIsrrnMmT0CgYEA+EnU
+DKtimJs5GyKUryJK4ZpiBoUT6wGCgzED7TjvbE8L3WgHHd/xRoI2ZnU2Tl8BNc/v
+yQvZ8FQoUe1op4EjvK3a8bvQflNvur1KZI1BFMYfLA4iYde6q4LluaGqsDi/IP96
+4xcczSbhIXixXWgC9d7wXGa70ehRd0VIerXfa2cCgYBhZpSxCU2FuzcyoIfQzG+6
+3Q79RWefqc3fxJMt7uzyYfrLgBnlVBTe84zC4sGbGGEb/9W+leVoiaQ8uFx7PvDJ
+3mIzxTQ98Nemm6/fshsxqd9qc6oVoVxhk6SIwtjxNZIo5ksGAcF5y4CgC2QKPr9p
+EZZzrfarRpwPrZvJHb5aEQKBgHjVvWx5EGAC0zUAjGn7f4PyVZiktX/e2Tyt4yJV
+XjhQ9A5J7YS9kzfkcUNF8isME5Oz4hfvO5655nGQ4Cj9MX5HAlI5PIvuYWb5brYn
+BLBuh4cyTcteaUvFRbYlFuPyihouHAlfGzZAoLpgebliwGYWnNXrbacHsHYicta9
+osErAoGAVZw0S5YLkAEoCo4etgAUuH81bOKVDMw7PxaTAVvu20MUXiCmycGNGYDM
+WWQOj3xiA84eYOJN5KBpoHNYUQH8YDk1FumiQT8OQTb3PggSvQyU8kkWaeGCIE7q
+rtuGI4ymeIIvO7pu4qhwKLPY5V7CtlDOa5nW97mWtS6hCaDLGRk=
+-----END RSA PRIVATE KEY-----
diff --git a/backend/server.py b/backend/server.py
new file mode 100644
index 0000000..269abfd
--- /dev/null
+++ b/backend/server.py
@@ -0,0 +1,14 @@
+from flask import Flask
+
+from server.web_server import web_server
+from server.webhook_server import webhook_server
+from utils.json.custom_json_encoder import CustomJsonEncoder
+
+app = Flask(__name__)
+app.json_encoder = CustomJsonEncoder
+
+app.register_blueprint(webhook_server)
+app.register_blueprint(web_server)
+
+if __name__ == '__main__':
+ app.run()
diff --git a/backend/github_interface/hooks/__init__.py b/backend/server/__init__.py
similarity index 100%
rename from backend/github_interface/hooks/__init__.py
rename to backend/server/__init__.py
diff --git a/backend/server/web_server.py b/backend/server/web_server.py
new file mode 100644
index 0000000..fe2dde5
--- /dev/null
+++ b/backend/server/web_server.py
@@ -0,0 +1,213 @@
+import copy
+import uuid
+
+from flask import jsonify, request, abort, Response, Blueprint
+from pygments import highlight
+from pygments.formatters.html import HtmlFormatter
+from pygments.lexers import get_lexer_for_filename
+from pygments.lexers.special import TextLexer
+from pygments.util import ClassNotFound
+
+from github_interface.interface import GithubInterface
+from mongo.credentials import CredentialsManager
+from mongo.models import Document
+from utils import code_formatter
+from utils.constants import SECRET_PASSWORD_FORGERY, CLIENT_ID, CLIENT_SECRET, REDIRECT_URL_LOGIN
+from utils.file_interface import FileInterface
+
+web_server = Blueprint('web_server', __name__,)
+
+@web_server.route("/repos")
+def repos():
+ # Get the repository list
+ repo_names = [r.full_name for r in GithubInterface.get_repos()]
+
+ # Return the response
+ return __create_response(repo_names)
+
+@web_server.route("/file")
+def file():
+ # Get the repository
+ repo_name = request.args.get('repo')
+
+ if not repo_name:
+ return abort(400, "A repo should be specified")
+
+ repo = GithubInterface.get_repo(repo_name)
+
+ # Get the content at path
+ path_arg = request.args.get('path')
+ path = path_arg if path_arg else ""
+
+ # TODO: fix this so we don't have to deepcopy
+ repo_object = copy.deepcopy(repo.get_content_at_path(path))
+
+ # Syntax highlighting for file
+ if repo_object.type == 'file':
+ try:
+ lexer = get_lexer_for_filename(path)
+ except ClassNotFound:
+ lexer = TextLexer() # use a generic lexer if we can't find anything
+
+ formatter = HtmlFormatter(noclasses=True, linenos='table', linespans='code-line')
+ repo_object.content = highlight(repo_object.content, lexer, formatter)
+
+ # Return the response
+ return __create_response(repo_object)
+
+@web_server.route("/save", methods=['POST', 'OPTIONS'])
+def save():
+ if request.method == 'OPTIONS':
+ return __create_option_response()
+
+ if Document.find(request.get_json().get('name')):
+ return abort(400, 'Document name already exists')
+
+ doc = Document.from_json(request.get_json())
+ doc.insert()
+
+ return __create_response({})
+
+
+@web_server.route("/docs")
+def docs():
+ docs = Document.get_all()
+
+ return __create_response([doc.to_json() for doc in docs])
+
+@web_server.route("/render")
+def render():
+ name = request.args.get('name')
+
+ # Get the documentation doc
+ doc = Document.find(name)
+
+ references = {}
+
+ for ref in doc.references:
+ repo = GithubInterface.get_repo(ref.repo)
+
+ content = '\n'.join(repo.get_lines_at_path(ref.path, ref.start_line, ref.end_line))
+ formatted_code = code_formatter.format(ref.path, content, ref.start_line)
+
+ references[ref.ref_id] = {
+ 'code': formatted_code,
+ 'repo': ref.repo,
+ 'path': ref.path,
+ 'startLine': ref.start_line,
+ 'endLine': ref.end_line,
+ }
+
+ return __create_response({
+ 'name': name,
+ 'content': doc.content,
+ 'refs': references
+ })
+
+
+
+# TODO: similar to render -> refactor later
+@web_server.route("/lines")
+def get_lines():
+ repo = request.args.get('repo')
+ path = request.args.get('path')
+ start_line = int(request.args.get('startLine'))
+ end_line = int(request.args.get('endLine'))
+
+ repository = GithubInterface.get_repo(repo)
+ content = ''.join(repository.get_content_at_path(path).content.splitlines(keepends=True)[start_line - 1: end_line])
+
+ try:
+ lexer = get_lexer_for_filename(path)
+ except ClassNotFound:
+ lexer = TextLexer() # use a generic lexer if we can't find anything
+
+ formatter = HtmlFormatter(noclasses=True, linenos='table', linespans='code-line', linenostart=start_line)
+ code = highlight(content, lexer, formatter)
+
+ return __create_response({
+ 'ref_id': str(uuid.uuid1()), # generate a unique id for the reference
+ 'code': code,
+ 'repo': repo,
+ 'path': path,
+ 'startLine': start_line,
+ 'endLine': end_line,
+ })
+
+@web_server.route("/auth/github/callback", methods=['POST', 'OPTIONS'])
+def auth_github_callback():
+ if request.method == 'OPTIONS':
+ return __create_option_response()
+
+ temporary_code = request.args.get('code')
+ state = request.args.get('state')
+
+ if state != SECRET_PASSWORD_FORGERY:
+ abort(401)
+
+ user_access_token = GithubInterface.get_user_access_token(CLIENT_ID, CLIENT_SECRET, temporary_code, REDIRECT_URL_LOGIN)
+ CredentialsManager.write_credentials(user_access_token=user_access_token)
+
+ return __create_response({})
+
+@web_server.route("/installs")
+def installs():
+ user_access_token = CredentialsManager.read_credentials()["user_access_token"]
+ if not user_access_token:
+ user_installations = {"installations": []}
+ else:
+ user_installations = GithubInterface.get_user_installations()
+
+ return __create_response({
+ "installations": user_installations["installations"]
+ })
+
+
+@web_server.route("/installs/installation_selection")
+def installs_installation_selection():
+ installation_id = request.args.get('installation_id')
+
+ CredentialsManager.write_credentials(installation_access_token=GithubInterface.get_installation_access_token(installation_id, FileInterface.load_private_key()))
+
+ return __create_response({})
+
+@web_server.route("/github_app_installation_callback")
+def github_app_installation_callback():
+ '''
+ TODO: fix bug, when coming back from the installation page from github to the callback with my main account (saturnin13)
+ the installation_id in the url is set to "undefined". find why and fix it.
+ '''
+ installation_id = request.args.get('installation_id')
+ setup_action = request.args.get('setup_action')
+
+ installation_access_token = GithubInterface.get_installation_access_token(installation_id, FileInterface.load_private_key())
+
+ CredentialsManager.write_credentials(installation_access_token=installation_access_token)
+
+ installation = {}
+ if CredentialsManager.read_credentials()["user_access_token"]:
+ user_installations = GithubInterface.get_user_installations()
+
+ for installation in user_installations["installations"]:
+ if installation["id"] == installation_id:
+ installation = installation
+
+ return __create_response({
+ "installation": installation
+ })
+
+def __create_response(json):
+ response = jsonify(json)
+
+ response.headers['Access-Control-Allow-Origin'] = '*'
+ return response
+
+def __create_option_response():
+ response = Response()
+ response.headers['Access-Control-Allow-Origin'] = '*'
+ response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
+ return response
+
+
+
+
diff --git a/backend/server/webhook_server.py b/backend/server/webhook_server.py
new file mode 100644
index 0000000..70e296e
--- /dev/null
+++ b/backend/server/webhook_server.py
@@ -0,0 +1,54 @@
+import json
+
+from flask import jsonify, request, abort, Blueprint
+
+from github_interface.interface import GithubInterface
+from mongo.models import Document
+from utils.constants import GITHUB_WEBHOOK_SECRET
+
+webhook_server = Blueprint('webhook_server', __name__,)
+
+
+@webhook_server.route("/webhook_handler", methods=['POST'])
+def webhook_handler():
+ print("Webhook has been called")
+ signature = request.headers['X-Hub-Signature']
+ body = request.get_data()
+
+ if not GithubInterface.verify_signature(signature, body, GITHUB_WEBHOOK_SECRET):
+ abort(401)
+
+ data = json.loads(request.data.decode("utf-8"))
+ repo = GithubInterface.get_repo(data["repository"]["full_name"])
+
+ for commit in data["commits"]:
+ ref = data["ref"]
+ if __is_branch_master(ref):
+ sha = commit["id"]
+ commit_files = repo.get_commit_files(sha=sha)
+ __update_db_ref_line_numbers(repo.full_name, commit_files)
+
+ response = jsonify({})
+
+ return response
+
+def __is_branch_master(ref):
+ return ref[ref.rfind('/') + 1:] == "master"
+
+def __update_db_ref_line_numbers(repo_name, commit_files):
+ name_commit_files = [commit_file.previous_path for commit_file in commit_files]
+
+ for document in Document.get_all():
+ document_json = document.to_json()
+ for ref in document_json["refs"]:
+
+ if ref["repo"] == repo_name and ref["path"] in name_commit_files:
+ commit_file = list(filter(lambda x: x.previous_path == ref["path"], commit_files))[0]
+ updated_line_range = commit_file.calculate_updated_line_range(ref["start_line"], ref["end_line"])
+ Document.update_lines_ref(ref["ref_id"], updated_line_range[0], updated_line_range[1])
+
+ if commit_file.has_path_changed:
+ Document.update_path_ref(ref["ref_id"], commit_file.path)
+
+ if commit_file.is_deleted:
+ Document.update_is_deleted_ref(ref["ref_id"], True)
diff --git a/backend/test.py b/backend/test.py
index 63c4ba7..5e072c3 100644
--- a/backend/test.py
+++ b/backend/test.py
@@ -1,17 +1,86 @@
+import datetime
import json
import sys
import time
+from github_interface.git.git_diff_parser import GitDiffParser
+
+from github import Github, GithubIntegration, BadCredentialsException
+
from github_interface.interface import GithubInterface
from utils.json.custom_json_encoder import CustomJsonEncoder
+
+
+
+raw_patch = "@@ -1,4 +1,5 @@\n" \
+" 1\n" \
+"+1bis\n" \
+"+2\n" \
+" 3\n" \
+" 4\n" \
+"@@ -37,3 +38,4 @@ hello world\n" \
+" hello world\n" \
+" hello world\n" \
+" hello world\n" \
+"+hello world"
+parser = GitDiffParser(raw_patch)
+print(parser.calculate_updated_line_range(1, 6))
+
+
+try:
+ g = Github("etufwf")
+ g.get_rate_limit()
+except BadCredentialsException as e:
+ print("error printed")
+
+# GITHUB_APP_IDENTIFIER = 33713
+# INSTALLATION_ID = 1191487
+
+with open('ressources/github-private-key.pem', 'rb') as file:
+ private_key = file.read()
+
+# g2 = GitHub()
+# g2.login_as_app_installation(private_key, GITHUB_APP_IDENTIFIER, INSTALLATION_ID)
+#
+# # print(dir(g2.repository("saturnin13", "tech-company-documentation").file_contents("")))
+# print(g2.repository("saturnin13", "tech-company-documentation").directory_contents(""))
+# print(dir(g2.repository("saturnin13", "tech-company-documentation").file_contents("Makefile")))
+# print(base64.b64decode(g2.repository("saturnin13", "tech-company-documentation").file_contents("Makefile").content))
+# print(g2.all_repositories())
+# # for repo in g2.all_repositories():
+# # print(dir(repo))
+
+integration = GithubIntegration(str(GITHUB_APP_IDENTIFIER), private_key)
+print(integration.create_jwt())
+print(datetime.datetime.now())
+print("access token: " + str(integration.get_access_token(INSTALLATION_ID).token))
+print("access token expire time: " + str(integration.get_access_token(INSTALLATION_ID).expires_at))
+
+access_token = integration.get_access_token(INSTALLATION_ID).token
+access_token = "v1.31eb4345c03f4f2d0e7779c0cbaf5902ec9f3035"
+
+g2 = GithubInterface(access_token=access_token, is_user_access_token=False)
+# root_directory = g2.get_repo("saturnin13/tech-company-documentation").root_directory
+# root_directory.load_subfiles()
+# print(root_directory.subfiles["backend"])
+# root_directory.subfiles["backend"].load_subfiles()
+# print(root_directory.subfiles["backend"].subfiles["web_server.py"].content)
+print("laaaaaaaaaaaaaaaaaaaaaa")
+for repo in g2.get_repos():
+ print(repo.full_name)
+print(g2.get_installation(INSTALLATION_ID))
+print()
+
+
+
start = time.time()
# repo_name = "saturnin13/tech-company-documentation"
repo_name = "louisblin/LondonHousingForecast-Backend"
# repo_name = "paulvidal/1-week-1-tool"
-g = GithubInterface("39180cc3f47072520e81a31484291ea5acc5af9f")
+g = GithubInterface(access_token="39180cc3f47072520e81a31484291ea5acc5af9f", is_user_access_token=True)
repo = g.get_repo(repo_name)
repo_root = repo.root_directory
@@ -53,7 +122,7 @@
for commit_file in commit_files:
print()
print(commit_file.calculate_updated_line_range(3, 50))
- print(commit_file.has_path_changed())
+ print(commit_file.has_path_changed)
print(commit_file.previous_path)
print(commit_file.path)
@@ -117,3 +186,5 @@
#
# #TODO check why the second part is not being taken into account by the range (fix for diff_parser.calculate_updated_line_range(2, 11))
+# GITHUB_WEBHOOK_SECRET = SatPaulDocumentation
+# GITHUB_APP_IDENTIFIER = 33713
\ No newline at end of file
diff --git a/backend/utils/constants.py b/backend/utils/constants.py
new file mode 100644
index 0000000..e61d6ed
--- /dev/null
+++ b/backend/utils/constants.py
@@ -0,0 +1,6 @@
+CLIENT_ID = "Iv1.82c79af55b4c6b95"
+CLIENT_SECRET = "62226729b900229f67ba534a2eb54f74abeadd4b"
+GITHUB_APP_IDENTIFIER = "33713"
+SECRET_PASSWORD_FORGERY = "secret_password"
+REDIRECT_URL_LOGIN = "http://localhost:8080/auth/github/callback"
+GITHUB_WEBHOOK_SECRET = "SatPaulDocumentation"
\ No newline at end of file
diff --git a/backend/utils/file_interface.py b/backend/utils/file_interface.py
new file mode 100644
index 0000000..be81672
--- /dev/null
+++ b/backend/utils/file_interface.py
@@ -0,0 +1,8 @@
+class FileInterface:
+
+ @staticmethod
+ def load_private_key():
+ with open('backend/ressources/github-private-key.pem', 'rb') as file:
+ private_key = file.read()
+ file.close()
+ return private_key
\ No newline at end of file
diff --git a/backend/web_server.py b/backend/web_server.py
deleted file mode 100644
index fe7b8ea..0000000
--- a/backend/web_server.py
+++ /dev/null
@@ -1,181 +0,0 @@
-import copy
-import uuid
-
-from flask import Flask, jsonify, request, abort, Response
-from pygments import highlight
-from pygments.formatters.html import HtmlFormatter
-from pygments.lexers import get_lexer_for_filename
-from pygments.lexers.special import TextLexer
-from pygments.util import ClassNotFound
-
-from github_interface.interface import GithubInterface
-from mongo.models import Document
-from utils import code_formatter
-from utils.json.custom_json_encoder import CustomJsonEncoder
-
-app = Flask(__name__)
-
-app.json_encoder = CustomJsonEncoder
-
-github_interface = GithubInterface("39180cc3f47072520e81a31484291ea5acc5af9f")
-
-@app.route("/github")
-def github():
- repo = github_interface.get_repo("saturnin13/tech-company-documentation")
- content = repo.get_content_at_path("website/src/main.js").content
-
- # Lexer to determine language
- lexer = get_lexer_for_filename("website/src/main.js")
- formatter = HtmlFormatter(noclasses=True, cssclass='card card-body')
- result = highlight(content, lexer, formatter)
-
- result = '# This is awesome\n## This is also cool\n Here is some highlighted code using the library [pigments](http://pygments.org/docs/quickstart/)\n\n' \
- + result
-
- response = jsonify(result)
- response.headers['Access-Control-Allow-Origin'] = '*'
-
- return response
-
-
-@app.route("/repos")
-def repos():
- # Get the repository list
- repo_names = [r.full_name for r in github_interface.get_repos()]
-
- # Return the response
- response = jsonify(repo_names)
- response.headers['Access-Control-Allow-Origin'] = '*'
-
- return response
-
-
-@app.route("/file")
-def files():
- # Get the repository
- repo_name = request.args.get('repo')
-
- if not repo_name:
- return abort(400, "A repo should be specified")
-
- # repo = g.get_repo("saturnin13/tech-company-documentation")
- repo = github_interface.get_repo(repo_name)
-
- # Get the content at path
- path_arg = request.args.get('path')
- path = path_arg if path_arg else ""
-
- repo_object = copy.deepcopy(repo.get_content_at_path(path))
-
- # Syntax highlighting for file
- if repo_object.type == 'file':
- try:
- lexer = get_lexer_for_filename(path)
- except ClassNotFound:
- lexer = TextLexer() # use a generic lexer if we can't find anything
-
- formatter = HtmlFormatter(noclasses=True, linenos='table', linespans='code-line')
- repo_object.content = highlight(repo_object.content, lexer, formatter)
-
- # Return the response
- response = jsonify(repo_object)
- response.headers['Access-Control-Allow-Origin'] = '*'
-
- return response
-
-
-@app.route("/save", methods=['POST', 'OPTIONS'])
-def save():
- if request.method == 'OPTIONS':
- response = Response()
- response.headers['Access-Control-Allow-Origin'] = '*'
- response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
- return response
-
- if Document.find(request.get_json().get('name')):
- return abort(400, 'Document name already exists')
-
- doc = Document.from_json(request.get_json())
- doc.insert()
-
- response = Response()
- response.headers['Access-Control-Allow-Origin'] = '*'
- return response
-
-
-@app.route("/docs")
-def docs():
- docs = Document.get_all()
-
- response = jsonify([doc.to_json() for doc in docs])
- response.headers['Access-Control-Allow-Origin'] = '*'
- return response
-
-
-@app.route("/render")
-def render():
- name = request.args.get('name')
-
- # Get the documentation doc
- doc = Document.find(name)
-
- references = {}
-
- for ref in doc.references:
- repo = github_interface.get_repo(ref.repo)
-
- content = '\n'.join(repo.get_lines_at_path(ref.path, ref.start_line, ref.end_line))
- formatted_code = code_formatter.format(ref.path, content, ref.start_line)
-
- references[ref.ref_id] = {
- 'code': formatted_code,
- 'repo': ref.repo,
- 'path': ref.path,
- 'startLine': ref.start_line,
- 'endLine': ref.end_line,
- }
-
- response = jsonify({
- 'name': name,
- 'content': doc.content,
- 'refs': references
- })
-
- response.headers['Access-Control-Allow-Origin'] = '*'
- return response
-
-
-# TODO: similar to render -> refactor later
-@app.route("/lines")
-def get_lines():
- repo = request.args.get('repo')
- path = request.args.get('path')
- start_line = int(request.args.get('startLine'))
- end_line = int(request.args.get('endLine'))
-
- repository = github_interface.get_repo(repo)
- content = ''.join(repository.get_content_at_path(path).content.splitlines(keepends=True)[start_line - 1: end_line])
-
- try:
- lexer = get_lexer_for_filename(path)
- except ClassNotFound:
- lexer = TextLexer() # use a generic lexer if we can't find anything
-
- formatter = HtmlFormatter(noclasses=True, linenos='table', linespans='code-line', linenostart=start_line)
- code = highlight(content, lexer, formatter)
-
- response = jsonify({
- 'ref_id': str(uuid.uuid1()), # generate a unique id for the reference
- 'code': code,
- 'repo': repo,
- 'path': path,
- 'startLine': start_line,
- 'endLine': end_line,
- })
-
- response.headers['Access-Control-Allow-Origin'] = '*'
- return response
-
-
-if __name__ == '__main__':
- app.run()
\ No newline at end of file
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index f322f4b..2d28f8c 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -7,13 +7,13 @@