(Arne Babenhauserheide)
2012-10-28: merge 0.1.4 into releases. releases merge 0.1.4 into releases.
diff --git a/.bugs/bugs b/.bugs/bugs --- a/.bugs/bugs +++ b/.bugs/bugs @@ -1,23 +1,27 @@ -push all bookmarks too | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:0661fcb89dfedcd564bdb2d4865d41b58494ac2e, time:1320547191.52 -add sourcecode coloring to the src files. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:09715a67cfabe2de0901f0472610c2285626e0e7, time:1319147685.11 -Add a list of branches, heads and tags to the summary page. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:0fde104c4206be8245ff0716ee2e91ea3971db8f, time:1319147651.17 -if b is used: a bugtracker: issue/<id>/<name> | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:1d631d51ff06b3bdca50e21da3d6a00bcb801c85, time:1319147632.52 -add css classes and ids everywhere, so this can be styled with CSS. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:2699812cf02c803fa338daf9ae039c43a30a0b5f, time:1322090683.01 -get mtimes from the repo instead of querying the FTP server. We know which files were changed. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:29210503551d0eafca67dda8d6fffbd40bf837dc, time:1319213074.57 -FIX: revision 0 is omitted: change that :) | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:29396f1753e45b5a37ffa0ce04d96c876d6b6722, time:1319209563.68 -parse the pushed repo, not the local one. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:2c26d62b62e3656ebcce43e7a24f627594911fb5, time:1322115065.37 +push all bookmarks too | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:0661fcb89dfedcd564bdb2d4865d41b58494ac2e, time:1320547191.52 +add sourcecode coloring to the src files. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:09715a67cfabe2de0901f0472610c2285626e0e7, time:1319147685.11 +offer different and customizeable ways to parse a site, including to just call an external applications. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:0ed55f757a6352bc3b2674153a3dc8eda91db843, time:1332932026.35 +Add a list of branches, heads and tags to the summary page. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:0fde104c4206be8245ff0716ee2e91ea3971db8f, time:1319147651.17 +if b is used: a bugtracker: issue/<id>/<name> | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:1d631d51ff06b3bdca50e21da3d6a00bcb801c85, time:1319147632.52 +add css classes and ids everywhere, so this can be styled with CSS. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:2699812cf02c803fa338daf9ae039c43a30a0b5f, time:1322090683.01 +get mtimes from the repo instead of querying the FTP server. We know which files were changed. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:29210503551d0eafca67dda8d6fffbd40bf837dc, time:1319213074.57 +FIX: revision 0 is omitted: change that :) | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:29396f1753e45b5a37ffa0ce04d96c876d6b6722, time:1319209563.68 +parse the pushed repo, not the local one. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:2c26d62b62e3656ebcce43e7a24f627594911fb5, time:1322115065.37 +revisions more structured, as list or similar. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:39cacfc83da6f6beecafdb745823d7f01018206b, time:1322159357.88 only write .statichgrepo print.css style.css and index.html when their data changed \(or \-\-force\) → read them and compare the contents. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:4f02149269a60fca85aa040116b2789d98c906f2, time:1319212903.98 -add proper caching of every ftp directory listing. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:750692931106d78ffc38c1ed63013c4dac4099dd, time:1319175393.07 -fork-/clone-info for each entry in [paths] with its incoming data (if it has some): | owner:, open:True, id:8621575e4016752e8987c8b294dfa9166f77eff3, time:1319147671.39 -More complex Readme parsing. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:94fbade896adbf6f696cfdb331021437dff3f30e, time:1319147671.39 -make the link from the /commit/*.html pages to the /src/*/[index.html] pages more obvious | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:ad936eaaba1693f7c44bd59916a19e6f3b3db27e, time:1319209748.93 -cache FTP directory listings for much faster upload of already existing sites. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:b1f6dfdaccc0346adaf0d42d361bbc2de00ee176, time:1319208814.12 -Idea: hg clone/push ftp://host.tld/path/to/repo → hg site --upload | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:b4693d2677e0a2b4ef9ee5dfbbe8c4742924604c, time:1319147779.76 -add linenumbers to the src files. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:b7bab6f46da7d224f97d0dac55a617d3a464d301, time:1319147678.56 -commits as commit/<rev>/ for long term viability. .html as suffix is not as long lived as a simple dirname. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:baaad4bdc13d7155048ce6a9dde92dc857b6a1ac, time:1319148414.16 -clone/<pathname>/ → incoming log (commits) + possibly an associated issue in b. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:c58557260a47597ac5057703e26a94df190a2a5d, time:1319147661.8 +sort tags in reverse order | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:61d256ab154e64597be604d6298daa545d4a96c7, time:1322159250.01 +add proper caching of every ftp directory listing. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:750692931106d78ffc38c1ed63013c4dac4099dd, time:1319175393.07 +fork-/clone-info for each entry in [paths] with its incoming data (if it has some): | owner:, open:True, id:8621575e4016752e8987c8b294dfa9166f77eff3, time:1319147671.39 +More complex Readme parsing. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:94fbade896adbf6f696cfdb331021437dff3f30e, time:1319147671.39 +make the link from the /commit/*.html pages to the /src/*/[index.html] pages more obvious | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:ad936eaaba1693f7c44bd59916a19e6f3b3db27e, time:1319209748.93 +cache FTP directory listings for much faster upload of already existing sites. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:b1f6dfdaccc0346adaf0d42d361bbc2de00ee176, time:1319208814.12 +Idea: hg clone/push ftp://host.tld/path/to/repo → hg site --upload | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:b4693d2677e0a2b4ef9ee5dfbbe8c4742924604c, time:1319147779.76 +add linenumbers to the src files. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:b7bab6f46da7d224f97d0dac55a617d3a464d301, time:1319147678.56 +commits as commit/<rev>/ for long term viability. .html as suffix is not as long lived as a simple dirname. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:baaad4bdc13d7155048ce6a9dde92dc857b6a1ac, time:1319148414.16 +clone/<pathname>/ → incoming log (commits) + possibly an associated issue in b. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:c58557260a47597ac5057703e26a94df190a2a5d, time:1319147661.8 no longer create raw files, since they can’t be served by all webservers and waste bandwidth and space (they are no longer linked anyway). | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:d1010e1933648f65af37d969bfb45f8d834fc8bb, time:1319148721.49 -check the hgweb templating for parsing the site. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:ef17f01dbe8ee58536fa8b345eb18d1efc639f15, time:1319208643.38 -maybe more advanced bookmarks pushing. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:ef8c12bfcc99686efc1f685a9be0be0c78922ca5, time:1322115049.48 -Treat branch heads specially: link on the main page. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:f531c27b38f9ea1749ded312f4f468c9ac33b930, time:1319147696.96 -allow setting user, password, server and path vie .hg/hgrc. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:fc575156316d56b70fda64725984b66cc8a2cfde, time:1322118134.69 +check the hgweb templating for parsing the site. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:ef17f01dbe8ee58536fa8b345eb18d1efc639f15, time:1319208643.38 +maybe more advanced bookmarks pushing. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:ef8c12bfcc99686efc1f685a9be0be0c78922ca5, time:1322115049.48 +crashes on missing readme. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:f4232c8a52fff730a4c63525ad597c063135e576, time:1332936115.69 +Treat branch heads specially: link on the main page. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:f531c27b38f9ea1749ded312f4f468c9ac33b930, time:1319147696.96 +allow setting user, password, server and path vie .hg/hgrc. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:fc575156316d56b70fda64725984b66cc8a2cfde, time:1322118134.69 diff --git a/.bugs/details/29210503551d0eafca67dda8d6fffbd40bf837dc.txt b/.bugs/details/29210503551d0eafca67dda8d6fffbd40bf837dc.txt new file mode 100644 --- /dev/null +++ b/.bugs/details/29210503551d0eafca67dda8d6fffbd40bf837dc.txt @@ -0,0 +1,27 @@ +# Lines starting with '#' and sections without content +# are not displayed by a call to 'details' +# +[paths] +# Paths related to this bug. +# suggested format: REPO_PATH:LINENUMBERS + + +[details] +# Additional details + + +[expected] +# The expected result + + +[actual] +# What happened instead + + +[reproduce] +# Reproduction steps + + +[comments] +# Comments and updates - leave your name +Won’t get the mtimes from the repo, because we don’t actually know what’s on the FTP server. diff --git a/.hgsigs b/.hgsigs new file mode 100644 --- /dev/null +++ b/.hgsigs @@ -0,0 +1,1 @@ +ef5367e461353fdb4cc6d0c76da7f9ebe8d368a6 0 iJwEAAEIAAYFAlCMfTkACgkQ3M8NswvBBUj3kQP9EYg7/POsz1VopA+CbzQfY93JAg6Ige4oRPY9vOoefhC9SwKaDd7NOHZYrYwRd/wojkTVm1IDjXrnK/ihzigIjagcXg0SZdfRDfD1Oy/ZPhvpumKbCkasZUKvripJanYr5GbKdoPDruI7mlVWUPed+iqs44LQCabTmflbHM/8E2k= diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -1,3 +1,5 @@ d30f0a2067a11a679b99b476dc51945628e23d3a v0.1 753a939405dc25d1fcd45355c1724d6d38874b4d 0.1.1 c2ee96c4f0138f6d16d552467da40ae21ec3e52a 0.1.2 +1c23d2ac584b436cd55f1c1dea43b70a4d3aa8ec 0.1.3 +3cc1d89231b249db4f3e139c33050c3549756c06 0.1.4 diff --git a/README.txt b/README.txt --- a/README.txt +++ b/README.txt @@ -17,7 +17,7 @@ Install: * Clone this repo. hg clone http://draketo.de/proj/hgsite/ * add this to the [extensions] section in your ~/.hgrc - site = path/to/site.py + site = path/to/staticsite.py if you have no [extensions] section, add it. Usage: diff --git a/site.py b/staticsite.py rename from site.py rename to staticsite.py --- a/site.py +++ b/staticsite.py @@ -18,17 +18,16 @@ GNU General Public License version 2 or """ 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 +from mercurial import cmdutil, util, scmutil +from mercurial import commands, dispatch from mercurial.i18n import _ -from mercurial import hg, discovery +from mercurial import hg, discovery, util, extensions _staticidentifier = ".statichgrepo" @@ -42,7 +41,7 @@ templates = { <title>{title}</title> </head> <body> -<h1>{reponame}</h1> +<h1 id="maintitle">{reponame}</h1> """, "srchead": """<!DOCTYPE html> <html><head> @@ -54,13 +53,26 @@ templates = { </head> <body> """, + "forkhead": """<!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>{forkname}</title> +</head> +<body> +<h1>{forkname} <small>(fork of <a href="../../">{reponame}</a>, found at {forkuri})</small></h1> +""", "foot": "</body></html>\n", "screenstyle": """ """, "printstyle": """ """, "manifesthead": """<h2>""" + _("Commit (click to see the diff)")+""": <a href='../../commit/{hex}.html'>{hex}</a></h2> <p>{desc}</p><p>{user}</p> + <h2>""" + _("Diffstat") + """</h2> +<pre>{diffstat}</pre> <h2>""" + _("Files in this revision") + "</h2>", - "commitlog": """\n<div style='float: right; padding-left: 0.5em'><em>({author|person})</em></div><strong> {date|shortdate}: <a href='{relativepath}src/{node}/index.html'>{desc|strip|fill68|firstline}</a></strong> <span style='font-size: xx-small'>{branches} {tags} {bookmarks}</span><p>{desc|escape}</p>\n""" + "commitlog": """\n<div style='float: right; padding-left: 0.5em'><em>({author|person})</em></div><strong> {date|shortdate}: <a href='{relativepath}src/{node}/index.html'>{desc|strip|fill68|firstline}</a></strong> <span style='font-size: xx-small'>{branches} {tags} {bookmarks}</span><p>{desc|escape}</p>\n""", } _indexregexp = re.compile("^\\.*index.html$") @@ -80,12 +92,17 @@ def contentequals(filepath, content): with open(filepath) as f: return f.read() == content except OSError: return not content + except IOError: return False # file does not exist. Empty != not existing. + # TODO: check: return True if content is None? -def parsereadme(filepath): +def parsereadme(filepath, truncated=False): """Parse the readme file""" with open(filepath) as r: - return "<pre>" + r.read() + "</pre>" - + readme = r.read() + if truncated: + return "<pre>" + "\n".join(readme.splitlines()[:5]) + "</pre>" + else: + return "<pre>" + readme + "</pre>" def overviewlogstring(ui, repo, revs, template=templates["commitlog"]): """Get the string for a log of the given revisions for the overview page.""" @@ -101,20 +118,37 @@ def overviewlogstring(ui, repo, revs, te def writeoverview(ui, repo, target, name): """Create the overview page""" overview = "" + # get the title overview += templates["head"].replace("{reponame}", name).replace("{title}", 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 += "\n".join(readme.splitlines()[:5]) + readme = parsereadme(os.path.join(repo.root, f)) + readme_intro = parsereadme(os.path.join(repo.root, f), truncated=True) + overview += "<div id='intro'>" + overview += readme_intro + overview += "</div>" break # now the links to the log and the files. - overview += "</pre>\n<p><a href='commits'>changelog</a> | <a href='src/" + repo["tip"].hex() + "/'>files</a></p>" + overview += "\n<p id='nav'><a href='commits'>changelog</a> | <a href='src/" + repo["tip"].hex() + "/'>files</a>" + # and the forks + forks = getforkinfo(ui, target) + if forks: + overview += " | " + _("forks: ") + for forkname, forkuri in forks.items(): + overview += "<a href='" + getforkdir(target, forkname) + "'>" + forkname + "</a> " + incoming, fn, localother = getincoming(ui, repo, otheruri=forkuri, othername=forkname) + overview += "<small>(" + str(len(incoming)) + outgoing, fn, localother = getoutgoing(ui, repo, otheruri=forkuri, othername=forkname) + overview += "<small>↓↑</small>" + str(len(outgoing)) + ")</small> " + + overview += "</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 += "\n<h2>Changes (<a href='commits'>full changelog</a>)</h2>\n" + overview += "\n<div id='shortlog'><h2>Changes (<a href='commits'>full changelog</a>)</h2>\n" ui.pushbuffer() t = cmdutil.changeset_templater(ui, repo, patch=False, diffopts=None, mapfile=None, buffered=False) t.use_template(templates["commitlog"].replace("{relativepath}", "")) @@ -122,6 +156,7 @@ def writeoverview(ui, repo, target, name ctx = repo.changectx(str(-c)) t.show(ctx) overview += ui.popbuffer() + overview += "</div>" # Add branch, bookmark and tag information, if they exist. branches = [] @@ -129,10 +164,16 @@ def writeoverview(ui, repo, target, name if branch and branch != "default": # not default branches.extend(heads) - tags = repo._tags - bookmarks = repo._bookmarks + try: + tags = repo._tags + except AttributeError: + tags = [] + try: + bookmarks = repo._bookmarks + except AttributeError: + bookmarks = [] if branches: # add branches - overview += "\n<h2>Branches</h2>\n" + overview += "\n<div id='branches'><h2>Branches</h2>\n" overview += overviewlogstring(ui, repo, branches, template=templates["commitlog"].replace( "{branches}", "XXXXX").replace( @@ -141,41 +182,45 @@ def writeoverview(ui, repo, target, name "{tags}", "XXXXX").replace( "{date|shortdate}", "{tags}").replace( "XXXXX", "{date|shortdate}")) + overview += "</div>" if len(tags) > 1: - overview += "\n<h2>Tags</h2>\n" + overview += "\n<div id='tags'><h2>Tags</h2>\n" overview += overviewlogstring(ui, repo, [tags[t] for t in tags if t != "tip"], template=templates["commitlog"].replace( "{tags}", "XXXXX").replace( "{date|shortdate}", "{tags}").replace( "XXXXX", "{date|shortdate}")) + overview += "</div>" if len(bookmarks): - overview += "\n<h2>Bookmarks</h2>\n" + overview += "\n<div id='bookmarks'><h2>Bookmarks</h2>\n" overview += overviewlogstring(ui, repo, bookmarks.values(), template=templates["commitlog"].replace( "{bookmarks}", "XXXXX").replace( "{date|shortdate}", "{bookmarks}").replace( "XXXXX", "{date|shortdate}")) - + overview += "</div>" # add the full readme - overview += "<h2>"+_("Readme")+"</h2>\n" + overview += "<div id='readme'><h2>"+_("Readme")+"</h2>\n" overview += readme + overview += "</div>" # finish the overview overview += templates["foot"] - if not contentequals(join(target, "index.html"), overview): - with open(join(target, "index.html"), "w") as f: + indexfile = os.path.join(target, "index.html") + if not contentequals(indexfile, overview): + with open(indexfile, "w") as f: f.write(overview) def writelog(ui, repo, target, name): """Write the full changelog, in steps of 100.""" - commits = join(target, "commits") + commits = os.path.join(target, "commits") # create the folders - if not isdir(commits): + if not os.path.isdir(commits): os.makedirs(commits) for i in range(len(repo.changelog)/100): d = commits+"-"+str(i+1)+"00" - if not isdir(d): + if not os.path.isdir(d): os.makedirs(d) # create the log files @@ -194,10 +239,10 @@ def writelog(ui, repo, target, name): logs[-1][-1] += "<p><a href=\"../commits-"+str(ck-2)+"00"+"\">later</a></p>" elif ck>1: logs[-1][-1] += "<p><a href=\"../commits\">later</a></p>" - logs.append([join(d, "index.html"), ""]) + logs.append([os.path.join(d, "index.html"), ""]) else: d = commits - logs.append([join(d, "index.html"), ""]) + logs.append([os.path.join(d, "index.html"), ""]) logs[-1][-1] += templates["head"].replace("{reponame}", "<a href='../'>"+name+"</a>").replace("{title}", name) for c in range(ck*100+1, min(len(repo.changelog)+1, (ck+1)*100)): @@ -211,21 +256,201 @@ def writelog(ui, repo, target, name): with open(filepath, "w") as f: f.write(data) +def getlocalother(repo, ui, otheruri, othername): + """Get a local clone of the repo identified by uri and name within .hg/paths. + + This creates that local clone! + """ + # if we cannot get the changes via bundlerepo, we create a + # local clone in .hg/paths/<othername>-<sha1-of-otheruri> and + # check from there. in case that local clone already exists, + # we tell it to pull there. The hash is necessary to prevent + # collisions when the uri changes. + if othername is None: + othername = "" + urihash = util.sha1(otheruri).hexdigest() + localcopy = os.path.join(repo.root, ".hg", "paths", + othername+"-"+urihash) + # if getting remote changes directly fails, we take the + # completely safe path: dispatch uses the only really stable + # interface: the cli. + if os.path.isdir(localcopy): + req = dispatch.request(["-R", localcopy, "pull", otheruri]) + else: + req = dispatch.request(["clone", otheruri, localcopy], ui=ui) + dispatch.dispatch(req) + other = hg.peer(repo, {}, localcopy) + return other + +def getincoming(ui, repo, otheruri, other=None, othername=None): + """Get incoming changes.""" + # Note: We cannot just use getcommonincoming and I do not yet know + # how to use its output to get good changes. TODO: do this nicer. + def cleanupfn(): + """non-operation cleanup function (default).""" + pass + # cannot do that for ftp or freenet insertion uris (freenet + # separates insertion and retrieval by private/public key) + isftpuri = otheruri.startswith("ftp://") + isfreenetpriv = "AQECAAE/" in otheruri + if isftpuri or isfreenetpriv: + chlist = [] + return chlist, cleanupfn, other + + if not other: + other = hg.peer(repo, {}, otheruri) + ui.pushbuffer() # ignore ui events + source, branches = hg.parseurl(otheruri, None) + revs, checkout = hg.addbranchrevs(repo, other, branches, None) + if revs: + revs = [other.lookup(rev) for rev in revs] + try: # FIXME: This breaks on http repos! + other, chlist, cleanupfn = hg.bundlerepo.getremotechanges(ui, repo, other, + revs, False, False) + except (AttributeError, util.Abort): + other = getlocalother(repo, ui, otheruri, othername) + other, chlist, cleanupfn = hg.bundlerepo.getremotechanges(ui, repo, other, + revs, False, False) + + ui.popbuffer() + return chlist, cleanupfn, other + +def getoutgoing(ui, repo, otheruri, other=None, othername=None): + def cleanupfn(): + """non-operation cleanup function (default).""" + pass + # cannot do that for ftp or freenet insertion uris (freenet + # separates insertion and retrieval by private/public key) + isftpuri = otheruri.startswith("ftp://") + isfreenetpriv = "AQECAAE/" in otheruri + if isftpuri or isfreenetpriv: + chlist = [] + return chlist, cleanupfn, other + + if not other: + other = hg.peer(repo, {}, otheruri) + + def outgoingchanges(repo, other): + from mercurial import discovery + fco = discovery.findcommonoutgoing + try: + og = fco(repo, other, force=True) + return og.missing + except AttributeError: # old client + common, outheads = og + o = repo.changelog.findmissing(common=common, heads=outheads) + return o + + other.ui.pushbuffer() # ignore ui events + + try: + chlist = outgoingchanges(repo, other) + except (AttributeError, util.Abort): + other.ui.popbuffer() + other = getlocalother(repo, ui, otheruri, othername) + other.ui.pushbuffer() + chlist = outgoingchanges(repo, other) + + other.ui.popbuffer() + return chlist, cleanupfn, other + + +def getforkinfo(ui, target): + """Name and Uri of all forks.""" + forks = dict(ui.configitems("paths")) + forkinfo = {} + for forkname, forkuri in forks.items(): + # ignore the static repo + if os.path.abspath(forkuri) == os.path.abspath(target): + continue + forkinfo[forkname] = forkuri + return forkinfo + +def safeuri(uri): + """Shareable uris: Hide password + hide freenet insert keys.""" + uri = util.hidepassword(uri) + freenetpriv = "AQECAAE/" + if "USK@" in uri and freenetpriv in uri: + uri = "freenet://USK@******" + uri[uri.index(freenetpriv)+len(freenetpriv)-1:] + return uri + +def getforkdata(ui, repo, target, name, forkname, forkuri): + """Write the site for a single fork.""" + # make sure the forkdir exists. + other = hg.peer(repo, {}, forkuri) + + # incrementally build the html + html = templates["forkhead"].replace( + "{forkname}", forkname).replace( + "{reponame}", name).replace( + "{forkuri}", safeuri(forkuri)) + + # prepare the log templater + t = cmdutil.changeset_templater(ui, repo, patch=False, diffopts=None, mapfile=None, buffered=False) + t.use_template(templates["commitlog"].replace( + "{relativepath}", "../")) + + # Add incoming commits + html += "<div id='incoming'><h2>Incoming commits</h2>" + chlist, cleanupfn, localother = getincoming(ui, repo, otheruri=forkuri, other=other, othername=forkname) + + ui.pushbuffer() + for ch in chlist: + ctx = localother.changectx(ch) + t.show(ctx) + html += ui.popbuffer() + cleanupfn() + + # add outgoing commits + html += "<div id='outgoing'><h2>Outgoing commits</h2>" + chlist, cleanupfn, localother = getoutgoing(ui, repo, forkuri, other=other, othername=forkname) + + ui.pushbuffer() + for ch in chlist: + ctx = repo.changectx(ch) + t.show(ctx) + html += ui.popbuffer() + cleanupfn() + + html += "</div>" + html += templates["foot"] + return html + +def getforkdir(target, forkname): + return os.path.join("forks", forkname) + +def writeforks(ui, repo, target, name): + """Write an info-page for each fork, defined in hg paths. + + relevant data: incoming commits, outgoing commits, branches and bookmarks not in fork or not in repo. Short: incoming (commits, branches, bookmarks), outgoing (incoming first means, we consider this repo to be the main repo). + """ + forkinfo = getforkinfo(ui, target) + for forkname, forkuri in forkinfo.items(): + # ignore the static repo itself + if os.path.abspath(forkuri) == os.path.abspath(target): + continue + forkdir = getforkdir(target, forkname) + if not os.path.isdir(os.path.join(target, forkdir)): + os.makedirs(os.path.join(target, forkdir)) + with open(os.path.join(target, forkdir, "index.html"), "w") as f: + f.write( + getforkdata(ui, repo, target, name, forkname, forkuri)) + def writecommits(ui, repo, target, name, force=False): """Write all not yet existing commit files.""" - commit = join(target, "commit") + commit = os.path.join(target, "commit") # create the folders - if not isdir(commit): + if not os.path.isdir(commit): os.makedirs(commit) t = cmdutil.changeset_templater(ui, repo, patch=False, diffopts=None, mapfile=None, buffered=False) t.use_template(templates["commitlog"].replace("{relativepath}", "../")) for c in range(len(repo.changelog)): ctx = repo.changectx(str(c)) - cpath = join(commit, ctx.hex() + ".html") - if not force and isfile(cpath): + cpath = os.path.join(commit, ctx.hex() + ".html") + if not force and os.path.isfile(cpath): continue with open(cpath, "w") as cf: cf.write(templates["head"].replace("{reponame}", "<a href='../'>"+name+"</a>").replace("{title}", name)) @@ -251,25 +476,40 @@ def parsesrcdata(data): 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") + return os.path.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) + return os.path.join(target,"raw",ctx.hex(),filename) -def createindex(target, ctx): +def ctxdiffstat(ui, repo, ctx): + """Get the diffstat of a change context.""" + command = "log -r " + ctx.hex() + " --stat --color=never" + req = dispatch.request(command.split(), ui=ui, repo=repo) + ui.pushbuffer() + dispatch.dispatch(req) + # FIXME: remove the color in an elegant way instead of fudging like this. + return ui.popbuffer().replace( + "[0;33m","").replace( + "[0;32m","").replace( + "[0m", "").replace( + "[0;31m", "").replace( + "[0m","") + +def createindex(ui, repo, 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()) + "{user}", ctx.user()).replace( + "{diffstat}", ctxdiffstat(ui, repo, ctx)) # 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 += "<li><a href='../../"+ os.path.join("src",lasteditctx.hex(), escapename(filename)+".html") + "'>" + filename + "</a>"# (<a href='../../" + os.path.join("raw",lasteditctx.hex(), filename) + "'>raw</a>)</li>" index += "</ul>" return index @@ -291,20 +531,20 @@ def writesourcetree(ui, repo, target, na # first write the raw data filepath = rawpath(target,ctx,filectx.path()) # skip already existing files - if not force and isfile(filepath): + if not force and os.path.isfile(filepath): continue try: - os.makedirs(dirname(filepath)) + os.makedirs(os.path.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) - if not force and isfile(filepath): + if not force and os.path.isfile(filepath): continue try: - os.makedirs(dirname(filepath)) + os.makedirs(os.path.dirname(filepath)) except OSError: pass # exists with open(filepath, "w") as f: f.write(templates["srchead"].replace("{filetitle}", name+": " + filename)) @@ -313,46 +553,46 @@ def writesourcetree(ui, repo, target, na # 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") + filepath = os.path.join(target,"src",ctx.hex(),"index.html") # skip already existing files - if not force and isfile(filepath): + if not force and os.path.isfile(filepath): continue try: - os.makedirs(dirname(filepath)) + os.makedirs(os.path.dirname(filepath)) except OSError: pass # exists with open(filepath, "w") as f: f.write(templates["head"].replace("{reponame}", "<a href='../../'>"+name+"</a>").replace("{title}", name)) - f.write(createindex(target, ctx)) + f.write(createindex(ui, repo, 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): + idfile = os.path.join(target, _staticidentifier) + if not os.path.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"]: + if not os.path.isfile(idfile): + if not ui.prompt("The target folder " + target + " has not yet been used as static repo. Really use it? (y/N)", default="n").lower() in ["y", "yes"]: return with open(idfile, "w") as i: i.write("") - if opts["name"]: - name = opts["name"] + if opts["sitename"]: + name = opts["sitename"] elif target != "static": name = target - else: name = basename(repo.root) + else: name = os.path.basename(repo.root) # first the stylesheets screenstyle = opts["screenstyle"] - screenfile = join(target, "style.css") + screenfile = os.path.join(target, "style.css") if screenstyle and not samefilecontent(screenstyle, screenfile): shutil.copyfile(screenstyle, screenfile) elif not contentequals(screenfile,templates["screenstyle"]): - with open(join(target, "style.css"), "w") as f: + with open(screenfile, "w") as f: f.write(templates["screenstyle"]) printstyle = opts["printstyle"] - printfile = join(target, "print.css") + printfile = os.path.join(target, "print.css") if printstyle and not samefilecontent(printstyle, printfile): shutil.copyfile(printstyle, printfile) elif not contentequals(printfile, templates["printstyle"]): @@ -371,8 +611,11 @@ def parsesite(ui, repo, target, **opts): # and all file data writesourcetree(ui, repo, target, name, force=opts["force"]) + # and all forks + writeforks(ui, repo, target, name) -def addrepo(ui, repo, target, bookmarks): + +def addrepo(ui, repo, target, bookmarks, force): """Add the repo to the target and make sure it is up to date.""" try: commands.init(ui, dest=target) @@ -382,16 +625,21 @@ def addrepo(ui, repo, target, bookmarks) ui.pushbuffer() if bookmarks: - commands.push(ui, repo, dest=target, bookmark=repo._bookmarks) + commands.push(ui, repo, dest=target, bookmark=repo._bookmarks, force=force) else: - commands.push(ui, repo, dest=target) + commands.push(ui, repo, dest=target, force=force) 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:]) + try: + user, password = ftpstring.split("@")[0].split(":") + serverandpath = "@".join(ftpstring.split("@")[1:]) + except ValueError: + ui.warn(_("FTP-upload: No @ in FTP-Url. We try anonymous access.\n")) + user, password = "anonymous", "" + serverandpath = ftpstring # no @, so we just take the whole string server = serverandpath.split("/")[0] ftppath = "/".join(serverandpath.split("/")[1:]) timeout = 10 @@ -404,7 +652,7 @@ def upload(ui, repo, target, ftpstring, ui.status(ftp.getwelcome(), "\n") # create the target dir. - serverdir = dirname(ftppath) + serverdir = os.path.dirname(ftppath) serverdirparts = ftppath.split("/") sd = serverdirparts[0] if not sd in ftp.nlst(): @@ -429,9 +677,9 @@ def upload(ui, repo, target, ftpstring, for d, dirnames, filenames in os.walk(target): for filename in filenames: - localfile = join(d, filename) + localfile = os.path.join(d, filename) serverfile = localfile[len(target)+1:] - serverdir = dirname(serverfile) + serverdir = os.path.dirname(serverfile) serverdirparts = serverdir.split("/") # print serverdirparts, serverfile with open(localfile, "rb") as f: @@ -449,7 +697,7 @@ def upload(ui, repo, target, ftpstring, for sdp in serverdirparts[1:]: sdold = sd - sd = join(sd, sdp) + sd = os.path.join(sd, sdp) #print sd, sdp #print ftp.nlst(sdold) if sd and not sd in _ftplistcache: # should happen only once per superdir @@ -498,8 +746,8 @@ def staticsite(ui, repo, target=None, ** # add the hg repo to the static site # currently we need to either include all bookmarks or not, because we don’t have the remote repo when parsing the site. # TODO: I don’t know if that is the correct way to go. Maybe always push all. - bookmarks = opts["bookmarks"] - addrepo(ui, repo, target, bookmarks) + bookmark = opts["bookmark"] + addrepo(ui, repo, target, bookmark, force=opts["force"]) # first: just create the site. parsesite(ui, repo, target, **opts) if opts["upload"]: @@ -507,18 +755,167 @@ def staticsite(ui, repo, target=None, ** upload(ui, repo, target, opts["upload"], opts["force"]) - cmdtable = { # "command-name": (function-call, options-list, help-string) "site": (staticsite, [ #('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.'), + ('n', 'sitename', "", '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'), - ('B', 'bookmarks', False, 'include the bookmarks')], + ('B', 'bookmark', False, 'include the bookmarks')], "[options] [folder]") } + +## add ftp as scheme to be handled by this plugin. + +wrapcmds = { # cmd: generic, target, fixdoc, ppopts, opts + 'push': (False, None, False, False, [ + ('', 'staticsite', None, 'show parent svn revision instead'), + ]) +} + +## Explicitely wrap functions to change local commands in case the remote repo is an FTP repo. See mercurial.extensions for more information. +# Get the module which holds the functions to wrap +# the new function: gets the original function as first argument and the originals args and kwds. +def findcommonoutgoing(orig, *args, **opts): + repo = args[1] + capable = getattr(repo, 'capable', lambda x: False) + if capable('ftp'): + class fakeoutgoing(object): + def __init__(self): + self.excluded = [] + self.missing = [] + self.commonheads = [] + return fakeoutgoing() + else: + return orig(*args, **opts) +# really wrap the functions +extensions.wrapfunction(discovery, 'findcommonoutgoing', findcommonoutgoing) + +# explicitely wrap commands in case the remote repo is an FTP repo. +def ftppush(orig, *args, **opts): + try: + ui, repo, path = args + path = ui.expandpath(path) + except ValueError: # no ftp string + ui, repo = args + path = ui.expandpath('default-push', 'default') + # only act differently, if the target is an FTP repo. + if not path.startswith("ftp"): + return orig(*args, **opts) + # first create the site at ._site + target = "._site" + ftpstring = path.replace("ftp://", "") + # fix the options to fit those of the site command + opts["name"] = opts["sitename"] + opts["upload"] = ftpstring + staticsite(ui, repo, target, **opts) + return 0 + +# really wrap the command +siteopts = [('', 'sitename', "", 'staticsite: the title of the site. Default: folder or last segment of the repo-path.'), + ('', 'screenstyle', "", 'use a custom stylesheet for display on screen'), + ('', 'printstyle', "", 'use a custom stylesheet for printing')] +entry = extensions.wrapcommand(commands.table, "push", ftppush) +entry[1].extend(siteopts) + +# Starting an FTP repo. Not yet used, except for throwing errors for missing commands and faking the lock. + +# TODO: repo -> peer +from mercurial import util +try: + from mercurial.peer import peerrepository +except ImportError: + from mercurial.repo import repository as peerrepository +try: + from mercurial.error import RepoError +except ImportError: + from mercurial.repo import RepoError + +# TODO: repo -> peer +class FTPRepository(peerrepository): + def __init__(self, ui, path, create): + self.create = create + self.ui = ui + self.path = path + self.capabilities = set(["ftp"]) + + def lock(self): + """We cannot really lock FTP repos, yet. + + TODO: Implement as locking the repo in the static site folder.""" + class DummyLock: + def release(self): + pass + l = DummyLock() + return l + + def url(self): + return self.path + + def lookup(self, key): + return key + + def cancopy(self): + return False + + def heads(self, *args, **opts): + """ + Whenever this function is hit, we abort. The traceback is useful for + figuring out where to intercept the functionality. + """ + raise util.Abort('command heads unavailable for FTP repositories') + + def pushkey(self, namespace, key, old, new): + return False + + def listkeys(self, namespace): + return {} + + def push(self, remote, force=False, revs=None, newbranch=None): + raise util.Abort('command push unavailable for FTP repositories') + + def pull(self, remote, heads=[], force=False): + raise util.Abort('command pull unavailable for FTP repositories') + + def findoutgoing(self, remote, base=None, heads=None, force=False): + raise util.Abort('command findoutgoing unavailable for FTP repositories') + + +class RepoContainer(object): + def __init__(self): + pass + + def __repr__(self): + return '<FTPRepository>' + + def instance(self, ui, url, create): + # Should this use urlmod.url(), or is manual parsing better? + #context = {} + return FTPRepository(ui, url, create) + +hg.schemes["ftp"] = RepoContainer() + +def test(): + import subprocess as sp + def showcall(args): + print args + sp.call(args) + os.chdir(os.path.dirname(__file__)) + # just check if loading the extension works + showcall(["hg", "--config", "extensions.site="+__file__]) + # check if I can create a site + showcall(["hg", "--config", "extensions.site="+__file__, "site", "-B", "-n", "mysite"]) + # check if uploading works: Only a valid test, if you have a + # post-push hook which does the uploading + showcall(["hg", "--config", "extensions.site="+__file__, "push"]) + # check if push directly to ftp works. Requires the path draketo + # to be set up in .hg/hgrc as ftp://user:password/path + showcall(["hg", "--config", "extensions.site="+__file__, "push", "draketo", "--sitename", "site extension"]) + +if __name__ == "__main__": + test()