Luke Ross

Colorado

4 releases git clone https://lukeross.name/projects/colorado.git/

Web-based git repository viewer.

Commit 7e7fd22845cec4c80bf5f9feacc3a51a6f10c7b4

Work so far

Committed 1 Jul 2017 by Luke Ross

.gitignore

@@ -0,0 +1,2 @@
+*.sqlite
+*.pyc


colorado_quickstart.py

@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+
+from flask import Flask
+
+from colorado.db import init_db, set_engine_uri
+from colorado.views import bp
+
+app = Flask(__name__)
+app.register_blueprint(bp, url_prefix="/c")
+
+set_engine_uri("sqlite:///quickstart.sqlite")
+init_db()
+app.run(debug=True)


setup.py

@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+from setuptools import setup
+
+setup(
+	name="Colorado",
+	version="0.1",
+	description="Colorado git browser",
+	author="Luke Ross",
+	author_email="luke@lukeross.name",
+	url="https://lukeross.name/",
+	install_requires=[
+		"flask",
+		"flask-sqlalchemy",
+		"gitpython",
+		"lxmlmeld",
+		"ponyalchemy",
+		"python-dateutil"
+	],
+	packages=["colorado"],
+	package_dir={"": "src"},
+	package_data={
+		"colorado": [
+			"static/*",
+			"templates/*.xml"
+		]
+	},
+	scripts=[
+		"colorado_quickstart.py"
+	],
+	license="proprietary"
+)


src/colorado/db.py

@@ -0,0 +1,24 @@
+from ponyalchemy import declarative_base, sessionmaker
+from sqlalchemy import create_engine
+from sqlalchemy.orm import scoped_session
+
+Base = declarative_base()
+
+session = scoped_session(sessionmaker(
+	autocommit=False,
+	autoflush=False,
+))
+
+
+def set_engine_uri(uri):
+	"""
+	Select which database the app will use
+	"""
+	session.configure(bind=create_engine(uri))
+
+
+def init_db():
+	"""
+	Set up the database and create any missing tables
+	"""
+	Base.metadata.create_all(session.bind)


src/colorado/repo.py

@@ -0,0 +1,42 @@
+import unicodedata as ud
+from git import Repo as GitRepo
+from sqlalchemy import Boolean, Column, Integer, Unicode
+
+from .db import Base
+
+
+def _make_slug(self):
+	name = context.current_parameters["name"]
+	return u"".join(
+		l if ud.category(l) in ("Lu", "Ll") else "_"
+		for l in name
+	).lower()
+
+
+class Repo(Base):
+	"""
+	A git repo
+	"""
+	__tablename__ = "repo"
+	id = Column(Integer, primary_key=True)
+	name = Column(Unicode)
+	slug = Column(Unicode, unique=True, default=_make_slug)
+	path = Column(Unicode, unique=True)
+	private = Column(Boolean, default=False)
+	description = Column(Unicode)
+
+	@property
+	def repo(self):
+		return GitRepo(self.path)
+
+	@classmethod
+	def public_repos(cls):
+		return (r for r in cls if not r.private)
+
+	@classmethod
+	def all_repos(cls):
+		return (r for r in cls)
+
+	@classmethod
+	def find_by_slug(cls, slug):
+		return (r for r in cls if r.slug == slug)


src/colorado/templates/index.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:meld="http://www.plope.com/software/meld3">
+
+<head>
+<meta name="viewport" content="width=device-width" />
+<title meld:id="html-title">Repository</title>
+<link href="#" meld:id="atom-feed" type="application/atom+xml" />
+<style type="text/css">
+<!--
+#repo-list { width: 100% }
+th { text-align: left }
+// -->
+</style>
+</head>
+
+<body>
+<h1>LukeRoss.name</h1>
+<h2>Repositories</h2>
+
+<table id="repo-list">
+<tr meld:id="repo">
+<th><a href="#" meld:id="repo-name">Colorado</a></th>
+<td meld:id="repo-desc">Description</td>
+<td meld:id="repo-updated">2017-06-01 12:34:45</td>
+</tr>
+</table>
+
+</body>
+</html>


src/colorado/templates/repo-home.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:meld="http://www.plope.com/software/meld3">
+
+<head>
+<meta name="viewport" content="width=device-width" />
+<title meld:id="html-title">Repository</title>
+<link href="#" meld:id="atom-feed" type="application/atom+xml" />
+<style type="text/css">
+<!--
+#file-list { width: 100% }
+#readme-box { border: solid thin #808080 }
+#readme-box h3 { margin-top: 0 }
+// -->
+</style>
+</head>
+
+<body>
+<h1>LukeRoss.name</h1>
+<h2 meld:id="repo-name">Repository</h2>
+
+<p meld:id="repo-desc">
+Brief introduction to the repo.
+</p>
+
+<div meld:id="toolbar">
+<select name="versions">
+<option meld:id="branch-dropdown">master</option>
+</select>
+<button href="#" meld:id="repo-commits">20 commits</button>
+<button href="#" meld:id="repo-issues">5 issues</button>
+<button href="#" meld:id="repo-releases">Download foo-master-20170612.tar.gz</button>
+<button href="#" meld:id="repo-clone">Clone http://example.com/clone-me</button>
+</div>
+
+<table id="file-list">
+<tr meld:id="file">
+<td meld:id="file-icon">.</td>
+<td meld:id="file-name">README.txt</td>
+<td meld:id="file-size">1.2k</td>
+<td meld:id="file-revdesc"><a href="#">some change</a></td>
+<td meld:id="file-when">2017-06-01 12:34:45</td>
+</tr>
+</table>
+
+<div id="readme-box">
+<h3 meld:id="readme-filename">README.txt</h3>
+<div meld:id="readme">
+This is the contents of README.txt
+</div>
+</div>
+
+</body>
+</html>


src/colorado/views.py

@@ -0,0 +1,88 @@
+from flask import abort, Blueprint, url_for
+from lxmlmeld import parse_xml
+from os import path
+from sqlalchemy.orm.exc import NoResultFound
+
+from .db import session
+from .repo import Repo
+
+bp = Blueprint(
+	"coloraro", __name__, static_folder="static", template_folder="templates"
+)
+
+doctype = (
+	"html", "-//W3C//DTD XHTML 1.0 Strict//EN",
+	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
+)
+
+
+def parse_xml_for_template(template_name, using=bp):
+	"""
+	Method: parse_xml_for_template
+
+	Opens and parses an XML template, looked up as a filename
+	relative to the Blueprint/Flask object provided.
+
+	Parameters:
+
+		template_name - The XML template filename to use
+		using - The Blueprint or Flask to check for templates
+
+	Returns:
+
+		An lxml Element
+	"""
+	return parse_xml(path.join(
+		path.dirname(path.abspath(__file__)),
+		using.template_folder,
+		"{}.xml".format(template_name)
+	))
+
+
+def write_template(doc):
+	"""
+	Method: write_template
+
+	Formats an lxml Element as an XHTML unicode object.
+
+	Parameters:
+
+		doc - An lxml Element
+
+	Returns:
+
+		A unicode string
+	"""
+	return doc.write_xhtmlstring(
+		doctype=doctype,
+		declaration=True,
+		encoding="UTF-8"
+	)
+
+
+@bp.route("/", methods=["GET"])
+def index():
+	"""
+	Start the app
+	"""
+	repos = session().run(Repo.public_repos())
+	tpl = parse_xml_for_template("index")
+	for ele, repo in tpl.findmeld("repo").repeat(repos):
+		ele.findmeld("repo-name").content(repo.name)
+		ele.findmeld("repo-name").set("href", url_for(".repo_home", slug=repo.slug))
+		ele.findmeld("repo-desc").content(repo.description)
+		ele.findmeld("repo-updated").content("")
+	return write_template(tpl)
+
+
+@bp.route("/<slug>/", methods=["GET"])
+def repo_home(slug):
+	try:
+		repo = session().run(Repo.find_by_slug(slug)).one()
+	except NoResultFound:
+		abort(403)
+
+	tpl = parse_xml_for_template("repo-home")
+	tpl.findmeld("repo-name").content(repo.name)
+	tpl.findmeld("repo-desc").content(repo.description)
+	return write_template(tpl)