changeset 2471:6904e1ef8ad1

merge with crew.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Tue, 20 Jun 2006 23:58:45 -0700
parents fe1689273f84 (current diff) 2e91ba371c4c (diff)
children 7a77934ece46
files mercurial/localrepo.py mercurial/util.py
diffstat 10 files changed, 270 insertions(+), 63 deletions(-) [+]
line wrap: on
line diff
--- a/doc/hgrc.5.txt
+++ b/doc/hgrc.5.txt
@@ -381,6 +381,14 @@ web::
     Default is false.
   allowpull;;
     Whether to allow pulling from the repository. Default is true.
+  allow_push;;
+    Whether to allow pushing to the repository.  If empty or not set,
+    push is not allowed.  If the special value "*", any remote user
+    can push, including unauthenticated users.  Otherwise, the remote
+    user must have been authenticated, and the authenticated user name
+    must be present in this list (separated by whitespace or ",").
+    The contents of the allow_push list are examined after the
+    deny_push list.
   allowzip;;
     (DEPRECATED) Whether to allow .zip downloading of repo revisions.
     Default is false. This feature creates temporary files.
@@ -391,6 +399,13 @@ web::
   contact;;
     Name or email address of the person in charge of the repository.
     Default is "unknown".
+  deny_push;;
+    Whether to deny pushing to the repository.  If empty or not set,
+    push is not denied.  If the special value "*", all remote users
+    are denied push.  Otherwise, unauthenticated users are all denied,
+    and any authenticated user name present in this list (separated by
+    whitespace or ",") is also denied.  The contents of the deny_push
+    list are examined before the allow_push list.
   description;;
     Textual description of the repository's purpose or contents.
     Default is "unknown".
@@ -407,6 +422,9 @@ web::
     Maximum number of files to list per changeset. Default is 10.
   port;;
     Port to listen on. Default is 8000.
+  push_ssl;;
+    Whether to require that inbound pushes be transported over SSL to
+    prevent password sniffing.  Default is true.
   style;;
     Which template map style to use.
   templates;;
--- a/mercurial/bdiff.c
+++ b/mercurial/bdiff.c
@@ -10,6 +10,7 @@
 */
 
 #include <Python.h>
+#include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
 
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -379,11 +379,20 @@ def dodiff(fp, ui, repo, node1, node2, f
     if node2:
         change = repo.changelog.read(node2)
         mmap2 = repo.manifest.read(change[0])
-        date2 = util.datestr(change[2])
+        _date2 = util.datestr(change[2])
+        def date2(f):
+            return _date2
         def read(f):
             return repo.file(f).read(mmap2[f])
     else:
-        date2 = util.datestr()
+        tz = util.makedate()[1]
+        _date2 = util.datestr()
+        def date2(f):
+            try:
+                return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
+            except IOError, err:
+                if err.errno != errno.ENOENT: raise
+                return _date2
         def read(f):
             return repo.wread(f)
 
@@ -401,17 +410,17 @@ def dodiff(fp, ui, repo, node1, node2, f
         if f in mmap:
             to = repo.file(f).read(mmap[f])
         tn = read(f)
-        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
+        fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, text=text,
                                showfunc=showfunc, ignorews=ignorews))
     for f in added:
         to = None
         tn = read(f)
-        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
+        fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, text=text,
                                showfunc=showfunc, ignorews=ignorews))
     for f in removed:
         to = repo.file(f).read(mmap[f])
         tn = None
-        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
+        fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, text=text,
                                showfunc=showfunc, ignorews=ignorews))
 
 def trimuser(ui, name, rev, revcache):
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -11,34 +11,60 @@ from demandload import *
 from i18n import gettext as _
 demandload(globals(), "localrepo bundlerepo httprepo sshrepo statichttprepo")
 
+def bundle(ui, path):
+    if path.startswith('bundle://'):
+        path = path[9:]
+    else:
+        path = path[7:]
+    s = path.split("+", 1)
+    if len(s) == 1:
+        repopath, bundlename = "", s[0]
+    else:
+        repopath, bundlename = s
+    return bundlerepo.bundlerepository(ui, repopath, bundlename)
+
+def hg(ui, path):
+    ui.warn(_("hg:// syntax is deprecated, please use http:// instead\n"))
+    return httprepo.httprepository(ui, path.replace("hg://", "http://"))
+
+def local_(ui, path, create=0):
+    return localrepo.localrepository(ui, path, create)
+
+def old_http(ui, path):
+    ui.warn(_("old-http:// syntax is deprecated, "
+              "please use static-http:// instead\n"))
+    return statichttprepo.statichttprepository(
+        ui, path.replace("old-http://", "http://"))
+
+def static_http(ui, path):
+    return statichttprepo.statichttprepository(
+        ui, path.replace("static-http://", "http://"))
+
+protocols = {
+    'bundle': bundle,
+    'file': local_,
+    'hg': hg,
+    'http': lambda ui, path: httprepo.httprepository(ui, path),
+    'https': lambda ui, path: httprepo.httpsrepository(ui, path),
+    'old-http': old_http,
+    'ssh': lambda ui, path: sshrepo.sshrepository(ui, path),
+    'static-http': static_http,
+    None: local_,
+    }
+
 def repository(ui, path=None, create=0):
-    if path:
-        if path.startswith("http://"):
-            return httprepo.httprepository(ui, path)
-        if path.startswith("https://"):
-            return httprepo.httpsrepository(ui, path)
-        if path.startswith("hg://"):
-            ui.warn(_("hg:// syntax is deprecated, "
-                      "please use http:// instead\n"))
-            return httprepo.httprepository(
-                ui, path.replace("hg://", "http://"))
-        if path.startswith("old-http://"):
-            ui.warn(_("old-http:// syntax is deprecated, "
-                      "please use static-http:// instead\n"))
-            return statichttprepo.statichttprepository(
-                ui, path.replace("old-http://", "http://"))
-        if path.startswith("static-http://"):
-            return statichttprepo.statichttprepository(
-                ui, path.replace("static-http://", "http://"))
-        if path.startswith("ssh://"):
-            return sshrepo.sshrepository(ui, path)
-        if path.startswith("bundle://"):
-            path = path[9:]
-            s = path.split("+", 1)
-            if  len(s) == 1:
-                repopath, bundlename = "", s[0]
-            else:
-                repopath, bundlename = s
-            return bundlerepo.bundlerepository(ui, repopath, bundlename)
-
-    return localrepo.localrepository(ui, path, create)
+    scheme = path
+    if scheme:
+        c = scheme.find(':')
+        scheme = c >= 0 and scheme[:c]
+    if not scheme: scheme = None
+    try:
+        ctor = protocols[scheme]
+        if create:
+            return ctor(ui, path, create)
+        return ctor(ui, path)
+    except KeyError:
+        raise util.Abort(_('protocol "%s" not known') % scheme)
+    except TypeError:
+        raise util.Abort(_('cannot create new repository over "%s" protocol') %
+                         (scheme or 'file'))
--- a/mercurial/hgweb/hgweb_mod.py
+++ b/mercurial/hgweb/hgweb_mod.py
@@ -10,7 +10,7 @@ import os
 import os.path
 import mimetypes
 from mercurial.demandload import demandload
-demandload(globals(), "re zlib ConfigParser cStringIO")
+demandload(globals(), "re zlib ConfigParser 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")
@@ -835,7 +835,97 @@ class hgweb(object):
                   or self.t("error", error="%r not found" % fname))
 
     def do_capabilities(self, req):
-        resp = ''
+        resp = 'unbundle'
         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.
+        default is policy to use if no config given.'''
+
+        user = req.env.get('REMOTE_USER')
+
+        deny = self.repo.ui.config('web', 'deny_' + op, '')
+        deny = deny.replace(',', ' ').split()
+
+        if deny and (not user or deny == ['*'] or user in deny):
+            return False
+
+        allow = self.repo.ui.config('web', 'allow_' + op, '')
+        allow = allow.replace(',', ' ').split()
+
+        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.repo.ui.configbool('web', 'push_ssl', True)
+        if ssl_req and not req.env.get('HTTPS'):
+            bail(_('ssl required\n'))
+            return
+
+        # 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
+
+        req.httphdr("application/mercurial-0.1")
+
+        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
+
+        # 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)
+
+            lock = self.repo.lock()
+            try:
+                if not check_heads():
+                    req.write('0\n')
+                    req.write(_('unsynced changes\n'))
+                    return
+
+                fp.seek(0)
+
+                # send addchangegroup output to client
+
+                old_stdout = sys.stdout
+                sys.stdout = cStringIO.StringIO()
+
+                try:
+                    ret = self.repo.addchangegroup(fp, 'serve')
+                    req.write('%d\n' % ret)
+                    req.write(sys.stdout.getvalue())
+                finally:
+                    sys.stdout = old_stdout
+            finally:
+                lock.release()
+        finally:
+            fp.close()
+            os.unlink(tempname)
--- a/mercurial/hgweb/request.py
+++ b/mercurial/hgweb/request.py
@@ -18,6 +18,9 @@ class hgrequest(object):
         self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
         self.will_close = True
 
+    def read(self, count=-1):
+        return self.inp.read(count)
+
     def write(self, *things):
         for thing in things:
             if hasattr(thing, "__iter__"):
@@ -42,9 +45,9 @@ class hgrequest(object):
             self.out.write("%s: %s\r\n" % header)
         self.out.write("\r\n")
 
-    def httphdr(self, type, filename=None, length=0):
-
-        headers = [('Content-type', type)]
+    def httphdr(self, type, filename=None, length=0, headers={}):
+        headers = headers.items()
+        headers.append(('Content-type', type))
         if filename:
             headers.append(('Content-disposition', 'attachment; filename=%s' %
                             filename))
--- a/mercurial/httprepo.py
+++ b/mercurial/httprepo.py
@@ -10,7 +10,7 @@ from remoterepo import *
 from i18n import gettext as _
 from demandload import *
 demandload(globals(), "hg os urllib urllib2 urlparse zlib util httplib")
-demandload(globals(), "keepalive")
+demandload(globals(), "errno keepalive tempfile socket")
 
 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
     def __init__(self, ui):
@@ -69,6 +69,22 @@ def netlocunsplit(host, port, user=None,
         return userpass + '@' + hostport
     return hostport
 
+class httpconnection(keepalive.HTTPConnection):
+    # must be able to send big bundle as stream.
+
+    def send(self, data):
+        if isinstance(data, str):
+            keepalive.HTTPConnection.send(self, data)
+        else:
+            # if auth required, some data sent twice, so rewind here
+            data.seek(0)
+            for chunk in util.filechunkiter(data):
+                keepalive.HTTPConnection.send(self, chunk)
+
+class httphandler(keepalive.HTTPHandler):
+    def http_open(self, req):
+        return self.do_open(httpconnection, req)
+
 class httprepository(remoterepository):
     def __init__(self, ui, path):
         self.caps = None
@@ -86,7 +102,7 @@ class httprepository(remoterepository):
 
         proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
         proxyauthinfo = None
-        handler = keepalive.HTTPHandler()
+        handler = httphandler()
 
         if proxyurl:
             # proxy can be proper url or host[:port]
@@ -154,6 +170,8 @@ class httprepository(remoterepository):
                 self.caps = self.do_read('capabilities').split()
             except hg.RepoError:
                 self.caps = ()
+            self.ui.debug(_('capabilities: %s\n') %
+                          (' '.join(self.caps or ['none'])))
         return self.caps
 
     capabilities = property(get_caps)
@@ -165,13 +183,19 @@ class httprepository(remoterepository):
         raise util.Abort(_('operation not supported over http'))
 
     def do_cmd(self, cmd, **args):
+        data = args.pop('data', None)
+        headers = args.pop('headers', {})
         self.ui.debug(_("sending %s command\n") % cmd)
         q = {"cmd": cmd}
         q.update(args)
         qs = urllib.urlencode(q)
         cu = "%s?%s" % (self.url, qs)
         try:
-            resp = urllib2.urlopen(cu)
+            resp = urllib2.urlopen(urllib2.Request(cu, data, headers))
+        except urllib2.HTTPError, inst:
+            if inst.code == 401:
+                raise util.Abort(_('authorization failed'))
+            raise
         except httplib.HTTPException, inst:
             self.ui.debug(_('http error while sending %s command\n') % cmd)
             self.ui.print_exc()
@@ -249,7 +273,34 @@ class httprepository(remoterepository):
         return util.chunkbuffer(zgenerator(util.filechunkiter(f)))
 
     def unbundle(self, cg, heads, source):
-        raise util.Abort(_('operation not supported over http'))
+        # have to stream bundle to a temp file because we do not have
+        # http 1.1 chunked transfer.
+
+        fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
+        fp = os.fdopen(fd, 'wb+')
+        try:
+            for chunk in util.filechunkiter(cg):
+                fp.write(chunk)
+            length = fp.tell()
+            try:
+                rfp = self.do_cmd(
+                    'unbundle', data=fp,
+                    headers={'content-length': length,
+                             'content-type': 'application/octet-stream'},
+                    heads=' '.join(map(hex, heads)))
+                try:
+                    ret = int(rfp.readline())
+                    self.ui.write(rfp.read())
+                    return ret
+                finally:
+                    rfp.close()
+            except socket.error, err:
+                if err[0] in (errno.ECONNRESET, errno.EPIPE):
+                    raise util.Abort(_('push failed: %s'), err[1])
+                raise util.Abort(err[1])
+        finally:
+            fp.close()
+            os.unlink(tempname)
 
 class httpsrepository(httprepository):
     pass
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -1115,9 +1115,8 @@ class localrepository(object):
         # servers, http servers).
 
         if 'unbundle' in remote.capabilities:
-            self.push_unbundle(remote, force, revs)
-        else:
-            self.push_addchangegroup(remote, force, revs)
+            return self.push_unbundle(remote, force, revs)
+        return self.push_addchangegroup(remote, force, revs)
 
     def prepush(self, remote, force, revs):
         base = {}
--- a/mercurial/mpatch.c
+++ b/mercurial/mpatch.c
@@ -23,13 +23,15 @@
 #include <Python.h>
 #include <stdlib.h>
 #include <string.h>
+
 #ifdef _WIN32
-#ifdef _MSC_VER
-#define inline __inline
+# ifdef _MSC_VER
+/* msvc 6.0 has problems */
+#  define inline __inline
 typedef unsigned long uint32_t;
-#else
-#include <stdint.h>
-#endif
+# else
+#  include <stdint.h>
+# endif
 static uint32_t ntohl(uint32_t x)
 {
 	return ((x & 0x000000ffUL) << 24) |
@@ -38,8 +40,10 @@ static uint32_t ntohl(uint32_t x)
 		((x & 0xff000000UL) >> 24);
 }
 #else
-#include <sys/types.h>
-#include <arpa/inet.h>
+/* not windows */
+# include <sys/types.h>
+# include <arpa/inet.h>
+# include <stdint.h>
 #endif
 
 static char mpatch_doc[] = "Efficient binary patching.";
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -821,16 +821,22 @@ class chunkbuffer(object):
         s, self.buf = self.buf[:l], buffer(self.buf, l)
         return s
 
-def filechunkiter(f, size = 65536):
-    """Create a generator that produces all the data in the file size
-    (default 65536) bytes at a time.  Chunks may be less than size
-    bytes if the chunk is the last chunk in the file, or the file is a
-    socket or some other type of file that sometimes reads less data
-    than is requested."""
-    s = f.read(size)
-    while len(s) > 0:
+def filechunkiter(f, size=65536, limit=None):
+    """Create a generator that produces the data in the file size
+    (default 65536) bytes at a time, up to optional limit (default is
+    to read all data).  Chunks may be less than size bytes if the
+    chunk is the last chunk in the file, or the file is a socket or
+    some other type of file that sometimes reads less data than is
+    requested."""
+    assert size >= 0
+    assert limit is None or limit >= 0
+    while True:
+        if limit is None: nbytes = size
+        else: nbytes = min(limit, size)
+        s = nbytes and f.read(nbytes)
+        if not s: break
+        if limit: limit -= len(s)
         yield s
-        s = f.read(size)
 
 def makedate():
     lt = time.localtime()