#!/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(
                "","")
    
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()