--- /dev/null
+Catalin Marinas <catalin.marinas@gmail.com>
+ http://www.procode.org/about.html
--- /dev/null
+ 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.
+\f
+ 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.)
+\f
+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.
+\f
+ 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.
+\f
+ 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
+\f
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+ <signature of Ty Coon>, 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.
--- /dev/null
+2005-07-09 Catalin Marinas <catalin.marinas@gmail.com>
+
+ * Release 0.4
+
+2005-07-09 Peter Osterlund <petero2@telia.com>
+
+ * Fix spelling errors
+
+2005-07-08 Catalin Marinas <catalin.marinas@gmail.com>
+
+ * stgit/main.py (diff): Add '--stat' option to 'diff'
+ (files): 'files' command implemented
+
+2005-07-08 Peter Osterlund <petero2@telia.com>
+
+ * stgit/git.py (diffstat): %(diffstat)s variable support in the
+ patch export template
+
+2005-07-07 Catalin Marinas <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * Add support for configurable merge tool via stgitrc
+
+ * Add support for configuration file (/etc/stgitrc, ~/.stgitrc,
+ .git/stgitrc)
+
+2005-07-02 Catalin Marinas <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * Fix exception reporting when the .git/HEAD link is not valid
+
+ * Empty patches are now marked
+
+2005-06-28 Catalin Marinas <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * stack.py modified to include all the series functions the Series
+ class
+
+2005-06-24 Catalin Marinas <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * stgit/main.py (push): --number option added to push
+ (pop): --number option added to push
+
+2005-06-22 Catalin Marinas <catalin.marinas@gmail.com>
+
+ * gitmergeonefile.py: temporary files are placed in <path>.local,
+ <path>.older and <path>.remote and only removed if the merge
+ succeeded
+
+2005-06-21 Catalin Marinas <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * 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 <catalin.marinas@gmail.com>
+
+ * Release 0.1
+
--- /dev/null
+For basic installation:
+
+ python setup.py install
+
+For more information:
+
+ http://docs.python.org/inst/inst.html
--- /dev/null
+include README MANIFEST.in AUTHORS COPYING INSTALL ChangeLog TODO stgitrc
+include examples/*.tmpl
--- /dev/null
+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 <cmd> (-h | --help)
+
+To initialise a tree (the tree must have been previously initialised
+with GIT):
+
+ stg init
+
+To add/delete files:
+
+ stg add [<file>*]
+ stg rm [<file>*]
+
+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]) | <tree-ish>'
+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 <name>
+ stg delete [<name or topmost>]
+
+The 'new' command also sets the topmost patch to the newly created
+one.
+
+To push/pop a patch to/from the stack:
+
+ stg push [<name or first unapplied>]
+ stg pop [<name or topmost>]
+
+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 [<dir-name or 'patches'>]
+
+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/<something>
+
+
+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.
--- /dev/null
+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
--- /dev/null
+
+
+Signed-off-by: Your Name <your.name@yourcompany.com>
--- /dev/null
+%(description)s
+---
+
+%(diffstat)s
+
--- /dev/null
+#!/usr/bin/env python
+"""Performs a 3-way merge for GIT files
+"""
+
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+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()
--- /dev/null
+#!/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'])],
+ )
--- /dev/null
+#!/usr/bin/env python
+# -*- python-mode -*-
+"""Takes care of starting the Init function
+"""
+
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+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()
--- /dev/null
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+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
+"""
--- /dev/null
+"""Handles the Stacked GIT configuration files
+"""
+
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+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'))
--- /dev/null
+"""Python GIT interface
+"""
+
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+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])
--- /dev/null
+"""Basic quilt-like functionality
+"""
+
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+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 <files/dirs...>',
+ [])
+
+
+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] <files...>',
+ [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] [<files...>]',
+ [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] [<files...>]\n\n'
+ 'The revision format is "([patch]/[bottom | top]) | <tree-ish>"',
+ [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] [<patch>]',
+ [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] <name>',
+ [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 <name>',
+ [])
+
+
+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] [<name>]',
+ [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] [<file>[ <file>]]',
+ [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 <dir> (or patches)',
+ '%prog [options] [<dir>]',
+ [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 <command> [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)
--- /dev/null
+"""Basic quilt-like functionality
+"""
+
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+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
--- /dev/null
+"""Common utility functions
+"""
+
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+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()
--- /dev/null
+version = '0.4'
--- /dev/null
+[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