chiark / gitweb /
Initial commit (Release 0.4) v0.4
authorCatalin Marinas <catalin.marinas@gmail.com>
Sun, 10 Jul 2005 07:29:31 +0000 (08:29 +0100)
committerCatalin Marinas <catalin.marinas@gmail.com>
Sun, 10 Jul 2005 07:29:31 +0000 (08:29 +0100)
20 files changed:
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
INSTALL [new file with mode: 0644]
MANIFEST.in [new file with mode: 0644]
README [new file with mode: 0644]
TODO [new file with mode: 0644]
examples/patchdescr.tmpl [new file with mode: 0644]
examples/patchexport.tmpl [new file with mode: 0644]
gitmergeonefile.py [new file with mode: 0755]
setup.py [new file with mode: 0755]
stg [new file with mode: 0755]
stgit/__init__.py [new file with mode: 0644]
stgit/config.py [new file with mode: 0644]
stgit/git.py [new file with mode: 0644]
stgit/main.py [new file with mode: 0644]
stgit/stack.py [new file with mode: 0644]
stgit/utils.py [new file with mode: 0644]
stgit/version.py [new file with mode: 0644]
stgitrc [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..098c1d3
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,2 @@
+Catalin Marinas <catalin.marinas@gmail.com>
+       http://www.procode.org/about.html
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
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.
+\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.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..30c4b93
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,153 @@
+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
+
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
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 (file)
index 0000000..587c4d7
--- /dev/null
@@ -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 (file)
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 <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.
diff --git a/TODO b/TODO
new file mode 100644 (file)
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 (file)
index 0000000..6480a2a
--- /dev/null
@@ -0,0 +1,3 @@
+
+
+Signed-off-by: Your Name <your.name@yourcompany.com>
diff --git a/examples/patchexport.tmpl b/examples/patchexport.tmpl
new file mode 100644 (file)
index 0000000..cbc938e
--- /dev/null
@@ -0,0 +1,5 @@
+%(description)s
+---
+
+%(diffstat)s
+
diff --git a/gitmergeonefile.py b/gitmergeonefile.py
new file mode 100755 (executable)
index 0000000..5b588dd
--- /dev/null
@@ -0,0 +1,210 @@
+#!/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()
diff --git a/setup.py b/setup.py
new file mode 100755 (executable)
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 (executable)
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 <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()
diff --git a/stgit/__init__.py b/stgit/__init__.py
new file mode 100644 (file)
index 0000000..4b03e3a
--- /dev/null
@@ -0,0 +1,16 @@
+__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
+"""
diff --git a/stgit/config.py b/stgit/config.py
new file mode 100644 (file)
index 0000000..3bbbd0d
--- /dev/null
@@ -0,0 +1,33 @@
+"""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'))
diff --git a/stgit/git.py b/stgit/git.py
new file mode 100644 (file)
index 0000000..e97288a
--- /dev/null
@@ -0,0 +1,413 @@
+"""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])
diff --git a/stgit/main.py b/stgit/main.py
new file mode 100644 (file)
index 0000000..dda6212
--- /dev/null
@@ -0,0 +1,819 @@
+"""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)
diff --git a/stgit/stack.py b/stgit/stack.py
new file mode 100644 (file)
index 0000000..851f998
--- /dev/null
@@ -0,0 +1,535 @@
+"""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
diff --git a/stgit/utils.py b/stgit/utils.py
new file mode 100644 (file)
index 0000000..9465fe0
--- /dev/null
@@ -0,0 +1,62 @@
+"""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()
diff --git a/stgit/version.py b/stgit/version.py
new file mode 100644 (file)
index 0000000..50a32f4
--- /dev/null
@@ -0,0 +1 @@
+version = '0.4'
diff --git a/stgitrc b/stgitrc
new file mode 100644 (file)
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