]> git.madduck.net Git - code/myrepos.git/commitdiff

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

myrepos (1.20130710) unstable; urgency=low
authorJoey Hess <joeyh@debian.org>
Wed, 10 Jul 2013 17:50:48 +0000 (13:50 -0400)
committerJoey Hess <joeyh@debian.org>
Wed, 10 Jul 2013 17:50:48 +0000 (13:50 -0400)
  * Avoid conflicting with mr so the dummy package can be installed and pull
    in this one.

# imported from the archive

32 files changed:
GPL [new file with mode: 0644]
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
debian/NEWS [new file with mode: 0644]
debian/changelog [new file with mode: 0644]
debian/compat [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/docs [new file with mode: 0644]
debian/examples [new file with mode: 0644]
debian/rules [new file with mode: 0755]
doc/index.mdwn [new file with mode: 0644]
doc/install.mdwn [new file with mode: 0644]
doc/news/mr_renamed_to_myrepos.mdwn [new file with mode: 0644]
doc/news/version_1.20130705.1.mdwn [new file with mode: 0644]
doc/news/version_1.20130705.mdwn [new file with mode: 0644]
doc/sidebar.mdwn [new file with mode: 0644]
doc/todo.mdwn [new file with mode: 0644]
doc/todo/detect_unregistered_repos_in_tree.mdwn [new file with mode: 0644]
doc/todo/done.mdwn [new file with mode: 0644]
doc/todo/smarter_chained_checkout.mdwn [new file with mode: 0644]
lib/git-fake-bare [new file with mode: 0644]
lib/git-subtree [new file with mode: 0644]
lib/git-svn [new file with mode: 0644]
lib/repo [new file with mode: 0644]
lib/unison [new file with mode: 0644]
lib/vcsh [new file with mode: 0644]
lib/vis [new file with mode: 0644]
mr [new file with mode: 0755]
mrconfig [new file with mode: 0644]
mrconfig.complex [new file with mode: 0644]
webcheckout [new file with mode: 0755]

diff --git a/GPL b/GPL
new file mode 100644 (file)
index 0000000..d511905
--- /dev/null
+++ b/GPL
@@ -0,0 +1,339 @@
+                   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.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..3718e32
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,32 @@
+PREFIX:=/usr
+
+mans=mr.1 webcheckout.1
+
+build: $(mans)
+
+mr.1: mr
+       pod2man -c mr mr > mr.1
+
+webcheckout.1: webcheckout
+       pod2man -c webcheckout webcheckout > webcheckout.1
+
+test:
+       (echo "[.]"; echo "checkout=") > mrconfig.tmp
+       ./mr --trust-all -c mrconfig.tmp ed | grep -q "horse"
+       rm -f mrconfig.tmp
+
+install: build
+       install -d ${DESTDIR}${PREFIX}/bin
+       install -d ${DESTDIR}${PREFIX}/share/man/man1
+       install -d ${DESTDIR}${PREFIX}/share/mr
+
+       install -m0755 mr ${DESTDIR}${PREFIX}/bin/
+       install -m0755 webcheckout ${DESTDIR}${PREFIX}/bin/
+
+       install -m0644 mr.1 ${DESTDIR}${PREFIX}/share/man/man1/
+       install -m0644 webcheckout.1 ${DESTDIR}${PREFIX}/share/man/man1/
+
+       install -m0644 lib/* ${DESTDIR}${PREFIX}/share/mr/
+
+clean:
+       rm -f $(mans)
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..750186d
--- /dev/null
+++ b/README
@@ -0,0 +1,33 @@
+myrepos, a tool to manage all your version control repos
+
+You have a lot of version control repositories. Sometimes you want to
+update them all at once. Or push out all your local changes. You use
+special command lines in some repositories to implement specific workflows.
+Myrepos provides a `mr` command, which is a tool to manage all your version
+control repositories.
+
+It supports git, svn, mercurial, bzr, darcs, cvs, fossil and veracity.
+
+Author: Joey Hess
+Homepage: http://myrepos.branchable.com/
+
+The mr command is intended to be very self-contained, since it might be
+useful to check it into ~/bin when keeping your home in version control. It
+has no dependencies aside from basic perl. (The included webcheckout
+command has more dependencies, specifically the LWP::Simple and
+HTML::Parser CPAN modules, and optionally the URI module.)
+
+To install mr, just copy mr into your PATH somewhere.
+
+To get started using mr, perhaps you already have some checked out
+repositories. Go into each one and run "mr register". Now mr has
+a list of them in ~/.mrconfig, which you can edit later to tune its
+operation.
+
+Suppose you've cd'd to ~/src, and it has many repositories under it.
+To update them all, run "mr update". To commit any pending changes in
+each, run "mr commit". To check the status of each, you could run
+"mr status".
+
+For further details, and lots of configuration options, see the mr(1) man
+page or the website, http://myrepos.branchable.com/
diff --git a/debian/NEWS b/debian/NEWS
new file mode 100644 (file)
index 0000000..a31227f
--- /dev/null
@@ -0,0 +1,13 @@
+mr (1.00) unstable; urgency=low
+
+  In this version, mr has changed to not trust all mrconfig files by
+  default. Untrusted files are only allowed to run carefully checked commands.
+  Only the main ~/.mrconfig is now trusted by default. If you have other
+  mrconfig files, containing unusual commands, that mr should trust, you
+  will need to list them in ~/.mrtrust.
+
+  Also in this version, the -p flag is on by default, so mr will
+  look for a .mrconfig file in the current directory or one of its parent
+  directories, in addition to the main ~/.mrconfig file.
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 19 Jan 2011 13:40:16 -0400
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..6581538
--- /dev/null
@@ -0,0 +1,648 @@
+myrepos (1.20130710) unstable; urgency=low
+
+  * Avoid conflicting with mr so the dummy package can be installed and pull
+    in this one.
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 10 Jul 2013 13:50:48 -0400
+
+myrepos (1.20130705.1) unstable; urgency=low
+
+  * Conflict and Replaces mr.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 05 Jul 2013 16:55:30 -0400
+
+myrepos (1.20130705) unstable; urgency=low
+
+  * The package is renamed to myrepos. It Provides mr, so can still be
+    installed by that name. The mr command is not renamed.
+  * Add make install rule.
+    Thanks, v4hn
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 05 Jul 2013 14:42:39 -0400
+
+mr (1.15) unstable; urgency=low
+
+  * Added lib/repo, for support for repo (as used in Android)
+    Closes: #705652 Thanks, Peter Eisentraut
+  * Better cvs status.
+    Closes: #694037 Thanks, Paul Wise
+
+ -- Joey Hess <joeyh@debian.org>  Sat, 04 May 2013 23:47:50 -0400
+
+mr (1.14) unstable; urgency=low
+
+  * Added a fetch command. Closes: #480580
+  * status: Now includes information about unpushed changes,
+    for git, git-svn, hg, and bzr. Closes: #693021
+  * Added lib/vis, an add-on to visualise repo history.
+    Closes: #693022 Thanks, Paul Wise
+  * Drop an extra -m from various commit/record commands.
+    Closes: #695478 Thanks, Paul Wise
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 13 Feb 2013 14:48:34 -0400
+
+mr (1.13) unstable; urgency=low
+
+  * Pass -q to cvs diff and update. Closes: #673367
+  * mr bootstrap: Now supports ssh:// urls.
+  * Simpler vcsh status command line. Closes: #685089
+  * Add grep subcommand. Uses ack-grep on VCS that do not have their own.
+    Closes: #685122
+
+ -- Joey Hess <joeyh@debian.org>  Sat, 25 Aug 2012 11:14:41 -0400
+
+mr (1.12) unstable; urgency=low
+
+  * Ignore nonzero exit status of hg pull, which can happen
+    when there were no changes to pull. Closes: #661870 
+  * Add vcsh to Suggests.
+  * Recognize a repo with a .git file as a git repo.
+  * Improve bzr register's heuristics to determine upstream repo.
+    Closes: #672843
+
+ -- Joey Hess <joeyh@debian.org>  Mon, 14 May 2012 10:42:52 -0400
+
+mr (1.11) unstable; urgency=low
+
+  * Now supports the veracity vcs. Thanks, Jimmy Tang.
+  * vcsh is now supported without including a plugin.
+  * After checkout, run fixups chdired to the just checked out directory.
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 14 Feb 2012 18:13:29 -0400
+
+mr (1.10) unstable; urgency=low
+
+  * Fix display of trust errors.
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 27 Dec 2011 12:13:15 -0400
+
+mr (1.09) unstable; urgency=low
+
+  * Remove dir_test hack and add a way for vcs tests to run perl code,
+    using this for the same optimisation. Fixes support for git-svn
+    etc. Closes: #652317
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 16 Dec 2011 13:29:40 -0400
+
+mr (1.08) unstable; urgency=low
+
+  * Fix vcs test code. Closes: #651976
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 13 Dec 2011 16:21:19 -0400
+
+mr (1.07) unstable; urgency=low
+
+  * Added support for vcsh, enable with: include = cat /usr/share/mr/vcsh
+    Thanks, Richard Hartmann 
+  * Block tty control codes in untrusted mr config files.
+  * Correct printing of line numbers when includes are used. Closes: #650952
+  * The previous fix for chaining to absolute paths broke chaining to relative
+    paths with more than one path segment. Thanks, Adam Spiers
+  * Support _append to add on to the existing value of a parameter.
+    Thanks, Adam Spiers
+  * Optimizations. Commands like "mr list" run up to 5 times faster.
+  * Fix shell escaping of parameters passed to mr commands. Closes: #644672
+  * Added --force option that disables repository skipping.
+  * Repositories using skip = lazy will not be checked out by "mr update"
+    or "mr checkout" unless --force is used.
+
+ -- Joey Hess <joeyh@debian.org>  Mon, 12 Dec 2011 15:52:16 -0400
+
+mr (1.06) unstable; urgency=low
+
+  * Fix propigation of failure from pre and post hooks and from fixups.
+  * Support chaining to absolute paths.
+  * Add support for skip = lazy, a mode where mr only operates on repositories
+    that are checked out.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 04 Nov 2011 17:03:17 -0400
+
+mr (1.05) unstable; urgency=low
+
+  * README now gives a quick into to using mr.
+  * Brought back the "deleted" parameter, which provides an easy way to
+    mark repositories that should be removed.
+  * Allow untrusted mrconfig files to set parameters to true/false.
+    So skip=true or deleted=true can be used in an untrusted mrconfig file.
+  * Also allow order=N in an untrusted mrconfig file.
+  * Support bzr checkouts, which are updated with "bzr update",
+    and to which bzr automatically pushes commits. Closes: #643589
+  * Use bzr branch, not deprecated bzr clone when registering bzr
+    repositories. Closes: #643591
+  * Allow bzr branch|clone|get|checkout in untrusted mrconfig files.
+  * Avoid using sed -r in git-fake-bare, for OSX portability.
+  * git-fake-bare: handle fake bare repositories with core.bare not set
+    (Thanks, Julien Rebetez)
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 27 Sep 2011 17:28:35 -0400
+
+mr (1.04) unstable; urgency=low
+
+  * Improve trust errors displayed while bootstrapping. Closes: #628234
+  * Allow mr register to be used with mrconfig file that does not yet
+    exist. Closes: #629217
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 19 Jun 2011 15:52:42 -0400
+
+mr (1.03) unstable; urgency=low
+
+  * Added git-subtree library. Thanks, Svend Sorensen
+  * Now --quiet supresses all output from commands run, as well
+    as from mr, unless a command fails. Closes: #529296
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 01 May 2011 19:09:35 -0400
+
+mr (1.02) unstable; urgency=low
+
+  * Fix bug in escaping.
+
+ -- Joey Hess <joeyh@debian.org>  Sat, 22 Jan 2011 01:18:23 -0400
+
+mr (1.01) unstable; urgency=low
+
+  * Add "mr run", which can run an arbitrary command in each repository.
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 20 Jan 2011 16:50:49 -0400
+
+mr (1.00) unstable; urgency=low
+
+  * Trust flag day. All mrconfig files except the main ~/.mrconfig are
+    now untrusted by default, until listed in ~/.mrtrust.
+  * The -p flag is now enabled by default. mr first reads ~/.mrconfig,
+    and then looks for an additional .mrconfig file in the current
+    directory or one of its parent directories. Closes: #557963
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 19 Jan 2011 14:28:38 -0400
+
+mr (0.51) unstable; urgency=low
+
+  * Fix display when absolute directories are configured in mrconfig.
+  * Add push to manpage synopsis. Closes: #603029
+  * Do not return a nonzero exit status when all repositories were skipped.
+    Closes: #607287
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 16 Dec 2010 12:53:32 -0400
+
+mr (0.50) unstable; urgency=low
+
+  * Now supports the Fossil VCS. (Thanks, Jimmy Tang)
+  * Added fixups hook, which can be used to run a command after
+    a repository is checked out or updated. Closes: #590868
+  * Added support for arbitrary pre and post hooks for all defined mr
+    commands. For example, pre_commit is run before all commits; post_update
+    is run after all updates. Closes: #481341
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 29 Aug 2010 15:00:01 -0400
+
+mr (0.49) unstable; urgency=low
+
+  * Update suggests for git-core to git transition.
+  * Use short mode status output for git and bzr.
+  * Typo. Closes: #586233, #588913
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 18 Jul 2010 23:08:06 -0400
+
+mr (0.48) unstable; urgency=low
+
+  * Fix the hours_since function built into mr's shell library to
+    not exit, but return a true/false exit status. This allows it to be
+    used outside of skip tests.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 05 Feb 2010 17:12:01 -0500
+
+mr (0.47) unstable; urgency=low
+
+  * Pass -L to curl to allow it to follow redirects when bootstrapping.
+    (Pavel Avgustinov)
+  * Allow empty sections to be used in mrconfig files.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 05 Feb 2010 16:40:29 -0500
+
+mr (0.46) unstable; urgency=low
+
+  * Avoid using abs_path to determine canonical repo path, as that fails
+    when the repo has not yet been checked out.
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 10 Dec 2009 17:28:26 -0500
+
+mr (0.45) unstable; urgency=low
+
+  * Fix pod error. Closes: #558012
+  * bootstrap: Improve stats display.
+  * Add --insecure option that can be used to ignore SSL cert errors
+    when bootstrapping. (Pavel Avgustinov)
+  * Fix handling of a repository in "."
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 08 Dec 2009 14:58:21 -0500
+
+mr (0.44) unstable; urgency=low
+
+  * Make mr -i pass -i to the shell, to ensure it knows it should be an
+    interactive shell.
+  * mr help: Support man on SunOS and OS X. (Geoff Davis)
+  * mr bootstrap: Fix cross-device rename failure. Closes: #557962
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 25 Nov 2009 12:39:08 -0500
+
+mr (0.43) unstable; urgency=low
+
+  * Set User-Agent to "mr" when downloading the mrconfig file
+    with mr bootstrap. Closes: #541778
+  * Man page typo. Closes: #545412
+  * mr bootstrap: Allow a directory to checkout into to be specified,
+    and create it if necessary.
+  * mr bootstrap: If the mrconfig file contains a repository named ".",
+    check it out into the top level of the directory bootstrapped.
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 08 Sep 2009 20:22:32 -0400
+
+mr (0.42) unstable; urgency=low
+
+  * Add support for ~/.mrtrust, which can be used to list trusted mrconfig
+    files. If you create this file, all files not listed in it will be treated
+    as untrusted, and carefully limited in what they can do. This improves
+    security when using mrconfig files provided by others.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 07 Aug 2009 16:38:31 -0400
+
+mr (0.41) unstable; urgency=low
+
+  * Add -p switch, that makes mr search the current directory and its
+    parents for a .mrconfig file to use.
+  * Add `mr bootstrap`, which downloads an url to a .mrconfig file
+    in the current directory and then checks out all repositories configured
+    in it. This is intended for projects that want to publish a mrconfig file
+    to automate use of their several repositories.
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 06 Aug 2009 22:38:58 -0400
+
+mr (0.40) unstable; urgency=low
+
+  * Pass --pull to bzr merge to avoid needing to commit
+    merged changes. Closes: #524352
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 31 Jul 2009 13:47:22 -0400
+
+mr (0.39) unstable; urgency=low
+
+  * Determine current branch for fake-bare update (martin f. krafft)
+
+ -- Joey Hess <joeyh@debian.org>  Mon, 09 Mar 2009 22:50:54 -0400
+
+mr (0.38) unstable; urgency=low
+
+  * Remove gitless lib. It turned out to be better to simply
+    use git clone --shared when checking out. Sorry for the churn..
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 22 Jan 2009 15:39:33 -0500
+
+mr (0.37) unstable; urgency=low
+
+  * Add "gitless" file to lib. 
+    This adds a special type of git repository, where the .git directory
+    is stored on a file server, to avoid wasting space with it on the client.
+  * Revert buggy change to directory printing code.
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 21 Jan 2009 01:40:59 -0500
+
+mr (0.36) unstable; urgency=low
+
+  * Add webcheckout command. See <http://kitenet.net/~joey/rfc/rel-vcs/>
+  * Fix display of repos configured at absolute path locations.
+
+ -- Joey Hess <joeyh@debian.org>  Sat, 10 Jan 2009 16:42:07 -0500
+
+mr (0.35) unstable; urgency=low
+
+  * Warn if an include command fails nonzero. Closes: #495306
+  * Remove stray character in pod that uglified man page. Closes: #495731
+  * Create ~/.mrlog not world readable.
+  * Pass additional options to darcs push when pushing. Closes: #495734
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 16 Oct 2008 14:51:00 -0400
+
+mr (0.34) unstable; urgency=low
+
+  * Fix bug when remembering failed commands in offline mode.
+
+ -- Joey Hess <joeyh@debian.org>  Sat, 02 Aug 2008 12:27:21 -0400
+
+mr (0.33) unstable; urgency=low
+
+  * Add a push subcommand, which pushes committed changes for DCVS, and 
+    does nothing for svn/cvs. Closes: #491865
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 22 Jul 2008 15:22:01 -0400
+
+mr (0.32) unstable; urgency=low
+
+  * Include the right (v2) version of GPL in the source.
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 22 Jul 2008 00:28:55 -0400
+
+mr (0.31) unstable; urgency=low
+
+  * Add an offline mode. When in offline mode, mr will remember commands
+    that failed (presumably due to lack of network access), and will run
+    them again once back online. (This is for all the laptop users
+    like me who don't want to have to remember which git repos to mr push
+    when they come back from a nice long hack in the woods. :-)
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 27 Jun 2008 23:19:23 -0400
+
+mr (0.30) unstable; urgency=low
+
+  [ martin f. krafft ]
+  * Several git-fake-bare improvements.
+
+  [ Joey Hess ]
+  * Fix mr -c <config> register. (Thanks, Daniel Bungert)
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 25 Jun 2008 13:38:31 -0400
+
+mr (0.29) unstable; urgency=low
+
+  * Allow environment variables in section headers. Pass such headers
+    through the shell to expand them. Closes: #480421
+  * For the checkout command, MR_REPO was set to the name of the parent
+    directory of the directory to checkout. It is supposed to be the
+    path to the directory itself though; fixed this. Closes: #480424
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 14 May 2008 19:38:18 -0400
+
+mr (0.28) unstable; urgency=low
+
+  * Use debhelper v7, rules file minimisation.
+  * Add a Makefile.
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 23 Apr 2008 21:56:06 -0400
+
+mr (0.27) unstable; urgency=low
+
+  * Ignore exit code from darcs whatsnew, which can be nonzero if there
+    are no changes. Closes: #476650
+
+ -- Joey Hess <joeyh@debian.org>  Sat, 19 Apr 2008 16:47:04 -0400
+
+mr (0.26) unstable; urgency=low
+
+  * Add -i option to start a shell if an operation fails. Closes: #474962
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 08 Apr 2008 20:58:44 -0400
+
+mr (0.25) unstable; urgency=low
+
+  * git-fake-bare: Add support for diff, commit, and record. (madduck)
+
+ -- Joey Hess <joeyh@debian.org>  Mon, 10 Mar 2008 15:56:17 -0400
+
+mr (0.24) unstable; urgency=low
+
+  * Correct GIT_CONFIG settings in git-svn. Closes: #463941
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 06 Feb 2008 13:04:30 -0500
+
+mr (0.23) unstable; urgency=low
+
+  * Fix tests in git-svn include file to use MR_REPO. Closes: #463941
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 05 Feb 2008 13:26:05 -0500
+
+mr (0.22) unstable; urgency=low
+
+  * Update bzr registration code to work with current version of bzr,
+    using bzr info to get the url. Closes: #463867
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 03 Feb 2008 17:04:59 -0500
+
+mr (0.21) unstable; urgency=low
+
+  * Add a git-svn file, which can be included to add support for git-svn
+    repositories. Thanks to Clifford W. Hansen and Bastian Kleineidam
+    for implementations. Closes: #462408
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 24 Jan 2008 13:45:05 -0500
+
+mr (0.20) unstable; urgency=low
+
+  * Add -q flag.
+  * darcs: Add -u to diff to get a more usual unified diff.
+  * Add a "record" subcommand, borrowing termonology from darcs. This does a
+    local commit, but does not push changes to remote repos.
+  * Improve unison support.
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 02 Jan 2008 22:49:53 -0500
+
+mr (0.19) unstable; urgency=low
+
+  * Support versions of man that don't use -l.
+  * Updating git repos no longer uses git-pull -t by default. Git makes it
+    to much of a PITA to do this, since -t makes git-pull require the
+    repository and refspec be specified at the command line (which is a bug in
+    git (#456035). mr used to hardcode those to "origin" and "master", but
+    that's not always the right choice. So give up on forcing git to be sane
+    about pulling down all tags. It's insane. Live with it, or configure your
+    own update command.
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 12 Dec 2007 21:30:45 -0500
+
+mr (0.18) unstable; urgency=low
+
+  * darcs: Use record subcommand, there is no commit subcommand. Closes: #453501
+  * darcs: De-p register code. Closes: #453502
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 29 Nov 2007 17:48:09 -0500
+
+mr (0.17) unstable; urgency=low
+
+  * Even bigger hammer: Set LC_ALL. Closes: #453305
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 29 Nov 2007 11:18:40 -0500
+
+mr (0.16) unstable; urgency=low
+
+  * Use LC_MESSAGES=C not LANG=C, as if the user has LC_MESSAGES set, LANG
+    won't override it. Closes: #453305
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 28 Nov 2007 15:46:38 -0500
+
+mr (0.15) unstable; urgency=low
+
+  [ Joey Hess ]
+  * Allow -n to be passed a number to specify how deep to go into
+    subdirectories to find repositories to act on. Closes: #450621
+
+  [ Josh Triplett ]
+  * Make -j with no argument run unlimited jobs in parallel, like make -j
+    Closes: #452467
+
+  [ Joey Hess ]
+  * Add a warning about runing too many jobs at a time.
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 25 Nov 2007 13:13:47 -0500
+
+mr (0.14) unstable; urgency=low
+
+  * Portability fixes for the non-POSIX shell of SunOS 5. Patch from Ken
+    Bloom. Closes: #449592
+  * Cut number of rcs tests run in half.
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 11 Nov 2007 01:10:18 -0500
+
+mr (0.13) unstable; urgency=low
+
+  * -c was broken, fix. Closes: #449539
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 06 Nov 2007 11:39:04 -0500
+
+mr (0.12) unstable; urgency=low
+
+  * Avoid creating parent directory for a checkout that will ultimately be
+    skipped.
+  * Don't try to test the repo type when doing a checkout, that can't work
+    since the repo isn't there yet. It was actually checking the repo type
+    of the parent directory, which caused several unexpected behaviors.
+
+ -- Joey Hess <joeyh@debian.org>  Sat, 03 Nov 2007 14:22:00 -0400
+
+mr (0.11) unstable; urgency=low
+
+  * Avoid using commands like git-config and instead use "git config".
+    In some configurations, only the main git command is in the path.
+  * Allow option bundling, mostly so "-j2" will work.
+  * Better error message if more than one repo type test matches a single
+    directory.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 02 Nov 2007 23:28:52 -0400
+
+mr (0.10) unstable; urgency=low
+
+  * Add a lib file for using unison with mr.
+  * Make registration work when no config file yet exists. Closes: #448422
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 28 Oct 2007 19:55:03 -0400
+
+mr (0.9) unstable; urgency=low
+
+  * Split up actions, so each rcs has its own set of action commands,
+    and mr tests to see what rcs is used by a repository, and dispatches
+    the command.
+
+    This will make it much easier to add new rcses, or modify just the
+    command that mr uses for one command for one rcs, without duplicating a
+    lot of code, and without needing to modify mr at all.
+
+    The old style unsplit actions are still supported, and are what most
+    mrconfig files will still use; this change is fully backwards compatible.
+  * Changed some things in the enviroment for the register action.
+    It's now run in the directory that the user specifies, and MR_REPO
+    is set to contain the basename of the directory that the checkout
+    command should check out. These changes should be backware compatible
+    to existing register actions.
+  * Add info and warning functions to the shell library. (madduck)
+  * Add git_bare and git_fake_bare rcs types to handle
+    bare and fake bare (non-bare with a detached worktree) git repositories.
+    (Diff and commit do not work yet to fake bare repos). (madduck)
+  * Add a vim modeline to preserve joey's tabbing prefs. (madduck)
+  * Add support for including one mrconfig file from another. Unlike chaining,
+    this doesn't change the paths, and is not tied to a particular
+    subdirectory. It's useful for loading up library mrconfig files.
+  * Split git fake-bare support out into a lib/git-fake-bare.
+    Partly because it's a good example of how to add a new revision control
+    type and use includes, and partly because it's currently too ugly to be
+    in mr itself due to bugs and limitations in git.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 26 Oct 2007 03:00:40 -0400
+
+mr (0.8) unstable; urgency=low
+
+  * Improve "in subdir" message.
+  * Patch from Simon McVittie, to pass -a to darcs commands to avoid
+    interactive updates. Closes: #447999
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 25 Oct 2007 06:22:45 -0400
+
+mr (0.7) unstable; urgency=low
+
+  * Fix inverted tests in tags command. (madduck)
+  * Patch from Simon McVittie, adding support for darcs repositories.
+    Closes: #447729
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 24 Oct 2007 01:18:27 -0400
+
+mr (0.6) unstable; urgency=low
+
+  * Add to the example mrconfig a "tags" command that lists tags.
+    (Currently only for svn and git.)
+  * hours_since was broken by design, and fixing it involved changing
+    its calling convention. If you used the old version, the new version
+    will error out.
+  * Add ability to reorder repos, if you want mr to act on a given repo first
+    or last.
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 21 Oct 2007 21:53:28 -0400
+
+mr (0.5) unstable; urgency=low
+
+  [ Joey Hess ]
+  * Removed special case repository deletion handling code. The same
+    thing can be accomplished in a mrconfig by skipping a repo unless
+    it exists, and printing a reminder on update. See the mrconfig file
+    for an example.
+  * Fix output of "mr config DEFAULT lib".
+  * Change mr update to use git pull -t origin master, to make sure new tags
+    are pulled. And since those might not always be the right parameters
+    for git pull, any parameters passed to mr update will replace them.
+
+  [ Alexander Wirt ]
+  * Add support for mercurial.
+
+  [ Joey Hess ]
+  * Incorporate code based on Anthony Towns's mrs, to allow running
+    multiple jobs in parallel. The -j flag controls this.
+    This can produce enormous speedups. For me, mr update takes
+    12 minutes, while mr -j 10 update takes 1 minute!
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 21 Oct 2007 01:27:23 -0400
+
+mr (0.4) unstable; urgency=low
+
+  * Fix mr register of a subdir to also work with -c.
+  * Signal handling for commands run by mr, including handling of SIGINT
+    to stop mr.
+  * Ensure parent dirs exist prior to checkout. (madduck)
+  * Output list of failed repos to stderr when -s is used.
+  * Fix a bug caused by a stupid typo.
+  * Add the -n switch, for disabling recursion.
+  * Allow for more complex deleted tests, such as marking a repo deleted on
+    some hosts, while not on others.
+  * Support registering CVS repositories. Closes: #447171
+  * MR_CONFIG now points to the config file that contains the repo mr is
+    acting on.
+  * Make mr register smarter about what mrconfig file to write to.
+    (It probably does what you'd expect it to do now; if it doesn't, -c
+    can still override.)
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 18 Oct 2007 16:20:34 -0400
+
+mr (0.3) unstable; urgency=low
+
+  * Add a check to make sure the expected directory exists after checkout.
+  * mr register will now write to whatever config file is specified with -c
+  * Typo fixes from madduck.
+
+ -- Joey Hess <joeyh@debian.org>  Wed, 17 Oct 2007 12:48:55 -0400
+
+mr (0.2) unstable; urgency=low
+
+  * Fix line number display for config file parse errors.
+  * Fix a bug in inheritance of default settings in chained .mrconfig files.
+  * Add the -s option.
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 16 Oct 2007 02:39:08 -0400
+
+mr (0.1) unstable; urgency=low
+
+  * First release.
+
+ -- Joey Hess <joeyh@debian.org>  Sun, 14 Oct 2007 14:14:40 -0400
diff --git a/debian/compat b/debian/compat
new file mode 100644 (file)
index 0000000..ec63514
--- /dev/null
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..a9b6f21
--- /dev/null
@@ -0,0 +1,38 @@
+Source: myrepos
+Section: vcs
+Priority: optional
+Build-Depends: debhelper (>= 9), dpkg-dev (>= 1.9.0)
+Maintainer: Joey Hess <joeyh@debian.org>
+Standards-Version: 3.9.4
+Homepage: http://myrepos.branchable.com/
+Vcs-Git: git://myrepos.branchable.com/
+
+Package: myrepos
+Architecture: all
+Section: vcs
+Depends: ${misc:Depends}
+Suggests: subversion, git-core | git (>= 1:1.7), cvs, bzr, mercurial, darcs, fossil, vcsh, liburi-perl, curl
+Provides: mr
+Replaces: mr
+Recommends: libwww-perl, libhtml-parser-perl, perl
+Description: tool to manage all your version control repos
+ The mr(1) command can checkout, update, or perform other actions on
+ a set of repositories as if they were one combined respository. It
+ supports any combination of git, svn, mercurial, bzr, darcs, cvs, vcsh,
+ fossil, and veracity repositories, and support for other version control
+ systems can easily be added. (There are extensions adding support for unison
+ and git-svn, among others.)
+ .
+ It is extremely configurable via simple shell scripting. Some examples
+ of things it can do include:
+ .
+  * Update a repository no more frequently than once every twelve hours.
+  * Run an arbitrary command before committing to a repository.
+  * When updating a git repository, pull from two different upstreams
+    and merge the two together.
+  * Run several repository updates in parallel, greatly speeding up
+    the update process.
+  * Remember actions that failed due to a laptop being offline, so they
+    can be retried when it comes back online.
+  .
+  This package also includes the webcheckout command.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..9c2279e
--- /dev/null
@@ -0,0 +1,7 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files: *
+Copyright: (c) 2007-2011 Joey Hess <joeyh@debian.org>
+License: GPL-2+
+ On Debian systems, the complete text of the GPL can be found in
+ /usr/share/common-licenses/GPL.
diff --git a/debian/docs b/debian/docs
new file mode 100644 (file)
index 0000000..e845566
--- /dev/null
@@ -0,0 +1 @@
+README
diff --git a/debian/examples b/debian/examples
new file mode 100644 (file)
index 0000000..3e7cc29
--- /dev/null
@@ -0,0 +1 @@
+mrconfig mrconfig.complex
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..d1c5d97
--- /dev/null
@@ -0,0 +1,7 @@
+#!/usr/bin/make -f
+%:
+       dh $@
+
+# Not intended for use by anyone except the author.
+announcedir:
+       @echo ${HOME}/src/myrepos/doc/news
diff --git a/doc/index.mdwn b/doc/index.mdwn
new file mode 100644 (file)
index 0000000..784fcf8
--- /dev/null
@@ -0,0 +1,81 @@
+You have a lot of version control repositories. Sometimes you want to
+update them all at once. Or push out all your local changes. You use
+special command lines in some repositories to implement specific workflows.
+Myrepos provides a `mr` command, which is a tool to manage all your version
+control repositories.
+
+## getting started
+
+All you need to get started is some already checked out repos.
+These could be using git, or bzr, mercurial or darcs, or many other version
+control systems. Doesn't matter, they're all supported!
+
+Inside each of your repositories, run `mr register`.
+That sets up a `~/.mrconfig` file listing your repositories.
+
+Now you can run `mr update` in your home directory, and it'll update
+every one of your repositories that you've registered with myrepos.
+
+Want to update repositories in parallel? `mr update -j5` will run 5
+concurrent jobs!
+
+If you run `mr update` inside a repository, it'll only act on that
+repository. In general, any `mr` command runs recursively over any
+repository located somewhere in or under the current directory.
+
+You can also run `mr commit`, `mr push`, `mr status`, `mr diff`, and a lot
+of other commands. These work no matter which version control system is
+used for a repository. Of course, you can still use the native version
+control commands too.
+
+Oh, and you can abbreviate any command to any unambiguous abbreviation. 
+`mr up`, `mr pu`, etc.
+
+Now, maybe you find that you always want to update one repository using
+`git pull --rebase`, instead of the default `git pull` that `mr update` runs. 
+No problem: The `~/.mrconfig` file makes it easy to override the command
+run for any repository. It's like a `Makefile` for repositories.
+
+       [foo]
+       checkout = git@github.com:joeyh/foo.git
+       update = git pull --rebase
+
+You can make up your own commands too:
+
+       [bar]
+       # This repository has an upstream, which I've forked; 
+       # set up a remote on checkout.
+       checkout = 
+               git clone git@github.com:joeyh/bar.git
+               cd bar
+               git remote add upstream git@github.com:barbar/bar.git
+       # make `mr zap` integrate from upstream
+       zap = 
+               git pull upstream
+               git merge upstream/master
+               git push origin master
+
+You can even define commands globally, so `mr` can use them in all repositories.
+
+       [DEFAULT]
+       # Teach mr how to `mr gc` in git repos.
+       git_gc = git gc "$@"
+
+This only scratches the surface of the ways you can use myrepos to automate
+and mange your repositories!
+
+Some more examples of things it can do include:
+
+* Update a repository no more frequently than once every twelve hours.
+* Run an arbitrary command before committing to a repository.
+* Remember actions that failed due to a laptop being offline, so they
+  can be retried when it comes back online.
+* Combine several related repositories into a single logical repository,
+  with its own top-level `.mrconfig` file that lists them and can be
+  chain loaded from `~/.mrconfig`.
+* Manage your whole home directory in version control.
+  (See [VCS-Home](http://vcs-home.branchable.com/))
+
+## news
+
+[[!inline pages="news/* and !*/Discussion" show="4" archive=yes]]
diff --git a/doc/install.mdwn b/doc/install.mdwn
new file mode 100644 (file)
index 0000000..7e27528
--- /dev/null
@@ -0,0 +1,7 @@
+`git clone git://myrepos.branchable.com/ myrepos`  
+Or get it [from github](https://github.com/joeyh/myrepos).
+
+It's a simple perl script, which can be copied anywhere to install.
+
+Myrepos is included in all recent versions of Debian. `apt-get install mr`  
+It's also in Mac Homebrew as `mr`.
diff --git a/doc/news/mr_renamed_to_myrepos.mdwn b/doc/news/mr_renamed_to_myrepos.mdwn
new file mode 100644 (file)
index 0000000..39bc885
--- /dev/null
@@ -0,0 +1,7 @@
+I've renamed mr because myrepos is a more descriptive name that's
+easier to search for.
+
+The `mr` command itself will not be renamed, this is only a renaming of the
+package.
+
+[[!meta author=Joey]]
diff --git a/doc/news/version_1.20130705.1.mdwn b/doc/news/version_1.20130705.1.mdwn
new file mode 100644 (file)
index 0000000..91f7cf3
--- /dev/null
@@ -0,0 +1,3 @@
+myrepos 1.20130705.1 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+   * Conflict and Replaces mr."""]]
\ No newline at end of file
diff --git a/doc/news/version_1.20130705.mdwn b/doc/news/version_1.20130705.mdwn
new file mode 100644 (file)
index 0000000..2053bfe
--- /dev/null
@@ -0,0 +1,6 @@
+myrepos 1.20130705 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+   * The package is renamed to myrepos. It Provides mr, so can still be
+     installed by that name. The mr command is not renamed.
+   * Add make install rule.
+     Thanks, v4hn"""]]
\ No newline at end of file
diff --git a/doc/sidebar.mdwn b/doc/sidebar.mdwn
new file mode 100644 (file)
index 0000000..d0b9866
--- /dev/null
@@ -0,0 +1,3 @@
+* [[install]]
+* [[todo]]
+* <a href="https://flattr.com/thing/39937/mr1" target="_blank"><img src="https://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0" /></a>
diff --git a/doc/todo.mdwn b/doc/todo.mdwn
new file mode 100644 (file)
index 0000000..417d7c2
--- /dev/null
@@ -0,0 +1,6 @@
+This is myrepos's todo list. Link items to [[todo/done]] when done.
+
+See also: [Debian BTS](http://bugs.debian.org/mr).
+
+[[!inline pages="./todo/* and !./todo/done and !link(done)
+and !*/Discussion" actions=yes postform=yes show=0 archive=yes]]
diff --git a/doc/todo/detect_unregistered_repos_in_tree.mdwn b/doc/todo/detect_unregistered_repos_in_tree.mdwn
new file mode 100644 (file)
index 0000000..bc2126b
--- /dev/null
@@ -0,0 +1,3 @@
+a way to detect repos in a tree that are not registered, and warn
+about or even auto-register them. (svn externals make this quite
+difficult!)
diff --git a/doc/todo/done.mdwn b/doc/todo/done.mdwn
new file mode 100644 (file)
index 0000000..e7c9808
--- /dev/null
@@ -0,0 +1,4 @@
+recently fixed [[todo]] items.
+
+[[!inline pages="./* and link(./done) and !*/Discussion" sort=mtime show=10
+archive=yes]]
diff --git a/doc/todo/smarter_chained_checkout.mdwn b/doc/todo/smarter_chained_checkout.mdwn
new file mode 100644 (file)
index 0000000..13e5d5a
--- /dev/null
@@ -0,0 +1,7 @@
+When there are chained mrconfig files, mr could be smarter about
+checkouts and updates. Ie, when a new version of an mrconfig file is
+checked out or updated, throw all the info from the old one away, and
+process the new one.
+
+Until this is fixed, checkouts and updates need to be manually repeated
+after mrconfig files have changes. (See #447553)
diff --git a/lib/git-fake-bare b/lib/git-fake-bare
new file mode 100644 (file)
index 0000000..5091d95
--- /dev/null
@@ -0,0 +1,86 @@
+# An example of how to add a new version control system type to mr.
+# git fake bare repositories have a detached workspace. One potential
+# application is storing dotfiles in git, keeping them checked out in
+# ones $HOME, but checked into different git repositories. This file adds
+# support for them, separate from the normal git support.
+
+# To make mr use this file, add a line like this inside the [DEFAULT]
+# section of your ~/.mrconfig
+#include = cat /usr/share/mr/git-fake-bare
+
+# And an example repo using it would look something like:
+#[.dotfiles]
+#checkout = git_fake_bare_checkout git://... .dotfiles <worktree, eg. ..>
+
+lib =
+       # git doesn't have an easy way to check out such a repo, so
+       # do it by hand
+       git_fake_bare_checkout() {
+               local url; url="$1"
+               local repo; repo="$2"
+               local worktree; worktree="$3"
+               git clone --no-checkout "$url" "$repo"
+               cd "$repo"
+               mkdir -p "$worktree"
+               PWD="`pwd`"
+               mv .git/* .
+               rmdir .git
+               GIT_DIR="$PWD" git read-tree HEAD
+               GIT_DIR="$PWD" git checkout-index -a --prefix="$worktree" || true
+               GIT_DIR="$PWD" git config core.worktree "$worktree"
+       }
+       git_get_worktree() {
+               local worktree
+               worktree="$(git config --get core.worktree)" || true
+               if [ -z "$worktree" ]; then
+                       error "git worktree is not set"
+               fi
+               worktree="${worktree%%/}/"
+               if [ ! -d "$worktree" ]; then
+                       error "git worktree $worktree does not exist"
+               fi
+               echo "$worktree"
+       }
+
+git_fake_bare_test = perl:
+       -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
+       -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
+       `GIT_DIR="$ENV{MR_REPO}" git config --get core.worktree` ne ""
+
+git_fake_bare_update =
+       args="$@"
+       branch="$(GIT_DIR="$MR_REPO" git symbolic-ref HEAD | sed -e 's,.*/,,')"
+       [ -z "$args" ] && args="-t origin $branch"
+       GIT_DIR="$MR_REPO" git pull $args
+
+git_fake_bare_status = GIT_DIR="$MR_REPO" git status -s "$@" || true
+
+git_fake_bare_commit =
+       cd "$(git_get_worktree)"
+       GIT_DIR="$MR_REPO" git commit -a "$@"
+       GIT_DIR="$MR_REPO" git push --all
+
+git_fake_bare_push =
+       GIT_DIR="$MR_REPO" git push --all
+
+git_fake_bare_record = 
+       cd "$(git_get_worktree)"
+       GIT_DIR="$MR_REPO" git commit -a "$@"
+
+git_fake_bare_diff =
+       cd "$(git_get_worktree)"
+       GIT_DIR="$MR_REPO" git diff "$@"
+
+git_fake_bare_log = GIT_DIR="$MR_REPO" git log "$@"
+
+git_fake_bare_register = 
+       url="$(LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url)" || true
+       if [ -z "$url" ]; then
+               error "cannot determine git url"
+       fi
+       worktree="$(git_get_worktree)"
+       echo "Registering git url: $url in $MR_CONFIG (with worktree $worktree)"
+       mr -c "$MR_CONFIG" config "`pwd`" \
+               checkout="git_fake_bare_checkout '$url' '$MR_REPO' '$worktree'"
+
+# vim:sw=8:sts=0:ts=8:noet
diff --git a/lib/git-subtree b/lib/git-subtree
new file mode 100644 (file)
index 0000000..3943924
--- /dev/null
@@ -0,0 +1,44 @@
+# Add support for Avery Pennarun's git-subtree
+#
+# To make mr use this file, add a line like this inside the [DEFAULT]
+# section of your ~/.mrconfig
+#include = cat /usr/share/mr/git-subtree
+#
+# Example:
+#
+# [repo]
+# checkout = git clone git.example.org:repo.git
+#
+# [repo/lib]
+# git_subtree_test = true
+# checkout = git subtree add --prefix=lib git.example.org:lib.git master
+# update = git_subtree_update --prefix=lib git.example.org:lib.git master
+# push = git_subtree_push --prefix=lib git.example.org:lib.git master
+
+lib =
+       git_get_toplevel() {
+               local toplevel
+               toplevel="$(git rev-parse --show-toplevel)" || true
+               if [ -z "$toplevel" ]; then
+                       error "git toplevel is not set"
+               fi
+               toplevel="${toplevel%%/}/"
+               if [ ! -d "$toplevel" ]; then
+                       error "git toplevel $toplevel does not exist"
+               fi
+               echo "$toplevel"
+       }
+       git_subtree_update() {
+               cd "$(git_get_toplevel)"
+               git subtree pull "$@"
+       }
+       git_subtree_push() {
+               cd "$(git_get_toplevel)"
+               git subtree push "$@"
+       }
+
+git_subtree_status = git status -s "$@" . || true
+git_subtree_commit = git add . && git commit "$@" && git_subtree_push
+git_subtree_record = git add . && git commit "$@"
+git_subtree_diff = git diff .
+git_subtree_log = git log .
diff --git a/lib/git-svn b/lib/git-svn
new file mode 100644 (file)
index 0000000..212923a
--- /dev/null
@@ -0,0 +1,35 @@
+# Adds support for git-svn repositories.
+
+# To make mr use this file, add a line like this inside the [DEFAULT]
+# section of your ~/.mrconfig
+#include = cat /usr/share/mr/git-svn
+#
+# Note that by default this makes mr update do a git svn fetch.
+# Some might prefer it to do a git svn rebase, if you do, you can
+# configure that as follows in your ~/.mrconfig:
+#git_svn_update = git svn rebase
+git_svn_update = git svn fetch
+git_svn_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true
+git_svn_commit = git svn dcommit
+git_svn_push = git svn dcommit
+git_svn_record = git commit -a "$@"
+git_svn_diff = git diff "$@"
+git_svn_log = git svn log "$@"
+
+git_test = perl:
+       -d "$ENV{MR_REPO}/.git" &&
+       `GIT_CONFIG="$ENV{MR_REPO}"/.git/config git config --get svn-remote.svn.url` eq ""
+
+git_svn_test = perl:
+       -d "$ENV{MR_REPO}/.git" &&
+       `GIT_CONFIG="$ENV{MR_REPO}"/.git/config git config --get svn-remote.svn.url` ne ""
+
+git_svn_register =
+       url="`LC_ALL=C git config --get svn-remote.svn.url`" || true
+       if [ -z "$url" ]; then
+               error "cannot determine git svn url"
+       fi
+       echo "Registering git svn url: $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="git svn clone '$url' '$MR_REPO'"
+
+# vim:sw=8:sts=0:ts=8:noet
diff --git a/lib/repo b/lib/repo
new file mode 100644 (file)
index 0000000..418276d
--- /dev/null
+++ b/lib/repo
@@ -0,0 +1,12 @@
+# Adds support for repo repositories.
+# http://source.android.com/source/version-control.html
+
+# To make mr use this file, add a line like this inside the [DEFAULT]
+# section of your ~/.mrconfig
+#include = cat /usr/share/mr/repo
+
+repo_test = perl: -d "$ENV{MR_REPO}/.repo"
+repo_diff = repo diff "$@"
+repo_grep = repo grep "$@"
+repo_status = repo status "$@"
+repo_update = repo sync "$@"
diff --git a/lib/unison b/lib/unison
new file mode 100644 (file)
index 0000000..ffe5205
--- /dev/null
@@ -0,0 +1,37 @@
+# This allows using unison as a "version control system" with mr.
+# 
+# You need to configure unison by setting up files in ~/.unison named
+# the same as the basenames of the directories you want to sync, and
+# containing unison configuration to sync them.
+#
+# By default commit will be interactive; you can set batch mode in the
+# config file to disable this. All other commands use batch mode by
+# default.
+
+# To make mr use this file, add a line like this inside the [DEFAULT]
+# section of your ~/.mrconfig
+#include = cat /usr/share/mr/unison
+
+# And an example repo using it would look something like:
+#[music]
+#unison_test = true
+#checkout = unison_checkout music
+
+lib =
+       # The name of the directory containing the repo is assumed to
+       # match that of a unison config file.
+       unison_config() {
+               basename "$MR_REPO"
+       }
+       unison_batch() {
+               unison -batch $(unison_config)
+       }
+       unison_checkout() {
+               mkdir "$1" && cd "$1" && unison -batch "$1"
+       }
+
+unison_update   = unison_batch
+unison_push     = unison_batch
+unison_commit   = unison $(unison_config)
+
+# vim:sw=8:sts=0:ts=8:noet
diff --git a/lib/vcsh b/lib/vcsh
new file mode 100644 (file)
index 0000000..56f81d5
--- /dev/null
+++ b/lib/vcsh
@@ -0,0 +1,10 @@
+# To make mr use this file, add a line like this inside the [DEFAULT]
+# section of your ~/.mrconfig
+#include = cat /usr/share/mr/vcsh
+
+# But, that's pointless to do, since the vcsh support has been moved from
+# this plugin into mr.
+
+# And an example repo using it would look something like:
+#[$HOME/.config/vcsh/repo.d/zsh.git]
+#checkout = vcsh clone git://github.com/RichiH/zshrc.git zsh
diff --git a/lib/vis b/lib/vis
new file mode 100644 (file)
index 0000000..6dff868
--- /dev/null
+++ b/lib/vis
@@ -0,0 +1,12 @@
+# Adds a "mr vis" command to visualise repository history.
+
+# To make mr use this file, add a line like this inside the [DEFAULT]
+# section of your ~/.mrconfig
+#include = cat /usr/share/mr/vis
+
+cvs_vis = cvs diff | $EDITOR -
+svn_vis = svn status | $EDITOR -
+git_svn_vis = gitk --all
+git_vis = gitk --all
+bzr_vis = bzr visualize
+hg_vis = hg view
diff --git a/mr b/mr
new file mode 100755 (executable)
index 0000000..435fabb
--- /dev/null
+++ b/mr
@@ -0,0 +1,2132 @@
+#!/usr/bin/perl
+
+=head1 NAME
+
+mr - a tool to manage all your version control repos
+
+=head1 SYNOPSIS
+
+B<mr> [options] checkout
+
+B<mr> [options] update
+
+B<mr> [options] status
+
+B<mr> [options] commit [-m "message"]
+
+B<mr> [options] record [-m "message"]
+
+B<mr> [options] fetch
+
+B<mr> [options] push
+
+B<mr> [options] diff
+
+B<mr> [options] log
+
+B<mr> [options] run command [param ...]
+
+B<mr> [options] bootstrap url [directory]
+
+B<mr> [options] register [repository]
+
+B<mr> [options] config section ["parameter=[value]" ...]
+
+B<mr> [options] action [params ...]
+
+B<mr> [options] [online|offline]
+
+B<mr> [options] remember action [params ...]
+
+=head1 DESCRIPTION
+
+B<mr> is a tool to manage all your version control repos. It can checkout,
+update, or perform other actions on a set of repositories as if they were
+one combined repository. It supports any combination of subversion, git,
+cvs, mercurial, bzr, darcs, fossil and veracity repositories, and support
+for other version control systems can easily be added.
+
+B<mr> cds into and operates on all registered repositories at or below your
+working directory. Or, if you are in a subdirectory of a repository that
+contains no other registered repositories, it will stay in that directory,
+and work on only that repository,
+
+B<mr> is configured by .mrconfig files, which list the repositories. It
+starts by reading the .mrconfig file in your home directory, and this can
+in turn chain load .mrconfig files from repositories. It also automatically
+looks for a .mrconfig file in the current directory, or in one of its
+parent directories.
+
+These predefined commands should be fairly familiar to users of any version
+control system:
+
+=over 4
+
+=item checkout (or co)
+
+Checks out any repositories that are not already checked out.
+
+=item update
+
+Updates each repository from its configured remote repository.
+
+If a repository isn't checked out yet, it will first check it out.
+
+=item status
+
+Displays a status report for each repository, showing what
+uncommitted changes are present in the repository. For distributed version
+control systems, also shows unpushed local branches.
+
+=item commit (or ci)
+
+Commits changes to each repository. (By default, changes are pushed to the
+remote repository too, when using distributed systems like git. If you
+don't like this default, you can change it in your .mrconfig, or use record
+instead.)
+
+The optional -m parameter allows specifying a commit message.
+
+=item record
+
+Records changes to the local repository, but does not push them to the
+remote repository. Only supported for distributed version control systems.
+
+The optional -m parameter allows specifying a commit message.
+
+=item fetch
+
+Fetches from each repository's remote repository, but does not
+update the working copy. Only supported for some distributed version
+control systems.
+
+=item push
+
+Pushes committed local changes to the remote repository. A no-op for
+centralized version control systems.
+
+=item diff
+
+Show a diff of uncommitted changes.
+
+=item log
+
+Show the commit log.
+
+=item run command [param ...]
+
+Runs the specified command in each repository.
+
+=back
+
+These commands are also available:
+
+=over 4
+
+=item bootstrap url [directory]
+
+Causes mr to download the url, and use it as a .mrconfig file to checkout
+the repositories listed in it, into the specified directory.
+
+To use scp to download, the url may have the form ssh://[user@]host:file
+
+The directory will be created if it does not exist. If no directory is
+specified, the current directory will be used.
+
+If the .mrconfig file includes a repository named ".", that
+is checked out into the top of the specified directory.
+
+=item list (or ls)
+
+List the repositories that mr will act on.
+
+=item register
+
+Register an existing repository in a mrconfig file. By default, the
+repository in the current directory is registered, or you can specify a
+directory to register.
+
+The mrconfig file that is modified is chosen by either the -c option, or by
+looking for the closest known one at or in a parent of the current directory.
+
+=item config
+
+Adds, modifies, removes, or prints a value from a mrconfig file. The next
+parameter is the name of the section the value is in. To add or modify
+values, use one or more instances of "parameter=value". Use "parameter=" to
+remove a parameter. Use just "parameter" to get the value of a parameter.
+
+For example, to add (or edit) a repository in src/foo:
+
+  mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
+
+To show the command that mr uses to update the repository in src/foo:
+
+  mr config src/foo update
+
+To see the built-in library of shell functions contained in mr:
+
+  mr config DEFAULT lib
+
+The mrconfig file that is used is chosen by either the -c option, or by
+looking for the closest known one at or in a parent of the current directory.
+
+=item offline
+
+Advises mr that it is in offline mode. Any commands that fail in
+offline mode will be remembered, and retried when mr is told it's online.
+
+=item online
+
+Advices mr that it is in online mode again. Commands that failed while in
+offline mode will be re-run.
+
+=item remember
+
+Remember a command, to be run later when mr re-enters online mode. This
+implicitly puts mr into offline mode. The command can be any regular mr
+command. This is useful when you know that a command will fail due to being
+offline, and so don't want to run it right now at all, but just remember
+to run it when you go back online.
+
+=item help
+
+Displays this help.
+
+=back
+
+Actions can be abbreviated to any unambiguous substring, so
+"mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr
+update"
+
+Additional parameters can be passed to most commands, and are passed on
+unchanged to the underlying version control system. This is mostly useful
+if the repositories mr will act on all use the same version control
+system.
+
+=head1 OPTIONS
+
+=over 4
+
+=item -d directory
+
+=item --directory directory
+
+Specifies the topmost directory that B<mr> should work in. The default is
+the current working directory.
+
+=item -c mrconfig
+
+=item --config mrconfig
+
+Use the specified mrconfig file. The default is to use both F<~/.mrconfig>
+as well as look for a F<.mrconfig> file in the current directory, or in one
+of its parent directories.
+
+=item -f
+
+=item --force
+
+Force mr to act on repositories that would normally be skipped due to their
+configuration.
+
+=item -v
+
+=item --verbose
+
+Be verbose.
+
+=item -q
+
+=item --quiet
+
+Be quiet. This suppresses mr's usual output, as well as any output from
+commands that are run (including stderr output). If a command fails,
+the output will be shown.
+
+=item -k
+
+=item --insecure
+
+Accept untrusted SSL certificates when bootstrapping.
+
+=item -s
+
+=item --stats
+
+Expand the statistics line displayed at the end to include information
+about exactly which repositories failed and were skipped, if any.
+
+=item -i
+
+=item --interactive
+
+Interactive mode. If a repository fails to be processed, a subshell will be
+started which you can use to resolve or investigate the problem. Exit the
+subshell to continue the mr run.
+
+=item -n [number]
+
+=item --no-recurse [number]
+
+If no number if specified, just operate on the repository for the current
+directory, do not recurse into deeper repositories.
+
+If a number is specified, will recurse into repositories at most that many
+subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
+but not ./src/packages/bar.
+
+=item -j [number]
+
+=item --jobs [number]
+
+Run the specified number of jobs in parallel, or an unlimited number of jobs
+with no number specified. This can greatly speed up operations such as updates.
+It is not recommended for interactive operations.
+
+Note that running more than 10 jobs at a time is likely to run afoul of
+ssh connection limits. Running between 3 and 5 jobs at a time will yield
+a good speedup in updates without loading the machine too much.
+
+=item -t
+
+=item --trust-all
+
+Trust all mrconfig files even if they are not listed in F<~/.mrtrust>.
+Use with caution.
+
+=item -p
+
+=item --path
+
+This obsolete flag is ignored.
+
+=back
+
+=head1 MRCONFIG FILES
+
+Here is an example F<.mrconfig> file:
+
+  [src]
+  checkout = svn checkout svn://svn.example.com/src/trunk src
+  chain = true
+
+  [src/linux-2.6]
+  checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
+       cd linux-2.6 &&
+       git checkout -b mybranch origin/master
+
+The F<.mrconfig> file uses a variant of the INI file format. Lines
+starting with "#" are comments. Values can be continued to the
+following line by indenting the line with whitespace.
+
+The C<DEFAULT> section allows setting default values for the sections that
+come after it.
+
+The C<ALIAS> section allows adding aliases for actions. Each parameter
+is an alias, and its value is the action to use.
+
+All other sections add repositories. The section header specifies the
+directory where the repository is located. This is relative to the directory
+that contains the mrconfig file, but you can also choose to use absolute
+paths. (Note that you can use environment variables in section names; they
+will be passed through the shell for expansion. For example, 
+C<[$HOSTNAME]>, or C<[${HOSTNAME}foo]>).
+
+Within a section, each parameter defines a shell command to run to handle a
+given action. mr contains default handlers for "update", "status",
+"commit", and other standard actions.
+
+Normally you only need to specify what to do for "checkout". Here you
+specify the command to run in order to create a checkout of the repository.
+The command will be run in the parent directory, and must create the
+repository's directory. So use C<git clone>, C<svn checkout>, C<bzr branch>
+or C<bzr checkout> (for a bound branch), etc.
+
+Note that these shell commands are run in a C<set -e> shell
+environment, where any additional parameters you pass are available in
+C<$@>. All commands other than "checkout" are run inside the repository,
+though not necessarily at the top of it.
+
+The C<MR_REPO> environment variable is set to the path to the top of the
+repository. (For the "register" action, "MR_REPO" is instead set to the 
+basename of the directory that should be created when checking the
+repository out.)
+
+The C<MR_CONFIG> environment variable is set to the .mrconfig file
+that defines the repo being acted on, or, if the repo is not yet in a config
+file, the F<.mrconfig> file that should be modified to register the repo.
+
+The C<MR_ACTION> environment variable is set to the command being run
+(update, checkout, etc).
+
+A few parameters have special meanings:
+
+=over 4
+
+=item skip
+
+If the "skip" parameter is set and its command returns true, then B<mr>
+will skip acting on that repository. The command is passed the action
+name in C<$1>.
+
+Here are two examples. The first skips the repo unless
+mr is run by joey. The second uses the hours_since function
+(included in mr's built-in library) to skip updating the repo unless it's
+been at least 12 hours since the last update.
+
+  [mystuff]
+  checkout = ...
+  skip = test `whoami` != joey
+
+  [linux]
+  checkout = ...
+  skip = [ "$1" = update ] && ! hours_since "$1" 12
+Another way to use skip is for a lazy checkout. This makes mr skip
+operating on a repo unless it already exists. To enable the 
+repo, you have to explicitly check it out (using "mr --force -d foo checkout").
+
+  [foo]
+  checkout = ...
+  skip = lazy
+
+=item order
+
+The "order" parameter can be used to override the default ordering of
+repositories. The default order value is 10. Use smaller values to make
+repositories be processed earlier, and larger values to make repositories
+be processed later.
+
+Note that if a repository is located in a subdirectory of another
+repository, ordering it to be processed earlier is not recommended.
+
+=item chain
+
+If the "chain" parameter is set and its command returns true, then B<mr>
+will try to load a F<.mrconfig> file from the root of the repository.
+
+=item include
+
+If the "include" parameter is set, its command is ran, and should output
+additional mrconfig file content. The content is included as if it were
+part of the including file.
+
+Unlike all other parameters, this parameter does not need to be placed
+within a section.
+
+B<mr> ships several libraries that can be included to add support for
+additional version control type things (unison, git-svn, git-fake-bare,
+git-subtree). To include them all, you could use:
+
+  include = cat /usr/share/mr/*
+
+See the individual files for details.
+
+=item deleted
+
+If the "deleted" parameter is set and its command returns true, then
+B<mr> will treat the repository as deleted. It won't ever actually delete
+the repository, but it will warn if it sees the repository's directory.
+This is useful when one mrconfig file is shared among multiple machines,
+to keep track of and remember to delete old repositories.
+
+=item lib
+
+The "lib" parameter can specify some shell code that will be run
+before each command, this can be a useful way to define shell
+functions for other commands to use. 
+
+Unlike most other parameters, this can be specified multiple times, in
+which case the chunks of shell code are accumulatively concatenated
+together.
+
+=item fixups
+
+If the "fixups" parameter is set, its command is run whenever a repository
+is checked out, or updated. This provides an easy way to do things
+like permissions fixups, or other tweaks to the repository content,
+whenever the repository is changed.
+
+=item VCS_action
+
+When looking for a command to run for a given action, mr first looks for
+a parameter with the same name as the action. If that is not found, it
+looks for a parameter named "VCS_action" (substituting in the name of the
+version control system and the action).
+
+Internally, mr has settings for "git_update", "svn_update", etc. To change
+the action that is performed for a given version control system, you can
+override these VCS specific actions. To add a new version control system,
+you can just add VCS specific actions for it.
+
+=item pre_ and post_
+
+If a "pre_action" parameter is set, its command is run before mr performs the
+specified action. Similarly, "post_action" parameters are run after mr
+successfully performs the specified action. For example, "pre_commit" is
+run before committing; "post_update" is run after updating.
+
+=item _append
+
+Any parameter can be suffixed with C<_append>, to add an additional value
+to the existing value of the parameter. In this way, actions 
+can be constructed accumulatively.
+
+=item VCS_test
+
+The name of the version control system is itself determined by
+running each defined "VCS_test" action, until one succeeds.
+
+=back
+
+=head1 UNTRUSTED MRCONFIG FILES
+
+Since mrconfig files can contain arbitrary shell commands, they can do
+anything. This flexibility is good, but it also allows a malicious mrconfig
+file to delete your whole home directory. Such a file might be contained
+inside a repository that your main F<~/.mrconfig> checks out. To
+avoid worries about evil commands in a mrconfig file, mr defaults to
+reading all mrconfig files other than the main F<~/.mrconfig> in untrusted
+mode. In untrusted mode, mrconfig files are limited to running only known
+safe commands (like "git clone") in a carefully checked manner.
+
+To configure mr to trust other mrconfig files, list them in F<~/.mrtrust>.
+One mrconfig file should be listed per line. Either the full pathname
+should be listed, or the pathname can start with F<~/> to specify a file
+relative to your home directory.
+
+=head1 OFFLINE LOG FILE
+
+The F<~/.mrlog> file contains commands that mr has remembered to run later,
+due to being offline. You can delete or edit this file to remove commands,
+or even to add other commands for 'mr online' to run. If the file is
+present, mr assumes it is in offline mode.
+
+=head1 EXTENSIONS
+
+mr can be extended to support things such as unison and git-svn. Some
+files providing such extensions are available in F</usr/share/mr/>. See
+the documentation in the files for details about using them.
+
+=head1 EXIT STATUS
+
+mr returns nonzero if a command failed in any of the repositories.
+
+=head1 AUTHOR
+
+Copyright 2007-2011 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+http://kitenet.net/~joey/code/mr/
+
+=cut
+
+use warnings;
+use strict;
+use Getopt::Long;
+use Cwd qw(getcwd abs_path);
+
+# things that can happen when mr runs a command
+use constant {
+       OK => 0,
+       FAILED => 1,
+       SKIPPED => 2,
+       ABORT => 3,
+};
+
+# configurables
+my $config_overridden=0;
+my $verbose=0;
+my $quiet=0;
+my $stats=0;
+my $force=0;
+my $insecure=0;
+my $interactive=0;
+my $max_depth;
+my $no_chdir=0;
+my $jobs=1;
+my $trust_all=0;
+my $directory=getcwd();
+
+my $HOME_MR_CONFIG = "$ENV{HOME}/.mrconfig";
+$ENV{MR_CONFIG}=find_mrconfig();
+
+# globals :-(
+my %config;
+my %configfiles;
+my %knownactions;
+my %alias;
+my (@ok, @failed, @skipped);
+
+main();
+
+sub shellquote {
+       my $i=shift;
+       $i=~s/'/'"'"'/g;
+       return "'$i'";
+}
+
+# Runs a shell command using a supplied function.
+# The lib will be included in the shell command line, and any params
+# will be available in the shell as $1, $2, etc.
+my $lastlib;
+sub runsh {
+       my ($action, $topdir, $subdir, $command, $params, $runner) = @_;
+
+       # optimisation: avoid running the shell for true and false
+       if ($command =~ /^\s*true\s*$/) {
+               $?=0;
+               return 0;
+       }
+       elsif ($command =~ /^\s*false\s*$/) {
+               $?=0;
+               return 1;
+       }
+       
+       my $quotedparams=join(" ", (map { shellquote($_) } @$params));
+       my $lib=exists $config{$topdir}{$subdir}{lib} ?
+                      $config{$topdir}{$subdir}{lib}."\n" : "";
+       if ($verbose && (! defined $lastlib || $lastlib ne $lib)) {
+               print "mr library now: >>$lib<<\n";
+               $lastlib=$lib;
+       }
+       my $shellcode="set -e;".$lib.
+               "my_sh(){ $command\n }; my_sh $quotedparams";
+       print "mr $action: running $action >>$command<<\n" if $verbose;
+       $runner->($shellcode);
+}
+
+my %perl_cache;
+sub perl {
+       my $id=shift;
+       my $s=shift;
+       if ($s =~ m/^perl:\s+(.*)/s) {
+               return $perl_cache{$1} if exists $perl_cache{$1};
+               my $sub=eval "sub {$1}";
+               if (! defined $sub) {
+                       print STDERR "mr: bad perl code in $id: $@\n";
+               }
+               return $perl_cache{$1} = $sub;
+       }
+       return undef;
+}
+
+my %vcs;
+sub vcs_test {
+       my ($action, $dir, $topdir, $subdir) = @_;
+
+       if (exists $vcs{$dir}) {
+               return $vcs{$dir};
+       }
+
+       my $test="";
+       my %perltest;
+       foreach my $vcs_test (
+                       sort {
+                               length $a <=> length $b 
+                                         ||
+                                      $a cmp $b
+                       } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
+               my ($vcs)=$vcs_test =~ /(.*)_test/;
+               my $p=perl($vcs_test, $config{$topdir}{$subdir}{$vcs_test});
+               if (defined $p) {
+                       $perltest{$vcs}=$p;
+               }
+               else {
+                       $test="my_$vcs_test() {\n$config{$topdir}{$subdir}{$vcs_test}\n}\n".$test;
+                       $test.="if my_$vcs_test; then echo $vcs; fi\n";
+               }
+       }
+
+       my @vcs;
+       foreach my $vcs (keys %perltest) {
+               if ($perltest{$vcs}->()) {
+                       push @vcs, $vcs;
+               }
+       }
+
+       push @vcs, split(/\n/,
+               runsh("vcs test", $topdir, $subdir, $test, [], sub {
+                       my $sh=shift;
+                       my $ret=`$sh`;
+                       return $ret;
+               })) if length $test;
+       if (@vcs > 1) {
+               print STDERR "mr $action: found multiple possible repository types (@vcs) for ".fulldir($topdir, $subdir)."\n";
+               return undef;
+       }
+       if (! @vcs) {
+               return $vcs{$dir}=undef;
+       }
+       else {
+               return $vcs{$dir}=$vcs[0];
+       }
+}
+       
+sub findcommand {
+       my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
+       
+       if (exists $config{$topdir}{$subdir}{$action}) {
+               return $config{$topdir}{$subdir}{$action};
+       }
+
+       if ($is_checkout) {
+               return undef;
+       }
+
+       my $vcs=vcs_test(@_);
+
+       if (defined $vcs && 
+           exists $config{$topdir}{$subdir}{$vcs."_".$action}) {
+               return $config{$topdir}{$subdir}{$vcs."_".$action};
+       }
+       else {
+               return undef;
+       }
+}
+
+sub fulldir {
+       my ($topdir, $subdir) = @_;
+       return $subdir =~ /^\// ? $subdir : $topdir.$subdir;
+}
+
+sub action {
+       my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
+       my $fulldir=fulldir($topdir, $subdir);
+       my $checkout_dir;
+
+       $ENV{MR_CONFIG}=$configfiles{$topdir};
+       my $is_checkout=($action eq 'checkout');
+       my $is_update=($action =~ /update/);
+
+       ($ENV{MR_REPO}=$dir) =~ s!/$!!;
+       $ENV{MR_ACTION}=$action;
+       
+       foreach my $testname ("skip", "deleted") {
+               next if $force && $testname eq "skip";
+
+               my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout);
+
+               if (defined $testcommand) {
+                       my $ret=runsh "$testname test", $topdir, $subdir,
+                               $testcommand, [$action],
+                               sub { system(shift()) };
+                       if ($ret != 0) {
+                               if (($? & 127) == 2) {
+                                       print STDERR "mr $action: interrupted\n";
+                                       return ABORT;
+                               }
+                               elsif ($? & 127) {
+                                       print STDERR "mr $action: $testname test received signal ".($? & 127)."\n";
+                                       return ABORT;
+                               }
+                       }
+                       if ($ret >> 8 == 0) {
+                               if ($testname eq "deleted") {
+                                       if (-d $dir) {
+                                               print STDERR "mr error: $dir should be deleted yet still exists\n";
+                                               return FAILED;
+                                       }
+                               }
+                               print "mr $action: skip $dir skipped\n" if $verbose;
+                               return SKIPPED;
+                       }
+               }
+       }
+
+       if ($is_checkout) {
+               $checkout_dir=$dir;
+               if (! $force_checkout) {
+                       if (-d $dir) {
+                               print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
+                               return SKIPPED;
+                       }
+       
+                       $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
+               }
+       }
+       elsif ($is_update) {
+               if (! -d $dir) {
+                       return action("checkout", $dir, $topdir, $subdir);
+               }
+       }
+
+       my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
+
+       if ($is_checkout && ! -d $dir) {
+               print "mr $action: creating parent directory $dir\n" if $verbose;
+               system("mkdir", "-p", $dir);
+       }
+
+       if (! $no_chdir && ! chdir($dir)) {
+               print STDERR "mr $action: failed to chdir to $dir: $!\n";
+               return FAILED;
+       }
+       elsif (! defined $command) {
+               my $vcs=vcs_test(@_);
+               if (! defined $vcs) {
+                       print STDERR "mr $action: unknown repository type and no defined $action command for $fulldir\n";
+                       return FAILED;
+               }
+               else {
+                       print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n";
+                       return SKIPPED;
+               }
+       }
+       else {
+               my $actionmsg;
+               if (! $no_chdir) {
+                       $actionmsg="mr $action: $fulldir";
+               }
+               else {
+                       my $s=$directory;
+                       $s=~s/^\Q$fulldir\E\/?//;
+                       $actionmsg="mr $action: $fulldir (in subdir $s)";
+               }
+               print "$actionmsg\n" unless $quiet;
+
+               my $hookret=hook("pre_$action", $topdir, $subdir);
+               return $hookret if $hookret != OK;
+
+               my $ret=runsh $action, $topdir, $subdir,
+                       $command, \@ARGV, sub {
+                               my $sh=shift;
+                               if ($quiet) {
+                                       my $output = qx/$sh 2>&1/;
+                                       my $ret = $?;
+                                       if ($ret != 0) {
+                                               print "$actionmsg\n";
+                                               print STDERR $output;
+                                       }
+                                       return $ret;
+                               }
+                               else {
+                                       system($sh);
+                               }
+                       };
+               if ($ret != 0) {
+                       if (($? & 127) == 2) {
+                               print STDERR "mr $action: interrupted\n";
+                               return ABORT;
+                       }
+                       elsif ($? & 127) {
+                               print STDERR "mr $action: received signal ".($? & 127)."\n";
+                               return ABORT;
+                       }
+                       print STDERR "mr $action: failed ($ret)\n" if $verbose;
+                       if ($ret >> 8 != 0) {
+                               print STDERR "mr $action: command failed\n";
+                               if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
+                                       # recreate original command line to
+                                       # remember, and avoid recursing
+                                       my @orig=@ARGV;
+                                       @ARGV=('-n', $action, @orig);
+                                       action("remember", $dir, $topdir, $subdir);
+                                       @ARGV=@orig;
+                               }
+                       }
+                       elsif ($ret != 0) {
+                               print STDERR "mr $action: command died ($ret)\n";
+                       }
+                       return FAILED;
+               }
+               else {
+                       if ($is_checkout && ! -d $dir) {
+                               print STDERR "mr $action: $dir missing after checkout\n";;
+                               return FAILED;
+                       }
+
+                       my $ret=hook("post_$action", $topdir, $subdir);
+                       return $ret if $ret != OK;
+                       
+                       if ($is_checkout || $is_update) {
+                               if ($is_checkout && ! $no_chdir) {
+                                       if (! chdir($checkout_dir)) {
+                                               print STDERR "mr $action: failed to chdir to $checkout_dir: $!\n";
+                                               return FAILED;
+                                       }
+                               }
+                               my $ret=hook("fixups", $topdir, $subdir);
+                               return $ret if $ret != OK;
+                       }
+                       
+                       return OK;
+               }
+       }
+}
+
+sub hook {
+       my ($hook, $topdir, $subdir) = @_;
+
+       my $command=$config{$topdir}{$subdir}{$hook};
+       return OK unless defined $command;
+       my $ret=runsh $hook, $topdir, $subdir, $command, [], sub {
+                       my $sh=shift;
+                       if ($quiet) {
+                               my $output = qx/$sh 2>&1/;
+                               my $ret = $?;
+                               if ($ret != 0) {
+                                       print STDERR $output;
+                               }
+                               return $ret;
+                       }
+                       else {
+                               system($sh);
+                       }
+               };
+       if ($ret != 0) {
+               if (($? & 127) == 2) {
+                       print STDERR "mr $hook: interrupted\n";
+                       return ABORT;
+               }
+               elsif ($? & 127) {
+                       print STDERR "mr $hook: received signal ".($? & 127)."\n";
+                       return ABORT;
+               }
+               else {
+                       return FAILED;
+               }
+       }
+
+       return OK;
+}
+
+# run actions on multiple repos, in parallel
+sub mrs {
+       my $action=shift;
+       my @repos=@_;
+
+       $| = 1;
+       my @active;
+       my @fhs;
+       my @out;
+       my $running=0;
+       while (@fhs or @repos) {
+               while ((!$jobs || $running < $jobs) && @repos) {
+                       $running++;
+                       my $repo = shift @repos;
+                       pipe(my $outfh, CHILD_STDOUT);
+                       pipe(my $errfh, CHILD_STDERR);
+                       my $pid;
+                       unless ($pid = fork) {
+                               die "mr $action: cannot fork: $!" unless defined $pid;
+                               open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
+                               open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
+                               close CHILD_STDOUT;
+                               close CHILD_STDERR;
+                               close $outfh;
+                               close $errfh;
+                               exit action($action, @$repo);
+                       }
+                       close CHILD_STDOUT;
+                       close CHILD_STDERR;
+                       push @active, [$pid, $repo];
+                       push @fhs, [$outfh, $errfh];
+                       push @out, ['',     ''];
+               }
+               my ($rin, $rout) = ('','');
+               my $nfound;
+               foreach my $fh (@fhs) {
+                       next unless defined $fh;
+                       vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
+                       vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
+               }
+               $nfound = select($rout=$rin, undef, undef, 1);
+               foreach my $channel (0, 1) {
+                       foreach my $i (0..$#fhs) {
+                               next unless defined $fhs[$i];
+                               my $fh = $fhs[$i][$channel];
+                               next unless defined $fh;
+                               if (vec($rout, fileno($fh), 1) == 1) {
+                                       my $r = '';
+                                       if (sysread($fh, $r, 1024) == 0) {
+                                               close($fh);
+                                               $fhs[$i][$channel] = undef;
+                                               if (! defined $fhs[$i][0] &&
+                                                   ! defined $fhs[$i][1]) {
+                                                       waitpid($active[$i][0], 0);
+                                                       print STDOUT $out[$i][0];
+                                                       print STDERR $out[$i][1];
+                                                       record($active[$i][1], $? >> 8);
+                                                       splice(@fhs, $i, 1);
+                                                       splice(@active, $i, 1);
+                                                       splice(@out, $i, 1);
+                                                       $running--;
+                                               }
+                                       }
+                                       $out[$i][$channel] .= $r;
+                               }
+                       }
+               }
+       }
+}
+
+sub record {
+       my $dir=shift()->[0];
+       my $ret=shift;
+
+       if ($ret == OK) {
+               push @ok, $dir;
+               print "\n" unless $quiet;
+       }
+       elsif ($ret == FAILED) {
+               if ($interactive) {
+                       chdir($dir) unless $no_chdir;
+                       print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
+                       system((getpwuid($<))[8], "-i");
+               }
+               push @failed, $dir;
+               print "\n" unless $quiet;
+       }
+       elsif ($ret == SKIPPED) {
+               push @skipped, $dir;
+       }
+       elsif ($ret == ABORT) {
+               exit 1;
+       }
+       else {
+               die "unknown exit status $ret";
+       }
+}
+
+sub showstats {
+       my $action=shift;
+       if (! @ok && ! @failed && ! @skipped) {
+               die "mr $action: no repositories found to work on\n";
+       }
+       print "mr $action: finished (".join("; ",
+               showstat($#ok+1, "ok", "ok"),
+               showstat($#failed+1, "failed", "failed"),
+               showstat($#skipped+1, "skipped", "skipped"),
+       ).")\n" unless $quiet;
+       if ($stats) {
+               if (@skipped) {
+                       print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
+               }
+               if (@failed) {
+                       print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
+               }
+       }
+}
+
+sub showstat {
+       my $count=shift;
+       my $singular=shift;
+       my $plural=shift;
+       if ($count) {
+               return "$count ".($count > 1 ? $plural : $singular);
+       }
+       return;
+}
+
+# an ordered list of repos
+sub repolist {
+       my @list;
+       foreach my $topdir (sort keys %config) {
+               foreach my $subdir (sort keys %{$config{$topdir}}) {
+                       push @list, {
+                               topdir => $topdir,
+                               subdir => $subdir,
+                               order => $config{$topdir}{$subdir}{order},
+                       };
+               }
+       }
+       return sort {
+               $a->{order}  <=> $b->{order}
+                            ||
+               $a->{topdir} cmp $b->{topdir}
+                            ||
+               $a->{subdir} cmp $b->{subdir}
+       } @list;
+}
+
+sub repodir {
+       my $repo=shift;
+       my $topdir=$repo->{topdir};
+       my $subdir=$repo->{subdir};
+       my $ret=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
+       $ret=~s/\/\.$//;
+       return $ret;
+}
+
+# Figure out which repos to act on.  Returns a list of array refs
+# in the format:
+#
+#   [ "$full_repo_path/", "$mr_config_path/", $section_header ]
+sub selectrepos {
+       my @repos;
+       foreach my $repo (repolist()) {
+               my $topdir=$repo->{topdir};
+               my $subdir=$repo->{subdir};
+
+               next if $subdir eq 'DEFAULT';
+               my $dir=repodir($repo);
+               my $d=$directory;
+               $dir.="/" unless $dir=~/\/$/;
+               $d.="/" unless $d=~/\/$/;
+               next if $dir ne $d && $dir !~ /^\Q$d\E/;
+               if (defined $max_depth) {
+                       my @a=split('/', $dir);
+                       my @b=split('/', $d);
+                       do { } while (@a && @b && shift(@a) eq shift(@b));
+                       next if @a > $max_depth || @b > $max_depth;
+               }
+               push @repos, [$dir, $topdir, $subdir];
+       }
+       if (! @repos) {
+               # fallback to find a leaf repo
+               foreach my $repo (reverse repolist()) {
+                       my $topdir=$repo->{topdir};
+                       my $subdir=$repo->{subdir};
+                       
+                       next if $subdir eq 'DEFAULT';
+                       my $dir=repodir($repo);
+                       my $d=$directory;
+                       $dir.="/" unless $dir=~/\/$/;
+                       $d.="/" unless $d=~/\/$/;
+                       if ($d=~/^\Q$dir\E/) {
+                               push @repos, [$dir, $topdir, $subdir];
+                               last;
+                       }
+               }
+               $no_chdir=1;
+       }
+       return @repos;
+}
+
+sub expandenv {
+       my $val=shift;
+       
+
+       if ($val=~/\$/) {
+               $val=`echo "$val"`;
+               chomp $val;
+       }
+       
+       return $val;
+}
+
+my %trusted;
+sub is_trusted_config {
+       my $config=shift; # must be abs_pathed already
+
+       # We always trust ~/.mrconfig.
+       return 1 if $config eq abs_path($HOME_MR_CONFIG);
+
+       return 1 if $trust_all;
+
+       my $trustfile=$ENV{HOME}."/.mrtrust";
+
+       if (! %trusted) {
+               $trusted{$HOME_MR_CONFIG}=1;
+               if (open (TRUST, "<", $trustfile)) {
+                       while (<TRUST>) {
+                               chomp;
+                               s/^~\//$ENV{HOME}\//;
+                               $trusted{abs_path($_)}=1;
+                       }
+                       close TRUST;
+               }
+       }
+
+       return $trusted{$config};
+}
+
+
+sub is_trusted_repo {
+       my $repo=shift;
+       
+       # Tightly limit what is allowed in a repo name.
+       # No ../, no absolute paths, and no unusual filenames
+       # that might try to escape to the shell.
+       return $repo =~ /^[-_.+\/A-Za-z0-9]+$/ &&
+              $repo !~ /\.\./ && $repo !~ /^\//;
+}
+
+sub is_trusted_checkout {
+       my $command=shift;
+       
+       # To determine if the command is safe, compare it with the
+       # *_trusted_checkout config settings. Those settings are
+       # templates for allowed commands, so make sure that each word
+       # of the command matches the corresponding word of the template.
+       
+       my @words;
+       foreach my $word (split(' ', $command)) {
+               # strip quoting
+               if ($word=~/^'(.*)'$/) {
+                       $word=$1;
+               }
+               elsif ($word=~/^"(.*)"$/) {
+                       $word=$1;
+               }
+
+               push @words, $word;
+       }
+
+       foreach my $key (grep { /_trusted_checkout$/ }
+                        keys %{$config{''}{DEFAULT}}) {
+               my @twords=split(' ', $config{''}{DEFAULT}{$key});
+               next if @words > @twords;
+
+               my $match=1;
+               my $url;
+               for (my $c=0; $c < @twords && $match; $c++) {
+                       if ($twords[$c] eq '$url') {
+                               # Match all the typical characters found in
+                               # urls, plus @ which svn can use. Note
+                               # that the "url" might also be a local
+                               # directory.
+                               $match=(
+                                       defined $words[$c] &&
+                                       $words[$c] =~ /^[-_.+:@\/A-Za-z0-9]+$/
+                               );
+                               $url=$words[$c];
+                       }
+                       elsif ($twords[$c] eq '$repo') {
+                               # If a repo is not specified, assume it
+                               # will be the last path component of the
+                               # url, or something derived from it, and
+                               # check that.
+                               if (! defined $words[$c] && defined $url) {
+                                       ($words[$c])=$url=~/\/([^\/]+)\/?$/;
+                               }
+
+                               $match=(
+                                       defined $words[$c] &&
+                                       is_trusted_repo($words[$c])
+                               );
+                       }
+                       elsif (defined $words[$c] && $words[$c]=~/^($twords[$c])$/) {
+                               $match=1;
+                       }
+                       else {
+                               $match=0;
+                       }
+               }
+               return 1 if $match;
+       }
+
+       return 0;
+}
+
+my %loaded;
+sub loadconfig {
+       my $f=shift;
+       my $dir=shift;
+       my $bootstrap_url=shift;
+
+       my @toload;
+
+       my $in;
+       my $trusted;
+       if (ref $f eq 'GLOB') {
+               $dir="";
+               $in=$f;
+               $trusted=1;
+       }
+       else {
+               my $absf=abs_path($f);
+               if ($loaded{$absf}) {
+                       return;
+               }
+               $loaded{$absf}=1;
+
+               $trusted=is_trusted_config($absf);
+
+               if (! defined $dir) {
+                       ($dir)=$f=~/^(.*\/)[^\/]+$/;
+                       if (! defined $dir) {
+                               $dir=".";
+                       }
+               }
+
+               $dir=abs_path($dir)."/";
+               
+               if (! exists $configfiles{$dir}) {
+                       $configfiles{$dir}=$f;
+               }
+
+               # copy in defaults from first parent
+               my $parent=$dir;
+               while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
+                       if ($parent eq '/') {
+                               $parent="";
+                       }
+                       if (exists $config{$parent} &&
+                           exists $config{$parent}{DEFAULT}) {
+                               $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
+                               last;
+                       }
+               }
+               
+               if (! -e $f) {
+                       return;
+               }
+
+               print "mr: loading config $f\n" if $verbose;
+               open($in, "<", $f) || die "mr: open $f: $!\n";
+       }
+       my @lines=<$in>;
+       close $in unless ref $f eq 'GLOB';
+
+       my $section;
+
+       # Keep track of the current line in the config file;
+       # when a file is included track the current line from the include.
+       my $lineno=0;
+       my $included=undef;
+
+       my $line;
+       my $nextline = sub {
+               if ($included) {
+                       $included--;
+               }
+               else {
+                       $included=undef;
+                       $lineno++;
+               }
+               $line=shift @lines;
+               chomp $line;
+               return $line;
+       };
+       my $lineerror = sub {
+               my $msg=shift;
+               if (defined $included) {
+                       die "mr: $msg at $f line $lineno, included line: $line\n";
+               }
+               else {
+                       die "mr: $msg at $f line $lineno\n";
+               }
+       };
+       my $trusterror = sub {
+               my $msg=shift;
+       
+               if (defined $bootstrap_url) {
+                       die "mr: $msg in untrusted $bootstrap_url line $lineno\n".
+                               "(To trust this url, --trust-all can be used; but please use caution;\n".
+                               "this can allow arbitrary code execution!)\n";
+               }
+               else {
+                       die "mr: $msg in untrusted $f line $lineno\n".
+                               "(To trust this file, list it in ~/.mrtrust.)\n";
+               }
+       };
+
+       while (@lines) {
+               $_=$nextline->();
+
+               if (! $trusted && /[[:cntrl:]]/) {
+                       $trusterror->("illegal control character");
+               }
+
+               next if /^\s*\#/ || /^\s*$/;
+               if (/^\[([^\]]*)\]\s*$/) {
+                       $section=$1;
+
+                       if (! $trusted) {
+                               if (! is_trusted_repo($section) ||
+                                   $section eq 'ALIAS' ||
+                                   $section eq 'DEFAULT') {
+                                       $trusterror->("illegal section \"[$section]\"");
+                               }
+                       }
+                       $section=expandenv($section) if $trusted;
+                       if ($section ne 'ALIAS' &&
+                           ! exists $config{$dir}{$section} &&
+                           exists $config{$dir}{DEFAULT}) {
+                               # copy in defaults
+                               $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
+                       }
+               }
+               elsif (/^(\w+)\s*=\s*(.*)/) {
+                       my $parameter=$1;
+                       my $value=$2;
+
+                       # continued value
+                       while (@lines && $lines[0]=~/^\s(.+)/) {
+                               $value.="\n$1";
+                               chomp $value;
+                               $nextline->();
+                       }
+
+                       if (! $trusted) {
+                               # Untrusted files can only contain a few
+                               # settings in specific known-safe formats.
+                               if ($parameter eq 'checkout') {
+                                       if (! is_trusted_checkout($value)) {
+                                               $trusterror->("illegal checkout command \"$value\"");
+                                       }
+                               }
+                               elsif ($parameter eq 'order') {
+                                       # not interpreted as a command, so
+                                       # safe.
+                               }
+                               elsif ($value eq 'true' || $value eq 'false') {
+                                       # skip=true , deleted=true etc are
+                                       # safe.
+                               }
+                               else {
+                                       $trusterror->("illegal setting \"$parameter=$value\"");
+                               }
+                       }
+
+                       if ($parameter eq "include") {
+                               print "mr: including output of \"$value\"\n" if $verbose;
+                               my @inc=`$value`;
+                               if ($?) {
+                                       print STDERR "mr: include command exited nonzero ($?)\n";
+                               }
+                               $included += @inc;
+                               unshift @lines, @inc;
+                               next;
+                       }
+
+                       if (! defined $section) {
+                               $lineerror->("parameter ($parameter) not in section");
+                       }
+                       if ($section eq 'ALIAS') {
+                               $alias{$parameter}=$value;
+                       }
+                       elsif ($parameter eq 'lib' or $parameter =~ s/_append$//) {
+                               $config{$dir}{$section}{$parameter}.="\n".$value."\n";
+                       }
+                       else {
+                               $config{$dir}{$section}{$parameter}=$value;
+                               if ($parameter =~ /.*_(.*)/) {
+                                       $knownactions{$1}=1;
+                               }
+                               else {
+                                       $knownactions{$parameter}=1;
+                               }
+                               if ($parameter eq 'chain' &&
+                                   length $dir && $section ne "DEFAULT") {
+                                       my $chaindir="$section";
+                                       if ($chaindir !~ m!^/!) {
+                                               $chaindir=$dir.$chaindir;
+                                       }
+                                       if (-e "$chaindir/.mrconfig") {
+                                               my $ret=system($value);
+                                               if ($ret != 0) {
+                                                       if (($? & 127) == 2) {
+                                                               print STDERR "mr: chain test interrupted\n";
+                                                               exit 2;
+                                                       }
+                                                       elsif ($? & 127) {
+                                                               print STDERR "mr: chain test received signal ".($? & 127)."\n";
+                                                       }
+                                               }
+                                               else {
+                                                       push @toload, ["$chaindir/.mrconfig", $chaindir];
+                                               }
+                                       }
+                               }
+                       }
+               }
+               else {
+                       $lineerror->("parse error");
+               }
+       }
+
+       foreach my $c (@toload) {
+               loadconfig(@$c);
+       }
+}
+
+sub startingconfig {
+       %alias=%config=%configfiles=%knownactions=%loaded=();
+       my $datapos=tell(DATA);
+       loadconfig(\*DATA);
+       seek(DATA,$datapos,0); # rewind
+}
+
+sub modifyconfig {
+       my $f=shift;
+       # the section to modify or add
+       my $targetsection=shift;
+       # fields to change in the section
+       # To remove a field, set its value to "".
+       my %changefields=@_;
+
+       my @lines;
+       my @out;
+
+       if (-e $f) {
+               open(my $in, "<", $f) || die "mr: open $f: $!\n";
+               @lines=<$in>;
+               close $in;
+       }
+
+       my $formatfield=sub {
+               my $field=shift;
+               my @value=split(/\n/, shift);
+
+               return "$field = ".shift(@value)."\n".
+                       join("", map { "\t$_\n" } @value);
+       };
+       my $addfields=sub {
+               my @blanks;
+               while ($out[$#out] =~ /^\s*$/) {
+                       unshift @blanks, pop @out;
+               }
+               foreach my $field (sort keys %changefields) {
+                       if (length $changefields{$field}) {
+                               push @out, "$field = $changefields{$field}\n";
+                               delete $changefields{$field};
+                       }
+               }
+               push @out, @blanks;
+       };
+
+       my $section;
+       while (@lines) {
+               $_=shift(@lines);
+
+               if (/^\s*\#/ || /^\s*$/) {
+                       push @out, $_;
+               }
+               elsif (/^\[([^\]]*)\]\s*$/) {
+                       if (defined $section && 
+                           $section eq $targetsection) {
+                               $addfields->();
+                       }
+
+                       $section=expandenv($1);
+
+                       push @out, $_;
+               }
+               elsif (/^(\w+)\s*=\s(.*)/) {
+                       my $parameter=$1;
+                       my $value=$2;
+
+                       # continued value
+                       while (@lines && $lines[0]=~/^\s(.+)/) {
+                               shift(@lines);
+                               $value.="\n$1";
+                               chomp $value;
+                       }
+
+                       if ($section eq $targetsection) {
+                               if (exists $changefields{$parameter}) {
+                                       if (length $changefields{$parameter}) {
+                                               $value=$changefields{$parameter};
+                                       }
+                                       delete $changefields{$parameter};
+                               }
+                       }
+
+                       push @out, $formatfield->($parameter, $value);
+               }
+       }
+
+       if (defined $section && 
+           $section eq $targetsection) {
+               $addfields->();
+       }
+       elsif (%changefields) {
+               push @out, "\n[$targetsection]\n";
+               foreach my $field (sort keys %changefields) {
+                       if (length $changefields{$field}) {
+                               push @out, $formatfield->($field, $changefields{$field});
+                       }
+               }
+       }
+
+       open(my $out, ">", $f) || die "mr: write $f: $!\n";
+       print $out @out;
+       close $out;     
+}
+
+sub dispatch {
+       my $action=shift;
+
+       # actions that do not operate on all repos
+       if ($action eq 'help') {
+               help(@ARGV);
+       }
+       elsif ($action eq 'config') {
+               config(@ARGV);
+       }
+       elsif ($action eq 'register') {
+               register(@ARGV);
+       }
+       elsif ($action eq 'bootstrap') {
+               bootstrap();
+       }
+       elsif ($action eq 'remember' ||
+              $action eq 'offline' ||
+              $action eq 'online') {
+               my @repos=selectrepos;
+               action($action, @{$repos[0]}) if @repos;
+               exit 0;
+       }
+
+       if (!$jobs || $jobs > 1) {
+               mrs($action, selectrepos());
+       }
+       else {
+               foreach my $repo (selectrepos()) {
+                       record($repo, action($action, @$repo));
+               }
+       }
+}
+
+sub help {
+       exec($config{''}{DEFAULT}{help}) || die "exec: $!";
+}
+
+sub config {
+       if (@_ < 2) {
+               die "mr config: not enough parameters\n";
+       }
+       my $section=shift;
+       if ($section=~/^\//) {
+               # try to convert to a path relative to the config file
+               my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
+               $dir=abs_path($dir);
+               $dir.="/" unless $dir=~/\/$/;
+               if ($section=~/^\Q$dir\E(.*)/) {
+                       $section=$1;
+               }
+       }
+       my %changefields;
+       foreach (@_) {
+               if (/^([^=]+)=(.*)$/) {
+                       $changefields{$1}=$2;
+               }
+               else {
+                       my $found=0;
+                       foreach my $topdir (sort keys %config) {
+                               if (exists $config{$topdir}{$section} &&
+                                   exists $config{$topdir}{$section}{$_}) {
+                                       print $config{$topdir}{$section}{$_}."\n";
+                                       $found=1;
+                                       last if $section eq 'DEFAULT';
+                               }
+                       }
+                       if (! $found) {
+                               die "mr config: $section $_ not set\n";
+                       }
+               }
+       }
+       modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
+       exit 0;
+}
+
+sub register {
+       if ($config_overridden) {
+               # Find the directory that the specified config file is
+               # located in.
+               ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
+       }
+       else {
+               # Find the closest known mrconfig file to the current
+               # directory.
+               $directory.="/" unless $directory=~/\/$/;
+               my $foundconfig=0;
+               foreach my $topdir (reverse sort keys %config) {
+                       next unless length $topdir;
+                       if ($directory=~/^\Q$topdir\E/) {
+                               $ENV{MR_CONFIG}=$configfiles{$topdir};
+                               $directory=$topdir;
+                               $foundconfig=1;
+                               last;
+                       }
+               }
+               if (! $foundconfig) {
+                       $directory=""; # no config file, use builtin
+               }
+       }
+       if (@ARGV) {
+               my $subdir=shift @ARGV;
+               if (! chdir($subdir)) {
+                       print STDERR "mr register: failed to chdir to $subdir: $!\n";
+               }
+       }
+
+       $ENV{MR_REPO}=getcwd();
+       my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
+       if (! defined $command) {
+               die "mr register: unknown repository type\n";
+       }
+
+       $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
+       $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
+               "my_action(){ $command\n }; my_action ".
+               join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+       print "mr register: running >>$command<<\n" if $verbose;
+       exec($command) || die "exec: $!";
+}
+
+sub bootstrap {
+       my $url=shift @ARGV;
+       my $dir=shift @ARGV || ".";
+       
+       if (! defined $url || ! length $url) {
+               die "mr: bootstrap requires url\n";
+       }
+       
+       # Download the config file to a temporary location.
+       eval q{use File::Temp};
+       die $@ if $@;
+       my $tmpconfig=File::Temp->new();
+       my @downloader;
+       if ($url =~ m!^ssh://(.*)!) {
+               @downloader = ("scp", $1, $tmpconfig);
+       }
+       else {
+               @downloader = ("curl", "-A", "mr", "-L", "-s", $url, "-o", $tmpconfig);
+               push(@downloader, "-k") if $insecure;
+       }
+       my $status = system(@downloader);
+       die "mr bootstrap: invalid SSL certificate for $url (consider -k)\n"
+               if $downloader[0] eq 'curl' && $status >> 8 == 60;
+       die "mr bootstrap: download of $url failed\n" if $status != 0;
+
+       if (! -e $dir) {
+               system("mkdir", "-p", $dir);
+       }
+       chdir($dir) || die "chdir $dir: $!";
+
+       # Special case to handle checkout of the "." repo, which 
+       # would normally be skipped.
+       my $topdir=abs_path(".")."/";
+       my @repo=($topdir, $topdir, ".");
+       loadconfig($tmpconfig, $topdir, $url);
+       record(\@repo, action("checkout", @repo, 1))
+               if exists $config{$topdir}{"."}{"checkout"};
+
+       if (-e ".mrconfig") {
+               print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $url\n";
+       }
+       else {
+               eval q{use File::Copy};
+               die $@ if $@;
+               move($tmpconfig, ".mrconfig") || die "rename: $!";
+       }
+
+       # Reload the config file (in case we got a different version)
+       # and checkout everything else.
+       startingconfig();
+       loadconfig(".mrconfig");
+       dispatch("checkout");
+       @skipped=grep { abs_path($_) ne abs_path($topdir) } @skipped;
+       showstats("bootstrap");
+       exitstats();
+}
+
+# alias expansion and command stemming
+sub expandaction {
+       my $action=shift;
+       if (exists $alias{$action}) {
+               $action=$alias{$action};
+       }
+       if (! exists $knownactions{$action}) {
+               my @matches = grep { /^\Q$action\E/ }
+                       keys %knownactions, keys %alias;
+               if (@matches == 1) {
+                       $action=$matches[0];
+               }
+               elsif (@matches == 0) {
+                       die "mr: unknown action \"$action\" (known actions: ".
+                               join(", ", sort keys %knownactions).")\n";
+               }
+               else {
+                       die "mr: ambiguous action \"$action\" (matches: ".
+                               join(", ", @matches).")\n";
+               }
+       }
+       return $action;
+}
+
+sub find_mrconfig {
+       my $dir=getcwd();
+       while (length $dir) {
+               if (-e "$dir/.mrconfig") {
+                       return "$dir/.mrconfig";
+               }
+               $dir=~s/\/[^\/]*$//;
+       }
+       return $HOME_MR_CONFIG;
+}
+
+sub getopts {
+       my @saved=@ARGV;
+       Getopt::Long::Configure("bundling", "no_permute");
+       my $result=GetOptions(
+               "d|directory=s" => sub { $directory=abs_path($_[1]) },
+               "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
+               "p|path" => sub { }, # now default, ignore
+               "f|force" => \$force,
+               "v|verbose" => \$verbose,
+               "q|quiet" => \$quiet,
+               "s|stats" => \$stats,
+               "k|insecure" => \$insecure,
+               "i|interactive" => \$interactive,
+               "n|no-recurse:i" => \$max_depth,
+               "j|jobs:i" => \$jobs,
+               "t|trust-all" => \$trust_all,
+       );
+       if (! $result || @ARGV < 1) {
+               die("Usage: mr [options] action [params ...]\n".
+                   "(Use mr help for man page.)\n");
+       }
+       
+       $ENV{MR_SWITCHES}="";
+       foreach my $option (@saved) {
+               last if $option eq $ARGV[0];
+               $ENV{MR_SWITCHES}.="$option ";
+       }
+}
+
+sub init {
+       $SIG{INT}=sub {
+               print STDERR "mr: interrupted\n";
+               exit 2;
+       };
+       
+       # This can happen if it's run in a directory that was removed
+       # or other strangeness.
+       if (! defined $directory) {
+               die("mr: failed to determine working directory\n");
+       }
+       # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
+       # the config file might be a symlink to elsewhere, and the directory it's
+       # in is significant.
+       if ($ENV{MR_CONFIG} !~ /^\//) {
+               $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
+       }
+       # Try to set MR_PATH to the path to the program.
+       eval {
+               use FindBin qw($Bin $Script);
+               $ENV{MR_PATH}=$Bin."/".$Script;
+       };
+}
+       
+sub exitstats {
+       if (@failed) {
+               exit 1;
+       }
+       else {
+               exit 0;
+       }
+}
+
+sub main {
+       getopts();
+       init();
+
+       startingconfig();
+       loadconfig($HOME_MR_CONFIG);
+       loadconfig($ENV{MR_CONFIG});
+       #use Data::Dumper; print Dumper(\%config);
+       
+       my $action=expandaction(shift @ARGV);
+       dispatch($action);
+
+       showstats($action);
+       exitstats();
+}
+
+# Finally, some useful actions that mr knows about by default.
+# These can be overridden in ~/.mrconfig.
+__DATA__
+[ALIAS]
+co = checkout
+ci = commit
+ls = list
+
+[DEFAULT]
+order = 10
+lib =
+       error() {
+               echo "mr: $@" >&2
+               exit 1
+       }
+       warning() {
+               echo "mr (warning): $@" >&2
+       }
+       info() {
+               echo "mr: $@" >&2
+       }
+       hours_since() {
+               if [ -z "$1" ] || [ -z "$2" ]; then
+                       error "mr: usage: hours_since action num"
+               fi
+               for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_; do
+                       if [ -e "$MR_REPO/$dir" ]; then
+                               flagfile="$MR_REPO/$dir/.mr_last$1"
+                               break
+                       fi
+               done
+               if [ -z "$flagfile" ]; then
+                       error "cannot determine flag filename"
+               fi
+               delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
+               if [ "$delta" -lt "$2" ]; then
+                       return 1
+               else
+                       touch "$flagfile"
+                       return 0
+               fi
+       }
+       is_bzr_checkout() {
+               LANG=C bzr info | egrep -q '^Checkout'
+       }
+       lazy() {
+               if [ -d "$MR_REPO" ]; then
+                       return 1
+               else
+                       return 0
+               fi
+       }
+
+svn_test = perl: -d "$ENV{MR_REPO}/.svn"
+git_test = perl: -e "$ENV{MR_REPO}/.git"
+bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
+cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
+hg_test  = perl: -d "$ENV{MR_REPO}/.hg"
+darcs_test = perl: -d "$ENV{MR_REPO}/_darcs"
+fossil_test = perl: -f "$ENV{MR_REPO}/_FOSSIL_"
+git_bare_test = perl: 
+       -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
+       -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
+       `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get core.bare` =~ /true/
+vcsh_test = perl:
+       -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
+       -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
+       `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get vcsh.vcsh` =~ /true/
+veracity_test  = perl: -d "$ENV{MR_REPO}/.sgdrawer"
+
+svn_update = svn update "$@"
+git_update = git pull "$@"
+bzr_update = 
+       if is_bzr_checkout; then
+               bzr update "$@"
+       else
+               bzr merge --pull "$@"
+       fi
+cvs_update = cvs -q update "$@"
+hg_update  = hg pull "$@"; hg update "$@"
+darcs_update = darcs pull -a "$@"
+fossil_update = fossil pull "$@"
+vcsh_update = vcsh run "$MR_REPO" git pull "$@"
+veracity_update = vv pull "$@" && vv update "$@"
+
+git_fetch = git fetch --all --prune --tags
+git_svn_fetch = git svn fetch
+darcs_fetch = darcs fetch
+hg_fetch = hg pull
+
+svn_status = svn status "$@"
+git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true
+bzr_status = bzr status --short "$@"; bzr missing
+cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date'
+hg_status  = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
+darcs_status = darcs whatsnew -ls "$@" || true
+fossil_status = fossil changes "$@"
+vcsh_status = vcsh run "$MR_REPO" git -c status.relativePaths=false status -s "$@" || true
+veracity_status = vv status "$@"
+
+svn_commit = svn commit "$@"
+git_commit = git commit -a "$@" && git push --all
+bzr_commit = 
+       if is_bzr_checkout; then
+               bzr commit "$@"
+       else
+               bzr commit "$@" && bzr push
+       fi
+cvs_commit = cvs commit "$@"
+hg_commit  = hg commit "$@" && hg push
+darcs_commit = darcs record -a "$@" && darcs push -a
+fossil_commit = fossil commit "$@"
+vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
+veracity_commit = vv commit "$@" && vv push
+
+git_record = git commit -a "$@"
+bzr_record =
+       if is_bzr_checkout; then
+               bzr commit --local "$@"
+       else
+               bzr commit "$@"
+       fi
+hg_record  = hg commit "$@"
+darcs_record = darcs record -a "$@"
+fossil_record = fossil commit "$@"
+vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
+veracity_record = vv commit "$@"
+
+svn_push = :
+git_push = git push "$@"
+bzr_push = bzr push "$@"
+cvs_push = :
+hg_push = hg push "$@"
+darcs_push = darcs push -a "$@"
+fossil_push = fossil push "$@"
+vcsh_push = vcsh run "$MR_REPO" git push "$@"
+veracity_push = vv push "$@"
+
+svn_diff = svn diff "$@"
+git_diff = git diff "$@"
+bzr_diff = bzr diff "$@"
+cvs_diff = cvs -q diff "$@"
+hg_diff  = hg diff "$@"
+darcs_diff = darcs diff -u "$@"
+fossil_diff = fossil diff "$@"
+vcsh_diff = vcsh run "$MR_REPO" git diff "$@"
+veracity_diff = vv diff "$@"
+
+svn_log = svn log "$@"
+git_log = git log "$@"
+bzr_log = bzr log "$@"
+cvs_log = cvs log "$@"
+hg_log  = hg log "$@"
+darcs_log = darcs changes "$@"
+git_bare_log = git log "$@"
+fossil_log = fossil timeline "$@"
+vcsh_log = vcsh run "$MR_REPO" git log "$@"
+veracity_log = vv log "$@"
+
+hg_grep = hg grep "$@"
+cvs_grep = ack-grep "$@"
+svn_grep = ack-grep "$@"
+git_svn_grep = git grep "$@"
+git_grep = git grep "$@"
+bzr_grep = ack-grep "$@"
+
+run = "$@"
+
+svn_register =
+       url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
+       if [ -z "$url" ]; then
+               error "cannot determine svn url"
+       fi
+       echo "Registering svn url: $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
+git_register = 
+       url="`LC_ALL=C git config --get remote.origin.url`" || true
+       if [ -z "$url" ]; then
+               error "cannot determine git url"
+       fi
+       echo "Registering git url: $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
+bzr_register =
+       url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
+       if [ -z "$url" ]; then
+               error "cannot determine bzr url"
+       fi
+       echo "Registering bzr url: $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'"
+cvs_register =
+       repo=`cat CVS/Repository`
+       root=`cat CVS/Root`
+       if [ -z "$root" ]; then
+               error "cannot determine cvs root"
+               fi
+       echo "Registering cvs repository $repo at root $root"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
+hg_register = 
+       url=`hg showconfig paths.default`
+       echo "Registering mercurial repo url: $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
+darcs_register = 
+       url=`cat _darcs/prefs/defaultrepo`
+       echo "Registering darcs repository $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
+git_bare_register = 
+       url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
+       if [ -z "$url" ]; then
+               error "cannot determine git url"
+       fi
+       echo "Registering git url: $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
+vcsh_register =
+       url="`LC_ALL=C vcsh run "$MR_REPO" git config --get remote.origin.url`" || true
+       if [ -z "$url" ]; then
+               error "cannot determine git url"
+       fi
+       echo "Registering git url: $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="vcsh clone '$url' '$MR_REPO'"
+fossil_register =
+       url=`fossil remote-url`
+       repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'`
+       echo "Registering fossil repository $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'"
+veracity_register =
+       url=`vv config | grep sync_targets | sed -e 's/sync_targets:*.//g' -e 's/ //g'`
+       repo=`vv repo info | grep repository | sed -e 's/Current repository:*.//g' -e 's/ //g'`
+       echo "Registering veracity repository $url in $MR_CONFIG"
+       mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && vv checkout '$repo'"
+
+svn_trusted_checkout = svn co $url $repo
+svn_alt_trusted_checkout = svn checkout $url $repo
+git_trusted_checkout = git clone $url $repo
+bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo
+# cvs: too hard
+hg_trusted_checkout = hg clone $url $repo
+darcs_trusted_checkout = darcs get $url $repo
+git_bare_trusted_checkout = git clone --bare $url $repo
+vcsh_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
+# fossil: messy to do
+veracity_trusted_checkout = vv clone $url $repo
+
+
+help =
+       case `uname -s` in
+               SunOS)
+               SHOWMANFILE="man -f"
+               ;;
+               Darwin)
+               SHOWMANFILE="man"
+               ;;
+               *)
+               SHOWMANFILE="man -l"
+               ;;
+       esac
+       if [ ! -e "$MR_PATH" ]; then
+               error "cannot find program path"
+       fi
+       tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
+       trap "rm -f $tmp" exit
+       pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
+       $SHOWMANFILE "$tmp" || error "man failed"
+list = true
+config = 
+bootstrap = 
+
+online =
+       if [ -s ~/.mrlog ]; then
+               info "running offline commands"
+               mv -f ~/.mrlog ~/.mrlog.old
+               if ! sh -e ~/.mrlog.old; then
+                       error "offline command failed; left in ~/.mrlog.old"
+               fi
+               rm -f ~/.mrlog.old
+       else
+               info "no offline commands to run"
+       fi
+offline =
+       umask 077
+       touch ~/.mrlog
+       info "offline mode enabled"
+remember =
+       info "remembering command: 'mr $@'"
+       command="mr -d '$(pwd)' $MR_SWITCHES"
+       for w in "$@"; do
+               command="$command '$w'"
+       done
+       if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
+               echo "$command" >> ~/.mrlog
+       fi
+
+ed = echo "A horse is a horse, of course, of course.."
+T = echo "I pity the fool."
+right = echo "Not found."
+
+# vim:sw=8:sts=0:ts=8:noet
+# Local variables:
+# indent-tabs-mode: t
+# cperl-indent-level: 8
+# End:
diff --git a/mrconfig b/mrconfig
new file mode 100644 (file)
index 0000000..a1abcd1
--- /dev/null
+++ b/mrconfig
@@ -0,0 +1,12 @@
+# A simple example config file for the mr(1) command.
+
+[mr]
+checkout = git clone git://git.kitenet.net/mr
+
+[linux-2.6]
+checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
+# only update once every 12 hours
+skip = ([ "$1" = update ] && ! hours_since "$1" 12)
+
+[debian-cd]
+checkout = svn co svn://svn.debian.org/debian-cd/trunk debian-cd
diff --git a/mrconfig.complex b/mrconfig.complex
new file mode 100644 (file)
index 0000000..b96c5bf
--- /dev/null
@@ -0,0 +1,137 @@
+# An example config file for the mr(1) command.
+#
+# This is fairly close to the config file used by the author
+# although slightly cut down.
+
+[DEFAULT]
+# Include all available libs.
+include = cat /usr/share/mr/* 2>/dev/null || true
+# Teach mr to run a few git and svn specific commands.
+svn_cleanup = svn cleanup "$@"
+git_gc = git gc "$@"
+git_tags = git tag -l
+svn_tags = svn ls "$(LC_ALL=C svn info . | grep -i ^URL: | cut -d ' ' -f 2 | sed -e 's/trunk/tags/')"
+# I prefer to git-svn rebase to fetch
+git_svn_update = git svn rebase
+# Tests used below.
+# - anon checks whether this is an anonymous checkout, by testing what url
+#   $HOME uses
+# - full checks whether I probably want a full checkout (quite large),
+#   if not, the checkout is minimal
+# - on checks whether the given host basename is one of the listed
+#   values. A value can also have a username in it, ie "joey@dodo".
+# - mylaptop only succeeds if it's on my main development laptop, which 
+#   gets lots of extra cruft
+lib = 
+       hostname="$(hostname)"
+       whoami="$(whoami)"
+       anon() {
+               ( GIT_CONFIG=$HOME/.git/config git config remote.origin.url || cat .git/remotes/origin ) | grep -q 'git://'
+       }
+       full() {
+               test "$whoami" = joey && ! anon
+       }
+       on() {
+               for host in $@; do
+                       if [ "${host%@*}" != "${host#*@}" ]; then
+                               if [ "$whoami" != "${host%@*}" ]; then
+                                       continue
+                               fi
+                               host="${host#*@}"
+                       fi
+                       if [ "$hostname" = "$host" ]; then
+                               return 0
+                       fi
+               done
+               return 1
+       }
+       mylaptop() {
+               on joey@gnu
+       }
+
+# The root of my home directory.
+[.]
+order = 1
+checkout =
+       if anon; then
+               git clone git://git.kitenet.net/joey/home joey
+       else
+               git clone ssh://git.kitenet.net/srv/git/kitenet.net/joey/home joey
+       fi
+
+[Maildir]
+# Dummy target to create Maildir. Doesn't run offlineimap since I have that
+# cronned on machines where I want it.
+update = :
+skip = ! full
+checkout = mkdir Maildir Maildir/cur Maildir/new Maildir/tmp; chmod 700 Maildir
+status = :
+
+[mail]
+checkout = git clone ssh://joey@git.kitenet.net/srv/git/joey/private/mail
+# I use mairix to index my mail archive; keep its index up-to-date.
+fixups = if [ "$(which mairix)" ]; then ionice -c 3 mairix -Q; fi
+skip = ! mylaptop
+
+[tmp]
+# This is a dummy target, all it does is run fixups at the end of
+# an update.
+fixups = $HOME/bin/fixups
+checkout = mkdir -p $HOME/tmp
+status = :
+order = 25
+
+[.etc]
+order = 2
+checkout = 
+       if anon; then
+               git clone git://git.kitenet.net/joey/home-etc .etc
+       else
+               git clone ssh://git.kitenet.net/srv/git/kitenet.net/joey/home-etc .etc
+       fi
+
+[.cron]
+checkout =
+       if anon; then
+               git clone git://git.kitenet.net/joey/cron .cron
+       else
+               git clone ssh://git.kitenet.net/srv/git/kitenet.net/joey/cron .cron
+       fi
+
+[packages/perl]
+order = 30
+checkout = svn co svn+ssh://joeyh@svn.debian.org/svn/pkg-perl/trunk perl
+skip = ! mylaptop || ([ "$1" = update ] && ! hours_since "$1" 12)
+
+[linux-2.6]
+order = 20
+checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
+skip = ! mylaptop || ([ "$1" = update ] && ! hours_since "$1" 12)
+push = error "as if!"
+
+[/etc]
+# I use etckeeper to keep /etc in git. But it only works if I'm root, 
+# and if it's not already in etc, skip it.
+skip = ! test -d /etc/.git || ! test "$(whoami)" = root
+
+[dpkg]
+# A merge of the upstream dpkg git repo and my own personal branch.
+checkout = 
+       git clone git://git.debian.org/git/dpkg/dpkg.git &&
+       cd dpkg &&
+       git remote add kite ssh://git.kitenet.net/srv/git/kitenet.net/dpkg &&
+       git fetch kite &&
+       git checkout -b sourcev3 kite/sourcev3
+update = git pull origin master && git pull kite sourcev3
+commit = git commit -a && git push kite
+push = git push kite
+skip = ! mylaptop
+
+[html/www.debian.org]
+# Still in CVS..urk!
+checkout = cvs -d ':ext:joeyh@cvs.debian.org:/cvs/webwml' co -d www.debian.org webwml
+# cvs sucks sufficiently that I prefer to run these commands by hand,
+# and only rarely
+update = echo "skipping cvs update (too slow)"
+status = echo "skipping cvs status (too ugly)"
+skip = ! mylaptop || ! full
diff --git a/webcheckout b/webcheckout
new file mode 100755 (executable)
index 0000000..e48d9de
--- /dev/null
@@ -0,0 +1,276 @@
+#!/usr/bin/perl
+
+=head1 NAME
+
+webcheckout - check out repositories referenced on a web page
+
+=head1 SYNOPSIS
+
+B<webcheckout> [options] url [destdir]
+
+=head1 DESCRIPTION
+
+B<webcheckout> downloads an url and parses it, looking for version control 
+repositories referenced by the page. It checks out each repository into
+a subdirectory of the current directory, using whatever VCS program is
+appropriate for that repository (git, svn, etc).
+
+The information about the repositories is embedded in the web page using
+the rel=vcs-* microformat, which is documented at
+<http://kitenet.net/~joey/rfc/rel-vcs/>.
+
+If the optional destdir parameter is specified, VCS programs will be asked
+to check out repositories into that directory. If there are multiple
+repositories to check out, each will be checked out into a separate
+subdirectory of the destdir.
+
+=head1 OPTIONS
+
+=over 4
+
+=item -a, --auth
+
+Prefer authenticated repositories. By default, webcheckout will use
+anonymous repositories when possible. If you have an account that
+allows you to use authenticated repositories, you might want to use this
+option.
+
+=item --no-act, -n
+
+Do not actually check anything out, just print out the commands that would
+be run to check out the repositories.
+
+=item --quiet, -q
+
+Quiet mode. Do not print out the commands being run. (The VCS commands
+may still be noisy however.)
+
+=back
+
+=head1 PREREQUISITES
+
+To use this program you will need lots of VCS programs installed,
+obviously. It also depends on the perl LWP and HTML::Parser modules.
+
+If the perl URI module is installed, webcheckout can heuristically guess
+what you mean by partial URLs, such as "kitenet.net/~joey"'
+
+=head1 AUTHOR
+
+Copyright 2009 Joey Hess <joey@kitenet.net>
+
+Licensed under the GNU GPL version 2 or higher.
+
+This program is included in myrepos <http://myrepos.branchable.com/>
+
+=cut
+
+use LWP::Simple;
+use HTML::Parser;
+use Getopt::Long;
+use warnings;
+use strict;
+
+# What to download.
+my $url;
+
+# Controls whether to print what is being done.
+my $quiet=0;
+
+# Controls whether to actually check anything out.
+my $noact=0;
+
+# Controls whether to perfer repos that use authentication.
+my $want_auth=0;
+
+# Controls where to check out to. If not set, the VCS is allowed to
+# decide.
+my $destdir;
+
+# how to perform checkouts
+my %handlers=(
+       git => sub { doit("git", "clone", shift, $destdir) },
+       svn => sub { doit("svn", "checkout", shift, $destdir) },
+       bzr => sub { doit("bzr", "branch", shift, $destdir) },
+);
+
+# Regexps matching urls that are used for anonymous
+# repository checkouts. The order is significant:
+# urls matching earlier in the list are preferred over
+# those matching later.
+my @anon_urls=(
+       qr/^git:\/\//i,
+       qr/^bzr:\/\//i,
+       qr/^svn:\/\//i,
+       qr/^http:\/\//i, # generally the worst transport
+);
+
+sub getopts {
+       Getopt::Long::Configure("bundling", "no_permute");
+       my $result=GetOptions(
+               "q|quiet" => \$quiet,
+               "n|noact" => \$noact,
+               "a|auth", => \$want_auth,
+       );
+       if (! $result || @ARGV < 1) {
+               die "usage: webcheckout [options] url [destdir]\n";
+       }
+
+       $url=shift @ARGV;
+       $destdir=shift @ARGV;
+
+       eval q{use URI::Heuristic};
+       if (! $@) {
+               $url=URI::Heuristic::uf_uristr($url);
+       }
+
+       if ($noact) {
+               $quiet=0;
+       }
+}
+
+sub doit {
+       my @args=grep { defined } @_;
+       print join(" ", @args)."\n" unless $quiet;
+       return 0 if $noact;
+       return system(@args);
+}
+
+# Is repo a better than repo b?
+sub better {
+       my ($a, $b)=@_;
+
+       my @anon;
+       foreach my $r (@anon_urls) {
+               if ($a->{href} =~ /$r/) {
+                       push @anon, $a;
+               }
+               elsif ($b->{href} =~ /$r/) {
+                       push @anon, $b;
+               }
+       }
+
+       if ($want_auth) {
+               # Whichever is authed is better.
+               return 1 if ! @anon || ! grep { $_ eq $a } @anon;
+               return 0 if ! grep { $_ eq $b } @anon;
+               # Neither is authed, so the better anon method wins.
+               return $anon[0] == $a;
+       }
+       else {
+               # Better anon method wins.
+               return @anon && $anon[0] == $a;
+       }
+}
+
+# Eliminate duplicate repositories from list.
+# Duplicate repositories have the same title, or the same href.
+sub dedup {
+       my %seenhref;
+       my %bytitle;
+       my @others;
+       foreach my $repo (@_) {
+               if (exists $repo->{title} &&
+                   length $repo->{title}) {
+                       if (exists $bytitle{$repo->{title}}) {
+                               my $other=$bytitle{$repo->{title}};
+                               next unless better($repo, $other);
+                               delete $bytitle{$other->{title}}
+                       }
+
+                       if (! $seenhref{$repo->{href}}++) {
+                               $bytitle{$repo->{title}}=$repo;
+                       }
+               }
+               else {
+                       push @others, $repo;
+               }
+       }
+
+       return values %bytitle, @others;
+}
+
+sub parse {
+       my $page=shift;
+
+       my @ret;
+       my $parser=HTML::Parser->new(api_version => 3);
+       my $abody=undef;
+       my $aref=undef;
+       $parser->handler(start => sub {
+               my $tagname=shift;
+               my $attr=shift;
+
+               return if ! exists $attr->{href} || ! length $attr->{href};
+               return if ! exists $attr->{rel} || $attr->{rel} !~ /^vcs-(.+)/i;
+               $attr->{type}=lc($1);
+
+               # need to collect the body of the <a> tag if there is no title
+               if ($tagname eq "a" && ! exists $attr->{title}) {
+                       $abody="";
+                       $aref=$attr;
+               }
+
+               push @ret, $attr;
+       }, "tagname, attr");
+       $parser->handler(text => sub {
+               if (defined $aref) {
+                       $abody.=join(" ", @_);
+               }
+       }, "text");
+       $parser->handler(end => sub {
+               my $tagname=shift;
+               if ($tagname eq "a" && defined $aref) {
+                       $aref->{title}=$abody;
+                       $aref=undef;
+                       $abody=undef;
+               }
+       }, "tagname");
+       $parser->report_tags(qw{link a});
+       $parser->parse($page);
+       $parser->eof;
+
+       return @ret;
+}
+
+getopts();
+
+my $page=get($url);
+if (! defined $page) {
+       die "failed to download $url\n";
+}
+
+my @repos=dedup(parse($page));
+if (! @repos) {
+       die "no repositories found on $url\n";
+}
+
+#use Data::Dumper;
+#print Dumper(\@repos);
+#exit;
+
+if (defined $destdir && @repos > 1) {
+       # create subdirs of $destdir for the multiple repos
+       if (! $noact) {
+               mkdir($destdir);
+               chdir($destdir) || die "failed to chdir to $destdir: $!";
+       }
+       $destdir=undef;
+}
+
+my $errors=0;
+foreach my $repo (@repos) {
+       my $handler=$handlers{$repo->{type}};
+       if ($handler) {
+               if ($handler->($repo->{href}) != 0) {
+                       print STDERR "failed to checkout ".$repo->{href}."\n";
+                       $errors++;
+               }
+       }
+       else {
+               print STDERR "unknown repository type ".$repo->{type}.
+                       " for ".$repo->{href}."\n";
+               $errors++;
+       }
+}
+exit($errors > 0);