hg site extension
 
(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(
+                "","")
+    
+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()