Luke Ross

Colorado

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

Web-based git repository viewer.

Commit decb0409a4ae533d264ad36c26c1a5e3a19f044a

v0.1 done!

Committed 7 Jul 2017 by Luke Ross

src/colorado/static/colorado.css

@@ -5,6 +5,10 @@
 .file-box { background-color: white; margin: 1em; padding: 1em; }
 .file-box h3 { margin-top: 0; }
 .rev-id { display: inline-block; max-width: 6em; text-overflow: ellipsis; overflow: hidden; }
+#rev-diff-id { display: inline-block; margin: 0; }
+.diff-hunk { font-weight: bold; }
+.diff-add { color: #008000; }
+.diff-del { color: #800000; }
 
 /* Dead? */
 


src/colorado/templates/repo-blob.xml

@@ -10,7 +10,7 @@
 
 <body>
 <h1>LukeRoss</h1>
-<h2 meld:id="repo-name">Repository</h2>
+<h2><a href="#" meld:id="repo-name">Repository</a></h2>
 
 <div meld:id="toolbar">
 <a href="#" meld:id="repo-branches"><span>2</span> branches</a>
@@ -23,9 +23,14 @@
 Brief introduction to the repo.
 </p>
 
-<div class="file-box">
-<h3><span meld:id="file-name">filename</span> @ <a href="#" meld:id="file-rev">abc123</a></h3>
-<pre meld:id="file-content">
+<p>
+<span meld:id="tree-part"><a href="#" meld:id="tree-part-name">foo</a> / </span>
+</p>
+
+<div class="file-box" meld:id="file-container">
+<h3><a href="#" meld:id="file-container-name">README.txt</a></h3>
+<p><em meld:id="file-container-not-viewable">This file cannot be previewed.</em></p>
+<pre meld:id="file-container-content">
 content
 </pre>
 </div>


src/colorado/templates/repo-home.xml

@@ -38,9 +38,10 @@ Brief introduction to the repo.
 </tr>
 </table>
 
-<div class="file-box" meld:id="readme-container">
-<h3 meld:id="readme-filename">README.txt</h3>
-<pre meld:id="readme">
+<div class="file-box" meld:id="file-container">
+<h3><a href="#" meld:id="file-container-name">README.txt</a></h3>
+<p><em meld:id="file-container-not-viewable">This file cannot be previewed.</em></p>
+<pre meld:id="file-container-content">
 This is the contents of README.txt
 </pre>
 </div>


src/colorado/templates/repo-revision.xml

@@ -5,9 +5,7 @@
 <head>
 <meta name="viewport" content="width=device-width" />
 <title meld:id="html-title">Repository</title>
-<script type="text/javascript" src="static/moment.min.js" meld:id="static-moment"></script>
 <link href="/static/colorado.css" rel="stylesheet" meld:id="static-css" type="text/css" />
-<script src="/static/colorado.js" meld:id="static-js" type="text/javascript"></script>
 </head>
 
 <body>
@@ -15,30 +13,29 @@
 <h2><a href="#" meld:id="repo-name">Repository</a></h2>
 
 <div meld:id="toolbar">
-<button class="click-button" href="#" meld:id="rev-browse">Browse</button>
-<button class="click-button" href="#" meld:id="rev-patch">Download</button>
+<a href="#" meld:id="repo-branches"><span>2</span> branches</a>
+<a href="#" meld:id="repo-tags"><span>5</span> releases</a>
+<a href="#" meld:id="repo-issues"><span>5</span> issues</a>
+<span meld:id="repo-clone">http://example.com/clone-me</span>
 </div>
 
-<div>
-<h3 style="display: inline-block; margin: 0;" title="abc123">Some revision</h3>
-<span class="dt-rel" title="2017-01-01 12:34:34">2017-01-01 12:34:34</span>
-</div>
-<p>Rest of description</p>
+<p meld:id="repo-desc">
+Brief introduction to the repo.
+</p>
+
+<h3 id="rev-diff-id">Commit <a href="#" meld:id="rev-id">abc123</a></h3>
+<p><em>Committed <a href="#" meld:id="rev-when" title="2017-01-01 12:34:34">2017-01-01 12:34:34</a> by
+<span meld:id="rev-author">Author</span></em></p>
+<p meld:id="rev-desc">Description</p>
 
 
 <div class="file-box" meld:id="file">
-<h3 meld:id="file-name"><span meld:id="file-from">filename</span> -> <span meld:id="file-to">filename</span></h3>
+<h3 meld:id="file-name"><span meld:id="file-change"><span meld:id="file-from">filename</span> -> </span><a href="#" meld:id="file-to">filename</a></h3>
+<p meld:id="file-message"><em>Information</em></p>
 <pre meld:id="file-diff">
 <span meld:id="file-diff-line">diff</span>
 </pre>
 </div>
 
-<script type="text/javascript">
-<!--
-bindClickButtons();
-setTimes();
-// -->
-</script>
-
 </body>
 </html>


src/colorado/views.py

@@ -1,5 +1,7 @@
 import arrow
-from flask import abort, Blueprint, send_file, url_for
+import chardet
+from flask import abort, Blueprint, g, send_file, url_for
+from io import BytesIO
 from itertools import chain
 from lxmlmeld import parse_xml
 from os import path
@@ -20,10 +22,16 @@ doctype = (
 )
 
 
+def nice_author(c):
+	if c.author == c.committer:
+		return c.author.name
+	else:
+		return "{} & {}".format(c.author.name, c.committer.name)
+
+
 def nice_size(num):
 	suffixes = ("", "k", "M", "G", "T")
 	for offset, _ in enumerate(suffixes):
-		print(num)
 		if num > 1023:
 			num /= 1024
 		else:
@@ -173,6 +181,11 @@ def repo_home(slug):
 	return top_level_view(slug, "branch", repo.get_master().name)
 
 
+@bp.route("/<slug>/commit/<id>/diff/raw", methods=["GET"])
+def raw_diff_view(slug, id):
+	repo, point, commit = look_up_thing(slug, "commit", id)
+
+
 @bp.route("/<slug>/commit/<id>/diff", methods=["GET"])
 def diff_view(slug, id):
 	repo, point, commit = look_up_thing(slug, "commit", id)
@@ -182,19 +195,60 @@ def diff_view(slug, id):
 		diffs = commit.parents[0].diff(commit, create_patch=True)
 	else:
 		diffs = commit.diff(create_patch=True)
+	tpl.findmeld("rev-id").content(commit.hexsha)
+	tpl.findmeld("rev-id").set("href", url_for(
+		".raw_diff_view", slug=slug, id=id
+	))
+	tpl.findmeld("rev-when").content(
+		arrow.get(commit.committed_datetime).humanize()
+	)
+	tpl.findmeld("rev-when").set("href", url_for(
+		".history_view", slug=slug, type="commit", id=id
+	))
+	tpl.findmeld("rev-when").set(
+		"title", commit.committed_datetime.isoformat(" ")
+	)
+	tpl.findmeld("rev-desc").content(commit.message)
+	tpl.findmeld("rev-author").content(nice_author(commit))
 
 	for ele, diff in tpl.findmeld("file").repeat(diffs):
-		ele.findmeld("file-diff").content(diff.diff)
+		message = None if diff.diff else "No content change"
 		if not diff.b_path:
 			ele.findmeld("file-from").content(diff.a_path)
 			ele.findmeld("file-to").content("(removed)")
+			ele.findmeld("file-to").tag = "em"
+			ele.findmeld("file-to").attributes.pop("href")
 		elif diff.a_path and diff.a_path != diff.b_path:
 			ele.findmeld("file-from").content(diff.a_path)
 			ele.findmeld("file-to").content(diff.b_path)
 			if not diff.diff:
-				ele.findmeld("file-diff").content("(file renamed)")
+				message = "File removed"
 		else:
-			ele.findmeld("file-name").content(diff.b_path)
+			ele.findmeld("file-change").deparent()
+			ele.findmeld("file-to").content(diff.b_path)
+			ele.findmeld("file-to").set("href", url_for(
+				".tree_view", slug=slug, type="commit", id=id, path=diff.b_path
+			))
+
+		if not message:
+			for holder, line in ele.findmeld("file-diff-line").repeat(diff.diff.split(b"\n")):
+				try:
+		 			holder.content(line + b"\n")
+				except ValueError:
+					message = "This diff cannot be displayed"
+					break
+				if line.startswith(b"+"):
+					holder.set("class", "diff-add")
+				elif line.startswith(b"-"):
+					holder.set("class", "diff-del")
+				elif line.startswith(b"@"):
+					holder.set("class", "diff-hunk")
+			else:
+				ele.findmeld("file-message").deparent()
+
+		if message:
+			ele.findmeld("file-message")[0].content(message)
+			ele.findmeld("file-diff").deparent()
 	return write_template(tpl)
 
 
@@ -210,6 +264,7 @@ def history_view(slug, type, id):
 		ele.findmeld("rev-desc").set("href", url_for(
 			".diff_view", slug=slug, id=c.hexsha
 		))
+		ele.findmeld("rev-author").content(nice_author(c))
 		ele.findmeld("rev-id").content(c.hexsha)
 		ele.findmeld("rev-id").set("href", url_for(
 			".top_level_view", slug=slug, type="commit", id=c.hexsha
@@ -233,6 +288,74 @@ def history_view(slug, type, id):
 	return write_template(tpl)
 
 
+def configure_breadcrumbs(tpl, slug, type, id, commit, path_parts):
+	curr = commit.tree
+	items = chain(
+			[(id, False)],
+			((p, True) for p in path_parts if p)
+	)
+	for ele, (text, is_part) in tpl.findmeld("tree-part").repeat(items):
+		ele = ele.findmeld("tree-part-name")
+		ele.content(text)
+		if is_part:
+			curr = curr.join(text)
+			ele.set("href", url_for(
+				".tree_view", slug=slug, type=type, id=id, path=curr.path
+			))
+		else:
+			ele.set("href", url_for(
+				".top_level_view", slug=slug, type=type, id=id
+			))
+
+
+def render_blob(ele, blob, dl_link):
+	whitelist = ("application/json", "application/javascript")
+	cannot_render = None
+	if not(blob.mime_type.startswith("text/") or blob.mime_type in whitelist):
+		cannot_render = "MIME type {} is not previewable".format(blob.mime_type)
+	elif blob.size > (1024 * 1024):
+		cannot_render = "Content too large"
+	data = None
+	if not cannot_render:
+		data = blob.data_stream.read()
+		encoding = chardet.detect(data)
+		if 'encoding' in encoding and encoding.get('confidence', 0) > 0.7:
+			try:
+				data = data.decode(encoding['encoding'])
+			except UnicodeDecodeError as e:
+				cannot_render = "Failed to convert to text"
+		else:
+			cannot_render = "Failed to guess content encoding"
+	ele.findmeld("file-container-name").content(blob.name)
+	ele.findmeld("file-container-name").set("href", dl_link)
+	if not cannot_render:
+		try:
+			ele.findmeld("file-container-content").content(data)
+		except ValueError as e:
+			cannot_render = "Not XML safe"
+		else:
+			ele.findmeld("file-container-not-viewable").deparent()
+
+	if cannot_render:
+		ele.findmeld("file-container-content").deparent()
+		ele.findmeld("file-container-not-viewable").set("title", cannot_render)
+
+
+@bp.route("/<slug>/<type>/<id>/raw/<path:path>", methods=["GET"])
+def raw_view(slug, type, id, path):
+	repo, point, commit = look_up_thing(slug, type, id)
+	try:
+		blob = commit.tree[path]
+	except KeyError:
+		abort(404)
+
+	if blob.type == "tree":
+		abort(404)
+
+	# blob goes out of scope too quickly!
+	return send_file(BytesIO(blob.data_stream.read()), blob.mime_type, True, blob.name)
+
+
 @bp.route("/<slug>/<type>/<id>/tree/<path:path>", methods=["GET"])
 def tree_view(slug, type, id, path):
 	repo, point, commit = look_up_thing(slug, type, id)
@@ -242,15 +365,13 @@ def tree_view(slug, type, id, path):
 		abort(404)
 
 	if blob.type == "tree":
-		return tree_base_view(slug, type, id, repo, commit, blob, False, False)
+		return tree_base_view(slug, type, id, repo, commit, blob, False)
 
 	tpl = parse_xml_for_template("repo-blob")
 	configure_template(tpl, repo=repo)
-	tpl.findmeld("file-name").content(blob.path)
-	tpl.findmeld("file-content").content(blob.data_stream.read().decode("utf-8"))
-	tpl.findmeld("file-rev").content(id)
-	tpl.findmeld("file-rev").set("href", url_for(
-		".top_level_view", slug=slug, type=type, id=id
+	configure_breadcrumbs(tpl, slug, type, id, commit, blob.path.split("/")[:-1])
+	render_blob(tpl, blob, url_for(
+		".raw_view", slug=slug, type=type, id=id, path=blob.path
 	))
 	return write_template(tpl)
 
@@ -259,10 +380,10 @@ def tree_view(slug, type, id, path):
 def top_level_view(slug, type, id):
 	repo, point, commit = look_up_thing(slug, type, id)
 	show_download = type in ("branch", "tag")
-	return tree_base_view(slug, type, id, repo, commit, commit.tree, True, show_download)
+	return tree_base_view(slug, type, id, repo, commit, commit.tree, show_download)
 
 
-def tree_base_view(slug, type, id, repo, commit, tree, show_readme=False, show_download=False):
+def tree_base_view(slug, type, id, repo, commit, tree, show_download=False):
 	seen = {}
 	by_file = {}
 	commit_count = 0
@@ -303,13 +424,14 @@ def tree_base_view(slug, type, id, repo, commit, tree, show_readme=False, show_d
 		tpl.findmeld("repo-download").deparent()
 		tpl.findmeld("repo-clone").deparent()
 
+	configure_breadcrumbs(tpl, slug, type, id, commit, tree.path.split("/"))
 	contents = chain(tree.trees, tree.blobs)
 	for ele, direntry in tpl.findmeld("file").repeat(contents):
-		ele.findmeld("file-name").content(direntry.name)
 		ele.findmeld("file-name").set("href", url_for(
 			".tree_view", slug=slug, type=type, id=id, path=direntry.path
 		))
 		if direntry.type == "blob":
+			ele.findmeld("file-name").content(direntry.name)
 			ele.findmeld("file-size").content(nice_size(direntry.size))
 			if direntry.hexsha in by_file:
 				file_commit = by_file[direntry.hexsha]
@@ -328,7 +450,8 @@ def tree_base_view(slug, type, id, repo, commit, tree, show_readme=False, show_d
 					"title", file_commit.committed_datetime.isoformat(" ")
 				)
 		else:
-			ele.findmeld("file-size").content("dir")
+			ele.findmeld("file-name").content(direntry.name + "/")
+			ele.findmeld("file-size").content("")
 			ele.findmeld("file-when").content("")
 			ele.findmeld("file-when").attrib.pop("title")
 			ele.findmeld("file-revdesc").deparent()
@@ -348,10 +471,11 @@ def tree_base_view(slug, type, id, repo, commit, tree, show_readme=False, show_d
 	)
 	if candidate_readmes:
 		readme = candidate_readmes[0]
-		tpl.findmeld("readme-filename").content(readme.name)
-		tpl.findmeld("readme").content(readme.data_stream.read().decode("utf-8"))
+		render_blob(tpl, readme, url_for(
+			".tree_view", slug=slug, type=type, id=id, path=readme.path
+		))
 	else:
-		tpl.findmeld("readme-container").deparent()
+		tpl.findmeld("file-container").deparent()
 	return write_template(tpl)