././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1603625840.7119823 git-crecord-20201025.0/0000775000000000000000000000000000000000000014250 5ustar00rootroot00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1603625840.703982 git-crecord-20201025.0/.github/0000775000000000000000000000000000000000000015610 5ustar00rootroot00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603616893.0 git-crecord-20201025.0/.github/FUNDING.yml0000664000000000000000000000011400000000000017421 0ustar00rootroot00000000000000github: andrewshadura ko_fi: andrewsh liberapay: andrewsh patreon: andrewsh ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1529923888.0 git-crecord-20201025.0/.hgignore0000644000000000000000000000002300000000000016044 0ustar00rootroot00000000000000syntax: glob *.pyc ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578254869.0 git-crecord-20201025.0/CONTRIBUTORS0000644000000000000000000000265000000000000016131 0ustar00rootroot00000000000000The original crecord code was ported to Git by: * Andrej Shadura 2016 The following people have contributed to crecord code in Mercurial: * Anton Shestakov 2016 * Jun Wu 2016 * Nathan Goldbaum 2016 * Pulkit Goyal <7895pulkit@gmail.com> 2016 * Ryan McElroy 2016 * Laurent Charignon 2015 * Matt Mackall 2015 The following people have contributed to the original crecord Mercurial extension: * Mark Edgington 2008-2014 * Harvey Chapman 2015 * Matt Mackall 2015 * Siddharth Agarwal 2015 * immerrr 2012-2014 * Jordi Gutiérrez Hermoso 2014 * Pierre-Yves David 2014 * Christian Ebert 2010-2011 * Peter Arrenbrecht 2011 * Steve Fink 2011 * Alexander Solovyov 2010 * Nicholas Riley 2010 * Daniel Beck 2009 * Greg Ward 2009 The following people have contributed to the parts of the Mercurial utils module used by crecord: * Matt Mackall 2006, 2015 * Eric St-Jean 2007 * Mads Kiilerich 2009 * Pierre-Yves David 2015 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1529923888.0 git-crecord-20201025.0/COPYING0000644000000000000000000004325400000000000015311 0ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1529923888.0 git-crecord-20201025.0/MANIFEST.in0000644000000000000000000000010100000000000015774 0ustar00rootroot00000000000000include CONTRIBUTORS include COPYING include *.rst include *.png ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1603625840.7119823 git-crecord-20201025.0/PKG-INFO0000664000000000000000000000762200000000000015354 0ustar00rootroot00000000000000Metadata-Version: 2.1 Name: git-crecord Version: 20201025.0 Summary: interactively select chunks to commit with Git Home-page: https://github.com/andrewshadura/git-crecord Author: Andrej Shadura Author-email: andrew@shadura.me License: GPL-2+ Description: =========== Git crecord =========== About ----- **git-crecord** is a Git subcommand which allows users to interactively select changes to commit or stage using a ncurses-based text user interface. It is a port of the Mercurial crecord extension originally written by Mark Edgington. .. image:: screenshot.png :alt: Screenshot of git-crecord in action git-crecord allows you to interactively choose among the changes you have made (with line-level granularity), and commit, stage or unstage only those changes you select. After committing or staging the selected changes, the unselected changes are still present in your working copy, so you can use crecord multiple times to split large changes into several smaller changesets. Installation ------------ git-crecord assumes you have Python 3.6 or later installed as ``/usr/bin/python3``. git-crecord ships with a setup.py installer based on setuptools. To install git-crecord, simply type:: ./setup.py install This will install git-crecord itself, its manpage and this README file into their proper locations. Alternatively, to install it manually, symlink ``git-crecord`` into the directory where Git can find it, which can be a directory in your ``$PATH``:: ln -s $PWD/git-crecord ~/.local/bin/git-crecord Now you should have a new subcommand available for you. When you're ready to commit some of your changes, type:: git crecord This will bring up a window where you can view all of your changes, and select/de-select changes. You can find more information on how to use it in the built-in help (press the '?' key). ``git crecord`` supports most popular options of ``git commit``: ``--author=``, ``--date=``, ``--message=``, ``--amend``, ``--signoff``. License ------- This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 text for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Authors ------- For the list of contributors, see CONTRIBUTORS. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Version Control Requires-Python: >=3.6 Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578254836.0 git-crecord-20201025.0/README.rst0000644000000000000000000000472200000000000015742 0ustar00rootroot00000000000000=========== Git crecord =========== About ----- **git-crecord** is a Git subcommand which allows users to interactively select changes to commit or stage using a ncurses-based text user interface. It is a port of the Mercurial crecord extension originally written by Mark Edgington. .. image:: screenshot.png :alt: Screenshot of git-crecord in action git-crecord allows you to interactively choose among the changes you have made (with line-level granularity), and commit, stage or unstage only those changes you select. After committing or staging the selected changes, the unselected changes are still present in your working copy, so you can use crecord multiple times to split large changes into several smaller changesets. Installation ------------ git-crecord assumes you have Python 3.6 or later installed as ``/usr/bin/python3``. git-crecord ships with a setup.py installer based on setuptools. To install git-crecord, simply type:: ./setup.py install This will install git-crecord itself, its manpage and this README file into their proper locations. Alternatively, to install it manually, symlink ``git-crecord`` into the directory where Git can find it, which can be a directory in your ``$PATH``:: ln -s $PWD/git-crecord ~/.local/bin/git-crecord Now you should have a new subcommand available for you. When you're ready to commit some of your changes, type:: git crecord This will bring up a window where you can view all of your changes, and select/de-select changes. You can find more information on how to use it in the built-in help (press the '?' key). ``git crecord`` supports most popular options of ``git commit``: ``--author=``, ``--date=``, ``--message=``, ``--amend``, ``--signoff``. License ------- This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 text for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Authors ------- For the list of contributors, see CONTRIBUTORS. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1529923888.0 git-crecord-20201025.0/TODO0000644000000000000000000000077700000000000014751 0ustar00rootroot00000000000000Unfinished, not working, not implemented, things to do: * when commit is interrupted, staging isn't always restored * staging/unstaging: - needs to select the correct base to apply patches to (partly implemented) - needs to actually stage changes (not implemented) - needs to leave the working directory in the correct state (partly implemented, needs testing) * scrolling with PgUp/PgDn leads to an incorrect display state sometimes (upstream issue?) * reduce the upstream delta ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603618814.0 git-crecord-20201025.0/git-crecord0000755000000000000000000000010100000000000016366 0ustar00rootroot00000000000000#!/usr/bin/env python3 from git_crecord.main import main main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603625840.0 git-crecord-20201025.0/git-crecord.10000664000000000000000000000752100000000000016541 0ustar00rootroot00000000000000.\" Man page generated from reStructuredText. . .TH GIT-CRECORD 1 "2016-12-25" "0.1" "Git" .SH NAME git-crecord \- interactively select changes to commit or stage . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp \fBgit crecord\fP [\-h] .sp \fBgit crecord\fP [\-v] [\-\-author=\fIAUTHOR\fP] [\-\-date=\fIDATE\fP] [\-m \fIMESSAGE\fP] [\-\-amend] [\-s] .SH DESCRIPTION .sp \fBgit\-crecord\fP is a Git subcommand which allows users to interactively select changes to commit or stage using a ncurses\-based text user interface. It is a port of the Mercurial crecord extension originally written by Mark Edgington. .sp git\-crecord allows you to interactively choose among the changes you have made (with line\-level granularity), and commit, stage or unstage only those changes you select. After committing or staging the selected changes, the unselected changes are still present in your working copy, so you can use crecord multiple times to split large changes into several smaller changesets. .SH OPTIONS .INDENT 0.0 .TP .BI \-\-author\fB= AUTHOR Override the commit author. Specify an explicit author using the standard \fBA U Thor \fP format. Otherwise \fIAUTHOR\fP is assumed to be a pattern and is used to search for an existing commit by that author (i.e. \fBrev\-list \-\-all \-i \-\-author=AUTHOR\fP); the commit author is then copied from the first such commit found. .TP .BI \-\-date\fB= DATE Override the author date used in the commit. .TP .BI \-m \ MESSAGE\fR,\fB \ \-\-message\fB= MESSAGE Use the given \fIMESSAGE\fP as the commit message. If multiple \fB\-m\fP options are given, their values are concatenated as separate paragraphs. .TP .BI \-C \ COMMIT\fR,\fB \ \-\-reuse\-message\fB= COMMIT Take an existing commit object, and reuse the log message and the authorship information (including the timestamp) when creating the commit. .TP .BI \-c \ COMMIT\fR,\fB \ \-\-reedit\-message\fB= COMMIT Like \fB\-C\fP, but with \fB\-c\fP the editor is invoked, so that the user can further edit the commit message. .TP .B \-\-reset\-author When used with \fB\-C\fP/\fB\-c\fP/\fB\-\-amend\fP options, or when committing after a conflicting cherry\-pick, declare that the authorship of the resulting commit now belongs to the committer. This also renews the author timestamp. .TP .B \-s\fP,\fB \-\-signoff Add \fBSigned\-off\-by\fP line by the committer at the end of the commit log message. .TP .B \-\-amend Amend previous commit. Replace the tip of the current branch by creating a new commit. The message from the original commit is used as the starting point, instead of an empty message, when no other message is specified from the command line via \fB\-m\fP option. The new commit has the same parents and author as the current one. .TP .BI \-S \ KEY\-ID\fR,\fB \ \-\-gpg\-sign \ KEY\-ID GPG\-sign commits. The \fIKEY\-ID\fP argument is optional and defaults to the committer identity. .TP .B \-\-no\-gpg\-sign Don’t sign this commit even if \fIcommit.gpgSign\fP is set. .TP .B \-v\fP,\fB \-\-verbose Be more verbose. .TP .B \-\-debug Show all sorts of debugging information. Implies \fB\-\-verbose\fP\&. .TP .B \-h Show this help message and exit. .UNINDENT .SH SEE ALSO .sp \fBgit\-commit\fP(1) .SH AUTHOR Andrej Shadura .\" Generated by docutils manpage writer. . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578254909.0 git-crecord-20201025.0/git-crecord.rst0000644000000000000000000000611400000000000017204 0ustar00rootroot00000000000000=========== git-crecord =========== ----------------------------------------------- interactively select changes to commit or stage ----------------------------------------------- :Author: Andrej Shadura :Date: 2016-12-25 :Version: 0.1 :Manual section: 1 :Manual group: Git SYNOPSIS ======== **git crecord** [-h] **git crecord** [-v] [--author=\ `AUTHOR`] [--date=\ `DATE`] [-m `MESSAGE`] [--amend] [-s] DESCRIPTION =========== **git-crecord** is a Git subcommand which allows users to interactively select changes to commit or stage using a ncurses-based text user interface. It is a port of the Mercurial crecord extension originally written by Mark Edgington. git-crecord allows you to interactively choose among the changes you have made (with line-level granularity), and commit, stage or unstage only those changes you select. After committing or staging the selected changes, the unselected changes are still present in your working copy, so you can use crecord multiple times to split large changes into several smaller changesets. OPTIONS ======= --author=AUTHOR Override the commit author. Specify an explicit author using the standard ``A U Thor `` format. Otherwise `AUTHOR` is assumed to be a pattern and is used to search for an existing commit by that author (i.e. ``rev-list --all -i --author=AUTHOR``); the commit author is then copied from the first such commit found. --date=DATE Override the author date used in the commit. -m MESSAGE, --message=MESSAGE Use the given `MESSAGE` as the commit message. If multiple ``-m`` options are given, their values are concatenated as separate paragraphs. -C COMMIT, --reuse-message=COMMIT Take an existing commit object, and reuse the log message and the authorship information (including the timestamp) when creating the commit. -c COMMIT, --reedit-message=COMMIT Like ``-C``, but with ``-c`` the editor is invoked, so that the user can further edit the commit message. --reset-author When used with ``-C``/``-c``/``--amend`` options, or when committing after a conflicting cherry-pick, declare that the authorship of the resulting commit now belongs to the committer. This also renews the author timestamp. -s, --signoff Add ``Signed-off-by`` line by the committer at the end of the commit log message. --amend Amend previous commit. Replace the tip of the current branch by creating a new commit. The message from the original commit is used as the starting point, instead of an empty message, when no other message is specified from the command line via ``-m`` option. The new commit has the same parents and author as the current one. -S KEY-ID, --gpg-sign KEY-ID GPG-sign commits. The `KEY-ID` argument is optional and defaults to the committer identity. --no-gpg-sign Don’t sign this commit even if `commit.gpgSign` is set. -v, --verbose Be more verbose. --debug Show all sorts of debugging information. Implies ``--verbose``. -h Show this help message and exit. SEE ALSO ======== **git-commit**\(1) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1603625840.707982 git-crecord-20201025.0/git_crecord/0000775000000000000000000000000000000000000016534 5ustar00rootroot00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575457655.0 git-crecord-20201025.0/git_crecord/__init__.py0000644000000000000000000000043300000000000020643 0ustar00rootroot00000000000000# crecord.py # # Copyright 2008 Mark Edgington # # This software may be used and distributed according to the terms of # the GNU General Public License, incorporated herein by reference. # # Much of this extension is based on Bryan O'Sullivan's record extension. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603617767.0 git-crecord-20201025.0/git_crecord/chunk_selector.py0000664000000000000000000014006300000000000022122 0ustar00rootroot00000000000000from __future__ import unicode_literals from gettext import gettext as _ from . import util from . import encoding code = encoding.encoding import os import re import sys import fcntl import struct import termios import signal from .crpatch import patch, uiheader, uihunk, uihunkline try: import curses import fcntl import termios curses.error fcntl.ioctl termios.TIOCGWINSZ except ImportError: # I have no idea if wcurses works with crecord... try: import wcurses as curses curses.error except ImportError: # wcurses is not shipped on Windows by default pass try: curses except NameError: raise util.Abort( _('the python curses/wcurses module is not available/installed')) _origstdout = sys.__stdout__ # used by gethw() def gethw(): """ Magically get the current height and width of the window (without initscr) This is a rip-off of a rip-off - taken from the bpython code. It is useful / necessary because otherwise curses.initscr() must be called, which can leave the terminal in a nasty state after exiting. """ h, w = struct.unpack( "hhhh", fcntl.ioctl(_origstdout, termios.TIOCGWINSZ, "\000"*8))[0:2] return h, w def chunkselector(opts, headerlist, ui): """ Curses interface to get selection of chunks, and mark the applied flags of the chosen chunks. """ chunkselector = CursesChunkSelector(headerlist, ui) # This is required for ncurses to display non-ASCII characters in default user # locale encoding correctly. --immerrr import locale locale.setlocale(locale.LC_ALL, '') class dummystdscr(object): def clear(self): pass def refresh(self): pass chunkselector.stdscr = dummystdscr() f = signal.getsignal(signal.SIGTSTP) curses.wrapper(chunkselector.main, opts) if chunkselector.initerr is not None: raise util.Abort(chunkselector.initerr) # ncurses does not restore signal handler for SIGTSTP signal.signal(signal.SIGTSTP, f) _headermessages = { # {operation: text} 'crecord': _('Select hunks to commit'), 'cstage': _('Select hunks to stage'), 'cunstage': _('Select hunks to keep'), } _confirmmessages = { 'crecord': _('Are you sure you want to commit the selected changes [Yn]?'), 'cstage': _('Are you sure you want to stage the selected changes [Yn]?'), 'cunstage': _('Are you sure you want to unstage the unselected changes [Yn]?'), } class CursesChunkSelector(object): def __init__(self, headerlist, ui): # put the headers into a patch object self.headerlist = patch(headerlist) self.ui = ui self.errorstr = None # list of all chunks self.chunklist = [] for h in headerlist: self.chunklist.append(h) self.chunklist.extend(h.hunks) # dictionary mapping (fgcolor, bgcolor) pairs to the # corresponding curses color-pair value. self.colorpairs = {} # maps custom nicknames of color-pairs to curses color-pair values self.colorpairnames = {} self.usecolor = True # the currently selected header, hunk, or hunk-line self.currentselecteditem = self.headerlist[0] # updated when printing out patch-display -- the 'lines' here are the # line positions *in the pad*, not on the screen. self.selecteditemstartline = 0 self.selecteditemendline = None # define indentation levels self.headerindentnumchars = 0 self.hunkindentnumchars = 3 self.hunklineindentnumchars = 6 # the first line of the pad to print to the screen self.firstlineofpadtoprint = 0 # keeps track of the number of lines in the pad self.numpadlines = None self.numstatuslines = 1 # keep a running count of the number of lines printed to the pad # (used for determining when the selected item begins/ends) self.linesprintedtopadsofar = 0 # the first line of the pad which is visible on the screen self.firstlineofpadtoprint = 0 # if the last 'toggle all' command caused all changes to be applied self.waslasttoggleallapplied = True def handlefirstlineevent(self): """ Handle 'g' to navigate to the top most file in the ncurses window. """ self.currentselecteditem = self.headerlist[0] currentitem = self.currentselecteditem # select the parent item recursively until we're at a header while True: nextitem = currentitem.parentitem() if nextitem is None: break else: currentitem = nextitem self.currentselecteditem = currentitem def handlelastlineevent(self): """ Handle 'G' to navigate to the bottom most file/hunk/line depending on the whether the fold is active or not. If the bottom most file is folded, it navigates to that file and stops there. If the bottom most file is unfolded, it navigates to the bottom most hunk in that file and stops there. If the bottom most hunk is unfolded, it navigates to the bottom most line in that hunk. """ currentitem = self.currentselecteditem nextitem = currentitem.nextitem() # select the child item recursively until we're at a footer while nextitem is not None: nextitem = currentitem.nextitem() if nextitem is None: break else: currentitem = nextitem self.currentselecteditem = currentitem self.recenterdisplayedarea() def uparrowevent(self): """ Try to select the previous item to the current item that has the most-indented level. For example, if a hunk is selected, try to select the last HunkLine of the hunk prior to the selected hunk. Or, if the first HunkLine of a hunk is currently selected, then select the hunk itself. """ currentitem = self.currentselecteditem nextitem = currentitem.previtem() if nextitem is None: # if no parent item (i.e. currentitem is the first header), then # no change... nextitem = currentitem self.currentselecteditem = nextitem self.recenterdisplayedarea() def uparrowshiftevent(self): """ Select (if possible) the previous item on the same level as the currently selected item. Otherwise, select (if possible) the parent-item of the currently selected item. """ currentitem = self.currentselecteditem nextitem = currentitem.prevsibling() # if there's no previous sibling, try choosing the parent if nextitem is None: nextitem = currentitem.parentitem() if nextitem is None: # if no parent item (i.e. currentitem is the first header), then # no change... nextitem = currentitem self.currentselecteditem = nextitem self.recenterdisplayedarea() def downarrowevent(self): """ Try to select the next item to the current item that has the most-indented level. For example, if a hunk is selected, select the first HunkLine of the selected hunk. Or, if the last HunkLine of a hunk is currently selected, then select the next hunk, if one exists, or if not, the next header if one exists. """ #self.startprintline += 1 #debug currentitem = self.currentselecteditem nextitem = currentitem.nextitem() # if there's no next item, keep the selection as-is if nextitem is None: nextitem = currentitem self.currentselecteditem = nextitem self.recenterdisplayedarea() def downarrowshiftevent(self): """ Select (if possible) the next item on the same level as the currently selected item. Otherwise, select (if possible) the next item on the same level as the parent item of the currently selected item. """ currentitem = self.currentselecteditem nextitem = currentitem.nextsibling() # if there's no next sibling, try choosing the parent's nextsibling if nextitem is None: try: nextitem = currentitem.parentitem().nextsibling() except AttributeError: # parentitem returned None, so nextsibling() can't be called nextitem = None if nextitem is None: # if parent has no next sibling, then no change... nextitem = currentitem self.currentselecteditem = nextitem self.recenterdisplayedarea() def rightarrowevent(self): """ Select (if possible) the first of this item's child-items. """ currentitem = self.currentselecteditem nextitem = currentitem.firstchild() # turn off folding if we want to show a child-item if currentitem.folded: self.togglefolded(currentitem) if nextitem is None: # if no next item on parent-level, then no change... nextitem = currentitem self.currentselecteditem = nextitem self.recenterdisplayedarea() def leftarrowevent(self): """ If the current item can be folded (i.e. it is an unfolded header or hunk), then fold it. Otherwise try select (if possible) the parent of this item. """ currentitem = self.currentselecteditem # try to fold the item if not isinstance(currentitem, uihunkline): if not currentitem.folded: self.togglefolded(item=currentitem) return # if it can't be folded, try to select the parent item nextitem = currentitem.parentitem() if nextitem is None: # if no item on parent-level, then no change... nextitem = currentitem if not nextitem.folded: self.togglefolded(item=nextitem) self.currentselecteditem = nextitem self.recenterdisplayedarea() def leftarrowshiftevent(self): """ Select the header of the current item (or fold current item if the current item is already a header). """ currentitem = self.currentselecteditem if isinstance(currentitem, uiheader): if not currentitem.folded: self.togglefolded(item=currentitem) return # select the parent item recursively until we're at a header while True: nextitem = currentitem.parentitem() if nextitem is None: break else: currentitem = nextitem self.currentselecteditem = currentitem self.recenterdisplayedarea() def updatescroll(self): "Scroll the screen to fully show the currently-selected" selstart = self.selecteditemstartline selend = self.selecteditemendline #selnumlines = selend - selstart padstart = self.firstlineofpadtoprint padend = padstart + self.yscreensize - self.numstatuslines - 1 # 'buffered' pad start/end values which scroll with a certain # top/bottom context margin padstartbuffered = padstart + 3 padendbuffered = padend - 3 if selend > padendbuffered: self.scrolllines(selend - padendbuffered) elif selstart < padstartbuffered: # negative values scroll in pgup direction self.scrolllines(selstart - padstartbuffered) def scrolllines(self, numlines): "Scroll the screen up (down) by numlines when numlines >0 (<0)." self.firstlineofpadtoprint += numlines if self.firstlineofpadtoprint < 0: self.firstlineofpadtoprint = 0 if self.firstlineofpadtoprint > self.numpadlines - 1: self.firstlineofpadtoprint = self.numpadlines - 1 def toggleapply(self, item=None): """ Toggle the applied flag of the specified item. If no item is specified, toggle the flag of the currently selected item. """ if item is None: item = self.currentselecteditem item.applied = not item.applied if isinstance(item, uiheader): item.partial = False if item.applied: # apply all its hunks for hnk in item.hunks: hnk.applied = True # apply all their hunklines for hunkline in hnk.changedlines: hunkline.applied = True else: # un-apply all its hunks for hnk in item.hunks: hnk.applied = False hnk.partial = False # un-apply all their hunklines for hunkline in hnk.changedlines: hunkline.applied = False elif isinstance(item, uihunk): item.partial = False # apply all it's hunklines for hunkline in item.changedlines: hunkline.applied = item.applied siblingappliedstatus = [hnk.applied for hnk in item.header.hunks] allsiblingsapplied = not (False in siblingappliedstatus) nosiblingsapplied = not (True in siblingappliedstatus) siblingspartialstatus = [hnk.partial for hnk in item.header.hunks] somesiblingspartial = (True in siblingspartialstatus) #cases where applied or partial should be removed from header # if no 'sibling' hunks are applied (including this hunk) if nosiblingsapplied: if not item.header.special(): item.header.applied = False item.header.partial = False else: # some/all parent siblings are applied item.header.applied = True item.header.partial = (somesiblingspartial or not allsiblingsapplied) elif isinstance(item, uihunkline): siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines] allsiblingsapplied = not (False in siblingappliedstatus) nosiblingsapplied = not (True in siblingappliedstatus) # if no 'sibling' lines are applied if nosiblingsapplied: item.hunk.applied = False item.hunk.partial = False elif allsiblingsapplied: item.hunk.applied = True item.hunk.partial = False else: # some siblings applied item.hunk.applied = True item.hunk.partial = True parentsiblingsapplied = [hnk.applied for hnk in item.hunk.header.hunks] noparentsiblingsapplied = not (True in parentsiblingsapplied) allparentsiblingsapplied = not (False in parentsiblingsapplied) parentsiblingspartial = [hnk.partial for hnk in item.hunk.header.hunks] someparentsiblingspartial = (True in parentsiblingspartial) # if all parent hunks are not applied, un-apply header if noparentsiblingsapplied: if not item.hunk.header.special(): item.hunk.header.applied = False item.hunk.header.partial = False # set the applied and partial status of the header if needed else: # some/all parent siblings are applied item.hunk.header.applied = True item.hunk.header.partial = (someparentsiblingspartial or not allparentsiblingsapplied) def toggleall(self): "Toggle the applied flag of all items." if self.waslasttoggleallapplied: # then unapply them this time for item in self.headerlist: if item.applied: self.toggleapply(item) else: for item in self.headerlist: if not item.applied: self.toggleapply(item) self.waslasttoggleallapplied = not self.waslasttoggleallapplied def togglefolded(self, item=None, foldparent=False): "Toggle folded flag of specified item (defaults to currently selected)" if item is None: item = self.currentselecteditem if foldparent or (isinstance(item, uiheader) and item.neverunfolded): if not isinstance(item, uiheader): # we need to select the parent item in this case self.currentselecteditem = item = item.parentitem() elif item.neverunfolded: item.neverunfolded = False # also fold any foldable children of the parent/current item if isinstance(item, uiheader): # the original OR 'new' item for child in item.allchildren(): child.folded = not item.folded if isinstance(item, (uiheader, uihunk)): item.folded = not item.folded def alignstring(self, instr, window): """ Add whitespace to the end of a string in order to make it fill the screen in the x direction. The current cursor position is taken into account when making this calculation. The string can span multiple lines. """ y, xstart = window.getyx() width = self.xscreensize # turn tabs into spaces instr = instr.expandtabs(4) strwidth = encoding.ucolwidth(instr) numspaces = width - ((strwidth + xstart) % width) return instr + " " * numspaces def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None, pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False): """ Print the string, text, with the specified colors and attributes, to the specified curses window object. The foreground and background colors are of the form curses.COLOR_XXXX, where XXXX is one of: [BLACK, BLUE, CYAN, GREEN, MAGENTA, RED, WHITE, YELLOW]. If pairname is provided, a color pair will be looked up in the self.colorpairnames dictionary. attrlist is a list containing text attributes in the form of curses.A_XXXX, where XXXX can be: [BOLD, DIM, NORMAL, STANDOUT, UNDERLINE]. If align == True, whitespace is added to the printed string such that the string stretches to the right border of the window. If showwhtspc == True, trailing whitespace of a string is highlighted. """ # preprocess the text, converting tabs to spaces text = text.expandtabs(4) # Strip \n, and convert control characters to ^[char] representation text = re.sub(r'[\x00-\x08\x0a-\x1f]', lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n')) if pair is not None: colorpair = pair elif pairname is not None: colorpair = self.colorpairnames[pairname] else: if fgcolor is None: fgcolor = -1 if bgcolor is None: bgcolor = -1 if (fgcolor, bgcolor) in self.colorpairs: colorpair = self.colorpairs[(fgcolor, bgcolor)] else: colorpair = self.getcolorpair(fgcolor, bgcolor) # add attributes if possible if attrlist is None: attrlist = [] if colorpair < 256: # then it is safe to apply all attributes for textattr in attrlist: colorpair |= textattr else: # just apply a select few (safe?) attributes for textattr in (curses.A_UNDERLINE, curses.A_BOLD): if textattr in attrlist: colorpair |= textattr y, xstart = self.chunkpad.getyx() t = "" # variable for counting lines printed # if requested, show trailing whitespace if showwhtspc: origlen = len(text) text = text.rstrip(' \n') # tabs have already been expanded strippedlen = len(text) numtrailingspaces = origlen - strippedlen if towin: window.addstr(text, colorpair) t += text if showwhtspc: wscolorpair = colorpair | curses.A_REVERSE if towin: for i in range(numtrailingspaces): window.addch(curses.ACS_CKBOARD, wscolorpair) t += " " * numtrailingspaces if align: if towin: extrawhitespace = self.alignstring("", window) window.addstr(extrawhitespace, colorpair) else: # need to use t, since the x position hasn't incremented extrawhitespace = self.alignstring(t, window) t += extrawhitespace # is reset to 0 at the beginning of printitem() linesprinted = (xstart + len(t)) // self.xscreensize self.linesprintedtopadsofar += linesprinted return t def _getstatuslinesegments(self): """-> [str]. return segments""" selected = self.currentselecteditem.applied segments = [ _headermessages[self.opts['operation']], '-', _('[x]=selected **=collapsed'), _('c: confirm'), _('q: abort'), _('arrow keys: move/expand/collapse'), _('space: deselect') if selected else _('space: select'), _('?: help'), ] return segments def _getstatuslines(self): """() -> [str]. return short help used in the top status window""" if self.errorstr is not None: lines = [self.errorstr, _('Press any key to continue')] else: # wrap segments to lines segments = self._getstatuslinesegments() width = self.xscreensize lines = [] lastwidth = width for s in segments: w = encoding.ucolwidth(s) sep = ' ' * (1 + (s and s[0] not in '-[')) if lastwidth + w + len(sep) >= width: lines.append(s) lastwidth = w else: lines[-1] += sep + s lastwidth += w + len(sep) if len(lines) != self.numstatuslines: self.numstatuslines = len(lines) self.statuswin.resize(self.numstatuslines, self.xscreensize) return [util.ellipsis(l, self.xscreensize - 1) for l in lines] def updatescreen(self): self.statuswin.erase() self.chunkpad.erase() printstring = self.printstring # print out the status lines at the top try: for line in self._getstatuslines(): printstring(self.statuswin, line, pairname="legend") self.statuswin.refresh() except curses.error: pass if self.errorstr is not None: return # print out the patch in the remaining part of the window try: self.printitem() self.updatescroll() self.chunkpad.refresh(self.firstlineofpadtoprint, 0, self.numstatuslines, 0, self.yscreensize - self.numstatuslines, self.xscreensize) except curses.error: pass def getstatusprefixstring(self, item): """ Create a string to prefix a line with which indicates whether 'item' is applied and/or folded. """ # create checkbox string if item.applied: if not isinstance(item, uihunkline) and item.partial: checkbox = "[~]" else: checkbox = "[x]" else: checkbox = "[ ]" try: if item.folded: checkbox += "**" if isinstance(item, uiheader): # one of "M", "A", or "D" (modified, added, deleted) filestatus = item.changetype checkbox += filestatus + " " else: checkbox += " " if isinstance(item, uiheader): # add two more spaces for headers checkbox += " " except AttributeError: # not foldable checkbox += " " return checkbox def printheader(self, header, selected=False, towin=True, ignorefolding=False): """ Print the header to the pad. If countLines is True, don't print anything, but just count the number of lines which would be printed. """ outstr = "" text = header.prettystr() chunkindex = self.chunklist.index(header) if chunkindex != 0 and not header.folded: # add separating line before headers outstr += self.printstring(self.chunkpad, '_' * self.xscreensize, towin=towin, align=False) # select color-pair based on if the header is selected colorpair = self.getcolorpair(name=selected and "selected" or "normal", attrlist=[curses.A_BOLD]) # print out each line of the chunk, expanding it to screen width # number of characters to indent lines on this level by indentnumchars = 0 checkbox = self.getstatusprefixstring(header) if not header.folded or ignorefolding: textlist = text.split("\n") linestr = checkbox + textlist[0] else: linestr = checkbox + header.filename() outstr += self.printstring(self.chunkpad, linestr, pair=colorpair, towin=towin) if not header.folded or ignorefolding: if len(textlist) > 1: for line in textlist[1:]: linestr = " "*(indentnumchars + len(checkbox)) + line outstr += self.printstring(self.chunkpad, linestr, pair=colorpair, towin=towin) return outstr def printhunklinesbefore(self, hunk, selected=False, towin=True, ignorefolding=False): "includes start/end line indicator" outstr = "" # where hunk is in list of siblings hunkindex = hunk.header.hunks.index(hunk) if hunkindex != 0: # add separating line before headers outstr += self.printstring(self.chunkpad, ' '*self.xscreensize, towin=towin, align=False) colorpair = self.getcolorpair(name=selected and "selected" or "normal", attrlist=[curses.A_BOLD]) # print out from-to line with checkbox checkbox = self.getstatusprefixstring(hunk) lineprefix = " "*self.hunkindentnumchars + checkbox frtoline = " " + hunk.getfromtoline().strip("\n") outstr += self.printstring(self.chunkpad, lineprefix, towin=towin, align=False) # add uncolored checkbox/indent outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair, towin=towin) if hunk.folded and not ignorefolding: # skip remainder of output return outstr # print out lines of the chunk preceding changed-lines for line in hunk.before: linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line outstr += self.printstring(self.chunkpad, linestr, towin=towin) return outstr def printhunklinesafter(self, hunk, towin=True, ignorefolding=False): outstr = "" if hunk.folded and not ignorefolding: return outstr # a bit superfluous, but to avoid hard-coding indent amount checkbox = self.getstatusprefixstring(hunk) for line in hunk.after: linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line outstr += self.printstring(self.chunkpad, linestr, towin=towin) return outstr def printhunkchangedline(self, hunkline, selected=False, towin=True): outstr = "" checkbox = self.getstatusprefixstring(hunkline) linestr = hunkline.prettystr().strip("\n") # select color-pair based on whether line is an addition/removal if selected: colorpair = self.getcolorpair(name="selected") elif linestr.startswith("+"): colorpair = self.getcolorpair(name="addition") elif linestr.startswith("-"): colorpair = self.getcolorpair(name="deletion") elif linestr.startswith("\\"): colorpair = self.getcolorpair(name="normal") lineprefix = " "*self.hunklineindentnumchars + checkbox outstr += self.printstring(self.chunkpad, lineprefix, towin=towin, align=False) # add uncolored checkbox/indent outstr += self.printstring(self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True) return outstr def printitem(self, item=None, ignorefolding=False, recursechildren=True, towin=True): """ Use __printitem() to print the the specified item.applied. If item is not specified, then print the entire patch. (hiding folded elements, etc. -- see __printitem() docstring) """ if item is None: item = self.headerlist if recursechildren: self.linesprintedtopadsofar = 0 outstr = [] self.__printitem(item, ignorefolding, recursechildren, outstr, towin=towin) return ''.join(outstr) def outofdisplayedarea(self): y, _ = self.chunkpad.getyx() # cursor location # * 2 here works but an optimization would be the max number of # consecutive non selectable lines # i.e the max number of context line for any hunk in the patch miny = min(0, self.firstlineofpadtoprint - self.yscreensize) maxy = self.firstlineofpadtoprint + self.yscreensize * 2 return y < miny or y > maxy def handleselection(self, item, recursechildren): selected = (item is self.currentselecteditem) if selected and recursechildren: # assumes line numbering starting from line 0 self.selecteditemstartline = self.linesprintedtopadsofar selecteditemlines = self.getnumlinesdisplayed(item, recursechildren=False) self.selecteditemendline = (self.selecteditemstartline + selecteditemlines - 1) return selected def __printitem(self, item, ignorefolding, recursechildren, outstr, towin=True): """ Recursive method for printing out patch/header/hunk/hunk-line data to screen. Also returns a string with all of the content of the displayed patch (not including coloring, etc.). If ignorefolding is True, then folded items are printed out. If recursechildren is False, then only print the item without its child items. """ if towin and self.outofdisplayedarea(): return selected = self.handleselection(item, recursechildren) # Patch object is a list of headers if isinstance(item, patch): if recursechildren: for hdr in item: self.__printitem(hdr, ignorefolding, recursechildren, outstr, towin) # TODO: eliminate all isinstance() calls if isinstance(item, uiheader): outstr.append(self.printheader(item, selected, towin=towin, ignorefolding=ignorefolding)) if recursechildren: for hnk in item.hunks: self.__printitem(hnk, ignorefolding, recursechildren, outstr, towin) elif (isinstance(item, uihunk) and ((not item.header.folded) or ignorefolding)): # print the hunk data which comes before the changed-lines outstr.append(self.printhunklinesbefore(item, selected, towin=towin, ignorefolding=ignorefolding)) if recursechildren: for l in item.changedlines: self.__printitem(l, ignorefolding, recursechildren, outstr, towin) outstr.append(self.printhunklinesafter(item, towin=towin, ignorefolding=ignorefolding)) elif (isinstance(item, uihunkline) and ((not item.hunk.folded) or ignorefolding)): outstr.append(self.printhunkchangedline(item, selected, towin=towin)) return outstr def getnumlinesdisplayed(self, item=None, ignorefolding=False, recursechildren=True): """ Return the number of lines which would be displayed if the item were to be printed to the display. The item will NOT be printed to the display (pad). If no item is given, assume the entire patch. If ignorefolding is True, folded items will be unfolded when counting the number of lines. """ # temporarily disable printing to windows by printstring patchdisplaystring = self.printitem(item, ignorefolding, recursechildren, towin=False) numlines = len(patchdisplaystring) // self.xscreensize return numlines def sigwinchhandler(self, n, frame): "Handle window resizing" try: curses.endwin() self.yscreensize, self.xscreensize = gethw() self.statuswin.resize(self.numstatuslines, self.xscreensize) self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) except curses.error: pass def getcolorpair(self, fgcolor=None, bgcolor=None, name=None, attrlist=None): """ Get a curses color pair, adding it to self.colorPairs if it is not already defined. An optional string, name, can be passed as a shortcut for referring to the color-pair. By default, if no arguments are specified, the white foreground / black background color-pair is returned. It is expected that this function will be used exclusively for initializing color pairs, and NOT curses.init_pair(). attrlist is used to 'flavor' the returned color-pair. This information is not stored in self.colorpairs. It contains attribute values like curses.A_BOLD. """ if (name is not None) and name in self.colorpairnames: # then get the associated color pair and return it colorpair = self.colorpairnames[name] else: if fgcolor is None: fgcolor = -1 if bgcolor is None: bgcolor = -1 if (fgcolor, bgcolor) in self.colorpairs: colorpair = self.colorpairs[(fgcolor, bgcolor)] else: pairindex = len(self.colorpairs) + 1 if self.usecolor: curses.init_pair(pairindex, fgcolor, bgcolor) colorpair = self.colorpairs[(fgcolor, bgcolor)] = ( curses.color_pair(pairindex)) if name is not None: self.colorpairnames[name] = curses.color_pair(pairindex) else: cval = 0 if name is not None: if name == 'selected': cval = curses.A_REVERSE self.colorpairnames[name] = cval colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval # add attributes if possible if attrlist is None: attrlist = [] if colorpair < 256: # then it is safe to apply all attributes for textattr in attrlist: colorpair |= textattr else: # just apply a select few (safe?) attributes for textattrib in (curses.A_UNDERLINE, curses.A_BOLD): if textattrib in attrlist: colorpair |= textattrib return colorpair def initcolorpair(self, *args, **kwargs): "Same as getcolorpair." self.getcolorpair(*args, **kwargs) def helpwindow(self): "Print a help window to the screen. Exit after any keypress." helptext = """ [press any key to return to the patch-display] crecord allows you to interactively choose among the changes you have made, and confirm only those changes you select for further processing by the command you are running (commit/stage/unstage), after confirming the selected changes, the unselected changes are still present in your working copy, so you can use crecord multiple times to split large changes into smaller changesets. The following are valid keystrokes: [SPACE] : (un-)select item ([~]/[X] = partly/fully applied) A : (un-)select all items Up/Down-arrow [k/j] : go to previous/next unfolded item PgUp/PgDn [K/J] : go to previous/next item of same type Right/Left-arrow [l/h] : go to child item / parent item Shift-Left-arrow [H] : go to parent header / fold selected header g : go to the top G : go to the bottom f : fold / unfold item, hiding/revealing its children F : fold / unfold parent item and all of its ancestors ctrl-l : scroll the selected line to the top of the screen a : toggle amend mode c : commit selected changes s : stage selected changes r : review/edit and commit selected changes q : quit without committing (no changes will be made) ? : help (what you're currently reading)""" helpwin = curses.newwin(self.yscreensize, 0, 0, 0) helplines = helptext.split("\n") helplines = helplines + [" "]*( self.yscreensize - self.numstatuslines - len(helplines) - 1) try: self.printstring(helpwin, helplines[0], pairname="legend") for line in helplines[1:]: self.printstring(helpwin, line, pairname="normal") except curses.error: pass helpwin.refresh() try: helpwin.getkey() except curses.error: pass def commitmessagewindow(self, commenttext): "Create a temporary commit message editing window on the screen." curses.raw() curses.def_prog_mode() curses.endwin() commenttext = self.ui.edit(commenttext, self.ui.username(), name=os.path.join(self.ui.repo.controldir(), 'COMMIT_EDITMSG')) curses.cbreak() self.stdscr.refresh() self.stdscr.keypad(1) # allow arrow-keys to continue to function return commenttext def confirmationwindow(self, windowtext): "Display an informational window, then wait for and return a keypress." lines = windowtext.split("\n") confirmwin = curses.newwin(len(lines), 0, 0, 0) try: for line in lines: self.printstring(confirmwin, line, pairname="selected") except curses.error: pass try: response = chr(confirmwin.getch()) except ValueError: response = None return response def confirmcommit(self, review=False): """Ask for 'Y' to be pressed to confirm selected. Return True if confirmed.""" if not self.opts['confirm']: return True if review: confirmtext = ( """If you answer yes to the following, the your currently chosen patch chunks will be loaded into an editor. You may modify the patch from the editor, and save the changes if you wish to change the patch. Otherwise, you can just close the editor without saving to accept the current patch as-is. NOTE: don't add/remove lines unless you also modify the range information. Failing to follow this rule will result in the commit aborting. Are you sure you want to review/edit and confirm the selected changes [Yn]? """) else: confirmtext = _confirmmessages[self.opts['operation']] response = self.confirmationwindow(confirmtext) if response is None or len(response) == 0 or response == "\n": response = "y" if response.lower().startswith("y"): return True else: return False def recenterdisplayedarea(self): """ once we scrolled with pg up pg down we can be pointing outside of the display zone. we print the patch with towin=False to compute the location of the selected item even though it is outside of the displayed zone and then update the scroll. """ self.printitem(towin=False) self.updatescroll() def toggleamend(self): """Toggle the amend flag. When the amend flag is set, a commit will modify the most recently committed changeset, instead of creating a new changeset. Otherwise, a new changeset will be created (the normal commit behavior). """ if self.opts.get('amend') is False: self.opts['amend'] = True msg = ("Amend option is turned on -- committing the currently " "selected changes will not create a new changeset, but " "instead update the most recently committed changeset.\n\n" "Press any key to continue.") elif self.opts.get('amend') is True: self.opts['amend'] = False msg = ("Amend option is turned off -- committing the currently " "selected changes will create a new changeset.\n\n" "Press any key to continue.") self.confirmationwindow(msg) def emptypatch(self): item = self.headerlist if not item: return True for header in item: if header.hunks: return False return True def handlekeypressed(self, keypressed): """ Perform actions based on pressed keys. Return true to exit the main loop. """ if keypressed in ["k", "KEY_UP"]: self.uparrowevent() elif keypressed in ["K", "KEY_PPAGE"]: self.uparrowshiftevent() elif keypressed in ["j", "KEY_DOWN"]: self.downarrowevent() elif keypressed in ["J", "KEY_NPAGE"]: self.downarrowshiftevent() elif keypressed in ["l", "KEY_RIGHT"]: self.rightarrowevent() elif keypressed in ["h", "KEY_LEFT"]: self.leftarrowevent() elif keypressed in ["H", "KEY_SLEFT"]: self.leftarrowshiftevent() elif keypressed in ["q"]: raise util.Abort(_('user quit')) elif keypressed in ['a']: self.toggleamend() elif keypressed in ["c"]: if self.confirmcommit(): self.opts['commit'] = True return True elif keypressed in ["s"]: self.opts['commit'] = False if self.confirmcommit(): self.opts['commit'] = False return True elif keypressed in ["r"]: if self.confirmcommit(review=True): self.opts['commit'] = True self.opts['crecord_reviewpatch'] = True return True elif keypressed in [' ']: self.toggleapply() elif keypressed in ['A']: self.toggleall() elif keypressed in ["f"]: self.togglefolded() elif keypressed in ["F"]: self.togglefolded(foldparent=True) elif keypressed in ["?"]: self.helpwindow() self.stdscr.clear() self.stdscr.refresh() elif len(keypressed) == 1 and curses.unctrl(keypressed) in [b"^L"]: # scroll the current line to the top of the screen, and redraw # everything self.scrolllines(self.selecteditemstartline) self.stdscr.clear() self.stdscr.refresh() elif keypressed in ["g", "KEY_HOME"]: self.handlefirstlineevent() elif keypressed in ["G", "KEY_END"]: self.handlelastlineevent() return False def main(self, stdscr, opts): """ Method to be wrapped by curses.wrapper() for selecting chunks. """ self.opts = opts origsigwinch = sentinel = object() if util.safehasattr(signal, 'SIGWINCH'): origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler) try: return self._main(stdscr) finally: if origsigwinch is not sentinel: signal.signal(signal.SIGWINCH, origsigwinch) def _main(self, stdscr): self.stdscr = stdscr # error during initialization, cannot be printed in the curses # interface, it should be printed by the calling code self.initerr = None self.yscreensize, self.xscreensize = self.stdscr.getmaxyx() curses.start_color() try: curses.use_default_colors() except curses.error: self.usecolor = False # In some situations we may have some cruft left on the "alternate # screen" from another program (or previous iterations of ourself), and # we won't clear it if the scroll region is small enough to comfortably # fit on the terminal. self.stdscr.clear() # don't display the cursor try: curses.curs_set(0) except curses.error: pass # available colors: black, blue, cyan, green, magenta, white, yellow # init_pair(color_id, foreground_color, background_color) self.initcolorpair(None, None, name="normal") self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA, name="selected") self.initcolorpair(curses.COLOR_RED, None, name="deletion") self.initcolorpair(curses.COLOR_GREEN, None, name="addition") self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend") # newwin([height, width,] begin_y, begin_x) self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0) self.statuswin.keypad(1) # interpret arrow-key, etc. ESC sequences # figure out how much space to allocate for the chunk-pad which is # used for displaying the patch # stupid hack to prevent getnumlinesdisplayed from failing self.chunkpad = curses.newpad(1, self.xscreensize) # add 1 so to account for last line text reaching end of line self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 try: self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) except curses.error: self.initerr = _('this diff is too large to be displayed') return # initialize selecteditemendline (initial start-line is 0) self.selecteditemendline = self.getnumlinesdisplayed( self.currentselecteditem, recursechildren=False) # option which enables/disables patch-review (in editor) step self.opts['crecord_reviewpatch'] = False if self.opts['author'] is not None: # make it accessible by self.ui.username() self.ui.setusername(self.opts['author']) while True: self.updatescreen() try: keypressed = self.statuswin.getkey() if self.errorstr is not None: self.errorstr = None continue except curses.error: keypressed = "FOOBAR" if self.handlekeypressed(keypressed): break ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603621358.0 git-crecord-20201025.0/git_crecord/crecord_core.py0000664000000000000000000001613100000000000021541 0ustar00rootroot00000000000000# crecord.py # # Copyright 2008 Mark Edgington # # This software may be used and distributed according to the terms of # the GNU General Public License, incorporated herein by reference. # # Much of this extension is based on Bryan O'Sullivan's record extension. '''text-gui based change selection during commit or qrefresh''' from gettext import gettext as _ from . import encoding from . import util import io import errno import os import tempfile import subprocess from . import crpatch from . import chunk_selector def dorecord(ui, repo, commitfunc, *pats, **opts): def recordfunc(ui, repo, message, match, opts): """This is generic record driver. Its job is to interactively filter local changes, and accordingly prepare working dir into a state, where the job can be delegated to non-interactive commit command such as 'commit' or 'qrefresh'. After the actual job is done by non-interactive command, working dir state is restored to original. In the end we'll record interesting changes, and everything else will be left in place, so the user can continue his work. """ git_args = ["git", "-c", "diff.mnemonicprefix=false", "diff", "--binary"] git_base = [] if opts['cached']: git_args.append("--cached") if not opts['index'] and repo.head(): git_base.append("HEAD") p = subprocess.Popen(git_args + git_base, stdout=subprocess.PIPE, close_fds=util.closefds) fp = p.stdout # 0. parse patch fromfiles = set() tofiles = set() chunks = crpatch.parsepatch(fp) for c in chunks: if isinstance(c, crpatch.uiheader): fromfile, tofile = c.files() if fromfile is not None: fromfiles.add(fromfile) if tofile is not None: tofiles.add(tofile) added = tofiles - fromfiles removed = fromfiles - tofiles modified = tofiles - added - removed changes = [modified, added, removed] # 1. filter patch, so we have intending-to apply subset of it chunks = crpatch.filterpatch(opts, chunks, chunk_selector.chunkselector, ui) p.wait() del fp contenders = set() for h in chunks: try: contenders.update(set(h.files())) except AttributeError: pass changed = changes[0] | changes[1] | changes[2] newfiles = [f for f in changed if f in contenders] if not newfiles: ui.status(_('no changes to record\n')) return 0 # 2. backup changed files, so we can restore them in the end backups = {} newly_added_backups = {} backupdir = os.path.join(repo.controldir(), 'record-backups') try: os.mkdir(backupdir) except OSError as err: if err.errno != errno.EEXIST: raise index_backup = None try: index_backup = repo.open_index() index_backup.backup_tree() # backup continues for f in newfiles: if f not in (modified | added): continue fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', dir=backupdir) os.close(fd) ui.debug('backup %r as %r\n' % (f, tmpname)) pathname = os.path.join(repo.path, f) if os.path.isfile(pathname): util.copyfile(pathname, tmpname) if f in modified: backups[f] = tmpname elif f in added: newly_added_backups[f] = tmpname fp = io.StringIO() all_backups = {} all_backups.update(backups) all_backups.update(newly_added_backups) for c in chunks: c.write(fp) dopatch = fp.tell() fp.seek(0) # 2.5 optionally review / modify patch in text editor if opts['crecord_reviewpatch']: patchtext = fp.read() reviewedpatch = ui.edit(patchtext, "") fp.truncate(0) fp.write(reviewedpatch) fp.seek(0) # 3a. apply filtered patch to clean repo (clean) if backups or any((f in contenders for f in removed)): util.system(['git', 'checkout', '-f'] + git_base + ['--'] + [f for f in newfiles if f not in added], onerr=util.Abort, errprefix=_("checkout failed")) # remove newly added files from 'clean' repo (so patch can apply) for f in newly_added_backups: pathname = os.path.join(repo.path, f) if os.path.isfile(pathname): os.unlink(pathname) # 3b. (apply) if dopatch: try: ui.debug('applying patch\n') ui.debug(fp.getvalue()) p = subprocess.Popen(["git", "apply", "--whitespace=nowarn"], stdin=subprocess.PIPE, close_fds=util.closefds) p.stdin.write(fp.read().encode(encoding.encoding)) p.stdin.close() p.wait() except Exception as err: s = str(err) if s: raise util.Abort(s) else: raise util.Abort(_('patch failed to apply')) del fp # 4. We prepared working directory according to filtered patch. # Now is the time to delegate the job to commit/qrefresh or the like! # it is important to first chdir to repo root -- we'll call a # highlevel command with list of pathnames relative to repo root newfiles = [os.path.join(repo.path, n) for n in newfiles] if opts['operation'] == 'crecord': ui.commit(*newfiles, **opts) else: ui.stage(*newfiles, **opts) ui.debug('previous staging contents backed up as tree %r\n' % index_backup.indextree) index_backup = None return 0 finally: # 5. finally restore backed-up files try: for realname, tmpname in backups.items(): ui.debug('restoring %r to %r\n' % (tmpname, realname)) util.copyfile(tmpname, os.path.join(repo.path, realname)) os.unlink(tmpname) for realname, tmpname in newly_added_backups.items(): ui.debug('restoring %r to %r\n' % (tmpname, realname)) util.copyfile(tmpname, os.path.join(repo.path, realname)) os.unlink(tmpname) os.rmdir(backupdir) if index_backup: index_backup.write() except (OSError, NameError): pass return recordfunc(ui, repo, "", None, opts) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575457655.0 git-crecord-20201025.0/git_crecord/crpatch.py0000644000000000000000000005735500000000000020547 0ustar00rootroot00000000000000from __future__ import unicode_literals # stuff related specifically to patch manipulation / parsing from gettext import gettext as _ import io import re class PatchError(Exception): pass lines_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@\s*(.*)') class linereader(object): # simple class to allow pushing lines back into the input stream def __init__(self, fp): self.fp = fp self.buf = [] def push(self, line): if line is not None: self.buf.append(line) def readline(self): if self.buf: l = self.buf[0] del self.buf[0] return l return self.fp.readline().decode('UTF-8') def __iter__(self): return iter(self.readline, '') def scanpatch(fp): """like patch.iterhunks, but yield different events - ('file', [header_lines + fromfile + tofile]) - ('context', [context_lines]) - ('hunk', [hunk_lines]) - ('range', (-start,len, +start,len, diffp)) """ lr = linereader(fp) def scanwhile(first, p): """scan lr while predicate holds""" lines = [first] for line in iter(lr.readline, ''): if p(line): lines.append(line) else: lr.push(line) break return lines for line in iter(lr.readline, ''): if line.startswith('diff --git a/'): def notheader(line): s = line.split(None, 1) return not s or s[0] not in ('---', 'diff') header = scanwhile(line, notheader) fromfile = lr.readline() if fromfile.startswith('---'): tofile = lr.readline() header += [fromfile, tofile] else: lr.push(fromfile) yield 'file', header elif line.startswith(' '): yield 'context', scanwhile(line, lambda l: l[0] in ' \\') elif line[0] in '-+': yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\') else: m = lines_re.match(line) if m: yield 'range', m.groups() else: raise PatchError('unknown patch content: %r' % line) class patchnode(object): """Abstract Class for Patch Graph Nodes (i.e. PatchRoot, header, hunk, HunkLine) """ def firstchild(self): raise NotImplementedError("method must be implemented by subclass") def lastchild(self): raise NotImplementedError("method must be implemented by subclass") def allchildren(self): "Return a list of all of the direct children of this node" raise NotImplementedError("method must be implemented by subclass") def nextsibling(self): """ Return the closest next item of the same type where there are no items of different types between the current item and this closest item. If no such item exists, return None. """ raise NotImplementedError("method must be implemented by subclass") def prevsibling(self): """ Return the closest previous item of the same type where there are no items of different types between the current item and this closest item. If no such item exists, return None. """ raise NotImplementedError("method must be implemented by subclass") def parentitem(self): raise NotImplementedError("method must be implemented by subclass") def nextitem(self, skipfolded=True): """ Try to return the next item closest to this item, regardless of item's type (header, hunk, or hunkline). If skipfolded == True, and the current item is folded, then the child items that are hidden due to folding will be skipped when determining the next item. If it is not possible to get the next item, return None. """ try: itemfolded = self.folded except AttributeError: itemfolded = False if skipfolded and itemfolded: nextitem = self.nextsibling() if nextitem is None: try: nextitem = self.parentitem().nextsibling() except AttributeError: nextitem = None return nextitem else: # try child item = self.firstchild() if item is not None: return item # else try next sibling item = self.nextsibling() if item is not None: return item try: # else try parent's next sibling item = self.parentitem().nextsibling() if item is not None: return item # else return grandparent's next sibling (or None) return self.parentitem().parentitem().nextsibling() except AttributeError: # parent and/or grandparent was None return None def previtem(self): """ Try to return the previous item closest to this item, regardless of item's type (header, hunk, or hunkline). If it is not possible to get the previous item, return None. """ # try previous sibling's last child's last child, # else try previous sibling's last child, else try previous sibling prevsibling = self.prevsibling() if prevsibling is not None: prevsiblinglastchild = prevsibling.lastchild() if ((prevsiblinglastchild is not None) and not prevsibling.folded): prevsiblinglclc = prevsiblinglastchild.lastchild() if ((prevsiblinglclc is not None) and not prevsiblinglastchild.folded): return prevsiblinglclc else: return prevsiblinglastchild else: return prevsibling # try parent (or None) return self.parentitem() class patch(patchnode, list): # TODO: rename PatchRoot """ List of header objects representing the patch. """ def __init__(self, headerlist): self.extend(headerlist) # add parent patch object reference to each header for header in self: header.patch = self class uiheader(patchnode): """patch header XXX shoudn't we move this to mercurial/patch.py ? """ diff_re = re.compile('diff --git a/(.*) b/(.*)$') allhunks_re = re.compile('(?:GIT binary patch|new file|deleted file) ') pretty_re = re.compile('(?:new file|deleted file) ') special_re = re.compile('(?:GIT binary patch|new|deleted|copy|rename) ') def __init__(self, header): self.header = header self.hunks = [] # flag to indicate whether to apply this chunk self.applied = True # flag which only affects the status display indicating if a node's # children are partially applied (i.e. some applied, some not). self.partial = False # flag to indicate whether to display as folded/unfolded to user self.folded = True # list of all headers in patch self.patch = None # flag is False if this header was ever unfolded from initial state self.neverunfolded = True # one-letter file status self._changetype = None def binary(self): """ Return True if the file represented by the header is a binary file. Otherwise return False. """ return any(h.startswith('GIT binary patch') for h in self.header) def pretty(self, fp): for h in self.header: if h.startswith('GIT binary patch'): fp.write(_('this modifies a binary file (all or nothing)\n')) break if self.pretty_re.match(h): fp.write(h) if self.binary(): fp.write(_('this is a binary file\n')) break if h.startswith('---'): fp.write(_('%d hunks, %d lines changed\n') % (len(self.hunks), sum([max(h.added, h.removed) for h in self.hunks]))) break fp.write(h) def prettystr(self): x = io.StringIO() self.pretty(x) return x.getvalue() def write(self, fp): fp.write(''.join(self.header)) def allhunks(self): """ Return True if the file which the header represents was changed completely (i.e. there is no possibility of applying a hunk of changes smaller than the size of the entire file.) Otherwise return False """ return any(self.allhunks_re.match(h) for h in self.header) def files(self): fromfile, tofile = self.diff_re.match(self.header[0]).groups() if self.changetype == 'D': tofile = None elif self.changetype == 'A': fromfile = None return [fromfile, tofile] def filename(self): files = self.files() return files[1] or files[0] def __repr__(self): return '
' % (' '.join(map(repr, self.files()))) def special(self): return any(self.special_re.match(h) for h in self.header) @property def changetype(self): if self._changetype is None: self._changetype = "M" for h in self.header: if h.startswith('new file'): self._changetype = "A" elif h.startswith('deleted file'): self._changetype = "D" elif h.startswith('copy from'): self._changetype = "C" elif h.startswith('rename from'): self._changetype = "R" return self._changetype def nextsibling(self): numheadersinpatch = len(self.patch) indexofthisheader = self.patch.index(self) if indexofthisheader < numheadersinpatch - 1: nextheader = self.patch[indexofthisheader + 1] return nextheader else: return None def prevsibling(self): indexofthisheader = self.patch.index(self) if indexofthisheader > 0: previousheader = self.patch[indexofthisheader - 1] return previousheader else: return None def parentitem(self): """ There is no 'real' parent item of a header that can be selected, so return None. """ return None def firstchild(self): "Return the first child of this item, if one exists. Otherwise None." if len(self.hunks) > 0: return self.hunks[0] else: return None def lastchild(self): "Return the last child of this item, if one exists. Otherwise None." if len(self.hunks) > 0: return self.hunks[-1] else: return None def allchildren(self): "Return a list of all of the direct children of this node" return self.hunks class uihunkline(patchnode): "Represents a changed line in a hunk" def __init__(self, linetext, hunk): self.linetext = linetext self.applied = True # the parent hunk to which this line belongs self.hunk = hunk # folding lines currently is not used/needed, but this flag is needed # in the prevItem method. self.folded = False def prettystr(self): return self.linetext def nextsibling(self): numlinesinhunk = len(self.hunk.changedlines) indexofthisline = self.hunk.changedlines.index(self) if (indexofthisline < numlinesinhunk - 1): nextline = self.hunk.changedlines[indexofthisline + 1] return nextline else: return None def prevsibling(self): indexofthisline = self.hunk.changedlines.index(self) if indexofthisline > 0: previousline = self.hunk.changedlines[indexofthisline - 1] return previousline else: return None def parentitem(self): "Return the parent to the current item" return self.hunk def firstchild(self): "Return the first child of this item, if one exists. Otherwise None." # hunk-lines don't have children return None def lastchild(self): "Return the last child of this item, if one exists. Otherwise None." # hunk-lines don't have children return None class uihunk(patchnode): """ui patch hunk, wraps a hunk and keep track of ui behavior """ maxcontext = 3 def __init__(self, header, fromline, toline, proc, before, hunk, after): def trimcontext(number, lines): delta = len(lines) - self.maxcontext if False and delta > 0: return number + delta, lines[:self.maxcontext] return number, lines self.header = header self.fromline, self.before = trimcontext(fromline, before) self.toline, self.after = trimcontext(toline, after) self.proc = proc self.changedlines = [uihunkline(line, self) for line in hunk] self.added, self.removed = self.countchanges() # used at end for detecting how many removed lines were un-applied self.originalremoved = self.removed # flag to indicate whether to display as folded/unfolded to user self.folded = True # flag to indicate whether to apply this chunk self.applied = True # flag which only affects the status display indicating if a node's # children are partially applied (i.e. some applied, some not). self.partial = False def nextsibling(self): numhunksinheader = len(self.header.hunks) indexofthishunk = self.header.hunks.index(self) if (indexofthishunk < numhunksinheader - 1): nexthunk = self.header.hunks[indexofthishunk + 1] return nexthunk else: return None def prevsibling(self): indexofthishunk = self.header.hunks.index(self) if indexofthishunk > 0: previoushunk = self.header.hunks[indexofthishunk - 1] return previoushunk else: return None def parentitem(self): "Return the parent to the current item" return self.header def firstchild(self): "Return the first child of this item, if one exists. Otherwise None." if len(self.changedlines) > 0: return self.changedlines[0] else: return None def lastchild(self): "Return the last child of this item, if one exists. Otherwise None." if len(self.changedlines) > 0: return self.changedlines[-1] else: return None def allchildren(self): "Return a list of all of the direct children of this node" return self.changedlines def countchanges(self): """changedlines -> (n+,n-)""" add = len([l for l in self.changedlines if l.applied and l.prettystr().startswith('+')]) rem = len([l for l in self.changedlines if l.applied and l.prettystr().startswith('-')]) return add, rem def getfromtoline(self): # calculate the number of removed lines converted to context lines removedconvertedtocontext = self.originalremoved - self.removed contextlen = (len(self.before) + len(self.after) + removedconvertedtocontext) if self.after and self.after[-1] == '\\ No newline at end of file\n': contextlen -= 1 fromlen = contextlen + self.removed tolen = contextlen + self.added # Diffutils manual, section "2.2.2.2 Detailed Description of Unified # Format": "An empty hunk is considered to end at the line that # precedes the hunk." # # So, if either of hunks is empty, decrease its line start. --immerrr # But only do this if fromline > 0, to avoid having, e.g fromline=-1. fromline, toline = self.fromline, self.toline if fromline != 0: if fromlen == 0: fromline -= 1 if tolen == 0 and toline > 0: toline -= 1 fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % ( fromline, fromlen, toline, tolen, self.proc and (' ' + self.proc)) return fromtoline def write(self, fp): # updated self.added/removed, which are used by getfromtoline() self.added, self.removed = self.countchanges() fp.write(self.getfromtoline()) hunklinelist = [] # add the following to the list: (1) all applied lines, and # (2) all unapplied removal lines (convert these to context lines) for changedline in self.changedlines: changedlinestr = changedline.prettystr() if changedline.applied: hunklinelist.append(changedlinestr) elif changedlinestr.startswith("-"): hunklinelist.append(" " + changedlinestr[1:]) fp.write(''.join(self.before + hunklinelist + self.after)) def reversehunks(self): m = {'+': '-', '-': '+', '\\': '\\'} hunk = ['%s%s' % (m[l.prettystr()[0:1]], l.prettystr()[1:]) for l in self.changedlines if l.applied] return uihunk(self.header, self.fromline, self.toline, self.proc, self.before, hunk, self.after) def unapplyhunks(self): m = {'+': '-', '-': '+', '\\': '\\'} hunklinelist = [] for changedline in self.changedlines: changedlinestr = changedline.prettystr() if not changedline.applied: hunklinelist.append('%s%s' % (m[changedlinestr[0]], changedlinestr[1:])) elif changedlinestr.startswith("+"): hunklinelist.append(" " + changedlinestr[1:]) return uihunk(self.header, self.fromline, self.toline, self.proc, self.before, hunklinelist, self.after) pretty = write def filename(self): return self.header.filename() def prettystr(self): x = io.StringIO() self.pretty(x) return x.getvalue() def __repr__(self): return '' % (self.filename(), self.fromline) def parsepatch(fp): "Parse a patch, returning a list of header and hunk objects." class parser(object): """patch parsing state machine""" def __init__(self): self.fromline = 0 self.toline = 0 self.proc = '' self.header = None self.context = [] self.before = [] self.hunk = [] self.headers = [] def addrange(self, limits): "Store range line info to associated instance variables." fromstart, fromend, tostart, toend, proc = limits self.fromline = int(fromstart) self.toline = int(tostart) self.proc = proc def add_new_hunk(self): """ Create a new complete hunk object, adding it to the latest header and to self.headers. Add all of the previously collected information about the hunk to the new hunk object. This information includes header, from/to-lines, function (self.proc), preceding context lines, changed lines, as well as the current context lines (which follow the changed lines). The size of the from/to lines are updated to be correct for the next hunk we parse. """ h = uihunk(self.header, self.fromline, self.toline, self.proc, self.before, self.hunk, self.context) self.header.hunks.append(h) self.headers.append(h) self.fromline += len(self.before) + h.removed + len(self.context) self.toline += len(self.before) + h.added + len(self.context) self.before = [] self.hunk = [] self.context = [] self.proc = '' def addcontext(self, context): """ Set the value of self.context. Also, if an unprocessed set of changelines was previously encountered, this is the condition for creating a complete hunk object. In this case, we create and add a new hunk object to the most recent header object, and to self.strem. """ self.context = context # if there have been changed lines encountered that haven't yet # been add to a hunk. if self.hunk: self.add_new_hunk() def addhunk(self, hunk): """ Store the changed lines in self.changedlines. Mark any context lines in the context-line buffer (self.context) as lines preceding the changed-lines (i.e. stored in self.before), and clear the context-line buffer. """ self.hunk = hunk self.before = self.context self.context = [] def newfile(self, hdr): """ Create a header object containing the header lines, and the filename the header applies to. Add the header to self.headers. """ # if there are any lines in the unchanged-lines buffer, create a # new hunk using them, and add it to the last header. if self.hunk: self.add_new_hunk() # create a new header and add it to self.header h = uiheader(hdr) self.headers.append(h) self.header = h def finished(self): # if there are any lines in the unchanged-lines buffer, create a # new hunk using them, and add it to the last header. if self.hunk: self.add_new_hunk() return self.headers transitions = { 'file': {'context': addcontext, 'file': newfile, 'hunk': addhunk, 'range': addrange}, 'context': {'file': newfile, 'hunk': addhunk, 'range': addrange}, 'hunk': {'context': addcontext, 'file': newfile, 'range': addrange}, 'range': {'context': addcontext, 'hunk': addhunk}, } p = parser() # run the state-machine state = 'context' for newstate, data in scanpatch(fp): try: p.transitions[state][newstate](p, data) except KeyError: raise PatchError('unhandled transition: %s -> %s' % (state, newstate)) state = newstate return p.finished() def filterpatch(opts, chunks, chunkselector, ui): """Interactively filter patch chunks into applied-only chunks""" chunks = list(chunks) # convert chunks list into structure suitable for displaying/modifying # with curses. Create a list of headers only. headers = [c for c in chunks if isinstance(c, uiheader)] # if there are no changed files if len(headers) == 0: return [] # let user choose headers/hunks/lines, and mark their applied flags accordingly chunkselector(opts, headers, ui) appliedHunkList = [] for hdr in headers: if (hdr.applied and (hdr.special() or hdr.binary() or len([h for h in hdr.hunks if h.applied]) > 0)): appliedHunkList.append(hdr) fixoffset = 0 for hnk in hdr.hunks: if hnk.applied: appliedHunkList.append(hnk) # adjust the 'to'-line offset of the hunk to be correct # after de-activating some of the other hunks for this file if fixoffset: #hnk = copy.copy(hnk) # necessary?? hnk.toline += fixoffset else: fixoffset += hnk.removed - hnk.added return appliedHunkList ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603619052.0 git-crecord-20201025.0/git_crecord/encoding.py0000644000000000000000000001260400000000000020675 0ustar00rootroot00000000000000# encoding.py - character transcoding support for Mercurial # # Copyright 2005-2009 Matt Mackall and others # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from __future__ import unicode_literals, absolute_import, print_function import array import locale import os import unicodedata import sys class pycompat: ispy3 = (sys.version_info[0] >= 3) if pycompat.ispy3: import builtins def _sysstr(s): """Return a keyword str to be passed to Python functions such as getattr() and str.encode() This never raises UnicodeDecodeError. Non-ascii characters are considered invalid and mapped to arbitrary but unique code points such that 'sysstr(a) != sysstr(b)' for all 'a != b'. """ if isinstance(s, builtins.str): return s return s.decode('latin-1') else: def _sysstr(s): return s if pycompat.ispy3: unichr = chr # encoding.environ is provided read-only, which may not be used to modify # the process environment _nativeenviron = (not pycompat.ispy3 or os.supports_bytes_environ) if not pycompat.ispy3: environ = os.environ elif _nativeenviron: environ = os.environb else: # preferred encoding isn't known yet; use utf-8 to avoid unicode error # and recreate it once encoding is settled environ = dict((k.encode('utf-8'), v.encode('utf-8')) for k, v in os.environ.items()) def _getpreferredencoding(): ''' On darwin, getpreferredencoding ignores the locale environment and always returns mac-roman. http://bugs.python.org/issue6202 fixes this for Python 2.7 and up. This is the same corrected code for earlier Python versions. However, we can't use a version check for this method, as some distributions patch Python to fix this. Instead, we use it as a 'fixer' for the mac-roman encoding, as it is unlikely that this encoding is the actually expected. ''' try: locale.CODESET except AttributeError: # Fall back to parsing environment variables :-( return locale.getdefaultlocale()[1] oldloc = locale.setlocale(locale.LC_CTYPE) locale.setlocale(locale.LC_CTYPE, "") result = locale.nl_langinfo(locale.CODESET) locale.setlocale(locale.LC_CTYPE, oldloc) return result _encodingfixers = { '646': lambda: 'ascii', 'ANSI_X3.4-1968': lambda: 'ascii', 'mac-roman': _getpreferredencoding } try: encoding = locale.getpreferredencoding() or 'ascii' encoding = _encodingfixers.get(encoding, lambda: encoding)() except locale.Error: encoding = 'ascii' encodingmode = "strict" fallbackencoding = 'ISO-8859-1' if not _nativeenviron: # now encoding and helper functions are available, recreate the environ # dict to be exported to other modules environ = dict((k.encode('utf-8'), v.encode('utf-8')) for k, v in os.environ.items()) # How to treat ambiguous-width characters. Set to 'WFA' to treat as wide. wide = "WF" def ucolwidth(d): "Find the column width of a Unicode string for display" eaw = getattr(unicodedata, 'east_asian_width', None) if eaw is not None: return sum([eaw(c) in wide and 2 or 1 for c in d]) return len(d) def getcols(s, start, c): '''Use colwidth to find a c-column substring of s starting at byte index start''' for x in range(start + c, len(s)): t = s[start:x] if colwidth(t) == c: return t def trim(s, width, ellipsis='', leftside=False): """Trim string 's' to at most 'width' columns (including 'ellipsis'). If 'leftside' is True, left side of string 's' is trimmed. 'ellipsis' is always placed at trimmed side. >>> ellipsis = '+++' >>> encoding = 'utf-8' >>> t = '1234567890' >>> print(trim(t, 12, ellipsis=ellipsis)) 1234567890 >>> print(trim(t, 10, ellipsis=ellipsis)) 1234567890 >>> print(trim(t, 8, ellipsis=ellipsis)) 12345+++ >>> print(trim(t, 8, ellipsis=ellipsis, leftside=True)) +++67890 >>> print(trim(t, 8)) 12345678 >>> print(trim(t, 8, leftside=True)) 34567890 >>> print(trim(t, 3, ellipsis=ellipsis)) +++ >>> print(trim(t, 1, ellipsis=ellipsis)) + >>> t = '\u3042\u3044\u3046\u3048\u304a' # 2 x 5 = 10 columns >>> print(trim(t, 12, ellipsis=ellipsis)) \u3042\u3044\u3046\u3048\u304a >>> print(trim(t, 10, ellipsis=ellipsis)) \u3042\u3044\u3046\u3048\u304a >>> print(trim(t, 8, ellipsis=ellipsis)) \u3042\u3044+++ >>> print(trim(t, 8, ellipsis=ellipsis, leftside=True)) +++\u3048\u304a >>> print(trim(t, 5)) \u3042\u3044 >>> print(trim(t, 5, leftside=True)) \u3048\u304a >>> print(trim(t, 4, ellipsis=ellipsis)) +++ >>> print(trim(t, 4, ellipsis=ellipsis, leftside=True)) +++ """ if ucolwidth(s) <= width: # trimming is not needed return s width -= len(ellipsis) if width <= 0: # no enough room even for ellipsis return ellipsis[:width + len(ellipsis)] if leftside: uslice = lambda i: s[i:] concat = lambda s: ellipsis + s else: uslice = lambda i: s[:-i] concat = lambda s: s + ellipsis for i in range(1, len(s)): usub = uslice(i) if ucolwidth(usub) <= width: return concat(usub) return ellipsis # no enough room for multi-column characters ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603619937.0 git-crecord-20201025.0/git_crecord/gitrepo.py0000644000000000000000000000352300000000000020560 0ustar00rootroot00000000000000import os import sys from . import util INDEX_FILENAME = "index" class GitTree(object): def __init__(self, tree): self._tree = tree def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._tree) def read(self): util.system(['git', 'read-tree', '--reset', self._tree], onerr=RuntimeError) class GitIndex(object): def __init__(self, filename): self._filename = filename self.indextree = None def __repr__(self): return "%s(%r, %r)" % (self.__class__.__name__, self._filename, self.indextree) def commit(self): return util.systemcall(['git', 'write-tree'], onerr=RuntimeError).rstrip('\n') def write(self): GitTree(self.indextree).read() def backup_tree(self): try: self.indextree = self.commit() except RuntimeError as inst: raise util.Abort('failed to read the index: %s' % inst) return self.indextree class GitRepo(object): def __init__(self, path): try: self.path = util.systemcall(['git', 'rev-parse', '--show-toplevel'], onerr=util.Abort).rstrip('\n') self._controldir = util.systemcall(['git', 'rev-parse', '--git-dir']).rstrip('\n') if not os.path.isdir(self._controldir): raise util.Abort except util.Abort: sys.exit(1) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self.path) def controldir(self): return os.path.abspath(self._controldir) def index_path(self): return os.path.join(self.controldir(), INDEX_FILENAME) def open_index(self): return GitIndex(self.index_path()) def head(self): return util.systemcall(['git', 'rev-parse', '--verify', '-q', 'HEAD']).rstrip('\n') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603617839.0 git-crecord-20201025.0/git_crecord/main.py0000664000000000000000000001707100000000000020040 0ustar00rootroot00000000000000from gettext import gettext as _ from .gitrepo import GitRepo import os import sys from . import crecord_core from . import util import tempfile import argparse class Config: def get(self, section, item, default=None): try: return util.systemcall(['git', 'config', '--get', '%s.%s' % (section, item)], onerr=KeyError).rstrip('\n') except KeyError: return default def set(self, section, item, value, source=""): raise NotImplementedError class Ui: def __init__(self, repo): self.repo = repo self.config = Config() self.debuglevel = 0 try: self._username = "%s <%s>" % (self.config.get("user", "name"), self.config.get("user", "email")) except KeyError: self._username = None def debug(self, *msg, **opts): if self.debuglevel < 2: return for m in msg: sys.stdout.write(m) def info(self, *msg, **opts): if self.debuglevel < 1: return sys.stdout.flush() for m in msg: sys.stderr.write(m) sys.stderr.flush() def status(self, *msg, **opts): for m in msg: sys.stdout.write(m) def warn(self, *msg, **opts): sys.stdout.flush() for m in msg: sys.stderr.write(m) sys.stderr.flush() def setdebuglevel(self, level): self.debuglevel = level def setusername(self, username): self._username = username def username(self): if self._username is None: util.Abort(_("no name or email for the author was given")) return self._username def geteditor(self): editor = 'sensible-editor' return (os.environ.get("GIT_EDITOR") or self.config.get("core", "editor") or os.environ.get("VISUAL") or os.environ.get("EDITOR", editor)) def edit(self, text, user, extra=None, name=None): fd = None if name is None: (fd, name) = tempfile.mkstemp(prefix='git-crecord-', suffix=".txt", text=True) try: if fd is not None: f = os.fdopen(fd, "w") else: f = open(name, "w") f.write(text) f.close() editor = self.geteditor() util.system("%s \"%s\"" % (editor, name), onerr=util.Abort, errprefix=_("edit failed")) f = open(name) t = f.read() f.close() finally: if fd is not None: os.unlink(name) return t def stage(self, *files, **opts): to_add = [f for f in files if os.path.exists(f)] if to_add: util.system(['git', 'add', '-f', '-N', '--'] + to_add, onerr=util.Abort, errprefix=_("add failed")) def commit(self, *files, **opts): (fd, name) = tempfile.mkstemp(prefix='git-crecord-', suffix=".txt", text=True) try: args = [] # git-commit doesn't play nice with empty lines # and comments in the commit message when --edit # is used with --file; # to work that around, use --template when # no message is specified and --file otherwise. f = os.fdopen(fd, "w") if opts['message'] is not None: f.write(opts['message']) f.close() if opts['cleanup'] is None: opts['cleanup'] = 'strip' for k, v in opts.items(): if k in ('author', 'date', 'amend', 'signoff', 'cleanup', 'reset_author', 'gpg_sign', 'no_gpg_sign', 'reedit_message', 'reuse_message', 'quiet'): if v is None: continue if isinstance(v, bool): if v is True: args.append('--%s' % k.replace('_', '-')) else: args.append('--%s=%s' % (k.replace('_', '-'), v)) to_add = [f for f in files if os.path.exists(f)] if to_add: util.system(['git', 'add', '-f', '-N', '--'] + to_add, onerr=util.Abort, errprefix=_("add failed")) if opts['message'] is None: util.system(['git', 'commit'] + args + ['--'] + list(files), onerr=util.Abort, errprefix=_("commit failed")) else: util.system(['git', 'commit', '-F', name] + args + ['--'] + list(files), onerr=util.Abort, errprefix=_("commit failed")) finally: os.unlink(name) def main(): prog = os.path.basename(sys.argv[0]).replace('-', ' ') subcommand = prog.split(' ')[-1].replace('.py', '') if subcommand == 'crecord': action = 'commit or stage' elif subcommand == 'cstage': action = 'stage' elif subcommand == 'cunstage': action = 'keep staged' parser = argparse.ArgumentParser(description='interactively select changes to %s' % action, prog=prog) parser.add_argument('--author', default=None, help='override author for commit') parser.add_argument('--date', default=None, help='override date for commit') parser.add_argument('-m', '--message', default=None, help='commit message') parser.add_argument('-c', '--reedit-message', metavar='COMMIT', default=None, help='reuse and edit message from specified commit') parser.add_argument('-C', '--reuse-message', metavar='COMMIT', default=None, help='reuse message from specified commit') parser.add_argument('--reset-author', action='store_true', default=False, help='the commit is authored by me now (used with -C/-c/--amend)') parser.add_argument('-s', '--signoff', action='store_true', default=False, help='add Signed-off-by:') parser.add_argument('--amend', action='store_true', default=False, help='amend previous commit') parser.add_argument('-S', '--gpg-sign', metavar='KEY-ID', nargs='?', const=True, default=None, help='GPG sign commit') parser.add_argument('--no-gpg-sign', action='store_true', default=False, help=argparse.SUPPRESS) parser.add_argument('-v', '--verbose', default=0, action='count', help='be more verbose') parser.add_argument('--debug', action='store_const', const=2, dest='verbose', help='be debuggingly verbose') parser.add_argument('--cleanup', default=None, help=argparse.SUPPRESS) parser.add_argument('--quiet', default=False, action='store_true', help='pass --quiet to git commit') parser.add_argument('--confirm', default=False, action='store_true', help='show confirmation prompt after selecting changes') group = parser.add_mutually_exclusive_group() group.add_argument('--cached', '--staged', action='store_true', default=False, help=argparse.SUPPRESS) group.add_argument('--index', action='store_true', default=False, help=argparse.SUPPRESS) args = parser.parse_args() opts = vars(args) opts['operation'] = subcommand if subcommand == 'cstage': opts['index'] = True if subcommand == 'cunstage': opts['cached'] = True repo = GitRepo(".") ui = Ui(repo) ui.setdebuglevel(opts['verbose']) os.chdir(repo.path) try: crecord_core.dorecord(ui, repo, None, **(opts)) except util.Abort as inst: sinst = str(inst) if opts['quiet'] and 'commit failed' in sinst: sys.exit(5) else: sys.stderr.write(_("abort: %s\n") % sinst) sys.exit(1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575457655.0 git-crecord-20201025.0/git_crecord/util.py0000644000000000000000000000653000000000000020065 0ustar00rootroot00000000000000# util.py - utility functions from Mercurial # # Copyright 2006, 2015 Matt Mackall # Copyright 2007 Eric St-Jean # Copyright 2009, 2011 Mads Kiilerich # Copyright 2015 Pierre-Yves David # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version from __future__ import unicode_literals from gettext import gettext as _ import os import subprocess import shutil import sys from . import encoding closefds = os.name == 'posix' def explainexit(code): """return a 2-tuple (desc, code) describing a subprocess status (codes from kill are negative - not os.system/wait encoding)""" if (code < 0) and (os.name == 'posix'): return _("killed by signal %d") % -code, -code else: return _("exited with status %d") % code, code class Abort(Exception): pass def system(cmd, cwd=None, onerr=None, errprefix=None): try: sys.stdout.flush() except Exception: pass if isinstance(cmd, list): shell = False prog = os.path.basename(cmd[0]) else: shell = True prog = os.path.basename(cmd.split(None, 1)[0]) rc = subprocess.call(cmd, shell=shell, close_fds=closefds, cwd=cwd) if rc and onerr: errmsg = '%s %s' % (prog, explainexit(rc)[0]) if errprefix: errmsg = '%s: %s' % (errprefix, errmsg) raise onerr(errmsg) return rc def systemcall(cmd, onerr=None, errprefix=None): try: sys.stdout.flush() except Exception: pass p = subprocess.Popen(cmd, stdout=subprocess.PIPE, close_fds=closefds) out = '' for line in iter(p.stdout.readline, b''): out = out + line.decode(encoding.encoding) p.wait() rc = p.returncode if rc and onerr: errmsg = '%s %s' % (os.path.basename(cmd[0]), explainexit(rc)[0]) if errprefix: errmsg = '%s: %s' % (errprefix, errmsg) raise onerr(errmsg) return out def copyfile(src, dest, hardlink=False, copystat=False): '''copy a file, preserving mode and optionally other stat info like atime/mtime''' if os.path.lexists(dest): os.unlink(dest) # hardlinks are problematic on CIFS, quietly ignore this flag # until we find a way to work around it cleanly (issue4546) if False and hardlink: try: os.link(src, dest) return except (IOError, OSError): pass # fall back to normal copy if os.path.islink(src): os.symlink(os.readlink(src), dest) # copytime is ignored for symlinks, but in general copytime isn't needed # for them anyway else: try: shutil.copyfile(src, dest) if copystat: # copystat also copies mode shutil.copystat(src, dest) else: shutil.copymode(src, dest) except shutil.Error as inst: raise Abort(str(inst)) def ellipsis(text, maxlength=400): """Trim string to at most maxlength (default: 400) columns in display.""" return encoding.trim(text, maxlength, ellipsis='...') _notset = object() def safehasattr(thing, attr): return getattr(thing, attr, _notset) is not _notset ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1603625840.7119823 git-crecord-20201025.0/git_crecord.egg-info/0000775000000000000000000000000000000000000020226 5ustar00rootroot00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603625839.0 git-crecord-20201025.0/git_crecord.egg-info/PKG-INFO0000664000000000000000000000762200000000000021332 0ustar00rootroot00000000000000Metadata-Version: 2.1 Name: git-crecord Version: 20201025.0 Summary: interactively select chunks to commit with Git Home-page: https://github.com/andrewshadura/git-crecord Author: Andrej Shadura Author-email: andrew@shadura.me License: GPL-2+ Description: =========== Git crecord =========== About ----- **git-crecord** is a Git subcommand which allows users to interactively select changes to commit or stage using a ncurses-based text user interface. It is a port of the Mercurial crecord extension originally written by Mark Edgington. .. image:: screenshot.png :alt: Screenshot of git-crecord in action git-crecord allows you to interactively choose among the changes you have made (with line-level granularity), and commit, stage or unstage only those changes you select. After committing or staging the selected changes, the unselected changes are still present in your working copy, so you can use crecord multiple times to split large changes into several smaller changesets. Installation ------------ git-crecord assumes you have Python 3.6 or later installed as ``/usr/bin/python3``. git-crecord ships with a setup.py installer based on setuptools. To install git-crecord, simply type:: ./setup.py install This will install git-crecord itself, its manpage and this README file into their proper locations. Alternatively, to install it manually, symlink ``git-crecord`` into the directory where Git can find it, which can be a directory in your ``$PATH``:: ln -s $PWD/git-crecord ~/.local/bin/git-crecord Now you should have a new subcommand available for you. When you're ready to commit some of your changes, type:: git crecord This will bring up a window where you can view all of your changes, and select/de-select changes. You can find more information on how to use it in the built-in help (press the '?' key). ``git crecord`` supports most popular options of ``git commit``: ``--author=``, ``--date=``, ``--message=``, ``--amend``, ``--signoff``. License ------- This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 text for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Authors ------- For the list of contributors, see CONTRIBUTORS. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Version Control Requires-Python: >=3.6 Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603625840.0 git-crecord-20201025.0/git_crecord.egg-info/SOURCES.txt0000664000000000000000000000101400000000000022106 0ustar00rootroot00000000000000.hgignore CONTRIBUTORS COPYING MANIFEST.in README.rst TODO git-crecord git-crecord.1 git-crecord.rst screenshot.png setup.cfg setup.py .github/FUNDING.yml git_crecord/__init__.py git_crecord/chunk_selector.py git_crecord/crecord_core.py git_crecord/crpatch.py git_crecord/encoding.py git_crecord/gitrepo.py git_crecord/main.py git_crecord/util.py git_crecord.egg-info/PKG-INFO git_crecord.egg-info/SOURCES.txt git_crecord.egg-info/dependency_links.txt git_crecord.egg-info/entry_points.txt git_crecord.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603625839.0 git-crecord-20201025.0/git_crecord.egg-info/dependency_links.txt0000664000000000000000000000000100000000000024274 0ustar00rootroot00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603625839.0 git-crecord-20201025.0/git_crecord.egg-info/entry_points.txt0000664000000000000000000000006700000000000023527 0ustar00rootroot00000000000000[console_scripts] git-crecord = git_crecord.main:main ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603625839.0 git-crecord-20201025.0/git_crecord.egg-info/top_level.txt0000664000000000000000000000001400000000000022753 0ustar00rootroot00000000000000git_crecord ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1529923889.0 git-crecord-20201025.0/screenshot.png0000644000000000000000000011337700000000000017145 0ustar00rootroot00000000000000PNG  IHDRKOᝳbKGDC pHYs  tIME  *n IDATxwxUހ߹7@@ қ RD \APq]eEYEADPWS" HSH@I ^n-{$d_ߜ33G2@ ;P@ (@  *h'[ԴیOR_ԍz4彾#`t"WW]ntNPw5fUM2z10/A"oᘮ}zX܂yF_ ^ z$5d 6:ż32R/UF繵zs3jVJ$Df9>9VM'chZ~5;Vf&xhe3tp $ԥ_SHÍF1Z^ V<˖E:>gqh޼++($?z6]K8;sE92||iu"y1[>;ͺ O SFfI '}0c?=2 w2>^~c% j:8Nf'3Cb ,&Ǿ/"Cut`W=@@:FsΛ{TF/ey5lR|p,cUZa^(ceUAK {Tv}|]0ѿec/.X|${ŕKZy0bJ4|%ǾT ը *!_6x :%Uŋ}N8-7_EޝKUEP 6IxtuV?\svN0VɂzzOksuE9^3ih`]I`p#YYNU%XOy+^lfT||ڊ k.cvЯFz4]#.\8=AIMd#EnV}*Wk$IQ vd@.)| v ⑰bXCjYyx!VC甪TfdqX~Y"ԇ~ ^*ןk}JԻ:'!=Vhd _-5c=_x(&ݫG+֗ՒlfFUE澨S9kq|G~_(K].6ǩym/_rRh[yuE.yzI@jI&7-ߧs XTD]䕨UDda:Ѫ0=7s _Ai,uD{KHvi'ҘC!5 c1WQ/BV.eF'BPl>SFor)W& HE. Dhl8cB_nN\͌rE6v|x`▟aK.5nDyPi?gU5 dX:tТOȬun\Y"u=KC_JΟ?krɑU4hsz $9杴"Lr9?gMĴq—"zj zyg"gdܰ %%6 F:3'A+CCiꭂ!Migkvo ͦl#q"&<]FF=sOk$.t.\W|'PU_cMikM/),ܪ=|5)C% B/0&?wr9{i/vNn=t:w|> |q-9ynyK*%rĶ2/flBNb7#x KN \le6 7~deX`f0.q· w`e]%l*ۧ>]ysG>6szWƲg]Qp cfloo;[)v[5bV+ Z;jӱhBdrrG ц,2\ hHգ~acD=Aal/o31;MY(/ҙ<2'0,ʗQ:v1&LS#K.pZa_2caĝ5EZ%t:ArqCPr /o3S(Yt^ lؚHJQU ])&bZaʤĄ_/Q]>_O>ՁAm0h܇ߒI˔8di}*NگOAkvS 0~gbQ2(y҅-8R̗KapOƌϑf,.򋳘.8_; sJvW%U(|9U9ue1vR㟉F: aēuSkK,vIG7+,MK=C22S}zP,8c:cJcϕY"Wx8+' K=Ǽ  TKaQO]EnRk¯29Lwye^ ;9D`/AYYhC!;v `rd{xIfJdt4gg~jy?r%o+'c^2 YV5!z D:b"6,F ~eeTvHkDWٙUn4xڷѓr8LD`cU}h -}\kʮcQZ!|Yi~su4]G1T +P8gMĴqAއ0-XZf  lN)@v֕aJ<+X"ʨ:)lTGŻ{zQ.‰:N=rӥkS0Wi_n%\ĭt*]rre2xPVTEߛ‘ z(.pC(-כ,.m2U)jTh\3WOJ>HW?joK;5bк~q_PVAbM]d0œJv Gh2^αf'2v$ jw,_wrZ[zMF@rw ñHԀ]v<9Py:rѯGm\ ߳[,>os$G[8xwqANYWܩ]Ȍϳɒ+uz Atj'ޝć eFV\v:N/]僚`էn;*QҧK9kmt4 V/u5E$[5r)UП{EFVI^kEdǍywqKq@׹?9w2Tj-ѡZtR5hlk ak7#ABT-s*"P̅.g39dYs} Vk}JkWvz̻`8؏J*Gkk4$s0-0r׷i`CU\NrZԎWRB%nLD~'/){.!-B|b'7L~ GuӱTQ/QZDR%Xdsbp0kP54L<6L`'29VRq\r0T?/$Vz)D]y.A9vj"B4ز ٱ;~Ҽ7~+\΍Yb ONUرv;tFZN#'7/-:ǯ:_ZIUA2aZ4ƠF.H!/SThGh P z}|ew}ޣ|^:σB 5 l>T+oihS>\[d{ؖ[ʖu) g vҲX%LV6g K-+{ X,eqt {spoTK |SmR3e9(nĐVzފ+ݛm}hB>r4LΩ QEڠ.-Oaw"Voc1,|dD+Wn:zj/%YyژyXٙݬcdlssz*ȗ2xO= T&ӱTE/]ki֪% kvn>1,(o<"7ﳳd]ɕ kNs7=l6~9[{KYF=#MV)//F6F-X"V@ v8\| W=ƩKY|vȓO2LAl }C44k!`7%jQ_ælKyi>['2ȭ_j z*'Hb/[}X(ScژI_dSSoEkCfYP6_m)LZT)zώLaz8"@U}D9&o-VTkc[# Wf(/a LJ^]By?^{YL{5ARPj"tY[ e(;U?iI,4zې~uhd+G#\Z#]d/S{z-rN3#"i2)Y,QRCm ` cdd! fbet8]Db6Q>G5cngmAފF*fC%]iჂ/݄x*g$SĴA—DA(@ @ &Q05w~Q>$kAFSZvs6my3fL ٕb*aZl ?\:|nj #AEl5Ynm^n%[gomA*bP`)dX_nUؼi1q_N)$Tߣ"ZzSpT/HHF2u@['h \<1s|xzH(3\8ʔ ڐ1瑐/WK~TskP30認)+&a$募t4mpLG.nTZG_GA\H;VLIMXnQJc2d#w*ЇeMgɧ"{4}!CCXE4:Z6joso^՘ Jޭج9B..e&,m E&]>l)ljPZp)m:sh( 5ks^k0NOOY^mx8׮ ih`]Iᣃnuڋͬ])#5xص5,7=Hj"#U:NTJ׬nEF7UιOoβ7MrFl{حwڡ5ཁeZzog͝鳥J~-$V%||f: YZ6;u6jw)3:EG%Bc՟h_rrs:?B]7zK2A,.wؚ:k!?#ȨYo4_~&(W[CZeWgvլI澨S9kqGv GܽH Ivpy'W,t$X$"zj zygڑ,t [BH; 1#8BUR?iuatTdrrY}:d$6%m *"ZDdJT򯕣= < nn:ܶu1@Hmv.H 1+Vk|ėB@hjͫ+r)WnlaUᥲkfMq:z.H]aٷ,]V;]c1ǹ|J1M94\>TDnže,dYՄ%(~eeTvHkDW6'*MذHj$ib4k0Hgg~ ;WI"At 1IK]LYPtEۀ¯29LwyeqGmAIH_uц,Cv;~xr}:D@?B2oRE$mBu{{hˑWfnRttxKEH+?^$ܓEB;mԯ]ɮd?q{LyW);癫`rd{xIE]Ʀeo ίVUE54 /8)~v#7%&bvcfrq3_EA60pXeǓycW~\;V^{(۔i^T/Ń^\hɩsiF zqnpR[Nq} )-q.[94\/J1H1J2xJS[ٵOq^.Dovp 31 A*&:b$q#>g pU7kk}RP17s0Gh2^αe0%)[ȭ6%ƾJ5#xzQRL3~4]?-K..$3o0 u(-">\]_Zy`xխ-?%N#+ RFwX6rTDi2 tc[\vTj"B4ز ٱ;~Ҽ o]+$4)(c.if-L Ud\a %@RrN7*B|T$5ѭ/e)]C@5hč6RA&\>CS#-Z$B"sc*ޱmerS G"O0H 6=6Тv=j)o^Aw=HSQ38ԧR+Ƙ6%Eܺ8݉Men 9*jPC锞S8Rj]9x]gb܌6L-tؐAuwеz~Dg39(nĐVzފ+2&h{F2,SŻ_^$/ttKIU̚Ň[O }% .,. 9Kiɛ(d-ߓ˽}~,欸Ykrqk<9LvIgt Vap3d|Gp,Y|vȓO2LAl2X.ez<;8Azs MkNs7=l6~9[{K-*W>àf RQǪцLΩKmɬqddf5>'%dV*+!__ IDATqb[kf& /\ #ۖzkca _3N廜FPº}ܐC\c9L S?E06Uz`?%)nu#6+/J6RZ]W7~M2*_Sm nk>)/s_άgYwF.#Y?e"Q]U4_Kd/:3qt KalؙDBln2J22>n&ە)DO ~SU|&Y$]RD)cYS$YӶ5E@p'pdE~^_wܱo=_SSO= Bƿ讛0I 5a-iMf+ Vd a4 Wu8jz |U-Tjl?%ql?Y;1wO3n-FVS nmTJb_W0 E ײg!tfp冊%#l޽Nj B_X~x7 VCKFOcQYtZ2<6 37d\k_1V.0o>}ssݼ:N7}l7*8q>voؖ:@MCf?ǚ\Sa,odipy!TGASYdJ$ޜmw[/\f(Bɱ :{&ңqU,K=B.}ɱ\mN _Z3{k93Měf6MlfQz]KAp/x8TH'?8IQq[)whAh%ud?`xtx&ؔbUBxw{7^llh| Η.(=%p+Sh/ڿGGҞ}| ~ѤS|n^ ѵ,=3/Um?EUеԹ #TJwX>s--7è'[$daי24OڱtNO6;)_i$+1zb?Nm  M0GM#О**sK,c*rÊ=\ gLko%۪oފH0^Ł I> bu#aQP5灖^C''< 2ZO@ Q~?,MbR|-Lv}@OKGSs˫%L4?.c?6ԦYV<_zOO`B{QLzEfG?F $ziRjCRWalЊ"._RרrѦ` Ƚ92Hޱ 迌^Ԡ~&nDԶx6J`Iar/ћ4񵒰f44 9WzHvEu9+Gd$ ʟ.t,""H>y9e>`NfS+m jj[64Ztm; G>}iޟO%Ne中.} YO&tWSX/Oc'Y}6%OU_9~ERjtFt5P)'ӃvLKӀ8{u YZ bb &¶qؚ^[{?c+~ 4ҋ!v f0h98y2_q^6;2jB[uV! [z{wė{ɼNM-,/ˎȴe$Zix3q\"eLX^q9b,f +'7rߠ@8xgz٨tN%򬙒Zs8[!ĔLX4~rOW8)-"98N`˩2eJP涮/|Csޢ}k_.}7]m-ӧ@ (T_cpVLZ#1gI&ޏNbDvwyiL'ř$:Oɕ<_H3lѸ`7B]s2Za99-T) -gIjAx,3=a nA/_2uLnC]oc3ƚR(֧븭;W Y9APV.l"!եBMP?ѳ?A=}XЪێ孩 H7堉y7$g0s7G3ePbR ^ftٛ&1JnV w2WgC_'3ِmz^ƿUz<Lkf1_Z>J HY "bL9|(o1V wt%֫xF F%T @ B Hh ZH@ vzg;z\{ƛVb/՚֟/ŝPG-I QbAi%E] RUzjUƩ CX35?X/JOPUjϲl)s< N |{.ڴD>3s~Äj6x(oWJeta<&U=bo7;S2,MuР o7@> y+^l[+> x-{M2nF02{E"'@*z<%%/fGhn6^'_k vl4o>Ț]k6QD镎s:mgwxIv9=uLzG㈫Xr%=z+cS]>xNƵ6ęQ$tļxƶ)KI>uܿ5> !tfp58ďF$4DuPT`,1Z: Oi>,{tc㟟Q2 mJM^Bף4x5~Ԭxi%@ [W^Lx8-{jX yYw7oEX~v$n$zjϗm?EUеԹ V`ڏGF:=ġ= $UT_ ?K`;p"{NEfUv=:9eoo4[As| %bl8^f'70_u& ];n |f2b0hd20pi<:d<rbklJQޥŴtpMV &zkBF4-<;-)8c ?,?&iƀA]"E|e_Wڱxߟ`\:Ucq fl']"fL  >LjҚ0ߗeS3lʗ$Ix>`\9T#%Nz(d;]Z@Ѷ9]X%_ժ#}X:r,j+_>"&uc^}3bY5YVĴ[I9[Iе꡴b~*hAVƱǂ`|[Ģן>Y|S w0 &ޜ)Q1Ou[8 jj[64%!y2|&vOIuMxaIar̀~ik%aͱkfNR䐚e"(&b8%jJG_}G٩oX|F0uFEKQVP zݓ36tPXbUh\F~n8ԇxJh@ -\oȀn#OGctR 3sPg꘏9~>qJ6,${O敡Ѥ͝ }ƧUFv+6@&+kqDзFn=ݹU_~d@]@+H񏑌XcXX O&tWSX/Oc@TU&dM*5{_$|ɒb6  g*Vwe%@ Dn%cM>YݗZA ԿW\ΚjSk0I,i{ٴT]Ws8:n[m`orSk0aaK͎ЖiUH,7+gM#b`-s'+-,/ˎȴWP™~ #|Ŵj Iy`tq B1y߉37̸Y=6b JՆᴋ@~`[ゕٷ%/)ټùv h eX彿"!X2Os&T @ =f-Y$ey䓌|anÿ+UL6l4?Mܹ+WHYl9MW`| msV%z`ϿCHOF+$#ҫC(7tyl.mS|]^3IDq&S"CY'!;yPDdP ^M.bjN9K2~t#ZꕇGcyyzStVcG~_@ *!@ [ @ P ]Q3@ U<< ěMĝ[#"ăyNn4Q{#W^֮7bxT@w ~Y:m 6L"b![ɡ2>nO-U+HԘFZ"[p z/@ 7 1T֒f~pid@-(d#YTeJ6*]7axIvAvvT` vl4o>Ț]k6QDQ9ˊ@Měah} t4ED͉l6z7%8q>voؖ:e8xl*1@+q ta诂g]gHs*ȩ 6*_.<ݫJ, {/@ M0GM#О*~w׎w_L9AMVuu?δ/ΟgcZb7oEX~v$n$zzSpqE@,=3>h]Q>m_>#l]}Hf}Wbts--7è'[$daי2*dd/ZΞ™ؗnUЧ碿Ҝu'жz醿LFe/ {/@ Ts)u[I =gxZ&uc^}3bY5DrookQ:A^I 3&JAr&5iM~ưNx ?)/+.Gg:q'ޙ|:b%,͌2mtݵNǩ-GK‹QvmsHK>XƂ$G,~߿L)}`w*SOW0m-Y1e(UL׀/cs0 l+umePhZDɜ8W.̥c@AqX}̀~ZIXs 3z(; uV' (yĎ#=.%BW~yI¿d^ͩ`|̳מ$~>SH/pjJnr.F.&%˪0NM]#3f0`ݹh\'ۭvp-Y6ZmX׷l,H^Ӥ<0g:oD!Y˘fGFMhִ*e@K^6UaUc +'7rߠ@8;tnɈeJdݒϞFz1,[-'n賲9o2=[MW>eeVMArL L6l4?M_,$~,&ӫm0>@ z5S1eFwkHi:DhF /\Ⱦ:<2f/A@ ns @ X @AM!1h @ B/ŝPKjB&ohM||8W_pij%@ [}yhkfp '(L4ufg¶+ذ3uPcklQÕc,}[ƵFLwݞu&%ҼLy%ᚍJetM$e=+G@ p{T !6e IDATKWK{Qj2q;<܈ `37di8g}?}sc N͙&Yk{&}3(=9|i౩ϮNƵ: "S"DnFhW[jsJAp/x8TH'?8II>u]gdD<x|@Měah}e#l޽NIJ'@p{`ڏGF:=ġ= $UdGK7d3s4у7֭91iow_l?EUеԹ VoފH0^Ł I> b9|it'{)lGײt<\ľt+1|4Gغ r蕋%q:=.GA՜Zz^4wP+Ux=gvq -(=%p+xY3] @ z[I =gxCZ&uc^}3bY5DrookQ:A^I 3&JAr&5iM~ưNx ?)/.H}_)CG߆Ckf^6Y:fZ{srUP[}+̈OB}og0WI6趰 k!B4@x""P&~>%:I)("0+u`/)I8mfpH3?FSkP a€4>]@+ZD9Zi3>;Wp?z ehƖGW7ҽ,E;d.EB!: QQ (,Q3tU OYw( W?SpNz:w-o ]-w:?` 2 -̐we PDz=ϳ}2Y5{'t=>k!{M>U, cR>^l|CԶk!B\6C3Xl&~~c1мK7Vx[u|)qSxZI&9.0Yߌ3D}L-B0)|ȢzӞg^?<=AXWQ_*e7f&&o\RJ8 ]'{F+^$Ẃ 2G3/+~$k΢Bq\_г-~sYu$S0qj?#±:e s=Ofo9L|F^f9ZG z6_~[6EUY~oi;zc96XgCn\=U9B!1Jw{;NB!ɛJB!$ !B B!BB!@(B!$ !B B!BB!@(B!$ !B B!BB!@(B!$ !B B!BB!@(B!$ !B B!BB!@(B!$ !B B!BB!@(B!$ !B B!BB!@(B!$ !BKޖ}r+)lMڤMڤMڤMڤI)tB!/UWO^B!5gUEoӧrBa#q+w gca_q3k\ʺ'>`uVdf7׳Wr fݘ0B}O2q'gbG &ܜLb|Wk] `32ԶLOb{q󓃱,wkB!F+J|] u?曏VSb$(T=itPun ,>SĖzZJSLe:j*@ Jh=w71ӹ>ndk:ŀk{*ywS12SHpcA!h NNƶ=iT˖*j47"}ik}rpRMsg[cA19cK=׷NUBv 76=ǓBF4|/(mH6&we#^5*T;mg8^ ȯ./fm\s[*=i5F䁹d9Xx*F"ɝI4EN*_Գa7p02?+NwO^p7-m Zo<4:eRUo-gK01`v&.Do9>yKˈjh 5"L㏓Bv_b5!4jYʊ1jpbHo 9sW1AaGa;4+m{b|G "IэS+ߑZzx'v:|edUL0dB u91#.ZLm9"sn6n-ǯUVRmMiN6VGrNƁTB |xjdil]!~hG'~ޖ1w%[4TE?~R-: Z2r2ܗX B!D ػ܍oE1FVBdDl̍f\t L[.5~Wv#vrr+aس-- îa0 b6yZ6+J%[\Ȳ# ՍxCj>O!iD"AcE91b (v!Ĵ@ޡ#quYg rH2mo/b>?Q9!6(1IΏy_Mlx Պ֬dт2nH6qwI B4@s:T7RfhXB@!*EӬm3f6SPQy[ JOXIes_U=XI~p> ئcɜVL3<ܙsoI9T̩BpؔK!QB[vCtn?4[Un H#[]ꑹG޼w3iS(K+b<֛ ՠ:`0׿:[סAoXC-1~VxUEU¾TGytvo^M=9~ϡ'7RkX '3"̩]5!16#+y?f&<5a7al4C"> 6|T2gMKB5J~=0 .WDeH"ws*uؓsE}8Tr2ĉPc=9ʕ\ \T孚ۡPBqr鄓i,7Jx9u 3$M۴o>Ϲ(3UM h)DTSA!P&>*$B!.cB!BqR țh/B(깟s>)B4A<$KnL{a2عߦbfw2m@۾dn83w@]Ձfؗ^Ŕ/@b{1ũSٺuNy>B!*cS)Dun (>#>(FBU1徾 ^MLw"軿;FI#:%VERXFVD [Iՙ4DuӺw>ּ4y&s}>-)A12SK?nK e|^(Z}^k`ܟ U8oP^G[yV6! B_Z>xft8HqĒ|pF KLA&d *5eLɌJW$`Ҕ ^?F.9ǬFug]9@h3=WݕD187^;U??ڟXLu^*m7}i璺Ob\?3_G6^w}Z!Ki1stp0Ud OlgɫSN)7:׃f7uik*}ZΖL?#~]7GqlF=1{.Hayh̓6tyA1bwP~!Gc)Td9[d:j]%b 2j:csO$~w ˶+$Ŵ-ð-Pͥi1$+ji?5]jI[Z˾5 kbvZu\+YrNɁ5m-U|3ItMRjBS%O|G /QiFnlHy1Ƶ[bSaGa;έ'fVwΚȘ_"ʨQf)+H1n}0+UxlʟyOk Q g]͐~r,'~؆xTũUYx`ko#RǗFfݍ/I*x|BdHb!t?Rui3ߙ % h=yi579[ ;1fF;ꘛR5A/$2;u!Bijf@YB!Z tw_l`{?㛷L qDHF^[Ȃ/>ī.%sዅ%DH|%wd[18r7xEqϫiUfT-:{)LUt;sˤhL'םI},2oM]Fܳc͂@+k+!W5|6-͢J/g!(qӏYɢwb '")vST !\Ţ$C^47ڧ%p_%sbg߾GA!k`ƧQn|mJg=Z!8 p1(*P3PDqg9m$?8cWlS ^RR1Jꑵ/'eJPK>w=w\2=Fv`Ciť-!JS\;BG-#nϐ{q䑵k헆 ]elJ$(Š0㰔RyjϩBpؔQbR79v}ifZiזz6}#96T=%wt|.-_?p{mZOmY2Z!DSM! idQUT(+~He}g[vCtn?4t:C];2@ B~h:8 YWlL tnEEl^H~A!|ՎY[:rk^nMV8tG:ڌDvTz75?i xE'[p 2vo/DP{9k[AoXC-1~V QDӮ=tE?RO6_;lso=!M8j. s<БkRS~n`AG%} ۼ$Q3yOц]HGb1rl8*ϰ\;13ᩁ {_=@ev#H@+d8H&6SӚ.NG 1W+Ibɽ МYvrueCg&|Nܠ(p _~Do ӶY/UGuC.\=)=MC{gL|{!{=skK]WHfķcY@dmVLp| -*܄ pm3%]fMƸG>6wᄪmZE3@_ !фTٝӬG'Ř|K̛J)C$H9>Y)ziӯ s0ArR>EAVc0}׆G!JWPOdֺ=G$ӥJ+da\:$}e Г!.Ne|(quUGd?@E~r_‘}'/r6 ސ Yp^U] ;,&}k*"i;|~:ڂkjm(@ԝ1gk8nN>qSS_sj/PBq:B!.c.c!B B!BB!@xR0sG#pt%z\&\ !Cl/@_>uq.fZ /?[vW8uv;PO!V:‰Pͺ1i `r;' N `wyxdږBFi c0iC$h[ ъ:~8, pF D̝[P$@y/HNŒ-gWSQz+sU&y y5td>!:%bO7VL5-ƜV41`v&.Do9>yK1^Y%b,W]f52P*A2vzwZJê% \!-nȕɑpoW:tTYݬ|c9r|kPD5;i1]ԧi;eVݸT;Iӆ֔ǚ!ͩe]e˒-l^{M}Za _/4M#xzlW:YhP]AM=y8cca[_Q_#F8q3;: J`x#%A twa֞O2mo/b>?Q9!6(4vKt||'\?Kd1۹::^Ԡ_َP*B epn^k}I2ֿь4B فq  CE0cPT @%c;},ӊI7t暇;su>V-)G %T ?26TaqXJ)sN]m$?8c?TAMEkH `x8ZrR:X XU~5ONA 2**XB@~[E!‹JFQȺbc;OϋFZP>vv?)[a9 IDAT]7#x{vo^M=9~ϡ'7Rkok>Ɯuܸu~hd]W[ikר*}?~Hʳ|Ӏu%x!:)`:Q}#_ +yH:OYZK硭8L UG&6V"BσͯW %{199r;G6J+"ӓv#G3!H eekBۇdMy%:Kq ('Z0;8Jq1r#g"\g.\=)=\P@BGܥƾ,goškRS~Oц]HGb1rl8*Pc'm^aqTu֦ 1pWzb:6LϫcU5uW@J yIB&!@x!ϸv5lѲ«8̲v< 4OT{iρ ,ܙɽG0dQVRte`,I@UAVby*)ʼn'pù6)WZ9Q韯fOtљƒo{8jv' SNd1p|g0FONR.U꨽6t=$wHwkjmː>WQwݢ;޾9ANwǍO\_7ʸWu$9xFT!&>*sb&?[5semc)X_7ٹﶾ]k:3PY(Câ,x=zmc?Źm^4%u.By[zr3*껅2e.x5NS=c侷CD|_}?v-aS#3ݰg6Ha)'> B!BQ8Hտ溠[v0 ۸38U+!F%<_rgS}G㑖7@rZ!]kJ2+m q˃|/^Ov‚q_A dCS@x棿2kzWB gW= B! p:mcdo%ͤs-*NL$/>ZO/կp}\)wδ8<,Y^B_˷?g]ֱ1,Xڴe8M\[_ !"¦cfVpWXnn{A$P[=.-+v}ՕXY^o˟~U>1${|u)V.3žyk!BH!u^p3&:-lOm}A^'-CPٸBjogյ/];ZWl|oᩏaTSrWT9~^%~bU̹?B!BqJmՖ(+<\WG9fl_.9oHZ!@(B!YP!BB!@(.:gt3k+~vp2ȵB!$6)b :JIq[>Τ|9 5&?OkIB!P!BH  mYǪqBԧxwl,odt:D0ff޺. l=o_p&Z"LaC6d6qvw{\#s"B!$6]/0#ՙ)$;j`ҭBKOL~IoKx(t>l{$#<1g&6]qH wxZ؂tpF"puDGFva/ȭcAs{_ڲ}ᓯB( νu0)ի!|L@2o[Byq zךfî!!=>Y[_W7+mז<("C -Y(4Bvgi66>wٽ<Ϩ#;/i*i"BH ( m}4k)('.~w07cO]QTL"B!e|)X~ͽӯa ADFYO[~3+_ё()$OIn 9>R=ܪC)u1K!}:9YQPSz%?$|]B Mwl3e\ a*oY/SYW$YyS珿HV;뿑?O͑:֧ocGZNN|u;).kIXƆ|JkrS Ox簂JhDABqAɻB!.s2B(B!P!BH B!X!B H_T+!B!sV>ub l'QS_;WH*[6Fy][sP0^İ/aǡ2￈0Koo1UevbHC׈36|g<.pB!Dzr3-?^tۿX;gJ܊JRɃs# O{\XNrL(zdή/=Y<:ەxxJ"D&RUYVlvg0U#'aUepU ;8N/4ho4ޡP]Lrݟϯ.ߴBquo1X#j o`u5ΧLNIǦfV#8@O>tQǿ#M1zәqbǫ0b/ 'B!/RiEg㧓'h^3T? \(!SE\k [auzU>^@u l3'VI_7URdjkFӱQIvlz͸&_G!:GQ[Ph+'JrJEŢW`VI)^5?g֫f W+=Fm}%Ī*7YWu^u3$ZJ(Ԁ3NW2EIRŊx֭QcNDS0Ct/N/]Y BZ5 {fws5/adkkbC|΋H(޷I%.5dU|-*B CK9httg2Qw/T]L!4;_Z1{ }U>f dxpe) ~,5;NF6B9|ɑ֞` \pfNEǨ<Ǡ z>-BK:1Wγ̷* Čh2hϞ4&xhnnp[T4Q/g}azx23[dB!jwɍ*F8;ym}X@ԗy퉮-. T9v҈â=v 'KVK*EPޗ0(ByFB!Ĺ'e'tL=^7B!5e|(B!@x2%2%GB!$V3W`?&P ۹EGRٲ6rfLp;h ]FH'-4hlN|DU< {G "BP@7,+#B4% ^Gɲ3~˔?-;sv\Z " y'F t&aǸ(.yDI-'O7aU6)zqGuw׀gk_vֱDŽG/!1E)C,B]5)=1)x*6aPEBx <ңLw*(쁠p򿶠4)!i&kf(F(wV(~9VZyF? sCp Ebgd#7P*W"w?py*:>THHSSV ~AjQàPH'))S&3B!.@EJL)1BKxuG).g70.d~xr(R[T6>@8o;u=seF(x54[E7ko;BG( ˟m`_-R1)$* *T7m2KwgaBޫ!x}:EDtaBkUDI[^Eį jAp*s9*0(7s2J(B\^iEh8qDE"bV (^bD5Ս8 \D=T5αWuT?7#nΡFeq) ez5 U@Q U !Y+~GᡓCufdγi`ڍ~ =g9DS=f7VQv r/oB!.@x'fŠZ rtkcG2YԄ VWA:Wb p͍%+څK-`g ַu>>WЕ#tA!:瘞gW?o݉Ă]8Vx0Aj(JssnkaCj"bn/!<,µ*aP6کiY!d UG b]$#Ra٨XIR뀪;M0%PN8φ0-'(Avj21ɣyGIjB!jFz"?T?g~%rVm|_HJгB{ ;kM. nEWвg^\ ExQǟ`~ \Bc5g4L[7"TC(xV p>*̔;!p8~c6 jax _2B(X#L¬tSbEC"cB!DvЉխ|/t>u^p3&Da06a;Н䬭Y!Kq980^d3?:QA?9W5#w]B!)_/l$m\ӹ&⬺ DM}מтMM j \yI?fGw^b%nԂ_UDη#B!ęKiPsAFB B!2X!BB!@(.y/\ԈO2?O:aKvv{?nv?B!$6bZ3Wi6ٹ3)p^]++p7׷y\RD&/ft}Irjl }-ₑz_q^#|q}x%iPf+{MB\02Bxw̸ym:Vemeհ`/6aCNV-hC]E%F'6:#5Yۤffa6:w;6´ζ:_OY[=,_4W'nqucKWb?)EYMěW{m_i!aSG6bO2L]3es&|=3Svhk t _ϸ y,[| 0:P} k!ŒVgjut+/ВSF0<')a^8@ WGtodFL\]ըJgʌoWvɃߋ!cyjqa=^ߧsM> Ǭ/; &DHjuJ~7AũFɅ"qLh֞^W2ydyYiv^W=Cxl}_3V5xobNZ_N5ukۛ*P_}-) YsrkJg505:L$@lACeGe~g94V;ϫ92{._~-XvR ̇yCH [n]z]CB{|)ǡoW4_\3K]͒̋d@HCgB!$3([Ll/`esdZ?O`3S[y,;RJ 17LJ{w_sqqvv.m \*("JBߐD|T(B墔J%U es9\fW6s~vvjn>uv>s~6w'Yw ~k~V\IDATip^}'۾+#H1fѱioVZDDxzOc!i}8q!0Zx~E[8P?yG6dP^JW .8.r+`ɷܶ=8{Ƚq1`*Y{Ս^~` nẅ՗lѫzl 3?R/fi)Siuz><O`tr~{{00yy]{],3Q aHx H`J^ {/&1o${r/q;dx*""@X(ycͧVA8]8Sqm t `.$gʵlّ-'˗ȱIRY[py5rG6|r'8Mpx1\ rGlk~9R/:tBx4Y XNBnlϚy `3XfJ~:Mh˵iqn7b`K_S;|>V.C6l4k K]ze=lO?cx=4evR,45n;lIOtAEMLŧa֑lO;DtR>k d;~CK#] d?w&*0&A3en쮽9P»-u=LUL9VsYT>{ü٘Q Pl%Сz60n"Kf{3qbeVum#eD;{d"?thF y)ܰd¹'n܁-)A|t߇"sU0v( K?2|x3yZ3bOmve0 ,OdKc z)*_g`O˙Qf Cch1 vn B@DDDDx#ڃKm̛0Ū1߲-X|ǫ#--3l t/;L# ^ndS3@OgkD} jx\ O$z15+ShĿT92[R`j90R.1)x“3?֨@ud .[k/~"""w&]CXL.eոAl²Pk0pa֬J03^sw0d7ft`3l`Kn>6'#NC9Ɯ3+7"xS,ݦ1_;ԯ:ϘE ""@b h@) YȤm~)h6WQoznb|mN3/vCz-slHeGRSWtgX5f)VuVC|b!!l*˭Xr ._ysy\#*8[-u`h}k9ԭH:[P,cU[_PϽM ?bLZeSZcXtl펈IMqkMiof!~\FQPDDD.kEDDDEDDDDPDDDDEDDDDPDDDDEDDDDPDDDDEDDDDPDDDDEDDDDPDDDDEDDDDPDDDDEDDDDPDDDDEDDDDPDJ #fWADDEF>I]K"@b, ٶ5V "@e+c#{M'RЬ6[GcշcD=G˜9v NebӰO}ɱ?3⩑KTη^ctWM?Yvzc+k׍m5YˮhnZXU\]̌eCd CK&뷘_3cٕ:YDeƲ)}bKbXyˢsu:0>rkXG4ǃLFо+')Q,Ӆ &{R{cސwѐѧb ^S/ŝ$9⒮ F_99vdx)wgE[{s"" 7SKT LrժPz L%hT܃_jYj?eʼn%mFTޕ`[o="+)v,i`ˏ#l\M9oW*vI]' qV2\d=@Z|9v 3mX-ؾYx\ﻟr.flk_ ڐr.f*=$W0~l1yR< ӜE< ]ߵٲj?ott|N6]z1Mv̱ oPDDoZ>㝈|iyAONf<]! eՙG\ԭLx?6,=]#rGs>ǁJ|+1 _ =(O5V z{yΔ\O3=f'1j2ţ;q*_:8&'d>o< .AǦܚ*:O,DDXO m=aNm$GLgJ'l/fKpfE+8AXK5^ۥ?K4Hc؜%4 yI5*)wlTbϸw( =US1KR.gsLЧQ_-c|u"""" """"@("""" r;)/-??wGbNt 4j{DDDDPDDDDrΔkvm%n6E0QIw_4Q)k,a$Vfh"b&P׳ fʬڊGˈ4:MLm+ϯCY?6>Zu #RT??Y[p]:mVrvF\8PC1_k sVY"""@x03^s'q^2XHZN\+ ~F_+b'"""@XaHx H`J#]""""w9(ܵ]beS 7IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1603625840.7119823 git-crecord-20201025.0/setup.cfg0000664000000000000000000000173000000000000016072 0ustar00rootroot00000000000000[metadata] name = git-crecord version = 20201025.0 url = https://github.com/andrewshadura/git-crecord author = Andrej Shadura author-email = andrew@shadura.me description = interactively select chunks to commit with Git long_description = file: README.rst long_description_content_type = text/x-rst license = GPL-2+ license_file = COPYING classifier = Development Status :: 4 - Beta Environment :: Console :: Curses Intended Audience :: Developers License :: OSI Approved :: GNU General Public License (GPL) Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Topic :: Software Development :: Version Control [options] python_requires = >= 3.6 packages = find: setup_requires = docutils >= 0.12 include_package_data = True [options.entry_points] console_scripts = git-crecord = git_crecord.main:main [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603618799.0 git-crecord-20201025.0/setup.py0000755000000000000000000000370700000000000015772 0ustar00rootroot00000000000000#!/usr/bin/env python3 import os import fnmatch from distutils import log from setuptools import setup, find_packages def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() def glob(fname): return fnmatch.filter(os.listdir(os.path.abspath(os.path.dirname(__file__))), fname) def generate_manpage(src, dst): import docutils.core log.info("generating a manpage from %s to %s", src, dst) docutils.core.publish_file(source_path=src, destination_path=dst, writer_name='manpage') def man_name(fname): import re matches = re.compile(r'^:Manual section: *([0-9]*)', re.MULTILINE).search(read(fname)) if matches: section = matches.groups()[0] else: section = '7' base = os.path.splitext(fname)[0] manfname = base + '.' + section return manfname def man_path(fname): category = fname.rsplit('.', 1)[1] return os.path.join('share', 'man', 'man' + category), [fname] def man_files(pattern): return list(map(man_path, map(man_name, glob(pattern)))) # monkey patch setuptools to use distutils owner/group functionality from setuptools.command import sdist sdist_org = sdist.sdist class sdist_new(sdist_org): def initialize_options(self): sdist_org.initialize_options(self) self.owner = self.group = 'root' sdist.sdist = sdist_new __manpages__ = 'git-*.rst' from setuptools.command import build_py build_py_org = build_py.build_py class build_py_new(build_py_org): def run(self): build_py_org.run(self) if not self.dry_run: for page in glob(__manpages__): generate_manpage(page, man_name(page)) build_py.build_py = build_py_new __name__ = "git-crecord" setup( data_files = [ (os.path.join('share', 'doc', __name__), glob('*.rst')), (os.path.join('share', 'doc', __name__), glob('*.png')), (os.path.join('share', 'doc', __name__), ['CONTRIBUTORS', 'COPYING']) ] + man_files(__manpages__) )