feat: add initial project setup with Flask and Git integration

Introduce the initial project structure for gitcloak, a Python-based private frontend for GitHub repositories. Key changes include:

- Added .gitignore to exclude virtual environment, bytecode files, and cache directories.
- Added LICENSE file using the MIT License.
- Created a basic `pyproject.toml` with project metadata and dependencies.
- Implemented Flask-based application to render repository structure and file contents.
- Added `Git` class to handle interactions with remote GitHub repositories using Dulwich and requests.
- Included HTML template for displaying repo content.
- Created sample `test.py` for testing Git class methods.

This setup enables foundational project functionality and establishes a clear structure for further development.
This commit is contained in:
Kumi 2024-06-18 17:00:05 +02:00
commit 117997a5d9
Signed by: kumi
GPG key ID: ECBCC9082395383F
10 changed files with 280 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
venv/
*.pyc
__pycache__/

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 Kumi Mitterer <gitcloak@kumi.email>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

0
README.md Normal file
View file

22
pyproject.toml Normal file
View file

@ -0,0 +1,22 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "gitcloak"
version = "0.0.1"
authors = [{ name = "Kumi Mitterer", email = "gitcloak@kumi.email" }]
description = "Simple Python-based private frontend for GitHub repositories"
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = ["requests", "flask", "markdown2"]
[project.urls]
"Homepage" = "https://git.private.coffee/kumi/gitcloak"
"Bug Tracker" = "https://git.private.coffee/kumi/gitcloak/issues"

0
src/gitcloak/__init__.py Normal file
View file

73
src/gitcloak/app.py Normal file
View file

@ -0,0 +1,73 @@
from flask import Flask, render_template, jsonify, abort
from .classes.git import Git
import markdown2
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
app = Flask(__name__)
@app.route("/<owner>/<repo>/", methods=["GET"])
@app.route("/<owner>/<repo>/tree/main/", methods=["GET"])
@app.route("/<owner>/<repo>/tree/main/<path:path>", methods=["GET"])
def get_tree(owner, repo, path=""):
repo_url = f"https://github.com/{owner}/{repo}.git"
git = Git(repo_url)
try:
directory_structure = git.get_directory_structure()
filtered_structure = [
entry[len(path) :]
for entry in directory_structure
if entry.startswith(path)
]
# Separate files and directories
directories = set(
[entry.split("/")[0] for entry in filtered_structure if "/" in entry]
)
files = [entry for entry in filtered_structure if "/" not in entry]
# Get README.md content if it exists
readme_content = None
if f"README.md" in files:
readme_md = git.get_file_content(f"{path}/README.md")
readme_content = markdown2.markdown(readme_md.decode("utf-8"))
return render_template(
"path.html",
owner=owner,
repo=repo,
path=path,
directories=directories,
files=files,
readme_content=readme_content,
)
except Exception as e:
logger.error(
f"Error getting directory structure for {path} in {owner}/{repo}: {e}"
)
abort(404, description=str(e))
@app.route("/<owner>/<repo>/raw/<path:file_path>", methods=["GET"])
def get_raw(owner, repo, file_path):
repo_url = f"https://github.com/{owner}/{repo}.git"
git = Git(repo_url)
try:
file_content = git.get_file_content(file_path)
return file_content
except Exception as e:
logger.error(
f"Error getting file content for {file_path} in {owner}/{repo}: {e}"
)
abort(404, description=str(e))
if __name__ == "__main__":
app.run(debug=True, port=8107)

View file

View file

@ -0,0 +1,86 @@
import requests
from dulwich.objects import Tree, Blob
from dulwich.client import HttpGitClient, get_transport_and_path
from dulwich.repo import MemoryRepo
class InMemoryRepo(MemoryRepo):
def get_tree(self, commit_sha):
commit = self.get_object(commit_sha)
return self.get_object(commit.tree)
def list_tree(self, tree, prefix=""):
for entry in tree.items():
entry_path = f"{prefix}/{entry.path.decode('utf-8')}" if prefix else entry.path.decode('utf-8')
if isinstance(self.get_object(entry.sha), Tree):
for _ in self.list_tree(self.get_object(entry.sha), entry_path):
yield(_)
else:
yield(entry_path)
def get_file_content(self, tree, file_path):
parts = file_path.split('/')
for entry in tree.items():
entry_name = entry.path.decode('utf-8')
if entry_name == parts[0]:
if len(parts) == 1:
file_obj = self.get_object(entry.sha)
if isinstance(file_obj, Blob):
return file_obj.data
else:
raise ValueError(f"Path {file_path} is not a file.")
else:
if isinstance(self.get_object(entry.sha), Tree):
return self.get_file_content(self.get_object(entry.sha), '/'.join(parts[1:]))
else:
raise ValueError(f"Path {file_path} is not a directory.")
raise ValueError(f"File {file_path} not found in the repository.")
class Git:
def __init__(self, repo_url):
self.repo_url = repo_url.rstrip('/')
self.client = HttpGitClient(self.repo_url)
def get_remote_refs(self):
client, path = get_transport_and_path(self.repo_url)
refs = client.fetch(path, self.repo)
return refs
def get_head_commit(self, refs):
return refs[b'HEAD']
def get_pack_data(self, commit_sha):
url = f"{self.repo_url}/git-upload-pack"
request_body = f"0032want {commit_sha} multi_ack_detailed side-band-64k thin-pack ofs-delta agent=git/2.28.0\n00000009done\n"
response = requests.post(url, data=request_body.encode('utf-8'))
response.raise_for_status()
return response.content
def get_directory_structure(self):
# Initialize an in-memory repository
self.repo = InMemoryRepo()
# Fetch the remote references and objects into the in-memory repository
refs = self.get_remote_refs()
head_commit_hash = self.get_head_commit(refs)
# Get the tree object for the HEAD commit
tree = self.repo.get_tree(head_commit_hash)
# List the directory structure
return list(self.repo.list_tree(tree))
def get_file_content(self, file_path):
file_path = file_path.lstrip('/')
# Initialize an in-memory repository
self.repo = InMemoryRepo()
# Fetch the remote references and objects into the in-memory repository
refs = self.get_remote_refs()
head_commit_hash = self.get_head_commit(refs)
# Get the tree object for the HEAD commit
tree = self.repo.get_tree(head_commit_hash)
# Get the file content
return self.repo.get_file_content(tree, file_path)

View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ owner }}/{{ repo }} - {{ path }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.directory,
.file {
margin: 5px 0;
}
.directory a,
.file a {
text-decoration: none;
color: #0366d6;
}
.directory a:hover,
.file a:hover {
text-decoration: underline;
}
.readme {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd;
background-color: #f6f8fa;
}
</style>
</head>
<body>
<h1>{{ owner }}/{{ repo }} - {{ path }}</h1>
<div>
<h2>Directories</h2>
<div>
{% for directory in directories %}
<div class="directory">
<a href="/{{ owner }}/{{ repo }}/tree/main/{{ path }}{{ directory }}"
>{{ directory }}</a
>
</div>
{% endfor %}
</div>
</div>
<div>
<h2>Files</h2>
<div>
{% for file in files %}
<div class="file">
<a href="/{{ owner }}/{{ repo }}/raw/{{ path }}{{ file }}"
>{{ file }}</a
>
</div>
{% endfor %}
</div>
</div>
{% if readme_content %}
<div class="readme">
<h2>README.md</h2>
<div>{{ readme_content|safe }}</div>
</div>
{% endif %}
</body>
</html>

11
test.py Normal file
View file

@ -0,0 +1,11 @@
from gitcloak.classes.git import Git
repo = "https://github.com/privatecoffee/transfer.coffee"
git = Git(repo)
print(git.get_directory_structure())
file_path = "public/dist/js/webtorrent.LICENSE"
content = git.get_file_content(file_path)
print(f"Type of content: {type(content)}")
content = content.decode('utf-8')
print(f"\nContent of {file_path}:\n{content}")