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