From: Catalin Marinas Date: Sun, 10 Jul 2005 07:29:31 +0000 (+0100) Subject: Initial commit (Release 0.4) X-Git-Tag: v0.4 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/stgit/commitdiff_plain/41a6d8591d5962dbfe8e372fff10c60e06718083?ds=inline Initial commit (Release 0.4) --- 41a6d8591d5962dbfe8e372fff10c60e06718083 diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..098c1d3 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Catalin Marinas + http://www.procode.org/about.html diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d60c31a --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..30c4b93 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,153 @@ +2005-07-09 Catalin Marinas + + * Release 0.4 + +2005-07-09 Peter Osterlund + + * Fix spelling errors + +2005-07-08 Catalin Marinas + + * stgit/main.py (diff): Add '--stat' option to 'diff' + (files): 'files' command implemented + +2005-07-08 Peter Osterlund + + * stgit/git.py (diffstat): %(diffstat)s variable support in the + patch export template + +2005-07-07 Catalin Marinas + + * stgit/main.py (resolved): Implemented a 'resolved' command to + mark conflicts as solved. The 'status' command now shows the + conflicts as well. 'refresh' fails if there are conflicts + +2005-07-06 Catalin Marinas + + * stgit/stack.py (edit_file): Added support for patchdescr.tmpl + + * stgit/main.py (export): Added support for more variables in the + patchexport.tmpl file + + * stgit/stack.py (Patch): Add support for author/comitter default + details configuration + + * stgit/main.py (push): '--undo' option added to push. This option + allows one to undo the last push operation and restores the old + boundaries of the patch (prior to the push operation) + (pop): pop optimised to switch directly to the last patch to be + popped + +2005-07-05 Catalin Marinas + + * stgit/main.py (pop): add '--to' option to 'pop' + (push): add '--to' and '--reverse' options to 'push' + + * gitmergeonefile.py: Added support for 'keeporig' option which + selects whether to delete or not the original files after a failed + merge + +2005-07-04 Catalin Marinas + + * Add support for configurable merge tool via stgitrc + + * Add support for configuration file (/etc/stgitrc, ~/.stgitrc, + .git/stgitrc) + +2005-07-02 Catalin Marinas + + * stgit/main.py (export): Added support for the patch description + template. At the moment, only the '%(description)s' variable is + supported + +2005-07-01 Catalin Marinas + + * stgit/main.py (refresh): Now it also checks for head != top + (export): Add the patch description to the exported patch files + +2005-06-30 Catalin Marinas + + * Fix exception reporting when the .git/HEAD link is not valid + + * Empty patches are now marked + +2005-06-28 Catalin Marinas + + * Release 0.3 + + * stgit/stack.py (Series.push_patch): if the merge with the new + base failed, inform the user that "refresh" should be run after + fixing the conflicts + + * stgit/main.py (new): checks for local changes and head != top + added + + * StGIT is now closer to Quilt in functionality. The 'commit' + command was removed ('refresh' is used instead). + +2005-06-25 Catalin Marinas + + * stack.py modified to include all the series functions the Series + class + +2005-06-24 Catalin Marinas + + * stgit/git.py (commit): commit tells before invoking the editor + and shows the return error code but without exiting if it is + non-zero + +2005-06-23 Catalin Marinas + + * stgit/main.py (push): --number option added to push + (pop): --number option added to push + +2005-06-22 Catalin Marinas + + * gitmergeonefile.py: temporary files are placed in .local, + .older and .remote and only removed if the merge + succeeded + +2005-06-21 Catalin Marinas + + * stgit/main.py (delete): 'delete' now requires the explicit patch + name as a safety measure + + * stgit/stack.py (pop_patch): sys.stdout.flush() added after the + first print + (push_patch): fix bug with 'push' not warning for empty patches + + * stgit/stack.py (push_patch): sys.stdout.flush() added after the + first print + +2005-06-20 Catalin Marinas + + * Release 0.2 + + * stgit/stack.py (delete_patch): bug when deleting the topmost + patch fixed + + * top/bottom files are backed up to top.old/bottom.old + automatically. The 'diff' command supports them as well + + * stg-upgrade.sh: upgrades the .git structure from stgit-0.1 to + stgit-0.2 + +2005-06-19 Catalin Marinas + + * Multiple heads and bases are now supported. A different series + is available for each head + + * gitmergeonefile.py: fix bug caused by not updating the cache + when merging with diff3 + + * stgit/stack.py: 'push' command reports a warning if the patch is + empty + + * stgit/git.py: commit supports an 'allowempty' parameter + + * os.path.join() used instead '+' for concatenating path names + +2005-06-15 Catalin Marinas + + * Release 0.1 + diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..c018351 --- /dev/null +++ b/INSTALL @@ -0,0 +1,7 @@ +For basic installation: + + python setup.py install + +For more information: + + http://docs.python.org/inst/inst.html diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..587c4d7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README MANIFEST.in AUTHORS COPYING INSTALL ChangeLog TODO stgitrc +include examples/*.tmpl diff --git a/README b/README new file mode 100644 index 0000000..dfb6442 --- /dev/null +++ b/README @@ -0,0 +1,171 @@ +Stacked GIT +----------- + +StGIT is a Python application providing similar functionality to Quilt +(i.e. pushing/poping patches to a stack) on top of GIT. These +operations are performed using the GIT merge algorithms. + +Note that StGIT is not an SCM interface for GIT. Use the GIT commands +or some other tools like Cogito for this. + +For the latest version see http://www.procode.org/stgit/ + + +Basic Operations +---------------- + +For a full list of commands: + + stg help + +For help on individual commands: + + stg (-h | --help) + +To initialise a tree (the tree must have been previously initialised +with GIT): + + stg init + +To add/delete files: + + stg add [*] + stg rm [*] + +To inspect the tree status: + + stg status + +To get a diff between 2 revisions: + + stg diff [-r rev1[:[rev2]]] + +A revision name can be of the form '([patch]/[bottom | top]) | ' +If the patch name is not specified but '/' is passed, the topmost +patch is considered. If neither 'bottom' or 'top' follows the '/', the +whole patch diff is displayed (this does not include the local +changes). + +Note than when the first patch is pushed to the stack, the current +HEAD is saved in the .git/refs/heads/base file for easy reference. + +To create/delete a patch: + + stg new + stg delete [] + +The 'new' command also sets the topmost patch to the newly created +one. + +To push/pop a patch to/from the stack: + + stg push [] + stg pop [] + +Note that the 'push' command can apply any patch in the unapplied +list. This is useful if you want to reorder the patches. + +To add the patch changes to the tree: + + stg refresh + +To inspect the patches applied: + + stg series + stg applied + stg unapplied + stg top + +To export a patch series: + + stg export [] + +The 'export' command supports options to automatically number the +patches (-n) or add the '.diff' extension (-d). + +StGIT does not yet provide support for cloning or pulling changes from +a different repository. Until this becomes available, run the +following commands: + + stg pop -a + your-git-script-for-pulling-and-merging + stg push -a + +You can also look in the TODO file for what's planned to be +implemented in the future. + + +Directory Structure +------------------- + +.git/ + objects/ + ??/ + +refs/ + heads/ + master - the master commit id + ... + bases/ + master - the bottom id of the stack (to get a big diff) + ... + tags/ + ... + branches/ + ... + patches/ + master/ + applied - list of applied patches + unapplied - list of not-yet applied patches + current - name of the topmost patch + patch1/ + first - the initial id of the patch (used for log) + bottom - the bottom id of the patch + top - the top id of the patch + patch2/ + ... + ... + +HEAD -> refs/heads/ + + +A Bit of StGIT Patch Theory +--------------------------- + +We assume that a patch is a diff between two nodes - bottom and top. A +node is a commit SHA1 id or tree SHA1 id in the GIT terminology: + +P - patch +N - node + +P = diff(Nt, Nb) + + Nb - bottom (start) node + Nt - top (end) node + Nf - first node (for log generation) + +For an ordered stack of patches: + +P1 = diff(N1, N0) +P2 = diff(N2, N1) +... + +Ps = P1 + P2 + P3 + ... = diff(Nst, Nsb) + + Ps - the big patch of the whole stack + Nsb - bottom stack node (= N0) + Nst - top stack node (= Nn) + +Applying (pushing) a patch on the stack (Nst can differ from Nb) is +done by diff3 merging. The new patch becomes: + +P' = diff(Nt', Nb') +Nb' = Nst +Nt' = diff3(Nst, Nb, Nt) + +(note that the diff3 parameters order is: branch1, ancestor, branch2) + +The above operation allows easy patch re-ordering. + +Removing (popping) a patch from the stack is done by simply setting +the Nst to Nb. diff --git a/TODO b/TODO new file mode 100644 index 0000000..55841b1 --- /dev/null +++ b/TODO @@ -0,0 +1,27 @@ +The TODO list for the short term: + +- tag (snapshot) command +- pull command (no longer rely on cogito or plain git) +- log command (it should also show the log per single patch) +- import command to import a series of patches or a single patch or a + patch from a different branch in the same tree +- better help for commands +- bug reporting tool + + +Other things after the list above is completed: + +- fold command (to merge 2 patches into one) +- automatic e-mail sending with the patches +- release 1.0 + + +The future, when time allows or someone else does it: + +- patches command to show the patches modifying a file +- patch dependency tracking +- multiple heads in a patch - useful for forking a patch, + synchronising with other patches (diff format or in other + repositories) +- remove the old base of the patch if there are no references to it +- write bash-completion script for the StGIT commands diff --git a/examples/patchdescr.tmpl b/examples/patchdescr.tmpl new file mode 100644 index 0000000..6480a2a --- /dev/null +++ b/examples/patchdescr.tmpl @@ -0,0 +1,3 @@ + + +Signed-off-by: Your Name diff --git a/examples/patchexport.tmpl b/examples/patchexport.tmpl new file mode 100644 index 0000000..cbc938e --- /dev/null +++ b/examples/patchexport.tmpl @@ -0,0 +1,5 @@ +%(description)s +--- + +%(diffstat)s + diff --git a/gitmergeonefile.py b/gitmergeonefile.py new file mode 100755 index 0000000..5b588dd --- /dev/null +++ b/gitmergeonefile.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +"""Performs a 3-way merge for GIT files +""" + +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys, os +from stgit.config import config +from stgit.utils import append_string + + +# +# Options +# +try: + merger = config.get('gitmergeonefile', 'merger') +except Exception, err: + print >> sys.stderr, 'Configuration error: %s' % err + sys.exit(1) + +if config.has_option('gitmergeonefile', 'keeporig'): + keeporig = config.get('gitmergeonefile', 'keeporig') +else: + keeporig = 'yes' + + +# +# Global variables +# +if 'GIT_DIR' in os.environ: + base_dir = os.environ['GIT_DIR'] +else: + base_dir = '.git' + + +# +# Utility functions +# +def __str2none(x): + if x == '': + return None + else: + return x + +def __output(cmd): + f = os.popen(cmd, 'r') + string = f.readline().strip() + if f.close(): + print >> sys.stderr, 'Error: failed to execute "%s"' % cmd + sys.exit(1) + return string + +def __checkout_files(): + """Check out the files passed as arguments + """ + global orig, src1, src2 + + if orig_hash: + orig = '%s.older' % path + tmp = __output('git-unpack-file %s' % orig_hash) + os.chmod(tmp, int(orig_mode, 8)) + os.rename(tmp, orig) + if file1_hash: + src1 = '%s.local' % path + tmp = __output('git-unpack-file %s' % file1_hash) + os.chmod(tmp, int(file1_mode, 8)) + os.rename(tmp, src1) + if file2_hash: + src2 = '%s.remote' % path + tmp = __output('git-unpack-file %s' % file2_hash) + os.chmod(tmp, int(file2_mode, 8)) + os.rename(tmp, src2) + +def __remove_files(): + """Remove any temporary files + """ + if orig_hash: + os.remove(orig) + if file1_hash: + os.remove(src1) + if file2_hash: + os.remove(src2) + pass + +def __conflict(): + """Write the conflict file for the 'path' variable and exit + """ + append_string(os.path.join(base_dir, 'conflicts'), path) + sys.exit(1) + + +# $1 - original file SHA1 (or empty) +# $2 - file in branch1 SHA1 (or empty) +# $3 - file in branch2 SHA1 (or empty) +# $4 - pathname in repository +# $5 - orignal file mode (or empty) +# $6 - file in branch1 mode (or empty) +# $7 - file in branch2 mode (or empty) +#print 'gitmerge.py "%s" "%s" "%s" "%s" "%s" "%s" "%s"' % tuple(sys.argv[1:8]) +orig_hash, file1_hash, file2_hash, path, orig_mode, file1_mode, file2_mode = \ + [__str2none(x) for x in sys.argv[1:8]] + + +# +# Main algorithm +# +__checkout_files() + +# file exists in origin +if orig_hash: + # modified in both + if file1_hash and file2_hash: + # if modes are the same (git-read-tree probably dealed with it) + if file1_hash == file2_hash: + if os.system('git-update-cache --cacheinfo %s %s %s' + % (file1_mode, file1_hash, path)) != 0: + print >> sys.stderr, 'Error: git-update-cache failed' + __conflict() + if os.system('git-checkout-cache -u -f -- %s' % path): + print >> sys.stderr, 'Error: git-checkout-cache failed' + __conflict() + if file1_mode != file2_mode: + print >> sys.stderr, \ + 'Error: File added in both, permissions conflict' + __conflict() + # 3-way merge + else: + merge_ok = os.system(merger % {'branch1': src1, + 'ancestor': orig, + 'branch2': src2, + 'output': path }) == 0 + + if merge_ok: + os.system('git-update-cache %s' % path) + __remove_files() + sys.exit(0) + else: + print >> sys.stderr, \ + 'Error: three-way merge tool failed for file "%s"' % path + # reset the cache to the first branch + os.system('git-update-cache --cacheinfo %s %s %s' + % (file1_mode, file1_hash, path)) + if keeporig != 'yes': + __remove_files() + __conflict() + # file deleted in both or deleted in one and unchanged in the other + elif not (file1_hash or file2_hash) \ + or file1_hash == orig_hash or file2_hash == orig_hash: + if os.path.exists(path): + os.remove(path) + __remove_files() + sys.exit(os.system('git-update-cache --remove %s' % path)) +# file does not exist in origin +else: + # file added in both + if file1_hash and file2_hash: + # files are the same + if file1_hash == file2_hash: + if os.system('git-update-cache --add --cacheinfo %s %s %s' + % (file1_mode, file1_hash, path)) != 0: + print >> sys.stderr, 'Error: git-update-cache failed' + __conflict() + if os.system('git-checkout-cache -u -f -- %s' % path): + print >> sys.stderr, 'Error: git-checkout-cache failed' + __conflict() + if file1_mode != file2_mode: + print >> sys.stderr, \ + 'Error: File "s" added in both, permissions conflict' \ + % path + __conflict() + # files are different + else: + print >> sys.stderr, \ + 'Error: File "%s" added in branches but different' % path + __conflict() + # file added in one + elif file1_hash or file2_hash: + if file1_hash: + mode = file1_mode + obj = file1_hash + else: + mode = file2_mode + obj = file2_hash + if os.system('git-update-cache --add --cacheinfo %s %s %s' + % (mode, obj, path)) != 0: + print >> sys.stderr, 'Error: git-update-cache failed' + __conflict() + __remove_files() + sys.exit(os.system('git-checkout-cache -u -f -- %s' % path)) + +# Un-handled case +print >> sys.stderr, 'Error: Un-handled merge conflict' +print >> sys.stderr, 'gitmerge.py "%s" "%s" "%s" "%s" "%s" "%s" "%s"' \ + % tuple(sys.argv[1:8]) +__conflict() diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..48d3631 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +from distutils.core import setup + +from stgit.version import version + +setup(name = 'stgit', + version = version, + license = 'GPLv2', + author = 'Catalin Marinas', + author_email = 'catalin.marinas@gmail.org', + url = 'http://www.procode.org/stgit/', + description = 'Stacked GIT', + long_description = 'Push/pop utility on top of GIT', + scripts = ['stg', 'gitmergeonefile.py'], + packages = ['stgit'], + data_files = [('/etc', ['stgitrc'])], + ) diff --git a/stg b/stg new file mode 100755 index 0000000..ca9307e --- /dev/null +++ b/stg @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- python-mode -*- +"""Takes care of starting the Init function +""" + +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from stgit.main import main + +main() diff --git a/stgit/__init__.py b/stgit/__init__.py new file mode 100644 index 0000000..4b03e3a --- /dev/null +++ b/stgit/__init__.py @@ -0,0 +1,16 @@ +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" diff --git a/stgit/config.py b/stgit/config.py new file mode 100644 index 0000000..3bbbd0d --- /dev/null +++ b/stgit/config.py @@ -0,0 +1,33 @@ +"""Handles the Stacked GIT configuration files +""" + +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import os, ConfigParser + + +if 'GIT_DIR' in os.environ: + __git_dir = os.environ['GIT_DIR'] +else: + __git_dir = '.git' + +config = ConfigParser.RawConfigParser() + +config.readfp(file('/etc/stgitrc')) +config.read(os.path.expanduser('~/.stgitrc')) +config.read(os.path.join(__git_dir, 'stgitrc')) diff --git a/stgit/git.py b/stgit/git.py new file mode 100644 index 0000000..e97288a --- /dev/null +++ b/stgit/git.py @@ -0,0 +1,413 @@ +"""Python GIT interface +""" + +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys, os, glob + +from stgit.utils import * + +# git exception class +class GitException(Exception): + pass + + +# Different start-up variables read from the environment +if 'GIT_DIR' in os.environ: + base_dir = os.environ['GIT_DIR'] +else: + base_dir = '.git' + +head_link = os.path.join(base_dir, 'HEAD') + + +# +# Classes +# +class Commit: + """Handle the commit objects + """ + def __init__(self, id_hash): + self.__id_hash = id_hash + f = os.popen('git-cat-file commit %s' % id_hash, 'r') + + for line in f: + if line == '\n': + break + field = line.strip().split(' ', 1) + if field[0] == 'tree': + self.__tree = field[1] + elif field[0] == 'parent': + self.__parent = field[1] + if field[0] == 'author': + self.__author = field[1] + if field[0] == 'comitter': + self.__committer = field[1] + self.__log = f.read() + + if f.close(): + raise GitException, 'Unknown commit id' + + def get_id_hash(self): + return self.__id_hash + + def get_tree(self): + return self.__tree + + def get_parent(self): + return self.__parent + + def get_author(self): + return self.__author + + def get_committer(self): + return self.__committer + + +# +# Functions +# +def get_conflicts(): + """Return the list of file conflicts + """ + conflicts_file = os.path.join(base_dir, 'conflicts') + if os.path.isfile(conflicts_file): + f = file(conflicts_file) + names = [line.strip() for line in f.readlines()] + f.close() + return names + else: + return None + +def __output(cmd): + f = os.popen(cmd, 'r') + string = f.readline().strip() + if f.close(): + raise GitException, '%s failed' % cmd + return string + +def __check_base_dir(): + return os.path.isdir(base_dir) + +def __tree_status(files = [], tree_id = 'HEAD', unknown = False): + """Returns a list of pairs - [status, filename] + """ + os.system('git-update-cache --refresh > /dev/null') + + cache_files = [] + + # unknown files + if unknown: + exclude_file = os.path.join(base_dir, 'exclude') + extra_exclude = '' + if os.path.exists(exclude_file): + extra_exclude += ' --exclude-from=%s' % exclude_file + fout = os.popen('git-ls-files --others' + ' --exclude="*.[ao]" --exclude=".*"' + ' --exclude=TAGS --exclude=tags --exclude="*~"' + ' --exclude="#*"' + extra_exclude, 'r') + cache_files += [('?', line.strip()) for line in fout] + + # conflicted files + conflicts = get_conflicts() + if not conflicts: + conflicts = [] + cache_files += [('C', filename) for filename in conflicts] + + # the rest + files_str = reduce(lambda x, y: x + ' ' + y, files, '') + fout = os.popen('git-diff-cache -r %s %s' % (tree_id, files_str), 'r') + for line in fout: + fs = tuple(line.split()[4:]) + if fs[1] not in conflicts: + cache_files.append(fs) + if fout.close(): + raise GitException, 'git-diff-cache failed' + + return cache_files + +def local_changes(): + """Return true if there are local changes in the tree + """ + return len(__tree_status()) != 0 + +def get_head(): + """Returns a string representing the HEAD + """ + return read_string(head_link) + +def get_head_file(): + """Returns the name of the file pointed to by the HEAD link + """ + # valid link + if os.path.islink(head_link) and os.path.isfile(head_link): + return os.path.basename(os.readlink(head_link)) + else: + raise GitException, 'Invalid .git/HEAD link. Git tree not initialised?' + +def __set_head(val): + """Sets the HEAD value + """ + write_string(head_link, val) + +def add(names): + """Add the files or recursively add the directory contents + """ + # generate the file list + files = [] + for i in names: + if not os.path.exists(i): + raise GitException, 'Unknown file or directory: %s' % i + + if os.path.isdir(i): + # recursive search. We only add files + for root, dirs, local_files in os.walk(i): + for name in [os.path.join(root, f) for f in local_files]: + if os.path.isfile(name): + files.append(os.path.normpath(name)) + elif os.path.isfile(i): + files.append(os.path.normpath(i)) + else: + raise GitException, '%s is not a file or directory' % i + + for f in files: + print 'Adding file %s' % f + if os.system('git-update-cache --add -- %s' % f) != 0: + raise GitException, 'Unable to add %s' % f + +def rm(files, force = False): + """Remove a file from the repository + """ + if force: + git_opt = '--force-remove' + else: + git_opt = '--remove' + + for f in files: + if force: + print 'Removing file %s' % f + if os.system('git-update-cache --force-remove -- %s' % f) != 0: + raise GitException, 'Unable to remove %s' % f + elif os.path.exists(f): + raise GitException, '%s exists. Remove it first' %f + else: + print 'Removing file %s' % f + if os.system('git-update-cache --remove -- %s' % f) != 0: + raise GitException, 'Unable to remove %s' % f + +def commit(message, files = [], parents = [], allowempty = False, + author_name = None, author_email = None, author_date = None, + committer_name = None, committer_email = None): + """Commit the current tree to repository + """ + first = (parents == []) + + # Get the tree status + if not first: + cache_files = __tree_status(files) + + if not first and len(cache_files) == 0 and not allowempty: + raise GitException, 'No changes to commit' + + # check for unresolved conflicts + if not first and len(filter(lambda x: x[0] not in ['M', 'N', 'D'], + cache_files)) != 0: + raise GitException, 'Commit failed: unresolved conflicts' + + # get the commit message + f = file('.commitmsg', 'w+') + if message[-1] == '\n': + f.write(message) + else: + print >> f, message + f.close() + + # update the cache + if not first: + for f in cache_files: + if f[0] == 'N': + git_flag = '--add' + elif f[0] == 'D': + git_flag = '--force-remove' + else: + git_flag = '--' + + if os.system('git-update-cache %s %s' % (git_flag, f[1])) != 0: + raise GitException, 'Failed git-update-cache -- %s' % f[1] + + # write the index to repository + tree_id = __output('git-write-tree') + + # the commit + cmd = '' + if author_name: + cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name + if author_email: + cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email + if author_date: + cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date + if committer_name: + cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name + if committer_email: + cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email + cmd += 'git-commit-tree %s' % tree_id + + # get the parents + for p in parents: + cmd += ' -p %s' % p + + cmd += ' < .commitmsg' + + commit_id = __output(cmd) + __set_head(commit_id) + os.remove('.commitmsg') + + return commit_id + +def merge(base, head1, head2): + """Perform a 3-way merge between base, head1 and head2 into the + local tree + """ + if os.system('git-read-tree -u -m %s %s %s' % (base, head1, head2)) != 0: + raise GitException, 'git-read-tree failed (local changes maybe?)' + + # this can fail if there are conflicts + if os.system('git-merge-cache -o gitmergeonefile.py -a') != 0: + raise GitException, 'git-merge-cache failed (possible conflicts)' + + # this should not fail + if os.system('git-checkout-cache -f -a') != 0: + raise GitException, 'Failed git-checkout-cache' + +def status(files = [], modified = False, new = False, deleted = False, + conflict = False, unknown = False): + """Show the tree status + """ + cache_files = __tree_status(files, unknown = True) + all = not (modified or new or deleted or conflict or unknown) + + if not all: + filestat = [] + if modified: + filestat.append('M') + if new: + filestat.append('N') + if deleted: + filestat.append('D') + if conflict: + filestat.append('C') + if unknown: + filestat.append('?') + cache_files = filter(lambda x: x[0] in filestat, cache_files) + + for fs in cache_files: + if all: + print '%s %s' % (fs[0], fs[1]) + else: + print '%s' % fs[1] + +def diff(files = [], rev1 = 'HEAD', rev2 = None, output = None, + append = False): + """Show the diff between rev1 and rev2 + """ + files_str = reduce(lambda x, y: x + ' ' + y, files, '') + + extra_args = '' + if output: + if append: + extra_args += ' >> %s' % output + else: + extra_args += ' > %s' % output + + os.system('git-update-cache --refresh > /dev/null') + + if rev2: + if os.system('git-diff-tree -p %s %s %s %s' + % (rev1, rev2, files_str, extra_args)) != 0: + raise GitException, 'git-diff-tree failed' + else: + if os.system('git-diff-cache -p %s %s %s' + % (rev1, files_str, extra_args)) != 0: + raise GitException, 'git-diff-cache failed' + +def diffstat(files = [], rev1 = 'HEAD', rev2 = None): + """Return the diffstat between rev1 and rev2 + """ + files_str = reduce(lambda x, y: x + ' ' + y, files, '') + + os.system('git-update-cache --refresh > /dev/null') + ds_cmd = '| git-apply --stat' + + if rev2: + f = os.popen('git-diff-tree -p %s %s %s %s' + % (rev1, rev2, files_str, ds_cmd), 'r') + str = f.read().rstrip() + if f.close(): + raise GitException, 'git-diff-tree failed' + else: + f = os.popen('git-diff-cache -p %s %s %s' + % (rev1, files_str, ds_cmd), 'r') + str = f.read().rstrip() + if f.close(): + raise GitException, 'git-diff-cache failed' + + return str + +def files(rev1, rev2): + """Return the files modified between rev1 and rev2 + """ + os.system('git-update-cache --refresh > /dev/null') + + str = '' + f = os.popen('git-diff-tree -r %s %s' % (rev1, rev2), + 'r') + for line in f: + str += '%s %s\n' % tuple(line.split()[4:]) + if f.close(): + raise GitException, 'git-diff-tree failed' + + return str.rstrip() + +def checkout(files = [], force = False): + """Check out the given or all files + """ + git_flags = '' + if force: + git_flags += ' -f' + if len(files) == 0: + git_flags += ' -a' + else: + git_flags += reduce(lambda x, y: x + ' ' + y, files, ' --') + + if os.system('git-checkout-cache -q -u%s' % git_flags) != 0: + raise GitException, 'Failed git-checkout-cache -q -u%s' % git_flags + +def switch(tree_id): + """Switch the tree to the given id + """ + to_delete = filter(lambda x: x[0] == 'N', __tree_status(tree_id = tree_id)) + + if os.system('git-read-tree -m %s' % tree_id) != 0: + raise GitException, 'Failed git-read-tree -m %s' % tree_id + + checkout(force = True) + __set_head(tree_id) + + # checkout doesn't remove files + for fs in to_delete: + os.remove(fs[1]) diff --git a/stgit/main.py b/stgit/main.py new file mode 100644 index 0000000..dda6212 --- /dev/null +++ b/stgit/main.py @@ -0,0 +1,819 @@ +"""Basic quilt-like functionality +""" + +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys, os +from optparse import OptionParser, make_option + +from utils import * +from stgit import stack, git +from stgit.version import version +from stgit.config import config + + +# Main exception class +class MainException(Exception): + pass + + +# Utility functions +def __git_id(string): + """Return the GIT id + """ + if not string: + return None + + string_list = string.split('/') + + if len(string_list) == 1: + patch_name = None + git_id = string_list[0] + + if git_id == 'HEAD': + return git.get_head() + if git_id == 'base': + return read_string(crt_series.get_base_file()) + + for path in [os.path.join(git.base_dir, 'refs', 'heads'), + os.path.join(git.base_dir, 'refs', 'tags')]: + id_file = os.path.join(path, git_id) + if os.path.isfile(id_file): + return read_string(id_file) + elif len(string_list) == 2: + patch_name = string_list[0] + if patch_name == '': + patch_name = crt_series.get_current() + git_id = string_list[1] + + if not patch_name: + raise MainException, 'No patches applied' + elif not (patch_name in crt_series.get_applied() + + crt_series.get_unapplied()): + raise MainException, 'Unknown patch "%s"' % patch_name + + if git_id == 'bottom': + return crt_series.get_patch(patch_name).get_bottom() + if git_id == 'top': + return crt_series.get_patch(patch_name).get_top() + + raise MainException, 'Unknown id: %s' % string + +def __check_local_changes(): + if git.local_changes(): + raise MainException, \ + 'local changes in the tree. Use "refresh" to commit them' + +def __check_head_top_equal(): + if not crt_series.head_top_equal(): + raise MainException, \ + 'HEAD and top are not the same. You probably committed\n' \ + ' changes to the tree ouside of StGIT. If you know what you\n' \ + ' are doing, use the "refresh -f" command' + +def __check_conflicts(): + if os.path.exists(os.path.join(git.base_dir, 'conflicts')): + raise MainException, 'Unsolved conflicts. Please resolve them first' + +def __print_crt_patch(): + patch = crt_series.get_current() + if patch: + print 'Now at patch "%s"' % patch + else: + print 'No patches applied' + + +# +# Command functions +# +class Command: + """This class is used to store the command details + """ + def __init__(self, func, help, usage, option_list): + self.func = func + self.help = help + self.usage = usage + self.option_list = option_list + + +def init(parser, options, args): + """Performs the repository initialisation + """ + if len(args) != 0: + parser.error('incorrect number of arguments') + + crt_series.init() + +init_cmd = \ + Command(init, + 'initialise the tree for use with StGIT', + '%prog', + []) + + +def add(parser, options, args): + """Add files or directories to the repository + """ + if len(args) < 1: + parser.error('incorrect number of arguments') + + git.add(args) + +add_cmd = \ + Command(add, + 'add files or directories to the repository', + '%prog ', + []) + + +def rm(parser, options, args): + """Remove files from the repository + """ + if len(args) < 1: + parser.error('incorrect number of arguments') + + git.rm(args, options.force) + +rm_cmd = \ + Command(rm, + 'remove files from the repository', + '%prog [options] ', + [make_option('-f', '--force', + help = 'force removing even if the file exists', + action = 'store_true')]) + + +def status(parser, options, args): + """Show the tree status + """ + git.status(args, options.modified, options.new, options.deleted, + options.conflict, options.unknown) + +status_cmd = \ + Command(status, + 'show the tree status', + '%prog [options] []', + [make_option('-m', '--modified', + help = 'show modified files only', + action = 'store_true'), + make_option('-n', '--new', + help = 'show new files only', + action = 'store_true'), + make_option('-d', '--deleted', + help = 'show deleted files only', + action = 'store_true'), + make_option('-c', '--conflict', + help = 'show conflict files only', + action = 'store_true'), + make_option('-u', '--unknown', + help = 'show unknown files only', + action = 'store_true')]) + + +def diff(parser, options, args): + """Show the tree diff + """ + if options.revs: + rev_list = options.revs.split(':') + rev_list_len = len(rev_list) + if rev_list_len == 1: + if rev_list[0][-1] == '/': + # the whole patch + rev1 = rev_list[0] + 'bottom' + rev2 = rev_list[0] + 'top' + else: + rev1 = rev_list[0] + rev2 = None + elif rev_list_len == 2: + rev1 = rev_list[0] + rev2 = rev_list[1] + if rev2 == '': + rev2 = 'HEAD' + else: + parser.error('incorrect parameters to -r') + else: + rev1 = 'HEAD' + rev2 = None + + if options.stat: + print git.diffstat(args, __git_id(rev1), __git_id(rev2)) + else: + git.diff(args, __git_id(rev1), __git_id(rev2)) + +diff_cmd = \ + Command(diff, + 'show the tree diff', + '%prog [options] []\n\n' + 'The revision format is "([patch]/[bottom | top]) | "', + [make_option('-r', metavar = 'rev1[:[rev2]]', dest = 'revs', + help = 'show the diff between revisions'), + make_option('-s', '--stat', + help = 'show the stat instead of the diff', + action = 'store_true')]) + + +def files(parser, options, args): + """Show the files modified by a patch (or the current patch) + """ + if len(args) == 0: + patch = '' + elif len(args) == 1: + patch = args[0] + else: + parser.error('incorrect number of arguments') + + rev1 = __git_id('%s/bottom' % patch) + rev2 = __git_id('%s/top' % patch) + + if options.stat: + print git.diffstat(rev1 = rev1, rev2 = rev2) + else: + print git.files(rev1, rev2) + +files_cmd = \ + Command(files, + 'show the files modified by a patch (or the current patch)', + '%prog [options] []', + [make_option('-s', '--stat', + help = 'show the diff stat', + action = 'store_true')]) + + +def refresh(parser, options, args): + if len(args) != 0: + parser.error('incorrect number of arguments') + + if config.has_option('stgit', 'autoresolved'): + autoresolved = config.get('stgit', 'autoresolved') + else: + autoresolved = 'no' + + if autoresolved != 'yes': + __check_conflicts() + + patch = crt_series.get_current() + if not patch: + raise MainException, 'No patches applied' + + if not options.force: + __check_head_top_equal() + + if git.local_changes() \ + or not crt_series.head_top_equal() \ + or options.edit or options.message \ + or options.authname or options.authemail or options.authdate \ + or options.commname or options.commemail: + print 'Refreshing patch "%s"...' % patch, + sys.stdout.flush() + + if autoresolved == 'yes': + __resolved_all() + crt_series.refresh_patch(message = options.message, + edit = options.edit, + author_name = options.authname, + author_email = options.authemail, + author_date = options.authdate, + committer_name = options.commname, + committer_email = options.commemail) + + print 'done' + else: + print 'Patch "%s" is already up to date' % patch + +refresh_cmd = \ + Command(refresh, + 'generate a new commit for the current patch', + '%prog [options]', + [make_option('-f', '--force', + help = 'force the refresh even if HEAD and '\ + 'top differ', + action = 'store_true'), + make_option('-e', '--edit', + help = 'invoke an editor for the patch '\ + 'description', + action = 'store_true'), + make_option('-m', '--message', + help = 'use MESSAGE as the patch ' \ + 'description'), + make_option('--authname', + help = 'use AUTHNAME as the author name'), + make_option('--authemail', + help = 'use AUTHEMAIL as the author e-mail'), + make_option('--authdate', + help = 'use AUTHDATE as the author date'), + make_option('--commname', + help = 'use COMMNAME as the committer name'), + make_option('--commemail', + help = 'use COMMEMAIL as the committer ' \ + 'e-mail')]) + + +def new(parser, options, args): + """Creates a new patch + """ + if len(args) != 1: + parser.error('incorrect number of arguments') + + __check_local_changes() + __check_conflicts() + __check_head_top_equal() + + crt_series.new_patch(args[0], message = options.message, + author_name = options.authname, + author_email = options.authemail, + author_date = options.authdate, + committer_name = options.commname, + committer_email = options.commemail) + +new_cmd = \ + Command(new, + 'create a new patch and make it the topmost one', + '%prog [options] ', + [make_option('-m', '--message', + help = 'use MESSAGE as the patch description'), + make_option('--authname', + help = 'use AUTHNAME as the author name'), + make_option('--authemail', + help = 'use AUTHEMAIL as the author e-mail'), + make_option('--authdate', + help = 'use AUTHDATE as the author date'), + make_option('--commname', + help = 'use COMMNAME as the committer name'), + make_option('--commemail', + help = 'use COMMEMAIL as the committer e-mail')]) + +def delete(parser, options, args): + """Deletes a patch + """ + if len(args) != 1: + parser.error('incorrect number of arguments') + + __check_local_changes() + __check_conflicts() + __check_head_top_equal() + + crt_series.delete_patch(args[0]) + print 'Patch "%s" successfully deleted' % args[0] + __print_crt_patch() + +delete_cmd = \ + Command(delete, + 'remove the topmost or any unapplied patch', + '%prog ', + []) + + +def push(parser, options, args): + """Pushes the given patch or all onto the series + """ + # If --undo is passed, do the work and exit + if options.undo: + patch = crt_series.get_current() + if not patch: + raise MainException, 'No patch to undo' + + print 'Undoing the "%s" push...' % patch, + sys.stdout.flush() + __resolved_all() + crt_series.undo_push() + print 'done' + __print_crt_patch() + + return + + __check_local_changes() + __check_conflicts() + __check_head_top_equal() + + unapplied = crt_series.get_unapplied() + if not unapplied: + raise MainException, 'No more patches to push' + + if options.to: + boundaries = options.to.split(':') + if len(boundaries) == 1: + if boundaries[0] not in unapplied: + raise MainException, 'Patch "%s" not unapplied' % boundaries[0] + patches = unapplied[:unapplied.index(boundaries[0])+1] + elif len(boundaries) == 2: + if boundaries[0] not in unapplied: + raise MainException, 'Patch "%s" not unapplied' % boundaries[0] + if boundaries[1] not in unapplied: + raise MainException, 'Patch "%s" not unapplied' % boundaries[1] + lb = unapplied.index(boundaries[0]) + hb = unapplied.index(boundaries[1]) + if lb > hb: + raise MainException, 'Patch "%s" after "%s"' \ + % (boundaries[0], boundaries[1]) + patches = unapplied[lb:hb+1] + else: + raise MainException, 'incorrect parameters to "--to"' + elif options.number: + patches = unapplied[:options.number] + elif options.all: + patches = unapplied + elif len(args) == 0: + patches = [unapplied[0]] + elif len(args) == 1: + patches = [args[0]] + else: + parser.error('incorrect number of arguments') + + if patches == []: + raise MainException, 'No patches to push' + + if options.reverse: + patches.reverse() + + for p in patches: + print 'Pushing patch "%s"...' % p, + sys.stdout.flush() + + crt_series.push_patch(p) + + if crt_series.empty_patch(p): + print 'done (empty patch)' + else: + print 'done' + __print_crt_patch() + +push_cmd = \ + Command(push, + 'push a patch on top of the series', + '%prog [options] []', + [make_option('-a', '--all', + help = 'push all the unapplied patches', + action = 'store_true'), + make_option('-n', '--number', type = 'int', + help = 'push the specified number of patches'), + make_option('-t', '--to', metavar = 'PATCH1[:PATCH2]', + help = 'push all patches to PATCH1 or between ' + 'PATCH1 and PATCH2'), + make_option('--reverse', + help = 'push the patches in reverse order', + action = 'store_true'), + make_option('--undo', + help = 'undo the last push operation', + action = 'store_true')]) + + +def pop(parser, options, args): + if len(args) != 0: + parser.error('incorrect number of arguments') + + __check_local_changes() + __check_conflicts() + __check_head_top_equal() + + applied = crt_series.get_applied() + if not applied: + raise MainException, 'No patches applied' + applied.reverse() + + if options.to: + if options.to not in applied: + raise MainException, 'Patch "%s" not applied' % options.to + patches = applied[:applied.index(options.to)] + elif options.number: + patches = applied[:options.number] + elif options.all: + patches = applied + else: + patches = [applied[0]] + + if patches == []: + raise MainException, 'No patches to pop' + + # pop everything to the given patch + p = patches[-1] + if len(patches) == 1: + print 'Popping patch "%s"...' % p, + else: + print 'Popping "%s" - "%s" patches...' % (patches[0], p), + sys.stdout.flush() + + crt_series.pop_patch(p) + + print 'done' + __print_crt_patch() + +pop_cmd = \ + Command(pop, + 'pop the top of the series', + '%prog [options]', + [make_option('-a', '--all', + help = 'pop all the applied patches', + action = 'store_true'), + make_option('-n', '--number', type = 'int', + help = 'pop the specified number of patches'), + make_option('-t', '--to', metavar = 'PATCH', + help = 'pop all patches up to PATCH')]) + + +def __resolved(filename): + for ext in ['.local', '.older', '.remote']: + fn = filename + ext + if os.path.isfile(fn): + os.remove(fn) + +def __resolved_all(): + conflicts = git.get_conflicts() + if conflicts: + for filename in conflicts: + __resolved(filename) + os.remove(os.path.join(git.base_dir, 'conflicts')) + +def resolved(parser, options, args): + if options.all: + __resolved_all() + return + + if len(args) == 0: + parser.error('incorrect number of arguments') + + conflicts = git.get_conflicts() + if not conflicts: + raise MainException, 'No more conflicts' + # check for arguments validity + for filename in args: + if not filename in conflicts: + raise MainException, 'No conflicts for "%s"' % filename + # resolved + for filename in args: + __resolved(filename) + del conflicts[conflicts.index(filename)] + + # save or remove the conflicts file + if conflicts == []: + os.remove(os.path.join(git.base_dir, 'conflicts')) + else: + f = file(os.path.join(git.base_dir, 'conflicts'), 'w+') + f.writelines([line + '\n' for line in conflicts]) + f.close() + +resolved_cmd = \ + Command(resolved, + 'mark a file conflict as solved', + '%prog [options] [[ ]]', + [make_option('-a', '--all', + help = 'mark all conflicts as solved', + action = 'store_true')]) + + +def series(parser, options, args): + if len(args) != 0: + parser.error('incorrect number of arguments') + + applied = crt_series.get_applied() + if len(applied) > 0: + for p in applied [0:-1]: + if crt_series.empty_patch(p): + print '0', p + else: + print '+', p + p = applied[-1] + + if crt_series.empty_patch(p): + print '0>%s' % p + else: + print '> %s' % p + + for p in crt_series.get_unapplied(): + if crt_series.empty_patch(p): + print '0', p + else: + print '-', p + +series_cmd = \ + Command(series, + 'print the patch series', + '%prog', + []) + + +def applied(parser, options, args): + if len(args) != 0: + parser.error('incorrect number of arguments') + + for p in crt_series.get_applied(): + print p + +applied_cmd = \ + Command(applied, + 'print the applied patches', + '%prog', + []) + + +def unapplied(parser, options, args): + if len(args) != 0: + parser.error('incorrect number of arguments') + + for p in crt_series.get_unapplied(): + print p + +unapplied_cmd = \ + Command(unapplied, + 'print the unapplied patches', + '%prog', + []) + + +def top(parser, options, args): + if len(args) != 0: + parser.error('incorrect number of arguments') + + name = crt_series.get_current() + if name: + print name + else: + raise MainException, 'No patches applied' + +top_cmd = \ + Command(top, + 'print the name of the top patch', + '%prog', + []) + + +def export(parser, options, args): + if len(args) == 0: + dirname = 'patches' + elif len(args) == 1: + dirname = args[0] + else: + parser.error('incorrect number of arguments') + + if git.local_changes(): + print 'Warning: local changes in the tree. ' \ + 'You might want to commit them first' + + if not os.path.isdir(dirname): + os.makedirs(dirname) + series = file(os.path.join(dirname, 'series'), 'w+') + + patches = crt_series.get_applied() + num = len(patches) + zpadding = len(str(num)) + if zpadding < 2: + zpadding = 2 + + patch_no = 1; + for p in patches: + pname = p + if options.diff: + pname = '%s.diff' % pname + if options.numbered: + pname = '%s-%s' % (str(patch_no).zfill(zpadding), pname) + pfile = os.path.join(dirname, pname) + print >> series, pname + + # get the template + patch_tmpl = os.path.join(git.base_dir, 'patchexport.tmpl') + if os.path.isfile(patch_tmpl): + tmpl = file(patch_tmpl).read() + else: + tmpl = '' + + # get the patch description + patch = crt_series.get_patch(p) + + tmpl_dict = {'description': patch.get_description().rstrip(), + 'diffstat': git.diffstat(rev1 = __git_id('%s/bottom' % p), + rev2 = __git_id('%s/top' % p)), + 'authname': patch.get_authname(), + 'authemail': patch.get_authemail(), + 'authdate': patch.get_authdate(), + 'commname': patch.get_commname(), + 'commemail': patch.get_commemail()} + for key in tmpl_dict: + if not tmpl_dict[key]: + tmpl_dict[key] = '' + + try: + descr = tmpl % tmpl_dict + except KeyError, err: + raise MainException, 'Unknown patch template variable: %s' \ + % err + except TypeError: + raise MainException, 'Only "%(name)s" variables are ' \ + 'supported in the patch template' + f = open(pfile, 'w+') + f.write(descr) + f.close() + + # write the diff + git.diff(rev1 = __git_id('%s/bottom' % p), + rev2 = __git_id('%s/top' % p), + output = pfile, append = True) + patch_no += 1 + + series.close() + +export_cmd = \ + Command(export, + 'exports a series of patches to (or patches)', + '%prog [options] []', + [make_option('-n', '--numbered', + help = 'number the patch names', + action = 'store_true'), + make_option('-d', '--diff', + help = 'append .diff to the patch names', + action = 'store_true')]) + +# +# The commands map +# +commands = { + 'init': init_cmd, + 'add': add_cmd, + 'rm': rm_cmd, + 'status': status_cmd, + 'diff': diff_cmd, + 'files': files_cmd, + 'new': new_cmd, + 'delete': delete_cmd, + 'push': push_cmd, + 'pop': pop_cmd, + 'resolved': resolved_cmd, + 'series': series_cmd, + 'applied': applied_cmd, + 'unapplied':unapplied_cmd, + 'top': top_cmd, + 'refresh': refresh_cmd, + 'export': export_cmd, + } + +def print_help(): + print 'usage: %s [options]' % os.path.basename(sys.argv[0]) + print + print 'commands:' + print ' help print this message' + + cmds = commands.keys() + cmds.sort() + for cmd in cmds: + print ' ' + cmd + ' ' * (12 - len(cmd)) + commands[cmd].help + +# +# The main function (command dispatcher) +# +def main(): + """The main function + """ + global crt_series + + prog = os.path.basename(sys.argv[0]) + + if len(sys.argv) < 2: + print >> sys.stderr, 'Unknown command' + print >> sys.stderr, \ + ' Try "%s help" for a list of supported commands' % prog + sys.exit(1) + + cmd = sys.argv[1] + + if cmd in ['-h', '--help', 'help']: + print_help() + sys.exit(0) + if cmd in ['-v', '--version']: + print '%s %s' % (prog, version) + sys.exit(0) + if not cmd in commands: + print >> sys.stderr, 'Unknown command: %s' % cmd + print >> sys.stderr, ' Try "%s help" for a list of supported commands' \ + % prog + sys.exit(1) + + # re-build the command line arguments + sys.argv[0] += ' %s' % cmd + del(sys.argv[1]) + + command = commands[cmd] + parser = OptionParser(usage = command.usage, + option_list = command.option_list) + options, args = parser.parse_args() + try: + crt_series = stack.Series() + command.func(parser, options, args) + except (IOError, MainException, stack.StackException, git.GitException), \ + err: + print >> sys.stderr, '%s %s: %s' % (prog, cmd, err) + sys.exit(2) + + sys.exit(0) diff --git a/stgit/stack.py b/stgit/stack.py new file mode 100644 index 0000000..851f998 --- /dev/null +++ b/stgit/stack.py @@ -0,0 +1,535 @@ +"""Basic quilt-like functionality +""" + +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys, os + +from stgit.utils import * +from stgit import git +from stgit.config import config + + +# stack exception class +class StackException(Exception): + pass + +# +# Functions +# +__comment_prefix = 'STG:' + +def __clean_comments(f): + """Removes lines marked for status in a commit file + """ + f.seek(0) + + # remove status-prefixed lines + lines = filter(lambda x: x[0:len(__comment_prefix)] != __comment_prefix, + f.readlines()) + # remove empty lines at the end + while len(lines) != 0 and lines[-1] == '\n': + del lines[-1] + + f.seek(0); f.truncate() + f.writelines(lines) + +def edit_file(string, comment): + fname = '.stgit.msg' + tmpl = os.path.join(git.base_dir, 'patchdescr.tmpl') + + f = file(fname, 'w+') + if string: + print >> f, string + elif os.path.isfile(tmpl): + print >> f, file(tmpl).read().rstrip() + else: + print >> f + print >> f, __comment_prefix, comment + print >> f, __comment_prefix, \ + 'Lines prefixed with "%s" will be automatically removed.' \ + % __comment_prefix + print >> f, __comment_prefix, \ + 'Trailing empty lines will be automatically removed.' + f.close() + + # the editor + if 'EDITOR' in os.environ: + editor = os.environ['EDITOR'] + else: + editor = 'vi' + editor += ' %s' % fname + + print 'Invoking the editor: "%s"...' % editor, + sys.stdout.flush() + print 'done (exit code: %d)' % os.system(editor) + + f = file(fname, 'r+') + + __clean_comments(f) + f.seek(0) + string = f.read() + + f.close() + os.remove(fname) + + return string + +# +# Classes +# + +class Patch: + """Basic patch implementation + """ + def __init__(self, name, patch_dir): + self.__patch_dir = patch_dir + self.__name = name + self.__dir = os.path.join(self.__patch_dir, self.__name) + + def create(self): + os.mkdir(self.__dir) + create_empty_file(os.path.join(self.__dir, 'bottom')) + create_empty_file(os.path.join(self.__dir, 'top')) + + def delete(self): + for f in os.listdir(self.__dir): + os.remove(os.path.join(self.__dir, f)) + os.rmdir(self.__dir) + + def get_name(self): + return self.__name + + def __get_field(self, name, multiline = False): + id_file = os.path.join(self.__dir, name) + if os.path.isfile(id_file): + string = read_string(id_file, multiline) + if string == '': + return None + else: + return string + else: + return None + + def __set_field(self, name, string, multiline = False): + fname = os.path.join(self.__dir, name) + if string and string != '': + write_string(fname, string, multiline) + elif os.path.isfile(fname): + os.remove(fname) + + def get_bottom(self): + return self.__get_field('bottom') + + def set_bottom(self, string, backup = False): + if backup: + self.__set_field('bottom.old', self.__get_field('bottom')) + self.__set_field('bottom', string) + + def get_top(self): + return self.__get_field('top') + + def set_top(self, string, backup = False): + if backup: + self.__set_field('top.old', self.__get_field('top')) + self.__set_field('top', string) + + def restore_old_boundaries(self): + bottom = self.__get_field('bottom.old') + top = self.__get_field('top.old') + + if top and bottom: + self.__set_field('bottom', bottom) + self.__set_field('top', top) + else: + raise StackException, 'No patch undo information' + + def get_description(self): + return self.__get_field('description', True) + + def set_description(self, string): + self.__set_field('description', string, True) + + def get_authname(self): + return self.__get_field('authname') + + def set_authname(self, string): + if not string and config.has_option('stgit', 'authname'): + string = config.get('stgit', 'authname') + self.__set_field('authname', string) + + def get_authemail(self): + return self.__get_field('authemail') + + def set_authemail(self, string): + if not string and config.has_option('stgit', 'authemail'): + string = config.get('stgit', 'authemail') + self.__set_field('authemail', string) + + def get_authdate(self): + return self.__get_field('authdate') + + def set_authdate(self, string): + self.__set_field('authdate', string) + + def get_commname(self): + return self.__get_field('commname') + + def set_commname(self, string): + if not string and config.has_option('stgit', 'commname'): + string = config.get('stgit', 'commname') + self.__set_field('commname', string) + + def get_commemail(self): + return self.__get_field('commemail') + + def set_commemail(self, string): + if not string and config.has_option('stgit', 'commemail'): + string = config.get('stgit', 'commemail') + self.__set_field('commemail', string) + + +class Series: + """Class including the operations on series + """ + def __init__(self, name = None): + """Takes a series name as the parameter. A valid .git/patches/name + directory should exist + """ + if name: + self.__name = name + else: + self.__name = git.get_head_file() + + if self.__name: + self.__patch_dir = os.path.join(git.base_dir, 'patches', + self.__name) + self.__base_file = os.path.join(git.base_dir, 'refs', 'bases', + self.__name) + self.__applied_file = os.path.join(self.__patch_dir, 'applied') + self.__unapplied_file = os.path.join(self.__patch_dir, 'unapplied') + self.__current_file = os.path.join(self.__patch_dir, 'current') + + def __set_current(self, name): + """Sets the topmost patch + """ + if name: + write_string(self.__current_file, name) + else: + create_empty_file(self.__current_file) + + def get_patch(self, name): + """Return a Patch object for the given name + """ + return Patch(name, self.__patch_dir) + + def get_current(self): + """Return a Patch object representing the topmost patch + """ + if os.path.isfile(self.__current_file): + name = read_string(self.__current_file) + else: + return None + if name == '': + return None + else: + return name + + def get_applied(self): + f = file(self.__applied_file) + names = [line.strip() for line in f.readlines()] + f.close() + return names + + def get_unapplied(self): + f = file(self.__unapplied_file) + names = [line.strip() for line in f.readlines()] + f.close() + return names + + def get_base_file(self): + return self.__base_file + + def __patch_is_current(self, patch): + return patch.get_name() == read_string(self.__current_file) + + def __patch_applied(self, name): + """Return true if the patch exists in the applied list + """ + return name in self.get_applied() + + def __patch_unapplied(self, name): + """Return true if the patch exists in the unapplied list + """ + return name in self.get_unapplied() + + def __begin_stack_check(self): + """Save the current HEAD into .git/refs/heads/base if the stack + is empty + """ + if len(self.get_applied()) == 0: + head = git.get_head() + if os.path.exists(self.__base_file): + raise StackException, 'stack empty but the base file exists' + write_string(self.__base_file, head) + + def __end_stack_check(self): + """Remove .git/refs/heads/base if the stack is empty + """ + if len(self.get_applied()) == 0: + if not os.path.exists(self.__base_file): + print 'Warning: stack empty but the base file is missing' + else: + os.remove(self.__base_file) + + def head_top_equal(self): + """Return true if the head and the top are the same + """ + crt = self.get_current() + if not crt: + # we don't care, no patches applied + return True + return git.get_head() == Patch(crt, self.__patch_dir).get_top() + + def init(self): + """Initialises the stgit series + """ + bases_dir = os.path.join(git.base_dir, 'refs', 'bases') + + if os.path.isdir(self.__patch_dir): + raise StackException, self.__patch_dir + ' already exists' + os.makedirs(self.__patch_dir) + + if not os.path.isdir(bases_dir): + os.makedirs(bases_dir) + + create_empty_file(self.__applied_file) + create_empty_file(self.__unapplied_file) + + def refresh_patch(self, message = None, edit = False, + author_name = None, author_email = None, + author_date = None, + committer_name = None, committer_email = None): + """Generates a new commit for the given patch + """ + name = self.get_current() + if not name: + raise StackException, 'No patches applied' + + patch = Patch(name, self.__patch_dir) + + descr = patch.get_description() + if not (message or descr): + edit = True + descr = '' + elif message: + descr = message + + if not message and edit: + descr = edit_file(descr.rstrip(), \ + 'Please edit the description for patch "%s" ' \ + 'above.' % name) + + if not author_name: + author_name = patch.get_authname() + if not author_email: + author_email = patch.get_authemail() + if not author_date: + author_date = patch.get_authdate() + if not committer_name: + committer_name = patch.get_commname() + if not committer_email: + committer_email = patch.get_commemail() + + commit_id = git.commit(message = descr, parents = [patch.get_bottom()], + allowempty = True, + author_name = author_name, + author_email = author_email, + author_date = author_date, + committer_name = committer_name, + committer_email = committer_email) + + patch.set_top(commit_id) + patch.set_description(descr) + patch.set_authname(author_name) + patch.set_authemail(author_email) + patch.set_authdate(author_date) + patch.set_commname(committer_name) + patch.set_commemail(committer_email) + + def new_patch(self, name, message = None, edit = False, + author_name = None, author_email = None, author_date = None, + committer_name = None, committer_email = None): + """Creates a new patch + """ + if self.__patch_applied(name) or self.__patch_unapplied(name): + raise StackException, 'Patch "%s" already exists' % name + + if not message: + descr = edit_file(None, \ + 'Please enter the description for patch "%s" ' \ + 'above.' % name) + + head = git.get_head() + + self.__begin_stack_check() + + patch = Patch(name, self.__patch_dir) + patch.create() + patch.set_bottom(head) + patch.set_top(head) + patch.set_description(descr) + patch.set_authname(author_name) + patch.set_authemail(author_email) + patch.set_authdate(author_date) + patch.set_commname(committer_name) + patch.set_commemail(committer_email) + + append_string(self.__applied_file, patch.get_name()) + self.__set_current(name) + + def delete_patch(self, name): + """Deletes a patch + """ + patch = Patch(name, self.__patch_dir) + + if self.__patch_is_current(patch): + self.pop_patch(name) + elif self.__patch_applied(name): + raise StackException, 'Cannot remove an applied patch, "%s", ' \ + 'which is not current' % name + elif not name in self.get_unapplied(): + raise StackException, 'Unknown patch "%s"' % name + + patch.delete() + + unapplied = self.get_unapplied() + unapplied.remove(name) + f = file(self.__unapplied_file, 'w+') + f.writelines([line + '\n' for line in unapplied]) + f.close() + + def push_patch(self, name): + """Pushes a patch on the stack + """ + unapplied = self.get_unapplied() + assert(name in unapplied) + + self.__begin_stack_check() + + patch = Patch(name, self.__patch_dir) + + head = git.get_head() + bottom = patch.get_bottom() + top = patch.get_top() + + ex = None + + # top != bottom always since we have a commit for each patch + if head == bottom: + # reset the backup information + patch.set_bottom(bottom, backup = True) + patch.set_top(top, backup = True) + + git.switch(top) + else: + # new patch needs to be refreshed. + # The current patch is empty after merge. + patch.set_bottom(head, backup = True) + patch.set_top(head, backup = True) + # merge/refresh can fail but the patch needs to be pushed + try: + git.merge(bottom, head, top) + except git.GitException, ex: + print >> sys.stderr, \ + 'The merge failed during "push". ' \ + 'Use "refresh" after fixing the conflicts' + pass + + append_string(self.__applied_file, name) + + unapplied.remove(name) + f = file(self.__unapplied_file, 'w+') + f.writelines([line + '\n' for line in unapplied]) + f.close() + + self.__set_current(name) + + if not ex: + # if the merge was OK and no conflicts, just refresh the patch + self.refresh_patch() + else: + raise StackException, str(ex) + + def undo_push(self): + name = self.get_current() + assert(name) + + patch = Patch(name, self.__patch_dir) + self.pop_patch(name) + patch.restore_old_boundaries() + + def pop_patch(self, name): + """Pops the top patch from the stack + """ + applied = self.get_applied() + applied.reverse() + assert(name in applied) + + patch = Patch(name, self.__patch_dir) + + git.switch(patch.get_bottom()) + + # save the new applied list + idx = applied.index(name) + 1 + + popped = applied[:idx] + popped.reverse() + unapplied = popped + self.get_unapplied() + + f = file(self.__unapplied_file, 'w+') + f.writelines([line + '\n' for line in unapplied]) + f.close() + + del applied[:idx] + applied.reverse() + + f = file(self.__applied_file, 'w+') + f.writelines([line + '\n' for line in applied]) + f.close() + + if applied == []: + self.__set_current(None) + else: + self.__set_current(applied[-1]) + + self.__end_stack_check() + + def empty_patch(self, name): + """Returns True if the patch is empty + """ + patch = Patch(name, self.__patch_dir) + bottom = patch.get_bottom() + top = patch.get_top() + + if bottom == top: + return True + elif git.Commit(top).get_tree() == git.Commit(bottom).get_tree(): + return True + + return False diff --git a/stgit/utils.py b/stgit/utils.py new file mode 100644 index 0000000..9465fe0 --- /dev/null +++ b/stgit/utils.py @@ -0,0 +1,62 @@ +"""Common utility functions +""" + +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +def read_string(filename, multiline = False): + """Reads the first line from a file + """ + f = file(filename, 'r') + if multiline: + string = f.read() + else: + string = f.readline().strip() + f.close() + return string + +def write_string(filename, string, multiline = False): + """Writes string to file and truncates it + """ + f = file(filename, 'w+') + if multiline: + f.write(string) + else: + print >> f, string + f.close() + +def append_string(filename, string): + """Appends string to file + """ + f = file(filename, 'a+') + print >> f, string + f.close() + +def insert_string(filename, string): + """Inserts a string at the beginning of the file + """ + f = file(filename, 'r+') + lines = f.readlines() + f.seek(0); f.truncate() + print >> f, string + f.writelines(lines) + f.close() + +def create_empty_file(name): + """Creates an empty file + """ + file(name, 'w+').close() diff --git a/stgit/version.py b/stgit/version.py new file mode 100644 index 0000000..50a32f4 --- /dev/null +++ b/stgit/version.py @@ -0,0 +1 @@ +version = '0.4' diff --git a/stgitrc b/stgitrc new file mode 100644 index 0000000..7698f31 --- /dev/null +++ b/stgitrc @@ -0,0 +1,29 @@ +[stgit] +# Default author/committer details +#authname: Your Name +#authemail: your.name@yourcompany.com +#commname: Your Name +#commemail: your.name@yourcompany.com + +# Set to 'yes' if you don't want to use the 'resolved' command. +# 'refresh' will automatically mark the conflicts as resolved +autoresolved: no + + +[gitmergeonefile] +# Different three-way merge tools below. Uncomment the preferred one. +# Note that the 'output' file contains the same data as 'branch1'. This +# is useful for tools that do not take an output parameter + +merger: diff3 -L local -L older -L remote -m -E \ + "%(branch1)s" "%(ancestor)s" "%(branch2)s" > "%(output)s" + +#merger: xxdiff --title1 local --title2 older --title3 remote \ +# --show-merged-pane -m -E -O -X -M "%(output)s" \ +# "%(branch1)s" "%(ancestor)s" "%(branch2)s" + +#merger: emacs --eval '(ediff-merge-files-with-ancestor +# "%(branch1)s" "%(branch2)s" "%(ancestor)s" nil "%(output)s")' + +# Leave the original files in the working tree in case of a merge conflict +keeporig: yes