Commits

Anonymous committed 71882f8

initial release

Comments (0)

Files changed (131)

+syntax: glob
+**.pyc
+**.pyd
+__version__.py
+html/
+build/
+dist/
+Output/
+**.swp
+**.coverage*
+**.orig
+8e55adf9b4887c3106e9a2e10d6b528cf4603125 1.2
+33d72a926c3f0443857cefc77fbe196cad15e9c9 1.2.1
+12efc40d6c50371e6a349040d5df5895d989b154 1.2.2
+9007e96a49241f1ce09dfea3a2bb9e7a6a92de36 1.2.3
+5f8636f6338357ed805f6a07cdb8db63b3724119 1.2.4
+7f62e281675b18a477501b72d837c079c580c4a9 1.2.5
+112f5d54058533929c7194295081d5e9af6004eb 1.2.6
+e7b2c70c286126eb0f743a7436fb0d31f3a633ab 1.2.7
+0549263ee163487aeddd656989a06b170a3968f6 1.2.8
+600b1098f0c83f61d4aab6ce686479cf0aed8095 1.3
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+Repoman
+=======
+
+Repoman is a Mercurial-based repository forest manager.
+
+Requirements:
+Python 2.6 or later
+Mercurial 2.2
+Hgtk from here: http://bitbucket.org/hstuart/hgtk#hgtk-default
+
+For Windows also:
+linkd.exe (find it on Google)
+[MASTER]
+ignore=.hg
+persistent=yes
+cache-size=500
+load-plugins=
+
+[REPORTS]
+reports=yes
+html=no
+parseable=no
+color=no
+files-output=no
+include-ids=yes
+
+[BASIC]
+good-names=ui,f
+
+[FORMAT]
+max-line-length=100
+
+[MESSAGES CONTROL]
+disable-msg=W0613,W0142
+#!/usr/bin/env python
+
+# Copyright 2009-2012 Edlund A/S
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+'''
+Edlund repository manager
+'''
+
+import sys
+
+from mercurial import demandimport
+demandimport.enable()
+import mercurial.util
+
+import repoman.dispatch
+
+for fp in (sys.stdin, sys.stdout, sys.stderr):
+    mercurial.util.setbinary(fp)
+
+repoman.dispatch.run(sys.argv[1:])
+#!/bin/sh
+
+# Copyright 2009-2012 Edlund A/S
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+${REPOWITHCOVERAGE} --simpleui $*

repoext/__init__.py

Empty file added.

repoext/archive.py

+# Copyright 2009-2012 Edlund A/S
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+'''
+command to create an unversioned archive of all modules of a forest
+
+This extension allows you to export an unversioned archive of a forest to a
+directory.
+'''
+
+import os
+
+import mercurial.commands 
+import mercurial.util
+
+from repoman import util
+
+
+def archive(ui, f, dest, **opts):
+    '''create an unversioned archive of all modules of a forest
+
+    The revisions used are the parents of the working directories of 
+    each of the modules in the forest. To archive different 
+    revisions, update each module individually to the desired 
+    revision before archiving.  
+
+    DESTINATION should be a non-existing or empty directory. 
+    '''
+    # check destination. Create or abort if necessary.
+    dest = os.path.abspath(dest)
+    if not os.path.exists(dest):
+        os.makedirs(dest)
+    elif not os.path.isdir(dest):
+        raise util.Abort('destination is not a directory')
+    elif os.listdir(dest):
+        raise util.Abort('destination directory is not empty')
+
+    # archival status file buffer
+    archival = [] 
+
+    # perform mercurial archive for all modules
+    for mod in f:
+        repo = f[mod]
+        module_dest = os.path.join(dest, mod)
+        mercurial.commands.archive(ui, repo, module_dest,
+                **util.get_mercurial_default_opts('archive'))
+
+        # read (and remove) archival status into buffer
+        hg_file_path = os.path.join(module_dest, '.hg_archival.txt')
+        with open(hg_file_path, 'r') as hg_file:
+            archival.append('mod: %s\n%s' % (mod, hg_file.read()))
+        mercurial.util.unlink(hg_file_path)
+
+    # write archival status to file
+    with open(os.path.join(dest, '.repo_archival.txt'), 'w') as repo_file:
+        repo_file.write('----------------------------------------------\n'.join(archival))
+
+
+
+cmdtable = {
+        'archive':
+            (archive,
+             [],
+             [],
+             'DESTINATION')
+        }

repoman/__init__.py

+# Copyright 2009-2012 Edlund A/S
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+'''
+The repoman library defines the core functionality of working with
+several modules in a coherent manner.
+'''

repoman/cmdutil.py

+# vim: set fileencoding=utf-8 :
+#
+# Copyright 2009-2012 Edlund A/S
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+'''
+Repoman
+Command utility functions
+'''
+
+
+import codecs
+import os.path
+import re
+import datetime
+import tempfile
+import shutil
+import urllib2
+
+import mercurial.hg
+import mercurial.commands
+import mercurial.encoding
+from mercurial.i18n import _
+from mercurial.node import nullid
+from mercurial import url
+
+from repoman.forest import Forest
+from repoman import util
+from repoman.util import AmbiguousCommand, UnknownCommand
+
+# TODO: Avoid using string-matching like this if possible.
+nullidstr = '0' * 40
+
+
+def modstr_tmpl(ui, modules, metadata=False, inout=False):
+    maxlen = modules and max([len(mod) for mod in modules]) or 0
+    if metadata:
+        if inout:
+            maxlen = max(maxlen, 14)
+        else:
+            maxlen = max(maxlen, 10)
+    return ui.emph_str('%%-%ds' % maxlen) + '  '
+
+def run_editor(forest, text):
+    olddir = os.getcwd()
+    os.chdir(forest.root)
+    text = forest.ui.edit("\n".join(text), forest.ui.username())
+    text = re.sub("(?m)^HG:.*\n", "", text)
+    os.chdir(olddir)
+    if not text.strip():
+        raise util.Abort(_("empty commit message"))
+    return text
+
+def get_commit_message(forest, text=[''], extra=[]):
+    text.extend([
+        '',
+        _("HG: Enter commit message.  Lines beginning with 'HG:' are removed."),
+        _("HG: Leave message empty to abort commit."),
+        "HG: --",
+        _("HG: user: %s") % forest.ui.username()])
+    if extra:
+        text.extend(extra)
+    text.append('')
+    return run_editor(forest, text)
+
+def get_branch_name_re_txt():
+    return ur'^([\.a-zæøå0-9]([-\.,_a-zæøå0-9]*[a-zæøå0-9])?/)*([a-zæøå0-9]([-\.,_a-zæøå0-9]*[a-zæøå0-9])?)$'
+
+def get_branch_name_re_errorhelp(name):
+    return '%s: branch names must be lowercase, slash separated strings' % name
+
+_branch_name_re = None
+
+def parse_branches(names, exact=False, current=None, default=None):
+    if not names:
+        if default is None:
+            b = [current] if current else names
+            if not exact:
+                return b, False
+            return b
+        else:
+            names = default
+
+    global _branch_name_re
+    if not _branch_name_re:
+        _branch_name_re = re.compile(get_branch_name_re_txt(), re.UNICODE)
+    if isinstance(names, str):
+        names = (names,)
+    newnames = []
+    for n in names:
+        if n == '*':
+            if exact:
+                raise util.Abort('"*" (all branches) is not allowed in this context')
+            return [], True
+        if n == '.':
+            if not current:
+                raise util.Abort('"." (current branch) is not allowed in this context')
+            n = current
+        elif not _branch_name_re.match(util.tounicode(n)):
+            raise util.Abort('%s: branch names must be lowercase, slash separated strings' % n)
+        if n not in newnames:
+            newnames.append(n)
+    if not exact:
+        return newnames, False
+    return newnames
+
+def build_alias_map(table):
+    aliases = {}
+    for c in table.iteritems():
+        for a in c[1][1]:
+            aliases[a] = c
+    return aliases
+
+def findcmd(cmd, table, aliases):
+    if aliases.has_key(cmd):
+        return aliases[cmd]
+    else:
+        cmds = []
+        for c in table.iteritems():
+            if(c[0].startswith(cmd)):
+                cmds.append(c)
+        if(len(cmds) == 1):
+            return cmds[0]
+        if(len(cmds) > 1):
+            raise AmbiguousCommand(cmd, cmds)
+        else:
+            raise UnknownCommand(cmd)
+
+def repository(ui, source, name):
+    return mercurial.hg.repository(ui, '%s/%s' % (source, name))
+
+def perform_clone(ui, source, name, base_dest, update=True, hide=False, rev=None):
+    dest = os.path.join(base_dest, name)
+    mercurial.hg.clone(ui, {}, '%s/%s' % (source, name), dest, update=update, rev=rev)
+    if hide:
+        util.hide(dest)
+
+def preclone(ui, source, dest, symlink=False):
+    ui.status('Cloning metadata\n')
+    util.mkdir_rec(dest)
+    lmd = '.repo'
+    gmd = '.root'
+    fui = ui.copy()
+    fui.filter = ''
+    perform_clone(fui, source, lmd, dest, hide=True)
+    if symlink:
+        destpath = os.path.join(dest, gmd)
+        if not util.symlink(os.path.join(source, gmd), destpath):
+            raise util.Abort('could not create symbolic link for forest metadata; check permissions')
+        util.hide(destpath)
+    else:
+        perform_clone(fui, source, gmd, dest, hide=True)
+    f = Forest(ui, dest)
+    f.paths['clone'] = '.' if util.is_local(source) and not os.path.isabs(source) else source
+    paths = relativize_paths(source, dest, f.paths)
+    f.paths.clear()
+    f.paths.update(paths)
+    return f
+
+def get_remote_info(ui, source):
+    '''
+    return information about a remote forest with minimal data transfer.
+    currently, it returns the list of modules in the remote forest.
+    '''
+    tmpdir = tempfile.mkdtemp()
+    lmd = '.repo'
+    with silentui(ui):
+        perform_clone(ui, source, lmd, tmpdir)
+        modules = util.read_utf8(os.path.join(tmpdir, lmd, 'modules')).split()
+    shutil.rmtree(tmpdir, ignore_errors=True)
+    return modules
+
+def pull(ui, repo, source, **opts):
+    '''Pull changes from a remote repository.
+
+    Custom version of mercurial.commands.pull.'''
+
+    # TODO: allow passing nodes in rev, skipping the extra lookup below.
+
+    source, branches = mercurial.hg.parseurl(ui.expandpath(source), opts.get('branch'))
+    other = opts.get('other') or mercurial.hg.repository(mercurial.hg.remoteui(repo, opts), source)
+    revs, checkout = mercurial.hg.addbranchrevs(repo, other, branches, opts.get('rev'))
+    if revs:
+        revs = [other.lookup(rev) for rev in revs]
+    modheads = repo.pull(other, heads=revs, force=opts.get('force'))
+    if opts.get('update'):
+        rv = mercurial.commands.postincoming(ui, repo, modheads, True, checkout)
+    else:
+        rv = modheads
+    if opts['rebase'] and len(repo.heads()) > 1:
+        rebase_mod = util.get_mercurial_extension(ui, 'rebase')
+        with quietui(ui, repo.ui, filter='saving bundle to '):
+            rebase_mod.rebase(ui, repo)
+    return rv
+
+def pull_metadata(forest, source=None, terse=False):
+    '''
+    update global metadata by pulling in from the given (or default) source.
+    '''
+    ui = forest.ui
+    if not terse:
+        ui.write_module_header('(metadata)')
+    tersefilter = '|\d+ files updated' if terse else ''
+    repo = forest.gmd_repo
+    with filterui(re.compile('^(pulling from |comparing with |searching for changes|adding |\\(run \'hg |not updating%s)' %
+                             tersefilter), ui, repo.ui):
+        pull(ui, repo, forest.getsource(forest.GMD, source), style='', template='',
+             rev=None, force=None, bundle=None, update=True, rebase=True)
+    if not terse:
+        ui.write('\n')
+
+def combine_changesets(repo, ctxs):
+    '''
+    combine the dirstate with a list of contexts, which must be ordered from
+    newest to oldest (the wdir context must not be included).
+    '''
+
+    # Prepare
+    p1, p2 = ctxs[-1].p1(), ctxs[-1].p2()
+    ctxs = [p1] + list(reversed(ctxs)) + [repo[None]]
+    modified, added, removed, forget = set(), set(), set(), set()
+    copies = dict()
+    wlock = repo.wlock()
+    try:
+
+        # Calculate cumulative changes
+        numctxs = len(ctxs)
+        for i in xrange(1, numctxs):
+            ctx = ctxs[i]
+            mod, add, rem = repo.status(ctxs[i-1], ctx)[:3]
+            for f in add:
+                if f in removed:
+                    modified.add(f)
+                    removed.discard(f)
+                else:
+                    added.add(f)
+                    forget.discard(f)
+                    copy = ctx[f].renamed()
+                    if copy:
+                        copy = copy[0]
+                        while copy in copies:
+                            copy = copies[copy]
+                        copies[f] = copy
+            for f in rem:
+                if f not in added:
+                    modified.discard(f)
+                    removed.add(f)
+                else:
+                    added.discard(f)
+                    forget.add(f)
+                    if f in copies:
+                        del copies[f]
+            for f in mod:
+                if f not in added:
+                    modified.add(f)
+
+        # Remove copied files where the source is lost
+        for f in [dst for dst, src in copies.iteritems() if src not in p1]:
+            del copies[f]
+
+        # Adjust dirstate
+        dirstate = repo.dirstate
+        dirstate.setparents(p1.node(), p2.node())
+        for f in added:
+            dirstate.add(f)
+            src = copies.get(f)
+            if src:
+                dirstate.copy(src, f)
+        for f in removed:
+            dirstate.remove(f)
+        for f in modified:
+            pl = dirstate._pl
+            dirstate._pl = (pl[0], 'temp' if pl[1] == nullid else pl[1])
+            dirstate.otherparent(f)
+            dirstate._pl = pl
+        for f in forget:
+            if f in dirstate:
+                dirstate.drop(f)
+
+        dirstate.write()
+
+    finally:
+        wlock.release()
+
+mq_mod = None
+mq_opts = None
+
+def strip(ui, repo, *nodes):
+    global mq_mod, mq_opts
+    if not mq_mod:
+        mq_mod = util.get_mercurial_extension(ui, 'mq')
+        mq_opts = util.get_mercurial_default_opts('strip', mq_mod.cmdtable)
+    with quietui(ui, repo.ui, filter='saving bundle to '):
+        for node in nodes:
+            mq_mod.strip(ui, repo, str(node), **mq_opts)
+
+def relativize_paths(source, dest, paths):
+    newpaths = {}
+    local = util.is_local(source)
+    for k, p in paths.iteritems():
+        if local:
+            if util.is_local(p) and not os.path.isabs(p):
+                p = os.path.relpath(source + '/' + p, dest)
+            newpaths[k] = p
+        else:
+            if not util.is_local(p):
+                newpaths[k] = p
+    return newpaths
+
+class filterui(object):
+    def __init__(self, filter, *ui):
+        self.ui = ui
+        self.filter = filter
+        if filter:
+            self.orig_f = [u.filter for u in ui]
+
+    def __enter__(self):
+        for u in self.ui:
+            u.filter = self.filter
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        for i in xrange(len(self.ui)):
+            self.ui[i].filter = self.orig_f[i]
+
+class silentui(object):
+    def __init__(self, *ui, **kwargs):
+        self.ui = ui
+        self.orig = [u.silent for u in ui]
+        self.filter = kwargs.get('filter')
+        if self.filter:
+            self.orig_f = [u.filter for u in ui]
+
+    def __enter__(self):
+        for u in self.ui:
+            if self.filter:
+                u.filter = self.filter
+            if not (u.debugflag or u.verbose):
+                u.silent = True
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        for i in xrange(len(self.ui)):
+            self.ui[i].silent = self.orig[i]
+            if self.filter:
+                self.ui[i].filter = self.orig_f[i]
+
+class quietui(object):
+    def __init__(self, *ui, **kwargs):
+        self.ui = ui
+        self.orig = [u.quiet for u in ui]
+        self.filter = kwargs.get('filter')
+        if self.filter:
+            self.orig_f = [u.filter for u in ui]
+
+    def __enter__(self):
+        for u in self.ui:
+            if self.filter:
+                u.filter = self.filter
+            if not (u.debugflag or u.verbose):
+                u.quiet = True
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        for i in xrange(len(self.ui)):
+            self.ui[i].quiet = self.orig[i]
+            if self.filter:
+                self.ui[i].filter = self.orig_f[i]
+
+def get_headmaps(f, modules, current, branches):
+    '''returns module -> branch -> [heads]'''
+    headmaps = None
+    if current:
+        headmaps = {}
+        for mod in modules:
+            ctx = f[mod]['.']
+            headmaps[mod] = {ctx.branch(): [ctx.node()]}
+    elif branches:
+        branchmaps = f.read_branches(branches)
+        bmh_map = dict((b, f.branch_heads(b, bm)) for b, bm in branchmaps)
+        headmaps = {}
+        for mod in modules:
+            headmaps[mod] = dict((b, bmh_map[b][mod]) for b in bmh_map if mod in bmh_map[b])
+    return headmaps
+
+def parse_date(datespec):
+    if datespec.startswith('<'):
+        dt = mercurial.util.parsedate(datespec[1:])
+        spec = '<'
+    elif datespec.startswith('>'):
+        dt = mercurial.util.parsedate(datespec[1:])
+        spec = '>'
+    elif ' to ' in datespec:
+        dt = [mercurial.util.parsedate(d) for d in datespec.split(' to ')]
+        spec = 'r'
+    else:
+        dt = mercurial.util.parsedate(datespec)
+        spec = 's'
+
+    return (spec, dt)
+
+def find_strip_bases(repo, csets):
+    '''
+    Given a repository and a list of nodes, find the minimal set of bases to use
+    when stripping off the corresponding changesets, without losing any other
+    changesets.
+
+    Algorithm:
+    1. loop from tip to minrev(csets): if r not in csets, remove parent(r)
+    2. loop over csets: add to bases if parent(r) not in csets
+    3. return bases
+    '''
+    cl = repo.changelog
+    minrev = min(cl.rev(n) for n in csets)
+    csetbin = set(csets)
+    for r in xrange(len(cl) - 1, minrev - 1, -1):
+        if cl.node(r) not in csetbin:
+            for p in cl.parentrevs(r):
+                if p != -1:
+                    csetbin.discard(cl.node(p))
+    bases = []
+    for n in csetbin:
+        p = cl.parents(n)
+        if p[0] not in csetbin and p[1] not in csetbin:
+            bases.append(n)
+    return bases
+
+def get_update_changeset_choice(forest, module, candidates):
+    return forest.ui.prompt('\nEnter update choice (s to skip, q to abort):').rstrip()
+
+# todo: refactor with command.update
+def timeupdate(ui, f, datespec, **opts):
+    spec, date = parse_date(datespec)
+    delta = opts['delta']
+    branch = opts['branch'] and opts['branch'] or f.branch
+    nodes = f.branch_tips(branch)
+
+    if spec == 'r':
+        mindate = date[0][0]
+        maxdate = date[1][0]
+    elif spec == 's':
+        mindate = date[0]
+        maxdate = date[0]
+    elif spec == '<':
+        mindate = None
+        maxdate = date[0]
+    elif spec == '>':
+        mindate = date[0]
+        maxdate = None
+
+    modules = sorted(set(f) & set(nodes))
+
+    # check for modified files
+    for mod in modules:
+        repo = f[mod]
+        if repo[None].files() or repo[None].deleted():
+            raise util.Abort('changed files in %s' % mod)
+
+    def find_candidates(head):
+        yield head
+        for anc in head.ancestors():
+            if anc.branch() == branch:
+                yield anc
+
+    def is_candidate(ctx, delta=0):
+        ctxd = ctx.date()[0]
+
+        if mindate is None:
+            return ctxd <= maxdate + delta
+        elif maxdate is None:
+            return ctxd >= mindate - delta
+        else:
+            return mindate - delta <= ctxd <= maxdate + delta
+
+    def prune_candidates(repo, cands):
+        if spec == '<':
+            cand = lambda x: repo[x].date() <= maxdate - delta
+            splice = lambda x: x[-2:] if x else []
+        else:
+            cand = lambda x: repo[x].date() <= mindate + delta
+            splice = lambda x: x[:2] if x else []
+
+        ccands = [rev for rev in cands if cand(rev)]
+        if ccands:
+            return ccands
+        return splice(cands)
+
+    choices = {}
+    branchmaps = f.branch_info(branch)
+
+    for mod in modules:
+        repo = f[mod]
+        meta_candidates = set([repo[branchmap[mod]].rev() for rev, branchmap in branchmaps if mod in branchmap])
+        repo_candidates = set([r.rev() for r in find_candidates(repo[nodes[mod]])])
+        candidates = sorted(meta_candidates | repo_candidates)
+
+        cands = [rev for rev in candidates if is_candidate(repo[rev])]
+        if spec in ['<', '>']:
+            cands = prune_candidates(repo, cands)
+
+        if not cands:
+            cands = [rev for rev in candidates if is_candidate(repo[rev], delta)]
+            if not cands:
+                maxr = [rev for rev in candidates if repo[rev].date()[0] <= mindate]
+                minr = [rev for rev in candidates if repo[rev].date()[0] >= maxdate]
+                if not maxr and not minr:
+                    cands = []
+                elif not maxr:
+                    cands = [min(minr)]
+                elif not minr:
+                    cands = [max(maxr)]
+                else:
+                    cands = [max(maxr), min(minr)]
+
+        ui.write_module_header(mod)
+        maxrevlen = len(str(len(cands)))
+        line1fmt = '%%-%dd:  %%s  %%s  (rev %%d)  %%s\n' % (maxrevlen,)
+        line2fmt = '%%%ds   %%s\n' % (maxrevlen,)
+        for i, rev in enumerate(cands):
+            ctx = repo[rev]
+            ui.write(line1fmt % (i + 1, 
+                mercurial.node.short(ctx.node()),
+                mercurial.util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
+                rev, util.longuser(ctx.user())))
+            ui.write(line2fmt % ('', ctx.description().splitlines()[0]))
+
+        while True:
+            choice = get_update_changeset_choice(f, mod, cands)
+            if choice in ['q', 'Q']:
+                return
+            elif choice in ['s', 'S']:
+                break
+            else:
+                try:
+                    choice = int(choice)
+                    if 1 <= choice <= len(cands):
+                        break
+                except Exception:
+                    continue
+        if choice in ['s', 'S']:
+            continue
+
+        ui.write('\n')
+        choices[mod] = cands[choice - 1]
+
+    if choices:
+        modtxt = modstr_tmpl(ui, list(choices))
+        ui.write_header('Updating modules to selected changesets\n')
+
+        for mod in sorted(choices):
+            ui.status(modtxt % mod)
+            repo = f[mod]
+            stats = mercurial.merge.update(repo, repo[choices[mod]].node(), False, True, None)
+            mercurial.hg._showstats(repo, stats)
+            repo.dirstate.setbranch(f.branch)
+
+
+def _parse_configuration(data, f):
+    return data.replace('{GMD}', f.gmd_path)
+
+def update_configurations(ui, f):
+    header = '########### DO NOT EDIT BELOW THIS LINE ###########\n'
+    footer = '########### DO NOT EDIT ABOVE THIS LINE ###########\n'
+
+    modconfig = f.module_configurations
+    for mod in f:
+        if mod not in modconfig or not os.path.exists(modconfig[mod]):
+            continue
+        with codecs.open(modconfig[mod], 'rb', 'utf-8') as fh:
+            data = _parse_configuration(fh.read(), f)
+
+        data = header + data + footer
+
+        repo = f[mod]
+        try:
+            with codecs.open(repo.join('hgrc'), 'rb', 'utf-8') as fh:
+                origdata = fh.read()
+        except UnicodeDecodeError:
+            with codecs.open(repo.join('hgrc'), 'rb', 'cp1252') as fh:
+                origdata = fh.read()
+
+        idx1 = origdata.find(header)
+        idx2 = origdata.find(footer)
+        if idx1 != -1 and idx2 != -1:
+            origdata = origdata[:idx1] + origdata[idx2 + len(footer):]
+
+        with codecs.open(repo.join('hgrc'), 'wb', 'utf-8') as fh:
+            fh.write(origdata)
+            fh.write(data)
+
+#
+# internal wrappable hooks
+#
+
+postpullhooks = []
+
+def post_pull_hook(ui, forest, firstrevs, branches, modmap):
+    for postpullhook in postpullhooks:
+        postpullhook(ui, forest, firstrevs, branches, modmap)
+
+def backup_changes(repo, rev):
+    stats = repo.status(rev)
+    backup = stats[0] + stats[2]
+    for b in backup:
+        path = repo.wjoin(b)
+        if os.path.isfile(path):
+            mercurial.util.copyfile(path, path + '.orig')

repoman/commands.py

+# Copyright 2009-2012 Edlund A/S
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+'''
+Commands exposed by repo.
+'''
+
+import time
+import datetime
+import os
+import socket
+import urllib2
+import urllib
+import getpass
+import re
+import shutil
+import json
+from collections import defaultdict
+
+import mercurial.commands
+import mercurial.cmdutil
+import mercurial.merge
+import mercurial.match
+import mercurial.util
+import mercurial.hg
+import mercurial.node
+import mercurial.encoding
+import mercurial.url
+import mercurial.changegroup
+import mercurial.discovery
+from mercurial.i18n import _
+from mercurial.node import nullid, short
+
+from tortoisehg.util.version import version as thgversion
+
+from repoman import util
+from repoman import cmdutil
+from repoman import forest as _forest
+from repoman import extensions
+import repoman.help
+
+fromlocal = mercurial.encoding.fromlocal
+tolocal = mercurial.encoding.tolocal
+
+findcommonin = mercurial.discovery.findcommonincoming
+findcommonout = mercurial.discovery.findcommonoutgoing
+
+def status(ui, f, modified=False, added=False, removed=False, deleted=False, unknown=False, **opts):
+    """show status of each module
+
+    Displays the status of each module, showing commitable changes. If no
+    options are given, modified, added, removed, deleted and unknown files are
+    shown. If no modules are given, all modules are processed."""
+
+    mask = (modified, added, removed, deleted, unknown)
+    if not any(mask):
+        mask = (True, True, True, True, True)
+
+    for mod in util.sortmodules(f):
+        stat = f[mod].status(unknown = mask[4])
+        st = [mask[i] and stat[i] or [] for i in range(0, 5)]
+        if not any(st):
+            continue
+        ui.write_module('\n%s\n' % mod)
+        for fname in st[0]:
+            ui.write('  M %s\n' % (fname,), label='status.modified')
+        for fname in st[1]:
+            ui.write('  A %s\n' % (fname,), label='status.added')
+        for fname in st[2]:
+            ui.write('  R %s\n' % (fname,), label='status.removed')
+        for fname in st[3]:
+            ui.write('  ! %s\n' % (fname,), label='status.deleted')
+        for fname in st[4]:
+            ui.write('  ? %s\n' % (fname,), label='status.unknown')
+    ui.write('\n')
+
+
+def commit(ui, f, message=False, date=None):
+    '''commit changes in each module
+
+    Commit changes in each module using the hgtk commit tool. If --message is
+    given, you will be prompted for a common commit message and the commit is
+    performed directly, bypassing hgtk commit. In that case you will not be
+    able to select or deselect files, and all committable files are included.'''
+
+    ui.status('Checking for changes... ')
+    extra = []
+    mergetext = ['']
+    changes = []
+    mergecommit = f.state and f.state.name == 'merge'
+
+    undo = []
+    if mergecommit:
+        if util.commit(f.gmd_repo, 'Fast-forward of branch %s' % f.branch, [f.branch_relpath(f.branch)], date=date):
+            undo.append(f.GMD)
+            for hg_undo, repo_undo in f.get_gmd_undo_files():
+                mercurial.util.rename(hg_undo, repo_undo)
+        message = True
+        mergetext = ['Merge %s to %s' % tuple(f.state.data)]
+
+    for mod in f:
+        ctx = f[mod][None]
+        modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
+        if modified or added or removed or len(ctx.parents()) > 1:
+            if not changes:
+                extra.append('HG: --')
+            changes.append(mod)
+            extra.append('HG: module: %s (branch %s)' %
+                         (mod, ctx.branch() or 'default'))
+            extra.extend([_("HG:     added %s") % file for file in added])
+            extra.extend([_("HG:     changed %s") % file for file in modified])
+            extra.extend([_("HG:     removed %s") % file for file in removed])
+
+    if not changes:
+        if mergecommit and undo:
+            ui.status(_('fast-forward merge committed\n'))
+            f.delete_state()
+        elif mergecommit:
+            # merge with only local fast-forward
+            ui.status(_('fast-forward merge\n'))
+            f.delete_state()
+        else:
+            ui.status(_('nothing changed\n'))
+        return
+    ui.status('\n')
+
+    if message:
+        text = cmdutil.get_commit_message(f, text=mergetext, extra=extra)
+    for mod in changes:
+        repo = f[mod]
+        if message:
+            if util.commit(repo, text, date=date):
+                undo.append(mod)
+        else:
+            mercurial.util.system('hgtk commit', cwd=repo.root,
+                                  onerr=util.Abort, errprefix='commit failed')
+    if undo:
+        f.write_undo('commit', 'Commit%s' % (' merge' if mergecommit else ''), undo)
+    if message:
+        ui.status('commit complete\n')
+        if f.state and f.state.name == 'merge':
+            f.delete_state()
+    else:
+        ui.status('commit tool launched\n')
+
+
+def info(ui, f):
+    """show information about repository
+
+    Displays information about the current repository and checks module branch status."""
+
+    # General information
+    ui.status('%s in directory %s\n' % (
+        f.solution and ('Solution %s' % ui.emph_str(f.solution))
+                    or 'Repository forest',
+                    ui.emph_str(util.normpath(ui, f.path))))
+    if f.virtual:
+        ui.status('Virtual clone from %s\n' % ui.emph_str(f.virtual))
+    ui.status('Working branch: %s\n' % ui.emph_str(f.branch))
+    if f.branches:
+        branches, all = cmdutil.parse_branches(f.branches, current=f.branch)
+        if all:
+            branches = ['all branches']
+        ui.status('Active branches: %s\n' % ', '.join([ui.emph_str(br) for br in branches]))
+    ui.status('Modules: %s\n' % util.wrap(', '.join(f.modules), 80, 9))
+    if f.state:
+        ui.warn('Operation in progress: %s\n' % f.state.name)
+
+    # Module checks
+    diskmods = [m for m in f]
+    if diskmods != f.modules:
+        ui.warn('The following modules are in the module list but not in the directory:\n')
+        ui.status(''.join(['  %s\n' % m for m in f.modules if m not in diskmods]))
+
+    # Solution checks
+    if f.solution:
+        solmod, solname = f.get_solution_modules(f.solution)
+        if not solmod:
+            ui.warn('Solution was deleted from metadata.\n')
+        else:
+            missing = [m for m in solmod if m not in f.modules]
+            exessive = [m for m in f.modules if m not in solmod]
+            if missing:
+                ui.warn('The following modules are in the solution but not in the current clone:\n')
+                ui.status(''.join(['  %s\n' % m for m in missing]))
+            if exessive:
+                ui.warn('The following modules are in the current clone but not in the solution:\n')
+                ui.status(''.join(['  %s\n' % m for m in exessive]))
+
+    # Branch information
+    tips = f.branch_tips(f.branch)
+    branches = dict((mod, f[mod].dirstate.branch()) for mod in f)
+    diff = [(mod, branches[mod]) for mod in f if branches[mod] != f.branch]
+    if len(diff):
+        ui.warn('Some modules are on different branches from the current working branch:\n')
+        ui.status(''.join(['  %s (%s)\n' % x for x in diff]))
+    par = [(mod, f[mod].dirstate.parents()[0]) for mod in f]
+    diff = [(mod, p) for mod, p in par if mod in tips and branches[mod] == f.branch and p != tips[mod]]
+    if len(diff):
+        ui.warn('Some modules are not at the current branch tip:\n')
+        ui.status(''.join(['  %s (%s)\n' % (mod, short(node)) for mod, node in diff]))
+
+
+def parents(ui, f, style=None, template=None):
+    """show parent changeset(s)
+
+    Displays the parent changeset(s) of the working directory in each module."""
+
+    # TODO: Burde vi ikke lave en separat style og bruge den i stedet
+    # for template?
+    if not template and not style:
+        template = 'rev {rev} ({node|short}) committed {date|age} by {author|person}\n  summary: {desc|firstline|fill68|tabindent}\n'
+    for mod in f:
+        ui.write_module_header(mod)
+        modrepo = f[mod]
+        mercurial.commands.parents(ui, modrepo, style=style, template=template)
+        ui.write('\n')
+
+
+def heads(ui, f, all=False, template=None):
+    '''show current module heads
+
+    Displays the heads of each module. If no options are given, only the
+    heads on the current branch of each module are shown.'''
+
+    for mod in f:
+        ui.write_module_header(mod)
+        modrepo = f[mod]
+
+        if all:
+            # mercurial.commands.heads does not show terribly useful
+            # information for all branches, instead we show heads
+            # from the branchmap directly
+            displayer = mercurial.cmdutil.show_changeset(ui, modrepo, {'template':template})
+            for bn, bheads in modrepo.branchmap().iteritems():
+                for n in bheads:
+                    displayer.show(modrepo[n])
+        else:
+            mercurial.commands.heads(ui, modrepo, '.', template=template)
+
+
+def serve(ui, f, port=8000, allow_push=False):
+    '''serves the repository via HTTP on the specified port
+
+    While the server is active, everyone with access may pull from your
+    repository; push is not allowed.
+
+    Use ctrl + c to stop the server.
+    '''
+    config = f.ljoin('hgweb.config')
+
+    for mod in f.get_all_modules():
+        rc = os.path.join(f.path, mod, '.hg', 'hgrc')
+        data = None
+        if os.path.exists(rc):
+            with open(rc, 'r') as fl:
+                data = fl.read()
+
+        if not os.path.exists(rc) or '[web]' not in data:
+            with open(rc, 'a') as fl:
+                fl.write('\n[web]\nstyle = gitweb\n')
+
+    with open(config, 'wb') as fl:
+        fl.write('[web]\nstyle = gitweb\n%s[paths]\n' %
+                ('push_ssl=False\nallow_push=*\n' if allow_push else ''))
+        for mod in f.get_all_modules():
+            fl.write('%(mod)s = %(mod)s\n' % {'mod': mod})
+
+    ui.status('Publishing %s to http://%s:%d/\n' % (f.path, socket.gethostname(), port))
+    opts = util.get_mercurial_default_opts('serve')
+    opts['port'] = port
+    opts['webdir_conf'] = config
+
+    cwd = os.getcwd()
+    try:
+        os.chdir(f.path)
+        mercurial.commands.serve(ui, f.lmd_repo, **opts)
+    finally:
+        os.chdir(cwd)
+
+
+def outgoing_commands(ui, f, operation, dest, branches, current, force, yes,
+                      style=None, template=None, bfile=None, strip=False,
+                      bundleall=False, metadatapull=True):
+
+    # Prepare.
+    if branches and current:
+        raise util.Abort('--branch and --current can\'t be given at the same time')
+    branches, all = cmdutil.parse_branches(branches, current=f.branch, default=f.branches)
+    branchmaps = f.read_branches(branches)
+    push = operation == 'push'
+    bundle = operation == 'bundle'
+    outgoing = operation == 'outgoing'
+    modules = [f.GMD] + util.sortmodules(f)
+    if bundleall:
+        modules.append(f.LMD)
+    if bundle:
+        if not os.path.splitext(bfile)[1]:
+            bfile += '.repo'
+        if strip:
+            strip_bases = {}
+
+    # Process a module.
+    def process_module(module, outgoing, modtxt, branchplural, heads, multi):
+        md = module == f.GMD or module == f.LMD
+        if md:
+            modname = '(metadata)' if outgoing else '(metadata out)'
+        else:
+            modname = module
+        if outgoing:
+            ui.write_module_header(modname)
+        else:
+            ui.write(modtxt % modname)
+        if multi:
+            ui.warn('multiple heads on the following branches: %s\n' % ', '.join(sorted(multi)))
+            if push and not outgoing:
+                return
+        repo = f[module]
+        if not force and hasattr(repo, 'mq') and repo.mq.applied:
+            warn = False
+            if not branches:
+                warn = True
+            else:
+                qtip = repo.mq.applied[-1].node
+                if any(n == qtip for n in heads):
+                    warn = True
+            if warn:
+                ui.warn('mq patches applied')
+                if push and not outgoing:
+                    ui.warn('; skipped\n')
+                    return
+                ui.warn('\n')
+
+        repo.ui.filter = ui.filter
+        remote_ui = mercurial.hg.remoteui(repo, {})
+        if bundleall:
+            source_repo = None
+        else:
+            source_repo = mercurial.hg.repository(remote_ui, f.getsource(module, dest))
+
+        if outgoing or bundle:
+            with cmdutil.quietui(repo.ui):
+                if bundleall:
+                    common = [nullid]
+                    csets = [repo[ctx].node() for ctx in repo]
+                else:
+                    commoninc = findcommonin(repo, source_repo, force=force)
+                    outg = findcommonout(repo, source_repo, commoninc=commoninc,
+                                                     force=force, onlyheads=heads)
+                    common, csets = outg.commonheads, outg.missing
+                    if not csets:
+                        outg = findcommonout(repo, source_repo, commoninc=commoninc, force=force)
+                        common, csets = outg.commonheads, outg.missing
+                        if csets:
+                            ui.status('no changes found on selected branch%s\n' % branchplural)
+                            if outgoing:
+                                ui.write('\n')
+                            return False
+            if not csets:
+                ui.status('no changes found\n')
+                if outgoing:
+                    ui.write('\n')
+                return False
+            if outgoing:
+                displayer = mercurial.cmdutil.show_changeset(ui, repo, {'template': template, 'style': style})
+                for n in csets:
+                    displayer.show(repo[n])
+            elif bundle:
+                cg = repo.getbundle('bundle', common=common, heads=heads)
+                mercurial.changegroup.writebundle(cg, repo.join('outgoing.hg'), 'HG10BZ')
+                if strip:
+                    strip_bases[module] = cmdutil.find_strip_bases(repo, csets)
+            return True
+        elif push:
+            try:
+                rv = repo.push(source_repo, force=force, revs=heads, newbranch=True)
+                if md and rv == 0:
+                    ui.warn(modtxt % ('',))
+                    ui.warn('(use "repo pull" to solve conflict and push again)\n')
+            except:
+                if md:
+                    ui.warn('error pushing metadata; please correct and push again\n')
+                raise
+
+    # Process all modules.
+    def process(outgoing):
+        mods = []
+        if bundleall:
+            source = None
+        else:
+            source = f.getsource(source=dest)
+        branchplural = 'es' if branches and len(branches) > 1 else ''
+        branchtxt = ('current head' if current else
+                     'all branches' if not branches else
+                     'branch%s: %s' % (branchplural, ', '.join(branches)))
+
+        # Prepare.
+        if outgoing:
+            ui.write_header('Outgoing changes to %s (%s)\n\n' % (util.normpath(ui, source), branchtxt))
+            modtxt = None
+        elif push or bundle:
+            if push:
+                ui.write_header('Pushing to %s (%s)\n' % (util.normpath(ui, source), branchtxt))
+            else:
+                ui.write_header('Bundling changes to %s (%s)\n' % (util.normpath(ui, source), branchtxt))
+            modtxt = cmdutil.modstr_tmpl(ui, modules, metadata=True, inout=True)
+            ui.filter = re.compile('^(comparing with |searching for changes$|remote: |adding |' +
+                                   'note: unsynced |\\(did you forget to merge\\? use push -f to force)')
+
+        # Pull metadata, read branch heads and check for multiple heads.
+        if not outgoing and not bundleall and metadatapull:
+            ui.write(modtxt % '(metadata in)')
+            cmdutil.pull_metadata(f, source=dest, terse=True)
+            f.gmd_repo.invalidate()
+        headmaps = cmdutil.get_headmaps(f, modules, current, branches)
+        multi = {}
+        aborting = False
+        modules2 = modules
+        if branches:
+            for mod in modules:
+                bmap = headmaps[mod]
+                mult = [b for b in bmap if len(bmap[b]) > 1]
+                if mult:
+                    multi[mod] = mult
+            if multi and push:
+                aborting = True
+                if not outgoing:
+                    modules2 = multi.keys()
+
+        # Perform outgoing/push operation.
+        for mod in modules2:
+            if branches and mod != f.GMD:
+                heads = [hd for hds in headmaps[mod].itervalues() for hd in hds]
+            else:
+                heads = None
+            if process_module(mod, outgoing, modtxt, branchplural, heads, multi.get(mod)):
+                mods.append(mod)
+        if push:
+            ui.filter = None
+        if aborting:
+            raise util.Abort('push aborted due to multiple heads')
+        return mods
+
+    # Preview (or display outgoing).
+    if outgoing or not yes:
+        modules = process(outgoing=True)
+        if modules and not outgoing:
+            answer = ui.prompt('%s these changes (y/yes/n/no)?' % operation.title(), 'n')
+            if answer not in ['y', 'yes']:
+                ui.status('%s aborted.\n' % operation.title())
+                return
+
+    # Push or bundle.
+    if not outgoing:
+        if modules:
+            modules = process(outgoing=False)
+            if modules and bundle:
+                ui.status('Writing bundle file %s\n' % bfile)
+                bf = _forest.Bundle(bfile, 'w')
+                for mod in modules:
+                    src = f[mod].join('outgoing.hg')
+                    bf.add(mod, src)
+                    try:
+                        os.unlink(src)
+                    except Exception:
+                        pass
+                bf.close()
+                if strip:
+                    ui.status('Stripping bundled changesets..')
+                    f.clear_undo()
+                    for mod, bases in strip_bases.iteritems():
+                        cmdutil.strip(ui, f[mod], *[f[mod][b].hex() for b in bases])
+                        ui.write('.')
+                    ui.status(' done\n')
+            if push:
+                f.clear_undo()
+        else:
+            ui.status('Nothing to %s.\n' % operation)
+
+def outgoing(ui, f, dest=None, **opts):
+    """show outgoing changes without pushing them
+
+    Displays outgoing changes to a source, without actually pushing them.
+    If no source is given, the original clone source is assumed. If no modules
+    are given, all modules are processed. If no branches are given, changes are
+    displayed for the current working branch only. If --current is given, only
+    the current repository heads are considered for pushing. Otherwise, if
+    there are multiple heads on any selected branch, a warning is displayed."""
+
+    outgoing_commands(ui, f, 'outgoing', dest, opts['branch'],
+                      opts['current'], opts['force'], yes=False,
+                      style=opts['style'], template=opts['template'])
+
+def push(ui, f, dest=None, **opts):
+    '''pushes local changes
+
+    If no source is given, the original clone source is assumed. If no modules
+    are given, all modules are processed. If no branches are given, changes are
+    pushed from the current working branch only. Otherwise, if there are
+    multiple heads on any branch to be pushed, an error is reported. Before
+    actually pushing, a review of outgoing changes is provided unless the
+    --noninteractive option is used.
+
+    During the push, each module can trigger an error being reported, in
+    particular that the push creates new remote heads. If the error reports
+    that new heads would be created, then you must solve that by first pulling
+    the latest changes and either rebasing your changes or merging them using
+    "repo rebase" or "hg merge" in each module that reported the error.
+    '''
+
+    outgoing_commands(ui, f, 'push', dest, opts['branch'],
+                      opts['current'], opts['force'],
+                      yes=opts['noninteractive'],
+                      style=opts['style'],
+                      template=opts['template'],
+                      metadatapull=not opts['no_metadata_pull'])
+
+def bundle(ui, f, fname, dest=None, **opts):
+    '''create a bundle
+
+    The bundle contains changes for all modules that have outgoing changes on
+    the selected branches.
+    '''
+
+    outgoing_commands(ui, f, 'bundle', dest,
+                      opts['branch'] if not opts['all'] else ['*'],
+                      opts['current'], opts['force'], yes=opts['noninteractive'],
+                      bfile=fname, strip=opts['strip'], bundleall=opts['all'])
+
+
+def ingoing_commands(ui, f, operation, source = 'default', branch=None,
+                     current=False, noupdate=False, style=None, template=None,
+                     log=False, modmap=None, clean=None):
+    '''Common code for incoming, pull and unbundle.'''
+
+    # Prepare.
+    if operation == 'incoming':
+        incoming = True
+        header = 'Incoming'
+    elif operation == 'pull':
+        incoming = False
+        header = 'Pulling'
+    else:
+        raise util.Abort('internal error; unknown ingoing_commands operation %s' % operation)
+
+    usebundle = os.path.isfile(source)
+    if current:
+        if modmap:
+            raise util.Abort('--modmap and --current can\'t be given at the same time')
+        if branch:
+            raise util.Abort('--branch and --current can\'t be given at the same time')
+        branches, all = None, False
+    else:
+        default = ['*'] if usebundle else f.branches
+        branches, all = cmdutil.parse_branches(branch, current=f.branch, default=default)
+
+    modules = list(f)
+    if usebundle:
+        bundle = _forest.Bundle(source)
+        bundlemd = f.GMD in bundle.modules
+        modules = [mod for mod in modules if mod in bundle.modules]
+    else:
+        bundle = None
+
+    modmap = dict((k.strip(), v.strip()) for k, v in (e.split('=', 1) for e in modmap))
+    for mod in modmap:
+        if mod not in modules:
+            raise util.Abort('invalid --modmap: module %s not in forest' % mod)
+
+    quiet = ui.quiet
+    sourctxt = source if bundle else f.getsource(source=source)
+    if all:
+        branchestxt = 'all branches'
+    elif current:
+        branchestxt = 'current branch'
+    else:
+        branchestxt = 'branches: %s' % ", ".join(branches)
+    ui.write_header('%s changes from %s (%s)\n' % (header, util.normpath(ui, sourctxt), branchestxt))
+    if bundle:
+        local_modules = f.get_all_modules(with_metadata=True, with_local=False)
+        missing_locally = [mod for mod in bundle.modules if mod not in local_modules]
+        if missing_locally:
+            modulesplural, verb = ('modules','are') if len(missing_locally) > 1 else ('module','is')
+            ui.warn('The %s %s in the bundle %s not part of the (local) forest\n' % 
+                (modulesplural, ", ".join(missing_locally), verb))
+    if not quiet and not(log and bundle): ui.write('\n')
+
+    opts = {}
+    opts['style'] = style
+    opts['template'] = template
+    opts['rev'] = opts['force'] = None
+    opts['bundle'] = f.gjoin('.bundle') if incoming and branches else None
+    opts['update'] = not incoming
+    opts['rebase'] = False
+
+    if quiet or log:
+        modstr = cmdutil.modstr_tmpl(ui, modules, metadata=True)
+
+    def header(text):
+        if quiet:
+            ui.write(modstr % text)
+        else:
+            ui.write_module_header(text)
+
+    # Write output in quiet mode
+    def quiet_write():
+        num_csets = len(ui.buffer)
+        del ui.buffer[:]
+        ui.buffered = False
+        ui.write('%d new changesets\n' % num_csets)
+
+    # Write warning if a bundle error occurs
+    def bundle_error(error):
+        ui.warn('bundle error: changeset %s is missing locally\n' % short(error.name))
+
+    # Process metadata.
+    undo = []
+    ui.filter = re.compile('^(pulling from |comparing with |searching for changes$|adding |\\(run \'hg |not updating)')
+    if branches:
+        oldmaps = dict((b, f.read_branch(b)) for b in branches)
+    repo = f.gmd_repo
+    if bundle:
+        if bundlemd:
+            bfile = repo.join('unbundle.hg')
+            bundle.get(f.GMD, bfile)
+            src = 'bundle:%s+%s' % (repo.root, bfile)
+        else:
+            src = None
+    else:
+        src = f.getsource(f.GMD, source)
+
+    anymeta = False
+    if src:
+        header('(metadata)')
+        try:
+            if incoming:
+                if quiet:
+                    opts['template'] = '.'
+                    ui.buffered = True
+                anymeta = mercurial.commands.incoming(ui, repo, src, **opts) == 0
+                if quiet:
+                    quiet_write()
+            else:
+                prevlen = len(repo)
+                cmdutil.pull(ui, repo, src, **opts) 
+                if len(repo) > prevlen:
+                    anymeta = prevlen
+                    undo.append(f.GMD)
+                    for hg_undo, repo_undo in f.get_gmd_undo_files():
+                        mercurial.util.copyfile(hg_undo, repo_undo)
+
+                    heads = [x for x in repo.heads() if repo[x].rev() != repo['.'].rev()]
+                    while len(heads) >= 1:
+                        if mercurial.hg.merge(repo, heads[0], remind=False):
+                            ui.prompt('resolve conflicts then press enter...')
+                        util.docmd_encoding(lambda: repo.commit('merge'))
+                        heads = [x for x in repo.heads() if repo[x].rev() != repo['.'].rev()]
+
+                # no need to explicitly update metadata, already done by the rebase after pull
+        except util.LookupError, e:
+            if not bundle:
+                raise
+            bundle_error(e)
+
+        if not incoming or not anymeta:
+            ui.status('\n')
+
+    opts['bundle'] = opts['update'] = opts['rebase'] = None
+    if incoming and log and not quiet:
+        quiet = True
+        opts['template'] = '.'
+
+    # Collect new branch maps (after metadata changes).
+    if branches:
+        skippedbranches = []
+        def skipwarn(b):
+            ui.warn('skipping unknown branch %s\n' % b)
+            skippedbranches.append(b)
+        newmaps = {}
+        if not anymeta or not incoming:
+            for branch in branches:
+                newmaps[branch] = bmap = f.read_branch(branch)
+                if not bmap:
+                    skipwarn(branch)
+        else:
+            brepo = f.bundle_repo(f.GMD, '.bundle')
+            tipctx = brepo['tip']
+            for branch in branches:
+                bpath = f.branch_relpath(branch)
+                if bpath not in tipctx:
+                    skipwarn(branch)
+                    continue
+                bdata = tipctx[bpath].data().split('\r\n')
+                newmaps[branch] = util.parse_branch(branch, bdata, oldmaps[branch].l)
+        branches = [b for b in branches if b not in skippedbranches]
+        if not branches:
+            ui.warn('no valid branches given. aborting.\n')
+            return
+
+        # Augment branches for module-mapped pulls.
+        if modmap:
+            for b in branches:
+                for d, s in modmap.iteritems():
+                    bdata = newmaps[b]
+                    if s in bdata and d not in bdata.g:
+                        bdata.l[d] = bdata[s]
+
+        branchupdate = newmaps.get(f.branch)
+    elif all:
+        branchupdate = f.read_branch(f.branch)
+
+    # Process regular modules.
+    ui.filter = re.compile('^(pulling from |comparing with |searching for changes$|adding |\\(run \'hg )')
+    anymods = {}
+    view_log = []
+    bundleerror = False
+    for mod in modules:
+        srcmod = modmap and modmap.get(mod) or mod
+        if srcmod != mod:
+            header('%s (from %s)' % (mod, srcmod))
+        else:
+            header(mod)
+        skip_ingoing = False
+        repo = f[mod]
+        if bundle:
+            bfile = repo.join('unbundle.hg')
+            bundle.get(mod, bfile)
+            src = 'bundle:%s+%s' % (repo.root, bfile)
+        else:
+            src = f.getsource(srcmod, source)
+        try:
+            if branches:
+                remote = mercurial.hg.repository(ui, src)
+                opts['other'] = remote
+                revs = []
+                bnames = []
+                for branch in branches:
+                    newbr = newmaps[branch]
+                    if not mod in newbr:
+                        continue
+                    oldbr = oldmaps[branch]
+                    if mod in oldbr and oldbr[mod] != newbr[mod]:
+                        ui.status('Branch %s was fast-forwarded to %s\n' % (branch, newbr[mod][:12]))
+                    if util.rev_exists(remote, branch):
+                        bnames.append(branch)
+                    node = newbr[mod]
+                    if not util.rev_exists(f[mod], node):
+                        revs.append(node)
+
+                if not revs and not bnames:
+                    skip_ingoing = True
+                if skip_ingoing:
+                    if quiet:
+                        ui.write('0 new changesets\n')
+                    else:
+                        ui.status('no changes found\n')
+
+            elif all: # all branches
+                bnames = []
+                revs = []
+
+            else: # dirstate branch
+                opts['other'] = None
+                revs = None
+                bnames = [f[mod].dirstate.branch()]
+
+            if not skip_ingoing:
+                opts['rev'] = revs
+                opts['branch'] = bnames
+                if incoming:
+                    if quiet:
+                        if log:
+                            opts['bundle'] = os.path.join(f[mod].path, 'incoming.hg')
+                        ui.buffered = True
+                    skip_ingoing = mercurial.commands.incoming(ui, f[mod], src, **opts) != 0
+                    if quiet:
+                        if log and not skip_ingoing:
+                            view_log.append(mod)
+                        quiet_write()
+                else:
+                    lastrev = len(f[mod])
+                    skip_ingoing = cmdutil.pull(ui, f[mod], src, **opts) == 0
+                    if not skip_ingoing:
+                        undo.append(mod)
+                        anymods[mod] = lastrev
+
+            if not incoming:
+                nodes = (not current) and branchupdate and f.module_branch_heads(f.branch, mod, branchupdate) or None
+                if nodes or current:
+                    wctx = repo[None]
+                    rev = current and repo.branchtags()[wctx.branch()] or nodes[0]
+                    if len(wctx.parents()) > 1 or wctx.p1() != repo[rev]:
+                        if noupdate:
+                            ui.status('note: module is not at branch head\n')
+                        else:
+                            if quiet:
+                                ui.write(modstr % '')
+                            p1 = repo['.']
+                            p2 = repo[rev]
+                            pa = p1.ancestor(p2)
+                            if not clean and pa != p1 and pa != p2 and p1.branch() == p2.branch():
+                                ui.warn('warning: update crosses branches (use "repo rebase" or "hg merge")\n')
+                            else:
+                                if clean:
+                                    cmdutil.backup_changes(repo, rev)
+                                    mercurial.hg.clean(repo, rev)
+                                else:
+                                    mercurial.hg.update(repo, rev)
+                                repo.dirstate.setbranch(f.branch)
+
+        except util.LookupError, e:
+            ui.buffered = False
+            if not bundle:
+                raise
+            bundle_error(e)
+            bundleerror = True
+            skip_ingoing = True
+
+        finally:
+            if not quiet and (skip_ingoing or not incoming):
+                ui.write('\n')
+            remote = opts.get('other')
+            if remote and hasattr(remote, 'close'):
+                remote.close()
+
+    # Update individual modules' .hg/hgrc files
+    if not incoming and anymeta:
+        cmdutil.update_configurations(ui, f)
+
+    # Post-pull hook.
+    anyrealmods = len(anymods)
+    if not incoming:
+        if anymeta is not False:
+            anymods[f.GMD] = anymeta
+        if not branches:
+            newmaps = {}
+        cmdutil.post_pull_hook(ui, f, anymods, newmaps, modmap)
+
+    # Update local branches for module-mapped pulls.
+    if modmap and branches and anyrealmods:
+        for b in newmaps:
+            f.write_branch(b, newmaps[b], oldmaps.get(b), local=True)
+
+    ui.filter = None
+    if bundle:
+        bundle.close()
+    if undo:
+        f.write_undo('pull', 'Pull from %s, %s' % (util.normpath(ui, sourctxt), branchestxt), undo)
+    if not bundleerror and view_log:
+        for mod in view_log:
+            mercurial.util.system('hgtk --fork -R bundle:.hg/incoming.hg log', cwd=f[mod].root,
+                    onerr=util.Abort, errprefix='log failed')
+        time.sleep(1)
+
+
+def incoming(ui, f, *args, **opts):
+    '''show incoming changes without pulling them in
+
+    Displays incoming changes from a source without actually pulling them in.
+    If no source is given, the original clone source is assumed. If no branches
+    are given, changes are displayed for the current working branch only.'''
+
+    ingoing_commands(ui, f, 'incoming', *args, **opts)
+
+def pull(ui, forest, *args, **opts):
+    '''pulls in changes from source
+
+    If no source is given, the original clone source is assumed. If no branches
+    are given, changes are pulled in on the current working branch only.
+
+    After pulling the current branch, an update will be performed similar to
+    issuing the command "repo update --merge". The --clean option can be used
+    to turn this into "repo update --clean" instead. Finally, the --noupdate
+    option can be used to skip the update.
+    '''
+
+    ingoing_commands(ui, forest, 'pull', *args, **opts)
+
+
+def update(ui, f, node=None, branch=None, **opts):
+    '''update the repository forest to the given branch name
+
+    The command recursively updates each module in the forest.
+    Status information is displayed along the way. If you update to a new
+    branch, this will be set as your current working branch (see repo info).
+
+    repo update can also be used to switch to local revision names (e.g.
+    bookmarks), if the --local option is given.
+
+    If --clean is specified, update will create a backup .orig file for each
+    file that has been modified, unless --nobackup is specified. To restore the
+    .orig files to modified files see 'repo help unrevert'.
+
+    If the --date option is given, the method tries to suggest changesets on
+    the selected branch that matches this date specification. It is impossible
+    to do this perfectly as rebases reorder the chronology of changesets. For
+    each module, a list of possible changesets that matches the specified date
+    range is suggested.
+
+    Each of the date specifications will only be applied to changesets on the
+    specified branch and their ancestors. If no branch is specified, the
+    forest's current branch will be used.
+
+    The delta option is only used for range and specific date specifications
+    where no changesets match exactly.
+
+    The date ranges can be specified in the following ways:
+
+    "<date"  Find the greatest changeset less than date.
+    ">date"  Find the least changeset greater than date.
+    "date1 to date2"  Find changesets between date1 and date2.
+    "date"  Find changeset at date.
+
+    To see how the exact date specifications are written, see "hg help dates".
+    '''
+
+    if node:
+        if branch:
+            raise util.Abort('please specify just one revision')
+        branch = node
+    branch = cmdutil.parse_branches(branch, exact=True, current=f.branch)[0]
+
+    clean = opts['clean']
+    nobackup = opts['nobackup']
+    merge = opts['merge']
+
+    if f.state and f.state.name == 'merge' and not clean:
+        raise util.Abort('merge in progress, use --clean to abort the merge')
+
+    # todo: refactor to use common update code further down