#!/usr/bin/env python
# encoding: utf-8
"""static
Create and/or upload a static copy of the repository.
The main goal is sharing Mercurial on servers with only FTP access and
statically served files, while providing the same information as hg
serve and full solutions like bitbucket and gitorious (naturally
without the interactivity).
"""
__plan__ = """
* Create the static-dir in the repo:
- Overview: Readme + commits + template ✔
- Changes: Commit-Log + each commit as commit/<hex> ✔
- source: a filetree, shown as sourcecode: src/<path> and raw/<path>
- if b is used: a bugtracker: issue/<id>/<name>
- fork-/clone-info for each entry in [paths] with its incoming data (if it has some):
clone/<pathname>/ → commit log + possibly an associated issue in b.
* Usage:
- hg static [--name] [-r] [folder] → parse the static folder for the current revision.
Mimic pull and clone wherever possible: This is a clone to <repo>/static
- hg static --upload <FTP-path> [folder] → update and upload the folder == clone/push
* Idea: hg clone/push ftp://host.tld/path/to/repo → hg static --upload
* Setup a new static repo or update an existing one: hg static --upload ftp://host.tld/path/to/repo
"""
import os
from os.path import join, isdir, isfile, basename
import shutil
import mercurial
from mercurial import cmdutil
from mercurial import commands
_staticidentifier = ".statichgrepo"
templates = {
"head": """<!DOCTYPE html>
<html><head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <!--duplicate for older browsers-->
<link rel="stylesheet" href="style.css" type="text/css" media="screen" />
<link rel="stylesheet" href="print.css" type="text/css" media="print" />
<title>{reponame}</title>
</head>
<body>
<h1>{reponame}</h1>
""",
"foot": "</body></html>",
"screenstyle": """ """,
"printstyle": """ """
}
def parsereadme(filepath):
"""Parse the readme file"""
with open(filepath) as r:
return "<pre>" + r.read() + "</pre>"
def writeoverview(ui, repo, target, name):
"""Create the overview page"""
overview = open(join(target, "index.html"), "w")
overview.write(templates["head"].replace("{reponame}", name))
# add a short identifier from the first line of the readme, if it
# exists # TODO: Parse different types of readme files
readme = name
for f in os.listdir(repo.root):
if f.lower().startswith("readme"):
readme = parsereadme(f)
overview.write( "\n".join(readme.splitlines()[:3]))
break
# now add the 5 most recent log entries
# divert all following ui output to a string, so we can just use standard functions
overview.write("<h2>Changes</h2>")
ui.pushbuffer()
t = cmdutil.changeset_templater(ui, repo, patch=False, diffopts=None, mapfile=None, buffered=False)
t.use_template("""<div style='float: right; padding-left: 0.5em'><em>({author|person})</em></div><strong> {date|shortdate}: <a href='commit/{node}.html'>{desc|strip|fill68|firstline}</a> <span style='font-size: xx-small'>{branches} {tags}</span><p>{desc|escape}</p>""")
for c in range(1, min(len(repo.changelog), 5)):
ctx = repo.changectx(str(-c))
t.show(ctx)
overview.write(ui.popbuffer())
# add the full readme
overview.write("<h2>Readme</h2>")
overview.write(readme)
# finish the overview
overview.write(templates["foot"])
def writelog(ui, repo, target, name):
"""Write the full changelog, in steps of 100."""
commits = join(target, "commits")
# create the folders
if not isdir(commits):
os.makedirs(commits)
for i in range(len(repo.changelog)/100):
d = commits+"-"+str(i+1)+"00"
if not isdir(d):
os.makedirs(d)
# create the log files
t = cmdutil.changeset_templater(ui, repo, patch=False, diffopts=None, mapfile=None, buffered=False)
t.use_template("""<div style='float: right; padding-left: 0.5em'><em>({author|person})</em></div><strong> {date|shortdate}: <a href='../commit/{node}.html'>{desc|strip|fill68|firstline}</a> <span style='font-size: xx-small'>{branches} {tags}</span><p>{desc|escape}</p>""")
logs = []
for ck in range(0, len(repo.changelog)/100+1):
ui.pushbuffer()
if ck:
dd = d
di = str(ck)+"00"
d = commits+"-"+di
logs[-1].write("<p><a href=\"../commits-"+di+"\">earlier</a></p>")
if ck>2:
# the older log gets a reference to the newer one
logs[-1].write("<p><a href=\"../commits-"+str(ck-2)+"00"+"\">later</a></p>")
elif ck>1:
logs[-1].write("<p><a href=\"../commits\">later</a></p>")
logs.append(open(join(d, "index.html"), "w"))
else:
d = commits
logs.append(open(join(d, "index.html"), "w"))
logs[-1].write(templates["head"].replace("{reponame}", name))
for c in range(ck*100+1, min(len(repo.changelog), (ck+1)*100)):
ctx = repo.changectx(str(-c))
t.show(ctx)
logs[-1].write(ui.popbuffer())
for l in logs:
l.write(templates["foot"].replace("{reponame}", name))
l.close()
def writecommits(ui, repo, target, name, force=False):
"""Write all not yet existing commit files."""
commit = join(target, "commit")
# create the folders
if not isdir(commit):
os.makedirs(commit)
t = cmdutil.changeset_templater(ui, repo, patch=False, diffopts=None, mapfile=None, buffered=False)
t.use_template("""<div style='float: right; padding-left: 0.5em'><em>({author|person})</em></div><strong> {date|shortdate}: <a href='../commit/{node}.html'>{desc|strip|fill68|firstline}</a> <span style='font-size: xx-small'>{branches} {tags}</span><p>{desc|escape}</p>""")
for c in range(len(repo.changelog)):
ctx = repo.changectx(str(c))
cpath = join(commit, ctx.hex() + ".html")
if not force and isfile(cpath):
continue
with open(cpath, "w") as cf:
cf.write(templates["head"].replace("{reponame}", name))
ui.pushbuffer()
t.show(ctx)
cf.write(ui.popbuffer())
ui.pushbuffer()
commands.diff(ui, repo, change=str(c), git=True)
cf.write("<pre>"+ui.popbuffer().replace("<", "<")+"</pre>")
cf.write(templates["foot"].replace("{reponame}", name))
def parsesite(ui, repo, target, **opts):
"""Create the static folder."""
idfile = join(target, _staticidentifier)
if not isdir(target):
# make sure the target exists
os.makedirs(target)
else: # make sure it is a staticrepo
if not isfile(idfile):
if not ui.prompt("The target folder exists is no static repo. Really use it?", default="n").lower() in ["y", "yes"]:
return
with open(idfile, "w") as i:
i.write("")
if opts["name"]:
name = opts["name"]
elif target != "static": name = target
else: name = basename(repo.root)
# first the stylesheets
screenstyle = opts["screenstyle"]
if screenstyle:
shutil.copyfile(screenstyle, join(target, "style.css"))
else:
with open(join(target, "style.css"), "w") as f:
f.write(templates["screenstyle"])
printstyle = opts["printstyle"]
if printstyle:
shutil.copyfile(printstyle, join(target, "print.css"))
else:
with open(join(target, "print.css"), "w") as f:
f.write(templates["printstyle"])
# then the overview
writeoverview(ui, repo, target, name)
# and the log
writelog(ui, repo, target, name)
# and all commit files
writecommits(ui, repo, target, name, force=opts["force"])
def addrepo(ui, repo, target):
"""Add the repo to the target and make sure it is up to date."""
try:
commands.init(ui, dest=target)
except mercurial.error.RepoError, e:
# already exists
pass
ui.pushbuffer()
commands.push(ui, repo, dest=target)
ui.popbuffer()
def static(ui, repo, target=None, **opts):
"""Create a static copy of the repository and/or upload it to an FTP server."""
if repo.root == target:
ui.warn("static target repo can’t be the current repo")
return
# first: just create the site.
if target is None: target = "static"
parsesite(ui, repo, target, **opts)
# add the hg repo to the static site
addrepo(ui, repo, target)
cmdtable = {
# "command-name": (function-call, options-list, help-string)
"static": (static,
[('r', 'rev', None, 'parse the given revision'),
('a', 'all', None, 'parse all revisions (requires much space)'),
('n', 'name', None, 'the repo name. Default: folder or last segment of the repo-path.'),
('u', 'upload', None, 'upload the repo'),
('f', 'force', False, 'force recreating all commit files. Slow.'),
('s', 'screenstyle', None, 'use a custom stylesheet for display on screen'),
('p', 'printstyle', None, 'use a custom stylesheet for printing')],
"[options] [folder]")
}