Mercurial > hg
changeset 5618:6e1a61b14bbf
Merge with stable
author | Bryan O'Sullivan <bos@serpentine.com> |
---|---|
date | Thu, 06 Dec 2007 13:11:36 -0800 |
parents | 88ca3e0fb6e5 (diff) 924fd86f0579 (current diff) |
children | 55d3e845736a |
files | hgext/convert/subversion.py |
diffstat | 78 files changed, 3593 insertions(+), 1368 deletions(-) [+] |
line wrap: on
line diff
--- a/CONTRIBUTORS Thu Dec 06 13:10:25 2007 -0800 +++ b/CONTRIBUTORS Thu Dec 06 13:11:36 2007 -0800 @@ -1,4 +1,7 @@ -Andrea Arcangeli <andrea at suse.de> +[This file is here for historical purposes, all recent contributors +should appear in the changelog directly] + +Andrea Arcangeli <andrea at suse.de> Thomas Arendsen Hein <thomas at intevation.de> Goffredo Baroncelli <kreijack at libero.it> Muli Ben-Yehuda <mulix at mulix.org> @@ -36,5 +39,3 @@ Rafael Villar Burke <pachi at mmn-arquitectos.com> Tristan Wibberley <tristan at wibberley.org> Mark Williamson <mark.williamson at cl.cam.ac.uk> - -If you are a contributor and don't see your name here, please let me know.
--- a/contrib/bash_completion Thu Dec 06 13:10:25 2007 -0800 +++ b/contrib/bash_completion Thu Dec 06 13:11:36 2007 -0800 @@ -305,6 +305,15 @@ _hg_ext_mq_patchlist qunapplied } +_hg_cmd_qgoto() +{ + if [[ "$prev" = @(-n|--name) ]]; then + _hg_ext_mq_queues + return + fi + _hg_ext_mq_patchlist qseries +} + _hg_cmd_qdelete() { local qcmd=qunapplied
--- a/contrib/hgk Thu Dec 06 13:10:25 2007 -0800 +++ b/contrib/hgk Thu Dec 06 13:11:36 2007 -0800 @@ -649,7 +649,7 @@ if {$stuffsaved} return if {![winfo viewable .]} return catch { - set f [open "~/.gitk-new" w] + set f [open "~/.hgk-new" w] puts $f [list set mainfont $mainfont] puts $f [list set curidfont $curidfont] puts $f [list set textfont $textfont] @@ -687,7 +687,7 @@ puts $f "#" puts $f "set authorcolors {$authorcolors}" close $f - file rename -force "~/.gitk-new" "~/.gitk" + file rename -force "~/.hgk-new" "~/.hgk" } set stuffsaved 1 } @@ -3847,7 +3847,7 @@ deeppink mediumorchid blue burlywood4 goldenrod slateblue red2 navy dimgrey } -catch {source ~/.gitk} +catch {source ~/.hgk} if {$curidfont == ""} { # initialize late based on current mainfont set curidfont "$mainfont bold italic underline"
--- a/doc/hgrc.5.txt Thu Dec 06 13:10:25 2007 -0800 +++ b/doc/hgrc.5.txt Thu Dec 06 13:11:36 2007 -0800 @@ -17,7 +17,9 @@ Mercurial reads configuration data from several files, if they exist. The names of these files depend on the system on which Mercurial is -installed. +installed. Windows registry keys contain PATH-like strings, every +part must reference a Mercurial.ini file or be a directory where *.rc +files will be read. (Unix) <install-root>/etc/mercurial/hgrc.d/*.rc:: (Unix) <install-root>/etc/mercurial/hgrc:: @@ -29,6 +31,8 @@ (Unix) /etc/mercurial/hgrc.d/*.rc:: (Unix) /etc/mercurial/hgrc:: +(Windows) HKEY_LOCAL_MACHINE\SOFTWARE\Mercurial:: + or:: (Windows) C:\Mercurial\Mercurial.ini:: Per-system configuration files, for the system on which Mercurial is running. Options in these files apply to all Mercurial
--- a/hg Thu Dec 06 13:10:25 2007 -0800 +++ b/hg Thu Dec 06 13:11:36 2007 -0800 @@ -10,5 +10,11 @@ # enable importing on demand to reduce startup time from mercurial import demandimport; demandimport.enable() +import sys +import mercurial.util import mercurial.dispatch + +for fp in (sys.stdin, sys.stdout, sys.stderr): + mercurial.util.set_binary(fp) + mercurial.dispatch.run()
--- a/hgext/convert/__init__.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/convert/__init__.py Thu Dec 06 13:11:36 2007 -0800 @@ -5,12 +5,12 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -from common import NoRepo, SKIPREV, converter_source, converter_sink +from common import NoRepo, SKIPREV, converter_source, converter_sink, mapfile from cvs import convert_cvs from darcs import darcs_source from git import convert_git from hg import mercurial_source, mercurial_sink -from subversion import svn_source, debugsvnlog +from subversion import debugsvnlog, svn_source, svn_sink import filemap import os, shutil @@ -29,6 +29,7 @@ sink_converters = [ ('hg', mercurial_sink), + ('svn', svn_sink), ] def convertsource(ui, path, type, rev): @@ -61,23 +62,10 @@ self.ui = ui self.opts = opts self.commitcache = {} - self.revmapfile = revmapfile - self.revmapfilefd = None self.authors = {} self.authorfile = None - self.maporder = [] - self.map = {} - try: - origrevmapfile = open(self.revmapfile, 'r') - for l in origrevmapfile: - sv, dv = l[:-1].split() - if sv not in self.map: - self.maporder.append(sv) - self.map[sv] = dv - origrevmapfile.close() - except IOError: - pass + self.map = mapfile(ui, revmapfile) # Read first the dst author map if any authorfile = self.dest.authorfile() @@ -165,16 +153,6 @@ return s - def mapentry(self, src, dst): - if self.revmapfilefd is None: - try: - self.revmapfilefd = open(self.revmapfile, "a") - except IOError, (errno, strerror): - raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror)) - self.map[src] = dst - self.revmapfilefd.write("%s %s\n" % (src, dst)) - self.revmapfilefd.flush() - def writeauthormap(self): authorfile = self.authorfile if authorfile: @@ -221,7 +199,7 @@ dest = SKIPREV else: dest = self.map[changes] - self.mapentry(rev, dest) + self.map[rev] = dest return files, copies = changes parents = [self.map[r] for r in commit.parents] @@ -249,13 +227,14 @@ self.dest.copyfile(copyf, f) newnode = self.dest.putcommit(filenames, parents, commit) - self.mapentry(rev, newnode) + self.source.converted(rev, newnode) + self.map[rev] = newnode def convert(self): try: self.source.before() self.dest.before() - self.source.setrevmap(self.map, self.maporder) + self.source.setrevmap(self.map) self.ui.status("scanning source...\n") heads = self.source.getheads() parents = self.walktree(heads) @@ -285,7 +264,7 @@ # write another hash correspondence to override the previous # one so we don't end up with extra tag heads if nrev: - self.mapentry(c, nrev) + self.map[c] = nrev self.writeauthormap() finally: @@ -296,8 +275,7 @@ self.dest.after() finally: self.source.after() - if self.revmapfilefd: - self.revmapfilefd.close() + self.map.close() def convert(ui, src, dest=None, revmapfile=None, **opts): """Convert a foreign SCM repository to a Mercurial one. @@ -311,6 +289,7 @@ Accepted destination formats: - Mercurial + - Subversion (history on branches is not preserved) If no revision is given, all revisions will be converted. Otherwise, convert will only import up to the named revision (given in a format @@ -353,6 +332,24 @@ The 'rename' directive renames a file or directory. To rename from a subdirectory into the root of the repository, use '.' as the path to rename to. + + Back end options: + + --config convert.hg.clonebranches=False (boolean) + hg target: XXX not documented + --config convert.hg.saverev=True (boolean) + hg source: allow target to preserve source revision ID + --config convert.hg.tagsbranch=default (branch name) + hg target: XXX not documented + --config convert.hg.usebranchnames=True (boolean) + hg target: preserve branch names + + --config convert.svn.branches=branches (directory name) + svn source: specify the directory containing branches + --config convert.svn.tags=tags (directory name) + svn source: specify the directory containing tags + --config convert.svn.trunk=trunk (directory name) + svn source: specify the name of the trunk branch """ util._encoding = 'UTF-8'
--- a/hgext/convert/common.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/convert/common.py Thu Dec 06 13:11:36 2007 -0800 @@ -1,6 +1,8 @@ # common code for the convert extension -import base64 +import base64, errno import cPickle as pickle +from mercurial import util +from mercurial.i18n import _ def encodeargs(args): def encodearg(s): @@ -15,6 +17,11 @@ s = base64.decodestring(s) return pickle.loads(s) +def checktool(exe, name=None): + name = name or exe + if not util.find_exe(exe): + raise util.Abort('cannot find required "%s" tool' % name) + class NoRepo(Exception): pass SKIPREV = 'SKIP' @@ -33,7 +40,7 @@ class converter_source(object): """Conversion source interface""" - def __init__(self, ui, path, rev=None): + def __init__(self, ui, path=None, rev=None): """Initialize conversion source (or raise NoRepo("message") exception if path is not a valid repository)""" self.ui = ui @@ -48,11 +55,8 @@ def after(self): pass - def setrevmap(self, revmap, order): - """set the map of already-converted revisions - - order is a list with the keys from revmap in the order they - appear in the revision map file.""" + def setrevmap(self, revmap): + """set the map of already-converted revisions""" pass def getheads(self): @@ -111,6 +115,11 @@ """ raise NotImplementedError() + def converted(self, rev, sinkrev): + '''Notify the source that a revision has been converted.''' + pass + + class converter_sink(object): """Conversion sink (target) interface""" @@ -184,3 +193,105 @@ filter empty revisions. """ pass + + def before(self): + pass + + def after(self): + pass + + +class commandline(object): + def __init__(self, ui, command): + self.ui = ui + self.command = command + + def prerun(self): + pass + + def postrun(self): + pass + + def _run(self, cmd, *args, **kwargs): + cmdline = [self.command, cmd] + list(args) + for k, v in kwargs.iteritems(): + if len(k) == 1: + cmdline.append('-' + k) + else: + cmdline.append('--' + k.replace('_', '-')) + try: + if len(k) == 1: + cmdline.append('' + v) + else: + cmdline[-1] += '=' + v + except TypeError: + pass + cmdline = [util.shellquote(arg) for arg in cmdline] + cmdline += ['<', util.nulldev] + cmdline = ' '.join(cmdline) + self.ui.debug(cmdline, '\n') + + self.prerun() + try: + return util.popen(cmdline) + finally: + self.postrun() + + def run(self, cmd, *args, **kwargs): + fp = self._run(cmd, *args, **kwargs) + output = fp.read() + self.ui.debug(output) + return output, fp.close() + + def checkexit(self, status, output=''): + if status: + if output: + self.ui.warn(_('%s error:\n') % self.command) + self.ui.warn(output) + msg = util.explain_exit(status)[0] + raise util.Abort(_('%s %s') % (self.command, msg)) + + def run0(self, cmd, *args, **kwargs): + output, status = self.run(cmd, *args, **kwargs) + self.checkexit(status, output) + return output + + +class mapfile(dict): + def __init__(self, ui, path): + super(mapfile, self).__init__() + self.ui = ui + self.path = path + self.fp = None + self.order = [] + self._read() + + def _read(self): + try: + fp = open(self.path, 'r') + except IOError, err: + if err.errno != errno.ENOENT: + raise + return + for line in fp: + key, value = line[:-1].split(' ', 1) + if key not in self: + self.order.append(key) + super(mapfile, self).__setitem__(key, value) + fp.close() + + def __setitem__(self, key, value): + if self.fp is None: + try: + self.fp = open(self.path, 'a') + except IOError, err: + raise util.Abort(_('could not open map file %r: %s') % + (self.path, err.strerror)) + self.fp.write('%s %s\n' % (key, value)) + self.fp.flush() + super(mapfile, self).__setitem__(key, value) + + def close(self): + if self.fp: + self.fp.close() + self.fp = None
--- a/hgext/convert/cvs.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/convert/cvs.py Thu Dec 06 13:11:36 2007 -0800 @@ -1,9 +1,10 @@ # CVS conversion code inspired by hg-cvs-import and git-cvsimport import os, locale, re, socket +from cStringIO import StringIO from mercurial import util -from common import NoRepo, commit, converter_source +from common import NoRepo, commit, converter_source, checktool class convert_cvs(converter_source): def __init__(self, ui, path, rev=None): @@ -13,6 +14,9 @@ if not os.path.exists(cvs): raise NoRepo("%s does not look like a CVS checkout" % path) + for tool in ('cvsps', 'cvs'): + checktool(tool) + self.changeset = {} self.files = {} self.tags = {} @@ -206,6 +210,20 @@ return self.heads def _getfile(self, name, rev): + + def chunkedread(fp, count): + # file-objects returned by socked.makefile() do not handle + # large read() requests very well. + chunksize = 65536 + output = StringIO() + while count > 0: + data = fp.read(min(count, chunksize)) + if not data: + raise util.Abort("%d bytes missing from remote file" % count) + count -= len(data) + output.write(data) + return output.getvalue() + if rev.endswith("(DEAD)"): raise IOError @@ -224,14 +242,14 @@ self.readp.readline() # entries mode = self.readp.readline()[:-1] count = int(self.readp.readline()[:-1]) - data = self.readp.read(count) + data = chunkedread(self.readp, count) elif line.startswith(" "): data += line[1:] elif line.startswith("M "): pass elif line.startswith("Mbinary "): count = int(self.readp.readline()[:-1]) - data = self.readp.read(count) + data = chunkedread(self.readp, count) else: if line == "ok\n": return (data, "x" in mode and "x" or "")
--- a/hgext/convert/darcs.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/convert/darcs.py Thu Dec 06 13:11:36 2007 -0800 @@ -1,6 +1,6 @@ # darcs support for the convert extension -from common import NoRepo, commit, converter_source +from common import NoRepo, checktool, commandline, commit, converter_source from mercurial.i18n import _ from mercurial import util import os, shutil, tempfile @@ -17,15 +17,18 @@ except ImportError: ElementTree = None -class darcs_source(converter_source): +class darcs_source(converter_source, commandline): def __init__(self, ui, path, rev=None): - super(darcs_source, self).__init__(ui, path, rev=rev) + converter_source.__init__(self, ui, path, rev=rev) + commandline.__init__(self, ui, 'darcs') # check for _darcs, ElementTree, _darcs/inventory so that we can # easily skip test-convert-darcs if ElementTree is not around if not os.path.exists(os.path.join(path, '_darcs')): raise NoRepo("%s does not look like a darcs repo" % path) + checktool('darcs') + if ElementTree is None: raise util.Abort(_("Python ElementTree module is not available")) @@ -45,7 +48,8 @@ output, status = self.run('init', repodir=self.tmppath) self.checkexit(status) - tree = self.xml('changes', '--xml-output', '--summary') + tree = self.xml('changes', xml_output=True, summary=True, + repodir=self.path) tagname = None child = None for elt in tree.findall('patch'): @@ -65,31 +69,9 @@ self.ui.debug('cleaning up %s\n' % self.tmppath) shutil.rmtree(self.tmppath, ignore_errors=True) - def _run(self, cmd, *args, **kwargs): - cmdline = ['darcs', cmd, '--repodir', kwargs.get('repodir', self.path)] - cmdline += args - cmdline = [util.shellquote(arg) for arg in cmdline] - cmdline += ['<', util.nulldev] - cmdline = ' '.join(cmdline) - self.ui.debug(cmdline, '\n') - return util.popen(cmdline) - - def run(self, cmd, *args, **kwargs): - fp = self._run(cmd, *args, **kwargs) - output = fp.read() - return output, fp.close() - - def checkexit(self, status, output=''): - if status: - if output: - self.ui.warn(_('darcs error:\n')) - self.ui.warn(output) - msg = util.explain_exit(status)[0] - raise util.Abort(_('darcs %s') % msg) - - def xml(self, cmd, *opts): + def xml(self, cmd, **kwargs): etree = ElementTree() - fp = self._run(cmd, *opts) + fp = self._run(cmd, **kwargs) etree.parse(fp) self.checkexit(fp.close()) return etree.getroot() @@ -105,15 +87,15 @@ desc=desc.strip(), parents=self.parents[rev]) def pull(self, rev): - output, status = self.run('pull', self.path, '--all', - '--match', 'hash %s' % rev, - '--no-test', '--no-posthook', - '--external-merge', '/bin/false', + output, status = self.run('pull', self.path, all=True, + match='hash %s' % rev, + no_test=True, no_posthook=True, + external_merge='/bin/false', repodir=self.tmppath) if status: if output.find('We have conflicts in') == -1: self.checkexit(status, output) - output, status = self.run('revert', '--all', repodir=self.tmppath) + output, status = self.run('revert', all=True, repodir=self.tmppath) self.checkexit(status, output) def getchanges(self, rev):
--- a/hgext/convert/filemap.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/convert/filemap.py Thu Dec 06 13:11:36 2007 -0800 @@ -7,7 +7,7 @@ import shlex from mercurial.i18n import _ from mercurial import util -from common import SKIPREV +from common import SKIPREV, converter_source def rpairs(name): e = len(name) @@ -110,9 +110,9 @@ # touch files we're interested in, but also merges that merge two # or more interesting revisions. -class filemap_source(object): +class filemap_source(converter_source): def __init__(self, ui, baseconverter, filemap): - self.ui = ui + super(filemap_source, self).__init__(ui) self.base = baseconverter self.filemapper = filemapper(ui, filemap) self.commits = {} @@ -128,7 +128,7 @@ self.children = {} self.seenchildren = {} - def setrevmap(self, revmap, order): + def setrevmap(self, revmap): # rebuild our state to make things restartable # # To avoid calling getcommit for every revision that has already @@ -143,7 +143,7 @@ seen = {SKIPREV: SKIPREV} dummyset = util.set() converted = [] - for rev in order: + for rev in revmap.order: mapped = revmap[rev] wanted = mapped not in seen if wanted: @@ -157,7 +157,7 @@ arg = None converted.append((rev, wanted, arg)) self.convertedorder = converted - return self.base.setrevmap(revmap, order) + return self.base.setrevmap(revmap) def rebuild(self): if self._rebuilt: @@ -344,9 +344,3 @@ def gettags(self): return self.base.gettags() - - def before(self): - pass - - def after(self): - pass
--- a/hgext/convert/git.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/convert/git.py Thu Dec 06 13:11:36 2007 -0800 @@ -3,7 +3,7 @@ import os from mercurial import util -from common import NoRepo, commit, converter_source +from common import NoRepo, commit, converter_source, checktool class convert_git(converter_source): # Windows does not support GIT_DIR= construct while other systems @@ -31,6 +31,9 @@ path += "/.git" if not os.path.exists(path + "/objects"): raise NoRepo("%s does not look like a Git repo" % path) + + checktool('git-rev-parse', 'git') + self.path = path def getheads(self):
--- a/hgext/convert/hg.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/convert/hg.py Thu Dec 06 13:11:36 2007 -0800 @@ -1,10 +1,16 @@ # hg backend for convert extension -# Note for hg->hg conversion: Old versions of Mercurial didn't trim -# the whitespace from the ends of commit messages, but new versions -# do. Changesets created by those older versions, then converted, may -# thus have different hashes for changesets that are otherwise -# identical. +# Notes for hg->hg conversion: +# +# * Old versions of Mercurial didn't trim the whitespace from the ends +# of commit messages, but new versions do. Changesets created by +# those older versions, then converted, may thus have different +# hashes for changesets that are otherwise identical. +# +# * By default, the source revision is stored in the converted +# revision. This will cause the converted revision to have a +# different identity than the source. To avoid this, use the +# following option: "--config convert.hg.saverev=false" import os, time @@ -24,8 +30,6 @@ if os.path.isdir(path) and len(os.listdir(path)) > 0: try: self.repo = hg.repository(self.ui, path) - ui.status(_('destination %s is a Mercurial repository\n') % - path) except hg.RepoError, err: ui.print_exc() raise NoRepo(err.args[0]) @@ -183,6 +187,7 @@ class mercurial_source(converter_source): def __init__(self, ui, path, rev=None): converter_source.__init__(self, ui, path, rev) + self.saverev = ui.configbool('convert', 'hg.saverev', True) try: self.repo = hg.repository(self.ui, path) # try to provoke an exception if this isn't really a hg @@ -195,6 +200,7 @@ self.lastrev = None self.lastctx = None self._changescache = None + self.convertfp = None def changectx(self, rev): if self.lastrev != rev: @@ -240,8 +246,12 @@ def getcommit(self, rev): ctx = self.changectx(rev) parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid] + if self.saverev: + crev = rev + else: + crev = None return commit(author=ctx.user(), date=util.datestr(ctx.date()), - desc=ctx.description(), parents=parents, + desc=ctx.description(), rev=crev, parents=parents, branch=ctx.branch(), extra=ctx.extra()) def gettags(self): @@ -258,3 +268,9 @@ return changes[0] + changes[1] + changes[2] + def converted(self, rev, destrev): + if self.convertfp is None: + self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'), + 'a') + self.convertfp.write('%s %s\n' % (destrev, rev)) + self.convertfp.flush()
--- a/hgext/convert/subversion.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/convert/subversion.py Thu Dec 06 13:11:36 2007 -0800 @@ -17,9 +17,13 @@ import locale import os +import re import sys import cPickle as pickle -from mercurial import util +import tempfile + +from mercurial import strutil, util +from mercurial.i18n import _ # Subversion stuff. Works best with very recent Python SVN bindings # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing @@ -28,6 +32,7 @@ from cStringIO import StringIO from common import NoRepo, commit, converter_source, encodeargs, decodeargs +from common import commandline, converter_sink, mapfile try: from svn.core import SubversionException, Pool @@ -149,9 +154,15 @@ self.head = self.revid(self.last_changed) self._changescache = None - def setrevmap(self, revmap, order): + if os.path.exists(os.path.join(url, '.svn/entries')): + self.wc = url + else: + self.wc = None + self.convertfp = None + + def setrevmap(self, revmap): lastrevs = {} - for revid in revmap.keys(): + for revid in revmap.iterkeys(): uuid, module, revnum = self.revsplit(revid) lastrevnum = lastrevs.setdefault(module, revnum) if revnum > lastrevnum: @@ -293,6 +304,15 @@ self.ui.note('no tags found at revision %d\n' % start) return tags + def converted(self, rev, destrev): + if not self.wc: + return + if self.convertfp is None: + self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'), + 'a') + self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev))) + self.convertfp.flush() + # -- helper functions -- def revid(self, revnum, module=None): @@ -664,3 +684,219 @@ pool = Pool() rpath = '/'.join([self.base, path]).strip('/') return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()] + +pre_revprop_change = '''#!/bin/sh + +REPOS="$1" +REV="$2" +USER="$3" +PROPNAME="$4" +ACTION="$5" + +if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi +if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi +if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi + +echo "Changing prohibited revision property" >&2 +exit 1 +''' + +class svn_sink(converter_sink, commandline): + commit_re = re.compile(r'Committed revision (\d+).', re.M) + + def prerun(self): + if self.wc: + os.chdir(self.wc) + + def postrun(self): + if self.wc: + os.chdir(self.cwd) + + def join(self, name): + return os.path.join(self.wc, '.svn', name) + + def revmapfile(self): + return self.join('hg-shamap') + + def authorfile(self): + return self.join('hg-authormap') + + def __init__(self, ui, path): + converter_sink.__init__(self, ui, path) + commandline.__init__(self, ui, 'svn') + self.delete = [] + self.wc = None + self.cwd = os.getcwd() + + path = os.path.realpath(path) + + created = False + if os.path.isfile(os.path.join(path, '.svn', 'entries')): + self.wc = path + self.run0('update') + else: + wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc') + + if os.path.isdir(os.path.dirname(path)): + if not os.path.exists(os.path.join(path, 'db', 'fs-type')): + ui.status(_('initializing svn repo %r\n') % + os.path.basename(path)) + commandline(ui, 'svnadmin').run0('create', path) + created = path + path = path.replace('\\', '/') + if not path.startswith('/'): + path = '/' + path + path = 'file://' + path + + ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath)) + self.run0('checkout', path, wcpath) + + self.wc = wcpath + self.opener = util.opener(self.wc) + self.wopener = util.opener(self.wc) + self.childmap = mapfile(ui, self.join('hg-childmap')) + self.is_exec = util.checkexec(self.wc) and util.is_exec or None + + if created: + hook = os.path.join(created, 'hooks', 'pre-revprop-change') + fp = open(hook, 'w') + fp.write(pre_revprop_change) + fp.close() + util.set_exec(hook, True) + + xport = transport.SvnRaTransport(url=geturl(path)) + self.uuid = svn.ra.get_uuid(xport.ra) + + def wjoin(self, *names): + return os.path.join(self.wc, *names) + + def putfile(self, filename, flags, data): + if 'l' in flags: + self.wopener.symlink(data, filename) + else: + try: + if os.path.islink(self.wjoin(filename)): + os.unlink(filename) + except OSError: + pass + self.wopener(filename, 'w').write(data) + + if self.is_exec: + was_exec = self.is_exec(self.wjoin(filename)) + else: + # On filesystems not supporting execute-bit, there is no way + # to know if it is set but asking subversion. Setting it + # systematically is just as expensive and much simpler. + was_exec = 'x' not in flags + + util.set_exec(self.wjoin(filename), 'x' in flags) + if was_exec: + if 'x' not in flags: + self.run0('propdel', 'svn:executable', filename) + else: + if 'x' in flags: + self.run0('propset', 'svn:executable', '*', filename) + + def delfile(self, name): + self.delete.append(name) + + def copyfile(self, source, dest): + # SVN's copy command pukes if the destination file exists, but + # our copyfile method expects to record a copy that has + # already occurred. Cross the semantic gap. + wdest = self.wjoin(dest) + exists = os.path.exists(wdest) + if exists: + fd, tempname = tempfile.mkstemp( + prefix='hg-copy-', dir=os.path.dirname(wdest)) + os.close(fd) + os.unlink(tempname) + os.rename(wdest, tempname) + try: + self.run0('copy', source, dest) + finally: + if exists: + try: + os.unlink(wdest) + except OSError: + pass + os.rename(tempname, wdest) + + def dirs_of(self, files): + dirs = set() + for f in files: + if os.path.isdir(self.wjoin(f)): + dirs.add(f) + for i in strutil.rfindall(f, '/'): + dirs.add(f[:i]) + return dirs + + def add_files(self, files): + add_dirs = [d for d in self.dirs_of(files) + if not os.path.exists(self.wjoin(d, '.svn', 'entries'))] + if add_dirs: + add_dirs.sort() + self.run('add', non_recursive=True, quiet=True, *add_dirs) + if files: + self.run('add', quiet=True, *files) + return files.union(add_dirs) + + def tidy_dirs(self, names): + dirs = list(self.dirs_of(names)) + dirs.sort(reverse=True) + deleted = [] + for d in dirs: + wd = self.wjoin(d) + if os.listdir(wd) == '.svn': + self.run0('delete', d) + deleted.append(d) + return deleted + + def addchild(self, parent, child): + self.childmap[parent] = child + + def revid(self, rev): + return u"svn:%s@%s" % (self.uuid, rev) + + def putcommit(self, files, parents, commit): + for parent in parents: + try: + return self.revid(self.childmap[parent]) + except KeyError: + pass + entries = set(self.delete) + if self.delete: + self.run0('delete', *self.delete) + self.delete = [] + files = util.frozenset(files) + entries.update(self.add_files(files.difference(entries))) + entries.update(self.tidy_dirs(entries)) + fd, messagefile = tempfile.mkstemp(prefix='hg-convert-') + fp = os.fdopen(fd, 'w') + fp.write(commit.desc) + fp.close() + try: + output = self.run0('commit', + username=util.shortuser(commit.author), + file=messagefile, + *list(entries)) + try: + rev = self.commit_re.search(output).group(1) + except AttributeError: + self.ui.warn(_('unexpected svn output:\n')) + self.ui.warn(output) + raise util.Abort(_('unable to cope with svn output')) + if commit.rev: + self.run('propset', 'hg:convert-rev', commit.rev, + revprop=True, revision=rev) + if commit.branch and commit.branch != 'default': + self.run('propset', 'hg:convert-branch', commit.branch, + revprop=True, revision=rev) + for parent in parents: + self.addchild(parent, rev) + return self.revid(rev) + finally: + os.unlink(messagefile) + + def puttags(self, tags): + self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
--- a/hgext/gpg.py Thu Dec 06 13:10:25 2007 -0800 +++ b/hgext/gpg.py Thu Dec 06 13:11:36 2007 -0800 @@ -249,7 +249,7 @@ message = opts['message'] if not message: message = "\n".join([_("Added signature for changeset %s") - % hgnode.hex(n) + % hgnode.short(n) for n in nodes]) try: repo.commit([".hgsigs"], message, opts['user'], opts['date'])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/highlight.py Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,147 @@ +""" +This is Mercurial extension for syntax highlighting in the file +revision view of hgweb. + +It depends on the pygments syntax highlighting library: +http://pygments.org/ + +To enable the extension add this to hgrc: + +[extensions] +hgext.highlight = + +There is a single configuration option: + +[web] +pygments_style = <style> + +The default is 'colorful'. If this is changed the corresponding CSS +file should be re-generated by running + +# pygmentize -f html -S <newstyle> + + +-- Adam Hupp <adam@hupp.org> + + +""" + +from mercurial import demandimport +demandimport.ignore.extend(['pkgutil', + 'pkg_resources', + '__main__',]) + +import mimetypes + +from mercurial.hgweb import hgweb_mod +from mercurial.hgweb.hgweb_mod import hgweb +from mercurial import util +from mercurial.hgweb.common import paritygen +from mercurial.node import hex + +from pygments import highlight +from pygments.util import ClassNotFound +from pygments.lexers import guess_lexer_for_filename, TextLexer +from pygments.formatters import HtmlFormatter + +SYNTAX_CSS = ('\n<link rel="stylesheet" href="#staticurl#highlight.css" ' + 'type="text/css" />') + +class StripedHtmlFormatter(HtmlFormatter): + def __init__(self, stripecount, *args, **kwargs): + super(StripedHtmlFormatter, self).__init__(*args, **kwargs) + self.stripecount = stripecount + + def wrap(self, source, outfile): + yield 0, "<div class='highlight'>" + yield 0, "<pre>" + parity = paritygen(self.stripecount) + + for n, i in source: + if n == 1: + i = "<div class='parity%s'>%s</div>" % (parity.next(), i) + yield n, i + + yield 0, "</pre>" + yield 0, "</div>" + + +def pygments_format(filename, rawtext, forcetext=False, stripecount=1, + style='colorful'): + if not forcetext: + try: + lexer = guess_lexer_for_filename(filename, rawtext) + except ClassNotFound: + lexer = TextLexer() + else: + lexer = TextLexer() + + formatter = StripedHtmlFormatter(stripecount, style=style, + linenos='inline') + + return highlight(rawtext, lexer, formatter) + + +def filerevision_pygments(self, tmpl, fctx): + """Reimplement hgweb.filerevision to use syntax highlighting""" + f = fctx.path() + + rawtext = fctx.data() + text = rawtext + + fl = fctx.filelog() + n = fctx.filenode() + + mt = mimetypes.guess_type(f)[0] + + if util.binary(text): + mt = mt or 'application/octet-stream' + text = "(binary:%s)" % mt + + # don't parse (binary:...) as anything + forcetext = True + else: + mt = mt or 'text/plain' + forcetext = False + + def lines(text): + for line in text.splitlines(True): + yield {"line": line} + + style = self.config("web", "pygments_style", "colorful") + + text_formatted = lines(pygments_format(f, text, + forcetext=forcetext, + stripecount=self.stripecount, + style=style)) + + # override per-line template + tmpl.cache['fileline'] = '#line#' + + # append a <link ...> to the syntax highlighting css + old_header = ''.join(tmpl('header')) + if SYNTAX_CSS not in old_header: + new_header = old_header + SYNTAX_CSS + tmpl.cache['header'] = new_header + + yield tmpl("filerevision", + file=f, + path=hgweb_mod._up(f), # fixme: make public + text=text_formatted, + raw=rawtext, + mimetype=mt, + rev=fctx.rev(), + node=hex(fctx.node()), + author=fctx.user(), + date=fctx.date(), + desc=fctx.description(), + parent=self.siblings(fctx.parents()), + child=self.siblings(fctx.children()), + rename=self.renamelink(fl, n), + permissions=fctx.manifest().flags(f)) + + +# monkeypatch in the new version +# should be safer than overriding the method in a derived class +# and then patching the class +hgweb.filerevision = filerevision_pygments
--- a/hgweb.cgi Thu Dec 06 13:10:25 2007 -0800 +++ b/hgweb.cgi Thu Dec 06 13:11:36 2007 -0800 @@ -22,10 +22,7 @@ #os.environ["HGENCODING"] = "UTF-8" from mercurial.hgweb.hgweb_mod import hgweb -from mercurial.hgweb.request import wsgiapplication import mercurial.hgweb.wsgicgi as wsgicgi -def make_web_app(): - return hgweb("/path/to/repo", "repository name") - -wsgicgi.launch(wsgiapplication(make_web_app)) +application = hgweb("/path/to/repo", "repository name") +wsgicgi.launch(application)
--- a/hgwebdir.cgi Thu Dec 06 13:10:25 2007 -0800 +++ b/hgwebdir.cgi Thu Dec 06 13:11:36 2007 -0800 @@ -22,7 +22,6 @@ #os.environ["HGENCODING"] = "UTF-8" from mercurial.hgweb.hgwebdir_mod import hgwebdir -from mercurial.hgweb.request import wsgiapplication import mercurial.hgweb.wsgicgi as wsgicgi # The config file looks like this. You can have paths to individual @@ -44,7 +43,5 @@ # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples # or use a dictionary with entries like 'virtual/path': '/real/path' -def make_web_app(): - return hgwebdir("hgweb.config") - -wsgicgi.launch(wsgiapplication(make_web_app)) +application = hgwebdir('hgweb.config') +wsgicgi.launch(application)
--- a/mercurial/bundlerepo.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/bundlerepo.py Thu Dec 06 13:11:36 2007 -0800 @@ -48,7 +48,7 @@ continue for p in (p1, p2): if not p in self.nodemap: - raise revlog.LookupError(_("unknown parent %s") % short(p1)) + raise revlog.LookupError(hex(p1), _("unknown parent %s") % short(p1)) if linkmapper is None: link = n else:
--- a/mercurial/cmdutil.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/cmdutil.py Thu Dec 06 13:11:36 2007 -0800 @@ -571,26 +571,26 @@ def showcopies(**args): c = [{'name': x[0], 'source': x[1]} for x in copies] return showlist('file_copy', c, plural='file_copies', **args) - - if self.ui.debugflag: - files = self.repo.status(log.parents(changenode)[0], changenode)[:3] - def showfiles(**args): - return showlist('file', files[0], **args) - def showadds(**args): - return showlist('file_add', files[1], **args) - def showdels(**args): - return showlist('file_del', files[2], **args) - def showmanifest(**args): - args = args.copy() - args.update(dict(rev=self.repo.manifest.rev(changes[0]), - node=hex(changes[0]))) - return self.t('manifest', **args) - else: - def showfiles(**args): - return showlist('file', changes[3], **args) - showadds = '' - showdels = '' - showmanifest = '' + + files = [] + def getfiles(): + if not files: + files[:] = self.repo.status( + log.parents(changenode)[0], changenode)[:3] + return files + def showfiles(**args): + return showlist('file', changes[3], **args) + def showmods(**args): + return showlist('file_mod', getfiles()[0], **args) + def showadds(**args): + return showlist('file_add', getfiles()[1], **args) + def showdels(**args): + return showlist('file_del', getfiles()[2], **args) + def showmanifest(**args): + args = args.copy() + args.update(dict(rev=self.repo.manifest.rev(changes[0]), + node=hex(changes[0]))) + return self.t('manifest', **args) defprops = { 'author': changes[1], @@ -599,6 +599,7 @@ 'desc': changes[4].strip(), 'file_adds': showadds, 'file_dels': showdels, + 'file_mods': showmods, 'files': showfiles, 'file_copies': showcopies, 'manifest': showmanifest,
--- a/mercurial/commands.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/commands.py Thu Dec 06 13:11:36 2007 -0800 @@ -1629,7 +1629,7 @@ if opts.get('exact'): if hex(n) != nodeid: repo.rollback() - raise util.Abort(_('patch is damaged' + + raise util.Abort(_('patch is damaged' ' or loses information')) finally: os.unlink(tmpname) @@ -1937,7 +1937,7 @@ if len(heads) == 1: msg = _('there is nothing to merge') if parent != repo.lookup(repo.workingctx().branch()): - msg = _('%s - use "hg update" instead' % msg) + msg = _('%s - use "hg update" instead') % msg raise util.Abort(msg) if parent not in heads:
--- a/mercurial/context.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/context.py Thu Dec 06 13:11:36 2007 -0800 @@ -100,13 +100,13 @@ try: return self._manifest[path], self._manifest.flags(path) except KeyError: - raise revlog.LookupError(_("'%s' not found in manifest") % path) + raise revlog.LookupError(path, _("'%s' not found in manifest") % path) if '_manifestdelta' in self.__dict__ or path in self.files(): if path in self._manifestdelta: return self._manifestdelta[path], self._manifestdelta.flags(path) node, flag = self._repo.manifest.find(self._changeset[0], path) if not node: - raise revlog.LookupError(_("'%s' not found in manifest") % path) + raise revlog.LookupError(path, _("'%s' not found in manifest") % path) return node, flag
--- a/mercurial/dispatch.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/dispatch.py Thu Dec 06 13:11:36 2007 -0800 @@ -125,7 +125,7 @@ ui.warn("\n%r\n" % util.ellipsis(inst[1])) except ImportError, inst: m = str(inst).split()[-1] - ui.warn(_("abort: could not import module %s!\n" % m)) + ui.warn(_("abort: could not import module %s!\n") % m) if m in "mpatch bdiff".split(): ui.warn(_("(did you forget to compile extensions?)\n")) elif m in "zlib".split():
--- a/mercurial/hgweb/common.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/hgweb/common.py Thu Dec 06 13:11:36 2007 -0800 @@ -6,7 +6,24 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -import os, mimetypes +import errno, mimetypes, os + +class ErrorResponse(Exception): + def __init__(self, code, message=None): + Exception.__init__(self) + self.code = code + if message: + self.message = message + else: + self.message = _statusmessage(code) + +def _statusmessage(code): + from BaseHTTPServer import BaseHTTPRequestHandler + responses = BaseHTTPRequestHandler.responses + return responses.get(code, ('Error', 'Unknown error'))[0] + +def statusmessage(code): + return '%d %s' % (code, _statusmessage(code)) def get_mtime(repo_path): store_path = os.path.join(repo_path, ".hg") @@ -40,9 +57,13 @@ req.header([('Content-type', ct), ('Content-length', str(os.path.getsize(path)))]) return file(path, 'rb').read() - except (TypeError, OSError): - # illegal fname or unreadable file - return "" + except TypeError: + raise ErrorResponse(500, 'illegal file name') + except OSError, err: + if err.errno == errno.ENOENT: + raise ErrorResponse(404) + else: + raise ErrorResponse(500, err.strerror) def style_map(templatepath, style): """Return path to mapfile for a given style.
--- a/mercurial/hgweb/hgweb_mod.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/hgweb/hgweb_mod.py Thu Dec 06 13:11:36 2007 -0800 @@ -6,13 +6,28 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -import os, mimetypes, re, zlib, mimetools, cStringIO, sys -import tempfile, urllib, bz2 +import os, mimetypes, re, mimetools, cStringIO from mercurial.node import * -from mercurial.i18n import gettext as _ -from mercurial import mdiff, ui, hg, util, archival, streamclone, patch +from mercurial import mdiff, ui, hg, util, archival, patch from mercurial import revlog, templater -from common import get_mtime, staticfile, style_map, paritygen +from common import ErrorResponse, get_mtime, style_map, paritygen +from request import wsgirequest +import webcommands, protocol + +shortcuts = { + 'cl': [('cmd', ['changelog']), ('rev', None)], + 'sl': [('cmd', ['shortlog']), ('rev', None)], + 'cs': [('cmd', ['changeset']), ('node', None)], + 'f': [('cmd', ['file']), ('filenode', None)], + 'fl': [('cmd', ['filelog']), ('filenode', None)], + 'fd': [('cmd', ['filediff']), ('node', None)], + 'fa': [('cmd', ['annotate']), ('filenode', None)], + 'mf': [('cmd', ['manifest']), ('manifest', None)], + 'ca': [('cmd', ['archive']), ('node', None)], + 'tags': [('cmd', ['tags'])], + 'tip': [('cmd', ['changeset']), ('node', ['tip'])], + 'static': [('cmd', ['static']), ('file', None)] +} def _up(p): if p[0] != "/": @@ -106,17 +121,200 @@ self.allowpull = self.configbool("web", "allowpull", True) self.encoding = self.config("web", "encoding", util._encoding) + def run(self): + if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."): + raise RuntimeError("This function is only intended to be called while running as a CGI script.") + import mercurial.hgweb.wsgicgi as wsgicgi + wsgicgi.launch(self) + + def __call__(self, env, respond): + req = wsgirequest(env, respond) + self.run_wsgi(req) + return req + + def run_wsgi(self, req): + + self.refresh() + + # expand form shortcuts + + for k in shortcuts.iterkeys(): + if k in req.form: + for name, value in shortcuts[k]: + if value is None: + value = req.form[k] + req.form[name] = value + del req.form[k] + + # work with CGI variables to create coherent structure + # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME + + req.url = req.env['SCRIPT_NAME'] + if not req.url.endswith('/'): + req.url += '/' + if req.env.has_key('REPO_NAME'): + req.url += req.env['REPO_NAME'] + '/' + + if req.env.get('PATH_INFO'): + parts = req.env.get('PATH_INFO').strip('/').split('/') + repo_parts = req.env.get('REPO_NAME', '').split('/') + if parts[:len(repo_parts)] == repo_parts: + parts = parts[len(repo_parts):] + query = '/'.join(parts) + else: + query = req.env['QUERY_STRING'].split('&', 1)[0] + query = query.split(';', 1)[0] + + # translate user-visible url structure to internal structure + + args = query.split('/', 2) + if 'cmd' not in req.form and args and args[0]: + + cmd = args.pop(0) + style = cmd.rfind('-') + if style != -1: + req.form['style'] = [cmd[:style]] + cmd = cmd[style+1:] + + # avoid accepting e.g. style parameter as command + if hasattr(webcommands, cmd) or hasattr(protocol, cmd): + req.form['cmd'] = [cmd] + + if args and args[0]: + node = args.pop(0) + req.form['node'] = [node] + if args: + req.form['file'] = args + + if cmd == 'static': + req.form['file'] = req.form['node'] + elif cmd == 'archive': + fn = req.form['node'][0] + for type_, spec in self.archive_specs.iteritems(): + ext = spec[2] + if fn.endswith(ext): + req.form['node'] = [fn[:-len(ext)]] + req.form['type'] = [type_] + + # actually process the request + + try: + + cmd = req.form.get('cmd', [''])[0] + if hasattr(protocol, cmd): + method = getattr(protocol, cmd) + method(self, req) + else: + tmpl = self.templater(req) + if cmd == '': + req.form['cmd'] = [tmpl.cache['default']] + cmd = req.form['cmd'][0] + method = getattr(webcommands, cmd) + method(self, req, tmpl) + del tmpl + + except revlog.LookupError, err: + req.respond(404, tmpl( + 'error', error='revision not found: %s' % err.name)) + except (hg.RepoError, revlog.RevlogError), inst: + req.respond('500 Internal Server Error', + tmpl('error', error=str(inst))) + except ErrorResponse, inst: + req.respond(inst.code, tmpl('error', error=inst.message)) + except AttributeError: + req.respond(400, tmpl('error', error='No such method: ' + cmd)) + + def templater(self, req): + + # determine scheme, port and server name + # this is needed to create absolute urls + + proto = req.env.get('wsgi.url_scheme') + if proto == 'https': + proto = 'https' + default_port = "443" + else: + proto = 'http' + default_port = "80" + + port = req.env["SERVER_PORT"] + port = port != default_port and (":" + port) or "" + urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port) + staticurl = self.config("web", "staticurl") or req.url + 'static/' + if not staticurl.endswith('/'): + staticurl += '/' + + # some functions for the templater + + def header(**map): + header_file = cStringIO.StringIO( + ''.join(tmpl("header", encoding=self.encoding, **map))) + msg = mimetools.Message(header_file, 0) + req.header(msg.items()) + yield header_file.read() + + def rawfileheader(**map): + req.header([('Content-type', map['mimetype']), + ('Content-disposition', 'filename=%s' % map['file']), + ('Content-length', str(len(map['raw'])))]) + yield '' + + def footer(**map): + yield tmpl("footer", **map) + + def motd(**map): + yield self.config("web", "motd", "") + + def sessionvars(**map): + fields = [] + if req.form.has_key('style'): + style = req.form['style'][0] + if style != self.config('web', 'style', ''): + fields.append(('style', style)) + + separator = req.url[-1] == '?' and ';' or '?' + for name, value in fields: + yield dict(name=name, value=value, separator=separator) + separator = ';' + + # figure out which style to use + + style = self.config("web", "style", "") + if req.form.has_key('style'): + style = req.form['style'][0] + mapfile = style_map(self.templatepath, style) + + if not self.reponame: + self.reponame = (self.config("web", "name") + or req.env.get('REPO_NAME') + or req.url.strip('/') or self.repo.root) + + # create the templater + + tmpl = templater.templater(mapfile, templater.common_filters, + defaults={"url": req.url, + "staticurl": staticurl, + "urlbase": urlbase, + "repo": self.reponame, + "header": header, + "footer": footer, + "motd": motd, + "rawfileheader": rawfileheader, + "sessionvars": sessionvars + }) + return tmpl + def archivelist(self, nodeid): allowed = self.configlist("web", "allow_archive") for i, spec in self.archive_specs.iteritems(): if i in allowed or self.configbool("web", "allow" + i): yield {"type" : i, "extension" : spec[2], "node" : nodeid} - def listfilediffs(self, files, changeset): + def listfilediffs(self, tmpl, files, changeset): for f in files[:self.maxfiles]: - yield self.t("filedifflink", node=hex(changeset), file=f) + yield tmpl("filedifflink", node=hex(changeset), file=f) if len(files) > self.maxfiles: - yield self.t("fileellipses") + yield tmpl("fileellipses") def siblings(self, siblings=[], hiderev=None, **args): siblings = [s for s in siblings if s.node() != nullid] @@ -148,11 +346,11 @@ branches.append({"name": branch}) return branches - def showtag(self, t1, node=nullid, **args): + def showtag(self, tmpl, t1, node=nullid, **args): for t in self.repo.nodetags(node): - yield self.t(t1, tag=t, **args) + yield tmpl(t1, tag=t, **args) - def diff(self, node1, node2, files): + def diff(self, tmpl, node1, node2, files): def filterfiles(filters, files): l = [x for x in files if x in filters] @@ -164,22 +362,22 @@ parity = paritygen(self.stripecount) def diffblock(diff, f, fn): - yield self.t("diffblock", - lines=prettyprintlines(diff), - parity=parity.next(), - file=f, - filenode=hex(fn or nullid)) + yield tmpl("diffblock", + lines=prettyprintlines(diff), + parity=parity.next(), + file=f, + filenode=hex(fn or nullid)) def prettyprintlines(diff): for l in diff.splitlines(1): if l.startswith('+'): - yield self.t("difflineplus", line=l) + yield tmpl("difflineplus", line=l) elif l.startswith('-'): - yield self.t("difflineminus", line=l) + yield tmpl("difflineminus", line=l) elif l.startswith('@'): - yield self.t("difflineat", line=l) + yield tmpl("difflineat", line=l) else: - yield self.t("diffline", line=l) + yield tmpl("diffline", line=l) r = self.repo c1 = r.changectx(node1) @@ -209,7 +407,7 @@ yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f, opts=diffopts), f, tn) - def changelog(self, ctx, shortlog=False): + def changelog(self, tmpl, ctx, shortlog=False): def changelist(limit=0,**map): cl = self.repo.changelog l = [] # build a list in forward order for efficiency @@ -224,7 +422,7 @@ "changelogtag": self.showtag("changelogtag",n), "desc": ctx.description(), "date": ctx.date(), - "files": self.listfilediffs(ctx.files(), n), + "files": self.listfilediffs(tmpl, ctx.files(), n), "rev": i, "node": hex(n), "tags": self.nodetagsdict(n), @@ -247,15 +445,15 @@ changenav = revnavgen(pos, maxchanges, count, self.repo.changectx) - yield self.t(shortlog and 'shortlog' or 'changelog', - changenav=changenav, - node=hex(cl.tip()), - rev=pos, changesets=count, - entries=lambda **x: changelist(limit=0,**x), - latestentry=lambda **x: changelist(limit=1,**x), - archives=self.archivelist("tip")) + yield tmpl(shortlog and 'shortlog' or 'changelog', + changenav=changenav, + node=hex(cl.tip()), + rev=pos, changesets=count, + entries=lambda **x: changelist(limit=0,**x), + latestentry=lambda **x: changelist(limit=1,**x), + archives=self.archivelist("tip")) - def search(self, query): + def search(self, tmpl, query): def changelist(**map): cl = self.repo.changelog @@ -286,19 +484,19 @@ count += 1 n = ctx.node() - yield self.t('searchentry', - parity=parity.next(), - author=ctx.user(), - parent=self.siblings(ctx.parents()), - child=self.siblings(ctx.children()), - changelogtag=self.showtag("changelogtag",n), - desc=ctx.description(), - date=ctx.date(), - files=self.listfilediffs(ctx.files(), n), - rev=ctx.rev(), - node=hex(n), - tags=self.nodetagsdict(n), - branches=self.nodebranchdict(ctx)) + yield tmpl('searchentry', + parity=parity.next(), + author=ctx.user(), + parent=self.siblings(ctx.parents()), + child=self.siblings(ctx.children()), + changelogtag=self.showtag("changelogtag",n), + desc=ctx.description(), + date=ctx.date(), + files=self.listfilediffs(tmpl, ctx.files(), n), + rev=ctx.rev(), + node=hex(n), + tags=self.nodetagsdict(n), + branches=self.nodebranchdict(ctx)) if count >= self.maxchanges: break @@ -306,13 +504,13 @@ cl = self.repo.changelog parity = paritygen(self.stripecount) - yield self.t('search', - query=query, - node=hex(cl.tip()), - entries=changelist, - archives=self.archivelist("tip")) + yield tmpl('search', + query=query, + node=hex(cl.tip()), + entries=changelist, + archives=self.archivelist("tip")) - def changeset(self, ctx): + def changeset(self, tmpl, ctx): n = ctx.node() parents = ctx.parents() p1 = parents[0].node() @@ -320,29 +518,29 @@ files = [] parity = paritygen(self.stripecount) for f in ctx.files(): - files.append(self.t("filenodelink", - node=hex(n), file=f, - parity=parity.next())) + files.append(tmpl("filenodelink", + node=hex(n), file=f, + parity=parity.next())) def diff(**map): - yield self.diff(p1, n, None) + yield self.diff(tmpl, p1, n, None) - yield self.t('changeset', - diff=diff, - rev=ctx.rev(), - node=hex(n), - parent=self.siblings(parents), - child=self.siblings(ctx.children()), - changesettag=self.showtag("changesettag",n), - author=ctx.user(), - desc=ctx.description(), - date=ctx.date(), - files=files, - archives=self.archivelist(hex(n)), - tags=self.nodetagsdict(n), - branches=self.nodebranchdict(ctx)) + yield tmpl('changeset', + diff=diff, + rev=ctx.rev(), + node=hex(n), + parent=self.siblings(parents), + child=self.siblings(ctx.children()), + changesettag=self.showtag("changesettag",n), + author=ctx.user(), + desc=ctx.description(), + date=ctx.date(), + files=files, + archives=self.archivelist(hex(n)), + tags=self.nodetagsdict(n), + branches=self.nodebranchdict(ctx)) - def filelog(self, fctx): + def filelog(self, tmpl, fctx): f = fctx.path() fl = fctx.filelog() count = fl.count() @@ -379,11 +577,11 @@ nodefunc = lambda x: fctx.filectx(fileid=x) nav = revnavgen(pos, pagelen, count, nodefunc) - yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav, - entries=lambda **x: entries(limit=0, **x), - latestentry=lambda **x: entries(limit=1, **x)) + yield tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav, + entries=lambda **x: entries(limit=0, **x), + latestentry=lambda **x: entries(limit=1, **x)) - def filerevision(self, fctx): + def filerevision(self, tmpl, fctx): f = fctx.path() text = fctx.data() fl = fctx.filelog() @@ -403,23 +601,23 @@ "linenumber": "% 6d" % (l + 1), "parity": parity.next()} - yield self.t("filerevision", - file=f, - path=_up(f), - text=lines(), - raw=rawtext, - mimetype=mt, - rev=fctx.rev(), - node=hex(fctx.node()), - author=fctx.user(), - date=fctx.date(), - desc=fctx.description(), - parent=self.siblings(fctx.parents()), - child=self.siblings(fctx.children()), - rename=self.renamelink(fl, n), - permissions=fctx.manifest().flags(f)) + yield tmpl("filerevision", + file=f, + path=_up(f), + text=lines(), + raw=rawtext, + mimetype=mt, + rev=fctx.rev(), + node=hex(fctx.node()), + author=fctx.user(), + date=fctx.date(), + desc=fctx.description(), + parent=self.siblings(fctx.parents()), + child=self.siblings(fctx.children()), + rename=self.renamelink(fl, n), + permissions=fctx.manifest().flags(f)) - def fileannotate(self, fctx): + def fileannotate(self, tmpl, fctx): f = fctx.path() n = fctx.filenode() fl = fctx.filelog() @@ -441,21 +639,21 @@ "file": f.path(), "line": l} - yield self.t("fileannotate", - file=f, - annotate=annotate, - path=_up(f), - rev=fctx.rev(), - node=hex(fctx.node()), - author=fctx.user(), - date=fctx.date(), - desc=fctx.description(), - rename=self.renamelink(fl, n), - parent=self.siblings(fctx.parents()), - child=self.siblings(fctx.children()), - permissions=fctx.manifest().flags(f)) + yield tmpl("fileannotate", + file=f, + annotate=annotate, + path=_up(f), + rev=fctx.rev(), + node=hex(fctx.node()), + author=fctx.user(), + date=fctx.date(), + desc=fctx.description(), + rename=self.renamelink(fl, n), + parent=self.siblings(fctx.parents()), + child=self.siblings(fctx.children()), + permissions=fctx.manifest().flags(f)) - def manifest(self, ctx, path): + def manifest(self, tmpl, ctx, path): mf = ctx.manifest() node = ctx.node() @@ -478,6 +676,9 @@ short = os.path.basename(remain) files[short] = (f, n) + if not files: + raise ErrorResponse(404, 'Path not found: ' + path) + def filelist(**map): fl = files.keys() fl.sort() @@ -506,19 +707,19 @@ "path": "%s%s" % (abspath, f), "basename": f[:-1]} - yield self.t("manifest", - rev=ctx.rev(), - node=hex(node), - path=abspath, - up=_up(abspath), - upparity=parity.next(), - fentries=filelist, - dentries=dirlist, - archives=self.archivelist(hex(node)), - tags=self.nodetagsdict(node), - branches=self.nodebranchdict(ctx)) + yield tmpl("manifest", + rev=ctx.rev(), + node=hex(node), + path=abspath, + up=_up(abspath), + upparity=parity.next(), + fentries=filelist, + dentries=dirlist, + archives=self.archivelist(hex(node)), + tags=self.nodetagsdict(node), + branches=self.nodebranchdict(ctx)) - def tags(self): + def tags(self, tmpl): i = self.repo.tagslist() i.reverse() parity = paritygen(self.stripecount) @@ -536,13 +737,13 @@ "date": self.repo.changectx(n).date(), "node": hex(n)} - yield self.t("tags", - node=hex(self.repo.changelog.tip()), - entries=lambda **x: entries(False,0, **x), - entriesnotip=lambda **x: entries(True,0, **x), - latestentry=lambda **x: entries(True,1, **x)) + yield tmpl("tags", + node=hex(self.repo.changelog.tip()), + entries=lambda **x: entries(False,0, **x), + entriesnotip=lambda **x: entries(True,0, **x), + latestentry=lambda **x: entries(True,1, **x)) - def summary(self): + def summary(self, tmpl): i = self.repo.tagslist() i.reverse() @@ -557,11 +758,11 @@ if count > 10: # limit to 10 tags break; - yield self.t("tagentry", - parity=parity.next(), - tag=k, - node=hex(n), - date=self.repo.changectx(n).date()) + yield tmpl("tagentry", + parity=parity.next(), + tag=k, + node=hex(n), + date=self.repo.changectx(n).date()) def branches(**map): @@ -587,8 +788,8 @@ n = ctx.node() hn = hex(n) - l.insert(0, self.t( - 'shortlogentry', + l.insert(0, tmpl( + 'shortlogentry', parity=parity.next(), author=ctx.user(), desc=ctx.description(), @@ -605,34 +806,34 @@ start = max(0, count - self.maxchanges) end = min(count, start + self.maxchanges) - yield self.t("summary", - desc=self.config("web", "description", "unknown"), - owner=(self.config("ui", "username") or # preferred - self.config("web", "contact") or # deprecated - self.config("web", "author", "unknown")), # also - lastchange=cl.read(cl.tip())[2], - tags=tagentries, - branches=branches, - shortlog=changelist, - node=hex(cl.tip()), - archives=self.archivelist("tip")) + yield tmpl("summary", + desc=self.config("web", "description", "unknown"), + owner=(self.config("ui", "username") or # preferred + self.config("web", "contact") or # deprecated + self.config("web", "author", "unknown")), # also + lastchange=cl.read(cl.tip())[2], + tags=tagentries, + branches=branches, + shortlog=changelist, + node=hex(cl.tip()), + archives=self.archivelist("tip")) - def filediff(self, fctx): + def filediff(self, tmpl, fctx): n = fctx.node() path = fctx.path() parents = fctx.parents() p1 = parents and parents[0].node() or nullid def diff(**map): - yield self.diff(p1, n, [path]) + yield self.diff(tmpl, p1, n, [path]) - yield self.t("filediff", - file=path, - node=hex(n), - rev=fctx.rev(), - parent=self.siblings(parents), - child=self.siblings(fctx.children()), - diff=diff) + yield tmpl("filediff", + file=path, + node=hex(n), + rev=fctx.rev(), + parent=self.siblings(parents), + child=self.siblings(fctx.children()), + diff=diff) archive_specs = { 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None), @@ -640,7 +841,7 @@ 'zip': ('application/zip', 'zip', '.zip', None), } - def archive(self, req, key, type_): + def archive(self, tmpl, req, key, type_): reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame)) cnode = self.repo.lookup(key) arch_version = key @@ -664,198 +865,6 @@ path = path.lstrip('/') return util.canonpath(self.repo.root, '', path) - def run(self): - if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."): - raise RuntimeError("This function is only intended to be called while running as a CGI script.") - import mercurial.hgweb.wsgicgi as wsgicgi - from request import wsgiapplication - def make_web_app(): - return self - wsgicgi.launch(wsgiapplication(make_web_app)) - - def run_wsgi(self, req): - def header(**map): - header_file = cStringIO.StringIO( - ''.join(self.t("header", encoding=self.encoding, **map))) - msg = mimetools.Message(header_file, 0) - req.header(msg.items()) - yield header_file.read() - - def rawfileheader(**map): - req.header([('Content-type', map['mimetype']), - ('Content-disposition', 'filename=%s' % map['file']), - ('Content-length', str(len(map['raw'])))]) - yield '' - - def footer(**map): - yield self.t("footer", **map) - - def motd(**map): - yield self.config("web", "motd", "") - - def expand_form(form): - shortcuts = { - 'cl': [('cmd', ['changelog']), ('rev', None)], - 'sl': [('cmd', ['shortlog']), ('rev', None)], - 'cs': [('cmd', ['changeset']), ('node', None)], - 'f': [('cmd', ['file']), ('filenode', None)], - 'fl': [('cmd', ['filelog']), ('filenode', None)], - 'fd': [('cmd', ['filediff']), ('node', None)], - 'fa': [('cmd', ['annotate']), ('filenode', None)], - 'mf': [('cmd', ['manifest']), ('manifest', None)], - 'ca': [('cmd', ['archive']), ('node', None)], - 'tags': [('cmd', ['tags'])], - 'tip': [('cmd', ['changeset']), ('node', ['tip'])], - 'static': [('cmd', ['static']), ('file', None)] - } - - for k in shortcuts.iterkeys(): - if form.has_key(k): - for name, value in shortcuts[k]: - if value is None: - value = form[k] - form[name] = value - del form[k] - - def rewrite_request(req): - '''translate new web interface to traditional format''' - - def spliturl(req): - def firstitem(query): - return query.split('&', 1)[0].split(';', 1)[0] - - def normurl(url): - inner = '/'.join([x for x in url.split('/') if x]) - tl = len(url) > 1 and url.endswith('/') and '/' or '' - - return '%s%s%s' % (url.startswith('/') and '/' or '', - inner, tl) - - root = normurl(urllib.unquote(req.env.get('REQUEST_URI', '').split('?', 1)[0])) - pi = normurl(req.env.get('PATH_INFO', '')) - if pi: - # strip leading / - pi = pi[1:] - if pi: - root = root[:root.rfind(pi)] - if req.env.has_key('REPO_NAME'): - rn = req.env['REPO_NAME'] + '/' - root += rn - query = pi[len(rn):] - else: - query = pi - else: - root += '?' - query = firstitem(req.env['QUERY_STRING']) - - return (root, query) - - req.url, query = spliturl(req) - - if req.form.has_key('cmd'): - # old style - return - - args = query.split('/', 2) - if not args or not args[0]: - return - - cmd = args.pop(0) - style = cmd.rfind('-') - if style != -1: - req.form['style'] = [cmd[:style]] - cmd = cmd[style+1:] - # avoid accepting e.g. style parameter as command - if hasattr(self, 'do_' + cmd): - req.form['cmd'] = [cmd] - - if args and args[0]: - node = args.pop(0) - req.form['node'] = [node] - if args: - req.form['file'] = args - - if cmd == 'static': - req.form['file'] = req.form['node'] - elif cmd == 'archive': - fn = req.form['node'][0] - for type_, spec in self.archive_specs.iteritems(): - ext = spec[2] - if fn.endswith(ext): - req.form['node'] = [fn[:-len(ext)]] - req.form['type'] = [type_] - - def sessionvars(**map): - fields = [] - if req.form.has_key('style'): - style = req.form['style'][0] - if style != self.config('web', 'style', ''): - fields.append(('style', style)) - - separator = req.url[-1] == '?' and ';' or '?' - for name, value in fields: - yield dict(name=name, value=value, separator=separator) - separator = ';' - - self.refresh() - - expand_form(req.form) - rewrite_request(req) - - style = self.config("web", "style", "") - if req.form.has_key('style'): - style = req.form['style'][0] - mapfile = style_map(self.templatepath, style) - - proto = req.env.get('wsgi.url_scheme') - if proto == 'https': - proto = 'https' - default_port = "443" - else: - proto = 'http' - default_port = "80" - - port = req.env["SERVER_PORT"] - port = port != default_port and (":" + port) or "" - urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port) - staticurl = self.config("web", "staticurl") or req.url + 'static/' - if not staticurl.endswith('/'): - staticurl += '/' - - if not self.reponame: - self.reponame = (self.config("web", "name") - or req.env.get('REPO_NAME') - or req.url.strip('/') or self.repo.root) - - self.t = templater.templater(mapfile, templater.common_filters, - defaults={"url": req.url, - "staticurl": staticurl, - "urlbase": urlbase, - "repo": self.reponame, - "header": header, - "footer": footer, - "motd": motd, - "rawfileheader": rawfileheader, - "sessionvars": sessionvars - }) - - try: - if not req.form.has_key('cmd'): - req.form['cmd'] = [self.t.cache['default']] - - cmd = req.form['cmd'][0] - - method = getattr(self, 'do_' + cmd, None) - if method: - try: - method(req) - except (hg.RepoError, revlog.RevlogError), inst: - req.write(self.t("error", error=str(inst))) - else: - req.write(self.t("error", error='No such method: ' + cmd)) - finally: - self.t = None - def changectx(self, req): if req.form.has_key('node'): changeid = req.form['node'][0] @@ -887,181 +896,6 @@ return fctx - def do_log(self, req): - if req.form.has_key('file') and req.form['file'][0]: - self.do_filelog(req) - else: - self.do_changelog(req) - - def do_rev(self, req): - self.do_changeset(req) - - def do_file(self, req): - path = self.cleanpath(req.form.get('file', [''])[0]) - if path: - try: - req.write(self.filerevision(self.filectx(req))) - return - except revlog.LookupError: - pass - - req.write(self.manifest(self.changectx(req), path)) - - def do_diff(self, req): - self.do_filediff(req) - - def do_changelog(self, req, shortlog = False): - if req.form.has_key('node'): - ctx = self.changectx(req) - else: - if req.form.has_key('rev'): - hi = req.form['rev'][0] - else: - hi = self.repo.changelog.count() - 1 - try: - ctx = self.repo.changectx(hi) - except hg.RepoError: - req.write(self.search(hi)) # XXX redirect to 404 page? - return - - req.write(self.changelog(ctx, shortlog = shortlog)) - - def do_shortlog(self, req): - self.do_changelog(req, shortlog = True) - - def do_changeset(self, req): - req.write(self.changeset(self.changectx(req))) - - def do_manifest(self, req): - req.write(self.manifest(self.changectx(req), - self.cleanpath(req.form['path'][0]))) - - def do_tags(self, req): - req.write(self.tags()) - - def do_summary(self, req): - req.write(self.summary()) - - def do_filediff(self, req): - req.write(self.filediff(self.filectx(req))) - - def do_annotate(self, req): - req.write(self.fileannotate(self.filectx(req))) - - def do_filelog(self, req): - req.write(self.filelog(self.filectx(req))) - - def do_lookup(self, req): - try: - r = hex(self.repo.lookup(req.form['key'][0])) - success = 1 - except Exception,inst: - r = str(inst) - success = 0 - resp = "%s %s\n" % (success, r) - req.httphdr("application/mercurial-0.1", length=len(resp)) - req.write(resp) - - def do_heads(self, req): - resp = " ".join(map(hex, self.repo.heads())) + "\n" - req.httphdr("application/mercurial-0.1", length=len(resp)) - req.write(resp) - - def do_branches(self, req): - nodes = [] - if req.form.has_key('nodes'): - nodes = map(bin, req.form['nodes'][0].split(" ")) - resp = cStringIO.StringIO() - for b in self.repo.branches(nodes): - resp.write(" ".join(map(hex, b)) + "\n") - resp = resp.getvalue() - req.httphdr("application/mercurial-0.1", length=len(resp)) - req.write(resp) - - def do_between(self, req): - if req.form.has_key('pairs'): - pairs = [map(bin, p.split("-")) - for p in req.form['pairs'][0].split(" ")] - resp = cStringIO.StringIO() - for b in self.repo.between(pairs): - resp.write(" ".join(map(hex, b)) + "\n") - resp = resp.getvalue() - req.httphdr("application/mercurial-0.1", length=len(resp)) - req.write(resp) - - def do_changegroup(self, req): - req.httphdr("application/mercurial-0.1") - nodes = [] - if not self.allowpull: - return - - if req.form.has_key('roots'): - nodes = map(bin, req.form['roots'][0].split(" ")) - - z = zlib.compressobj() - f = self.repo.changegroup(nodes, 'serve') - while 1: - chunk = f.read(4096) - if not chunk: - break - req.write(z.compress(chunk)) - - req.write(z.flush()) - - def do_changegroupsubset(self, req): - req.httphdr("application/mercurial-0.1") - bases = [] - heads = [] - if not self.allowpull: - return - - if req.form.has_key('bases'): - bases = [bin(x) for x in req.form['bases'][0].split(' ')] - if req.form.has_key('heads'): - heads = [bin(x) for x in req.form['heads'][0].split(' ')] - - z = zlib.compressobj() - f = self.repo.changegroupsubset(bases, heads, 'serve') - while 1: - chunk = f.read(4096) - if not chunk: - break - req.write(z.compress(chunk)) - - req.write(z.flush()) - - def do_archive(self, req): - type_ = req.form['type'][0] - allowed = self.configlist("web", "allow_archive") - if (type_ in self.archives and (type_ in allowed or - self.configbool("web", "allow" + type_, False))): - self.archive(req, req.form['node'][0], type_) - return - - req.write(self.t("error")) - - def do_static(self, req): - fname = req.form['file'][0] - # a repo owner may set web.static in .hg/hgrc to get any file - # readable by the user running the CGI script - static = self.config("web", "static", - os.path.join(self.templatepath, "static"), - untrusted=False) - req.write(staticfile(static, fname, req) - or self.t("error", error="%r not found" % fname)) - - def do_capabilities(self, req): - caps = ['lookup', 'changegroupsubset'] - if self.configbool('server', 'uncompressed'): - caps.append('stream=%d' % self.repo.changelog.version) - # XXX: make configurable and/or share code with do_unbundle: - unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN'] - if unbundleversions: - caps.append('unbundle=%s' % ','.join(unbundleversions)) - resp = ' '.join(caps) - req.httphdr("application/mercurial-0.1", length=len(resp)) - req.write(resp) - def check_perm(self, req, op, default): '''check permission for operation based on user auth. return true if op allowed, else false. @@ -1075,134 +909,3 @@ allow = self.configlist('web', 'allow_' + op) return (allow and (allow == ['*'] or user in allow)) or default - - def do_unbundle(self, req): - def bail(response, headers={}): - length = int(req.env['CONTENT_LENGTH']) - for s in util.filechunkiter(req, limit=length): - # drain incoming bundle, else client will not see - # response when run outside cgi script - pass - req.httphdr("application/mercurial-0.1", headers=headers) - req.write('0\n') - req.write(response) - - # require ssl by default, auth info cannot be sniffed and - # replayed - ssl_req = self.configbool('web', 'push_ssl', True) - if ssl_req: - if req.env.get('wsgi.url_scheme') != 'https': - bail(_('ssl required\n')) - return - proto = 'https' - else: - proto = 'http' - - # do not allow push unless explicitly allowed - if not self.check_perm(req, 'push', False): - bail(_('push not authorized\n'), - headers={'status': '401 Unauthorized'}) - return - - their_heads = req.form['heads'][0].split(' ') - - def check_heads(): - heads = map(hex, self.repo.heads()) - return their_heads == [hex('force')] or their_heads == heads - - # fail early if possible - if not check_heads(): - bail(_('unsynced changes\n')) - return - - req.httphdr("application/mercurial-0.1") - - # do not lock repo until all changegroup data is - # streamed. save to temporary file. - - fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') - fp = os.fdopen(fd, 'wb+') - try: - length = int(req.env['CONTENT_LENGTH']) - for s in util.filechunkiter(req, limit=length): - fp.write(s) - - try: - lock = self.repo.lock() - try: - if not check_heads(): - req.write('0\n') - req.write(_('unsynced changes\n')) - return - - fp.seek(0) - header = fp.read(6) - if not header.startswith("HG"): - # old client with uncompressed bundle - def generator(f): - yield header - for chunk in f: - yield chunk - elif not header.startswith("HG10"): - req.write("0\n") - req.write(_("unknown bundle version\n")) - return - elif header == "HG10GZ": - def generator(f): - zd = zlib.decompressobj() - for chunk in f: - yield zd.decompress(chunk) - elif header == "HG10BZ": - def generator(f): - zd = bz2.BZ2Decompressor() - zd.decompress("BZ") - for chunk in f: - yield zd.decompress(chunk) - elif header == "HG10UN": - def generator(f): - for chunk in f: - yield chunk - else: - req.write("0\n") - req.write(_("unknown bundle compression type\n")) - return - gen = generator(util.filechunkiter(fp, 4096)) - - # send addchangegroup output to client - - old_stdout = sys.stdout - sys.stdout = cStringIO.StringIO() - - try: - url = 'remote:%s:%s' % (proto, - req.env.get('REMOTE_HOST', '')) - try: - ret = self.repo.addchangegroup( - util.chunkbuffer(gen), 'serve', url) - except util.Abort, inst: - sys.stdout.write("abort: %s\n" % inst) - ret = 0 - finally: - val = sys.stdout.getvalue() - sys.stdout = old_stdout - req.write('%d\n' % ret) - req.write(val) - finally: - del lock - except (OSError, IOError), inst: - req.write('0\n') - filename = getattr(inst, 'filename', '') - # Don't send our filesystem layout to the client - if filename.startswith(self.repo.root): - filename = filename[len(self.repo.root)+1:] - else: - filename = '' - error = getattr(inst, 'strerror', 'Unknown error') - req.write('%s: %s\n' % (error, filename)) - finally: - fp.close() - os.unlink(tempname) - - def do_stream_out(self, req): - req.httphdr("application/mercurial-0.1") - streamclone.stream_out(self.repo, req, untrusted=True)
--- a/mercurial/hgweb/hgwebdir_mod.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/hgweb/hgwebdir_mod.py Thu Dec 06 13:11:36 2007 -0800 @@ -9,8 +9,9 @@ import os, mimetools, cStringIO from mercurial.i18n import gettext as _ from mercurial import ui, hg, util, templater -from common import get_mtime, staticfile, style_map, paritygen +from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen from hgweb_mod import hgweb +from request import wsgirequest # This is a stopgap class hgwebdir(object): @@ -19,7 +20,8 @@ return [(util.pconvert(name).strip('/'), path) for name, path in items] - self.parentui = parentui + self.parentui = parentui or ui.ui(report_untrusted=False, + interactive = False) self.motd = None self.style = None self.stripecount = None @@ -60,60 +62,74 @@ if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."): raise RuntimeError("This function is only intended to be called while running as a CGI script.") import mercurial.hgweb.wsgicgi as wsgicgi - from request import wsgiapplication - def make_web_app(): - return self - wsgicgi.launch(wsgiapplication(make_web_app)) + wsgicgi.launch(self) + + def __call__(self, env, respond): + req = wsgirequest(env, respond) + self.run_wsgi(req) + return req def run_wsgi(self, req): - def header(**map): - header_file = cStringIO.StringIO( - ''.join(tmpl("header", encoding=util._encoding, **map))) - msg = mimetools.Message(header_file, 0) - req.header(msg.items()) - yield header_file.read() - def footer(**map): - yield tmpl("footer", **map) + try: + try: - def motd(**map): - if self.motd is not None: - yield self.motd - else: - yield config('web', 'motd', '') + virtual = req.env.get("PATH_INFO", "").strip('/') + + # a static file + if virtual.startswith('static/') or 'static' in req.form: + static = os.path.join(templater.templatepath(), 'static') + if virtual.startswith('static/'): + fname = virtual[7:] + else: + fname = req.form['static'][0] + req.write(staticfile(static, fname, req)) + return - parentui = self.parentui or ui.ui(report_untrusted=False, - interactive=False) - - def config(section, name, default=None, untrusted=True): - return parentui.config(section, name, default, untrusted) + # top-level index + elif not virtual: + tmpl = self.templater(req) + self.makeindex(req, tmpl) + return - url = req.env['REQUEST_URI'].split('?')[0] - if not url.endswith('/'): - url += '/' - pathinfo = req.env.get('PATH_INFO', '').strip('/') + '/' - base = url[:len(url) - len(pathinfo)] - if not base.endswith('/'): - base += '/' - - staticurl = config('web', 'staticurl') or base + 'static/' - if not staticurl.endswith('/'): - staticurl += '/' + # nested indexes and hgwebs + repos = dict(self.repos) + while virtual: + real = repos.get(virtual) + if real: + req.env['REPO_NAME'] = virtual + try: + repo = hg.repository(self.parentui, real) + hgweb(repo).run_wsgi(req) + return + except IOError, inst: + raise ErrorResponse(500, inst.strerror) + except hg.RepoError, inst: + raise ErrorResponse(500, str(inst)) - style = self.style - if style is None: - style = config('web', 'style', '') - if req.form.has_key('style'): - style = req.form['style'][0] - if self.stripecount is None: - self.stripecount = int(config('web', 'stripes', 1)) - mapfile = style_map(templater.templatepath(), style) - tmpl = templater.templater(mapfile, templater.common_filters, - defaults={"header": header, - "footer": footer, - "motd": motd, - "url": url, - "staticurl": staticurl}) + # browse subdirectories + subdir = virtual + '/' + if [r for r in repos if r.startswith(subdir)]: + tmpl = self.templater(req) + self.makeindex(req, tmpl, subdir) + return + + up = virtual.rfind('/') + if up < 0: + break + virtual = virtual[:up] + + # prefixes not found + tmpl = self.templater(req) + req.respond(404, tmpl("notfound", repo=virtual)) + + except ErrorResponse, err: + tmpl = self.templater(req) + req.respond(err.code, tmpl('error', error=err.message or '')) + finally: + tmpl = None + + def makeindex(self, req, tmpl, subdir=""): def archivelist(ui, nodeid, url): allowed = ui.configlist("web", "allow_archive", untrusted=True) @@ -143,7 +159,7 @@ continue name = name[len(subdir):] - u = ui.ui(parentui=parentui) + u = ui.ui(parentui=self.parentui) try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except Exception, e: @@ -155,8 +171,10 @@ if u.configbool("web", "hidden", untrusted=True): continue - url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name]) - .replace("//", "/")) + '/' + parts = [req.env['PATH_INFO'], name] + if req.env['SCRIPT_NAME']: + parts.insert(0, req.env['SCRIPT_NAME']) + url = ('/'.join(parts).replace("//", "/")) + '/' # update time with local timezone try: @@ -195,66 +213,65 @@ row['parity'] = parity.next() yield row - def makeindex(req, subdir=""): - sortable = ["name", "description", "contact", "lastchange"] - sortcolumn, descending = self.repos_sorted - if req.form.has_key('sort'): - sortcolumn = req.form['sort'][0] - descending = sortcolumn.startswith('-') - if descending: - sortcolumn = sortcolumn[1:] - if sortcolumn not in sortable: - sortcolumn = "" + sortable = ["name", "description", "contact", "lastchange"] + sortcolumn, descending = self.repos_sorted + if req.form.has_key('sort'): + sortcolumn = req.form['sort'][0] + descending = sortcolumn.startswith('-') + if descending: + sortcolumn = sortcolumn[1:] + if sortcolumn not in sortable: + sortcolumn = "" - sort = [("sort_%s" % column, - "%s%s" % ((not descending and column == sortcolumn) - and "-" or "", column)) - for column in sortable] - req.write(tmpl("index", entries=entries, subdir=subdir, - sortcolumn=sortcolumn, descending=descending, - **dict(sort))) + sort = [("sort_%s" % column, + "%s%s" % ((not descending and column == sortcolumn) + and "-" or "", column)) + for column in sortable] + req.write(tmpl("index", entries=entries, subdir=subdir, + sortcolumn=sortcolumn, descending=descending, + **dict(sort))) + + def templater(self, req): + + def header(**map): + header_file = cStringIO.StringIO( + ''.join(tmpl("header", encoding=util._encoding, **map))) + msg = mimetools.Message(header_file, 0) + req.header(msg.items()) + yield header_file.read() + + def footer(**map): + yield tmpl("footer", **map) - try: - virtual = req.env.get("PATH_INFO", "").strip('/') - if virtual.startswith('static/'): - static = os.path.join(templater.templatepath(), 'static') - fname = virtual[7:] - req.write(staticfile(static, fname, req) or - tmpl('error', error='%r not found' % fname)) - elif virtual: - repos = dict(self.repos) - while virtual: - real = repos.get(virtual) - if real: - req.env['REPO_NAME'] = virtual - try: - repo = hg.repository(parentui, real) - hgweb(repo).run_wsgi(req) - except IOError, inst: - req.write(tmpl("error", error=inst.strerror)) - except hg.RepoError, inst: - req.write(tmpl("error", error=str(inst))) - return + def motd(**map): + if self.motd is not None: + yield self.motd + else: + yield config('web', 'motd', '') + + def config(section, name, default=None, untrusted=True): + return self.parentui.config(section, name, default, untrusted) + + url = req.env.get('SCRIPT_NAME', '') + if not url.endswith('/'): + url += '/' - # browse subdirectories - subdir = virtual + '/' - if [r for r in repos if r.startswith(subdir)]: - makeindex(req, subdir) - return - - up = virtual.rfind('/') - if up < 0: - break - virtual = virtual[:up] + staticurl = config('web', 'staticurl') or url + 'static/' + if not staticurl.endswith('/'): + staticurl += '/' - req.write(tmpl("notfound", repo=virtual)) - else: - if req.form.has_key('static'): - static = os.path.join(templater.templatepath(), "static") - fname = req.form['static'][0] - req.write(staticfile(static, fname, req) - or tmpl("error", error="%r not found" % fname)) - else: - makeindex(req) - finally: - tmpl = None + style = self.style + if style is None: + style = config('web', 'style', '') + if req.form.has_key('style'): + style = req.form['style'][0] + if self.stripecount is None: + self.stripecount = int(config('web', 'stripes', 1)) + mapfile = style_map(templater.templatepath(), style) + tmpl = templater.templater(mapfile, templater.common_filters, + defaults={"header": header, + "footer": footer, + "motd": motd, + "url": url, + "staticurl": staticurl}) + return tmpl
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/hgweb/protocol.py Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,237 @@ +# +# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> +# Copyright 2005-2007 Matt Mackall <mpm@selenic.com> +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +import cStringIO, zlib, bz2, tempfile, errno, os, sys +from mercurial import util, streamclone +from mercurial.i18n import gettext as _ +from mercurial.node import * + +def lookup(web, req): + try: + r = hex(web.repo.lookup(req.form['key'][0])) + success = 1 + except Exception,inst: + r = str(inst) + success = 0 + resp = "%s %s\n" % (success, r) + req.httphdr("application/mercurial-0.1", length=len(resp)) + req.write(resp) + +def heads(web, req): + resp = " ".join(map(hex, web.repo.heads())) + "\n" + req.httphdr("application/mercurial-0.1", length=len(resp)) + req.write(resp) + +def branches(web, req): + nodes = [] + if req.form.has_key('nodes'): + nodes = map(bin, req.form['nodes'][0].split(" ")) + resp = cStringIO.StringIO() + for b in web.repo.branches(nodes): + resp.write(" ".join(map(hex, b)) + "\n") + resp = resp.getvalue() + req.httphdr("application/mercurial-0.1", length=len(resp)) + req.write(resp) + +def between(web, req): + if req.form.has_key('pairs'): + pairs = [map(bin, p.split("-")) + for p in req.form['pairs'][0].split(" ")] + resp = cStringIO.StringIO() + for b in web.repo.between(pairs): + resp.write(" ".join(map(hex, b)) + "\n") + resp = resp.getvalue() + req.httphdr("application/mercurial-0.1", length=len(resp)) + req.write(resp) + +def changegroup(web, req): + req.httphdr("application/mercurial-0.1") + nodes = [] + if not web.allowpull: + return + + if req.form.has_key('roots'): + nodes = map(bin, req.form['roots'][0].split(" ")) + + z = zlib.compressobj() + f = web.repo.changegroup(nodes, 'serve') + while 1: + chunk = f.read(4096) + if not chunk: + break + req.write(z.compress(chunk)) + + req.write(z.flush()) + +def changegroupsubset(web, req): + req.httphdr("application/mercurial-0.1") + bases = [] + heads = [] + if not web.allowpull: + return + + if req.form.has_key('bases'): + bases = [bin(x) for x in req.form['bases'][0].split(' ')] + if req.form.has_key('heads'): + heads = [bin(x) for x in req.form['heads'][0].split(' ')] + + z = zlib.compressobj() + f = web.repo.changegroupsubset(bases, heads, 'serve') + while 1: + chunk = f.read(4096) + if not chunk: + break + req.write(z.compress(chunk)) + + req.write(z.flush()) + +def capabilities(web, req): + caps = ['lookup', 'changegroupsubset'] + if web.configbool('server', 'uncompressed'): + caps.append('stream=%d' % web.repo.changelog.version) + # XXX: make configurable and/or share code with do_unbundle: + unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN'] + if unbundleversions: + caps.append('unbundle=%s' % ','.join(unbundleversions)) + resp = ' '.join(caps) + req.httphdr("application/mercurial-0.1", length=len(resp)) + req.write(resp) + +def unbundle(web, req): + def bail(response, headers={}): + length = int(req.env['CONTENT_LENGTH']) + for s in util.filechunkiter(req, limit=length): + # drain incoming bundle, else client will not see + # response when run outside cgi script + pass + req.httphdr("application/mercurial-0.1", headers=headers) + req.write('0\n') + req.write(response) + + # require ssl by default, auth info cannot be sniffed and + # replayed + ssl_req = web.configbool('web', 'push_ssl', True) + if ssl_req: + if req.env.get('wsgi.url_scheme') != 'https': + bail(_('ssl required\n')) + return + proto = 'https' + else: + proto = 'http' + + # do not allow push unless explicitly allowed + if not web.check_perm(req, 'push', False): + bail(_('push not authorized\n'), + headers={'status': '401 Unauthorized'}) + return + + their_heads = req.form['heads'][0].split(' ') + + def check_heads(): + heads = map(hex, web.repo.heads()) + return their_heads == [hex('force')] or their_heads == heads + + # fail early if possible + if not check_heads(): + bail(_('unsynced changes\n')) + return + + req.httphdr("application/mercurial-0.1") + + # do not lock repo until all changegroup data is + # streamed. save to temporary file. + + fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') + fp = os.fdopen(fd, 'wb+') + try: + length = int(req.env['CONTENT_LENGTH']) + for s in util.filechunkiter(req, limit=length): + fp.write(s) + + try: + lock = web.repo.lock() + try: + if not check_heads(): + req.write('0\n') + req.write(_('unsynced changes\n')) + return + + fp.seek(0) + header = fp.read(6) + if not header.startswith("HG"): + # old client with uncompressed bundle + def generator(f): + yield header + for chunk in f: + yield chunk + elif not header.startswith("HG10"): + req.write("0\n") + req.write(_("unknown bundle version\n")) + return + elif header == "HG10GZ": + def generator(f): + zd = zlib.decompressobj() + for chunk in f: + yield zd.decompress(chunk) + elif header == "HG10BZ": + def generator(f): + zd = bz2.BZ2Decompressor() + zd.decompress("BZ") + for chunk in f: + yield zd.decompress(chunk) + elif header == "HG10UN": + def generator(f): + for chunk in f: + yield chunk + else: + req.write("0\n") + req.write(_("unknown bundle compression type\n")) + return + gen = generator(util.filechunkiter(fp, 4096)) + + # send addchangegroup output to client + + old_stdout = sys.stdout + sys.stdout = cStringIO.StringIO() + + try: + url = 'remote:%s:%s' % (proto, + req.env.get('REMOTE_HOST', '')) + try: + ret = web.repo.addchangegroup( + util.chunkbuffer(gen), 'serve', url) + except util.Abort, inst: + sys.stdout.write("abort: %s\n" % inst) + ret = 0 + finally: + val = sys.stdout.getvalue() + sys.stdout = old_stdout + req.write('%d\n' % ret) + req.write(val) + finally: + del lock + except (OSError, IOError), inst: + req.write('0\n') + filename = getattr(inst, 'filename', '') + # Don't send our filesystem layout to the client + if filename.startswith(web.repo.root): + filename = filename[len(web.repo.root)+1:] + else: + filename = '' + error = getattr(inst, 'strerror', 'Unknown error') + if inst.errno == errno.ENOENT: + code = 404 + else: + code = 500 + req.respond(code, '%s: %s\n' % (error, filename)) + finally: + fp.close() + os.unlink(tempname) + +def stream_out(web, req): + req.httphdr("application/mercurial-0.1") + streamclone.stream_out(web.repo, req, untrusted=True)
--- a/mercurial/hgweb/request.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/hgweb/request.py Thu Dec 06 13:11:36 2007 -0800 @@ -8,16 +8,10 @@ import socket, cgi, errno from mercurial.i18n import gettext as _ - -class wsgiapplication(object): - def __init__(self, destmaker): - self.destmaker = destmaker +from common import ErrorResponse, statusmessage - def __call__(self, wsgienv, start_response): - return _wsgirequest(self.destmaker(), wsgienv, start_response) - -class _wsgirequest(object): - def __init__(self, destination, wsgienv, start_response): +class wsgirequest(object): + def __init__(self, wsgienv, start_response): version = wsgienv['wsgi.version'] if (version < (1, 0)) or (version >= (2, 0)): raise RuntimeError("Unknown and unsupported WSGI version %d.%d" @@ -32,7 +26,6 @@ self.form = cgi.parse(self.inp, self.env, keep_blank_values=1) self.start_response = start_response self.headers = [] - destination.run_wsgi(self) out = property(lambda self: self) @@ -42,25 +35,32 @@ def read(self, count=-1): return self.inp.read(count) - def write(self, *things): + def respond(self, status, *things): for thing in things: if hasattr(thing, "__iter__"): for part in thing: - self.write(part) + self.respond(status, part) else: thing = str(thing) if self.server_write is None: if not self.headers: raise RuntimeError("request.write called before headers sent (%s)." % thing) - self.server_write = self.start_response('200 Script output follows', + if isinstance(status, ErrorResponse): + status = statusmessage(status.code) + elif isinstance(status, int): + status = statusmessage(status) + self.server_write = self.start_response(status, self.headers) self.start_response = None - self.headers = None + self.headers = [] try: self.server_write(thing) except socket.error, inst: if inst[0] != errno.ECONNRESET: raise + + def write(self, *things): + self.respond('200 Script output follows', *things) def writelines(self, lines): for line in lines: @@ -84,3 +84,9 @@ if length: headers.append(('Content-length', str(length))) self.header(headers) + +def wsgiapplication(app_maker): + application = app_maker() + def run_wsgi(env, respond): + application(env, respond) + return run_wsgi
--- a/mercurial/hgweb/server.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/hgweb/server.py Thu Dec 06 13:11:36 2007 -0800 @@ -10,7 +10,6 @@ from mercurial import ui, hg, util, templater from hgweb_mod import hgweb from hgwebdir_mod import hgwebdir -from request import wsgiapplication from mercurial.i18n import gettext as _ def _splitURI(uri): @@ -85,6 +84,7 @@ env['SERVER_NAME'] = self.server.server_name env['SERVER_PORT'] = str(self.server.server_port) env['REQUEST_URI'] = self.path + env['SCRIPT_NAME'] = '' env['PATH_INFO'] = path_info env['REMOTE_HOST'] = self.client_address[0] env['REMOTE_ADDR'] = self.client_address[0] @@ -121,10 +121,7 @@ self.saved_headers = [] self.sent_headers = False self.length = None - req = self.server.reqmaker(env, self._start_response) - for data in req: - if data: - self._write(data) + self.server.application(env, self._start_response) def send_headers(self): if not self.saved_status: @@ -250,7 +247,7 @@ raise hg.RepoError(_("There is no Mercurial repository here" " (.hg not found)")) return hgwebobj - self.reqmaker = wsgiapplication(make_handler) + self.application = make_handler() addr = address if addr in ('', '::'):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/hgweb/webcommands.py Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,92 @@ +# +# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> +# Copyright 2005-2007 Matt Mackall <mpm@selenic.com> +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +import os +from mercurial import revlog +from common import staticfile + +def log(web, req, tmpl): + if req.form.has_key('file') and req.form['file'][0]: + filelog(web, req, tmpl) + else: + changelog(web, req, tmpl) + +def file(web, req, tmpl): + path = web.cleanpath(req.form.get('file', [''])[0]) + if path: + try: + req.write(web.filerevision(tmpl, web.filectx(req))) + return + except revlog.LookupError: + pass + + req.write(web.manifest(tmpl, web.changectx(req), path)) + +def changelog(web, req, tmpl, shortlog = False): + if req.form.has_key('node'): + ctx = web.changectx(req) + else: + if req.form.has_key('rev'): + hi = req.form['rev'][0] + else: + hi = web.repo.changelog.count() - 1 + try: + ctx = web.repo.changectx(hi) + except hg.RepoError: + req.write(web.search(tmpl, hi)) # XXX redirect to 404 page? + return + + req.write(web.changelog(tmpl, ctx, shortlog = shortlog)) + +def shortlog(web, req, tmpl): + changelog(web, req, tmpl, shortlog = True) + +def changeset(web, req, tmpl): + req.write(web.changeset(tmpl, web.changectx(req))) + +rev = changeset + +def manifest(web, req, tmpl): + req.write(web.manifest(tmpl, web.changectx(req), + web.cleanpath(req.form['path'][0]))) + +def tags(web, req, tmpl): + req.write(web.tags(tmpl)) + +def summary(web, req, tmpl): + req.write(web.summary(tmpl)) + +def filediff(web, req, tmpl): + req.write(web.filediff(tmpl, web.filectx(req))) + +diff = filediff + +def annotate(web, req, tmpl): + req.write(web.fileannotate(tmpl, web.filectx(req))) + +def filelog(web, req, tmpl): + req.write(web.filelog(tmpl, web.filectx(req))) + +def archive(web, req, tmpl): + type_ = req.form['type'][0] + allowed = web.configlist("web", "allow_archive") + if (type_ in web.archives and (type_ in allowed or + web.configbool("web", "allow" + type_, False))): + web.archive(tmpl, req, req.form['node'][0], type_) + return + + req.respond(400, tmpl('error', + error='Unsupported archive type: %s' % type_)) + +def static(web, req, tmpl): + fname = req.form['file'][0] + # a repo owner may set web.static in .hg/hgrc to get any file + # readable by the user running the CGI script + static = web.config("web", "static", + os.path.join(web.templatepath, "static"), + untrusted=False) + req.write(staticfile(static, fname, req))
--- a/mercurial/hgweb/wsgicgi.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/hgweb/wsgicgi.py Thu Dec 06 13:11:36 2007 -0800 @@ -16,6 +16,7 @@ util.set_binary(sys.stdout) environ = dict(os.environ.items()) + environ.setdefault('PATH_INFO', '') environ['wsgi.input'] = sys.stdin environ['wsgi.errors'] = sys.stderr environ['wsgi.version'] = (1, 0) @@ -61,13 +62,4 @@ headers_set[:] = [status, response_headers] return write - result = application(environ, start_response) - try: - for data in result: - if data: # don't send headers until body appears - write(data) - if not headers_sent: - write('') # send headers now if body was empty - finally: - if hasattr(result,'close'): - result.close() + application(environ, start_response)
--- a/mercurial/localrepo.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/localrepo.py Thu Dec 06 13:11:36 2007 -0800 @@ -1710,6 +1710,8 @@ # Go through all our files in order sorted by name. for fname in changedfiles: filerevlog = self.file(fname) + if filerevlog.count() == 0: + raise util.abort(_("empty or missing revlog for %s") % fname) # Toss out the filenodes that the recipient isn't really # missing. if msng_filenode_set.has_key(fname): @@ -1794,6 +1796,8 @@ for fname in changedfiles: filerevlog = self.file(fname) + if filerevlog.count() == 0: + raise util.abort(_("empty or missing revlog for %s") % fname) nodeiter = gennodelst(filerevlog) nodeiter = list(nodeiter) if nodeiter:
--- a/mercurial/revlog.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/revlog.py Thu Dec 06 13:11:36 2007 -0800 @@ -31,8 +31,13 @@ class RevlogError(Exception): pass + class LookupError(RevlogError): - pass + def __init__(self, name, message=None): + if message is None: + message = _('not found: %s') % name + RevlogError.__init__(self, message) + self.name = name def getoffset(q): return int(q >> 16) @@ -321,7 +326,7 @@ e = _unpack(indexformatv0, cur) # transform to revlogv1 format e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3], - nodemap[e[4]], nodemap[e[5]], e[6]) + nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6]) index.append(e2) nodemap[e[6]] = n n += 1 @@ -516,7 +521,7 @@ try: return self.nodemap[node] except KeyError: - raise LookupError(_('%s: no node %s') % (self.indexfile, hex(node))) + raise LookupError(hex(node), _('%s: no node %s') % (self.indexfile, hex(node))) def node(self, rev): return self.index[rev][7] def linkrev(self, node): @@ -836,7 +841,8 @@ for n in self.nodemap: if n.startswith(bin_id) and hex(n).startswith(id): if node is not None: - raise LookupError(_("Ambiguous identifier")) + raise LookupError(hex(node), + _("Ambiguous identifier")) node = n if node is not None: return node @@ -855,7 +861,7 @@ if n: return n - raise LookupError(_("No match found")) + raise LookupError(id, _("No match found")) def cmp(self, node, text): """compare text with a given file revision""" @@ -1166,13 +1172,13 @@ for p in (p1, p2): if not p in self.nodemap: - raise LookupError(_("unknown parent %s") % short(p)) + raise LookupError(hex(p), _("unknown parent %s") % short(p)) if not chain: # retrieve the parent revision of the delta chain chain = p1 if not chain in self.nodemap: - raise LookupError(_("unknown base %s") % short(chain[:4])) + raise LookupError(hex(chain), _("unknown base %s") % short(chain[:4])) # full versions are inserted when the needed deltas become # comparable to the uncompressed text or when the previous
--- a/mercurial/ui.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/ui.py Thu Dec 06 13:11:36 2007 -0800 @@ -403,7 +403,12 @@ readline.read_history_file except ImportError: pass - return raw_input(prompt) + line = raw_input(prompt) + # When stdin is in binary mode on Windows, it can cause + # raw_input() to emit an extra trailing carriage return + if os.linesep == '\r\n' and line and line[-1] == '\r': + line = line[:-1] + return line def prompt(self, msg, pat=None, default="y", matchflags=0): if not self.interactive: return default
--- a/mercurial/util_win32.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/util_win32.py Thu Dec 06 13:11:36 2007 -0800 @@ -16,6 +16,7 @@ from i18n import _ import errno, os, pywintypes, win32con, win32file, win32process import cStringIO, winerror +import osutil from win32com.shell import shell,shellcon class WinError: @@ -179,6 +180,20 @@ def system_rcpath_win32(): '''return default os-specific hgrc search path''' + try: + value = win32api.RegQueryValue( + win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Mercurial') + rcpath = [] + for p in value.split(os.pathsep): + if p.lower().endswith('mercurial.ini'): + rcpath.append(p) + elif os.path.isdir(p): + for f, kind in osutil.listdir(p): + if f.endswith('.rc'): + rcpath.append(os.path.join(p, f)) + return rcpath + except pywintypes.error: + pass proc = win32api.GetCurrentProcess() try: # This will fail on windows < NT
--- a/mercurial/verify.py Thu Dec 06 13:10:25 2007 -0800 +++ b/mercurial/verify.py Thu Dec 06 13:11:36 2007 -0800 @@ -62,9 +62,14 @@ repo.ui.status(_("repository uses revlog format %d\n") % (revlogv1 and 1 or 0)) + havecl = havemf = 1 seen = {} repo.ui.status(_("checking changesets\n")) - checksize(repo.changelog, "changelog") + if repo.changelog.count() == 0 and repo.manifest.count() > 1: + havecl = 0 + err(0, _("empty or missing 00changelog.i")) + else: + checksize(repo.changelog, "changelog") for i in xrange(repo.changelog.count()): changesets += 1 @@ -96,14 +101,18 @@ seen = {} repo.ui.status(_("checking manifests\n")) - checkversion(repo.manifest, "manifest") - checksize(repo.manifest, "manifest") + if repo.changelog.count() > 0 and repo.manifest.count() == 0: + havemf = 0 + err(0, _("empty or missing 00manifest.i")) + else: + checkversion(repo.manifest, "manifest") + checksize(repo.manifest, "manifest") for i in xrange(repo.manifest.count()): n = repo.manifest.node(i) l = repo.manifest.linkrev(n) - if l < 0 or l >= repo.changelog.count(): + if l < 0 or (havecl and l >= repo.changelog.count()): err(None, _("bad link (%d) at manifest revision %d") % (l, i)) if n in neededmanifests: @@ -132,38 +141,51 @@ repo.ui.status(_("crosschecking files in changesets and manifests\n")) - nm = neededmanifests.items() - nm.sort() - for m, c in nm: - err(m, _("changeset refers to unknown manifest %s") % short(c)) - del neededmanifests, nm + if havemf > 0: + nm = [(c, m) for m, c in neededmanifests.items()] + nm.sort() + for c, m in nm: + err(c, _("changeset refers to unknown manifest %s") % short(m)) + del neededmanifests, nm - for f in filenodes: - if f not in filelinkrevs: - lrs = [repo.manifest.linkrev(n) for n in filenodes[f]] - lrs.sort() - err(lrs[0], _("in manifest but not in changeset"), f) + if havecl: + fl = filenodes.keys() + fl.sort() + for f in fl: + if f not in filelinkrevs: + lrs = [repo.manifest.linkrev(n) for n in filenodes[f]] + lrs.sort() + err(lrs[0], _("in manifest but not in changeset"), f) + del fl - for f in filelinkrevs: - if f not in filenodes: - lr = filelinkrevs[f][0] - err(lr, _("in changeset but not in manifest"), f) + if havemf: + fl = filelinkrevs.keys() + fl.sort() + for f in fl: + if f not in filenodes: + lr = filelinkrevs[f][0] + err(lr, _("in changeset but not in manifest"), f) + del fl repo.ui.status(_("checking files\n")) - ff = filenodes.keys() + ff = dict.fromkeys(filenodes.keys() + filelinkrevs.keys()).keys() ff.sort() for f in ff: if f == "/dev/null": continue files += 1 if not f: - lr = repo.manifest.linkrev(filenodes[f][0]) - err(lr, _("file without name in manifest %s") % short(ff[n])) + lr = filelinkrevs[f][0] + err(lr, _("file without name in manifest")) continue fl = repo.file(f) checkversion(fl, f) checksize(fl, f) + if fl.count() == 0: + err(filelinkrevs[f][0], _("empty or missing revlog"), f) + continue + seen = {} nodes = {nullid: 1} for i in xrange(fl.count()): @@ -171,7 +193,7 @@ n = fl.node(i) flr = fl.linkrev(n) - if flr not in filelinkrevs.get(f, []): + if flr < 0 or (havecl and flr not in filelinkrevs.get(f, [])): if flr < 0 or flr >= repo.changelog.count(): err(None, _("rev %d point to nonexistent changeset %d") % (i, flr), f) @@ -182,14 +204,16 @@ warn(_(" (expected %s)") % filelinkrevs[f][0]) flr = None # can't be trusted else: - filelinkrevs[f].remove(flr) + if havecl: + filelinkrevs[f].remove(flr) if n in seen: err(flr, _("duplicate revision %d") % i, f) - if n not in filenodes[f]: - err(flr, _("%s not in manifests") % (short(n)), f) - else: - del filenodes[f][n] + if f in filenodes: + if havemf and n not in filenodes[f]: + err(flr, _("%s not in manifests") % (short(n)), f) + else: + del filenodes[f][n] # verify contents try: @@ -230,11 +254,12 @@ (short(n), inst), f) # cross-check - fns = [(repo.manifest.linkrev(filenodes[f][n]), n) - for n in filenodes[f]] - fns.sort() - for lr, node in fns: - err(lr, _("%s in manifests not found") % short(node), f) + if f in filenodes: + fns = [(repo.manifest.linkrev(filenodes[f][n]), n) + for n in filenodes[f]] + fns.sort() + for lr, node in fns: + err(lr, _("%s in manifests not found") % short(node), f) repo.ui.status(_("%d files, %d changesets, %d total revisions\n") % (files, changesets, revisions)) @@ -247,4 +272,3 @@ repo.ui.warn(_("(first damaged changeset appears to be %d)\n") % firstbad[0]) return 1 -
--- a/templates/gitweb/notfound.tmpl Thu Dec 06 13:10:25 2007 -0800 +++ b/templates/gitweb/notfound.tmpl Thu Dec 06 13:11:36 2007 -0800 @@ -1,5 +1,5 @@ {header} -<title>Mercurial repositories index</title> +<title>Mercurial repository not found</title> </head> <body>
--- a/templates/map-cmdline.default Thu Dec 06 13:10:25 2007 -0800 +++ b/templates/map-cmdline.default Thu Dec 06 13:11:36 2007 -0800 @@ -1,10 +1,13 @@ changeset = 'changeset: {rev}:{node|short}\n{branches}{tags}{parents}user: {author}\ndate: {date|date}\nsummary: {desc|firstline}\n\n' changeset_quiet = '{rev}:{node|short}\n' -changeset_verbose = 'changeset: {rev}:{node|short}\n{branches}{tags}{parents}{manifest}user: {author}\ndate: {date|date}\n{files}{file_adds}{file_dels}{file_copies}description:\n{desc|strip}\n\n\n' -changeset_debug = 'changeset: {rev}:{node}\n{branches}{tags}{parents}{manifest}user: {author}\ndate: {date|date}\n{files}{file_adds}{file_dels}{file_copies}{extras}description:\n{desc|strip}\n\n\n' +changeset_verbose = 'changeset: {rev}:{node|short}\n{branches}{tags}{parents}user: {author}\ndate: {date|date}\n{files}{file_copies}description:\n{desc|strip}\n\n\n' +changeset_debug = 'changeset: {rev}:{node}\n{branches}{tags}{parents}{manifest}user: {author}\ndate: {date|date}\n{file_mods}{file_adds}{file_dels}{file_copies}{extras}description:\n{desc|strip}\n\n\n' start_files = 'files: ' file = ' {file}' end_files = '\n' +start_file_mods = 'files: ' +file_mod = ' {file_mod}' +end_file_mods = '\n' start_file_adds = 'files+: ' file_add = ' {file_add}' end_file_adds = '\n'
--- a/templates/notfound.tmpl Thu Dec 06 13:10:25 2007 -0800 +++ b/templates/notfound.tmpl Thu Dec 06 13:11:36 2007 -0800 @@ -1,9 +1,9 @@ #header# -<title>Mercurial repositories index</title> +<title>Mercurial repository not found</title> </head> <body> -<h2>Mercurial Repositories</h2> +<h2>Mercurial repository not found</h2> The specified repository "#repo|escape#" is unknown, sorry.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/raw/error.tmpl Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,2 @@ +#header# +error: #error#
--- a/templates/raw/map Thu Dec 06 13:10:25 2007 -0800 +++ b/templates/raw/map Thu Dec 06 13:11:36 2007 -0800 @@ -18,4 +18,6 @@ manifestdirentry = 'drwxr-xr-x {basename}\n' manifestfileentry = '{permissions|permissions} {size} {basename}\n' index = index.tmpl +notfound = notfound.tmpl +error = error.tmpl indexentry = '#url#\n'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/raw/notfound.tmpl Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,2 @@ +#header# +error: repository #repo# not found
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/static/highlight.css Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,59 @@ +.c { color: #808080 } /* Comment */ +.err { color: #F00000; background-color: #F0A0A0 } /* Error */ +.k { color: #008000; font-weight: bold } /* Keyword */ +.o { color: #303030 } /* Operator */ +.cm { color: #808080 } /* Comment.Multiline */ +.cp { color: #507090 } /* Comment.Preproc */ +.c1 { color: #808080 } /* Comment.Single */ +.cs { color: #cc0000; font-weight: bold } /* Comment.Special */ +.gd { color: #A00000 } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.gr { color: #FF0000 } /* Generic.Error */ +.gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.gi { color: #00A000 } /* Generic.Inserted */ +.go { color: #808080 } /* Generic.Output */ +.gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.gt { color: #0040D0 } /* Generic.Traceback */ +.kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.kp { color: #003080; font-weight: bold } /* Keyword.Pseudo */ +.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #303090; font-weight: bold } /* Keyword.Type */ +.m { color: #6000E0; font-weight: bold } /* Literal.Number */ +.s { background-color: #fff0f0 } /* Literal.String */ +.na { color: #0000C0 } /* Name.Attribute */ +.nb { color: #007020 } /* Name.Builtin */ +.nc { color: #B00060; font-weight: bold } /* Name.Class */ +.no { color: #003060; font-weight: bold } /* Name.Constant */ +.nd { color: #505050; font-weight: bold } /* Name.Decorator */ +.ni { color: #800000; font-weight: bold } /* Name.Entity */ +.ne { color: #F00000; font-weight: bold } /* Name.Exception */ +.nf { color: #0060B0; font-weight: bold } /* Name.Function */ +.nl { color: #907000; font-weight: bold } /* Name.Label */ +.nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.nt { color: #007000 } /* Name.Tag */ +.nv { color: #906030 } /* Name.Variable */ +.ow { color: #000000; font-weight: bold } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mf { color: #6000E0; font-weight: bold } /* Literal.Number.Float */ +.mh { color: #005080; font-weight: bold } /* Literal.Number.Hex */ +.mi { color: #0000D0; font-weight: bold } /* Literal.Number.Integer */ +.mo { color: #4000E0; font-weight: bold } /* Literal.Number.Oct */ +.sb { background-color: #fff0f0 } /* Literal.String.Backtick */ +.sc { color: #0040D0 } /* Literal.String.Char */ +.sd { color: #D04020 } /* Literal.String.Doc */ +.s2 { background-color: #fff0f0 } /* Literal.String.Double */ +.se { color: #606060; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */ +.sh { background-color: #fff0f0 } /* Literal.String.Heredoc */ +.si { background-color: #e0e0e0 } /* Literal.String.Interpol */ +.sx { color: #D02000; background-color: #fff0f0 } /* Literal.String.Other */ +.sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */ +.s1 { background-color: #fff0f0 } /* Literal.String.Single */ +.ss { color: #A06000 } /* Literal.String.Symbol */ +.bp { color: #007020 } /* Name.Builtin.Pseudo */ +.vc { color: #306090 } /* Name.Variable.Class */ +.vg { color: #d07000; font-weight: bold } /* Name.Variable.Global */ +.vi { color: #3030B0 } /* Name.Variable.Instance */ +.il { color: #0000D0; font-weight: bold } /* Literal.Number.Integer.Long */
--- a/tests/coverage.py Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/coverage.py Thu Dec 06 13:11:36 2007 -0800 @@ -22,15 +22,20 @@ # interface and limitations. See [GDR 2001-12-04b] for requirements and # design. -"""Usage: +r"""Usage: -coverage.py -x MODULE.py [ARG1 ARG2 ...] +coverage.py -x [-p] MODULE.py [ARG1 ARG2 ...] Execute module, passing the given command-line arguments, collecting - coverage data. + coverage data. With the -p option, write to a temporary file containing + the machine name and process ID. coverage.py -e Erase collected coverage data. +coverage.py -c + Collect data from multiple coverage files (as created by -p option above) + and store it into a single file representing the union of the coverage. + coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ... Report on the statement coverage for the given files. With the -m option, show line numbers of the statements that weren't executed. @@ -49,16 +54,26 @@ Coverage data is saved in the file .coverage by default. Set the COVERAGE_FILE environment variable to save it somewhere else.""" -__version__ = "2.5.20051204" # see detailed history at the end of this file. +__version__ = "2.77.20070729" # see detailed history at the end of this file. import compiler import compiler.visitor +import glob import os import re import string +import symbol import sys import threading +import token import types +from socket import gethostname + +# Python version compatibility +try: + strclass = basestring # new to 2.3 +except: + strclass = str # 2. IMPLEMENTATION # @@ -81,25 +96,29 @@ # names to increase speed. class StatementFindingAstVisitor(compiler.visitor.ASTVisitor): + """ A visitor for a parsed Abstract Syntax Tree which finds executable + statements. + """ def __init__(self, statements, excluded, suite_spots): compiler.visitor.ASTVisitor.__init__(self) self.statements = statements self.excluded = excluded self.suite_spots = suite_spots self.excluding_suite = 0 - + def doRecursive(self, node): - self.recordNodeLine(node) for n in node.getChildNodes(): self.dispatch(n) visitStmt = visitModule = doRecursive - + def doCode(self, node): if hasattr(node, 'decorators') and node.decorators: self.dispatch(node.decorators) - self.doSuite(node, node.code) - + self.recordAndDispatch(node.code) + else: + self.doSuite(node, node.code) + visitFunction = visitClass = doCode def getFirstLine(self, node): @@ -119,17 +138,40 @@ for n in node.getChildNodes(): lineno = max(lineno, self.getLastLine(n)) return lineno - + def doStatement(self, node): self.recordLine(self.getFirstLine(node)) - visitAssert = visitAssign = visitAssTuple = visitDiscard = visitPrint = \ + visitAssert = visitAssign = visitAssTuple = visitPrint = \ visitPrintnl = visitRaise = visitSubscript = visitDecorators = \ doStatement + + def visitPass(self, node): + # Pass statements have weird interactions with docstrings. If this + # pass statement is part of one of those pairs, claim that the statement + # is on the later of the two lines. + l = node.lineno + if l: + lines = self.suite_spots.get(l, [l,l]) + self.statements[lines[1]] = 1 + + def visitDiscard(self, node): + # Discard nodes are statements that execute an expression, but then + # discard the results. This includes function calls, so we can't + # ignore them all. But if the expression is a constant, the statement + # won't be "executed", so don't count it now. + if node.expr.__class__.__name__ != 'Const': + self.doStatement(node) def recordNodeLine(self, node): - return self.recordLine(node.lineno) - + # Stmt nodes often have None, but shouldn't claim the first line of + # their children (because the first child might be an ignorable line + # like "global a"). + if node.__class__.__name__ != 'Stmt': + return self.recordLine(self.getFirstLine(node)) + else: + return 0 + def recordLine(self, lineno): # Returns a bool, whether the line is included or excluded. if lineno: @@ -137,7 +179,7 @@ # keyword. if lineno in self.suite_spots: lineno = self.suite_spots[lineno][0] - # If we're inside an exluded suite, record that this line was + # If we're inside an excluded suite, record that this line was # excluded. if self.excluding_suite: self.excluded[lineno] = 1 @@ -153,9 +195,9 @@ self.statements[lineno] = 1 return 1 return 0 - + default = recordNodeLine - + def recordAndDispatch(self, node): self.recordNodeLine(node) self.dispatch(node) @@ -166,7 +208,7 @@ self.excluding_suite = 1 self.recordAndDispatch(body) self.excluding_suite = exsuite - + def doPlainWordSuite(self, prevsuite, suite): # Finding the exclude lines for else's is tricky, because they aren't # present in the compiler parse tree. Look at the previous suite, @@ -180,15 +222,17 @@ break else: self.doSuite(None, suite) - + def doElse(self, prevsuite, node): if node.else_: self.doPlainWordSuite(prevsuite, node.else_) - + def visitFor(self, node): self.doSuite(node, node.body) self.doElse(node.body, node) + visitWhile = visitFor + def visitIf(self, node): # The first test has to be handled separately from the rest. # The first test is credited to the line with the "if", but the others @@ -198,10 +242,6 @@ self.doSuite(t, n) self.doElse(node.tests[-1][1], node) - def visitWhile(self, node): - self.doSuite(node, node.body) - self.doElse(node.body, node) - def visitTryExcept(self, node): self.doSuite(node, node.body) for i in range(len(node.handlers)): @@ -216,11 +256,14 @@ else: self.doSuite(a, h) self.doElse(node.handlers[-1][2], node) - + def visitTryFinally(self, node): self.doSuite(node, node.body) self.doPlainWordSuite(node.body, node.final) - + + def visitWith(self, node): + self.doSuite(node, node.body) + def visitGlobal(self, node): # "global" statements don't execute like others (they don't call the # trace function), so don't record their line numbers. @@ -228,9 +271,9 @@ the_coverage = None +class CoverageException(Exception): pass + class coverage: - error = "coverage error" - # Name of the cache file (unless environment variable is set). cache_default = ".coverage" @@ -240,7 +283,7 @@ # A dictionary with an entry for (Python source file name, line number # in that file) if that line has been executed. c = {} - + # A map from canonical Python source file name to a dictionary in # which there's an entry for each line number that has been # executed. @@ -257,53 +300,58 @@ def __init__(self): global the_coverage if the_coverage: - raise self.error, "Only one coverage object allowed." + raise CoverageException, "Only one coverage object allowed." self.usecache = 1 self.cache = None + self.parallel_mode = False self.exclude_re = '' self.nesting = 0 self.cstack = [] self.xstack = [] - self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.path.sep) + self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.sep) + self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]') - # t(f, x, y). This method is passed to sys.settrace as a trace function. - # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and + # t(f, x, y). This method is passed to sys.settrace as a trace function. + # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and # the arguments and return value of the trace function. # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code # objects. - - def t(self, f, w, a): #pragma: no cover - #print w, f.f_code.co_filename, f.f_lineno + + def t(self, f, w, unused): #pragma: no cover if w == 'line': + #print "Executing %s @ %d" % (f.f_code.co_filename, f.f_lineno) self.c[(f.f_code.co_filename, f.f_lineno)] = 1 for c in self.cstack: c[(f.f_code.co_filename, f.f_lineno)] = 1 return self.t - - def help(self, error=None): + + def help(self, error=None): #pragma: no cover if error: print error print print __doc__ sys.exit(1) - def command_line(self): + def command_line(self, argv, help_fn=None): import getopt + help_fn = help_fn or self.help settings = {} optmap = { '-a': 'annotate', + '-c': 'collect', '-d:': 'directory=', '-e': 'erase', '-h': 'help', '-i': 'ignore-errors', '-m': 'show-missing', + '-p': 'parallel-mode', '-r': 'report', '-x': 'execute', - '-o': 'omit=', + '-o:': 'omit=', } short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '') long_opts = optmap.values() - options, args = getopt.getopt(sys.argv[1:], short_opts, long_opts) + options, args = getopt.getopt(argv, short_opts, long_opts) for o, a in options: if optmap.has_key(o): settings[optmap[o]] = 1 @@ -312,69 +360,84 @@ elif o[2:] in long_opts: settings[o[2:]] = 1 elif o[2:] + '=' in long_opts: - settings[o[2:]] = a - else: - self.help("Unknown option: '%s'." % o) + settings[o[2:]+'='] = a + else: #pragma: no cover + pass # Can't get here, because getopt won't return anything unknown. + if settings.get('help'): - self.help() + help_fn() + for i in ['erase', 'execute']: - for j in ['annotate', 'report']: + for j in ['annotate', 'report', 'collect']: if settings.get(i) and settings.get(j): - self.help("You can't specify the '%s' and '%s' " + help_fn("You can't specify the '%s' and '%s' " "options at the same time." % (i, j)) + args_needed = (settings.get('execute') or settings.get('annotate') or settings.get('report')) - action = settings.get('erase') or args_needed + action = (settings.get('erase') + or settings.get('collect') + or args_needed) if not action: - self.help("You must specify at least one of -e, -x, -r, or -a.") + help_fn("You must specify at least one of -e, -x, -c, -r, or -a.") if not args_needed and args: - self.help("Unexpected arguments %s." % args) - + help_fn("Unexpected arguments: %s" % " ".join(args)) + + self.parallel_mode = settings.get('parallel-mode') self.get_ready() - self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]') if settings.get('erase'): self.erase() if settings.get('execute'): if not args: - self.help("Nothing to do.") + help_fn("Nothing to do.") sys.argv = args self.start() import __main__ sys.path[0] = os.path.dirname(sys.argv[0]) execfile(sys.argv[0], __main__.__dict__) + if settings.get('collect'): + self.collect() if not args: args = self.cexecuted.keys() + ignore_errors = settings.get('ignore-errors') show_missing = settings.get('show-missing') - directory = settings.get('directory') - omit = filter(None, settings.get('omit', '').split(',')) - omit += ['/<'] # Always skip /<string> etc. + directory = settings.get('directory=') + + omit = settings.get('omit=') + if omit is not None: + omit = omit.split(',') + else: + omit = [] if settings.get('report'): self.report(args, show_missing, ignore_errors, omit_prefixes=omit) if settings.get('annotate'): self.annotate(args, directory, ignore_errors, omit_prefixes=omit) - def use_cache(self, usecache): + def use_cache(self, usecache, cache_file=None): self.usecache = usecache - - def get_ready(self): + if cache_file and not self.cache: + self.cache_default = cache_file + + def get_ready(self, parallel_mode=False): if self.usecache and not self.cache: - self.cache = os.path.abspath(os.environ.get(self.cache_env, - self.cache_default)) + self.cache = os.environ.get(self.cache_env, self.cache_default) + if self.parallel_mode: + self.cache += "." + gethostname() + "." + str(os.getpid()) self.restore() self.analysis_cache = {} - - def start(self): + + def start(self, parallel_mode=False): self.get_ready() if self.nesting == 0: #pragma: no cover sys.settrace(self.t) if hasattr(threading, 'settrace'): threading.settrace(self.t) self.nesting += 1 - + def stop(self): self.nesting -= 1 if self.nesting == 0: #pragma: no cover @@ -383,12 +446,12 @@ threading.settrace(None) def erase(self): + self.get_ready() self.c = {} self.analysis_cache = {} self.cexecuted = {} if self.cache and os.path.exists(self.cache): os.remove(self.cache) - self.exclude_re = "" def exclude(self, re): if self.exclude_re: @@ -398,7 +461,7 @@ def begin_recursive(self): self.cstack.append(self.c) self.xstack.append(self.exclude_re) - + def end_recursive(self): self.c = self.cstack.pop() self.exclude_re = self.xstack.pop() @@ -406,8 +469,6 @@ # save(). Save coverage data to the coverage cache. def save(self): - # move to directory that must exist. - os.chdir(os.sep) if self.usecache and self.cache: self.canonicalize_filenames() cache = open(self.cache, 'wb') @@ -421,17 +482,45 @@ self.c = {} self.cexecuted = {} assert self.usecache - if not os.path.exists(self.cache): - return + if os.path.exists(self.cache): + self.cexecuted = self.restore_file(self.cache) + + def restore_file(self, file_name): try: - cache = open(self.cache, 'rb') + cache = open(file_name, 'rb') import marshal cexecuted = marshal.load(cache) cache.close() if isinstance(cexecuted, types.DictType): - self.cexecuted = cexecuted + return cexecuted + else: + return {} except: - pass + return {} + + # collect(). Collect data in multiple files produced by parallel mode + + def collect(self): + cache_dir, local = os.path.split(self.cache) + for f in os.listdir(cache_dir or '.'): + if not f.startswith(local): + continue + + full_path = os.path.join(cache_dir, f) + cexecuted = self.restore_file(full_path) + self.merge_data(cexecuted) + + def merge_data(self, new_data): + for file_name, file_data in new_data.items(): + if self.cexecuted.has_key(file_name): + self.merge_file_data(self.cexecuted[file_name], file_data) + else: + self.cexecuted[file_name] = file_data + + def merge_file_data(self, cache_data, new_data): + for line_number in new_data.keys(): + if not cache_data.has_key(line_number): + cache_data[line_number] = new_data[line_number] # canonical_filename(filename). Return a canonical filename for the # file (that is, an absolute path with no redundant components and @@ -452,11 +541,14 @@ self.canonical_filename_cache[filename] = cf return self.canonical_filename_cache[filename] - # canonicalize_filenames(). Copy results from "c" to "cexecuted", + # canonicalize_filenames(). Copy results from "c" to "cexecuted", # canonicalizing filenames on the way. Clear the "c" map. def canonicalize_filenames(self): for filename, lineno in self.c.keys(): + if filename == '<string>': + # Can't do anything useful with exec'd strings, so skip them. + continue f = self.canonical_filename(filename) if not self.cexecuted.has_key(f): self.cexecuted[f] = {} @@ -468,18 +560,20 @@ def morf_filename(self, morf): if isinstance(morf, types.ModuleType): if not hasattr(morf, '__file__'): - raise self.error, "Module has no __file__ attribute." - file = morf.__file__ + raise CoverageException, "Module has no __file__ attribute." + f = morf.__file__ else: - file = morf - return self.canonical_filename(file) + f = morf + return self.canonical_filename(f) # analyze_morf(morf). Analyze the module or filename passed as # the argument. If the source code can't be found, raise an error. # Otherwise, return a tuple of (1) the canonical filename of the # source code for the module, (2) a list of lines of statements - # in the source code, and (3) a list of lines of excluded statements. - + # in the source code, (3) a list of lines of excluded statements, + # and (4), a map of line numbers to multi-line line number ranges, for + # statements that cross lines. + def analyze_morf(self, morf): if self.analysis_cache.has_key(morf): return self.analysis_cache[morf] @@ -487,30 +581,69 @@ ext = os.path.splitext(filename)[1] if ext == '.pyc': if not os.path.exists(filename[0:-1]): - raise self.error, ("No source for compiled code '%s'." + raise CoverageException, ("No source for compiled code '%s'." % filename) filename = filename[0:-1] elif ext != '.py': - raise self.error, "File '%s' not Python source." % filename + raise CoverageException, "File '%s' not Python source." % filename source = open(filename, 'r') - lines, excluded_lines = self.find_executable_statements( + lines, excluded_lines, line_map = self.find_executable_statements( source.read(), exclude=self.exclude_re ) source.close() - result = filename, lines, excluded_lines + result = filename, lines, excluded_lines, line_map self.analysis_cache[morf] = result return result + def first_line_of_tree(self, tree): + while True: + if len(tree) == 3 and type(tree[2]) == type(1): + return tree[2] + tree = tree[1] + + def last_line_of_tree(self, tree): + while True: + if len(tree) == 3 and type(tree[2]) == type(1): + return tree[2] + tree = tree[-1] + + def find_docstring_pass_pair(self, tree, spots): + for i in range(1, len(tree)): + if self.is_string_constant(tree[i]) and self.is_pass_stmt(tree[i+1]): + first_line = self.first_line_of_tree(tree[i]) + last_line = self.last_line_of_tree(tree[i+1]) + self.record_multiline(spots, first_line, last_line) + + def is_string_constant(self, tree): + try: + return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.expr_stmt + except: + return False + + def is_pass_stmt(self, tree): + try: + return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.pass_stmt + except: + return False + + def record_multiline(self, spots, i, j): + for l in range(i, j+1): + spots[l] = (i, j) + def get_suite_spots(self, tree, spots): - import symbol, token + """ Analyze a parse tree to find suite introducers which span a number + of lines. + """ for i in range(1, len(tree)): - if isinstance(tree[i], tuple): + if type(tree[i]) == type(()): if tree[i][0] == symbol.suite: # Found a suite, look back for the colon and keyword. lineno_colon = lineno_word = None for j in range(i-1, 0, -1): if tree[j][0] == token.COLON: - lineno_colon = tree[j][2] + # Colons are never executed themselves: we want the + # line number of the last token before the colon. + lineno_colon = self.last_line_of_tree(tree[j-1]) elif tree[j][0] == token.NAME: if tree[j][1] == 'elif': # Find the line number of the first non-terminal @@ -532,8 +665,18 @@ if lineno_colon and lineno_word: # Found colon and keyword, mark all the lines # between the two with the two line numbers. - for l in range(lineno_word, lineno_colon+1): - spots[l] = (lineno_word, lineno_colon) + self.record_multiline(spots, lineno_word, lineno_colon) + + # "pass" statements are tricky: different versions of Python + # treat them differently, especially in the common case of a + # function with a doc string and a single pass statement. + self.find_docstring_pass_pair(tree[i], spots) + + elif tree[i][0] == symbol.simple_stmt: + first_line = self.first_line_of_tree(tree[i]) + last_line = self.last_line_of_tree(tree[i]) + if first_line != last_line: + self.record_multiline(spots, first_line, last_line) self.get_suite_spots(tree[i], spots) def find_executable_statements(self, text, exclude=None): @@ -547,10 +690,13 @@ if reExclude.search(lines[i]): excluded[i+1] = 1 + # Parse the code and analyze the parse tree to find out which statements + # are multiline, and where suites begin and end. import parser tree = parser.suite(text+'\n\n').totuple(1) self.get_suite_spots(tree, suite_spots) - + #print "Suite spots:", suite_spots + # Use the compiler module to parse the text and find the executable # statements. We add newlines to be impervious to final partial lines. statements = {} @@ -562,7 +708,7 @@ lines.sort() excluded_lines = excluded.keys() excluded_lines.sort() - return lines, excluded_lines + return lines, excluded_lines, suite_spots # format_lines(statements, lines). Format a list of line numbers # for printing by coalescing groups of lines as long as the lines @@ -595,7 +741,8 @@ return "%d" % start else: return "%d-%d" % (start, end) - return string.join(map(stringify, pairs), ", ") + ret = string.join(map(stringify, pairs), ", ") + return ret # Backward compatibility with version 1. def analysis(self, morf): @@ -603,13 +750,17 @@ return f, s, m, mf def analysis2(self, morf): - filename, statements, excluded = self.analyze_morf(morf) + filename, statements, excluded, line_map = self.analyze_morf(morf) self.canonicalize_filenames() if not self.cexecuted.has_key(filename): self.cexecuted[filename] = {} missing = [] for line in statements: - if not self.cexecuted[filename].has_key(line): + lines = line_map.get(line, [line, line]) + for l in range(lines[0], lines[1]+1): + if self.cexecuted[filename].has_key(l): + break + else: missing.append(line) return (filename, statements, excluded, missing, self.format_lines(statements, missing)) @@ -647,6 +798,15 @@ def report(self, morfs, show_missing=1, ignore_errors=0, file=None, omit_prefixes=[]): if not isinstance(morfs, types.ListType): morfs = [morfs] + # On windows, the shell doesn't expand wildcards. Do it here. + globbed = [] + for morf in morfs: + if isinstance(morf, strclass): + globbed.extend(glob.glob(morf)) + else: + globbed.append(morf) + morfs = globbed + morfs = self.filter_by_prefix(morfs, omit_prefixes) morfs.sort(self.morf_name_compare) @@ -684,8 +844,8 @@ raise except: if not ignore_errors: - type, msg = sys.exc_info()[0:2] - print >>file, fmt_err % (name, type, msg) + typ, msg = sys.exc_info()[0:2] + print >>file, fmt_err % (name, typ, msg) if len(morfs) > 1: print >>file, "-" * len(header) if total_statements > 0: @@ -713,7 +873,7 @@ except: if not ignore_errors: raise - + def annotate_file(self, filename, statements, excluded, missing, directory=None): source = open(filename, 'r') if directory: @@ -741,7 +901,7 @@ if self.blank_re.match(line): dest.write(' ') elif self.else_re.match(line): - # Special logic for lines containing only 'else:'. + # Special logic for lines containing only 'else:'. # See [GDR 2001-12-04b, 3.2]. if i >= len(statements) and j >= len(missing): dest.write('! ') @@ -765,18 +925,41 @@ the_coverage = coverage() # Module functions call methods in the singleton object. -def use_cache(*args, **kw): return the_coverage.use_cache(*args, **kw) -def start(*args, **kw): return the_coverage.start(*args, **kw) -def stop(*args, **kw): return the_coverage.stop(*args, **kw) -def erase(*args, **kw): return the_coverage.erase(*args, **kw) -def begin_recursive(*args, **kw): return the_coverage.begin_recursive(*args, **kw) -def end_recursive(*args, **kw): return the_coverage.end_recursive(*args, **kw) -def exclude(*args, **kw): return the_coverage.exclude(*args, **kw) -def analysis(*args, **kw): return the_coverage.analysis(*args, **kw) -def analysis2(*args, **kw): return the_coverage.analysis2(*args, **kw) -def report(*args, **kw): return the_coverage.report(*args, **kw) -def annotate(*args, **kw): return the_coverage.annotate(*args, **kw) -def annotate_file(*args, **kw): return the_coverage.annotate_file(*args, **kw) +def use_cache(*args, **kw): + return the_coverage.use_cache(*args, **kw) + +def start(*args, **kw): + return the_coverage.start(*args, **kw) + +def stop(*args, **kw): + return the_coverage.stop(*args, **kw) + +def erase(*args, **kw): + return the_coverage.erase(*args, **kw) + +def begin_recursive(*args, **kw): + return the_coverage.begin_recursive(*args, **kw) + +def end_recursive(*args, **kw): + return the_coverage.end_recursive(*args, **kw) + +def exclude(*args, **kw): + return the_coverage.exclude(*args, **kw) + +def analysis(*args, **kw): + return the_coverage.analysis(*args, **kw) + +def analysis2(*args, **kw): + return the_coverage.analysis2(*args, **kw) + +def report(*args, **kw): + return the_coverage.report(*args, **kw) + +def annotate(*args, **kw): + return the_coverage.annotate(*args, **kw) + +def annotate_file(*args, **kw): + return the_coverage.annotate_file(*args, **kw) # Save coverage data when Python exits. (The atexit module wasn't # introduced until Python 2.0, so use sys.exitfunc when it's not @@ -789,7 +972,7 @@ # Command-line interface. if __name__ == '__main__': - the_coverage.command_line() + the_coverage.command_line(sys.argv[1:]) # A. REFERENCES @@ -850,7 +1033,7 @@ # Thanks, Allen. # # 2005-12-02 NMB Call threading.settrace so that all threads are measured. -# Thanks Martin Fuzzey. Add a file argument to report so that reports can be +# Thanks Martin Fuzzey. Add a file argument to report so that reports can be # captured to a different destination. # # 2005-12-03 NMB coverage.py can now measure itself. @@ -858,10 +1041,46 @@ # 2005-12-04 NMB Adapted Greg Rogers' patch for using relative filenames, # and sorting and omitting files to report on. # +# 2006-07-23 NMB Applied Joseph Tate's patch for function decorators. +# +# 2006-08-21 NMB Applied Sigve Tjora and Mark van der Wal's fixes for argument +# handling. +# +# 2006-08-22 NMB Applied Geoff Bache's parallel mode patch. +# +# 2006-08-23 NMB Refactorings to improve testability. Fixes to command-line +# logic for parallel mode and collect. +# +# 2006-08-25 NMB "#pragma: nocover" is excluded by default. +# +# 2006-09-10 NMB Properly ignore docstrings and other constant expressions that +# appear in the middle of a function, a problem reported by Tim Leslie. +# Minor changes to avoid lint warnings. +# +# 2006-09-17 NMB coverage.erase() shouldn't clobber the exclude regex. +# Change how parallel mode is invoked, and fix erase() so that it erases the +# cache when called programmatically. +# +# 2007-07-21 NMB In reports, ignore code executed from strings, since we can't +# do anything useful with it anyway. +# Better file handling on Linux, thanks Guillaume Chazarain. +# Better shell support on Windows, thanks Noel O'Boyle. +# Python 2.2 support maintained, thanks Catherine Proulx. +# +# 2007-07-22 NMB Python 2.5 now fully supported. The method of dealing with +# multi-line statements is now less sensitive to the exact line that Python +# reports during execution. Pass statements are handled specially so that their +# disappearance during execution won't throw off the measurement. +# +# 2007-07-23 NMB Now Python 2.5 is *really* fully supported: the body of the +# new with statement is counted as executable. +# +# 2007-07-29 NMB Better packaging. + # C. COPYRIGHT AND LICENCE # # Copyright 2001 Gareth Rees. All rights reserved. -# Copyright 2004-2005 Ned Batchelder. All rights reserved. +# Copyright 2004-2007 Ned Batchelder. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are @@ -888,4 +1107,4 @@ # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. # -# $Id: coverage.py 26 2005-12-04 18:42:44Z ned $ +# $Id: coverage.py 74 2007-07-29 22:28:35Z nedbat $
--- a/tests/get-with-headers.py Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/get-with-headers.py Thu Dec 06 13:11:36 2007 -0800 @@ -14,3 +14,7 @@ print "%s: %s" % (h, response.getheader(h)) print sys.stdout.write(response.read()) + +if 200 <= response.status <= 299: + sys.exit(0) +sys.exit(1)
--- a/tests/test-command-template Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-command-template Thu Dec 06 13:11:36 2007 -0800 @@ -89,8 +89,8 @@ cat changelog echo "# keys work" -for key in author branches date desc file_adds file_dels files \ - manifest node parents rev tags; do +for key in author branches date desc file_adds file_dels file_mods \ + files manifest node parents rev tags; do for mode in '' --verbose --debug; do hg log $mode --template "$key$mode: {$key}\n" done
--- a/tests/test-command-template.out Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-command-template.out Thu Dec 06 13:11:36 2007 -0800 @@ -260,22 +260,22 @@ other 3 desc--debug: line 1 line 2 -file_adds: -file_adds: +file_adds: second file_adds: -file_adds: -file_adds: -file_adds: +file_adds: d file_adds: file_adds: +file_adds: c +file_adds: b +file_adds: a +file_adds--verbose: second file_adds--verbose: -file_adds--verbose: +file_adds--verbose: d file_adds--verbose: file_adds--verbose: -file_adds--verbose: -file_adds--verbose: -file_adds--verbose: -file_adds--verbose: +file_adds--verbose: c +file_adds--verbose: b +file_adds--verbose: a file_adds--debug: second file_adds--debug: file_adds--debug: d @@ -308,6 +308,30 @@ file_dels--debug: file_dels--debug: file_dels--debug: +file_mods: +file_mods: +file_mods: +file_mods: +file_mods: c +file_mods: +file_mods: +file_mods: +file_mods--verbose: +file_mods--verbose: +file_mods--verbose: +file_mods--verbose: +file_mods--verbose: c +file_mods--verbose: +file_mods--verbose: +file_mods--verbose: +file_mods--debug: +file_mods--debug: +file_mods--debug: +file_mods--debug: +file_mods--debug: c +file_mods--debug: +file_mods--debug: +file_mods--debug: files: second files: files: d @@ -324,30 +348,30 @@ files--verbose: c files--verbose: b files--verbose: a -files--debug: +files--debug: second files--debug: -files--debug: +files--debug: d files--debug: files--debug: c -files--debug: -files--debug: -files--debug: -manifest: -manifest: -manifest: -manifest: -manifest: -manifest: -manifest: -manifest: -manifest--verbose: -manifest--verbose: -manifest--verbose: -manifest--verbose: -manifest--verbose: -manifest--verbose: -manifest--verbose: -manifest--verbose: +files--debug: c +files--debug: b +files--debug: a +manifest: 7:f2dbc354b94e +manifest: 6:91015e9dbdd7 +manifest: 5:4dc3def4f9b4 +manifest: 4:90ae8dda64e1 +manifest: 3:cb5a1327723b +manifest: 2:6e0e82995c35 +manifest: 1:4e8d705b1e53 +manifest: 0:a0c8bcbbb45c +manifest--verbose: 7:f2dbc354b94e +manifest--verbose: 6:91015e9dbdd7 +manifest--verbose: 5:4dc3def4f9b4 +manifest--verbose: 4:90ae8dda64e1 +manifest--verbose: 3:cb5a1327723b +manifest--verbose: 2:6e0e82995c35 +manifest--verbose: 1:4e8d705b1e53 +manifest--verbose: 0:a0c8bcbbb45c manifest--debug: 7:f2dbc354b94e5ec0b4f10680ee0cee816101d0bf manifest--debug: 6:91015e9dbdd76a6791085d12b0a0ec7fcd22ffbf manifest--debug: 5:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
--- a/tests/test-convert Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-convert Thu Dec 06 13:11:36 2007 -0800 @@ -1,7 +1,11 @@ #!/bin/sh -echo "[extensions]" >> $HGRCPATH -echo "convert=" >> $HGRCPATH +cat >> $HGRCPATH <<EOF +[extensions] +convert= +[convert] +hg.saverev=False +EOF hg help convert
--- a/tests/test-convert-cvs.out Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-convert-cvs.out Thu Dec 06 13:11:36 2007 -0800 @@ -44,7 +44,6 @@ checking in src/a,v checking in src/b/c,v % convert again -destination src-hg is a Mercurial repository connecting to cvsrepo scanning source... sorting... @@ -56,7 +55,6 @@ c c % convert again with --filemap -destination src-filemap is a Mercurial repository connecting to cvsrepo scanning source... sorting...
--- a/tests/test-convert-hg-sink Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-convert-hg-sink Thu Dec 06 13:11:36 2007 -0800 @@ -1,7 +1,11 @@ #!/bin/sh -echo "[extensions]" >> $HGRCPATH -echo "hgext.convert=" >> $HGRCPATH +cat >> $HGRCPATH <<EOF +[extensions] +convert= +[convert] +hg.saverev=False +EOF hg init orig cd orig
--- a/tests/test-convert-hg-sink.out Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-convert-hg-sink.out Thu Dec 06 13:11:36 2007 -0800 @@ -37,7 +37,6 @@ a 0 -1 unset baz copy: bar -> baz % add a new revision in the original repo -destination new is a Mercurial repository scanning source... sorting... converting...
--- a/tests/test-convert-hg-source Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-convert-hg-source Thu Dec 06 13:11:36 2007 -0800 @@ -1,7 +1,11 @@ #!/bin/sh -echo "[extensions]" >> $HGRCPATH -echo "hgext.convert=" >> $HGRCPATH +cat >> $HGRCPATH <<EOF +[extensions] +convert= +[convert] +hg.saverev=False +EOF hg init orig cd orig
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-convert-hg-svn Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,66 @@ +#!/bin/sh + +"$TESTDIR/hghave" svn svn-bindings || exit 80 + +fix_path() +{ + tr '\\' / +} + +echo "[extensions]" >> $HGRCPATH +echo "convert = " >> $HGRCPATH + +svnpath=`pwd`/svn-repo +svnadmin create $svnpath + +cat > $svnpath/hooks/pre-revprop-change <<'EOF' +#!/bin/sh + +REPOS="$1" +REV="$2" +USER="$3" +PROPNAME="$4" +ACTION="$5" + +if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi +if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi +if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi + +echo "Changing prohibited revision property" >&2 +exit 1 +EOF +chmod +x $svnpath/hooks/pre-revprop-change + +svnurl=file://$svnpath +svn co $svnurl $svnpath-wc + +cd $svnpath-wc +echo a > a +svn add a +svn ci -m'added a' a + +cd .. + +echo % initial roundtrip +hg convert -s svn -d hg $svnpath-wc $svnpath-hg | grep -v initializing +hg convert -s hg -d svn $svnpath-hg $svnpath-wc + +echo % second roundtrip should do nothing +hg convert -s svn -d hg $svnpath-wc $svnpath-hg +hg convert -s hg -d svn $svnpath-hg $svnpath-wc + +echo % new hg rev + +hg clone $svnpath-hg $svnpath-work +echo b > $svnpath-work/b +hg --cwd $svnpath-work add b +hg --cwd $svnpath-work ci -mb + +echo % echo hg to svn +hg --cwd $svnpath-hg pull -q $svnpath-work +hg convert -s hg -d svn $svnpath-hg $svnpath-wc + +echo % svn back to hg should do nothing +hg convert -s svn -d hg $svnpath-wc $svnpath-hg +echo % hg back to svn should do nothing +hg convert -s hg -d svn $svnpath-hg $svnpath-wc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-convert-hg-svn.out Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,35 @@ +Checked out revision 0. +A a +Adding a +Transmitting file data . +Committed revision 1. +% initial roundtrip +scanning source... +sorting... +converting... +0 added a +scanning source... +sorting... +converting... +% second roundtrip should do nothing +scanning source... +sorting... +converting... +scanning source... +sorting... +converting... +% new hg rev +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +% echo hg to svn +scanning source... +sorting... +converting... +0 b +% svn back to hg should do nothing +scanning source... +sorting... +converting... +% hg back to svn should do nothing +scanning source... +sorting... +converting...
--- a/tests/test-convert-svn Thu Dec 06 13:10:25 2007 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -#!/bin/sh - -"$TESTDIR/hghave" svn svn-bindings || exit 80 - -fix_path() -{ - tr '\\' / -} - -echo "[extensions]" >> $HGRCPATH -echo "convert = " >> $HGRCPATH - -svnadmin create svn-repo - -echo % initial svn import -mkdir t -cd t -echo a > a -cd .. - -svnpath=`pwd | fix_path` -# SVN wants all paths to start with a slash. Unfortunately, -# Windows ones don't. Handle that. -expr $svnpath : "\/" > /dev/null -if [ $? -ne 0 ]; then - svnpath='/'$svnpath -fi - -svnurl=file://$svnpath/svn-repo/trunk -svn import -m init t $svnurl | fix_path - -echo % update svn repository -svn co $svnurl t2 | fix_path -cd t2 -echo b >> a -echo b > b -svn add b -svn ci -m changea -cd .. - -echo % convert to hg once -hg convert $svnurl - -echo % update svn repository again -cd t2 -echo c >> a -echo c >> b -svn ci -m changeb -cd .. - -echo % test incremental conversion -hg convert $svnurl - -echo % test filemap -echo 'include b' > filemap -hg convert --filemap filemap $svnurl fmap -echo '[extensions]' >> $HGRCPATH -echo 'hgext.graphlog =' >> $HGRCPATH -hg glog -R fmap --template '#rev# #desc|firstline# files: #files#\n' - -######################################## - -echo "# now tests that it works with trunk/branches/tags layout" -echo -echo % initial svn import -mkdir projA -cd projA -mkdir trunk -mkdir branches -mkdir tags -cd .. - -svnurl=file://$svnpath/svn-repo/projA -svn import -m "init projA" projA $svnurl | fix_path - - -echo % update svn repository -svn co $svnurl/trunk A | fix_path -cd A -echo hello > letter.txt -svn add letter.txt -svn ci -m hello - -echo world >> letter.txt -svn ci -m world - -svn copy -m "tag v0.1" $svnurl/trunk $svnurl/tags/v0.1 - -echo 'nice day today!' >> letter.txt -svn ci -m "nice day" -cd .. - -echo % convert to hg once -hg convert $svnurl A-hg - -echo % update svn repository again -cd A -echo "see second letter" >> letter.txt -echo "nice to meet you" > letter2.txt -svn add letter2.txt -svn ci -m "second letter" - -svn copy -m "tag v0.2" $svnurl/trunk $svnurl/tags/v0.2 - -echo "blah-blah-blah" >> letter2.txt -svn ci -m "work in progress" -cd .. - -echo % test incremental conversion -hg convert $svnurl A-hg - -cd A-hg -hg glog --template '#rev# #desc|firstline# files: #files#\n' -hg tags -q -cd ..
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-convert-svn-sink Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,93 @@ +#!/bin/sh + +"$TESTDIR/hghave" svn svn-bindings || exit 80 + +echo "[extensions]" >> $HGRCPATH +echo "convert = " >> $HGRCPATH + +hg init a + +echo a > a/a +mkdir -p a/d1/d2 +echo b > a/d1/d2/b +echo % add +hg --cwd a ci -d '0 0' -A -m 'add a file' + +echo a >> a/a +echo % modify +hg --cwd a ci -d '1 0' -m 'modify a file' +hg --cwd a tip -q + +hg convert -d svn a +(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=2 | sed 's,<date>.*,<date/>,') +ls a a-hg-wc +cmp a/a a-hg-wc/a && echo same || echo different + +hg --cwd a mv a b +echo % rename +hg --cwd a ci -d '2 0' -m 'rename a file' +hg --cwd a tip -q + +hg convert -d svn a +(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,') +ls a a-hg-wc + +hg --cwd a cp b c +echo % copy +hg --cwd a ci -d '3 0' -m 'copy a file' +hg --cwd a tip -q + +hg convert -d svn a +(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,') +ls a a-hg-wc + +hg --cwd a rm b +echo % remove +hg --cwd a ci -d '4 0' -m 'remove a file' +hg --cwd a tip -q + +hg convert -d svn a +(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,') +ls a a-hg-wc + +chmod +x a/c +echo % executable +hg --cwd a ci -d '5 0' -m 'make a file executable' +hg --cwd a tip -q + +hg convert -d svn a +(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,') +test -x a-hg-wc/c && echo executable || echo not executable + +echo % branchy history + +hg init b +echo base > b/b +hg --cwd b ci -d '0 0' -Ambase + +echo left-1 >> b/b +echo left-1 > b/left-1 +hg --cwd b ci -d '1 0' -Amleft-1 + +echo left-2 >> b/b +echo left-2 > b/left-2 +hg --cwd b ci -d '2 0' -Amleft-2 + +hg --cwd b up 0 + +echo right-1 >> b/b +echo right-1 > b/right-1 +hg --cwd b ci -d '3 0' -Amright-1 + +echo right-2 >> b/b +echo right-2 > b/right-2 +hg --cwd b ci -d '4 0' -Amright-2 + +hg --cwd b up -C 2 +hg --cwd b merge +hg --cwd b revert -r 2 b +hg --cwd b ci -d '5 0' -m 'merge' + +hg convert -d svn b +echo % expect 4 changes +(cd b-hg-wc; svn up; svn st -v; svn log --xml -v | sed 's,<date>.*,<date/>,')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-convert-svn-sink.out Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,281 @@ +% add +adding a +adding d1/d2/b +% modify +1:e0e2b8a9156b +assuming destination a-hg +initializing svn repo 'a-hg' +initializing svn wc 'a-hg-wc' +scanning source... +sorting... +converting... +1 add a file +0 modify a file +At revision 2. + 2 2 test . + 2 2 test a + 2 1 test d1 + 2 1 test d1/d2 + 2 1 test d1/d2/b +<?xml version="1.0"?> +<log> +<logentry + revision="2"> +<author>test</author> +<date/> +<paths> +<path + action="M">/a</path> +</paths> +<msg>modify a file</msg> +</logentry> +<logentry + revision="1"> +<author>test</author> +<date/> +<paths> +<path + action="A">/a</path> +<path + action="A">/d1</path> +<path + action="A">/d1/d2</path> +<path + action="A">/d1/d2/b</path> +</paths> +<msg>add a file</msg> +</logentry> +</log> +a: +a +d1 + +a-hg-wc: +a +d1 +same +% rename +2:7009fc4efb34 +assuming destination a-hg +initializing svn wc 'a-hg-wc' +scanning source... +sorting... +converting... +0 rename a file +At revision 3. + 3 3 test . + 3 3 test b + 3 1 test d1 + 3 1 test d1/d2 + 3 1 test d1/d2/b +<?xml version="1.0"?> +<log> +<logentry + revision="3"> +<author>test</author> +<date/> +<paths> +<path + action="D">/a</path> +<path + copyfrom-path="/a" + copyfrom-rev="2" + action="A">/b</path> +</paths> +<msg>rename a file</msg> +</logentry> +</log> +a: +b +d1 + +a-hg-wc: +b +d1 +% copy +3:56c519973ce6 +assuming destination a-hg +initializing svn wc 'a-hg-wc' +scanning source... +sorting... +converting... +0 copy a file +At revision 4. + 4 4 test . + 4 3 test b + 4 4 test c + 4 1 test d1 + 4 1 test d1/d2 + 4 1 test d1/d2/b +<?xml version="1.0"?> +<log> +<logentry + revision="4"> +<author>test</author> +<date/> +<paths> +<path + copyfrom-path="/b" + copyfrom-rev="3" + action="A">/c</path> +</paths> +<msg>copy a file</msg> +</logentry> +</log> +a: +b +c +d1 + +a-hg-wc: +b +c +d1 +% remove +4:ed4dc9a6f585 +assuming destination a-hg +initializing svn wc 'a-hg-wc' +scanning source... +sorting... +converting... +0 remove a file +At revision 5. + 5 5 test . + 5 4 test c + 5 1 test d1 + 5 1 test d1/d2 + 5 1 test d1/d2/b +<?xml version="1.0"?> +<log> +<logentry + revision="5"> +<author>test</author> +<date/> +<paths> +<path + action="D">/b</path> +</paths> +<msg>remove a file</msg> +</logentry> +</log> +a: +c +d1 + +a-hg-wc: +c +d1 +% executable +5:f205b3636d77 +svn: Path 'b' does not exist +assuming destination a-hg +initializing svn wc 'a-hg-wc' +scanning source... +sorting... +converting... +0 make a file executable +abort: svn exited with status 1 +At revision 5. + 5 5 test . + M 5 4 test c + 5 1 test d1 + 5 1 test d1/d2 + 5 1 test d1/d2/b +<?xml version="1.0"?> +<log> +<logentry + revision="5"> +<author>test</author> +<date/> +<paths> +<path + action="D">/b</path> +</paths> +<msg>remove a file</msg> +</logentry> +</log> +executable +% branchy history +adding b +adding left-1 +adding left-2 +1 files updated, 0 files merged, 2 files removed, 0 files unresolved +adding right-1 +adding right-2 +3 files updated, 0 files merged, 2 files removed, 0 files unresolved +warning: conflicts during merge. +merging b +merging b failed! +2 files updated, 0 files merged, 0 files removed, 1 files unresolved +There are unresolved merges, you can redo the full merge using: + hg update -C 2 + hg merge 4 +assuming destination b-hg +initializing svn repo 'b-hg' +initializing svn wc 'b-hg-wc' +scanning source... +sorting... +converting... +5 base +4 left-1 +3 left-2 +2 right-1 +1 right-2 +0 merge +% expect 4 changes +At revision 4. + 4 4 test . + 4 3 test b + 4 2 test left-1 + 4 3 test left-2 + 4 4 test right-1 + 4 4 test right-2 +<?xml version="1.0"?> +<log> +<logentry + revision="4"> +<author>test</author> +<date/> +<paths> +<path + action="A">/right-1</path> +<path + action="A">/right-2</path> +</paths> +<msg>merge</msg> +</logentry> +<logentry + revision="3"> +<author>test</author> +<date/> +<paths> +<path + action="M">/b</path> +<path + action="A">/left-2</path> +</paths> +<msg>left-2</msg> +</logentry> +<logentry + revision="2"> +<author>test</author> +<date/> +<paths> +<path + action="M">/b</path> +<path + action="A">/left-1</path> +</paths> +<msg>left-1</msg> +</logentry> +<logentry + revision="1"> +<author>test</author> +<date/> +<paths> +<path + action="A">/b</path> +</paths> +<msg>base</msg> +</logentry> +</log>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-convert-svn-source Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,115 @@ +#!/bin/sh + +"$TESTDIR/hghave" svn svn-bindings || exit 80 + +fix_path() +{ + tr '\\' / +} + +echo "[extensions]" >> $HGRCPATH +echo "convert = " >> $HGRCPATH + +svnadmin create svn-repo + +echo % initial svn import +mkdir t +cd t +echo a > a +cd .. + +svnpath=`pwd | fix_path` +# SVN wants all paths to start with a slash. Unfortunately, +# Windows ones don't. Handle that. +expr $svnpath : "\/" > /dev/null +if [ $? -ne 0 ]; then + svnpath='/'$svnpath +fi + +svnurl=file://$svnpath/svn-repo/trunk +svn import -m init t $svnurl | fix_path + +echo % update svn repository +svn co $svnurl t2 | fix_path +cd t2 +echo b >> a +echo b > b +svn add b +svn ci -m changea +cd .. + +echo % convert to hg once +hg convert $svnurl + +echo % update svn repository again +cd t2 +echo c >> a +echo c >> b +svn ci -m changeb +cd .. + +echo % test incremental conversion +hg convert $svnurl + +echo % test filemap +echo 'include b' > filemap +hg convert --filemap filemap $svnurl fmap +echo '[extensions]' >> $HGRCPATH +echo 'hgext.graphlog =' >> $HGRCPATH +hg glog -R fmap --template '#rev# #desc|firstline# files: #files#\n' + +######################################## + +echo "# now tests that it works with trunk/branches/tags layout" +echo +echo % initial svn import +mkdir projA +cd projA +mkdir trunk +mkdir branches +mkdir tags +cd .. + +svnurl=file://$svnpath/svn-repo/projA +svn import -m "init projA" projA $svnurl | fix_path + + +echo % update svn repository +svn co $svnurl/trunk A | fix_path +cd A +echo hello > letter.txt +svn add letter.txt +svn ci -m hello + +echo world >> letter.txt +svn ci -m world + +svn copy -m "tag v0.1" $svnurl/trunk $svnurl/tags/v0.1 + +echo 'nice day today!' >> letter.txt +svn ci -m "nice day" +cd .. + +echo % convert to hg once +hg convert $svnurl A-hg + +echo % update svn repository again +cd A +echo "see second letter" >> letter.txt +echo "nice to meet you" > letter2.txt +svn add letter2.txt +svn ci -m "second letter" + +svn copy -m "tag v0.2" $svnurl/trunk $svnurl/tags/v0.2 + +echo "blah-blah-blah" >> letter2.txt +svn ci -m "work in progress" +cd .. + +echo % test incremental conversion +hg convert $svnurl A-hg + +cd A-hg +hg glog --template '#rev# #desc|firstline# files: #files#\n' +hg tags -q +cd ..
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-convert-svn-source.out Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,112 @@ +% initial svn import +Adding t/a + +Committed revision 1. +% update svn repository +A t2/a +Checked out revision 1. +A b +Sending a +Adding b +Transmitting file data .. +Committed revision 2. +% convert to hg once +assuming destination trunk-hg +initializing destination trunk-hg repository +scanning source... +sorting... +converting... +1 init +0 changea +% update svn repository again +Sending a +Sending b +Transmitting file data .. +Committed revision 3. +% test incremental conversion +assuming destination trunk-hg +scanning source... +sorting... +converting... +0 changeb +% test filemap +initializing destination fmap repository +scanning source... +sorting... +converting... +2 init +1 changea +0 changeb +o 1 changeb files: b +| +o 0 changea files: b + +# now tests that it works with trunk/branches/tags layout + +% initial svn import +Adding projA/trunk +Adding projA/branches +Adding projA/tags + +Committed revision 4. +% update svn repository +Checked out revision 4. +A letter.txt +Adding letter.txt +Transmitting file data . +Committed revision 5. +Sending letter.txt +Transmitting file data . +Committed revision 6. + +Committed revision 7. +Sending letter.txt +Transmitting file data . +Committed revision 8. +% convert to hg once +initializing destination A-hg repository +scanning source... +sorting... +converting... +3 init projA +2 hello +1 world +0 nice day +updating tags +% update svn repository again +A letter2.txt +Sending letter.txt +Adding letter2.txt +Transmitting file data .. +Committed revision 9. + +Committed revision 10. +Sending letter2.txt +Transmitting file data . +Committed revision 11. +% test incremental conversion +scanning source... +sorting... +converting... +1 second letter +0 work in progress +updating tags +o 7 update tags files: .hgtags +| +o 6 work in progress files: letter2.txt +| +o 5 second letter files: letter.txt letter2.txt +| +o 4 update tags files: .hgtags +| +o 3 nice day files: letter.txt +| +o 2 world files: letter.txt +| +o 1 hello files: letter.txt +| +o 0 init projA files: + +tip +v0.2 +v0.1
--- a/tests/test-convert-svn.out Thu Dec 06 13:10:25 2007 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +0,0 @@ -% initial svn import -Adding t/a - -Committed revision 1. -% update svn repository -A t2/a -Checked out revision 1. -A b -Sending a -Adding b -Transmitting file data .. -Committed revision 2. -% convert to hg once -assuming destination trunk-hg -initializing destination trunk-hg repository -scanning source... -sorting... -converting... -1 init -0 changea -% update svn repository again -Sending a -Sending b -Transmitting file data .. -Committed revision 3. -% test incremental conversion -assuming destination trunk-hg -destination trunk-hg is a Mercurial repository -scanning source... -sorting... -converting... -0 changeb -% test filemap -initializing destination fmap repository -scanning source... -sorting... -converting... -2 init -1 changea -0 changeb -o 1 changeb files: b -| -o 0 changea files: b - -# now tests that it works with trunk/branches/tags layout - -% initial svn import -Adding projA/trunk -Adding projA/branches -Adding projA/tags - -Committed revision 4. -% update svn repository -Checked out revision 4. -A letter.txt -Adding letter.txt -Transmitting file data . -Committed revision 5. -Sending letter.txt -Transmitting file data . -Committed revision 6. - -Committed revision 7. -Sending letter.txt -Transmitting file data . -Committed revision 8. -% convert to hg once -initializing destination A-hg repository -scanning source... -sorting... -converting... -3 init projA -2 hello -1 world -0 nice day -updating tags -% update svn repository again -A letter2.txt -Sending letter.txt -Adding letter2.txt -Transmitting file data .. -Committed revision 9. - -Committed revision 10. -Sending letter2.txt -Transmitting file data . -Committed revision 11. -% test incremental conversion -destination A-hg is a Mercurial repository -scanning source... -sorting... -converting... -1 second letter -0 work in progress -updating tags -o 7 update tags files: .hgtags -| -o 6 work in progress files: letter2.txt -| -o 5 second letter files: letter.txt letter2.txt -| -o 4 update tags files: .hgtags -| -o 3 nice day files: letter.txt -| -o 2 world files: letter.txt -| -o 1 hello files: letter.txt -| -o 0 init projA files: - -tip -v0.2 -v0.1
--- a/tests/test-convert.out Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-convert.out Thu Dec 06 13:11:36 2007 -0800 @@ -11,6 +11,7 @@ Accepted destination formats: - Mercurial + - Subversion (history on branches is not preserved) If no revision is given, all revisions will be converted. Otherwise, convert will only import up to the named revision (given in a format @@ -54,6 +55,24 @@ subdirectory into the root of the repository, use '.' as the path to rename to. + Back end options: + + --config convert.hg.clonebranches=False (boolean) + hg target: XXX not documented + --config convert.hg.saverev=True (boolean) + hg source: allow target to preserve source revision ID + --config convert.hg.tagsbranch=default (branch name) + hg target: XXX not documented + --config convert.hg.usebranchnames=True (boolean) + hg target: preserve branch names + + --config convert.svn.branches=branches (directory name) + svn source: specify the directory containing branches + --config convert.svn.tags=tags (directory name) + svn source: specify the directory containing tags + --config convert.svn.trunk=trunk (directory name) + svn source: specify the name of the trunk branch + options: -A --authors username mapping filename
--- a/tests/test-execute-bit Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-execute-bit Thu Dec 06 13:11:36 2007 -0800 @@ -1,5 +1,7 @@ #!/bin/sh +"$TESTDIR/hghave" execbit || exit 80 + hg init echo a > a hg ci -d'0 0' -Am'not executable'
--- a/tests/test-hgweb Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-hgweb Thu Dec 06 13:11:36 2007 -0800 @@ -1,4 +1,5 @@ #!/bin/sh +# Some tests for hgweb. Tests static files, plain files and different 404's. hg init test cd test @@ -7,7 +8,25 @@ echo foo > foo hg ci -Ambase -d '0 0' hg serve -p $HGPORT -d --pid-file=hg.pid +cat hg.pid >> $DAEMON_PIDS echo % manifest ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/?style=raw') ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/da?style=raw') -kill `cat hg.pid` + +echo % plain file +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?style=raw' + +echo % should give a 404 - static file that does not exist +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/bogus' + +echo % should give a 404 - bad revision +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/spam/foo?style=raw' + +echo % should give a 400 - bad command +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?cmd=spam&style=raw' | sed 's/400.*/400/' + +echo % should give a 404 - file does not exist +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork?style=raw' + +echo % static file +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/style-gitweb.css'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-hgweb-commands Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,53 @@ +#!/bin/sh +# An attempt at more fully testing the hgweb web interface. +# The following things are tested elsewhere and are therefore omitted: +# - archive, tested in test-archive +# - unbundle, tested in test-push-http +# - changegroupsubset, tested in test-pull + +echo % Set up the repo +hg init test +cd test +mkdir da +echo foo > da/foo +echo foo > foo +hg ci -d'0 0' -Ambase +hg tag 1.0 +hg serve -n test -p $HGPORT -d --pid-file=hg.pid -E errors.log +cat hg.pid >> $DAEMON_PIDS + +echo % Logs and changes +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/log/?style=atom' | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//" +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/log/1/?style=atom' | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//" +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/log/1/foo/?style=atom' | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//" +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/shortlog/' | sed "s/[0-9]* years/many years/" +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/rev/1/?style=raw' + +echo % File-related +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/file/1/foo/?style=raw' +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/annotate/1/foo/?style=raw' +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/file/1/?style=raw' +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/filediff/1/foo/?style=raw' + +echo % Overviews +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/tags/?style=atom' | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//" +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/summary/?style=gitweb' | sed "s/[0-9]* years ago/long ago/" + +echo % capabilities +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/capabilities' +echo % heads +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/heads' +echo % lookup +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/lookup/1' +echo % branches +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/branches' +echo % changegroup +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/changegroup' +echo % stream_out +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/stream_out' + +echo % Static files +"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/static/style.css' + +echo % ERRORS ENCOUNTERED +cat errors.log
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-hgweb-no-request-uri Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,77 @@ +#!/bin/sh +# This tests if hgweb and hgwebdir still work if the REQUEST_URI variable is +# no longer passed with the request. Instead, SCRIPT_NAME and PATH_INFO +# should be used from d74fc8dec2b4 onward to route the request. + +mkdir repo +cd repo +hg init +echo foo > bar +hg add bar +hg commit -m "test" -d "0 0" -u "Testing" +hg tip + +cat > request.py <<EOF +from mercurial.hgweb import hgweb, hgwebdir +from StringIO import StringIO +import os, sys + +errors = StringIO() +input = StringIO() + +def startrsp(headers, data): + print '---- HEADERS' + print headers + print '---- DATA' + print data + return output.write + +env = { + 'wsgi.version': (1, 0), + 'wsgi.url_scheme': 'http', + 'wsgi.errors': errors, + 'wsgi.input': input, + 'wsgi.multithread': False, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, + 'REQUEST_METHOD': 'GET', + 'SCRIPT_NAME': '', + 'SERVER_NAME': '127.0.0.1', + 'SERVER_PORT': os.environ['HGPORT'], + 'SERVER_PROTOCOL': 'HTTP/1.0' +} + +output = StringIO() +env['PATH_INFO'] = '/' +env['QUERY_STRING'] = 'style=atom' +hgweb('.', name = 'repo')(env, startrsp) +print output.getvalue() +print '---- ERRORS' +print errors.getvalue() + +output = StringIO() +env['PATH_INFO'] = '/file/tip/' +env['QUERY_STRING'] = 'style=raw' +hgweb('.', name = 'repo')(env, startrsp) +print output.getvalue() +print '---- ERRORS' +print errors.getvalue() + +output = StringIO() +env['PATH_INFO'] = '/' +env['QUERY_STRING'] = 'style=raw' +hgwebdir({'repo': '.'})(env, startrsp) +print output.getvalue() +print '---- ERRORS' +print errors.getvalue() + +output = StringIO() +env['PATH_INFO'] = '/repo/file/tip/' +env['QUERY_STRING'] = 'style=raw' +hgwebdir({'repo': '.'})(env, startrsp) +print output.getvalue() +print '---- ERRORS' +print errors.getvalue() +EOF + +python request.py | sed "s/http:\/\/127\.0\.0\.1:[0-9]*\//http:\/\/127.0.0.1\//"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-hgweb-no-request-uri.out Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,72 @@ +changeset: 0:4cbec7e6f8c4 +tag: tip +user: Testing +date: Thu Jan 01 00:00:00 1970 +0000 +summary: test + +---- HEADERS +200 Script output follows +---- DATA +[('content-type', 'application/atom+xml; charset=ascii')] +<?xml version="1.0" encoding="ascii"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + <!-- Changelog --> + <id>http://127.0.0.1/</id> + <link rel="self" href="http://127.0.0.1/atom-log"/> + <link rel="alternate" href="http://127.0.0.1/"/> + <title>repo Changelog</title> + <updated>1970-01-01T00:00:00+00:00</updated> + + <entry> + <title>test</title> + <id>http://www.selenic.com/mercurial/#changeset-4cbec7e6f8c42eb52b6b52670e1f7560ae9a101e</id> + <link href="http://127.0.0.1/rev/4cbec7e6f8c42eb52b6b52670e1f7560ae9a101e"/> + <author> + <name>Testing</name> + <email>Testing</email> + </author> + <updated>1970-01-01T00:00:00+00:00</updated> + <published>1970-01-01T00:00:00+00:00</published> + <content type="xhtml"> + <div xmlns="http://www.w3.org/1999/xhtml"> + <pre xml:space="preserve">test</pre> + </div> + </content> + </entry> + +</feed> + +---- ERRORS + +---- HEADERS +200 Script output follows +---- DATA +[('content-type', 'text/plain; charset=ascii')] + +-rw-r--r-- 4 bar + + + +---- ERRORS + +---- HEADERS +200 Script output follows +---- DATA +[('content-type', 'text/plain; charset=ascii')] + +/repo/ + + +---- ERRORS + +---- HEADERS +200 Script output follows +---- DATA +[('content-type', 'text/plain; charset=ascii')] + +-rw-r--r-- 4 bar + + + +---- ERRORS +
--- a/tests/test-hgweb.out Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-hgweb.out Thu Dec 06 13:11:36 2007 -0800 @@ -14,3 +14,123 @@ -rw-r--r-- 4 foo +% plain file +200 Script output follows + +foo +% should give a 404 - static file that does not exist +404 Not Found + +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html> +<head> +<link rel="icon" href="/static/hgicon.png" type="image/png"> +<meta name="robots" content="index, nofollow" /> +<link rel="stylesheet" href="/static/style.css" type="text/css" /> + +<title>Mercurial Error</title> +</head> +<body> + +<h2>Mercurial Error</h2> + +<p> +An error occured while processing your request: +</p> +<p> +Not Found +</p> + + +<div class="logo"> +powered by<br/> +<a href="http://www.selenic.com/mercurial/">mercurial</a> +</div> + +</body> +</html> + +% should give a 404 - bad revision +404 Not Found + + +error: revision not found: spam +% should give a 400 - bad command +400 + + +error: No such method: spam +% should give a 404 - file does not exist +404 Not Found + + +error: Path not found: bork/ +% static file +200 Script output follows + +body { font-family: sans-serif; font-size: 12px; margin:0px; border:solid #d9d8d1; border-width:1px; margin:10px; } +a { color:#0000cc; } +a:hover, a:visited, a:active { color:#880000; } +div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; } +div.page_header a:visited { color:#0000cc; } +div.page_header a:hover { color:#880000; } +div.page_nav { padding:8px; } +div.page_nav a:visited { color:#0000cc; } +div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px} +div.page_footer { padding:4px 8px; background-color: #d9d8d1; } +div.page_footer_text { float:left; color:#555555; font-style:italic; } +div.page_body { padding:8px; } +div.title, a.title { + display:block; padding:6px 8px; + font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000; +} +a.title:hover { background-color: #d9d8d1; } +div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; } +div.log_body { padding:8px 8px 8px 150px; } +.age { white-space:nowrap; } +span.age { position:relative; float:left; width:142px; font-style:italic; } +div.log_link { + padding:0px 8px; + font-size:10px; font-family:sans-serif; font-style:normal; + position:relative; float:left; width:136px; +} +div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; } +a.list { text-decoration:none; color:#000000; } +a.list:hover { text-decoration:underline; color:#880000; } +table { padding:8px 4px; } +th { padding:2px 5px; font-size:12px; text-align:left; } +tr.light:hover, .parity0:hover { background-color:#edece6; } +tr.dark, .parity1 { background-color:#f6f6f0; } +tr.dark:hover, .parity1:hover { background-color:#edece6; } +td { padding:2px 5px; font-size:12px; vertical-align:top; } +td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; } +div.pre { font-family:monospace; font-size:12px; white-space:pre; } +div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; } +div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; } +div.search { margin:4px 8px; position:absolute; top:56px; right:12px } +.linenr { color:#999999; text-decoration:none } +a.rss_logo { + float:right; padding:3px 0px; width:35px; line-height:10px; + border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e; + color:#ffffff; background-color:#ff6600; + font-weight:bold; font-family:sans-serif; font-size:10px; + text-align:center; text-decoration:none; +} +a.rss_logo:hover { background-color:#ee5500; } +pre { margin: 0; } +span.logtags span { + padding: 0px 4px; + font-size: 10px; + font-weight: normal; + border: 1px solid; + background-color: #ffaaff; + border-color: #ffccff #ff00ee #ff00ee #ffccff; +} +span.logtags span.tagtag { + background-color: #ffffaa; + border-color: #ffffcc #ffee00 #ffee00 #ffffcc; +} +span.logtags span.branchtag { + background-color: #aaffaa; + border-color: #ccffcc #00cc33 #00cc33 #ccffcc; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-hgwebdir Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,84 @@ +#!/bin/sh +# Tests some basic hgwebdir functionality. Tests setting up paths and +# collection, different forms of 404s and the subdirectory support. + +mkdir webdir +cd webdir + +hg init a +echo a > a/a +hg --cwd a ci -Ama -d'1 0' + +hg init b +echo b > b/b +hg --cwd b ci -Amb -d'2 0' + +hg init c +echo c > c/c +hg --cwd c ci -Amc -d'3 0' +root=`pwd` + +cd .. + +cat > paths.conf <<EOF +[paths] +a=$root/a +b=$root/b +EOF + +hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf paths.conf \ + -A access-paths.log -E error-paths-1.log +cat hg.pid >> $DAEMON_PIDS + +echo % should give a 404 - file does not exist +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/bork?style=raw' + +echo % should succeed +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw' +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/a?style=raw' +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw' + +echo % should give a 404 - repo is not published +"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw' + +cat > paths.conf <<EOF +[paths] +t/a/=$root/a +b=$root/b +EOF + +hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \ + -A access-paths.log -E error-paths-2.log +cat hg.pid >> $DAEMON_PIDS + +echo % should succeed, slashy names +"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw' +"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t?style=raw' +"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=raw' +"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a?style=atom' \ + | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//" +"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/?style=atom' \ + | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//" +"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/file/tip/a?style=raw' + +cat > collections.conf <<EOF +[collections] +$root=$root +EOF + +hg serve -p $HGPORT2 -d --pid-file=hg.pid --webdir-conf collections.conf \ + -A access-collections.log -E error-collections.log +cat hg.pid >> $DAEMON_PIDS + +echo % should succeed +"$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/?style=raw' +"$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/file/tip/a?style=raw' +"$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/b/file/tip/b?style=raw' +"$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/c/file/tip/c?style=raw' + +echo % paths errors 1 +cat error-paths-1.log +echo % paths errors 2 +cat error-paths-2.log +echo % collections errors +cat error-collections.log
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-hgwebdir.out Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,124 @@ +adding a +adding b +adding c +% should give a 404 - file does not exist +404 Not Found + + +error: Path not found: bork/ +% should succeed +200 Script output follows + + +/a/ +/b/ + +200 Script output follows + +a +200 Script output follows + +b +% should give a 404 - repo is not published +404 Not Found + + +error: repository c not found +% should succeed, slashy names +200 Script output follows + + +/b/ +/t/a/ + +200 Script output follows + + +/t/a/ + +200 Script output follows + + +/t/a/ + +200 Script output follows + +<?xml version="1.0" encoding="ascii"?> +<feed xmlns="http://127.0.0.1/2005/Atom"> + <!-- Changelog --> + <id>http://127.0.0.1/t/a/</id> + <link rel="self" href="http://127.0.0.1/t/a/atom-log"/> + <link rel="alternate" href="http://127.0.0.1/t/a/"/> + <title>t/a Changelog</title> + <updated>1970-01-01T00:00:01+00:00</updated> + + <entry> + <title>a</title> + <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id> + <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/> + <author> + <name>test</name> + <email>test</email> + </author> + <updated>1970-01-01T00:00:01+00:00</updated> + <published>1970-01-01T00:00:01+00:00</published> + <content type="xhtml"> + <div xmlns="http://127.0.0.1/1999/xhtml"> + <pre xml:space="preserve">a</pre> + </div> + </content> + </entry> + +</feed> +200 Script output follows + +<?xml version="1.0" encoding="ascii"?> +<feed xmlns="http://127.0.0.1/2005/Atom"> + <!-- Changelog --> + <id>http://127.0.0.1/t/a/</id> + <link rel="self" href="http://127.0.0.1/t/a/atom-log"/> + <link rel="alternate" href="http://127.0.0.1/t/a/"/> + <title>t/a Changelog</title> + <updated>1970-01-01T00:00:01+00:00</updated> + + <entry> + <title>a</title> + <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id> + <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/> + <author> + <name>test</name> + <email>test</email> + </author> + <updated>1970-01-01T00:00:01+00:00</updated> + <published>1970-01-01T00:00:01+00:00</published> + <content type="xhtml"> + <div xmlns="http://127.0.0.1/1999/xhtml"> + <pre xml:space="preserve">a</pre> + </div> + </content> + </entry> + +</feed> +200 Script output follows + +a +% should succeed +200 Script output follows + + +/a/ +/b/ +/c/ + +200 Script output follows + +a +200 Script output follows + +b +200 Script output follows + +c +% paths errors 1 +% paths errors 2 +% collections errors
--- a/tests/test-import Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-import Thu Dec 06 13:11:36 2007 -0800 @@ -57,7 +57,7 @@ cat > mkmsg.py <<EOF import email.Message, sys msg = email.Message.Message() -msg.set_payload('email commit message\n' + open('tip.patch').read()) +msg.set_payload('email commit message\n' + open('tip.patch', 'rb').read()) msg['Subject'] = 'email patch' msg['From'] = 'email patcher' sys.stdout.write(msg.as_string())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-merge-types Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,36 @@ +#!/bin/sh + +hg init +echo a > a +hg ci -Amadd + +chmod +x a +hg ci -mexecutable + +hg up 0 +rm a +ln -s symlink a +hg ci -msymlink + +hg merge + +echo % symlink is left parent, executable is right + +if [ -L a ]; then + echo a is a symlink + readlink a +elif [ -x a ]; then + echo a is executable +fi + +hg update -C 1 +hg merge + +echo % symlink is right parent, executable is left + +if [ -L a ]; then + echo a is a symlink + readlink a +elif [ -x a ]; then + echo a is executable +fi
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-merge-types.out Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,1 @@ +### This test is for a known, unfixed bug ###
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-newcgi Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,91 @@ +#!/bin/sh +# This tests if CGI files from after d0db3462d568 but +# before d74fc8dec2b4 still work. + +hg init test + +cat >hgweb.cgi <<HGWEB +#!/usr/bin/env python +# +# An example CGI script to use hgweb, edit as necessary + +import cgitb +cgitb.enable() + +from mercurial import demandimport; demandimport.enable() +from mercurial.hgweb import hgweb +from mercurial.hgweb import wsgicgi +from mercurial.hgweb.request import wsgiapplication + +def make_web_app(): + return hgweb("test", "Empty test repository") + +wsgicgi.launch(wsgiapplication(make_web_app)) +HGWEB +chmod 755 hgweb.cgi + +cat >hgweb.config <<HGWEBDIRCONF +[paths] +test = test +HGWEBDIRCONF + +cat >hgwebdir.cgi <<HGWEBDIR +#!/usr/bin/env python +# +# An example CGI script to export multiple hgweb repos, edit as necessary + +import cgitb +cgitb.enable() + +from mercurial import demandimport; demandimport.enable() +from mercurial.hgweb import hgwebdir +from mercurial.hgweb import wsgicgi +from mercurial.hgweb.request import wsgiapplication + +def make_web_app(): + return hgwebdir("hgweb.config") + +wsgicgi.launch(wsgiapplication(make_web_app)) +HGWEBDIR +chmod 755 hgwebdir.cgi + +DOCUMENT_ROOT="/var/www/hg"; export DOCUMENT_ROOT +GATEWAY_INTERFACE="CGI/1.1"; export GATEWAY_INTERFACE +HTTP_ACCEPT="text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"; export HTTP_ACCEPT +HTTP_ACCEPT_CHARSET="ISO-8859-1,utf-8;q=0.7,*;q=0.7"; export HTTP_ACCEPT_CHARSET +HTTP_ACCEPT_ENCODING="gzip,deflate"; export HTTP_ACCEPT_ENCODING +HTTP_ACCEPT_LANGUAGE="en-us,en;q=0.5"; export HTTP_ACCEPT_LANGUAGE +HTTP_CACHE_CONTROL="max-age=0"; export HTTP_CACHE_CONTROL +HTTP_CONNECTION="keep-alive"; export HTTP_CONNECTION +HTTP_HOST="hg.omnifarious.org"; export HTTP_HOST +HTTP_KEEP_ALIVE="300"; export HTTP_KEEP_ALIVE +HTTP_USER_AGENT="Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4"; export HTTP_USER_AGENT +PATH_INFO="/"; export PATH_INFO +PATH_TRANSLATED="/var/www/hg/index.html"; export PATH_TRANSLATED +QUERY_STRING=""; export QUERY_STRING +REMOTE_ADDR="127.0.0.2"; export REMOTE_ADDR +REMOTE_PORT="44703"; export REMOTE_PORT +REQUEST_METHOD="GET"; export REQUEST_METHOD +REQUEST_URI="/test/"; export REQUEST_URI +SCRIPT_FILENAME="/home/hopper/hg_public/test.cgi"; export SCRIPT_FILENAME +SCRIPT_NAME="/test"; export SCRIPT_NAME +SCRIPT_URI="http://hg.omnifarious.org/test/"; export SCRIPT_URI +SCRIPT_URL="/test/"; export SCRIPT_URL +SERVER_ADDR="127.0.0.1"; export SERVER_ADDR +SERVER_ADMIN="eric@localhost"; export SERVER_ADMIN +SERVER_NAME="hg.omnifarious.org"; export SERVER_NAME +SERVER_PORT="80"; export SERVER_PORT +SERVER_PROTOCOL="HTTP/1.1"; export SERVER_PROTOCOL +SERVER_SIGNATURE="<address>Apache/2.0.53 (Fedora) Server at hg.omnifarious.org Port 80</address>\; export SERVER_SIGNATURE +" +SERVER_SOFTWARE="Apache/2.0.53 (Fedora)"; export SERVER_SOFTWARE +python hgweb.cgi >page1 2>&1 ; echo $? +python hgwebdir.cgi >page2 2>&1 ; echo $? +PATH_INFO="/test/" +PATH_TRANSLATED="/var/something/test.cgi" +REQUEST_URI="/test/test/" +SCRIPT_URI="http://hg.omnifarious.org/test/test/" +SCRIPT_URL="/test/test/" +python hgwebdir.cgi >page3 2>&1 ; echo $? +fgrep -i error page1 page2 page3 && exit 1 +exit 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-newcgi.out Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,3 @@ +0 +0 +0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-newercgi Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,84 @@ +#!/bin/sh +# This is a rudimentary test of the CGI files as of d74fc8dec2b4. + +hg init test + +cat >hgweb.cgi <<HGWEB +#!/usr/bin/env python +# +# An example CGI script to use hgweb, edit as necessary + +import cgitb +cgitb.enable() + +from mercurial import demandimport; demandimport.enable() +from mercurial.hgweb import hgweb +from mercurial.hgweb import wsgicgi + +application = hgweb("test", "Empty test repository") +wsgicgi.launch(application) +HGWEB +chmod 755 hgweb.cgi + +cat >hgweb.config <<HGWEBDIRCONF +[paths] +test = test +HGWEBDIRCONF + +cat >hgwebdir.cgi <<HGWEBDIR +#!/usr/bin/env python +# +# An example CGI script to export multiple hgweb repos, edit as necessary + +import cgitb +cgitb.enable() + +from mercurial import demandimport; demandimport.enable() +from mercurial.hgweb import hgwebdir +from mercurial.hgweb import wsgicgi + +application = hgwebdir("hgweb.config") +wsgicgi.launch(application) +HGWEBDIR +chmod 755 hgwebdir.cgi + +DOCUMENT_ROOT="/var/www/hg"; export DOCUMENT_ROOT +GATEWAY_INTERFACE="CGI/1.1"; export GATEWAY_INTERFACE +HTTP_ACCEPT="text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"; export HTTP_ACCEPT +HTTP_ACCEPT_CHARSET="ISO-8859-1,utf-8;q=0.7,*;q=0.7"; export HTTP_ACCEPT_CHARSET +HTTP_ACCEPT_ENCODING="gzip,deflate"; export HTTP_ACCEPT_ENCODING +HTTP_ACCEPT_LANGUAGE="en-us,en;q=0.5"; export HTTP_ACCEPT_LANGUAGE +HTTP_CACHE_CONTROL="max-age=0"; export HTTP_CACHE_CONTROL +HTTP_CONNECTION="keep-alive"; export HTTP_CONNECTION +HTTP_HOST="hg.omnifarious.org"; export HTTP_HOST +HTTP_KEEP_ALIVE="300"; export HTTP_KEEP_ALIVE +HTTP_USER_AGENT="Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4"; export HTTP_USER_AGENT +PATH_INFO="/"; export PATH_INFO +PATH_TRANSLATED="/var/www/hg/index.html"; export PATH_TRANSLATED +QUERY_STRING=""; export QUERY_STRING +REMOTE_ADDR="127.0.0.2"; export REMOTE_ADDR +REMOTE_PORT="44703"; export REMOTE_PORT +REQUEST_METHOD="GET"; export REQUEST_METHOD +REQUEST_URI="/test/"; export REQUEST_URI +SCRIPT_FILENAME="/home/hopper/hg_public/test.cgi"; export SCRIPT_FILENAME +SCRIPT_NAME="/test"; export SCRIPT_NAME +SCRIPT_URI="http://hg.omnifarious.org/test/"; export SCRIPT_URI +SCRIPT_URL="/test/"; export SCRIPT_URL +SERVER_ADDR="127.0.0.1"; export SERVER_ADDR +SERVER_ADMIN="eric@localhost"; export SERVER_ADMIN +SERVER_NAME="hg.omnifarious.org"; export SERVER_NAME +SERVER_PORT="80"; export SERVER_PORT +SERVER_PROTOCOL="HTTP/1.1"; export SERVER_PROTOCOL +SERVER_SIGNATURE="<address>Apache/2.0.53 (Fedora) Server at hg.omnifarious.org Port 80</address>\; export SERVER_SIGNATURE +" +SERVER_SOFTWARE="Apache/2.0.53 (Fedora)"; export SERVER_SOFTWARE +python hgweb.cgi >page1 2>&1 ; echo $? +python hgwebdir.cgi >page2 2>&1 ; echo $? +PATH_INFO="/test/" +PATH_TRANSLATED="/var/something/test.cgi" +REQUEST_URI="/test/test/" +SCRIPT_URI="http://hg.omnifarious.org/test/test/" +SCRIPT_URL="/test/test/" +python hgwebdir.cgi >page3 2>&1 ; echo $? +fgrep -i error page1 page2 page3 && exit 1 +exit 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-newercgi.out Thu Dec 06 13:11:36 2007 -0800 @@ -0,0 +1,3 @@ +0 +0 +0
--- a/tests/test-non-interactive-wsgi Thu Dec 06 13:10:25 2007 -0800 +++ b/tests/test-non-interactive-wsgi Thu Dec 06 13:11:36 2007 -0800 @@ -1,4 +1,6 @@ #!/bin/sh +# Tests if hgweb can run without touching sys.stdin, as is required +# by the WSGI standard and strictly implemented by mod_wsgi. mkdir repo cd repo @@ -11,7 +13,6 @@ cat > request.py <<EOF from mercurial import dispatch from mercurial.hgweb.hgweb_mod import hgweb -from mercurial.hgweb.request import _wsgirequest from mercurial.ui import ui from mercurial import hg from StringIO import StringIO @@ -62,7 +63,7 @@ 'SERVER_PROTOCOL': 'HTTP/1.0' } -_wsgirequest(hgweb('.'), env, startrsp) +hgweb('.')(env, startrsp) print '---- ERRORS' print errors.getvalue() EOF