# HG changeset patch # User Vadim Gelfer # Date 1150873125 25200 # Node ID 6904e1ef8ad16202c03e488eb8faa59eaabe2a93 # Parent fe1689273f84d9ab7dc51cce677ee72b4f540f7a# Parent 2e91ba371c4c469d44b85b4f4a24b8ee841fd2f1 merge with crew. diff --git a/doc/hgrc.5.txt b/doc/hgrc.5.txt --- 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;; diff --git a/mercurial/bdiff.c b/mercurial/bdiff.c --- a/mercurial/bdiff.c +++ b/mercurial/bdiff.c @@ -10,6 +10,7 @@ */ #include +#include #include #include diff --git a/mercurial/commands.py b/mercurial/commands.py --- 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): diff --git a/mercurial/hg.py b/mercurial/hg.py --- 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')) 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,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) diff --git a/mercurial/hgweb/request.py b/mercurial/hgweb/request.py --- 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)) diff --git a/mercurial/httprepo.py b/mercurial/httprepo.py --- 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 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- 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 = {} diff --git a/mercurial/mpatch.c b/mercurial/mpatch.c --- a/mercurial/mpatch.c +++ b/mercurial/mpatch.c @@ -23,13 +23,15 @@ #include #include #include + #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 -#endif +# else +# include +# 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 -#include +/* not windows */ +# include +# include +# include #endif static char mpatch_doc[] = "Efficient binary patching."; diff --git a/mercurial/util.py b/mercurial/util.py --- 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()