- #!/usr/bin/env python
- # encoding: utf-8
-
- """hgsite
-
- Create and/or upload a static copy of the repository.
-
- The main goal is sharing Mercurial on servers with only FTP access and
- statically served files, while providing the same information as hg
- serve and full solutions like bitbucket and gitorious (naturally
- without the interactivity).
- """
-
- __copyright__ = """Copyright 2011 Arne Babenhauserheide
-
- This software may be used and distributed according to the terms of the
- GNU General Public License version 2 or any later version.
- """
-
- import os
- import shutil
- import re
- import mercurial
- import ftplib
- import socket
- import datetime
- from mercurial import cmdutil, util, scmutil
- from mercurial import commands, dispatch
- from mercurial.i18n import _
- from mercurial import hg, discovery, util, extensions
-
- _staticidentifier = ".statichgrepo"
-
- templates = {
- "head": """<!DOCTYPE html>
- <html><head>
- <meta charset="utf-8" />
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <!--duplicate for older browsers-->
- <link rel="stylesheet" href="style.css" type="text/css" media="screen" />
- <link rel="stylesheet" href="print.css" type="text/css" media="print" />
- <title>{title}</title>
- </head>
- <body>
- <h1 id="maintitle">{reponame}</h1>
- """,
- "srchead": """<!DOCTYPE html>
- <html><head>
- <meta charset="utf-8" />
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <!--duplicate for older browsers-->
- <link rel="stylesheet" href="style.css" type="text/css" media="screen" />
- <link rel="stylesheet" href="print.css" type="text/css" media="print" />
- <title>{filetitle}</title>
- </head>
- <body>
- """,
- "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""",
- }
-
- _indexregexp = re.compile("^\\.*index.html$")
-
-
- def samefilecontent(filepath1, filepath2):
- """Check if the content of the two referenced files is equal."""
- try:
- with open(filepath1) as f1:
- with open(filepath2) as f2:
- return f1.read() == f2.read()
- except OSError: return False
-
- def contentequals(filepath, content):
- """Check if the files content is content."""
- try:
- 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, truncated=False):
- """Parse the readme file"""
- with open(filepath) as r:
- 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."""
- ui.pushbuffer()
- t = cmdutil.changeset_templater(ui, repo, patch=False, diffopts=None, mapfile=None, buffered=False)
- t.use_template(template.replace("{relativepath}", ""))
- for c in revs:
- ctx = repo.changectx(c)
- t.show(ctx)
- return ui.popbuffer()
-
-
- 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(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 += "\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<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}", ""))
- for c in range(1, min(len(repo.changelog), 5)):
- ctx = repo.changectx(str(-c))
- t.show(ctx)
- overview += ui.popbuffer()
- overview += "</div>"
-
- # Add branch, bookmark and tag information, if they exist.
- branches = []
- for branch, heads in repo.branchmap().items():
- if branch and branch != "default": # not default
- branches.extend(heads)
-
- try:
- tags = repo._tags
- except AttributeError:
- tags = []
- try:
- bookmarks = repo._bookmarks
- except AttributeError:
- bookmarks = []
- if branches: # add branches
- overview += "\n<div id='branches'><h2>Branches</h2>\n"
- overview += overviewlogstring(ui, repo, branches,
- template=templates["commitlog"].replace(
- "{branches}", "XXXXX").replace(
- "{date|shortdate}", "{branches}").replace(
- "XXXXX", "{date|shortdate}").replace(
- "{tags}", "XXXXX").replace(
- "{date|shortdate}", "{tags}").replace(
- "XXXXX", "{date|shortdate}"))
- overview += "</div>"
- if len(tags) > 1:
- 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<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 += "<div id='readme'><h2>"+_("Readme")+"</h2>\n"
- overview += readme
- overview += "</div>"
-
- # finish the overview
- overview += templates["foot"]
- 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 = os.path.join(target, "commits")
-
- # create the folders
- 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 os.path.isdir(d):
- os.makedirs(d)
-
- # create the log files
- t = cmdutil.changeset_templater(ui, repo, patch=False, diffopts=None, mapfile=None, buffered=False)
- t.use_template(templates["commitlog"].replace("{relativepath}", "../"))
- logs = []
- for ck in range(len(repo.changelog)/100+1):
- ui.pushbuffer()
- if ck:
- dd = d
- di = str(ck)+"00"
- d = commits+"-"+di
- logs[-1][-1] += "<p><a href=\"../commits-"+di+"\">earlier</a></p>"
- if ck>2:
- # the older log gets a reference to the newer one
- 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([os.path.join(d, "index.html"), ""])
- else:
- d = commits
- 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)):
- ctx = repo.changectx(str(-c))
- t.show(ctx)
- logs[-1][-1] += ui.popbuffer()
-
- for filepath,data in logs:
- data += templates["foot"].replace("{reponame}", "<a href='../'>"+name+"</a>")
- if not contentequals(filepath,data):
- 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 = os.path.join(target, "commit")
-
- # create the folders
- 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 = 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))
- ui.pushbuffer()
- t.show(ctx)
- cf.write(ui.popbuffer())
- ui.pushbuffer()
- commands.diff(ui, repo, change=str(c), git=True)
- cf.write("<pre>"+ui.popbuffer().replace("<", "<")+"</pre>")
- cf.write(templates["foot"].replace("{reponame}", "<a href='../'>"+name+"</a>"))
-
-
- def escapename(filename):
- """escape index.html as .index.html and .ind… as ..ind… and so fort."""
- if _indexregexp.match(filename) is not None:
- return "." + filename
- else: return filename
-
-
- def parsesrcdata(data):
- """Parse a src file into a html file."""
- return "<pre>"+data.replace("<", "<")+"</pre>"
-
- def srcpath(target, ctx, filename):
- """Get the relative path to the static sourcefile for an already escaped filename."""
- return 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 os.path.join(target,"raw",ctx.hex(),filename)
-
- 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()).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='../../"+ 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
-
- def writesourcetree(ui, repo, target, name, force, rawfiles=False):
- """Write manifests for all commits and websites for all files.
-
- * For each file, write sites for all revisions where the file was changed: under src/<hex>/path as html site (with linenumbers and maybe colored source), under raw/<hex>/<path> as plain files. If there is an index.html file, write it as .index.html. If there also is .index.html, turn it to ..index.html, …
- * For each commit write an index with links to the included files at their latest revisions before/at the commit.
- """
- # first write all files in all commits.
- for c in range(len(repo.changelog)):
- ctx = repo.changectx(str(c))
- for filename in ctx.files():
- try:
- filectx = ctx.filectx(filename)
- except LookupError, e:
- ui.warn("File not found, likely moved ", e, "\n")
- if rawfiles:
- # first write the raw data
- filepath = rawpath(target,ctx,filectx.path())
- # skip already existing files
- if not force and os.path.isfile(filepath):
- continue
- try:
- 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 os.path.isfile(filepath):
- continue
- try:
- os.makedirs(os.path.dirname(filepath))
- except OSError: pass # exists
- with open(filepath, "w") as f:
- f.write(templates["srchead"].replace("{filetitle}", name+": " + filename))
- f.write(parsesrcdata(filectx.data()))
- f.write(templates["foot"].replace("{reponame}", name))
- # then write manifests for all commits
- for c in range(len(repo.changelog)):
- ctx = repo.changectx(str(c))
- filepath = os.path.join(target,"src",ctx.hex(),"index.html")
- # skip already existing files
- if not force and os.path.isfile(filepath):
- continue
- try:
- 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(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 = 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 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["sitename"]:
- name = opts["sitename"]
- elif target != "static": name = target
- else: name = os.path.basename(repo.root)
-
- # first the stylesheets
- screenstyle = opts["screenstyle"]
- 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(screenfile, "w") as f:
- f.write(templates["screenstyle"])
- printstyle = opts["printstyle"]
- printfile = os.path.join(target, "print.css")
- if printstyle and not samefilecontent(printstyle, printfile):
- shutil.copyfile(printstyle, printfile)
- elif not contentequals(printfile, templates["printstyle"]):
- with open(printfile, "w") as f:
- f.write(templates["printstyle"])
-
- # then the overview
- writeoverview(ui, repo, target, name)
-
- # and the log
- writelog(ui, repo, target, name)
-
- # and all commit files
- writecommits(ui, repo, target, name, force=opts["force"])
-
- # and all file data
- writesourcetree(ui, repo, target, name, force=opts["force"])
-
- # and all forks
- writeforks(ui, repo, target, name)
-
-
- 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)
- except mercurial.error.RepoError, e:
- # already exists
- pass
-
- ui.pushbuffer()
- if bookmarks:
- commands.push(ui, repo, dest=target, bookmark=repo._bookmarks, force=force)
- else:
- 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."""
- 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
- try:
- ftp = ftplib.FTP(server, user, password, "", timeout)
- except socket.timeout:
- ui.warn(_("connection to "), server, _(" timed out after "), timeout, _(" seconds.\n"))
- return
-
- ui.status(ftp.getwelcome(), "\n")
-
- # create the target dir.
- serverdir = os.path.dirname(ftppath)
- serverdirparts = ftppath.split("/")
- sd = serverdirparts[0]
- if not sd in ftp.nlst():
- ftp.mkd(sd)
- for sdp in serverdirparts[1:]:
- sdo = sd
- sd = os.path.join(sd, sdp)
- if not sd in ftp.nlst(sdo):
- ftp.mkd(sd)
-
-
- ftp.cwd(ftppath)
- if not ftp.pwd() == "/" + ftppath:
- ui.warn(_("not in the correct ftp directory. Cowardly bailing out.\n"))
- return
-
- #ftp.dir()
- #return
- ftpfeatures = ftp.sendcmd("FEAT")
- featuremtime = " MDTM" in ftpfeatures.splitlines()
- _ftplistcache = set()
-
- for d, dirnames, filenames in os.walk(target):
- for filename in filenames:
- localfile = os.path.join(d, filename)
- serverfile = localfile[len(target)+1:]
- serverdir = os.path.dirname(serverfile)
- serverdirparts = serverdir.split("/")
- # print serverdirparts, serverfile
- with open(localfile, "rb") as f:
- sd = serverdirparts[0]
- if sd and not sd in _ftplistcache: # should happen only once per superdir
- _ftplistcache.update(set(ftp.nlst()))
- if sd and not sd in _ftplistcache:
- try:
- ui.status(_("creating directory "), sd, "\n")
- ftp.mkd(sd)
- _ftplistcache.add(sd)
- except ftplib.error_perm, resp:
- ui.warn(_("could not create directory "), sd, ": " , resp, "\n")
- else: _ftplistcache.add(sd)
-
- for sdp in serverdirparts[1:]:
- sdold = sd
- 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
- _ftplistcache.update(set(ftp.nlst(sdold)))
- if sd and not sd in _ftplistcache:
- try:
- ui.status(_("creating directory "), sd, "\n")
- ftp.mkd(sd)
- _ftplistcache.add(sd)
- except ftplib.error_perm, resp:
- ui.warn(_("could not create directory "),
- sd, ": " , resp, "\n")
-
- if not serverfile in _ftplistcache: # should happen for existing files only once per dir.
- _ftplistcache.update(set(ftp.nlst(serverdir)))
- if not serverfile in _ftplistcache or force:
- if force:
- ui.status(_("uploading "), serverfile,
- _(" because I am forced to.\n"))
- else:
- ui.status(_("uploading "), serverfile,
- _(" because it is not yet online.\n"))
-
- ftp.storbinary("STOR "+ serverfile, f)
- else:
- # reupload the file if the file on the server is older than the local file.
- if featuremtime:
- ftpmtime = ftp.sendcmd("MDTM " + serverfile).split()[1]
- localmtime = os.stat(localfile).st_mtime
- localmtimestr = datetime.datetime.utcfromtimestamp(localmtime).strftime("%Y%m%d%H%M%S")
- newer = int(localmtimestr) > int(ftpmtime)
- if newer:
- ui.status(_("uploading "), serverfile,
- _(" because it is newer than the file on the FTP server.\n"))
- ftp.storbinary("STOR "+ serverfile, f)
-
-
-
- def staticsite(ui, repo, target=None, **opts):
- """Create a static copy of the repository and/or upload it to an FTP server."""
- if repo.root == target:
- ui.warn(_("static target repo can’t be the current repo"))
- return
- if not target: target = "static"
- #print repo["."].branch()
- # 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.
- bookmark = opts["bookmark"]
- addrepo(ui, repo, target, bookmark, force=opts["force"])
- # first: just create the site.
- parsesite(ui, repo, target, **opts)
- if opts["upload"]:
- # upload the repo
- 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', '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', '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()