import os
import sys
import json
import time
import threading
import subprocess
import shutil
from pathlib import Path
from datetime import datetime
from functools import wraps
from flask import Flask, request, jsonify, render_template_string, send_file, abort
from dulwich.repo import Repo
from dulwich.web import HTTPGitApplication, make_wsgi_chain
from dulwich.objects import Blob, Tree, Commit
from dulwich.index import build_index_from_tree
from dulwich.diff_tree import tree_changes
from dulwich.pack import write_pack_objects
from dulwich.protocol import ZERO_SHA
import requests
REPOS_DIR = Path("./repos")
REPOS_DIR.mkdir(exist_ok=True)
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
OPENROUTER_MODEL = "qwen/qwen-2.5-coder-32b-instruct"
app = Flask(__name__)
git_app = HTTPGitApplication(REPOS_DIR, export_all=True)
git_wsgi = make_wsgi_chain(git_app)
HTML_TEMPLATE = """
Git Server
🗂 Git Server
{% if repo_name %}
Repos
{% for part in breadcrumb %}
/
{% if part.url %}
{{ part.name }}{% else %}
{{ part.name }}{% endif %}
{% endfor %}
{{ repo_name }}
{% if view == 'tree' %}
{% for item in tree_items %}
{% endfor %}
{% elif view == 'blob' %}
{{ blob_path }}
{% elif view == 'log' %}
{% elif view == 'commit' %}
{{ commit.id }}
{{ commit.message }}
{{ commit.author }} • {{ commit.date }}
Changes
{% endif %}
{% else %}
{% endif %}
"""
def get_repo_path(name):
return REPOS_DIR / f"{name}.git"
def list_repos():
repos = []
for path in REPOS_DIR.iterdir():
if path.is_dir() and path.name.endswith(".git"):
repos.append(path.name[:-4])
return sorted(repos)
def open_repo(name):
path = get_repo_path(name)
if not path.exists():
return None
return Repo(str(path))
def get_tree(repo, tree_id, path=""):
tree = repo[tree_id]
items = []
for entry in tree.items():
obj = repo[entry.sha]
item_path = f"{path}/{entry.path.decode()}" if path else entry.path.decode()
if isinstance(obj, Tree):
items.append({
"name": entry.path.decode(),
"type": "tree",
"url": f"/{repo.path.name[:-4]}/tree/{entry.sha.decode()}/{item_path.lstrip('/')}"
})
elif isinstance(obj, Blob):
items.append({
"name": entry.path.decode(),
"type": "blob",
"url": f"/{repo.path.name[:-4]}/blob/{entry.sha.decode()}/{item_path.lstrip('/')}"
})
items.sort(key=lambda x: (x["type"] != "tree", x["name"].lower()))
return items
def get_blob_content(repo, blob_id):
blob = repo[blob_id]
try:
return blob.data.decode("utf-8")
except UnicodeDecodeError:
return ""
def get_commits(repo, limit=50):
commits = []
walker = repo.get_walker()
for entry in walker:
if len(commits) >= limit:
break
commit = entry.commit
commits.append({
"id": commit.id.decode(),
"short_id": commit.id.decode()[:8],
"message": commit.message.decode().strip(),
"author": commit.author.decode(),
"date": datetime.fromtimestamp(commit.commit_time).strftime("%Y-%m-%d %H:%M:%S")
})
return commits
def get_commit_diff(repo, commit_id):
commit = repo[commit_id]
parent_id = commit.parents[0] if commit.parents else ZERO_SHA
parent_tree = repo[parent_id].tree if parent_id != ZERO_SHA else None
commit_tree = commit.tree
changes = list(tree_changes(repo.object_store, parent_tree, commit_tree))
diff_lines = []
for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes:
oldpath = oldpath.decode() if oldpath else ""
newpath = newpath.decode() if newpath else ""
if oldsha == newsha:
continue
diff_lines.append(f"diff --git a/{oldpath or newpath} b/{newpath or oldpath}")
if oldsha == ZERO_SHA:
diff_lines.append("new file mode 100644")
diff_lines.append("--- /dev/null")
diff_lines.append(f"+++ b/{newpath}")
blob = repo[newsha]
for line in blob.data.decode("utf-8", errors="replace").splitlines():
diff_lines.append(f"+{line}")
elif newsha == ZERO_SHA:
diff_lines.append("deleted file mode 100644")
diff_lines.append(f"--- a/{oldpath}")
diff_lines.append("+++ /dev/null")
blob = repo[oldsha]
for line in blob.data.decode("utf-8", errors="replace").splitlines():
diff_lines.append(f"-{line}")
else:
diff_lines.append(f"--- a/{oldpath}")
diff_lines.append(f"+++ b/{newpath}")
old_blob = repo[oldsha]
new_blob = repo[newsha]
old_lines = old_blob.data.decode("utf-8", errors="replace").splitlines()
new_lines = new_blob.data.decode("utf-8", errors="replace").splitlines()
import difflib
for line in difflib.unified_diff(old_lines, new_lines, lineterm=""):
if line.startswith("---") or line.startswith("+++") or line.startswith("@@"):
diff_lines.append(line)
elif line.startswith("+"):
diff_lines.append(f"{line}")
elif line.startswith("-"):
diff_lines.append(f"{line}")
else:
diff_lines.append(line)
return "\n".join(diff_lines)
def build_breadcrumb(repo_name, path_parts):
breadcrumb = []
current = ""
for part in path_parts:
current = f"{current}/{part}" if current else part
breadcrumb.append({
"name": part,
"url": f"/{repo_name}/tree/HEAD/{current}"
})
return breadcrumb
@app.route("/")
def index():
repos = list_repos()
return render_template_string(HTML_TEMPLATE, repos=repos)
@app.route("/api/repos", methods=["POST"])
def create_repo():
data = request.get_json() or {}
name = data.get("name") or request.form.get("name")
if not name:
return jsonify({"error": "name required"}), 400
name = name.strip().replace("/", "-")
repo_path = get_repo_path(name)
if repo_path.exists():
return jsonify({"error": "repository exists"}), 400
Repo.init_bare(str(repo_path))
return jsonify({"name": name, "url": f"/{name}"}), 201
@app.route("/api/repos//delete", methods=["POST"])
def delete_repo(name):
repo_path = get_repo_path(name)
if repo_path.exists():
shutil.rmtree(repo_path)
return jsonify({"ok": True})
@app.route("/")
def repo_home(repo_name):
repo = open_repo(repo_name)
if not repo:
abort(404)
head = repo.head()
try:
tree_id = repo[head].tree
except:
tree_id = None
return repo_tree(repo_name, tree_id.decode() if tree_id else "HEAD", "")
@app.route("//tree//")
@app.route("//tree/")
def repo_tree(repo_name, tree_id, path=""):
repo = open_repo(repo_name)
if not repo:
abort(404)
try:
tree_obj = repo[tree_id.encode() if len(tree_id) == 40 else tree_id]
if not isinstance(tree_obj, Tree):
tree_obj = repo[repo[tree_id.encode() if len(tree_id) == 40 else tree_id].tree]
except:
abort(404)
items = get_tree(repo, tree_obj.id, path)
path_parts = [p for p in path.split("/") if p]
breadcrumb = build_breadcrumb(repo_name, path_parts)
return render_template_string(HTML_TEMPLATE,
repo_name=repo_name, view="tree", tree_items=items, breadcrumb=breadcrumb)
@app.route("//blob//")
def repo_blob(repo_name, blob_id, path):
repo = open_repo(repo_name)
if not repo:
abort(404)
content = get_blob_content(repo, blob_id.encode() if len(blob_id) == 40 else blob_id)
path_parts = [p for p in path.split("/") if p]
breadcrumb = build_breadcrumb(repo_name, path_parts[:-1])
return render_template_string(HTML_TEMPLATE,
repo_name=repo_name, view="blob", blob_path=path, blob_content=content, breadcrumb=breadcrumb)
@app.route("//log")
def repo_log(repo_name):
repo = open_repo(repo_name)
if not repo:
abort(404)
commits = get_commits(repo)
breadcrumb = [{"name": "log", "url": None}]
return render_template_string(HTML_TEMPLATE,
repo_name=repo_name, view="log", commits=commits, breadcrumb=breadcrumb)
@app.route("//commit/")
def repo_commit(repo_name, commit_id):
repo = open_repo(repo_name)
if not repo:
abort(404)
try:
commit = repo[commit_id.encode() if len(commit_id) == 40 else commit_id]
except:
abort(404)
diff = get_commit_diff(repo, commit.id)
commit_info = {
"id": commit.id.decode(),
"message": commit.message.decode().strip(),
"author": commit.author.decode(),
"date": datetime.fromtimestamp(commit.commit_time).strftime("%Y-%m-%d %H:%M:%S")
}
breadcrumb = [{"name": "log", "url": f"/{repo_name}/log"}, {"name": commit_id[:8], "url": None}]
return render_template_string(HTML_TEMPLATE,
repo_name=repo_name, view="commit", commit=commit_info, commit_diff=diff, breadcrumb=breadcrumb)
def scan_and_fix_repos():
while True:
time.sleep(60)
if not OPENROUTER_API_KEY:
continue
for repo_name in list_repos():
try:
fix_repo_sorry_todo(repo_name)
except Exception as e:
print(f"Error fixing {repo_name}: {e}", file=sys.stderr)
def fix_repo_sorry_todo(repo_name):
repo = open_repo(repo_name)
if not repo:
return
head = repo.head()
try:
head_commit = repo[head]
tree_id = head_commit.tree
except:
return
files_to_fix = []
collect_files(repo, tree_id, "", files_to_fix)
if not files_to_fix:
return
for file_path, blob_id in files_to_fix:
content = get_blob_content(repo, blob_id)
if "sorry" not in content.lower() and "todo" not in content.lower():
continue
fixed_content = call_openrouter(file_path, content)
if fixed_content and fixed_content != content:
commit_fix(repo, repo_name, file_path, blob_id, fixed_content, head_commit)
def collect_files(repo, tree_id, prefix, result):
tree = repo[tree_id]
for entry in tree.items():
obj = repo[entry.sha]
path = f"{prefix}/{entry.path.decode()}" if prefix else entry.path.decode()
if isinstance(obj, Tree):
collect_files(repo, entry.sha, path, result)
elif isinstance(obj, Blob):
result.append((path, entry.sha))
def call_openrouter(file_path, content):
prompt = f"""Fix the following code file. Replace any 'sorry' or 'TODO' comments with proper implementations.
Only output the fixed file content, no explanations, no markdown.
File: {file_path}
```{content}```"""
headers = {
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json"
}
data = {
"model": OPENROUTER_MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"max_tokens": 8192
}
try:
resp = requests.post(OPENROUTER_URL, headers=headers, json=data, timeout=60)
resp.raise_for_status()
result = resp.json()
fixed = result["choices"][0]["message"]["content"]
if fixed.startswith("```") and fixed.endswith("```"):
lines = fixed.split("\n")
fixed = "\n".join(lines[1:-1])
return fixed
except Exception as e:
print(f"OpenRouter error: {e}", file=sys.stderr)
return None
def commit_fix(repo, repo_name, file_path, old_blob_id, new_content, parent_commit):
tree_id = parent_commit.tree
tree = repo[tree_id]
path_parts = file_path.split("/")
new_tree_id = update_tree_recursive(repo, tree, path_parts, 0, new_content.encode())
author = f"Auto Fix ".encode()
commit = Commit()
commit.tree = new_tree_id
commit.parents = [parent_commit.id]
commit.author = author
commit.committer = author
commit.commit_time = int(time.time())
commit.commit_timezone = 0
commit.encoding = b"UTF-8"
commit.message = f"Auto-fix: resolve sorry/TODO in {file_path}\n".encode()
repo.object_store.add_object(commit)
repo.refs[b"refs/heads/master"] = commit.id
print(f"Committed fix for {file_path} in {repo_name}: {commit.id.decode()[:8]}")
def update_tree_recursive(repo, tree, path_parts, index, new_content):
if index == len(path_parts) - 1:
blob = Blob.from_string(new_content)
repo.object_store.add_object(blob)
new_items = list(tree.items())
for i, entry in enumerate(new_items):
if entry.path.decode() == path_parts[index]:
new_items[i] = entry._replace(sha=blob.id, mode=0o100644)
break
else:
from dulwich.objects import TreeEntry
new_items.append(TreeEntry(0o100644, path_parts[index].encode(), blob.id))
new_tree = Tree()
for entry in new_items:
new_tree.add(entry.path, entry.mode, entry.sha)
repo.object_store.add_object(new_tree)
return new_tree.id
subtree_id = None
for entry in tree.items():
if entry.path.decode() == path_parts[index]:
subtree_id = entry.sha
break
if not subtree_id:
return tree.id
subtree = repo[subtree_id]
new_subtree_id = update_tree_recursive(repo, subtree, path_parts, index + 1, new_content)
new_items = list(tree.items())
for i, entry in enumerate(new_items):
if entry.path.decode() == path_parts[index]:
new_items[i] = entry._replace(sha=new_subtree_id)
break
new_tree = Tree()
for entry in new_items:
new_tree.add(entry.path, entry.mode, entry.sha)
repo.object_store.add_object(new_tree)
return new_tree.id
class CombinedApp:
def __init__(self, flask_app, git_wsgi):
self.flask_app = flask_app
self.git_wsgi = git_wsgi
def __call__(self, environ, start_response):
path = environ.get("PATH_INFO", "")
if path.startswith("/git/") or path == "/git" or path.startswith("/info/refs") or path.startswith("/git-upload-pack") or path.startswith("/git-receive-pack"):
environ["PATH_INFO"] = path[4:] if path.startswith("/git") else path
return self.git_wsgi(environ, start_response)
return self.flask_app(environ, start_response)
if __name__ == "__main__":
from werkzeug.serving import run_simple
scanner_thread = threading.Thread(target=scan_and_fix_repos, daemon=True)
scanner_thread.start()
combined = CombinedApp(app, git_wsgi)
print("Starting Git server on http://localhost:7777")
print("Git HTTP endpoint: http://localhost:7777/git/.git")
run_simple("0.0.0.0", 7777, combined, use_reloader=False, threaded=True)