From: Joey Hess Date: Wed, 10 Jul 2013 17:50:48 +0000 (-0400) Subject: myrepos (1.20130710) unstable; urgency=low X-Git-Url: https://git.madduck.net/code/myrepos.git/commitdiff_plain/a8129ccd024e1ceef56cfd8bb45580e98641d8e6 myrepos (1.20130710) unstable; urgency=low * Avoid conflicting with mr so the dummy package can be installed and pull in this one. # imported from the archive --- a8129ccd024e1ceef56cfd8bb45580e98641d8e6 diff --git a/GPL b/GPL new file mode 100644 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. + + + Copyright (C) + + 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. + + , 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 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 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 index 0000000..a31227f --- /dev/null +++ b/debian/NEWS @@ -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 Wed, 19 Jan 2011 13:40:16 -0400 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..6581538 --- /dev/null +++ b/debian/changelog @@ -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 Wed, 10 Jul 2013 13:50:48 -0400 + +myrepos (1.20130705.1) unstable; urgency=low + + * Conflict and Replaces mr. + + -- Joey Hess 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 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 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 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 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 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 Tue, 14 Feb 2012 18:13:29 -0400 + +mr (1.10) unstable; urgency=low + + * Fix display of trust errors. + + -- Joey Hess 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 Fri, 16 Dec 2011 13:29:40 -0400 + +mr (1.08) unstable; urgency=low + + * Fix vcs test code. Closes: #651976 + + -- Joey Hess 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 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 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 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 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 Sun, 01 May 2011 19:09:35 -0400 + +mr (1.02) unstable; urgency=low + + * Fix bug in escaping. + + -- Joey Hess 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Wed, 21 Jan 2009 01:40:59 -0500 + +mr (0.36) unstable; urgency=low + + * Add webcheckout command. See + * Fix display of repos configured at absolute path locations. + + -- Joey Hess 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 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 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 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 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 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 register. (Thanks, Daniel Bungert) + + -- Joey Hess 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 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 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 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 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 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 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 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 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 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 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 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 Thu, 29 Nov 2007 17:48:09 -0500 + +mr (0.17) unstable; urgency=low + + * Even bigger hammer: Set LC_ALL. Closes: #453305 + + -- Joey Hess 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 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 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 Sun, 11 Nov 2007 01:10:18 -0500 + +mr (0.13) unstable; urgency=low + + * -c was broken, fix. Closes: #449539 + + -- Joey Hess 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 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 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 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 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 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 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 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 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 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 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 Tue, 16 Oct 2007 02:39:08 -0400 + +mr (0.1) unstable; urgency=low + + * First release. + + -- Joey Hess Sun, 14 Oct 2007 14:14:40 -0400 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..a9b6f21 --- /dev/null +++ b/debian/control @@ -0,0 +1,38 @@ +Source: myrepos +Section: vcs +Priority: optional +Build-Depends: debhelper (>= 9), dpkg-dev (>= 1.9.0) +Maintainer: Joey Hess +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 index 0000000..9c2279e --- /dev/null +++ b/debian/copyright @@ -0,0 +1,7 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: * +Copyright: (c) 2007-2011 Joey Hess +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 index 0000000..e845566 --- /dev/null +++ b/debian/docs @@ -0,0 +1 @@ +README diff --git a/debian/examples b/debian/examples new file mode 100644 index 0000000..3e7cc29 --- /dev/null +++ b/debian/examples @@ -0,0 +1 @@ +mrconfig mrconfig.complex diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..d1c5d97 --- /dev/null +++ b/debian/rules @@ -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 index 0000000..784fcf8 --- /dev/null +++ b/doc/index.mdwn @@ -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 index 0000000..7e27528 --- /dev/null +++ b/doc/install.mdwn @@ -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 index 0000000..39bc885 --- /dev/null +++ b/doc/news/mr_renamed_to_myrepos.mdwn @@ -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 index 0000000..91f7cf3 --- /dev/null +++ b/doc/news/version_1.20130705.1.mdwn @@ -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 index 0000000..2053bfe --- /dev/null +++ b/doc/news/version_1.20130705.mdwn @@ -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 index 0000000..d0b9866 --- /dev/null +++ b/doc/sidebar.mdwn @@ -0,0 +1,3 @@ +* [[install]] +* [[todo]] +* Flattr this diff --git a/doc/todo.mdwn b/doc/todo.mdwn new file mode 100644 index 0000000..417d7c2 --- /dev/null +++ b/doc/todo.mdwn @@ -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 index 0000000..bc2126b --- /dev/null +++ b/doc/todo/detect_unregistered_repos_in_tree.mdwn @@ -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 index 0000000..e7c9808 --- /dev/null +++ b/doc/todo/done.mdwn @@ -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 index 0000000..13e5d5a --- /dev/null +++ b/doc/todo/smarter_chained_checkout.mdwn @@ -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 index 0000000..5091d95 --- /dev/null +++ b/lib/git-fake-bare @@ -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 + +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 index 0000000..3943924 --- /dev/null +++ b/lib/git-subtree @@ -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 index 0000000..212923a --- /dev/null +++ b/lib/git-svn @@ -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 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 index 0000000..ffe5205 --- /dev/null +++ b/lib/unison @@ -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 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 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 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 [options] checkout + +B [options] update + +B [options] status + +B [options] commit [-m "message"] + +B [options] record [-m "message"] + +B [options] fetch + +B [options] push + +B [options] diff + +B [options] log + +B [options] run command [param ...] + +B [options] bootstrap url [directory] + +B [options] register [repository] + +B [options] config section ["parameter=[value]" ...] + +B [options] action [params ...] + +B [options] [online|offline] + +B [options] remember action [params ...] + +=head1 DESCRIPTION + +B 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 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 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 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 section allows setting default values for the sections that +come after it. + +The C 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, C, C +or C (for a bound branch), etc. + +Note that these shell commands are run in a C 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 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 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 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 +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 +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 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 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. 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 + +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 () { + 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 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 index 0000000..b96c5bf --- /dev/null +++ b/mrconfig.complex @@ -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 index 0000000..e48d9de --- /dev/null +++ b/webcheckout @@ -0,0 +1,276 @@ +#!/usr/bin/perl + +=head1 NAME + +webcheckout - check out repositories referenced on a web page + +=head1 SYNOPSIS + +B [options] url [destdir] + +=head1 DESCRIPTION + +B 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 +. + +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 + +Licensed under the GNU GPL version 2 or higher. + +This program is included in myrepos + +=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 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);