(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()