# HG changeset patch # User Thomas Arendsen Hein # Date 1151696128 -7200 # Node ID 7a90e0c77f43f9f34232bd0c1493b9b90803a83f # Parent d181845bdc5127abd9ef290a53044c0a7754a47b# Parent c1974f65d781f35b0a6c7ad9a7a87d1a6d66e2b4 Merge with crew. diff --git a/contrib/macosx/Readme.html b/contrib/macosx/Readme.html --- a/contrib/macosx/Readme.html +++ b/contrib/macosx/Readme.html @@ -18,13 +18,10 @@


This is not a stand-alone version of Mercurial.


-

To use it, you must have the “official unofficial” MacPython 2.4.1 installed.

+

To use it, you must have the Universal MacPython 2.4.3 from www.python.org installed.


-

You can download MacPython 2.4.1 from here:

-

http://python.org/ftp/python/2.4.1/MacPython-OSX-2.4.1-1.dmg

-


-

For more information on MacPython, go here:

-

http://undefined.org/python

+

You can download MacPython 2.4.3 from here:

+

http://www.python.org/ftp/python/2.4.3/Universal-MacPython-2.4.3-2006-04-07.dmg


After you install


diff --git a/contrib/macosx/Welcome.html b/contrib/macosx/Welcome.html --- a/contrib/macosx/Welcome.html +++ b/contrib/macosx/Welcome.html @@ -12,6 +12,6 @@

This is a prepackaged release of Mercurial for Mac OS X.


-

It is based on Mercurial 0.8.

+

It is based on Mercurial 0.9.

diff --git a/contrib/mercurial.el b/contrib/mercurial.el --- a/contrib/mercurial.el +++ b/contrib/mercurial.el @@ -653,7 +653,7 @@ The Mercurial mode user interface is bas you're already familiar with VC, the same keybindings and functions will generally work. -Below is a list of many common SCM tasks. In the list, `G/L' +Below is a list of many common SCM tasks. In the list, `G/L\' indicates whether a key binding is global (G) to a repository or local (L) to a file. Many commands take a prefix argument. @@ -682,6 +682,8 @@ Pull changes G Update working directory after pull G C-c h u hg-update See changes that can be pushed G C-c h . hg-outgoing Push changes G C-c h > hg-push" + (unless vc-make-backup-files + (set (make-local-variable 'backup-inhibited) t)) (run-hooks 'hg-mode-hook)) (defun hg-find-file-hook () @@ -729,6 +731,8 @@ With a prefix argument, prompt for the p (goto-char 0) (cd (hg-root path))) (when update + (unless vc-make-backup-files + (set (make-local-variable 'backup-inhibited) t)) (with-current-buffer buf (hg-mode-line))))) @@ -968,6 +972,7 @@ With a prefix argument, prompt for the p (cd (hg-root path))) (when update (with-current-buffer buf + (set (make-local-variable 'backup-inhibited) nil) (hg-mode-line))))) (defun hg-incoming (&optional repo) diff --git a/hgext/mq.py b/hgext/mq.py --- a/hgext/mq.py +++ b/hgext/mq.py @@ -214,7 +214,6 @@ class queue: return pp[0] if p1 in arevs: return pp[1] - return None return pp[0] def mergepatch(self, repo, mergeq, series, wlock): @@ -386,15 +385,21 @@ class queue: self.ui.write("Local changes found, refresh first\n") sys.exit(1) def new(self, repo, patch, msg=None, force=None): - if not force: - self.check_localchanges(repo) + commitfiles = [] + (c, a, r, d, u) = repo.changes(None, None) + if c or a or d or r: + if not force: + raise util.Abort(_("Local changes found, refresh first")) + else: + commitfiles = c + a + r self.check_toppatch(repo) wlock = repo.wlock() insert = self.series_end() if msg: - n = repo.commit([], "[mq]: %s" % msg, force=True, wlock=wlock) + n = repo.commit(commitfiles, "[mq]: %s" % msg, force=True, + wlock=wlock) else: - n = repo.commit([], + n = repo.commit(commitfiles, "New patch: %s" % patch, force=True, wlock=wlock) if n == None: self.ui.warn("repo commit failed\n") @@ -412,6 +417,8 @@ class queue: wlock = None r = self.qrepo() if r: r.add([patch]) + if commitfiles: + self.refresh(repo, short=True) def strip(self, repo, rev, update=True, backup="all", wlock=None): def limitheads(chlog, stop): diff --git a/hgweb.cgi b/hgweb.cgi --- a/hgweb.cgi +++ b/hgweb.cgi @@ -6,7 +6,11 @@ import cgitb, os, sys cgitb.enable() # sys.path.insert(0, "/path/to/python/lib") # if not a system-wide install -from mercurial import hgweb +from mercurial.hgweb.hgweb_mod import hgweb +from mercurial.hgweb.request import wsgiapplication +import mercurial.hgweb.wsgicgi as wsgicgi -h = hgweb.hgweb("/path/to/repo", "repository name") -h.run() +def make_web_app(): + return hgweb("/path/to/repo", "repository name") + +wsgicgi.launch(wsgiapplication(make_web_app)) diff --git a/hgwebdir.cgi b/hgwebdir.cgi --- a/hgwebdir.cgi +++ b/hgwebdir.cgi @@ -6,7 +6,9 @@ import cgitb, sys cgitb.enable() # sys.path.insert(0, "/path/to/python/lib") # if not a system-wide install -from mercurial import hgweb +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 # repos, collections of repos in a directory tree, or both. @@ -27,5 +29,7 @@ from mercurial import hgweb # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples # or use a dictionary with entries like 'virtual/path': '/real/path' -h = hgweb.hgwebdir("hgweb.config") -h.run() +def make_web_app(): + return hgwebdir("hgweb.config") + +wsgicgi.launch(wsgiapplication(make_web_app)) diff --git a/mercurial/changelog.py b/mercurial/changelog.py --- a/mercurial/changelog.py +++ b/mercurial/changelog.py @@ -39,21 +39,10 @@ class changelog(revlog): def add(self, manifest, list, desc, transaction, p1=None, p2=None, user=None, date=None): if date: - # validate explicit (probably user-specified) date and - # time zone offset. values must fit in signed 32 bits for - # current 32-bit linux runtimes. timezones go from UTC-12 - # to UTC+14 - try: - when, offset = map(int, date.split(' ')) - except ValueError: - raise ValueError(_('invalid date: %r') % date) - if abs(when) > 0x7fffffff: - raise ValueError(_('date exceeds 32 bits: %d') % when) - if offset < -50400 or offset > 43200: - raise ValueError(_('impossible time zone offset: %d') % offset) + parseddate = "%d %d" % util.parsedate(date) else: - date = "%d %d" % util.makedate() + parseddate = "%d %d" % util.makedate() list.sort() - l = [hex(manifest), user, date] + list + ["", desc] + l = [hex(manifest), user, parseddate] + list + ["", desc] text = "\n".join(l) return self.addrevision(text, transaction, self.count(), p1, p2) diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -12,7 +12,7 @@ demandload(globals(), "os re sys signal demandload(globals(), "fancyopts ui hg util lock revlog templater bundlerepo") demandload(globals(), "fnmatch mdiff random signal tempfile time") demandload(globals(), "traceback errno socket version struct atexit sets bz2") -demandload(globals(), "archival changegroup") +demandload(globals(), "archival cStringIO changegroup email.Parser") demandload(globals(), "hgweb.server sshserver") class UnknownCommand(Exception): @@ -1719,11 +1719,16 @@ def import_(ui, repo, patch1, *patches, If there are outstanding changes in the working directory, import will abort unless given the -f flag. - If a patch looks like a mail message (its first line starts with - "From " or looks like an RFC822 header), it will not be applied - unless the -f option is used. The importer neither parses nor - discards mail headers, so use -f only to override the "mailness" - safety check, not to import a real mail message. + You can import a patch straight from a mail message. Even patches + as attachments work (body part must be type text/plain or + text/x-patch to be used). From and Subject headers of email + message are used as default committer and commit message. All + text/plain body parts before first diff are added to commit + message. + + If imported patch was generated by hg export, user and description + from patch override values from message headers and body. Values + given on command line with -m and -u override these. To read a patch from standard input, use patch name "-". """ @@ -1739,79 +1744,98 @@ def import_(ui, repo, patch1, *patches, # attempt to detect the start of a patch # (this heuristic is borrowed from quilt) - diffre = re.compile(r'(?:Index:[ \t]|diff[ \t]|RCS file: |' + + diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' + 'retrieving revision [0-9]+(\.[0-9]+)*$|' + - '(---|\*\*\*)[ \t])') + '(---|\*\*\*)[ \t])', re.MULTILINE) for patch in patches: pf = os.path.join(d, patch) - message = [] + message = None user = None date = None hgpatch = False + + p = email.Parser.Parser() if pf == '-': - f = sys.stdin - fd, tmpname = tempfile.mkstemp(prefix='hg-patch-') - pf = tmpname - tmpfp = os.fdopen(fd, 'w') + msg = p.parse(sys.stdin) ui.status(_("applying patch from stdin\n")) else: - f = open(pf) - tmpfp, tmpname = None, None + msg = p.parse(file(pf)) ui.status(_("applying %s\n") % patch) + + fd, tmpname = tempfile.mkstemp(prefix='hg-patch-') + tmpfp = os.fdopen(fd, 'w') try: - while True: - line = f.readline() - if not line: break - if tmpfp: tmpfp.write(line) - line = line.rstrip() - if (not message and not hgpatch and - mailre.match(line) and not opts['force']): - if len(line) > 35: - line = line[:32] + '...' - raise util.Abort(_('first line looks like a ' - 'mail header: ') + line) - if diffre.match(line): + message = msg['Subject'] + if message: + message = message.replace('\n\t', ' ') + ui.debug('Subject: %s\n' % message) + user = msg['From'] + if user: + ui.debug('From: %s\n' % user) + diffs_seen = 0 + ok_types = ('text/plain', 'text/x-patch') + for part in msg.walk(): + content_type = part.get_content_type() + ui.debug('Content-Type: %s\n' % content_type) + if content_type not in ok_types: + continue + payload = part.get_payload(decode=True) + m = diffre.search(payload) + if m: + ui.debug(_('found patch at byte %d\n') % m.start(0)) + diffs_seen += 1 + hgpatch = False + fp = cStringIO.StringIO() + if message: + fp.write(message) + fp.write('\n') + for line in payload[:m.start(0)].splitlines(): + if line.startswith('# HG changeset patch'): + ui.debug(_('patch generated by hg export\n')) + hgpatch = True + # drop earlier commit message content + fp.seek(0) + fp.truncate() + elif hgpatch: + if line.startswith('# User '): + user = line[7:] + ui.debug('From: %s\n' % user) + elif line.startswith("# Date "): + date = line[7:] + if not line.startswith('# '): + fp.write(line) + fp.write('\n') + message = fp.getvalue() if tmpfp: - for chunk in util.filechunkiter(f): - tmpfp.write(chunk) - break - elif hgpatch: - # parse values when importing the result of an hg export - if line.startswith("# User "): - user = line[7:] - ui.debug(_('User: %s\n') % user) - elif line.startswith("# Date "): - date = line[7:] - elif not line.startswith("# ") and line: - message.append(line) - hgpatch = False - elif line == '# HG changeset patch': - hgpatch = True - message = [] # We may have collected garbage - elif message or line: - message.append(line) + tmpfp.write(payload) + if not payload.endswith('\n'): + tmpfp.write('\n') + elif not diffs_seen and message and content_type == 'text/plain': + message += '\n' + payload if opts['message']: # pickup the cmdline msg message = opts['message'] elif message: # pickup the patch msg - message = '\n'.join(message).rstrip() + message = message.strip() else: # launch the editor message = None ui.debug(_('message:\n%s\n') % message) - if tmpfp: tmpfp.close() - files = util.patch(strip, pf, ui) - + tmpfp.close() + if not diffs_seen: + raise util.Abort(_('no diffs found')) + + files = util.patch(strip, tmpname, ui) if len(files) > 0: addremove_lock(ui, repo, files, {}) repo.commit(files, message, user, date) finally: - if tmpname: os.unlink(tmpname) + os.unlink(tmpname) def incoming(ui, repo, source="default", **opts): """show new changesets found in source @@ -1851,7 +1875,10 @@ def incoming(ui, repo, source="default", # use the created uncompressed bundlerepo other = bundlerepo.bundlerepository(ui, repo.root, fname) - o = other.changelog.nodesbetween(incoming)[0] + revs = None + if opts['rev']: + revs = [other.lookup(rev) for rev in opts['rev']] + o = other.changelog.nodesbetween(incoming, revs)[0] if opts['newest_first']: o.reverse() displayer = show_changeset(ui, other, opts) @@ -2061,13 +2088,16 @@ def outgoing(ui, repo, dest=None, **opts ui.setconfig("ui", "ssh", opts['ssh']) if opts['remotecmd']: ui.setconfig("ui", "remotecmd", opts['remotecmd']) + revs = None + if opts['rev']: + revs = [repo.lookup(rev) for rev in opts['rev']] other = hg.repository(ui, dest) o = repo.findoutgoing(other, force=opts['force']) if not o: ui.status(_("no changes found\n")) return - o = repo.changelog.nodesbetween(o)[0] + o = repo.changelog.nodesbetween(o, revs)[0] if opts['newest_first']: o.reverse() displayer = show_changeset(ui, repo, opts) @@ -2998,11 +3028,13 @@ table = { ('n', 'newest-first', None, _('show newest record first')), ('', 'bundle', '', _('file to store the bundles into')), ('p', 'patch', None, _('show patch')), + ('r', 'rev', [], _('a specific revision you would like to pull')), ('', 'template', '', _('display with template')), ('e', 'ssh', '', _('specify ssh command to use')), ('', 'remotecmd', '', _('specify hg command to run on the remote side'))], - _('hg incoming [-p] [-n] [-M] [--bundle FILENAME] [SOURCE]')), + _('hg incoming [-p] [-n] [-M] [-r REV]...' + '[--bundle FILENAME] [SOURCE]')), "^init": (init, [], _('hg init [DEST]')), "locate": (locate, @@ -3040,12 +3072,13 @@ table = { _('run even when remote repository is unrelated')), ('p', 'patch', None, _('show patch')), ('', 'style', '', _('display using template map file')), + ('r', 'rev', [], _('a specific revision you would like to push')), ('n', 'newest-first', None, _('show newest record first')), ('', 'template', '', _('display with template')), ('e', 'ssh', '', _('specify ssh command to use')), ('', 'remotecmd', '', _('specify hg command to run on the remote side'))], - _('hg outgoing [-M] [-p] [-n] [DEST]')), + _('hg outgoing [-M] [-p] [-n] [-r REV]... [DEST]')), "^parents": (parents, [('b', 'branches', None, _('show branches')), diff --git a/mercurial/hgweb/common.py b/mercurial/hgweb/common.py --- a/mercurial/hgweb/common.py +++ b/mercurial/hgweb/common.py @@ -17,7 +17,7 @@ def get_mtime(repo_path): else: return os.stat(hg_path).st_mtime -def staticfile(directory, fname): +def staticfile(directory, fname, req): """return a file inside directory with guessed content-type header fname always uses '/' as directory separator and isn't allowed to @@ -36,7 +36,9 @@ def staticfile(directory, fname): try: os.stat(path) ct = mimetypes.guess_type(path)[0] or "text/plain" - return "Content-type: %s\n\n%s" % (ct, file(path).read()) + req.header([('Content-type', ct), + ('Content-length', os.path.getsize(path))]) + return file(path).read() except (TypeError, OSError): # illegal fname or unreadable file return "" diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py --- a/mercurial/hgweb/hgweb_mod.py +++ b/mercurial/hgweb/hgweb_mod.py @@ -10,9 +10,8 @@ import os import os.path import mimetypes from mercurial.demandload import demandload -demandload(globals(), "re zlib ConfigParser cStringIO sys tempfile") +demandload(globals(), "re zlib ConfigParser mimetools cStringIO sys tempfile") demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater") -demandload(globals(), "mercurial.hgweb.request:hgrequest") demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile") from mercurial.node import * from mercurial.i18n import gettext as _ @@ -651,9 +650,12 @@ class hgweb(object): raise Exception("suspicious path") return p - def run(self, req=hgrequest()): + def run(self, req): def header(**map): - yield self.t("header", **map) + header_file = cStringIO.StringIO(''.join(self.t("header", **map))) + msg = mimetools.Message(header_file, 0) + req.header(msg.items()) + yield header_file.read() def footer(**map): yield self.t("footer", @@ -724,7 +726,6 @@ class hgweb(object): method(req) else: req.write(self.t("error")) - req.done() def do_changelog(self, req): hi = self.repo.changelog.count() - 1 @@ -830,7 +831,7 @@ class hgweb(object): static = self.repo.ui.config("web", "static", os.path.join(self.templatepath, "static")) - req.write(staticfile(static, fname) + req.write(staticfile(static, fname, req) or self.t("error", error="%r not found" % fname)) def do_capabilities(self, req): diff --git a/mercurial/hgweb/hgwebdir_mod.py b/mercurial/hgweb/hgwebdir_mod.py --- a/mercurial/hgweb/hgwebdir_mod.py +++ b/mercurial/hgweb/hgwebdir_mod.py @@ -8,10 +8,9 @@ import os from mercurial.demandload import demandload -demandload(globals(), "ConfigParser") +demandload(globals(), "ConfigParser mimetools cStringIO") demandload(globals(), "mercurial:ui,hg,util,templater") demandload(globals(), "mercurial.hgweb.hgweb_mod:hgweb") -demandload(globals(), "mercurial.hgweb.request:hgrequest") demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile") from mercurial.i18n import gettext as _ @@ -47,9 +46,12 @@ class hgwebdir(object): self.repos.append((name.lstrip(os.sep), repo)) self.repos.sort() - def run(self, req=hgrequest()): + def run(self, req): def header(**map): - yield tmpl("header", **map) + header_file = cStringIO.StringIO(''.join(tmpl("header", **map))) + msg = mimetools.Message(header_file, 0) + req.header(msg.items()) + yield header_file.read() def footer(**map): yield tmpl("footer", motd=self.motd, **map) @@ -133,7 +135,7 @@ class hgwebdir(object): if req.form.has_key('static'): static = os.path.join(templater.templatepath(), "static") fname = req.form['static'][0] - req.write(staticfile(static, fname) + req.write(staticfile(static, fname, req) or tmpl("error", error="%r not found" % fname)) else: sortable = ["name", "description", "contact", "lastchange"] diff --git a/mercurial/hgweb/request.py b/mercurial/hgweb/request.py --- a/mercurial/hgweb/request.py +++ b/mercurial/hgweb/request.py @@ -10,13 +10,48 @@ from mercurial.demandload import demandl demandload(globals(), "socket sys cgi os errno") from mercurial.i18n import gettext as _ -class hgrequest(object): - def __init__(self, inp=None, out=None, env=None): - self.inp = inp or sys.stdin - self.out = out or sys.stdout - self.env = env or os.environ +class wsgiapplication(object): + def __init__(self, destmaker): + self.destmaker = destmaker + + def __call__(self, wsgienv, start_response): + return _wsgirequest(self.destmaker(), wsgienv, start_response) + +class _wsgioutputfile(object): + def __init__(self, request): + self.request = request + + def write(self, data): + self.request.write(data) + def writelines(self, lines): + for line in lines: + self.write(line) + def flush(self): + return None + def close(self): + return None + +class _wsgirequest(object): + def __init__(self, destination, wsgienv, start_response): + version = wsgienv['wsgi.version'] + if (version < (1,0)) or (version >= (2, 0)): + raise RuntimeError("Unknown and unsupported WSGI version %d.%d" \ + % version) + self.inp = wsgienv['wsgi.input'] + self.out = _wsgioutputfile(self) + self.server_write = None + self.err = wsgienv['wsgi.errors'] + self.threaded = wsgienv['wsgi.multithread'] + self.multiprocess = wsgienv['wsgi.multiprocess'] + self.run_once = wsgienv['wsgi.run_once'] + self.env = wsgienv self.form = cgi.parse(self.inp, self.env, keep_blank_values=1) - self.will_close = True + self.start_response = start_response + self.headers = [] + destination.run(self) + + def __iter__(self): + return iter([]) def read(self, count=-1): return self.inp.read(count) @@ -27,23 +62,22 @@ class hgrequest(object): for part in thing: self.write(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', + self.headers) + self.start_response = None + self.headers = None try: - self.out.write(str(thing)) + self.server_write(thing) except socket.error, inst: if inst[0] != errno.ECONNRESET: raise - def done(self): - if self.will_close: - self.inp.close() - self.out.close() - else: - self.out.flush() - def header(self, headers=[('Content-type','text/html')]): - for header in headers: - self.out.write("%s: %s\r\n" % header) - self.out.write("\r\n") + self.headers.extend(headers) def httphdr(self, type, filename=None, length=0, headers={}): headers = headers.items() @@ -51,12 +85,6 @@ class hgrequest(object): if filename: headers.append(('Content-disposition', 'attachment; filename=%s' % filename)) - # we do not yet support http 1.1 chunked transfer, so we have - # to force connection to close if content-length not known if length: headers.append(('Content-length', str(length))) - self.will_close = False - else: - headers.append(('Connection', 'close')) - self.will_close = True self.header(headers) diff --git a/mercurial/hgweb/server.py b/mercurial/hgweb/server.py --- a/mercurial/hgweb/server.py +++ b/mercurial/hgweb/server.py @@ -10,7 +10,7 @@ from mercurial.demandload import demandl import os, sys, errno demandload(globals(), "urllib BaseHTTPServer socket SocketServer") demandload(globals(), "mercurial:ui,hg,util,templater") -demandload(globals(), "hgweb_mod:hgweb hgwebdir_mod:hgwebdir request:hgrequest") +demandload(globals(), "hgweb_mod:hgweb hgwebdir_mod:hgwebdir request:wsgiapplication") from mercurial.i18n import gettext as _ def _splitURI(uri): @@ -25,6 +25,17 @@ def _splitURI(uri): path, query = uri, '' return urllib.unquote(path), query +class _error_logger(object): + def __init__(self, handler): + self.handler = handler + def flush(self): + pass + def write(str): + self.writelines(str.split('\n')) + def writelines(seq): + for msg in seq: + self.handler.log_error("HG error: %s", msg) + class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler): def __init__(self, *args, **kargs): self.protocol_version = 'HTTP/1.1' @@ -76,17 +87,72 @@ class _hgwebhandler(object, BaseHTTPServ length = self.headers.getheader('content-length') if length: env['CONTENT_LENGTH'] = length - accept = [] - for line in self.headers.getallmatchingheaders('accept'): - if line[:1] in "\t\n\r ": - accept.append(line.strip()) - else: - accept = accept + line[7:].split(',') - env['HTTP_ACCEPT'] = ','.join(accept) + for header in [h for h in self.headers.keys() \ + if h not in ('content-type', 'content-length')]: + hkey = 'HTTP_' + header.replace('-', '_').upper() + hval = self.headers.getheader(header) + hval = hval.replace('\n', '').strip() + if hval: + env[hkey] = hval + env['SERVER_PROTOCOL'] = self.request_version + env['wsgi.version'] = (1, 0) + env['wsgi.url_scheme'] = 'http' + env['wsgi.input'] = self.rfile + env['wsgi.errors'] = _error_logger(self) + env['wsgi.multithread'] = isinstance(self.server, + SocketServer.ThreadingMixIn) + env['wsgi.multiprocess'] = isinstance(self.server, + SocketServer.ForkingMixIn) + env['wsgi.run_once'] = 0 + + self.close_connection = True + self.saved_status = None + 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) - req = hgrequest(self.rfile, self.wfile, env) - self.send_response(200, "Script output follows") - self.close_connection = self.server.make_and_run_handler(req) + def send_headers(self): + if not self.saved_status: + raise AssertionError("Sending headers before start_response() called") + saved_status = self.saved_status.split(None, 1) + saved_status[0] = int(saved_status[0]) + self.send_response(*saved_status) + should_close = True + for h in self.saved_headers: + self.send_header(*h) + if h[0].lower() == 'content-length': + should_close = False + self.length = int(h[1]) + if should_close: + self.send_header('Connection', 'close') + self.close_connection = should_close + self.end_headers() + self.sent_headers = True + + def _start_response(self, http_status, headers, exc_info=None): + code, msg = http_status.split(None, 1) + code = int(code) + self.saved_status = http_status + bad_headers = ('connection', 'transfer-encoding') + self.saved_headers = [ h for h in headers \ + if h[0].lower() not in bad_headers ] + return self._write + + def _write(self, data): + if not self.saved_status: + raise AssertionError("data written before start_response() called") + elif not self.sent_headers: + self.send_headers() + if self.length is not None: + if len(data) > self.length: + raise AssertionError("Content-length header sent, but more bytes than specified are being written.") + self.length = self.length - len(data) + self.wfile.write(data) + self.wfile.flush() def create_server(ui, repo): use_threads = True @@ -126,8 +192,9 @@ def create_server(ui, repo): self.webdir_conf = webdir_conf self.webdirmaker = hgwebdir self.repoviewmaker = hgweb + self.reqmaker = wsgiapplication(self.make_handler) - def make_and_run_handler(self, req): + def make_handler(self): if self.webdir_conf: hgwebobj = self.webdirmaker(self.webdir_conf) elif self.repo is not None: @@ -135,8 +202,7 @@ def create_server(ui, repo): repo.origroot)) else: raise hg.RepoError(_('no repo found')) - hgwebobj.run(req) - return req.will_close + return hgwebobj class IPv6HTTPServer(MercurialHTTPServer): address_family = getattr(socket, 'AF_INET6', None) @@ -144,7 +210,7 @@ def create_server(ui, repo): def __init__(self, *args, **kwargs): if self.address_family is None: raise hg.RepoError(_('IPv6 not available on this system')) - super(IPv6HTTPServer, self).__init__(*args, **kargs) + super(IPv6HTTPServer, self).__init__(*args, **kwargs) if use_ipv6: return IPv6HTTPServer((address, port), _hgwebhandler) diff --git a/mercurial/hgweb/wsgicgi.py b/mercurial/hgweb/wsgicgi.py new file mode 100644 --- /dev/null +++ b/mercurial/hgweb/wsgicgi.py @@ -0,0 +1,69 @@ +# hgweb/wsgicgi.py - CGI->WSGI translator +# +# Copyright 2006 Eric Hopper +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +# +# This was originally copied from the public domain code at +# http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side + +import os, sys + +def launch(application): + + environ = dict(os.environ.items()) + environ['wsgi.input'] = sys.stdin + environ['wsgi.errors'] = sys.stderr + environ['wsgi.version'] = (1,0) + environ['wsgi.multithread'] = False + environ['wsgi.multiprocess'] = True + environ['wsgi.run_once'] = True + + if environ.get('HTTPS','off') in ('on','1'): + environ['wsgi.url_scheme'] = 'https' + else: + environ['wsgi.url_scheme'] = 'http' + + headers_set = [] + headers_sent = [] + + def write(data): + if not headers_set: + raise AssertionError("write() before start_response()") + + elif not headers_sent: + # Before the first output, send the stored headers + status, response_headers = headers_sent[:] = headers_set + sys.stdout.write('Status: %s\r\n' % status) + for header in response_headers: + sys.stdout.write('%s: %s\r\n' % header) + sys.stdout.write('\r\n') + + sys.stdout.write(data) + sys.stdout.flush() + + def start_response(status,response_headers,exc_info=None): + if exc_info: + try: + if headers_sent: + # Re-raise original exception if headers sent + raise exc_info[0], exc_info[1], exc_info[2] + finally: + exc_info = None # avoid dangling circular ref + elif headers_set: + raise AssertionError("Headers already set!") + + 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() diff --git a/mercurial/templater.py b/mercurial/templater.py --- a/mercurial/templater.py +++ b/mercurial/templater.py @@ -225,6 +225,10 @@ def isodate(date): '''turn a (timestamp, tzoff) tuple into an iso 8631 date and time.''' return util.datestr(date, format='%Y-%m-%d %H:%M') +def hgdate(date): + '''turn a (timestamp, tzoff) tuple into an hg cset timestamp.''' + return "%d %d" % date + def nl2br(text): '''replace raw newlines with xhtml line breaks.''' return text.replace('\n', '
\n') @@ -282,6 +286,7 @@ common_filters = { "fill76": lambda x: fill(x, width=76), "firstline": lambda x: x.splitlines(1)[0].rstrip('\r\n'), "tabindent": lambda x: indent(x, '\t'), + "hgdate": hgdate, "isodate": isodate, "obfuscate": obfuscate, "permissions": lambda x: x and "-rwxr-xr-x" or "-rw-r--r--", diff --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -859,6 +859,49 @@ def datestr(date=None, format='%a %b %d s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60)) return s +def strdate(string, format='%a %b %d %H:%M:%S %Y'): + """parse a localized time string and return a (unixtime, offset) tuple. + if the string cannot be parsed, ValueError is raised.""" + def hastimezone(string): + return (string[-4:].isdigit() and + (string[-5] == '+' or string[-5] == '-') and + string[-6].isspace()) + + if hastimezone(string): + date, tz = string.rsplit(None, 1) + tz = int(tz) + offset = - 3600 * (tz / 100) - 60 * (tz % 100) + else: + date, offset = string, 0 + when = int(time.mktime(time.strptime(date, format))) + offset + return when, offset + +def parsedate(string, formats=('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M')): + """parse a localized time string and return a (unixtime, offset) tuple. + The date may be a "unixtime offset" string or in one of the specified + formats.""" + try: + when, offset = map(int, string.split(' ')) + except ValueError: + for format in formats: + try: + when, offset = strdate(string, format) + except ValueError: + pass + else: + break + else: + raise ValueError(_('invalid date: %r') % string) + # validate explicit (probably user-specified) date and + # time zone offset. values must fit in signed 32 bits for + # current 32-bit linux runtimes. timezones go from UTC-12 + # to UTC+14 + if abs(when) > 0x7fffffff: + raise ValueError(_('date exceeds 32 bits: %d') % when) + if offset < -50400 or offset > 43200: + raise ValueError(_('impossible time zone offset: %d') % offset) + return when, offset + def shortuser(user): """Return a short representation of a user name or email address.""" f = user.find('@') diff --git a/templates/changeset-raw.tmpl b/templates/changeset-raw.tmpl --- a/templates/changeset-raw.tmpl +++ b/templates/changeset-raw.tmpl @@ -1,7 +1,7 @@ #header# # HG changeset patch # User #author# -# Date #date|date# +# Date #date|hgdate# # Node ID #node# #parent%changesetparent# #desc# diff --git a/templates/map-raw b/templates/map-raw --- a/templates/map-raw +++ b/templates/map-raw @@ -5,8 +5,8 @@ difflineplus = '#line#' difflineminus = '#line#' difflineat = '#line#' diffline = '#line#' -changesetparent = '# parent: #node#' -changesetchild = '# child: #node#' +changesetparent = '# Parent #node#' +changesetchild = '# Child #node#' filenodelink = '' filerevision = 'Content-Type: #mimetype#\nContent-Disposition: filename=#file#\n\n#raw#' fileline = '#line#' diff --git a/tests/test-import b/tests/test-import new file mode 100755 --- /dev/null +++ b/tests/test-import @@ -0,0 +1,81 @@ +#!/bin/sh + +hg init a +echo line 1 > a/a +hg --cwd a ci -d '0 0' -Ama + +echo line 2 >> a/a +hg --cwd a ci -u someone -d '1 0' -m'second change' + +echo % import exported patch +hg clone -r0 a b +hg --cwd a export tip > tip.patch +hg --cwd b import ../tip.patch +echo % message should be same +hg --cwd b tip | grep 'second change' +echo % committer should be same +hg --cwd b tip | grep someone +rm -rf b + +echo % import of plain diff should fail without message +hg clone -r0 a b +hg --cwd a diff -r0:1 > tip.patch +hg --cwd b import ../tip.patch +rm -rf b + +echo % import of plain diff should be ok with message +hg clone -r0 a b +hg --cwd a diff -r0:1 > tip.patch +hg --cwd b import -mpatch ../tip.patch +rm -rf b + +echo % import from stdin +hg clone -r0 a b +hg --cwd a export tip | hg --cwd b import - +rm -rf b + +echo % override commit message +hg clone -r0 a b +hg --cwd a export tip | hg --cwd b import -m 'override' - +hg --cwd b tip | grep override +rm -rf b + +cat > mkmsg.py < tip.patch +python mkmsg.py > msg.patch +hg --cwd b import ../msg.patch +hg --cwd b tip | grep email +rm -rf b + +echo % plain diff in email, no subject, message body +hg clone -r0 a b +grep -v '^Subject:' msg.patch | hg --cwd b import - +rm -rf b + +echo % plain diff in email, subject, no message body +hg clone -r0 a b +grep -v '^email ' msg.patch | hg --cwd b import - +rm -rf b + +echo % plain diff in email, no subject, no message body, should fail +hg clone -r0 a b +grep -v '^\(Subject\|email\)' msg.patch | hg --cwd b import - +rm -rf b + +echo % hg export in email, should use patch header +hg clone -r0 a b +hg --cwd a export tip > tip.patch +python mkmsg.py | hg --cwd b import - +hg --cwd b tip | grep second +rm -rf b + diff --git a/tests/test-import.out b/tests/test-import.out new file mode 100644 --- /dev/null +++ b/tests/test-import.out @@ -0,0 +1,103 @@ +adding a +% import exported patch +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying ../tip.patch +patching file a +% message should be same +summary: second change +% committer should be same +user: someone +% import of plain diff should fail without message +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying ../tip.patch +patching file a +transaction abort! +rollback completed +% import of plain diff should be ok with message +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying ../tip.patch +patching file a +% import from stdin +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying patch from stdin +patching file a +% override commit message +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying patch from stdin +patching file a +summary: override +% plain diff in email, subject, message body +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying ../msg.patch +patching file a +user: email patcher +summary: email patch +% plain diff in email, no subject, message body +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying patch from stdin +patching file a +% plain diff in email, subject, no message body +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying patch from stdin +patching file a +% plain diff in email, no subject, no message body, should fail +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying patch from stdin +patching file a +transaction abort! +rollback completed +% hg export in email, should use patch header +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying patch from stdin +patching file a +summary: second change diff --git a/tests/test-incoming-outgoing b/tests/test-incoming-outgoing --- a/tests/test-incoming-outgoing +++ b/tests/test-incoming-outgoing @@ -14,8 +14,10 @@ cd .. hg init new # http incoming http_proxy= hg -R new incoming http://localhost:20059/ +http_proxy= hg -R new incoming -r 4 http://localhost:20059/ # local incoming hg -R new incoming test +hg -R new incoming -r 4 test # test with --bundle http_proxy= hg -R new incoming --bundle test.hg http://localhost:20059/ @@ -42,5 +44,6 @@ hg verify cd .. hg -R test-dev outgoing test http_proxy= hg -R test-dev outgoing http://localhost:20059/ +http_proxy= hg -R test-dev outgoing -r 11 http://localhost:20059/ kill `cat test/hg.pid` diff --git a/tests/test-incoming-outgoing.out b/tests/test-incoming-outgoing.out --- a/tests/test-incoming-outgoing.out +++ b/tests/test-incoming-outgoing.out @@ -75,6 +75,31 @@ user: test date: Mon Jan 12 13:46:40 1970 +0000 summary: 4 +changeset: 0:9cb21d99fe27 +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 0 + +changeset: 1:d717f5dfad6a +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 1 + +changeset: 2:c0d6b86da426 +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 2 + +changeset: 3:dfacbd43b3fe +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 3 + +changeset: 4:1f3a964b6022 +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 4 + changeset: 5:c028bcc7a28a user: test date: Mon Jan 12 13:46:40 1970 +0000 @@ -121,6 +146,31 @@ user: test date: Mon Jan 12 13:46:40 1970 +0000 summary: 4 +changeset: 0:9cb21d99fe27 +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 0 + +changeset: 1:d717f5dfad6a +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 1 + +changeset: 2:c0d6b86da426 +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 2 + +changeset: 3:dfacbd43b3fe +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 3 + +changeset: 4:1f3a964b6022 +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 4 + changeset: 5:c028bcc7a28a user: test date: Mon Jan 12 13:46:40 1970 +0000 @@ -270,3 +320,19 @@ user: test date: Mon Jan 12 13:46:40 1970 +0000 summary: 13 +searching for changes +changeset: 9:3741c3ad1096 +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 9 + +changeset: 10:de4143c8d9a5 +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 10 + +changeset: 11:0e1c188b9a7a +user: test +date: Mon Jan 12 13:46:40 1970 +0000 +summary: 11 + diff --git a/tests/test-parse-date b/tests/test-parse-date new file mode 100755 --- /dev/null +++ b/tests/test-parse-date @@ -0,0 +1,16 @@ +#!/bin/sh + +hg init +echo "test-parse-date" > a +hg add a +hg ci -d "2006-02-01 13:00:30" -m "rev 0" +echo "hi!" >> a +hg ci -d "2006-02-01 13:00:30 -0500" -m "rev 1" +hg tag -d "2006-04-15 13:30" "Hi" +hg backout --merge -d "2006-04-15 13:30 +0200" -m "rev 3" 1 +hg ci -d "1150000000 14400" -m "rev 4 (merge)" +echo "fail" >> a +hg ci -d "should fail" -m "fail" +hg ci -d "100000000000000000 1400" -m "fail" +hg ci -d "100000 1400000" -m "fail" +hg log --template '{date|date}\n' diff --git a/tests/test-parse-date.out b/tests/test-parse-date.out new file mode 100644 --- /dev/null +++ b/tests/test-parse-date.out @@ -0,0 +1,19 @@ +reverting a +changeset 3:107ce1ee2b43 backs out changeset 1:25a1420a55f8 +merging with changeset 2:99a1acecff55 +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +abort: invalid date: 'should fail' +transaction abort! +rollback completed +abort: date exceeds 32 bits: 100000000000000000 +transaction abort! +rollback completed +abort: impossible time zone offset: 1400000 +transaction abort! +rollback completed +Sun Jun 11 00:26:40 2006 -0400 +Sat Apr 15 13:30:00 2006 +0200 +Sat Apr 15 13:30:00 2006 +0000 +Wed Feb 01 13:00:30 2006 -0500 +Wed Feb 01 13:00:30 2006 +0000