├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── git-remote-testgitifyhg ├── gitifyhg ├── __init__.py ├── apiwrapper.py ├── gitexporter.py ├── gitifyhg.py ├── hgimporter.py └── util.py ├── setup.py └── test ├── Makefile ├── aggregate-results.sh ├── sharness.sh ├── test-lib.sh ├── test_anonymous_branches.t ├── test_author.t ├── test_bookmarks.t ├── test_clone.t ├── test_clone_file_operations.t ├── test_notes.t ├── test_pull.t ├── test_push.t ├── test_push_tags.t ├── test_spaces.t └── test_special_cases.t /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | dist 4 | *.pyc 5 | .tox 6 | gitifyhg.egg-info 7 | pytestdebug.log 8 | test/trash directory* 9 | test/test-results 10 | test/.prove 11 | venv/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | # install recent git version, and make sure the git config 6 | # contains suitable default 7 | before_install: 8 | - sudo apt-get update -qq 9 | - sudo add-apt-repository -y ppa:pdoes/ppa 10 | - sudo apt-get install -qq git 11 | - git config --global user.email you@example.com 12 | - git config --global user.name Gitifyhg 13 | - git config --global push.default simple 14 | 15 | # install dependencies and gitifyhg itself; 16 | # py.test is already preinstalled 17 | install: 18 | - "pip install -q path.py>=2.5" 19 | - "pip install -q https://www.mercurial-scm.org/release/mercurial-$HG_VERSION.tar.gz" 20 | - "python setup.py -q install" 21 | 22 | # specify various mercurial versions to test against 23 | env: 24 | - HG_VERSION=2.5 25 | - HG_VERSION=2.6 26 | - HG_VERSION=2.8 27 | - HG_VERSION=2.9 28 | - HG_VERSION=3.0 29 | - HG_VERSION=3.1 30 | - HG_VERSION=3.2 31 | - HG_VERSION=3.3 32 | - HG_VERSION=3.4 33 | - HG_VERSION=3.5 34 | - HG_VERSION=3.6 35 | - HG_VERSION=3.7 36 | - HG_VERSION=3.8 37 | - HG_VERSION=3.9 38 | - HG_VERSION=4.0 39 | - HG_VERSION=4.0.1 40 | 41 | 42 | 43 | # command to run actual tests 44 | # We also output the git and hg versions to make sure we are using 45 | # the right ones. 46 | # Also, switch the traceback format to 'short' to work around 47 | # a bug in py.test <= 2.3.4. 48 | script: 49 | - git --version 50 | - hg --version 51 | - cd test 52 | - make 53 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include git-remote-testgitifyhg 3 | recursive-include test Makefile *.sh *.t 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. 2 | Copyright 2012-2013 Dusty Phillips 3 | 4 | This file is part of gitifyhg. 5 | gitifyhg is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | gitifyhg is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with gitifyhg. If not, see . 17 | 18 | 19 | gitifyhg 20 | ======== 21 | This git remote allows you to do local development in a git repository and push 22 | changes to an upstream mercurial repository. It does this seamlessly and allows 23 | pushing and pulling to named branches in the upstream repository. 24 | 25 | ``gitifyhg`` does not rely on hg-git, and allows you to push and pull to and from 26 | a mercurial repository from right inside git. You do not need to adapt your 27 | git workflow in any way aside from cloning a gitifyhg url. 28 | 29 | This is a robust and usable git to hg bridge that has been tested in production settings. It has a large test suite and better documentation than 30 | the `alternatives we know about `_. 31 | It has been tested on several large mercurial repositories (including that 32 | of mercurial itself and the pypy repository) that break with various other 33 | git-to-hg bridge projects and is used daily in normal workflow scenarios. 34 | 35 | That said, gitifyhg is not yet complete. Some of the features that 36 | are not fully working include: 37 | 38 | * anonymous branches are dropped, only the tip of a named branch iqs kept 39 | * remote branch and bookmark tracking is not 100% stable 40 | * pushing octopus merges is not supported 41 | * cloning mercurial branches that are subdirectories of other branches fails 42 | * cloning duplicate case sensitive names on case insensitive filesystems (mac, windows) fails 43 | 44 | However, if you are looking for a git-svn type of workflow that allows you to 45 | clone mercurial repositories, work in local git branches, rebase those 46 | branches and push them back to mercurial, you have found it. It works. Try it. 47 | 48 | Unfortunately, though, maintenance of gitifyhg has fallen off. You can pick it up if you like, but you may also be interested in working on the `git-remote-hg` script that is shipping with the main git project. We're told it's more functional than it once was and that the improvements gitifyhg once had over it have been addressed. We have not confirmed this, but it's worth checking out. Because it has support from the git developers, it's probably the best project to get behind at this time. 49 | 50 | URLS 51 | ---- 52 | * `source `_ 53 | * `issues `_ 54 | * `pypi package `_ 55 | * `Dusty Phillips `_ 56 | * `Inspired by Felipe Contreras 57 | `_ 58 | 59 | Dependencies 60 | ------------ 61 | ``gitifyhg`` has been tested to run on CPython 2.7. Any python that 62 | supports Mercurial should be supported. Sadly, this excludes both pypy and 63 | CPython 3. 64 | 65 | ``gitifyhg`` requires at least Mercurial 2.5, older versions are currently 66 | not supported. We perform continuous testing against various Mercurial 67 | versions ranging from 2.5 to 4.0.1. However, this does not completely rule 68 | out the possibility of compatibility issues, so we recommend using Mercurial 69 | 3.9.x or 4.0.x, as this is what ``gitifyhg`` is primarily developed for. 70 | Should you actually encounter any compatibility issues with any older or 71 | newer Mercurial versions, please submit an issue. 72 | 73 | It has been tested on Arch Linux and Mac OS X. In general it should 74 | work equally well on other Unix-like operating systems like *BSD or Solaris. 75 | All bets are off with Windows, but please let us know if it works or you fixed 76 | it. 77 | 78 | ``gitifyhg`` explicitly depends on: 79 | 80 | * `path.py `_ 81 | * `Mercurial `_ 82 | 83 | These packages will be installed automatically by ``easy_install``, 84 | ``pip``, ``setup.py install``, or ``setup.py develop``. 85 | 86 | ``gitifyhg`` also expects the following to be installed on your OS: 87 | 88 | * `python2 `_ 89 | * `git `_ 90 | 91 | Install 92 | ------- 93 | ``gitifyhg`` is a properly designed Python package. You can get it from 94 | `pypi `_ using either :: 95 | 96 | pip install gitifyhg 97 | 98 | or :: 99 | 100 | easy_install gitifyhg 101 | 102 | ``gitifyhg`` works in a `virtualenv `_, but you're 103 | probably just as well off to install it at the system level. 104 | 105 | You can also install ``gitifyhg`` manually with :: 106 | 107 | git clone https://github.com/buchuki/gitifyhg.git 108 | python setup.py install 109 | 110 | If you want to hack on it, use ``setup.py develop``, instead. In this case, you 111 | probably **are** better off using a ``virtualenv``. 112 | 113 | Instructions 114 | ------------ 115 | ``gitifyhg`` is a git remote. Once installed, you can clone any Mercurial repo 116 | using :: 117 | 118 | git clone gitifyhg:: 119 | 120 | Now run ``git branch -r`` to see the list of Mercurial branches. If it was 121 | a named branch upstream, it will be named branches/ in git. 122 | Bookmarks are referred to directly by their name. 123 | For now, we recommend only interacting with named branches. 124 | 125 | ``master`` automatically tracks the default branch. You can check out any 126 | named mercurial branch using :: 127 | 128 | git checkout --track origin/branches/ 129 | 130 | As a standard git practice, we recommend creating your own local branch 131 | to work on. Then change to the tracked branch and ``git pull`` to get 132 | upstream changes. Rebase your working branch onto that branch before pushing :: 133 | 134 | git checkout -b working_ 135 | # hack add commit ad nauseam 136 | git checkout branches/ 137 | git pull 138 | git checkout working_ 139 | git rebase branches/ 140 | git checkout branches/ 141 | git merge working_ 142 | git push 143 | 144 | You can create new named upstream branches by giving them the ``branches/`` 145 | prefix :: 146 | 147 | git checkout -b "branches/my_new_branch" 148 | # hack add commit 149 | git push --set-upstream origin branches/my_new_branch 150 | 151 | And that's really it, you just use standard git commands and the remote 152 | takes care of the details. Just be cautious of incoming anonymous branches, 153 | don't do any octopus merges and you should be set. 154 | 155 | Caveats 156 | ~~~~~~~ 157 | Mercurial allows spaces in branch, bookmark, and tag names, while 158 | git does not. To keep git from choking if upstream has spaces in names, gitifyhg 159 | will replace them with three underscores and has the sense to convert between 160 | the two formats when pushing and pulling. 161 | 162 | Mercurial does not support lightweight tags. Tags in mercurial that get pushed 163 | to the remote repo require an extra commit in he mercurial history. If you push 164 | a lightweight tag, then gitifyhg will set a default user, date, and commit 165 | message for you. However, if you create a heavyweight tag using 166 | ``git tag --message="commit message"``, gitifyhg will use the commit 167 | information associated with that tag when you run ``git push --tags``. 168 | 169 | By default, gitifyhg ignores branches that have been closed in Mercurial. This 170 | supplies a substantial cloning speedup on large repos, and alleviates a few 171 | issues we are still working out in conflicting branch names. If you would like 172 | to clone a repository including closed branches, first set the 173 | GITIFYHG_ALLOW_CLOSED_BRANCHES environment variable. 174 | 175 | If you have any trouble, please let us know via the issue tracker, preferably 176 | with pull requests containing test cases. 177 | 178 | Communicating with Mercurial Users 179 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 180 | One problem with using git to access Mercurial repos is that the sha identifiers 181 | in the two DVCSs are different. This makes it difficult to discuss or share 182 | patches on mailing lists or other mediums. 183 | 184 | Gitifyhg alleviates this by storing Mercurial's sha1 identifiers in a git-notes 185 | ref. If you need to discuss SHA1s with upstream Mercurial users, issue 186 | the following commands:: 187 | 188 | $ ls .git/refs/notes/ 189 | hg hg-ceda6818a39a022ef11ba5ee2d7964f57cb3accf 190 | # note the SHA1 above and adapt the following command 191 | git symbolic-ref refs/notes/hg refs/notes/hg-ceda6818a39a022ef11ba5ee2d7964f57cb3accf 192 | git config core.notesRef refs/notes/hg 193 | 194 | From now on, your git-log output will include lines that look like the 195 | following for each pulled ref:: 196 | 197 | Notes (hg): 198 | e6eabc9d7e24f55e829d0848380f6645e57f4b6a 199 | 200 | That is the Mercurial SHA1 identifier of the commit in question; you can paste 201 | that into an e-mail or chat message to discuss a specific commit with other 202 | users. 203 | 204 | If somebody else mentions a commit by it's hg SHA1 identifier, you can search 205 | for that commit in git using:: 206 | 207 | git log --grep= 208 | 209 | Development 210 | ----------- 211 | You can hack on gitifyhg by forking the 212 | `github `_ repository. All the code is 213 | in the ``gitifyhg.py`` file, and tests are in the ``test`` directory. 214 | 215 | We recommend developing in a `virtualenv `_ :: 216 | 217 | cd gitifyhg 218 | virtualenv -p python2.7 venv 219 | . venv/bin/activate 220 | python setup.py develop 221 | 222 | There is currently a problem where if you have a development version of gitifyhg 223 | in an active virtualenv and a stable version installed at the system level, git 224 | will pick the system level gitifyhg regardless of the PATH setting in the 225 | virtualenv. The only workaround I have found is to temporarily uninstall the 226 | system version. 227 | 228 | If you want debugging information out of gitifyhg, set the DEBUG_GITIFYHG=on 229 | environment variable. This is done automatically if you are running the test 230 | suite. 231 | 232 | The gitifyhg remote is called by git and commands are passed on stdin. 233 | Output is sent to stdout. The protocol is described at 234 | https://www.kernel.org/pub/software/scm/git/docs/git-remote-helpers.html 235 | The git remote prints INPUT and OUTPUT lines for each of these to help 236 | introspect the protocol. 237 | 238 | We expect pep8 compliance on contributions. If possible, enable highlighting 239 | of pep8 violations in your editor before committing. 240 | 241 | The gitifyhg mailing list is hosted on 242 | `Google groups `_, but we 243 | prefer the `issue tracker `_ 244 | for most development and decision-making related discussions. 245 | 246 | Testing 247 | ======= 248 | 249 | Tests are continuously run by Travis-CI: |BuildStatus|_ 250 | 251 | .. |BuildStatus| image:: https://secure.travis-ci.org/buchuki/gitifyhg.png 252 | .. _BuildStatus: http://travis-ci.org/buchuki/gitifyhg 253 | 254 | Note that testing has recently changed. We used to use `py.test `_ 255 | and `tox `_ to run our tests. We've recently switched to 256 | `sharness `_ both because it's easier to 257 | test command-line tools with and because it is the same infrastructure used by 258 | git itself. 259 | 260 | To test with sharness, simply `cd test` and run `make`. You can run individual 261 | test files with `./test-name.t`. 262 | 263 | License 264 | ------- 265 | 266 | gitifyhg is copyright 2012-2013 Dusty Phillips and is licensed under the 267 | `GNU General Public License `_ 268 | 269 | Credits 270 | ------- 271 | Dusty Phillips is the primary author of ``gitifyhg``. 272 | 273 | The current version was heavily inspired by and borrows code from Felipe Contreras's 274 | `git-remote-hg `_ 275 | project. 276 | 277 | Other contributors include (alphabetical order): 278 | 279 | * Alex Sydell 280 | * Jason Chu 281 | * Jed Brown 282 | * Max Horn 283 | * Paul Price 284 | -------------------------------------------------------------------------------- /git-remote-testgitifyhg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # This bin script is intended only for testing. 4 | # It will not be used by the installed egg. 5 | # 6 | # For testing, just put the path to this bin script ahead of all 7 | # others, e.g.: 8 | # export PATH=/path/to/gitifyhg/bin:$PATH 9 | 10 | import os 11 | import sys 12 | 13 | # Set up sys.path so our package is before all others 14 | # (including eggs of ourselves that have been installed). 15 | bindir = os.path.dirname(__file__) 16 | sys.path.insert(1, os.path.abspath(os.path.join(bindir, ".."))) 17 | 18 | from gitifyhg import gitifyhg 19 | gitifyhg.main() 20 | -------------------------------------------------------------------------------- /gitifyhg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusty-phillips/gitifyhg/58767ad31d03e3cbb26db25f268b05e95fa84e21/gitifyhg/__init__.py -------------------------------------------------------------------------------- /gitifyhg/apiwrapper.py: -------------------------------------------------------------------------------- 1 | from mercurial.context import memctx, memfilectx 2 | from mercurial.util import version as hg_version 3 | from distutils.version import StrictVersion 4 | from mercurial import extensions 5 | 6 | # Conditional imports depending on Mercurial version 7 | if hg_version() >= '3.7': 8 | from mercurial.bookmarks import _readactive 9 | elif hg_version() >= '3.5' and hg_version() < '3.7': 10 | from mercurial.bookmarks import readactive 11 | else: 12 | from mercurial.bookmarks import readcurrent 13 | 14 | if hg_version() >= '3.2': 15 | from mercurial.util import digester 16 | else: 17 | from mercurial.util import sha1 18 | 19 | if hg_version() >= '3.0': 20 | from mercurial import exchange 21 | else: 22 | from mercurial.localrepo import localrepository as exchange 23 | 24 | # Functions wrapping the Mercurial API. They follow the naming convention of 25 | # hg_[function name] 26 | 27 | def hg_pull(repo, peer, heads=None, force=False): 28 | return exchange.pull(repo, peer, heads=heads, force=force) 29 | 30 | def hg_push(repo, peer, force=False, newbranch=None): 31 | return exchange.push(repo, peer, force=force, newbranch=newbranch) 32 | 33 | def hg_readactive(repo): 34 | if hg_version() >= '3.7': 35 | return _readactive(repo,repo._bookmarks) 36 | elif hg_version() >= '3.5' and hg_version() < '3.7': 37 | return readactive(repo) 38 | else: 39 | return readcurrent(repo) 40 | 41 | def hg_sha1(url): 42 | encoded = url.encode('utf-8') 43 | 44 | if hg_version() >= '3.2': 45 | d = digester(['md5', 'sha1']) 46 | d.update(encoded) 47 | return d['sha1'] 48 | else: 49 | return sha1(encoded).hexdigest() 50 | 51 | def hg_memfilectx(repo, path, data, is_link=False, is_exec=False, copied=None): 52 | if hg_version() >= '3.1': 53 | return memfilectx(repo, path, data, is_link, is_exec, copied) 54 | else: 55 | return memfilectx(path, data, is_link, is_exec, copied) 56 | 57 | def hg_strip(repo, processed_nodes): 58 | class dummyui(object): 59 | def debug(self, msg): 60 | pass 61 | 62 | if StrictVersion(hg_version()) >= StrictVersion('2.8'): 63 | stripext = extensions.load(dummyui(), 'strip', '') 64 | return stripext.strip(dummyui(), repo, processed_nodes) 65 | else: 66 | return repo.mq.strip(repo, processed_nodes) 67 | 68 | # Helper Functions to help with changes to the mercurial API 69 | 70 | def handle_deleted_file(): 71 | if hg_version() >= '3.2': 72 | return 73 | else: 74 | raise IOError -------------------------------------------------------------------------------- /gitifyhg/gitexporter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Dusty Phillips 2 | 3 | # This file is part of gitifyhg. 4 | 5 | # gitifyhg is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # gitifyhg is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with gitifyhg. If not, see . 17 | 18 | # Some of this code comes from https://github.com/felipec/git/tree/fc/remote/hg 19 | # but much of it has been rewritten. 20 | 21 | from mercurial.context import memctx 22 | from mercurial.error import Abort 23 | from mercurial.node import hex as hghex # What idiot overrode a builtin? 24 | from mercurial.node import short as hgshort 25 | from mercurial.bookmarks import pushbookmark 26 | from mercurial.scmutil import revsingle 27 | from mercurial.util import version as hg_version 28 | from mercurial import encoding 29 | 30 | from .util import (die, output, git_to_hg_spaces, hgmode, branch_tip, 31 | ref_to_name_reftype, BRANCH, BOOKMARK, TAG, user_config) 32 | 33 | from apiwrapper import (hg_strip, hg_memfilectx, hg_push, handle_deleted_file) 34 | 35 | class GitExporter(object): 36 | 37 | '''A processor when the remote receives a git-remote `export` command. 38 | Provides export information to push commits from git to the mercurial 39 | repository.''' 40 | 41 | NULL_PARENT = '\0' * 20 42 | 43 | def __init__(self, hgremote, parser): 44 | self.hgremote = hgremote 45 | self.marks = self.hgremote.marks 46 | self.parsed_refs = self.hgremote.parsed_refs 47 | self.parsed_tags = {} # refs to tuple of (message, author) 48 | self.blob_marks = self.hgremote.blob_marks 49 | self.repo = self.hgremote.repo 50 | self.parser = parser 51 | self.processed_marks = set() 52 | self.processed_nodes = [] 53 | self.hgrc = user_config() 54 | 55 | def process(self): 56 | self.marks.store() # checkpoint 57 | new_branch = False 58 | push_bookmarks = [] 59 | self.parser.read_line() 60 | for line in self.parser.read_block('done'): 61 | command = line.split()[0] 62 | if command not in ('blob', 'commit', 'reset', 'tag', 'feature'): 63 | die('unhandled command: %s' % line) 64 | getattr(self, 'do_%s' % command)() 65 | 66 | updated_refs = {} 67 | for ref, node in self.parsed_refs.iteritems(): 68 | if ref.startswith(self.hgremote.prefix): 69 | # This seems to be a git fast-export bug 70 | continue 71 | name, reftype = ref_to_name_reftype(ref) 72 | name = git_to_hg_spaces(name) 73 | if reftype == BRANCH: 74 | if name not in self.hgremote.branches: 75 | new_branch = True 76 | elif reftype == BOOKMARK: 77 | old = self.hgremote.bookmarks.get(name) 78 | old = old.hex() if old else '' 79 | if not pushbookmark(self.repo, name, old, node): 80 | continue 81 | push_bookmarks.append((name, old, hghex(node))) 82 | elif reftype == TAG: 83 | self.write_tag(name, node) 84 | else: 85 | assert False, "unexpected reftype: %s" % reftype 86 | updated_refs[ref] = node 87 | 88 | success = False 89 | try: 90 | hg_push(self.repo, self.hgremote.peer, False, new_branch) 91 | 92 | for bookmark, old, new in push_bookmarks: 93 | self.hgremote.peer.pushkey('bookmarks', bookmark, old, new) 94 | self.marks.store() 95 | success = True 96 | except Abort as e: 97 | # mercurial.error.Abort: push creates new remote head f14531ca4e2d! 98 | if e.message.startswith("push creates new remote head"): 99 | self.marks.load() # restore from checkpoint 100 | # strip revs, implementation finds min revision from list 101 | if self.processed_nodes: 102 | hg_strip(self.repo, self.processed_nodes) 103 | else: 104 | die("unknown hg exception: %s" % e) 105 | # TODO: handle network/other errors? 106 | 107 | for ref, node in updated_refs.items(): 108 | if success: 109 | status = "" 110 | name, reftype = ref_to_name_reftype(ref) 111 | gitify_ref = self.hgremote.make_gitify_ref(name, reftype) 112 | last_known_rev = self.marks.tips.get(gitify_ref) 113 | new_rev = self.repo[node].rev() 114 | if last_known_rev is not None and last_known_rev == new_rev: 115 | # up to date status tells git that nothing has changed 116 | # during the push for this ref, which prevents it from 117 | # printing pointless status info to the user such as: 118 | # * [new branch] master -> master 119 | status = " up to date" 120 | output("ok %s%s" % (ref, status)) 121 | else: 122 | output("error %s non-fast forward" % ref) # TODO: other errors as well 123 | output() 124 | 125 | if not success: 126 | # wait until fast-export finishes to muck with the marks file 127 | self.remove_processed_git_marks() 128 | 129 | def remove_processed_git_marks(self): 130 | with self.hgremote.marks_git_path.open() as fread: 131 | with self.hgremote.marks_git_path.open('r+') as fwrite: 132 | for line in fread: 133 | if not line.startswith(':'): 134 | die("invalid line in marks-git: " + line) 135 | mark = line[1:].split()[0] 136 | if mark not in self.processed_marks: 137 | fwrite.write(line) 138 | fwrite.truncate() 139 | 140 | def do_blob(self): 141 | mark = self.parser.read_mark() 142 | self.blob_marks[mark] = self.parser.read_data() 143 | self.parser.read_line() 144 | 145 | def do_reset(self): 146 | ref = self.parser.line.split()[1] 147 | 148 | # If the next line is a commit, allow it to process normally 149 | if not self.parser.peek().startswith('from'): 150 | return 151 | 152 | from_mark = self.parser.read_mark() 153 | from_revision = self.marks.mark_to_revision(from_mark) 154 | self.parsed_refs[ref] = from_revision 155 | 156 | # skip a line 157 | self.parser.read_line() 158 | 159 | def do_commit(self): 160 | files = {} 161 | extra = {} 162 | from_mark = merge_mark = None 163 | 164 | ref = self.parser.line.split()[1] 165 | 166 | commit_mark = self.parser.read_mark() 167 | author = self.parser.read_author() 168 | committer = self.parser.read_author() 169 | data = self.parser.read_data() 170 | if self.parser.peek().startswith('from'): 171 | from_mark = self.parser.read_mark() 172 | if self.parser.peek().startswith('merge'): 173 | merge_mark = self.parser.read_mark() 174 | if self.parser.peek().startswith('merge'): 175 | die('Octopus merges are not yet supported') 176 | 177 | self.parser.read_line() 178 | 179 | for line in self.parser.read_block(''): 180 | if line.startswith('M'): 181 | t, mode, mark_ref, path = line.split(' ', 3) 182 | mark = int(mark_ref[1:]) 183 | filespec = {'mode': hgmode(mode), 'data': self.blob_marks[mark]} 184 | elif line.startswith('D'): 185 | t, path = line.split(' ', 1) 186 | filespec = {'deleted': True} 187 | if path[0] == '"' and path[-1] == '"': 188 | path = path.decode('string-escape')[1:-1] 189 | files[path] = filespec 190 | 191 | user, date, tz = author 192 | 193 | if committer != author: 194 | extra['committer'] = "%s %u %u" % committer 195 | 196 | if from_mark: 197 | parent_from = self.marks.mark_to_revision(from_mark) 198 | else: 199 | parent_from = self.NULL_PARENT 200 | 201 | if merge_mark: 202 | parent_merge = self.marks.mark_to_revision(merge_mark) 203 | else: 204 | parent_merge = self.NULL_PARENT 205 | 206 | # hg needs to know about files that changed from either parent 207 | # whereas git only cares if it changed from the first parent. 208 | if merge_mark: 209 | for file in self.repo[parent_from].files(): 210 | if file not in files and file in\ 211 | self.repo[parent_from].manifest(): 212 | files[file] = {'ctx': self.repo[parent_from][file]} 213 | 214 | name, reftype = ref_to_name_reftype(ref) 215 | if reftype == BRANCH: 216 | extra['branch'] = git_to_hg_spaces(name) 217 | 218 | def get_filectx(repo, memctx, file): 219 | filespec = files[file] 220 | 221 | if 'deleted' in filespec: 222 | return handle_deleted_file() 223 | if 'ctx' in filespec: 224 | return filespec['ctx'] 225 | is_exec = filespec['mode'] == 'x' 226 | is_link = filespec['mode'] == 'l' 227 | rename = filespec.get('rename', None) 228 | 229 | return hg_memfilectx(repo,file, filespec['data'],is_link, is_exec, rename) 230 | 231 | ctx = memctx(self.repo, (parent_from, parent_merge), data, 232 | files.keys(), get_filectx, user, (date, tz), extra) 233 | 234 | tmp = encoding.encoding 235 | encoding.encoding = 'utf-8' 236 | node = self.repo.commitctx(ctx) 237 | encoding.encoding = tmp 238 | 239 | self.parsed_refs[ref] = node 240 | self.marks.new_mark(node, commit_mark) 241 | self.processed_marks.add(str(commit_mark)) 242 | self.processed_nodes.append(node) 243 | 244 | def do_tag(self): 245 | name = self.parser.line.split()[1] 246 | self.parser.read_mark() 247 | tagger = self.parser.read_author() 248 | message = self.parser.read_data() 249 | self.parser.read_line() 250 | self.parsed_tags[git_to_hg_spaces(name)] = tagger, message 251 | 252 | def do_feature(self): 253 | pass # Ignore 254 | 255 | def write_tag(self, name, node): 256 | branch = self.repo[node].branch() 257 | # Calling self.repo.tag() doesn't append the tag to the correct 258 | # commit. So I copied some of localrepo._tag into here. 259 | # But that method, like much of mercurial's code, is ugly. 260 | # So I then rewrote it. 261 | 262 | tags_revision = revsingle(self.repo, hghex(branch_tip(self.repo, branch))) 263 | if '.hgtags' in tags_revision: 264 | old_tags = tags_revision['.hgtags'].data() 265 | else: 266 | old_tags = '' 267 | newtags = [old_tags] 268 | if old_tags and old_tags[-1] != '\n': 269 | newtags.append('\n') 270 | 271 | encoded_tag = encoding.fromlocal(name) 272 | tag_line = '%s %s' % (hghex(node), encoded_tag) 273 | if tag_line in old_tags: 274 | return # Don't commit a tag that was previously committed 275 | newtags.append(tag_line) 276 | 277 | def get_filectx(repo, memctx, file): 278 | return hg_memfilectx(repo, file, ''.join(newtags)) 279 | 280 | if name in self.parsed_tags: 281 | author, message = self.parsed_tags[name] 282 | user, date, tz = author 283 | date_tz = (date, tz) 284 | else: 285 | message = "Added tag %s for changeset %s" % (name, hgshort(node)) 286 | user = self.hgrc.get("ui", "username", None) 287 | date_tz = None # XXX insert current date here 288 | ctx = memctx(self.repo, 289 | (branch_tip(self.repo, branch), self.NULL_PARENT), message, 290 | ['.hgtags'], get_filectx, user, date_tz, {'branch': branch}) 291 | 292 | tmp = encoding.encoding 293 | encoding.encoding = 'utf-8' 294 | node = self.repo.commitctx(ctx) 295 | encoding.encoding = tmp 296 | -------------------------------------------------------------------------------- /gitifyhg/gitifyhg.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Dusty Phillips 2 | 3 | # This file is part of gitifyhg. 4 | 5 | # gitifyhg is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # gitifyhg is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with gitifyhg. If not, see . 17 | 18 | # Some of this code comes from https://github.com/felipec/git/tree/fc/remote/hg 19 | # but much of it has been rewritten. 20 | 21 | import sys 22 | import os 23 | import re 24 | import optparse 25 | import subprocess 26 | from path import Path as p 27 | 28 | # Enable "plain" mode to make us resilient against changes to the locale, as we 29 | # rely on parsing certain messages produced by Mercurial. See issue #26. 30 | os.environ['HGPLAIN'] = '1' 31 | # Disable loading of the user's $HOME/.hgrc as extensions can cause weird 32 | # interactions and it's better to run in a known state. 33 | os.environ['HGRCPATH'] = '' 34 | 35 | from .util import (log, die, output, branch_head, GitMarks, 36 | HGMarks, hg_to_git_spaces, name_reftype_to_ref, BRANCH, BOOKMARK, TAG, 37 | version, deactivate_stdout) 38 | 39 | # Version specific libraries from the Mercurial API 40 | from mercurial import hg 41 | from mercurial import encoding 42 | from mercurial.bookmarks import listbookmarks 43 | from mercurial.ui import ui 44 | from mercurial.error import Abort, RepoError 45 | from mercurial.util import version as hg_version 46 | 47 | from .util import (log, die, output, branch_head, GitMarks, 48 | HGMarks, hg_to_git_spaces, name_reftype_to_ref, BRANCH, BOOKMARK, TAG, 49 | version, deactivate_stdout) 50 | 51 | from apiwrapper import (hg_sha1, hg_readactive, hg_pull) 52 | from .hgimporter import HGImporter 53 | from .gitexporter import GitExporter 54 | 55 | class GitRemoteParser(object): 56 | '''Parser for stdin that processes the git-remote protocol.''' 57 | 58 | def __init__(self): 59 | self.peek_stack = [] 60 | self.read_line() 61 | 62 | def read_line(self): 63 | '''Read a line from the standard input.''' 64 | if self.peek_stack: 65 | self.line = self.peek_stack.pop(0) 66 | else: 67 | self.line = sys.stdin.readline().strip() 68 | log("INPUT: %s" % self.line) 69 | return self.line 70 | 71 | def peek(self): 72 | '''Look at the next line and store it so that it can still be returned 73 | by read_line.''' 74 | line = sys.stdin.readline().strip() 75 | self.peek_stack.append(line) 76 | return line 77 | 78 | def read_mark(self): 79 | '''The remote protocol contains lines of the format mark: number. 80 | Return the mark.''' 81 | return int(self.read_line().partition(':')[-1]) 82 | 83 | def read_data(self): 84 | '''Read all data following a data line for the given number of bytes''' 85 | self.read_line() 86 | if not self.line.startswith('data'): 87 | return None 88 | size = int(self.line.partition(' ')[-1]) 89 | return sys.stdin.read(size) 90 | 91 | def read_author(self): 92 | '''Read and parse an author string. Return a tuple of 93 | (user string, date, git_tz).''' 94 | self.read_line() 95 | AUTHOR_RE = re.compile(r'^(?:author|committer|tagger)(?: ([^<>]+)?)? <([^<>]*)> (\d+) ([+-]\d+)') 96 | match = AUTHOR_RE.match(self.line) 97 | if not match: 98 | return None 99 | 100 | user, email, date, tz = match.groups() 101 | if user is None: 102 | user = '' 103 | user += ' <' + email + '>' 104 | 105 | date = int(date) 106 | tz = -(((int(tz) / 100) * 3600) + ((int(tz) % 100) * 60)) 107 | return (user, date, tz) 108 | 109 | def read_block(self, sentinel): 110 | '''Yield a block of lines one by one until the sentinel value 111 | is returned. Sentinel may be an empty string, 'done', or other values 112 | depending on what block is being read.''' 113 | while self.line != sentinel: 114 | yield self.line 115 | self.line = self.read_line() 116 | 117 | class HGRemote(object): 118 | def __init__(self, alias, url): 119 | if hg.islocal(url.encode('utf-8')): 120 | url = p(url).abspath() 121 | # Force git to use an absolute path in the future 122 | remote_name = os.path.basename(sys.argv[0]).replace("git-remote-", "") 123 | cmd = ['git', 'config', 'remote.%s.url' % alias, "%s::%s" % ( 124 | remote_name, url)] 125 | subprocess.call(cmd) 126 | 127 | # use hash of URL as unique identifier in various places. 128 | # this has the advantage over 'alias' that it stays constant 129 | # when the user does a "git remote rename old new". 130 | 131 | self.uuid = hg_sha1(url) 132 | 133 | gitdir = p(os.environ['GIT_DIR'].decode('utf-8')) 134 | self.remotedir = gitdir.joinpath('hg', self.uuid) 135 | self.marks_git_path = self.remotedir.joinpath('marks-git') 136 | self.marks_hg_path = self.remotedir.joinpath('marks-hg') 137 | self.marks = HGMarks(self.marks_hg_path) 138 | self.git_marks = GitMarks(self.marks_git_path) 139 | self.parsed_refs = {} 140 | self.blob_marks = {} 141 | self.branches = {} 142 | self.bookmarks = {} 143 | 144 | self.prefix = 'refs/hg/%s' % alias 145 | self.alias = alias 146 | self.url = url 147 | self.build_repo(url) 148 | 149 | def build_repo(self, url): 150 | '''Make the Mercurial repo object self.repo available. If the local 151 | clone does not exist, clone it, otherwise, ensure it is fetched.''' 152 | myui = ui() 153 | myui.setconfig('ui', 'interactive', 'off') 154 | myui.setconfig('extensions', 'mq', '') 155 | # FIXME: the following is a hack to achieve hg-git / remote-git compatibility 156 | # at least for *local* operations. still need to figure out what the right 157 | # thing to do is. 158 | myui.setconfig('phases', 'publish', False) 159 | 160 | local_path = self.remotedir.joinpath('clone') 161 | if not local_path.exists(): 162 | try: 163 | self.peer, dstpeer = hg.clone(myui, {}, url.encode('utf-8'), 164 | local_path.encode('utf-8'), update=False, pull=True) 165 | except (RepoError, Abort) as e: 166 | sys.stderr.write("abort: %s\n" % e) 167 | if e.hint: 168 | sys.stderr.write("(%s)\n" % e.hint) 169 | sys.exit(-1) 170 | 171 | self.repo = dstpeer.local() 172 | else: 173 | self.repo = hg.repository(myui, local_path.encode('utf-8')) 174 | self.peer = hg.peer(myui, {}, url.encode('utf-8')) 175 | hg_pull(self.repo, self.peer, None, True) 176 | 177 | self.marks.upgrade_marks(self) 178 | 179 | def make_gitify_ref(self, name, reftype): 180 | if not isinstance(name, unicode): 181 | name = name.decode('utf-8') 182 | if reftype == BRANCH: 183 | if name == 'default': 184 | # I have no idea where 'bookmarks' comes from in this case. 185 | # I don't think there is meant to be many bookmarks/master ref, 186 | # but this is what I had to do to make tests pass when special 187 | # casing the master/default dichotomy. Something is still fishy 188 | # here, but it's less fishy than it was. See issue #34. 189 | return "%s/bookmarks/master" % self.prefix 190 | else: 191 | return '%s/branches/%s' % (self.prefix, name) 192 | elif reftype == BOOKMARK: 193 | return '%s/bookmarks/%s' % (self.prefix, name) 194 | elif reftype == TAG: 195 | return '%s/tags/%s' % (self.prefix, name) 196 | else: 197 | assert False, "unknown reftype: %s" % reftype 198 | 199 | def process(self): 200 | '''Process the messages coming in on stdin using the git-remote 201 | protocol and respond appropriately''' 202 | parser = GitRemoteParser() 203 | 204 | for line in parser.read_block(''): 205 | command = line.split()[0] 206 | if command not in ('capabilities', 'list', 'import', 'export'): 207 | die('unhandled command: %s' % line) 208 | getattr(self, 'do_%s' % command)(parser) 209 | 210 | try: 211 | self.marks.store() 212 | except IOError as e: 213 | if e.errno == 2 and e.filename == self.marks_hg_path: 214 | log("The marks file has been removed. This usually suggests " 215 | "that a git clone operation failed. " 216 | "To debug, set environment variable DEBUG_GITIFYHG " 217 | "and rerun. ", "ERROR") 218 | die("Error updating marks.") 219 | raise 220 | 221 | def do_capabilities(self, parser): 222 | '''Process the capabilities request when incoming from git-remote. 223 | ''' 224 | output(u"import") 225 | output(u"export") 226 | for reftype in (BRANCH, BOOKMARK, TAG): 227 | output(u"refspec %s:%s" % 228 | (name_reftype_to_ref('*', reftype), 229 | self.make_gitify_ref('*', reftype))) 230 | 231 | if self.marks_git_path.exists(): 232 | output(u"*import-marks %s" % self.marks_git_path) 233 | output(u"*export-marks %s" % self.marks_git_path) 234 | 235 | output() 236 | 237 | def _change_hash(self, changectx): 238 | node = changectx.node() 239 | if node and self.marks.is_marked(node): 240 | mark = self.marks.revision_to_mark(node) 241 | if self.git_marks.has_mark(mark): 242 | return self.git_marks.mark_to_hash(mark) 243 | return '?' 244 | 245 | def do_list(self, parser): 246 | '''List all references in the mercurial repository. This includes 247 | the current head, all branches, tags, and bookmarks.''' 248 | 249 | current_branch = self.repo.dirstate.branch() 250 | 251 | # Update the head reference 252 | head = hg_readactive(self.repo) 253 | 254 | if head: 255 | node = self.repo[head] 256 | else: 257 | # If there is no bookmark for head, mock one 258 | head = current_branch 259 | node = self.repo['.'] 260 | # I think this means an initial clone occured and we haven't 261 | # hg updated yet in the local clone 262 | if not node: 263 | if 'default' in self.repo: 264 | node = self.repo['default'] 265 | else: # empty repository or head is at 0 commit 266 | output() 267 | return 268 | head = head if head != 'default' else 'master' 269 | #self.bookmarks[head] = node 270 | 271 | self.headnode = (head, node) 272 | 273 | # Update the bookmark references 274 | for bookmark, node in listbookmarks(self.repo).iteritems(): 275 | self.bookmarks[bookmark] = self.repo[node] 276 | 277 | # update the named branch references 278 | for branch in self.repo.branchmap(): 279 | # FIXME: Probably a git config instead of an env var would make 280 | # people happier here. 281 | clone_closed = os.environ.get("GITIFYHG_ALLOW_CLOSED_BRANCHES") != None 282 | heads = self.repo.branchheads(branch, closed=clone_closed) 283 | if heads: 284 | self.branches[branch] = heads 285 | 286 | # list the head reference 287 | output("@refs/heads/%s HEAD" % self.headnode[0]) 288 | 289 | # list the named branch references 290 | for branch in self.branches: 291 | output("%s %s" % 292 | (self._change_hash(branch_head(self, branch)), 293 | name_reftype_to_ref(hg_to_git_spaces(branch), BRANCH))) 294 | 295 | # list the bookmark references 296 | for bookmark, changectx in self.bookmarks.items(): 297 | if bookmark != "master": 298 | output("%s %s" % 299 | (self._change_hash(changectx), 300 | name_reftype_to_ref(hg_to_git_spaces(bookmark), BOOKMARK))) 301 | 302 | # list the tags 303 | for tag, node in self.repo.tagslist(): 304 | if tag != "tip": 305 | output("%s %s" % 306 | (self._change_hash(self.repo[node]), 307 | name_reftype_to_ref(hg_to_git_spaces(tag), TAG))) 308 | 309 | output() 310 | 311 | def do_import(self, parser): 312 | HGImporter(self, parser).process() 313 | 314 | def do_export(self, parser): 315 | GitExporter(self, parser).process() 316 | 317 | def log_versions(level="DEBUG"): 318 | log("gitifyhg version %s" % version(), level=level) 319 | log("Mercurial version %s" % hg_version(), level=level) 320 | log("Python version %s" % (sys.version.replace("\n", "")), level=level) 321 | 322 | def main(): 323 | '''Main entry point for the git-remote-gitifyhg command. Parses sys.argv 324 | and constructs a parser from the result. 325 | ''' 326 | log_versions() 327 | 328 | name = os.path.basename(sys.argv[0]).replace("git-remote-", "") 329 | description = """This is a remote helper for git to interact with hg. 330 | You should generally not call this executable directly; it will be called 331 | by git if you put this executable on your PATH and set your git remote to: 332 | %s:: 333 | """ % name 334 | 335 | parser = optparse.OptionParser(usage="usage: %prog [options] ", description=description) 336 | parser.add_option("-v", "--version", default=False, action="store_true", 337 | help="Print version number only") 338 | opts, args = parser.parse_args() 339 | if opts.version: 340 | log_versions("VERSION") 341 | sys.exit(0) 342 | if not args: 343 | parser.print_help() 344 | sys.exit(0) 345 | 346 | deactivate_stdout() 347 | HGRemote(*[x.decode('utf-8') for x in args]).process() 348 | try: 349 | sys.stderr.close() 350 | except: 351 | pass 352 | 353 | if __name__ == '__main__': 354 | sys.exit(main()) 355 | -------------------------------------------------------------------------------- /gitifyhg/hgimporter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Dusty Phillips 2 | 3 | # This file is part of gitifyhg. 4 | 5 | # gitifyhg is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # gitifyhg is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with gitifyhg. If not, see . 17 | 18 | # Some of this code comes from https://github.com/felipec/git/tree/fc/remote/hg 19 | # but much of it has been rewritten. 20 | 21 | import time 22 | import re 23 | 24 | from mercurial import encoding 25 | 26 | from .util import (log, output, gittz, gitmode, 27 | git_to_hg_spaces, hg_to_git_spaces, branch_head, ref_to_name_reftype, 28 | BRANCH, BOOKMARK, TAG, relative_path) 29 | 30 | AUTHOR = re.compile(r'^([^<>]+)?(<(?:[^<>]*)>| [^ ]*@.*|[<>].*)$') 31 | 32 | 33 | def sanitize_author(author): 34 | '''Mercurial allows a more freeform user string than git, so we have to 35 | massage it to be compatible. Git expects "name ", where email can be 36 | empty (as long as it's surrounded by <>).''' 37 | name = '' 38 | email = '' 39 | author = author.replace('"', '') 40 | match = AUTHOR.match(author) 41 | if match: 42 | if match.group(1): # handle 'None', e.g for input "" 43 | name = match.group(1).strip() 44 | email = match.group(2).translate(None, "<>").strip() 45 | else: 46 | author = author.translate(None, "<>").strip() 47 | if "@" in author: 48 | email = author 49 | else: 50 | name = author 51 | 52 | if not name: 53 | name = 'Unknown' 54 | 55 | return "%s <%s>" % (name, email) 56 | 57 | 58 | class HGImporter(object): 59 | 60 | '''A processor when the remote receives a git-remote import command. 61 | Provides import information from the mercurial repository to git.''' 62 | def __init__(self, hgremote, parser): 63 | self.hgremote = hgremote 64 | self.marks = self.hgremote.marks 65 | self.prefix = self.hgremote.prefix 66 | self.repo = self.hgremote.repo 67 | self.parser = parser 68 | self.notes_committed = 0 69 | 70 | def process(self): 71 | output("feature done") 72 | if self.hgremote.marks_git_path.exists(): 73 | output("feature import-marks=%s" % self.hgremote.marks_git_path) 74 | output("feature export-marks=%s" % self.hgremote.marks_git_path) 75 | output("feature notes") 76 | 77 | tmp = encoding.encoding 78 | encoding.encoding = 'utf-8' 79 | 80 | self.commit_count = 0 81 | while self.parser.line.startswith('import'): 82 | ref = self.parser.line.split()[1] 83 | 84 | if ref == 'HEAD': 85 | self.process_ref( 86 | self.hgremote.headnode[0], 87 | BOOKMARK, 88 | self.hgremote.headnode[1]) 89 | else: 90 | name, reftype = ref_to_name_reftype(ref) 91 | if reftype == BRANCH: 92 | head = branch_head(self.hgremote, git_to_hg_spaces(name)) 93 | elif reftype == BOOKMARK: 94 | head = self.hgremote.bookmarks[git_to_hg_spaces(name)] 95 | elif reftype == TAG: 96 | head = self.repo[git_to_hg_spaces(name)] 97 | else: 98 | assert False, "unexpected reftype: %s" % reftype 99 | self.process_ref(name, reftype, head) 100 | 101 | self.process_notes() 102 | 103 | self.parser.read_line() 104 | 105 | encoding.encoding = tmp 106 | output('done') 107 | 108 | def process_notes(self): 109 | last_notes_mark = self.marks.notes_mark if self.marks.notes_mark is not None else 0 110 | mark_to_hgsha1 = [(mark, self.repo[rev].hex()) for rev, mark in 111 | self.marks.revisions_to_marks.iteritems() if mark > last_notes_mark] 112 | if not mark_to_hgsha1 or self.commit_count < 1: 113 | return 114 | output("commit refs/notes/hg-%s" % (self.hgremote.uuid)) 115 | output("mark :%d" % (self.marks.new_notes_mark())) 116 | output("committer %s %s" % (int(time.time()), time.strftime('%z'))) 117 | message = u"hg from %s (%s)\n" % (self.prefix, self.hgremote.url) 118 | message = message.encode("utf-8") 119 | output("data %d" % (len(message))) 120 | output(message) 121 | if last_notes_mark > 0: 122 | output("from :%d" % (last_notes_mark)) 123 | for mark, hgsha1 in mark_to_hgsha1: 124 | output("N inline :%d" % (mark)) 125 | output("data 40") 126 | output(hgsha1) 127 | output() 128 | 129 | def process_ref(self, name, reftype, head): 130 | gitify_ref = self.hgremote.make_gitify_ref(name, reftype) 131 | tip = self.marks.tips.get(gitify_ref, 0) 132 | 133 | revs = xrange(tip, head.rev() + 1) 134 | count = 0 135 | 136 | for rev in revs: 137 | node = self.repo[rev].node() 138 | if self.marks.is_marked(node): 139 | continue 140 | 141 | (manifest, user, (time, tz), files, description, extra 142 | ) = self.repo.changelog.read(self.repo[rev].node()) 143 | 144 | user = sanitize_author(user) 145 | author = "%s %d %s" % (user, time, gittz(tz)) 146 | 147 | if 'committer' in extra: 148 | user, time, tz = extra['committer'].rsplit(' ', 2) 149 | user = sanitize_author(user) 150 | committer = "%s %s %s" % (user, time, gittz(int(tz))) 151 | else: 152 | committer = author 153 | 154 | parents = [p for p in self.repo.changelog.parentrevs(rev) if p >= 0] 155 | 156 | if parents: 157 | modified, removed = self.get_filechanges(self.repo[rev], 158 | parents[0]) 159 | else: 160 | modified, removed = self.repo[rev].manifest().keys(), [] 161 | 162 | if not parents and rev: 163 | output('reset %s' % gitify_ref) 164 | 165 | output("commit %s" % gitify_ref) 166 | output("mark :%d" % (self.marks.get_mark(node))) 167 | output("author %s" % (author)) 168 | output("committer %s" % (committer)) 169 | output("data %d" % (len(description))) 170 | output(description) 171 | 172 | if parents: 173 | output("from :%s" % (self.marks.revision_to_mark(self.repo[parents[0]].node()))) 174 | if len(parents) > 1: 175 | output("merge :%s" % (self.marks.revision_to_mark(self.repo[parents[1]].node()))) 176 | 177 | for file in modified: 178 | filecontext = self.repo[rev].filectx(file) 179 | data = filecontext.data() 180 | output("M %s inline %s" % ( 181 | gitmode(filecontext.flags()), relative_path(filecontext.path()))) 182 | output("data %d" % len(data)) 183 | output(data) 184 | for file in removed: 185 | output("D %s" % (relative_path(file))) 186 | output() 187 | 188 | count += 1 189 | if (count % 100 == 0): 190 | output("progress revision %d '%s' (%d/%d)" % ( 191 | rev, name, count, len(revs))) 192 | output("#############################################################") 193 | 194 | # make sure the ref is updated 195 | output("reset %s" % gitify_ref) 196 | output("from :%u" % self.marks.revision_to_mark(head.node())) 197 | output() 198 | 199 | self.marks.tips[gitify_ref] = head.rev() 200 | self.commit_count += count 201 | 202 | def get_filechanges(self, context, parent): 203 | modified = set() 204 | added = set() 205 | removed = set() 206 | 207 | current = context.manifest() 208 | previous = self.repo[parent].manifest().copy() 209 | 210 | for fn in current: 211 | if fn in previous: 212 | if (current.flags(fn) != previous.flags(fn) or current[fn] != previous[fn]): 213 | modified.add(fn) 214 | del previous[fn] 215 | else: 216 | added.add(fn) 217 | removed |= set(previous.keys()) 218 | 219 | return added | modified, removed 220 | -------------------------------------------------------------------------------- /gitifyhg/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | 5 | from mercurial.node import hex as hghex # What idiot overrode a builtin? 6 | from mercurial.node import bin as hgbin 7 | from mercurial.config import config 8 | from mercurial.scmutil import userrcpath 9 | 10 | 11 | DEBUG_GITIFYHG = os.environ.get("DEBUG_GITIFYHG") != None 12 | 13 | 14 | BRANCH = 'branch' 15 | BOOKMARK = 'bookmark' 16 | TAG = 'tag' 17 | 18 | actual_stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) # Ensure stdout is unbuffered 19 | 20 | 21 | def deactivate_stdout(): 22 | """Hijack stdout to prevent mercurial from inadvertently talking to git. 23 | 24 | Mere interactive=off and ui.pushbuffer() don't seem to work. 25 | """ 26 | class DummyOut(object): 27 | def write(self, x): 28 | pass 29 | 30 | def flush(self): 31 | pass 32 | sys.stdout = DummyOut() 33 | 34 | 35 | def log(msg, level="DEBUG"): 36 | '''The git remote operates on stdin and stdout, so all debugging information 37 | has to go to stderr.''' 38 | if DEBUG_GITIFYHG or level != "DEBUG": 39 | sys.stderr.write(u'%s: %r\n' % (level, msg)) 40 | 41 | 42 | def die(msg, *args): 43 | log(msg, 'ERROR', *args) 44 | sys.exit(1) 45 | 46 | 47 | def output(msg=''): 48 | if isinstance(msg, unicode): 49 | msg = msg.encode('utf-8') 50 | log("OUT: %s" % msg) 51 | print >> actual_stdout, msg 52 | 53 | 54 | def version(): 55 | """Return version of gitifyhg""" 56 | try: 57 | import pkg_resources 58 | return pkg_resources.get_distribution("gitifyhg").version 59 | except Exception: 60 | return "UNKNOWN" 61 | 62 | 63 | def gittz(tz): 64 | return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60) 65 | 66 | 67 | def gitmode(flags): 68 | if 'l' in flags: 69 | return '120000' 70 | elif 'x' in flags: 71 | return '100755' 72 | else: 73 | return '100644' 74 | 75 | 76 | def hgmode(mode): 77 | modes = {'100755': 'x', '120000': 'l'} 78 | return modes.get(mode, '') 79 | 80 | 81 | def hg_to_git_spaces(name): 82 | '''Spaces are allowed in mercurial, but not in git. We convert them to 83 | the unlikely string ___''' 84 | return name.replace(' ', '___') 85 | 86 | 87 | def git_to_hg_spaces(name): 88 | '''But when we push back to mercurial, we need to convert it the other way.''' 89 | return name.replace('___', ' ') 90 | 91 | 92 | def branch_tip(repo, branch): 93 | '''HG has a lovely branch_tip method, but it requires mercurial 2.4 94 | This function provides backwards compatibility. If we ever get to 95 | drop older versions, we can drop this function.''' 96 | if hasattr(repo, 'branchtip'): 97 | return repo.branchtip(branch) 98 | else: 99 | return repo.branchtags()[branch] 100 | 101 | 102 | def branch_head(hgremote, branch): 103 | try: 104 | heads = hgremote.branches[branch] 105 | except KeyError: 106 | return 107 | 108 | if len(heads) > 1: 109 | log("Branch '%s' has more than one head, consider merging" % ( 110 | branch), "WARNING") 111 | tip = branch_tip(hgremote.repo, branch) 112 | else: 113 | tip = heads[0] 114 | 115 | return hgremote.repo[tip] 116 | 117 | 118 | def ref_to_name_reftype(ref): 119 | '''Converts a git ref into a name (e.g., the name of that branch, tag, etc.) 120 | and its hg type (one of BRANCH, BOOKMARK, or TAG).''' 121 | if ref == 'refs/heads/master': 122 | return ('default', BRANCH) 123 | elif ref.startswith('refs/heads/branches/'): 124 | return (ref[len('refs/heads/branches/'):], BRANCH) 125 | elif ref.startswith('refs/heads/'): 126 | return (ref[len('refs/heads/'):], BOOKMARK) 127 | elif ref.startswith('refs/tags/'): 128 | return (ref[len('refs/tags/'):], TAG) 129 | else: 130 | assert False, "unexpected ref: %s" % ref 131 | 132 | 133 | def name_reftype_to_ref(name, reftype): 134 | '''Converts a name and type (e.g., '1.0' and 'tags') into a git ref.''' 135 | if reftype == BRANCH: 136 | if name == 'default': 137 | return 'refs/heads/master' 138 | else: 139 | return 'refs/heads/branches/%s' % name 140 | elif reftype == BOOKMARK: 141 | return 'refs/heads/%s' % name 142 | elif reftype == TAG: 143 | return 'refs/tags/%s' % name 144 | assert False, "unknown reftype: %s" % reftype 145 | 146 | 147 | def user_config(): 148 | """Read the Mercurial user configuration 149 | 150 | This is typically ~/.hgrc on POSIX. This is returned 151 | as a Mercurial.config.config object. 152 | """ 153 | hgrc = config() 154 | for cfg in userrcpath(): 155 | if not os.path.exists(cfg): 156 | log("NOT reading missing cfg: " + cfg) 157 | continue 158 | log("Reading config: " + cfg) 159 | hgrc.read(cfg) 160 | return hgrc 161 | 162 | 163 | def relative_path(path): 164 | """Ensure path is relative""" 165 | return os.path.relpath(path, '/') if os.path.isabs(path) else path 166 | 167 | 168 | class HGMarks(object): 169 | '''Maps integer marks to specific string mercurial revision identifiers. 170 | Identifiers are passed as binary nodes and converted to/from hex strings 171 | before and after storage.''' 172 | 173 | def __init__(self, storage_path): 174 | ''':param storage_path: The file that marks are stored in between calls. 175 | Marks are stored in json format.''' 176 | self.storage_path = storage_path 177 | self.load() 178 | 179 | def load(self): 180 | '''Load the marks from the storage file''' 181 | if self.storage_path.exists(): 182 | with self.storage_path.open() as file: 183 | loaded = json.load(file) 184 | 185 | self.tips = loaded['tips'] 186 | self.revisions_to_marks = loaded['revisions_to_marks'] 187 | self.last_mark = loaded['last-mark'] 188 | self.marks_to_revisions = dict([(int(v), k) for k, v in 189 | self.revisions_to_marks.iteritems()]) 190 | self.notes_mark = loaded.get('notes-mark', None) 191 | self.marks_version = loaded.get('marks-version', 1) 192 | else: 193 | self.tips = {} 194 | self.revisions_to_marks = {} 195 | self.marks_to_revisions = {} 196 | self.last_mark = 0 197 | self.notes_mark = None 198 | self.marks_version = 3 199 | 200 | def store(self): 201 | '''Save marks to the storage file.''' 202 | with self.storage_path.open('w') as file: 203 | file.write( 204 | json.dumps({ 205 | 'tips': self.tips, 206 | 'revisions_to_marks': self.revisions_to_marks, 207 | 'last-mark': self.last_mark, 208 | 'notes-mark': self.notes_mark, 209 | 'marks-version': self.marks_version, 210 | }).decode('UTF-8') 211 | ) 212 | 213 | def upgrade_marks(self, hgremote): 214 | if self.marks_version == 1: # Convert from integer reversions to hgsha1 215 | log("Upgrading marks-hg from hg sequence number to SHA1", "WARNING") 216 | self.marks_to_revisions = dict( 217 | (mark, hghex(hgremote.repo.changelog.node(int(rev)))) for mark, rev in self.marks_to_revisions.iteritems()) 218 | self.revisions_to_marks = dict( 219 | (hghex(hgremote.repo.changelog.node(int(rev))), mark) for rev, mark in self.revisions_to_marks.iteritems()) 220 | self.marks_version = 2 221 | log("Upgrade complete", "WARNING") 222 | if self.marks_version == 2: # Convert tips to use gitify refs as keys 223 | log("Upgrading marks-hg tips", "WARNING") 224 | self.tips = dict( 225 | ("%s/%s" % (hgremote.prefix, reftype_and_name), tip) for reftype_and_name, tip in self.tips.iteritems()) 226 | self.marks_version = 3 227 | log("Upgrade complete", "WARNING") 228 | 229 | def mark_to_revision(self, mark): 230 | return hgbin(self.marks_to_revisions[mark]) 231 | 232 | def revision_to_mark(self, revision): 233 | return self.revisions_to_marks[hghex(revision)] 234 | 235 | def get_mark(self, revision): 236 | self.last_mark += 1 237 | self.revisions_to_marks[hghex(revision)] = self.last_mark 238 | return self.last_mark 239 | 240 | def new_mark(self, revision, mark): 241 | self.revisions_to_marks[hghex(revision)] = mark 242 | self.marks_to_revisions[mark] = hghex(revision) 243 | self.last_mark = mark 244 | 245 | def is_marked(self, revision): 246 | return hghex(revision) in self.revisions_to_marks 247 | 248 | def new_notes_mark(self): 249 | self.last_mark += 1 250 | self.notes_mark = self.last_mark 251 | return self.notes_mark 252 | 253 | 254 | class GitMarks(object): 255 | '''Maps integer marks to git commit hashes.''' 256 | 257 | def __init__(self, storage_path): 258 | ''':param storage_path: The file that marks are stored in between calls.''' 259 | self.storage_path = storage_path 260 | self.load() 261 | 262 | def load(self): 263 | '''Load the marks from the storage file.''' 264 | # TODO: Combine remove_processed_git_marks with this, perhaps by using 265 | # an OrderedDict to write entires back out in the order they came in. 266 | self.marks_to_hashes = {} 267 | if self.storage_path.exists(): 268 | with self.storage_path.open() as file: 269 | for line in file: 270 | if not line.startswith(':'): 271 | die("invalid line in marks-git: " + line) 272 | mark, sha1 = line[1:].split() 273 | self.marks_to_hashes[mark] = sha1 274 | 275 | def has_mark(self, mark): 276 | return str(mark) in self.marks_to_hashes 277 | 278 | def mark_to_hash(self, mark): 279 | return self.marks_to_hashes[str(mark)] 280 | 281 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013 Dusty Phillips 2 | 3 | # This file is part of gitifyhg. 4 | 5 | # gitifyhg is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # gitifyhg is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with gitifyhg. If not, see . 17 | 18 | 19 | from setuptools import setup 20 | setup( 21 | name="gitifyhg", 22 | author="Dusty Phillips", 23 | author_email="dusty@buchuki.com", 24 | url="https://github.com/buchuki/gitifyhg", 25 | description="Use git as client for hg repos", 26 | version="0.8.6", 27 | packages=['gitifyhg'], 28 | install_requires=[ 29 | 'path.py>=2.5', 30 | 'Mercurial>=2.5, <4.0.2', 31 | ], 32 | entry_points={ 33 | 'console_scripts': [ 34 | 'git-remote-gitifyhg = gitifyhg.gitifyhg:main', 35 | ], 36 | }, 37 | classifiers=[ 38 | 'Development Status :: 4 - Beta', 39 | 'Intended Audience :: Developers', 40 | 'Programming Language :: Python :: 2.7', 41 | 'Topic :: Software Development :: Version Control', 42 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)' 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | # Run tests 2 | # 3 | # Copyright (c) 2011-2012 Mathias Lafeldt 4 | # Copyright (c) 2005-2012 Git project 5 | # Copyright (c) 2005-2012 Junio C Hamano 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see http://www.gnu.org/licenses/ . 19 | 20 | SHELL_PATH ?= $(SHELL) 21 | SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) 22 | RM ?= rm -f 23 | PROVE ?= prove 24 | AGGREGATE_SCRIPT ?= aggregate-results.sh 25 | DEFAULT_TEST_TARGET ?= test 26 | 27 | T = $(wildcard *.t) 28 | 29 | all: $(DEFAULT_TEST_TARGET) 30 | 31 | test: pre-clean 32 | $(MAKE) aggregate-results-and-cleanup 33 | 34 | prove: pre-clean 35 | @echo "*** prove ***"; $(PROVE) --exec '$(SHELL_PATH_SQ)' $(PROVE_OPTS) $(T) :: $(TEST_OPTS) 36 | $(MAKE) clean-except-prove-cache 37 | 38 | $(T): 39 | @echo "*** $@ ***"; '$(SHELL_PATH_SQ)' $@ $(TEST_OPTS) 40 | 41 | pre-clean: 42 | $(RM) -r test-results 43 | 44 | clean-except-prove-cache: 45 | $(RM) -r 'trash directory'.* test-results 46 | 47 | clean: clean-except-prove-cache 48 | $(RM) .prove 49 | 50 | aggregate-results-and-cleanup: $(T) 51 | $(MAKE) aggregate-results 52 | $(MAKE) clean 53 | 54 | aggregate-results: 55 | for f in test-results/*.counts; do \ 56 | echo "$$f"; \ 57 | done | '$(SHELL_PATH_SQ)' '$(AGGREGATE_SCRIPT)' 58 | 59 | .PHONY: all test prove $(T) pre-clean clean 60 | .PHONY: aggregate-results-and-cleanup aggregate-results 61 | -------------------------------------------------------------------------------- /test/aggregate-results.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2008-2012 Git project 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see http://www.gnu.org/licenses/ . 17 | 18 | failed_tests= 19 | fixed=0 20 | success=0 21 | failed=0 22 | broken=0 23 | total=0 24 | 25 | while read file; do 26 | while read type value; do 27 | case $type in 28 | '') 29 | continue ;; 30 | fixed) 31 | fixed=$(($fixed + $value)) ;; 32 | success) 33 | success=$(($success + $value)) ;; 34 | failed) 35 | failed=$(($failed + $value)) 36 | if test $value != 0; then 37 | test_name=$(expr "$file" : 'test-results/\(.*\)\.[0-9]*\.counts') 38 | failed_tests="$failed_tests $test_name" 39 | fi 40 | ;; 41 | broken) 42 | broken=$(($broken + $value)) ;; 43 | total) 44 | total=$(($total + $value)) ;; 45 | esac 46 | done <"$file" 47 | done 48 | 49 | if test -n "$failed_tests"; then 50 | printf "\nfailed test(s):$failed_tests\n\n" 51 | fi 52 | 53 | printf "%-8s%d\n" fixed $fixed 54 | printf "%-8s%d\n" success $success 55 | printf "%-8s%d\n" failed $failed 56 | printf "%-8s%d\n" broken $broken 57 | printf "%-8s%d\n" total $total 58 | -------------------------------------------------------------------------------- /test/sharness.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2011-2012 Mathias Lafeldt 4 | # Copyright (c) 2005-2012 Git project 5 | # Copyright (c) 2005-2012 Junio C Hamano 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see http://www.gnu.org/licenses/ . 19 | 20 | # Public: Current version of Sharness. 21 | SHARNESS_VERSION="0.3.0" 22 | export SHARNESS_VERSION 23 | 24 | # Public: The file extension for tests. By default, it is set to "t". 25 | : ${SHARNESS_TEST_EXTENSION:=t} 26 | export SHARNESS_TEST_EXTENSION 27 | 28 | # Keep the original TERM for say_color 29 | ORIGINAL_TERM=$TERM 30 | 31 | # For repeatability, reset the environment to a known state. 32 | LANG=C 33 | LC_ALL=C 34 | PAGER=cat 35 | TZ=UTC 36 | TERM=dumb 37 | EDITOR=: 38 | export LANG LC_ALL PAGER TZ TERM EDITOR 39 | unset VISUAL CDPATH GREP_OPTIONS 40 | 41 | # Line feed 42 | LF=' 43 | ' 44 | 45 | [ "x$ORIGINAL_TERM" != "xdumb" ] && ( 46 | TERM=$ORIGINAL_TERM && 47 | export TERM && 48 | [ -t 1 ] && 49 | tput bold >/dev/null 2>&1 && 50 | tput setaf 1 >/dev/null 2>&1 && 51 | tput sgr0 >/dev/null 2>&1 52 | ) && 53 | color=t 54 | 55 | while test "$#" -ne 0; do 56 | case "$1" in 57 | -d|--d|--de|--deb|--debu|--debug) 58 | debug=t; shift ;; 59 | -i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate) 60 | immediate=t; shift ;; 61 | -l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests) 62 | TEST_LONG=t; export TEST_LONG; shift ;; 63 | -h|--h|--he|--hel|--help) 64 | help=t; shift ;; 65 | -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) 66 | verbose=t; shift ;; 67 | -q|--q|--qu|--qui|--quie|--quiet) 68 | # Ignore --quiet under a TAP::Harness. Saying how many tests 69 | # passed without the ok/not ok details is always an error. 70 | test -z "$HARNESS_ACTIVE" && quiet=t; shift ;; 71 | --no-color) 72 | color=; shift ;; 73 | --root=*) 74 | root=$(expr "z$1" : 'z[^=]*=\(.*\)') 75 | shift ;; 76 | *) 77 | echo "error: unknown test option '$1'" >&2; exit 1 ;; 78 | esac 79 | done 80 | 81 | if test -n "$color"; then 82 | say_color() { 83 | ( 84 | TERM=$ORIGINAL_TERM 85 | export TERM 86 | case "$1" in 87 | error) 88 | tput bold; tput setaf 1;; # bold red 89 | skip) 90 | tput setaf 4;; # blue 91 | warn) 92 | tput setaf 3;; # brown/yellow 93 | pass) 94 | tput setaf 2;; # green 95 | info) 96 | tput setaf 6;; # cyan 97 | *) 98 | test -n "$quiet" && return;; 99 | esac 100 | shift 101 | printf "%s" "$*" 102 | tput sgr0 103 | echo 104 | ) 105 | } 106 | else 107 | say_color() { 108 | test -z "$1" && test -n "$quiet" && return 109 | shift 110 | printf "%s\n" "$*" 111 | } 112 | fi 113 | 114 | error() { 115 | say_color error "error: $*" 116 | EXIT_OK=t 117 | exit 1 118 | } 119 | 120 | say() { 121 | say_color info "$*" 122 | } 123 | 124 | test -n "$test_description" || error "Test script did not set test_description." 125 | 126 | if test "$help" = "t"; then 127 | echo "$test_description" 128 | exit 0 129 | fi 130 | 131 | exec 5>&1 132 | exec 6<&0 133 | if test "$verbose" = "t"; then 134 | exec 4>&2 3>&1 135 | else 136 | exec 4>/dev/null 3>/dev/null 137 | fi 138 | 139 | test_failure=0 140 | test_count=0 141 | test_fixed=0 142 | test_broken=0 143 | test_success=0 144 | 145 | die() { 146 | code=$? 147 | if test -n "$EXIT_OK"; then 148 | exit $code 149 | else 150 | echo >&5 "FATAL: Unexpected exit with code $code" 151 | exit 1 152 | fi 153 | } 154 | 155 | EXIT_OK= 156 | trap 'die' EXIT 157 | 158 | # Public: Define that a test prerequisite is available. 159 | # 160 | # The prerequisite can later be checked explicitly using test_have_prereq or 161 | # implicitly by specifying the prerequisite name in calls to test_expect_success 162 | # or test_expect_failure. 163 | # 164 | # $1 - Name of prerequiste (a simple word, in all capital letters by convention) 165 | # 166 | # Examples 167 | # 168 | # # Set PYTHON prerequisite if interpreter is available. 169 | # command -v python >/dev/null && test_set_prereq PYTHON 170 | # 171 | # # Set prerequisite depending on some variable. 172 | # test -z "$NO_GETTEXT" && test_set_prereq GETTEXT 173 | # 174 | # Returns nothing. 175 | test_set_prereq() { 176 | satisfied_prereq="$satisfied_prereq$1 " 177 | } 178 | satisfied_prereq=" " 179 | 180 | # Public: Check if one or more test prerequisites are defined. 181 | # 182 | # The prerequisites must have previously been set with test_set_prereq. 183 | # The most common use of this is to skip all the tests if some essential 184 | # prerequisite is missing. 185 | # 186 | # $1 - Comma-separated list of test prerequisites. 187 | # 188 | # Examples 189 | # 190 | # # Skip all remaining tests if prerequisite is not set. 191 | # if ! test_have_prereq PERL; then 192 | # skip_all='skipping perl interface tests, perl not available' 193 | # test_done 194 | # fi 195 | # 196 | # Returns 0 if all prerequisites are defined or 1 otherwise. 197 | test_have_prereq() { 198 | # prerequisites can be concatenated with ',' 199 | save_IFS=$IFS 200 | IFS=, 201 | set -- $* 202 | IFS=$save_IFS 203 | 204 | total_prereq=0 205 | ok_prereq=0 206 | missing_prereq= 207 | 208 | for prerequisite; do 209 | case "$prerequisite" in 210 | !*) 211 | negative_prereq=t 212 | prerequisite=${prerequisite#!} 213 | ;; 214 | *) 215 | negative_prereq= 216 | esac 217 | 218 | total_prereq=$(($total_prereq + 1)) 219 | case "$satisfied_prereq" in 220 | *" $prerequisite "*) 221 | satisfied_this_prereq=t 222 | ;; 223 | *) 224 | satisfied_this_prereq= 225 | esac 226 | 227 | case "$satisfied_this_prereq,$negative_prereq" in 228 | t,|,t) 229 | ok_prereq=$(($ok_prereq + 1)) 230 | ;; 231 | *) 232 | # Keep a list of missing prerequisites; restore 233 | # the negative marker if necessary. 234 | prerequisite=${negative_prereq:+!}$prerequisite 235 | if test -z "$missing_prereq"; then 236 | missing_prereq=$prerequisite 237 | else 238 | missing_prereq="$prerequisite,$missing_prereq" 239 | fi 240 | esac 241 | done 242 | 243 | test $total_prereq = $ok_prereq 244 | } 245 | 246 | # You are not expected to call test_ok_ and test_failure_ directly, use 247 | # the text_expect_* functions instead. 248 | 249 | test_ok_() { 250 | test_success=$(($test_success + 1)) 251 | say_color "" "ok $test_count - $@" 252 | } 253 | 254 | test_failure_() { 255 | test_failure=$(($test_failure + 1)) 256 | say_color error "not ok $test_count - $1" 257 | shift 258 | echo "$@" | sed -e 's/^/# /' 259 | test "$immediate" = "" || { EXIT_OK=t; exit 1; } 260 | } 261 | 262 | test_known_broken_ok_() { 263 | test_fixed=$(($test_fixed + 1)) 264 | say_color error "ok $test_count - $@ # TODO known breakage vanished" 265 | } 266 | 267 | test_known_broken_failure_() { 268 | test_broken=$(($test_broken + 1)) 269 | say_color warn "not ok $test_count - $@ # TODO known breakage" 270 | } 271 | 272 | # Public: Execute commands in debug mode. 273 | # 274 | # Takes a single argument and evaluates it only when the test script is started 275 | # with --debug. This is primarily meant for use during the development of test 276 | # scripts. 277 | # 278 | # $1 - Commands to be executed. 279 | # 280 | # Examples 281 | # 282 | # test_debug "cat some_log_file" 283 | # 284 | # Returns the exit code of the last command executed in debug mode or 0 285 | # otherwise. 286 | test_debug() { 287 | test "$debug" = "" || eval "$1" 288 | } 289 | 290 | test_eval_() { 291 | # This is a separate function because some tests use 292 | # "return" to end a test_expect_success block early. 293 | eval &3 2>&4 "$*" 294 | } 295 | 296 | test_run_() { 297 | test_cleanup=: 298 | expecting_failure=$2 299 | test_eval_ "$1" 300 | eval_ret=$? 301 | 302 | if test -z "$immediate" || test $eval_ret = 0 || test -n "$expecting_failure"; then 303 | test_eval_ "$test_cleanup" 304 | fi 305 | if test "$verbose" = "t" && test -n "$HARNESS_ACTIVE"; then 306 | echo "" 307 | fi 308 | return "$eval_ret" 309 | } 310 | 311 | test_skip_() { 312 | test_count=$(($test_count + 1)) 313 | to_skip= 314 | for skp in $SKIP_TESTS; do 315 | case $this_test.$test_count in 316 | $skp) 317 | to_skip=t 318 | break 319 | esac 320 | done 321 | if test -z "$to_skip" && test -n "$test_prereq" && ! test_have_prereq "$test_prereq"; then 322 | to_skip=t 323 | fi 324 | case "$to_skip" in 325 | t) 326 | of_prereq= 327 | if test "$missing_prereq" != "$test_prereq"; then 328 | of_prereq=" of $test_prereq" 329 | fi 330 | 331 | say_color skip >&3 "skipping test: $@" 332 | say_color skip "ok $test_count # skip $1 (missing $missing_prereq${of_prereq})" 333 | : true 334 | ;; 335 | *) 336 | false 337 | ;; 338 | esac 339 | } 340 | 341 | # Public: Run test commands and expect them to succeed. 342 | # 343 | # When the test passed, an "ok" message is printed and the number of successful 344 | # tests is incremented. When it failed, a "not ok" message is printed and the 345 | # number of failed tests is incremented. 346 | # 347 | # With --immediate, exit test immediately upon the first failed test. 348 | # 349 | # Usually takes two arguments: 350 | # $1 - Test description 351 | # $2 - Commands to be executed. 352 | # 353 | # With three arguments, the first will be taken to be a prerequisite: 354 | # $1 - Comma-separated list of test prerequisites. The test will be skipped if 355 | # not all of the given prerequisites are set. To negate a prerequisite, 356 | # put a "!" in front of it. 357 | # $2 - Test description 358 | # $3 - Commands to be executed. 359 | # 360 | # Examples 361 | # 362 | # test_expect_success \ 363 | # 'git-write-tree should be able to write an empty tree.' \ 364 | # 'tree=$(git-write-tree)' 365 | # 366 | # # Test depending on one prerequisite. 367 | # test_expect_success TTY 'git --paginate rev-list uses a pager' \ 368 | # ' ... ' 369 | # 370 | # # Multiple prerequisites are separated by a comma. 371 | # test_expect_success PERL,PYTHON 'yo dawg' \ 372 | # ' test $(perl -E 'print eval "1 +" . qx[python -c "print 2"]') == "4" ' 373 | # 374 | # Returns nothing. 375 | test_expect_success() { 376 | test "$#" = 3 && { test_prereq=$1; shift; } || test_prereq= 377 | test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_success" 378 | export test_prereq 379 | if ! test_skip_ "$@"; then 380 | say >&3 "expecting success: $2" 381 | if test_run_ "$2"; then 382 | test_ok_ "$1" 383 | else 384 | test_failure_ "$@" 385 | fi 386 | fi 387 | echo >&3 "" 388 | } 389 | 390 | # Public: Run test commands and expect them to fail. Used to demonstrate a known 391 | # breakage. 392 | # 393 | # This is NOT the opposite of test_expect_success, but rather used to mark a 394 | # test that demonstrates a known breakage. 395 | # 396 | # When the test passed, an "ok" message is printed and the number of fixed tests 397 | # is incremented. When it failed, a "not ok" message is printed and the number 398 | # of tests still broken is incremented. 399 | # 400 | # Failures from these tests won't cause --immediate to stop. 401 | # 402 | # Usually takes two arguments: 403 | # $1 - Test description 404 | # $2 - Commands to be executed. 405 | # 406 | # With three arguments, the first will be taken to be a prerequisite: 407 | # $1 - Comma-separated list of test prerequisites. The test will be skipped if 408 | # not all of the given prerequisites are set. To negate a prerequisite, 409 | # put a "!" in front of it. 410 | # $2 - Test description 411 | # $3 - Commands to be executed. 412 | # 413 | # Returns nothing. 414 | test_expect_failure() { 415 | test "$#" = 3 && { test_prereq=$1; shift; } || test_prereq= 416 | test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_failure" 417 | export test_prereq 418 | if ! test_skip_ "$@"; then 419 | say >&3 "checking known breakage: $2" 420 | if test_run_ "$2" expecting_failure; then 421 | test_known_broken_ok_ "$1" 422 | else 423 | test_known_broken_failure_ "$1" 424 | fi 425 | fi 426 | echo >&3 "" 427 | } 428 | 429 | # Public: Run command and ensure that it fails in a controlled way. 430 | # 431 | # Use it instead of "! ". For example, when dies due to a 432 | # segfault, test_must_fail diagnoses it as an error, while "! " would 433 | # mistakenly be treated as just another expected failure. 434 | # 435 | # This is one of the prefix functions to be used inside test_expect_success or 436 | # test_expect_failure. 437 | # 438 | # $1.. - Command to be executed. 439 | # 440 | # Examples 441 | # 442 | # test_expect_success 'complain and die' ' 443 | # do something && 444 | # do something else && 445 | # test_must_fail git checkout ../outerspace 446 | # ' 447 | # 448 | # Returns 1 if the command succeeded (exit code 0). 449 | # Returns 1 if the command died by signal (exit codes 130-192) 450 | # Returns 1 if the command could not be found (exit code 127). 451 | # Returns 0 otherwise. 452 | test_must_fail() { 453 | "$@" 454 | exit_code=$? 455 | if test $exit_code = 0; then 456 | echo >&2 "test_must_fail: command succeeded: $*" 457 | return 1 458 | elif test $exit_code -gt 129 -a $exit_code -le 192; then 459 | echo >&2 "test_must_fail: died by signal: $*" 460 | return 1 461 | elif test $exit_code = 127; then 462 | echo >&2 "test_must_fail: command not found: $*" 463 | return 1 464 | fi 465 | return 0 466 | } 467 | 468 | # Public: Run command and ensure that it succeeds or fails in a controlled way. 469 | # 470 | # Similar to test_must_fail, but tolerates success too. Use it instead of 471 | # " || :" to catch failures caused by a segfault, for instance. 472 | # 473 | # This is one of the prefix functions to be used inside test_expect_success or 474 | # test_expect_failure. 475 | # 476 | # $1.. - Command to be executed. 477 | # 478 | # Examples 479 | # 480 | # test_expect_success 'some command works without configuration' ' 481 | # test_might_fail git config --unset all.configuration && 482 | # do something 483 | # ' 484 | # 485 | # Returns 1 if the command died by signal (exit codes 130-192) 486 | # Returns 1 if the command could not be found (exit code 127). 487 | # Returns 0 otherwise. 488 | test_might_fail() { 489 | "$@" 490 | exit_code=$? 491 | if test $exit_code -gt 129 -a $exit_code -le 192; then 492 | echo >&2 "test_might_fail: died by signal: $*" 493 | return 1 494 | elif test $exit_code = 127; then 495 | echo >&2 "test_might_fail: command not found: $*" 496 | return 1 497 | fi 498 | return 0 499 | } 500 | 501 | # Public: Run command and ensure it exits with a given exit code. 502 | # 503 | # This is one of the prefix functions to be used inside test_expect_success or 504 | # test_expect_failure. 505 | # 506 | # $1 - Expected exit code. 507 | # $2.. - Command to be executed. 508 | # 509 | # Examples 510 | # 511 | # test_expect_success 'Merge with d/f conflicts' ' 512 | # test_expect_code 1 git merge "merge msg" B master 513 | # ' 514 | # 515 | # Returns 0 if the expected exit code is returned or 1 otherwise. 516 | test_expect_code() { 517 | want_code=$1 518 | shift 519 | "$@" 520 | exit_code=$? 521 | if test $exit_code = $want_code; then 522 | return 0 523 | fi 524 | 525 | echo >&2 "test_expect_code: command exited with $exit_code, we wanted $want_code $*" 526 | return 1 527 | } 528 | 529 | # Public: Compare two files to see if expected output matches actual output. 530 | # 531 | # The TEST_CMP variable defines the command used for the comparision; it 532 | # defaults to "diff -u". Only when the test script was started with --verbose, 533 | # will the command's output, the diff, be printed to the standard output. 534 | # 535 | # This is one of the prefix functions to be used inside test_expect_success or 536 | # test_expect_failure. 537 | # 538 | # $1 - Path to file with expected output. 539 | # $2 - Path to file with actual output. 540 | # 541 | # Examples 542 | # 543 | # test_expect_success 'foo works' ' 544 | # echo expected >expected && 545 | # foo >actual && 546 | # test_cmp expected actual 547 | # ' 548 | # 549 | # Returns the exit code of the command set by TEST_CMP. 550 | test_cmp() { 551 | ${TEST_CMP:-diff -u} "$@" 552 | } 553 | 554 | # Public: Schedule cleanup commands to be run unconditionally at the end of a 555 | # test. 556 | # 557 | # If some cleanup command fails, the test will not pass. With --immediate, no 558 | # cleanup is done to help diagnose what went wrong. 559 | # 560 | # This is one of the prefix functions to be used inside test_expect_success or 561 | # test_expect_failure. 562 | # 563 | # $1.. - Commands to prepend to the list of cleanup commands. 564 | # 565 | # Examples 566 | # 567 | # test_expect_success 'test core.capslock' ' 568 | # git config core.capslock true && 569 | # test_when_finished "git config --unset core.capslock" && 570 | # do_something 571 | # ' 572 | # 573 | # Returns the exit code of the last cleanup command executed. 574 | test_when_finished() { 575 | test_cleanup="{ $* 576 | } && (exit \"\$eval_ret\"); eval_ret=\$?; $test_cleanup" 577 | } 578 | 579 | # Public: Summarize test results and exit with an appropriate error code. 580 | # 581 | # Must be called at the end of each test script. 582 | # 583 | # Can also be used to stop tests early and skip all remaining tests. For this, 584 | # set skip_all to a string explaining why the tests were skipped before calling 585 | # test_done. 586 | # 587 | # Examples 588 | # 589 | # # Each test script must call test_done at the end. 590 | # test_done 591 | # 592 | # # Skip all remaining tests if prerequisite is not set. 593 | # if ! test_have_prereq PERL; then 594 | # skip_all='skipping perl interface tests, perl not available' 595 | # test_done 596 | # fi 597 | # 598 | # Returns 0 if all tests passed or 1 if there was a failure. 599 | test_done() { 600 | EXIT_OK=t 601 | 602 | if test -z "$HARNESS_ACTIVE"; then 603 | test_results_dir="$SHARNESS_TEST_DIRECTORY/test-results" 604 | mkdir -p "$test_results_dir" 605 | test_results_path="$test_results_dir/${SHARNESS_TEST_FILE%.$SHARNESS_TEST_EXTENSION}.$$.counts" 606 | 607 | cat >>"$test_results_path" <<-EOF 608 | total $test_count 609 | success $test_success 610 | fixed $test_fixed 611 | broken $test_broken 612 | failed $test_failure 613 | 614 | EOF 615 | fi 616 | 617 | if test "$test_fixed" != 0; then 618 | say_color error "# $test_fixed known breakage(s) vanished; please update test(s)" 619 | fi 620 | if test "$test_broken" != 0; then 621 | say_color warn "# still have $test_broken known breakage(s)" 622 | fi 623 | if test "$test_broken" != 0 || test "$test_fixed" != 0; then 624 | test_remaining=$(( $test_count - $test_broken - $test_fixed )) 625 | msg="remaining $test_remaining test(s)" 626 | else 627 | test_remaining=$test_count 628 | msg="$test_count test(s)" 629 | fi 630 | 631 | case "$test_failure" in 632 | 0) 633 | # Maybe print SKIP message 634 | if test -n "$skip_all" && test $test_count -gt 0; then 635 | error "Can't use skip_all after running some tests" 636 | fi 637 | [ -z "$skip_all" ] || skip_all=" # SKIP $skip_all" 638 | 639 | if test $test_remaining -gt 0; then 640 | say_color pass "# passed all $msg" 641 | fi 642 | say "1..$test_count$skip_all" 643 | 644 | test -d "$remove_trash" && 645 | cd "$(dirname "$remove_trash")" && 646 | rm -rf "$(basename "$remove_trash")" 647 | 648 | exit 0 ;; 649 | 650 | *) 651 | say_color error "# failed $test_failure among $msg" 652 | say "1..$test_count" 653 | 654 | exit 1 ;; 655 | 656 | esac 657 | } 658 | 659 | # Public: Root directory containing tests. Tests can override this variable, 660 | # e.g. for testing Sharness itself. 661 | : ${SHARNESS_TEST_DIRECTORY:=$(pwd)} 662 | export SHARNESS_TEST_DIRECTORY 663 | 664 | # Public: Build directory that will be added to PATH. By default, it is set to 665 | # the parent directory of SHARNESS_TEST_DIRECTORY. 666 | : ${SHARNESS_BUILD_DIRECTORY:="$SHARNESS_TEST_DIRECTORY/.."} 667 | PATH="$SHARNESS_BUILD_DIRECTORY:$PATH" 668 | export PATH SHARNESS_BUILD_DIRECTORY 669 | 670 | # Public: Path to test script currently executed. 671 | SHARNESS_TEST_FILE="$0" 672 | export SHARNESS_TEST_FILE 673 | 674 | # Prepare test area. 675 | test_dir="trash directory.$(basename "$SHARNESS_TEST_FILE" ".$SHARNESS_TEST_EXTENSION")" 676 | test -n "$root" && test_dir="$root/$test_dir" 677 | case "$test_dir" in 678 | /*) SHARNESS_TRASH_DIRECTORY="$test_dir" ;; 679 | *) SHARNESS_TRASH_DIRECTORY="$SHARNESS_TEST_DIRECTORY/$test_dir" ;; 680 | esac 681 | test "$debug" = "t" || remove_trash="$SHARNESS_TRASH_DIRECTORY" 682 | rm -rf "$test_dir" || { 683 | EXIT_OK=t 684 | echo >&5 "FATAL: Cannot prepare test area" 685 | exit 1 686 | } 687 | 688 | # Public: Empty trash directory, the test area, provided for each test. The HOME 689 | # variable is set to that directory too. 690 | export SHARNESS_TRASH_DIRECTORY 691 | 692 | HOME="$SHARNESS_TRASH_DIRECTORY" 693 | export HOME 694 | 695 | mkdir -p "$test_dir" || exit 1 696 | # Use -P to resolve symlinks in our working directory so that the cwd 697 | # in subprocesses like git equals our $PWD (for pathname comparisons). 698 | cd -P "$test_dir" || exit 1 699 | 700 | this_test=${SHARNESS_TEST_FILE##*/} 701 | this_test=${this_test%.$SHARNESS_TEST_EXTENSION} 702 | for skp in $SKIP_TESTS; do 703 | case "$this_test" in 704 | $skp) 705 | say_color info >&3 "skipping test $this_test altogether" 706 | skip_all="skip all tests in $this_test" 707 | test_done 708 | esac 709 | done 710 | 711 | # vi: set ts=4 sw=4 noet : 712 | -------------------------------------------------------------------------------- /test/test-lib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./sharness.sh 4 | 5 | export GIT_AUTHOR_EMAIL=git.user@example.com 6 | export GIT_AUTHOR_NAME='Git User' 7 | export GIT_USER="$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" 8 | export HG_USER="Hg User " 9 | export DEBUG_GITIFYHG=$debug 10 | export GIT_PAGER=cat 11 | export HGRCPATH="$HOME/.hgrc" 12 | export NL=' 13 | ' 14 | 15 | make_hg_repo() { 16 | hg init hg_repo && 17 | cd hg_repo && 18 | echo 'a\n' >> test_file && 19 | hg add test_file && 20 | hg commit --message="a" --user="$HG_USER" 21 | } 22 | 23 | clone_repo() { 24 | cd .. && 25 | test_expect_code 0 git clone "testgitifyhg::hg_repo" git_clone && 26 | cd git_clone && 27 | git config user.email $GIT_AUTHOR_EMAIL && 28 | git config user.name "$GIT_USER" 29 | } 30 | 31 | make_cloned_repo() { 32 | make_hg_repo && 33 | clone_repo && 34 | cd ../hg_repo 35 | } 36 | 37 | make_hg_commit() { 38 | if test $# -eq 3 ; then 39 | user=$3 40 | else 41 | user=$HG_USER 42 | fi 43 | echo "$1" >> $2 && 44 | hg add $2 && 45 | hg commit -m "$1" --user="$user" 46 | } 47 | 48 | make_git_commit() { 49 | echo "$1" >> "$2" && 50 | git add "$2" && 51 | git commit -m "$1" 52 | } 53 | 54 | assert_git_messages() { 55 | if test $# -eq 2 ; then 56 | test "`git log --pretty=format:%B $2`" = "$1" 57 | else 58 | test "`git log --pretty=format:%B`" = "$1" 59 | fi 60 | } 61 | 62 | assert_hg_messages() { 63 | if test $# -eq 2 ; then 64 | test "`hg log --template=\"{desc}\n\" -r $2`" = "$1" 65 | else 66 | test "`hg log --template=\"{desc}\n\"`" = "$1" 67 | fi 68 | } 69 | 70 | assert_hg_author() { 71 | if test $# -eq 2 ; then 72 | rev=$2 73 | else 74 | rev=tip 75 | fi 76 | test "`hg log --template='{author}' --rev=$rev`" = "$1" 77 | } 78 | 79 | assert_git_author() { 80 | if test $# -eq 2 ; then 81 | ref=$2 82 | else 83 | ref=HEAD 84 | fi 85 | test "`git show -s --format='%an <%ae>' $ref`" = "$1" 86 | } 87 | 88 | assert_git_count() { 89 | if test $# -eq 2 ; then 90 | ref=$2 91 | else 92 | ref=HEAD 93 | fi 94 | test `git rev-list $ref --count` -eq $1 95 | } 96 | 97 | assert_hg_count() { 98 | if test $# -eq 2 ; then 99 | rev=$2 100 | else 101 | rev=tip 102 | fi 103 | 104 | test "`hg log -q -r 0:$rev | wc -l`" -eq "$1" 105 | } 106 | 107 | assert_git_notes() { 108 | git notes --ref=hg merge $(basename $(ls .git/refs/notes/hg-*)) && 109 | git log --pretty="format:%N" --notes='hg' | grep -v '^$' 110 | echo $1 111 | test "`git log --pretty="format:%N" --notes='hg' | grep -v '^$'`" = "$1" 112 | } -------------------------------------------------------------------------------- /test/test_anonymous_branches.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg clone, pull, and push with spaces' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_failure 'anonymous branches dont work' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | 20 | make_hg_repo && 21 | make_hg_commit b test_file && 22 | hg update -r 0 && 23 | make_hg_commit c test_file && 24 | cd .. && 25 | 26 | git clone testgitifyhg::hg_repo git_clone 2>&1 | grep "more than one head" && 27 | 28 | # TODO: "more than one head" is the correct response for now, but a more 29 | # appropriate result would be to clone the extra commits, perhaps naming 30 | # the branch anonymous/ or something. assert False to mark an expected 31 | # failure. 32 | 33 | false && 34 | 35 | cd .. 36 | ' 37 | 38 | test_expect_failure 'anonymous branch from named branch' ' 39 | test_when_finished "rm -rf hg_repo git_clone" && 40 | make_hg_repo && 41 | hg branch featurebranch && 42 | make_hg_commit b test_file && 43 | make_hg_commit c test_file && 44 | hg update -r 1 && 45 | make_hg_commit d test_file && 46 | hg update default && 47 | make_hg_commit e test_file && 48 | 49 | cd .. && 50 | git clone testgitifyhg::hg_repo git_clone 2>&1 | grep "more than one head" && 51 | cd git_clone && 52 | test "`git branch -r`" = " origin/HEAD -> origin/master 53 | origin/branches/featurebranch 54 | origin/master" && 55 | 56 | # TODO: Same issue as above test. 57 | 58 | false && 59 | 60 | cd .. 61 | ' 62 | 63 | test_expect_failure 'pull from anonymous branch' ' 64 | test_when_finished "rm -rf hg_repo git_clone" && 65 | 66 | make_hg_repo && 67 | make_hg_commit b test_file && 68 | 69 | clone_repo && 70 | cd ../hg_repo && 71 | make_hg_commit c test_file && 72 | hg update --rev=-2 && 73 | make_hg_commit c2 test_file && 74 | 75 | cd ../git_clone && 76 | git pull && 77 | 78 | # TODO: pulling anonymous branches are currently pruned, need to test 79 | # and assert that they are actually dealt with properly. 80 | false && 81 | 82 | cd .. 83 | ' 84 | 85 | test_done 86 | -------------------------------------------------------------------------------- /test/test_author.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg authors' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_success 'push email is correct' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | 20 | make_hg_repo && 21 | clone_repo && 22 | make_git_commit b test_file && 23 | git push && 24 | cd ../hg_repo && 25 | hg update && 26 | assert_hg_author "$GIT_USER" && 27 | make_hg_commit "c" test_file && 28 | assert_hg_author "$HG_USER" && 29 | cd ../git_clone && 30 | git pull && 31 | assert_git_author "$GIT_USER" "HEAD^" && 32 | assert_git_author "$HG_USER" && 33 | 34 | 35 | cd .. 36 | ' 37 | 38 | test_expect_success 'author all good' ' 39 | test_when_finished "rm -rf hg_repo git_clone" && 40 | 41 | make_hg_repo && 42 | make_hg_commit b test_file "all is good " && 43 | 44 | clone_repo && 45 | git show -s --format="%an <%ae>" 46 | assert_git_author "all is good " && 47 | 48 | cd .. 49 | ' 50 | 51 | test_expect_success 'author no email' ' 52 | test_when_finished "rm -rf hg_repo git_clone" && 53 | 54 | make_hg_repo && 55 | make_hg_commit b test_file "no email supplied" && 56 | 57 | clone_repo && 58 | assert_git_author "no email supplied <>" && 59 | 60 | cd .. 61 | ' 62 | 63 | test_expect_success 'author only email' ' 64 | test_when_finished "rm -rf hg_repo git_clone" && 65 | 66 | make_hg_repo && 67 | make_hg_commit b test_file "" && 68 | 69 | clone_repo && 70 | assert_git_author "Unknown " && 71 | 72 | cd .. 73 | ' 74 | 75 | test_expect_success 'author not quoted only email' ' 76 | test_when_finished "rm -rf hg_repo git_clone" && 77 | 78 | make_hg_repo && 79 | make_hg_commit b test_file "email@example.com" && 80 | 81 | clone_repo && 82 | assert_git_author "Unknown " && 83 | 84 | cd .. 85 | ' 86 | 87 | test_expect_success 'author no spaces before email' ' 88 | test_when_finished "rm -rf hg_repo git_clone" && 89 | 90 | make_hg_repo && 91 | make_hg_commit b test_file "no space before email" && 92 | 93 | clone_repo && 94 | assert_git_author "no space before email " && 95 | 96 | cd .. 97 | ' 98 | 99 | # See #22 100 | test_expect_success 'author no email quoting' ' 101 | test_when_finished "rm -rf hg_repo git_clone" && 102 | 103 | make_hg_repo && 104 | make_hg_commit b test_file "no email quoting email@example.com" && 105 | 106 | clone_repo && 107 | assert_git_author "no email quoting " && 108 | 109 | cd .. 110 | ' 111 | 112 | # See #22 113 | test_expect_success 'author missing end quote' ' 114 | test_when_finished "rm -rf hg_repo git_clone" && 115 | 116 | make_hg_repo && 117 | make_hg_commit b test_file "missing end quote " && 121 | 122 | cd .. 123 | ' 124 | 125 | test_expect_success 'author obfuscated email' ' 126 | test_when_finished "rm -rf hg_repo git_clone" && 127 | 128 | make_hg_repo && 129 | make_hg_commit b test_file "Author " && 130 | 131 | clone_repo && 132 | assert_git_author "Author " && 133 | 134 | cd .. 135 | ' 136 | 137 | test_expect_success 'author abuse quotes' ' 138 | test_when_finished "rm -rf hg_repo git_clone" && 139 | 140 | make_hg_repo && 141 | make_hg_commit b test_file "totally >>> bad <<< quote can be used in hg <><><" && 142 | 143 | clone_repo && 144 | assert_git_author "totally " && 145 | 146 | cd .. 147 | ' 148 | 149 | 150 | test_done -------------------------------------------------------------------------------- /test/test_bookmarks.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg bookmark management' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_success 'clone bookmark' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | make_hg_repo && 20 | hg bookmark featurebookmark && 21 | make_hg_commit b test_file && 22 | 23 | clone_repo && 24 | 25 | test "`git branch -r`" = " origin/HEAD -> origin/master 26 | origin/featurebookmark 27 | origin/master" && 28 | 29 | git checkout origin/featurebookmark && 30 | assert_git_messages "b${NL}a" && 31 | git checkout master && 32 | assert_git_messages "b${NL}a" && 33 | 34 | cd .. 35 | ' 36 | 37 | test_expect_success 'clone divergent bookmarks' ' 38 | test_when_finished "rm -rf hg_repo git_clone" && 39 | make_hg_repo && 40 | hg bookmark bookmark_one && 41 | make_hg_commit b test_file && 42 | hg update -r 0 && 43 | make_hg_commit c test_file && 44 | hg bookmark bookmark_two && 45 | make_hg_commit d test_file && 46 | 47 | clone_repo && 48 | 49 | test "`git branch -r`" = " origin/HEAD -> origin/master 50 | origin/bookmark_one 51 | origin/bookmark_two 52 | origin/master" && 53 | 54 | git checkout origin/bookmark_one && 55 | assert_git_messages "b${NL}a" && 56 | 57 | git checkout origin/bookmark_two && 58 | assert_git_messages "d${NL}c${NL}a" && 59 | 60 | cd .. 61 | ' 62 | 63 | test_expect_success 'clone bookmark not at tip' ' 64 | test_when_finished "rm -rf hg_repo git_clone" && 65 | make_hg_repo && 66 | make_hg_commit b test_file && 67 | hg update -r 0 && 68 | hg bookmark bookmark_one && 69 | hg update tip && 70 | 71 | clone_repo && 72 | 73 | test "`git branch -r`" = " origin/HEAD -> origin/master 74 | origin/bookmark_one 75 | origin/master" && 76 | 77 | git checkout bookmark_one && 78 | assert_git_messages "a" && 79 | git checkout master && 80 | assert_git_messages "b${NL}a" && 81 | 82 | cd .. 83 | ' 84 | 85 | # See issue #13 86 | test_expect_success 'clone bookmark named master not at tip' ' 87 | test_when_finished "rm -rf hg_repo git_clone" && 88 | make_hg_repo && 89 | make_hg_commit b test_file && 90 | hg update -r 0 && 91 | hg bookmark master && 92 | hg update tip && 93 | 94 | clone_repo && 95 | 96 | cd .. 97 | ' 98 | 99 | test_expect_success 'push to bookmark' ' 100 | test_when_finished "rm -rf hg_repo git_clone" && 101 | 102 | make_hg_repo && 103 | hg bookmark feature && 104 | make_hg_commit b test_file && 105 | clone_repo && 106 | git checkout --track origin/feature && 107 | make_git_commit c test_file && 108 | git push && 109 | 110 | cd ../hg_repo && 111 | hg update && 112 | 113 | assert_hg_messages "c${NL}b${NL}a" && 114 | hg bookmark | grep feature && 115 | hg update feature && 116 | test_cmp test_file ../git_clone/test_file && 117 | 118 | cd .. 119 | ' 120 | 121 | test_expect_success 'push multiple bookmarks' ' 122 | test_when_finished "rm -rf hg_repo git_clone" && 123 | 124 | make_hg_repo && 125 | hg bookmark feature && 126 | make_hg_commit b test_file && 127 | hg update --rev 0 && 128 | hg bookmark feature2 && 129 | make_hg_commit c test_file && 130 | 131 | clone_repo && 132 | git checkout --track origin/feature && 133 | make_git_commit d test_file && 134 | git push && 135 | 136 | cd ../hg_repo && 137 | assert_hg_messages "d${NL}c${NL}b${NL}a" && 138 | assert_hg_messages "a${NL}b${NL}d" "0..feature" && 139 | assert_hg_messages "a${NL}c" "0..feature2" && 140 | 141 | hg update feature && 142 | hg bookmark | grep feature && 143 | test_cmp test_file ../git_clone/test_file && 144 | 145 | cd .. 146 | ' 147 | 148 | test_expect_success 'push new bookmark' ' 149 | test_when_finished "rm -rf hg_repo git_clone" && 150 | 151 | make_hg_repo && 152 | clone_repo && 153 | git checkout -b anewbranch && 154 | make_git_commit b test_file && 155 | git push --set-upstream origin anewbranch && 156 | 157 | cd ../hg_repo && 158 | assert_hg_messages "b${NL}a" && 159 | hg bookmark | grep anewbranch && 160 | hg tip | grep anewbranch && 161 | 162 | cd .. 163 | ' 164 | 165 | test_expect_failure 'pull_from_bookmark' ' 166 | test_when_finished "rm -rf hg_repo git_clone" && 167 | 168 | make_hg_repo && 169 | hg bookmark feature && 170 | make_hg_commit b test_file && 171 | hg update -r 0 && 172 | hg bookmark feature2 && 173 | make_hg_commit c test_file && 174 | 175 | clone_repo && 176 | git checkout origin/feature --track && 177 | assert_git_messages "b${NL}a" && 178 | 179 | cd ../hg_repo && 180 | hg update feature && 181 | make_hg_commit d test_file && 182 | hg update feature2 && 183 | make_hg_commit e test_file && 184 | 185 | cd ../git_clone && 186 | git pull origin feature && 187 | assert_git_messages "d${NL}b${NL}a" && 188 | git checkout origin/feature2 --track && 189 | assert_git_messages "c${NL}a" && 190 | 191 | git pull origin feature2 && 192 | assert_git_messages "e${NL}c${NL}a" && 193 | 194 | # TODO: Pulling into a bookmark does not seem to be working. Find the 195 | # problem and fix. 196 | 197 | cd .. 198 | ' 199 | 200 | test_done 201 | -------------------------------------------------------------------------------- /test/test_clone.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg clones' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_success 'basic clone with default branch and two commits' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | make_hg_repo && 20 | make_hg_commit b test_file && 21 | clone_repo && 22 | test_cmp ../hg_repo/test_file test_file && 23 | test -d .git && 24 | assert_git_messages "b${NL}a" && 25 | 26 | cd .. 27 | ' 28 | test_expect_success 'clone linear branch, no multiple parents' ' 29 | test_when_finished "rm -rf hg_repo git_clone" && 30 | make_hg_repo && 31 | hg branch featurebranch && 32 | make_hg_commit b test_file && 33 | clone_repo && 34 | assert_git_messages "a" && 35 | test "`git branch -r`" = " origin/HEAD -> origin/master 36 | origin/branches/featurebranch 37 | origin/master" && 38 | 39 | git checkout branches/featurebranch && 40 | test_cmp ../hg_repo/test_file test_file && 41 | assert_git_messages "b${NL}a" && 42 | 43 | cd .. 44 | ' 45 | 46 | test_expect_success 'clone simple divergent branch' ' 47 | test_when_finished "rm -rf hg_repo git_clone" && 48 | make_hg_repo && 49 | hg branch featurebranch && 50 | make_hg_commit b test_file && 51 | hg update default && 52 | make_hg_commit c c && 53 | clone_repo && 54 | assert_git_messages "c${NL}a" && 55 | git checkout "origin/branches/featurebranch" && 56 | assert_git_messages "b${NL}a" && 57 | 58 | cd .. 59 | ' 60 | 61 | test_expect_success 'clone merged branch' ' 62 | test_when_finished "rm -rf hg_repo git_clone" && 63 | 64 | make_hg_repo && 65 | hg branch featurebranch && 66 | make_hg_commit b test_file && 67 | hg update default && 68 | make_hg_commit c c && 69 | hg merge featurebranch && 70 | hg commit -m "merge" && 71 | make_hg_commit d test_file && 72 | 73 | clone_repo && 74 | 75 | assert_git_messages "d${NL}merge${NL}c${NL}b${NL}a" && 76 | git checkout origin/branches/featurebranch && 77 | assert_git_messages "b${NL}a" 78 | 79 | cd .. 80 | ' 81 | 82 | test_expect_success 'clone basic tag' ' 83 | test_when_finished "rm -rf hg_repo git_clone" && 84 | 85 | make_hg_repo && 86 | make_hg_commit b test_file && 87 | hg tag "this_is_tagged" && 88 | make_hg_commit c test_file && 89 | 90 | clone_repo && 91 | 92 | test $(git tag) = "this_is_tagged" && 93 | git checkout this_is_tagged && 94 | assert_git_messages "b${NL}a" && 95 | 96 | cd .. 97 | ' 98 | 99 | test_expect_success 'clone close branch' ' 100 | test_when_finished "rm -rf hg_repo git_clone" && 101 | test_when_finished "unset GITIFYHG_ALLOW_CLOSED_BRANCHES" && 102 | 103 | export GITIFYHG_ALLOW_CLOSED_BRANCHES=on && 104 | make_hg_repo && 105 | hg branch feature && 106 | make_hg_commit b b && 107 | hg update default && 108 | make_hg_commit c c && 109 | hg update feature && 110 | echo d >> b && 111 | hg commit --close-branch -m "d" && 112 | 113 | clone_repo && 114 | test "`git branch -r`" = " origin/HEAD -> origin/master 115 | origin/branches/feature 116 | origin/master" && 117 | assert_git_messages "c${NL}a" && 118 | git checkout origin/branches/feature && 119 | assert_git_messages "d${NL}b${NL}a" && 120 | 121 | cd .. 122 | ' 123 | 124 | test_expect_success 'no implicit clone close branch' ' 125 | test_when_finished "rm -rf hg_repo git_clone" && 126 | echo $GITIFYHG_ALLOW_CLOSED_BRANCHES && 127 | 128 | make_hg_repo && 129 | hg branch feature && 130 | make_hg_commit b b && 131 | hg update default && 132 | make_hg_commit c c && 133 | hg update feature && 134 | echo d >> b && 135 | hg commit --close-branch -m "d" && 136 | 137 | clone_repo && 138 | git branch -r && 139 | test "`git branch -r`" = " origin/HEAD -> origin/master 140 | origin/master" && 141 | assert_git_messages "c${NL}a" && 142 | 143 | cd .. 144 | ' 145 | 146 | test_done -------------------------------------------------------------------------------- /test/test_clone_file_operations.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test various file operations in gitifyhg clones' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_success 'cloning a removed file works' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | 20 | make_hg_repo && 21 | make_hg_commit b test_file && 22 | echo "b" 23 | hg rm test_file && 24 | hg commit -m "c" && 25 | 26 | clone_repo && 27 | 28 | test_expect_code 2 ls test_file && 29 | 30 | cd .. 31 | ' 32 | 33 | # See issue #36 34 | test_expect_failure 'cloning a file replaced with a directory' ' 35 | test_when_finished "rm -rf hg_repo git_clone" && 36 | 37 | make_hg_repo && 38 | make_hg_commit "b" dir_or_file && 39 | 40 | hg rm dir_or_file && 41 | mkdir dir_or_file && 42 | make_hg_commit c dir_or_file/test_file && 43 | 44 | clone_repo && 45 | test -d dir_or_file && 46 | test -f dir_or_file/test_file && 47 | 48 | cd .. 49 | ' 50 | 51 | # also issue #36 52 | test_expect_failure 'clone replacing a symlink with a directory' ' 53 | test_when_finished "rm -rf hg_repo git_clone" && 54 | 55 | make_hg_repo && 56 | ln -s test_file dir_or_link && 57 | hg add dir_or_link && 58 | hg commit -m "b" && 59 | hg rm dir_or_link && 60 | mkdir dir_or_link && 61 | make_hg_commit c dir_or_link/test_file && 62 | 63 | clone_repo && 64 | 65 | test -d dir_or_link && 66 | test -f dir_or_link/test_file && 67 | 68 | cd .. 69 | ' 70 | 71 | test_expect_success 'clone replace directory with a file' ' 72 | test_when_finished "rm -rf hg_repo git_clone" && 73 | 74 | make_hg_repo && 75 | mkdir dir_or_file && 76 | make_hg_commit "b" dir_or_file/test_file && 77 | hg rm dir_or_file/test_file && 78 | make_hg_commit "c" dir_or_file && 79 | 80 | clone_repo && 81 | 82 | test -f dir_or_file && 83 | 84 | cd .. 85 | ' 86 | 87 | test_expect_success 'clone replace file with a symlink' ' 88 | test_when_finished "rm -rf hg_repo git_clone" && 89 | 90 | make_hg_repo && 91 | make_hg_commit b link_or_file && 92 | hg rm link_or_file && 93 | ln -s test_file link_or_file && 94 | hg add link_or_file && 95 | hg commit -m "c" && 96 | 97 | clone_repo && 98 | 99 | test -f link_or_file && 100 | test -L link_or_file && 101 | 102 | cd .. 103 | ' 104 | 105 | test_expect_success 'clone replace directory with symlink' ' 106 | test_when_finished "rm -rf hg_repo git_clone" && 107 | 108 | make_hg_repo && 109 | mkdir dir_or_link && 110 | make_hg_commit b dir_or_link/test_file && 111 | hg rm dir_or_link/test_file && 112 | ln -s test_file dir_or_link && 113 | hg add dir_or_link && 114 | hg commit -m c 115 | 116 | clone_repo && 117 | 118 | test -f dir_or_link && 119 | test -L dir_or_link && 120 | 121 | cd .. 122 | ' 123 | 124 | test_done 125 | -------------------------------------------------------------------------------- /test/test_notes.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg notes' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_success 'basic clone with notes' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | 20 | make_hg_repo && 21 | make_hg_commit b test_file && 22 | hgsha1s=`hg log --template "{node}\n"` && 23 | 24 | clone_repo && 25 | 26 | assert_git_messages "b${NL}a" && 27 | assert_git_notes "$hgsha1s" && 28 | 29 | 30 | cd .. 31 | ' 32 | 33 | test_expect_success 'basic pull with notes' ' 34 | test_when_finished "rm -rf hg_repo git_clone" && 35 | 36 | make_cloned_repo && 37 | make_hg_commit b test_file && 38 | hgsha1s=`hg log --template "{node}\n"` && 39 | 40 | cd ../git_clone && 41 | git pull && 42 | 43 | assert_git_messages "b${NL}a" && 44 | assert_git_notes "$hgsha1s" && 45 | 46 | cd .. 47 | ' 48 | 49 | test_expect_success 'pull notes rename remote' ' 50 | test_when_finished "rm -rf hg_repo git_clone" && 51 | 52 | make_hg_repo && 53 | cd .. && 54 | mkdir git_clone && 55 | cd git_clone && 56 | git init && 57 | git remote add --fetch the_remote testgitifyhg::../hg_repo && 58 | git pull the_remote master && 59 | assert_git_messages "a" && 60 | cd ../hg_repo && 61 | make_hg_commit b test_file && 62 | make_hg_commit c test_file && 63 | cd ../git_clone && 64 | git fetch the_remote && 65 | assert_git_count 3 the_remote/master && 66 | cd ../hg_repo && 67 | make_hg_commit d test_file && 68 | hgsha1s=`hg log --template "{node}\n"` && 69 | 70 | cd ../git_clone && 71 | git remote rename the_remote new_remote_name && 72 | git pull new_remote_name master && 73 | assert_git_messages "d${NL}c${NL}b${NL}a" && 74 | assert_git_notes "$hgsha1s" && 75 | 76 | cd .. 77 | ' 78 | 79 | # see 30 80 | test_expect_failure 'simple push updates notes' ' 81 | test_when_finished "rm -rf hg_repo git_clone" && 82 | 83 | make_hg_repo && 84 | clone_repo && 85 | make_git_commit b test_file && 86 | git push && 87 | cd ../hg_repo && 88 | hgsha1s=`hg log --template "{node}\n"` && 89 | cd ../git_clone && 90 | test_expect_code 0 git fetch && 91 | assert_git_count 2 'origin' && 92 | assert_git_notes $hgsha1s && 93 | 94 | cd .. 95 | ' 96 | 97 | test_expect_success 'simple push updates after pull' ' 98 | test_when_finished "rm -rf hg_repo git_clone" && 99 | 100 | make_hg_repo && 101 | clone_repo && 102 | make_git_commit b test_file && 103 | git push && 104 | test_expect_code 0 git fetch && 105 | cd ../hg_repo && 106 | hg update && 107 | make_hg_commit "c" test_file && 108 | hgsha1s=`hg log --template "{node}\n"` && 109 | cd ../git_clone && 110 | git pull && 111 | assert_git_count 3 && 112 | assert_git_notes "$hgsha1s" && 113 | 114 | cd .. 115 | ' 116 | 117 | test_done 118 | -------------------------------------------------------------------------------- /test/test_pull.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg pull from hg' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_success 'basic pull' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | 20 | make_cloned_repo && 21 | make_hg_commit b test_file && 22 | cd ../git_clone && 23 | git pull && 24 | 25 | assert_git_messages "b${NL}a" && 26 | 27 | cd .. 28 | ' 29 | 30 | test_expect_success 'pull named remote' ' 31 | test_when_finished "rm -rf hg_repo git_clone" && 32 | 33 | make_hg_repo && 34 | cd .. && 35 | mkdir git_repo && 36 | cd git_repo && 37 | git init && 38 | git remote add --fetch the_remote testgitifyhg::../hg_repo 39 | git pull the_remote master && 40 | assert_git_messages a && 41 | cd ../hg_repo && 42 | make_hg_commit b test_file && 43 | make_hg_commit c test_file && 44 | cd ../git_repo && 45 | git fetch the_remote && 46 | assert_git_messages "c${NL}b${NL}a" the_remote/master && 47 | 48 | cd ../hg_repo && 49 | make_hg_commit d test_file && 50 | 51 | cd ../git_repo && 52 | git remote rename the_remote new_remote_name && 53 | git pull new_remote_name master && 54 | assert_git_messages "d${NL}c${NL}b${NL}a" && 55 | 56 | 57 | cd .. 58 | ' 59 | 60 | test_expect_success 'pull from named branch' ' 61 | test_when_finished "rm -rf hg_repo git_clone" && 62 | 63 | make_hg_repo && 64 | hg branch feature && 65 | make_hg_commit b test_file && 66 | 67 | clone_repo && 68 | cd ../hg_repo && 69 | make_hg_commit c test_file && 70 | cd ../git_clone && 71 | git checkout origin/branches/feature --track && 72 | assert_git_messages "b${NL}a" && 73 | git pull && 74 | assert_git_messages "c${NL}b${NL}a" && 75 | 76 | cd .. 77 | ' 78 | 79 | test_expect_success 'pull conflict' ' 80 | test_when_finished "rm -rf hg_repo git_clone" && 81 | 82 | make_cloned_repo && 83 | make_hg_commit b test_file && 84 | cd ../git_clone && 85 | make_git_commit c test_file && 86 | 87 | git pull 2>&1 | grep "Automatic merge failed" && 88 | 89 | cd .. 90 | ' 91 | 92 | test_expect_success 'pull auto merge' ' 93 | test_when_finished "rm -rf hg_repo git_clone" && 94 | 95 | make_cloned_repo && 96 | make_hg_commit b test_file && 97 | cd ../git_clone && 98 | make_git_commit c c && 99 | git pull && 100 | assert_git_count 4 && 101 | # Merge order appears to be non-deterministic, but I would like to see 102 | # this better tested. 103 | 104 | cd .. 105 | ' 106 | 107 | test_expect_success 'pull tags' ' 108 | test_when_finished "rm -rf hg_repo git_clone" && 109 | 110 | make_cloned_repo && 111 | hg tag tag1 && 112 | cd ../git_clone && 113 | git pull && 114 | git tag | grep tag1 && 115 | 116 | cd .. 117 | ' 118 | 119 | test_done 120 | -------------------------------------------------------------------------------- /test/test_push.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg push' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | test_expect_success 'simple push from master' ' 17 | test_when_finished "rm -rf hg_repo git_clone" && 18 | make_hg_repo && 19 | clone_repo && 20 | make_git_commit b test_file && 21 | git push && 22 | # Make sure that the remote ref has updated 23 | test "`git log --pretty=format:%B origin`" = "b${NL}${NL}a" && 24 | 25 | cd ../hg_repo && 26 | assert_hg_messages "b${NL}a" && 27 | hg update && 28 | test_cmp ../git_clone/test_file test_file && 29 | 30 | cd .. 31 | ' 32 | 33 | test_expect_success 'push not create bookmark' ' 34 | test_when_finished "rm -rf hg_repo git_clone" && 35 | make_hg_repo && 36 | clone_repo && 37 | make_git_commit b test_file && 38 | git push && 39 | cd ../hg_repo && 40 | 41 | test "`hg bookmarks`" = "no bookmarks set" && 42 | 43 | cd .. 44 | ' 45 | 46 | test_expect_success 'test push empty repo' ' 47 | test_when_finished "rm -rf hg_repo git_clone" && 48 | 49 | mkdir hg_repo && 50 | cd hg_repo && 51 | hg init && 52 | 53 | clone_repo && 54 | git status | grep "Initial commit" && 55 | make_git_commit a test_file && 56 | git push origin master && 57 | cd ../hg_repo && 58 | assert_hg_messages "a" && 59 | hg update && 60 | test_cmp test_file ../git_clone/test_file && 61 | 62 | cd .. 63 | ' 64 | 65 | test_expect_success 'push conflict default' ' 66 | test_when_finished "rm -rf hg_repo git_clone" && 67 | 68 | make_hg_repo && 69 | 70 | clone_repo && 71 | cd ../hg_repo && 72 | make_hg_commit b test_file && 73 | cd ../git_clone && 74 | make_git_commit c test_file && 75 | test_expect_code 1 git push && 76 | # test it again because we were having issues with it succeeding the second time 77 | test_expect_code 1 git push && 78 | 79 | cd .. 80 | ' 81 | 82 | test_expect_success 'push to named branch' ' 83 | test_when_finished "rm -rf hg_repo git_clone" && 84 | 85 | make_hg_repo && 86 | hg branch branch_one && 87 | make_hg_commit b test_file && 88 | 89 | clone_repo && 90 | git checkout -t "origin/branches/branch_one" && 91 | make_git_commit c test_file && 92 | git push && 93 | 94 | cd ../hg_repo && 95 | assert_hg_messages "c${NL}b${NL}a" && 96 | hg update tip && 97 | test `hg branch` = "branch_one" && 98 | 99 | cd .. 100 | ' 101 | 102 | test_expect_success 'push merged named branch' ' 103 | test_when_finished "rm -rf hg_repo git_clone" && 104 | 105 | make_hg_repo && 106 | hg branch branch-one && 107 | make_hg_commit b1 b && 108 | hg update default && 109 | make_hg_commit c1 c && 110 | 111 | clone_repo && 112 | git merge origin/branches/branch_one && 113 | git push && 114 | 115 | cd ../hg_repo && 116 | hg update && 117 | hg log --template="{desc}" && 118 | assert_hg_messages "Merge${NL}c1${NL}b1${NL}a" 119 | 120 | cd .. 121 | ' 122 | 123 | test_expect_success 'push new named branch' ' 124 | test_when_finished "rm -rf hg_repo git_clone" && 125 | 126 | make_hg_repo && 127 | clone_repo && 128 | git checkout -b branches/branch_one && 129 | make_git_commit b test_file && 130 | git push --set-upstream origin branches/branch_one && 131 | 132 | cd ../hg_repo && 133 | assert_hg_messages "b${NL}a" 134 | hg update tip && 135 | test `hg branch` = "branch_one" && 136 | 137 | cd .. 138 | ' 139 | 140 | test_expect_success 'push conflict named branch' ' 141 | test_when_finished "rm -rf hg_repo git_clone" && 142 | 143 | make_hg_repo && 144 | hg branch feature && 145 | make_hg_commit b test_file && 146 | clone_repo && 147 | cd ../hg_repo && 148 | make_hg_commit c test_file && 149 | cd ../git_clone && 150 | git checkout --track origin/branches/feature && 151 | make_git_commit d test_file && 152 | test_expect_code 1 git push && 153 | 154 | cd .. 155 | ' 156 | 157 | test_expect_success 'fetch after bad push updates master' ' 158 | test_when_finished "rm -rf hg_repo git_clone" && 159 | 160 | make_hg_repo && 161 | clone_repo && 162 | cd ../hg_repo && 163 | make_hg_commit b test_file && 164 | cd ../git_clone && 165 | make_git_commit c c && 166 | test_expect_code 1 git push && 167 | git fetch && 168 | assert_git_messages "b${NL}a" origin/master && 169 | git pull --rebase && 170 | assert_git_messages "c${NL}${NL}b${NL}a" && 171 | git push && 172 | cd ../hg_repo && 173 | hg log --template="{desc}\n" 174 | assert_hg_messages "c${NL}b${NL}a" && 175 | 176 | cd .. 177 | ' 178 | 179 | test_expect_success 'test push after merge' ' 180 | test_when_finished "rm -rf hg_repo git_clone" && 181 | 182 | make_hg_repo && 183 | clone_repo && 184 | cd ../hg_repo && 185 | make_hg_commit b "test_file" && 186 | cd ../git_clone && 187 | make_git_commit c c && 188 | git pull && # automatically merges 189 | assert_git_count 4 && 190 | git push && 191 | cd ../hg_repo && 192 | assert_hg_count 4 && 193 | 194 | cd .. 195 | ' 196 | 197 | test_expect_success 'push two commits' ' 198 | test_when_finished "rm -rf hg_repo git_clone" && 199 | 200 | make_hg_repo && 201 | clone_repo && 202 | make_git_commit b test_file && 203 | make_git_commit c test_file && 204 | git push && 205 | cd ../hg_repo && 206 | assert_hg_messages "c${NL}b${NL}a" 207 | cd .. 208 | ' 209 | 210 | test_expect_success 'push up to date' ' 211 | test_when_finished "rm -rf hg_repo git_clone" && 212 | 213 | make_hg_repo && 214 | clone_repo && 215 | git push 2>&1 | grep "Everything up-to-date" && 216 | 217 | # push with a commit on hg default/git master 218 | cd ../hg_repo && 219 | make_hg_commit b test_file && 220 | cd ../git_clone && 221 | git pull && 222 | git push 2>&1 | grep "Everything up-to-date" && 223 | 224 | # push with a commit on non-default branch 225 | cd ../hg_repo && 226 | hg branch new_branch && 227 | make_hg_commit c test_file && 228 | cd ../git_clone && 229 | git pull && 230 | git checkout origin/branches/new_branch --track && 231 | git push 2>&1 | grep "Everything up-to-date" && 232 | 233 | make_git_commit d test_file && 234 | out=`git push origin branches/new_branch 2>&1` && 235 | echo -e $out && 236 | echo $out | grep "branches/new_branch -> branches/new_branch" && 237 | 238 | cd .. 239 | ' 240 | 241 | test_expect_success 'test git push messages' ' 242 | test_when_finished "rm -rf hg_repo git_clone" && 243 | 244 | make_hg_repo && 245 | clone_repo && 246 | make_git_commit b test_file && 247 | out=`git push 2>&1` && 248 | ! echo $out | grep "new branch" && 249 | echo $out | grep "master -> master" && 250 | 251 | git checkout -b branches/test_branch && 252 | make_git_commit c test_file && 253 | out=`git push --set-upstream origin branches/test_branch 2>&1` && 254 | echo $out | grep "new branch" && 255 | echo $out | grep "branches/test_branch -> branches/test_branch" && 256 | 257 | make_git_commit c test_file && 258 | out=`git push 2>&1` && 259 | ! echo $out | grep "new branch" && 260 | echo $out | grep "branches/test_branch -> branches/test_branch" && 261 | 262 | cd .. 263 | ' 264 | 265 | test_expect_success 'handle paths with whitespace' ' 266 | test_when_finished "rm -rf hg_repo git_clone" && 267 | make_hg_repo && 268 | clone_repo && 269 | make_git_commit b "test file" && 270 | git push && 271 | # Make sure that the remote ref has updated 272 | test "`git log --pretty=format:%B origin`" = "b${NL}${NL}a" && 273 | 274 | cd ../hg_repo && 275 | assert_hg_messages "b${NL}a" && 276 | hg update && 277 | test_cmp "../git_clone/test file" "test file" && 278 | 279 | cd .. 280 | ' 281 | 282 | test_expect_success 'handle paths with quotes' ' 283 | test_when_finished "rm -rf hg_repo git_clone" && 284 | make_hg_repo && 285 | clone_repo && 286 | make_git_commit b "\"test file\"" && 287 | git push && 288 | # Make sure that the remote ref has updated 289 | test "`git log --pretty=format:%B origin`" = "b${NL}${NL}a" && 290 | 291 | cd ../hg_repo && 292 | assert_hg_messages "b${NL}a" && 293 | hg update && 294 | test_cmp "../git_clone/\"test file\"" "\"test file\"" && 295 | 296 | cd .. 297 | ' 298 | 299 | test_done 300 | -------------------------------------------------------------------------------- /test/test_push_tags.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg push tags' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_success 'push lightweight tag' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | 20 | make_hg_repo && 21 | clone_repo && 22 | git tag "this_is_a_tag" && 23 | git push --tags && 24 | 25 | cd ../hg_repo && 26 | hg tags | grep "this_is_a_tag" && 27 | assert_hg_count 2 && 28 | 29 | cd .. 30 | ' 31 | 32 | # FIXME: See #77 33 | test_expect_success 'lightweight tag sets hg username' ' 34 | test_when_finished "rm -rf hg_repo git_clone .hgrc" && 35 | user="Lite Wait " && 36 | 37 | # NOTE: sharness set #HOME to the working directory for us, so this is 38 | # the default hgrc. 39 | echo "[ui]${NL}username=$user" > .hgrc 40 | make_hg_repo && 41 | clone_repo && 42 | git tag "lightweight" && 43 | git push --tags && 44 | 45 | cd ../hg_repo && 46 | assert_hg_count 2 && 47 | assert_hg_author "$user" && 48 | 49 | cd .. 50 | ' 51 | 52 | test_expect_success 'push tag with subsequent commits' ' 53 | test_when_finished "rm -rf hg_repo git_clone" && 54 | 55 | make_hg_repo && 56 | clone_repo && 57 | git tag this_is_a_tag && 58 | make_git_commit b test_file && 59 | git push origin HEAD --tags && 60 | 61 | cd ../hg_repo && 62 | hg tags | grep this_is_a_tag && 63 | assert_hg_messages "Added tag this_is_a_tag for changeset $(hg id --id)${NL}b${NL}a" 64 | cd .. 65 | ' 66 | 67 | test_expect_success 'push tag with previous commits' ' 68 | test_when_finished "rm -rf hg_repo git_clone" && 69 | 70 | make_hg_repo && 71 | hgsha1=`hg id --id` && 72 | hg tag an_old_tag && 73 | clone_repo && 74 | make_git_commit b test_file && 75 | git tag this_is_a_tag && 76 | git push --tags && 77 | 78 | cd ../hg_repo && 79 | hg tags | grep this_is_a_tag && 80 | assert_hg_messages "Added tag this_is_a_tag for changeset $(hg id --id -r 2)${NL}b${NL}Added tag an_old_tag for changeset $hgsha1${NL}a" && 81 | 82 | cd .. 83 | ' 84 | 85 | test_expect_success 'push messaged tag' ' 86 | test_when_finished "rm -rf hg_repo git_clone" && 87 | 88 | make_hg_repo && 89 | clone_repo && 90 | git tag this_is_a_tag --message="I tagged a message and a user" && 91 | git push --tags && 92 | 93 | cd ../hg_repo && 94 | hg tags | grep this_is_a_tag && 95 | assert_hg_messages "I tagged a message and a user${NL}a" && 96 | hg log && 97 | # FIXME: I feel like this should be $GIT_USER 98 | # but git seems to be passing me the e-mail twice. Is this a bug in 99 | # git or something gitifyhg needs to parse? 100 | assert_hg_author "$GIT_AUTHOR_NAME $GIT_AUTHOR_EMAIL <$GIT_AUTHOR_EMAIL>" && 101 | cd .. 102 | ' 103 | 104 | test_expect_success 'push tag different branch' ' 105 | test_when_finished "rm -rf hg_repo git_clone" && 106 | 107 | make_hg_repo && 108 | hg branch branch_one && 109 | make_hg_commit b test_file && 110 | clone_repo && 111 | git checkout origin/branches/branch_one --track && 112 | git tag this_is_a_tag && 113 | git push --tags && 114 | 115 | cd ../hg_repo && 116 | hg tags | grep this_is_a_tag && 117 | assert_hg_count 3 && 118 | hg update tip && 119 | hg branch | grep branch_one && 120 | 121 | cd .. 122 | ' 123 | 124 | test_expect_success 'push only new tag' ' 125 | test_when_finished "rm -rf hg_repo git_clone" && 126 | 127 | make_hg_repo && 128 | hg tag an_old_tag && 129 | clone_repo && 130 | git tag this_is_a_tag && 131 | git push --tags && 132 | 133 | cd ../hg_repo && 134 | test `hg tags | wc -l` -eq 3 && # 3 is tip 135 | hg tags | grep this_is_a_tag && 136 | hg tags | grep an_old_tag && 137 | assert_hg_count 3 && 138 | 139 | cd .. 140 | ' 141 | 142 | test_done 143 | -------------------------------------------------------------------------------- /test/test_spaces.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg clone, pull, and push with spaces' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | test_expect_success 'clone branch with spaces' ' 18 | test_when_finished "rm -rf hg_repo git_clone" && 19 | make_hg_repo && 20 | hg branch "feature branch" && 21 | make_hg_commit b test_file && 22 | clone_repo && 23 | assert_git_messages "a" && 24 | test "`git branch -r`" = " origin/HEAD -> origin/master 25 | origin/branches/feature___branch 26 | origin/master" && 27 | 28 | git checkout branches/feature___branch && 29 | test_cmp ../hg_repo/test_file test_file && 30 | assert_git_messages "b${NL}a" && 31 | 32 | cd .. 33 | 34 | ' 35 | 36 | test_expect_success 'clone bookmark with spaces' ' 37 | test_when_finished "rm -rf hg_repo git_clone" && 38 | make_hg_repo && 39 | hg bookmark "feature bookmark" 40 | make_hg_commit b test_file 41 | 42 | clone_repo 43 | 44 | test "`git branch -r`" = " origin/HEAD -> origin/master 45 | origin/feature___bookmark 46 | origin/master" && 47 | 48 | git checkout origin/feature___bookmark && 49 | assert_git_messages "b${NL}a" && 50 | git checkout master && 51 | assert_git_messages "b${NL}a" && 52 | 53 | cd .. 54 | ' 55 | 56 | test_expect_success 'clone tag with spaces' ' 57 | test_when_finished "rm -rf hg_repo git_clone" && 58 | 59 | make_hg_repo && 60 | make_hg_commit b test_file && 61 | hg tag "this is tagged" && 62 | make_hg_commit c test_file && 63 | 64 | clone_repo && 65 | 66 | test $(git tag) = "this___is___tagged" && 67 | git checkout this___is___tagged && 68 | assert_git_messages "b${NL}a" && 69 | 70 | cd .. 71 | ' 72 | 73 | test_expect_success 'push to named branch with spaces' ' 74 | test_when_finished "rm -rf hg_repo git_clone" && 75 | 76 | make_hg_repo && 77 | hg branch "branch one" && 78 | make_hg_commit b test_file && 79 | 80 | clone_repo && 81 | git checkout -t "origin/branches/branch___one" && 82 | make_git_commit c test_file && 83 | git push && 84 | 85 | cd ../hg_repo && 86 | hg log --template="{desc}\n" && 87 | assert_hg_messages "c${NL}b${NL}a" && 88 | hg update tip && 89 | test "`hg branch`" = "branch one" && 90 | 91 | cd .. 92 | ' 93 | 94 | test_expect_success 'push to bookmark with spaces' ' 95 | test_when_finished "rm -rf hg_repo git_clone" && 96 | 97 | make_hg_repo && 98 | hg bookmark "feature bookmark" && 99 | make_hg_commit b test_file && 100 | clone_repo && 101 | git checkout --track origin/feature___bookmark && 102 | make_git_commit c test_file && 103 | git push && 104 | 105 | cd ../hg_repo && 106 | hg update && 107 | 108 | assert_hg_messages "c${NL}b${NL}a" && 109 | hg bookmark | grep "feature bookmark" && 110 | hg update "feature bookmark" && 111 | test_cmp test_file ../git_clone/test_file && 112 | 113 | cd .. 114 | ' 115 | 116 | test_expect_success 'push tag with spaces' ' 117 | test_when_finished "rm -rf hg_repo git_clone" && 118 | 119 | make_hg_repo && 120 | clone_repo && 121 | git tag "this___is___a___tag" && 122 | git push --tags && 123 | 124 | cd ../hg_repo && 125 | hg tags | grep "this is a tag" && 126 | 127 | cd .. 128 | ' 129 | 130 | test_expect_success 'push after rm file with spaces' ' 131 | test_when_finished "rm -rf hg_repo git_clone" && 132 | 133 | make_hg_repo && 134 | clone_repo && 135 | make_git_commit b "name with spaces" && 136 | 137 | git rm "name with spaces" && 138 | git commit -m "c" && 139 | 140 | git push && 141 | 142 | cd ../hg_repo && 143 | 144 | hg update && 145 | ls && 146 | 147 | cd .. 148 | ' 149 | 150 | test_expect_success 'push named branch with spaces' ' 151 | test_when_finished "rm -rf hg_repo git_clone" && 152 | 153 | make_hg_repo && 154 | hg branch "feature one" && 155 | make_hg_commit b test_file && 156 | clone_repo && 157 | cd ../hg_repo && 158 | make_hg_commit c test_file && 159 | cd ../git_clone && 160 | git checkout origin/branches/feature___one --track && 161 | assert_git_messages "b${NL}a" && 162 | git pull && 163 | assert_git_messages "c${NL}b${NL}a" && 164 | 165 | cd .. 166 | ' 167 | 168 | test_expect_success 'pull tags with spaces' ' 169 | test_when_finished "rm -rf hg_repo git_clone" && 170 | 171 | make_cloned_repo && 172 | hg tag "tag one" && 173 | cd ../git_clone && 174 | git pull && 175 | git tag | grep tag___one && 176 | 177 | cd .. 178 | ' 179 | 180 | 181 | test_done 182 | -------------------------------------------------------------------------------- /test/test_special_cases.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='Test gitifyhg notes' 4 | 5 | . ./test-lib.sh 6 | 7 | # if ! test_have_prereq PYTHON; then 8 | # skip_all='skipping gitifyhg tests; python not available' 9 | # test_done 10 | # fi 11 | 12 | # if ! "$PYTHON_PATH" -c 'import mercurial'; then 13 | # skip_all='skipping gitifyhg tests; mercurial not available' 14 | # test_done 15 | # fi 16 | 17 | SB=$'\xE2\x98\xA0' 18 | 19 | test_expect_failure 'unicode paths' ' 20 | # NOTE: This is failing, but I do not know why. It works in py.test. 21 | # The error is in os.getcwdu. I am not sure if it is a test problem or 22 | # a bug in gitifyhg. 23 | 24 | # This test has been ported from py.test but not fully tested due to the 25 | # early error. 26 | oldlang=$LANG && 27 | oldlc=$LC_ALL && 28 | export LANG="en_US.utf8" && 29 | export LC_ALL="en_US.utf8" && 30 | test_when_finished "rm -rf hg${SB}repo git${SB}clone" && 31 | test_when_finished "export LANG=$oldlang" && 32 | test_when_finished "export LC_ALL=$oldlc" && 33 | 34 | mkdir hg${SB}repo && 35 | cd hg${SB}repo && 36 | echo $SB > file${SB} && 37 | hg init && 38 | hg add file${SB} && 39 | hg commit -m ${SB} --user="$HG_USER" && 40 | cd .. && 41 | git clone testgitifyhg::hg${SB}repo git${SB}clone && 42 | cd git${SB}clone && 43 | git config user.email $GIT_AUTHOR_EMAIL && 44 | git config user.name "$GIT_USER" 45 | assert_git_messages "${SB}" && 46 | 47 | echo ${SB} >> file${SB} && 48 | git add file${SB} && 49 | git commit -m ${SB}2 && 50 | git push && 51 | cd ../hg${SB}repo && 52 | hg update && 53 | assert_hg_messages "${SB}2${NL}${SB}" && 54 | 55 | echo ${SB} >> file${SB} && 56 | hg commit -m "${SB}3" --user="$HG_USER" && 57 | 58 | cd ../git${SB}clone && 59 | git pull && 60 | assert_git_messages "${SB}3${NL}${SB}2${NL}${NL}${SB}" && 61 | 62 | cd .. 63 | ' 64 | 65 | test_expect_failure 'executable bit' ' 66 | test_when_finished "rm -rf hg_repo git_clone" && 67 | 68 | make_hg_repo && 69 | echo b >> test_file && 70 | chmod 644 test_file && 71 | hg commit -m "make file" && 72 | chmod 755 test_file && 73 | hg commit -m "make executable" && 74 | chmod 644 test_file && 75 | hg commit -m "make unexecutable" && 76 | 77 | clone_repo && 78 | test ! -x test_file && 79 | git checkout HEAD^ && 80 | test -x test_file && 81 | git checkout HEAD^ && 82 | test ! -x test_file && 83 | 84 | git checkout master && 85 | chmod 755 test_file && 86 | git add test_file && 87 | git commit -m "make executable again" && 88 | git push && 89 | 90 | cd ../hg_repo && 91 | hg update && 92 | test -x test_file && 93 | 94 | cd .. 95 | ' 96 | test_expect_failure 'symlinks' ' 97 | test_when_finished "rm -rf hg_repo git_clone" && 98 | 99 | make_hg_repo && 100 | ln -s test_file linked && 101 | hg add linked && 102 | hg commit -m c && 103 | 104 | clone_repo && 105 | test -L linked && 106 | ln -s test_file second_link && 107 | git add second_link && 108 | git commit -m d && 109 | git push && 110 | 111 | cd ../hg_repo && 112 | hg update && 113 | test -L second_link && 114 | 115 | cd .. 116 | ' 117 | 118 | test_done 119 | 120 | 121 | here 122 | --------------------------------------------------------------------------------