durin42-hg-git-a3c3b8077cbe/.hg_archival.txt0000644000000000000000000000017112104602314016617 0ustar 00000000000000repo: 06366111af3c6a2ffa06333ed60d3ed3b9ec0763 node: a3c3b8077cbeec7c381a4b312d722d575a738610 branch: default tag: 0.4.0 durin42-hg-git-a3c3b8077cbe/.gitignore0000644000000000000000000000005012104602314015515 0ustar 00000000000000*.pyc tests/*.err build dist *.egg-info durin42-hg-git-a3c3b8077cbe/.hgignore0000644000000000000000000000006512104602314015336 0ustar 00000000000000syntax: glob *.pyc tests/*.err build dist *.egg-info durin42-hg-git-a3c3b8077cbe/.hgtags0000644000000000000000000000114312104602314015007 0ustar 00000000000000505d7cdca19838bfb270193e0709154a2dad5c19 0.1.0 77d6c9eb02fb96abb6a933170a912e94e3141f22 0.2.0 21ead8190d9c5f2a5a7500bab1d89b5373eda040 0.2.1 a222399a59d7a960f9ac41b89d3d0041bdf3e78c 0.2.2 5d39b98e5083a0897328f97c4c9795e64d45aac8 0.2.3 b53421918a89a20f35dc6b6c3974234fe45a5512 0.2.4 bc72dd89c2c911b3b844bd6a1e3841ca16cec59c 0.2.5 46d390f404da3add53c1f8de91216388f791cd82 0.2.6 fa3edeec7ed16dec6a16bd3e99bc3feba93115c3 0.3.0 556c3c586c4aa52f587ccc4d2d65b370a7e9037f 0.3.1 a9c0b93488d4d082f813c6d91c8e473505a026c4 0.3.2 9d44dafbb31c14126be151b78c7a41b3c110fd97 0.3.3 586b7aa9646641b3b1083ab349bb186c79aa646b 0.3.4 durin42-hg-git-a3c3b8077cbe/COPYING0000644000000000000000000004311012104602314014564 0ustar 00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 Library 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 Library General Public License instead of this License. durin42-hg-git-a3c3b8077cbe/DESIGN.txt0000644000000000000000000000752512104602314015255 0ustar 00000000000000Hg-Git Plugin Design Notes ========================== This plugin is designed to allow you to push to a Git server over the Git protocol and to pull from a Git based project. All data is stored in Hg native format with a mapping table. People collaborating in Git should not even be able to tell that you're using Hg to collaborate on their project (except for the '--HG--' section added to commit message). Nothing should need to be kept in the Git format - you should be able to run 'hg gclear' at any time to wipe out the Git directory and everything can be rebuilt losslessly from the existing Hg data - it is a cache only. We are using the Dulwich library, which I've modified quite a bit - I'll have to get these changes back upstream at some point. I've been adding 'TODO' comments all over the place where I only partially implemented something so I could get the entire first draft of functionality completed. The TODO areas should be mostly edge cases (executable bits, file rename info, tags, submodules, etc). Lossless Two Way ================ We need to store data that Git records that Merc does not in a git/extra_data file. This would be parents over two and committer information (author will be mapped to Hg committer). This way two Hg developers can collaborate without the Git transport messing up the local commits. Each Git commit should be reproducible as a Merc ID and vice versa on any system without losing data (ie changing the SHA). Branch Translation Policy ========================= Branches in Hg and Git are pretty different. This is meant to provide a clear policy on how one is converted to the other. * Without Bookmarks: * If you don't have bookmarks enabled, Git simply maps your 'tip' to the 'master' branch of the repository you're pushing to, since that is the most commonly used default branch name. Actually, pulling will map whatever the server points to as HEAD, but when pushing it will assume 'master' is your tip. $ hg gpush origin # will push tip to remote 'master' $ hg gpush origin master # same as above $ hg gpush origin --all # same as above If the remote server has divergent branches (branches with commits not reachable from HEAD) it will basically ignore them, not convert them into Hg changesets. It will tell you this (and why) when fetched. Conversely, on pushing, Hg named branches are ignored if they are not reachable from traversing the parents of tip. (SC: is this best?) * With Bookmarks: * If you have bookmarks enabled, it will treat your bookmarks like Git branches and will only push up references if you specify them. hg gpush origin # will error, you have to specify a branch hg gpush origin master # pushes the master branch hg gpush origin --all # pushes all local branches If a fetch gets branches, it _will_ import them and will create bookmarks that point to them, even if they have no common ancestors with the mainline (HEAD). * Other points * If you do not specify a remote name, it will assume 'origin'. This is helpful if you do not have bookmarks enabled as it will push tip automatically. If you have bookmarks enabled this is not helpful because you have to specify a branch name after. Eventually, I would like to setup tracking branch refspecs much like Git - say 'this local branch pushes and pulls to/from this remote and branch', but that will be one of the last things done. Testing Hg-Git ============== Tests are implemented in the Mercurial-standard way of small shell scripts. The scripts are located in the tests directory, and to run them you should change to that directory and then run tests/run-tests.py from the Mercurial sources. For example, if you have a copy of the Mercurial source code at /Stuff/hg-crew, you would do something like this: cd tests ; /Stuff/hg-crew/tests/run-tests.py And you should see output like this: . # Ran 1 tests, 0 skipped, 0 failed. durin42-hg-git-a3c3b8077cbe/Makefile0000644000000000000000000000226412104602314015176 0ustar 00000000000000PYTHON=python help: @echo 'Commonly used make targets:' @echo ' tests - run all tests in the automatic test suite' @echo ' all-version-tests - run all tests against many hg versions' @echo ' tests-%s - run all tests in the specified hg version' all: help tests: cd tests && $(PYTHON) run-tests.py --with-hg=`which hg` $(TESTFLAGS) test-%: cd tests && $(PYTHON) run-tests.py --with-hg=`which hg` $(TESTFLAGS) $@ tests-%: @echo "Path to crew repo is $(CREW) - set this with CREW= if needed." hg -R $(CREW) checkout $$(echo $@ | sed s/tests-//) && \ (cd $(CREW) ; $(MAKE) clean ) && \ cd tests && $(PYTHON) $(CREW)/tests/run-tests.py $(TESTFLAGS) # This is intended to be the authoritative list of Hg versions that this # extension is tested with. Versions prior to the version that ships in the # latest Ubuntu LTS release (2.0.2 for 12.04 LTS) may be dropped if they # interfere with new development. The latest released minor version should be # listed for each major version; earlier minor versions are not needed. all-version-tests: tests-1.9.3 tests-2.0.2 tests-2.1.2 tests-2.2.3 \ tests-2.3.1 tests-tip .PHONY: tests all-version-tests durin42-hg-git-a3c3b8077cbe/README.md0000644000000000000000000001732312104602314015017 0ustar 00000000000000Hg-Git Mercurial Plugin ======================= * Homepage: http://hg-git.github.com/ * https://bitbucket.org/durin42/hg-git (primary) * https://github.com/schacon/hg-git (mirror) This is the Hg-Git plugin for Mercurial, adding the ability to push and pull to/from a Git server repository from Hg. This means you can collaborate on Git based projects from Hg, or use a Git server as a collaboration point for a team with developers using both Git and Hg. The Hg-Git plugin can convert commits/changesets losslessly from one system to another, so you can push via an Hg repository and another Hg client can pull it and their changeset node ids will be identical - Mercurial data does not get lost in translation. It is intended that Hg users may wish to use this to collaborate even if no Git users are involved in the project, and it may even provide some advantages if you're using Bookmarks (see below). Dependencies ============ This plugin is implemented entirely in Python - there are no Git binary dependencies, you do not need to have Git installed on your system. The only dependencies are Mercurial and Dulwich. The plugin is known to work on Hg versions 1.9.3 through 2.3.1 and requires at least Dulwich 0.8.6. Usage ===== You can clone a Git repository from Hg by running `hg clone [dest]`. For example, if you were to run $ hg clone git://github.com/schacon/hg-git.git Hg-Git would clone the repository and convert it to an Hg repository for you. If you want to clone a github repository for later pushing (or any other repository you access via ssh), you need to convert the ssh url to a format with an explicit protocol prefix. For example, the git url with push access git@github.com:schacon/hg-git.git would read git+ssh://git@github.com/schacon/hg-git.git (Mind the switch from colon to slash after the host!) Your clone command would thus look like this: $ hg clone git+ssh://git@github.com/schacon/hg-git.git If you are starting from an existing Hg repository, you have to set up a Git repository somewhere that you have push access to, add a path entry for it in your .hg/hgrc file, and then run `hg push [name]` from within your repository. For example: $ cd hg-git # (an Hg repository) $ # edit .hg/hgrc and add the target git url in the paths section $ hg push This will convert all your Hg data into Git objects and push them to the Git server. Now that you have an Hg repository that can push/pull to/from a Git repository, you can fetch updates with `hg pull`. $ hg pull That will pull down any commits that have been pushed to the server in the meantime and give you a new head that you can merge in. Hg-Git can also be used to convert a Mercurial repository to Git. You can use a local repository or a remote repository accessed via SSH, HTTP or HTTPS. Use the following commands to convert the repository (it assumes your running this in $HOME). $ mkdir git-repo; cd git-repo; git init; cd .. $ cd hg-repo $ hg bookmarks hg $ hg push ../git-repo The hg bookmark is necessary to prevent problems as otherwise hg-git pushes to the currently checked out branch confusing Git. This will create a branch named hg in the Git repository. To get the changes in master use the following command (only necessary in the first run, later just use git merge or rebase). $ cd git-repo $ git checkout -b master hg To import new changesets into the Git repository just rerun the hg push command and then use git merge or git rebase in your Git repository. Commands ======== gclear ------ TODO gimport ------- TODO gexport ------- TODO git-cleanup ----------- TODO Hg Bookmarks Integration ======================== Hg-Git pushes your bookmarks up to the Git server as branches and will pull Git branches down and set them up as bookmarks. Installing ========== Clone this repository somewhere and make the 'extensions' section in your `~/.hgrc` file look something like this: [extensions] hggit = [path-to]/hg-git/hggit That will enable the Hg-Git extension for you. This plugin is currently tested against the following Mercurial versions: * 1.9.3 * 2.0.2 * 2.1.2 * 2.2.3 * 2.3.1 Configuration ============= git.intree ---------- hg-git keeps a git repository clone for reading and updating. By default, the git clone is the subdirectory `git` in your local Mercurial repository. If you would like this git clone to be at the same level of your Mercurial repository instead (named `.git`), add the following to your `hgrc`: [git] intree = True git.authors ----------- Git uses a strict convention for "author names" when representing changesets, using the form `[realname] [email address]`. Mercurial encourages this convention as well but is not as strict, so it's not uncommon for a Mercurial repo to have authors listed as simple usernames. hg-git by default will translate such names using the email address `none@none`, which then shows up unpleasantly on GitHub as "illegal email address". The `git.authors` option provides for an "authors translation file" that will be used during outgoing transfers from mercurial to git only, by modifying `hgrc` as such: [git] authors = authors.txt Where `authors.txt` is the name of a text file containing author name translations, one per each line, using the following format: johnny = John Smith dougie = Doug Johnson Empty lines and lines starting with a "#" are ignored. It should be noted that **this translation is on the hg->git side only**. Changesets coming from Git back to Mercurial will not translate back into hg usernames, so it's best that the same username/email combination be used on both the hg and git sides; the author file is mostly useful for translating legacy changesets. git.branch_bookmark_suffix --------------------------- hg-git does not convert between Mercurial named branches and git branches as the two are conceptually different; instead, it uses Mercurial bookmarks to represent the concept of a git branch. Therefore, when translating an hg repo over to git, you typically need to create bookmarks to mirror all the named branches that you'd like to see transferred over to git. The major caveat with this is that you can't use the same name for your bookmark as that of the named branch, and furthermore there's no feasible way to rename a branch in Mercurial. For the use case where one would like to transfer an hg repo over to git, and maintain the same named branches as are present on the hg side, the `branch_bookmark_suffix` might be all that's needed. This presents a string "suffix" that will be recognized on each bookmark name, and stripped off as the bookmark is translated to a git branch: [git] branch_bookmark_suffix=_bookmark Above, if an hg repo had a named branch called `release_6_maintenance`, you could then link it to a bookmark called `release_6_maintenance_bookmark`. hg-git will then strip off the `_bookmark` suffix from this bookmark name, and create a git branch called `release_6_maintenance`. When pulling back from git to hg, the `_bookmark` suffix is then applied back, if and only if an hg named branch of that name exists. E.g., when changes to the `release_6_maintenance` branch are checked into git, these will be placed into the `release_6_maintenance_bookmark` bookmark on hg. But if a new branch called `release_7_maintenance` were pulled over to hg, and there was not a `release_7_maintenance` named branch already, the bookmark will be named `release_7_maintenance` with no usage of the suffix. The `branch_bookmark_suffix` option is, like the `authors` option, intended for migrating legacy hg named branches. Going forward, an hg repo that is to be linked with a git repo should only use bookmarks for named branching. durin42-hg-git-a3c3b8077cbe/TODO.txt0000644000000000000000000000146712104602314015050 0ustar 00000000000000GENERAL ========== * work fine with eclipse plugin or tortoise-hg MAPPING ISSUES ============== * work in Git on a named branch created in Hg is forward-ported to be named branch commits in Hg and stripped back out if re-exported REMOTE/BRANCH STUFF ===================== * explain what branch mapping policy determined when updating refs * error nicer if pushing to remote without push access (over git://) WEBSITE =========== * more usage documentation * screencast SPEED/EFFICIENCY ================ * dulwich improvements - don't send blobs/trees already on server - thin packs - packfile creation benchmarking (seems to take a while sometimes) - at least provide status output MAYBE ========== * submodules? * .gitignore, etc - try to convert? - (probably not automatically, but perhaps a generator?) durin42-hg-git-a3c3b8077cbe/hggit/__init__.py0000644000000000000000000001721512104602314016753 0ustar 00000000000000# git.py - git server bridge # # Copyright 2008 Scott Chacon # also some code (and help) borrowed from durin42 # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. '''push and pull from a Git server This extension lets you communicate (push and pull) with a Git server. This way you can use Git hosting for your project or collaborate with a project that is in Git. A bridger of worlds, this plugin be. Try hg clone git:// or hg clone git+ssh:// For more information and instructions, see :hg:`help git` ''' from bisect import insort import inspect import os from mercurial import bundlerepo from mercurial import commands from mercurial import demandimport from mercurial import discovery from mercurial import extensions from mercurial import help from mercurial import hg from mercurial import localrepo from mercurial import revset from mercurial import templatekw from mercurial import util as hgutil from mercurial import url from mercurial.i18n import _ demandimport.ignore.extend([ 'collections', ]) import gitrepo, hgrepo from git_handler import GitHandler testedwith = '1.9.3 2.0.2 2.1.2 2.2.3 2.3.1' buglink = 'https://bitbucket.org/durin42/hg-git/issues' # support for `hg clone git://github.com/defunkt/facebox.git` # also hg clone git+ssh://git@github.com/schacon/simplegit.git _gitschemes = ('git', 'git+ssh', 'git+http', 'git+https') for _scheme in _gitschemes: hg.schemes[_scheme] = gitrepo # support for `hg clone localgitrepo` _oldlocal = hg.schemes['file'] try: urlcls = hgutil.url except AttributeError: class urlcls(object): def __init__(self, path): self.p = hgutil.drop_scheme('file', path) def localpath(self): return self.p def _local(path): p = urlcls(path).localpath() if (os.path.exists(os.path.join(p, '.git')) and not os.path.exists(os.path.join(p, '.hg'))): return gitrepo # detect a bare repository if (os.path.exists(os.path.join(p, 'HEAD')) and os.path.exists(os.path.join(p, 'objects')) and os.path.exists(os.path.join(p, 'refs')) and not os.path.exists(os.path.join(p, '.hg'))): return gitrepo return _oldlocal(path) hg.schemes['file'] = _local hgdefaultdest = hg.defaultdest def defaultdest(source): for scheme in _gitschemes: if source.startswith('%s://' % scheme) and source.endswith('.git'): source = source[:-4] break return hgdefaultdest(source) hg.defaultdest = defaultdest # defend against tracebacks if we specify -r in 'hg pull' def safebranchrevs(orig, lrepo, repo, branches, revs): revs, co = orig(lrepo, repo, branches, revs) if getattr(lrepo, 'changelog', False) and co not in lrepo.changelog: co = None return revs, co if getattr(hg, 'addbranchrevs', False): extensions.wrapfunction(hg, 'addbranchrevs', safebranchrevs) def extsetup(): templatekw.keywords.update({'gitnode': gitnodekw}) revset.symbols.update({ 'fromgit': revset_fromgit, 'gitnode': revset_gitnode }) helpdir = os.path.join(os.path.dirname(__file__), 'help') entry = (['git'], _("Working with Git Repositories"), lambda: open(os.path.join(helpdir, 'git.rst')).read()) # in 1.6 and earler the help table is a tuple if getattr(help.helptable, 'extend', None): insort(help.helptable, entry) else: help.helptable = help.helptable + (entry,) def reposetup(ui, repo): if not isinstance(repo, gitrepo.gitrepo): klass = hgrepo.generate_repo_subclass(repo.__class__) repo.__class__ = klass def gimport(ui, repo, remote_name=None): git = GitHandler(repo, ui) git.import_commits(remote_name) def gexport(ui, repo): git = GitHandler(repo, ui) git.export_commits() def gclear(ui, repo): repo.ui.status(_("clearing out the git cache data\n")) git = GitHandler(repo, ui) git.clear() def git_cleanup(ui, repo): new_map = [] for line in repo.opener(GitHandler.mapfile): gitsha, hgsha = line.strip().split(' ', 1) if hgsha in repo: new_map.append('%s %s\n' % (gitsha, hgsha)) f = repo.opener(GitHandler.mapfile, 'wb') map(f.write, new_map) ui.status(_('git commit map cleaned\n')) # drop this when we're 1.6-only, this just backports new behavior def sortednodetags(orig, *args, **kwargs): ret = orig(*args, **kwargs) ret.sort() return ret extensions.wrapfunction(localrepo.localrepository, 'nodetags', sortednodetags) def findcommonoutgoing(orig, repo, other, *args, **kwargs): if isinstance(other, gitrepo.gitrepo): git = GitHandler(repo, repo.ui) heads = git.get_refs(other.path)[0] kw = {} kw.update(kwargs) for val, k in zip(args, ('onlyheads', 'force', 'commoninc', 'portable')): kw[k] = val force = kw.get('force', False) commoninc = kw.get('commoninc', None) if commoninc is None: commoninc = discovery.findcommonincoming(repo, other, heads=heads, force=force) kw['commoninc'] = commoninc return orig(repo, other, **kw) return orig(repo, other, *args, **kwargs) extensions.wrapfunction(discovery, 'findcommonoutgoing', findcommonoutgoing) def getremotechanges(orig, ui, repo, other, *args, **opts): if isinstance(other, gitrepo.gitrepo): if args: revs = args[0] else: revs = opts.get('onlyheads', opts.get('revs')) git = GitHandler(repo, ui) r, c, cleanup = git.getremotechanges(other, revs) # ugh. This is ugly even by mercurial API compatibility standards if 'onlyheads' not in orig.func_code.co_varnames: cleanup = None return r, c, cleanup return orig(ui, repo, other, *args, **opts) try: extensions.wrapfunction(bundlerepo, 'getremotechanges', getremotechanges) except AttributeError: # 1.7+ pass def peer(orig, uiorrepo, *args, **opts): newpeer = orig(uiorrepo, *args, **opts) if isinstance(newpeer, gitrepo.gitrepo): if isinstance(uiorrepo, localrepo.localrepository): newpeer.localrepo = uiorrepo return newpeer extensions.wrapfunction(hg, 'peer', peer) def revset_fromgit(repo, subset, x): '''``fromgit()`` Select changesets that originate from Git. ''' args = revset.getargs(x, 0, 0, "fromgit takes no arguments") git = GitHandler(repo, repo.ui) return [r for r in subset if git.map_git_get(repo[r].hex()) is not None] def revset_gitnode(repo, subset, x): '''``gitnode(hash)`` Select changesets that originate in the given Git revision. ''' args = revset.getargs(x, 1, 1, "gitnode takes one argument") rev = revset.getstring(args[0], "the argument to gitnode() must be a hash") git = GitHandler(repo, repo.ui) def matches(r): gitnode = git.map_git_get(repo[r].hex()) if gitnode is None: return False return rev in [gitnode, gitnode[:12]] return [r for r in subset if matches(r)] def gitnodekw(**args): """:gitnode: String. The Git changeset identification hash, as a 40 hexadecimal digit string.""" node = args['ctx'] repo = args['repo'] git = GitHandler(repo, repo.ui) gitnode = git.map_git_get(node.hex()) if gitnode is None: gitnode = '' return gitnode cmdtable = { "gimport": (gimport, [], _('hg gimport')), "gexport": (gexport, [], _('hg gexport')), "gclear": (gclear, [], _('Clears out the Git cached data')), "git-cleanup": (git_cleanup, [], _( "Cleans up git repository after history editing")) } durin42-hg-git-a3c3b8077cbe/hggit/_ssh.py0000644000000000000000000000205212104602314016141 0ustar 00000000000000from mercurial import util class SSHVendor(object): """Parent class for ui-linked Vendor classes.""" def generate_ssh_vendor(ui): """ Allows dulwich to use hg's ui.ssh config. The dulwich.client.get_ssh_vendor property should point to the return value. """ class _Vendor(SSHVendor): def connect_ssh(self, host, command, username=None, port=None): from dulwich.client import SubprocessWrapper from mercurial import util import subprocess sshcmd = ui.config("ui", "ssh", "ssh") args = util.sshargs(sshcmd, host, username, port) cmd = '%s %s %s' % (sshcmd, args, util.shellquote(' '.join(command))) ui.debug('calling ssh: %s\n' % cmd) print command proc = subprocess.Popen(util.quotecommand(cmd), shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) return SubprocessWrapper(proc) return _Vendor durin42-hg-git-a3c3b8077cbe/hggit/git_handler.py0000644000000000000000000014712512104602314017500 0ustar 00000000000000import os, math, urllib, re import stat, posixpath, StringIO from dulwich.errors import HangupException, GitProtocolError, UpdateRefsError from dulwich.index import commit_tree from dulwich.objects import Blob, Commit, Tag, Tree, parse_timezone, S_IFGITLINK from dulwich.pack import create_delta, apply_delta from dulwich.repo import Repo from dulwich import client from dulwich import config as dul_config try: from mercurial import bookmarks bookmarks.update from mercurial import commands except ImportError: from hgext import bookmarks try: from mercurial.error import RepoError except ImportError: from mercurial.repo import RepoError from mercurial.i18n import _ from mercurial.node import hex, bin, nullid from mercurial import context, util as hgutil from mercurial import error import _ssh import util from overlay import overlayrepo RE_GIT_AUTHOR = re.compile('^(.*?) ?\<(.*?)(?:\>(.*))?$') RE_GIT_SANITIZE_AUTHOR = re.compile('[<>\n]') RE_GIT_AUTHOR_EXTRA = re.compile('^(.*?)\ ext:\((.*)\) <(.*)\>$') # Test for git:// and git+ssh:// URI. # Support several URL forms, including separating the # host and path with either a / or : (sepr) RE_GIT_URI = re.compile( r'^(?Pgit([+]ssh)?://)(?P.*?)(:(?P\d+))?' r'(?P[:/])(?P.*)$') RE_NEWLINES = re.compile('[\r\n]') RE_GIT_PROGRESS = re.compile('\((\d+)/(\d+)\)') RE_AUTHOR_FILE = re.compile('\s*=\s*') class GitProgress(object): """convert git server progress strings into mercurial progress""" def __init__(self, ui): self.ui = ui self.lasttopic = None self.msgbuf = '' def progress(self, msg): # 'Counting objects: 33640, done.\n' # 'Compressing objects: 0% (1/9955) \r msgs = RE_NEWLINES.split(self.msgbuf + msg) self.msgbuf = msgs.pop() for msg in msgs: td = msg.split(':', 1) data = td.pop() if not td: self.flush(data) continue topic = td[0] m = RE_GIT_PROGRESS.search(data) if m: if self.lasttopic and self.lasttopic != topic: self.flush() self.lasttopic = topic pos, total = map(int, m.group(1, 2)) util.progress(self.ui, topic, pos, total=total) else: self.flush(msg) def flush(self, msg=None): if self.lasttopic: util.progress(self.ui, self.lasttopic, None) self.lasttopic = None if msg: self.ui.note(msg + '\n') class GitHandler(object): mapfile = 'git-mapfile' tagsfile = 'git-tags' def __init__(self, dest_repo, ui): self.repo = dest_repo self.ui = ui if ui.configbool('git', 'intree'): self.gitdir = self.repo.wjoin('.git') else: self.gitdir = self.repo.join('git') self.init_author_file() self.paths = ui.configitems('paths') self.branch_bookmark_suffix = ui.config('git', 'branch_bookmark_suffix') self._map_git_real = {} self._map_hg_real = {} self.load_tags() @property def _map_git(self): if not self._map_git_real: self.load_map() return self._map_git_real @property def _map_hg(self): if not self._map_hg_real: self.load_map() return self._map_hg_real # make the git data directory def init_if_missing(self): if os.path.exists(self.gitdir): self.git = Repo(self.gitdir) else: os.mkdir(self.gitdir) self.git = Repo.init_bare(self.gitdir) def init_author_file(self): self.author_map = {} if self.ui.config('git', 'authors'): f = open(self.repo.wjoin( self.ui.config('git', 'authors'))) try: for line in f: line = line.strip() if not line or line.startswith('#'): continue from_, to = RE_AUTHOR_FILE.split(line, 2) self.author_map[from_] = to finally: f.close() ## FILE LOAD AND SAVE METHODS def map_set(self, gitsha, hgsha): self._map_git[gitsha] = hgsha self._map_hg[hgsha] = gitsha def map_hg_get(self, gitsha): return self._map_git.get(gitsha) def map_git_get(self, hgsha): return self._map_hg.get(hgsha) def load_map(self): if os.path.exists(self.repo.join(self.mapfile)): for line in self.repo.opener(self.mapfile): gitsha, hgsha = line.strip().split(' ', 1) self._map_git_real[gitsha] = hgsha self._map_hg_real[hgsha] = gitsha def save_map(self): file = self.repo.opener(self.mapfile, 'w+', atomictemp=True) for hgsha, gitsha in sorted(self._map_hg.iteritems()): file.write("%s %s\n" % (gitsha, hgsha)) # If this complains that NoneType is not callable, then # atomictempfile no longer has either of rename (pre-1.9) or # close (post-1.9) getattr(file, 'rename', getattr(file, 'close', None))() def load_tags(self): self.tags = {} if os.path.exists(self.repo.join(self.tagsfile)): for line in self.repo.opener(self.tagsfile): sha, name = line.strip().split(' ', 1) self.tags[name] = sha def save_tags(self): file = self.repo.opener(self.tagsfile, 'w+', atomictemp=True) for name, sha in sorted(self.tags.iteritems()): if not self.repo.tagtype(name) == 'global': file.write("%s %s\n" % (sha, name)) # If this complains that NoneType is not callable, then # atomictempfile no longer has either of rename (pre-1.9) or # close (post-1.9) getattr(file, 'rename', getattr(file, 'close', None))() ## END FILE LOAD AND SAVE METHODS ## COMMANDS METHODS def import_commits(self, remote_name): self.import_git_objects(remote_name) self.update_hg_bookmarks(self.git.get_refs()) self.save_map() def fetch(self, remote, heads): self.export_commits() refs = self.fetch_pack(remote, heads) remote_name = self.remote_name(remote) oldrefs = self.git.get_refs() if refs: self.import_git_objects(remote_name, refs) self.import_tags(refs) self.update_hg_bookmarks(refs) if remote_name: self.update_remote_branches(remote_name, refs) elif not self.paths: # intial cloning self.update_remote_branches('default', refs) # "Activate" a tipmost bookmark. bms = getattr(self.repo['tip'], 'bookmarks', lambda : None)() if bms: bookmarks.setcurrent(self.repo, bms[0]) def remoteref(ref): rn = remote_name or 'default' return 'refs/remotes/' + rn + ref[10:] modheads = [refs[k] for k in refs if k.startswith('refs/heads/') and not k.endswith('^{}') and refs[k] != oldrefs.get(remoteref(k))] if not modheads: self.ui.status(_("no changes found\n")) self.save_map() return len(modheads) def export_commits(self): try: self.export_git_objects() self.export_hg_tags() self.update_references() finally: self.save_map() def get_refs(self, remote): self.export_commits() client, path = self.get_transport_and_path(remote) old_refs = {} new_refs = {} def changed(refs): old_refs.update(refs) to_push = set(self.local_heads().values() + self.tags.values()) new_refs.update(self.get_changed_refs(refs, to_push, True)) return refs # always return the same refs to make the send a no-op try: client.send_pack(path, changed, lambda have, want: []) changed_refs = [ref for ref, sha in new_refs.iteritems() if sha != old_refs.get(ref)] new = [bin(self.map_hg_get(new_refs[ref])) for ref in changed_refs] old = {} for r in old_refs: old_ref = self.map_hg_get(old_refs[r]) if old_ref: old[bin(old_ref)] = 1 return old, new except (HangupException, GitProtocolError), e: raise hgutil.Abort(_("git remote error: ") + str(e)) def push(self, remote, revs, force): self.export_commits() old_refs, new_refs = self.upload_pack(remote, revs, force) remote_name = self.remote_name(remote) if remote_name and new_refs: for ref, new_sha in new_refs.iteritems(): old_sha = old_refs.get(ref) if old_sha is None: if self.ui.verbose: self.ui.note("adding reference %s::%s => GIT:%s\n" % (remote_name, ref, new_sha[0:8])) else: self.ui.status("adding reference %s\n" % ref) elif new_sha != old_sha: if self.ui.verbose: self.ui.note("updating reference %s::%s => GIT:%s\n" % (remote_name, ref, new_sha[0:8])) else: self.ui.status("updating reference %s\n" % ref) else: self.ui.debug("unchanged reference %s::%s => GIT:%s\n" % (remote_name, ref, new_sha[0:8])) self.update_remote_branches(remote_name, new_refs) if old_refs == new_refs: self.ui.status(_("no changes found\n")) ret = None elif len(new_refs) > len(old_refs): ret = 1 + (len(new_refs) - len(old_refs)) elif len(old_refs) > len(new_refs): ret = -1 - (len(new_refs) - len(old_refs)) else: ret = 1 return ret def clear(self): mapfile = self.repo.join(self.mapfile) if os.path.exists(self.gitdir): for root, dirs, files in os.walk(self.gitdir, topdown=False): for name in files: os.remove(os.path.join(root, name)) for name in dirs: os.rmdir(os.path.join(root, name)) os.rmdir(self.gitdir) if os.path.exists(mapfile): os.remove(mapfile) # incoming support def getremotechanges(self, remote, revs): self.export_commits() refs = self.fetch_pack(remote.path, revs) # refs contains all remote refs. Prune to only those requested. if revs: reqrefs = {} for rev in revs: for n in ('refs/heads/' + rev, 'refs/tags/' + rev): if n in refs: reqrefs[n] = refs[n] else: reqrefs = refs commits = [bin(c) for c in self.getnewgitcommits(reqrefs)[1]] b = overlayrepo(self, commits, refs) return (b, commits, lambda: None) ## CHANGESET CONVERSION METHODS def export_git_objects(self): self.init_if_missing() nodes = [self.repo.lookup(n) for n in self.repo] export = [node for node in nodes if not hex(node) in self._map_hg] total = len(export) if total: self.ui.note(_("exporting hg objects to git\n")) for i, rev in enumerate(export): util.progress(self.ui, 'exporting', i, total=total) ctx = self.repo.changectx(rev) state = ctx.extra().get('hg-git', None) if state == 'octopus': self.ui.debug("revision %d is a part " "of octopus explosion\n" % ctx.rev()) continue self.export_hg_commit(rev) util.progress(self.ui, 'importing', None, total=total) # convert this commit into git objects # go through the manifest, convert all blobs/trees we don't have # write the commit object (with metadata info) def export_hg_commit(self, rev): self.ui.note(_("converting revision %s\n") % hex(rev)) oldenc = self.swap_out_encoding() ctx = self.repo.changectx(rev) extra = ctx.extra() commit = Commit() (time, timezone) = ctx.date() # work around to bad timezone offets - dulwich does not handle # sub minute based timezones. In the one known case, it was a # manual edit that led to the unusual value. Based on that, # there is no reason to round one way or the other, so do the # simplest and round down. timezone -= (timezone % 60) commit.author = self.get_git_author(ctx) commit.author_time = int(time) commit.author_timezone = -timezone if 'committer' in extra: # fixup timezone (name, timestamp, timezone) = extra['committer'].rsplit(' ', 2) commit.committer = name commit.commit_time = timestamp # work around a timezone format change if int(timezone) % 60 != 0: #pragma: no cover timezone = parse_timezone(timezone) # Newer versions of Dulwich return a tuple here if isinstance(timezone, tuple): timezone, neg_utc = timezone commit._commit_timezone_neg_utc = neg_utc else: timezone = -int(timezone) commit.commit_timezone = timezone else: commit.committer = commit.author commit.commit_time = commit.author_time commit.commit_timezone = commit.author_timezone commit.parents = [] for parent in self.get_git_parents(ctx): hgsha = hex(parent.node()) git_sha = self.map_git_get(hgsha) if git_sha: if git_sha not in self.git.object_store: raise hgutil.Abort(_('Parent SHA-1 not present in Git' 'repo: %s' % git_sha)) commit.parents.append(git_sha) commit.message = self.get_git_message(ctx) if 'encoding' in extra: commit.encoding = extra['encoding'] tree_sha = commit_tree(self.git.object_store, self.iterblobs(ctx)) if tree_sha not in self.git.object_store: raise hgutil.Abort(_('Tree SHA-1 not present in Git repo: %s' % tree_sha)) commit.tree = tree_sha self.git.object_store.add_object(commit) self.map_set(commit.id, ctx.hex()) self.swap_out_encoding(oldenc) return commit.id def get_valid_git_username_email(self, name): r"""Sanitize usernames and emails to fit git's restrictions. The following is taken from the man page of git's fast-import command: [...] Likewise LF means one (and only one) linefeed [...] committer The committer command indicates who made this commit, and when they made it. Here is the person's display name (for example "Com M Itter") and is the person's email address ("cm@example.com[1]"). LT and GT are the literal less-than (\x3c) and greater-than (\x3e) symbols. These are required to delimit the email address from the other fields in the line. Note that and are free-form and may contain any sequence of bytes, except LT, GT and LF. is typically UTF-8 encoded. Accordingly, this function makes sure that there are none of the characters <, >, or \n in any string which will be used for a git username or email. Before this, it first removes left angle brackets and spaces from the beginning, and right angle brackets and spaces from the end, of this string, to convert such things as " " to "john@doe.com" for convenience. TESTS: >>> from mercurial.ui import ui >>> g = GitHandler('', ui()).get_valid_git_username_email >>> g('John Doe') 'John Doe' >>> g('john@doe.com') 'john@doe.com' >>> g(' ') 'john@doe.com' >>> g(' > > ') 'random???garbage?' >>> g('Typo in hgrc >but.hg-git@handles.it.gracefully>') 'Typo in hgrc ?but.hg-git@handles.it.gracefully' """ return RE_GIT_SANITIZE_AUTHOR.sub('?', name.lstrip('< ').rstrip('> ')) def get_git_author(self, ctx): # hg authors might not have emails author = ctx.user() # see if a translation exists author = self.author_map.get(author, author) # check for git author pattern compliance a = RE_GIT_AUTHOR.match(author) if a: name = self.get_valid_git_username_email(a.group(1)) email = self.get_valid_git_username_email(a.group(2)) if a.group(3) != None and len(a.group(3)) != 0: name += ' ext:(' + urllib.quote(a.group(3)) + ')' author = self.get_valid_git_username_email(name) + ' <' + self.get_valid_git_username_email(email) + '>' elif '@' in author: author = self.get_valid_git_username_email(author) + ' <' + self.get_valid_git_username_email(author) + '>' else: author = self.get_valid_git_username_email(author) + ' ' if 'author' in ctx.extra(): author = "".join(apply_delta(author, ctx.extra()['author'])) return author def get_git_parents(self, ctx): def is_octopus_part(ctx): return ctx.extra().get('hg-git', None) in ('octopus', 'octopus-done') parents = [] if ctx.extra().get('hg-git', None) == 'octopus-done': # implode octopus parents part = ctx while is_octopus_part(part): (p1, p2) = part.parents() assert not is_octopus_part(p1) parents.append(p1) part = p2 parents.append(p2) else: parents = ctx.parents() return parents def get_git_message(self, ctx): extra = ctx.extra() message = ctx.description() + "\n" if 'message' in extra: message = "".join(apply_delta(message, extra['message'])) # HG EXTRA INFORMATION add_extras = False extra_message = '' if not ctx.branch() == 'default': add_extras = True extra_message += "branch : " + ctx.branch() + "\n" renames = [] for f in ctx.files(): if f not in ctx.manifest(): continue rename = ctx.filectx(f).renamed() if rename: renames.append((rename[0], f)) if renames: add_extras = True for oldfile, newfile in renames: extra_message += "rename : " + oldfile + " => " + newfile + "\n" for key, value in extra.iteritems(): if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'): continue else: add_extras = True extra_message += "extra : " + key + " : " + urllib.quote(value) + "\n" if add_extras: message += "\n--HG--\n" + extra_message return message def iterblobs(self, ctx): if '.hgsubstate' in ctx: hgsub = util.OrderedDict() if '.hgsub' in ctx: hgsub = util.parse_hgsub(ctx['.hgsub'].data().splitlines()) hgsubstate = util.parse_hgsubstate(ctx['.hgsubstate'].data().splitlines()) for path, sha in hgsubstate.iteritems(): try: if path in hgsub and not hgsub[path].startswith('[git]'): # some other kind of a repository (e.g. [hg]) # that keeps its state in .hgsubstate, shall ignore continue yield path, sha, S_IFGITLINK except ValueError: pass for f in ctx: if f == '.hgsubstate' or f == '.hgsub': continue fctx = ctx[f] blobid = self.map_git_get(hex(fctx.filenode())) if not blobid: blob = Blob.from_string(fctx.data()) self.git.object_store.add_object(blob) self.map_set(blob.id, hex(fctx.filenode())) blobid = blob.id if 'l' in ctx.flags(f): mode = 0120000 elif 'x' in ctx.flags(f): mode = 0100755 else: mode = 0100644 yield f, blobid, mode def getnewgitcommits(self, refs=None): self.init_if_missing() # import heads and fetched tags as remote references todo = [] done = set() convert_list = {} # get a list of all the head shas seenheads = set() if refs is None: refs = self.git.refs.as_dict() if refs: for sha in refs.itervalues(): # refs contains all the refs in the server, not just the ones # we are pulling if sha in self.git.object_store: obj = self.git.get_object(sha) while isinstance(obj, Tag): obj_type, sha = obj.object obj = self.git.get_object(sha) if isinstance (obj, Commit) and sha not in seenheads: seenheads.add(sha) todo.append(sha) # sort by commit date def commitdate(sha): obj = self.git.get_object(sha) return obj.commit_time-obj.commit_timezone todo.sort(key=commitdate, reverse=True) # traverse the heads getting a list of all the unique commits commits = [] seen = set(todo) while todo: sha = todo[-1] if sha in done: todo.pop() continue assert isinstance(sha, str) obj = self.git.get_object(sha) assert isinstance(obj, Commit) for p in obj.parents: if p not in done: todo.append(p) break else: commits.append(sha) convert_list[sha] = obj done.add(sha) todo.pop() return convert_list, [commit for commit in commits if not commit in self._map_git] def import_git_objects(self, remote_name=None, refs=None): convert_list, commits = self.getnewgitcommits(refs) # import each of the commits, oldest first total = len(commits) if total: self.ui.status(_("importing git objects into hg\n")) for i, csha in enumerate(commits): util.progress(self.ui, 'importing', i, total=total, unit='commits') commit = convert_list[csha] self.import_git_commit(commit) util.progress(self.ui, 'importing', None, total=total, unit='commits') # Remove any dangling tag references. for name, rev in self.repo.tags().items(): if not rev in self.repo: if hasattr(self, 'tagscache') and self.tagscache and \ 'name' in self.tagscache: # Mercurial 1.4 and earlier. del self.repo.tagscache[name] elif hasattr(self, '_tags') and self._tags and \ 'name' in self._tags: # Mercurial 1.5 and later. del self.repo._tags[name] if (hgutil.safehasattr(self.repo, '_tagtypes') and self.repo._tagtypes and name in self.repo._tagtypes): # Mercurial 1.9 and earlier. del self.repo._tagtypes[name] elif (hgutil.safehasattr(self.repo, 'tagscache') and self.repo.tagscache and hgutil.safehasattr(self.repo.tagscache, '_tagtypes') and self.repo.tagscache._tagtypes and name in self.repo.tagscache._tagtypes): # Mercurial 2.0 and later. del self.repo.tagscache._tagtypes[name] def import_git_commit(self, commit): self.ui.debug(_("importing: %s\n") % commit.id) (strip_message, hg_renames, hg_branch, extra) = self.extract_hg_metadata(commit.message) # get a list of the changed, added, removed files files = self.get_files_changed(commit) # Handle gitlinks: collect gitlinks = self.collect_gitlinks(commit.tree) git_commit_tree = self.git[commit.tree] # Analyze hgsubstate and build an updated version # using SHAs from gitlinks hgsubstate = None if gitlinks: hgsubstate = util.parse_hgsubstate(self.git_file_readlines(git_commit_tree, '.hgsubstate')) for path, sha in gitlinks: hgsubstate[path] = sha # in case .hgsubstate wasn't among changed files # force its inclusion files['.hgsubstate'] = (False, 0100644, None) # Analyze .hgsub and merge with .gitmodules hgsub = None gitmodules = self.parse_gitmodules(git_commit_tree) if gitmodules or gitlinks: hgsub = util.parse_hgsub(self.git_file_readlines(git_commit_tree, '.hgsub')) for (sm_path, sm_url, sm_name) in gitmodules: hgsub[sm_path] = '[git]' + sm_url files['.hgsub'] = (False, 0100644, None) elif commit.parents and '.gitmodules' in self.git[self.git[commit.parents[0]].tree]: # no .gitmodules in this commit, however present in the parent # mark its hg counterpart as deleted (assuming .hgsub is there # due to the same import_git_commit process files['.hgsub'] = (True, 0100644, None) date = (commit.author_time, -commit.author_timezone) text = strip_message origtext = text try: text.decode('utf-8') except UnicodeDecodeError: text = self.decode_guess(text, commit.encoding) text = '\n'.join([l.rstrip() for l in text.splitlines()]).strip('\n') if text + '\n' != origtext: extra['message'] = create_delta(text +'\n', origtext) author = commit.author # convert extra data back to the end if ' ext:' in commit.author: m = RE_GIT_AUTHOR_EXTRA.match(commit.author) if m: name = m.group(1) ex = urllib.unquote(m.group(2)) email = m.group(3) author = name + ' <' + email + '>' + ex if ' ' in commit.author: author = commit.author[:-12] try: author.decode('utf-8') except UnicodeDecodeError: origauthor = author author = self.decode_guess(author, commit.encoding) extra['author'] = create_delta(author, origauthor) oldenc = self.swap_out_encoding() def findconvergedfiles(p1, p2): # If any files have the same contents in both parents of a merge # (and are therefore not reported as changed by Git) but are at # different file revisions in Mercurial (because they arrived at # those contents in different ways), we need to include them in # the list of changed files so that Mercurial can join up their # filelog histories (same as if the merge was done in Mercurial to # begin with). if p2 == nullid: return [] manifest1 = self.repo.changectx(p1).manifest() manifest2 = self.repo.changectx(p2).manifest() return [path for path, node1 in manifest1.iteritems() if path not in files and manifest2.get(path, node1) != node1] def getfilectx(repo, memctx, f): info = files.get(f) if info != None: # it's a file reported as modified from Git delete, mode, sha = info if delete: raise IOError if not sha: # indicates there's no git counterpart e = '' copied_path = None if '.hgsubstate' == f: data = util.serialize_hgsubstate(hgsubstate) elif '.hgsub' == f: data = util.serialize_hgsub(hgsub) else: data = self.git[sha].data copied_path = hg_renames.get(f) e = self.convert_git_int_mode(mode) else: # it's a converged file fc = context.filectx(self.repo, f, changeid=memctx.p1().rev()) data = fc.data() e = fc.flags() copied_path = fc.renamed() return context.memfilectx(f, data, 'l' in e, 'x' in e, copied_path) gparents = map(self.map_hg_get, commit.parents) p1, p2 = (nullid, nullid) octopus = False if len(gparents) > 1: # merge, possibly octopus def commit_octopus(p1, p2): ctx = context.memctx(self.repo, (p1, p2), text, list(files) + findconvergedfiles(p1, p2), getfilectx, author, date, {'hg-git': 'octopus'}) return hex(self.repo.commitctx(ctx)) octopus = len(gparents) > 2 p2 = gparents.pop() p1 = gparents.pop() while len(gparents) > 0: p2 = commit_octopus(p1, p2) p1 = gparents.pop() else: if gparents: p1 = gparents.pop() pa = None if not (p2 == nullid): node1 = self.repo.changectx(p1) node2 = self.repo.changectx(p2) pa = node1.ancestor(node2) # if named branch, add to extra if hg_branch: extra['branch'] = hg_branch # if committer is different than author, add it to extra if commit.author != commit.committer \ or commit.author_time != commit.commit_time \ or commit.author_timezone != commit.commit_timezone: extra['committer'] = "%s %d %d" % ( commit.committer, commit.commit_time, -commit.commit_timezone) if commit.encoding: extra['encoding'] = commit.encoding if hg_branch: extra['branch'] = hg_branch if octopus: extra['hg-git'] ='octopus-done' # TODO use 'n in self.repo' when we require hg 1.5 def repo_contains(n): try: return bool(self.repo.lookup(n)) except error.RepoLookupError: return False if not (repo_contains(p1) and repo_contains(p2)): raise hgutil.Abort(_('you appear to have run strip - ' 'please run hg git-cleanup')) ctx = context.memctx(self.repo, (p1, p2), text, list(files) + findconvergedfiles(p1, p2), getfilectx, author, date, extra) node = self.repo.commitctx(ctx) self.swap_out_encoding(oldenc) # save changeset to mapping file cs = hex(node) self.map_set(commit.id, cs) ## PACK UPLOADING AND FETCHING def upload_pack(self, remote, revs, force): client, path = self.get_transport_and_path(remote) old_refs = {} change_totals = {} def changed(refs): self.ui.status(_("searching for changes\n")) old_refs.update(refs) to_push = revs or set(self.local_heads().values() + self.tags.values()) return self.get_changed_refs(refs, to_push, force) def genpack(have, want): commits = [] for mo in self.git.object_store.find_missing_objects(have, want): (sha, name) = mo o = self.git.object_store[sha] t = type(o) change_totals[t] = change_totals.get(t, 0) + 1 if isinstance(o, Commit): commits.append(sha) commit_count = len(commits) self.ui.note(_("%d commits found\n") % commit_count) if commit_count > 0: self.ui.debug(_("list of commits:\n")) for commit in commits: self.ui.debug("%s\n" % commit) self.ui.status(_("adding objects\n")) return self.git.object_store.generate_pack_contents(have, want) try: new_refs = client.send_pack(path, changed, genpack) if len(change_totals) > 0: self.ui.status(_("added %d commits with %d trees" " and %d blobs\n") % (change_totals.get(Commit, 0), change_totals.get(Tree, 0), change_totals.get(Blob, 0))) return old_refs, new_refs except (HangupException, GitProtocolError), e: raise hgutil.Abort(_("git remote error: ") + str(e)) def get_changed_refs(self, refs, revs, force): new_refs = refs.copy() #The remote repo is empty and the local one doesn't have bookmarks/tags if refs.keys()[0] == 'capabilities^{}': if not self.local_heads(): tip = self.repo.lookup('tip') if tip != nullid: del new_refs['capabilities^{}'] tip = hex(tip) try: commands.bookmark(self.ui, self.repo, 'master', tip, force=True) except NameError: bookmarks.bookmark(self.ui, self.repo, 'master', tip, force=True) bookmarks.setcurrent(self.repo, 'master') new_refs['refs/heads/master'] = self.map_git_get(tip) for rev in revs: ctx = self.repo[rev] if getattr(ctx, 'bookmarks', None): labels = lambda c: ctx.tags() + [ fltr for fltr, bm in self._filter_for_bookmarks(ctx.bookmarks()) ] else: labels = lambda c: ctx.tags() prep = lambda itr: [i.replace(' ', '_') for i in itr] heads = [t for t in prep(labels(ctx)) if t in self.local_heads()] tags = [t for t in prep(labels(ctx)) if t in self.tags] if not (heads or tags): raise hgutil.Abort("revision %s cannot be pushed since" " it doesn't have a ref" % ctx) # Check if the tags the server is advertising are annotated tags, # by attempting to retrieve it from the our git repo, and building a # list of these tags. # # This is possible, even though (currently) annotated tags are # dereferenced and stored as lightweight ones, as the annotated tag # is still stored in the git repo. uptodate_annotated_tags = [] for r in tags: ref = 'refs/tags/'+r # Check tag. if not ref in refs: continue try: # We're not using Repo.tag(), as it's deprecated. tag = self.git.get_object(refs[ref]) if not isinstance(tag, Tag): continue except KeyError: continue # If we've reached here, the tag's good. uptodate_annotated_tags.append(ref) for r in heads + tags: if r in heads: ref = 'refs/heads/'+r else: ref = 'refs/tags/'+r if ref not in refs: new_refs[ref] = self.map_git_get(ctx.hex()) elif new_refs[ref] in self._map_git: rctx = self.repo[self.map_hg_get(new_refs[ref])] if rctx.ancestor(ctx) == rctx or force: new_refs[ref] = self.map_git_get(ctx.hex()) else: raise hgutil.Abort("pushing %s overwrites %s" % (ref, ctx)) elif ref in uptodate_annotated_tags: # we already have the annotated tag. pass else: raise hgutil.Abort("%s changed on the server, please pull " "and merge before pushing" % ref) return new_refs def fetch_pack(self, remote_name, heads=None): client, path = self.get_transport_and_path(remote_name) graphwalker = self.git.get_graph_walker() def determine_wants(refs): if heads: want = [] # contains pairs of ('refs/(heads|tags|...)/foo', 'foo') # if ref is just '', then we get ('foo', 'foo') stripped_refs = [ (r, r[r.find('/', r.find('/')+1)+1:]) for r in refs] for h in heads: r = [pair[0] for pair in stripped_refs if pair[1] == h] if not r: raise hgutil.Abort("ref %s not found on remote server" % h) elif len(r) == 1: want.append(refs[r[0]]) else: raise hgutil.Abort("ambiguous reference %s: %r" % (h, r)) else: want = [sha for ref, sha in refs.iteritems() if not ref.endswith('^{}') and ( ref.startswith('refs/heads/') or ref.startswith('refs/tags/') ) ] want = [x for x in want if x not in self.git] return want f, commit = self.git.object_store.add_pack() try: try: progress = GitProgress(self.ui) ret = client.fetch_pack(path, determine_wants, graphwalker, f.write, progress.progress) progress.flush() return ret except (HangupException, GitProtocolError), e: raise hgutil.Abort(_("git remote error: ") + str(e)) finally: commit() ## REFERENCES HANDLING def update_references(self): heads = self.local_heads() # Create a local Git branch name for each # Mercurial bookmark. for key in heads: git_ref = self.map_git_get(heads[key]) if git_ref: self.git.refs['refs/heads/' + key] = self.map_git_get(heads[key]) def export_hg_tags(self): for tag, sha in self.repo.tags().iteritems(): if self.repo.tagtype(tag) in ('global', 'git'): tag = tag.replace(' ', '_') target = self.map_git_get(hex(sha)) if target is not None: self.git.refs['refs/tags/' + tag] = target self.tags[tag] = hex(sha) else: self.repo.ui.warn( 'Skipping export of tag %s because it ' 'has no matching git revision.' % tag) def _filter_for_bookmarks(self, bms): if not self.branch_bookmark_suffix: return [(bm, bm) for bm in bms] else: def _filter_bm(bm): if bm.endswith(self.branch_bookmark_suffix): return bm[0:-(len(self.branch_bookmark_suffix))] else: return bm return [(_filter_bm(bm), bm) for bm in bms] def local_heads(self): try: if getattr(bookmarks, 'parse', None): bms = bookmarks.parse(self.repo) else: bms = self.repo._bookmarks return dict([(filtered_bm, hex(bms[bm])) for filtered_bm, bm in self._filter_for_bookmarks(bms)]) except AttributeError: #pragma: no cover return {} def import_tags(self, refs): keys = refs.keys() if not keys: return for k in keys[:]: ref_name = k parts = k.split('/') if parts[0] == 'refs' and parts[1] == 'tags': ref_name = "/".join([v for v in parts[2:]]) # refs contains all the refs in the server, not just # the ones we are pulling if refs[k] not in self.git.object_store: continue if ref_name[-3:] == '^{}': ref_name = ref_name[:-3] if not ref_name in self.repo.tags(): obj = self.git.get_object(refs[k]) sha = None if isinstance (obj, Commit): # lightweight sha = self.map_hg_get(refs[k]) self.tags[ref_name] = sha elif isinstance (obj, Tag): # annotated (obj_type, obj_sha) = obj.object obj = self.git.get_object(obj_sha) if isinstance (obj, Commit): sha = self.map_hg_get(obj_sha) # TODO: better handling for annotated tags self.tags[ref_name] = sha self.save_tags() def update_hg_bookmarks(self, refs): try: oldbm = getattr(bookmarks, 'parse', None) if oldbm: bms = bookmarks.parse(self.repo) else: bms = self.repo._bookmarks heads = dict([(ref[11:],refs[ref]) for ref in refs if ref.startswith('refs/heads/')]) for head, sha in heads.iteritems(): # refs contains all the refs in the server, not just # the ones we are pulling if sha not in self.git.object_store: continue hgsha = bin(self.map_hg_get(sha)) if not head in bms: # new branch bms[head] = hgsha else: bm = self.repo[bms[head]] if bm.ancestor(self.repo[hgsha]) == bm: # fast forward bms[head] = hgsha # if there's a branch bookmark suffix, # then add it on to all bookmark names # that would otherwise conflict with a branch # name if self.branch_bookmark_suffix: real_branch_names = self.repo.branchmap() bms = dict( ( bm_name + self.branch_bookmark_suffix if bm_name in real_branch_names else bm_name, bms[bm_name] ) for bm_name in bms ) if heads: if oldbm: bookmarks.write(self.repo, bms) else: self.repo._bookmarks = bms if getattr(bms, 'write', None): # hg >= 2.5 bms.write() else: # hg < 2.5 bookmarks.write(self.repo) except AttributeError: self.ui.warn(_('creating bookmarks failed, do you have' ' bookmarks enabled?\n')) def update_remote_branches(self, remote_name, refs): tagfile = self.repo.join(os.path.join('git-remote-refs')) tags = self.repo.gitrefs() # since we re-write all refs for this remote each time, prune # all entries matching this remote from our tags list now so # that we avoid any stale refs hanging around forever for t in list(tags): if t.startswith(remote_name + '/'): del tags[t] tags = dict((k, hex(v)) for k, v in tags.iteritems()) store = self.git.object_store for ref_name, sha in refs.iteritems(): if ref_name.startswith('refs/heads'): if sha not in store: continue hgsha = self.map_hg_get(sha) head = ref_name[11:] tags['/'.join((remote_name, head))] = hgsha # TODO(durin42): what is this doing? new_ref = 'refs/remotes/%s/%s' % (remote_name, head) self.git.refs[new_ref] = sha elif (ref_name.startswith('refs/tags') and not ref_name.endswith('^{}')): self.git.refs[ref_name] = sha tf = open(tagfile, 'wb') for tag, node in tags.iteritems(): tf.write('%s %s\n' % (node, tag)) tf.close() ## UTILITY FUNCTIONS def convert_git_int_mode(self, mode): # TODO: make these into constants convert = { 0100644: '', 0100755: 'x', 0120000: 'l'} if mode in convert: return convert[mode] return '' def extract_hg_metadata(self, message): split = message.split("\n--HG--\n", 1) renames = {} extra = {} branch = False if len(split) == 2: message, meta = split lines = meta.split("\n") for line in lines: if line == '': continue if ' : ' not in line: break command, data = line.split(" : ", 1) if command == 'rename': before, after = data.split(" => ", 1) renames[after] = before if command == 'branch': branch = data if command == 'extra': before, after = data.split(" : ", 1) extra[before] = urllib.unquote(after) return (message, renames, branch, extra) def get_file(self, commit, f): otree = self.git.tree(commit.tree) parts = f.split('/') for part in parts: (mode, sha) = otree[part] obj = self.git.get_object(sha) if isinstance (obj, Blob): return (mode, sha, obj._text) elif isinstance(obj, Tree): otree = obj def get_files_changed(self, commit): tree = commit.tree btree = None if commit.parents: btree = self.git[commit.parents[0]].tree changes = self.git.object_store.tree_changes(btree, tree) files = {} for (oldfile, newfile), (oldmode, newmode), (oldsha, newsha) in changes: # don't create new submodules if newmode == 0160000: if oldfile: # become a regular delete newfile, newmode = None, None else: continue # so old submodules shoudn't exist if oldmode == 0160000: if newfile: # become a regular add oldfile, oldmode = None, None else: continue if newfile is None: file = oldfile delete = True else: file = newfile delete = False files[file] = (delete, newmode, newsha) return files def collect_gitlinks(self, tree_id): """Walk the tree and collect all gitlink entries :param tree_id: sha of the commit tree :return: list of tuples (commit sha, full entry path) """ queue = [(tree_id, '')] gitlinks = [] while queue: e, path = queue.pop(0) o = self.git.object_store[e] for (name, mode, sha) in o.iteritems(): if mode == S_IFGITLINK: gitlinks.append((posixpath.join(path, name), sha)) elif stat.S_ISDIR(mode): queue.append((sha, posixpath.join(path, name))) return gitlinks def parse_gitmodules(self, tree_obj): """Parse .gitmodules from a git tree specified by tree_obj :return: list of tuples (submodule path, url, name), where name is quoted part of the section's name, or empty list if nothing found """ rv = [] try: unused_mode,gitmodules_sha = tree_obj['.gitmodules'] except KeyError: return rv gitmodules_content = self.git[gitmodules_sha].data fo = StringIO.StringIO(gitmodules_content) tt = dul_config.ConfigFile.from_file(fo) for section in tt.keys(): section_kind, section_name = section if section_kind == 'submodule': sm_path = tt.get(section, 'path') sm_url = tt.get(section, 'url') rv.append((sm_path, sm_url, section_name)) return rv def git_file_readlines(self, tree_obj, fname): """Read content of a named entry from the git commit tree :return: list of lines """ if fname in tree_obj: unused_mode, sha = tree_obj[fname] content = self.git[sha].data return content.splitlines() return [] def remote_name(self, remote): names = [name for name, path in self.paths if path == remote] if names: return names[0] # Stolen from hgsubversion def swap_out_encoding(self, new_encoding='UTF-8'): try: from mercurial import encoding old = encoding.encoding encoding.encoding = new_encoding except ImportError: old = hgutil._encoding hgutil._encoding = new_encoding return old def decode_guess(self, string, encoding): # text is not valid utf-8, try to make sense of it if encoding: try: return string.decode(encoding).encode('utf-8') except UnicodeDecodeError: pass try: return string.decode('latin-1').encode('utf-8') except UnicodeDecodeError: return string.decode('ascii', 'replace').encode('utf-8') def get_transport_and_path(self, uri): # pass hg's ui.ssh config to dulwich if not issubclass(client.get_ssh_vendor, _ssh.SSHVendor): client.get_ssh_vendor = _ssh.generate_ssh_vendor(self.ui) git_match = RE_GIT_URI.match(uri) if git_match: res = git_match.groupdict() transport = client.SSHGitClient if 'ssh' in res['scheme'] else client.TCPGitClient host, port, sepr, path = res['host'], res['port'], res['sepr'], res['path'] if sepr == '/': path = '/' + path # strip trailing slash for heroku-style URLs # ssh+git://git@heroku.com:project.git/ if sepr == ':' and path.endswith('.git/'): path = path.rstrip('/') if port: client.port = port return transport(host, thin_packs=False, port=port), path httpclient = getattr(client, 'HttpGitClient', None) if uri.startswith('git+http://') or uri.startswith('git+https://'): uri = uri[4:] if uri.startswith('http://') or uri.startswith('https://'): if not httpclient: raise RepoError('git via HTTP requires dulwich 0.8.1 or later') else: return client.HttpGitClient(uri, thin_packs=False), uri # if its not git or git+ssh, try a local url.. return client.SubprocessGitClient(thin_packs=False), uri durin42-hg-git-a3c3b8077cbe/hggit/gitrepo.py0000644000000000000000000000431212104602314016657 0ustar 00000000000000import os from mercurial import util try: from mercurial.error import RepoError except ImportError: from mercurial.repo import RepoError try: from mercurial.peer import peerrepository except ImportError: from mercurial.repo import repository as peerrepository from git_handler import GitHandler from overlay import overlayrepo from mercurial.node import bin class gitrepo(peerrepository): capabilities = ['lookup'] def _capabilities(self): return self.capabilities def __init__(self, ui, path, create): if create: # pragma: no cover raise util.Abort('Cannot create a git repository.') self.ui = ui self.path = path self.localrepo = None self.handler = None def _initializehandler(self): if self.handler is None and self.localrepo is not None: self.handler = GitHandler(self.localrepo, self.localrepo.ui) return self.handler def url(self): return self.path def lookup(self, key): if isinstance(key, str): return key def local(self): if not self.path: raise RepoError def heads(self): return [] def listkeys(self, namespace): if namespace == 'namespaces': return {'bookmarks':''} elif namespace == 'bookmarks': handler = self._initializehandler() if handler: handler.export_commits() refs = handler.fetch_pack(self.path) reqrefs = refs convertlist, commits = handler.getnewgitcommits(reqrefs) newcommits = [bin(c) for c in commits] b = overlayrepo(handler, newcommits, refs) stripped_refs = dict([ (ref[11:], b.node(refs[ref])) for ref in refs.keys() if ref.startswith('refs/heads/')]) return stripped_refs return {} def pushkey(self, namespace, key, old, new): return False # used by incoming in hg <= 1.6 def branches(self, nodes): return [] instance = gitrepo def islocal(path): u = util.url(path) return not u.scheme or u.scheme == 'file' durin42-hg-git-a3c3b8077cbe/hggit/help/git.rst0000644000000000000000000000540412104602314017104 0ustar 00000000000000Basic Use --------- You can clone a Git repository from Hg by running `hg clone [dest]`. For example, if you were to run:: $ hg clone git://github.com/schacon/hg-git.git Hg-Git would clone the repository and convert it to an Hg repository for you. There are a number of different protocols that can be used for Git repositories. Examples of Git repository URLs include:: https://github.com/schacon/hg-git.git http://code.google.com/p/guava-libraries ssh://git@github.com:schacon/hg-git.git git://github.com/schacon/hg-git.git ../hg-git (local file path) For the HTTP, HTTPS, and SSH protocols, it isn't clear based solely on the URL whether the remote repository should be treated as a Mercurial repository or a Git repository. Thus, to specify that a URL should use Git, prepend the URL with "git+". For example, an HTTPS URL would start with "git+https://". Also, note that Git doesn't require the specification of the protocol for SSH, but Mercurial does. Hg-Git automatically detects whether file paths should be treated as Git repositories by their contents. If you are starting from an existing Hg repository, you have to set up a Git repository somewhere that you have push access to, add a path entry for it in your .hg/hgrc file, and then run `hg push [name]` from within your repository. For example:: $ cd hg-git # (an Hg repository) $ # edit .hg/hgrc and add the target Git URL in the paths section $ hg push This will convert all your Hg data into Git objects and push them to the Git server. Pulling new revisions into a repository is the same as from any other Mercurial source. Within the earlier examples, the following commands are all equivalent:: $ hg pull $ hg pull default $ hg pull git://github.com/schacon/hg-git.git Git branches are exposed in Hg as bookmarks, while Git remotes are exposed as Hg local tags. See `hg help bookmarks` and `hg help tags` for further information. Finding and displaying Git revisions ------------------------------------ For displaying the Git revision ID, Hg-Git provides a template keyword: :gitnode: String. The Git changeset identification hash, as a 40 hexadecimal digit string. For example:: $ hg log --template='{rev}:{node|short}:{gitnode|short} {desc}\n' $ hg log --template='hg: {node}\ngit: {gitnode}\n{date|isodate} {author}\n{desc}\n\n' For finding changesets from Git, Hg-Git extends revsets to provide two new selectors: :fromgit: Select changesets that originate from Git. Takes no arguments. :gitnode: Select changesets that originate in a specific Git revision. Takes a revision argument. For example:: $ hg log -r 'fromgit()' $ hg log -r 'gitnode(84f75b909fc3)' Revsets are accepted by several Mercurial commands for specifying revisions. See ``hg help revsets`` for details. durin42-hg-git-a3c3b8077cbe/hggit/hgrepo.py0000644000000000000000000000607512104602314016502 0ustar 00000000000000import os from mercurial.node import bin from git_handler import GitHandler from gitrepo import gitrepo def generate_repo_subclass(baseclass): class hgrepo(baseclass): def pull(self, remote, heads=None, force=False): if isinstance(remote, gitrepo): git = GitHandler(self, self.ui) return git.fetch(remote.path, heads) else: #pragma: no cover return super(hgrepo, self).pull(remote, heads, force) # TODO figure out something useful to do with the newbranch param def push(self, remote, force=False, revs=None, newbranch=None): if isinstance(remote, gitrepo): git = GitHandler(self, self.ui) return git.push(remote.path, revs, force) else: #pragma: no cover # newbranch was added in 1.6 if newbranch is None: return super(hgrepo, self).push(remote, force, revs) else: return super(hgrepo, self).push(remote, force, revs, newbranch) def findoutgoing(self, remote, base=None, heads=None, force=False): if isinstance(remote, gitrepo): git = GitHandler(self, self.ui) base, heads = git.get_refs(remote.path) out, h = super(hgrepo, self).findoutgoing(remote, base, heads, force) return out else: #pragma: no cover return super(hgrepo, self).findoutgoing(remote, base, heads, force) def _findtags(self): (tags, tagtypes) = super(hgrepo, self)._findtags() git = GitHandler(self, self.ui) for tag, rev in git.tags.iteritems(): tags[tag] = bin(rev) tagtypes[tag] = 'git' tags.update(self.gitrefs()) return (tags, tagtypes) def gitrefs(self): tagfile = self.join(os.path.join('git-remote-refs')) if os.path.exists(tagfile): tf = open(tagfile, 'rb') tagdata = tf.read().split('\n') td = [line.split(' ', 1) for line in tagdata if line] return dict([(name, bin(sha)) for sha, name in td]) return {} def tags(self): if hasattr(self, 'tagscache') and self.tagscache: # Mercurial 1.4 and earlier. return self.tagscache elif hasattr(self, '_tags') and self._tags: # Mercurial 1.5 and later. return self._tags git = GitHandler(self, self.ui) tagscache = super(hgrepo, self).tags() tagscache.update(self.gitrefs()) for tag, rev in git.tags.iteritems(): if tag in tagscache: continue tagscache[tag] = bin(rev) if hasattr(self, '_tagstypecache'): # Only present in Mercurial 1.3 and earlier. self._tagstypecache[tag] = 'git' return tagscache return hgrepo durin42-hg-git-a3c3b8077cbe/hggit/overlay.py0000644000000000000000000001574612104602314016704 0ustar 00000000000000# overlay classes for repositories # unifies access to unimported git objects and committed hg objects # designed to support incoming # # incomplete, implemented on demand from mercurial import context from mercurial.node import bin, hex, nullid class overlaymanifest(object): def __init__(self, repo, sha): self.repo = repo self.tree = repo.handler.git.get_object(sha) self._map = None self._flagmap = None def withflags(self): self.load() return set([path for path, flag in self._flagmap.iteritems() if flag & 020100]) def copy(self): return overlaymanifest(self.repo, self.tree.id) def keys(self): self.load() return self._map.keys() def flags(self, path): self.load() def hgflag(gitflag): if gitflag & 0100: return 'x' elif gitflag & 020000: return 'l' else: return '' return hgflag(self._flagmap[path]) def load(self): if self._map is not None: return self._map = {} self._flagmap = {} def addtree(tree, dirname): for entry in tree.iteritems(): if entry.mode & 040000: # expand directory subtree = self.repo.handler.git.get_object(entry.sha) addtree(subtree, dirname + entry.path + '/') else: path = dirname + entry.path self._map[path] = bin(entry.sha) self._flagmap[path] = entry.mode addtree(self.tree, '') def __iter__(self): self.load() return self._map.__iter__() def __getitem__(self, path): self.load() return self._map[path] def __delitem__(self, path): del self._map[path] class overlayfilectx(object): def __init__(self, repo, path, fileid=None): self.repo = repo self._path = path self.fileid = fileid # this is a hack to skip copy detection def ancestors(self): return [self, self] def rev(self): return -1 def path(self): return self._path def filelog(self): return self.fileid def data(self): blob = self.repo.handler.git.get_object(self.fileid) return blob.data class overlaychangectx(context.changectx): def __init__(self, repo, sha): self.repo = repo self.commit = repo.handler.git.get_object(sha) def node(self): return bin(self.commit.id) def rev(self): return self.repo.rev(bin(self.commit.id)) def date(self): return self.commit.author_time, self.commit.author_timezone def branch(self): return 'default' def user(self): return self.commit.author def files(self): return [] def extra(self): return {} def description(self): return self.commit.message def parents(self): return [overlaychangectx(self.repo, sha) for sha in self.commit.parents] def manifestnode(self): return bin(self.commit.tree) def hex(self): return self.commit.id def tags(self): return [] def bookmarks(self): return [] def manifest(self): return overlaymanifest(self.repo, self.commit.tree) def filectx(self, path, filelog=None): mf = self.manifest() return overlayfilectx(self.repo, path, mf[path]) def flags(self, path): mf = self.manifest() return mf.flags(path) def __nonzero__(self): return True def phase(self): try: from mercurial import phases return phases.draft except ImportError: return 1 class overlayrevlog(object): def __init__(self, repo, base): self.repo = repo self.base = base def parents(self, n): gitrev = self.repo.revmap.get(n) if not gitrev: # we've reached a revision we have return self.base.parents(n) commit = self.repo.handler.git.get_object(n) def gitorhg(n): hn = self.repo.handler.map_hg_get(hex(n)) if hn is not None: return bin(hn) return n # currently ignores the octopus p1 = gitorhg(bin(commit.parents[0])) if len(commit.parents) > 1: p2 = gitorhg(bin(commit.parents[1])) else: p2 = nullid return [p1, p2] def parentrevs(self, rev): return [self.rev(p) for p in self.parents(self.node(rev))] def node(self, rev): gitnode = self.repo.nodemap.get(rev) if gitnode is None: return self.base.node(rev) return gitnode def rev(self, n): gitrev = self.repo.revmap.get(n) if gitrev is None: return self.base.rev(n) return gitrev def nodesbetween(self, nodelist, revs): # this is called by pre-1.9 incoming with the nodelist we returned from # getremotechanges. Just return it back. return [nodelist] def __len__(self): return len(self.repo.handler.repo) + len(self.repo.revmap) class overlayrepo(object): def __init__(self, handler, commits, refs): self.handler = handler self.changelog = overlayrevlog(self, handler.repo.changelog) self.manifest = overlayrevlog(self, handler.repo.manifest) # for incoming -p self.root = handler.repo.root self.getcwd = handler.repo.getcwd self.status = handler.repo.status self.ui = handler.repo.ui self.revmap = None self.nodemap = None self.refmap = None self.tagmap = None self._makemaps(commits, refs) def __getitem__(self, n): if n not in self.revmap: return self.handler.repo[n] return overlaychangectx(self, n) def node(self, n): """Returns an Hg or Git hash for the specified Git hash""" if bin(n) in self.revmap: return n return self.handler.map_hg_get(n) def nodebookmarks(self, n): return self.refmap.get(n, []) def nodetags(self, n): return self.tagmap.get(n, []) def rev(self, n): return self.revmap[n] def filectx(self, path, fileid=None): return overlayfilectx(self, path, fileid=fileid) def _makemaps(self, commits, refs): baserev = self.handler.repo['tip'].rev() self.revmap = {} self.nodemap = {} for i, n in enumerate(commits): rev = baserev + i + 1 self.revmap[n] = rev self.nodemap[rev] = n self.refmap = {} self.tagmap = {} for ref in refs: if ref.startswith('refs/heads/'): refname = ref[11:] self.refmap.setdefault(bin(refs[ref]), []).append(refname) elif ref.startswith('refs/tags/'): tagname = ref[10:] self.tagmap.setdefault(bin(refs[ref]), []).append(tagname) durin42-hg-git-a3c3b8077cbe/hggit/util.py0000644000000000000000000000237612104602314016173 0ustar 00000000000000"""Compatability functions for old Mercurial versions.""" try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict def progress(ui, *args, **kwargs): """Shim for progress on hg < 1.4. Remove when 1.3 is dropped.""" getattr(ui, 'progress', lambda *x, **kw: None)(*args, **kwargs) def parse_hgsub(lines): """Fills OrderedDict with hgsub file content passed as list of lines""" rv = OrderedDict() for l in lines: ls = l.strip(); if not ls or ls[0] == '#': continue name, value = l.split('=', 1) rv[name.strip()] = value.strip() return rv def serialize_hgsub(data): """Produces a string from OrderedDict hgsub content""" return ''.join(['%s = %s\n' % (n,v) for n,v in data.iteritems()]) def parse_hgsubstate(lines): """Fills OrderedDict with hgsubtate file content passed as list of lines""" rv = OrderedDict() for l in lines: ls = l.strip(); if not ls or ls[0] == '#': continue value, name = l.split(' ', 1) rv[name.strip()] = value.strip() return rv def serialize_hgsubstate(data): """Produces a string from OrderedDict hgsubstate content""" return ''.join(['%s %s\n' % (data[n], n) for n in sorted(data)]) durin42-hg-git-a3c3b8077cbe/setup.py0000644000000000000000000000156612104602314015254 0ustar 00000000000000try: from setuptools import setup except: from distutils.core import setup try: from collections import OrderedDict extra_req = [] except ImportError: extra_req = ['ordereddict>=1.1'] setup( name='hg-git', version='0.4.0', author='Scott Chacon', maintainer='Augie Fackler', maintainer_email='durin42@gmail.com', url='http://hg-git.github.com/', description='push and pull from a Git server using Mercurial', long_description=""" This extension lets you communicate (push and pull) with a Git server. This way you can use Git hosting for your project or collaborate with a project that is in Git. A bridger of worlds, this plugin be. """.strip(), keywords='hg git mercurial', license='GPLv2', packages=['hggit'], package_data={ 'hggit': ['help/git.rst'] }, install_requires=['dulwich>=0.8.6'] + extra_req, ) durin42-hg-git-a3c3b8077cbe/tests/hghave0000755000000000000000000000405712104602314016072 0ustar 00000000000000#!/usr/bin/env python """Test the running system for features availability. Exit with zero if all features are there, non-zero otherwise. If a feature name is prefixed with "no-", the absence of feature is tested. """ import optparse import sys import hghave checks = hghave.checks def list_features(): for name, feature in checks.iteritems(): desc = feature[1] print name + ':', desc def test_features(): failed = 0 for name, feature in checks.iteritems(): check, _ = feature try: check() except Exception, e: print "feature %s failed: %s" % (name, e) failed += 1 return failed parser = optparse.OptionParser("%prog [options] [features]") parser.add_option("--test-features", action="store_true", help="test available features") parser.add_option("--list-features", action="store_true", help="list available features") parser.add_option("-q", "--quiet", action="store_true", help="check features silently") if __name__ == '__main__': options, args = parser.parse_args() if options.list_features: list_features() sys.exit(0) if options.test_features: sys.exit(test_features()) quiet = options.quiet failures = 0 def error(msg): global failures if not quiet: sys.stderr.write(msg + '\n') failures += 1 for feature in args: negate = feature.startswith('no-') if negate: feature = feature[3:] if feature not in checks: error('skipped: unknown feature: ' + feature) continue check, desc = checks[feature] try: available = check() except Exception, e: error('hghave check failed: ' + feature) continue if not negate and not available: error('skipped: missing feature: ' + desc) elif negate and available: error('skipped: system supports %s' % desc) if failures != 0: sys.exit(1) durin42-hg-git-a3c3b8077cbe/tests/hghave.py0000755000000000000000000002130012104602314016507 0ustar 00000000000000import os, stat, socket import re import sys import tempfile tempprefix = 'hg-hghave-' def matchoutput(cmd, regexp, ignorestatus=False): """Return True if cmd executes successfully and its output is matched by the supplied regular expression. """ r = re.compile(regexp) fh = os.popen(cmd) s = fh.read() try: ret = fh.close() except IOError: # Happen in Windows test environment ret = 1 return (ignorestatus or ret is None) and r.search(s) def has_baz(): return matchoutput('baz --version 2>&1', r'baz Bazaar version') def has_bzr(): try: import bzrlib return bzrlib.__doc__ is not None except ImportError: return False def has_bzr114(): try: import bzrlib return (bzrlib.__doc__ is not None and bzrlib.version_info[:2] >= (1, 14)) except ImportError: return False def has_cvs(): re = r'Concurrent Versions System.*?server' return matchoutput('cvs --version 2>&1', re) and not has_msys() def has_darcs(): return matchoutput('darcs --version', r'2\.[2-9]', True) def has_mtn(): return matchoutput('mtn --version', r'monotone', True) and not matchoutput( 'mtn --version', r'monotone 0\.', True) def has_eol_in_paths(): try: fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r') os.close(fd) os.remove(path) return True except (IOError, OSError): return False def has_executablebit(): try: EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix) try: os.close(fh) m = os.stat(fn).st_mode & 0777 new_file_has_exec = m & EXECFLAGS os.chmod(fn, m ^ EXECFLAGS) exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m) finally: os.unlink(fn) except (IOError, OSError): # we don't care, the user probably won't be able to commit anyway return False return not (new_file_has_exec or exec_flags_cannot_flip) def has_icasefs(): # Stolen from mercurial.util fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix) os.close(fd) try: s1 = os.stat(path) d, b = os.path.split(path) p2 = os.path.join(d, b.upper()) if path == p2: p2 = os.path.join(d, b.lower()) try: s2 = os.stat(p2) return s2 == s1 except OSError: return False finally: os.remove(path) def has_inotify(): try: import hgext.inotify.linux.watcher except ImportError: return False name = tempfile.mktemp(dir='.', prefix=tempprefix) sock = socket.socket(socket.AF_UNIX) try: sock.bind(name) except socket.error, err: return False sock.close() os.unlink(name) return True def has_fifo(): if getattr(os, "mkfifo", None) is None: return False name = tempfile.mktemp(dir='.', prefix=tempprefix) try: os.mkfifo(name) os.unlink(name) return True except OSError: return False def has_cacheable_fs(): from mercurial import util fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix) os.close(fd) try: return util.cachestat(path).cacheable() finally: os.remove(path) def has_lsprof(): try: import _lsprof return True except ImportError: return False def has_gettext(): return matchoutput('msgfmt --version', 'GNU gettext-tools') def has_git(): return matchoutput('git --version 2>&1', r'^git version') def has_docutils(): try: from docutils.core import publish_cmdline return True except ImportError: return False def getsvnversion(): m = matchoutput('svn --version 2>&1', r'^svn,\s+version\s+(\d+)\.(\d+)') if not m: return (0, 0) return (int(m.group(1)), int(m.group(2))) def has_svn15(): return getsvnversion() >= (1, 5) def has_svn13(): return getsvnversion() >= (1, 3) def has_svn(): return matchoutput('svn --version 2>&1', r'^svn, version') and \ matchoutput('svnadmin --version 2>&1', r'^svnadmin, version') def has_svn_bindings(): try: import svn.core version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR if version < (1, 4): return False return True except ImportError: return False def has_p4(): return (matchoutput('p4 -V', r'Rev\. P4/') and matchoutput('p4d -V', r'Rev\. P4D/')) def has_symlink(): if getattr(os, "symlink", None) is None: return False name = tempfile.mktemp(dir='.', prefix=tempprefix) try: os.symlink(".", name) os.unlink(name) return True except (OSError, AttributeError): return False def has_hardlink(): from mercurial import util fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix) os.close(fh) name = tempfile.mktemp(dir='.', prefix=tempprefix) try: try: util.oslink(fn, name) os.unlink(name) return True except OSError: return False finally: os.unlink(fn) def has_tla(): return matchoutput('tla --version 2>&1', r'The GNU Arch Revision') def has_gpg(): return matchoutput('gpg --version 2>&1', r'GnuPG') def has_unix_permissions(): d = tempfile.mkdtemp(dir='.', prefix=tempprefix) try: fname = os.path.join(d, 'foo') for umask in (077, 007, 022): os.umask(umask) f = open(fname, 'w') f.close() mode = os.stat(fname).st_mode os.unlink(fname) if mode & 0777 != ~umask & 0666: return False return True finally: os.rmdir(d) def has_pyflakes(): return matchoutput("sh -c \"echo 'import re' 2>&1 | pyflakes\"", r":1: 're' imported but unused", True) def has_pygments(): try: import pygments return True except ImportError: return False def has_outer_repo(): # failing for other reasons than 'no repo' imply that there is a repo return not matchoutput('hg root 2>&1', r'abort: no repository found', True) def has_ssl(): try: import ssl import OpenSSL OpenSSL.SSL.Context return True except ImportError: return False def has_windows(): return os.name == 'nt' def has_system_sh(): return os.name != 'nt' def has_serve(): return os.name != 'nt' # gross approximation def has_tic(): return matchoutput('test -x "`which tic`"', '') def has_msys(): return os.getenv('MSYSTEM') checks = { "true": (lambda: True, "yak shaving"), "false": (lambda: False, "nail clipper"), "baz": (has_baz, "GNU Arch baz client"), "bzr": (has_bzr, "Canonical's Bazaar client"), "bzr114": (has_bzr114, "Canonical's Bazaar client >= 1.14"), "cacheable": (has_cacheable_fs, "cacheable filesystem"), "cvs": (has_cvs, "cvs client/server"), "darcs": (has_darcs, "darcs client"), "docutils": (has_docutils, "Docutils text processing library"), "eol-in-paths": (has_eol_in_paths, "end-of-lines in paths"), "execbit": (has_executablebit, "executable bit"), "fifo": (has_fifo, "named pipes"), "gettext": (has_gettext, "GNU Gettext (msgfmt)"), "git": (has_git, "git command line client"), "gpg": (has_gpg, "gpg client"), "hardlink": (has_hardlink, "hardlinks"), "icasefs": (has_icasefs, "case insensitive file system"), "inotify": (has_inotify, "inotify extension support"), "lsprof": (has_lsprof, "python lsprof module"), "mtn": (has_mtn, "monotone client (>= 1.0)"), "outer-repo": (has_outer_repo, "outer repo"), "p4": (has_p4, "Perforce server and client"), "pyflakes": (has_pyflakes, "Pyflakes python linter"), "pygments": (has_pygments, "Pygments source highlighting library"), "serve": (has_serve, "platform and python can manage 'hg serve -d'"), "ssl": (has_ssl, "python >= 2.6 ssl module and python OpenSSL"), "svn": (has_svn, "subversion client and admin tools"), "svn13": (has_svn13, "subversion client and admin tools >= 1.3"), "svn15": (has_svn15, "subversion client and admin tools >= 1.5"), "svn-bindings": (has_svn_bindings, "subversion python bindings"), "symlink": (has_symlink, "symbolic links"), "system-sh": (has_system_sh, "system() uses sh"), "tic": (has_tic, "terminfo compiler"), "tla": (has_tla, "GNU Arch tla client"), "unix-permissions": (has_unix_permissions, "unix-style permissions"), "windows": (has_windows, "Windows"), "msys": (has_msys, "Windows with MSYS"), } durin42-hg-git-a3c3b8077cbe/tests/latin-1-encoding0000644000000000000000000000062112104602314017647 0ustar 00000000000000# -*- coding: latin-1 -*- # this file contains some latin-1 messages for test-encoding GIT_AUTHOR_NAME='tést èncödîng'; export GIT_AUTHOR_NAME echo beta > beta git add beta fn_git_commit -m 'add beta' echo gamma > gamma git add gamma fn_git_commit -m 'add gämmâ' # test the commit encoding field git config i18n.commitencoding latin-1 echo delta > delta git add delta fn_git_commit -m 'add déltà' durin42-hg-git-a3c3b8077cbe/tests/run-tests.py0000755000000000000000000012751512104602314017230 0ustar 00000000000000#!/usr/bin/env python # # run-tests.py - Run a set of tests on Mercurial # # Copyright 2006 Matt Mackall # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. # Modifying this script is tricky because it has many modes: # - serial (default) vs parallel (-jN, N > 1) # - no coverage (default) vs coverage (-c, -C, -s) # - temp install (default) vs specific hg script (--with-hg, --local) # - tests are a mix of shell scripts and Python scripts # # If you change this script, it is recommended that you ensure you # haven't broken it by running it in various modes with a representative # sample of test scripts. For example: # # 1) serial, no coverage, temp install: # ./run-tests.py test-s* # 2) serial, no coverage, local hg: # ./run-tests.py --local test-s* # 3) serial, coverage, temp install: # ./run-tests.py -c test-s* # 4) serial, coverage, local hg: # ./run-tests.py -c --local test-s* # unsupported # 5) parallel, no coverage, temp install: # ./run-tests.py -j2 test-s* # 6) parallel, no coverage, local hg: # ./run-tests.py -j2 --local test-s* # 7) parallel, coverage, temp install: # ./run-tests.py -j2 -c test-s* # currently broken # 8) parallel, coverage, local install: # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) # 9) parallel, custom tmp dir: # ./run-tests.py -j2 --tmpdir /tmp/myhgtests # # (You could use any subset of the tests: test-s* happens to match # enough that it's worth doing parallel runs, few enough that it # completes fairly quickly, includes both shell and Python scripts, and # includes some scripts that run daemon processes.) from distutils import version import difflib import errno import optparse import os import shutil import subprocess import signal import sys import tempfile import time import re import threading processlock = threading.Lock() closefds = os.name == 'posix' def Popen4(cmd, wd, timeout): processlock.acquire() p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, close_fds=closefds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) processlock.release() p.fromchild = p.stdout p.tochild = p.stdin p.childerr = p.stderr p.timeout = False if timeout: def t(): start = time.time() while time.time() - start < timeout and p.returncode is None: time.sleep(.1) p.timeout = True if p.returncode is None: terminate(p) threading.Thread(target=t).start() return p # reserved exit code to skip test (used by hghave) SKIPPED_STATUS = 80 SKIPPED_PREFIX = 'skipped: ' FAILED_PREFIX = 'hghave check failed: ' PYTHON = sys.executable.replace('\\', '/') IMPL_PATH = 'PYTHONPATH' if 'java' in sys.platform: IMPL_PATH = 'JYTHONPATH' requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"] defaults = { 'jobs': ('HGTEST_JOBS', 1), 'timeout': ('HGTEST_TIMEOUT', 180), 'port': ('HGTEST_PORT', 20059), 'shell': ('HGTEST_SHELL', 'sh'), } def parselistfiles(files, listtype, warn=True): entries = dict() for filename in files: try: path = os.path.expanduser(os.path.expandvars(filename)) f = open(path, "r") except IOError, err: if err.errno != errno.ENOENT: raise if warn: print "warning: no such %s file: %s" % (listtype, filename) continue for line in f.readlines(): line = line.split('#', 1)[0].strip() if line: entries[line] = filename f.close() return entries def parseargs(): parser = optparse.OptionParser("%prog [options] [tests]") # keep these sorted parser.add_option("--blacklist", action="append", help="skip tests listed in the specified blacklist file") parser.add_option("--whitelist", action="append", help="always run tests listed in the specified whitelist file") parser.add_option("-C", "--annotate", action="store_true", help="output files annotated with coverage") parser.add_option("--child", type="int", help="run as child process, summary to given fd") parser.add_option("-c", "--cover", action="store_true", help="print a test coverage report") parser.add_option("-d", "--debug", action="store_true", help="debug mode: write output of test scripts to console" " rather than capturing and diff'ing it (disables timeout)") parser.add_option("-f", "--first", action="store_true", help="exit on the first test failure") parser.add_option("-H", "--htmlcov", action="store_true", help="create an HTML report of the coverage of the files") parser.add_option("--inotify", action="store_true", help="enable inotify extension when running tests") parser.add_option("-i", "--interactive", action="store_true", help="prompt to accept changed output") parser.add_option("-j", "--jobs", type="int", help="number of jobs to run in parallel" " (default: $%s or %d)" % defaults['jobs']) parser.add_option("--keep-tmpdir", action="store_true", help="keep temporary directory after running tests") parser.add_option("-k", "--keywords", help="run tests matching keywords") parser.add_option("-l", "--local", action="store_true", help="shortcut for --with-hg=/../hg") parser.add_option("-n", "--nodiff", action="store_true", help="skip showing test changes") parser.add_option("-p", "--port", type="int", help="port on which servers should listen" " (default: $%s or %d)" % defaults['port']) parser.add_option("--pure", action="store_true", help="use pure Python code instead of C extensions") parser.add_option("-R", "--restart", action="store_true", help="restart at last error") parser.add_option("-r", "--retest", action="store_true", help="retest failed tests") parser.add_option("-S", "--noskips", action="store_true", help="don't report skip tests verbosely") parser.add_option("--shell", type="string", help="shell to use (default: $%s or %s)" % defaults['shell']) parser.add_option("-t", "--timeout", type="int", help="kill errant tests after TIMEOUT seconds" " (default: $%s or %d)" % defaults['timeout']) parser.add_option("--tmpdir", type="string", help="run tests in the given temporary directory" " (implies --keep-tmpdir)") parser.add_option("-v", "--verbose", action="store_true", help="output verbose messages") parser.add_option("--view", type="string", help="external diff viewer") parser.add_option("--with-hg", type="string", metavar="HG", help="test using specified hg script rather than a " "temporary installation") parser.add_option("-3", "--py3k-warnings", action="store_true", help="enable Py3k warnings on Python 2.6+") parser.add_option('--extra-config-opt', action="append", help='set the given config opt in the test hgrc') for option, (envvar, default) in defaults.items(): defaults[option] = type(default)(os.environ.get(envvar, default)) parser.set_defaults(**defaults) (options, args) = parser.parse_args() # jython is always pure if 'java' in sys.platform or '__pypy__' in sys.modules: options.pure = True if options.with_hg: options.with_hg = os.path.expanduser(options.with_hg) if not (os.path.isfile(options.with_hg) and os.access(options.with_hg, os.X_OK)): parser.error('--with-hg must specify an executable hg script') if not os.path.basename(options.with_hg) == 'hg': sys.stderr.write('warning: --with-hg should specify an hg script\n') if options.local: testdir = os.path.dirname(os.path.realpath(sys.argv[0])) hgbin = os.path.join(os.path.dirname(testdir), 'hg') if os.name != 'nt' and not os.access(hgbin, os.X_OK): parser.error('--local specified, but %r not found or not executable' % hgbin) options.with_hg = hgbin options.anycoverage = options.cover or options.annotate or options.htmlcov if options.anycoverage: try: import coverage covver = version.StrictVersion(coverage.__version__).version if covver < (3, 3): parser.error('coverage options require coverage 3.3 or later') except ImportError: parser.error('coverage options now require the coverage package') if options.anycoverage and options.local: # this needs some path mangling somewhere, I guess parser.error("sorry, coverage options do not work when --local " "is specified") global vlog if options.verbose: if options.jobs > 1 or options.child is not None: pid = "[%d]" % os.getpid() else: pid = None def vlog(*msg): iolock.acquire() if pid: print pid, for m in msg: print m, print sys.stdout.flush() iolock.release() else: vlog = lambda *msg: None if options.tmpdir: options.tmpdir = os.path.expanduser(options.tmpdir) if options.jobs < 1: parser.error('--jobs must be positive') if options.interactive and options.jobs > 1: print '(--interactive overrides --jobs)' options.jobs = 1 if options.interactive and options.debug: parser.error("-i/--interactive and -d/--debug are incompatible") if options.debug: if options.timeout != defaults['timeout']: sys.stderr.write( 'warning: --timeout option ignored with --debug\n') options.timeout = 0 if options.py3k_warnings: if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): parser.error('--py3k-warnings can only be used on Python 2.6+') if options.blacklist: options.blacklist = parselistfiles(options.blacklist, 'blacklist') if options.whitelist: options.whitelisted = parselistfiles(options.whitelist, 'whitelist', warn=options.child is None) else: options.whitelisted = {} return (options, args) def rename(src, dst): """Like os.rename(), trade atomicity and opened files friendliness for existing destination support. """ shutil.copy(src, dst) os.remove(src) def splitnewlines(text): '''like str.splitlines, but only split on newlines. keep line endings.''' i = 0 lines = [] while True: n = text.find('\n', i) if n == -1: last = text[i:] if last: lines.append(last) return lines lines.append(text[i:n + 1]) i = n + 1 def parsehghaveoutput(lines): '''Parse hghave log lines. Return tuple of lists (missing, failed): * the missing/unknown features * the features for which existence check failed''' missing = [] failed = [] for line in lines: if line.startswith(SKIPPED_PREFIX): line = line.splitlines()[0] missing.append(line[len(SKIPPED_PREFIX):]) elif line.startswith(FAILED_PREFIX): line = line.splitlines()[0] failed.append(line[len(FAILED_PREFIX):]) return missing, failed def showdiff(expected, output, ref, err): print for line in difflib.unified_diff(expected, output, ref, err): sys.stdout.write(line) def findprogram(program): """Search PATH for a executable program""" for p in os.environ.get('PATH', os.defpath).split(os.pathsep): name = os.path.join(p, program) if os.name == 'nt' or os.access(name, os.X_OK): return name return None def checktools(): # Before we go any further, check for pre-requisite tools # stuff from coreutils (cat, rm, etc) are not tested for p in requiredtools: if os.name == 'nt': p += '.exe' found = findprogram(p) if found: vlog("# Found prerequisite", p, "at", found) else: print "WARNING: Did not find prerequisite tool: "+p def terminate(proc): """Terminate subprocess (with fallback for Python versions < 2.6)""" vlog('# Terminating process %d' % proc.pid) try: getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))() except OSError: pass def killdaemons(): # Kill off any leftover daemon processes try: fp = open(DAEMON_PIDS) for line in fp: try: pid = int(line) except ValueError: continue try: os.kill(pid, 0) vlog('# Killing daemon process %d' % pid) os.kill(pid, signal.SIGTERM) time.sleep(0.1) os.kill(pid, 0) vlog('# Daemon process %d is stuck - really killing it' % pid) os.kill(pid, signal.SIGKILL) except OSError, err: if err.errno != errno.ESRCH: raise fp.close() os.unlink(DAEMON_PIDS) except IOError: pass def cleanup(options): if not options.keep_tmpdir: vlog("# Cleaning up HGTMP", HGTMP) shutil.rmtree(HGTMP, True) def usecorrectpython(): # some tests run python interpreter. they must use same # interpreter we use or bad things will happen. exedir, exename = os.path.split(sys.executable) if exename in ('python', 'python.exe'): path = findprogram(exename) if os.path.dirname(path) == exedir: return else: exename = 'python' vlog('# Making python executable in test path use correct Python') mypython = os.path.join(BINDIR, exename) try: os.symlink(sys.executable, mypython) except AttributeError: # windows fallback shutil.copyfile(sys.executable, mypython) shutil.copymode(sys.executable, mypython) def installhg(options): vlog("# Performing temporary installation of HG") installerrs = os.path.join("tests", "install.err") pure = options.pure and "--pure" or "" # Run installer in hg root script = os.path.realpath(sys.argv[0]) hgroot = os.path.dirname(os.path.dirname(script)) os.chdir(hgroot) nohome = '--home=""' if os.name == 'nt': # The --home="" trick works only on OS where os.sep == '/' # because of a distutils convert_path() fast-path. Avoid it at # least on Windows for now, deal with .pydistutils.cfg bugs # when they happen. nohome = '' cmd = ('%s setup.py %s clean --all' ' build --build-base="%s"' ' install --force --prefix="%s" --install-lib="%s"' ' --install-scripts="%s" %s >%s 2>&1' % (sys.executable, pure, os.path.join(HGTMP, "build"), INST, PYTHONDIR, BINDIR, nohome, installerrs)) vlog("# Running", cmd) if os.system(cmd) == 0: if not options.verbose: os.remove(installerrs) else: f = open(installerrs) for line in f: print line, f.close() sys.exit(1) os.chdir(TESTDIR) usecorrectpython() vlog("# Installing dummy diffstat") f = open(os.path.join(BINDIR, 'diffstat'), 'w') f.write('#!' + sys.executable + '\n' 'import sys\n' 'files = 0\n' 'for line in sys.stdin:\n' ' if line.startswith("diff "):\n' ' files += 1\n' 'sys.stdout.write("files patched: %d\\n" % files)\n') f.close() os.chmod(os.path.join(BINDIR, 'diffstat'), 0700) if options.py3k_warnings and not options.anycoverage: vlog("# Updating hg command to enable Py3k Warnings switch") f = open(os.path.join(BINDIR, 'hg'), 'r') lines = [line.rstrip() for line in f] lines[0] += ' -3' f.close() f = open(os.path.join(BINDIR, 'hg'), 'w') for line in lines: f.write(line + '\n') f.close() hgbat = os.path.join(BINDIR, 'hg.bat') if os.path.isfile(hgbat): # hg.bat expects to be put in bin/scripts while run-tests.py # installation layout put it in bin/ directly. Fix it f = open(hgbat, 'rb') data = f.read() f.close() if '"%~dp0..\python" "%~dp0hg" %*' in data: data = data.replace('"%~dp0..\python" "%~dp0hg" %*', '"%~dp0python" "%~dp0hg" %*') f = open(hgbat, 'wb') f.write(data) f.close() else: print 'WARNING: cannot fix hg.bat reference to python.exe' if options.anycoverage: custom = os.path.join(TESTDIR, 'sitecustomize.py') target = os.path.join(PYTHONDIR, 'sitecustomize.py') vlog('# Installing coverage trigger to %s' % target) shutil.copyfile(custom, target) rc = os.path.join(TESTDIR, '.coveragerc') vlog('# Installing coverage rc to %s' % rc) os.environ['COVERAGE_PROCESS_START'] = rc fn = os.path.join(INST, '..', '.coverage') os.environ['COVERAGE_FILE'] = fn def outputcoverage(options): vlog('# Producing coverage report') os.chdir(PYTHONDIR) def covrun(*args): cmd = 'coverage %s' % ' '.join(args) vlog('# Running: %s' % cmd) os.system(cmd) if options.child: return covrun('-c') omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR]) covrun('-i', '-r', '"--omit=%s"' % omit) # report if options.htmlcov: htmldir = os.path.join(TESTDIR, 'htmlcov') covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit) if options.annotate: adir = os.path.join(TESTDIR, 'annotated') if not os.path.isdir(adir): os.mkdir(adir) covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit) def pytest(test, wd, options, replacements): py3kswitch = options.py3k_warnings and ' -3' or '' cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test) vlog("# Running", cmd) return run(cmd, wd, options, replacements) def shtest(test, wd, options, replacements): cmd = '%s "%s"' % (options.shell, test) vlog("# Running", cmd) return run(cmd, wd, options, replacements) needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256)) escapemap.update({'\\': '\\\\', '\r': r'\r'}) def escapef(m): return escapemap[m.group(0)] def stringescape(s): return escapesub(escapef, s) def rematch(el, l): try: # ensure that the regex matches to the end of the string return re.match(el + r'\Z', l) except re.error: # el is an invalid regex return False def globmatch(el, l): # The only supported special characters are * and ? plus / which also # matches \ on windows. Escaping of these caracters is supported. i, n = 0, len(el) res = '' while i < n: c = el[i] i += 1 if c == '\\' and el[i] in '*?\\/': res += el[i - 1:i + 1] i += 1 elif c == '*': res += '.*' elif c == '?': res += '.' elif c == '/' and os.name == 'nt': res += '[/\\\\]' else: res += re.escape(c) return rematch(res, l) def linematch(el, l): if el == l: # perfect match (fast) return True if (el and (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', l) or el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', l) or el.endswith(" (esc)\n") and (el[:-7].decode('string-escape') + '\n' == l or el[:-7].decode('string-escape').replace('\r', '') + '\n' == l and os.name == 'nt'))): return True return False def tsttest(test, wd, options, replacements): # We generate a shell script which outputs unique markers to line # up script results with our source. These markers include input # line number and the last return code salt = "SALT" + str(time.time()) def addsalt(line, inpython): if inpython: script.append('%s %d 0\n' % (salt, line)) else: script.append('echo %s %s $?\n' % (salt, line)) # After we run the shell script, we re-unify the script output # with non-active parts of the source, with synchronization by our # SALT line number markers. The after table contains the # non-active components, ordered by line number after = {} pos = prepos = -1 # Expected shellscript output expected = {} # We keep track of whether or not we're in a Python block so we # can generate the surrounding doctest magic inpython = False # True or False when in a true or false conditional section skipping = None def hghave(reqs): # TODO: do something smarter when all other uses of hghave is gone tdir = TESTDIR.replace('\\', '/') proc = Popen4('%s -c "%s/hghave %s"' % (options.shell, tdir, ' '.join(reqs)), wd, 0) proc.communicate() ret = proc.wait() if wifexited(ret): ret = os.WEXITSTATUS(ret) return ret == 0 f = open(test) t = f.readlines() f.close() script = [] if options.debug: script.append('set -x\n') if os.getenv('MSYSTEM'): script.append('alias pwd="pwd -W"\n') for n, l in enumerate(t): if not l.endswith('\n'): l += '\n' if l.startswith('#if'): if skipping is not None: after.setdefault(pos, []).append(' !!! nested #if\n') skipping = not hghave(l.split()[1:]) after.setdefault(pos, []).append(l) elif l.startswith('#else'): if skipping is None: after.setdefault(pos, []).append(' !!! missing #if\n') skipping = not skipping after.setdefault(pos, []).append(l) elif l.startswith('#endif'): if skipping is None: after.setdefault(pos, []).append(' !!! missing #if\n') skipping = None after.setdefault(pos, []).append(l) elif skipping: after.setdefault(pos, []).append(l) elif l.startswith(' >>> '): # python inlines after.setdefault(pos, []).append(l) prepos = pos pos = n if not inpython: # we've just entered a Python block, add the header inpython = True addsalt(prepos, False) # make sure we report the exit code script.append('%s -m heredoctest < '): # continuations after.setdefault(prepos, []).append(l) script.append(l[4:]) elif l.startswith(' '): # results # queue up a list of expected results expected.setdefault(pos, []).append(l[2:]) else: if inpython: script.append("EOF\n") inpython = False # non-command/result - queue up for merged output after.setdefault(pos, []).append(l) if inpython: script.append("EOF\n") if skipping is not None: after.setdefault(pos, []).append(' !!! missing #endif\n') addsalt(n + 1, False) # Write out the script and execute it fd, name = tempfile.mkstemp(suffix='hg-tst') try: for l in script: os.write(fd, l) os.close(fd) cmd = '%s "%s"' % (options.shell, name) vlog("# Running", cmd) exitcode, output = run(cmd, wd, options, replacements) # do not merge output if skipped, return hghave message instead # similarly, with --debug, output is None if exitcode == SKIPPED_STATUS or output is None: return exitcode, output finally: os.remove(name) # Merge the script output back into a unified test pos = -1 postout = [] ret = 0 for n, l in enumerate(output): lout, lcmd = l, None if salt in l: lout, lcmd = l.split(salt, 1) if lout: if lcmd: # output block had no trailing newline, clean up lout += ' (no-eol)\n' # find the expected output at the current position el = None if pos in expected and expected[pos]: el = expected[pos].pop(0) if linematch(el, lout): postout.append(" " + el) else: if needescape(lout): lout = stringescape(lout.rstrip('\n')) + " (esc)\n" postout.append(" " + lout) # let diff deal with it if lcmd: # add on last return code ret = int(lcmd.split()[1]) if ret != 0: postout.append(" [%s]\n" % ret) if pos in after: # merge in non-active test bits postout += after.pop(pos) pos = int(lcmd.split()[0]) if pos in after: postout += after.pop(pos) return exitcode, postout wifexited = getattr(os, "WIFEXITED", lambda x: False) def run(cmd, wd, options, replacements): """Run command in a sub-process, capturing the output (stdout and stderr). Return a tuple (exitcode, output). output is None in debug mode.""" # TODO: Use subprocess.Popen if we're running on Python 2.4 if options.debug: proc = subprocess.Popen(cmd, shell=True, cwd=wd) ret = proc.wait() return (ret, None) proc = Popen4(cmd, wd, options.timeout) def cleanup(): terminate(proc) ret = proc.wait() if ret == 0: ret = signal.SIGTERM << 8 killdaemons() return ret output = '' proc.tochild.close() try: output = proc.fromchild.read() except KeyboardInterrupt: vlog('# Handling keyboard interrupt') cleanup() raise ret = proc.wait() if wifexited(ret): ret = os.WEXITSTATUS(ret) if proc.timeout: ret = 'timeout' if ret: killdaemons() for s, r in replacements: output = re.sub(s, r, output) return ret, splitnewlines(output) def runone(options, test): '''tristate output: None -> skipped True -> passed False -> failed''' global results, resultslock, iolock testpath = os.path.join(TESTDIR, test) def result(l, e): resultslock.acquire() results[l].append(e) resultslock.release() def skip(msg): if not options.verbose: result('s', (test, msg)) else: iolock.acquire() print "\nSkipping %s: %s" % (testpath, msg) iolock.release() return None def fail(msg, ret): if not options.nodiff: iolock.acquire() print "\nERROR: %s %s" % (testpath, msg) iolock.release() if (not ret and options.interactive and os.path.exists(testpath + ".err")): iolock.acquire() print "Accept this change? [n] ", answer = sys.stdin.readline().strip() iolock.release() if answer.lower() in "y yes".split(): if test.endswith(".t"): rename(testpath + ".err", testpath) else: rename(testpath + ".err", testpath + ".out") result('p', test) return result('f', (test, msg)) def success(): result('p', test) def ignore(msg): result('i', (test, msg)) if (os.path.basename(test).startswith("test-") and '~' not in test and ('.' not in test or test.endswith('.py') or test.endswith('.bat') or test.endswith('.t'))): if not os.path.exists(test): skip("doesn't exist") return None else: vlog('# Test file', test, 'not supported, ignoring') return None # not a supported test, don't record if not (options.whitelisted and test in options.whitelisted): if options.blacklist and test in options.blacklist: skip("blacklisted") return None if options.retest and not os.path.exists(test + ".err"): ignore("not retesting") return None if options.keywords: fp = open(test) t = fp.read().lower() + test.lower() fp.close() for k in options.keywords.lower().split(): if k in t: break else: ignore("doesn't match keyword") return None vlog("# Test", test) # create a fresh hgrc hgrc = open(HGRCPATH, 'w+') hgrc.write('[ui]\n') hgrc.write('slash = True\n') hgrc.write('[defaults]\n') hgrc.write('backout = -d "0 0"\n') hgrc.write('commit = -d "0 0"\n') hgrc.write('tag = -d "0 0"\n') if options.inotify: hgrc.write('[extensions]\n') hgrc.write('inotify=\n') hgrc.write('[inotify]\n') hgrc.write('pidfile=%s\n' % DAEMON_PIDS) hgrc.write('appendpid=True\n') if options.extra_config_opt: for opt in options.extra_config_opt: section, key = opt.split('.', 1) assert '=' in key, ('extra config opt %s must ' 'have an = for assignment' % opt) hgrc.write('[%s]\n%s\n' % (section, key)) hgrc.close() ref = os.path.join(TESTDIR, test+".out") err = os.path.join(TESTDIR, test+".err") if os.path.exists(err): os.remove(err) # Remove any previous output files try: tf = open(testpath) firstline = tf.readline().rstrip() tf.close() except IOError: firstline = '' lctest = test.lower() if lctest.endswith('.py') or firstline == '#!/usr/bin/env python': runner = pytest elif lctest.endswith('.t'): runner = tsttest ref = testpath else: # do not try to run non-executable programs if not os.access(testpath, os.X_OK): return skip("not executable") runner = shtest # Make a tmp subdirectory to work in testtmp = os.environ["TESTTMP"] = os.environ["HOME"] = \ os.path.join(HGTMP, os.path.basename(test)) replacements = [ (r':%s\b' % options.port, ':$HGPORT'), (r':%s\b' % (options.port + 1), ':$HGPORT1'), (r':%s\b' % (options.port + 2), ':$HGPORT2'), ] if os.name == 'nt': replacements.append((r'\r\n', '\n')) replacements.append( (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c for c in testtmp), '$TESTTMP')) else: replacements.append((re.escape(testtmp), '$TESTTMP')) os.mkdir(testtmp) ret, out = runner(testpath, testtmp, options, replacements) vlog("# Ret was:", ret) mark = '.' skipped = (ret == SKIPPED_STATUS) # If we're not in --debug mode and reference output file exists, # check test output against it. if options.debug: refout = None # to match "out is None" elif os.path.exists(ref): f = open(ref, "r") refout = list(splitnewlines(f.read())) f.close() else: refout = [] if (ret != 0 or out != refout) and not skipped and not options.debug: # Save errors to a file for diagnosis f = open(err, "wb") for line in out: f.write(line) f.close() if skipped: mark = 's' if out is None: # debug mode: nothing to parse missing = ['unknown'] failed = None else: missing, failed = parsehghaveoutput(out) if not missing: missing = ['irrelevant'] if failed: fail("hghave failed checking for %s" % failed[-1], ret) skipped = False else: skip(missing[-1]) elif ret == 'timeout': mark = 't' fail("timed out", ret) elif out != refout: mark = '!' if not options.nodiff: iolock.acquire() if options.view: os.system("%s %s %s" % (options.view, ref, err)) else: showdiff(refout, out, ref, err) iolock.release() if ret: fail("output changed and returned error code %d" % ret, ret) else: fail("output changed", ret) ret = 1 elif ret: mark = '!' fail("returned error code %d" % ret, ret) else: success() if not options.verbose: iolock.acquire() sys.stdout.write(mark) sys.stdout.flush() iolock.release() killdaemons() if not options.keep_tmpdir: shutil.rmtree(testtmp, True) if skipped: return None return ret == 0 _hgpath = None def _gethgpath(): """Return the path to the mercurial package that is actually found by the current Python interpreter.""" global _hgpath if _hgpath is not None: return _hgpath cmd = '%s -c "import mercurial; print mercurial.__path__[0]"' pipe = os.popen(cmd % PYTHON) try: _hgpath = pipe.read().strip() finally: pipe.close() return _hgpath def _checkhglib(verb): """Ensure that the 'mercurial' package imported by python is the one we expect it to be. If not, print a warning to stderr.""" expecthg = os.path.join(PYTHONDIR, 'mercurial') actualhg = _gethgpath() if os.path.abspath(actualhg) != os.path.abspath(expecthg): sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n' ' (expected %s)\n' % (verb, actualhg, expecthg)) def runchildren(options, tests): if INST: installhg(options) _checkhglib("Testing") optcopy = dict(options.__dict__) optcopy['jobs'] = 1 # Because whitelist has to override keyword matches, we have to # actually load the whitelist in the children as well, so we allow # the list of whitelist files to pass through and be parsed in the # children, but not the dict of whitelisted tests resulting from # the parse, used here to override blacklisted tests. whitelist = optcopy['whitelisted'] or [] del optcopy['whitelisted'] blacklist = optcopy['blacklist'] or [] del optcopy['blacklist'] blacklisted = [] if optcopy['with_hg'] is None: optcopy['with_hg'] = os.path.join(BINDIR, "hg") optcopy.pop('anycoverage', None) opts = [] for opt, value in optcopy.iteritems(): name = '--' + opt.replace('_', '-') if value is True: opts.append(name) elif isinstance(value, list): for v in value: opts.append(name + '=' + str(v)) elif value is not None: opts.append(name + '=' + str(value)) tests.reverse() jobs = [[] for j in xrange(options.jobs)] while tests: for job in jobs: if not tests: break test = tests.pop() if test not in whitelist and test in blacklist: blacklisted.append(test) else: job.append(test) fps = {} for j, job in enumerate(jobs): if not job: continue rfd, wfd = os.pipe() childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)] childtmp = os.path.join(HGTMP, 'child%d' % j) childopts += ['--tmpdir', childtmp] cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job vlog(' '.join(cmdline)) fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r') os.close(wfd) signal.signal(signal.SIGINT, signal.SIG_IGN) failures = 0 tested, skipped, failed = 0, 0, 0 skips = [] fails = [] while fps: pid, status = os.wait() fp = fps.pop(pid) l = fp.read().splitlines() try: test, skip, fail = map(int, l[:3]) except ValueError: test, skip, fail = 0, 0, 0 split = -fail or len(l) for s in l[3:split]: skips.append(s.split(" ", 1)) for s in l[split:]: fails.append(s.split(" ", 1)) tested += test skipped += skip failed += fail vlog('pid %d exited, status %d' % (pid, status)) failures |= status print skipped += len(blacklisted) if not options.noskips: for s in skips: print "Skipped %s: %s" % (s[0], s[1]) for s in blacklisted: print "Skipped %s: blacklisted" % s for s in fails: print "Failed %s: %s" % (s[0], s[1]) _checkhglib("Tested") print "# Ran %d tests, %d skipped, %d failed." % ( tested, skipped, failed) if options.anycoverage: outputcoverage(options) sys.exit(failures != 0) results = dict(p=[], f=[], s=[], i=[]) resultslock = threading.Lock() iolock = threading.Lock() def runqueue(options, tests, results): for test in tests: ret = runone(options, test) if options.first and ret is not None and not ret: break def runtests(options, tests): global DAEMON_PIDS, HGRCPATH DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids') HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc') try: if INST: installhg(options) _checkhglib("Testing") if options.restart: orig = list(tests) while tests: if os.path.exists(tests[0] + ".err"): break tests.pop(0) if not tests: print "running all tests" tests = orig runqueue(options, tests, results) failed = len(results['f']) tested = len(results['p']) + failed skipped = len(results['s']) ignored = len(results['i']) if options.child: fp = os.fdopen(options.child, 'w') fp.write('%d\n%d\n%d\n' % (tested, skipped, failed)) for s in results['s']: fp.write("%s %s\n" % s) for s in results['f']: fp.write("%s %s\n" % s) fp.close() else: print for s in results['s']: print "Skipped %s: %s" % s for s in results['f']: print "Failed %s: %s" % s _checkhglib("Tested") print "# Ran %d tests, %d skipped, %d failed." % ( tested, skipped + ignored, failed) if options.anycoverage: outputcoverage(options) except KeyboardInterrupt: failed = True print "\ninterrupted!" if failed: sys.exit(1) def main(): (options, args) = parseargs() if not options.child: os.umask(022) checktools() if len(args) == 0: args = os.listdir(".") args.sort() tests = args # Reset some environment variables to well-known values so that # the tests produce repeatable output. os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C' os.environ['TZ'] = 'GMT' os.environ["EMAIL"] = "Foo Bar " os.environ['CDPATH'] = '' os.environ['COLUMNS'] = '80' os.environ['GREP_OPTIONS'] = '' os.environ['http_proxy'] = '' os.environ['no_proxy'] = '' os.environ['NO_PROXY'] = '' os.environ['TERM'] = 'xterm' # unset env related to hooks for k in os.environ.keys(): if k.startswith('HG_'): # can't remove on solaris os.environ[k] = '' del os.environ[k] global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE TESTDIR = os.environ["TESTDIR"] = os.getcwd() if options.tmpdir: options.keep_tmpdir = True tmpdir = options.tmpdir if os.path.exists(tmpdir): # Meaning of tmpdir has changed since 1.3: we used to create # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if # tmpdir already exists. sys.exit("error: temp dir %r already exists" % tmpdir) # Automatically removing tmpdir sounds convenient, but could # really annoy anyone in the habit of using "--tmpdir=/tmp" # or "--tmpdir=$HOME". #vlog("# Removing temp dir", tmpdir) #shutil.rmtree(tmpdir) os.makedirs(tmpdir) else: d = None if os.name == 'nt': # without this, we get the default temp dir location, but # in all lowercase, which causes troubles with paths (issue3490) d = os.getenv('TMP') tmpdir = tempfile.mkdtemp('', 'hgtests.', d) HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir) DAEMON_PIDS = None HGRCPATH = None os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' os.environ["HGMERGE"] = "internal:merge" os.environ["HGUSER"] = "test" os.environ["HGENCODING"] = "ascii" os.environ["HGENCODINGMODE"] = "strict" os.environ["HGPORT"] = str(options.port) os.environ["HGPORT1"] = str(options.port + 1) os.environ["HGPORT2"] = str(options.port + 2) if options.with_hg: INST = None BINDIR = os.path.dirname(os.path.realpath(options.with_hg)) # This looks redundant with how Python initializes sys.path from # the location of the script being executed. Needed because the # "hg" specified by --with-hg is not the only Python script # executed in the test suite that needs to import 'mercurial' # ... which means it's not really redundant at all. PYTHONDIR = BINDIR else: INST = os.path.join(HGTMP, "install") BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin") PYTHONDIR = os.path.join(INST, "lib", "python") os.environ["BINDIR"] = BINDIR os.environ["PYTHON"] = PYTHON if not options.child: path = [BINDIR] + os.environ["PATH"].split(os.pathsep) os.environ["PATH"] = os.pathsep.join(path) # Include TESTDIR in PYTHONPATH so that out-of-tree extensions # can run .../tests/run-tests.py test-foo where test-foo # adds an extension to HGRC pypath = [PYTHONDIR, TESTDIR] # We have to augment PYTHONPATH, rather than simply replacing # it, in case external libraries are only available via current # PYTHONPATH. (In particular, the Subversion bindings on OS X # are in /opt/subversion.) oldpypath = os.environ.get(IMPL_PATH) if oldpypath: pypath.append(oldpypath) os.environ[IMPL_PATH] = os.pathsep.join(pypath) COVERAGE_FILE = os.path.join(TESTDIR, ".coverage") vlog("# Using TESTDIR", TESTDIR) vlog("# Using HGTMP", HGTMP) vlog("# Using PATH", os.environ["PATH"]) vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH]) try: if len(tests) > 1 and options.jobs > 1: runchildren(options, tests) else: runtests(options, tests) finally: time.sleep(.1) cleanup(options) if __name__ == '__main__': main() durin42-hg-git-a3c3b8077cbe/tests/test-bookmark-workflow.t0000644000000000000000000001214612104602314021517 0ustar 00000000000000This test demonstrates how Hg works with remote Hg bookmarks compared with remote branches via Hg-Git. Ideally, they would behave identically. In practice, some differences are unavoidable, but we should try to minimize them. This test should not bother testing the behavior of bookmark creation, deletion, activation, deactivation, etc. These behaviors, while important to the end user, don't vary at all when Hg-Git is in use. Only the synchonization of bookmarks should be considered "under test", and mutation of bookmarks locally is only to provide a test fixture. Load commonly used test logic $ . "$TESTDIR/testutil" Skip if Mercurial < 2.1; workflow was different before that $ python -c 'from mercurial import util ; assert \ > util.version() != "unknown" and util.version() >= "2.1"' || exit 80 $ gitcount=10 $ gitcommit() > { > GIT_AUTHOR_DATE="2007-01-01 00:00:$gitcount +0000" > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" > git commit "$@" >/dev/null 2>/dev/null || echo "git commit error" > gitcount=`expr $gitcount + 1` > } $ hgcount=10 $ hgcommit() > { > HGDATE="2007-01-01 00:00:$hgcount +0000" > hg commit -u "test " -d "$HGDATE" "$@" >/dev/null 2>/dev/null || echo "hg commit error" > hgcount=`expr $hgcount + 1` > } $ gitstate() > { > git log --format=" %h \"%s\" refs:%d" $@ | sed 's/HEAD, //' > } $ hgstate() > { > hg log --template " {rev} {node|short} \"{desc}\" bookmarks: [{bookmarks}]\n" $@ > } $ hggitstate() > { > hg log --template " {rev} {node|short} {gitnode|short} \"{desc}\" bookmarks: [{bookmarks}]\n" $@ > } Initialize remote hg and git repos with equivalent initial contents $ hg init hgremoterepo $ cd hgremoterepo $ hg bookmark master $ for f in alpha beta gamma delta; do > echo $f > $f; hg add $f; hgcommit -m "add $f" > done $ hg bookmark -r 1 b1 $ hgstate 3 fc2664cac217 "add delta" bookmarks: [master] 2 d85ced7ae9d6 "add gamma" bookmarks: [] 1 7bcd915dc873 "add beta" bookmarks: [b1] 0 3442585be8a6 "add alpha" bookmarks: [] $ cd .. $ git init -q gitremoterepo $ cd gitremoterepo $ for f in alpha beta gamma delta; do > echo $f > $f; git add $f; gitcommit -m "add $f" > done $ git branch b1 9497a4e $ gitstate 55b133e "add delta" refs: (master) d338971 "add gamma" refs: 9497a4e "add beta" refs: (b1) 7eeab2e "add alpha" refs: $ cd .. Cloning transfers all bookmarks from remote to local $ hg clone -q hgremoterepo purehglocalrepo $ cd purehglocalrepo $ hgstate 3 fc2664cac217 "add delta" bookmarks: [master] 2 d85ced7ae9d6 "add gamma" bookmarks: [] 1 7bcd915dc873 "add beta" bookmarks: [b1] 0 3442585be8a6 "add alpha" bookmarks: [] $ cd .. $ hg clone -q gitremoterepo hggitlocalrepo $ cd hggitlocalrepo $ hggitstate 3 fc2664cac217 55b133e1d558 "add delta" bookmarks: [master] 2 d85ced7ae9d6 d338971a96e2 "add gamma" bookmarks: [] 1 7bcd915dc873 9497a4ee62e1 "add beta" bookmarks: [b1] 0 3442585be8a6 7eeab2ea75ec "add alpha" bookmarks: [] $ cd .. No changes $ cd purehglocalrepo $ hg outgoing comparing with $TESTTMP/hgremoterepo searching for changes no changes found [1] $ hg outgoing -B comparing with $TESTTMP/hgremoterepo searching for changed bookmarks no changed bookmarks found [1] $ hg push pushing to $TESTTMP/hgremoterepo searching for changes no changes found [1] $ cd .. $ cd hggitlocalrepo $ hg outgoing comparing with $TESTTMP/gitremoterepo searching for changes no changes found [1] $ hg outgoing -B comparing with $TESTTMP/gitremoterepo searching for changed bookmarks no changed bookmarks found [1] $ hg push pushing to $TESTTMP/gitremoterepo searching for changes no changes found [1] $ cd .. Changed bookmarks, but not revs $ cd purehglocalrepo $ hg bookmark -fr 2 b1 $ hg bookmark -r 0 b2 $ hgstate 3 fc2664cac217 "add delta" bookmarks: [master] 2 d85ced7ae9d6 "add gamma" bookmarks: [b1] 1 7bcd915dc873 "add beta" bookmarks: [] 0 3442585be8a6 "add alpha" bookmarks: [b2] $ hg outgoing comparing with $TESTTMP/hgremoterepo searching for changes no changes found [1] As of 2.3, Mercurial's outgoing -B doesn't actually show changed bookmarks It only shows "new" bookmarks. Thus, b1 doesn't show up. $ hg outgoing -B comparing with $TESTTMP/hgremoterepo searching for changed bookmarks b2 3442585be8a6 $ cd .. $ cd hggitlocalrepo $ hg bookmark -fr 2 b1 $ hg bookmark -r 0 b2 $ hgstate 3 fc2664cac217 "add delta" bookmarks: [master] 2 d85ced7ae9d6 "add gamma" bookmarks: [b1] 1 7bcd915dc873 "add beta" bookmarks: [] 0 3442585be8a6 "add alpha" bookmarks: [b2] $ hg outgoing comparing with $TESTTMP/gitremoterepo searching for changes no changes found [1] As of 2.3, Mercurial's outgoing -B doesn't actually show changed bookmarks It only shows "new" bookmarks. Thus, b1 doesn't show up. $ hg outgoing -B comparing with $TESTTMP/gitremoterepo searching for changed bookmarks b2 3442585be8a6 $ cd .. durin42-hg-git-a3c3b8077cbe/tests/test-clone.t0000755000000000000000000000300412104602314017136 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git tag alpha $ git checkout -b beta 2>&1 | sed s/\'/\"/g Switched to a new branch "beta" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. clone a tag $ hg clone -r alpha gitrepo hgrepo-a | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-a log --graph | egrep -v ': *(beta|master)' @ changeset: 0:3442585be8a6 tag: alpha tag: default/master tag: tip user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha clone a branch $ hg clone -r beta gitrepo hgrepo-b | grep -v '^updating' importing git objects into hg 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-b log --graph | egrep -v ': *(beta|master)' @ changeset: 1:7bcd915dc873 | tag: default/beta | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:3442585be8a6 tag: alpha tag: default/master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha durin42-hg-git-a3c3b8077cbe/tests/test-conflict-1.t0000644000000000000000000000366112104602314020003 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo1 $ cd hgrepo1 $ echo A > afile $ hg add afile $ hg ci -m "origin" $ echo B > afile $ hg ci -m "A->B" $ hg up -r0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo C > afile $ hg ci -m "A->C" created new head $ hg merge -r1 2>&1 | sed 's/-C ./-C/' | egrep -v '^merging afile$' | sed 's/incomplete.*/failed!/' warning: conflicts during merge. merging afile failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges or 'hg update -C' to abandon resolve using first parent $ echo C > afile $ hg resolve -m afile $ hg ci -m "merge to C" $ hg log --graph --style compact | sed 's/\[.*\]//g' @ 3:2,1 6c53bc0f062f 1970-01-01 00:00 +0000 test |\ merge to C | | | o 2:0 ea82b67264a1 1970-01-01 00:00 +0000 test | | A->C | | o | 1 7205e83b5a3f 1970-01-01 00:00 +0000 test |/ A->B | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin $ cd .. $ git init --bare gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/ $ cd hgrepo1 $ hg bookmark -r tip master $ hg push -r master ../gitrepo pushing to ../gitrepo searching for changes adding objects added 4 commits with 3 trees and 3 blobs $ cd .. $ hg clone gitrepo hgrepo2 | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved expect the same revision ids as above $ hg -R hgrepo2 log --graph --style compact | sed 's/\[.*\]//g' @ 3:1,2 6c53bc0f062f 1970-01-01 00:00 +0000 test |\ merge to C | | | o 2:0 7205e83b5a3f 1970-01-01 00:00 +0000 test | | A->B | | o | 1 ea82b67264a1 1970-01-01 00:00 +0000 test |/ A->C | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin durin42-hg-git-a3c3b8077cbe/tests/test-conflict-2.t0000644000000000000000000000366212104602314020005 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo1 $ cd hgrepo1 $ echo A > afile $ hg add afile $ hg ci -m "origin" $ echo B > afile $ hg ci -m "A->B" $ hg up -r0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo C > afile $ hg ci -m "A->C" created new head $ hg merge -r1 2>&1 | sed 's/-C ./-C/' | egrep -v '^merging afile$' | sed 's/incomplete.*/failed!/' warning: conflicts during merge. merging afile failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges or 'hg update -C' to abandon resolve using second parent $ echo B > afile $ hg resolve -m afile $ hg ci -m "merge to B" $ hg log --graph --style compact | sed 's/\[.*\]//g' @ 3:2,1 120385945d08 1970-01-01 00:00 +0000 test |\ merge to B | | | o 2:0 ea82b67264a1 1970-01-01 00:00 +0000 test | | A->C | | o | 1 7205e83b5a3f 1970-01-01 00:00 +0000 test |/ A->B | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin $ cd .. $ git init --bare gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/ $ cd hgrepo1 $ hg bookmark -r tip master $ hg push -r master ../gitrepo pushing to ../gitrepo searching for changes adding objects added 4 commits with 3 trees and 3 blobs $ cd .. $ hg clone gitrepo hgrepo2 | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved expect the same revision ids as above $ hg -R hgrepo2 log --graph --style compact | sed 's/\[.*\]//g' @ 3:1,2 120385945d08 1970-01-01 00:00 +0000 test |\ merge to B | | | o 2:0 7205e83b5a3f 1970-01-01 00:00 +0000 test | | A->B | | o | 1 ea82b67264a1 1970-01-01 00:00 +0000 test |/ A->C | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin durin42-hg-git-a3c3b8077cbe/tests/test-convergedmerge.t0000644000000000000000000000354312104602314021037 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo1 $ cd hgrepo1 $ echo A > afile $ hg add afile $ hg ci -m "origin" $ echo B > afile $ hg ci -m "A->B" $ echo C > afile $ hg ci -m "B->C" $ hg up -r0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo C > afile $ hg ci -m "A->C" created new head $ hg merge -r2 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ hg ci -m "merge" $ hg log --graph --style compact | sed 's/\[.*\]//g' @ 4:3,2 eaa21d002113 1970-01-01 00:00 +0000 test |\ merge | | | o 3:0 ea82b67264a1 1970-01-01 00:00 +0000 test | | A->C | | o | 2 0dbe4ac1a758 1970-01-01 00:00 +0000 test | | B->C | | o | 1 7205e83b5a3f 1970-01-01 00:00 +0000 test |/ A->B | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin $ cd .. $ git init --bare gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/ $ cd hgrepo1 $ hg bookmark -r4 master $ hg push -r master ../gitrepo pushing to ../gitrepo searching for changes adding objects added 5 commits with 3 trees and 3 blobs $ cd .. $ hg clone gitrepo hgrepo2 | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved expect the same revision ids as above $ hg -R hgrepo2 log --graph --style compact | sed 's/\[.*\]//g' @ 4:1,3 eaa21d002113 1970-01-01 00:00 +0000 test |\ merge | | | o 3 0dbe4ac1a758 1970-01-01 00:00 +0000 test | | B->C | | | o 2:0 7205e83b5a3f 1970-01-01 00:00 +0000 test | | A->B | | o | 1 ea82b67264a1 1970-01-01 00:00 +0000 test |/ A->C | o 0 5d1a6b64f9d0 1970-01-01 00:00 +0000 test origin durin42-hg-git-a3c3b8077cbe/tests/test-empty-working-tree.t0000644000000000000000000000165712104602314021620 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ git commit --allow-empty -m empty >/dev/null 2>/dev/null || echo "git commit error" $ cd .. $ git init --bare gitrepo2 Initialized empty Git repository in $TESTTMP/gitrepo2/ $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log -r tip --template 'files: {files}\n' files: $ hg gclear clearing out the git cache data $ hg push ../gitrepo2 pushing to ../gitrepo2 searching for changes adding objects added 1 commits with 1 trees and 0 blobs $ cd .. $ git --git-dir=gitrepo2 log --pretty=medium commit 678256865a8c85ae925bf834369264193c88f8de Author: test Date: Mon Jan 1 00:00:00 2007 +0000 empty durin42-hg-git-a3c3b8077cbe/tests/test-encoding.t0000644000000000000000000001125212104602314017625 0ustar 00000000000000# -*- coding: utf-8 -*- Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo utf-8 encoded commit message $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add älphà' Create some commits using latin1 encoding The warning message changed in Git 1.8.0 $ . $TESTDIR/latin-1-encoding Warning: commit message (did|does) not conform to UTF-8. (re) You may want to amend it after fixing the message, or set the config variable i18n.commitencoding to the encoding your project uses. Warning: commit message (did|does) not conform to UTF-8. (re) You may want to amend it after fixing the message, or set the config variable i18n.commitencoding to the encoding your project uses. $ cd .. $ git init --bare gitrepo2 Initialized empty Git repository in $TESTTMP/gitrepo2/ $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 4 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo Latin1 commit messages started being automatically converted to UTF-8 in Git 1.8.0, so we accept the output of either version. $ HGENCODING=utf-8 hg log --graph --debug | grep -v ': *master' | grep -v 'phase:' | grep -v ': *author=' | grep -v ': *message=' @ changeset: 3:(8549ee7fe0801b2dafc06047ca6f66d36da709f5|c3d3e39fc04f7e2e8cdb95f090415ec1ddc1be70) (re) | tag: default/master | tag: tip | parent: 2:(0422fbb4ec39fb69e87b94a3874ac890333de11a|f8aa41895a3a771a72520ca205a4685b76649fdd) (re) | parent: -1:0000000000000000000000000000000000000000 | manifest: 3:ea49f93388380ead5601c8fcbfa187516e7c2ed8 | user: tést èncödîng | date: Mon Jan 01 00:00:13 2007 +0000 | files+: delta | extra: branch=default | extra: committer=test 1167609613 0 | extra: encoding=latin-1 | description: | add déltà | | o changeset: 2:(0422fbb4ec39fb69e87b94a3874ac890333de11a|f8aa41895a3a771a72520ca205a4685b76649fdd) (re) | parent: 1:(9f6268bfc9eb3956c5ab8752d7b983b0ffe57115|955b24cf6f8f293741d3f39110c6fe554c292533) (re) | parent: -1:0000000000000000000000000000000000000000 | manifest: 2:f580e7da3673c137370da2b931a1dee83590d7b4 | user: tést èncödîng | date: Mon Jan 01 00:00:12 2007 +0000 | files+: gamma | extra: branch=default | extra: committer=test 1167609612 0 | description: | add gämmâ | | o changeset: 1:(9f6268bfc9eb3956c5ab8752d7b983b0ffe57115|955b24cf6f8f293741d3f39110c6fe554c292533) (re) | parent: 0:bb7d36568d6188ce0de2392246c43f6f213df954 | parent: -1:0000000000000000000000000000000000000000 | manifest: 1:f0bd6fbafbaebe4bb59c35108428f6fce152431d | user: tést èncödîng | date: Mon Jan 01 00:00:11 2007 +0000 | files+: beta | extra: branch=default | extra: committer=test 1167609611 0 | description: | add beta | | o changeset: 0:bb7d36568d6188ce0de2392246c43f6f213df954 parent: -1:0000000000000000000000000000000000000000 parent: -1:0000000000000000000000000000000000000000 manifest: 0:8b8a0e87dfd7a0706c0524afa8ba67e20544cbf0 user: test date: Mon Jan 01 00:00:10 2007 +0000 files+: alpha extra: branch=default description: add älphà $ hg gclear clearing out the git cache data $ hg push ../gitrepo2 pushing to ../gitrepo2 searching for changes adding objects added 4 commits with 4 trees and 4 blobs $ cd .. Latin1 commit messages started being automatically converted to UTF-8 in Git 1.8.0, so we accept the output of either version. $ git --git-dir=gitrepo2 log --pretty=medium commit (da0edb01d4f3d1abf08b1be298379b0b2960e680|51c509c1c7eeb8f0a5b20aa3e894e8823f39171f) (re) Author: t\xe9st \xe8nc\xf6d\xeeng (esc) Date: Mon Jan 1 00:00:13 2007 +0000 add d\xe9lt\xe0 (esc) commit (2372b6c8f1b91f2db8ae5eb0f9e0427c318b449c|bd576458238cbda49ffcfbafef5242e103f1bc24) (re) Author: * (glob) Date: Mon Jan 1 00:00:12 2007 +0000 add g*mm* (glob) commit (9ef7f6dcffe643b89ba63f3323621b9a923e4802|7a7e86fc1b24db03109c9fe5da28b352de59ce90) (re) Author: * (glob) Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 0530b75d8c203e10dc934292a6a4032c6e958a83 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add älphà durin42-hg-git-a3c3b8077cbe/tests/test-file-removal.t0000644000000000000000000000543412104602314020426 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ mkdir foo $ echo blah > foo/bar $ git add foo $ fn_git_commit -m 'add foo' $ git rm alpha rm 'alpha' $ fn_git_commit -m 'remove alpha' $ git rm foo/bar rm 'foo/bar' $ fn_git_commit -m 'remove foo/bar' final manifest in git is just beta $ git ls-files beta $ cd .. $ git init --bare gitrepo2 Initialized empty Git repository in $TESTTMP/gitrepo2/ $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log --graph | grep -v ': *master' @ changeset: 4:ea41a3f0ed10 | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:14 2007 +0000 | summary: remove foo/bar | o changeset: 3:c84537f94bcc | user: test | date: Mon Jan 01 00:00:13 2007 +0000 | summary: remove alpha | o changeset: 2:e25450e1354f | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add foo | o changeset: 1:7bcd915dc873 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:3442585be8a6 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha make sure alpha is not in this manifest $ hg manifest -r 3 beta foo/bar make sure that only beta is in the manifest $ hg manifest beta $ hg gclear clearing out the git cache data $ hg push ../gitrepo2 pushing to ../gitrepo2 searching for changes adding objects added 5 commits with 6 trees and 3 blobs $ cd .. $ git --git-dir=gitrepo2 log --pretty=medium commit b991de8952c482a7cd51162674ffff8474862218 Author: test Date: Mon Jan 1 00:00:14 2007 +0000 remove foo/bar commit b0edaf0adac19392cf2867498b983bc5192b41dd Author: test Date: Mon Jan 1 00:00:13 2007 +0000 remove alpha commit f2d0d5bfa905e12dee728b509b96cf265bb6ee43 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 add foo commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha durin42-hg-git-a3c3b8077cbe/tests/test-git-clone.t0000755000000000000000000000172312104602314017725 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo log --graph | grep -v ': *master' @ changeset: 1:7bcd915dc873 | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:3442585be8a6 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha we should have some bookmarks $ hg -R hgrepo book * master 1:7bcd915dc873 durin42-hg-git-a3c3b8077cbe/tests/test-git-submodules.t0000644000000000000000000000471412104602314021007 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo1 Initialized empty Git repository in $TESTTMP/gitrepo1/.git/ $ cd gitrepo1 $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ cd .. $ git init gitsubrepo Initialized empty Git repository in $TESTTMP/gitsubrepo/.git/ $ cd gitsubrepo $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ mkdir gitrepo2 $ cd gitrepo2 $ rmpwd="import sys; print sys.stdin.read().replace('$(dirname $(pwd))/', '')" different versions of git spell the dir differently. Older versions use the full path to the directory all the time, whereas newer version spell it sanely as it was given (eg . in a newer version, while older git will use the full normalized path for .) $ clonefilt='s/Cloning into/Initialized empty Git repository in/;s/in .*/in .../' $ git clone ../gitrepo1 . | python -c "$rmpwd" | sed "$clonefilt" | egrep -v '^done\.$' Initialized empty Git repository in ... $ git submodule add ../gitsubrepo subrepo | python -c "$rmpwd" | sed "$clonefilt" | egrep -v '^done\.$' Initialized empty Git repository in ... $ git commit -m 'add subrepo' | sed 's/, 0 deletions(-)//' [master e42b08b] add subrepo 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 subrepo $ git rm --cached subrepo rm 'subrepo' $ git rm .gitmodules rm '.gitmodules' $ git commit -m 'rm subrepo' | sed 's/, 0 deletions(-)//' | sed 's/, 0 insertions(+)//' [master 7e4c934] rm subrepo 2 files changed, 4 deletions(-) delete mode 100644 .gitmodules delete mode 160000 subrepo $ cd .. $ hg clone gitrepo2 hgrepo | grep -v '^updating' importing git objects into hg 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo log --graph | grep -v ': *master' @ changeset: 2:76fda365fbbb | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: rm subrepo | o changeset: 1:2f69b1b8a6f8 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add subrepo | o changeset: 0:3442585be8a6 user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha we should have some bookmarks $ hg -R hgrepo book * master 2:76fda365fbbb durin42-hg-git-a3c3b8077cbe/tests/test-git-tags.t0000644000000000000000000000237612104602314017565 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ git config receive.denyCurrentBranch ignore $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ fn_git_tag alpha $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ fn_git_tag -a -m 'added tag beta' beta $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log --graph | grep -v ': *master' @ changeset: 1:99dcc15b7b07 | tag: beta | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add beta | o changeset: 0:3442585be8a6 tag: alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ echo beta-fix >> beta $ hg commit -m 'fix for beta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ cd .. durin42-hg-git-a3c3b8077cbe/tests/test-git-workflow.t0000755000000000000000000000440512104602314020477 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo $ cd hgrepo $ echo alpha > alpha $ hg add alpha $ fn_hg_commit -m "add alpha" $ hg log --graph --debug | grep -v phase: @ changeset: 0:0221c246a56712c6aa64e5ee382244d8a471b1e2 tag: tip parent: -1:0000000000000000000000000000000000000000 parent: -1:0000000000000000000000000000000000000000 manifest: 0:8b8a0e87dfd7a0706c0524afa8ba67e20544cbf0 user: test date: Mon Jan 01 00:00:10 2007 +0000 files+: alpha extra: branch=default description: add alpha $ cd .. configure for use from git $ hg clone hgrepo gitrepo updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd gitrepo $ hg book master $ hg up null 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ echo "[git]" >> .hg/hgrc $ echo "intree = True" >> .hg/hgrc $ hg gexport do some work $ git config core.bare false $ git checkout master 2>&1 | sed s/\'/\"/g Already on "master" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' get things back to hg $ hg gimport importing git objects into hg $ hg log --graph --debug | grep -v ': *master' | grep -v phase: o changeset: 1:7108ae7bd184226a29b8203619a8253d314643bf | tag: tip | parent: 0:0221c246a56712c6aa64e5ee382244d8a471b1e2 | parent: -1:0000000000000000000000000000000000000000 | manifest: 1:f0bd6fbafbaebe4bb59c35108428f6fce152431d | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | files+: beta | extra: branch=default | description: | add beta | | o changeset: 0:0221c246a56712c6aa64e5ee382244d8a471b1e2 parent: -1:0000000000000000000000000000000000000000 parent: -1:0000000000000000000000000000000000000000 manifest: 0:8b8a0e87dfd7a0706c0524afa8ba67e20544cbf0 user: test date: Mon Jan 01 00:00:10 2007 +0000 files+: alpha extra: branch=default description: add alpha gimport should have updated the bookmarks as well $ hg bookmarks master 1:7108ae7bd184 durin42-hg-git-a3c3b8077cbe/tests/test-help.t0000755000000000000000000000071412104602314016773 0ustar 00000000000000Tests that the various help files are properly registered Load commonly used test logic $ . "$TESTDIR/testutil" $ hg help | grep 'git' | sed 's/ */ /g' hggit push and pull from a Git server git Working with Git Repositories $ hg help hggit | grep 'help git' | sed 's/:hg:`help git`/"hg help git"/g' For more information and instructions, see "hg help git" $ hg help git | grep 'Working with Git Repositories' Working with Git Repositories durin42-hg-git-a3c3b8077cbe/tests/test-hg-author.t0000644000000000000000000001616012104602314017740 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -b not-master Switched to a new branch 'not-master' $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg co master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo beta > beta $ hg add beta $ fn_hg_commit -u "test" -m 'add beta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo gamma >> beta $ fn_hg_commit -u "test (comment)" -m 'modify beta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo gamma > gamma $ hg add gamma $ fn_hg_commit -u "" -m 'add gamma' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo delta > delta $ hg add delta $ fn_hg_commit -u "name" -m 'add delta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo epsilon > epsilon $ hg add epsilon $ fn_hg_commit -u "name zeta $ hg add zeta $ fn_hg_commit -u " test " -m 'add zeta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo eta > eta $ hg add eta $ fn_hg_commit -u "test < test@example.com >" -m 'add eta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ echo theta > theta $ hg add theta $ fn_hg_commit -u "test >test@example.com>" -m 'add theta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master $ hg log --graph | egrep -v ': *(not-master|master)' @ changeset: 8:d3c51ce68cfd | tag: default/master | tag: tip | user: test >test@example.com> | date: Mon Jan 01 00:00:18 2007 +0000 | summary: add theta | o changeset: 7:b90e988091a2 | user: test < test@example.com > | date: Mon Jan 01 00:00:17 2007 +0000 | summary: add eta | o changeset: 6:7ede2f971cae | user: test | date: Mon Jan 01 00:00:16 2007 +0000 | summary: add zeta | o changeset: 5:1454a94056ec | user: name | date: Mon Jan 01 00:00:14 2007 +0000 | summary: add delta | o changeset: 3:8da3ab8b31d0 | user: | date: Mon Jan 01 00:00:13 2007 +0000 | summary: add gamma | o changeset: 2:92d33c0dd6e1 | user: test (comment) | date: Mon Jan 01 00:00:12 2007 +0000 | summary: modify beta | o changeset: 1:0564f526fb0f | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:3442585be8a6 tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. $ hg clone gitrepo hgrepo2 | grep -v '^updating' importing git objects into hg 8 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo2 log --graph | egrep -v ': *(not-master|master)' @ changeset: 8:efec0270e295 | tag: default/master | tag: tip | user: test ?test@example.com | date: Mon Jan 01 00:00:18 2007 +0000 | summary: add theta | o changeset: 7:8ab87d5066e4 | user: test | date: Mon Jan 01 00:00:17 2007 +0000 | summary: add eta | o changeset: 6:ff226cc916bd | user: test | date: Mon Jan 01 00:00:16 2007 +0000 | summary: add zeta | o changeset: 5:5f1557c62c53 | user: name | date: Mon Jan 01 00:00:15 2007 +0000 | summary: add epsilon | o changeset: 4:fc51727b28fe | user: name | date: Mon Jan 01 00:00:14 2007 +0000 | summary: add delta | o changeset: 3:8da3ab8b31d0 | user: | date: Mon Jan 01 00:00:13 2007 +0000 | summary: add gamma | o changeset: 2:92d33c0dd6e1 | user: test (comment) | date: Mon Jan 01 00:00:12 2007 +0000 | summary: modify beta | o changeset: 1:0564f526fb0f | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:3442585be8a6 tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ git --git-dir=gitrepo/.git log --pretty=medium master commit 1e03e913eca571b86ee06d3c1ddd795dde9ca917 Author: test ?test@example.com Date: Mon Jan 1 00:00:18 2007 +0000 add theta commit 8c878c9764e96e67ed9f62b3f317d156bf71bc52 Author: test Date: Mon Jan 1 00:00:17 2007 +0000 add eta commit d21e26b48c6136340dd1212bb45ba0e9debb130c Author: test Date: Mon Jan 1 00:00:16 2007 +0000 add zeta commit ee985f124d2f13ee8ad2a346a6d1b0ada8b0d491 Author: name Date: Mon Jan 1 00:00:15 2007 +0000 add epsilon commit d16592507ac83a6a633b90ca255f65e5d024f0bc Author: name Date: Mon Jan 1 00:00:14 2007 +0000 add delta commit fee30180efc4943fb916de04fcf6a64b638d9325 Author: Date: Mon Jan 1 00:00:13 2007 +0000 add gamma commit 2b9ec6a47b93191986a79eeb771e461c4508c7c4 Author: test ext:(%20%28comment%29) Date: Mon Jan 1 00:00:12 2007 +0000 modify beta commit cffa0e8d8ad5f284c69c898c0f3c1e32d078af8a Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha durin42-hg-git-a3c3b8077cbe/tests/test-hg-branch.t0000644000000000000000000000473112104602314017674 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -b not-master Switched to a new branch 'not-master' $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg co master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg mv alpha beta $ fn_hg_commit -m 'rename alpha to beta' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 0 blobs updating reference refs/heads/master $ hg branch gamma | grep -v 'permanent and global' marked working directory as branch gamma $ fn_hg_commit -m 'started branch gamma' $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 0 blobs updating reference refs/heads/master $ hg log --graph | egrep -v ': *(not-master|master)' @ changeset: 2:05aed681ccb3 | branch: gamma | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: started branch gamma | o changeset: 1:a31e374801c9 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: rename alpha to beta | o changeset: 0:3442585be8a6 tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. $ hg clone gitrepo hgrepo2 | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo2 log --graph | egrep -v ': *(not-master|master)' o changeset: 2:05aed681ccb3 | branch: gamma | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: started branch gamma | @ changeset: 1:a31e374801c9 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: rename alpha to beta | o changeset: 0:3442585be8a6 tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha durin42-hg-git-a3c3b8077cbe/tests/test-hg-tags.t0000644000000000000000000000413112104602314017367 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -b not-master Switched to a new branch 'not-master' $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg co master 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ fn_hg_tag alpha $ hg push pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs adding reference refs/tags/alpha updating reference refs/heads/master $ hg log --graph | egrep -v ': *(not-master|master)' @ changeset: 1:d529e9229f6d | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: Added tag alpha for changeset 3442585be8a6 | o changeset: 0:3442585be8a6 tag: alpha tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. $ cd gitrepo git should have the tag alpha $ git tag -l alpha $ cd .. $ hg clone gitrepo hgrepo2 | grep -v '^updating' importing git objects into hg 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo2 log --graph | egrep -v ': *(not-master|master)' @ changeset: 1:d529e9229f6d | tag: default/master | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: Added tag alpha for changeset 3442585be8a6 | o changeset: 0:3442585be8a6 tag: alpha tag: default/not-master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha the tag should be in .hgtags $ cat hgrepo2/.hgtags 3442585be8a60c6cd476bbc4e45755339f2a23ef alpha durin42-hg-git-a3c3b8077cbe/tests/test-incoming.t0000755000000000000000000000675412104602314017660 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo incoming | grep -v 'no changes found' | grep -v 'bookmark:' comparing with $TESTTMP/gitrepo $ cd gitrepo $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ hg -R hgrepo incoming | grep -v 'no changes found' | grep -v 'bookmark:' comparing with $TESTTMP/gitrepo changeset: 1:9497a4ee62e1 user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta $ cd gitrepo $ git checkout -b b1 HEAD^ Switched to a new branch 'b1' $ mkdir d $ echo gamma > d/gamma $ git add d/gamma $ fn_git_commit -m'add d/gamma' $ git tag t1 $ echo gamma 2 >> d/gamma $ git add d/gamma $ fn_git_commit -m'add d/gamma line 2' $ cd ../hgrepo $ hg incoming -p | grep -v 'no changes found' | grep -v 'bookmark:' comparing with $TESTTMP/gitrepo changeset: 1:9497a4ee62e1 user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta diff -r 3442585be8a6 -r 9497a4ee62e1 beta --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/beta Mon Jan 01 00:00:11 2007 +0000 @@ -0,0 +1,1 @@ +beta changeset: 2:9865e289be73 tag: t1 parent: 0:3442585be8a6 user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add d/gamma diff -r 3442585be8a6 -r 9865e289be73 d/gamma --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/d/gamma Mon Jan 01 00:00:12 2007 +0000 @@ -0,0 +1,1 @@ +gamma changeset: 3:5202f48c20c9 user: test date: Mon Jan 01 00:00:13 2007 +0000 summary: add d/gamma line 2 diff -r 9865e289be73 -r 5202f48c20c9 d/gamma --- a/d/gamma Mon Jan 01 00:00:12 2007 +0000 +++ b/d/gamma Mon Jan 01 00:00:13 2007 +0000 @@ -1,1 +1,2 @@ gamma +gamma 2 incoming -r $ hg incoming -r master | grep -v 'no changes found' | grep -v 'bookmark:' comparing with $TESTTMP/gitrepo changeset: 1:9497a4ee62e1 user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta $ hg incoming -r b1 | grep -v 'no changes found' | grep -v 'bookmark:' comparing with $TESTTMP/gitrepo changeset: 1:9865e289be73 tag: t1 user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add d/gamma changeset: 2:5202f48c20c9 user: test date: Mon Jan 01 00:00:13 2007 +0000 summary: add d/gamma line 2 $ hg incoming -r t1 | grep -v 'no changes found' | grep -v 'bookmark:' comparing with $TESTTMP/gitrepo changeset: 1:9865e289be73 tag: t1 user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add d/gamma nothing incoming after pull "adding remote bookmark" message was added in Mercurial 2.3 $ hg pull | grep -v "adding remote bookmark" pulling from $TESTTMP/gitrepo importing git objects into hg (run 'hg heads' to see heads, 'hg merge' to merge) $ hg incoming | grep -v 'no changes found' | grep -v 'bookmark:' comparing with $TESTTMP/gitrepo durin42-hg-git-a3c3b8077cbe/tests/test-keywords.t0000755000000000000000000000241712104602314017714 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ echo gamma > gamma $ hg add gamma $ hg commit -m 'add gamma' $ hg log --template "{rev} {node} {node|short} {gitnode} {gitnode|short}\n" 2 a9da0c7c9bb7574b0f3139ab65cabac7468d6b8d a9da0c7c9bb7 1 7bcd915dc873c654b822f01b0a39269b2739e86d 7bcd915dc873 9497a4ee62e16ee641860d7677cdb2589ea15554 9497a4ee62e1 0 3442585be8a60c6cd476bbc4e45755339f2a23ef 3442585be8a6 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 7eeab2ea75ec $ hg log --template "fromgit {rev}\n" --rev "fromgit()" fromgit 0 fromgit 1 $ hg log --template "gitnode_existsA {rev}\n" --rev "gitnode(9497a4ee62e16ee641860d7677cdb2589ea15554)" gitnode_existsA 1 $ hg log --template "gitnode_existsB {rev}\n" --rev "gitnode(7eeab2ea75ec)" gitnode_existsB 0 $ hg log --template "gitnode_notexists {rev}\n" --rev "gitnode(1234567890ab)" durin42-hg-git-a3c3b8077cbe/tests/test-merge.t0000644000000000000000000000455712104602314017150 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git checkout -b beta 2>&1 | sed s/\'/\"/g Switched to a new branch "beta" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout master 2>&1 | sed s/\'/\"/g Switched to branch "master" $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' clean merge $ git merge beta | sed "s/the '//;s/' strategy//" | sed 's/^Merge.*recursive.*$/Merge successful/' | sed 's/files/file/;s/insertions/insertion/;s/, 0 deletions.*//' | sed 's/| */| /' Merge successful beta | 1 + 1 file changed, 1 insertion(+) create mode 100644 beta $ cd .. $ git init --bare gitrepo2 Initialized empty Git repository in $TESTTMP/gitrepo2/ $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo clear the cache to be sure it is regenerated correctly $ hg gclear clearing out the git cache data $ hg push ../gitrepo2 pushing to ../gitrepo2 searching for changes adding objects added 4 commits with 4 trees and 3 blobs $ cd .. git log in repo pushed from hg $ git --git-dir=gitrepo2 log --pretty=medium master | sed 's/\.\.\.//g' commit 5806851511aaf3bfe813ae3a86c5027165fa9b96 Merge: e5023f9 9497a4e Author: test Date: Mon Jan 1 00:00:12 2007 +0000 Merge branch 'beta' commit e5023f9e5cb24fdcec7b6c127cec45d8888e35a9 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 add gamma commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha $ git --git-dir=gitrepo2 log --pretty=medium beta | sed 's/\.\.\.//g' commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha durin42-hg-git-a3c3b8077cbe/tests/test-octopus.t0000644000000000000000000000566312104602314017544 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git checkout -b branch1 2>&1 | sed s/\'/\"/g Switched to a new branch "branch1" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ git checkout -b branch2 master 2>&1 | sed s/\'/\"/g Switched to a new branch "branch2" $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ git checkout master 2>&1 | sed s/\'/\"/g Switched to branch "master" $ echo delta > delta $ git add delta $ fn_git_commit -m 'add delta' $ git merge branch1 branch2 | sed "s/the '//;s/' strategy//" | sed 's/^Merge.*octopus.*$/Merge successful/;s/, 0 deletions.*//' | sed 's/| */| /' Trying simple merge with branch1 Trying simple merge with branch2 Merge successful beta | 1 + gamma | 1 + 2 files changed, 2 insertions(+) create mode 100644 beta create mode 100644 gamma $ cd .. $ git init --bare gitrepo2 Initialized empty Git repository in $TESTTMP/gitrepo2/ $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 4 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log --graph --style compact | sed 's/\[.*\]//g' @ 5:3,4 6523aa9f4775 2007-01-01 00:00 +0000 test |\ Merge branches 'branch1' and 'branch2' | | | o 4:1,2 7f6c791a169f 2007-01-01 00:00 +0000 test | |\ Merge branches 'branch1' and 'branch2' | | | o | | 3:0 1436150b86c2 2007-01-01 00:00 +0000 test | | | add delta | | | +---o 2:0 37c124f2d0a0 2007-01-01 00:00 +0000 test | | add gamma | | | o 1 7bcd915dc873 2007-01-01 00:00 +0000 test |/ add beta | o 0 3442585be8a6 2007-01-01 00:00 +0000 test add alpha $ hg gclear clearing out the git cache data $ hg push ../gitrepo2 pushing to ../gitrepo2 searching for changes adding objects added 5 commits with 5 trees and 4 blobs $ cd .. $ git --git-dir=gitrepo2 log --pretty=medium | sed s/\\.\\.\\.//g commit f0c7ec180419a130636d0c333fc34c1462cab4b5 Merge: d8e22dd 9497a4e e5023f9 Author: test Date: Mon Jan 1 00:00:13 2007 +0000 Merge branches 'branch1' and 'branch2' commit d8e22ddb015d06460ccbb4508d2184c12c8a7c4c Author: test Date: Mon Jan 1 00:00:13 2007 +0000 add delta commit e5023f9e5cb24fdcec7b6c127cec45d8888e35a9 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 add gamma commit 9497a4ee62e16ee641860d7677cdb2589ea15554 Author: test Date: Mon Jan 1 00:00:11 2007 +0000 add beta commit 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 Author: test Date: Mon Jan 1 00:00:10 2007 +0000 add alpha durin42-hg-git-a3c3b8077cbe/tests/test-outgoing.t0000755000000000000000000000733212104602314017701 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git branch alpha $ git show-ref 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 refs/heads/alpha 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 refs/heads/master $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg update -q master $ echo beta > beta $ hg add beta $ fn_hg_commit -m 'add beta' $ echo gamma > gamma $ hg add gamma $ fn_hg_commit -m 'add gamma' $ hg book -r 1 beta $ hg outgoing | sed 's/bookmark: /tag: /' | grep -v 'searching for changes' comparing with */gitrepo (glob) changeset: 1:0564f526fb0f tag: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta changeset: 2:72f56395749d tag: master tag: tip user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ hg outgoing -r beta | sed 's/bookmark: /tag: /' | grep -v 'searching for changes' comparing with */gitrepo (glob) changeset: 1:0564f526fb0f tag: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta $ hg outgoing -r master | sed 's/bookmark: /tag: /' | grep -v 'searching for changes' comparing with */gitrepo (glob) changeset: 1:0564f526fb0f tag: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta changeset: 2:72f56395749d tag: master tag: tip user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ cd .. some more work on master from git $ cd gitrepo Check state of refs after outgoing $ git show-ref 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 refs/heads/alpha 7eeab2ea75ec1ac0ff3d500b5b6f8a3447dd7c03 refs/heads/master $ git checkout master 2>&1 | sed s/\'/\"/g Already on "master" $ echo delta > delta $ git add delta $ fn_git_commit -m "add delta" $ cd .. $ cd hgrepo this will fail # maybe we should try to make it work $ hg outgoing comparing with */gitrepo (glob) abort: refs/heads/master changed on the server, please pull and merge before pushing [255] let's pull and try again $ hg pull 2>&1 | grep -v 'divergent bookmark' pulling from */gitrepo (glob) importing git objects into hg (run 'hg update' to get a working copy) $ hg outgoing | sed 's/bookmark: /tag: /' | grep -v 'searching for changes' comparing with */gitrepo (glob) changeset: 1:0564f526fb0f tag: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta changeset: 2:72f56395749d tag: master user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ hg outgoing -r beta | sed 's/bookmark: /tag: /' | grep -v 'searching for changes' comparing with */gitrepo (glob) changeset: 1:0564f526fb0f tag: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta $ hg outgoing -r master | sed 's/bookmark: /tag: /' | grep -v 'searching for changes' comparing with */gitrepo (glob) changeset: 1:0564f526fb0f tag: beta user: test date: Mon Jan 01 00:00:11 2007 +0000 summary: add beta changeset: 2:72f56395749d tag: master user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ cd .. durin42-hg-git-a3c3b8077cbe/tests/test-pull-after-strip.t0000755000000000000000000000552312104602314021260 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git tag alpha $ git checkout -b beta 2>&1 | sed s/\'/\"/g Switched to a new branch "beta" $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. clone a tag $ hg clone -r alpha gitrepo hgrepo-a | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-a log --graph | egrep -v ': *(beta|master)' @ changeset: 0:3442585be8a6 tag: alpha tag: default/master tag: tip user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha clone a branch $ hg clone -r beta gitrepo hgrepo-b | grep -v '^updating' importing git objects into hg 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo-b log --graph | egrep -v ': *(beta|master)' @ changeset: 1:7bcd915dc873 | tag: default/beta | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | o changeset: 0:3442585be8a6 tag: alpha tag: default/master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd gitrepo $ echo beta line 2 >> beta $ git add beta $ fn_git_commit -m 'add to beta' $ cd .. $ cd hgrepo-b $ hg strip tip 2>&1 | grep -v saving | grep -v backup 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ hg pull -r beta pulling from $TESTTMP/gitrepo importing git objects into hg abort: you appear to have run strip - please run hg git-cleanup [255] $ hg git-cleanup git commit map cleaned pull works after 'hg git-cleanup' "adding remote bookmark" message was added in Mercurial 2.3 $ hg pull -r beta | grep -v "adding remote bookmark" pulling from $TESTTMP/gitrepo importing git objects into hg (run 'hg update' to get a working copy) $ hg log --graph | egrep -v 'bookmark: *(alpha|beta|master)' o changeset: 2:611948b1ec6a | tag: default/beta | tag: tip | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add to beta | o changeset: 1:7bcd915dc873 | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:3442585be8a6 tag: alpha tag: default/master user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha $ cd .. durin42-hg-git-a3c3b8077cbe/tests/test-pull.t0000644000000000000000000000777612104602314017033 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" set up a git repo with some commits, branches and a tag $ git init -q gitrepo $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git tag t_alpha $ git checkout -qb beta $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. clone a tag (ideally we'd want to pull it, but that seems broken for now) # $ hg init hgrepo # $ echo "[paths]" >> hgrepo/.hg/hgrc # $ echo "default=$TESTTMP/gitrepo" >> hgrepo/.hg/hgrc # $ hg -R hgrepo pull -r t_alpha $ hg clone -r t_alpha gitrepo hgrepo importing git objects into hg updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R hgrepo log --graph @ changeset: 0:3442585be8a6 bookmark: master tag: default/master tag: t_alpha tag: tip user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha pull a branch $ hg -R hgrepo pull -r beta pulling from $TESTTMP/gitrepo importing git objects into hg (run 'hg update' to get a working copy) $ hg -R hgrepo log --graph o changeset: 1:7bcd915dc873 | bookmark: beta | tag: default/beta | tag: tip | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:3442585be8a6 bookmark: master tag: default/master tag: t_alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha add another commit and tag to the git repo $ cd gitrepo $ git tag t_beta $ git checkout -q master $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ cd .. pull everything else $ hg -R hgrepo pull pulling from $TESTTMP/gitrepo importing git objects into hg (run 'hg update' to get a working copy) $ hg -R hgrepo log --graph o changeset: 2:37c124f2d0a0 | bookmark: master | tag: default/master | tag: tip | parent: 0:3442585be8a6 | user: test | date: Mon Jan 01 00:00:12 2007 +0000 | summary: add gamma | | o changeset: 1:7bcd915dc873 |/ bookmark: beta | tag: default/beta | tag: t_beta | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:3442585be8a6 tag: t_alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha add a merge to the git repo $ cd gitrepo $ git merge beta | sed 's/| */| /' Merge made by the 'recursive' strategy. beta | 1 + 1 file changed, 1 insertion(+) create mode 100644 beta $ cd .. pull the merge $ hg -R hgrepo pull pulling from $TESTTMP/gitrepo importing git objects into hg (run 'hg update' to get a working copy) $ hg -R hgrepo log --graph o changeset: 3:b8668fddf56c |\ bookmark: master | | tag: default/master | | tag: tip | | parent: 2:37c124f2d0a0 | | parent: 1:7bcd915dc873 | | user: test | | date: Mon Jan 01 00:00:12 2007 +0000 | | summary: Merge branch 'beta' | | | o changeset: 2:37c124f2d0a0 | | parent: 0:3442585be8a6 | | user: test | | date: Mon Jan 01 00:00:12 2007 +0000 | | summary: add gamma | | o | changeset: 1:7bcd915dc873 |/ bookmark: beta | tag: default/beta | tag: t_beta | user: test | date: Mon Jan 01 00:00:11 2007 +0000 | summary: add beta | @ changeset: 0:3442585be8a6 tag: t_alpha user: test date: Mon Jan 01 00:00:10 2007 +0000 summary: add alpha durin42-hg-git-a3c3b8077cbe/tests/test-push-r.t0000755000000000000000000001022512104602314017257 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init test $ cd test $ cat >>afile < 0 > EOF $ hg add afile $ hg commit -m "0.0" $ cat >>afile < 1 > EOF $ hg commit -m "0.1" $ cat >>afile < 2 > EOF $ hg commit -m "0.2" $ cat >>afile < 3 > EOF $ hg commit -m "0.3" $ hg update -C 0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cat >>afile < 1 > EOF $ hg commit -m "1.1" created new head $ cat >>afile < 2 > EOF $ hg commit -m "1.2" $ cat >fred < a line > EOF $ cat >>afile < 3 > EOF $ hg add fred $ hg commit -m "1.3" $ hg mv afile adifferentfile $ hg commit -m "1.3m" $ hg update -C 3 1 files updated, 0 files merged, 2 files removed, 0 files unresolved $ hg mv afile anotherfile $ hg commit -m "0.3m" $ cd .. $ for i in 0 1 2 3 4 5 6 7 8; do > mkdir test-"$i" > hg --cwd test-"$i" init > hg -R test push -r "$i" test-"$i" > cd test-"$i" > hg verify > cd .. > done pushing to test-0 searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 1 files, 1 changesets, 1 total revisions pushing to test-1 searching for changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 1 files, 2 changesets, 2 total revisions pushing to test-2 searching for changes adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 1 files, 3 changesets, 3 total revisions pushing to test-3 searching for changes adding changesets adding manifests adding file changes added 4 changesets with 4 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 1 files, 4 changesets, 4 total revisions pushing to test-4 searching for changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 1 files, 2 changesets, 2 total revisions pushing to test-5 searching for changes adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 1 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 1 files, 3 changesets, 3 total revisions pushing to test-6 searching for changes adding changesets adding manifests adding file changes added 4 changesets with 5 changes to 2 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 2 files, 4 changesets, 5 total revisions pushing to test-7 searching for changes adding changesets adding manifests adding file changes added 5 changesets with 6 changes to 3 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 3 files, 5 changesets, 6 total revisions pushing to test-8 searching for changes adding changesets adding manifests adding file changes added 5 changesets with 5 changes to 2 files checking changesets checking manifests crosschecking files in changesets and manifests checking files 2 files, 5 changesets, 5 total revisions $ cd test-8 $ hg pull ../test-7 pulling from ../test-7 searching for changes adding changesets adding manifests adding file changes added 4 changesets with 2 changes to 3 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg verify checking changesets checking manifests crosschecking files in changesets and manifests checking files 4 files, 9 changesets, 7 total revisions durin42-hg-git-a3c3b8077cbe/tests/test-push.t0000644000000000000000000000700612104602314017020 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ echo alpha > alpha $ git add alpha $ fn_git_commit -m "add alpha" $ git checkout -b not-master 2>&1 | sed s/\'/\"/g Switched to a new branch "not-master" $ cd .. $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ echo beta > beta $ hg add beta $ fn_hg_commit -m 'add beta' $ echo gamma > gamma $ hg add gamma $ fn_hg_commit -m 'add gamma' $ hg book -r 1 beta $ hg push -r beta pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs adding reference refs/heads/beta $ cd .. should have two different branches $ cd gitrepo $ git branch -v beta cffa0e8 add beta master 7eeab2e add alpha * not-master 7eeab2e add alpha some more work on master from git $ git checkout master 2>&1 | sed s/\'/\"/g Switched to branch "master" $ echo delta > delta $ git add delta $ fn_git_commit -m "add delta" $ git checkout not-master 2>&1 | sed s/\'/\"/g Switched to branch "not-master" $ cd .. $ cd hgrepo this should fail $ hg push -r master pushing to $TESTTMP/gitrepo searching for changes abort: refs/heads/master changed on the server, please pull and merge before pushing [255] ... even with -f $ hg push -fr master pushing to $TESTTMP/gitrepo searching for changes abort: refs/heads/master changed on the server, please pull and merge before pushing [255] $ hg pull 2>&1 | grep -v 'divergent bookmark' pulling from $TESTTMP/gitrepo importing git objects into hg (run 'hg update' to get a working copy) TODO shouldn't need to do this since we're (in theory) pushing master explicitly, which should not implicitly also push the not-master ref. $ hg book not-master -r default/not-master --force master and default/master should be diferent $ hg log -r master | grep -v ': *master' changeset: 2:72f56395749d user: test date: Mon Jan 01 00:00:12 2007 +0000 summary: add gamma $ hg log -r default/master | grep -v 'master@default' changeset: 3:1436150b86c2 tag: default/master tag: tip parent: 0:3442585be8a6 user: test date: Mon Jan 01 00:00:13 2007 +0000 summary: add delta this should also fail $ hg push -r master pushing to $TESTTMP/gitrepo searching for changes abort: pushing refs/heads/master overwrites 72f56395749d [255] ... but succeed with -f $ hg push -fr master pushing to $TESTTMP/gitrepo searching for changes adding objects added 1 commits with 1 trees and 1 blobs updating reference refs/heads/master this should fail, no changes to push The exit code for this was broken in Mercurial (incorrectly returning 0) until issue3228 was fixed in 2.1 $ hg push -r master && false pushing to $TESTTMP/gitrepo searching for changes no changes found [1] $ cd .. Push empty Hg repo to empty Git repo (issue #58) Since there aren't any changes, exit code 1 is expected in modern Mercurial. However, since it varies between supported Mercurial versions, we need to force it to consistency for now. (see issue3228, fixed in Mercurial 2.1) $ hg init hgrepo2 $ git init -q --bare gitrepo2 $ hg -R hgrepo2 push gitrepo2 && false pushing to gitrepo2 searching for changes no changes found [1] durin42-hg-git-a3c3b8077cbe/tests/test-subrepos.t0000644000000000000000000000777212104602314017715 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitsubrepo Initialized empty Git repository in $TESTTMP/gitsubrepo/.git/ $ cd gitsubrepo $ echo beta > beta $ git add beta $ fn_git_commit -m 'add beta' $ cd .. $ git init gitrepo1 Initialized empty Git repository in $TESTTMP/gitrepo1/.git/ $ cd gitrepo1 $ echo alpha > alpha $ git add alpha $ fn_git_commit -m 'add alpha' $ git submodule add ../gitsubrepo subrepo1 Cloning into 'subrepo1'... done. $ fn_git_commit -m 'add subrepo1' $ git submodule add ../gitsubrepo xyz/subrepo2 Cloning into 'xyz/subrepo2'... done. $ fn_git_commit -m 'add subrepo2' we are going to push to this repo from our hg clone, allow commits despite working copy presense $ git config receive.denyCurrentBranch ignore $ cd .. Ensure gitlinks are transformed to .hgsubstate on hg pull from git $ hg clone gitrepo1 hgrepo importing git objects into hg updating to branch default cloning subrepo subrepo1 from $TESTTMP/gitsubrepo cloning subrepo xyz/subrepo2 from $TESTTMP/gitsubrepo 4 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg bookmarks -f -r default master 1. Ensure gitlinks are transformed to .hgsubstate on hg <- git pull .hgsub shall list two [git] subrepos $ cat .hgsub subrepo1 = [git]../gitsubrepo xyz/subrepo2 = [git]../gitsubrepo .hgsubstate shall list two idenitcal revisions $ cat .hgsubstate 56f0304c5250308f14cfbafdc27bd12d40154d17 subrepo1 56f0304c5250308f14cfbafdc27bd12d40154d17 xyz/subrepo2 hg status shall NOT report .hgsub and .hgsubstate as untracked - either ignored or unmodified $ hg status --unknown .hgsub .hgsubstate $ hg status --modified .hgsub .hgsubstate $ cd .. 2. Check gitmodules are preserved during hg -> git push $ cd gitsubrepo $ echo gamma > gamma $ git add gamma $ fn_git_commit -m 'add gamma' $ cd .. $ cd hgrepo $ cd xyz/subrepo2 $ git pull | sed 's/files/file/;s/insertions/insertion/;s/, 0 deletions.*//' | sed 's/| */| /' From $TESTTMP/gitsubrepo 56f0304..aabf7cd master -> origin/master Updating 56f0304..aabf7cd Fast-forward gamma | 1 + 1 file changed, 1 insertion(+) create mode 100644 gamma $ cd ../.. $ echo xxx >> alpha $ hg commit -m 'Update subrepo2 from hg' | grep -v "committing subrepository" || true $ hg push pushing to $TESTTMP/gitrepo1 searching for changes adding objects added 1 commits with 2 trees and 1 blobs updating reference refs/heads/master $ cd .. $ cd gitrepo1 there shall be two gitlink entries, with values matching that in .hgsubstate $ git ls-tree -r HEAD^{tree} | grep 'commit' 160000 commit 56f0304c5250308f14cfbafdc27bd12d40154d17 subrepo1 160000 commit aabf7cd015089aff0b84596e69aa37b24a3d090a xyz/subrepo2 bring working copy to HEAD state (it's not bare repo) $ git reset --hard HEAD is now at 4663c49 Update subrepo2 from hg $ cd .. 3. Check .hgsub and .hgsubstate from git repository are merged, not overwritten $ hg init hgsub $ cd hgsub $ echo delta > delta $ hg add delta $ fn_hg_commit -m "add delta" $ echo "`hg tip --template '{node}'` hgsub" > ../gitrepo1/.hgsubstate $ echo "hgsub = $(pwd)" > ../gitrepo1/.hgsub $ cd ../gitrepo1 $ git add .hgsubstate .hgsub $ fn_git_commit -m "Test3. Prepare .hgsub and .hgsubstate sources" $ cd ../hgrepo $ hg pull pulling from $TESTTMP/gitrepo1 importing git objects into hg (run 'hg update' to get a working copy) $ hg checkout -C cloning subrepo hgsub from $TESTTMP/hgsub 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd .. pull shall bring .hgsub entry which was added to the git repo $ cat hgrepo/.hgsub hgsub = $TESTTMP/hgsub subrepo1 = [git]../gitsubrepo xyz/subrepo2 = [git]../gitsubrepo .hgsubstate shall list revision of the subrepo added through git repo $ cat hgrepo/.hgsubstate 481ec30d580f333ae3a77f94c973ce37b69d5bda hgsub 56f0304c5250308f14cfbafdc27bd12d40154d17 subrepo1 aabf7cd015089aff0b84596e69aa37b24a3d090a xyz/subrepo2 durin42-hg-git-a3c3b8077cbe/tests/test-timezone.t0000755000000000000000000000152212104602314017673 0ustar 00000000000000This test shows how dulwich fails to convert a commit accepted by hg. In the real world case, it was a hand edit by the user to change the timezone field in an export. However, if it is good enough for hg, we have to make it good enough for git. Load commonly used test logic $ . "$TESTDIR/testutil" $ hg init hgrepo $ cd hgrepo $ touch beta $ hg add beta $ fn_hg_commit -m "test commit" $ cat >patch2 < # HG changeset patch > # User J. User > # Date 1337962044 25201 > # Node ID 1111111111111111111111111111111111111111 > # Parent 0000000000000000000000000000000000000000 > Patch with sub-minute time zone > > diff --git a/alpha b/alpha > new file mode 100644 > --- /dev/null > +++ b/alpha > @@ -0,0 +1,1 @@ > +alpha > EOF $ hg import patch2 applying patch2 $ hg gexport durin42-hg-git-a3c3b8077cbe/tests/test-tree-decomposition.t0000644000000000000000000000275412104602314021657 0ustar 00000000000000Load commonly used test logic $ . "$TESTDIR/testutil" $ git init gitrepo Initialized empty Git repository in $TESTTMP/gitrepo/.git/ $ cd gitrepo $ mkdir d1 $ echo a > d1/f1 $ echo b > d1/f2 $ git add d1/f1 d1/f2 $ fn_git_commit -m initial $ mkdir d2 $ git mv d1/f2 d2/f2 $ fn_git_commit -m 'rename' $ rm -r d1 $ echo c > d1 $ git add d1 $ fn_git_commit -m 'replace a dir with a file' $ cd .. $ git init --bare gitrepo2 Initialized empty Git repository in $TESTTMP/gitrepo2/ $ hg clone gitrepo hgrepo | grep -v '^updating' importing git objects into hg 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd hgrepo $ hg log --template 'adds: {file_adds}\ndels: {file_dels}\n' adds: d1 dels: d1/f1 adds: d2/f2 dels: d1/f2 adds: d1/f1 d1/f2 dels: $ hg gclear clearing out the git cache data $ hg push ../gitrepo2 pushing to ../gitrepo2 searching for changes adding objects added 3 commits with 6 trees and 3 blobs $ cd .. $ git --git-dir=gitrepo2 log --pretty=medium commit 6e0dbd8cd92ed4823c69cb48d8a2b81f904e6e69 Author: test Date: Mon Jan 1 00:00:12 2007 +0000 replace a dir with a file commit a1874d5cd0b1549ed729e36f0da4a93ed36259ee Author: test Date: Mon Jan 1 00:00:11 2007 +0000 rename commit 102c17a5deda49db3f10ec5573f9378867098b7c Author: test Date: Mon Jan 1 00:00:10 2007 +0000 initial durin42-hg-git-a3c3b8077cbe/tests/test-url-parsing.py0000755000000000000000000001021112104602314020464 0ustar 00000000000000import sys try: import dulwich except ImportError: print "skipped: missing feature: dulwich" sys.exit(80) import os, tempfile, unittest, shutil from mercurial import ui, hg, commands sys.path.append(os.path.join(os.path.dirname(__file__), os.path.pardir)) from hggit.git_handler import GitHandler class TestUrlParsing(object): def setUp(self): # create a test repo location. self.tmpdir = tempfile.mkdtemp('hg-git_url-test') commands.init(ui.ui(), self.tmpdir) repo = hg.repository(ui.ui(), self.tmpdir) self.handler = GitHandler(repo, ui.ui()) def tearDown(self): # remove the temp repo shutil.rmtree(self.tmpdir) def assertEquals(self, l, r): print '%% expect %r' % (r, ) print l assert l == r def test_ssh_github_style_slash(self): url = "git+ssh://git@github.com/webjam/webjam.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, '/webjam/webjam.git') self.assertEquals(client.host, 'git@github.com') def test_ssh_github_style_colon_number_starting_username(self): url = "git+ssh://git@github.com:42qu/vps.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, '42qu/vps.git') self.assertEquals(client.host, 'git@github.com') def test_ssh_github_style_colon(self): url = "git+ssh://git@github.com:webjam/webjam.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, 'webjam/webjam.git') self.assertEquals(client.host, 'git@github.com') def test_ssh_heroku_style(self): url = "git+ssh://git@heroku.com:webjam.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, 'webjam.git') self.assertEquals(client.host, 'git@heroku.com') # also test that it works even if heroku isn't in the name url = "git+ssh://git@compatible.com:webjam.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, 'webjam.git') self.assertEquals(client.host, 'git@compatible.com') def test_ssh_heroku_style_with_trailing_slash(self): # some versions of mercurial add a trailing slash even if # the user didn't supply one. url = "git+ssh://git@heroku.com:webjam.git/" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, 'webjam.git') self.assertEquals(client.host, 'git@heroku.com') def test_heroku_style_with_port(self): url = "git+ssh://git@heroku.com:999:webjam.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, 'webjam.git') self.assertEquals(client.host, 'git@heroku.com') self.assertEquals(client.port, '999') def test_gitdaemon_style(self): url = "git://github.com/webjam/webjam.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, '/webjam/webjam.git') try: self.assertEquals(client._host, 'github.com') except AttributeError: self.assertEquals(client.host, 'github.com') def test_ssh_github_style_slash_with_port(self): url = "git+ssh://git@github.com:10022/webjam/webjam.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, '/webjam/webjam.git') self.assertEquals(client.host, 'git@github.com') self.assertEquals(client.port, '10022') def test_gitdaemon_style_with_port(self): url = "git://github.com:19418/webjam/webjam.git" client, path = self.handler.get_transport_and_path(url) self.assertEquals(path, '/webjam/webjam.git') try: self.assertEquals(client._host, 'github.com') except AttributeError: self.assertEquals(client.host, 'github.com') self.assertEquals(client._port, '19418') if __name__ == '__main__': tc = TestUrlParsing() for test in sorted([t for t in dir(tc) if t.startswith('test_')]): tc.setUp() getattr(tc, test)() tc.tearDown() durin42-hg-git-a3c3b8077cbe/tests/test-url-parsing.py.out0000644000000000000000000000155712104602314021304 0ustar 00000000000000% expect '/webjam/webjam.git' /webjam/webjam.git % expect 'github.com' github.com % expect '/webjam/webjam.git' /webjam/webjam.git % expect 'github.com' github.com % expect '19418' 19418 % expect 'webjam.git' webjam.git % expect 'git@heroku.com' git@heroku.com % expect '999' 999 % expect 'webjam/webjam.git' webjam/webjam.git % expect 'git@github.com' git@github.com % expect '42qu/vps.git' 42qu/vps.git % expect 'git@github.com' git@github.com % expect '/webjam/webjam.git' /webjam/webjam.git % expect 'git@github.com' git@github.com % expect '/webjam/webjam.git' /webjam/webjam.git % expect 'git@github.com' git@github.com % expect '10022' 10022 % expect 'webjam.git' webjam.git % expect 'git@heroku.com' git@heroku.com % expect 'webjam.git' webjam.git % expect 'git@compatible.com' git@compatible.com % expect 'webjam.git' webjam.git % expect 'git@heroku.com' git@heroku.com durin42-hg-git-a3c3b8077cbe/tests/testutil0000755000000000000000000000360512104602314016503 0ustar 00000000000000#!/bin/sh # This file holds logic that is used in many tests. # It can be called in a test like this: # $ . "$TESTDIR/testutil" # Activate extensions echo "[extensions]" >> $HGRCPATH echo "hggit=$(echo $(dirname $TESTDIR))/hggit" >> $HGRCPATH # Not needed in Mercurial 2.3+, as graphlog was integrated into core echo 'graphlog=' >> $HGRCPATH echo 'mq=' >> $HGRCPATH # Standard checks for external dependencies # We use the git command-line client and dulwich in pretty much all the tests. # Thus, to avoid repetitively declaring that requirement in almost every test, # we just call the checks in all tests that include this library. python -c 'import dulwich' || { echo "skipped: missing feature: dulwich" && exit 80 } "$TESTDIR/hghave" git || exit 80 GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE # Functions to commit and tag in Mercurial and Git in a predictable manner count=10 fn_git_commit() { GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000" GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git commit "$@" >/dev/null || echo "git commit error" count=`expr $count + 1` } fn_hg_commit() { HGDATE="2007-01-01 00:00:$count +0000" hg commit -d "$HGDATE" "$@" >/dev/null || echo "hg commit error" count=`expr $count + 1` } fn_git_tag() { GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000" GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git tag "$@" >/dev/null || echo "git tag error" count=`expr $count + 1` } fn_hg_tag() { HGDATE="2007-01-01 00:00:$count +0000" hg tag -d "$HGDATE" "$@" >/dev/null || echo "hg tag error" count=`expr $count + 1` }