# HG changeset patch # User Vadim Gelfer # Date 1148054232 25200 # Node ID 7cbe8cd69d6b0a381510875d57c37f05dc91babf # Parent c58a403aa830b798564e4ad88eec8236000301f7# Parent d9ca698e3c5a051075cce3aa84e1308939c13f4b merge with crew. diff -r c58a403aa830 -r 7cbe8cd69d6b contrib/mercurial.el --- a/contrib/mercurial.el Fri May 19 08:54:28 2006 -0700 +++ b/contrib/mercurial.el Fri May 19 08:57:12 2006 -0700 @@ -382,14 +382,27 @@ (set-buffer hg-prev-buffer)) (let ((path (or default (buffer-file-name)))) (if (or (not path) current-prefix-arg) - (expand-file-name - (read-file-name (format "File, directory or pattern%s: " - (or prompt "")) - (and path (file-name-directory path)) - nil nil - (and path (file-name-nondirectory path)) - 'hg-file-history)) - path)))) + (expand-file-name + (eval (list* 'read-file-name + (format "File, directory or pattern%s: " + (or prompt "")) + (and path (file-name-directory path)) + nil nil + (and path (file-name-nondirectory path)) + (if hg-running-xemacs + (cons (quote 'hg-file-history) nil) + nil)))) + path)))) + +(defun hg-read-number (&optional prompt default) + "Read a integer value." + (save-excursion + (if (or (not default) current-prefix-arg) + (string-to-number + (eval (list* 'read-string + (or prompt "") + (if default (cons (format "%d" default) nil) nil)))) + default))) (defun hg-read-config () "Return an alist of (key . value) pairs of Mercurial config data. @@ -950,36 +963,55 @@ (kill-entire-line)) (run-hooks 'hg-log-mode-hook)) -(defun hg-log (path &optional rev1 rev2) - "Display the revision history of PATH, between REV1 and REV2. -REV1 defaults to hg-log-limit changes from the tip revision, while -REV2 defaults to the tip. +(defun hg-log (path &optional rev1 rev2 log-limit) + "Display the revision history of PATH. +History is displayed between REV1 and REV2. +Number of displayed changesets is limited to LOG-LIMIT. +REV1 defaults to the tip, while +REV2 defaults to `hg-rev-completion-limit' changes from the tip revision. +LOG-LIMIT defaults to `hg-log-limit'. With a prefix argument, prompt for each parameter." (interactive (list (hg-read-file-name " to log") - (hg-read-rev " to start with" "-1") - (hg-read-rev " to end with" (format "-%d" hg-log-limit)))) + (hg-read-rev " to start with" + "tip") + (hg-read-rev " to end with" + (format "%d" (- hg-rev-completion-limit))) + (hg-read-number "Output limited to: " + hg-log-limit))) (let ((a-path (hg-abbrev-file-name path)) - (r1 (or rev1 (format "-%d" hg-log-limit))) - (r2 (or rev2 rev1 "-1"))) + (r1 (or rev1 (format "-%d" hg-rev-completion-limit))) + (r2 (or rev2 rev1 "tip")) + (limit (format "%d" (or log-limit hg-log-limit)))) (hg-view-output ((if (equal r1 r2) - (format "Mercurial: Log of rev %s of %s" rev1 a-path) - (format "Mercurial: Log from rev %s to %s of %s" - r1 r2 a-path))) - (let ((revs (format "%s:%s" r1 r2))) - (if (> (length path) (length (hg-root path))) - (call-process (hg-binary) nil t nil "log" "-r" revs path) - (call-process (hg-binary) nil t nil "log" "-r" revs))) + (format "Mercurial: Log of rev %s of %s" rev1 a-path) + (format + "Mercurial: at most %s log(s) from rev %s to %s of %s" + limit r1 r2 a-path))) + (eval (list* 'call-process (hg-binary) nil t nil + "log" + "-r" (format "%s:%s" r1 r2) + "-l" limit + (if (> (length path) (length (hg-root path))) + (cons path nil) + nil))) (hg-log-mode)))) -(defun hg-log-repo (path &optional rev1 rev2) +(defun hg-log-repo (path &optional rev1 rev2 log-limit) "Display the revision history of the repository containing PATH. -History is displayed between REV1, which defaults to the tip, and -REV2, which defaults to the initial revision. -Variable hg-log-limit controls the number of log entries displayed." +History is displayed between REV1 and REV2. +Number of displayed changesets is limited to LOG-LIMIT, +REV1 defaults to the tip, while +REV2 defaults to `hg-rev-completion-limit' changes from the tip revision. +LOG-LIMIT defaults to `hg-log-limit'. +With a prefix argument, prompt for each parameter." (interactive (list (hg-read-file-name " to log") - (hg-read-rev " to start with" "tip") - (hg-read-rev " to end with" (format "-%d" hg-log-limit)))) - (hg-log (hg-root path) rev1 rev2)) + (hg-read-rev " to start with" + "tip") + (hg-read-rev " to end with" + (format "%d" (- hg-rev-completion-limit))) + (hg-read-number "Output limited to: " + hg-log-limit))) + (hg-log (hg-root path) rev1 rev2 log-limit)) (defun hg-outgoing (&optional repo) "Display changesets present locally that are not present in REPO." diff -r c58a403aa830 -r 7cbe8cd69d6b contrib/win32/ReadMe.html --- a/contrib/win32/ReadMe.html Fri May 19 08:54:28 2006 -0700 +++ b/contrib/win32/ReadMe.html Fri May 19 08:57:12 2006 -0700 @@ -89,6 +89,16 @@

This command should print a useful help message. If it does, other Mercurial commands should work fine for you.

+

Configuration notes

+

The default editor for commit messages is 'vi'. You can set the EDITOR + (or HGEDITOR) environment variable to specify your preference or set it in + mercurial.ini:

+
+[ui]
+editor = whatever
+
+ +

Reporting problems

Before you report any problems, please consult the /.hg/hgrc:: Per-repository configuration options that only apply in a diff -r c58a403aa830 -r 7cbe8cd69d6b hgext/bugzilla.py --- a/hgext/bugzilla.py Fri May 19 08:54:28 2006 -0700 +++ b/hgext/bugzilla.py Fri May 19 08:57:12 2006 -0700 @@ -22,13 +22,16 @@ # # config items: # +# section name is 'bugzilla'. +# [bugzilla] +# # REQUIRED: # host = bugzilla # mysql server where bugzilla database lives # password = ** # user's password # version = 2.16 # version of bugzilla installed # # OPTIONAL: -# bzuser = ... # bugzilla user id to record comments with +# bzuser = ... # fallback bugzilla user name to record comments with # db = bugs # database to connect to # notify = ... # command to run to get bugzilla to send mail # regexp = ... # regexp to match bug ids (must contain one "()" group) @@ -39,6 +42,15 @@ # user = bugs # user to connect to database as # [web] # baseurl = http://hgserver/... # root of hg web site for browsing commits +# +# if hg committer names are not same as bugzilla user names, use +# "usermap" feature to map from committer email to bugzilla user name. +# usermap can be in hgrc or separate config file. +# +# [bugzilla] +# usermap = filename # cfg file with "committer"="bugzilla user" info +# [usermap] +# committer_email = bugzilla_user_name from mercurial.demandload import * from mercurial.i18n import gettext as _ @@ -60,6 +72,9 @@ passwd = self.ui.config('bugzilla', 'password') db = self.ui.config('bugzilla', 'db', 'bugs') timeout = int(self.ui.config('bugzilla', 'timeout', 5)) + usermap = self.ui.config('bugzilla', 'usermap') + if usermap: + self.ui.readconfig(usermap) self.ui.note(_('connecting to %s:%s as %s, password %s\n') % (host, db, user, '*' * len(passwd))) self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd, @@ -139,18 +154,29 @@ self.user_ids[user] = userid return userid - def add_comment(self, bugid, text, prefuser): + def map_committer(self, user): + '''map name of committer to bugzilla user name.''' + for committer, bzuser in self.ui.configitems('usermap'): + if committer.lower() == user.lower(): + return bzuser + return user + + def add_comment(self, bugid, text, committer): '''add comment to bug. try adding comment as committer of changeset, otherwise as default bugzilla user.''' + user = self.map_committer(committer) try: - userid = self.get_user_id(prefuser) + userid = self.get_user_id(user) except KeyError: try: defaultuser = self.ui.config('bugzilla', 'bzuser') + if not defaultuser: + raise util.Abort(_('cannot find bugzilla user id for %s') % + user) userid = self.get_user_id(defaultuser) except KeyError: - raise util.Abort(_('cannot find user id for %s or %s') % - (prefuser, defaultuser)) + raise util.Abort(_('cannot find bugzilla user id for %s or %s') % + (user, defaultuser)) now = time.strftime('%Y-%m-%d %H:%M:%S') self.run('''insert into longdescs (bug_id, who, bug_when, thetext) diff -r c58a403aa830 -r 7cbe8cd69d6b hgext/hbisect.py --- a/hgext/hbisect.py Fri May 19 08:54:28 2006 -0700 +++ b/hgext/hbisect.py Fri May 19 08:57:12 2006 -0700 @@ -173,7 +173,7 @@ self.ui.warn("Could not find the first bad revision\n") sys.exit(1) self.ui.write( - "The first bad revision is : %s\n" % hg.hex(self.badrev)) + "The first bad revision is: %s\n" % hg.hex(self.badrev)) sys.exit(0) self.ui.write("%d revisions left\n" % tot) best_rev = None diff -r c58a403aa830 -r 7cbe8cd69d6b hgext/mq.py --- a/hgext/mq.py Fri May 19 08:54:28 2006 -0700 +++ b/hgext/mq.py Fri May 19 08:57:12 2006 -0700 @@ -102,6 +102,7 @@ message = [] comments = [] user = None + date = None format = None subject = None diffstart = 0 @@ -119,6 +120,8 @@ # parse values when importing the result of an hg export if line.startswith("# User "): user = line[7:] + elif line.startswith("# Date "): + date = line[7:] elif not line.startswith("# ") and line: message.append(line) format = None @@ -136,7 +139,7 @@ # when looking for tags (subject: from: etc) they # end once you find a blank line in the source format = "tagdone" - else: + elif message or line: message.append(line) comments.append(line) @@ -149,7 +152,7 @@ if format and format.startswith("tag") and subject: message.insert(0, "") message.insert(0, subject) - return (message, comments, user, diffstart > 1) + return (message, comments, user, date, diffstart > 1) def mergeone(self, repo, mergeq, head, patch, rev, wlock): # first try just applying the patch @@ -179,7 +182,7 @@ self.ui.warn("repo commit failed\n") sys.exit(1) try: - message, comments, user, patchfound = mergeq.readheaders(patch) + message, comments, user, date, patchfound = mergeq.readheaders(patch) except: self.ui.warn("Unable to read %s\n" % patch) sys.exit(1) @@ -267,7 +270,7 @@ pf = os.path.join(patchdir, patch) try: - message, comments, user, patchfound = self.readheaders(patch) + message, comments, user, date, patchfound = self.readheaders(patch) except: self.ui.warn("Unable to read %s\n" % pf) err = 1 @@ -326,7 +329,7 @@ if len(files) > 0: commands.addremove_lock(self.ui, repo, files, opts={}, wlock=wlock) - n = repo.commit(files, message, user, force=1, lock=lock, + n = repo.commit(files, message, user, date, force=1, lock=lock, wlock=wlock) if n == None: @@ -716,7 +719,7 @@ top = revlog.bin(top) cparents = repo.changelog.parents(top) patchparent = self.qparents(repo, top) - message, comments, user, patchfound = self.readheaders(patch) + message, comments, user, date, patchfound = self.readheaders(patch) patchf = self.opener(patch, "w") if comments: diff -r c58a403aa830 -r 7cbe8cd69d6b mercurial/commands.py --- a/mercurial/commands.py Fri May 19 08:54:28 2006 -0700 +++ b/mercurial/commands.py Fri May 19 08:57:12 2006 -0700 @@ -1392,6 +1392,7 @@ fp.write("# HG changeset patch\n") fp.write("# User %s\n" % change[1]) + fp.write("# Date %d %d\n" % change[2]) fp.write("# Node ID %s\n" % hex(node)) fp.write("# Parent %s\n" % hex(prev)) if len(parents) > 1: @@ -1687,6 +1688,7 @@ message = [] user = None + date = None hgpatch = False for line in file(pf): line = line.rstrip() @@ -1703,27 +1705,29 @@ 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 - else: + elif message or line: message.append(line) # make sure message isn't empty if not message: message = _("imported patch %s\n") % patch else: - message = "%s\n" % '\n'.join(message) + message = '\n'.join(message).rstrip() ui.debug(_('message:\n%s\n') % message) files = util.patch(strip, pf, ui) if len(files) > 0: addremove_lock(ui, repo, files, {}) - repo.commit(files, message, user) + repo.commit(files, message, user, date) def incoming(ui, repo, source="default", **opts): """show new changesets found in source @@ -2185,34 +2189,42 @@ entire project history. If the files still exist in the working directory, they will be deleted from it. If invoked with --after, files that have been manually deleted are marked as removed. + + Modified files and added files are not removed by default. To + remove them, use the -f/--force option. """ names = [] if not opts['after'] and not pats: raise util.Abort(_('no files specified')) - def okaytoremove(abs, rel, exact): - modified, added, removed, deleted, unknown = repo.changes(files=[abs]) + files, matchfn, anypats = matchpats(repo, pats, opts) + exact = dict.fromkeys(files) + mardu = map(dict.fromkeys, repo.changes(files=files, match=matchfn)) + modified, added, removed, deleted, unknown = mardu + remove, forget = [], [] + for src, abs, rel, exact in walk(repo, pats, opts): reason = None - if not deleted and opts['after']: + if abs not in deleted and opts['after']: reason = _('is still present') - elif modified and not opts['force']: - reason = _('is modified') - elif added: - reason = _('has been marked for add') - elif unknown: + elif abs in modified and not opts['force']: + reason = _('is modified (use -f to force removal)') + elif abs in added: + if opts['force']: + forget.append(abs) + continue + reason = _('has been marked for add (use -f to force removal)') + elif abs in unknown: reason = _('is not managed') - elif removed: - return False + elif abs in removed: + continue if reason: if exact: ui.warn(_('not removing %s: file %s\n') % (rel, reason)) else: - return True - for src, abs, rel, exact in walk(repo, pats, opts): - if okaytoremove(abs, rel, exact): if ui.verbose or not exact: ui.status(_('removing %s\n') % rel) - names.append(abs) - repo.remove(names, unlink=not opts['after']) + remove.append(abs) + repo.forget(forget) + repo.remove(remove, unlink=not opts['after']) def rename(ui, repo, *pats, **opts): """rename files; equivalent of copy + remove diff -r c58a403aa830 -r 7cbe8cd69d6b mercurial/demandload.py --- a/mercurial/demandload.py Fri May 19 08:54:28 2006 -0700 +++ b/mercurial/demandload.py Fri May 19 08:57:12 2006 -0700 @@ -81,6 +81,10 @@ return getattr(importer.module(), target) + def __call__(self, *args, **kwargs): + target = object.__getattribute__(self, 'module')() + return target(*args, **kwargs) + def demandload(scope, modules): '''import modules into scope when each is first used. diff -r c58a403aa830 -r 7cbe8cd69d6b mercurial/hgweb.py --- a/mercurial/hgweb.py Fri May 19 08:54:28 2006 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1142 +0,0 @@ -# hgweb.py - web interface to a mercurial repository -# -# Copyright 21 May 2005 - (c) 2005 Jake Edge -# Copyright 2005 Matt Mackall -# -# This software may be used and distributed according to the terms -# of the GNU General Public License, incorporated herein by reference. - -import os, cgi, sys -import mimetypes -from demandload import demandload -demandload(globals(), "mdiff time re socket zlib errno ui hg ConfigParser") -demandload(globals(), "tempfile StringIO BaseHTTPServer util SocketServer") -demandload(globals(), "archival mimetypes templater urllib") -from node import * -from i18n import gettext as _ - -def splitURI(uri): - """ Return path and query splited from uri - - Just like CGI environment, the path is unquoted, the query is - not. - """ - if '?' in uri: - path, query = uri.split('?', 1) - else: - path, query = uri, '' - return urllib.unquote(path), query - -def up(p): - if p[0] != "/": - p = "/" + p - if p[-1] == "/": - p = p[:-1] - up = os.path.dirname(p) - if up == "/": - return "/" - return up + "/" - -def get_mtime(repo_path): - hg_path = os.path.join(repo_path, ".hg") - cl_path = os.path.join(hg_path, "00changelog.i") - if os.path.exists(os.path.join(cl_path)): - return os.stat(cl_path).st_mtime - else: - return os.stat(hg_path).st_mtime - -def staticfile(directory, fname): - """return a file inside directory with guessed content-type header - - fname always uses '/' as directory separator and isn't allowed to - contain unusual path components. - Content-type is guessed using the mimetypes module. - Return an empty string if fname is illegal or file not found. - - """ - parts = fname.split('/') - path = directory - for part in parts: - if (part in ('', os.curdir, os.pardir) or - os.sep in part or os.altsep is not None and os.altsep in part): - return "" - path = os.path.join(path, part) - try: - os.stat(path) - ct = mimetypes.guess_type(path)[0] or "text/plain" - return "Content-type: %s\n\n%s" % (ct, file(path).read()) - except (TypeError, OSError): - # illegal fname or unreadable file - return "" - -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 - self.form = cgi.parse(self.inp, self.env, keep_blank_values=1) - - def write(self, *things): - for thing in things: - if hasattr(thing, "__iter__"): - for part in thing: - self.write(part) - else: - try: - self.out.write(str(thing)) - except socket.error, inst: - if inst[0] != errno.ECONNRESET: - raise - - 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") - - def httphdr(self, type, file="", size=0): - - headers = [('Content-type', type)] - if file: - headers.append(('Content-disposition', 'attachment; filename=%s' % file)) - if size > 0: - headers.append(('Content-length', str(size))) - self.header(headers) - -class hgweb(object): - def __init__(self, repo, name=None): - if type(repo) == type(""): - self.repo = hg.repository(ui.ui(), repo) - else: - self.repo = repo - - self.mtime = -1 - self.reponame = name - self.archives = 'zip', 'gz', 'bz2' - - def refresh(self): - mtime = get_mtime(self.repo.root) - if mtime != self.mtime: - self.mtime = mtime - self.repo = hg.repository(self.repo.ui, self.repo.root) - self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10)) - self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10)) - self.allowpull = self.repo.ui.configbool("web", "allowpull", True) - - def archivelist(self, nodeid): - for i in self.archives: - if self.repo.ui.configbool("web", "allow" + i, False): - yield {"type" : i, "node" : nodeid, "url": ""} - - def listfiles(self, files, mf): - for f in files[:self.maxfiles]: - yield self.t("filenodelink", node=hex(mf[f]), file=f) - if len(files) > self.maxfiles: - yield self.t("fileellipses") - - def listfilediffs(self, files, changeset): - for f in files[:self.maxfiles]: - yield self.t("filedifflink", node=hex(changeset), file=f) - if len(files) > self.maxfiles: - yield self.t("fileellipses") - - def siblings(self, siblings=[], rev=None, hiderev=None, **args): - if not rev: - rev = lambda x: "" - siblings = [s for s in siblings if s != nullid] - if len(siblings) == 1 and rev(siblings[0]) == hiderev: - return - for s in siblings: - yield dict(node=hex(s), rev=rev(s), **args) - - def renamelink(self, fl, node): - r = fl.renamed(node) - if r: - return [dict(file=r[0], node=hex(r[1]))] - return [] - - def showtag(self, t1, node=nullid, **args): - for t in self.repo.nodetags(node): - yield self.t(t1, tag=t, **args) - - def diff(self, node1, node2, files): - def filterfiles(filters, files): - l = [x for x in files if x in filters] - - for t in filters: - if t and t[-1] != os.sep: - t += os.sep - l += [x for x in files if x.startswith(t)] - return l - - parity = [0] - def diffblock(diff, f, fn): - yield self.t("diffblock", - lines=prettyprintlines(diff), - parity=parity[0], - file=f, - filenode=hex(fn or nullid)) - parity[0] = 1 - parity[0] - - def prettyprintlines(diff): - for l in diff.splitlines(1): - if l.startswith('+'): - yield self.t("difflineplus", line=l) - elif l.startswith('-'): - yield self.t("difflineminus", line=l) - elif l.startswith('@'): - yield self.t("difflineat", line=l) - else: - yield self.t("diffline", line=l) - - r = self.repo - cl = r.changelog - mf = r.manifest - change1 = cl.read(node1) - change2 = cl.read(node2) - mmap1 = mf.read(change1[0]) - mmap2 = mf.read(change2[0]) - date1 = util.datestr(change1[2]) - date2 = util.datestr(change2[2]) - - modified, added, removed, deleted, unknown = r.changes(node1, node2) - if files: - modified, added, removed = map(lambda x: filterfiles(files, x), - (modified, added, removed)) - - diffopts = self.repo.ui.diffopts() - showfunc = diffopts['showfunc'] - ignorews = diffopts['ignorews'] - for f in modified: - to = r.file(f).read(mmap1[f]) - tn = r.file(f).read(mmap2[f]) - yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, - showfunc=showfunc, ignorews=ignorews), f, tn) - for f in added: - to = None - tn = r.file(f).read(mmap2[f]) - yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, - showfunc=showfunc, ignorews=ignorews), f, tn) - for f in removed: - to = r.file(f).read(mmap1[f]) - tn = None - yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, - showfunc=showfunc, ignorews=ignorews), f, tn) - - def changelog(self, pos): - def changenav(**map): - def seq(factor, maxchanges=None): - if maxchanges: - yield maxchanges - if maxchanges >= 20 and maxchanges <= 40: - yield 50 - else: - yield 1 * factor - yield 3 * factor - for f in seq(factor * 10): - yield f - - l = [] - last = 0 - for f in seq(1, self.maxchanges): - if f < self.maxchanges or f <= last: - continue - if f > count: - break - last = f - r = "%d" % f - if pos + f < count: - l.append(("+" + r, pos + f)) - if pos - f >= 0: - l.insert(0, ("-" + r, pos - f)) - - yield {"rev": 0, "label": "(0)"} - - for label, rev in l: - yield {"label": label, "rev": rev} - - yield {"label": "tip", "rev": "tip"} - - def changelist(**map): - parity = (start - end) & 1 - cl = self.repo.changelog - l = [] # build a list in forward order for efficiency - for i in range(start, end): - n = cl.node(i) - changes = cl.read(n) - hn = hex(n) - - l.insert(0, {"parity": parity, - "author": changes[1], - "parent": self.siblings(cl.parents(n), cl.rev, - cl.rev(n) - 1), - "child": self.siblings(cl.children(n), cl.rev, - cl.rev(n) + 1), - "changelogtag": self.showtag("changelogtag",n), - "manifest": hex(changes[0]), - "desc": changes[4], - "date": changes[2], - "files": self.listfilediffs(changes[3], n), - "rev": i, - "node": hn}) - parity = 1 - parity - - for e in l: - yield e - - cl = self.repo.changelog - mf = cl.read(cl.tip())[0] - count = cl.count() - start = max(0, pos - self.maxchanges + 1) - end = min(count, start + self.maxchanges) - pos = end - 1 - - yield self.t('changelog', - changenav=changenav, - manifest=hex(mf), - rev=pos, changesets=count, entries=changelist, - archives=self.archivelist("tip")) - - def search(self, query): - - def changelist(**map): - cl = self.repo.changelog - count = 0 - qw = query.lower().split() - - def revgen(): - for i in range(cl.count() - 1, 0, -100): - l = [] - for j in range(max(0, i - 100), i): - n = cl.node(j) - changes = cl.read(n) - l.append((n, j, changes)) - l.reverse() - for e in l: - yield e - - for n, i, changes in revgen(): - miss = 0 - for q in qw: - if not (q in changes[1].lower() or - q in changes[4].lower() or - q in " ".join(changes[3][:20]).lower()): - miss = 1 - break - if miss: - continue - - count += 1 - hn = hex(n) - - yield self.t('searchentry', - parity=count & 1, - author=changes[1], - parent=self.siblings(cl.parents(n), cl.rev), - child=self.siblings(cl.children(n), cl.rev), - changelogtag=self.showtag("changelogtag",n), - manifest=hex(changes[0]), - desc=changes[4], - date=changes[2], - files=self.listfilediffs(changes[3], n), - rev=i, - node=hn) - - if count >= self.maxchanges: - break - - cl = self.repo.changelog - mf = cl.read(cl.tip())[0] - - yield self.t('search', - query=query, - manifest=hex(mf), - entries=changelist) - - def changeset(self, nodeid): - cl = self.repo.changelog - n = self.repo.lookup(nodeid) - nodeid = hex(n) - changes = cl.read(n) - p1 = cl.parents(n)[0] - - files = [] - mf = self.repo.manifest.read(changes[0]) - for f in changes[3]: - files.append(self.t("filenodelink", - filenode=hex(mf.get(f, nullid)), file=f)) - - def diff(**map): - yield self.diff(p1, n, None) - - yield self.t('changeset', - diff=diff, - rev=cl.rev(n), - node=nodeid, - parent=self.siblings(cl.parents(n), cl.rev), - child=self.siblings(cl.children(n), cl.rev), - changesettag=self.showtag("changesettag",n), - manifest=hex(changes[0]), - author=changes[1], - desc=changes[4], - date=changes[2], - files=files, - archives=self.archivelist(nodeid)) - - def filelog(self, f, filenode): - cl = self.repo.changelog - fl = self.repo.file(f) - filenode = hex(fl.lookup(filenode)) - count = fl.count() - - def entries(**map): - l = [] - parity = (count - 1) & 1 - - for i in range(count): - n = fl.node(i) - lr = fl.linkrev(n) - cn = cl.node(lr) - cs = cl.read(cl.node(lr)) - - l.insert(0, {"parity": parity, - "filenode": hex(n), - "filerev": i, - "file": f, - "node": hex(cn), - "author": cs[1], - "date": cs[2], - "rename": self.renamelink(fl, n), - "parent": self.siblings(fl.parents(n), - fl.rev, file=f), - "child": self.siblings(fl.children(n), - fl.rev, file=f), - "desc": cs[4]}) - parity = 1 - parity - - for e in l: - yield e - - yield self.t("filelog", file=f, filenode=filenode, entries=entries) - - def filerevision(self, f, node): - fl = self.repo.file(f) - n = fl.lookup(node) - node = hex(n) - text = fl.read(n) - changerev = fl.linkrev(n) - cl = self.repo.changelog - cn = cl.node(changerev) - cs = cl.read(cn) - mfn = cs[0] - - mt = mimetypes.guess_type(f)[0] - rawtext = text - if util.binary(text): - mt = mt or 'application/octet-stream' - text = "(binary:%s)" % mt - mt = mt or 'text/plain' - - def lines(): - for l, t in enumerate(text.splitlines(1)): - yield {"line": t, - "linenumber": "% 6d" % (l + 1), - "parity": l & 1} - - yield self.t("filerevision", - file=f, - filenode=node, - path=up(f), - text=lines(), - raw=rawtext, - mimetype=mt, - rev=changerev, - node=hex(cn), - manifest=hex(mfn), - author=cs[1], - date=cs[2], - parent=self.siblings(fl.parents(n), fl.rev, file=f), - child=self.siblings(fl.children(n), fl.rev, file=f), - rename=self.renamelink(fl, n), - permissions=self.repo.manifest.readflags(mfn)[f]) - - def fileannotate(self, f, node): - bcache = {} - ncache = {} - fl = self.repo.file(f) - n = fl.lookup(node) - node = hex(n) - changerev = fl.linkrev(n) - - cl = self.repo.changelog - cn = cl.node(changerev) - cs = cl.read(cn) - mfn = cs[0] - - def annotate(**map): - parity = 1 - last = None - for r, l in fl.annotate(n): - try: - cnode = ncache[r] - except KeyError: - cnode = ncache[r] = self.repo.changelog.node(r) - - try: - name = bcache[r] - except KeyError: - cl = self.repo.changelog.read(cnode) - bcache[r] = name = self.repo.ui.shortuser(cl[1]) - - if last != cnode: - parity = 1 - parity - last = cnode - - yield {"parity": parity, - "node": hex(cnode), - "rev": r, - "author": name, - "file": f, - "line": l} - - yield self.t("fileannotate", - file=f, - filenode=node, - annotate=annotate, - path=up(f), - rev=changerev, - node=hex(cn), - manifest=hex(mfn), - author=cs[1], - date=cs[2], - rename=self.renamelink(fl, n), - parent=self.siblings(fl.parents(n), fl.rev, file=f), - child=self.siblings(fl.children(n), fl.rev, file=f), - permissions=self.repo.manifest.readflags(mfn)[f]) - - def manifest(self, mnode, path): - man = self.repo.manifest - mn = man.lookup(mnode) - mnode = hex(mn) - mf = man.read(mn) - rev = man.rev(mn) - node = self.repo.changelog.node(rev) - mff = man.readflags(mn) - - files = {} - - p = path[1:] - if p and p[-1] != "/": - p += "/" - l = len(p) - - for f,n in mf.items(): - if f[:l] != p: - continue - remain = f[l:] - if "/" in remain: - short = remain[:remain.find("/") + 1] # bleah - files[short] = (f, None) - else: - short = os.path.basename(remain) - files[short] = (f, n) - - def filelist(**map): - parity = 0 - fl = files.keys() - fl.sort() - for f in fl: - full, fnode = files[f] - if not fnode: - continue - - yield {"file": full, - "manifest": mnode, - "filenode": hex(fnode), - "parity": parity, - "basename": f, - "permissions": mff[full]} - parity = 1 - parity - - def dirlist(**map): - parity = 0 - fl = files.keys() - fl.sort() - for f in fl: - full, fnode = files[f] - if fnode: - continue - - yield {"parity": parity, - "path": os.path.join(path, f), - "manifest": mnode, - "basename": f[:-1]} - parity = 1 - parity - - yield self.t("manifest", - manifest=mnode, - rev=rev, - node=hex(node), - path=path, - up=up(path), - fentries=filelist, - dentries=dirlist, - archives=self.archivelist(hex(node))) - - def tags(self): - cl = self.repo.changelog - mf = cl.read(cl.tip())[0] - - i = self.repo.tagslist() - i.reverse() - - def entries(notip=False, **map): - parity = 0 - for k,n in i: - if notip and k == "tip": continue - yield {"parity": parity, - "tag": k, - "tagmanifest": hex(cl.read(n)[0]), - "date": cl.read(n)[2], - "node": hex(n)} - parity = 1 - parity - - yield self.t("tags", - manifest=hex(mf), - entries=lambda **x: entries(False, **x), - entriesnotip=lambda **x: entries(True, **x)) - - def summary(self): - cl = self.repo.changelog - mf = cl.read(cl.tip())[0] - - i = self.repo.tagslist() - i.reverse() - - def tagentries(**map): - parity = 0 - count = 0 - for k,n in i: - if k == "tip": # skip tip - continue; - - count += 1 - if count > 10: # limit to 10 tags - break; - - c = cl.read(n) - m = c[0] - t = c[2] - - yield self.t("tagentry", - parity = parity, - tag = k, - node = hex(n), - date = t, - tagmanifest = hex(m)) - parity = 1 - parity - - def changelist(**map): - parity = 0 - cl = self.repo.changelog - l = [] # build a list in forward order for efficiency - for i in range(start, end): - n = cl.node(i) - changes = cl.read(n) - hn = hex(n) - t = changes[2] - - l.insert(0, self.t( - 'shortlogentry', - parity = parity, - author = changes[1], - manifest = hex(changes[0]), - desc = changes[4], - date = t, - rev = i, - node = hn)) - parity = 1 - parity - - yield l - - cl = self.repo.changelog - mf = cl.read(cl.tip())[0] - count = cl.count() - start = max(0, count - self.maxchanges) - end = min(count, start + self.maxchanges) - pos = end - 1 - - yield self.t("summary", - desc = self.repo.ui.config("web", "description", "unknown"), - owner = (self.repo.ui.config("ui", "username") or # preferred - self.repo.ui.config("web", "contact") or # deprecated - self.repo.ui.config("web", "author", "unknown")), # also - lastchange = (0, 0), # FIXME - manifest = hex(mf), - tags = tagentries, - shortlog = changelist) - - def filediff(self, file, changeset): - cl = self.repo.changelog - n = self.repo.lookup(changeset) - changeset = hex(n) - p1 = cl.parents(n)[0] - cs = cl.read(n) - mf = self.repo.manifest.read(cs[0]) - - def diff(**map): - yield self.diff(p1, n, [file]) - - yield self.t("filediff", - file=file, - filenode=hex(mf.get(file, nullid)), - node=changeset, - rev=self.repo.changelog.rev(n), - parent=self.siblings(cl.parents(n), cl.rev), - child=self.siblings(cl.children(n), cl.rev), - diff=diff) - - archive_specs = { - 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'), - 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'), - 'zip': ('application/zip', 'zip', '.zip', None), - } - - def archive(self, req, cnode, type): - reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame)) - name = "%s-%s" % (reponame, short(cnode)) - mimetype, artype, extension, encoding = self.archive_specs[type] - headers = [('Content-type', mimetype), - ('Content-disposition', 'attachment; filename=%s%s' % - (name, extension))] - if encoding: - headers.append(('Content-encoding', encoding)) - req.header(headers) - archival.archive(self.repo, req.out, cnode, artype, prefix=name) - - # add tags to things - # tags -> list of changesets corresponding to tags - # find tag, changeset, file - - def run(self, req=hgrequest()): - def clean(path): - p = util.normpath(path) - if p[:2] == "..": - raise "suspicious path" - return p - - def header(**map): - yield self.t("header", **map) - - def footer(**map): - yield self.t("footer", - motd=self.repo.ui.config("web", "motd", ""), - **map) - - def expand_form(form): - shortcuts = { - 'cl': [('cmd', ['changelog']), ('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] - - self.refresh() - - expand_form(req.form) - - t = self.repo.ui.config("web", "templates", templater.templatepath()) - static = self.repo.ui.config("web", "static", os.path.join(t,"static")) - m = os.path.join(t, "map") - style = self.repo.ui.config("web", "style", "") - if req.form.has_key('style'): - style = req.form['style'][0] - if style: - b = os.path.basename("map-" + style) - p = os.path.join(t, b) - if os.path.isfile(p): - m = p - - port = req.env["SERVER_PORT"] - port = port != "80" and (":" + port) or "" - uri = req.env["REQUEST_URI"] - if "?" in uri: - uri = uri.split("?")[0] - url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri) - if not self.reponame: - self.reponame = (self.repo.ui.config("web", "name") - or uri.strip('/') or self.repo.root) - - self.t = templater.templater(m, templater.common_filters, - defaults={"url": url, - "repo": self.reponame, - "header": header, - "footer": footer, - }) - - if not req.form.has_key('cmd'): - req.form['cmd'] = [self.t.cache['default'],] - - cmd = req.form['cmd'][0] - if cmd == 'changelog': - hi = self.repo.changelog.count() - 1 - if req.form.has_key('rev'): - hi = req.form['rev'][0] - try: - hi = self.repo.changelog.rev(self.repo.lookup(hi)) - except hg.RepoError: - req.write(self.search(hi)) # XXX redirect to 404 page? - return - - req.write(self.changelog(hi)) - - elif cmd == 'changeset': - req.write(self.changeset(req.form['node'][0])) - - elif cmd == 'manifest': - req.write(self.manifest(req.form['manifest'][0], - clean(req.form['path'][0]))) - - elif cmd == 'tags': - req.write(self.tags()) - - elif cmd == 'summary': - req.write(self.summary()) - - elif cmd == 'filediff': - req.write(self.filediff(clean(req.form['file'][0]), - req.form['node'][0])) - - elif cmd == 'file': - req.write(self.filerevision(clean(req.form['file'][0]), - req.form['filenode'][0])) - - elif cmd == 'annotate': - req.write(self.fileannotate(clean(req.form['file'][0]), - req.form['filenode'][0])) - - elif cmd == 'filelog': - req.write(self.filelog(clean(req.form['file'][0]), - req.form['filenode'][0])) - - elif cmd == 'heads': - req.httphdr("application/mercurial-0.1") - h = self.repo.heads() - req.write(" ".join(map(hex, h)) + "\n") - - elif cmd == 'branches': - req.httphdr("application/mercurial-0.1") - nodes = [] - if req.form.has_key('nodes'): - nodes = map(bin, req.form['nodes'][0].split(" ")) - for b in self.repo.branches(nodes): - req.write(" ".join(map(hex, b)) + "\n") - - elif cmd == 'between': - req.httphdr("application/mercurial-0.1") - nodes = [] - if req.form.has_key('pairs'): - pairs = [map(bin, p.split("-")) - for p in req.form['pairs'][0].split(" ")] - for b in self.repo.between(pairs): - req.write(" ".join(map(hex, b)) + "\n") - - elif cmd == 'changegroup': - 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()) - - elif cmd == 'archive': - changeset = self.repo.lookup(req.form['node'][0]) - type = req.form['type'][0] - if (type in self.archives and - self.repo.ui.configbool("web", "allow" + type, False)): - self.archive(req, changeset, type) - return - - req.write(self.t("error")) - - elif cmd == 'static': - fname = req.form['file'][0] - req.write(staticfile(static, fname) - or self.t("error", error="%r not found" % fname)) - - else: - req.write(self.t("error")) - -def create_server(ui, repo): - use_threads = True - - def openlog(opt, default): - if opt and opt != '-': - return open(opt, 'w') - return default - - address = ui.config("web", "address", "") - port = int(ui.config("web", "port", 8000)) - use_ipv6 = ui.configbool("web", "ipv6") - webdir_conf = ui.config("web", "webdir_conf") - accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout) - errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr) - - if use_threads: - try: - from threading import activeCount - except ImportError: - use_threads = False - - if use_threads: - _mixin = SocketServer.ThreadingMixIn - else: - if hasattr(os, "fork"): - _mixin = SocketServer.ForkingMixIn - else: - class _mixin: pass - - class MercurialHTTPServer(_mixin, BaseHTTPServer.HTTPServer): - pass - - class IPv6HTTPServer(MercurialHTTPServer): - address_family = getattr(socket, 'AF_INET6', None) - - def __init__(self, *args, **kwargs): - if self.address_family is None: - raise hg.RepoError(_('IPv6 not available on this system')) - BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) - - class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler): - - def log_error(self, format, *args): - errorlog.write("%s - - [%s] %s\n" % (self.address_string(), - self.log_date_time_string(), - format % args)) - - def log_message(self, format, *args): - accesslog.write("%s - - [%s] %s\n" % (self.address_string(), - self.log_date_time_string(), - format % args)) - - def do_POST(self): - try: - self.do_hgweb() - except socket.error, inst: - if inst[0] != errno.EPIPE: - raise - - def do_GET(self): - self.do_POST() - - def do_hgweb(self): - path_info, query = splitURI(self.path) - - env = {} - env['GATEWAY_INTERFACE'] = 'CGI/1.1' - env['REQUEST_METHOD'] = self.command - env['SERVER_NAME'] = self.server.server_name - env['SERVER_PORT'] = str(self.server.server_port) - env['REQUEST_URI'] = "/" - env['PATH_INFO'] = path_info - if query: - env['QUERY_STRING'] = query - host = self.address_string() - if host != self.client_address[0]: - env['REMOTE_HOST'] = host - env['REMOTE_ADDR'] = self.client_address[0] - - if self.headers.typeheader is None: - env['CONTENT_TYPE'] = self.headers.type - else: - env['CONTENT_TYPE'] = self.headers.typeheader - 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) - - req = hgrequest(self.rfile, self.wfile, env) - self.send_response(200, "Script output follows") - - if webdir_conf: - hgwebobj = hgwebdir(webdir_conf) - elif repo is not None: - hgwebobj = hgweb(repo.__class__(repo.ui, repo.origroot)) - else: - raise hg.RepoError(_('no repo found')) - hgwebobj.run(req) - - - if use_ipv6: - return IPv6HTTPServer((address, port), hgwebhandler) - else: - return MercurialHTTPServer((address, port), hgwebhandler) - -# This is a stopgap -class hgwebdir(object): - def __init__(self, config): - def cleannames(items): - return [(name.strip(os.sep), path) for name, path in items] - - self.motd = "" - self.repos_sorted = ('name', False) - if isinstance(config, (list, tuple)): - self.repos = cleannames(config) - self.repos_sorted = ('', False) - elif isinstance(config, dict): - self.repos = cleannames(config.items()) - self.repos.sort() - else: - cp = ConfigParser.SafeConfigParser() - cp.read(config) - self.repos = [] - if cp.has_section('web') and cp.has_option('web', 'motd'): - self.motd = cp.get('web', 'motd') - if cp.has_section('paths'): - self.repos.extend(cleannames(cp.items('paths'))) - if cp.has_section('collections'): - for prefix, root in cp.items('collections'): - for path in util.walkrepos(root): - repo = os.path.normpath(path) - name = repo - if name.startswith(prefix): - name = name[len(prefix):] - self.repos.append((name.lstrip(os.sep), repo)) - self.repos.sort() - - def run(self, req=hgrequest()): - def header(**map): - yield tmpl("header", **map) - - def footer(**map): - yield tmpl("footer", motd=self.motd, **map) - - m = os.path.join(templater.templatepath(), "map") - tmpl = templater.templater(m, templater.common_filters, - defaults={"header": header, - "footer": footer}) - - def archivelist(ui, nodeid, url): - for i in ['zip', 'gz', 'bz2']: - if ui.configbool("web", "allow" + i, False): - yield {"type" : i, "node": nodeid, "url": url} - - def entries(sortcolumn="", descending=False, **map): - rows = [] - parity = 0 - for name, path in self.repos: - u = ui.ui() - try: - u.readconfig(os.path.join(path, '.hg', 'hgrc')) - except IOError: - pass - get = u.config - - url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name]) - .replace("//", "/")) - - # update time with local timezone - try: - d = (get_mtime(path), util.makedate()[1]) - except OSError: - continue - - contact = (get("ui", "username") or # preferred - get("web", "contact") or # deprecated - get("web", "author", "")) # also - description = get("web", "description", "") - name = get("web", "name", name) - row = dict(contact=contact or "unknown", - contact_sort=contact.upper() or "unknown", - name=name, - name_sort=name, - url=url, - description=description or "unknown", - description_sort=description.upper() or "unknown", - lastchange=d, - lastchange_sort=d[1]-d[0], - archives=archivelist(u, "tip", url)) - if (not sortcolumn - or (sortcolumn, descending) == self.repos_sorted): - # fast path for unsorted output - row['parity'] = parity - parity = 1 - parity - yield row - else: - rows.append((row["%s_sort" % sortcolumn], row)) - if rows: - rows.sort() - if descending: - rows.reverse() - for key, row in rows: - row['parity'] = parity - parity = 1 - parity - yield row - - virtual = req.env.get("PATH_INFO", "").strip('/') - if virtual: - real = dict(self.repos).get(virtual) - if real: - try: - hgweb(real).run(req) - except IOError, inst: - req.write(tmpl("error", error=inst.strerror)) - except hg.RepoError, inst: - req.write(tmpl("error", error=str(inst))) - else: - 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) - or tmpl("error", error="%r not found" % fname)) - else: - 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, - sortcolumn=sortcolumn, descending=descending, - **dict(sort))) diff -r c58a403aa830 -r 7cbe8cd69d6b mercurial/hgweb/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/hgweb/__init__.py Fri May 19 08:57:12 2006 -0700 @@ -0,0 +1,1142 @@ +# hgweb.py - web interface to a mercurial repository +# +# Copyright 21 May 2005 - (c) 2005 Jake Edge +# Copyright 2005 Matt Mackall +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +import os, cgi, sys +import mimetypes +from mercurial.demandload import demandload +demandload(globals(), "time re socket zlib errno ConfigParser tempfile") +demandload(globals(), "StringIO BaseHTTPServer SocketServer urllib") +demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater") +from mercurial.node import * +from mercurial.i18n import gettext as _ + +def splitURI(uri): + """ Return path and query splited from uri + + Just like CGI environment, the path is unquoted, the query is + not. + """ + if '?' in uri: + path, query = uri.split('?', 1) + else: + path, query = uri, '' + return urllib.unquote(path), query + +def up(p): + if p[0] != "/": + p = "/" + p + if p[-1] == "/": + p = p[:-1] + up = os.path.dirname(p) + if up == "/": + return "/" + return up + "/" + +def get_mtime(repo_path): + hg_path = os.path.join(repo_path, ".hg") + cl_path = os.path.join(hg_path, "00changelog.i") + if os.path.exists(os.path.join(cl_path)): + return os.stat(cl_path).st_mtime + else: + return os.stat(hg_path).st_mtime + +def staticfile(directory, fname): + """return a file inside directory with guessed content-type header + + fname always uses '/' as directory separator and isn't allowed to + contain unusual path components. + Content-type is guessed using the mimetypes module. + Return an empty string if fname is illegal or file not found. + + """ + parts = fname.split('/') + path = directory + for part in parts: + if (part in ('', os.curdir, os.pardir) or + os.sep in part or os.altsep is not None and os.altsep in part): + return "" + path = os.path.join(path, part) + try: + os.stat(path) + ct = mimetypes.guess_type(path)[0] or "text/plain" + return "Content-type: %s\n\n%s" % (ct, file(path).read()) + except (TypeError, OSError): + # illegal fname or unreadable file + return "" + +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 + self.form = cgi.parse(self.inp, self.env, keep_blank_values=1) + + def write(self, *things): + for thing in things: + if hasattr(thing, "__iter__"): + for part in thing: + self.write(part) + else: + try: + self.out.write(str(thing)) + except socket.error, inst: + if inst[0] != errno.ECONNRESET: + raise + + 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") + + def httphdr(self, type, file="", size=0): + + headers = [('Content-type', type)] + if file: + headers.append(('Content-disposition', 'attachment; filename=%s' % file)) + if size > 0: + headers.append(('Content-length', str(size))) + self.header(headers) + +class hgweb(object): + def __init__(self, repo, name=None): + if type(repo) == type(""): + self.repo = hg.repository(ui.ui(), repo) + else: + self.repo = repo + + self.mtime = -1 + self.reponame = name + self.archives = 'zip', 'gz', 'bz2' + + def refresh(self): + mtime = get_mtime(self.repo.root) + if mtime != self.mtime: + self.mtime = mtime + self.repo = hg.repository(self.repo.ui, self.repo.root) + self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10)) + self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10)) + self.allowpull = self.repo.ui.configbool("web", "allowpull", True) + + def archivelist(self, nodeid): + for i in self.archives: + if self.repo.ui.configbool("web", "allow" + i, False): + yield {"type" : i, "node" : nodeid, "url": ""} + + def listfiles(self, files, mf): + for f in files[:self.maxfiles]: + yield self.t("filenodelink", node=hex(mf[f]), file=f) + if len(files) > self.maxfiles: + yield self.t("fileellipses") + + def listfilediffs(self, files, changeset): + for f in files[:self.maxfiles]: + yield self.t("filedifflink", node=hex(changeset), file=f) + if len(files) > self.maxfiles: + yield self.t("fileellipses") + + def siblings(self, siblings=[], rev=None, hiderev=None, **args): + if not rev: + rev = lambda x: "" + siblings = [s for s in siblings if s != nullid] + if len(siblings) == 1 and rev(siblings[0]) == hiderev: + return + for s in siblings: + yield dict(node=hex(s), rev=rev(s), **args) + + def renamelink(self, fl, node): + r = fl.renamed(node) + if r: + return [dict(file=r[0], node=hex(r[1]))] + return [] + + def showtag(self, t1, node=nullid, **args): + for t in self.repo.nodetags(node): + yield self.t(t1, tag=t, **args) + + def diff(self, node1, node2, files): + def filterfiles(filters, files): + l = [x for x in files if x in filters] + + for t in filters: + if t and t[-1] != os.sep: + t += os.sep + l += [x for x in files if x.startswith(t)] + return l + + parity = [0] + def diffblock(diff, f, fn): + yield self.t("diffblock", + lines=prettyprintlines(diff), + parity=parity[0], + file=f, + filenode=hex(fn or nullid)) + parity[0] = 1 - parity[0] + + def prettyprintlines(diff): + for l in diff.splitlines(1): + if l.startswith('+'): + yield self.t("difflineplus", line=l) + elif l.startswith('-'): + yield self.t("difflineminus", line=l) + elif l.startswith('@'): + yield self.t("difflineat", line=l) + else: + yield self.t("diffline", line=l) + + r = self.repo + cl = r.changelog + mf = r.manifest + change1 = cl.read(node1) + change2 = cl.read(node2) + mmap1 = mf.read(change1[0]) + mmap2 = mf.read(change2[0]) + date1 = util.datestr(change1[2]) + date2 = util.datestr(change2[2]) + + modified, added, removed, deleted, unknown = r.changes(node1, node2) + if files: + modified, added, removed = map(lambda x: filterfiles(files, x), + (modified, added, removed)) + + diffopts = self.repo.ui.diffopts() + showfunc = diffopts['showfunc'] + ignorews = diffopts['ignorews'] + for f in modified: + to = r.file(f).read(mmap1[f]) + tn = r.file(f).read(mmap2[f]) + yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, + showfunc=showfunc, ignorews=ignorews), f, tn) + for f in added: + to = None + tn = r.file(f).read(mmap2[f]) + yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, + showfunc=showfunc, ignorews=ignorews), f, tn) + for f in removed: + to = r.file(f).read(mmap1[f]) + tn = None + yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, + showfunc=showfunc, ignorews=ignorews), f, tn) + + def changelog(self, pos): + def changenav(**map): + def seq(factor, maxchanges=None): + if maxchanges: + yield maxchanges + if maxchanges >= 20 and maxchanges <= 40: + yield 50 + else: + yield 1 * factor + yield 3 * factor + for f in seq(factor * 10): + yield f + + l = [] + last = 0 + for f in seq(1, self.maxchanges): + if f < self.maxchanges or f <= last: + continue + if f > count: + break + last = f + r = "%d" % f + if pos + f < count: + l.append(("+" + r, pos + f)) + if pos - f >= 0: + l.insert(0, ("-" + r, pos - f)) + + yield {"rev": 0, "label": "(0)"} + + for label, rev in l: + yield {"label": label, "rev": rev} + + yield {"label": "tip", "rev": "tip"} + + def changelist(**map): + parity = (start - end) & 1 + cl = self.repo.changelog + l = [] # build a list in forward order for efficiency + for i in range(start, end): + n = cl.node(i) + changes = cl.read(n) + hn = hex(n) + + l.insert(0, {"parity": parity, + "author": changes[1], + "parent": self.siblings(cl.parents(n), cl.rev, + cl.rev(n) - 1), + "child": self.siblings(cl.children(n), cl.rev, + cl.rev(n) + 1), + "changelogtag": self.showtag("changelogtag",n), + "manifest": hex(changes[0]), + "desc": changes[4], + "date": changes[2], + "files": self.listfilediffs(changes[3], n), + "rev": i, + "node": hn}) + parity = 1 - parity + + for e in l: + yield e + + cl = self.repo.changelog + mf = cl.read(cl.tip())[0] + count = cl.count() + start = max(0, pos - self.maxchanges + 1) + end = min(count, start + self.maxchanges) + pos = end - 1 + + yield self.t('changelog', + changenav=changenav, + manifest=hex(mf), + rev=pos, changesets=count, entries=changelist, + archives=self.archivelist("tip")) + + def search(self, query): + + def changelist(**map): + cl = self.repo.changelog + count = 0 + qw = query.lower().split() + + def revgen(): + for i in range(cl.count() - 1, 0, -100): + l = [] + for j in range(max(0, i - 100), i): + n = cl.node(j) + changes = cl.read(n) + l.append((n, j, changes)) + l.reverse() + for e in l: + yield e + + for n, i, changes in revgen(): + miss = 0 + for q in qw: + if not (q in changes[1].lower() or + q in changes[4].lower() or + q in " ".join(changes[3][:20]).lower()): + miss = 1 + break + if miss: + continue + + count += 1 + hn = hex(n) + + yield self.t('searchentry', + parity=count & 1, + author=changes[1], + parent=self.siblings(cl.parents(n), cl.rev), + child=self.siblings(cl.children(n), cl.rev), + changelogtag=self.showtag("changelogtag",n), + manifest=hex(changes[0]), + desc=changes[4], + date=changes[2], + files=self.listfilediffs(changes[3], n), + rev=i, + node=hn) + + if count >= self.maxchanges: + break + + cl = self.repo.changelog + mf = cl.read(cl.tip())[0] + + yield self.t('search', + query=query, + manifest=hex(mf), + entries=changelist) + + def changeset(self, nodeid): + cl = self.repo.changelog + n = self.repo.lookup(nodeid) + nodeid = hex(n) + changes = cl.read(n) + p1 = cl.parents(n)[0] + + files = [] + mf = self.repo.manifest.read(changes[0]) + for f in changes[3]: + files.append(self.t("filenodelink", + filenode=hex(mf.get(f, nullid)), file=f)) + + def diff(**map): + yield self.diff(p1, n, None) + + yield self.t('changeset', + diff=diff, + rev=cl.rev(n), + node=nodeid, + parent=self.siblings(cl.parents(n), cl.rev), + child=self.siblings(cl.children(n), cl.rev), + changesettag=self.showtag("changesettag",n), + manifest=hex(changes[0]), + author=changes[1], + desc=changes[4], + date=changes[2], + files=files, + archives=self.archivelist(nodeid)) + + def filelog(self, f, filenode): + cl = self.repo.changelog + fl = self.repo.file(f) + filenode = hex(fl.lookup(filenode)) + count = fl.count() + + def entries(**map): + l = [] + parity = (count - 1) & 1 + + for i in range(count): + n = fl.node(i) + lr = fl.linkrev(n) + cn = cl.node(lr) + cs = cl.read(cl.node(lr)) + + l.insert(0, {"parity": parity, + "filenode": hex(n), + "filerev": i, + "file": f, + "node": hex(cn), + "author": cs[1], + "date": cs[2], + "rename": self.renamelink(fl, n), + "parent": self.siblings(fl.parents(n), + fl.rev, file=f), + "child": self.siblings(fl.children(n), + fl.rev, file=f), + "desc": cs[4]}) + parity = 1 - parity + + for e in l: + yield e + + yield self.t("filelog", file=f, filenode=filenode, entries=entries) + + def filerevision(self, f, node): + fl = self.repo.file(f) + n = fl.lookup(node) + node = hex(n) + text = fl.read(n) + changerev = fl.linkrev(n) + cl = self.repo.changelog + cn = cl.node(changerev) + cs = cl.read(cn) + mfn = cs[0] + + mt = mimetypes.guess_type(f)[0] + rawtext = text + if util.binary(text): + mt = mt or 'application/octet-stream' + text = "(binary:%s)" % mt + mt = mt or 'text/plain' + + def lines(): + for l, t in enumerate(text.splitlines(1)): + yield {"line": t, + "linenumber": "% 6d" % (l + 1), + "parity": l & 1} + + yield self.t("filerevision", + file=f, + filenode=node, + path=up(f), + text=lines(), + raw=rawtext, + mimetype=mt, + rev=changerev, + node=hex(cn), + manifest=hex(mfn), + author=cs[1], + date=cs[2], + parent=self.siblings(fl.parents(n), fl.rev, file=f), + child=self.siblings(fl.children(n), fl.rev, file=f), + rename=self.renamelink(fl, n), + permissions=self.repo.manifest.readflags(mfn)[f]) + + def fileannotate(self, f, node): + bcache = {} + ncache = {} + fl = self.repo.file(f) + n = fl.lookup(node) + node = hex(n) + changerev = fl.linkrev(n) + + cl = self.repo.changelog + cn = cl.node(changerev) + cs = cl.read(cn) + mfn = cs[0] + + def annotate(**map): + parity = 1 + last = None + for r, l in fl.annotate(n): + try: + cnode = ncache[r] + except KeyError: + cnode = ncache[r] = self.repo.changelog.node(r) + + try: + name = bcache[r] + except KeyError: + cl = self.repo.changelog.read(cnode) + bcache[r] = name = self.repo.ui.shortuser(cl[1]) + + if last != cnode: + parity = 1 - parity + last = cnode + + yield {"parity": parity, + "node": hex(cnode), + "rev": r, + "author": name, + "file": f, + "line": l} + + yield self.t("fileannotate", + file=f, + filenode=node, + annotate=annotate, + path=up(f), + rev=changerev, + node=hex(cn), + manifest=hex(mfn), + author=cs[1], + date=cs[2], + rename=self.renamelink(fl, n), + parent=self.siblings(fl.parents(n), fl.rev, file=f), + child=self.siblings(fl.children(n), fl.rev, file=f), + permissions=self.repo.manifest.readflags(mfn)[f]) + + def manifest(self, mnode, path): + man = self.repo.manifest + mn = man.lookup(mnode) + mnode = hex(mn) + mf = man.read(mn) + rev = man.rev(mn) + node = self.repo.changelog.node(rev) + mff = man.readflags(mn) + + files = {} + + p = path[1:] + if p and p[-1] != "/": + p += "/" + l = len(p) + + for f,n in mf.items(): + if f[:l] != p: + continue + remain = f[l:] + if "/" in remain: + short = remain[:remain.find("/") + 1] # bleah + files[short] = (f, None) + else: + short = os.path.basename(remain) + files[short] = (f, n) + + def filelist(**map): + parity = 0 + fl = files.keys() + fl.sort() + for f in fl: + full, fnode = files[f] + if not fnode: + continue + + yield {"file": full, + "manifest": mnode, + "filenode": hex(fnode), + "parity": parity, + "basename": f, + "permissions": mff[full]} + parity = 1 - parity + + def dirlist(**map): + parity = 0 + fl = files.keys() + fl.sort() + for f in fl: + full, fnode = files[f] + if fnode: + continue + + yield {"parity": parity, + "path": os.path.join(path, f), + "manifest": mnode, + "basename": f[:-1]} + parity = 1 - parity + + yield self.t("manifest", + manifest=mnode, + rev=rev, + node=hex(node), + path=path, + up=up(path), + fentries=filelist, + dentries=dirlist, + archives=self.archivelist(hex(node))) + + def tags(self): + cl = self.repo.changelog + mf = cl.read(cl.tip())[0] + + i = self.repo.tagslist() + i.reverse() + + def entries(notip=False, **map): + parity = 0 + for k,n in i: + if notip and k == "tip": continue + yield {"parity": parity, + "tag": k, + "tagmanifest": hex(cl.read(n)[0]), + "date": cl.read(n)[2], + "node": hex(n)} + parity = 1 - parity + + yield self.t("tags", + manifest=hex(mf), + entries=lambda **x: entries(False, **x), + entriesnotip=lambda **x: entries(True, **x)) + + def summary(self): + cl = self.repo.changelog + mf = cl.read(cl.tip())[0] + + i = self.repo.tagslist() + i.reverse() + + def tagentries(**map): + parity = 0 + count = 0 + for k,n in i: + if k == "tip": # skip tip + continue; + + count += 1 + if count > 10: # limit to 10 tags + break; + + c = cl.read(n) + m = c[0] + t = c[2] + + yield self.t("tagentry", + parity = parity, + tag = k, + node = hex(n), + date = t, + tagmanifest = hex(m)) + parity = 1 - parity + + def changelist(**map): + parity = 0 + cl = self.repo.changelog + l = [] # build a list in forward order for efficiency + for i in range(start, end): + n = cl.node(i) + changes = cl.read(n) + hn = hex(n) + t = changes[2] + + l.insert(0, self.t( + 'shortlogentry', + parity = parity, + author = changes[1], + manifest = hex(changes[0]), + desc = changes[4], + date = t, + rev = i, + node = hn)) + parity = 1 - parity + + yield l + + cl = self.repo.changelog + mf = cl.read(cl.tip())[0] + count = cl.count() + start = max(0, count - self.maxchanges) + end = min(count, start + self.maxchanges) + pos = end - 1 + + yield self.t("summary", + desc = self.repo.ui.config("web", "description", "unknown"), + owner = (self.repo.ui.config("ui", "username") or # preferred + self.repo.ui.config("web", "contact") or # deprecated + self.repo.ui.config("web", "author", "unknown")), # also + lastchange = (0, 0), # FIXME + manifest = hex(mf), + tags = tagentries, + shortlog = changelist) + + def filediff(self, file, changeset): + cl = self.repo.changelog + n = self.repo.lookup(changeset) + changeset = hex(n) + p1 = cl.parents(n)[0] + cs = cl.read(n) + mf = self.repo.manifest.read(cs[0]) + + def diff(**map): + yield self.diff(p1, n, [file]) + + yield self.t("filediff", + file=file, + filenode=hex(mf.get(file, nullid)), + node=changeset, + rev=self.repo.changelog.rev(n), + parent=self.siblings(cl.parents(n), cl.rev), + child=self.siblings(cl.children(n), cl.rev), + diff=diff) + + archive_specs = { + 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'), + 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'), + 'zip': ('application/zip', 'zip', '.zip', None), + } + + def archive(self, req, cnode, type): + reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame)) + name = "%s-%s" % (reponame, short(cnode)) + mimetype, artype, extension, encoding = self.archive_specs[type] + headers = [('Content-type', mimetype), + ('Content-disposition', 'attachment; filename=%s%s' % + (name, extension))] + if encoding: + headers.append(('Content-encoding', encoding)) + req.header(headers) + archival.archive(self.repo, req.out, cnode, artype, prefix=name) + + # add tags to things + # tags -> list of changesets corresponding to tags + # find tag, changeset, file + + def run(self, req=hgrequest()): + def clean(path): + p = util.normpath(path) + if p[:2] == "..": + raise "suspicious path" + return p + + def header(**map): + yield self.t("header", **map) + + def footer(**map): + yield self.t("footer", + motd=self.repo.ui.config("web", "motd", ""), + **map) + + def expand_form(form): + shortcuts = { + 'cl': [('cmd', ['changelog']), ('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] + + self.refresh() + + expand_form(req.form) + + t = self.repo.ui.config("web", "templates", templater.templatepath()) + static = self.repo.ui.config("web", "static", os.path.join(t,"static")) + m = os.path.join(t, "map") + style = self.repo.ui.config("web", "style", "") + if req.form.has_key('style'): + style = req.form['style'][0] + if style: + b = os.path.basename("map-" + style) + p = os.path.join(t, b) + if os.path.isfile(p): + m = p + + port = req.env["SERVER_PORT"] + port = port != "80" and (":" + port) or "" + uri = req.env["REQUEST_URI"] + if "?" in uri: + uri = uri.split("?")[0] + url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri) + if not self.reponame: + self.reponame = (self.repo.ui.config("web", "name") + or uri.strip('/') or self.repo.root) + + self.t = templater.templater(m, templater.common_filters, + defaults={"url": url, + "repo": self.reponame, + "header": header, + "footer": footer, + }) + + if not req.form.has_key('cmd'): + req.form['cmd'] = [self.t.cache['default'],] + + cmd = req.form['cmd'][0] + if cmd == 'changelog': + hi = self.repo.changelog.count() - 1 + if req.form.has_key('rev'): + hi = req.form['rev'][0] + try: + hi = self.repo.changelog.rev(self.repo.lookup(hi)) + except hg.RepoError: + req.write(self.search(hi)) # XXX redirect to 404 page? + return + + req.write(self.changelog(hi)) + + elif cmd == 'changeset': + req.write(self.changeset(req.form['node'][0])) + + elif cmd == 'manifest': + req.write(self.manifest(req.form['manifest'][0], + clean(req.form['path'][0]))) + + elif cmd == 'tags': + req.write(self.tags()) + + elif cmd == 'summary': + req.write(self.summary()) + + elif cmd == 'filediff': + req.write(self.filediff(clean(req.form['file'][0]), + req.form['node'][0])) + + elif cmd == 'file': + req.write(self.filerevision(clean(req.form['file'][0]), + req.form['filenode'][0])) + + elif cmd == 'annotate': + req.write(self.fileannotate(clean(req.form['file'][0]), + req.form['filenode'][0])) + + elif cmd == 'filelog': + req.write(self.filelog(clean(req.form['file'][0]), + req.form['filenode'][0])) + + elif cmd == 'heads': + req.httphdr("application/mercurial-0.1") + h = self.repo.heads() + req.write(" ".join(map(hex, h)) + "\n") + + elif cmd == 'branches': + req.httphdr("application/mercurial-0.1") + nodes = [] + if req.form.has_key('nodes'): + nodes = map(bin, req.form['nodes'][0].split(" ")) + for b in self.repo.branches(nodes): + req.write(" ".join(map(hex, b)) + "\n") + + elif cmd == 'between': + req.httphdr("application/mercurial-0.1") + nodes = [] + if req.form.has_key('pairs'): + pairs = [map(bin, p.split("-")) + for p in req.form['pairs'][0].split(" ")] + for b in self.repo.between(pairs): + req.write(" ".join(map(hex, b)) + "\n") + + elif cmd == 'changegroup': + 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()) + + elif cmd == 'archive': + changeset = self.repo.lookup(req.form['node'][0]) + type = req.form['type'][0] + if (type in self.archives and + self.repo.ui.configbool("web", "allow" + type, False)): + self.archive(req, changeset, type) + return + + req.write(self.t("error")) + + elif cmd == 'static': + fname = req.form['file'][0] + req.write(staticfile(static, fname) + or self.t("error", error="%r not found" % fname)) + + else: + req.write(self.t("error")) + +def create_server(ui, repo): + use_threads = True + + def openlog(opt, default): + if opt and opt != '-': + return open(opt, 'w') + return default + + address = ui.config("web", "address", "") + port = int(ui.config("web", "port", 8000)) + use_ipv6 = ui.configbool("web", "ipv6") + webdir_conf = ui.config("web", "webdir_conf") + accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout) + errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr) + + if use_threads: + try: + from threading import activeCount + except ImportError: + use_threads = False + + if use_threads: + _mixin = SocketServer.ThreadingMixIn + else: + if hasattr(os, "fork"): + _mixin = SocketServer.ForkingMixIn + else: + class _mixin: pass + + class MercurialHTTPServer(_mixin, BaseHTTPServer.HTTPServer): + pass + + class IPv6HTTPServer(MercurialHTTPServer): + address_family = getattr(socket, 'AF_INET6', None) + + def __init__(self, *args, **kwargs): + if self.address_family is None: + raise hg.RepoError(_('IPv6 not available on this system')) + BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) + + class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler): + + def log_error(self, format, *args): + errorlog.write("%s - - [%s] %s\n" % (self.address_string(), + self.log_date_time_string(), + format % args)) + + def log_message(self, format, *args): + accesslog.write("%s - - [%s] %s\n" % (self.address_string(), + self.log_date_time_string(), + format % args)) + + def do_POST(self): + try: + self.do_hgweb() + except socket.error, inst: + if inst[0] != errno.EPIPE: + raise + + def do_GET(self): + self.do_POST() + + def do_hgweb(self): + path_info, query = splitURI(self.path) + + env = {} + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + env['REQUEST_METHOD'] = self.command + env['SERVER_NAME'] = self.server.server_name + env['SERVER_PORT'] = str(self.server.server_port) + env['REQUEST_URI'] = "/" + env['PATH_INFO'] = path_info + if query: + env['QUERY_STRING'] = query + host = self.address_string() + if host != self.client_address[0]: + env['REMOTE_HOST'] = host + env['REMOTE_ADDR'] = self.client_address[0] + + if self.headers.typeheader is None: + env['CONTENT_TYPE'] = self.headers.type + else: + env['CONTENT_TYPE'] = self.headers.typeheader + 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) + + req = hgrequest(self.rfile, self.wfile, env) + self.send_response(200, "Script output follows") + + if webdir_conf: + hgwebobj = hgwebdir(webdir_conf) + elif repo is not None: + hgwebobj = hgweb(repo.__class__(repo.ui, repo.origroot)) + else: + raise hg.RepoError(_('no repo found')) + hgwebobj.run(req) + + + if use_ipv6: + return IPv6HTTPServer((address, port), hgwebhandler) + else: + return MercurialHTTPServer((address, port), hgwebhandler) + +# This is a stopgap +class hgwebdir(object): + def __init__(self, config): + def cleannames(items): + return [(name.strip(os.sep), path) for name, path in items] + + self.motd = "" + self.repos_sorted = ('name', False) + if isinstance(config, (list, tuple)): + self.repos = cleannames(config) + self.repos_sorted = ('', False) + elif isinstance(config, dict): + self.repos = cleannames(config.items()) + self.repos.sort() + else: + cp = ConfigParser.SafeConfigParser() + cp.read(config) + self.repos = [] + if cp.has_section('web') and cp.has_option('web', 'motd'): + self.motd = cp.get('web', 'motd') + if cp.has_section('paths'): + self.repos.extend(cleannames(cp.items('paths'))) + if cp.has_section('collections'): + for prefix, root in cp.items('collections'): + for path in util.walkrepos(root): + repo = os.path.normpath(path) + name = repo + if name.startswith(prefix): + name = name[len(prefix):] + self.repos.append((name.lstrip(os.sep), repo)) + self.repos.sort() + + def run(self, req=hgrequest()): + def header(**map): + yield tmpl("header", **map) + + def footer(**map): + yield tmpl("footer", motd=self.motd, **map) + + m = os.path.join(templater.templatepath(), "map") + tmpl = templater.templater(m, templater.common_filters, + defaults={"header": header, + "footer": footer}) + + def archivelist(ui, nodeid, url): + for i in ['zip', 'gz', 'bz2']: + if ui.configbool("web", "allow" + i, False): + yield {"type" : i, "node": nodeid, "url": url} + + def entries(sortcolumn="", descending=False, **map): + rows = [] + parity = 0 + for name, path in self.repos: + u = ui.ui() + try: + u.readconfig(os.path.join(path, '.hg', 'hgrc')) + except IOError: + pass + get = u.config + + url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name]) + .replace("//", "/")) + + # update time with local timezone + try: + d = (get_mtime(path), util.makedate()[1]) + except OSError: + continue + + contact = (get("ui", "username") or # preferred + get("web", "contact") or # deprecated + get("web", "author", "")) # also + description = get("web", "description", "") + name = get("web", "name", name) + row = dict(contact=contact or "unknown", + contact_sort=contact.upper() or "unknown", + name=name, + name_sort=name, + url=url, + description=description or "unknown", + description_sort=description.upper() or "unknown", + lastchange=d, + lastchange_sort=d[1]-d[0], + archives=archivelist(u, "tip", url)) + if (not sortcolumn + or (sortcolumn, descending) == self.repos_sorted): + # fast path for unsorted output + row['parity'] = parity + parity = 1 - parity + yield row + else: + rows.append((row["%s_sort" % sortcolumn], row)) + if rows: + rows.sort() + if descending: + rows.reverse() + for key, row in rows: + row['parity'] = parity + parity = 1 - parity + yield row + + virtual = req.env.get("PATH_INFO", "").strip('/') + if virtual: + real = dict(self.repos).get(virtual) + if real: + try: + hgweb(real).run(req) + except IOError, inst: + req.write(tmpl("error", error=inst.strerror)) + except hg.RepoError, inst: + req.write(tmpl("error", error=str(inst))) + else: + 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) + or tmpl("error", error="%r not found" % fname)) + else: + 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, + sortcolumn=sortcolumn, descending=descending, + **dict(sort))) diff -r c58a403aa830 -r 7cbe8cd69d6b mercurial/localrepo.py --- a/mercurial/localrepo.py Fri May 19 08:54:28 2006 -0700 +++ b/mercurial/localrepo.py Fri May 19 08:57:12 2006 -0700 @@ -166,37 +166,44 @@ return s = l.split(" ", 1) if len(s) != 2: - self.ui.warn(_("%s: ignoring invalid tag\n") % context) + self.ui.warn(_("%s: cannot parse entry\n") % context) return node, key = s + key = key.strip() try: bin_n = bin(node) except TypeError: - self.ui.warn(_("%s: ignoring invalid tag\n") % context) + self.ui.warn(_("%s: node '%s' is not well formed\n") % + (context, node)) return if bin_n not in self.changelog.nodemap: - self.ui.warn(_("%s: ignoring invalid tag\n") % context) + self.ui.warn(_("%s: tag '%s' refers to unknown node\n") % + (context, key)) return - self.tagscache[key.strip()] = bin_n + self.tagscache[key] = bin_n - # read each head of the tags file, ending with the tip + # read the tags file from each head, ending with the tip, # and add each tag found to the map, with "newer" ones # taking precedence + heads = self.heads() + heads.reverse() fl = self.file(".hgtags") - h = fl.heads() - h.reverse() - for r in h: + for node in heads: + change = self.changelog.read(node) + rev = self.changelog.rev(node) + fn, ff = self.manifest.find(change[0], '.hgtags') + if fn is None: continue count = 0 - for l in fl.read(r).splitlines(): + for l in fl.read(fn).splitlines(): count += 1 - parsetag(l, ".hgtags:%d" % count) - + parsetag(l, _(".hgtags (rev %d:%s), line %d") % + (rev, short(node), count)) try: f = self.opener("localtags") count = 0 for l in f: count += 1 - parsetag(l, "localtags:%d" % count) + parsetag(l, _("localtags, line %d") % count) except IOError: pass @@ -550,12 +557,15 @@ # run editor in the repository root olddir = os.getcwd() os.chdir(self.root) - edittext = self.ui.edit("\n".join(edittext), user) + text = self.ui.edit("\n".join(edittext), user) os.chdir(olddir) - if not edittext.rstrip(): - return None - text = edittext + lines = [line.rstrip() for line in text.rstrip().splitlines()] + while lines and not lines[0]: + del lines[0] + if not lines: + return None + text = '\n'.join(lines) n = self.changelog.add(mn, changed + remove, text, tr, p1, p2, user, date) self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1, parent2=xp2) diff -r c58a403aa830 -r 7cbe8cd69d6b mercurial/manifest.py --- a/mercurial/manifest.py Fri May 19 08:54:28 2006 -0700 +++ b/mercurial/manifest.py Fri May 19 08:57:12 2006 -0700 @@ -43,48 +43,61 @@ def diff(self, a, b): return mdiff.textdiff(str(a), str(b)) + def _search(self, m, s, lo=0, hi=None): + '''return a tuple (start, end) that says where to find s within m. + + If the string is found m[start:end] are the line containing + that string. If start == end the string was not found and + they indicate the proper sorted insertion point. This was + taken from bisect_left, and modified to find line start/end as + it goes along. + + m should be a buffer or a string + s is a string''' + def advance(i, c): + while i < lenm and m[i] != c: + i += 1 + return i + lenm = len(m) + if not hi: + hi = lenm + while lo < hi: + mid = (lo + hi) // 2 + start = mid + while start > 0 and m[start-1] != '\n': + start -= 1 + end = advance(start, '\0') + if m[start:end] < s: + # we know that after the null there are 40 bytes of sha1 + # this translates to the bisect lo = mid + 1 + lo = advance(end + 40, '\n') + 1 + else: + # this translates to the bisect hi = mid + hi = start + end = advance(lo, '\0') + found = m[lo:end] + if cmp(s, found) == 0: + # we know that after the null there are 40 bytes of sha1 + end = advance(end + 40, '\n') + return (lo, end+1) + else: + return (lo, lo) + + def find(self, node, f): + '''look up entry for a single file efficiently. + return (node, flag) pair if found, (None, None) if not.''' + if self.mapcache and node == self.mapcache[0]: + return self.mapcache[1].get(f), self.mapcache[2].get(f) + text = self.revision(node) + start, end = self._search(text, f) + if start == end: + return None, None + l = text[start:end] + f, n = l.split('\0') + return bin(n[:40]), n[40:-1] == 'x' + def add(self, map, flags, transaction, link, p1=None, p2=None, changed=None): - - # returns a tuple (start, end). If the string is found - # m[start:end] are the line containing that string. If start == end - # the string was not found and they indicate the proper sorted - # insertion point. This was taken from bisect_left, and modified - # to find line start/end as it goes along. - # - # m should be a buffer or a string - # s is a string - # - def manifestsearch(m, s, lo=0, hi=None): - def advance(i, c): - while i < lenm and m[i] != c: - i += 1 - return i - lenm = len(m) - if not hi: - hi = lenm - while lo < hi: - mid = (lo + hi) // 2 - start = mid - while start > 0 and m[start-1] != '\n': - start -= 1 - end = advance(start, '\0') - if m[start:end] < s: - # we know that after the null there are 40 bytes of sha1 - # this translates to the bisect lo = mid + 1 - lo = advance(end + 40, '\n') + 1 - else: - # this translates to the bisect hi = mid - hi = start - end = advance(lo, '\0') - found = m[lo:end] - if cmp(s, found) == 0: - # we know that after the null there are 40 bytes of sha1 - end = advance(end + 40, '\n') - return (lo, end+1) - else: - return (lo, lo) - # apply the changes collected during the bisect loop to our addlist # return a delta suitable for addrevision def addlistdelta(addlist, x): @@ -137,7 +150,7 @@ for w in work: f = w[0] # bs will either be the index of the item or the insert point - start, end = manifestsearch(addbuf, f, start) + start, end = self._search(addbuf, f, start) if w[1] == 0: l = "%s\000%s%s\n" % (f, hex(map[f]), flags[f] and "x" or '') diff -r c58a403aa830 -r 7cbe8cd69d6b mercurial/util.py --- a/mercurial/util.py Fri May 19 08:54:28 2006 -0700 +++ b/mercurial/util.py Fri May 19 08:57:12 2006 -0700 @@ -94,7 +94,7 @@ """apply the patch to the working directory. a list of patched files is returned""" patcher = find_in_path('gpatch', os.environ.get('PATH', ''), 'patch') - fp = os.popen('"%s" -p%d < "%s"' % (patcher, strip, patchname)) + fp = os.popen('%s -p%d < "%s"' % (patcher, strip, patchname)) files = {} for line in fp: line = line.rstrip() @@ -734,7 +734,7 @@ def rename(self): if not self.closed: posixfile.close(self) - rename(self.temp, self.__name) + rename(self.temp, localpath(self.__name)) def __del__(self): if not self.closed: try: diff -r c58a403aa830 -r 7cbe8cd69d6b mercurial/util_win32.py --- a/mercurial/util_win32.py Fri May 19 08:54:28 2006 -0700 +++ b/mercurial/util_win32.py Fri May 19 08:57:12 2006 -0700 @@ -194,7 +194,7 @@ # We are on win < nt: fetch the APPDATA directory location and use # the parent directory as the user home dir. appdir = shell.SHGetPathFromIDList( - qshell.SHGetSpecialFolderLocation(0, shellcon.CSIDL_APPDATA)) + shell.SHGetSpecialFolderLocation(0, shellcon.CSIDL_APPDATA)) userdir = os.path.dirname(appdir) return os.path.join(userdir, 'mercurial.ini') diff -r c58a403aa830 -r 7cbe8cd69d6b setup.py --- a/setup.py Fri May 19 08:54:28 2006 -0700 +++ b/setup.py Fri May 19 08:57:12 2006 -0700 @@ -56,6 +56,7 @@ else: self.includes = self.includes.split(',') mercurial.packagescan.scan(self.build_lib,'mercurial') + mercurial.packagescan.scan(self.build_lib,'mercurial/hgweb') mercurial.packagescan.scan(self.build_lib,'hgext') self.includes += mercurial.packagescan.getmodules() build_exe.finalize_options(self) @@ -85,7 +86,7 @@ url='http://selenic.com/mercurial', description='Scalable distributed SCM', license='GNU GPL', - packages=['mercurial', 'hgext'], + packages=['mercurial', 'mercurial.hgweb', 'hgext'], ext_modules=[Extension('mercurial.mpatch', ['mercurial/mpatch.c']), Extension('mercurial.bdiff', ['mercurial/bdiff.c'])], data_files=[('mercurial/templates', diff -r c58a403aa830 -r 7cbe8cd69d6b tests/test-backout.out --- a/tests/test-backout.out Fri May 19 08:54:28 2006 -0700 +++ b/tests/test-backout.out Fri May 19 08:57:12 2006 -0700 @@ -1,19 +1,19 @@ # basic operation adding a -changeset 2:c86754337410 backs out changeset 1:a820f4f40a57 +changeset 2:b38a34ddfd9f backs out changeset 1:a820f4f40a57 a # file that was removed is recreated adding a adding a -changeset 2:d2d961bd79f2 backs out changeset 1:76862dcce372 +changeset 2:44cd84c7349a backs out changeset 1:76862dcce372 content # backout of backout is as if nothing happened removing a -changeset 3:8a7eeb5ab5ce backs out changeset 2:d2d961bd79f2 +changeset 3:0dd8a0ed5e99 backs out changeset 2:44cd84c7349a cat: a: No such file or directory # backout with merge adding a -changeset 3:3c9e845b409c backs out changeset 1:314f55b1bf23 +changeset 3:6c77ecc28460 backs out changeset 1:314f55b1bf23 merging with changeset 2:b66ea5b77abb merging a 0 files updated, 1 files merged, 0 files removed, 0 files unresolved diff -r c58a403aa830 -r 7cbe8cd69d6b tests/test-command-template.out --- a/tests/test-command-template.out Fri May 19 08:54:28 2006 -0700 +++ b/tests/test-command-template.out Fri May 19 08:57:12 2006 -0700 @@ -6,40 +6,40 @@ 43a46 > files: # compact style works -3[tip] 8c7f028fbabf 1970-01-16 01:06 +0000 person +3[tip] 10e46f2dcbf4 1970-01-16 01:06 +0000 person no user, no domain -2 259081bc29d1 1970-01-14 21:20 +0000 other +2 97054abb4ab8 1970-01-14 21:20 +0000 other no person -1 1c37ba774509 1970-01-13 17:33 +0000 other +1 b608e9d1a3f0 1970-01-13 17:33 +0000 other other 1 -0 6eb5362d59ec 1970-01-12 13:46 +0000 user +0 1e4e1b8f71e0 1970-01-12 13:46 +0000 user line 1 -3[tip] 8c7f028fbabf 1970-01-16 01:06 +0000 person +3[tip] 10e46f2dcbf4 1970-01-16 01:06 +0000 person no user, no domain -2 259081bc29d1 1970-01-14 21:20 +0000 other +2 97054abb4ab8 1970-01-14 21:20 +0000 other no person -1 1c37ba774509 1970-01-13 17:33 +0000 other +1 b608e9d1a3f0 1970-01-13 17:33 +0000 other other 1 -0 6eb5362d59ec 1970-01-12 13:46 +0000 user +0 1e4e1b8f71e0 1970-01-12 13:46 +0000 user line 1 -3[tip]:2,-1 8c7f028fbabf 1970-01-16 01:06 +0000 person +3[tip]:2,-1 10e46f2dcbf4 1970-01-16 01:06 +0000 person no user, no domain -2:1,-1 259081bc29d1 1970-01-14 21:20 +0000 other +2:1,-1 97054abb4ab8 1970-01-14 21:20 +0000 other no person -1:0,-1 1c37ba774509 1970-01-13 17:33 +0000 other +1:0,-1 b608e9d1a3f0 1970-01-13 17:33 +0000 other other 1 -0:-1,-1 6eb5362d59ec 1970-01-12 13:46 +0000 user +0:-1,-1 1e4e1b8f71e0 1970-01-12 13:46 +0000 user line 1 # error if style not readable @@ -103,30 +103,24 @@ other 2 other 3 - desc: line 1 line 2 - desc--verbose: no user, no domain desc--verbose: no person desc--verbose: other 1 other 2 other 3 - desc--verbose: line 1 line 2 - desc--debug: no user, no domain desc--debug: no person desc--debug: other 1 other 2 other 3 - desc--debug: line 1 line 2 - file_adds: file_adds: file_adds: @@ -175,18 +169,18 @@ manifest--debug: 2:6e0e82995c35 manifest--debug: 1:4e8d705b1e53 manifest--debug: 0:a0c8bcbbb45c -node: 8c7f028fbabf93fde80ef788885370b36abeff33 -node: 259081bc29d176c6ae17af5dd01a3440b3288c97 -node: 1c37ba7745099d0f206b3a663abcfe127b037433 -node: 6eb5362d59ec784e4431d3e140c8cc6e1b77ce82 -node--verbose: 8c7f028fbabf93fde80ef788885370b36abeff33 -node--verbose: 259081bc29d176c6ae17af5dd01a3440b3288c97 -node--verbose: 1c37ba7745099d0f206b3a663abcfe127b037433 -node--verbose: 6eb5362d59ec784e4431d3e140c8cc6e1b77ce82 -node--debug: 8c7f028fbabf93fde80ef788885370b36abeff33 -node--debug: 259081bc29d176c6ae17af5dd01a3440b3288c97 -node--debug: 1c37ba7745099d0f206b3a663abcfe127b037433 -node--debug: 6eb5362d59ec784e4431d3e140c8cc6e1b77ce82 +node: 10e46f2dcbf4823578cf180f33ecf0b957964c47 +node: 97054abb4ab824450e9164180baf491ae0078465 +node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965 +node: 1e4e1b8f71e05681d422154f5421e385fec3454f +node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47 +node--verbose: 97054abb4ab824450e9164180baf491ae0078465 +node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965 +node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f +node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47 +node--debug: 97054abb4ab824450e9164180baf491ae0078465 +node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965 +node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f parents: parents: parents: @@ -195,9 +189,9 @@ parents--verbose: parents--verbose: parents--verbose: -parents--debug: 2:259081bc29d1 -1:000000000000 -parents--debug: 1:1c37ba774509 -1:000000000000 -parents--debug: 0:6eb5362d59ec -1:000000000000 +parents--debug: 2:97054abb4ab8 -1:000000000000 +parents--debug: 1:b608e9d1a3f0 -1:000000000000 +parents--debug: 0:1e4e1b8f71e0 -1:000000000000 parents--debug: -1:000000000000 -1:000000000000 rev: 3 rev: 2 @@ -252,10 +246,10 @@ no person other 1 line 1 -8c7f028fbabf -259081bc29d1 -1c37ba774509 -6eb5362d59ec +10e46f2dcbf4 +97054abb4ab8 +b608e9d1a3f0 +1e4e1b8f71e0 # error on syntax abort: t:3: unmatched quotes # done diff -r c58a403aa830 -r 7cbe8cd69d6b tests/test-globalopts --- a/tests/test-globalopts Fri May 19 08:54:28 2006 -0700 +++ b/tests/test-globalopts Fri May 19 08:57:12 2006 -0700 @@ -62,7 +62,7 @@ hg --cwd a --time tip 2>&1 | grep '^Time:' | sed 's/[0-9][0-9]*/x/g' echo %% --version -hg --version -q | sed 's/version [a-f0-9+]*/version xxx/' +hg --version -q | sed 's/version \([a-f0-9+]*\|unknown\)/version xxx/' echo %% -h/--help hg -h diff -r c58a403aa830 -r 7cbe8cd69d6b tests/test-remove --- a/tests/test-remove Fri May 19 08:54:28 2006 -0700 +++ b/tests/test-remove Fri May 19 08:57:12 2006 -0700 @@ -3,6 +3,7 @@ hg init a cd a echo a > foo +hg rm foo hg add foo hg commit -m 1 -d "1000000 0" hg remove @@ -17,5 +18,15 @@ hg log -p -r 0 hg log -p -r 1 +echo a > a +hg add a +hg rm a +hg rm -f a +echo b > b +hg ci -A -m 3 -d "1000001 0" +echo c >> b +hg rm b +hg rm -f b + cd .. hg clone a b diff -r c58a403aa830 -r 7cbe8cd69d6b tests/test-remove.out --- a/tests/test-remove.out Fri May 19 08:54:28 2006 -0700 +++ b/tests/test-remove.out Fri May 19 08:57:12 2006 -0700 @@ -1,8 +1,10 @@ +not removing foo: file is not managed abort: no files specified undeleting foo removing foo # HG changeset patch # User test +# Date 1000000 0 # Node ID 8ba83d44753d6259db5ce6524974dd1174e90f47 # Parent 0000000000000000000000000000000000000000 1 @@ -14,6 +16,7 @@ +a # HG changeset patch # User test +# Date 1000000 0 # Node ID a1fce69c50d97881c5c014ab23f580f720c78678 # Parent 8ba83d44753d6259db5ce6524974dd1174e90f47 2 @@ -48,4 +51,8 @@ -a -0 files updated, 0 files merged, 0 files removed, 0 files unresolved +not removing a: file has been marked for add (use -f to force removal) +adding a +adding b +not removing b: file is modified (use -f to force removal) +2 files updated, 0 files merged, 0 files removed, 0 files unresolved diff -r c58a403aa830 -r 7cbe8cd69d6b tests/test-tags --- a/tests/test-tags Fri May 19 08:54:28 2006 -0700 +++ b/tests/test-tags Fri May 19 08:57:12 2006 -0700 @@ -32,12 +32,31 @@ hg status hg commit -m "merge" -d "1000000 0" + +# create fake head, make sure tag not visible afterwards +cp .hgtags tags +hg tag -d "1000000 0" last +hg rm .hgtags +hg commit -m "remove" -d "1000000 0" + +mv tags .hgtags +hg add .hgtags +hg commit -m "readd" -d "1000000 0" + +hg tags + # invalid tags echo "spam" >> .hgtags echo >> .hgtags echo "foo bar" >> .hgtags echo "$T invalid" | sed "s/..../a5a5/" >> .hg/localtags hg commit -m "tags" -d "1000000 0" + +# report tag parse error on other head +hg up 3 +echo 'x y' >> .hgtags +hg commit -m "head" -d "1000000 0" + hg tags hg tip diff -r c58a403aa830 -r 7cbe8cd69d6b tests/test-tags.out --- a/tests/test-tags.out Fri May 19 08:54:28 2006 -0700 +++ b/tests/test-tags.out Fri May 19 08:57:12 2006 -0700 @@ -16,17 +16,26 @@ (branch merge, don't forget to commit) 8216907a933d+8a3ca90d111d+ tip M .hgtags -.hgtags:2: ignoring invalid tag -.hgtags:4: ignoring invalid tag -localtags:1: ignoring invalid tag -tip 4:fd868a874787a7b5af31e1675666ce691c803035 +tip 6:c6af9d771a81bb9c7f267ec03491224a9f8ba1cd first 0:0acdaf8983679e0aac16e811534eb49d7ee1f2b4 -changeset: 4:fd868a874787 -.hgtags:2: ignoring invalid tag -.hgtags:4: ignoring invalid tag -localtags:1: ignoring invalid tag +.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry +.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed +localtags, line 1: tag 'invalid' refers to unknown node +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry +.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed +.hgtags (rev 8:4ca6f1b1a68c), line 2: node 'x' is not well formed +localtags, line 1: tag 'invalid' refers to unknown node +tip 8:4ca6f1b1a68c77be687a03aaeb1614671ba59b20 +first 0:0acdaf8983679e0aac16e811534eb49d7ee1f2b4 +changeset: 8:4ca6f1b1a68c +.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry +.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed +.hgtags (rev 8:4ca6f1b1a68c), line 2: node 'x' is not well formed +localtags, line 1: tag 'invalid' refers to unknown node tag: tip +parent: 3:b2ef3841386b user: test date: Mon Jan 12 13:46:40 1970 +0000 -summary: tags +summary: head