mercurial/hgweb.py
changeset 2311 b832b6eb65ab
parent 2307 5b3a3e35f084
child 2312 4f04368423ec
equal deleted inserted replaced
2307:5b3a3e35f084 2311:b832b6eb65ab
     1 # hgweb.py - web interface to a mercurial repository
       
     2 #
       
     3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
       
     4 # Copyright 2005 Matt Mackall <mpm@selenic.com>
       
     5 #
       
     6 # This software may be used and distributed according to the terms
       
     7 # of the GNU General Public License, incorporated herein by reference.
       
     8 
       
     9 import os, cgi, sys
       
    10 import mimetypes
       
    11 from demandload import demandload
       
    12 demandload(globals(), "mdiff time re socket zlib errno ui hg ConfigParser")
       
    13 demandload(globals(), "tempfile StringIO BaseHTTPServer util SocketServer")
       
    14 demandload(globals(), "archival mimetypes templater urllib")
       
    15 from node import *
       
    16 from i18n import gettext as _
       
    17 
       
    18 def splitURI(uri):
       
    19     """ Return path and query splited from uri
       
    20 
       
    21     Just like CGI environment, the path is unquoted, the query is
       
    22     not.
       
    23     """
       
    24     if '?' in uri:
       
    25         path, query = uri.split('?', 1)
       
    26     else:
       
    27         path, query = uri, ''
       
    28     return urllib.unquote(path), query
       
    29 
       
    30 def up(p):
       
    31     if p[0] != "/":
       
    32         p = "/" + p
       
    33     if p[-1] == "/":
       
    34         p = p[:-1]
       
    35     up = os.path.dirname(p)
       
    36     if up == "/":
       
    37         return "/"
       
    38     return up + "/"
       
    39 
       
    40 def get_mtime(repo_path):
       
    41     hg_path = os.path.join(repo_path, ".hg")
       
    42     cl_path = os.path.join(hg_path, "00changelog.i")
       
    43     if os.path.exists(os.path.join(cl_path)):
       
    44         return os.stat(cl_path).st_mtime
       
    45     else:
       
    46         return os.stat(hg_path).st_mtime
       
    47 
       
    48 def staticfile(directory, fname):
       
    49     """return a file inside directory with guessed content-type header
       
    50 
       
    51     fname always uses '/' as directory separator and isn't allowed to
       
    52     contain unusual path components.
       
    53     Content-type is guessed using the mimetypes module.
       
    54     Return an empty string if fname is illegal or file not found.
       
    55 
       
    56     """
       
    57     parts = fname.split('/')
       
    58     path = directory
       
    59     for part in parts:
       
    60         if (part in ('', os.curdir, os.pardir) or
       
    61             os.sep in part or os.altsep is not None and os.altsep in part):
       
    62             return ""
       
    63         path = os.path.join(path, part)
       
    64     try:
       
    65         os.stat(path)
       
    66         ct = mimetypes.guess_type(path)[0] or "text/plain"
       
    67         return "Content-type: %s\n\n%s" % (ct, file(path).read())
       
    68     except (TypeError, OSError):
       
    69         # illegal fname or unreadable file
       
    70         return ""
       
    71 
       
    72 class hgrequest(object):
       
    73     def __init__(self, inp=None, out=None, env=None):
       
    74         self.inp = inp or sys.stdin
       
    75         self.out = out or sys.stdout
       
    76         self.env = env or os.environ
       
    77         self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
       
    78 
       
    79     def write(self, *things):
       
    80         for thing in things:
       
    81             if hasattr(thing, "__iter__"):
       
    82                 for part in thing:
       
    83                     self.write(part)
       
    84             else:
       
    85                 try:
       
    86                     self.out.write(str(thing))
       
    87                 except socket.error, inst:
       
    88                     if inst[0] != errno.ECONNRESET:
       
    89                         raise
       
    90 
       
    91     def header(self, headers=[('Content-type','text/html')]):
       
    92         for header in headers:
       
    93             self.out.write("%s: %s\r\n" % header)
       
    94         self.out.write("\r\n")
       
    95 
       
    96     def httphdr(self, type, file="", size=0):
       
    97 
       
    98         headers = [('Content-type', type)]
       
    99         if file:
       
   100             headers.append(('Content-disposition', 'attachment; filename=%s' % file))
       
   101         if size > 0:
       
   102             headers.append(('Content-length', str(size)))
       
   103         self.header(headers)
       
   104 
       
   105 class hgweb(object):
       
   106     def __init__(self, repo, name=None):
       
   107         if type(repo) == type(""):
       
   108             self.repo = hg.repository(ui.ui(), repo)
       
   109         else:
       
   110             self.repo = repo
       
   111 
       
   112         self.mtime = -1
       
   113         self.reponame = name
       
   114         self.archives = 'zip', 'gz', 'bz2'
       
   115 
       
   116     def refresh(self):
       
   117         mtime = get_mtime(self.repo.root)
       
   118         if mtime != self.mtime:
       
   119             self.mtime = mtime
       
   120             self.repo = hg.repository(self.repo.ui, self.repo.root)
       
   121             self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
       
   122             self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
       
   123             self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
       
   124 
       
   125     def archivelist(self, nodeid):
       
   126         for i in self.archives:
       
   127             if self.repo.ui.configbool("web", "allow" + i, False):
       
   128                 yield {"type" : i, "node" : nodeid, "url": ""}
       
   129 
       
   130     def listfiles(self, files, mf):
       
   131         for f in files[:self.maxfiles]:
       
   132             yield self.t("filenodelink", node=hex(mf[f]), file=f)
       
   133         if len(files) > self.maxfiles:
       
   134             yield self.t("fileellipses")
       
   135 
       
   136     def listfilediffs(self, files, changeset):
       
   137         for f in files[:self.maxfiles]:
       
   138             yield self.t("filedifflink", node=hex(changeset), file=f)
       
   139         if len(files) > self.maxfiles:
       
   140             yield self.t("fileellipses")
       
   141 
       
   142     def siblings(self, siblings=[], rev=None, hiderev=None, **args):
       
   143         if not rev:
       
   144             rev = lambda x: ""
       
   145         siblings = [s for s in siblings if s != nullid]
       
   146         if len(siblings) == 1 and rev(siblings[0]) == hiderev:
       
   147             return
       
   148         for s in siblings:
       
   149             yield dict(node=hex(s), rev=rev(s), **args)
       
   150 
       
   151     def renamelink(self, fl, node):
       
   152         r = fl.renamed(node)
       
   153         if r:
       
   154             return [dict(file=r[0], node=hex(r[1]))]
       
   155         return []
       
   156 
       
   157     def showtag(self, t1, node=nullid, **args):
       
   158         for t in self.repo.nodetags(node):
       
   159              yield self.t(t1, tag=t, **args)
       
   160 
       
   161     def diff(self, node1, node2, files):
       
   162         def filterfiles(filters, files):
       
   163             l = [x for x in files if x in filters]
       
   164 
       
   165             for t in filters:
       
   166                 if t and t[-1] != os.sep:
       
   167                     t += os.sep
       
   168                 l += [x for x in files if x.startswith(t)]
       
   169             return l
       
   170 
       
   171         parity = [0]
       
   172         def diffblock(diff, f, fn):
       
   173             yield self.t("diffblock",
       
   174                          lines=prettyprintlines(diff),
       
   175                          parity=parity[0],
       
   176                          file=f,
       
   177                          filenode=hex(fn or nullid))
       
   178             parity[0] = 1 - parity[0]
       
   179 
       
   180         def prettyprintlines(diff):
       
   181             for l in diff.splitlines(1):
       
   182                 if l.startswith('+'):
       
   183                     yield self.t("difflineplus", line=l)
       
   184                 elif l.startswith('-'):
       
   185                     yield self.t("difflineminus", line=l)
       
   186                 elif l.startswith('@'):
       
   187                     yield self.t("difflineat", line=l)
       
   188                 else:
       
   189                     yield self.t("diffline", line=l)
       
   190 
       
   191         r = self.repo
       
   192         cl = r.changelog
       
   193         mf = r.manifest
       
   194         change1 = cl.read(node1)
       
   195         change2 = cl.read(node2)
       
   196         mmap1 = mf.read(change1[0])
       
   197         mmap2 = mf.read(change2[0])
       
   198         date1 = util.datestr(change1[2])
       
   199         date2 = util.datestr(change2[2])
       
   200 
       
   201         modified, added, removed, deleted, unknown = r.changes(node1, node2)
       
   202         if files:
       
   203             modified, added, removed = map(lambda x: filterfiles(files, x),
       
   204                                            (modified, added, removed))
       
   205 
       
   206         diffopts = self.repo.ui.diffopts()
       
   207         showfunc = diffopts['showfunc']
       
   208         ignorews = diffopts['ignorews']
       
   209         for f in modified:
       
   210             to = r.file(f).read(mmap1[f])
       
   211             tn = r.file(f).read(mmap2[f])
       
   212             yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
       
   213                             showfunc=showfunc, ignorews=ignorews), f, tn)
       
   214         for f in added:
       
   215             to = None
       
   216             tn = r.file(f).read(mmap2[f])
       
   217             yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
       
   218                             showfunc=showfunc, ignorews=ignorews), f, tn)
       
   219         for f in removed:
       
   220             to = r.file(f).read(mmap1[f])
       
   221             tn = None
       
   222             yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
       
   223                             showfunc=showfunc, ignorews=ignorews), f, tn)
       
   224 
       
   225     def changelog(self, pos):
       
   226         def changenav(**map):
       
   227             def seq(factor, maxchanges=None):
       
   228                 if maxchanges:
       
   229                     yield maxchanges
       
   230                     if maxchanges >= 20 and maxchanges <= 40:
       
   231                         yield 50
       
   232                 else:
       
   233                     yield 1 * factor
       
   234                     yield 3 * factor
       
   235                 for f in seq(factor * 10):
       
   236                     yield f
       
   237 
       
   238             l = []
       
   239             last = 0
       
   240             for f in seq(1, self.maxchanges):
       
   241                 if f < self.maxchanges or f <= last:
       
   242                     continue
       
   243                 if f > count:
       
   244                     break
       
   245                 last = f
       
   246                 r = "%d" % f
       
   247                 if pos + f < count:
       
   248                     l.append(("+" + r, pos + f))
       
   249                 if pos - f >= 0:
       
   250                     l.insert(0, ("-" + r, pos - f))
       
   251 
       
   252             yield {"rev": 0, "label": "(0)"}
       
   253 
       
   254             for label, rev in l:
       
   255                 yield {"label": label, "rev": rev}
       
   256 
       
   257             yield {"label": "tip", "rev": "tip"}
       
   258 
       
   259         def changelist(**map):
       
   260             parity = (start - end) & 1
       
   261             cl = self.repo.changelog
       
   262             l = [] # build a list in forward order for efficiency
       
   263             for i in range(start, end):
       
   264                 n = cl.node(i)
       
   265                 changes = cl.read(n)
       
   266                 hn = hex(n)
       
   267 
       
   268                 l.insert(0, {"parity": parity,
       
   269                              "author": changes[1],
       
   270                              "parent": self.siblings(cl.parents(n), cl.rev,
       
   271                                                      cl.rev(n) - 1),
       
   272                              "child": self.siblings(cl.children(n), cl.rev,
       
   273                                                     cl.rev(n) + 1),
       
   274                              "changelogtag": self.showtag("changelogtag",n),
       
   275                              "manifest": hex(changes[0]),
       
   276                              "desc": changes[4],
       
   277                              "date": changes[2],
       
   278                              "files": self.listfilediffs(changes[3], n),
       
   279                              "rev": i,
       
   280                              "node": hn})
       
   281                 parity = 1 - parity
       
   282 
       
   283             for e in l:
       
   284                 yield e
       
   285 
       
   286         cl = self.repo.changelog
       
   287         mf = cl.read(cl.tip())[0]
       
   288         count = cl.count()
       
   289         start = max(0, pos - self.maxchanges + 1)
       
   290         end = min(count, start + self.maxchanges)
       
   291         pos = end - 1
       
   292 
       
   293         yield self.t('changelog',
       
   294                      changenav=changenav,
       
   295                      manifest=hex(mf),
       
   296                      rev=pos, changesets=count, entries=changelist,
       
   297                      archives=self.archivelist("tip"))
       
   298 
       
   299     def search(self, query):
       
   300 
       
   301         def changelist(**map):
       
   302             cl = self.repo.changelog
       
   303             count = 0
       
   304             qw = query.lower().split()
       
   305 
       
   306             def revgen():
       
   307                 for i in range(cl.count() - 1, 0, -100):
       
   308                     l = []
       
   309                     for j in range(max(0, i - 100), i):
       
   310                         n = cl.node(j)
       
   311                         changes = cl.read(n)
       
   312                         l.append((n, j, changes))
       
   313                     l.reverse()
       
   314                     for e in l:
       
   315                         yield e
       
   316 
       
   317             for n, i, changes in revgen():
       
   318                 miss = 0
       
   319                 for q in qw:
       
   320                     if not (q in changes[1].lower() or
       
   321                             q in changes[4].lower() or
       
   322                             q in " ".join(changes[3][:20]).lower()):
       
   323                         miss = 1
       
   324                         break
       
   325                 if miss:
       
   326                     continue
       
   327 
       
   328                 count += 1
       
   329                 hn = hex(n)
       
   330 
       
   331                 yield self.t('searchentry',
       
   332                              parity=count & 1,
       
   333                              author=changes[1],
       
   334                              parent=self.siblings(cl.parents(n), cl.rev),
       
   335                              child=self.siblings(cl.children(n), cl.rev),
       
   336                              changelogtag=self.showtag("changelogtag",n),
       
   337                              manifest=hex(changes[0]),
       
   338                              desc=changes[4],
       
   339                              date=changes[2],
       
   340                              files=self.listfilediffs(changes[3], n),
       
   341                              rev=i,
       
   342                              node=hn)
       
   343 
       
   344                 if count >= self.maxchanges:
       
   345                     break
       
   346 
       
   347         cl = self.repo.changelog
       
   348         mf = cl.read(cl.tip())[0]
       
   349 
       
   350         yield self.t('search',
       
   351                      query=query,
       
   352                      manifest=hex(mf),
       
   353                      entries=changelist)
       
   354 
       
   355     def changeset(self, nodeid):
       
   356         cl = self.repo.changelog
       
   357         n = self.repo.lookup(nodeid)
       
   358         nodeid = hex(n)
       
   359         changes = cl.read(n)
       
   360         p1 = cl.parents(n)[0]
       
   361 
       
   362         files = []
       
   363         mf = self.repo.manifest.read(changes[0])
       
   364         for f in changes[3]:
       
   365             files.append(self.t("filenodelink",
       
   366                                 filenode=hex(mf.get(f, nullid)), file=f))
       
   367 
       
   368         def diff(**map):
       
   369             yield self.diff(p1, n, None)
       
   370 
       
   371         yield self.t('changeset',
       
   372                      diff=diff,
       
   373                      rev=cl.rev(n),
       
   374                      node=nodeid,
       
   375                      parent=self.siblings(cl.parents(n), cl.rev),
       
   376                      child=self.siblings(cl.children(n), cl.rev),
       
   377                      changesettag=self.showtag("changesettag",n),
       
   378                      manifest=hex(changes[0]),
       
   379                      author=changes[1],
       
   380                      desc=changes[4],
       
   381                      date=changes[2],
       
   382                      files=files,
       
   383                      archives=self.archivelist(nodeid))
       
   384 
       
   385     def filelog(self, f, filenode):
       
   386         cl = self.repo.changelog
       
   387         fl = self.repo.file(f)
       
   388         filenode = hex(fl.lookup(filenode))
       
   389         count = fl.count()
       
   390 
       
   391         def entries(**map):
       
   392             l = []
       
   393             parity = (count - 1) & 1
       
   394 
       
   395             for i in range(count):
       
   396                 n = fl.node(i)
       
   397                 lr = fl.linkrev(n)
       
   398                 cn = cl.node(lr)
       
   399                 cs = cl.read(cl.node(lr))
       
   400 
       
   401                 l.insert(0, {"parity": parity,
       
   402                              "filenode": hex(n),
       
   403                              "filerev": i,
       
   404                              "file": f,
       
   405                              "node": hex(cn),
       
   406                              "author": cs[1],
       
   407                              "date": cs[2],
       
   408                              "rename": self.renamelink(fl, n),
       
   409                              "parent": self.siblings(fl.parents(n),
       
   410                                                      fl.rev, file=f),
       
   411                              "child": self.siblings(fl.children(n),
       
   412                                                     fl.rev, file=f),
       
   413                              "desc": cs[4]})
       
   414                 parity = 1 - parity
       
   415 
       
   416             for e in l:
       
   417                 yield e
       
   418 
       
   419         yield self.t("filelog", file=f, filenode=filenode, entries=entries)
       
   420 
       
   421     def filerevision(self, f, node):
       
   422         fl = self.repo.file(f)
       
   423         n = fl.lookup(node)
       
   424         node = hex(n)
       
   425         text = fl.read(n)
       
   426         changerev = fl.linkrev(n)
       
   427         cl = self.repo.changelog
       
   428         cn = cl.node(changerev)
       
   429         cs = cl.read(cn)
       
   430         mfn = cs[0]
       
   431 
       
   432         mt = mimetypes.guess_type(f)[0]
       
   433         rawtext = text
       
   434         if util.binary(text):
       
   435             mt = mt or 'application/octet-stream'
       
   436             text = "(binary:%s)" % mt
       
   437         mt = mt or 'text/plain'
       
   438 
       
   439         def lines():
       
   440             for l, t in enumerate(text.splitlines(1)):
       
   441                 yield {"line": t,
       
   442                        "linenumber": "% 6d" % (l + 1),
       
   443                        "parity": l & 1}
       
   444 
       
   445         yield self.t("filerevision",
       
   446                      file=f,
       
   447                      filenode=node,
       
   448                      path=up(f),
       
   449                      text=lines(),
       
   450                      raw=rawtext,
       
   451                      mimetype=mt,
       
   452                      rev=changerev,
       
   453                      node=hex(cn),
       
   454                      manifest=hex(mfn),
       
   455                      author=cs[1],
       
   456                      date=cs[2],
       
   457                      parent=self.siblings(fl.parents(n), fl.rev, file=f),
       
   458                      child=self.siblings(fl.children(n), fl.rev, file=f),
       
   459                      rename=self.renamelink(fl, n),
       
   460                      permissions=self.repo.manifest.readflags(mfn)[f])
       
   461 
       
   462     def fileannotate(self, f, node):
       
   463         bcache = {}
       
   464         ncache = {}
       
   465         fl = self.repo.file(f)
       
   466         n = fl.lookup(node)
       
   467         node = hex(n)
       
   468         changerev = fl.linkrev(n)
       
   469 
       
   470         cl = self.repo.changelog
       
   471         cn = cl.node(changerev)
       
   472         cs = cl.read(cn)
       
   473         mfn = cs[0]
       
   474 
       
   475         def annotate(**map):
       
   476             parity = 1
       
   477             last = None
       
   478             for r, l in fl.annotate(n):
       
   479                 try:
       
   480                     cnode = ncache[r]
       
   481                 except KeyError:
       
   482                     cnode = ncache[r] = self.repo.changelog.node(r)
       
   483 
       
   484                 try:
       
   485                     name = bcache[r]
       
   486                 except KeyError:
       
   487                     cl = self.repo.changelog.read(cnode)
       
   488                     bcache[r] = name = self.repo.ui.shortuser(cl[1])
       
   489 
       
   490                 if last != cnode:
       
   491                     parity = 1 - parity
       
   492                     last = cnode
       
   493 
       
   494                 yield {"parity": parity,
       
   495                        "node": hex(cnode),
       
   496                        "rev": r,
       
   497                        "author": name,
       
   498                        "file": f,
       
   499                        "line": l}
       
   500 
       
   501         yield self.t("fileannotate",
       
   502                      file=f,
       
   503                      filenode=node,
       
   504                      annotate=annotate,
       
   505                      path=up(f),
       
   506                      rev=changerev,
       
   507                      node=hex(cn),
       
   508                      manifest=hex(mfn),
       
   509                      author=cs[1],
       
   510                      date=cs[2],
       
   511                      rename=self.renamelink(fl, n),
       
   512                      parent=self.siblings(fl.parents(n), fl.rev, file=f),
       
   513                      child=self.siblings(fl.children(n), fl.rev, file=f),
       
   514                      permissions=self.repo.manifest.readflags(mfn)[f])
       
   515 
       
   516     def manifest(self, mnode, path):
       
   517         man = self.repo.manifest
       
   518         mn = man.lookup(mnode)
       
   519         mnode = hex(mn)
       
   520         mf = man.read(mn)
       
   521         rev = man.rev(mn)
       
   522         node = self.repo.changelog.node(rev)
       
   523         mff = man.readflags(mn)
       
   524 
       
   525         files = {}
       
   526 
       
   527         p = path[1:]
       
   528         if p and p[-1] != "/":
       
   529             p += "/"
       
   530         l = len(p)
       
   531 
       
   532         for f,n in mf.items():
       
   533             if f[:l] != p:
       
   534                 continue
       
   535             remain = f[l:]
       
   536             if "/" in remain:
       
   537                 short = remain[:remain.find("/") + 1] # bleah
       
   538                 files[short] = (f, None)
       
   539             else:
       
   540                 short = os.path.basename(remain)
       
   541                 files[short] = (f, n)
       
   542 
       
   543         def filelist(**map):
       
   544             parity = 0
       
   545             fl = files.keys()
       
   546             fl.sort()
       
   547             for f in fl:
       
   548                 full, fnode = files[f]
       
   549                 if not fnode:
       
   550                     continue
       
   551 
       
   552                 yield {"file": full,
       
   553                        "manifest": mnode,
       
   554                        "filenode": hex(fnode),
       
   555                        "parity": parity,
       
   556                        "basename": f,
       
   557                        "permissions": mff[full]}
       
   558                 parity = 1 - parity
       
   559 
       
   560         def dirlist(**map):
       
   561             parity = 0
       
   562             fl = files.keys()
       
   563             fl.sort()
       
   564             for f in fl:
       
   565                 full, fnode = files[f]
       
   566                 if fnode:
       
   567                     continue
       
   568 
       
   569                 yield {"parity": parity,
       
   570                        "path": os.path.join(path, f),
       
   571                        "manifest": mnode,
       
   572                        "basename": f[:-1]}
       
   573                 parity = 1 - parity
       
   574 
       
   575         yield self.t("manifest",
       
   576                      manifest=mnode,
       
   577                      rev=rev,
       
   578                      node=hex(node),
       
   579                      path=path,
       
   580                      up=up(path),
       
   581                      fentries=filelist,
       
   582                      dentries=dirlist,
       
   583                      archives=self.archivelist(hex(node)))
       
   584 
       
   585     def tags(self):
       
   586         cl = self.repo.changelog
       
   587         mf = cl.read(cl.tip())[0]
       
   588 
       
   589         i = self.repo.tagslist()
       
   590         i.reverse()
       
   591 
       
   592         def entries(notip=False, **map):
       
   593             parity = 0
       
   594             for k,n in i:
       
   595                 if notip and k == "tip": continue
       
   596                 yield {"parity": parity,
       
   597                        "tag": k,
       
   598                        "tagmanifest": hex(cl.read(n)[0]),
       
   599                        "date": cl.read(n)[2],
       
   600                        "node": hex(n)}
       
   601                 parity = 1 - parity
       
   602 
       
   603         yield self.t("tags",
       
   604                      manifest=hex(mf),
       
   605                      entries=lambda **x: entries(False, **x),
       
   606                      entriesnotip=lambda **x: entries(True, **x))
       
   607 
       
   608     def summary(self):
       
   609         cl = self.repo.changelog
       
   610         mf = cl.read(cl.tip())[0]
       
   611 
       
   612         i = self.repo.tagslist()
       
   613         i.reverse()
       
   614 
       
   615         def tagentries(**map):
       
   616             parity = 0
       
   617             count = 0
       
   618             for k,n in i:
       
   619                 if k == "tip": # skip tip
       
   620                     continue;
       
   621 
       
   622                 count += 1
       
   623                 if count > 10: # limit to 10 tags
       
   624                     break;
       
   625 
       
   626                 c = cl.read(n)
       
   627                 m = c[0]
       
   628                 t = c[2]
       
   629 
       
   630                 yield self.t("tagentry",
       
   631                              parity = parity,
       
   632                              tag = k,
       
   633                              node = hex(n),
       
   634                              date = t,
       
   635                              tagmanifest = hex(m))
       
   636                 parity = 1 - parity
       
   637 
       
   638         def changelist(**map):
       
   639             parity = 0
       
   640             cl = self.repo.changelog
       
   641             l = [] # build a list in forward order for efficiency
       
   642             for i in range(start, end):
       
   643                 n = cl.node(i)
       
   644                 changes = cl.read(n)
       
   645                 hn = hex(n)
       
   646                 t = changes[2]
       
   647 
       
   648                 l.insert(0, self.t(
       
   649                     'shortlogentry',
       
   650                     parity = parity,
       
   651                     author = changes[1],
       
   652                     manifest = hex(changes[0]),
       
   653                     desc = changes[4],
       
   654                     date = t,
       
   655                     rev = i,
       
   656                     node = hn))
       
   657                 parity = 1 - parity
       
   658 
       
   659             yield l
       
   660 
       
   661         cl = self.repo.changelog
       
   662         mf = cl.read(cl.tip())[0]
       
   663         count = cl.count()
       
   664         start = max(0, count - self.maxchanges)
       
   665         end = min(count, start + self.maxchanges)
       
   666         pos = end - 1
       
   667 
       
   668         yield self.t("summary",
       
   669                  desc = self.repo.ui.config("web", "description", "unknown"),
       
   670                  owner = (self.repo.ui.config("ui", "username") or # preferred
       
   671                           self.repo.ui.config("web", "contact") or # deprecated
       
   672                           self.repo.ui.config("web", "author", "unknown")), # also
       
   673                  lastchange = (0, 0), # FIXME
       
   674                  manifest = hex(mf),
       
   675                  tags = tagentries,
       
   676                  shortlog = changelist)
       
   677 
       
   678     def filediff(self, file, changeset):
       
   679         cl = self.repo.changelog
       
   680         n = self.repo.lookup(changeset)
       
   681         changeset = hex(n)
       
   682         p1 = cl.parents(n)[0]
       
   683         cs = cl.read(n)
       
   684         mf = self.repo.manifest.read(cs[0])
       
   685 
       
   686         def diff(**map):
       
   687             yield self.diff(p1, n, [file])
       
   688 
       
   689         yield self.t("filediff",
       
   690                      file=file,
       
   691                      filenode=hex(mf.get(file, nullid)),
       
   692                      node=changeset,
       
   693                      rev=self.repo.changelog.rev(n),
       
   694                      parent=self.siblings(cl.parents(n), cl.rev),
       
   695                      child=self.siblings(cl.children(n), cl.rev),
       
   696                      diff=diff)
       
   697 
       
   698     archive_specs = {
       
   699         'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
       
   700         'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
       
   701         'zip': ('application/zip', 'zip', '.zip', None),
       
   702         }
       
   703 
       
   704     def archive(self, req, cnode, type):
       
   705         reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
       
   706         name = "%s-%s" % (reponame, short(cnode))
       
   707         mimetype, artype, extension, encoding = self.archive_specs[type]
       
   708         headers = [('Content-type', mimetype),
       
   709                    ('Content-disposition', 'attachment; filename=%s%s' %
       
   710                     (name, extension))]
       
   711         if encoding:
       
   712             headers.append(('Content-encoding', encoding))
       
   713         req.header(headers)
       
   714         archival.archive(self.repo, req.out, cnode, artype, prefix=name)
       
   715 
       
   716     # add tags to things
       
   717     # tags -> list of changesets corresponding to tags
       
   718     # find tag, changeset, file
       
   719 
       
   720     def run(self, req=hgrequest()):
       
   721         def clean(path):
       
   722             p = util.normpath(path)
       
   723             if p[:2] == "..":
       
   724                 raise "suspicious path"
       
   725             return p
       
   726 
       
   727         def header(**map):
       
   728             yield self.t("header", **map)
       
   729 
       
   730         def footer(**map):
       
   731             yield self.t("footer",
       
   732                          motd=self.repo.ui.config("web", "motd", ""),
       
   733                          **map)
       
   734 
       
   735         def expand_form(form):
       
   736             shortcuts = {
       
   737                 'cl': [('cmd', ['changelog']), ('rev', None)],
       
   738                 'cs': [('cmd', ['changeset']), ('node', None)],
       
   739                 'f': [('cmd', ['file']), ('filenode', None)],
       
   740                 'fl': [('cmd', ['filelog']), ('filenode', None)],
       
   741                 'fd': [('cmd', ['filediff']), ('node', None)],
       
   742                 'fa': [('cmd', ['annotate']), ('filenode', None)],
       
   743                 'mf': [('cmd', ['manifest']), ('manifest', None)],
       
   744                 'ca': [('cmd', ['archive']), ('node', None)],
       
   745                 'tags': [('cmd', ['tags'])],
       
   746                 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
       
   747                 'static': [('cmd', ['static']), ('file', None)]
       
   748             }
       
   749 
       
   750             for k in shortcuts.iterkeys():
       
   751                 if form.has_key(k):
       
   752                     for name, value in shortcuts[k]:
       
   753                         if value is None:
       
   754                             value = form[k]
       
   755                         form[name] = value
       
   756                     del form[k]
       
   757 
       
   758         self.refresh()
       
   759 
       
   760         expand_form(req.form)
       
   761 
       
   762         t = self.repo.ui.config("web", "templates", templater.templatepath())
       
   763         static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
       
   764         m = os.path.join(t, "map")
       
   765         style = self.repo.ui.config("web", "style", "")
       
   766         if req.form.has_key('style'):
       
   767             style = req.form['style'][0]
       
   768         if style:
       
   769             b = os.path.basename("map-" + style)
       
   770             p = os.path.join(t, b)
       
   771             if os.path.isfile(p):
       
   772                 m = p
       
   773 
       
   774         port = req.env["SERVER_PORT"]
       
   775         port = port != "80" and (":" + port) or ""
       
   776         uri = req.env["REQUEST_URI"]
       
   777         if "?" in uri:
       
   778             uri = uri.split("?")[0]
       
   779         url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
       
   780         if not self.reponame:
       
   781             self.reponame = (self.repo.ui.config("web", "name")
       
   782                              or uri.strip('/') or self.repo.root)
       
   783 
       
   784         self.t = templater.templater(m, templater.common_filters,
       
   785                                      defaults={"url": url,
       
   786                                                "repo": self.reponame,
       
   787                                                "header": header,
       
   788                                                "footer": footer,
       
   789                                                })
       
   790 
       
   791         if not req.form.has_key('cmd'):
       
   792             req.form['cmd'] = [self.t.cache['default'],]
       
   793 
       
   794         cmd = req.form['cmd'][0]
       
   795         if cmd == 'changelog':
       
   796             hi = self.repo.changelog.count() - 1
       
   797             if req.form.has_key('rev'):
       
   798                 hi = req.form['rev'][0]
       
   799                 try:
       
   800                     hi = self.repo.changelog.rev(self.repo.lookup(hi))
       
   801                 except hg.RepoError:
       
   802                     req.write(self.search(hi)) # XXX redirect to 404 page?
       
   803                     return
       
   804 
       
   805             req.write(self.changelog(hi))
       
   806 
       
   807         elif cmd == 'changeset':
       
   808             req.write(self.changeset(req.form['node'][0]))
       
   809 
       
   810         elif cmd == 'manifest':
       
   811             req.write(self.manifest(req.form['manifest'][0],
       
   812                                     clean(req.form['path'][0])))
       
   813 
       
   814         elif cmd == 'tags':
       
   815             req.write(self.tags())
       
   816 
       
   817         elif cmd == 'summary':
       
   818             req.write(self.summary())
       
   819 
       
   820         elif cmd == 'filediff':
       
   821             req.write(self.filediff(clean(req.form['file'][0]),
       
   822                                     req.form['node'][0]))
       
   823 
       
   824         elif cmd == 'file':
       
   825             req.write(self.filerevision(clean(req.form['file'][0]),
       
   826                                         req.form['filenode'][0]))
       
   827 
       
   828         elif cmd == 'annotate':
       
   829             req.write(self.fileannotate(clean(req.form['file'][0]),
       
   830                                         req.form['filenode'][0]))
       
   831 
       
   832         elif cmd == 'filelog':
       
   833             req.write(self.filelog(clean(req.form['file'][0]),
       
   834                                    req.form['filenode'][0]))
       
   835 
       
   836         elif cmd == 'heads':
       
   837             req.httphdr("application/mercurial-0.1")
       
   838             h = self.repo.heads()
       
   839             req.write(" ".join(map(hex, h)) + "\n")
       
   840 
       
   841         elif cmd == 'branches':
       
   842             req.httphdr("application/mercurial-0.1")
       
   843             nodes = []
       
   844             if req.form.has_key('nodes'):
       
   845                 nodes = map(bin, req.form['nodes'][0].split(" "))
       
   846             for b in self.repo.branches(nodes):
       
   847                 req.write(" ".join(map(hex, b)) + "\n")
       
   848 
       
   849         elif cmd == 'between':
       
   850             req.httphdr("application/mercurial-0.1")
       
   851             nodes = []
       
   852             if req.form.has_key('pairs'):
       
   853                 pairs = [map(bin, p.split("-"))
       
   854                          for p in req.form['pairs'][0].split(" ")]
       
   855             for b in self.repo.between(pairs):
       
   856                 req.write(" ".join(map(hex, b)) + "\n")
       
   857 
       
   858         elif cmd == 'changegroup':
       
   859             req.httphdr("application/mercurial-0.1")
       
   860             nodes = []
       
   861             if not self.allowpull:
       
   862                 return
       
   863 
       
   864             if req.form.has_key('roots'):
       
   865                 nodes = map(bin, req.form['roots'][0].split(" "))
       
   866 
       
   867             z = zlib.compressobj()
       
   868             f = self.repo.changegroup(nodes, 'serve')
       
   869             while 1:
       
   870                 chunk = f.read(4096)
       
   871                 if not chunk:
       
   872                     break
       
   873                 req.write(z.compress(chunk))
       
   874 
       
   875             req.write(z.flush())
       
   876 
       
   877         elif cmd == 'archive':
       
   878             changeset = self.repo.lookup(req.form['node'][0])
       
   879             type = req.form['type'][0]
       
   880             if (type in self.archives and
       
   881                 self.repo.ui.configbool("web", "allow" + type, False)):
       
   882                 self.archive(req, changeset, type)
       
   883                 return
       
   884 
       
   885             req.write(self.t("error"))
       
   886 
       
   887         elif cmd == 'static':
       
   888             fname = req.form['file'][0]
       
   889             req.write(staticfile(static, fname)
       
   890                       or self.t("error", error="%r not found" % fname))
       
   891 
       
   892         else:
       
   893             req.write(self.t("error"))
       
   894 
       
   895 def create_server(ui, repo):
       
   896     use_threads = True
       
   897 
       
   898     def openlog(opt, default):
       
   899         if opt and opt != '-':
       
   900             return open(opt, 'w')
       
   901         return default
       
   902 
       
   903     address = ui.config("web", "address", "")
       
   904     port = int(ui.config("web", "port", 8000))
       
   905     use_ipv6 = ui.configbool("web", "ipv6")
       
   906     webdir_conf = ui.config("web", "webdir_conf")
       
   907     accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout)
       
   908     errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr)
       
   909 
       
   910     if use_threads:
       
   911         try:
       
   912             from threading import activeCount
       
   913         except ImportError:
       
   914             use_threads = False
       
   915 
       
   916     if use_threads:
       
   917         _mixin = SocketServer.ThreadingMixIn
       
   918     else:
       
   919         if hasattr(os, "fork"):
       
   920             _mixin = SocketServer.ForkingMixIn
       
   921         else:
       
   922             class _mixin: pass
       
   923 
       
   924     class MercurialHTTPServer(_mixin, BaseHTTPServer.HTTPServer):
       
   925         pass
       
   926 
       
   927     class IPv6HTTPServer(MercurialHTTPServer):
       
   928         address_family = getattr(socket, 'AF_INET6', None)
       
   929 
       
   930         def __init__(self, *args, **kwargs):
       
   931             if self.address_family is None:
       
   932                 raise hg.RepoError(_('IPv6 not available on this system'))
       
   933             BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
       
   934 
       
   935     class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler):
       
   936 
       
   937         def log_error(self, format, *args):
       
   938             errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
       
   939                                                  self.log_date_time_string(),
       
   940                                                  format % args))
       
   941 
       
   942         def log_message(self, format, *args):
       
   943             accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
       
   944                                                   self.log_date_time_string(),
       
   945                                                   format % args))
       
   946 
       
   947         def do_POST(self):
       
   948             try:
       
   949                 self.do_hgweb()
       
   950             except socket.error, inst:
       
   951                 if inst[0] != errno.EPIPE:
       
   952                     raise
       
   953 
       
   954         def do_GET(self):
       
   955             self.do_POST()
       
   956 
       
   957         def do_hgweb(self):
       
   958             path_info, query = splitURI(self.path)
       
   959 
       
   960             env = {}
       
   961             env['GATEWAY_INTERFACE'] = 'CGI/1.1'
       
   962             env['REQUEST_METHOD'] = self.command
       
   963             env['SERVER_NAME'] = self.server.server_name
       
   964             env['SERVER_PORT'] = str(self.server.server_port)
       
   965             env['REQUEST_URI'] = "/"
       
   966             env['PATH_INFO'] = path_info
       
   967             if query:
       
   968                 env['QUERY_STRING'] = query
       
   969             host = self.address_string()
       
   970             if host != self.client_address[0]:
       
   971                 env['REMOTE_HOST'] = host
       
   972                 env['REMOTE_ADDR'] = self.client_address[0]
       
   973 
       
   974             if self.headers.typeheader is None:
       
   975                 env['CONTENT_TYPE'] = self.headers.type
       
   976             else:
       
   977                 env['CONTENT_TYPE'] = self.headers.typeheader
       
   978             length = self.headers.getheader('content-length')
       
   979             if length:
       
   980                 env['CONTENT_LENGTH'] = length
       
   981             accept = []
       
   982             for line in self.headers.getallmatchingheaders('accept'):
       
   983                 if line[:1] in "\t\n\r ":
       
   984                     accept.append(line.strip())
       
   985                 else:
       
   986                     accept = accept + line[7:].split(',')
       
   987             env['HTTP_ACCEPT'] = ','.join(accept)
       
   988 
       
   989             req = hgrequest(self.rfile, self.wfile, env)
       
   990             self.send_response(200, "Script output follows")
       
   991 
       
   992             if webdir_conf:
       
   993                 hgwebobj = hgwebdir(webdir_conf)
       
   994             elif repo is not None:
       
   995                 hgwebobj = hgweb(repo.__class__(repo.ui, repo.origroot))
       
   996             else:
       
   997                 raise hg.RepoError(_('no repo found'))
       
   998             hgwebobj.run(req)
       
   999 
       
  1000 
       
  1001     if use_ipv6:
       
  1002         return IPv6HTTPServer((address, port), hgwebhandler)
       
  1003     else:
       
  1004         return MercurialHTTPServer((address, port), hgwebhandler)
       
  1005 
       
  1006 # This is a stopgap
       
  1007 class hgwebdir(object):
       
  1008     def __init__(self, config):
       
  1009         def cleannames(items):
       
  1010             return [(name.strip(os.sep), path) for name, path in items]
       
  1011 
       
  1012         self.motd = ""
       
  1013         self.repos_sorted = ('name', False)
       
  1014         if isinstance(config, (list, tuple)):
       
  1015             self.repos = cleannames(config)
       
  1016             self.repos_sorted = ('', False)
       
  1017         elif isinstance(config, dict):
       
  1018             self.repos = cleannames(config.items())
       
  1019             self.repos.sort()
       
  1020         else:
       
  1021             cp = ConfigParser.SafeConfigParser()
       
  1022             cp.read(config)
       
  1023             self.repos = []
       
  1024             if cp.has_section('web') and cp.has_option('web', 'motd'):
       
  1025                 self.motd = cp.get('web', 'motd')
       
  1026             if cp.has_section('paths'):
       
  1027                 self.repos.extend(cleannames(cp.items('paths')))
       
  1028             if cp.has_section('collections'):
       
  1029                 for prefix, root in cp.items('collections'):
       
  1030                     for path in util.walkrepos(root):
       
  1031                         repo = os.path.normpath(path)
       
  1032                         name = repo
       
  1033                         if name.startswith(prefix):
       
  1034                             name = name[len(prefix):]
       
  1035                         self.repos.append((name.lstrip(os.sep), repo))
       
  1036             self.repos.sort()
       
  1037 
       
  1038     def run(self, req=hgrequest()):
       
  1039         def header(**map):
       
  1040             yield tmpl("header", **map)
       
  1041 
       
  1042         def footer(**map):
       
  1043             yield tmpl("footer", motd=self.motd, **map)
       
  1044 
       
  1045         m = os.path.join(templater.templatepath(), "map")
       
  1046         tmpl = templater.templater(m, templater.common_filters,
       
  1047                                    defaults={"header": header,
       
  1048                                              "footer": footer})
       
  1049 
       
  1050         def archivelist(ui, nodeid, url):
       
  1051             for i in ['zip', 'gz', 'bz2']:
       
  1052                 if ui.configbool("web", "allow" + i, False):
       
  1053                     yield {"type" : i, "node": nodeid, "url": url}
       
  1054 
       
  1055         def entries(sortcolumn="", descending=False, **map):
       
  1056             rows = []
       
  1057             parity = 0
       
  1058             for name, path in self.repos:
       
  1059                 u = ui.ui()
       
  1060                 try:
       
  1061                     u.readconfig(os.path.join(path, '.hg', 'hgrc'))
       
  1062                 except IOError:
       
  1063                     pass
       
  1064                 get = u.config
       
  1065 
       
  1066                 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
       
  1067                        .replace("//", "/"))
       
  1068 
       
  1069                 # update time with local timezone
       
  1070                 try:
       
  1071                     d = (get_mtime(path), util.makedate()[1])
       
  1072                 except OSError:
       
  1073                     continue
       
  1074 
       
  1075                 contact = (get("ui", "username") or # preferred
       
  1076                            get("web", "contact") or # deprecated
       
  1077                            get("web", "author", "")) # also
       
  1078                 description = get("web", "description", "")
       
  1079                 name = get("web", "name", name)
       
  1080                 row = dict(contact=contact or "unknown",
       
  1081                            contact_sort=contact.upper() or "unknown",
       
  1082                            name=name,
       
  1083                            name_sort=name,
       
  1084                            url=url,
       
  1085                            description=description or "unknown",
       
  1086                            description_sort=description.upper() or "unknown",
       
  1087                            lastchange=d,
       
  1088                            lastchange_sort=d[1]-d[0],
       
  1089                            archives=archivelist(u, "tip", url))
       
  1090                 if (not sortcolumn
       
  1091                     or (sortcolumn, descending) == self.repos_sorted):
       
  1092                     # fast path for unsorted output
       
  1093                     row['parity'] = parity
       
  1094                     parity = 1 - parity
       
  1095                     yield row
       
  1096                 else:
       
  1097                     rows.append((row["%s_sort" % sortcolumn], row))
       
  1098             if rows:
       
  1099                 rows.sort()
       
  1100                 if descending:
       
  1101                     rows.reverse()
       
  1102                 for key, row in rows:
       
  1103                     row['parity'] = parity
       
  1104                     parity = 1 - parity
       
  1105                     yield row
       
  1106 
       
  1107         virtual = req.env.get("PATH_INFO", "").strip('/')
       
  1108         if virtual:
       
  1109             real = dict(self.repos).get(virtual)
       
  1110             if real:
       
  1111                 try:
       
  1112                     hgweb(real).run(req)
       
  1113                 except IOError, inst:
       
  1114                     req.write(tmpl("error", error=inst.strerror))
       
  1115                 except hg.RepoError, inst:
       
  1116                     req.write(tmpl("error", error=str(inst)))
       
  1117             else:
       
  1118                 req.write(tmpl("notfound", repo=virtual))
       
  1119         else:
       
  1120             if req.form.has_key('static'):
       
  1121                 static = os.path.join(templater.templatepath(), "static")
       
  1122                 fname = req.form['static'][0]
       
  1123                 req.write(staticfile(static, fname)
       
  1124                           or tmpl("error", error="%r not found" % fname))
       
  1125             else:
       
  1126                 sortable = ["name", "description", "contact", "lastchange"]
       
  1127                 sortcolumn, descending = self.repos_sorted
       
  1128                 if req.form.has_key('sort'):
       
  1129                     sortcolumn = req.form['sort'][0]
       
  1130                     descending = sortcolumn.startswith('-')
       
  1131                     if descending:
       
  1132                         sortcolumn = sortcolumn[1:]
       
  1133                     if sortcolumn not in sortable:
       
  1134                         sortcolumn = ""
       
  1135 
       
  1136                 sort = [("sort_%s" % column,
       
  1137                          "%s%s" % ((not descending and column == sortcolumn)
       
  1138                                    and "-" or "", column))
       
  1139                         for column in sortable]
       
  1140                 req.write(tmpl("index", entries=entries,
       
  1141                                sortcolumn=sortcolumn, descending=descending,
       
  1142                                **dict(sort)))