- #!/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/<hex>/<path> and raw/<hex>/<path>
- as HTML (src) and as plain files (raw)
- file-links in revision infos: link to the latest change in the file.
- → manifest ⇒ filelog ⇒ revision before the current.
- src/<hex>/index.html: manifest with file-links. ✔
- - Upload it to ftp ✔
-
- - Add a list of branches, heads and tags to the summary page.
- - 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>/ → incoming log (commits) + possibly an associated issue in b.
- - More complex Readme parsing.
- - add linenumbers to the src files.
- - add sourcecode coloring to the src files.
- - Treat branch heads specially: link on the main page.
-
- * 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, dirname
- import shutil
- import re
- import mercurial
- import ftplib
- import socket
- import datetime
- 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>
- """,
- "srchead": """<!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>{filetitle}</title>
- </head>
- <body>
- """,
- "foot": "</body></html>",
- "screenstyle": """ """,
- "printstyle": """ """,
- "manifesthead": """<h2>Commit: <a href='../../commit/{hex}.html'>{hex}</a></h2>
- <p>{desc}</p><p>{user}</p>
- <h2>Files in this revision</h2>"""
-
- }
-
- _indexregexp = re.compile("^\\.*index.html$")
-
-
- 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 the links to the log and the files.
- overview.write("<p><a href='commits'>changelog</a> | <a href='src/" + repo["tip"].hex() + "/'>files</a></p>")
- # 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 (<a href='commits'>full changelog</a>)</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}", "<a href='../'>"+name+"</a>"))
- 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}", "<a href='../'>"+name+"</a>"))
- 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='../src/{node}/'>{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}", "<a href='../'>"+name+"</a>"))
- 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}", "<a href='../'>"+name+"</a>"))
-
-
- def escapename(filename):
- """escape index.html as .index.html and .ind… as ..ind… and so fort."""
- if _indexregexp.match(filename) is not None:
- return "." + filename
- else: return filename
-
-
- def parsesrcdata(data):
- """Parse a src file into a html file."""
- return "<pre>"+data.replace("<", "<")+"</pre>"
-
- def srcpath(target, ctx, filename):
- """Get the relative path to the static sourcefile for an already escaped filename."""
- return join(target,"src",ctx.hex(),filename+".html")
-
- def rawpath(target, ctx, filename):
- """Get the relative path to the static sourcefile for an already escaped filename."""
- return join(target,"raw",ctx.hex(),filename)
-
- def createindex(target, ctx):
- """Create an index page for the changecontext: the commit message + the user + all files in the changecontext."""
- # first the head
- index = templates["manifesthead"].replace(
- "{hex}", ctx.hex()).replace(
- "{desc}", ctx.description()).replace(
- "{user}", ctx.user())
- # then the files
- index += "<ul>"
- for filename in ctx:
- filectx = ctx[filename]
- lasteditctx = filectx.filectx(filectx.filerev())
- index += "<li><a href='../../"+ join("src",lasteditctx.hex(), escapename(filename)+".html") + "'>" + filename + "</a>"# (<a href='../../" + join("raw",lasteditctx.hex(), filename) + "'>raw</a>)</li>"
- index += "</ul>"
- return index
-
- def writesourcetree(ui, repo, target, name, force):
- """Write manifests for all commits and websites for all files.
-
- * For each file, write sites for all revisions where the file was changed: under src/<hex>/path as html site (with linenumbers and maybe colored source), under raw/<hex>/<path> as plain files. If there is an index.html file, write it as .index.html. If there also is .index.html, turn it to ..index.html, …
- * For each commit write an index with links to the included files at their latest revisions before/at the commit.
- """
- # first write all files in all commits.
- for c in range(len(repo.changelog)):
- ctx = repo.changectx(str(c))
- for filename in ctx.files():
- filectx = ctx[filename]
- # first write the raw data
- filepath = rawpath(target,ctx,filectx.path())
- # skip already existing files
- if not force and isfile(filepath):
- continue
- try:
- os.makedirs(dirname(filepath))
- except OSError: pass # exists
- with open(filepath, "w") as f:
- f.write(filectx.data())
- # then write it as html
- _filenameescaped = escapename(filectx.path())
- filepath = srcpath(target,ctx,_filenameescaped)
- try:
- os.makedirs(dirname(filepath))
- except OSError: pass # exists
- with open(filepath, "w") as f:
- f.write(templates["srchead"].replace("{filetitle}", name+": " + filename))
- f.write(parsesrcdata(filectx.data()))
- f.write(templates["foot"].replace("{reponame}", name))
- # then write manifests for all commits
- for c in range(len(repo.changelog)):
- ctx = repo.changectx(str(c))
- filepath = join(target,"src",ctx.hex(),"index.html")
- # skip already existing files
- if not force and isfile(filepath):
- continue
- try:
- os.makedirs(dirname(filepath))
- except OSError: pass # exists
- with open(filepath, "w") as f:
- f.write(templates["head"].replace("{reponame}", "<a href='../../'>"+name+"</a>"))
- f.write(createindex(target, ctx))
- f.write(templates["foot"].replace("{reponame}", "<a href='../../'>"+name+"</a>"))
-
-
- 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"])
-
- # and all file data
- writesourcetree(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 upload(ui, repo, target, ftpstring, force):
- """upload the repo to the FTP server identified by the ftp string."""
- user, password = ftpstring.split("@")[0].split(":")
- serverandpath = "@".join(ftpstring.split("@")[1:])
- server = serverandpath.split("/")[0]
- ftppath = "/".join(serverandpath.split("/")[1:])
- timeout = 10
- try:
- ftp = ftplib.FTP(server, user, password, "", timeout)
- except socket.timeout:
- ui.warn("connection to ", server, " timed out after ", timeout, " seconds.\n")
- return
-
- ui.status(ftp.getwelcome(), "\n")
-
- # create the target dir.
- serverdir = dirname(ftppath)
- serverdirparts = ftppath.split("/")
- sd = serverdirparts[0]
- if not sd in ftp.nlst():
- ftp.mkd(sd)
- for sdp in serverdirparts[1:]:
- sdo = sd
- sd = os.path.join(sd, sdp)
- if not sd in ftp.nlst(sdo):
- ftp.mkd(sd)
-
-
- ftp.cwd(ftppath)
- if not ftp.pwd() == "/" + ftppath:
- ui.warn("not in the correct ftp directory. Cowardly bailing out.\n")
- return
-
- #ftp.dir()
- #return
- ftpfeatures = ftp.sendcmd("FEAT")
-
- _ftpdircache = set()
-
- for d, dirnames, filenames in os.walk(target):
- for filename in filenames:
- localfile = join(d, filename)
- serverfile = localfile[len(target)+1:]
- serverdir = dirname(serverfile)
- serverdirparts = serverdir.split("/")
- # print serverdirparts, serverfile
- with open(localfile, "rb") as f:
- sd = serverdirparts[0]
- if sd and not sd in _ftpdircache and not sd in ftp.nlst():
- try:
- ui.status("creating directory ", sd, "\n")
- ftp.mkd(sd)
- _ftpdircache.add(sd)
- except ftplib.error_perm, resp:
- ui.warn("could not create directory ", sd, ": " , resp, "\n")
- else: _ftpdircache.add(sd)
-
- for sdp in serverdirparts[1:]:
- sdold = sd
- sd = join(sd, sdp)
- #print sd, sdp
- #print ftp.nlst(sdold)
- if sd and not sd in _ftpdircache and not sd in ftp.nlst(sdold):
- try:
- ui.status("creating directory ", sd, "\n")
- ftp.mkd(sd)
- _ftpdircache.add(sd)
- except ftplib.error_perm, resp:
- ui.warn("could not create directory ", sd, ": " , resp, "\n")
- else: _ftpdircache.add(sd)
-
-
- if not serverfile in ftp.nlst(serverdir) or force:
- if force:
- ui.status("uploading ", serverfile, " because I am forced to.\n")
- else:
- ui.status("uploading ", serverfile, " because it is not yet online.\n")
-
- ftp.storbinary("STOR "+ serverfile, f)
- else:
- # reupload the file if the file on the server is older than the local file.
- if " MDTM" in ftpfeatures.splitlines():
- ftpmtime = ftp.sendcmd("MDTM " + serverfile).split()[1]
- localmtime = os.stat(localfile).st_mtime
- localmtimestr = datetime.datetime.utcfromtimestamp(localmtime).strftime("%Y%m%d%H%M%S")
- newer = int(localmtimestr) > int(ftpmtime)
- if newer:
- ui.status("uploading ", serverfile, " because it is newer than the file on the FTP server.\n")
- ftp.storbinary("STOR "+ serverfile, f)
-
-
-
- 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 not target: target = "static"
- parsesite(ui, repo, target, **opts)
- # add the hg repo to the static site
- addrepo(ui, repo, target)
- if opts["upload"]:
- # upload the repo
- upload(ui, repo, target, opts["upload"], opts["force"])
-
-
-
- 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', "", 'the repo name. Default: folder or last segment of the repo-path.'),
- ('u', 'upload', "", 'upload the repo to the given ftp host. Format: user:password@host/path/to/dir'),
- ('f', 'force', False, 'force recreating all commit files. Slow.'),
- ('s', 'screenstyle', "", 'use a custom stylesheet for display on screen'),
- ('p', 'printstyle', "", 'use a custom stylesheet for printing')],
- "[options] [folder]")
- }