├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGES.rst ├── CONTRIBUTING.rst ├── INSTALL.rst ├── LICENSE.txt ├── README.rst ├── docs ├── Makefile ├── _static │ └── .gitignore ├── authors.rst ├── changes.rst ├── conf.py ├── index.rst ├── license.rst └── maintainer-guide.rst ├── git_explode ├── __init__.py ├── cli.py ├── exploder.py ├── fragment.py ├── gitutils.py ├── listener.py └── topics.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tests ├── conftest.py ├── test_exploder.py └── travis_install.sh └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | source = git_explode 5 | # omit = bad_file.py 6 | 7 | [report] 8 | # Regexes for lines to exclude from consideration 9 | exclude_lines = 10 | # Have to re-enable the standard pragma 11 | pragma: no cover 12 | 13 | # Don't complain about missing debug-only code: 14 | def __repr__ 15 | if self\.debug 16 | 17 | # Don't complain if tests don't hit defensive assertion code: 18 | raise AssertionError 19 | raise NotImplementedError 20 | 21 | # Don't complain if non-runnable code isn't run: 22 | if 0: 23 | if __name__ == .__main__.: 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary and binary files 2 | *~ 3 | *.py[cod] 4 | *.so 5 | *.cfg 6 | !setup.cfg 7 | *.orig 8 | *.log 9 | *.pot 10 | __pycache__/* 11 | .cache/* 12 | .*.swp 13 | 14 | # Project files 15 | .ropeproject 16 | .project 17 | .pydevproject 18 | .settings 19 | .idea 20 | 21 | # Package files 22 | *.egg 23 | *.eggs/ 24 | .installed.cfg 25 | *.egg-info 26 | 27 | # Unittest and coverage 28 | htmlcov/* 29 | .coverage 30 | .tox 31 | junit.xml 32 | coverage.xml 33 | 34 | # Build and docs folder/files 35 | build/* 36 | dist/* 37 | sdist/* 38 | docs/api/* 39 | docs/_build/* 40 | cover/* 41 | MANIFEST 42 | /.pytest_cache/ 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis configuration file using the build matrix feature 2 | # Read more under http://docs.travis-ci.com/user/build-configuration/ 3 | # THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! 4 | 5 | sudo: false 6 | language: python 7 | virtualenv: 8 | system_site_packages: true 9 | env: 10 | matrix: 11 | - DISTRIB="ubuntu" PYTHON_VERSION="2.7" COVERAGE="true" 12 | - DISTRIB="conda" PYTHON_VERSION="2.7" COVERAGE="false" 13 | - DISTRIB="conda" PYTHON_VERSION="3.3" COVERAGE="false" 14 | - DISTRIB="conda" PYTHON_VERSION="3.4" COVERAGE="false" 15 | addons: 16 | apt: 17 | packages: 18 | - git 19 | - python-pip 20 | install: 21 | - source tests/travis_install.sh 22 | - pip install -r requirements.txt 23 | before_script: 24 | - git config --global user.email "you@example.com" 25 | - git config --global user.name "Your Name" 26 | script: 27 | - python setup.py test 28 | after_success: 29 | - if [[ "$COVERAGE" == "true" ]]; then coveralls || echo "failed"; fi 30 | cache: 31 | - apt 32 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Developers 3 | ========== 4 | 5 | * Adam Spiers 6 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 0.0.1 6 | ============= 7 | 8 | - First release. 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Contributing to `git-explode` 3 | =============================== 4 | 5 | Issue tracking 6 | ============== 7 | 8 | Any kind of feedback is very welcome; please first check that your bug 9 | / issue / enhancement request is not already listed here: 10 | 11 | * https://github.com/aspiers/git-explode/issues 12 | 13 | and if not then file a new issue. 14 | 15 | Helping with development 16 | ======================== 17 | 18 | Any `pull request `_ 19 | providing an enhancement or bugfix is extremely welcome! 20 | 21 | However my spare time to work on this project is very limited, so 22 | please follow these 23 | `guidelines on contributing `_ so that you can help me to help you ;-) 24 | 25 | Thanks in advance! 26 | -------------------------------------------------------------------------------- /INSTALL.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | ``git-explode`` requires `git-deps 5 | `_, which requires `pygit2 6 | `_, which in return requires `libgit2 7 | `_. These dependencies can potentially 8 | make installation slightly tricky in some cases, but if you manage to 9 | get ``git-deps`` installed and working, then it is very likely that 10 | ``git-explode`` will install on top with no extra problems. 11 | 12 | Therefore the recommended installation procedure is to first follow 13 | `the instructions for installing git-deps 14 | `_, and 15 | then install ``git-explode`` on top via the same technique you used to 16 | install ``git-deps``, which is typically a standard Python module 17 | installation technique. So for example if you installed ``git-deps`` 18 | system-wide on Linux via:: 19 | 20 | sudo pip install git-deps 21 | 22 | then you should repeat that technique for ``git-explode``:: 23 | 24 | sudo pip install git-explode 25 | 26 | Similarly if you installed ``git-deps`` just for the current user via:: 27 | 28 | pip install --user git-deps 29 | 30 | then repeat that for ``git-explode``:: 31 | 32 | pip install --user git-explode 33 | 34 | If you encounter problems, please `report them `_! 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | git-explode 3 | =========== 4 | 5 | ``git-explode`` is a tool for automatically exploding a single git 6 | branch into a number of smaller branches which are textually 7 | independent. It uses `git-deps 8 | `_ to automatically detect 9 | textual dependencies between the commits in the branch, and calculates 10 | the grouping and ordering of commits into independent sub-topics from 11 | which the new branches are created. 12 | 13 | I have `blogged about git-explode and related tools 14 | `_, and also 15 | publically spoken about the tool several times: 16 | 17 | - `a presentation at the openSUSE Summit in Nashville, Apr 2019 `_ 18 | - `a presentation at the OpenStack PTG in Denver, Sept 2018 `_ (`watch the video `_) 19 | - `a presentation at the London Git User Meetup in May 2018 `_ (`watch the video `_) 20 | 21 | 22 | Use case #1 23 | =========== 24 | 25 | The most obvious use case for this tool is helping improve the 26 | "hygiene" of branch management, so that each branch in your repository 27 | is tightly and cleanly scoped to a single logical topic. 28 | 29 | For example during work on feature branch, you might become aware of 30 | an opportunity to refactor some existing code, and might decide to 31 | take advantage of that opportunity immediately, by adding refactoring 32 | commits to the tip of the feature branch. And during the refactoring, 33 | you may even spot a bug, and end up also adding a bugfix to the same 34 | feature branch. 35 | 36 | So now you have a feature branch which is polluted by commits which 37 | perform refactoring and bugfixing. If you were to submit this branch 38 | for code review as a single `GitHub pull request 39 | `_ (or `GitLab 40 | merge request 41 | `_, or 42 | `Gerrit change topic 43 | `_), 44 | it would be a lot harder for your collaborators to review than if you 45 | had separately submitted three smaller reviews, one for the bugfix, 46 | one for the refactoring, and one for the new feature. 47 | 48 | In this scenario, ``git-explode`` comes to the rescue! Rather than you 49 | having to manually separate out the commits into topic branches, it 50 | can do all the hard work for you with a single command. 51 | 52 | 53 | Textual vs. semantic (in)dependence 54 | =================================== 55 | 56 | Astute readers will note that textual independence (as detected by 57 | ``git-deps`` and used by ``git-explode``) is not the same as semantic / 58 | logical independence. Textual independence means that the changes can 59 | be applied in any order without incurring conflicts, but this is not a 60 | reliable indicator of logical independence. (This caveat is also 61 | noted in `the README for git-deps 62 | `_.) 63 | 64 | For example a change to a function and corresponding changes to the 65 | tests and/or documentation for that function would typically exist in 66 | different files. So if those changes were in separate commits within 67 | a branch, running ``git-explode`` on the branch would place those 68 | commits in separate branches even though they are logically related, 69 | because changes in different files (or even in different areas of the 70 | same files) are textually independent. 71 | 72 | So in this case, ``git-explode`` would not behave exactly how we might 73 | want. And for as long as AI is an unsolved problem, it is very 74 | unlikely that it will ever develop totally reliable behaviour. 75 | So does that mean ``git-explode`` is useless? Absolutely not! 76 | 77 | Firstly, when `best practices 78 | `_ 79 | for `commit structuring 80 | `_ 81 | are adhered to, changes which are strongly logically related should be 82 | placed within the same commit anyway. So in the example above, a 83 | change to a function and corresponding changes to the tests and/or 84 | documentation for that function should all be within a single commit. 85 | (Although this is not the only valid approach; for a more advanced 86 | meta-history grouping mechanism, see `git-dendrify 87 | `_.) 88 | 89 | Secondly, whilst textual independence does not imply logical 90 | independence, the converse is much more commonly true: logical 91 | independence typically implies textual independence. So while it 92 | might not be too uncommon for ``git-explode`` to separate 93 | logically-related changes into different branches, it should be pretty 94 | rare that it groups logically *unrelated* changes on the same branch. 95 | Combining this with the fact that ``git`` makes it easier to join 96 | commits on separate branches back together into one branch than to 97 | split them apart suggests that ``git-explode`` still has plenty of 98 | potential for saving effort. 99 | 100 | Thirdly, it is often unhelpful to allow `the quest for the perfect 101 | become the enemy of the good 102 | `_ - a 103 | tool does not have to be perfect to be useful; it only has to be 104 | better than performing the same task without the tool. 105 | 106 | Further discussion on these points can be found in `an old thread from 107 | the git mailing list 108 | `_. 109 | 110 | Ultimately though, `"the proof is in the pudding" 111 | `_, so try 112 | it out and see! 113 | 114 | 115 | Other use cases 116 | =============== 117 | 118 | As already explained above, the most obvious use case is cleaning up 119 | the mess caused by logically independent commits all mashed together 120 | into one branch. However here are some further use cases. If you 121 | can think of others, please `submit them `_! 122 | 123 | 124 | Use case #2: Decompose changes for porting 125 | ------------------------------------------ 126 | 127 | If you need to backport or forward-port changes between two branches, 128 | ``git-explode`` could be used to first decompose the source branch into 129 | textually independent topic branches. Then before any porting starts, 130 | informed decisions can be made about which topics to port and which 131 | not to port, and in which order. Each decomposed topic branch is 132 | guaranteed to be textually independent from the others, so they can be 133 | ported separately - indeed even concurrently by different people - 134 | thereby greatly reducing the risk of conflicts when the independent 135 | topic branches are merged together into the target branch. 136 | 137 | 138 | Use case #3: Publishing a previously private codebase 139 | ----------------------------------------------------- 140 | 141 | Emmet's idea about a company who needs to publish a private 142 | codebase but needs to clean it up first. Similar to 1. but on a 143 | bigger scale. 144 | 145 | 146 | Use case #4: Breaking down giant commits 147 | ---------------------------------------- 148 | 149 | Split giant commit into commits one per hunk, then regroup into topics 150 | along with previous related commits. (Note that the previous related 151 | commits are required here for the regrouping to work, since hunks 152 | within a single commit are by definition independent of each other.) 153 | 154 | 155 | Installation 156 | ============ 157 | 158 | Please see `the INSTALL.rst file `_. 159 | 160 | 161 | Usage 162 | ===== 163 | 164 | Usage is fairly self-explanatory if you run ``git explode -h``:: 165 | 166 | usage: git-explode [-h] [--version] [-d] [-p PREFIX] [-c NUM] BASE HEAD 167 | 168 | Explode linear sequence of commits into topic branches 169 | 170 | positional arguments: 171 | BASE base of sequence to explode 172 | HEAD head of sequence to explode 173 | 174 | optional arguments: 175 | -h, --help show this help message and exit 176 | --version show program's version number and exit 177 | -d, --debug Show debugging 178 | -p PREFIX, --prefix PREFIX 179 | prefix for all created topic branches 180 | -c NUM, --context-lines NUM 181 | Number of lines of diff context to use [1] 182 | 183 | 184 | Development / support / feedback 185 | ================================ 186 | 187 | Please see `the CONTRIBUTING.rst file `_. 188 | 189 | 190 | History 191 | ======= 192 | 193 | I first announced the intention to build this tool `on the git mailing 194 | list in May 2016 195 | `_; 196 | however at the time I was under the mistaken impression that I could 197 | build it out of `the git-splice and git-transplant commands 198 | `_ 199 | which I was working on at that time. 200 | 201 | Thanks to SUSE's generous `Hack Week `_ 202 | policy, I have had the luxury of working on this as a `Hack Week project 203 | `_. 204 | 205 | In May 2018 I took advantage of another Hack Week to apply more polish 206 | and make the first release. This was in preparation for demonstrating 207 | the software at `a Meetup event 208 | `_ of the `Git 209 | London User Group `_. 210 | 211 | 212 | License 213 | ======= 214 | 215 | Released under `GPL version 2 `_ in order to be consistent 216 | with `git's license 217 | `_, but I'm open to 218 | the idea of dual-licensing if there's a convincing reason. 219 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/git-explode.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/git-explode.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $HOME/.local/share/devhelp/git-explode" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $HOME/.local/share/devhelp/git-explode" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | # Empty directory 2 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. _authors: 2 | .. include:: ../AUTHORS.rst 3 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. _changes: 2 | .. include:: ../CHANGES.rst 3 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This file is execfile()d with the current directory set to its containing dir. 4 | # 5 | # Note that not all possible configuration values are present in this 6 | # autogenerated file. 7 | # 8 | # All configuration values have a default; values that are commented out 9 | # serve to show the default. 10 | 11 | import sys 12 | 13 | # If extensions (or modules to document with autodoc) are in another directory, 14 | # add these directories to sys.path here. If the directory is relative to the 15 | # documentation root, use os.path.abspath to make it absolute, like shown here. 16 | # sys.path.insert(0, os.path.abspath('.')) 17 | 18 | # -- Hack for ReadTheDocs ------------------------------------------------------ 19 | # This hack is necessary since RTD does not issue `sphinx-apidoc` before running 20 | # `sphinx-build -b html . _build/html`. See Issue: 21 | # https://github.com/rtfd/readthedocs.org/issues/1139 22 | # DON'T FORGET: Check the box "Install your project inside a virtualenv using 23 | # setup.py install" in the RTD Advanced Settings. 24 | import os 25 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 26 | if on_rtd: 27 | import inspect 28 | from sphinx import apidoc 29 | 30 | __location__ = os.path.join(os.getcwd(), os.path.dirname( 31 | inspect.getfile(inspect.currentframe()))) 32 | 33 | output_dir = os.path.join(__location__, "../docs/api") 34 | module_dir = os.path.join(__location__, "../git_explode") 35 | cmd_line_template = "sphinx-apidoc -f -o {outputdir} {moduledir}" 36 | cmd_line = cmd_line_template.format(outputdir=output_dir, moduledir=module_dir) 37 | apidoc.main(cmd_line.split(" ")) 38 | 39 | # -- General configuration ----------------------------------------------------- 40 | 41 | # If your documentation needs a minimal Sphinx version, state it here. 42 | # needs_sphinx = '1.0' 43 | 44 | # Add any Sphinx extension module names here, as strings. They can be extensions 45 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 46 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 47 | 'sphinx.ext.autosummary', 'sphinx.ext.viewcode', 'sphinx.ext.coverage', 48 | 'sphinx.ext.doctest', 'sphinx.ext.ifconfig', 'sphinx.ext.pngmath', 49 | 'sphinx.ext.napoleon'] 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ['_templates'] 53 | 54 | # The suffix of source filenames. 55 | source_suffix = '.rst' 56 | 57 | # The encoding of source files. 58 | # source_encoding = 'utf-8-sig' 59 | 60 | # The master toctree document. 61 | master_doc = 'index' 62 | 63 | # General information about the project. 64 | project = u'git-explode' 65 | copyright = u'2016, Adam Spiers' 66 | 67 | # The version info for the project you're documenting, acts as replacement for 68 | # |version| and |release|, also used in various other places throughout the 69 | # built documents. 70 | # 71 | # The short X.Y version. 72 | version = '' # Is set by calling `setup.py docs` 73 | # The full version, including alpha/beta/rc tags. 74 | release = '' # Is set by calling `setup.py docs` 75 | 76 | # The language for content autogenerated by Sphinx. Refer to documentation 77 | # for a list of supported languages. 78 | # language = None 79 | 80 | # There are two options for replacing |today|: either, you set today to some 81 | # non-false value, then it is used: 82 | # today = '' 83 | # Else, today_fmt is used as the format for a strftime call. 84 | # today_fmt = '%B %d, %Y' 85 | 86 | # List of patterns, relative to source directory, that match files and 87 | # directories to ignore when looking for source files. 88 | exclude_patterns = ['_build'] 89 | 90 | # The reST default role (used for this markup: `text`) to use for all documents. 91 | # default_role = None 92 | 93 | # If true, '()' will be appended to :func: etc. cross-reference text. 94 | # add_function_parentheses = True 95 | 96 | # If true, the current module name will be prepended to all description 97 | # unit titles (such as .. function::). 98 | # add_module_names = True 99 | 100 | # If true, sectionauthor and moduleauthor directives will be shown in the 101 | # output. They are ignored by default. 102 | # show_authors = False 103 | 104 | # The name of the Pygments (syntax highlighting) style to use. 105 | pygments_style = 'sphinx' 106 | 107 | # A list of ignored prefixes for module index sorting. 108 | # modindex_common_prefix = [] 109 | 110 | # If true, keep warnings as "system message" paragraphs in the built documents. 111 | # keep_warnings = False 112 | 113 | 114 | # -- Options for HTML output --------------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | html_theme = 'alabaster' 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | # html_theme_options = {} 124 | 125 | # Add any paths that contain custom themes here, relative to this directory. 126 | # html_theme_path = [] 127 | 128 | # The name for this set of Sphinx documents. If None, it defaults to 129 | # " v documentation". 130 | try: 131 | from git_explode import __version__ as version 132 | except ImportError: 133 | pass 134 | else: 135 | release = version 136 | 137 | # A shorter title for the navigation bar. Default is the same as html_title. 138 | # html_short_title = None 139 | 140 | # The name of an image file (relative to this directory) to place at the top 141 | # of the sidebar. 142 | # html_logo = "" 143 | 144 | # The name of an image file (within the static path) to use as favicon of the 145 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 146 | # pixels large. 147 | # html_favicon = None 148 | 149 | # Add any paths that contain custom static files (such as style sheets) here, 150 | # relative to this directory. They are copied after the builtin static files, 151 | # so a file named "default.css" will overwrite the builtin "default.css". 152 | html_static_path = ['_static'] 153 | 154 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 155 | # using the given strftime format. 156 | # html_last_updated_fmt = '%b %d, %Y' 157 | 158 | # If true, SmartyPants will be used to convert quotes and dashes to 159 | # typographically correct entities. 160 | # html_use_smartypants = True 161 | 162 | # Custom sidebar templates, maps document names to template names. 163 | # html_sidebars = {} 164 | 165 | # Additional templates that should be rendered to pages, maps page names to 166 | # template names. 167 | # html_additional_pages = {} 168 | 169 | # If false, no module index is generated. 170 | # html_domain_indices = True 171 | 172 | # If false, no index is generated. 173 | # html_use_index = True 174 | 175 | # If true, the index is split into individual pages for each letter. 176 | # html_split_index = False 177 | 178 | # If true, links to the reST sources are added to the pages. 179 | # html_show_sourcelink = True 180 | 181 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 182 | # html_show_sphinx = True 183 | 184 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 185 | # html_show_copyright = True 186 | 187 | # If true, an OpenSearch description file will be output, and all pages will 188 | # contain a tag referring to it. The value of this option must be the 189 | # base URL from which the finished HTML is served. 190 | # html_use_opensearch = '' 191 | 192 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 193 | # html_file_suffix = None 194 | 195 | # Output file base name for HTML help builder. 196 | htmlhelp_basename = 'git_explode-doc' 197 | 198 | 199 | # -- Options for LaTeX output -------------------------------------------------- 200 | 201 | latex_elements = { 202 | # The paper size ('letterpaper' or 'a4paper'). 203 | # 'papersize': 'letterpaper', 204 | 205 | # The font size ('10pt', '11pt' or '12pt'). 206 | # 'pointsize': '10pt', 207 | 208 | # Additional stuff for the LaTeX preamble. 209 | # 'preamble': '', 210 | } 211 | 212 | # Grouping the document tree into LaTeX files. List of tuples 213 | # (source start file, target name, title, author, documentclass [howto/manual]). 214 | latex_documents = [ 215 | ('index', 'user_guide.tex', u'git-explode Documentation', 216 | u'Adam Spiers', 'manual'), 217 | ] 218 | 219 | # The name of an image file (relative to this directory) to place at the top of 220 | # the title page. 221 | # latex_logo = "" 222 | 223 | # For "manual" documents, if this is true, then toplevel headings are parts, 224 | # not chapters. 225 | # latex_use_parts = False 226 | 227 | # If true, show page references after internal links. 228 | # latex_show_pagerefs = False 229 | 230 | # If true, show URL addresses after external links. 231 | # latex_show_urls = False 232 | 233 | # Documents to append as an appendix to all manuals. 234 | # latex_appendices = [] 235 | 236 | # If false, no module index is generated. 237 | # latex_domain_indices = True 238 | 239 | # -- External mapping ------------------------------------------------------------ 240 | python_version = '.'.join(map(str, sys.version_info[0:2])) 241 | intersphinx_mapping = { 242 | 'sphinx': ('http://sphinx.pocoo.org', None), 243 | 'python': ('http://docs.python.org/' + python_version, None), 244 | 'matplotlib': ('http://matplotlib.sourceforge.net', None), 245 | 'numpy': ('http://docs.scipy.org/doc/numpy', None), 246 | 'sklearn': ('http://scikit-learn.org/stable', None), 247 | 'pandas': ('http://pandas.pydata.org/pandas-docs/stable', None), 248 | 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), 249 | } 250 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | git-explode 3 | =========== 4 | 5 | This will become the main documentation of **git-explode**. 6 | 7 | Unfortunately it hasn't been written yet, but in the mean time there 8 | is plenty of useful information on `the GitHub project 9 | `_. 10 | 11 | 12 | Contents 13 | ======== 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | License 19 | Authors 20 | Changelog 21 | Module Reference 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | ======= 4 | License 5 | ======= 6 | 7 | .. literalinclude:: ../LICENSE.txt 8 | -------------------------------------------------------------------------------- /docs/maintainer-guide.rst: -------------------------------------------------------------------------------- 1 | .. _release: 2 | 3 | ================== 4 | Maintainer guide 5 | ================== 6 | 7 | This guide contains a playbook for tasks the maintainer will need to 8 | perform. 9 | 10 | 11 | Initial setup 12 | ============= 13 | 14 | - Create a PyPI account 15 | 16 | - Configure ``~/.pypirc`` with credentials 17 | 18 | - ``pip install twine`` 19 | 20 | 21 | How to make a new release of git-explode 22 | ======================================== 23 | 24 | - Ensure everything is committed and the git working tree is clean. 25 | 26 | - Ensure all change have been pushed to the remote branch. 27 | 28 | - Run ``tox`` to check everything is OK. 29 | 30 | - Decide a new version. Release candidates should take the form 31 | ``1.2.3rc4``. 32 | 33 | - ``git tag -s $version`` 34 | 35 | - ``tox -e sdist`` 36 | 37 | - ``twine dist/git-explode-$version.tar.gz`` 38 | 39 | - Check the new version appears at ``_. 40 | 41 | - Test installation via at least one of the documented options, e.g. 42 | ``pip install git-explode`` within a virtualenv. 43 | 44 | - Test / update the Docker-based installation. 45 | -------------------------------------------------------------------------------- /git_explode/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | try: 4 | __version__ = pkg_resources.get_distribution(__name__).version 5 | except pkg_resources.DistributionNotFound: 6 | __version__ = 'unknown' 7 | -------------------------------------------------------------------------------- /git_explode/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function, absolute_import 5 | 6 | import argparse 7 | import sys 8 | 9 | from git_deps.gitutils import GitUtils 10 | from git_explode import __version__ 11 | from git_explode.exploder import GitExploder 12 | 13 | 14 | def parse_args(args): 15 | """ 16 | Parse command line parameters 17 | 18 | :param args: command line parameters as list of strings 19 | :return: command line parameters as :obj:`argparse.Namespace` 20 | """ 21 | ##################################################################### 22 | # REMINDER!! If you change this, remember to update README.rst too. 23 | ##################################################################### 24 | parser = argparse.ArgumentParser( 25 | description="Explode linear sequence of commits into topic branches") 26 | parser.add_argument( 27 | '--version', 28 | action='version', 29 | version='git-explode {ver}'.format(ver=__version__)) 30 | parser.add_argument( 31 | '-d', '--debug', 32 | dest='debug', 33 | action='store_true', 34 | help='Show debugging') 35 | parser.add_argument( 36 | '-p', '--prefix', 37 | dest="prefix", 38 | help="prefix for all created topic branches", 39 | type=str, 40 | metavar="PREFIX") 41 | parser.add_argument( 42 | '-c', '--context-lines', 43 | dest='context_lines', 44 | help='Number of lines of diff context to use [%(default)s]', 45 | type=int, 46 | metavar='NUM', 47 | default=1) 48 | parser.add_argument( 49 | dest="base", 50 | help="base of sequence to explode", 51 | type=str, 52 | metavar="BASE") 53 | parser.add_argument( 54 | dest="head", 55 | help="head of sequence to explode", 56 | type=str, 57 | metavar="HEAD") 58 | 59 | return parser.parse_args(args) 60 | 61 | 62 | def main(args): 63 | args = parse_args(args) 64 | repo = GitUtils.get_repo() 65 | exploder = GitExploder(repo, args.base, args.head, args.debug, 66 | args.context_lines) 67 | exploder.run() 68 | 69 | 70 | def run(): 71 | main(sys.argv[1:]) 72 | 73 | 74 | if __name__ == "__main__": 75 | run() 76 | -------------------------------------------------------------------------------- /git_explode/exploder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import copy 5 | import six 6 | from ostruct import OpenStruct 7 | 8 | from git_deps.detector import DependencyDetector 9 | from git_deps.gitutils import GitUtils 10 | from git_deps.utils import abort, standard_logger 11 | from git_explode.gitutils import GitUtils as GitExplodeUtils 12 | from git_explode.listener import ExplodeDependencyListener 13 | from git_explode.topics import TopicManager 14 | 15 | 16 | class GitExploder(object): 17 | """Explode a linear sequence of git commits into multiple independent 18 | topic branches. 19 | 20 | """ 21 | def __init__(self, repo, base, head, debug, context_lines): 22 | self.logger = standard_logger('git-explode', debug) 23 | 24 | self.debug = debug 25 | self.repo = repo 26 | self.base = base 27 | self.base_commit = GitUtils.ref_commit(repo, base) 28 | self.logger.debug("base commit %s is %s" % 29 | (base, GitUtils.commit_summary(self.base_commit))) 30 | self.head = head 31 | self.context_lines = context_lines 32 | self.topic_mgr = TopicManager('topic%d', self.logger) 33 | 34 | # Map commits to their exploded version 35 | self.exploded = {} 36 | 37 | def run(self): 38 | orig_head = GitExplodeUtils.get_head() 39 | commits, deps_from, deps_on = self.get_dependencies() 40 | self.explode(commits, deps_from, deps_on) 41 | self.checkout(orig_head) 42 | 43 | def get_dependencies(self): 44 | """ 45 | Detect commit dependency tree, and return a tuple of dicts mapping 46 | this in both directions. Note that the dependency tree goes in 47 | the reverse direction to the git commit graph, in that the leaves 48 | of the dependency tree are the oldest commits, because newer 49 | commits depend on older commits 50 | 51 | :return: (dependencies_from, dependencies_on) 52 | """ 53 | 54 | detector_args = OpenStruct({ 55 | 'recurse': True, 56 | 'exclude_commits': [self.base], 57 | 'debug': self.debug, 58 | 'context_lines': self.context_lines, 59 | }) 60 | detector = DependencyDetector(detector_args, self.repo) 61 | listener = ExplodeDependencyListener({}) 62 | detector.add_listener(listener) 63 | 64 | revs = GitUtils.rev_list("%s..%s" % (self.base, self.head)) 65 | for rev in revs: 66 | try: 67 | detector.find_dependencies(rev) 68 | except KeyboardInterrupt: 69 | pass 70 | 71 | return (detector.commits, 72 | listener.dependencies_from(), 73 | listener.dependencies_on()) 74 | 75 | def explode(self, commits, deps_from, deps_on): 76 | """ 77 | Walk the dependency tree breadth-first starting with the 78 | leaves at the bottom. 79 | 80 | For each commit, figure out whether it should be exploded 81 | 82 | :param commits: dict mapping SHA1 hashes to pygit2.Commit objects 83 | :param deps_from: dict mapping dependents to dependencies 84 | :param deps_on: dict mapping in opposite direction 85 | """ 86 | todo = self.get_leaves(commits, deps_from) 87 | 88 | # Each time we explode a commit, we'll remove it from any 89 | # dict which is a value of this dict. 90 | unexploded_deps_from = copy.deepcopy(deps_from) 91 | 92 | self.logger.debug("Initial queue of leaves:") 93 | for commit in todo: 94 | self.logger.debug(' ' + GitUtils.commit_summary(commit)) 95 | 96 | self.current_branch = None 97 | 98 | while todo: 99 | commit = todo.pop(0) 100 | sha = commit.hex 101 | self.logger.debug("Exploding %s" % GitUtils.commit_summary(commit)) 102 | if unexploded_deps_from[sha]: 103 | abort("BUG: unexploded deps from %s" % 104 | GitUtils.commit_summary(commit)) 105 | 106 | deps = deps_from[sha] 107 | self.prepare_cherrypick_base(sha, deps, commits) 108 | self.cherry_pick(sha) 109 | 110 | self.queue_new_leaves(todo, commit, commits, deps_on, 111 | unexploded_deps_from) 112 | 113 | def prepare_cherrypick_base(self, sha, deps, commits): 114 | if not deps: 115 | branch = self.topic_mgr.next() 116 | # We don't assign the topic here, because it will get 117 | # assigned by cherry_pick(), and it needs to be done there 118 | # to also catch the case where we are cherry-picking to 119 | # update an existing branch. 120 | self.checkout_new(branch, self.base) 121 | return 122 | 123 | deps = deps.keys() 124 | assert len(deps) >= 1 125 | self.logger.debug(" deps: %s" % ' '.join([d[:8] for d in deps])) 126 | 127 | existing_branch = self.topic_mgr.lookup(*deps) 128 | if len(deps) == 1: 129 | if existing_branch is None: 130 | self.checkout_new_dependent_topic(deps) 131 | else: 132 | branch = existing_branch 133 | self.checkout(branch) 134 | elif len(deps) > 1: 135 | # We'll need to base the cherry-pick on a merge commit 136 | if existing_branch is None: 137 | self.checkout_new_dependent_topic(deps) 138 | to_merge = (self.exploded[dep] for dep in deps[1:]) 139 | GitExplodeUtils.git('merge', *to_merge) 140 | else: 141 | # Can reuse existing merge commit, but 142 | # create a new branch at the same point 143 | self.checkout_new(branch, existing_branch) 144 | 145 | def queue_new_leaves(self, todo, exploded_commit, commits, deps_on, 146 | unexploded_deps_from): 147 | """When a commit is exploded, there may be other commits in the 148 | dependency tree which only had a single dependency on this 149 | commit. In that case they have effectively become leaves on 150 | the dependency tree of unexploded commits, so they should be 151 | added to the explode queue. 152 | 153 | """ 154 | sha1 = exploded_commit.hex 155 | for dependent in deps_on[sha1]: 156 | del unexploded_deps_from[dependent][sha1] 157 | if not unexploded_deps_from[dependent]: 158 | new = commits[dependent] 159 | self.logger.debug("+ pushed to queue: %s" % 160 | GitUtils.commit_summary(new)) 161 | todo.insert(0, new) 162 | 163 | def get_leaves(self, commits, deps_from): 164 | """ 165 | Return all the leaves of the dependency tree, i.e. commits with 166 | no child dependencies 167 | """ 168 | leaves = [] 169 | for sha, dependencies in six.iteritems(deps_from): 170 | if len(dependencies) == 0: 171 | leaves.append(commits[sha]) 172 | return leaves 173 | 174 | def checkout(self, branch): 175 | if self.current_branch == branch: 176 | return 177 | # self.logger.debug("checkout %s" % branch) 178 | GitExplodeUtils.checkout(branch) 179 | self.current_branch = branch 180 | 181 | def checkout_new(self, branch, at): 182 | assert self.current_branch != branch 183 | # self.logger.debug("checkout -b %s %s" % (branch, at)) 184 | GitExplodeUtils.checkout_new(branch, at) 185 | self.current_branch = branch 186 | 187 | def checkout_new_dependent_topic(self, deps): 188 | branch = self.topic_mgr.register(*deps) 189 | base = self.exploded[deps[0]] 190 | self.checkout_new(branch, base) 191 | 192 | def cherry_pick(self, sha): 193 | GitExplodeUtils.git('cherry-pick', sha) 194 | self.update_current_topic(sha) 195 | head = GitExplodeUtils.get_head_sha1() 196 | self.exploded[sha] = head 197 | commit = GitUtils.ref_commit(self.repo, sha) 198 | self.logger.debug("- cherry-picked %s as %s (%s)" % 199 | (sha[:8], self.exploded[sha][:8], 200 | GitUtils.oneline(commit))) 201 | 202 | def update_current_topic(self, *commits): 203 | self.topic_mgr.assign(self.current_branch, *commits) 204 | -------------------------------------------------------------------------------- /git_explode/fragment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class Fragment(object): 6 | """Tracks fragments of the git explosion, i.e. commits which 7 | were exploded off the source branch into smaller topic branches. 8 | 9 | """ 10 | i = 0 11 | fragments = {} 12 | 13 | def lookup(self, *commits): 14 | name = self._name_for(*commits) 15 | return self.topics.get(name) 16 | 17 | def register(self, *commits): 18 | new = self._next() 19 | self.assign(new, *commits) 20 | return new 21 | 22 | def assign(self, topic, *commits): 23 | name = self._name_for(*commits) 24 | self.topics[name] = topic 25 | 26 | def _name_for(self, *commits): 27 | return ' '.join(sorted(commits)) 28 | 29 | def _next(self): 30 | self.i += 1 31 | return self.template % self.i 32 | -------------------------------------------------------------------------------- /git_explode/gitutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function, absolute_import 5 | 6 | import subprocess 7 | 8 | 9 | class GitUtils(object): 10 | @classmethod 11 | def git(cls, *args): 12 | cmd_words = ['git'] + list(args) 13 | print(' '.join(cmd_words)) 14 | return cls.quiet_git(*args) 15 | 16 | @classmethod 17 | def quiet_git(cls, *args): 18 | cmd_words = ['git'] + list(args) 19 | output = subprocess.check_output(cmd_words, universal_newlines=True) 20 | return output.rstrip() 21 | 22 | @classmethod 23 | def get_head(cls): 24 | """Retrieve the branch or reference to the current HEAD. 25 | """ 26 | try: 27 | return cls.quiet_git('symbolic-ref', '--short', '-q', 'HEAD') 28 | except subprocess.CalledProcessError: 29 | return cls.get_head_sha1() 30 | 31 | @classmethod 32 | def get_head_sha1(cls): 33 | return cls.quiet_git('rev-parse', 'HEAD') 34 | 35 | @classmethod 36 | def checkout(cls, branch): 37 | cls.git('checkout', '-q', branch) 38 | 39 | @classmethod 40 | def checkout_new(cls, branch, at): 41 | cls.git('checkout', '-q', '-b', branch, at) 42 | -------------------------------------------------------------------------------- /git_explode/listener.py: -------------------------------------------------------------------------------- 1 | from git_deps.listener.base import DependencyListener 2 | 3 | 4 | class ExplodeDependencyListener(DependencyListener): 5 | """Dependency listener for use when building a dependency tree to be 6 | used for exploding the commits into multiple topic branches. 7 | """ 8 | 9 | def __init__(self, options): 10 | super(ExplodeDependencyListener, self).__init__(options) 11 | 12 | # Map each commit to a dict whose keys are the dependencies of 13 | # that commit which haven't yet been exploded into a topic 14 | # branch. 15 | self._dependencies_from = {} 16 | self._dependencies_on = {} 17 | 18 | def new_commit(self, commit): 19 | """Adds the commit if it doesn't already exist. 20 | """ 21 | sha1 = commit.hex 22 | for d in (self._dependencies_from, self._dependencies_on): 23 | if sha1 not in d: 24 | d[sha1] = {} 25 | 26 | def new_dependency(self, dependee, dependency, path, line_num): 27 | src = dependee.hex 28 | dst = dependency.hex 29 | 30 | cause = "%s:%d" % (path, line_num) 31 | self._dependencies_from[src][dst] = cause 32 | self._dependencies_on[dst][src] = cause 33 | 34 | def dependencies_from(self): 35 | return self._dependencies_from 36 | 37 | def dependencies_on(self): 38 | return self._dependencies_on 39 | -------------------------------------------------------------------------------- /git_explode/topics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class TopicManager(object): 6 | """Acts as a factory for topic branch names, and a registry for 7 | mapping both individual commits and merges of commits to topic 8 | branch names. 9 | 10 | This ensures we always know onto which topic branch to explode 11 | (cherry-pick) a new commit. 12 | 13 | """ 14 | i = 0 15 | topics = {} 16 | commits = {} 17 | 18 | def __init__(self, template, logger): 19 | self.template = template 20 | self.logger = logger 21 | 22 | def lookup(self, *commits): 23 | name = self._name_for(*commits) 24 | return self.topics.get(name) 25 | 26 | def register(self, *commits): 27 | new = self.next() 28 | self.assign(new, *commits) 29 | return new 30 | 31 | def _assign(self, topic, *commits): 32 | name = self._name_for(*commits) 33 | self.topics[name] = topic 34 | self.commits[topic] = name 35 | self.logger.debug(" Assigned %s to %s" % (topic, name)) 36 | 37 | def assign(self, topic, *commits): 38 | old_commits = self.commits.get(topic) 39 | if old_commits: 40 | self.unassign(topic, old_commits) 41 | self._assign(topic, *commits) 42 | 43 | def unassign(self, topic, commits): 44 | del self.topics[commits] 45 | del self.commits[topic] 46 | self.logger.debug(" Unassigned %s from %s" % (topic, commits)) 47 | 48 | def _name_for(self, *commits): 49 | return ' '.join(sorted(commits)) 50 | 51 | def next(self): 52 | self.i += 1 53 | return self.template % self.i 54 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git-deps >= 1.0.1 2 | ostruct 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = git-explode 3 | summary = Explode linear sequence of commits into topic branches 4 | author = Adam Spiers 5 | author-email = git@adamspiers.org 6 | license = GPL-2+ 7 | home-page = https://github.com/aspiers/git-explode 8 | description-file = README.rst 9 | classifier = 10 | Development Status :: 4 - Beta 11 | Environment :: Console 12 | Intended Audience :: Developers 13 | License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) 14 | Natural Language :: English 15 | Operating System :: OS Independent 16 | Programming Language :: Python 17 | Topic :: Software Development :: Version Control 18 | Topic :: Utilities 19 | 20 | [entry_points] 21 | console_scripts = 22 | git-explode = git_explode.cli:run 23 | 24 | [files] 25 | packages = 26 | git_explode 27 | 28 | [test] 29 | # py.test options when running `python setup.py test` 30 | addopts = tests 31 | 32 | [pytest] 33 | # Options for py.test: 34 | # Specify command line options as you would do when invoking py.test directly. 35 | # e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml 36 | # in order to write a coverage file that can be read by Jenkins. 37 | addopts = 38 | --cov git_explode --cov-report term-missing 39 | --verbose 40 | 41 | [aliases] 42 | docs = build_sphinx 43 | 44 | [bdist_wheel] 45 | # Use this option if your package is pure-python 46 | universal = 1 47 | 48 | [build_sphinx] 49 | source_dir = docs 50 | build_dir = docs/_build 51 | 52 | [pbr] 53 | # Let pbr run sphinx-apidoc 54 | autodoc_tree_index_modules = True 55 | # autodoc_tree_excludes = ... 56 | # Let pbr itself generate the apidoc 57 | # autodoc_index_modules = True 58 | # autodoc_exclude_modules = ... 59 | # Convert warnings to errors 60 | # warnerrors = True 61 | 62 | [devpi:upload] 63 | # Options for the devpi: PyPI server and packaging tool 64 | # VCS export must be deactivated since we are using setuptools-scm 65 | no-vcs = 1 66 | formats = bdist_wheel 67 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Setup file for git_explode. 5 | 6 | This file was generated with PyScaffold 2.5.6, a tool that easily 7 | puts up a scaffold for your new Python project. Learn more under: 8 | http://pyscaffold.readthedocs.org/ 9 | """ 10 | 11 | import sys 12 | from setuptools import setup 13 | 14 | 15 | def setup_package(): 16 | needs_sphinx = {'build_sphinx', 'upload_docs'}.intersection(sys.argv) 17 | sphinx = ['sphinx'] if needs_sphinx else [] 18 | setup( 19 | setup_requires=[ 20 | 'six', 21 | 'pyscaffold>=2.5.10,<2.6a0' 22 | ] + sphinx, 23 | use_pyscaffold=True 24 | ) 25 | 26 | 27 | if __name__ == "__main__": 28 | setup_package() 29 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # Add requirements only needed for your unittests and during development here. 2 | # They will be installed automatically when running `python setup.py test`. 3 | # ATTENTION: Don't remove pytest-cov and pytest as they are needed. 4 | pytest-cov 5 | pytest 6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Dummy conftest.py for git_explode. 5 | 6 | If you don't know what this is for, just leave it empty. 7 | Read more about conftest.py under: 8 | https://pytest.org/latest/plugins.html 9 | """ 10 | from __future__ import print_function, absolute_import, division 11 | 12 | # import pytest 13 | -------------------------------------------------------------------------------- /tests/test_exploder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # import pytest 5 | 6 | from git_explode.exploder import GitExploder 7 | from git_deps.gitutils import GitUtils 8 | 9 | 10 | def test_new(): 11 | repo = GitUtils.get_repo() 12 | exploder = GitExploder(repo, "HEAD~5", "HEAD", False, 1) 13 | assert exploder is not None 14 | -------------------------------------------------------------------------------- /tests/travis_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is meant to be called by the "install" step defined in 3 | # .travis.yml. See http://docs.travis-ci.com/ for more details. 4 | # The behavior of the script is controlled by environment variabled defined 5 | # in the .travis.yml in the top level folder of the project. 6 | # 7 | # This script is taken from Scikit-Learn (http://scikit-learn.org/) 8 | # 9 | # THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! 10 | 11 | set -e 12 | 13 | if [[ "$DISTRIB" == "conda" ]]; then 14 | # Deactivate the travis-provided virtual environment and setup a 15 | # conda-based environment instead 16 | deactivate 17 | 18 | # Use the miniconda installer for faster download / install of conda 19 | # itself 20 | wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh \ 21 | -O miniconda.sh 22 | chmod +x miniconda.sh && ./miniconda.sh -b -p $HOME/miniconda 23 | export PATH=$HOME/miniconda/bin:$PATH 24 | conda update --yes conda 25 | 26 | # Configure the conda environment and put it in the path using the 27 | # provided versions 28 | conda create -n testenv --yes python=$PYTHON_VERSION pip 29 | source activate testenv 30 | 31 | elif [[ "$DISTRIB" == "ubuntu" ]]; then 32 | # Use standard ubuntu packages in their default version 33 | fi 34 | 35 | if [[ "$COVERAGE" == "true" ]]; then 36 | pip install coverage coveralls 37 | fi 38 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox configuration file 2 | # Read more under https://tox.readthedocs.org/ 3 | 4 | [tox] 5 | minversion = 1.8 6 | envlist = py27,py34,py36,flake8 7 | skip_missing_interpreters = True 8 | 9 | [testenv] 10 | usedevelop = True 11 | changedir = tests 12 | commands = 13 | py.test {posargs} 14 | deps = 15 | #{distshare}/git-deps-*.zip 16 | pytest 17 | -r{toxinidir}/requirements.txt 18 | 19 | [testenv:flake8] 20 | skip_install = True 21 | changedir = {toxinidir} 22 | deps = flake8 23 | commands = flake8 setup.py git_explode tests 24 | 25 | # Options for pytest 26 | [pytest] 27 | addopts = -rsxXf 28 | 29 | [testenv:sdist] 30 | changedir = {toxinidir} 31 | commands = python setup.py sdist 32 | --------------------------------------------------------------------------------