GitBack-Machine / app.py
Onyxl's picture
Fix: inject HF_TOKEN secret for dataset writes + fix cache path separators
26d26dc verified
Raw
History Blame Contribute Delete
9.18 kB
import gradio as gr
import requests
import json
import os
from datetime import datetime
import io
from huggingface_hub import HfApi
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN") or os.environ.get("HF_ACCESS_TOKEN", "")
BUCKETS = [
"GigaBites-AI-Team/gitback-cache-bucket-1",
"MegaBites-AI/gitback-cache-bucket-2",
"KiloBites-ai/gitback-cache-bucket-3",
]
def get_api():
return HfApi(token=HF_TOKEN)
def github_headers():
h = {"Accept": "application/vnd.github+json"}
if GITHUB_TOKEN:
h["Authorization"] = f"Bearer {GITHUB_TOKEN}"
return h
def hf_headers():
if HF_TOKEN:
return {"Authorization": f"Bearer {HF_TOKEN}"}
return {}
def cache_key(owner, repo, sha):
return f"{owner}__{repo}/{sha}.json"
def index_key(owner, repo):
return f"{owner}__{repo}/_index.json"
def check_cache(owner, repo, sha):
key = cache_key(owner, repo, sha)
for bucket in BUCKETS:
url = f"https://huggingface.co/datasets/{bucket}/resolve/main/{key}?raw=true"
r = requests.get(url, headers=hf_headers(), timeout=8)
if r.status_code == 200:
try:
return r.json(), bucket
except:
continue
return None, None
def get_index(owner, repo):
key = index_key(owner, repo)
primary_bucket = BUCKETS[hash(f"{owner}/{repo}") % len(BUCKETS)]
url = f"https://huggingface.co/datasets/{primary_bucket}/resolve/main/{key}?raw=true"
r = requests.get(url, headers=hf_headers(), timeout=8)
if r.status_code == 200:
try:
return r.json(), primary_bucket
except:
pass
return {}, primary_bucket
def update_index(owner, repo, sha, bucket):
index, primary_bucket = get_index(owner, repo)
if sha in index:
return
index[sha] = {"bucket": bucket, "cached_at": datetime.utcnow().isoformat()}
key = index_key(owner, repo)
try:
api = get_api()
content = json.dumps(index, indent=2).encode()
api.upload_file(
path_or_fileobj=io.BytesIO(content),
path_in_repo=key,
repo_id=primary_bucket,
repo_type="dataset",
commit_message=f"Index: {owner}/{repo}@{sha[:7]}"
)
except Exception as e:
print(f"Index update failed: {e}")
def save_to_cache(owner, repo, sha, data):
# Final dedup check
existing, existing_bucket = check_cache(owner, repo, sha)
if existing:
print(f"Dedup: {sha[:7]} already in {existing_bucket}, skipping.")
return existing_bucket
# Deterministic bucket β€” same SHA always maps to same bucket
bucket = BUCKETS[hash(sha) % len(BUCKETS)]
key = cache_key(owner, repo, sha)
try:
api = get_api()
content = json.dumps(data, indent=2).encode()
api.upload_file(
path_or_fileobj=io.BytesIO(content),
path_in_repo=key,
repo_id=bucket,
repo_type="dataset",
commit_message=f"Snapshot: {owner}/{repo}@{sha[:7]}"
)
print(f"βœ… Cached {sha[:7]} β†’ {bucket}")
update_index(owner, repo, sha, bucket)
except Exception as e:
print(f"❌ Cache write failed: {e}")
return f"error: {e}"
return bucket
def fetch_commits(owner, repo):
url = f"https://api.github.com/repos/{owner}/{repo}/commits?per_page=30"
r = requests.get(url, headers=github_headers(), timeout=10)
if r.status_code != 200:
return None, f"❌ Error: {r.json().get('message', 'Unknown error')}"
return r.json(), None
def fetch_snapshot(owner, repo, sha):
# Check cache first
cached, bucket = check_cache(owner, repo, sha)
if cached:
cached["_from_cache"] = True
cached["_cache_bucket"] = bucket
return cached
# Fetch from GitHub
r = requests.get(
f"https://api.github.com/repos/{owner}/{repo}/commits/{sha}",
headers=github_headers(), timeout=10
)
if r.status_code != 200:
return None
data = r.json()
snapshot = {
"sha": data["sha"],
"message": data["commit"]["message"],
"author": data["commit"]["author"]["name"],
"date": data["commit"]["author"]["date"],
"files": [
{
"filename": f["filename"],
"status": f["status"],
"additions": f.get("additions", 0),
"deletions": f.get("deletions", 0),
"patch": f.get("patch", "")[:2000]
}
for f in data.get("files", [])
],
"_from_cache": False,
}
bucket = save_to_cache(owner, repo, sha, snapshot)
snapshot["_cache_bucket"] = bucket
return snapshot
def search_repo(repo_input):
repo_input = repo_input.strip().strip("/")
parts = repo_input.replace("https://github.com/", "").split("/")
if len(parts) < 2:
return gr.update(choices=[], value=None), "⚠️ Enter a valid repo like `owner/repo`", ""
owner, repo = parts[0], parts[1]
commits, err = fetch_commits(owner, repo)
if err:
return gr.update(choices=[], value=None), err, ""
choices = [
f"{c['sha'][:7]} β€” {c['commit']['message'][:60]} ({c['commit']['author']['name']})"
for c in commits
]
shas = [c["sha"] for c in commits]
return (
gr.update(choices=choices, value=None),
f"βœ… Found **{len(commits)}** recent commits for `{owner}/{repo}`",
json.dumps({"owner": owner, "repo": repo, "shas": shas})
)
def view_snapshot(selection, state_json):
if not selection or not state_json:
return "Select a commit above to view its snapshot."
state = json.loads(state_json)
idx = [s[:7] for s in state["shas"]].index(selection[:7])
sha = state["shas"][idx]
owner, repo = state["owner"], state["repo"]
snap = fetch_snapshot(owner, repo, sha)
if not snap:
return "❌ Could not fetch snapshot."
if snap.get("_from_cache"):
source = f"⚑ **Served from cache** (`{snap.get('_cache_bucket', '')}`) β€” instant load!"
else:
source = f"🌐 **Fetched live from GitHub** β†’ saved to `{snap.get('_cache_bucket', 'cache')}` (dedup guaranteed)"
out = f"""## πŸ“Έ Snapshot: `{snap['sha'][:7]}`
{source}
**Message:** {snap['message']}
**Author:** {snap['author']}
**Date:** {snap['date']}
**Files changed:** {len(snap['files'])}
---
"""
for f in snap["files"][:10]:
out += f"\n### `{f['filename']}` β€” {f['status']} (+{f['additions']} / -{f['deletions']})\n"
if f["patch"]:
out += f"```diff\n{f['patch']}\n```\n"
if len(snap["files"]) > 10:
out += f"\n*...and {len(snap['files']) - 10} more files.*"
return out
TOAST_JS = """
function showScrollToast() {
const existing = document.getElementById('gitback-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'gitback-toast';
toast.innerText = 'πŸ‘‡ Scroll down to see the snapshot';
toast.style.cssText = `
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
background: #1f2937;
color: #f9fafb;
padding: 12px 24px;
border-radius: 999px;
font-size: 15px;
font-weight: 500;
box-shadow: 0 4px 20px rgba(0,0,0,0.35);
z-index: 9999;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
white-space: nowrap;
`;
document.body.appendChild(toast);
requestAnimationFrame(() => {
requestAnimationFrame(() => { toast.style.opacity = '1'; });
});
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 400);
}, 3000);
return [];
}
"""
with gr.Blocks(title="GitBack Machine", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# πŸ•°οΈ GitBack Machine
### Every GitHub commit, cached forever. Never duplicated.
Search any public repo β†’ browse commits β†’ click any snapshot to view it instantly.
Each snapshot is stored **exactly once**, assigned to a bucket by SHA hash. Concurrent requests are deduplicated automatically.
""")
with gr.Row():
repo_input = gr.Textbox(
placeholder="owner/repo or full GitHub URL",
label="GitHub Repository", scale=4
)
search_btn = gr.Button("πŸ” Search", variant="primary", scale=1)
status = gr.Markdown("")
state = gr.Textbox(visible=False)
commit_list = gr.Radio(choices=[], label="πŸ“‹ Commits β€” click to view snapshot", interactive=True)
snapshot_view = gr.Markdown("*Search a repo and select a commit to view its snapshot.*")
search_btn.click(search_repo, inputs=repo_input, outputs=[commit_list, status, state])
repo_input.submit(search_repo, inputs=repo_input, outputs=[commit_list, status, state])
commit_list.change(fn=lambda: None, inputs=[], outputs=[], js=TOAST_JS)
commit_list.change(view_snapshot, inputs=[commit_list, state], outputs=snapshot_view)
demo.launch()