changeset 2803:987c31e2a08c

Merge with crew
author Matt Mackall <mpm@selenic.com>
date Mon, 07 Aug 2006 16:47:06 -0500
parents fdc232d8a193 (current diff) df220d0974dd (diff)
children 4b20daa25f15
files hgext/mq.py mercurial/commands.py mercurial/localrepo.py mercurial/merge.py
diffstat 10 files changed, 369 insertions(+), 95 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/fetch.py	Mon Aug 07 16:47:06 2006 -0500
@@ -0,0 +1,93 @@
+# fetch.py - pull and merge remote changes
+#
+# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+from mercurial.demandload import *
+from mercurial.i18n import gettext as _
+from mercurial.node import *
+demandload(globals(), 'mercurial:commands,hg,node,util')
+
+def fetch(ui, repo, source='default', **opts):
+    '''Pull changes from a remote repository, merge new changes if needed.
+
+    This finds all changes from the repository at the specified path
+    or URL and adds them to the local repository.
+
+    If the pulled changes add a new head, the head is automatically
+    merged, and the result of the merge is committed.  Otherwise, the
+    working directory is updated.'''
+
+    def postincoming(other, modheads):
+        if modheads == 0:
+            return 0
+        if modheads == 1:
+            return commands.doupdate(ui, repo)
+        newheads = repo.heads(parent)
+        newchildren = [n for n in repo.heads(parent) if n != parent]
+        newparent = parent
+        if newchildren:
+            commands.doupdate(ui, repo, node=hex(newchildren[0]))
+            newparent = newchildren[0]
+        newheads = [n for n in repo.heads() if n != newparent]
+        err = False
+        if newheads:
+            ui.status(_('merging with new head %d:%s\n') %
+                      (repo.changelog.rev(newheads[0]), short(newheads[0])))
+            err = repo.update(newheads[0], allow=True, remind=False)
+        if not err and len(newheads) > 1:
+            ui.status(_('not merging with %d other new heads '
+                        '(use "hg heads" and "hg merge" to merge them)') %
+                      (len(newheads) - 1))
+        if not err:
+            mod, add, rem = repo.status()[:3]
+            message = (commands.logmessage(opts) or
+                       (_('Automated merge with %s') % other.url()))
+            n = repo.commit(mod + add + rem, message,
+                            opts['user'], opts['date'],
+                            force_editor=opts.get('force_editor'))
+            ui.status(_('new changeset %d:%s merges remote changes '
+                        'with local\n') % (repo.changelog.rev(n),
+                                           short(n)))
+    def pull():
+        commands.setremoteconfig(ui, opts)
+
+        other = hg.repository(ui, ui.expandpath(source))
+        ui.status(_('pulling from %s\n') % source)
+        revs = None
+        if opts['rev'] and not other.local():
+            raise util.Abort(_("fetch -r doesn't work for remote repositories yet"))
+        elif opts['rev']:
+            revs = [other.lookup(rev) for rev in opts['rev']]
+        modheads = repo.pull(other, heads=revs)
+        return postincoming(other, modheads)
+        
+    parent, p2 = repo.dirstate.parents()
+    if parent != repo.changelog.tip():
+        raise util.Abort(_('working dir not at tip '
+                           '(use "hg update" to check out tip)'))
+    if p2 != nullid:
+        raise util.Abort(_('outstanding uncommitted merge'))
+    mod, add, rem = repo.status()[:3]
+    if mod or add or rem:
+        raise util.Abort(_('outstanding uncommitted changes'))
+    if len(repo.heads()) > 1:
+        raise util.Abort(_('multiple heads in this repository '
+                           '(use "hg heads" and "hg merge" to merge them)'))
+    return pull()
+
+cmdtable = {
+    'fetch':
+    (fetch,
+     [('e', 'ssh', '', _('specify ssh command to use')),
+      ('m', 'message', '', _('use <text> as commit message')),
+      ('l', 'logfile', '', _('read the commit message from <file>')),
+      ('d', 'date', '', _('record datecode as commit date')),
+      ('u', 'user', '', _('record user as commiter')),
+      ('r', 'rev', [], _('a specific revision you would like to pull')),
+      ('f', 'force-editor', None, _('edit commit message')),
+      ('', 'remotecmd', '', _('hg command to run on the remote side'))],
+     'hg fetch [SOURCE]'),
+    }
--- a/hgext/mq.py	Mon Aug 07 16:27:09 2006 -0500
+++ b/hgext/mq.py	Mon Aug 07 16:47:06 2006 -0500
@@ -39,6 +39,16 @@
 
 commands.norepo += " qclone qversion"
 
+class StatusEntry:
+    def __init__(self, rev, name=None):
+        if not name:
+            self.rev, self.name = rev.split(':')
+        else:
+            self.rev, self.name = rev, name
+
+    def __str__(self):
+        return self.rev + ':' + self.name
+
 class queue:
     def __init__(self, ui, path, patchdir=None):
         self.basepath = path
@@ -60,7 +70,8 @@
         self.parse_series()
 
         if os.path.exists(os.path.join(self.path, self.status_path)):
-            self.applied = self.opener(self.status_path).read().splitlines()
+            self.applied = [StatusEntry(l)
+                            for l in self.opener(self.status_path).read().splitlines()]
 
     def find_series(self, patch):
         pre = re.compile("(\s*)([^#]+)")
@@ -88,7 +99,7 @@
             for i in items:
                 print >> fp, i
             fp.close()
-        if self.applied_dirty: write_list(self.applied, self.status_path)
+        if self.applied_dirty: write_list(map(str, self.applied), self.status_path)
         if self.series_dirty: write_list(self.full_series, self.series_path)
 
     def readheaders(self, patch):
@@ -209,12 +220,10 @@
                 return p1
             if len(self.applied) == 0:
                 return None
-            (top, patch) = self.applied[-1].split(':')
-            top = revlog.bin(top)
-            return top
+            return revlog.bin(self.applied[-1].rev)
         pp = repo.changelog.parents(rev)
         if pp[1] != revlog.nullid:
-            arevs = [ x.split(':')[0] for x in self.applied ]
+            arevs = [ x.rev for x in self.applied ]
             p0 = revlog.hex(pp[0])
             p1 = revlog.hex(pp[1])
             if p0 in arevs:
@@ -234,7 +243,7 @@
             pname = ".hg.patches.merge.marker"
             n = repo.commit(None, '[mq]: merge marker', user=None, force=1,
                             wlock=wlock)
-            self.applied.append(revlog.hex(n) + ":" + pname)
+            self.applied.append(StatusEntry(revlog.hex(n), pname))
             self.applied_dirty = 1
 
         head = self.qparents(repo)
@@ -252,7 +261,7 @@
             rev = revlog.bin(info[1])
             (err, head) = self.mergeone(repo, mergeq, head, patch, rev, wlock)
             if head:
-                self.applied.append(revlog.hex(head) + ":" + patch)
+                self.applied.append(StatusEntry(revlog.hex(head), patch))
                 self.applied_dirty = 1
             if err:
                 return (err, head)
@@ -263,8 +272,8 @@
         patchfile: file name of patch'''
         try:
             pp = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
-            f = os.popen("%s -d '%s' -p1 --no-backup-if-mismatch < '%s'" %
-                         (pp, repo.root, patchfile))
+            f = os.popen("%s -d %s -p1 --no-backup-if-mismatch < %s" %
+                         (pp, util.shellquote(repo.root), util.shellquote(patchfile)))
         except:
             self.ui.warn("patch failed, unable to continue (try -v)\n")
             return (None, [], False)
@@ -275,11 +284,7 @@
             if self.ui.verbose:
                 self.ui.warn(l + "\n")
             if l[:14] == 'patching file ':
-                pf = os.path.normpath(l[14:])
-                # when patch finds a space in the file name, it puts
-                # single quotes around the filename.  strip them off
-                if pf[0] == "'" and pf[-1] == "'":
-                    pf = pf[1:-1]
+                pf = os.path.normpath(util.parse_patch_output(l))
                 if pf not in files:
                     files.append(pf)
                 printed_file = False
@@ -351,7 +356,7 @@
                 raise util.Abort(_("repo commit failed"))
 
             if update_status:
-                self.applied.append(revlog.hex(n) + ":" + patch)
+                self.applied.append(StatusEntry(revlog.hex(n), patch))
 
             if patcherr:
                 if not patchfound:
@@ -389,8 +394,7 @@
 
     def check_toppatch(self, repo):
         if len(self.applied) > 0:
-            (top, patch) = self.applied[-1].split(':')
-            top = revlog.bin(top)
+            top = revlog.bin(self.applied[-1].rev)
             pp = repo.dirstate.parents()
             if top not in pp:
                 raise util.Abort(_("queue top not at same revision as working directory"))
@@ -421,7 +425,7 @@
         if n == None:
             raise util.Abort(_("repo commit failed"))
         self.full_series[insert:insert] = [patch]
-        self.applied.append(revlog.hex(n) + ":" + patch)
+        self.applied.append(StatusEntry(revlog.hex(n), patch))
         self.parse_series()
         self.series_dirty = 1
         self.applied_dirty = 1
@@ -501,9 +505,9 @@
             # we go in two steps here so the strip loop happens in a
             # sensible order.  When stripping many files, this helps keep
             # our disk access patterns under control.
-            list = seen.keys()
-            list.sort()
-            for f in list:
+            seen_list = seen.keys()
+            seen_list.sort()
+            for f in seen_list:
                 ff = repo.file(f)
                 filerev = seen[f]
                 if filerev != 0:
@@ -535,7 +539,6 @@
         saveheads = []
         savebases = {}
 
-        tip = chlog.tip()
         heads = limitheads(chlog, rev)
         seen = {}
 
@@ -566,7 +569,7 @@
                         savebases[x] = 1
 
         # create a changegroup for all the branches we need to keep
-        if backup is "all":
+        if backup == "all":
             backupch = repo.changegroupsubset([rev], chlog.heads(), 'strip')
             bundle(backupch)
         if saveheads:
@@ -581,16 +584,15 @@
         if saveheads:
             self.ui.status("adding branch\n")
             commands.unbundle(self.ui, repo, chgrpfile, update=False)
-            if backup is not "strip":
+            if backup != "strip":
                 os.unlink(chgrpfile)
 
     def isapplied(self, patch):
         """returns (index, rev, patch)"""
         for i in xrange(len(self.applied)):
-            p = self.applied[i]
-            a = p.split(':')
-            if a[1] == patch:
-                return (i, a[0], a[1])
+            a = self.applied[i]
+            if a.name == patch:
+                return (i, a.rev, a.name)
         return None
 
     # if the exact patch name does not exist, we try a few 
@@ -693,7 +695,7 @@
             ret = self.mergepatch(repo, mergeq, s, wlock)
         else:
             ret = self.apply(repo, s, list, wlock=wlock)
-        top = self.applied[-1].split(':')[1]
+        top = self.applied[-1].name
         if ret[0]:
             self.ui.write("Errors during apply, please fix and refresh %s\n" %
                           top)
@@ -730,7 +732,7 @@
 
         if not update:
             parents = repo.dirstate.parents()
-            rr = [ revlog.bin(x.split(':')[0]) for x in self.applied ]
+            rr = [ revlog.bin(x.rev) for x in self.applied ]
             for p in parents:
                 if p in rr:
                     self.ui.warn("qpop: forcing dirstate update\n")
@@ -751,7 +753,7 @@
             if popi >= end:
                 self.ui.warn("qpop: %s is already at the top\n" % patch)
                 return
-        info = [ popi ] + self.applied[popi].split(':')
+        info = [ popi ] + [self.applied[popi].rev, self.applied[popi].name]
 
         start = info[0]
         rev = revlog.bin(info[1])
@@ -784,7 +786,7 @@
         self.strip(repo, rev, update=False, backup='strip', wlock=wlock)
         del self.applied[start:end]
         if len(self.applied):
-            self.ui.write("Now at: %s\n" % self.applied[-1].split(':')[1])
+            self.ui.write("Now at: %s\n" % self.applied[-1].name)
         else:
             self.ui.write("Patch queue now empty\n")
 
@@ -802,8 +804,7 @@
             return
         wlock = repo.wlock()
         self.check_toppatch(repo)
-        qp = self.qparents(repo)
-        (top, patch) = self.applied[-1].split(':')
+        (top, patch) = (self.applied[-1].rev, self.applied[-1].name)
         top = revlog.bin(top)
         cparents = repo.changelog.parents(top)
         patchparent = self.qparents(repo, top)
@@ -899,7 +900,7 @@
 
             self.strip(repo, top, update=False, backup='strip', wlock=wlock)
             n = repo.commit(filelist, message, changes[1], force=1, wlock=wlock)
-            self.applied[-1] = revlog.hex(n) + ':' + patch
+            self.applied[-1] = StatusEntry(revlog.hex(n), patch)
             self.applied_dirty = 1
         else:
             commands.dodiff(patchf, self.ui, repo, patchparent, None)
@@ -921,10 +922,7 @@
             start = self.series_end()
         else:
             start = self.series.index(patch) + 1
-        for p in self.series[start:]:
-            if self.ui.verbose:
-                self.ui.write("%d " % self.series.index(p))
-            self.ui.write("%s\n" % p)
+        return [(i, self.series[i]) for i in xrange(start, len(self.series))]
 
     def qseries(self, repo, missing=None, summary=False):
         start = self.series_end()
@@ -944,7 +942,7 @@
                     msg = ''
                 self.ui.write('%s%s\n' % (patch, msg))
         else:
-            list = []
+            msng_list = []
             for root, dirs, files in os.walk(self.path):
                 d = root[len(self.path) + 1:]
                 for f in files:
@@ -952,13 +950,12 @@
                     if (fl not in self.series and
                         fl not in (self.status_path, self.series_path)
                         and not fl.startswith('.')):
-                        list.append(fl)
-            list.sort()
-            if list:
-                for x in list:
-                    if self.ui.verbose:
-                        self.ui.write("D ")
-                    self.ui.write("%s\n" % x)
+                        msng_list.append(fl)
+            msng_list.sort()
+            for x in msng_list:
+                if self.ui.verbose:
+                    self.ui.write("D ")
+                self.ui.write("%s\n" % x)
 
     def issaveline(self, l):
         name = l.split(':')[1]
@@ -987,12 +984,11 @@
                 qpp = [ hg.bin(x) for x in l ]
             elif datastart != None:
                 l = lines[i].rstrip()
-                index = l.index(':')
-                id = l[:index]
-                file = l[index + 1:]
-                if id:
-                    applied.append(l)
-                series.append(file)
+                se = StatusEntry(l)
+                file_ = se.name
+                if se.rev:
+                    applied.append(se)
+                series.append(file_)
         if datastart == None:
             self.ui.warn("No saved patch data found\n")
             return 1
@@ -1043,18 +1039,18 @@
             pp = r.dirstate.parents()
             msg += "\nDirstate: %s %s" % (hg.hex(pp[0]), hg.hex(pp[1]))
         msg += "\n\nPatch Data:\n"
-        text = msg + "\n".join(self.applied) + '\n' + (ar and "\n".join(ar)
+        text = msg + "\n".join(str(self.applied)) + '\n' + (ar and "\n".join(ar)
                                                        + '\n' or "")
         n = repo.commit(None, text, user=None, force=1)
         if not n:
             self.ui.warn("repo commit failed\n")
             return 1
-        self.applied.append(revlog.hex(n) + ":" + '.hg.patches.save.line')
+        self.applied.append(StatusEntry(revlog.hex(n),'.hg.patches.save.line'))
         self.applied_dirty = 1
 
     def full_series_end(self):
         if len(self.applied) > 0:
-            (top, p) = self.applied[-1].split(':')
+            p = self.applied[-1].name
             end = self.find_series(p)
             if end == None:
                 return len(self.full_series)
@@ -1064,7 +1060,7 @@
     def series_end(self):
         end = 0
         if len(self.applied) > 0:
-            (top, p) = self.applied[-1].split(':')
+            p = self.applied[-1].name
             try:
                 end = self.series.index(p)
             except ValueError:
@@ -1084,8 +1080,7 @@
             self.ui.write("%s\n" % p)
 
     def appliedname(self, index):
-        p = self.applied[index]
-        pname = p.split(':')[1]
+        pname = self.applied[index].name
         if not self.ui.verbose:
             p = pname
         else:
@@ -1173,8 +1168,10 @@
 
 def unapplied(ui, repo, patch=None, **opts):
     """print the patches not yet applied"""
-    repo.mq.unapplied(repo, patch)
-    return 0
+    for i, p in repo.mq.unapplied(repo, patch):
+        if ui.verbose:
+            ui.write("%d " % i)
+        ui.write("%s\n" % p)
 
 def qimport(ui, repo, *filename, **opts):
     """import a patch"""
@@ -1223,7 +1220,7 @@
     if sr.local():
         reposetup(ui, sr)
         if sr.mq.applied:
-            qbase = revlog.bin(sr.mq.applied[0].split(':')[0])
+            qbase = revlog.bin(sr.mq.applied[0].rev)
             if not hg.islocal(dest):
                 destrev = sr.parents(qbase)[0]
     ui.note(_('cloning main repo\n'))
@@ -1286,7 +1283,7 @@
     If neither is specified, the patch header is empty and the
     commit message is 'New patch: PATCH'"""
     q = repo.mq
-    message=commands.logmessage(**opts)
+    message = commands.logmessage(**opts)
     q.new(repo, patch, msg=message, force=opts['force'])
     q.save_dirty()
     return 0
@@ -1294,11 +1291,11 @@
 def refresh(ui, repo, **opts):
     """update the current patch"""
     q = repo.mq
-    message=commands.logmessage(**opts)
+    message = commands.logmessage(**opts)
     if opts['edit']:
         if message:
             raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
-        patch = q.applied[-1].split(':')[1]
+        patch = q.applied[-1].name
         (message, comment, user, date, hasdiff) = q.readheaders(patch)
         message = ui.edit('\n'.join(message), user or ui.username())
     q.refresh(repo, msg=message, short=opts['short'])
@@ -1331,7 +1328,7 @@
     if not q.check_toppatch(repo):
         raise util.Abort(_('No patches applied\n'))
 
-    message=commands.logmessage(**opts)
+    message = commands.logmessage(**opts)
     if opts['edit']:
         if message:
             raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
@@ -1342,7 +1339,7 @@
     for f in files:
         patch = q.lookup(f)
         if patch in patches or patch == parent:
-            self.ui.warn(_('Skipping already folded patch %s') % patch)
+            ui.warn(_('Skipping already folded patch %s') % patch)
         if q.isapplied(patch):
             raise util.Abort(_('qfold cannot fold already applied patch %s') % patch)
         patches.append(patch)
@@ -1388,20 +1385,20 @@
     ui.write('\n'.join(message) + '\n')
 
 def lastsavename(path):
-    (dir, base) = os.path.split(path)
-    names = os.listdir(dir)
+    (directory, base) = os.path.split(path)
+    names = os.listdir(directory)
     namere = re.compile("%s.([0-9]+)" % base)
-    max = None
+    maxindex = None
     maxname = None
     for f in names:
         m = namere.match(f)
         if m:
             index = int(m.group(1))
-            if max == None or index > max:
-                max = index
+            if maxindex == None or index > maxindex:
+                maxindex = index
                 maxname = f
     if maxname:
-        return (os.path.join(dir, maxname), max)
+        return (os.path.join(directory, maxname), maxindex)
     return (None, None)
 
 def savename(path):
@@ -1482,7 +1479,7 @@
 
     info = q.isapplied(patch)
     if info:
-        q.applied[info[0]] = info[1] + ':' + name
+        q.applied[info[0]] = StatusEntry(info[1], name)
     q.applied_dirty = 1
 
     util.rename(os.path.join(q.path, patch), absdest)
@@ -1508,7 +1505,7 @@
 def save(ui, repo, **opts):
     """save current queue state"""
     q = repo.mq
-    message=commands.logmessage(**opts)
+    message = commands.logmessage(**opts)
     ret = q.save(repo, msg=message)
     if ret:
         return ret
@@ -1563,7 +1560,7 @@
             if not q.applied:
                 return tagscache
 
-            mqtags = [patch.split(':') for patch in q.applied]
+            mqtags = [(patch.rev, patch.name) for patch in q.applied]
             mqtags.append((mqtags[-1][0], 'qtip'))
             mqtags.append((mqtags[0][0], 'qbase'))
             for patch in mqtags:
--- a/hgext/notify.py	Mon Aug 07 16:27:09 2006 -0500
+++ b/hgext/notify.py	Mon Aug 07 16:47:06 2006 -0500
@@ -255,7 +255,7 @@
     changegroup. else send one email per changeset.'''
     n = notifier(ui, repo, hooktype)
     if not n.subs:
-        ui.debug(_('notify: no subscribers to this repo\n'))
+        ui.debug(_('notify: no subscribers to repo %s\n' % n.root))
         return
     if n.skipsource(source):
         ui.debug(_('notify: changes have source "%s" - skipping\n') %
--- a/hgext/patchbomb.py	Mon Aug 07 16:27:09 2006 -0500
+++ b/hgext/patchbomb.py	Mon Aug 07 16:47:06 2006 -0500
@@ -288,7 +288,8 @@
             fp.close()
         else:
             ui.status('Sending ', m['Subject'], ' ...\n')
-            m.__delitem__('bcc')
+            # Exim does not remove the Bcc field
+            del m['Bcc']
             mail.sendmail(sender, to + bcc + cc, m.as_string(0))
 
 cmdtable = {
--- a/mercurial/commands.py	Mon Aug 07 16:27:09 2006 -0500
+++ b/mercurial/commands.py	Mon Aug 07 16:47:06 2006 -0500
@@ -40,7 +40,7 @@
         return [util.normpath(os.path.join(cwd, x)) for x in args]
     return args
 
-def logmessage(**opts):
+def logmessage(opts):
     """ get the log message according to -m and -l option """
     message = opts['message']
     logfile = opts['logfile']
@@ -125,12 +125,22 @@
 
 
     files, matchfn, anypats = matchpats(repo, pats, opts)
-    follow = opts.get('follow')
+    follow = opts.get('follow') or opts.get('follow_first')
 
     if repo.changelog.count() == 0:
         return [], False, matchfn
 
-    revs = map(int, revrange(ui, repo, opts['rev'] or ['tip:0']))
+    if follow:
+        p = repo.dirstate.parents()[0]
+        if p == nullid:
+            ui.warn(_('No working directory revision; defaulting to tip\n'))
+            start = 'tip'
+        else:
+            start = repo.changelog.rev(p)
+        defrange = '%s:0' % start
+    else:
+        defrange = 'tip:0'
+    revs = map(int, revrange(ui, repo, opts['rev'] or [defrange]))
     wanted = {}
     slowpath = anypats
     fncache = {}
@@ -206,10 +216,55 @@
                 wanted[rev] = 1
 
     def iterate():
+        class followfilter:
+            def __init__(self, onlyfirst=False):
+                self.startrev = -1
+                self.roots = []
+                self.onlyfirst = onlyfirst
+
+            def match(self, rev):
+                def realparents(rev):
+                    if self.onlyfirst:
+                        return repo.changelog.parentrevs(rev)[0:1]
+                    else:
+                        return filter(lambda x: x != -1, repo.changelog.parentrevs(rev))
+
+                if self.startrev == -1:
+                    self.startrev = rev
+                    return True
+
+                if rev > self.startrev:
+                    # forward: all descendants
+                    if not self.roots:
+                        self.roots.append(self.startrev)
+                    for parent in realparents(rev):
+                        if parent in self.roots:
+                            self.roots.append(rev)
+                            return True
+                else:
+                    # backwards: all parents
+                    if not self.roots:
+                        self.roots.extend(realparents(self.startrev))
+                    if rev in self.roots:
+                        self.roots.remove(rev)
+                        self.roots.extend(realparents(rev))
+                        return True
+
+                return False
+
+        if follow and not files:
+            ff = followfilter(onlyfirst=opts.get('follow_first'))
+            def want(rev):
+                if rev not in wanted:
+                    return False
+                return ff.match(rev)
+        else:
+            def want(rev):
+                return rev in wanted
+
         for i, window in increasing_windows(0, len(revs)):
             yield 'window', revs[0] < revs[-1], revs[-1]
-            nrevs = [rev for rev in revs[i:i+window]
-                     if rev in wanted]
+            nrevs = [rev for rev in revs[i:i+window] if want(rev)]
             srevs = list(nrevs)
             srevs.sort()
             for rev in srevs:
@@ -1041,7 +1096,7 @@
     If no commit message is specified, the editor configured in your hgrc
     or in the EDITOR environment variable is started to enter a message.
     """
-    message = logmessage(**opts)
+    message = logmessage(opts)
 
     if opts['addremove']:
         addremove_lock(ui, repo, pats, opts)
@@ -1972,8 +2027,14 @@
     project.
 
     File history is shown without following rename or copy history of
-    files.  Use -f/--follow to follow history across renames and
-    copies.
+    files.  Use -f/--follow with a file name to follow history across
+    renames and copies. --follow without a file name will only show
+    ancestors or descendants of the starting revision. --follow-first
+    only follows the first parent of merge revisions.
+
+    If no revision range is specified, the default is tip:0 unless
+    --follow is set, in which case the working directory parent is
+    used as the starting revision.
 
     By default this command outputs: changeset id and hash, tags,
     non-trivial parents, user, date and time, and a summary for each
@@ -2728,8 +2789,8 @@
     necessary.  The file '.hg/localtags' is used for local tags (not
     shared among repositories).
     """
-    if name == "tip":
-        raise util.Abort(_("the name 'tip' is reserved"))
+    if name in ['tip', '.']:
+        raise util.Abort(_("the name '%s' is reserved") % name)
     if rev_ is not None:
         ui.warn(_("use of 'hg tag NAME [REV]' is deprecated, "
                   "please use 'hg tag [-r REV] NAME' instead\n"))
@@ -3087,7 +3148,9 @@
         (log,
          [('b', 'branches', None, _('show branches')),
           ('f', 'follow', None,
-           _('follow file history across copies and renames')),
+           _('follow changeset history, or file history across copies and renames')),
+          ('', 'follow-first', None,
+           _('only follow the first parent of merge changesets')),
           ('k', 'keyword', [], _('search for a keyword')),
           ('l', 'limit', '', _('limit number of changes displayed')),
           ('r', 'rev', [], _('show the specified revision or range')),
--- a/mercurial/localrepo.py	Mon Aug 07 16:27:09 2006 -0500
+++ b/mercurial/localrepo.py	Mon Aug 07 16:47:06 2006 -0500
@@ -292,6 +292,10 @@
         try:
             return self.tags()[key]
         except KeyError:
+            if key == '.':
+                key = self.dirstate.parents()[0]
+                if key == nullid:
+                    raise repo.RepoError(_("no revision checked out"))
             try:
                 return self.changelog.lookup(key)
             except:
@@ -1693,6 +1697,7 @@
 
         return newheads - oldheads + 1
 
+
     def stream_in(self, remote):
         fp = remote.stream_out()
         resp = int(fp.readline())
--- a/mercurial/merge.py	Mon Aug 07 16:27:09 2006 -0500
+++ b/mercurial/merge.py	Mon Aug 07 16:47:06 2006 -0500
@@ -48,7 +48,8 @@
     return r
 
 def update(repo, node, allow=False, force=False, choose=None,
-           moddirstate=True, forcemerge=False, wlock=None, show_stats=True):
+           moddirstate=True, forcemerge=False, wlock=None, show_stats=True,
+           remind=True):
     pl = repo.dirstate.parents()
     if not force and pl[1] != nullid:
         raise util.Abort(_("outstanding uncommitted merges"))
@@ -337,7 +338,7 @@
                                 "  hg merge %s\n"
                                 % (repo.changelog.rev(p1),
                                     repo.changelog.rev(p2))))
-            else:
+            elif remind:
                 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
         elif failedmerge:
             repo.ui.status(_("There are unresolved merges with"
--- a/mercurial/util.py	Mon Aug 07 16:27:09 2006 -0500
+++ b/mercurial/util.py	Mon Aug 07 16:47:06 2006 -0500
@@ -99,9 +99,9 @@
     patcher = find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
     args = []
     if cwd:
-        args.append('-d "%s"' % cwd)
-    fp = os.popen('%s %s -p%d < "%s"' % (patcher, ' '.join(args), strip,
-                                         patchname))
+        args.append('-d %s' % shellquote(cwd))
+    fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
+                                       shellquote(patchname)))
     files = {}
     for line in fp:
         line = line.rstrip()
@@ -611,6 +611,9 @@
     def samestat(s1, s2):
         return False
 
+    def shellquote(s):
+        return '"%s"' % s.replace('"', '\\"')
+
     def explain_exit(code):
         return _("exited with status %d") % code, code
 
@@ -700,6 +703,9 @@
             else:
                 raise
 
+    def shellquote(s):
+        return "'%s'" % s.replace("'", "'\\''")
+
     def testpid(pid):
         '''return False if pid dead, True if running or not sure'''
         try:
--- a/tests/test-log	Mon Aug 07 16:27:09 2006 -0500
+++ b/tests/test-log	Mon Aug 07 16:47:06 2006 -0500
@@ -28,3 +28,38 @@
 hg log -vf a
 echo % many renames
 hg log -vf e
+
+# log --follow tests
+hg init ../follow
+cd ../follow
+echo base > base
+hg ci -Ambase -d '1 0'
+
+echo r1 >> base
+hg ci -Amr1 -d '1 0'
+echo r2 >> base
+hg ci -Amr2 -d '1 0'
+
+hg up -C 1
+echo b1 > b1
+hg ci -Amb1 -d '1 0'
+
+echo % log -f
+hg log -f
+
+hg up -C 0
+echo b2 > b2
+hg ci -Amb2 -d '1 0'
+
+echo % log -f -r 1:tip
+hg log -f -r 1:tip
+
+hg up -C 3
+hg merge tip
+hg ci -mm12 -d '1 0'
+
+echo postm >> b1
+hg ci -Amb1.1 -d'1 0'
+
+echo % log --follow-first
+hg log --follow-first
--- a/tests/test-log.out	Mon Aug 07 16:27:09 2006 -0500
+++ b/tests/test-log.out	Mon Aug 07 16:47:06 2006 -0500
@@ -76,3 +76,76 @@
 a
 
 
+adding base
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+adding b1
+% log -f
+changeset:   3:e62f78d544b4
+tag:         tip
+parent:      1:3d5bf5654eda
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     b1
+
+changeset:   1:3d5bf5654eda
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     r1
+
+changeset:   0:67e992f2c4f3
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     base
+
+1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+adding b2
+% log -f -r 1:tip
+changeset:   1:3d5bf5654eda
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     r1
+
+changeset:   2:60c670bf5b30
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     r2
+
+changeset:   3:e62f78d544b4
+parent:      1:3d5bf5654eda
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     b1
+
+2 files updated, 0 files merged, 1 files removed, 0 files unresolved
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+% log --follow-first
+changeset:   6:2404bbcab562
+tag:         tip
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     b1.1
+
+changeset:   5:302e9dd6890d
+parent:      3:e62f78d544b4
+parent:      4:ddb82e70d1a1
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     m12
+
+changeset:   3:e62f78d544b4
+parent:      1:3d5bf5654eda
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     b1
+
+changeset:   1:3d5bf5654eda
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     r1
+
+changeset:   0:67e992f2c4f3
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     base
+