Merge branch 'stable'
authorCatalin Marinas <catalin.marinas@gmail.com>
Wed, 28 Jan 2009 23:07:37 +0000 (23:07 +0000)
committerCatalin Marinas <catalin.marinas@gmail.com>
Wed, 28 Jan 2009 23:07:37 +0000 (23:07 +0000)
Conflicts:
stgit/commands/files.py
stgit/commands/mail.py
stgit/git.py

166 files changed:
.gitignore
AUTHORS
Documentation/.gitignore
Documentation/COMMAND-TEMPLATE.txt [deleted file]
Documentation/Makefile
Documentation/SubmittingPatches [new file with mode: 0644]
Documentation/asciidoc.conf
Documentation/stg-branch.txt [deleted file]
Documentation/stg-clone.txt [deleted file]
Documentation/stg-cp.txt [deleted file]
Documentation/stg-init.txt [deleted file]
Documentation/stg-new.txt [deleted file]
Documentation/stg-sink.txt [deleted file]
Documentation/stg.txt
Documentation/tutorial.txt
INSTALL
Makefile
README
TODO
contrib/.gitignore [new file with mode: 0644]
contrib/Makefile [new file with mode: 0644]
contrib/diffcol.sh
contrib/stgit-completion.bash [deleted file]
contrib/stgit.el [new file with mode: 0644]
debian/rules
examples/gitconfig
perf/.gitignore [new file with mode: 0644]
perf/create_synthetic_repo.py [new file with mode: 0644]
perf/find_patchbomb.py [new file with mode: 0644]
perf/perftest.py [new file with mode: 0644]
perf/setup.sh [new file with mode: 0644]
setup.py
stg-build [new file with mode: 0755]
stgit/argparse.py [new file with mode: 0644]
stgit/commands/.gitignore [new file with mode: 0644]
stgit/commands/__init__.py
stgit/commands/add.py [deleted file]
stgit/commands/applied.py [deleted file]
stgit/commands/branch.py
stgit/commands/clean.py
stgit/commands/clone.py
stgit/commands/coalesce.py [new file with mode: 0644]
stgit/commands/commit.py
stgit/commands/common.py
stgit/commands/copy.py [deleted file]
stgit/commands/delete.py
stgit/commands/diff.py
stgit/commands/edit.py
stgit/commands/export.py
stgit/commands/files.py
stgit/commands/float.py
stgit/commands/fold.py
stgit/commands/goto.py
stgit/commands/hide.py
stgit/commands/id.py
stgit/commands/imprt.py
stgit/commands/init.py
stgit/commands/log.py
stgit/commands/mail.py
stgit/commands/new.py
stgit/commands/patches.py
stgit/commands/pick.py
stgit/commands/pop.py
stgit/commands/pull.py
stgit/commands/push.py
stgit/commands/rebase.py
stgit/commands/redo.py [new file with mode: 0644]
stgit/commands/refresh.py
stgit/commands/rename.py
stgit/commands/repair.py
stgit/commands/reset.py [new file with mode: 0644]
stgit/commands/resolved.py
stgit/commands/rm.py [deleted file]
stgit/commands/series.py
stgit/commands/show.py
stgit/commands/sink.py
stgit/commands/status.py
stgit/commands/sync.py
stgit/commands/top.py
stgit/commands/unapplied.py [deleted file]
stgit/commands/uncommit.py
stgit/commands/undo.py [new file with mode: 0644]
stgit/commands/unhide.py
stgit/completion.py [new file with mode: 0644]
stgit/config.py
stgit/git.py
stgit/gitmergeonefile.py
stgit/lib/__init__.py [new file with mode: 0644]
stgit/lib/edit.py [new file with mode: 0644]
stgit/lib/git.py [new file with mode: 0644]
stgit/lib/log.py [new file with mode: 0644]
stgit/lib/stack.py [new file with mode: 0644]
stgit/lib/stackupgrade.py [new file with mode: 0644]
stgit/lib/transaction.py [new file with mode: 0644]
stgit/main.py
stgit/out.py
stgit/run.py
stgit/stack.py
stgit/utils.py
stgit/version.py
t/README
t/t0001-subdir-branches.sh
t/t0002-status.sh
t/t1000-branch-create.sh
t/t1001-branch-rename.sh
t/t1002-branch-clone.sh
t/t1003-new.sh
t/t1200-push-modified.sh
t/t1201-pull-trailing.sh
t/t1202-push-undo.sh
t/t1203-pop.sh
t/t1203-push-conflict.sh [new file with mode: 0755]
t/t1204-pop-keep.sh
t/t1205-push-subdir.sh
t/t1206-push-hidden.sh [new file with mode: 0755]
t/t1300-uncommit.sh
t/t1301-repair.sh
t/t1302-repair-interop.sh
t/t1303-commit.sh [new file with mode: 0755]
t/t1400-patch-history.sh [deleted file]
t/t1500-float.sh
t/t1501-sink.sh
t/t1600-delete-one.sh
t/t1601-delete-many.sh
t/t1602-delete-spill.sh [new file with mode: 0755]
t/t1700-goto-top.sh
t/t1701-goto-hidden.sh [new file with mode: 0755]
t/t1800-import.sh
t/t1800-import/patches/attribution.patch [new file with mode: 0644]
t/t1800-import/patches/delete-extra-lines.patch [new file with mode: 0644]
t/t1800-import/patches/fifth-stanza.patch [new file with mode: 0644]
t/t1800-import/patches/first-stanza.patch [new file with mode: 0644]
t/t1800-import/patches/fourth-stanza.patch [new file with mode: 0644]
t/t1800-import/patches/second-stanza.patch [new file with mode: 0644]
t/t1800-import/patches/series [new file with mode: 0644]
t/t1800-import/patches/seventh-stanza.patch [new file with mode: 0644]
t/t1800-import/patches/sixth-stanza.patch [new file with mode: 0644]
t/t1800-import/patches/third-stanza.patch [new file with mode: 0644]
t/t1900-mail.sh
t/t2000-sync.sh
t/t2100-pull-policy-fetch.sh
t/t2101-pull-policy-pull.sh
t/t2102-pull-policy-rebase.sh
t/t2200-rebase.sh
t/t2300-refresh-subdir.sh
t/t2500-clean.sh
t/t2600-coalesce.sh [new file with mode: 0755]
t/t2700-refresh.sh
t/t2701-refresh-p.sh
t/t2702-refresh-rm.sh [new file with mode: 0755]
t/t2800-goto-subdir.sh [new file with mode: 0755]
t/t2900-rename.sh
t/t3000-dirty-merge.sh [new file with mode: 0755]
t/t3100-reset.sh [new file with mode: 0755]
t/t3101-reset-hard.sh [new file with mode: 0755]
t/t3102-undo.sh [new file with mode: 0755]
t/t3103-undo-hard.sh [new file with mode: 0755]
t/t3104-redo.sh [new file with mode: 0755]
t/t3105-undo-external-mod.sh [new file with mode: 0755]
t/t3300-edit.sh [new file with mode: 0755]
t/t3400-pick.sh [new file with mode: 0755]
t/t4000-upgrade.sh
t/test-lib.sh
templates/mailattch.tmpl
templates/patchexport.tmpl
templates/patchmail.tmpl

index 91dbad2..e7fffb0 100644 (file)
@@ -6,3 +6,4 @@ patches-*
 release.sh
 setup.cfg.rpm
 snapshot.sh
+stgit-completion.bash
diff --git a/AUTHORS b/AUTHORS
index 098c1d3..6617806 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,2 +1,4 @@
 Catalin Marinas <catalin.marinas@gmail.com>
-       http://www.procode.org/about.html
+        http://www.procode.org/about.html
+Karl Hasselström <kha@treskal.com>
+        http://www.treskal.com/kalle/
index 6da6dc4..c72f35c 100644 (file)
@@ -1,5 +1,6 @@
 *.html
 *.pdf
-*.7
-*.1
+*.[1-9]
+stg-*.txt
 doc.dep
+command-list.txt
diff --git a/Documentation/COMMAND-TEMPLATE.txt b/Documentation/COMMAND-TEMPLATE.txt
deleted file mode 100644 (file)
index 7bc029a..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-stg-NAME(1)
-==========
-XXX <xxx@xxx.xx>
-v0.12.1, Xxxx 2007
-
-NAME
-----
-stg-NAME - stgdesc:NAME[]
-
-SYNOPSIS
---------
-[verse]
-'stg' NAME [OPTIONS] XXX
-
-DESCRIPTION
------------
-
-XXX
-
-OPTIONS
--------
-
-XXX
-
-FILES
------
-
-       <templates>/XXX
-
-ENVIRONMENT VARIABLES
----------------------
-
-       XXX
-
-GIT CONFIGURATION VARIABLES
----------------------------
-
-       stgit.XXX
-
-StGIT
------
-Part of the StGIT suite - see gitlink:stg[1].
index 2e5d23c..dd029d5 100644 (file)
@@ -1,22 +1,21 @@
-MAN1_TXT=$(wildcard stg*.txt)
-MAN7_TXT=
+COMMANDS = $(shell ../stg-build --commands)
+COMMANDS_TXT = $(patsubst %,stg-%.txt,$(COMMANDS))
 
-DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT) $(MAN7_TXT))
+MAN1_TXT= stg.txt $(COMMANDS_TXT)
 
-ARTICLES = 
-# with their own formatting rules.
-SP_ARTICLES = tutorial
+DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT))
 
-DOC_HTML += $(patsubst %,%.html,$(ARTICLES) $(SP_ARTICLES))
-DOC_PDF += $(patsubst %,%.pdf,$(ARTICLES) $(SP_ARTICLES))
+ARTICLES = tutorial
+
+DOC_HTML += $(patsubst %,%.html,$(ARTICLES))
+DOC_PDF += $(patsubst %,%.pdf,$(ARTICLES))
 
 DOC_MAN1=$(patsubst %.txt,%.1,$(MAN1_TXT))
-DOC_MAN7=$(patsubst %.txt,%.7,$(MAN7_TXT))
 
 prefix?=$(HOME)
-mandir?=$(prefix)/man
+htmldir?=$(prefix)/share/doc/stgit
+mandir?=$(prefix)/share/man
 man1dir=$(mandir)/man1
-man7dir=$(mandir)/man7
 # DESTDIR=
 
 ASCIIDOC=asciidoc --unsafe
@@ -37,16 +36,17 @@ all: html man
 html: $(DOC_HTML)
 pdf: $(DOC_PDF)
 
-$(DOC_HTML) $(DOC_MAN1) $(DOC_MAN7): asciidoc.conf
+$(DOC_HTML) $(DOC_MAN1): asciidoc.conf
 
-man: man1 man7
+man: man1
 man1: $(DOC_MAN1)
-man7: $(DOC_MAN7)
-
 install: man
-       $(INSTALL) -d -m755 $(DESTDIR)$(man1dir) $(DESTDIR)$(man7dir)
+       $(INSTALL) -d -m755 $(DESTDIR)$(man1dir)
        $(INSTALL) -m644 $(DOC_MAN1) $(DESTDIR)$(man1dir)
-       $(INSTALL) -m644 $(DOC_MAN7) $(DESTDIR)$(man7dir)
+
+install-html: html
+       $(INSTALL) -d -m755 $(DESTDIR)$(htmldir)
+       $(INSTALL) -m644 $(DOC_HTML) $(DESTDIR)$(htmldir)
 #
 # Determine "include::" file references in asciidoc files.
 #
@@ -58,12 +58,20 @@ doc.dep : $(wildcard *.txt) build-docdep.perl
 -include doc.dep
 
 clean:
-       rm -f *.xml *.html *.pdf *.1 *.7 doc.dep
+       rm -f *.xml *.html *.pdf *.1 doc.dep $(COMMANDS_TXT) command-list.txt
+
+ALL_PY = $(shell find ../stgit -name '*.py')
+
+$(COMMANDS_TXT): $(ALL_PY)
+       ../stg-build --asciidoc $(basename $(subst stg-,,$@)) > $@
+
+command-list.txt: $(ALL_PY)
+       ../stg-build --cmd-list > $@
 
 %.html : %.txt
        $(ASCIIDOC) -b xhtml11 -d manpage -f asciidoc.conf $(ASCIIDOC_EXTRA) $<
 
-%.1 %.7 : %.xml
+%.1 : %.xml
        xmlto -m callouts.xsl man $<
 
 %.xml : %.txt
@@ -74,6 +82,7 @@ clean:
 
 # special formatting rules
 tutorial.html : %.html : %.txt
-       $(ASCIIDOC) -b xhtml11 -d article -f tutorial.conf $(ASCIIDOC_EXTRA) $<
+       $(ASCIIDOC) -b xhtml11 -d article -a toc -f tutorial.conf \
+                $(ASCIIDOC_EXTRA) $<
 tutorial.xml : %.xml : %.txt
        $(ASCIIDOC) -b docbook -d article -f tutorial.conf $<
diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
new file mode 100644 (file)
index 0000000..ec2d3d6
--- /dev/null
@@ -0,0 +1,419 @@
+Checklist (and a short version for the impatient):
+
+        Commits:
+
+        - Make commits of logical units.
+        - Check for unnecessary whitespace with "git diff --check"
+          before committing.
+        - Do not check in commented out code or unneeded files.
+        - Provide a meaningful commit message.
+        - The first line of the commit message should be a short
+          description and should skip the full stop.
+        - If you want your work included in StGit, add a
+          "Signed-off-by: Your Name <you@example.com>" line to the
+          commit message (or just use the option "-s" when
+          committing) to confirm that you agree to the Developer's
+          Certificate of Origin.
+        - Make sure that you have tests for the bug you are fixing.
+        - Make sure that the test suite passes after your commit.
+
+        Patch:
+
+        - Preferably use "stg mail" to send patches. The first time,
+          it's a good idea to try to mail the patches to yourself to
+          see that everything works.
+        - Do not PGP sign your patch.
+        - Do not attach your patch, but read in the mail.
+          body, unless you cannot teach your mailer to
+          leave the formatting of the patch alone.
+        - Be careful doing cut & paste into your mailer, not to
+          corrupt whitespaces.
+        - Provide additional information (which is unsuitable for the
+          commit message) between the "---" and the diffstat. (The -E
+          option to stg mail lets you edit the message before you send
+          it out.)
+        - If you change, add, or remove a command line option or
+          make some other user interface change, the associated
+          documentation should be updated as well.
+        - If your name is not writable in ASCII, make sure that
+          you send off a message in the correct encoding.
+        - Send the patch to the list (git@vger.kernel.org) and the
+          maintainer (catalin.marinas@gmail.com) if (and only if) the
+          patch is ready for inclusion.
+
+
+Long version:
+
+
+1. Make separate commits for logically separate changes.
+
+   Unless your patch is really trivial, you should not be sending out
+   a patch that was generated between your working tree and your
+   commit head. Instead, always make a commit with complete commit
+   message and generate a series of patches from your repository. It
+   is a good discipline.
+
+   Describe the technical detail of the change(s).
+
+   If your description starts to get too long, that's a sign that you
+   probably need to split up your commit to finer grained pieces.
+
+   Oh, another thing. I am picky about whitespaces. Please run git
+   diff --check on your changes before you commit.
+
+
+2. Generate your patch using Git tools out of your commits.
+
+   Git based diff tools (Git, Cogito, and StGit included) generate
+   unidiff which is the preferred format.
+
+   You do not have to be afraid to use -M option to "git diff" and
+   friends, if your patch involves file renames. The receiving end can
+   handle them just fine.
+
+   Please make sure your patch does not include any extra files which
+   do not belong in a patch submission. Make sure to review your patch
+   after generating it, to ensure accuracy. Before sending out, please
+   make sure it cleanly applies to the "master" branch head. If you
+   are preparing a work based on some other branch, that is fine, but
+   please mark it as such.
+
+
+3. Sending your patches.
+
+   StGit patches should be sent to the Git mailing list
+   (git@vger.kernel.org), and preferably CCed to the StGit maintainer
+   (catalin.marinas@gmail.com). The recipients need to be able to read
+   and comment on the changes you are submitting. It is important for
+   a developer to be able to "quote" your changes, using standard
+   e-mail tools, so that they may comment on specific portions of your
+   code. For this reason, all patches should be submitted "inline".
+   WARNING: Be wary of your MUAs word-wrap corrupting your patch. Do
+   not cut-n-paste your patch; you can lose tabs that way if you are
+   not careful.
+
+   It is a common convention to prefix your subject line with [StGit
+   PATCH]. This lets people easily distinguish patches to StGit from
+   other e-mail discussions and patches meant for Git itself. Use of
+   additional markers after PATCH and the closing bracket to mark the
+   nature of the patch is also encouraged. E.g. [PATCH/RFC] is often
+   used when the patch is not ready to be applied but it is for
+   discussion, [PATCH v2], [PATCH v3] etc. are often seen when you are
+   sending an update to what you have previously sent.
+
+   "stg mail" command follows the best current practice to format the
+   body of an e-mail message. At the beginning of the patch should
+   come your commit message, ending with the Signed-off-by: lines, and
+   a line that consists of three dashes, followed by the diffstat
+   information and the patch itself. If you are forwarding a patch
+   from somebody else, optionally, at the beginning of the e-mail
+   message just before the commit message starts, you can put a
+   "From:" line to name that person.
+
+   You often want to add additional explanation about the patch, other
+   than the commit message itself. Place such "cover letter" material
+   between the three dash lines and the diffstat. If you have comments
+   about a whole series of patches, you can include them in a separate
+   cover mail message (the -e option to stg mail).
+
+   Do not attach the patch as a MIME attachment, compressed or not. Do
+   not let your e-mail client send quoted-printable. Do not let your
+   e-mail client send format=flowed which would destroy whitespaces in
+   your patches. Many popular e-mail applications will not always
+   transmit a MIME attachment as plain text, making it impossible to
+   comment on your code. A MIME attachment also takes a bit more time
+   to process. This does not decrease the likelihood of your
+   MIME-attached change being accepted, but it makes it more likely
+   that it will be postponed.
+
+   Exception: If your mailer is mangling patches then someone may ask
+   you to re-send them using MIME, that is OK.
+
+   Do not PGP sign your patch, at least for now. Most likely, your
+   maintainer or other people on the list would not have your PGP key
+   and would not bother obtaining it anyway. Your patch is not judged
+   by who you are; a good patch from an unknown origin has a far
+   better chance of being accepted than a patch from a known,
+   respected origin that is done poorly or does incorrect things.
+
+
+4. Sign your work
+
+   To improve tracking of who did what, we've borrowed the "sign-off"
+   procedure from the Git and Linux kernel projects on patches that
+   are being emailed around. Although StGit is a lot smaller project
+   it is a good discipline to follow it.
+
+   The sign-off is a simple line at the end of the explanation for the
+   patch, which certifies that you wrote it or otherwise have the
+   right to pass it on as a open-source patch. The rules are pretty
+   simple: if you can certify the below:
+
+        Developer's Certificate of Origin 1.1
+
+        By making a contribution to this project, I certify that:
+
+        (a) The contribution was created in whole or in part by me and
+            I have the right to submit it under the open source
+            license indicated in the file; or
+
+        (b) The contribution is based upon previous work that, to the
+            best of my knowledge, is covered under an appropriate open
+            source license and I have the right under that license to
+            submit that work with modifications, whether created in
+            whole or in part by me, under the same open source license
+            (unless I am permitted to submit under a different
+            license), as indicated in the file; or
+
+        (c) The contribution was provided directly to me by some other
+            person who certified (a), (b) or (c) and I have not
+            modified it.
+
+        (d) I understand and agree that this project and the
+            contribution are public and that a record of the
+            contribution (including all personal information I submit
+            with it, including my sign-off) is maintained indefinitely
+            and may be redistributed consistent with this project or
+            the open source license(s) involved.
+
+   then you just add a line saying
+
+       Signed-off-by: Random J Developer <random@developer.example.org>
+
+   This line can be automatically added by StGit by any command that
+   accepts the --sign option.
+
+   Notice that you can place your own Signed-off-by: line when
+   forwarding somebody else's patch with the above rules for D-C-O.
+   Indeed you are encouraged to do so. Do not forget to place an
+   in-body "From: " line at the beginning to properly attribute the
+   change to its true author (see (2) above).
+
+   Also notice that a real name is used in the Signed-off-by: line.
+   Please don't hide your real name.
+
+   Some people also put extra tags at the end.
+
+   "Acked-by:" says that the patch was reviewed by a person who is
+   more familiar with the issues and the area the patch attempts to
+   modify. "Tested-by:" says the patch was tested by the person and
+   found to have the desired effect.
+
+
+------------------------------------------------
+MUA specific hints
+
+Some of patches I receive or pick up from the list share common
+patterns of breakage.  Please make sure your MUA is set up
+properly not to corrupt whitespaces.  Here are two common ones
+I have seen:
+
+* Empty context lines that do not have _any_ whitespace.
+
+* Non empty context lines that have one extra whitespace at the
+  beginning.
+
+One test you could do yourself if your MUA is set up correctly is:
+
+* Send the patch to yourself, exactly the way you would, except
+  To: and Cc: lines, which would not contain the list and
+  maintainer address.
+
+* Save that patch to a file in UNIX mailbox format.  Call it say
+  a.patch.
+
+* Try to apply to the tip of the "master" branch from the
+  public repository:
+
+    $ git fetch http://homepage.ntlworld.com/cmarinas/stgit.git master:test-apply
+    $ git checkout test-apply
+    $ git reset --hard
+    $ stg init
+    $ stg import -M a.patch
+
+If it does not apply correctly, there can be various reasons.
+
+* Your patch itself does not apply cleanly.  That is _bad_ but
+  does not have much to do with your MUA.  Please rebase the
+  patch appropriately.
+
+* Your MUA corrupted your patch; "stg import" would complain that
+  the patch does not apply.
+
+* Check the imported patch with e.g. "stg show". If it isn't exactly
+  what you would want to see in the commit log message, it is very
+  likely that the maintainer would end up hand editing the log
+  message when he applies your patch. Things like "Hi, this is my
+  first patch.\n", if you really want to put in the patch e-mail,
+  should come after the three-dash line that signals the end of the
+  commit message.
+
+
+Pine
+----
+
+(Johannes Schindelin)
+
+I don't know how many people still use pine, but for those poor
+souls it may be good to mention that the quell-flowed-text is
+needed for recent versions.
+
+... the "no-strip-whitespace-before-send" option, too. AFAIK it
+was introduced in 4.60.
+
+(Linus Torvalds)
+
+And 4.58 needs at least this.
+
+---
+diff-tree 8326dd8350be64ac7fc805f6563a1d61ad10d32c (from e886a61f76edf5410573e92e38ce22974f9c40f1)
+Author: Linus Torvalds <torvalds@g5.osdl.org>
+Date:   Mon Aug 15 17:23:51 2005 -0700
+
+    Fix pine whitespace-corruption bug
+
+    There's no excuse for unconditionally removing whitespace from
+    the pico buffers on close.
+
+diff --git a/pico/pico.c b/pico/pico.c
+--- a/pico/pico.c
++++ b/pico/pico.c
+@@ -219,7 +219,9 @@ PICO *pm;
+            switch(pico_all_done){      /* prepare for/handle final events */
+              case COMP_EXIT :          /* already confirmed */
+                packheader();
++#if 0
+                stripwhitespace();
++#endif
+                c |= COMP_EXIT;
+                break;
+
+
+(Daniel Barkalow)
+
+> A patch to SubmittingPatches, MUA specific help section for
+> users of Pine 4.63 would be very much appreciated.
+
+Ah, it looks like a recent version changed the default behavior to do the
+right thing, and inverted the sense of the configuration option. (Either
+that or Gentoo did it.) So you need to set the
+"no-strip-whitespace-before-send" option, unless the option you have is
+"strip-whitespace-before-send", in which case you should avoid checking
+it.
+
+
+Thunderbird
+-----------
+
+(A Large Angry SCM)
+
+Here are some hints on how to successfully submit patches inline using
+Thunderbird.
+
+This recipe appears to work with the current [*1*] Thunderbird from Suse.
+
+The following Thunderbird extensions are needed:
+        AboutConfig 0.5
+                http://aboutconfig.mozdev.org/
+        External Editor 0.7.2
+                http://globs.org/articles.php?lng=en&pg=8
+
+1) Prepare the patch as a text file using your method of choice.
+
+2) Before opening a compose window, use Edit->Account Settings to
+uncheck the "Compose messages in HTML format" setting in the
+"Composition & Addressing" panel of the account to be used to send the
+patch. [*2*]
+
+3) In the main Thunderbird window, _before_ you open the compose window
+for the patch, use Tools->about:config to set the following to the
+indicated values:
+        mailnews.send_plaintext_flowed  => false
+        mailnews.wraplength             => 0
+
+4) Open a compose window and click the external editor icon.
+
+5) In the external editor window, read in the patch file and exit the
+editor normally.
+
+6) Back in the compose window: Add whatever other text you wish to the
+message, complete the addressing and subject fields, and press send.
+
+7) Optionally, undo the about:config/account settings changes made in
+steps 2 & 3.
+
+
+[Footnotes]
+*1* Version 1.0 (20041207) from the MozillaThunderbird-1.0-5 rpm of Suse
+9.3 professional updates.
+
+*2* It may be possible to do this with about:config and the following
+settings but I haven't tried, yet.
+        mail.html_compose                       => false
+        mail.identity.default.compose_html      => false
+        mail.identity.id?.compose_html          => false
+
+(Lukas Sandström)
+
+There is a script in contrib/thunderbird-patch-inline which can help
+you include patches with Thunderbird in an easy way. To use it, do the
+steps above and then use the script as the external editor.
+
+Gnus
+----
+
+'|' in the *Summary* buffer can be used to pipe the current
+message to an external program, and this is a handy way to drive
+"git am".  However, if the message is MIME encoded, what is
+piped into the program is the representation you see in your
+*Article* buffer after unwrapping MIME.  This is often not what
+you would want for two reasons.  It tends to screw up non ASCII
+characters (most notably in people's names), and also
+whitespaces (fatal in patches).  Running 'C-u g' to display the
+message in raw form before using '|' to run the pipe can work
+this problem around.
+
+
+KMail
+-----
+
+This should help you to submit patches inline using KMail.
+
+1) Prepare the patch as a text file.
+
+2) Click on New Mail.
+
+3) Go under "Options" in the Composer window and be sure that
+"Word wrap" is not set.
+
+4) Use Message -> Insert file... and insert the patch.
+
+5) Back in the compose window: add whatever other text you wish to the
+message, complete the addressing and subject fields, and press send.
+
+
+Gmail
+-----
+
+Submitting properly formatted patches via Gmail is simple now that
+IMAP support is available. First, edit your ~/.gitconfig to specify your
+account settings:
+
+[imap]
+        folder = "[Gmail]/Drafts"
+        host = imaps://imap.gmail.com
+        user = user@gmail.com
+        pass = p4ssw0rd
+        port = 993
+        sslverify = false
+
+Next, ensure that your Gmail settings are correct. In "Settings" the
+"Use Unicode (UTF-8) encoding for outgoing messages" should be checked.
+
+Once your commits are ready to send to the mailing list, run the following
+command to send the patch emails to your Gmail Drafts folder.
+
+        $ git format-patch -M --stdout origin/master | git imap-send
+
+Go to your Gmail account, open the Drafts folder, find the patch email, fill
+in the To: and CC: fields and send away!
index 3df8814..f2c0ede 100644 (file)
-## gitlink: macro
-#
-# Usage: gitlink:command[manpage-section]
-#
-# Note, {0} is the manpage section, while {target} is the command.
-#
-# Show GIT link as: <command>(<section>); if section is defined, else just show
-# the command.
-
 [attributes]
-caret=^
+asterisk=&#42;
+plus=&#43;
+caret=&#94;
 startsb=&#91;
 endsb=&#93;
 tilde=&#126;
 
 ifdef::backend-docbook[]
-[gitlink-inlinemacro]
-{0%{target}}
-{0#<citerefentry>}
-{0#<refentrytitle>{target}</refentrytitle><manvolnum>{0}</manvolnum>}
-{0#</citerefentry>}
-endif::backend-docbook[]
-
-ifdef::backend-docbook[]
+ifndef::docbook-xsl-172[]
 # "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this.
+# v1.72 breaks with this because it replaces dots not in roff requests.
 [listingblock]
 <example><title>{title}</title>
 <literallayout>
+ifdef::doctype-manpage[]
+&#10;.ft C&#10;
+endif::doctype-manpage[]
 |
+ifdef::doctype-manpage[]
+&#10;.ft&#10;
+endif::doctype-manpage[]
 </literallayout>
 {title#}</example>
+endif::docbook-xsl-172[]
+endif::backend-docbook[]
+
+ifdef::doctype-manpage[]
+ifdef::backend-docbook[]
+[header]
+template::[header-declarations]
+<refentry>
+<refmeta>
+<refentrytitle>{mantitle}</refentrytitle>
+<manvolnum>{manvolnum}</manvolnum>
+<refmiscinfo class="source">StGit</refmiscinfo>
+<refmiscinfo class="version">{stgit_version}</refmiscinfo>
+<refmiscinfo class="manual">StGit Manual</refmiscinfo>
+</refmeta>
+<refnamediv>
+  <refname>{manname}</refname>
+  <refpurpose>{manpurpose}</refpurpose>
+</refnamediv>
+endif::backend-docbook[]
+endif::doctype-manpage[]
+
+## htmllink: macro
+#
+# Usage: htmllink:url[text]
+
+ifdef::backend-docbook[]
+[htmllink-inlinemacro]
+{0}
+endif::backend-docbook[]
+
+ifdef::backend-xhtml11[]
+[htmllink-inlinemacro]
+<a href="{target}">{0}</a>
+endif::backend-xhtml11[]
+
+## manlink: macro
+#
+# Usage: manlink:command[manpage-section]
+#
+# Note, {0} is the manpage section, while {target} is the command.
+#
+# Show link as: <command>(<section>); if section is defined, else just
+# show the command.
+
+ifdef::backend-docbook[]
+[manlink-inlinemacro]
+{0%{target}}
+{0#<citerefentry>}
+{0#<refentrytitle>{target}</refentrytitle><manvolnum>{0}</manvolnum>}
+{0#</citerefentry>}
 endif::backend-docbook[]
 
 ifdef::backend-xhtml11[]
-[gitlink-inlinemacro]
+[manlink-inlinemacro]
 <a href="{target}.html">{target}{0?({0})}</a>
 endif::backend-xhtml11[]
 
-# stglink
+## stglink: macro
+#
+# Usage: stglink:command[]
+#
+# Show StGit link as: stg-<command>(1) in man pages, stg <command> in
+# html.
 
 ifdef::backend-docbook[]
 [stglink-inlinemacro]
 <citerefentry>
-<refentrytitle>stg {target}</refentrytitle><manvolnum>1</manvolnum>
+  <refentrytitle>stg-{target}</refentrytitle><manvolnum>1</manvolnum>
 </citerefentry>
 endif::backend-docbook[]
 
 ifdef::backend-xhtml11[]
 [stglink-inlinemacro]
-<a href="stg-{target}.html">stg {target}(1)</a>
+<a href="stg-{target}.html">stg {target}</a>
 endif::backend-xhtml11[]
 
-# stgdesc
-[stgdesc-inlinemacro]
-{sys:../stg help|grep "  {target}" | tr -s ' '| cut -d' ' -f3-}
+## stgsublink: macro
+#
+# Usage: stgsublink:command[]
+#
+# Show StGit link as: <command> in man pages, stg <command> in
+# html.
 
-[stgdesc-blockmacro]
-stglink:{target}[]::
-       stgdesc:{target}[]
+ifdef::backend-docbook[]
+[stgsublink-inlinemacro]
+{target}
+endif::backend-docbook[]
+
+ifdef::backend-xhtml11[]
+[stgsublink-inlinemacro]
+<a href="stg-{target}.html">{target}</a>
+endif::backend-xhtml11[]
diff --git a/Documentation/stg-branch.txt b/Documentation/stg-branch.txt
deleted file mode 100644 (file)
index 25ca951..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-stg-branch(1)
-=============
-Yann Dirson <ydirson@altern.org>
-v0.12.1, April 2007
-
-NAME
-----
-stg-branch - stgdesc:branch[]
-
-SYNOPSIS
---------
-[verse]
-'stg' branch
-'stg' branch <branch>
-'stg' branch --list
-'stg' branch --create <newstack> [<commit-id>]
-'stg' branch --clone [<newstack>]
-'stg' branch --rename <oldname> <newname>
-'stg' branch --description=<description> [<branch>]
-'stg' branch --protect [<branch>]
-'stg' branch --unprotect [<branch>]
-'stg' branch --delete [--force] <branch>
-
-DESCRIPTION
------------
-
-Assorted operations on branches.
-
-no command, no argument::
-       Display the name of the current branch.
-
-no command, one argument::
-       Switch to the named <branch>.
-
-'stg' branch [-l | --list]::
-       Display the list of branches in the current repository,
-       suffixed by the branch description if any, and optionally
-       prefixed by the letter flags 's' if the branch is an StGIT
-       stack, and 'p' if the StGIT stack is protected.  The current
-       branch is shown with a leading ">" character.
-
-'stg' branch [-c | --create]::
-       Create a new StGIT stack based at the specified commit, or at
-       the current HEAD if not specified.  The repository HEAD is
-       switched to the new stack.
-+
-StGIT will try to detect the branch off which the new stack is forked,
-as well as the remote repository from which that parent branch is
-taken (if any), so running stglink:pull[] will effectively pull new
-commits from the correct branch.  It will warn if it cannot guess the
-parent branch (eg. if you do not specify a branch name as
-<commit-id>).
-
-'stg' branch --clone::
-       Clone the current stack, under the name <newstack> if
-       specified, or using the current stack's name suffixed by a
-       timestamp.
-+
-The description of the new stack is set to tell it is a clone of the
-current stack.  The parent information of the new stack is copied from
-the current stack.
-+
-Cloning a GIT branch that is not an StGIT stack is similar to creating
-a new stack off the current branch.
-
-'stg' branch [-r | --rename]::
-       Rename the stack named <oldname> to <newname>.
-
-'stg' branch [-d <desc> | --description=<desc>]::
-       Set the branch description.
-
-'stg' branch [-p | --protect]::
-       Protect the named stack or the current one, preventing
-       further StGIT operations from modifying this stack.
-
-'stg' branch [-u | --unprotect]::
-       Remove a "protected" flag previously set with '--protect'.
-
-'stg' branch --delete::
-       Delete the named <branch>.  If there are any patches left in
-       the series, StGIT will refuse to delete it, unless '--force'
-       is specified.
-+
-A protected branch cannot be deleted, it must be unprotected first
-(see above).
-+
-When the current branch is deleted, the repository HEAD is switched to
-the "master" branch if it exists.
-+
-Branch "master" is treated specially (see bug #8732), in that only the
-StGIT metadata are removed, the GIT branch itself is not destroyed.
-
-OPTIONS
--------
-
---force::
-       Force a delete when the series is not empty.
-
-FILES
------
-
-       $GIT_DIR/patches/<branch>/description
-       $GIT_DIR/patches/<branch>/protected
-
-GIT CONFIGURATION VARIABLES
----------------------------
-
-       branch.<branchname>.remote
-       branch.<branchname>.merge
-       branch.<branchname>.stgit.parentbranch
-
-StGIT
------
-Part of the StGIT suite - see gitlink:stg[1].
diff --git a/Documentation/stg-clone.txt b/Documentation/stg-clone.txt
deleted file mode 100644 (file)
index 19a8b79..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-stg-clone(1)
-==========
-Yann Dirson <ydirson@altern.org>
-v0.12.1, April 2007
-
-NAME
-----
-stg-clone - stgdesc:clone[]
-
-SYNOPSIS
---------
-[verse]
-'stg' clone <repository> <dir>
-
-DESCRIPTION
------------
-
-Clone a GIT <repository> into the local <dir> (using
-gitlink:git-clone[1]) and initialise the resulting "master" branch as
-a patch stack.
-
-This operation is for example suitable to start working using the
-"tracking branch" workflow (see gitlink:stg[1]).  Other means to setup
-an StGIT stack are stglink:init[] and the '--create' and '--clone'
-commands of stglink:branch[].
-
-The target directory named by <dir> will be created by this command,
-and must not exist beforehand.
-
-StGIT
------
-Part of the StGIT suite - see gitlink:stg[1].
diff --git a/Documentation/stg-cp.txt b/Documentation/stg-cp.txt
deleted file mode 100644 (file)
index 2314925..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-stg-cp(1)
-=========
-Yann Dirson <ydirson@altern.org>
-v0.13, March 2007
-
-NAME
-----
-stg-cp - stgdesc:cp[]
-
-SYNOPSIS
---------
-[verse]
-'stg' cp [OPTIONS] <file|dir> <newname>
-'stg' cp [OPTIONS] <files|dirs...> <dir>
-
-DESCRIPTION
------------
-
-Make git-controlled copies of git-controlled files.  The copies are
-added to the Git index, so you can add them to a patch with
-stglink:refresh[].
-
-In the first form, copy a single file or a single directory, with a
-new name.  The parent directory of <newname> must already exist;
-<newname> itself must not already exist, or the command will be
-interpreted as one of the second form.
-
-In the second form, copy one or several files and/or directories, into
-an existing directory.
-
-Directories are copied recursively.  Only the git-controlled files
-under the named directories are copied and added to the index.  Any
-file not known to Git will not be copied.
-
-CAVEATS
--------
-
-This command does not allow yet to overwrite an existing file (whether
-it could be recovered from Git or not).  Further more, when copying a
-directory, the second form does not allow to proceed if a directory by
-that name already exists inside the target, even when no file inside
-that directory would be overwritten.
-
-FUTURE OPTIONS
---------------
-
-No options are supported yet.  The following options may be
-implemented in the future.
-
---all::
-       Also copy files not known to Git when copying a directory.
-
---force::
-       Force overwriting of target files, even if overwritten files
-       have non-committed changes or are not known to Git.
-
---dry-run::
-       Show which files would be added, and which would be modified
-       if --force would be added.
-
-StGIT
------
-Part of the StGIT suite - see gitlink:stg[1].
diff --git a/Documentation/stg-init.txt b/Documentation/stg-init.txt
deleted file mode 100644 (file)
index 03877e0..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-stg-init(1)
-==========
-Yann Dirson <ydirson@altern.org>
-v0.12.1, April 2007
-
-NAME
-----
-stg-init - stgdesc:init[]
-
-SYNOPSIS
---------
-[verse]
-'stg' init
-
-DESCRIPTION
------------
-
-Initialise the current GIT branch to be used as an StGIT stack.  You
-must already be in a GIT repository and .git/HEAD must point to a
-valid file in refs/heads/.
-
-This operation is for example suitable to start working using the
-"development branch" workflow (see gitlink:stg[1]).  Other means to setup
-an StGIT stack are stglink:clone[] and the '--create' and '--clone'
-commands of stglink:branch[].
-
-StGIT
------
-Part of the StGIT suite - see gitlink:stg[1].
diff --git a/Documentation/stg-new.txt b/Documentation/stg-new.txt
deleted file mode 100644 (file)
index fbf2f67..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-stg-new(1)
-==========
-Yann Dirson <ydirson@altern.org>
-v0.12.1, February 2007
-
-NAME
-----
-stg-new - stgdesc:new[]
-
-SYNOPSIS
---------
-[verse]
-'stg' new [OPTIONS] [name]
-
-DESCRIPTION
------------
-
-Create a new, empty patch with the given <name> on the current stack.
-The new patch is created on top of the currently applied patches, and
-is made the new top of the stack.  The local changes in the working
-tree are not included in the patch. A stglink:refresh[] command is
-needed for this.
-
-The given <name> must be unique in the stack, and may only contain
-alphanumeric characters, dashes and underscores. If no name is given,
-one is generated from the first line of the commit message.
-
-An editor will be launched to edit the commit message to be used for
-the patch, unless the '--message' flag already specified one.  The
-'patchdescr.tmpl' template file is used if available to pre-fill the
-editor.  The editor to use is taken from the first of the following
-sources of information, and defaults to 'vi':
-
-. the 'stgit.editor' GIT configuration variable
-. the 'EDITOR' environment variable
-
-The message and other GIT commit attributes can be modified later
-using stglink:refresh[].
-
-AUTHOR AND COMMITTER INFORMATION
---------------------------------
-
-The author name (resp. email) to record in the StGIT patch is taken
-from the first of the following sources for the information:
-
-. the '--authname' (resp. '--authemail') or '--author' flag on command-line
-. the 'GIT_AUTHOR_NAME' (resp. 'GIT_AUTHOR_EMAIL') environment variable
-. the 'user.name' (resp. 'user.email') GIT configuration variable
-
-Similarly, the committer name (resp. email) is taken from the first of
-the following sources:
-
-. the '--commname' (resp. '--commemail') flag on command-line
-. the 'GIT_COMMITTER_NAME' (resp. 'GIT_COMMITTER_EMAIL') environment variable
-. the 'user.name' (resp. 'user.email') GIT configuration variable
-
-The GIT commit generated by stglink:refresh[] will use these
-informations when available.  If one of them is not available, GIT
-will pick the value from your machine's configuration at that time, as
-described in gitlink:git-commit-tree[1].
-
-OPTIONS
--------
-
---message=<message>::
--m <message>::
-       Use <message> as the patch description.
-
---showpatch::
--s::
-       Show the patch content in the editor buffer.  This flag does
-       nothing if '-m' is also specified.
-
---author="Name <email@company>"::
--a "Name <email@company>"::
-       Use "Name <email@company>" as the author details.  This form
-       sets both 'authname' and 'authemail'.
-
---authname=<name>::
-       Use <name> as the author name
---authemail=<email>::
-       Use <email> as the author e-mail
---authdate=<date>::
-       Use <date> as the author date
-
---commname=<name>::
-       Use <name> as the committer name
---commemail=<email>::
-       Use <email> as the committer e-mail
-
-FILES
------
-
-       <templates>/patchdescr.tmpl
-
-ENVIRONMENT VARIABLES
----------------------
-
-       GIT_AUTHOR_NAME
-       GIT_AUTHOR_EMAIL
-       GIT_AUTHOR_DATE
-       GIT_COMMITTER_NAME
-       GIT_COMMITTER_EMAIL
-       EDITOR
-
-GIT CONFIGURATION VARIABLES
----------------------------
-
-       user.name
-       user.email
-       stgit.editor
-
-StGIT
------
-Part of the StGIT suite - see gitlink:stg[1].
diff --git a/Documentation/stg-sink.txt b/Documentation/stg-sink.txt
deleted file mode 100644 (file)
index 0f569be..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-stg-sink(1)
-===========
-Yann Dirson <ydirson@altern.org>
-v0.13, April 2007
-
-NAME
-----
-stg-sink - stgdesc:sink[]
-
-SYNOPSIS
---------
-[verse]
-'stg' sink [--to=<target>] [--nopush] [<patches>]
-
-DESCRIPTION
------------
-
-This is the opposite operation of stglink:float[]: move the specified
-patches down the stack.  It is for example useful to group stable
-patches near the bottom of the stack, where they are less likely to be
-impacted by the push of another patch, and from where they can be more
-easily committed or pushed.
-
-If no patch is specified on command-line, the current patch gets sunk.
-By default patches are sunk to the bottom of the stack, but the
-'--to' option allows to place them under any applied patch.
-
-Sinking internally involves popping all patches (or all patches
-including <target patch>), then pushing the patches to sink, and then
-(unless '--nopush' is also given) pushing back into place the
-formerly-applied patches.
-
-
-OPTIONS
--------
-
---to=<TARGET>::
--t <TARGET>::
-       Specify a target patch to place the patches below, instead of
-       sinking them to the bottom of the stack.
-
---nopush::
--n::
-       Do not push back on the stack the formerly-applied patches.
-       Only the patches to sink are pushed.
-
-StGIT
------
-Part of the StGIT suite - see gitlink:stg[1].
index f6cd815..fc8fd7c 100644 (file)
@@ -1,11 +1,10 @@
 stg(1)
 ======
 Yann Dirson <ydirson@altern.org>
-v0.12.1, February 2007
 
 NAME
 ----
-stg - manage stacks of patches using the GIT content tracker
+stg - Manage stacks of patches using the Git content tracker
 
 SYNOPSIS
 --------
@@ -17,86 +16,121 @@ SYNOPSIS
 DESCRIPTION
 -----------
 
-StGIT (Stacked GIT) is an application providing similar functionality
-to Quilt (i.e. pushing/popping patches to/from a stack), on top of
-GIT. These operations are performed using GIT commands and the patches
-are stored as GIT commit objects, allowing easy merging of the StGIT
-patches into other repositories using standard GIT functionality.
+StGit (Stacked Git) is an application that provides a convenient way
+to maintain a 'patch stack' on top of a Git branch:
 
-An StGIT stack is a GIT branch with additional information to help
-making changes to individual patches you already committed, rather
-than making changes by adding new commits.  It is thus a
-non-forwarding, or rewinding branch: the old head of the branch is
-often not reachable as one of the new head's ancestors.
+  * The topmost (most recent) commits of a branch are given names.
+    Such a named commit is called a 'patch'.
 
-Typical uses of StGIT include:
+  * After making changes to the worktree, you can incorporate the
+    changes into an existing patch; this is called 'refreshing'. You
+    may refresh any patch, not just the topmost one.
+
+  * You can 'pop' a patch: temporarily putting it aside, so that the
+    patch below it becomes the topmost patch. Later you may 'push' it
+    onto the stack again. Pushing and popping can be used to reorder
+    patches.
+
+  * You can easily 'rebase' your patch stack on top of any other Git
+    commit. (The 'base' of a patch stack is the most recent Git commit
+    that is not an StGit patch.) For example, if you started making
+    patches on top of someone else's branch, and that person publishes
+    an updated branch, you can take all your patches and apply them on
+    top of the updated branch.
+
+  * As you would expect, changing what is below a patch can cause that
+    patch to no longer apply cleanly -- this can occur when you
+    reorder patches, rebase patches, or refresh a non-topmost patch.
+    StGit uses Git's rename-aware three-way merge capability to
+    automatically fix up what it can; if it still fails, it lets you
+    manually resolve the conflict just like you would resolve a merge
+    conflict in Git.
+
+  * The patch stack is just some extra metadata attached to regular
+    Git commits, so you can continue to use most Git tools along with
+    StGit.
+
+Typical uses
+~~~~~~~~~~~~
 
 Tracking branch::
-       Tracking changes from a remote branch, while maintaining local
-       modifications against that branch, possibly with the intent of
-       sending some patches upstream.  StGIT assists in preparing and
-       cleaning up patches until they are acceptable upstream, as
-       well as maintaining local patches not meant to be sent
-       upstream.
+
+  Tracking changes from a remote branch, while maintaining local
+  modifications against that branch, possibly with the intent of
+  sending some patches upstream. You can modify your patch stack as
+  much as you want, and when your patches are finally accepted
+  upstream, the permanent recorded Git history will contain just the
+  final sequence of patches, and not the messy sequence of edits that
+  produced them.
 +
-In such a setup, typically all commits on your branch are StGIT
-patches; the stack base is the branch point where your changes "fork"
-off their parent branch.
+Commands of interest in this workflow are e.g. stgsublink:rebase[] and
+stgsublink:mail[].
 
 Development branch::
-       Preparing and testing your commits before publishing them,
-       separating your features from unrelated bugfixes collected
-       while developping.
+
+  Even if you have no "upstream" to send patches to, you can use StGit
+  as a convenient way to modify the recent history of a Git branch.
+  For example, instead of first committing change 'A', then change
+  'B', and then 'A2' to fix 'A' because it wasn't quite right, you
+  could incorporate the fix directly into 'A'. This way of working
+  results in a much more readable Git history than if you had
+  immortalized every misstep you made on your way to the right
+  solution.
 +
-In such a setup, not all commits on your branch need to be StGIT
-patches; there may be regular GIT commits below your stack base.
+Commands of interest in this workflow are e.g. stgsublink:uncommit[],
+which can be used to move the patch stack base downwards -- i.e., turn
+Git commits into StGit patches after the fact -- and
+stgsublink:commit[], its inverse.
+
+For more information, see htmllink:tutorial.html[the tutorial].
 
-Patches
-~~~~~~~
+Specifying patches
+~~~~~~~~~~~~~~~~~~
 
-Many StGIT commands take references to StGIT patches as arguments.
+Many StGit commands take references to StGit patches as arguments.
 Patches in the stack are identified with short names, each of which
 must be unique in the stack.
 
-Patches in the current stack are just referred to by their name.  Some
-commands allow you to specify a patch in another stack of the repository;
-this is done by suffixing the patch name with an '@' sign followed by the
-branch name (eg. 'thispatch@otherbranch').
+Patches in the current branch are simply referred to by their name.
+Some commands allow you to specify a patch in another branch of the
+repository; this is done by prefixing the patch name with the branch
+name and a colon (e.g. +otherbranch:thatpatch+).
+
+Specifying commits
+~~~~~~~~~~~~~~~~~~
+
+Some StGit commands take Git commits as arguments. StGit accepts all
+commit expressions that Git does; and in addition, a patch name
+(optionally prefixed by a branch name and a colon) is allowed in this
+context. The usual Git modifiers $$^$$ and $$~$$ are also allowed;
+e.g., +abranch:apatch~2+ is the grandparent of the commit that is the
+patch +apatch+ on branch +abranch+.
 
-A number of positions in the stack related to the patch are also
-accessible through '//' suffixes.  For example, 'patch//top' is
-equivalent to 'patch', and 'patch//bottom' refers to the commit below
-'patch' (i.e. the patch below, or the stack base if this is the
-bottom-most patch).  Similarly '//top.old' and '//bottom.old'
-refer to the previous version of the patch (before the last
-stglink:push[] or stglink:refresh[] operation).  When referring to the
-current patch, its name can be omitted (eg. 'currentpatch//bottom.old'
-can be abbreviated as 'bottom.old').
+Instead of a patch name, you can say +$${base}$$+ to refer to the
+stack base (the commit just below the bottommost patch); so,
++abranch:$${base}$$+ is the base of the stack in branch +abranch+.
 
-If you need to pass a given StGIT reference to a git command,
-stglink:id[] will convert it to a git commit id.
+If you need to pass a given StGit reference to a Git command,
+stglink:id[] will convert it to a Git commit id for you.
 
 OPTIONS
 -------
 
-The following generic option flags are available.  Additional options
-are available per-command, and documented in the command-specific
-documentation.
+The following generic option flags are available. Additional options
+are available for (and documented with) the different subcommands.
 
 --version::
-       Prints the StGIT suite version that the 'stg' program came
-       from, as well as version of other components used, such as GIT
-       and Python.
+  Prints the StGit version, as well as version of other components
+  used, such as Git and Python.
 
 --help::
-       Prints the synopsis and a list of all commands.  If a git
-       command is given this option will display the specific help
-       for that command.
+  Prints the synopsis and a list of all subcommands. If an StGit
+  subcommand is given, prints the synposis for that subcommand.
 
 STGIT COMMANDS
 --------------
 
-We divide StGIT commands in thematic groups, according to the primary
+We divide StGit commands in thematic groups, according to the primary
 type of object they create or change.
 
 ifdef::backend-docbook[]
@@ -105,155 +139,21 @@ description is available in individual command manpages.  Those
 manpages are named 'stg-<command>(1)'.
 endif::backend-docbook[]
 
-Generic commands
-~~~~~~~~~~~~~~~~
-
-User-support commands not touching the repository.
-
-stg help::
-       stgdesc:help[]
-stg version::
-       stgdesc:version[]
-stg copyright::
-       stgdesc:copyright[]
-
-Repository commands
-~~~~~~~~~~~~~~~~~~~
-
-stglink:clone[]::
-       stgdesc:clone[]
-stglink:id[]::
-       stgdesc:id[]
-
-Stack commands
-~~~~~~~~~~~~~~
-
-Stack management
-^^^^^^^^^^^^^^^^
-
-stglink:branch[]::
-       stgdesc:branch[]
-stglink:init[]::
-       stgdesc:init[]
-stglink:clean[]::
-       stgdesc:clean[]
-stglink:pull[]::
-       stgdesc:pull[]
-stglink:rebase[]::
-       stgdesc:rebase[]
-
-stglink:commit[]::
-       stgdesc:commit[]
-stglink:uncommit[]::
-       stgdesc:uncommit[]
-stglink:repair[]::
-       stgdesc:repair[]
-
-Controlling what patches are applied
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-stglink:series[]::
-       stgdesc:series[]
-stglink:push[]::
-       stgdesc:push[]
-stglink:pop[]::
-       stgdesc:pop[]
-stglink:goto[]::
-       stgdesc:goto[]
-stglink:float[]::
-       stgdesc:float[]
-stglink:sink[]::
-       stgdesc:sink[]
-stglink:applied[]::
-       stgdesc:applied[]
-stglink:unapplied[]::
-       stgdesc:unapplied[]
-stglink:top[]::
-       stgdesc:top[]
-
-stglink:hide[]::
-       stgdesc:hide[]
-stglink:unhide[]::
-       stgdesc:unhide[]
-
-Miscellaneous stack commands
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-stglink:patches[]::
-       stgdesc:patches[]
-
-
-Patch commands
-~~~~~~~~~~~~~~
-
-Patch management
-^^^^^^^^^^^^^^^^
-
-stglink:new[]::
-       stgdesc:new[]
-stglink:delete[]::
-       stgdesc:delete[]
-stglink:rename[]::
-       stgdesc:rename[]
-stglink:log[]::
-       stgdesc:log[]
-
-Controlling patch contents
-^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-stglink:files[]::
-       stgdesc:files[]
-stglink:show[]::
-       stgdesc:show[]
-stglink:refresh[]::
-       stgdesc:refresh[]
-stglink:fold[]::
-       stgdesc:fold[]
-stglink:pick[]::
-       stgdesc:pick[]
-stglink:sync[]::
-       stgdesc:sync[]
-
-Interaction with the rest of the world
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-stglink:export[]::
-       stgdesc:export[]
-stglink:import[]::
-       stgdesc:import[]
-stglink:mail[]::
-       stgdesc:mail[]
-
-
-Working-copy commands
-~~~~~~~~~~~~~~~~~~~~~
-
-stglink:add[]::
-       stgdesc:add[]
-stglink:rm[]::
-       stgdesc:rm[]
-stglink:cp[]::
-       stgdesc:cp[]
-stglink:status[]::
-       stgdesc:status[]
-stglink:diff[]::
-       stgdesc:diff[]
-stglink:resolved[]::
-       stgdesc:resolved[]
+include::command-list.txt[]
 
 CONFIGURATION MECHANISM
 -----------------------
 
-Starting with 0.12, StGIT uses the same configuration mechanism as
-GIT.  See gitlink:git[7] for more details.
+StGit uses the same configuration mechanism as Git. See manlink:git[7]
+for more details.
 
 TEMPLATES
 ---------
 
-A number of StGIT commands make use of template files to provide
-useful default texts to be edited by the user.  These '<name>.tmpl'
+A number of StGit commands make use of template files to provide
+useful default texts to be edited by the user. These +<name>.tmpl+
 template files are searched in the following directories:
 
-       $GITDIR/
-       $HOME/.stgit/templates/
-       /usr/share/stgit/templates/
+  . +$GITDIR/+ (in practice, the +.git/+ directory in your repository)
+  . +$HOME/.stgit/templates/+
+  . +/usr/share/stgit/templates/+
index b040d29..283b358 100644 (file)
-StGIT Tutorial
+StGit tutorial
 ##############
 
+StGit is a command-line application that provides functionality
+similar to htmllink:http://savannah.nongnu.org/projects/quilt/[Quilt]
+(i.e. pushing/popping patches to/from a stack), but using Git instead
+of +diff+ and +patch+. StGit stores its patches in a Git repository as
+normal Git commits, and provides a number of commands to manipulate
+them in various ways.
 
-StGIT is a Python application that provides functionality similar to
-quilt (i.e. pushing/popping patches to/from a stack) using GIT instead
-of 'diff' and 'patch'. StGIT stores its patches in a GIT repository as
-normal GIT commit objects.
-StGIT is not an SCM interface on top of GIT. For standard SCM
-operations, either use GIT's porcelain commands or the Cogito tool.
-StGIT is available for download at http://www.procode.org/stgit/ .
-This tutorial assumes you are already familiar with GIT. For more
-information on GIT, see the GIT_tutorial or git(7) .
+This tutorial assumes you are already familiar with the basics of Git
+(for example, branches, commits, and conflicts). For more information
+on Git, see manlink:git[1] or htmllink:http://git.or.cz/[the Git home
+page].
 
 
-Basic Operation
+Help
+====
+
+For a full list of StGit commands:
+
+  $ stg help
+
+For quick help on individual subcommands:
+
+  $ stg help <cmd>
+
+For more extensive help on a subcommand:
+
+  $ man stg-<cmd>
+
+(The documentation is also available in htmllink:stg.html[HTML
+format].)
+
+
+Getting started
 ===============
 
-Help
-----
+StGit is not a stand-alone program -- it operates on a Git repository
+that you have already created, using +git init+ or +git clone+. So get
+one of those; if you don't have one at hand, try for example
+
+  $ git clone http://homepage.ntlworld.com/cmarinas/stgit.git
+  $ cd stgit
+
+Before you can create StGit patches, you have to run stglink:init[]:
+
+  $ stg init
+
+This initializes the StGit metadata for the current branch. (So if you
+want to have StGit patches in another branch too, you need to run +stg
+init+ again in that branch.)
+
+NOTE: As a shortcut, stglink:clone[] will run +git clone+ followed by
++stg init+ for you.
+
+
+Creating a patch
+----------------
+
+Now we're ready to create our first patch:
+
+  $ stg new my-first-patch
+
+This will create a patch called +my-first-patch+, and open an editor
+to let you edit the patch's commit message. (If you don't give a name
+on the command line, StGit will make one up based on the first line of
+the commit message.) This patch is empty, as stglink:show[] will tell
+you:
+
+  $ stg show
+
+But it won't stay that way for long! Open one of the files in your
+favorite text editor, change something, and save. You now have some
+local changes in your tree:
 
-For a full list of StGIT commands:
+  $ stg status
+  M stgit/main.py
 
-  stg help
+Then stgsublink:refresh[] the patch:
 
-For help on individual subcommands:
+  $ stg refresh
 
-  stg <cmd> (-h | --help)
+And voilà -- the patch is no longer empty:
 
+  $ stg show
+  commit 3de32068c600d40d8af2a9cf1f1c762570ae9610
+  Author: Audrey U. Thor <author@example.com>
+  Date:   Sat Oct 4 16:10:54 2008 +0200
 
-Repository initialisation
--------------------------
+      Tell the world that I've made a patch
 
-In stand-alone mode, StGIT is used in conjunction with a GIT repository
-that is already initialised (using 'git init'). StGIT cannot be used
-outside of a GIT repository.
-Any branch in a GIT repository may be managed by StGIT. Each branch
-managed by StGIT contains an independent series of StGIT patches.
-To initialise an existing GIT branch to be managed by StGIT, cd into the
-top of your GIT repository, check out the branch you'd like to manage
-with StGIT, and type:
+  diff --git a/stgit/main.py b/stgit/main.py
+  index e324179..6398958 100644
+  --- a/stgit/main.py
+  +++ b/stgit/main.py
+  @@ -171,6 +171,7 @@ def _main():
+       sys.exit(ret or utils.STGIT_SUCCESS)
+
+   def main():
+  +    print 'My first patch!'
+       try:
+           _main()
+       finally:
+
+(I'm assuming you're already familiar with
+htmllink:http://en.wikipedia.org/wiki/Diff#Unified_format[unified
+diff] patches like this from Git, but it's really quite simple; in
+this example, I've added the +$$print 'My first patch!'$$+ line to the
+file +stgit/main.py+, at around line 171.)
+
+Since the patch is also a regular Git commit, you can also look at it
+with regular Git tools such as manlink:gitk[].
+
+Creating another patch
+----------------------
+
+We want to make another improvement, so let's create a new patch for
+it:
+
+  $ echo 'Audrey U. Thor' > AUTHORS
+  $ stg new credit --message 'Give me some credit'
+  $ stg refresh
+
+Note that we can give the commit message on the command line, and that
+it doesn't matter whether we run stglink:new[] before or after we edit
+the files.
+
+So now we have two patches:
+
+  $ stg series --description
+  + my-first-patch # This is my first patch
+  > credit         # Give me some credit
+
+stglink:series[] lists the patches from bottom to top; +$$+$$+ means
+that a patch is 'applied', and +>+ that it is the 'current', or
+topmost, patch.
+
+If we want to make further changes to the topmost patch, we just edit
+the files and run +stg refresh+. But what if we wanted to change
++my-first-patch+? The simplest way is to stgsublink:pop[] the +credit+
+patch, so that +my-first-patch+ becomes topmost again:
+
+  $ stg pop credit
+  Checking for changes in the working directory ... done
+  Popping patch "credit" ... done
+  Now at patch "my-first-patch"
+  $ stg series --description
+  > my-first-patch # This is my first patch
+  - credit         # Give me some credit
+
+stglink:series[] now shows that +my-first-patch+ is topmost again,
+which means that stglink:refresh[] will update it with any changes we
+make.
+
+The minus sign says that +credit+ is 'unapplied' -- this means that
+it's been temporarily put aside. If you look at the +AUTHORS+ file,
+you'll see that our change to it is gone; and tools such as
+manlink:gitk[] will not show it, because it's been edited out of the
+Git history. But it's just one stglink:push[] command away from being
+restored:
+
+  $ stg push credit
+  Checking for changes in the working directory ... done
+  Fast-forwarded patch "credit"
+  Now at patch "credit"
+
+NOTE: You can omit the patch name argument to stglink:push[] and
+stglink:pop[]. If you do, you will push the next unapplied patch, and
+pop the topmost patch, respectively.
+
+NOTE: There are at least two more ways to update a non-topmost patch.
+One is to use stglink:refresh[] with the +$$--patch$$+ flag, the other
+to create a new patch for the update and then merge it into the other
+patch with stglink:coalesce[].
+
+
+Keeping commit messages up to date
+----------------------------------
+
+Since StGit is all about creating readable Git history (or a readable
+patch series, which is essentially the same thing), one thing you'll
+want to pay attention to is the commit messages of your patches.
+stglink:new[] asks you for a commit message when you create a new
+patch, but as time goes by and you refresh the patch again and again,
+chances are that the original commit message isn't quite correct
+anymore. Fortunately, editing the commit message is very easy:
+
+  $ stg edit <patch-name>
+
+In addition to stglink:edit[], you can also give the +$$--edit$$+ flag
+to stglink:refresh[] -- that way, you get to change the commit message
+and update the patch at the same time. Use whichever feels most
+natural to you.
+
+NOTE: stglink:edit[] has a +$$--diff$$+ flag, which gives you the diff
+text and not just the commit message in your editor. Be aware, though,
+that if you change the diff so that it no longer applies, the edit
+will be saved to a file instead of being carried out. If you're not
+comfortable editing diffs, just treat +$$--diff$$+ as a way to get to
+'see' the diff while you edit the commit message.
+
+If the patch changes considerably, it might even deserve a new name.
+stglink:rename[] is your friend there.
+
+
+Conflicts
+---------
+
+Normally, when you pop a patch, change something, and then later push
+it again, StGit sorts out everything for you automatically. For
+example, let's create two patches that modify different files:
+
+  $ stg clone http://homepage.ntlworld.com/cmarinas/stgit.git stgit
+  $ cd stgit
+  $ stg new first --message 'First patch'
+  $ echo '- Do something' >> TODO
+  $ stg refresh
+  $ stg new second --message 'Second patch'
+  $ echo '- Install something' >> INSTALL
+  $ stg refresh
+
+then pop them both:
+
+  $ stg pop --all
+
+and then push them in the opposite order:
+
+  $ stg push second first
+  $ stg series
+  + second
+  > first
+
+StGit had no problems reordering these patches for us, since they
+didn't touch the same file. But it would have worked just fine even if
+they had touched the same file, as long as they didn't change the same
+part of the file. But what if they did? Let's find out.
+
+  $ stg pop
+  Checking for changes in the working directory ... done
+  Popping patch "first" ... done
+  Now at patch "second"
+  $ echo '- Do something else' >> TODO
+  $ stg refresh
+
+Now, both patches add a new line at the end of +TODO+. So what happens
+when we try to have them both applied?
+
+  $ stg push
+  Pushing patch "first" ...
+    CONFLICT (content): Merge conflict in TODO
+    Error: The merge failed during "push".
+           Revert the operation with "stg undo".
+    stg push: 1 conflict(s)
+
+StGit is telling us that it couldn't figure out how to push +first+ on
+top of +second+, now that they both modify +TODO+. We can take a look
+at the situation with stglink:status[]:
+
+  $ stg status
+  ? TODO.ancestor
+  ? TODO.current
+  ? TODO.patched
+  C TODO
+
+As we were told by stglink:push[], the conflict is in the file +TODO+.
+(If the patch was bigger and touched multiple files, they would all be
+listed here; prefixed with +C+ if they had conflicts, and +M+ if StGit
+managed to automatically resolve everything in the file.)
+
+NOTE: +TODO.ancestor+, +TODO.current+, and +TODO.patched+ are the
+three versions of the file that StGit tried to merge. The +.current+
+file is the version before the patch was applied, +.patched+ is the
+version in the patch we tried to push, and +.ancestor+ the version
+that contains neither of the added lines.
+
+At this point, we have two options:
+
+  1. Undo the failed merge with stglink:undo[]. (Remember to use the
+     +$$--hard$$+ flag, since the unresolved conflict means the
+     worktree is not clean.)
+
+  2. Manually resolve the conflict.
+
+To resolve the conflict, open +TODO+ in your favorite editor. It ends
+like this:
+
+----------------------------------------------------------------------
+- numeric shortcuts for naming patches near top (eg. +1, -2)
+- (config?) parameter for number of patches included by "series -s"
+<<<<<<< current:TODO
+- Do something else
+=======
+- Do something
+>>>>>>> patched:TODO
+----------------------------------------------------------------------
+
+The 'conflict markers' +<<<<<<<+, +=======+, and +>>>>>>>+ indicate
+which lines were already there (+current+) and which were added by the
+patch (+patched+). Edit the file so that it looks like it should; in
+this case, we want something like this:
+
+----------------------------------------------------------------------
+- numeric shortcuts for naming patches near top (eg. +1, -2)
+- (config?) parameter for number of patches included by "series -s"
+- Do something
+- Do something else
+----------------------------------------------------------------------
+
+Note that ``looks like it should'' includes removing the conflict
+markers.
+
+Now that we've resolved the conflict, we just need to tell StGit about
+it:
+
+  $ stg resolved TODO
+  $ stg status
+  M TODO
+
++TODO+ is listed as being modified, not in conflict. And we know from
+before how to deal with modified files:
+
+  $ stg refresh
+
+The conflict is now resolved. We can see that +first+ now looks a
+little different; it no longer adds a line at the end of the file:
+
+  $ stg show
+  commit 8e3ae5f6fa6e9a5f831353524da5e0b91727338e
+  Author: Audrey U. Thor <author@example.com>
+  Date:   Sun Oct 5 14:43:42 2008 +0200
+
+      First patch
+
+  diff --git a/TODO b/TODO
+  index 812d236..4ef3841 100644
+  --- a/TODO
+  +++ b/TODO
+  @@ -24,4 +24,5 @@ The future, when time allows or if someone else does them:
+     they have scripts for moving the changes in one to the others)
+   - numeric shortcuts for naming patches near top (eg. +1, -2)
+   - (config?) parameter for number of patches included by "series -s"
+  +- Do something
+   - Do something else
+
+
+Workflow: Development branch
+============================
+
+One common use of StGit is to ``polish'' a Git branch before you
+publish it for others to see. Such history falsification can often be
+a 'good' thing -- when you (or someone else) needs to look at what you
+did six months later, you are not really interested in all the false
+starts and the steps needed to corect them. What you want is the final
+solution, presented in a way that makes it easy to read and
+understand.
+
+Of course, there are limits. Editing the last few days' worth of
+history is probably a good idea; editing the last few months' probably
+isn't. A rule of thumb might be to not mess with history old enough
+that you don't remember the details anymore. And rewriting history
+that you have published for others to see (and base their own work on)
+usually just makes everyone more confused, not less.
+
+So, let's take a concrete example. Say that you're hacking on StGit,
+and have made several Git commits as your work progressed, with commit
+messages such as ``Improve the snarfle cache'', ``Remove debug
+printout'', ``New snarfle cache test'', ``Oops, spell function name
+correctly'', ``Fix documentation error'', and ``More snarfle cache''.
+
+Now, this is the actual history, but for obvious reasons, this isn't
+the kind of history you'd ideally want to find when you six months
+from now try to figure out exactly where that elusive snarfle cache
+bug was introduced. So let's turn this into the history we can be
+proud of. The first step is to make StGit patches out of all those Git
+commits:
+
+  $ stg uncommit --number 6
+  Uncommitting 6 patches ...
+    Now at patch "more-snarfle-cache"
+  done
+  $ stg series --description
+  + improve-the-snarfle-cache      # Improve the snarfle cache
+  + remove-debug-printout          # Remove debug printout
+  + new-snarfle-cache-test         # New snarfle cache test
+  + oops-spell-function-name-corre # Oops, spell function name correctly
+  + fix-documentation-error        # Fix documentation error
+  > more-snarfle-cache             # More snarfle cache
+
+As you can see, stglink:uncommit[] adds StGit metadata to the last few
+Git commits, turning them into StGit patches so that we can do stuff
+with them.
+
+NOTE: With the +$$--number$$+ flag, stglink:uncommit[] uncommits that
+many commits and generates names for them based on their commit
+messages. If you like, you can instead list the patch names you want
+on the command line.
+
+At this point, there are a number of things we could do:
+
+  * Continue developing, and take advantage of e.g. stglink:goto[] or
+    +stg refresh $$--patch$$+ to stick updates in the right patch to
+    begin with.
+
+  * Use e.g. stglink:float[], stglink:sink[], stglink:push[], and
+    stglink:pop[] to reorder patches.
+
+  * Use stglink:coalesce[] to merge two or more patches into one.
+    stgsublink:coalesce[] pushes and pops so that the patches to be
+    merged are consecutive and unrelated patches aren't in the way,
+    then makes one big patch out of the patches to be merged, and
+    finally pushes the other patches back.
++
+Of course, as always when there is pushing involved, there is the
+possibility of conflicts. If a push results in a conflict, the
+operation will be halted, and we'll be given the option of either
+resolving the conflict or undoing.
+
+Once we feel that the history is as good as it's going to get, we can
+remove the StGit metadata, turning the patches back into regular Git
+commits again:
+
+  $ stg commit --all
+
+TIP: stglink:commit[] can also commit specific patches (named on the
+command line), leaving the rest alone. This can be used to retire
+patches as they mature, while keeping the newer and more volatile
+patches as patches.
+
+
+Workflow: Tracking branch
+=========================
+
+In the 'Development branch' workflow described above, we didn't have
+to worry about other people; we're working on our branch, they are
+presumably working on theirs, and when the time comes and we're ready
+to publish our branch, we'll probably end up merging our branch with
+those other peoples'. That's how Git is designed to work.
+
+Or rather, one of the ways Git is designed to work. An alternative,
+popular in e.g. the Linux kernel community (for which Git was
+originally created), is that contributors send their patches by e-mail
+to a mailing list. Others read the patches, try them out, and provide
+feedback; often, the patch author is asked to send a new and improved
+version of the patches. Once the project maintainer is satisfied that
+the patches are good, she'll 'apply' them to a branch and publish it.
+
+StGit is ideally suited for the process of creating patches, mailing
+them out for review, revising them, mailing them off again, and
+eventually getting them accepted.
+
+
+Getting patches upstream
+------------------------
 
-  stg init
+We've already covered how to clone a Git repository and start writing
+patches. As for the next step, there are two commands you might use to
+get patches out of StGit: stglink:mail[] and stglink:export[].
+stglink:export[] will export your patches to a filesystem directory as
+one text file per patch, which can be useful if you are going to send
+the patches by something other than e-mail. Most of the time, though,
+stglink:mail[] is what you want.
+
+NOTE: Git comes with tools for sending commits via e-mail. Since StGit
+patches are Git commits, you can use the Git tools if you like them
+better for some reason.
 
-Run the 'stg init' command for any pre-existing GIT branches intended to
-be used with StGIT.
-You can switch between GIT branches with:
+NOTE: For exporting single patches -- as opposed to a whole bunch of
+them -- you could also use stglink:show[] or stglink:diff[].
 
-  stg branch [<branch name>]
+Mailing a patch is as easy as this:
 
-This checks out the named branch and places you at the topmost applied
-StGIT patch in that branch.
-Alternately, you can create branches using only StGIT commands, which
-will automatically prepare them for use with StGIT:
+  $ stg mail --to recipient@example.com <patches>
 
-  stg branch --create <new branch>
+You can list one or more patches, or ranges of patches. Each patch
+will be sent as a separate mail, with the first line of the commit
+message as subject line. Try mailing patches to yourself to see what
+the result looks like.
 
+NOTE: stglink:mail[] uses +sendmail+ on your computer to send the
+mails. If you don't have +sendmail+ properly set up, you can instruct
+it to use any SMTP server with the +$$--smtp-server$$+ flag.
 
-Working with remote repositories
---------------------------------
+There are many command-line options to control exactly how mails are
+sent, as well as a message template you can modify if you want. The
+man page has all the details; I'll just mention two more here.
 
-With a single command, StGIT can create and initialize a GIT repository
-which mirrors a remote GIT repository. This is known as cloning. All GIT
-transports are supported.
-To clone a repository, use:
++$$--edit-cover$$+ will open an editor and let you write an
+introductory message; all the patch mails will then be sent as replies
+to this 'cover message'. This is usually a good idea if you send more
+than one patch, so that reviewers can get a quick overview of the
+patches you sent.
 
-  stg clone <repository> <local-dir>
++$$--edit-patches$$+ will let you edit each patch before it is sent.
+You can change anything, but note that you are only editing the
+outgoing mail, not the patch itself; if you want to make changes to
+the patch, you probably want to use the regular StGit commands to do
+so. What this 'is' useful for, though, is to add notes for the patch
+recipients:
 
-This creates a fresh local repository, initialises a GIT database in it,
-pulls the latest version of the remote, and creates and initialises a
-'master' branch for use with StGIT.
-At any time you can pull the latest changes from the remote repository.
-By default, StGIT pulls from the location stored in .git/branches/
-origin, and updates the base of the current branch.
-To pull the latest changes from a remote repository, use:
+----------------------------------------------------------------------
+From: Audrey U. Thor <author@example.com>
+Subject: [PATCH] First line of the commit message
 
-  stg pull [<branch> or 'origin']
+The rest of the commit message
 
-This command removes all applied StGIT patches from the current branch,
-updates the branch's base commit, then attempts to re-apply the patches.
-Any merge conflicts will halt this process, allowing you to clean up the
-conflicts and continue (see below).
-If the maintainer of the remote repository includes one of your patches
-in the published repository that you pull from, StGIT can usually
-recognize that an incoming patch from the remote matches one of yours,
-and it turns your local version into an empty patch.
-To automatically delete empty patches after a pull, use:
+---
 
-  stg clean
+Everything after the line with the three dashes and before the diff is
+just a comment, and not part of the commit message. If there's
+anything you want the patch recipients to see, but that shouldn't be
+recorded in the history if the patch is accepted, write it here.
 
-As a convention, you should avoid working in the 'master' branch and use
-it only as a reference, since it reflects someone else's work. If you
-decide to publish your GIT repository, you'll want your own work
-separated into its own branch to make it convenient for others to pull
-just your patches.
+ stgit/main.py |    1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
 
-Getting started: creating a patch
----------------------------------
 
-Changes to your working directory are saved in a patch. An StGIT patch
-is simply a saved set of modifications to your working directory, plus a
-saved description. To create an empty StGIT patch in the current branch:
+diff --git a/stgit/main.py b/stgit/main.py
+index e324179..6398958 100644
+--- a/stgit/main.py
++++ b/stgit/main.py
+@@ -171,6 +171,7 @@ def _main():
+     sys.exit(ret or utils.STGIT_SUCCESS)
 
-  stg new <name>
+ def main():
++    print 'My first patch!'
+     try:
+         _main()
+     finally:
+----------------------------------------------------------------------
 
-To save the changes you've made (that is, to refresh a patch), use:
 
-  stg refresh
+Rebasing a patch series
+-----------------------
 
-To discard changes in your working directory, use:
+While you are busy writing, submitting, and revising your patch
+series, other people will be doing the same thing. As a result, even
+though you started writing your patches on top of what was the latest
+history at the time, your stack base will grow ever more out of date.
 
-  git checkout -f
+When you clone a repository,
 
-This restores your working directory to the state it was in the last
-time the patch was refreshed.
-Modified files that haven't been saved via a refresh operation can be
-viewed with:
+  $ stg clone http://homepage.ntlworld.com/cmarinas/stgit.git stgit
 
-  stg status
+you initially get one local branch, +master+. You also get a number of
+'remote' branches, one for each branch in the repository you cloned.
+In the case of the StGit repository, these are
++remotes/origin/stable+, +remotes/origin/master+, and
++remotes/origin/proposed+. +remotes+ means that it's not a local
+branch, just a snapshot of a branch in another repository; and
++origin+ is the default name for the first remote repository (you can
+set up more; see the man page for +git remote+).
 
-You can view modified files that have already been saved into a patch:
+Right after cloning, +master+ and +remotes/origin/master+ point at the
+same commit. When you start writing patches, +master+ will advance,
+and always point at the current topmost patch, but
++remotes/origin/master+ will stay the same because it represents the
+master branch in the repository you cloned from -- your 'upstream'
+repository.
 
-  stg files
+Unless you are the only one working on the project, however, the
+upstream repository will not stay the same forever. New commits will
+be added to its branches; to update your clone, run
 
-The 'stg refresh' command automatically notes changes to files that
-already exist in the working directory, but you have to tell StGIT
-explicitly if you add, remove, or rename files.
-To record the addition or deletion of files in your new patch:
+  $ git remote update
 
-  stg add [<file>*]
-  stg rm [<file>*]
+This will update all your remote branches, but won't touch your local
+branches. To get the latest changes into your local +master+ branch,
+use stglink:rebase[]:
 
-To record the renaming of a file in your new patch, issue both of these
-commands:
+  $ stg rebase remotes/origin/master
 
-  stg rm <oldfilename>
-  stg add <newfilename>
+This command will do three things:
 
+  1. Pop all patches, so that your local branch (+master+, in this
+     example) points at the stack base. This is the same commit that
+     +remotes/origin/master+ pointed at at the time you started
+     writing your patches.
 
-Stack manipulation: managing multiple patches
----------------------------------------------
+  2. Set the stack base to the given commit (the current, updated
+     value of +remotes/origin/master+).
 
-StGIT can manage more than one patch at a time. A series of StGIT
-patches in a GIT branch are known collectively as a stack. The new patch
-you created above is now the topmost patch in your stack. You can always
-see the name of the topmost (current) patch with:
+  3. Push the patches that were popped in the first step.
 
-  stg top
+The end result is that your patches are now applied on top of the
+latest version of +remotes/origin/master+.
 
-The topmost patch is used as the default patch for most StGIT
-operations. It is the default target of the 'stg refresh' command, for
-example.
-Patches that are pushed onto the stack are referred to as applied, and
-patches that are popped off the stack are referred to as unapplied.
-To push/pop a patch to/from a stack:
+The primary reason for rebasing is to reduce the amount of conflicts
+between your work and others'. If one of your patches changes the same
+part of the same file as a patch someone else has written, you will
+get a conflict when you run stglink:rebase[] the next time after the
+other person's patch has been accepted upstream. It is almost always
+less work to rebase often and resolve these one at a time, rather than
+a whole lot at once. After all, you have to rebase eventually; if you
+mail out patches that are based on an outdated branch, everyone who
+tries to apply them has to resolve the conflicts instead. There are
+more effective ways to get popular.
 
-  stg push [--all | <name>]
-  stg pop [--all]
 
-The last patch you pushed is the topmost patch. This patch is always in
-the applied list; StGIT can't operate on an unapplied patch unless you
-apply it first.
-You can display the order of patches in a stack with one of these
-commands:
+When your patches are accepted
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-  stg series
-  stg applied
-  stg unapplied
+If and when some or all of your patches are accepted upstream, you
+update and rebase just like usual -- but be sure to use the
++$$--merged$$+ flag to stglink:rebase[]:
 
-By default the 'stg push' command applies the first patch in the
-unapplied list, but you can push any patch in the unapplied list by
-giving the name of the patch. This is useful if you want to reorder the
-patches in a stack.
-During a push operation, merge conflicts can occur (especially if you
-are changing the order of the patches in your stack). If the push causes
-merge conflicts, they need to be fixed and 'stg resolved' run (see
-below). A 'push' operation can also be reverted with 'stg push --undo'.
-A few more stack basics; to rename a patch:
+  $ git remote update
+  $ stg rebase --merged remotes/origin/master
 
-  stg rename <old-name> <new-name>
+This flag makes the rebase operation better at detecting that your
+patches have been merged, at some cost in performance.
 
-To delete a patch:
+The patches that had been merged will still be present in your patch
+stack after the rebase, but they will be empty, since the change they
+added is now already present in the stack base. Run stglink:clean[] to
+get rid of such empty patches if you don't want them hanging around:
 
-  stg delete <name>
+  $ stg clean
 
-This permanently discards the named patch. In other words, the patch no
-longer appears in either the applied or unapplied lists, and cannot be
-reapplied to the series.
-You may want to make patches in your stack a permanent part of your GIT
-repository, for example if you are publishing your repository to others.
-To do this, use:
 
-  stg commit
+Importing patches
+-----------------
 
-This merges all applied patches in your patch series into the GIT
-repository and removes them from your stack. Use this command only if
-you want to permanently store the applied patches and no longer manage
-them with StGIT.
+While you are busy producing patches, there's hopefully someone -- the
+'maintainer' -- at the other end who recieves them and 'applies' them
+to her Git tree, which is then published for all (or parts of) the
+world to see.
 
-Converting between StGIT patches and text diffs
------------------------------------------------
+It's perfectly fine for this person to not have the foggiest idea what
+StGit is. In that case, she'll probably apply your patches with
+something like +git am+, and everything will just work, exactly as if
+you'd used Git to send those patches. But she might be an StGit user
+too, in which case she might use stglink:import[].
+
+There are basically four kinds if stuff you can import with
+stglink:import[]:
+
+  1. A patch in a file.
+
+  2. Several files containing one patch each, and a 'series' file
+     listing those other files in the correct order.
+
+  3. An e-mail containing a single patch.
+
+  4. A mailbox file (in standard Unix +mbox+ format) containing
+     multiple e-mails with one patch in each.
+
+
+Importing a plain patch
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Importing a plain patch, such as produced by e.g. GNU +diff+, +git
+diff+, +git show+, stglink:diff[], or stglink:show[], is very easy.
+Just say
+
+  $ stg import my-patch
+
+and you'll have a new patch at the top of your stack.
+
+If you don't give a file name on the command line, stglink:import[]
+will read the patch from its standard input -- in other words, you can
+pipe a patch to it directly from the command that produces it.
+
+By default, the new patch's name will be taken from the file name, and
+its commit message and author info will be taken from the beginning of
+the patch, if they are there. However, there are command line switches
+to override all of these things; see the man page for details.
+
+
+Importing several patches at once
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Some programs -- among them stglink:export[] -- will create a bunch of
+files with one patch in each, and a 'series' file (often called
++series+) listing the other files in the correct order. Give
++$$--series$$+ and the name of the series file to stglink:import[],
+and it will import all the patches for you, in the correct order.
+
+
+Importing a patch from an e-mail
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Importing a patch from an e-mail is simple too:
+
+  $ stg import --mail my-mail
+
+The e-mail should be in standard Git mail format (which is what e.g.
+stglink:mail[] produces) -- that is, with the patch in-line in the
+mail, not attached. The authorship info is taken from the mail
+headers, and the commit message is read from the 'Subject:' line and
+the mail body.
+
+If you don't give a file name, the mail will be read from the standard
+input. This means that, if your mail reader supports it, you can pipe
+a mail directly to +stg import $$--mail$$+ and the patch will be
+applied.
+
+
+Importing a mailbox full of patches
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Finally, in case importing one patch at a time is too much work,
+stglink:import[] also accepts an entire Unix +mbox+-format mailbox,
+either on the command line or on its standard input; just use the
++$$--mbox$$+ flag. Each mail should contain one patch, and is imported
+just like with +$$--mail$$+.
+
+Mailboxes full of patches are produced by e.g. stglink:mail[] with the
++$$--mbox$$+ flag, but most mail readers can produce them too, meaning
+that you can copy all the patch mails you want to apply to a separate
+mailbox, and then import them all in one go.
+
+
+Other stuff that needs to be placed somewhere
+=============================================
+
+
+Undo
+----
+
+TODO:: undo, redo, log, reset
+
+
+Interoperating with Git
+-----------------------
+
+TODO::
+
+* git commit + repair
+
+* git reset HEAD~n + repair
+
+* don't do git rebase or git merge, because it won't work
+
+
+Patch stuff
+-----------
+
+TODO:: This section needs revising. I've only fixed the formatting.
+Most of it should go under "Workflow: Tracking branch"
+
+As mentioned in the introduction, StGit stores modifications to your
+working tree in the form of Git commits. This means if you want to
+apply your changes to a tree not managed by Git, or send your changes
+to someone else in e-mail, you need to convert your StGit patches into
+normal textual diffs that can be applied with the GNU patch command.
+stglink:diff[] is a powerful way to generate and view textual diffs of
+patches managed by StGit.
 
-As mentioned in the introduction, StGIT stores modifications to your
-working tree in the form of GIT commits. This means if you want to apply
-your changes to a tree not managed by GIT, or send your changes to
-someone else in e-mail, you need to convert your StGIT patches into
-normal textual diffs that can be applied with the GNU 'patch' command.
-The 'stg diff' command is a powerful way to generate and view textual
-diffs of patches managed by StGIT.
 To view a diff of the topmost patch:
 
-  stg diff -r /
+  stg diff -r /
 
 Observe that this does not show any changes in the working directory
-that have not been saved by a 'refresh'. To view just the changes you've
-made since the last refresh, use:
+that have not been saved by a stgsublink:refresh[]. To view just the
+changes you've made since the last refresh, use:
 
-  stg diff -r /top
+  stg diff -r /top
 
 If you want to see the changes made by the patch combined with any
 unsaved changes in the working directory, try:
 
-  stg diff -r /bottom
+  stg diff -r /bottom
 
 You can also show the changes to any patch in your stack with:
 
-  stg diff -r <patch>/
+  stg diff -r <patch>/
 
 Use this command to view all the changes in your stack up through the
 current patch:
 
-  stg diff -r base
+  stg diff -r base
 
-The 'stg diff' command supports a number of other features that are very
-useful. Be sure to take a look at the help information for this command.
-To convert your StGIT patches into patch files:
+stglink:diff[] supports a number of other features that are very
+useful. Be sure to take a look at the help information for this
+command. To convert your StGit patches into patch files:
 
-  stg export [--range=[<patch1>[:<patch2>]]] [<dir-name>]
+  stg export [--range=[<patch1>[:<patch2>]]] [<dir-name>]
 
-The 'export' command supports options to automatically number the
-patches (-n) or add the '.diff' extension (-d). If you don't tell "stg
-export" where to put the patches, it will create directory named "patch-
-branchname" in your current directory, and store the patches there.
-To e-mail a patch or range of patches:
+stglink:export[] supports options to automatically number the patches
+(+-n+) or add the +.diff+ extension (+-d+). If you don't tell
+stgsublink:export[] where to put the patches, it will create directory
+named +patch-<branchname>+ in your current directory, and store the
+patches there. To e-mail a patch or range of patches:
 
-  stg mail [--to=...] (--all | --range=[<patch1>[:<patch2>]] | <patch>)
+  stg mail [--to=...] (--all | --range=[<patch1>[:<patch2>]] | <patch>)
 
-"stg mail" has a lot of options, so read the output of "stg mail -h" for
-more information.
-You can also import an existing GNU diff patch file as a new StGIT patch
-with a single command. "stg import" will automatically parse through the
-patch file and extract a patch description. Use:
+stglink:mail[] has a lot of options, so read the output of +stg mail
+-h+ for more information.
 
-  stg import [<file>]
+You can also import an existing GNU diff patch file as a new StGit
+patch with a single command. stglink:import[] will automatically parse
+through the patch file and extract a patch description. Use:
 
-This is the equivalent of "stg new" followed by "patch -i <file>", then
-"stg refresh -e".
-Sometimes the patch file won't apply cleanly. In that case, "stg import"
-will leave you with an empty StGIT patch, to which you then apply the
-patch file by hand using "patch -i" and your favorite editor.
-To merge a GNU diff file (defaulting to the standard input) into the
-topmost patch:
+  $ stg import [<file>]
 
-  stg fold [<file>]
+This is the equivalent of
 
-This command supports a '--threeway' option which applies the patch onto
-the bottom of the topmost one and performs a three-way merge.
+  $ stg new
+  $ patch -i <file>
+  $ stg refresh -e
 
+Sometimes the patch file won't apply cleanly. In that case,
+stglink:import[] will leave you with an empty StGit patch, to which
+you then apply the patch file by hand using "patch -i" and your
+favorite editor.
 
-Advanced Usage
-==============
+To merge a GNU diff file (defaulting to the standard input) into the
+topmost patch:
 
-Handling merge conflicts
-------------------------
+  $ stg fold [<file>]
 
-Pushing a patch on the stack can fail if the patch cannot be applied
-cleanly. This usually happens if there are overlapping changes in the
-tree, the patch depends on another patch which is not applied, or if a
-patch was not merged upstream in the exact form it was sent.
-The 'push' operation stops after the first patch with conflicts. The
-'status' command shows the conflict files by marking them with a 'C'. If
-the 'keeporig' options is set to 'yes' (the default), the original files
-involved in the merge operations are left in the tree as <file>.older,
-<file>.local and <file>.remote for better analysis of the conflict. If
-'diff3' is used as the merger (the default), markers are added to the
-conflicted files as well.
-Run the 'resolved' command to mark the conflicts resolved and remove the
-temporary merge files from the working tree. Then run the 'refresh'
-command to update the StGIT patch with the modifications you made to
-resolve the conflict.
-
-
-Configuration file
-------------------
-
-StGIT tries to read the configuration options from the following files:
-/etc/stgitrc, ~/.stgitrc and .git/stgitrc. The latter overrides the
-options in the former files. If no file is found, the defaults are used.
-An example configuration file with options description can be found in
-the examples/ directory. Most users would probably only define the
-'smtpserver' option used by the 'mail' command.
-The gitmergeonefile.py script does the three-way merging on individual
-files using the tool specified by the 'merger' option. The user can
-specify a smarter tool to be used.
+This command supports a +$$--threeway$$+ option which applies the
+patch onto the bottom of the topmost one and performs a three-way
+merge.
 
 
 Templates
 ---------
 
-The 'export' and 'mail' commands use templates for generating the patch
-files or e-mails. The default templates are installed under <prefix>/
-share/stgit/templates/ and, combined with the extra options available
-for the commands, should be enough for most users. The template format
-uses the standard Python string formatting rules. The variables
-available are shown in the the help message for the commands.
-The 'mail' command can also send an initial e-mail for which there is no
-default template. The <prefix>/share/stgit/examples/firstmail.tmpl file
-can be used as an example.
-A default description for new patches can be defined in the .git/
-patchdescr.tmpl file. This is useful for things like signed-off-by
-lines.
-
-
-Merging two patches into one
-----------------------------
-
-There is no command to do this directly at the moment but one can export
-the patch to be merged and use the 'stg fold' command on the generated
-diff file. Assuming that the merged patch was not already applied, the
-operation will succeed. Pushing the merged patch onto the stack will
-result in an empty patch (StGIT notifying the user) that can be safely
-deleted.
-
-
-Technical Information
-=====================
-
-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.
-
-
-Layout of the .git directory
-----------------------------
-
-  HEAD                 -> refs/heads/<something>
-  objects/
-    ??/
-    ...
-  refs/
-    heads/
-      master           - the master commit id
-      ...
-    tags/
-      ...
-    branches/
-      ...
-    patches/
-      master/
-        applied                - list of applied patches
-        unapplied              - list of not-yet applied patches
-        current                - name of the topmost patch
-        patch1/
-          bottom               - the bottom id of the patch
-          top          - the top id of the patch
-       description     - the patch description
-       authname        - author's name
-       authemail       - author's e-mail
-       commname        - committer's name
-       commemail       - committer's e-mail
-        patch2/
-          ...
-        ...
-      ...
+TODO:: This section needs revising. I've only fixed the formatting.
+
+stglink:export[] and stglink:mail[] use templates for generating the
+patch files or e-mails. The default templates are installed under
++<prefix>/share/stgit/templates/+ and, combined with the extra options
+available for these commands, should be enough for most users. The
+template format uses the standard Python string formatting rules. The
+variables available are listed in the the manual pages for each
+command. stglink:mail[] can also send an initial 'cover' e-mail for
+which there is no default template. The
++<prefix>/share/stgit/examples/firstmail.tmpl+ file can be used as an
+example. A default description for new patches can be defined in the
++.git/ patchdescr.tmpl+ file. This is useful for things like
+signed-off-by lines.
diff --git a/INSTALL b/INSTALL
index 8d2bebd..3da4efd 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -1,13 +1,18 @@
 For basic installation:
 
-       python setup.py install
+       $ make all doc ;# as yourself
+       $ make install install-doc ;# as yourself
 
 By default, the above command installs StGIT in the
 $HOME/{bin,lib,share} directories. For a different location, use the
---prefix option.
+prefix option.
 
-       python setup.py install --prefix=/usr
+       $ make prefix=/usr all doc #; as yourself
+       # make prefix=/usr install install-doc #; as root
 
-For more information:
+Issues of note:
+
+- To build and install the documentation, you need to have the
+  asciidoc/xmlto toolchain.  The default build target ("make all")
+  does _not_ build them.
 
-       http://docs.python.org/inst/inst.html
index c890b8e..0fa5c6a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,19 +1,42 @@
-PREFIX ?= $(HOME)
+prefix ?= $(HOME)
 DESTDIR        ?= /
 PYTHON ?= python
 
-all:
+TEST_PATCHES ?= ..
+
+all: build
        $(PYTHON) setup.py build
 
-install:
-       $(PYTHON) setup.py install --prefix=$(PREFIX) --root=$(DESTDIR) --force
+build: stgit/commands/cmdlist.py stgit-completion.bash
+
+ALL_PY = $(shell find stgit -name '*.py')
+
+stgit/commands/cmdlist.py: $(ALL_PY)
+       $(PYTHON) stg-build --py-cmd-list > $@
+
+stgit-completion.bash: $(ALL_PY)
+       $(PYTHON) stg-build --bash-completion > $@
+
+install: build
+       $(PYTHON) setup.py install --prefix=$(prefix) --root=$(DESTDIR) --force
 
 doc:
        cd Documentation && $(MAKE) all
 
-test:
+install-doc:
+       $(MAKE) -C Documentation install
+
+install-html:
+       $(MAKE) -C Documentation install-html
+
+test: build
        cd t && $(MAKE) all
 
+test_patches: build
+       for patch in $$(stg series --noprefix $(TEST_PATCHES)); do \
+               stg goto $$patch && $(MAKE) test || break; \
+       done
+
 clean:
        for dir in Documentation t; do \
                (cd $$dir && $(MAKE) clean); \
@@ -22,8 +45,13 @@ clean:
        rm -f stgit/*.pyc
        rm -f stgit/commands/*.pyc
        rm -f TAGS
+       rm -f stgit/commands/cmdlist.py
 
 tags:
+       ctags -R stgit/*
+
+TAGS:
        ctags -e -R stgit/*
 
-.PHONY: all install doc test clean
+.PHONY: all build install doc install-doc install-html test test_patches \
+       clean tags TAGS
diff --git a/README b/README
index 4f20154..311c08d 100644 (file)
--- a/README
+++ b/README
@@ -1,15 +1,15 @@
-StGIT is a Python application providing similar functionality to Quilt
-(i.e. pushing/popping patches to/from a stack) on top of GIT. These
-operations are performed using GIT commands and the patches are stored
-as GIT commit objects, allowing easy merging of the StGIT patches into
-other repositories using standard GIT functionality.
+StGit is a Python application providing similar functionality to Quilt
+(i.e. pushing/popping patches to/from a stack) on top of Git. These
+operations are performed using Git commands and the patches are stored
+as Git commit objects, allowing easy merging of the StGit patches into
+other repositories using standard Git functionality.
 
-Note that StGIT is not an SCM interface on top of GIT and it expects a
-previously initialised GIT repository (unless it is cloned using StGIT
-directly). For standard SCM operations, use plain GIT commands.
+Note that StGit is not an SCM interface on top of Git and it expects a
+previously initialised Git repository (unless it is cloned using StGit
+directly). For standard SCM operations, use plain Git commands.
 
 For the latest version see http://www.procode.org/stgit/
-For a tutorial see http://wiki.procode.org/cgi-bin/wiki.cgi/StGIT_Tutorial
+For a tutorial see http://wiki.procode.org/cgi-bin/wiki.cgi/StGit_Tutorial
 
 Bugs or feature requests should be sent to the git@vger.kernel.org
-mailing list or the StGIT project page - http://gna.org/projects/stgit/
+mailing list or the StGit project page - http://gna.org/projects/stgit/
diff --git a/TODO b/TODO
index 884b831..a01daef 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,16 +1,24 @@
 The TODO list before 1.0:
 
 - more regression tests
-- stg help should probably pipe through the $PAGER
-- fix StGIT to run correctly in subdirectories
+
+- Convert the remaining commands to the new infrastructure.
+
+- Go through the design of the UI and make sure there's nothing hard
+  to change in there that we'll regret later.
+
+- Write a user guide. I'm thinking a document on the order of 10-30
+  pages that'll explain why one would want to use StGit, and how.
+
+- Make sure the rest of the documentation is in good shape.
+
 - use a separate index for some commands (refresh, fold etc.) so that
   files already added/removed are not automatically checked in
-- debian package support
-- man page
-- document the workflow on the StGIT wiki
-- maybe a separate undo command rather than passing a --undo option to
-  push and refresh
-- use same configuration file as GIT
+
+    + This is easily done with the new infrastructure. refresh now
+      uses a separate index when appropriate. fold has not yet been
+      converted.
+
 - release 1.0
 
 
@@ -20,9 +28,5 @@ The future, when time allows or if someone else does them:
 - multiple heads in a patch - useful for forking a patch,
   synchronising with other patches (diff format or in other
   repositories)
-- commit directly to a patch which is not top
-- patch synchronisation between between branches (as some people,
-  including me have the same patches based on different branches and
-  they have scripts for moving the changes in one to the others)
 - numeric shortcuts for naming patches near top (eg. +1, -2)
 - (config?) parameter for number of patches included by "series -s"
diff --git a/contrib/.gitignore b/contrib/.gitignore
new file mode 100644 (file)
index 0000000..c531d98
--- /dev/null
@@ -0,0 +1 @@
+*.elc
diff --git a/contrib/Makefile b/contrib/Makefile
new file mode 100644 (file)
index 0000000..8556910
--- /dev/null
@@ -0,0 +1,19 @@
+EMACS = emacs
+
+ELC = stgit.elc
+INSTALL ?= install
+INSTALL_ELC = $(INSTALL) -m 644
+prefix ?= $(HOME)
+emacsdir = $(prefix)/share/emacs/site-lisp
+RM ?= rm -f
+
+all: $(ELC)
+
+install: all
+       $(INSTALL) -d $(DESTDIR)$(emacsdir)
+       $(INSTALL_ELC) $(ELC:.elc=.el) $(ELC) $(DESTDIR)$(emacsdir)
+
+%.elc: %.el
+       $(EMACS) -batch -f batch-byte-compile $<
+
+clean:; $(RM) $(ELC)
index ea9109d..eecc87a 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 
 # Code copied from Quilt (http://savannah.nongnu.org/projects/quilt)
 #
diff --git a/contrib/stgit-completion.bash b/contrib/stgit-completion.bash
deleted file mode 100644 (file)
index 8d49e21..0000000
+++ /dev/null
@@ -1,275 +0,0 @@
-# bash completion support for StGIT                        -*- shell-script -*-
-#
-# Copyright (C) 2006, Karl Hasselström <kha@treskal.com>
-# Based on git-completion.sh
-#
-# To use these routines:
-#
-#    1. Copy this file to somewhere (e.g. ~/.stgit-completion.bash).
-#
-#    2. Add the following line to your .bashrc:
-#         . ~/.stgit-completion.bash
-
-_stg_commands="
-    add
-    applied
-    branch
-    delete
-    diff
-    clean
-    clone
-    commit
-    cp
-    edit
-    export
-    files
-    float
-    fold
-    goto
-    hide
-    id
-    import
-    init
-    log
-    mail
-    new
-    patches
-    pick
-    pop
-    pull
-    push
-    rebase
-    refresh
-    rename
-    repair
-    resolved
-    rm
-    series
-    show
-    sink
-    status
-    sync
-    top
-    unapplied
-    uncommit
-    unhide
-"
-
-# The path to .git, or empty if we're not in a repository.
-_gitdir ()
-{
-    echo "$(git rev-parse --git-dir 2>/dev/null)"
-}
-
-# Name of the current branch, or empty if there isn't one.
-_current_branch ()
-{
-    local b=$(git symbolic-ref HEAD 2>/dev/null)
-    echo ${b#refs/heads/}
-}
-
-# List of all applied patches.
-_applied_patches ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$(_current_branch)/applied"
-}
-
-# List of all unapplied patches.
-_unapplied_patches ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$(_current_branch)/unapplied"
-}
-
-# List of all applied patches.
-_hidden_patches ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$(_current_branch)/hidden"
-}
-
-# List of all patches.
-_all_patches ()
-{
-    local b=$(_current_branch)
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$b/applied" "$g/patches/$b/unapplied"
-}
-
-# List of all patches except the current patch.
-_all_other_patches ()
-{
-    local b=$(_current_branch)
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$b/applied" "$g/patches/$b/unapplied" \
-        | grep -v "^$(cat $g/patches/$b/current 2> /dev/null)$"
-}
-
-_all_branches ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && (cd $g/patches/ && echo *)
-}
-
-_conflicting_files ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && stg status --conflict
-}
-
-_dirty_files ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && stg status --modified --new --deleted
-}
-
-_unknown_files ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && stg status --unknown
-}
-
-_known_files ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && git ls-files
-}
-
-# List the command options
-_cmd_options ()
-{
-    stg $1 --help 2>/dev/null | grep -e " --[A-Za-z]" | sed -e "s/.*\(--[^ =]\+\).*/\1/"
-}
-
-# Generate completions for patches and patch ranges from the given
-# patch list function, and options from the given list.
-_complete_patch_range ()
-{
-    local patchlist="$1" options="$2"
-    local pfx cur="${COMP_WORDS[COMP_CWORD]}"
-    case "$cur" in
-        *..*)
-            pfx="${cur%..*}.."
-            cur="${cur#*..}"
-            COMPREPLY=($(compgen -P "$pfx" -W "$($patchlist)" -- "$cur"))
-            ;;
-        *)
-            COMPREPLY=($(compgen -W "$options $($patchlist)" -- "$cur"))
-            ;;
-    esac
-}
-
-_complete_patch_range_options ()
-{
-    local patchlist="$1" options="$2" patch_options="$3"
-    local prev="${COMP_WORDS[COMP_CWORD-1]}"
-    local cur="${COMP_WORDS[COMP_CWORD]}"
-    local popt
-    for popt in $patch_options; do
-        if [ $prev == $popt ]; then
-            _complete_patch_range $patchlist
-            return
-        fi
-    done
-    COMPREPLY=($(compgen -W "$options" -- "$cur"))
-}
-
-_complete_branch ()
-{
-     COMPREPLY=($(compgen -W "$(_cmd_options $1) $($2)" -- "${COMP_WORDS[COMP_CWORD]}"))
-}
-
-# Generate completions for options from the given list.
-_complete_options ()
-{
-    local options="$1"
-    COMPREPLY=($(compgen -W "$options" -- "${COMP_WORDS[COMP_CWORD]}"))
-}
-
-_complete_files ()
-{
-    COMPREPLY=($(compgen -W "$(_cmd_options $1) $2" -- "${COMP_WORDS[COMP_CWORD]}"))
-}
-
-_stg_common ()
-{
-    _complete_options "$(_cmd_options $1)"
-}
-
-_stg_patches ()
-{
-    _complete_patch_range "$2" "$(_cmd_options $1)"
-}
-
-_stg_patches_options ()
-{
-    _complete_patch_range_options "$2" "$(_cmd_options $1)" "$3"
-}
-
-_stg_help ()
-{
-    _complete_options "$_stg_commands"
-}
-
-_stg ()
-{
-    local i c=1 command
-
-    while [ $c -lt $COMP_CWORD ]; do
-        if [ $c == 1 ]; then
-            command="${COMP_WORDS[c]}"
-        fi
-        c=$((++c))
-    done
-
-    # Complete name of subcommand.
-    if [ $c -eq $COMP_CWORD -a -z "$command" ]; then
-        COMPREPLY=($(compgen \
-            -W "--help --version copyright help $_stg_commands" \
-            -- "${COMP_WORDS[COMP_CWORD]}"))
-        return;
-    fi
-
-    # Complete arguments to subcommands.
-    case "$command" in
-        # generic commands
-        help)   _stg_help ;;
-        # repository commands
-        id)     _stg_patches $command _all_patches ;;
-        # stack commands
-        float)  _stg_patches $command _all_patches ;;
-        goto)   _stg_patches $command _all_other_patches ;;
-        hide)   _stg_patches $command _unapplied_patches ;;
-        pop)    _stg_patches $command _applied_patches ;;
-        push)   _stg_patches $command _unapplied_patches ;;
-        series) _stg_patches $command _all_patches ;;
-        sink)   _stg_patches $command _all_patches ;;
-        unhide) _stg_patches $command _hidden_patches ;;
-        # patch commands
-        delete) _stg_patches $command _all_patches ;;
-        edit)   _stg_patches $command _applied_patches ;;
-        export) _stg_patches $command _all_patches ;;
-        files)  _stg_patches $command _all_patches ;;
-        log)    _stg_patches $command _all_patches ;;
-        mail)   _stg_patches $command _all_patches ;;
-        pick)   _stg_patches $command _unapplied_patches ;;
-#      refresh)_stg_patches_options $command _applied_patches "-p --patch" ;;
-        refresh) _complete_files $command "$(_dirty_files)" ;;
-        rename) _stg_patches $command _all_patches ;;
-        show)   _stg_patches $command _all_patches ;;
-        sync)   _stg_patches $command _applied_patches ;;
-        # working-copy commands
-        diff)   _stg_patches_options $command _applied_patches "-r --range" ;;
-       resolved) _complete_files $command "$(_conflicting_files)" ;;
-       add)    _complete_files $command "$(_unknown_files)" ;;
-#      rm)     _complete_files $command "$(_known_files)" ;;
-       # commands that usually raher accept branches
-       branch) _complete_branch $command _all_branches ;;
-       rebase) _complete_branch $command _all_branches ;;
-        # all the other commands
-        *)      _stg_common $command ;;
-    esac
-}
-
-complete -o default -F _stg stg
diff --git a/contrib/stgit.el b/contrib/stgit.el
new file mode 100644 (file)
index 0000000..7c83a9e
--- /dev/null
@@ -0,0 +1,884 @@
+;; stgit.el: An emacs mode for StGit
+;;
+;; Copyright (C) 2007 David KÃ¥gedal <davidk@lysator.liu.se>
+;;
+;; To install: put this file on the load-path and place the following
+;; in your .emacs file:
+;;
+;;    (require 'stgit)
+;;
+;; To start: `M-x stgit'
+
+(require 'git nil t)
+
+(defun stgit (dir)
+  "Manage StGit patches for the tree in DIR."
+  (interactive "DDirectory: \n")
+  (switch-to-stgit-buffer (git-get-top-dir dir))
+  (stgit-reload))
+
+(unless (fboundp 'git-get-top-dir)
+  (defun git-get-top-dir (dir)
+    "Retrieve the top-level directory of a git tree."
+    (let ((cdup (with-output-to-string
+                  (with-current-buffer standard-output
+                    (cd dir)
+                    (unless (eq 0 (call-process "git" nil t nil
+                                                "rev-parse" "--show-cdup"))
+                      (error "Cannot find top-level git tree for %s" dir))))))
+      (expand-file-name (concat (file-name-as-directory dir)
+                                (car (split-string cdup "\n")))))))
+
+(defun stgit-refresh-git-status (&optional dir)
+  "If it exists, refresh the `git-status' buffer belonging to
+directory DIR or `default-directory'"
+  (when (and (fboundp 'git-find-status-buffer)
+             (fboundp 'git-refresh-status))
+    (let* ((top-dir (git-get-top-dir (or dir default-directory)))
+           (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
+      (when git-status-buffer
+        (with-current-buffer git-status-buffer
+          (git-refresh-status))))))
+
+(defun switch-to-stgit-buffer (dir)
+  "Switch to a (possibly new) buffer displaying StGit patches for DIR."
+  (setq dir (file-name-as-directory dir))
+  (let ((buffers (buffer-list)))
+    (while (and buffers
+                (not (with-current-buffer (car buffers)
+                       (and (eq major-mode 'stgit-mode)
+                            (string= default-directory dir)))))
+      (setq buffers (cdr buffers)))
+    (switch-to-buffer (if buffers
+                          (car buffers)
+                        (create-stgit-buffer dir)))))
+
+(defun create-stgit-buffer (dir)
+  "Create a buffer for showing StGit patches.
+Argument DIR is the repository path."
+  (let ((buf (create-file-buffer (concat dir "*stgit*")))
+        (inhibit-read-only t))
+    (with-current-buffer buf
+      (setq default-directory dir)
+      (stgit-mode)
+      (setq buffer-read-only t))
+    buf))
+
+(defmacro stgit-capture-output (name &rest body)
+  "Capture StGit output and show it in a window at the end."
+  `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
+         (stgit-dir default-directory)
+         (inhibit-read-only t))
+     (with-current-buffer output-buf
+       (erase-buffer)
+       (setq default-directory stgit-dir)
+       (setq buffer-read-only t))
+     (let ((standard-output output-buf))
+       ,@body)
+     (with-current-buffer output-buf
+       (set-buffer-modified-p nil)
+       (setq buffer-read-only t)
+       (if (< (point-min) (point-max))
+           (display-buffer output-buf t)))))
+(put 'stgit-capture-output 'lisp-indent-function 1)
+
+(defun stgit-make-run-args (args)
+  "Return a copy of ARGS with its elements converted to strings."
+  (mapcar (lambda (x)
+            ;; don't use (format "%s" ...) to limit type errors
+            (cond ((stringp x) x)
+                  ((integerp x) (number-to-string x))
+                  ((symbolp x) (symbol-name x))
+                  (t
+                   (error "Bad element in stgit-make-run-args args: %S" x))))
+          args))
+
+(defun stgit-run-silent (&rest args)
+  (setq args (stgit-make-run-args args))
+  (apply 'call-process "stg" nil standard-output nil args))
+
+(defun stgit-run (&rest args)
+  (setq args (stgit-make-run-args args))
+  (let ((msgcmd (mapconcat #'identity args " ")))
+    (message "Running stg %s..." msgcmd)
+    (apply 'call-process "stg" nil standard-output nil args)
+    (message "Running stg %s...done" msgcmd)))
+
+(defun stgit-run-git (&rest args)
+  (setq args (stgit-make-run-args args))
+  (let ((msgcmd (mapconcat #'identity args " ")))
+    (message "Running git %s..." msgcmd)
+    (apply 'call-process "git" nil standard-output nil args)
+    (message "Running git %s...done" msgcmd)))
+
+(defun stgit-run-git-silent (&rest args)
+  (setq args (stgit-make-run-args args))
+  (apply 'call-process "git" nil standard-output nil args))
+
+(defun stgit-reload ()
+  "Update the contents of the StGit buffer."
+  (interactive)
+  (let ((inhibit-read-only t)
+        (curline (line-number-at-pos))
+        (curpatch (stgit-patch-at-point)))
+    (erase-buffer)
+    (insert "Branch: ")
+    (stgit-run-silent "branch")
+    (stgit-run-silent "series" "--description" "--empty")
+    (stgit-rescan)
+    (if curpatch
+        (stgit-goto-patch curpatch)
+      (goto-line curline)))
+  (stgit-refresh-git-status))
+
+(defgroup stgit nil
+  "A user interface for the StGit patch maintenance tool."
+  :group 'tools)
+
+(defface stgit-description-face
+  '((((background dark)) (:foreground "tan"))
+    (((background light)) (:foreground "dark red")))
+  "The face used for StGit descriptions"
+  :group 'stgit)
+
+(defface stgit-top-patch-face
+  '((((background dark)) (:weight bold :foreground "yellow"))
+    (((background light)) (:weight bold :foreground "purple"))
+    (t (:weight bold)))
+  "The face used for the top patch names"
+  :group 'stgit)
+
+(defface stgit-applied-patch-face
+  '((((background dark)) (:foreground "light yellow"))
+    (((background light)) (:foreground "purple"))
+    (t ()))
+  "The face used for applied patch names"
+  :group 'stgit)
+
+(defface stgit-unapplied-patch-face
+  '((((background dark)) (:foreground "gray80"))
+    (((background light)) (:foreground "orchid"))
+    (t ()))
+  "The face used for unapplied patch names"
+  :group 'stgit)
+
+(defface stgit-modified-file-face
+  '((((class color) (background light)) (:foreground "purple"))
+    (((class color) (background dark)) (:foreground "salmon")))
+  "StGit mode face used for modified file status"
+  :group 'stgit)
+
+(defface stgit-unmerged-file-face
+  '((((class color) (background light)) (:foreground "red" :bold t))
+    (((class color) (background dark)) (:foreground "red" :bold t)))
+  "StGit mode face used for unmerged file status"
+  :group 'stgit)
+
+(defface stgit-unknown-file-face
+  '((((class color) (background light)) (:foreground "goldenrod" :bold t))
+    (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
+  "StGit mode face used for unknown file status"
+  :group 'stgit)
+
+(defface stgit-file-permission-face
+  '((((class color) (background light)) (:foreground "green" :bold t))
+    (((class color) (background dark)) (:foreground "green" :bold t)))
+  "StGit mode face used for permission changes."
+  :group 'stgit)
+
+(defcustom stgit-expand-find-copies-harder
+  nil
+  "Try harder to find copied files when listing patches.
+
+When not nil, runs git diff-tree with the --find-copies-harder
+flag, which reduces performance."
+  :type 'boolean
+  :group 'stgit)
+
+(defconst stgit-file-status-code-strings
+  (mapcar (lambda (arg)
+            (cons (car arg)
+                  (propertize (cadr arg) 'face (car (cddr arg)))))
+          '((add         "Added"       stgit-modified-file-face)
+            (copy        "Copied"      stgit-modified-file-face)
+            (delete      "Deleted"     stgit-modified-file-face)
+            (modify      "Modified"    stgit-modified-file-face)
+            (rename      "Renamed"     stgit-modified-file-face)
+            (mode-change "Mode change" stgit-modified-file-face)
+            (unmerged    "Unmerged"    stgit-unmerged-file-face)
+            (unknown     "Unknown"     stgit-unknown-file-face)))
+  "Alist of code symbols to description strings")
+
+(defun stgit-file-status-code-as-string (code)
+  "Return stgit status code as string"
+  (let ((str (assq (if (consp code) (car code) code)
+                   stgit-file-status-code-strings)))
+    (when str
+      (format "%-11s  "
+              (if (and str (consp code) (/= (cdr code) 100))
+                  (format "%s %s" (cdr str)
+                          (propertize (format "%d%%" (cdr code))
+                                      'face 'stgit-description-face))
+                (cdr str))))))
+
+(defun stgit-file-status-code (str &optional score)
+  "Return stgit status code from git status string"
+  (let ((code (assoc str '(("A" . add)
+                           ("C" . copy)
+                           ("D" . delete)
+                           ("M" . modify)
+                           ("R" . rename)
+                           ("T" . mode-change)
+                           ("U" . unmerged)
+                           ("X" . unknown)))))
+    (setq code (if code (cdr code) 'unknown))
+    (when (stringp score)
+      (if (> (length score) 0)
+          (setq score (string-to-number score))
+        (setq score nil)))
+    (if score (cons code score) code)))
+
+(defconst stgit-file-type-strings
+  '((#o100 . "file")
+    (#o120 . "symlink")
+    (#o160 . "subproject"))
+  "Alist of names of file types")
+
+(defun stgit-file-type-string (type)
+  "Return string describing file type TYPE (the high bits of file permission).
+Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
+  (let ((type-str (assoc type stgit-file-type-strings)))
+    (or (and type-str (cdr type-str))
+       (format "unknown type %o" type))))
+
+(defun stgit-file-type-change-string (old-perm new-perm)
+  "Return string describing file type change from OLD-PERM to NEW-PERM.
+Cf. `stgit-file-type-string'."
+  (let ((old-type (lsh old-perm -9))
+        (new-type (lsh new-perm -9)))
+    (cond ((= old-type new-type) "")
+          ((zerop new-type) "")
+          ((zerop old-type)
+           (if (= new-type #o100)
+               ""
+             (format "   (%s)" (stgit-file-type-string new-type))))
+          (t (format "   (%s -> %s)"
+                     (stgit-file-type-string old-type)
+                     (stgit-file-type-string new-type))))))
+
+(defun stgit-file-mode-change-string (old-perm new-perm)
+  "Return string describing file mode change from OLD-PERM to NEW-PERM.
+Cf. `stgit-file-type-change-string'."
+  (setq old-perm (logand old-perm #o777)
+        new-perm (logand new-perm #o777))
+  (if (or (= old-perm new-perm)
+          (zerop old-perm)
+          (zerop new-perm))
+      ""
+    (let* ((modified       (logxor old-perm new-perm))
+          (not-x-modified (logand (logxor old-perm new-perm) #o666)))
+      (cond ((zerop modified) "")
+            ((and (zerop not-x-modified)
+                  (or (and (eq #o111 (logand old-perm #o111))
+                           (propertize "-x" 'face 'stgit-file-permission-face))
+                      (and (eq #o111 (logand new-perm #o111))
+                           (propertize "+x" 'face
+                                       'stgit-file-permission-face)))))
+            (t (concat (propertize (format "%o" old-perm)
+                                   'face 'stgit-file-permission-face)
+                       (propertize " -> "
+                                   'face 'stgit-description-face)
+                       (propertize (format "%o" new-perm)
+                                   'face 'stgit-file-permission-face)))))))
+
+(defun stgit-expand-patch (patchsym)
+  "Expand (show modification of) the patch with name PATCHSYM (a
+symbol) at point.
+`stgit-expand-find-copies-harder' controls how hard to try to
+find copied files."
+  (save-excursion
+    (forward-line)
+    (let* ((start (point))
+           (result (with-output-to-string
+                     (stgit-run-git "diff-tree" "-r" "-z"
+                                    (if stgit-expand-find-copies-harder
+                                        "--find-copies-harder"
+                                      "-C")
+                                    (stgit-id patchsym)))))
+      (let (mstart)
+        (while (string-match "\0:\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} \\(\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\\|\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\\)"
+                             result mstart)
+          (let ((copy-or-rename (match-string 4 result))
+                (old-perm       (read (format "#o%s" (match-string 1 result))))
+                (new-perm       (read (format "#o%s" (match-string 2 result))))
+                (line-start (point))
+                status
+                change
+                properties)
+            (insert "    ")
+            (if copy-or-rename
+                (let ((cr-score       (match-string 5 result))
+                      (cr-from-file   (match-string 6 result))
+                      (cr-to-file     (match-string 7 result)))
+                  (setq status (stgit-file-status-code copy-or-rename
+                                                       cr-score)
+                        properties (list 'stgit-old-file cr-from-file
+                                         'stgit-new-file cr-to-file)
+                        change (concat
+                                cr-from-file
+                                (propertize " -> "
+                                            'face 'stgit-description-face)
+                                cr-to-file)))
+              (setq status (stgit-file-status-code (match-string 8 result))
+                    properties (list 'stgit-file (match-string 9 result))
+                    change (match-string 9 result)))
+
+            (let ((mode-change (stgit-file-mode-change-string old-perm
+                                                              new-perm)))
+              (insert (format "%-12s" (stgit-file-status-code-as-string
+                                       status))
+                      mode-change
+                      (if (> (length mode-change) 0) " " "")
+                      change
+                      (propertize (stgit-file-type-change-string old-perm
+                                                                 new-perm)
+                                  'face 'stgit-description-face)
+                      ?\n))
+            (add-text-properties line-start (point) properties))
+          (setq mstart (match-end 0))))
+      (when (= start (point))
+        (insert "    <no files>\n"))
+      (put-text-property start (point) 'stgit-file-patchsym patchsym))))
+
+(defun stgit-rescan ()
+  "Rescan the status buffer."
+  (save-excursion
+    (let ((marked ())
+         found-any)
+      (goto-char (point-min))
+      (while (not (eobp))
+        (cond ((looking-at "Branch: \\(.*\\)")
+               (put-text-property (match-beginning 1) (match-end 1)
+                                  'face 'bold))
+              ((looking-at "\\([0 ]\\)\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
+              (setq found-any t)
+               (let ((empty (match-string 1))
+                    (state (match-string 2))
+                     (patchsym (intern (match-string 4))))
+                 (put-text-property
+                  (match-beginning 4) (match-end 4) 'face
+                  (cond ((string= state ">") 'stgit-top-patch-face)
+                        ((string= state "+") 'stgit-applied-patch-face)
+                        ((string= state "-") 'stgit-unapplied-patch-face)))
+                 (put-text-property (match-beginning 5) (match-end 5)
+                                    'face 'stgit-description-face)
+                 (when (memq patchsym stgit-marked-patches)
+                   (save-excursion
+                    (replace-match "*" nil nil nil 3))
+                   (setq marked (cons patchsym marked)))
+                 (put-text-property (match-beginning 0) (match-end 0)
+                                    'stgit-patchsym patchsym)
+                 (when (memq patchsym stgit-expanded-patches)
+                   (stgit-expand-patch patchsym))
+                (when (equal "0" empty)
+                  (save-excursion
+                    (goto-char (match-beginning 5))
+                    (insert "(empty) ")))
+                (delete-char 1)
+                 ))
+              ((or (looking-at "stg series: Branch \".*\" not initialised")
+                   (looking-at "stg series: .*: branch not initialized"))
+              (setq found-any t)
+               (forward-line 1)
+               (insert "Run M-x stgit-init to initialise")))
+        (forward-line 1))
+      (setq stgit-marked-patches (nreverse marked))
+      (unless found-any
+       (insert "\n  "
+               (propertize "no patches in series"
+                           'face 'stgit-description-face))))))
+
+(defun stgit-select ()
+  "Expand or collapse the current entry"
+  (interactive)
+  (let ((curpatch (stgit-patch-at-point)))
+    (if (not curpatch)
+        (let ((patched-file (stgit-patched-file-at-point)))
+          (unless patched-file
+            (error "No patch or file on the current line"))
+          (let ((filename (expand-file-name (cdr patched-file))))
+            (unless (file-exists-p filename)
+              (error "File does not exist"))
+            (find-file filename)))
+      (setq stgit-expanded-patches
+            (if (memq curpatch stgit-expanded-patches)
+                (delq curpatch stgit-expanded-patches)
+              (cons curpatch stgit-expanded-patches)))
+      (stgit-reload))))
+
+(defun stgit-find-file-other-window ()
+  "Open file at point in other window"
+  (interactive)
+  (let ((patched-file (stgit-patched-file-at-point)))
+    (unless patched-file
+      (error "No file on the current line"))
+    (let ((filename (expand-file-name (cdr patched-file))))
+      (unless (file-exists-p filename)
+        (error "File does not exist"))
+      (find-file-other-window filename))))
+
+(defun stgit-quit ()
+  "Hide the stgit buffer."
+  (interactive)
+  (bury-buffer))
+
+(defun stgit-git-status ()
+  "Show status using `git-status'."
+  (interactive)
+  (unless (fboundp 'git-status)
+    (error "The stgit-git-status command requires git-status"))
+  (let ((dir default-directory))
+    (save-selected-window
+      (pop-to-buffer nil)
+      (git-status dir))))
+
+(defun stgit-next-line (&optional arg try-vscroll)
+  "Move cursor vertically down ARG lines"
+  (interactive "p\np")
+  (next-line arg try-vscroll)
+  (when (looking-at "  \\S-")
+    (forward-char 2)))
+
+(defun stgit-previous-line (&optional arg try-vscroll)
+  "Move cursor vertically up ARG lines"
+  (interactive "p\np")
+  (previous-line arg try-vscroll)
+  (when (looking-at "  \\S-")
+    (forward-char 2)))
+
+(defun stgit-next-patch (&optional arg)
+  "Move cursor down ARG patches"
+  (interactive "p")
+  (unless arg
+    (setq arg 1))
+  (if (< arg 0)
+      (stgit-previous-patch (- arg))
+    (while (not (zerop arg))
+      (setq arg (1- arg))
+      (while (progn (stgit-next-line)
+                    (not (stgit-patch-at-point)))))))
+
+(defun stgit-previous-patch (&optional arg)
+  "Move cursor up ARG patches"
+  (interactive "p")
+  (unless arg
+    (setq arg 1))
+  (if (< arg 0)
+      (stgit-next-patch (- arg))
+    (while (not (zerop arg))
+      (setq arg (1- arg))
+      (while (progn (stgit-previous-line)
+                    (not (stgit-patch-at-point)))))))
+
+(defvar stgit-mode-hook nil
+  "Run after `stgit-mode' is setup.")
+
+(defvar stgit-mode-map nil
+  "Keymap for StGit major mode.")
+
+(unless stgit-mode-map
+  (setq stgit-mode-map (make-keymap))
+  (suppress-keymap stgit-mode-map)
+  (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
+        '((" " .        stgit-mark)
+          ("m" .        stgit-mark)
+          ("\d" .       stgit-unmark-up)
+          ("u" .        stgit-unmark-down)
+          ("?" .        stgit-help)
+          ("h" .        stgit-help)
+          ("p" .        stgit-previous-line)
+          ("n" .        stgit-next-line)
+          ("\C-p" .     stgit-previous-patch)
+          ("\C-n" .     stgit-next-patch)
+          ("\M-{" .     stgit-previous-patch)
+          ("\M-}" .     stgit-next-patch)
+          ("s" .        stgit-git-status)
+          ("g" .        stgit-reload)
+          ("r" .        stgit-refresh)
+          ("\C-c\C-r" . stgit-rename)
+          ("e" .        stgit-edit)
+          ("c" .        stgit-coalesce)
+          ("N" .        stgit-new)
+          ("R" .        stgit-repair)
+          ("C" .        stgit-commit)
+          ("U" .        stgit-uncommit)
+          ("\r" .       stgit-select)
+          ("o" .        stgit-find-file-other-window)
+          (">" .        stgit-push-next)
+          ("<" .        stgit-pop-next)
+          ("P" .        stgit-push-or-pop)
+          ("G" .        stgit-goto)
+          ("=" .        stgit-show)
+          ("D" .        stgit-delete)
+          ([(control ?/)] . stgit-undo)
+          ("\C-_" .     stgit-undo)
+          ("q" . stgit-quit))))
+
+(defun stgit-mode ()
+  "Major mode for interacting with StGit.
+Commands:
+\\{stgit-mode-map}"
+  (kill-all-local-variables)
+  (buffer-disable-undo)
+  (setq mode-name "StGit"
+        major-mode 'stgit-mode
+        goal-column 2)
+  (use-local-map stgit-mode-map)
+  (set (make-local-variable 'list-buffers-directory) default-directory)
+  (set (make-local-variable 'stgit-marked-patches) nil)
+  (set (make-local-variable 'stgit-expanded-patches) nil)
+  (set-variable 'truncate-lines 't)
+  (run-hooks 'stgit-mode-hook))
+
+(defun stgit-add-mark (patchsym)
+  "Mark the patch PATCHSYM."
+  (setq stgit-marked-patches (cons patchsym stgit-marked-patches)))
+
+(defun stgit-remove-mark (patchsym)
+  "Unmark the patch PATCHSYM."
+  (setq stgit-marked-patches (delq patchsym stgit-marked-patches)))
+
+(defun stgit-clear-marks ()
+  "Unmark all patches."
+  (setq stgit-marked-patches '()))
+
+(defun stgit-patch-at-point (&optional cause-error allow-file)
+  "Return the patch name on the current line as a symbol.
+If CAUSE-ERROR is not nil, signal an error if none found.
+If ALLOW-FILE is not nil, also handle when point is on a file of
+a patch."
+  (or (get-text-property (point) 'stgit-patchsym)
+      (and allow-file
+           (get-text-property (point) 'stgit-file-patchsym))
+      (when cause-error
+        (error "No patch on this line"))))
+
+(defun stgit-patched-file-at-point (&optional both-files)
+  "Returns a cons of the patchsym and file name at point. For
+copies and renames, return the new file if the patch is either
+applied. If BOTH-FILES is non-nil, return a cons of the old and
+the new file names instead of just one name."
+  (let ((patchsym (get-text-property (point) 'stgit-file-patchsym))
+        (file     (get-text-property (point) 'stgit-file)))
+    (cond ((not patchsym) nil)
+          (file (cons patchsym file))
+          (both-files
+           (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
+                                (get-text-property (point) 'stgit-new-file))))
+          (t
+           (let ((file-sym (save-excursion
+                             (stgit-previous-patch)
+                             (unless (eq (stgit-patch-at-point)
+                                         patchsym)
+                               (error "Cannot find the %s patch" patchsym))
+                             (beginning-of-line)
+                             (if (= (char-after) ?-)
+                                 'stgit-old-file 
+                               'stgit-new-file))))
+             (cons patchsym (get-text-property (point) file-sym)))))))
+
+(defun stgit-patches-marked-or-at-point ()
+  "Return the symbols of the marked patches, or the patch on the current line."
+  (if stgit-marked-patches
+      stgit-marked-patches
+    (let ((patch (stgit-patch-at-point)))
+      (if patch
+          (list patch)
+        '()))))
+
+(defun stgit-goto-patch (patchsym)
+  "Move point to the line containing patch PATCHSYM.
+If that patch cannot be found, return nil."
+  (let ((p (text-property-any (point-min) (point-max)
+                              'stgit-patchsym patchsym)))
+    (when p
+      (goto-char p)
+      (move-to-column goal-column))))
+
+(defun stgit-init ()
+  "Run stg init."
+  (interactive)
+  (stgit-capture-output nil
+    (stgit-run "init"))
+  (stgit-reload))
+
+(defun stgit-mark ()
+  "Mark the patch under point."
+  (interactive)
+  (let ((patch (stgit-patch-at-point t)))
+    (stgit-add-mark patch)
+    (stgit-reload))
+  (stgit-next-patch))
+
+(defun stgit-unmark-up ()
+  "Remove mark from the patch on the previous line."
+  (interactive)
+  (stgit-previous-patch)
+  (stgit-remove-mark (stgit-patch-at-point t))
+  (stgit-reload))
+
+(defun stgit-unmark-down ()
+  "Remove mark from the patch on the current line."
+  (interactive)
+  (stgit-remove-mark (stgit-patch-at-point t))
+  (stgit-reload)
+  (stgit-next-patch))
+
+(defun stgit-rename (name)
+  "Rename the patch under point to NAME."
+  (interactive (list (read-string "Patch name: "
+                                  (symbol-name (stgit-patch-at-point t)))))
+  (let ((old-patchsym (stgit-patch-at-point t)))
+    (stgit-capture-output nil
+      (stgit-run "rename" old-patchsym name))
+    (let ((name-sym (intern name)))
+      (when (memq old-patchsym stgit-expanded-patches)
+        (setq stgit-expanded-patches
+            (cons name-sym (delq old-patchsym stgit-expanded-patches))))
+      (when (memq old-patchsym stgit-marked-patches)
+        (setq stgit-marked-patches
+            (cons name-sym (delq old-patchsym stgit-marked-patches))))
+      (stgit-reload)
+      (stgit-goto-patch name-sym))))
+
+(defun stgit-repair ()
+  "Run stg repair."
+  (interactive)
+  (stgit-capture-output nil
+    (stgit-run "repair"))
+  (stgit-reload))
+
+(defun stgit-commit (count)
+  "Run stg commit on COUNT commits.
+Interactively, the prefix argument is used as COUNT."
+  (interactive "p")
+  (stgit-capture-output nil (stgit-run "commit" "-n" count))
+  (stgit-reload))
+
+(defun stgit-uncommit (count)
+  "Run stg uncommit on COUNT commits.
+Interactively, the prefix argument is used as COUNT."
+  (interactive "p")
+  (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
+  (stgit-reload))
+
+(defun stgit-push-next (npatches)
+  "Push the first unapplied patch.
+With numeric prefix argument, push that many patches."
+  (interactive "p")
+  (stgit-capture-output nil (stgit-run "push" "-n" npatches))
+  (stgit-reload)
+  (stgit-refresh-git-status))
+
+(defun stgit-pop-next (npatches)
+  "Pop the topmost applied patch.
+With numeric prefix argument, pop that many patches."
+  (interactive "p")
+  (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
+  (stgit-reload)
+  (stgit-refresh-git-status))
+
+(defun stgit-applied-at-point ()
+  "Is the patch on the current line applied?"
+  (save-excursion
+    (beginning-of-line)
+    (looking-at "[>+]")))
+
+(defun stgit-push-or-pop ()
+  "Push or pop the patch on the current line."
+  (interactive)
+  (let ((patchsym (stgit-patch-at-point t))
+        (applied (stgit-applied-at-point)))
+    (stgit-capture-output nil
+      (stgit-run (if applied "pop" "push") patchsym))
+    (stgit-reload)))
+
+(defun stgit-goto ()
+  "Go to the patch on the current line."
+  (interactive)
+  (let ((patchsym (stgit-patch-at-point t)))
+    (stgit-capture-output nil
+      (stgit-run "goto" patchsym))
+    (stgit-reload)))
+
+(defun stgit-id (patchsym)
+  "Return the git commit id for PATCHSYM."
+  (let ((result (with-output-to-string
+                  (stgit-run-silent "id" patchsym))))
+    (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
+      (error "Cannot find commit id for %s" patchsym))
+    (match-string 1 result)))
+
+(defun stgit-show ()
+  "Show the patch on the current line."
+  (interactive)
+  (stgit-capture-output "*StGit patch*"
+    (let ((patchsym (stgit-patch-at-point)))
+      (if (not patchsym)
+          (let ((patched-file (stgit-patched-file-at-point t)))
+            (unless patched-file
+              (error "No patch or file at point"))
+            (let ((id (stgit-id (car patched-file))))
+             (if (consp (cdr patched-file))
+                 ;; two files (copy or rename)
+                 (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
+                                (cadr patched-file) (cddr patched-file))
+               ;; just one file
+               (stgit-run-git "diff" (concat id "^") id "--"
+                              (cdr patched-file)))))
+        (stgit-run "show" "-O" "--patch-with-stat" patchsym))
+      (with-current-buffer standard-output
+       (goto-char (point-min))
+       (diff-mode)))))
+
+(defun stgit-edit ()
+  "Edit the patch on the current line."
+  (interactive)
+  (let ((patchsym (stgit-patch-at-point t))
+        (edit-buf (get-buffer-create "*StGit edit*"))
+        (dir default-directory))
+    (log-edit 'stgit-confirm-edit t nil edit-buf)
+    (set (make-local-variable 'stgit-edit-patchsym) patchsym)
+    (setq default-directory dir)
+    (let ((standard-output edit-buf))
+      (stgit-run-silent "edit" "--save-template=-" patchsym))))
+
+(defun stgit-confirm-edit ()
+  (interactive)
+  (let ((file (make-temp-file "stgit-edit-")))
+    (write-region (point-min) (point-max) file)
+    (stgit-capture-output nil
+      (stgit-run "edit" "-f" file stgit-edit-patchsym))
+    (with-current-buffer log-edit-parent-buffer
+      (stgit-reload))))
+
+(defun stgit-new (add-sign)
+  "Create a new patch.
+With a prefix argument, include a \"Signed-off-by:\" line at the
+end of the patch."
+  (interactive "P")
+  (let ((edit-buf (get-buffer-create "*StGit edit*"))
+        (dir default-directory))
+    (log-edit 'stgit-confirm-new t nil edit-buf)
+    (setq default-directory dir)
+    (when add-sign
+      (save-excursion
+        (let ((standard-output (current-buffer)))
+          (stgit-run-silent "new" "--sign" "--save-template=-"))))))
+
+(defun stgit-confirm-new ()
+  (interactive)
+  (let ((file (make-temp-file "stgit-edit-")))
+    (write-region (point-min) (point-max) file)
+    (stgit-capture-output nil
+      (stgit-run "new" "-f" file))
+    (with-current-buffer log-edit-parent-buffer
+      (stgit-reload))))
+
+(defun stgit-create-patch-name (description)
+  "Create a patch name from a long description"
+  (let ((patch ""))
+    (while (> (length description) 0)
+      (cond ((string-match "\\`[a-zA-Z_-]+" description)
+             (setq patch (downcase (concat patch
+                                           (match-string 0 description))))
+             (setq description (substring description (match-end 0))))
+            ((string-match "\\` +" description)
+             (setq patch (concat patch "-"))
+             (setq description (substring description (match-end 0))))
+            ((string-match "\\`[^a-zA-Z_-]+" description)
+             (setq description (substring description (match-end 0))))))
+    (cond ((= (length patch) 0)
+           "patch")
+          ((> (length patch) 20)
+           (substring patch 0 20))
+          (t patch))))
+
+(defun stgit-delete (patchsyms)
+  "Delete the patches in PATCHSYMS.
+Interactively, delete the marked patches, or the patch at point."
+  (interactive (list (stgit-patches-marked-or-at-point)))
+  (unless patchsyms
+    (error "No patches to delete"))
+  (let ((npatches (length patchsyms)))
+    (when (yes-or-no-p (format "Really delete %d patch%s? "
+                              npatches
+                              (if (= 1 npatches) "" "es")))
+      (stgit-capture-output nil
+       (apply 'stgit-run "delete" patchsyms))
+      (stgit-reload))))
+
+(defun stgit-coalesce (patchsyms)
+  "Coalesce the patches in PATCHSYMS.
+Interactively, coalesce the marked patches."
+  (interactive (list stgit-marked-patches))
+  (when (< (length patchsyms) 2)
+    (error "Need at least two patches to coalesce"))
+  (let ((edit-buf (get-buffer-create "*StGit edit*"))
+        (dir default-directory))
+    (log-edit 'stgit-confirm-coalesce t nil edit-buf)
+    (set (make-local-variable 'stgit-patchsyms) patchsyms)
+    (setq default-directory dir)
+    (let ((standard-output edit-buf))
+      (apply 'stgit-run-silent "coalesce" "--save-template=-" patchsyms))))
+
+(defun stgit-confirm-coalesce ()
+  (interactive)
+  (let ((file (make-temp-file "stgit-edit-")))
+    (write-region (point-min) (point-max) file)
+    (stgit-capture-output nil
+      (apply 'stgit-run "coalesce" "-f" file stgit-patchsyms))
+    (with-current-buffer log-edit-parent-buffer
+      (stgit-clear-marks)
+      ;; Go to first marked patch and stay there
+      (goto-char (point-min))
+      (re-search-forward (concat "^[>+-]\\*") nil t)
+      (move-to-column goal-column)
+      (let ((pos (point)))
+        (stgit-reload)
+        (goto-char pos)))))
+
+(defun stgit-help ()
+  "Display help for the StGit mode."
+  (interactive)
+  (describe-function 'stgit-mode))
+
+(defun stgit-undo (&optional arg)
+  "Run stg undo.
+With prefix argument, run it with the --hard flag."
+  (interactive "P")
+  (stgit-capture-output nil
+    (if arg
+        (stgit-run "undo" "--hard")
+      (stgit-run "undo")))
+  (stgit-reload))
+
+(defun stgit-refresh (&optional arg)
+  "Run stg refresh.
+With prefix argument, refresh the marked patch or the patch under point."
+  (interactive "P")
+  (let ((patchargs (if arg
+                       (let ((patches (stgit-patches-marked-or-at-point)))
+                         (cond ((null patches)
+                                (error "No patch to update"))
+                               ((> (length patches) 1)
+                                (error "Too many patches selected"))
+                               (t
+                                (cons "-p" patches))))
+                     nil)))
+    (stgit-capture-output nil
+      (apply 'stgit-run "refresh" patchargs))
+    (stgit-refresh-git-status))
+  (stgit-reload))
+
+(provide 'stgit)
index 5688680..795c77b 100755 (executable)
@@ -33,9 +33,7 @@ build: build-stamp
 build-stamp: configure-stamp
        dh_testdir
 
-       # Add here commands to compile the package.
-       $(MAKE)
-       #docbook-to-man debian/stgit.sgml > stgit.1
+       $(MAKE) all doc
 
        touch build-stamp
 
@@ -56,7 +54,7 @@ install: build
        dh_installdirs
 
        # Add here commands to install the package into debian/stgit.
-       $(MAKE) DESTDIR=$(CURDIR)/debian/stgit PREFIX=/usr install
+       $(MAKE) DESTDIR=$(CURDIR)/debian/stgit prefix=/usr install install-doc install-html
 
 # Build architecture-independent files here.
 binary-indep: build install
index b7a6629..9efc089 100644 (file)
        # Automatically Bcc the address below
        #autobcc = your.name@yourcompany.com
 
+       # Automatically sign newly created patches
+       #autosign = Signed-off-by
+
        # Set to 'yes' if you don't want to use the 'resolved' command.
        # 'refresh' will automatically mark the conflicts as resolved
        #autoresolved = no
 
        # SMTP server for sending patches
+       #smtpserver = /usr/sbin/sendmail -t -i
        #smtpserver = localhost:25
 
        # Set to 'yes' to use SMTP over TLS
        # To support local parent branches:
        #pull-policy = rebase
 
-       # The three-way merge tool. 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 current -L ancestor -L patched -m -E \
-       #       \"%(branch1)s\" \"%(ancestor)s\" \"%(branch2)s\" \
-       #       > \"%(output)s\"
-
        # Interactive two/three-way merge tool. It is executed by the
        # 'resolved --interactive' command
        #i3merge = xxdiff --title1 current --title2 ancestor --title3 patched \
        # The maximum length of an automatically generated patch name
        #namelenth = 30
 
+       # Extra options to pass to "git diff" (extend/override with
+       # -O/--diff-opts). For example, -M turns on rename detection.
+       #diff-opts = -M
+
 [mail "alias"]
        # E-mail aliases used with the 'mail' command
        git = git@vger.kernel.org
diff --git a/perf/.gitignore b/perf/.gitignore
new file mode 100644 (file)
index 0000000..dfae110
--- /dev/null
@@ -0,0 +1,2 @@
+/*.orig
+/*.trash
diff --git a/perf/create_synthetic_repo.py b/perf/create_synthetic_repo.py
new file mode 100644 (file)
index 0000000..4d6ef6b
--- /dev/null
@@ -0,0 +1,61 @@
+next_mark = 1
+def get_mark():
+    global next_mark
+    next_mark += 1
+    return (next_mark - 1)
+
+def write_data(s):
+    print 'data %d' % len(s)
+    print s
+
+def write_blob(s):
+    print 'blob'
+    m = get_mark()
+    print 'mark :%d' % m
+    write_data(s)
+    return m
+
+def write_commit(branch, files, msg, parent = None):
+    print 'commit %s' % branch
+    m = get_mark()
+    print 'mark :%d' % m
+    auth = 'X Ample <xa@example.com> %d +0000' % (1000000000 + m)
+    print 'author %s' % auth
+    print 'committer %s' % auth
+    write_data(msg)
+    if parent != None:
+        print 'from :%d' % parent
+    for fn, fm in sorted(files.iteritems()):
+        print 'M 100644 :%d %s' % (fm, fn)
+    return m
+
+def set_ref(ref, mark):
+    print 'reset %s' % ref
+    print 'from :%d' % mark
+
+def stdblob(fn):
+    return ''.join('%d %s\n' % (x, fn) for x in xrange(10))
+
+def iter_paths():
+    for i in xrange(32):
+        for j in xrange(32):
+            for k in xrange(32):
+                yield '%02d/%02d/%02d' % (i, j, k)
+
+def setup():
+    def t(name): return 'refs/tags/%s' % name
+    files = dict((fn, write_blob(stdblob(fn))) for fn in iter_paths())
+    initial = write_commit(t('bomb-base'), files, 'Initial commit')
+    set_ref(t('bomb-top'), initial)
+    for fn in iter_paths():
+        write_commit(t('bomb-top'),
+                     { fn: write_blob(stdblob(fn) + 'Last line\n') },
+                     'Add last line to %s' % fn)
+    write_commit(t('add-file'), { 'woo-hoo.txt': write_blob('woo-hoo\n') },
+                 'Add a new file', parent = initial)
+    files = dict((fn, write_blob('First line\n' + stdblob(fn)))
+                 for fn in iter_paths())
+    write_commit(t('modify-all'), files, 'Add first line to all files',
+                 parent = initial)
+
+setup()
diff --git a/perf/find_patchbomb.py b/perf/find_patchbomb.py
new file mode 100644 (file)
index 0000000..69a78c7
--- /dev/null
@@ -0,0 +1,31 @@
+# Feed this with git rev-list HEAD --parents
+
+import sys
+
+parents = {}
+for line in sys.stdin.readlines():
+    commits = line.split()
+    parents[commits[0]] = commits[1:]
+
+sequence_num = {}
+stack = []
+for commit in parents.keys():
+    stack.append(commit)
+    while stack:
+        c = stack.pop()
+        if c in sequence_num:
+            continue
+        ps = parents[c]
+        if len(ps) == 1:
+            p = ps[0]
+            if p in sequence_num:
+                sequence_num[c] = 1 + sequence_num[p]
+            else:
+                stack.append(c)
+                stack.append(p)
+        else:
+            sequence_num[c] = 0
+
+(num, commit) = max((num, commit) for (commit, num)
+                    in sequence_num.iteritems())
+print '%s is a sequence of %d patches' % (commit, num)
diff --git a/perf/perftest.py b/perf/perftest.py
new file mode 100644 (file)
index 0000000..e5ed04b
--- /dev/null
@@ -0,0 +1,96 @@
+import datetime, os, os.path, subprocess, sys
+
+def duration(t1, t2):
+    d = t2 - t1
+    return 86400*d.days + d.seconds + 1e-6*d.microseconds
+
+class Run(object):
+    def __init__(self):
+        self.__cwd = None
+        self.__log = []
+    def __logfile(self, cmd):
+        fn = os.path.join(os.getcwd(), '%04d.log' % len(self.__log))
+        f = open(fn, 'w')
+        f.write(' '.join(cmd) + '\n' + '-'*70 + '\n\n')
+        f.close()
+        return fn
+    def __call__(self, *cmd, **args):
+        env = dict(os.environ)
+        env['STGIT_SUBPROCESS_LOG'] = 'profile:' + self.__logfile(cmd)
+        kwargs = { 'cwd': self.__cwd, 'env': env }
+        if args.get('capture_stdout', False):
+            kwargs['stdout'] = subprocess.PIPE
+        start = datetime.datetime.now()
+        p = subprocess.Popen(cmd, **kwargs)
+        (out, err) = p.communicate()
+        stop = datetime.datetime.now()
+        self.__log.append((cmd, duration(start, stop)))
+        return out
+    def cd(self, dir):
+        self.__cwd = dir
+    def summary(self):
+        def pcmd(c): return ' '.join(c)
+        def ptime(t): return '%.3f' % t
+        (cs, times) = zip(*self.__log)
+        ttime = sum(times)
+        cl = max(len(pcmd(c)) for c in cs)
+        tl = max(len(ptime(t)) for t in list(times) + [ttime])
+        for (c, t) in self.__log:
+            print '%*s  %*s' % (tl, ptime(t), -cl, pcmd(c))
+        print '%*s' % (tl, ptime(ttime))
+
+perftests = {}
+perftestdesc = {}
+def perftest(desc, name = None):
+    def decorator(f):
+        def g():
+            r = Run()
+            f(r)
+            r.summary()
+        perftests[name or f.__name__] = g
+        perftestdesc[name or f.__name__] = desc
+        return g
+    return decorator
+
+def copy_testdir(dir):
+    tmp = dir + '.trash'
+    r = Run()
+    r('rsync', '-a', '--delete', dir + '.orig/', tmp)
+    return tmp
+
+def new_rebase(r, ref):
+    top = r('stg', 'top', capture_stdout = True)
+    r('stg', 'pop', '-a')
+    r('git', 'reset', '--hard', ref)
+    r('stg', 'goto', top.strip())
+
+def old_rebase(r, ref):
+    r('stg', 'rebase', ref)
+
+def def_rebasetest(rebase, dir, tag):
+    @perftest('%s rebase onto %s in %s' % (rebase, tag, dir),
+              'rebase-%srebase-%s-%s' % (rebase, tag, dir))
+    def rebasetest(r):
+        r.cd(copy_testdir(dir))
+        r('stg', 'init')
+        if dir == 'synt':
+            r('stg', 'uncommit', '-n', '500')
+        else:
+            r('stg', 'uncommit', '-x', '-t', 'bomb-base')
+        if rebase == 'new':
+            new_rebase(r, tag)
+        else:
+            old_rebase(r, tag)
+for rebase in ['old', 'new']:
+    for (dir, tag) in [('synt', 'add-file'),
+                       ('synt', 'modify-all'),
+                       ('linux', 'add-file')]:
+        def_rebasetest(rebase, dir, tag)
+
+args = sys.argv[1:]
+if len(args) == 0:
+    for (fun, desc) in sorted(perftestdesc.iteritems()):
+        print '%s: %s' % (fun, desc)
+else:
+    for test in args:
+        perftests[test]()
diff --git a/perf/setup.sh b/perf/setup.sh
new file mode 100644 (file)
index 0000000..b92ddfc
--- /dev/null
@@ -0,0 +1,52 @@
+krepo='git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git'
+
+get_linux() {
+    rm -rf linux.orig
+    git clone "$krepo" linux.orig
+}
+
+mod_linux() {
+    # Tag the top and base of a very long linear sequence of commits.
+    git tag bomb-top 85040bcb4643cba578839e953f25e2d1965d83d0
+    git tag bomb-base bomb-top~1470
+
+    # Add a file at the base of the linear sequence.
+    git checkout bomb-base
+    echo "woo-hoo" > woo-hoo.txt
+    git add woo-hoo.txt
+    git commit -m "Add a file"
+    git tag add-file
+
+    # Clean up and go to start position.
+    git gc
+    git update-ref refs/heads/master bomb-top
+    git checkout master
+}
+
+setup_linux () {
+    get_linux
+    ( cd linux.orig && mod_linux )
+}
+
+create_empty () {
+    dir="$1"
+    rm -rf $dir
+    mkdir $dir
+    ( cd $dir && git init )
+}
+
+fill_synthetic () {
+    python ../create_synthetic_repo.py | git fast-import
+    git gc --aggressive
+    git update-ref refs/heads/master bomb-top
+    git checkout master
+}
+
+setup_synthetic()
+{
+    create_empty synt.orig
+    ( cd synt.orig && fill_synthetic )
+}
+
+setup_linux
+setup_synthetic
index 10b3715..fb67958 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
 import sys, glob, os
 from distutils.core import setup
 
-from stgit.version import version, git_min_ver, python_min_ver
+from stgit import version
 
 def __version_to_list(version):
     """Convert a version string to a list of numbers or strings
@@ -28,9 +28,9 @@ def __check_python_version():
     """Check the minimum Python version
     """
     pyver = '.'.join(map(lambda x: str(x), sys.version_info))
-    if not __check_min_version(python_min_ver, pyver):
+    if not __check_min_version(version.python_min_ver, pyver):
         print >> sys.stderr, 'Python version %s or newer required. Found %s' \
-              % (python_min_ver, pyver)
+              % (version.python_min_ver, pyver)
         sys.exit(1)
 
 def __check_git_version():
@@ -38,11 +38,31 @@ def __check_git_version():
     """
     from stgit.run import Run
     gitver = Run('git', '--version').output_one_line().split()[2]
-    if not __check_min_version(git_min_ver, gitver):
+    if not __check_min_version(version.git_min_ver, gitver):
         print >> sys.stderr, 'GIT version %s or newer required. Found %s' \
-              % (git_min_ver, gitver)
+              % (version.git_min_ver, gitver)
         sys.exit(1)
 
+def __run_setup():
+    setup(name = 'stgit',
+          version = version.version,
+          license = 'GPLv2',
+          author = 'Catalin Marinas',
+          author_email = 'catalin.marinas@gmail.com',
+          url = 'http://www.procode.org/stgit/',
+          description = 'Stacked GIT',
+          long_description = 'Push/pop utility on top of GIT',
+          scripts = ['stg'],
+          packages = ['stgit', 'stgit.commands', 'stgit.lib'],
+          data_files = [
+            ('share/stgit/templates', glob.glob('templates/*.tmpl')),
+            ('share/stgit/examples', glob.glob('examples/*.tmpl')),
+            ('share/stgit/examples', ['examples/gitconfig']),
+            ('share/stgit/contrib', ['contrib/diffcol.sh',
+                                     'contrib/stgbashprompt.sh']),
+            ('share/stgit/completion', ['stgit-completion.bash'])
+            ])
+
 # Check the minimum versions required
 if sys.argv[1] in ['install', 'build']:
     __check_python_version()
@@ -51,24 +71,11 @@ if sys.argv[1] in ['install', 'build']:
 # ensure readable template files
 old_mask = os.umask(0022)
 
-setup(name = 'stgit',
-      version = version,
-      license = 'GPLv2',
-      author = 'Catalin Marinas',
-      author_email = 'catalin.marinas@gmail.com',
-      url = 'http://www.procode.org/stgit/',
-      description = 'Stacked GIT',
-      long_description = 'Push/pop utility on top of GIT',
-      scripts = ['stg'],
-      packages = ['stgit', 'stgit.commands'],
-      data_files = [('share/stgit/templates', glob.glob('templates/*.tmpl')),
-                    ('share/stgit/examples', glob.glob('examples/*.tmpl')),
-                    ('share/stgit/examples', ['examples/gitconfig']),
-                    ('share/stgit/contrib', ['contrib/diffcol.sh',
-                                             'contrib/stgbashprompt.sh',
-                                             'contrib/stgit-completion.bash']),
-                    ('share/doc/stgit', glob.glob('doc/*.txt'))]
-      )
+try:
+    version.write_builtin_version()
+    __run_setup()
+finally:
+    version.delete_builtin_version()
 
 # restore the old mask
 os.umask(old_mask)
diff --git a/stg-build b/stg-build
new file mode 100755 (executable)
index 0000000..2af6523
--- /dev/null
+++ b/stg-build
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- python -*-
+import optparse, sys
+import stgit.main
+from stgit import argparse, commands, completion
+
+def main():
+    op = optparse.OptionParser()
+    op.add_option('--asciidoc', metavar = 'CMD',
+                  help = 'Print asciidoc documentation for a command')
+    op.add_option('--commands', action = 'store_true',
+                  help = 'Print list of all stg subcommands')
+    op.add_option('--cmd-list', action = 'store_true',
+                  help = 'Print asciidoc command list')
+    op.add_option('--py-cmd-list', action = 'store_true',
+                  help = 'Write Python command list')
+    op.add_option('--bash-completion', action = 'store_true',
+                  help = 'Write bash completion code')
+    options, args = op.parse_args()
+    if args:
+        op.error('Wrong number of arguments')
+    if options.asciidoc:
+        argparse.write_asciidoc(stgit.main.commands[options.asciidoc],
+                                sys.stdout)
+    elif options.commands:
+        for cmd in sorted(commands.get_commands(
+                allow_cached = False).iterkeys()):
+            print cmd
+    elif options.cmd_list:
+        commands.asciidoc_command_list(
+            commands.get_commands(allow_cached = False), sys.stdout)
+    elif options.py_cmd_list:
+        commands.py_commands(commands.get_commands(allow_cached = False),
+                             sys.stdout)
+    elif options.bash_completion:
+        completion.write_completion(sys.stdout)
+    else:
+        op.error('No command')
+
+if __name__ == '__main__':
+    main()
diff --git a/stgit/argparse.py b/stgit/argparse.py
new file mode 100644 (file)
index 0000000..418a506
--- /dev/null
@@ -0,0 +1,281 @@
+"""This module provides a layer on top of the standard library's
+C{optparse} module, so that we can easily generate both interactive
+help and asciidoc documentation (such as man pages)."""
+
+import optparse, sys, textwrap
+from stgit import utils
+from stgit.config import config
+
+def _splitlist(lst, split_on):
+    """Iterate over the sublists of lst that are separated by an element e
+    such that split_on(e) is true."""
+    current = []
+    for e in lst:
+        if split_on(e):
+            yield current
+            current = []
+        else:
+            current.append(e)
+    yield current
+
+def _paragraphs(s):
+    """Split a string s into a list of paragraphs, each of which is a list
+    of lines."""
+    lines = [line.rstrip() for line in textwrap.dedent(s).strip().splitlines()]
+    return [p for p in _splitlist(lines, lambda line: not line.strip()) if p]
+
+class opt(object):
+    """Represents a command-line flag."""
+    def __init__(self, *pargs, **kwargs):
+        self.pargs = pargs
+        self.kwargs = kwargs
+    def get_option(self):
+        kwargs = dict(self.kwargs)
+        kwargs['help'] = kwargs['short']
+        for k in ['short', 'long', 'args']:
+            kwargs.pop(k, None)
+        return optparse.make_option(*self.pargs, **kwargs)
+    def metavar(self):
+        o = self.get_option()
+        if not o.takes_value():
+            return None
+        if o.metavar:
+            return o.metavar
+        for flag in self.pargs:
+            if flag.startswith('--'):
+                return utils.strip_prefix('--', flag).upper()
+        raise Exception('Cannot determine metavar')
+    def write_asciidoc(self, f):
+        for flag in self.pargs:
+            f.write(flag)
+            m = self.metavar()
+            if m:
+                f.write(' ' + m)
+            f.write('::\n')
+        paras = _paragraphs(self.kwargs.get('long', self.kwargs['short'] + '.'))
+        for line in paras[0]:
+            f.write(' '*8 + line + '\n')
+        for para in paras[1:]:
+            f.write('+\n')
+            for line in para:
+                f.write(line + '\n')
+    @property
+    def flags(self):
+        return self.pargs
+    @property
+    def args(self):
+        if self.kwargs.get('action', None) in ['store_true', 'store_false']:
+            default = []
+        else:
+            default = [files]
+        return self.kwargs.get('args', default)
+
+def _cmd_name(cmd_mod):
+    return getattr(cmd_mod, 'name', cmd_mod.__name__.split('.')[-1])
+
+def make_option_parser(cmd):
+    pad = ' '*len('Usage: ')
+    return optparse.OptionParser(
+        prog = 'stg %s' % _cmd_name(cmd),
+        usage = (('\n' + pad).join('%%prog %s' % u for u in cmd.usage) +
+                 '\n\n' + cmd.help),
+        option_list = [o.get_option() for o in cmd.options])
+
+def _write_underlined(s, u, f):
+    f.write(s + '\n')
+    f.write(u*len(s) + '\n')
+
+def write_asciidoc(cmd, f):
+    _write_underlined('stg-%s(1)' % _cmd_name(cmd), '=', f)
+    f.write('\n')
+    _write_underlined('NAME', '-', f)
+    f.write('stg-%s - %s\n\n' % (_cmd_name(cmd), cmd.help))
+    _write_underlined('SYNOPSIS', '-', f)
+    f.write('[verse]\n')
+    for u in cmd.usage:
+        f.write("'stg' %s %s\n" % (_cmd_name(cmd), u))
+    f.write('\n')
+    _write_underlined('DESCRIPTION', '-', f)
+    f.write('\n%s\n\n' % cmd.description.strip('\n'))
+    if cmd.options:
+        _write_underlined('OPTIONS', '-', f)
+        for o in cmd.options:
+            o.write_asciidoc(f)
+            f.write('\n')
+    _write_underlined('StGit', '-', f)
+    f.write('Part of the StGit suite - see manlink:stg[1]\n')
+
+def sign_options():
+    def callback(option, opt_str, value, parser, sign_str):
+        if parser.values.sign_str not in [None, sign_str]:
+            raise optparse.OptionValueError(
+                '--ack and --sign were both specified')
+        parser.values.sign_str = sign_str
+    return [
+        opt('--sign', action = 'callback', dest = 'sign_str', args = [],
+            callback = callback, callback_args = ('Signed-off-by',),
+            short = 'Add "Signed-off-by:" line', long = """
+            Add a "Signed-off-by:" to the end of the patch."""),
+        opt('--ack', action = 'callback', dest = 'sign_str', args = [],
+            callback = callback, callback_args = ('Acked-by',),
+            short = 'Add "Acked-by:" line', long = """
+            Add an "Acked-by:" line to the end of the patch.""")]
+
+def message_options(save_template):
+    def no_dup(parser):
+        if parser.values.message != None:
+            raise optparse.OptionValueError(
+                'Cannot give more than one --message or --file')
+    def no_combine(parser):
+        if (save_template and parser.values.message != None
+            and parser.values.save_template != None):
+            raise optparse.OptionValueError(
+                'Cannot give both --message/--file and --save-template')
+    def msg_callback(option, opt_str, value, parser):
+        no_dup(parser)
+        parser.values.message = value
+        no_combine(parser)
+    def file_callback(option, opt_str, value, parser):
+        no_dup(parser)
+        if value == '-':
+            parser.values.message = sys.stdin.read()
+        else:
+            f = file(value)
+            parser.values.message = f.read()
+            f.close()
+        no_combine(parser)
+    def templ_callback(option, opt_str, value, parser):
+        if value == '-':
+            def w(s):
+                sys.stdout.write(s)
+        else:
+            def w(s):
+                f = file(value, 'w+')
+                f.write(s)
+                f.close()
+        parser.values.save_template = w
+        no_combine(parser)
+    opts = [
+        opt('-m', '--message', action = 'callback',
+            callback = msg_callback, dest = 'message', type = 'string',
+            short = 'Use MESSAGE instead of invoking the editor'),
+        opt('-f', '--file', action = 'callback', callback = file_callback,
+            dest = 'message', type = 'string', args = [files],
+            short = 'Use FILE instead of invoking the editor', long = """
+            Use the contents of FILE instead of invoking the editor.
+            (If FILE is "-", write to stdout.)""")]
+    if save_template:
+        opts.append(
+            opt('--save-template', action = 'callback', dest = 'save_template',
+                callback = templ_callback, metavar = 'FILE', type = 'string',
+                short = 'Save the message template to FILE and exit', long = """
+                Instead of running the command, just write the message
+                template to FILE, and exit. (If FILE is "-", write to
+                stdout.)
+
+                When driving StGit from another program, it is often
+                useful to first call a command with '--save-template',
+                then let the user edit the message, and then call the
+                same command with '--file'."""))
+    return opts
+
+def diff_opts_option():
+    def diff_opts_callback(option, opt_str, value, parser):
+        if value:
+            parser.values.diff_flags.extend(value.split())
+        else:
+            parser.values.diff_flags = []
+    return [
+        opt('-O', '--diff-opts', dest = 'diff_flags',
+            default = (config.get('stgit.diff-opts') or '').split(),
+            action = 'callback', callback = diff_opts_callback,
+            type = 'string', metavar = 'OPTIONS',
+            args = [strings('-M', '-C')],
+            short = 'Extra options to pass to "git diff"')]
+
+def _person_opts(person, short):
+    """Sets options.<person> to a function that modifies a Person
+    according to the commandline options."""
+    def short_callback(option, opt_str, value, parser, field):
+        f = getattr(parser.values, person)
+        setattr(parser.values, person,
+                lambda p: getattr(f(p), 'set_' + field)(value))
+    def full_callback(option, opt_str, value, parser):
+        ne = utils.parse_name_email(value)
+        if not ne:
+            raise optparse.OptionValueError(
+                'Bad %s specification: %r' % (opt_str, value))
+        name, email = ne
+        short_callback(option, opt_str, name, parser, 'name')
+        short_callback(option, opt_str, email, parser, 'email')
+    return (
+        [opt('--%s' % person, metavar = '"NAME <EMAIL>"', type = 'string',
+             action = 'callback', callback = full_callback, dest = person,
+             default = lambda p: p, short = 'Set the %s details' % person)] +
+        [opt('--%s%s' % (short, f), metavar = f.upper(), type = 'string',
+             action = 'callback', callback = short_callback, dest = person,
+             callback_args = (f,), short = 'Set the %s %s' % (person, f))
+         for f in ['name', 'email', 'date']])
+
+def author_options():
+    return _person_opts('author', 'auth')
+
+class CompgenBase(object):
+    def actions(self, var): return set()
+    def words(self, var): return set()
+    def command(self, var):
+        cmd = ['compgen']
+        for act in self.actions(var):
+            cmd += ['-A', act]
+        words = self.words(var)
+        if words:
+            cmd += ['-W', '"%s"' % ' '.join(words)]
+        cmd += ['--', '"%s"' % var]
+        return ' '.join(cmd)
+
+class CompgenJoin(CompgenBase):
+    def __init__(self, a, b):
+        assert isinstance(a, CompgenBase)
+        assert isinstance(b, CompgenBase)
+        self.__a = a
+        self.__b = b
+    def words(self, var): return self.__a.words(var) | self.__b.words(var)
+    def actions(self, var): return self.__a.actions(var) | self.__b.actions(var)
+
+class Compgen(CompgenBase):
+    def __init__(self, words = frozenset(), actions = frozenset()):
+        self.__words = set(words)
+        self.__actions = set(actions)
+    def actions(self, var): return self.__actions
+    def words(self, var): return self.__words
+
+def compjoin(compgens):
+    comp = Compgen()
+    for c in compgens:
+        comp = CompgenJoin(comp, c)
+    return comp
+
+all_branches = Compgen(['$(_all_branches)'])
+stg_branches = Compgen(['$(_stg_branches)'])
+applied_patches = Compgen(['$(_applied_patches)'])
+other_applied_patches = Compgen(['$(_other_applied_patches)'])
+unapplied_patches = Compgen(['$(_unapplied_patches)'])
+hidden_patches = Compgen(['$(_hidden_patches)'])
+commit = Compgen(['$(_all_branches) $(_tags) $(_remotes)'])
+conflicting_files = Compgen(['$(_conflicting_files)'])
+dirty_files = Compgen(['$(_dirty_files)'])
+unknown_files = Compgen(['$(_unknown_files)'])
+known_files = Compgen(['$(_known_files)'])
+repo = Compgen(actions = ['directory'])
+dir = Compgen(actions = ['directory'])
+files = Compgen(actions = ['file'])
+def strings(*ss): return Compgen(ss)
+class patch_range(CompgenBase):
+    def __init__(self, *endpoints):
+        self.__endpoints = endpoints
+    def words(self, var):
+        words = set()
+        for e in self.__endpoints:
+            assert not e.actions(var)
+            words |= e.words(var)
+        return set(['$(_patch_range "%s" "%s")' % (' '.join(words), var)])
diff --git a/stgit/commands/.gitignore b/stgit/commands/.gitignore
new file mode 100644 (file)
index 0000000..eff10ee
--- /dev/null
@@ -0,0 +1 @@
+/cmdlist.py
index 4b03e3a..54a9326 100644 (file)
@@ -1,5 +1,8 @@
+# -*- coding: utf-8 -*-
+
 __copyright__ = """
 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+Copyright (C) 2008, Karl Hasselström <kha@treskal.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
@@ -14,3 +17,78 @@ 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
+from stgit import utils
+
+def get_command(mod):
+    """Import and return the given command module."""
+    return __import__(__name__ + '.' + mod, globals(), locals(), ['*'])
+
+_kinds = [('repo', 'Repository commands'),
+          ('stack', 'Stack (branch) commands'),
+          ('patch', 'Patch commands'),
+          ('wc', 'Index/worktree commands')]
+_kind_order = [kind for kind, desc in _kinds]
+_kinds = dict(_kinds)
+
+def _find_commands():
+    for p in __path__:
+        for fn in os.listdir(p):
+            if not fn.endswith('.py'):
+                continue
+            mod = utils.strip_suffix('.py', fn)
+            m = get_command(mod)
+            if not hasattr(m, 'usage'):
+                continue
+            yield mod, m
+
+def get_commands(allow_cached = True):
+    """Return a map from command name to a tuple of module name, command
+    type, and one-line command help."""
+    if allow_cached:
+        try:
+            from stgit.commands.cmdlist import command_list
+            return command_list
+        except ImportError:
+            # cmdlist.py doesn't exist, so do it the expensive way.
+            pass
+    return dict((getattr(m, 'name', mod), (mod, _kinds[m.kind], m.help))
+                for mod, m in _find_commands())
+
+def py_commands(commands, f):
+    f.write('command_list = {\n')
+    for key, val in sorted(commands.iteritems()):
+        f.write('    %r: %r,\n' % (key, val))
+    f.write('    }\n')
+
+def _command_list(commands):
+    kinds = {}
+    for cmd, (mod, kind, help) in commands.iteritems():
+        kinds.setdefault(kind, {})[cmd] = help
+    for kind in _kind_order:
+        kind = _kinds[kind]
+        yield kind, sorted(kinds[kind].iteritems())
+
+def pretty_command_list(commands, f):
+    cmd_len = max(len(cmd) for cmd in commands.iterkeys())
+    sep = ''
+    for kind, cmds in _command_list(commands):
+        f.write(sep)
+        sep = '\n'
+        f.write('%s:\n' % kind)
+        for cmd, help in cmds:
+            f.write('  %*s  %s\n' % (-cmd_len, cmd, help))
+
+def _write_underlined(s, u, f):
+    f.write(s + '\n')
+    f.write(u*len(s) + '\n')
+
+def asciidoc_command_list(commands, f):
+    for kind, cmds in _command_list(commands):
+        _write_underlined(kind, '~', f)
+        f.write('\n')
+        for cmd, help in cmds:
+            f.write('stgsublink:%s[]::\n' % cmd)
+            f.write('    %s\n' % help)
+        f.write('\n')
diff --git a/stgit/commands/add.py b/stgit/commands/add.py
deleted file mode 100644 (file)
index ceea188..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-
-__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 stgit.commands.common import *
-from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'add files or directories to the repository'
-usage = """%prog [options] <files/dirs...>
-
-Add the files or directories passed as arguments to the
-repository. When a directory name is given, all the files and
-subdirectories are recursively added."""
-
-directory = DirectoryHasRepository(needs_current_series = False)
-options = []
-
-
-def func(parser, options, args):
-    """Add files or directories to the repository
-    """
-    if len(args) < 1:
-        parser.error('incorrect number of arguments')
-
-    git.add(args)
diff --git a/stgit/commands/applied.py b/stgit/commands/applied.py
deleted file mode 100644 (file)
index 45d0926..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-
-__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 stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git
-
-
-help = 'print the applied patches'
-usage = """%prog [options]
-
-List the patches from the series which were already pushed onto the
-stack.  They are listed in the order in which they were pushed, the
-last one being the current (topmost) patch."""
-
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
-           make_option('-c', '--count',
-                       help = 'print the number of applied patches',
-                       action = 'store_true')]
-
-
-def func(parser, options, args):
-    """Show the applied patches
-    """
-    if len(args) != 0:
-        parser.error('incorrect number of arguments')
-
-    applied = crt_series.get_applied()
-
-    if options.count:
-        out.stdout(len(applied))
-    else:
-        for p in applied:
-            out.stdout(p)
index 50684bb..3d912fc 100644 (file)
@@ -1,6 +1,3 @@
-"""Branch command
-"""
-
 __copyright__ = """
 Copyright (C) 2005, Chuck Lever <cel@netapp.com>
 
@@ -19,55 +16,95 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os, time, re
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git, basedir
-
-
-help = 'manage patch stacks'
-usage = """%prog [options] branch-name [commit-id]
-
+from stgit import argparse, stack, git, basedir
+from stgit.lib import log
+
+help = 'Branch operations: switch, list, create, rename, delete, ...'
+kind = 'stack'
+usage = ['',
+         '<branch>',
+         '--list',
+         '--create <new-branch> [<committish>]',
+         '--clone [<new-branch>]',
+         '--rename <old-name> <new-name>',
+         '--protect [<branch>]',
+         '--unprotect [<branch>]',
+         '--delete [--force] <branch>',
+         '--description=<description> [<branch>]']
+description = """
 Create, clone, switch between, rename, or delete development branches
-within a git repository.  By default, a single branch called 'master'
-is always created in a new repository.  This subcommand allows you to
-manage several patch series in the same repository via GIT branches.
-
-When displaying the branches, the names can be prefixed with
-'s' (StGIT managed) or 'p' (protected).
-
-If not given any options, switch to the named branch."""
-
-directory = DirectoryGotoToplevel()
-options = [make_option('-c', '--create',
-                       help = 'create a new development branch',
-                       action = 'store_true'),
-           make_option('--clone',
-                       help = 'clone the contents of the current branch',
-                       action = 'store_true'),
-           make_option('--delete',
-                       help = 'delete an existing development branch',
-                       action = 'store_true'),
-           make_option('-d', '--description',
-                       help = 'set the branch description'),
-           make_option('--force',
-                       help = 'force a delete when the series is not empty',
-                       action = 'store_true'),
-           make_option('-l', '--list',
-                       help = 'list branches contained in this repository',
-                       action = 'store_true'),
-           make_option('-p', '--protect',
-                       help = 'prevent StGIT from modifying this branch',
-                       action = 'store_true'),
-           make_option('-r', '--rename',
-                       help = 'rename an existing development branch',
-                       action = 'store_true'),
-           make_option('-u', '--unprotect',
-                       help = 'allow StGIT to modify this branch',
-                       action = 'store_true')]
-
+within a git repository.
+
+'stg branch'::
+        Display the name of the current branch.
+
+'stg branch' <branch>::
+        Switch to the given branch."""
+
+args = [argparse.all_branches]
+options = [
+    opt('-l', '--list', action = 'store_true',
+        short = 'List the branches contained in this repository', long = """
+        List each branch in the current repository, followed by its
+        branch description (if any). The current branch is prefixed
+        with '>'. Branches that have been initialized for StGit (with
+        stglink:init[]) are prefixed with 's'. Protected branches are
+        prefixed with 'p'."""),
+    opt('-c', '--create', action = 'store_true',
+        short = 'Create (and switch to) a new branch', long = """
+        Create (and switch to) a new branch. The new branch is already
+        initialized as an StGit patch stack, so you do not have to run
+        stglink:init[] manually. If you give a committish argument,
+        the new branch is based there; otherwise, it is based at the
+        current HEAD.
+
+        StGit will try to detect the branch off of which the new
+        branch is forked, as well as the remote repository from which
+        that parent branch is taken (if any), so that running
+        stglink:pull[] will automatically pull new commits from the
+        correct branch. It will warn if it cannot guess the parent
+        branch (e.g. if you do not specify a branch name as
+        committish)."""),
+    opt('--clone', action = 'store_true',
+        short = 'Clone the contents of the current branch', long = """
+        Clone the current branch, under the name <new-branch> if
+        specified, or using the current branch's name plus a
+        timestamp.
+
+        The description of the new branch is set to tell it is a clone
+        of the current branch. The parent information of the new
+        branch is copied from the current branch."""),
+    opt('-r', '--rename', action = 'store_true',
+        short = 'Rename an existing branch'),
+    opt('-p', '--protect', action = 'store_true',
+        short = 'Prevent StGit from modifying a branch', long = """
+        Prevent StGit from modifying a branch -- either the current
+        one, or one named on the command line."""),
+    opt('-u', '--unprotect', action = 'store_true',
+        short = 'Allow StGit to modify a branch', long = """
+        Allow StGit to modify a branch -- either the current one, or
+        one named on the command line. This undoes the effect of an
+        earlier 'stg branch --protect' command."""),
+    opt('--delete', action = 'store_true',
+        short = 'Delete a branch', long = """
+        Delete the named branch. If there are any patches left in the
+        branch, StGit will refuse to delete it unless you give the
+        '--force' flag.
+
+        A protected branch cannot be deleted; it must be unprotected
+        first (see '--unprotect' above).
+
+        If you delete the current branch, you are switched to the
+        "master" branch, if it exists."""),
+    opt('-d', '--description', short = 'Set the branch description'),
+    opt('--force', action = 'store_true',
+        short = 'Force a delete when the series is not empty')]
+
+directory = DirectoryGotoToplevel(log = False)
 
 def __is_current_branch(branch_name):
     return crt_series.get_name() == branch_name
@@ -161,6 +198,7 @@ def func(parser, options, args):
                                    parent_branch = parentbranch)
 
         out.info('Branch "%s" created' % args[0])
+        log.compat_log_entry('branch --create')
         return
 
     elif options.clone:
@@ -181,6 +219,8 @@ def func(parser, options, args):
         crt_series.clone(clone)
         out.done()
 
+        log.copy_log(log.default_repo(), crt_series.get_name(), clone,
+                     'branch --clone')
         return
 
     elif options.delete:
@@ -188,6 +228,7 @@ def func(parser, options, args):
         if len(args) != 1:
             parser.error('incorrect number of arguments')
         __delete_branch(args[0], options.force)
+        log.delete_log(log.default_repo(), args[0])
         return
 
     elif options.list:
@@ -195,13 +236,16 @@ def func(parser, options, args):
         if len(args) != 0:
             parser.error('incorrect number of arguments')
 
-        branches = git.get_heads()
-        branches.sort()
+        branches = set(git.get_heads())
+        for br in set(branches):
+            m = re.match(r'^(.*)\.stgit$', br)
+            if m and m.group(1) in branches:
+                branches.remove(br)
 
         if branches:
             out.info('Available branches:')
             max_len = max([len(i) for i in branches])
-            for i in branches:
+            for i in sorted(branches):
                 __print_branch(i, max_len)
         else:
             out.info('No branches')
@@ -238,7 +282,7 @@ def func(parser, options, args):
         stack.Series(args[0]).rename(args[1])
 
         out.info('Renamed branch "%s" to "%s"' % (args[0], args[1]))
-
+        log.rename_log(log.default_repo(), args[0], args[1], 'branch --rename')
         return
 
     elif options.unprotect:
index c703418..9b48e7b 100644 (file)
@@ -15,43 +15,45 @@ 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 stgit.commands.common import *
-from stgit.utils import *
+from stgit.argparse import opt
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'delete the empty patches in the series'
-usage = """%prog [options]
+from stgit.commands import common
+from stgit.lib import transaction
 
+help = 'Delete the empty patches in the series'
+kind = 'stack'
+usage = ['']
+description = """
 Delete the empty patches in the whole series or only those applied or
 unapplied. A patch is considered empty if the two commit objects
 representing its boundaries refer to the same tree object."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-a', '--applied',
-                       help = 'delete the empty applied patches',
-                       action = 'store_true'),
-           make_option('-u', '--unapplied',
-                       help = 'delete the empty unapplied patches',
-                       action = 'store_true')]
-
-
-def __delete_empty(patches, applied):
-    """Delete the empty patches
-    """
-    for p in patches:
-        if crt_series.empty_patch(p):
-            out.start('Deleting patch "%s"' % p)
-            if applied and crt_series.patch_applied(p):
-                crt_series.pop_patch(p)
-            crt_series.delete_patch(p)
-            out.done()
-        elif applied and crt_series.patch_unapplied(p):
-            crt_series.push_patch(p)
+args = []
+options = [
+    opt('-a', '--applied', action = 'store_true',
+        short = 'Delete the empty applied patches'),
+    opt('-u', '--unapplied', action = 'store_true',
+        short = 'Delete the empty unapplied patches')]
+
+directory = common.DirectoryHasRepositoryLib()
+
+def _clean(stack, clean_applied, clean_unapplied):
+    trans = transaction.StackTransaction(stack, 'clean', allow_conflicts = True)
+    def del_patch(pn):
+        if pn in stack.patchorder.applied:
+            if pn == stack.patchorder.applied[-1]:
+                # We're about to clean away the topmost patch. Don't
+                # do that if we have conflicts, since that means the
+                # patch is only empty because the conflicts have made
+                # us dump its contents into the index and worktree.
+                if stack.repository.default_index.conflicts():
+                    return False
+            return clean_applied and trans.patches[pn].data.is_nochange()
+        elif pn in stack.patchorder.unapplied:
+            return clean_unapplied and trans.patches[pn].data.is_nochange()
+    for pn in trans.delete_patches(del_patch):
+        trans.push_patch(pn)
+    trans.run()
 
 def func(parser, options, args):
     """Delete the empty patches in the series
@@ -59,19 +61,8 @@ def func(parser, options, args):
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    check_local_changes()
-    check_conflicts()
-    check_head_top_equal(crt_series)
-
     if not (options.applied or options.unapplied):
         options.applied = options.unapplied = True
 
-    if options.applied:
-        applied = crt_series.get_applied()
-        __delete_empty(applied, True)
-
-    if options.unapplied:
-        unapplied = crt_series.get_unapplied()
-        __delete_empty(unapplied, False)
-
-    print_crt_patch(crt_series)
+    _clean(directory.repository.current_stack,
+           options.applied, options.unapplied)
index c3b0bbe..efb7198 100644 (file)
@@ -16,22 +16,29 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
+help = 'Make a local clone of a remote repository'
+kind = 'repo'
+usage = ['<repository> <dir>']
+description = """
+Clone a git repository into the local directory <dir> (using
+stglink:clone[]) and initialise the local branch "master".
 
-help = 'make a local clone of a remote repository'
-usage = """%prog [options] <repository> <dir>
+This operation is for example suitable to start working using the
+"tracking branch" workflow (see link:stg[1]). Other means to setup
+an StGit stack are stglink:init[] and the '--create' and '--clone'
+commands of stglink:branch[].
 
-Clone a GIT <repository> into the local <dir> and initialise the
-patch stack."""
+The target directory <dir> will be created by this command, and must
+not already exist."""
 
-directory = DirectoryAnywhere(needs_current_series = False)
+args = [argparse.repo, argparse.dir]
 options = []
 
+directory = DirectoryAnywhere(needs_current_series = False, log = False)
 
 def func(parser, options, args):
     """Clone the <repository> into the local <dir> and initialises the
diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
new file mode 100644 (file)
index 0000000..4b5c00a
--- /dev/null
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2007, Karl Hasselström <kha@treskal.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.argparse import opt
+from stgit.out import *
+from stgit import argparse, utils
+from stgit.commands import common
+from stgit.lib import git, transaction
+
+help = 'Coalesce two or more patches into one'
+kind = 'stack'
+usage = ['[options] <patches>']
+description = """
+Coalesce two or more patches, creating one big patch that contains all
+their changes.
+
+If there are conflicts when reordering the patches to match the order
+you specify, you will have to resolve them manually just as if you had
+done a sequence of pushes and pops yourself."""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
+options = [opt('-n', '--name', short = 'Name of coalesced patch')
+           ] + argparse.message_options(save_template = True)
+
+directory = common.DirectoryHasRepositoryLib()
+
+class SaveTemplateDone(Exception):
+    pass
+
+def _coalesce_patches(trans, patches, msg, save_template):
+    cd = trans.patches[patches[0]].data
+    cd = git.CommitData(tree = cd.tree, parents = cd.parents)
+    for pn in patches[1:]:
+        c = trans.patches[pn]
+        tree = trans.stack.repository.simple_merge(
+            base = c.data.parent.data.tree,
+            ours = cd.tree, theirs = c.data.tree)
+        if not tree:
+            return None
+        cd = cd.set_tree(tree)
+    if msg == None:
+        msg = '\n\n'.join('%s\n\n%s' % (pn.ljust(70, '-'),
+                                        trans.patches[pn].data.message)
+                          for pn in patches)
+        if save_template:
+            save_template(msg)
+            raise SaveTemplateDone()
+        else:
+            msg = utils.edit_string(msg, '.stgit-coalesce.txt').strip()
+    cd = cd.set_message(msg)
+
+    return cd
+
+def _coalesce(stack, iw, name, msg, save_template, patches):
+
+    # If a name was supplied on the command line, make sure it's OK.
+    def bad_name(pn):
+        return pn not in patches and stack.patches.exists(pn)
+    def get_name(cd):
+        return name or utils.make_patch_name(cd.message, bad_name)
+    if name and bad_name(name):
+        raise common.CmdException('Patch name "%s" already taken')
+
+    def make_coalesced_patch(trans, new_commit_data):
+        name = get_name(new_commit_data)
+        trans.patches[name] = stack.repository.commit(new_commit_data)
+        trans.unapplied.insert(0, name)
+
+    trans = transaction.StackTransaction(stack, 'coalesce',
+                                         allow_conflicts = True)
+    push_new_patch = bool(set(patches) & set(trans.applied))
+    try:
+        new_commit_data = _coalesce_patches(trans, patches, msg, save_template)
+        if new_commit_data:
+            # We were able to construct the coalesced commit
+            # automatically. So just delete its constituent patches.
+            to_push = trans.delete_patches(lambda pn: pn in patches)
+        else:
+            # Automatic construction failed. So push the patches
+            # consecutively, so that a second construction attempt is
+            # guaranteed to work.
+            to_push = trans.pop_patches(lambda pn: pn in patches)
+            for pn in patches:
+                trans.push_patch(pn, iw)
+            new_commit_data = _coalesce_patches(trans, patches, msg,
+                                                save_template)
+            assert not trans.delete_patches(lambda pn: pn in patches)
+        make_coalesced_patch(trans, new_commit_data)
+
+        # Push the new patch if necessary, and any unrelated patches we've
+        # had to pop out of the way.
+        if push_new_patch:
+            trans.push_patch(get_name(new_commit_data), iw)
+        for pn in to_push:
+            trans.push_patch(pn, iw)
+    except SaveTemplateDone:
+        trans.abort(iw)
+        return
+    except transaction.TransactionHalted:
+        pass
+    return trans.run(iw)
+
+def func(parser, options, args):
+    stack = directory.repository.current_stack
+    patches = common.parse_patches(args, list(stack.patchorder.all))
+    if len(patches) < 2:
+        raise common.CmdException('Need at least two patches')
+    return _coalesce(stack, stack.repository.default_iw, options.name,
+                     options.message, options.save_template, patches)
index e56f5a0..dd8d6e6 100644 (file)
@@ -15,53 +15,94 @@ 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 stgit.commands.common import *
-from stgit.utils import *
+from stgit.argparse import opt
+from stgit.commands import common
+from stgit.lib import transaction
 from stgit.out import *
-from stgit import stack, git
-
-help = 'permanently store the applied patches into stack base'
-usage = """%prog [options]
-
-Merge the applied patches into the base of the current stack and
-remove them from the series while advancing the base.
-
-Use this command only if you want to permanently store the applied
-patches and no longer manage them with StGIT."""
-
-directory = DirectoryGotoToplevel()
-options = []
-
+from stgit import argparse
+
+help = 'Permanently store the applied patches into the stack base'
+kind = 'stack'
+usage = ['',
+         '<patchnames>',
+         '-n NUM',
+         '--all']
+description = """
+Merge one or more patches into the base of the current stack and
+remove them from the series while advancing the base. This is the
+opposite of 'stg uncommit'. Use this command if you no longer want to
+manage a patch with StGIT.
+
+By default, the bottommost patch is committed. If patch names are
+given, the stack is rearranged so that those patches are at the
+bottom, and then they are committed.
+
+The -n/--number option specifies the number of applied patches to
+commit (counting from the bottom of the stack). If -a/--all is given,
+all applied patches are committed."""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
+options = [
+    opt('-n', '--number', type = 'int',
+        short = 'Commit the specified number of patches'),
+    opt('-a', '--all', action = 'store_true',
+        short = 'Commit all applied patches')]
+
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
-    """Merge the applied patches into the base of the current stack
-       and remove them from the series while advancing the base
-    """
-    if len(args) != 0:
-        parser.error('incorrect number of arguments')
-
-    check_local_changes()
-    check_conflicts()
-    check_head_top_equal(crt_series)
-
-    applied = crt_series.get_applied()
-    if not applied:
-        raise CmdException, 'No patches applied'
-
-    if crt_series.get_protected():
-        raise CmdException, 'This branch is protected.  Commit is not permitted'
-
-    crt_head = git.get_head()
-
-    out.start('Committing %d patches' % len(applied))
-
-    crt_series.pop_patch(applied[0])
-    git.switch(crt_head)
-
-    for patch in applied:
-        crt_series.delete_patch(patch)
-
-    out.done()
+    """Commit a number of patches."""
+    stack = directory.repository.current_stack
+    args = common.parse_patches(args, list(stack.patchorder.all_visible))
+    if len([x for x in [args, options.number != None, options.all] if x]) > 1:
+        parser.error('too many options')
+    if args:
+        patches = [pn for pn in stack.patchorder.all_visible if pn in args]
+        bad = set(args) - set(patches)
+        if bad:
+            raise common.CmdException('Nonexistent or hidden patch names: %s'
+                                      % ', '.join(sorted(bad)))
+    elif options.number != None:
+        if options.number <= len(stack.patchorder.applied):
+            patches = stack.patchorder.applied[:options.number]
+        else:
+            raise common.CmdException('There are not that many applied patches')
+    elif options.all:
+        patches = stack.patchorder.applied
+    else:
+        patches = stack.patchorder.applied[:1]
+    if not patches:
+        raise common.CmdException('No patches to commit')
+
+    iw = stack.repository.default_iw
+    def allow_conflicts(trans):
+        # As long as the topmost patch stays where it is, it's OK to
+        # run "stg commit" with conflicts in the index.
+        return len(trans.applied) >= 1
+    trans = transaction.StackTransaction(stack, 'commit',
+                                         allow_conflicts = allow_conflicts)
+    try:
+        common_prefix = 0
+        for i in xrange(min(len(stack.patchorder.applied), len(patches))):
+            if stack.patchorder.applied[i] == patches[i]:
+                common_prefix += 1
+        if common_prefix < len(patches):
+            to_push = trans.pop_patches(
+                lambda pn: pn in stack.patchorder.applied[common_prefix:])
+            for pn in patches[common_prefix:]:
+                trans.push_patch(pn, iw)
+        else:
+            to_push = []
+        new_base = trans.patches[patches[-1]]
+        for pn in patches:
+            trans.patches[pn] = None
+        trans.applied = [pn for pn in trans.applied if pn not in patches]
+        trans.base = new_base
+        out.info('Committed %d patch%s' % (len(patches),
+                                           ['es', ''][len(patches) == 1]))
+        for pn in to_push:
+            trans.push_patch(pn, iw)
+    except transaction.TransactionHalted:
+        pass
+    return trans.run(iw)
index 3840387..6bb3685 100644 (file)
@@ -18,121 +18,87 @@ 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, os.path, re
-from optparse import OptionParser, make_option
-
+import sys, os, os.path, re, email.Utils
 from stgit.exception import *
 from stgit.utils import *
 from stgit.out import *
 from stgit.run import *
 from stgit import stack, git, basedir
 from stgit.config import config, file_extensions
-
+from stgit.lib import stack as libstack
+from stgit.lib import git as libgit
+from stgit.lib import log
 
 # Command exception class
 class CmdException(StgException):
     pass
 
 # Utility functions
-class RevParseException(StgException):
-    """Revision spec parse error."""
-    pass
-
 def parse_rev(rev):
-    """Parse a revision specification into its
-    patchname@branchname//patch_id parts. If no branch name has a slash
-    in it, also accept / instead of //."""
-    if '/' in ''.join(git.get_heads()):
-        # We have branch names with / in them.
-        branch_chars = r'[^@]'
-        patch_id_mark = r'//'
-    else:
-        # No / in branch names.
-        branch_chars = r'[^@/]'
-        patch_id_mark = r'(/|//)'
-    patch_re = r'(?P<patch>[^@/]+)'
-    branch_re = r'@(?P<branch>%s+)' % branch_chars
-    patch_id_re = r'%s(?P<patch_id>[a-z.]*)' % patch_id_mark
-
-    # Try //patch_id.
-    m = re.match(r'^%s$' % patch_id_re, rev)
-    if m:
-        return None, None, m.group('patch_id')
-
-    # Try path[@branch]//patch_id.
-    m = re.match(r'^%s(%s)?%s$' % (patch_re, branch_re, patch_id_re), rev)
-    if m:
-        return m.group('patch'), m.group('branch'), m.group('patch_id')
-
-    # Try patch[@branch].
-    m = re.match(r'^%s(%s)?$' % (patch_re, branch_re), rev)
-    if m:
-        return m.group('patch'), m.group('branch'), None
-
-    # No, we can't parse that.
-    raise RevParseException
+    """Parse a revision specification into its branch:patch parts.
+    """
+    try:
+        branch, patch = rev.split(':', 1)
+    except ValueError:
+        branch = None
+        patch = rev
+
+    return (branch, patch)
 
 def git_id(crt_series, rev):
     """Return the GIT id
     """
-    if not rev:
-        return None
+    # TODO: remove this function once all the occurrences were converted
+    # to git_commit()
+    repository = libstack.Repository.default()
+    return git_commit(rev, repository, crt_series.get_name()).sha1
+
+def git_commit(name, repository, branch_name = None):
+    """Return the a Commit object if 'name' is a patch name or Git commit.
+    The patch names allowed are in the form '<branch>:<patch>' and can
+    be followed by standard symbols used by git rev-parse. If <patch>
+    is '{base}', it represents the bottom of the stack.
+    """
+    # Try a [branch:]patch name first
+    branch, patch = parse_rev(name)
+    if not branch:
+        branch = branch_name or repository.current_branch_name
 
-    # try a GIT revision first
-    try:
-        return git.rev_parse(rev + '^{commit}')
-    except git.GitException:
-        pass
+    # The stack base
+    if patch.startswith('{base}'):
+        base_id = repository.get_stack(branch).base.sha1
+        return repository.rev_parse(base_id +
+                                    strip_prefix('{base}', patch))
 
-    # try an StGIT patch name
+    # Other combination of branch and patch
     try:
-        patch, branch, patch_id = parse_rev(rev)
-        if branch == None:
-            series = crt_series
-        else:
-            series = stack.Series(branch)
-        if patch == None:
-            patch = series.get_current()
-            if not patch:
-                raise CmdException, 'No patches applied'
-        if patch in series.get_applied() or patch in series.get_unapplied() or \
-               patch in series.get_hidden():
-            if patch_id in ['top', '', None]:
-                return series.get_patch(patch).get_top()
-            elif patch_id == 'bottom':
-                return series.get_patch(patch).get_bottom()
-            elif patch_id == 'top.old':
-                return series.get_patch(patch).get_old_top()
-            elif patch_id == 'bottom.old':
-                return series.get_patch(patch).get_old_bottom()
-            elif patch_id == 'log':
-                return series.get_patch(patch).get_log()
-        if patch == 'base' and patch_id == None:
-            return series.get_base()
-    except RevParseException:
-        pass
-    except stack.StackException:
+        return repository.rev_parse('patches/%s/%s' % (branch, patch),
+                                    discard_stderr = True)
+    except libgit.RepositoryException:
         pass
 
-    raise CmdException, 'Unknown patch or revision: %s' % rev
+    # Try a Git commit
+    try:
+        return repository.rev_parse(name, discard_stderr = True)
+    except libgit.RepositoryException:
+        raise CmdException('%s: Unknown patch or revision name' % name)
 
 def check_local_changes():
     if git.local_changes():
-        raise CmdException, \
-              'local changes in the tree. Use "refresh" or "status --reset"'
+        raise CmdException('local changes in the tree. Use "refresh" or'
+                           ' "status --reset"')
 
 def check_head_top_equal(crt_series):
     if not crt_series.head_top_equal():
-        raise CmdException(
-"""HEAD and top are not the same. This can happen if you
-   modify a branch with git. "stg repair --help" explains
-   more about what to do next.""")
+        raise CmdException('HEAD and top are not the same. This can happen'
+                           ' if you modify a branch with git. "stg repair'
+                           ' --help" explains more about what to do next.')
 
 def check_conflicts():
-    if os.path.exists(os.path.join(basedir.get(), 'conflicts')):
-        raise CmdException, \
-              'Unsolved conflicts. Please resolve them first or\n' \
-              '  revert the changes with "status --reset"'
+    if git.get_conflicts():
+        raise CmdException('Unsolved conflicts. Please fix the conflicts'
+                           ' then use "resolve <files>" or revert the'
+                           ' changes with "status --reset".')
 
 def print_crt_patch(crt_series, branch = None):
     if not branch:
@@ -145,29 +111,9 @@ def print_crt_patch(crt_series, branch = None):
     else:
         out.info('No patches applied')
 
-def resolved(filename, reset = None):
-    if reset:
-        reset_file = filename + file_extensions()[reset]
-        if os.path.isfile(reset_file):
-            if os.path.isfile(filename):
-                os.remove(filename)
-            os.rename(reset_file, filename)
-            # update the access and modificatied times
-            os.utime(filename, None)
-
-    git.update_cache([filename], force = True)
-
-    for ext in file_extensions().values():
-        fn = filename + ext
-        if os.path.isfile(fn):
-            os.remove(fn)
-
 def resolved_all(reset = None):
     conflicts = git.get_conflicts()
-    if conflicts:
-        for filename in conflicts:
-            resolved(filename, reset)
-        os.remove(os.path.join(basedir.get(), 'conflicts'))
+    git.resolved(conflicts, reset)
 
 def push_patches(crt_series, patches, check_merged = False):
     """Push multiple patches onto the stack. This function is shared
@@ -228,6 +174,8 @@ def parse_patches(patch_args, patch_list, boundary = 0, ordered = False):
     a list. The names can be individual patches and/or in the
     patch1..patch2 format.
     """
+    # in case it receives a tuple
+    patch_list = list(patch_list)
     patches = []
 
     for name in patch_args:
@@ -286,49 +234,33 @@ def parse_patches(patch_args, patch_list, boundary = 0, ordered = False):
     return patches
 
 def name_email(address):
-    """Return a tuple consisting of the name and email parsed from a
-    standard 'name <email>' or 'email (name)' string
-    """
-    address = re.sub('[\\\\"]', '\\\\\g<0>', address)
-    str_list = re.findall('^(.*)\s*<(.*)>\s*$', address)
-    if not str_list:
-        str_list = re.findall('^(.*)\s*\((.*)\)\s*$', address)
-        if not str_list:
-            raise CmdException, 'Incorrect "name <email>"/"email (name)" string: %s' % address
-        return ( str_list[0][1], str_list[0][0] )
-
-    return str_list[0]
+    p = email.Utils.parseaddr(address)
+    if p[1]:
+        return p
+    else:
+        raise CmdException('Incorrect "name <email>"/"email (name)" string: %s'
+                           % address)
 
 def name_email_date(address):
-    """Return a tuple consisting of the name, email and date parsed
-    from a 'name <email> date' string
-    """
-    address = re.sub('[\\\\"]', '\\\\\g<0>', address)
-    str_list = re.findall('^(.*)\s*<(.*)>\s*(.*)\s*$', address)
-    if not str_list:
-        raise CmdException, 'Incorrect "name <email> date" string: %s' % address
-
-    return str_list[0]
+    p = parse_name_email_date(address)
+    if p:
+        return p
+    else:
+        raise CmdException('Incorrect "name <email> date" string: %s' % address)
 
-def address_or_alias(addr_str):
-    """Return the address if it contains an e-mail address or look up
+def address_or_alias(addr_pair):
+    """Return a name-email tuple the e-mail address is valid or look up
     the aliases in the config files.
     """
-    def __address_or_alias(addr):
-        if not addr:
-            return None
-        if addr.find('@') >= 0:
-            # it's an e-mail address
-            return addr
-        alias = config.get('mail.alias.'+addr)
-        if alias:
-            # it's an alias
-            return alias
-        raise CmdException, 'unknown e-mail alias: %s' % addr
-
-    addr_list = [__address_or_alias(addr.strip())
-                 for addr in addr_str.split(',')]
-    return ', '.join([addr for addr in addr_list if addr])
+    addr = addr_pair[1]
+    if '@' in addr:
+        # it's an e-mail address
+        return addr_pair
+    alias = config.get('mail.alias.' + addr)
+    if alias:
+        # it's an alias
+        return name_email(alias)
+    raise CmdException, 'unknown e-mail alias: %s' % addr
 
 def prepare_rebase(crt_series):
     # pop all patches
@@ -482,12 +414,15 @@ def parse_mail(msg):
 
     return (descr, authname, authemail, authdate, diff)
 
-def parse_patch(fobj):
-    """Parse the input file and return (description, authname,
+def parse_patch(text, contains_diff):
+    """Parse the input text and return (description, authname,
     authemail, authdate, diff)
     """
-    descr, diff = __split_descr_diff(fobj.read())
-    descr, authname, authemail, authdate = __parse_description(descr)
+    if contains_diff:
+        (text, diff) = __split_descr_diff(text)
+    else:
+        diff = None
+    (descr, authname, authemail, authdate) = __parse_description(text)
 
     # we don't yet have an agreed place for the creation date.
     # Just return None
@@ -508,8 +443,9 @@ class DirectoryException(StgException):
     pass
 
 class _Directory(object):
-    def __init__(self, needs_current_series = True):
+    def __init__(self, needs_current_series = True, log = True):
         self.needs_current_series =  needs_current_series
+        self.log = log
     @readonly_constant_property
     def git_dir(self):
         try:
@@ -542,6 +478,9 @@ class _Directory(object):
                        ).output_one_line()]
     def cd_to_topdir(self):
         os.chdir(self.__topdir_path)
+    def write_log(self, msg):
+        if self.log:
+            log.compat_log_entry(msg)
 
 class DirectoryAnywhere(_Directory):
     def setup(self):
@@ -550,6 +489,7 @@ class DirectoryAnywhere(_Directory):
 class DirectoryHasRepository(_Directory):
     def setup(self):
         self.git_dir # might throw an exception
+        log.compat_log_external_mods()
 
 class DirectoryInWorktree(DirectoryHasRepository):
     def setup(self):
@@ -561,3 +501,12 @@ class DirectoryGotoToplevel(DirectoryInWorktree):
     def setup(self):
         DirectoryInWorktree.setup(self)
         self.cd_to_topdir()
+
+class DirectoryHasRepositoryLib(_Directory):
+    """For commands that use the new infrastructure in stgit.lib.*."""
+    def __init__(self):
+        self.needs_current_series = False
+        self.log = False # stgit.lib.transaction handles logging
+    def setup(self):
+        # This will throw an exception if we don't have a repository.
+        self.repository = libstack.Repository.default()
diff --git a/stgit/commands/copy.py b/stgit/commands/copy.py
deleted file mode 100644 (file)
index e94dd66..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-
-__copyright__ = """
-Copyright (C) 2007, Yann Dirson <ydirson@altern.org>
-
-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 stgit.commands.common import *
-from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'copy files inside the repository'
-usage = """%prog [options] [<file/dir> <newname> | <files/dirs...> <dir>]
-
-Copy of the files and dirs passed as arguments under another name or
-location inside the same repository."""
-
-directory = DirectoryHasRepository()
-options = []
-
-def func(parser, options, args):
-    """Copy files inside the repository
-    """
-    if len(args) < 1:
-        parser.error('incorrect number of arguments')
-
-    if not crt_series.get_current():
-        raise CmdException, 'No patches applied'
-
-    git.copy(args[0:-1], args[-1])
index 1696cb9..fdcb5c9 100644 (file)
@@ -16,67 +16,63 @@ 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 stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git
-
-
-help = 'delete patches'
-usage = """%prog [options] <patch1> [<patch2>] [<patch3>..<patch4>]
-
-Delete the patches passed as arguments. If an applied patch is to be
-deleted, all other patches applied on top of it must be deleted too,
-and they must be explicitly specified, since this command will not try
-to delete a patch unless you explicitly ask it to. If any applied
-patches are deleted, they are popped from the stack.
-
-Note that the 'delete' operation is irreversible."""
-
-directory = DirectoryGotoToplevel()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one')]
+from stgit.argparse import opt
+from stgit.commands import common
+from stgit.lib import transaction
+from stgit import argparse
+
+help = 'Delete patches'
+kind = 'patch'
+usage = ['[options] <patch1> [<patch2>] [<patch3>..<patch4>]']
+description = """
+Delete the patches passed as arguments."""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
+options = [
+    opt('--spill', action = 'store_true',
+        short = 'Spill patch contents to worktree and index', long = """
+        Delete the patches, but do not touch the index and worktree.
+        This only works with applied patches at the top of the stack.
+        The effect is to "spill" the patch contents into the index and
+        worktree. This can be useful e.g. if you want to split a patch
+        into several smaller pieces."""),
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch')]
+
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
-    """Deletes one or more patches.
-    """
-    applied_patches = crt_series.get_applied()
-    unapplied_patches = crt_series.get_unapplied()
-    all_patches = applied_patches + unapplied_patches
-
+    """Delete one or more patches."""
+    stack = directory.repository.get_stack(options.branch)
+    if options.branch:
+        iw = None # can't use index/workdir to manipulate another branch
+    else:
+        iw = stack.repository.default_iw
     if args:
-        patches = parse_patches(args, all_patches, len(applied_patches))
+        patches = set(common.parse_patches(args, list(stack.patchorder.all),
+                                           len(stack.patchorder.applied)))
     else:
         parser.error('No patches specified')
 
-    applied = []
+    if options.spill:
+        if set(stack.patchorder.applied[-len(patches):]) != patches:
+            parser.error('Can only spill topmost applied patches')
+        iw = None # don't touch index+worktree
 
-    # find the applied patches to be deleted. We can only delete
-    # consecutive patches in the applied range
-    for patch in applied_patches[::-1]:
-        if patch in patches:
-            applied.append(patch)
-            patches.remove(patch)
+    def allow_conflicts(trans):
+        # Allow conflicts if the topmost patch stays the same.
+        if stack.patchorder.applied:
+            return (trans.applied
+                    and trans.applied[-1] == stack.patchorder.applied[-1])
         else:
-            break
-
-    # any applied patches to be deleted but not in consecutive order?
-    for patch in patches:
-        if patch in applied_patches:
-            raise CmdException, 'Cannot delete the applied patch "%s"' % patch
-
-    if applied and not options.branch:
-        check_local_changes()
-        check_conflicts()
-        check_head_top_equal(crt_series)
-
-    # delete the patches
-    for patch in applied + patches:
-        crt_series.delete_patch(patch)
-        out.info('Patch "%s" successfully deleted' % patch)
-
-    if not options.branch:
-        print_crt_patch(crt_series)
+            return not trans.applied
+    trans = transaction.StackTransaction(stack, 'delete',
+                                         allow_conflicts = allow_conflicts)
+    try:
+        to_push = trans.delete_patches(lambda pn: pn in patches)
+        for pn in to_push:
+            trans.push_patch(pn, iw)
+    except transaction.TransactionHalted:
+        pass
+    return trans.run(iw)
index 791b1ce..7d2f719 100644 (file)
@@ -17,41 +17,39 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
 from pydoc import pager
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'show the tree diff'
-usage = """%prog [options] [<files or dirs>]
+from stgit import argparse, stack, git
+from stgit.lib import git as gitlib
 
+help = 'Show the tree diff'
+kind = 'wc'
+usage = ['[options] [<files or dirs>]']
+description = """
 Show the diff (default) or diffstat between the current working copy
-or a tree-ish object and another tree-ish object. File names can also
-be given to restrict the diff output. The tree-ish object can be a
-standard git commit, tag or tree. In addition to these, the command
-also supports 'base', representing the bottom of the current stack,
-and '[patch][//[bottom | top]]' for the patch boundaries (defaulting to
-the current one):
-
-rev = '([patch][//[bottom | top]]) | <tree-ish> | base'
-
-If neither bottom nor top are given but a '//' is present, the command
-shows the specified patch (defaulting to the current one)."""
-
-directory = DirectoryHasRepository()
-options = [make_option('-r', '--range',
-                       metavar = 'rev1[..[rev2]]', dest = 'revs',
-                       help = 'show the diff between revisions'),
-           make_option('-O', '--diff-opts',
-                       help = 'options to pass to git diff'),
-           make_option('-s', '--stat',
-                       help = 'show the stat instead of the diff',
-                       action = 'store_true')]
-
+or a tree-ish object and another tree-ish object (defaulting to HEAD).
+File names can also be given to restrict the diff output. The
+tree-ish object can be an StGIT patch, a standard git commit, tag or
+tree. In addition to these, the command also supports '{base}',
+representing the bottom of the current stack.
+
+rev = '[branch:](<patch>|{base}) | <tree-ish>'"""
+
+args = [argparse.known_files, argparse.dirty_files]
+options = [
+    opt('-r', '--range', metavar = 'rev1[..[rev2]]', dest = 'revs',
+        args = [argparse.patch_range(argparse.applied_patches,
+                                     argparse.unapplied_patches,
+                                     argparse.hidden_patches)],
+        short = 'Show the diff between revisions'),
+    opt('-s', '--stat', action = 'store_true',
+        short = 'Show the stat instead of the diff'),
+    ] + argparse.diff_opts_option()
+
+directory = DirectoryHasRepository(log = False)
 
 def func(parser, options, args):
     """Show the tree diff
@@ -63,17 +61,8 @@ def func(parser, options, args):
         rev_list = options.revs.split('..')
         rev_list_len = len(rev_list)
         if rev_list_len == 1:
-            rev = rev_list[0]
-            if rev.endswith('/'):
-                # the whole patch
-                rev = strip_suffix('/', rev)
-                if rev.endswith('/'):
-                    rev = strip_suffix('/', rev)
-                rev1 = rev + '//bottom'
-                rev2 = rev + '//top'
-            else:
-                rev1 = rev_list[0]
-                rev2 = None
+            rev1 = rev_list[0]
+            rev2 = None
         elif rev_list_len == 2:
             rev1 = rev_list[0]
             rev2 = rev_list[1]
@@ -83,16 +72,11 @@ def func(parser, options, args):
         rev1 = 'HEAD'
         rev2 = None
 
-    if options.diff_opts:
-        diff_flags = options.diff_opts.split()
-    else:
-        diff_flags = []
-
+    diff_str = git.diff(args, git_id(crt_series, rev1),
+                        rev2 and git_id(crt_series, rev2),
+                        diff_flags = options.diff_flags)
     if options.stat:
-        out.stdout_raw(git.diffstat(args, git_id(crt_series, rev1),
-                                    git_id(crt_series, rev2)) + '\n')
+        out.stdout_raw(gitlib.diffstat(diff_str) + '\n')
     else:
-        diff_str = git.diff(args, git_id(crt_series, rev1),
-                            git_id(crt_series, rev2), diff_flags = diff_flags )
         if diff_str:
             pager(diff_str)
index 36d2e01..ed785aa 100644 (file)
@@ -18,18 +18,16 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-from optparse import OptionParser, make_option
-from email.Utils import formatdate
-
-from stgit.commands.common import *
-from stgit.utils import *
+from stgit.argparse import opt
+from stgit import argparse, git, utils
+from stgit.commands import common
+from stgit.lib import git as gitlib, transaction, edit
 from stgit.out import *
-from stgit import stack, git
-
 
 help = 'edit a patch description or diff'
-usage = """%prog [options] [<patch>]
-
+kind = 'patch'
+usage = ['[options] [<patch>]']
+description = """
 Edit the description and author information of the given patch (or the
 current patch if no patch name was given). With --diff, also edit the
 diff.
@@ -49,196 +47,96 @@ separator:
   Diff text
 
 Command-line options can be used to modify specific information
-without invoking the editor.
-
-If the patch diff is edited but the patch application fails, the
-rejected patch is stored in the .stgit-failed.patch file (and also in
-.stgit-edit.{diff,txt}). The edited patch can be replaced with one of
-these files using the '--file' and '--diff' options.
-"""
-
-directory = DirectoryGotoToplevel()
-options = [make_option('-d', '--diff',
-                       help = 'edit the patch diff',
-                       action = 'store_true'),
-           make_option('-f', '--file',
-                       help = 'use FILE instead of invoking the editor'),
-           make_option('-O', '--diff-opts',
-                       help = 'options to pass to git diff'),
-           make_option('--undo',
-                       help = 'revert the commit generated by the last edit',
-                       action = 'store_true'),
-           make_option('-a', '--annotate', metavar = 'NOTE',
-                       help = 'annotate the patch log entry'),
-           make_option('-m', '--message',
-                       help = 'replace the patch description with MESSAGE'),
-           make_option('--author', metavar = '"NAME <EMAIL>"',
-                       help = 'replae the author details with "NAME <EMAIL>"'),
-           make_option('--authname',
-                       help = 'replace the author name with AUTHNAME'),
-           make_option('--authemail',
-                       help = 'replace the author e-mail with AUTHEMAIL'),
-           make_option('--authdate',
-                       help = 'replace the author date with AUTHDATE'),
-           make_option('--commname',
-                       help = 'replace the committer name with COMMNAME'),
-           make_option('--commemail',
-                       help = 'replace the committer e-mail with COMMEMAIL')
-           ] + make_sign_options()
-
-def __update_patch(pname, fname, options):
-    """Update the current patch from the given file.
-    """
-    patch = crt_series.get_patch(pname)
-
-    bottom = patch.get_bottom()
-    top = patch.get_top()
-
-    f = open(fname)
-    message, author_name, author_email, author_date, diff = parse_patch(f)
-    f.close()
-
-    out.start('Updating patch "%s"' % pname)
-
-    if options.diff:
-        git.switch(bottom)
-        try:
-            git.apply_patch(fname)
-        except:
-            # avoid inconsistent repository state
-            git.switch(top)
-            raise
-
-    crt_series.refresh_patch(message = message,
-                             author_name = author_name,
-                             author_email = author_email,
-                             author_date = author_date,
-                             backup = True, log = 'edit')
-
-    if crt_series.empty_patch(pname):
-        out.done('empty patch')
-    else:
-        out.done()
-
-def __edit_update_patch(pname, options):
-    """Edit the given patch interactively.
-    """
-    patch = crt_series.get_patch(pname)
-
-    if options.diff_opts:
-        if not options.diff:
-            raise CmdException, '--diff-opts only available with --diff'
-        diff_flags = options.diff_opts.split()
-    else:
-        diff_flags = []
-
-    # generate the file to be edited
-    descr = patch.get_description().strip()
-    authdate = patch.get_authdate()
-
-    tmpl = 'From: %(authname)s <%(authemail)s>\n'
-    if authdate:
-        tmpl += 'Date: %(authdate)s\n'
-    tmpl += '\n%(descr)s\n'
-
-    tmpl_dict = {
-        'descr': descr,
-        'authname': patch.get_authname(),
-        'authemail': patch.get_authemail(),
-        'authdate': patch.get_authdate()
-        }
-
-    if options.diff:
-        # add the patch diff to the edited file
-        bottom = patch.get_bottom()
-        top = patch.get_top()
-
-        tmpl += '---\n\n' \
-                '%(diffstat)s\n' \
-                '%(diff)s'
-
-        tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top)
-        tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top,
-                                     diff_flags = diff_flags)
-
-    for key in tmpl_dict:
-        # make empty strings if key is not available
-        if tmpl_dict[key] is None:
-            tmpl_dict[key] = ''
-
-    text = tmpl % tmpl_dict
-
-    if options.diff:
-        fname = '.stgit-edit.diff'
-    else:
-        fname = '.stgit-edit.txt'
-
-    # write the file to be edited
-    f = open(fname, 'w+')
-    f.write(text)
-    f.close()
-
-    # invoke the editor
-    call_editor(fname)
-
-    __update_patch(pname, fname, options)
+without invoking the editor. (With the --edit option, the editor is
+invoked even if such command-line options are given.)
+
+If the patch diff is edited but does not apply, no changes are made to
+the patch at all. The edited patch is saved to a file which you can
+feed to "stg edit --file", once you have made sure it does apply."""
+
+args = [argparse.applied_patches, argparse.unapplied_patches,
+        argparse.hidden_patches]
+options = [
+    opt('-d', '--diff', action = 'store_true',
+        short = 'Edit the patch diff'),
+    opt('-e', '--edit', action = 'store_true',
+        short = 'Invoke interactive editor'),
+    ] + (argparse.sign_options() +
+         argparse.message_options(save_template = True) +
+         argparse.author_options() + argparse.diff_opts_option())
+
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
     """Edit the given patch or the current one.
     """
-    crt_pname = crt_series.get_current()
+    stack = directory.repository.current_stack
 
-    if not args:
-        pname = crt_pname
-        if not pname:
-            raise CmdException, 'No patches applied'
+    if len(args) == 0:
+        if not stack.patchorder.applied:
+            raise common.CmdException(
+                'Cannot edit top patch, because no patches are applied')
+        patchname = stack.patchorder.applied[-1]
     elif len(args) == 1:
-        pname = args[0]
-        if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
-            raise CmdException, 'Cannot edit unapplied or hidden patches'
-        elif not crt_series.patch_applied(pname):
-            raise CmdException, 'Unknown patch "%s"' % pname
+        [patchname] = args
+        if not stack.patches.exists(patchname):
+            raise common.CmdException('%s: no such patch' % patchname)
     else:
-        parser.error('incorrect number of arguments')
-
-    check_local_changes()
-    check_conflicts()
-    check_head_top_equal(crt_series)
-
-    if pname != crt_pname:
-        # Go to the patch to be edited
-        applied = crt_series.get_applied()
-        between = applied[:applied.index(pname):-1]
-        pop_patches(crt_series, between)
-
-    if options.author:
-        options.authname, options.authemail = name_email(options.author)
-
-    if options.undo:
-        out.start('Undoing the editing of "%s"' % pname)
-        crt_series.undo_refresh()
-        out.done()
-    elif options.message or options.authname or options.authemail \
-             or options.authdate or options.commname or options.commemail \
-             or options.sign_str:
-        # just refresh the patch with the given information
-        out.start('Updating patch "%s"' % pname)
-        crt_series.refresh_patch(message = options.message,
-                                 author_name = options.authname,
-                                 author_email = options.authemail,
-                                 author_date = options.authdate,
-                                 committer_name = options.commname,
-                                 committer_email = options.commemail,
-                                 backup = True, sign_str = options.sign_str,
-                                 log = 'edit',
-                                 notes = options.annotate)
-        out.done()
-    elif options.file:
-        __update_patch(pname, options.file, options)
+        parser.error('Cannot edit more than one patch')
+
+    cd = orig_cd = stack.patches.get(patchname).commit.data
+
+    cd, failed_diff = edit.auto_edit_patch(
+        stack.repository, cd, msg = options.message, contains_diff = True,
+        author = options.author, committer = lambda p: p,
+        sign_str = options.sign_str)
+
+    if options.save_template:
+        options.save_template(
+            edit.patch_desc(stack.repository, cd,
+                            options.diff, options.diff_flags, failed_diff))
+        return utils.STGIT_SUCCESS
+
+    if cd == orig_cd or options.edit:
+        cd, failed_diff = edit.interactive_edit_patch(
+            stack.repository, cd, options.diff, options.diff_flags, failed_diff)
+
+    def failed():
+        fn = '.stgit-failed.patch'
+        f = file(fn, 'w')
+        f.write(edit.patch_desc(stack.repository, cd,
+                                options.diff, options.diff_flags, failed_diff))
+        f.close()
+        out.error('Edited patch did not apply.',
+                  'It has been saved to "%s".' % fn)
+        return utils.STGIT_COMMAND_ERROR
+
+    # If we couldn't apply the patch, fail without even trying to
+    # effect any of the changes.
+    if failed_diff:
+        return failed()
+
+    # The patch applied, so now we have to rewrite the StGit patch
+    # (and any patches on top of it).
+    iw = stack.repository.default_iw
+    trans = transaction.StackTransaction(stack, 'edit', allow_conflicts = True)
+    if patchname in trans.applied:
+        popped = trans.applied[trans.applied.index(patchname)+1:]
+        assert not trans.pop_patches(lambda pn: pn in popped)
     else:
-        __edit_update_patch(pname, options)
-
-    if pname != crt_pname:
-        # Push the patches back
-        between.reverse()
-        push_patches(crt_series, between)
+        popped = []
+    trans.patches[patchname] = stack.repository.commit(cd)
+    try:
+        for pn in popped:
+            trans.push_patch(pn, iw)
+    except transaction.TransactionHalted:
+        pass
+    try:
+        # Either a complete success, or a conflict during push. But in
+        # either case, we've successfully effected the edits the user
+        # asked us for.
+        return trans.run(iw)
+    except transaction.TransactionException:
+        # Transaction aborted -- we couldn't check out files due to
+        # dirty index/worktree. The edits were not carried out.
+        return failed()
index c4fb7e3..1236b01 100644 (file)
@@ -18,18 +18,18 @@ 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 stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git, templates
-
-
-help = 'exports patches to a directory'
-usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
-
+import os
+import sys
+from stgit.argparse import opt
+from stgit.commands import common
+from stgit import argparse, git, templates
+from stgit.out import out
+from stgit.lib import git as gitlib
+
+help = 'Export patches to a directory'
+kind = 'patch'
+usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
+description = """
 Export a range of applied patches to a given directory (defaults to
 'patches-<branch>') in a standard unified GNU diff format. A template
 file (defaulting to '.git/patchexport.tmpl' or
@@ -46,38 +46,39 @@ file:
   %(authemail)s   - author's e-mail
   %(authdate)s    - patch creation date
   %(commname)s    - committer's name
-  %(commemail)s   - committer's e-mail
-"""
-
-directory = DirectoryHasRepository()
-options = [make_option('-d', '--dir',
-                       help = 'export patches to DIR instead of the default'),
-           make_option('-p', '--patch',
-                       help = 'append .patch to the patch names',
-                       action = 'store_true'),
-           make_option('-e', '--extension',
-                       help = 'append .EXTENSION to the patch names'),
-           make_option('-n', '--numbered',
-                       help = 'prefix the patch names with order numbers',
-                       action = 'store_true'),
-           make_option('-t', '--template', metavar = 'FILE',
-                       help = 'Use FILE as a template'),
-           make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
-           make_option('-O', '--diff-opts',
-                       help = 'options to pass to git diff'),
-           make_option('-s', '--stdout',
-                       help = 'dump the patches to the standard output',
-                       action = 'store_true')]
-
+  %(commemail)s   - committer's e-mail"""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
+options = [
+    opt('-d', '--dir', args = [argparse.dir],
+        short = 'Export patches to DIR instead of the default'),
+    opt('-p', '--patch', action = 'store_true',
+        short = 'Append .patch to the patch names'),
+    opt('-e', '--extension',
+        short = 'Append .EXTENSION to the patch names'),
+    opt('-n', '--numbered', action = 'store_true',
+        short = 'Prefix the patch names with order numbers'),
+    opt('-t', '--template', metavar = 'FILE', args = [argparse.files],
+        short = 'Use FILE as a template'),
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch'),
+    opt('-s', '--stdout', action = 'store_true',
+        short = 'Dump the patches to the standard output'),
+    ] + argparse.diff_opts_option()
+
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
     """Export a range of patches.
     """
+    stack = directory.repository.get_stack(options.branch)
+
     if options.dir:
         dirname = options.dir
     else:
-        dirname = 'patches-%s' % crt_series.get_name()
+        dirname = 'patches-%s' % stack.name
         directory.cd_to_topdir()
 
     if not options.branch and git.local_changes():
@@ -89,21 +90,16 @@ def func(parser, options, args):
             os.makedirs(dirname)
         series = file(os.path.join(dirname, 'series'), 'w+')
 
-    if options.diff_opts:
-        diff_flags = options.diff_opts.split()
-    else:
-        diff_flags = []
-
-    applied = crt_series.get_applied()
-    unapplied = crt_series.get_unapplied()
+    applied = stack.patchorder.applied
+    unapplied = stack.patchorder.unapplied
     if len(args) != 0:
-        patches = parse_patches(args, applied + unapplied, len(applied))
+        patches = common.parse_patches(args, applied + unapplied, len(applied))
     else:
         patches = applied
 
     num = len(patches)
     if num == 0:
-        raise CmdException, 'No patches applied'
+        raise common.CmdException, 'No patches applied'
 
     zpadding = len(str(num))
     if zpadding < 2:
@@ -119,7 +115,7 @@ def func(parser, options, args):
 
     # note the base commit for this series
     if not options.stdout:
-        base_commit = crt_series.get_patch(patches[0]).get_bottom()
+        base_commit = stack.patches.get(patches[0]).commit.sha1
         print >> series, '# This series applies on GIT commit %s' % base_commit
 
     patch_no = 1;
@@ -136,25 +132,27 @@ def func(parser, options, args):
             print >> series, pname
 
         # get the patch description
-        patch = crt_series.get_patch(p)
+        patch = stack.patches.get(p)
+        cd = patch.commit.data
 
-        descr = patch.get_description().strip()
+        descr = cd.message.strip()
         descr_lines = descr.split('\n')
 
         short_descr = descr_lines[0].rstrip()
         long_descr = reduce(lambda x, y: x + '\n' + y,
                             descr_lines[1:], '').strip()
 
-        tmpl_dict = {'description': patch.get_description().rstrip(),
+        diff = stack.repository.diff_tree(cd.parent.data.tree, cd.tree, options.diff_flags)
+
+        tmpl_dict = {'description': descr,
                      'shortdescr': short_descr,
                      'longdescr': long_descr,
-                     'diffstat': git.diffstat(rev1 = patch.get_bottom(),
-                                              rev2 = patch.get_top()),
-                     'authname': patch.get_authname(),
-                     'authemail': patch.get_authemail(),
-                     'authdate': patch.get_authdate(),
-                     'commname': patch.get_commname(),
-                     'commemail': patch.get_commemail()}
+                     'diffstat': gitlib.diffstat(diff).rstrip(),
+                     'authname': cd.author.name,
+                     'authemail': cd.author.email,
+                     'authdate': cd.author.date.isoformat(),
+                     'commname': cd.committer.name,
+                     'commemail': cd.committer.email}
         for key in tmpl_dict:
             if not tmpl_dict[key]:
                 tmpl_dict[key] = ''
@@ -162,10 +160,10 @@ def func(parser, options, args):
         try:
             descr = tmpl % tmpl_dict
         except KeyError, err:
-            raise CmdException, 'Unknown patch template variable: %s' \
+            raise common.CmdException, 'Unknown patch template variable: %s' \
                   % err
         except TypeError:
-            raise CmdException, 'Only "%(name)s" variables are ' \
+            raise common.CmdException, 'Only "%(name)s" variables are ' \
                   'supported in the patch template'
 
         if options.stdout:
@@ -175,13 +173,11 @@ def func(parser, options, args):
 
         if options.stdout and num > 1:
             print '-'*79
-            print patch.get_name()
+            print patch.name
             print '-'*79
 
         f.write(descr)
-        f.write(git.diff(rev1 = patch.get_bottom(),
-                         rev2 = patch.get_top(),
-                         diff_flags = diff_flags))
+        f.write(diff)
         if not options.stdout:
             f.close()
         patch_no += 1
index af772b8..a2ed6a2 100644 (file)
@@ -17,58 +17,55 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'show the files modified by a patch (or the current patch)'
-usage = """%prog [options] [<patch>]
+from stgit import argparse, stack, git
+from stgit.lib import git as gitlib
 
+help = 'Show the files modified by a patch (or the current patch)'
+kind = 'patch'
+usage = ['[options] [[<branch>:]<patch>]']
+description = """
 List the files modified by the given patch (defaulting to the current
 one). Passing the '--stat' option shows the diff statistics for the
 given patch. Note that this command doesn't show the files modified in
 the working tree and not yet included in the patch by a 'refresh'
 command. Use the 'diff' or 'status' commands for these files."""
 
-directory = DirectoryHasRepository()
-options = [make_option('-s', '--stat',
-                       help = 'show the diff stat',
-                       action = 'store_true'),
-           make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
-           make_option('-O', '--diff-opts',
-                       help = 'options to pass to git diff'),
-           make_option('--bare',
-                       help = 'bare file names (useful for scripting)',
-                       action = 'store_true')]
+args = [argparse.applied_patches, argparse.unapplied_patches,
+        argparse.hidden_patches]
+options = [
+    opt('-s', '--stat', action = 'store_true',
+        short = 'Show the diffstat'),
+    opt('--bare', action = 'store_true',
+        short = 'Bare file names (useful for scripting)'),
+    ] + argparse.diff_opts_option()
 
+directory = DirectoryHasRepository(log = False)
 
 def func(parser, options, args):
     """Show the files modified by a patch (or the current patch)
     """
     if len(args) == 0:
-        patch = ''
+        patch = 'HEAD'
     elif len(args) == 1:
         patch = args[0]
     else:
         parser.error('incorrect number of arguments')
 
-    if options.diff_opts:
-        diff_flags = options.diff_opts.split()
-    else:
-        diff_flags = []
-
-    rev1 = git_id(crt_series, '%s//bottom' % patch)
-    rev2 = git_id(crt_series, '%s//top' % patch)
+    rev1 = git_id(crt_series, '%s^' % patch)
+    rev2 = git_id(crt_series, '%s' % patch)
 
     if options.stat:
-        out.stdout_raw(git.diffstat(rev1 = rev1, rev2 = rev2,
-                                    diff_flags = diff_flags) + '\n')
+        output = gitlib.diffstat(git.diff(rev1 = rev1, rev2 = rev2,
+                                          diff_flags = options.diff_flags))
     elif options.bare:
-        out.stdout_raw(git.barefiles(rev1, rev2) + '\n')
+        output = git.barefiles(rev1, rev2)
     else:
-        out.stdout_raw(git.files(rev1, rev2, diff_flags = diff_flags) + '\n')
+        output = git.files(rev1, rev2, diff_flags = options.diff_flags)
+    if output:
+        if not output.endswith('\n'):
+            output += '\n'
+        out.stdout_raw(output)
index 5af7e85..7c3dcdf 100644 (file)
@@ -17,24 +17,28 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
-
-help = 'push patches to the top, even if applied'
-usage = """%prog [<patches> | -s [<series>] ]
+from stgit import argparse, stack, git
 
+help = 'Push patches to the top, even if applied'
+kind = 'stack'
+usage = ['<patches>',
+         '-s <series>']
+description = """
 Push a patch or a range of patches to the top even if applied. The
 necessary pop and push operations will be performed to accomplish
 this. The '--series' option can be used to rearrange the (top) patches
 as specified by the given series file (or the standard input)."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-s', '--series',
-                       help = 'rearrange according to a series file',
-                       action = 'store_true')]
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
+options = [
+    opt('-s', '--series', action = 'store_true',
+        short = 'Rearrange according to a series file')]
+
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Pops and pushed to make the named patch the topmost patch
index 3930a1f..66a2dd9 100644 (file)
@@ -16,17 +16,16 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'integrate a GNU diff patch into the current patch'
-usage = """%prog [options] [<file>]
+from stgit import argparse, stack, git
 
+help = 'Integrate a GNU diff patch into the current patch'
+kind = 'patch'
+usage = ['[options] [<file>]']
+description = """
 Apply the given GNU diff file (or the standard input) onto the top of
 the current patch. With the '--threeway' option, the patch is applied
 onto the bottom of the current patch and a three-way merge is
@@ -34,13 +33,14 @@ performed with the current top. With the --base option, the patch is
 applied onto the specified base and a three-way merged is performed
 with the current top."""
 
-directory = DirectoryHasRepository()
-options = [make_option('-t', '--threeway',
-                       help = 'perform a three-way merge with the current patch',
-                       action = 'store_true'),
-           make_option('-b', '--base',
-                       help = 'use BASE instead of HEAD applying the patch')]
+args = [argparse.files]
+options = [
+    opt('-t', '--threeway', action = 'store_true',
+        short = 'Perform a three-way merge with the current patch'),
+    opt('-b', '--base', args = [argparse.commit],
+        short = 'Use BASE instead of HEAD applying the patch')]
 
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Integrate a GNU diff patch into the current patch
index 84b840b..60a917e 100644 (file)
@@ -15,53 +15,41 @@ 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 stgit.commands.common import *
-from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'push or pop patches to the given one'
-usage = """%prog [options] <name>
-
+from stgit.commands import common
+from stgit.lib import transaction
+from stgit import argparse
+
+help = 'Push or pop patches to the given one'
+kind = 'stack'
+usage = ['<patch-name>']
+description = """
 Push/pop patches to/from the stack until the one given on the command
-line becomes current. There is no '--undo' option for 'goto'. Use the
-'push --undo' command for this."""
+line becomes current."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-k', '--keep',
-                       help = 'keep the local changes when popping patches',
-                       action = 'store_true')]
+args = [argparse.other_applied_patches, argparse.unapplied_patches]
+options = []
 
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
-    """Pushes the given patch or all onto the series
-    """
     if len(args) != 1:
         parser.error('incorrect number of arguments')
-
-    check_conflicts()
-    check_head_top_equal(crt_series)
-
-    if not options.keep:
-        check_local_changes()
-
-    applied = crt_series.get_applied()
-    unapplied = crt_series.get_unapplied()
     patch = args[0]
 
-    if patch in applied:
-        applied.reverse()
-        patches = applied[:applied.index(patch)]
-        pop_patches(crt_series, patches, options.keep)
-    elif patch in unapplied:
-        if options.keep:
-            raise CmdException, 'Cannot use --keep with patch pushing'
-        patches = unapplied[:unapplied.index(patch)+1]
-        push_patches(crt_series, patches)
+    stack = directory.repository.current_stack
+    iw = stack.repository.default_iw
+    trans = transaction.StackTransaction(stack, 'goto')
+    if patch in trans.applied:
+        to_pop = set(trans.applied[trans.applied.index(patch)+1:])
+        assert not trans.pop_patches(lambda pn: pn in to_pop)
+    elif patch in trans.unapplied:
+        try:
+            for pn in trans.unapplied[:trans.unapplied.index(patch)+1]:
+                trans.push_patch(pn, iw)
+        except transaction.TransactionHalted:
+            pass
+    elif patch in trans.hidden:
+        raise common.CmdException('Cannot goto a hidden patch')
     else:
-        raise CmdException, 'Patch "%s" does not exist' % patch
-
-    print_crt_patch(crt_series)
+        raise common.CmdException('Patch "%s" does not exist' % patch)
+    return trans.run(iw)
index 1a38907..014febb 100644 (file)
@@ -16,23 +16,26 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'hide a patch in the series'
-usage = """%prog [options] <patch-range>
+from stgit import argparse, stack, git
 
+help = 'Hide a patch in the series'
+kind = 'stack'
+usage = ['[options] <patch-range>']
+description = """
 Hide a range of unapplied patches so that they are no longer shown in
 the plain 'series' command output."""
 
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one')]
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
+options = [
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch')]
+
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Hide a range of patch in the series
index 94b0229..566edcc 100644 (file)
@@ -15,28 +15,27 @@ 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 stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git
-
-
-help = 'print the GIT hash value of a StGIT reference'
-usage = """%prog [options] [id]
-
-Print the hash value of a GIT id (defaulting to HEAD). In addition to
-the standard GIT id's like heads and tags, this command also accepts
-'base[@<branch>]' and '[<patch>[@<branch>]][//[bottom | top]]'. If no
-'top' or 'bottom' are passed and <patch> is a valid patch name, 'top'
-will be used by default."""
-
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one')]
-
+from stgit.out import out
+from stgit.commands import common
+from stgit.lib import stack
+from stgit import argparse
+
+help = 'Print the git hash value of a StGit reference'
+kind = 'repo'
+usage = ['[options] [id]']
+description = """
+Print the SHA1 value of a Git id (defaulting to HEAD). In addition to
+the standard Git id's like heads and tags, this command also accepts
+'[<branch>:]<patch>' and '[<branch>:]{base}' showing the id of a patch
+or the base of the stack. If no branch is specified, it defaults to the
+current one. The bottom of a patch is accessible with the
+'[<branch>:]<patch>^' format."""
+
+args = [argparse.applied_patches, argparse.unapplied_patches,
+        argparse.hidden_patches]
+options = []
+
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
     """Show the applied patches
@@ -48,4 +47,4 @@ def func(parser, options, args):
     else:
         parser.error('incorrect number of arguments')
 
-    out.stdout(git_id(crt_series, id_str))
+    out.stdout(common.git_commit(id_str, directory.repository).sha1)
index cd44d3f..97a6a09 100644 (file)
@@ -15,20 +15,20 @@ 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, re, email
+import sys, os, re, email, tarfile
 from mailbox import UnixMailbox
 from StringIO import StringIO
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'import a GNU diff file as a new patch'
-usage = """%prog [options] [<file>|<url>]
+from stgit import argparse, stack, git
 
+name = 'import'
+help = 'Import a GNU diff file as a new patch'
+kind = 'patch'
+usage = ['[options] [<file>|<url>]']
+description = """
 Create a new patch and apply the given GNU diff file (or the standard
 input). By default, the file name is used as the patch name but this
 can be overridden with the '--name' option. The patch can either be a
@@ -44,52 +44,42 @@ stack.
 The patch description has to be separated from the data with a '---'
 line."""
 
-directory = DirectoryHasRepository()
-options = [make_option('-m', '--mail',
-                       help = 'import the patch from a standard e-mail file',
-                       action = 'store_true'),
-           make_option('-M', '--mbox',
-                       help = 'import a series of patches from an mbox file',
-                       action = 'store_true'),
-           make_option('-s', '--series',
-                       help = 'import a series of patches',
-                       action = 'store_true'),
-           make_option('-u', '--url',
-                       help = 'import a patch from a URL',
-                       action = 'store_true'),
-           make_option('-n', '--name',
-                       help = 'use NAME as the patch name'),
-           make_option('-t', '--strip',
-                       help = 'strip numbering and extension from patch name',
-                       action = 'store_true'),
-           make_option('-i', '--ignore',
-                       help = 'ignore the applied patches in the series',
-                       action = 'store_true'),
-           make_option('--replace',
-                       help = 'replace the unapplied patches in the series',
-                       action = 'store_true'),
-           make_option('-b', '--base',
-                       help = 'use BASE instead of HEAD for file importing'),
-           make_option('-e', '--edit',
-                       help = 'invoke an editor for the patch description',
-                       action = 'store_true'),
-           make_option('-p', '--showpatch',
-                       help = 'show the patch content in the editor buffer',
-                       action = 'store_true'),
-           make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
-                       help = 'use "NAME <EMAIL>" as the author details'),
-           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')
-           ] + make_sign_options()
-
+args = [argparse.files]
+options = [
+    opt('-m', '--mail', action = 'store_true',
+        short = 'Import the patch from a standard e-mail file'),
+    opt('-M', '--mbox', action = 'store_true',
+        short = 'Import a series of patches from an mbox file'),
+    opt('-s', '--series', action = 'store_true',
+        short = 'Import a series of patches', long = """
+        Import a series of patches from a series file or a tar archive."""),
+    opt('-u', '--url', action = 'store_true',
+        short = 'Import a patch from a URL'),
+    opt('-n', '--name',
+        short = 'Use NAME as the patch name'),
+    opt('-t', '--strip', action = 'store_true',
+        short = 'Strip numbering and extension from patch name'),
+    opt('-i', '--ignore', action = 'store_true',
+        short = 'Ignore the applied patches in the series'),
+    opt('--replace', action = 'store_true',
+        short = 'Replace the unapplied patches in the series'),
+    opt('-b', '--base', args = [argparse.commit],
+        short = 'Use BASE instead of HEAD for file importing'),
+    opt('-e', '--edit', action = 'store_true',
+        short = 'Invoke an editor for the patch description'),
+    opt('-p', '--showpatch', action = 'store_true',
+        short = 'Show the patch content in the editor buffer'),
+    opt('-a', '--author', metavar = '"NAME <EMAIL>"',
+        short = 'Use "NAME <EMAIL>" as the author details'),
+    opt('--authname',
+        short = 'Use AUTHNAME as the author name'),
+    opt('--authemail',
+        short = 'Use AUTHEMAIL as the author e-mail'),
+    opt('--authdate',
+        short = 'Use AUTHDATE as the author date'),
+    ] + argparse.sign_options()
+
+directory = DirectoryHasRepository(log = True)
 
 def __strip_patch_name(name):
     stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
@@ -136,8 +126,6 @@ def __create_patch(filename, message, author_name, author_email,
     if not message:
         can_edit = False
 
-    committer_name = committer_email = None
-
     if options.author:
         options.authname, options.authemail = name_email(options.author)
 
@@ -148,17 +136,11 @@ def __create_patch(filename, message, author_name, author_email,
         author_email = options.authemail
     if options.authdate:
         author_date = options.authdate
-    if options.commname:
-        committer_name = options.commname
-    if options.commemail:
-        committer_email = options.commemail
 
     crt_series.new_patch(patch, message = message, can_edit = False,
                          author_name = author_name,
                          author_email = author_email,
-                         author_date = author_date,
-                         committer_name = committer_name,
-                         committer_email = committer_email)
+                         author_date = author_date)
 
     if not diff:
         out.warn('No diff found, creating empty patch')
@@ -175,14 +157,42 @@ def __create_patch(filename, message, author_name, author_email,
                                  backup = False)
         out.done()
 
+def __mkpatchname(name, suffix):
+    if name.lower().endswith(suffix.lower()):
+        return name[:-len(suffix)]
+    return name
+
+def __get_handle_and_name(filename):
+    """Return a file object and a patch name derived from filename
+    """
+    # see if it's a gzip'ed or bzip2'ed patch
+    import bz2, gzip
+    for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]:
+        try:
+            f = copen(filename)
+            f.read(1)
+            f.seek(0)
+            return (f, __mkpatchname(filename, ext))
+        except IOError, e:
+            pass
+
+    # plain old file...
+    return (open(filename), filename)
+
 def __import_file(filename, options, patch = None):
     """Import a patch from a file or standard input
     """
+    pname = None
     if filename:
-        f = file(filename)
+        (f, pname) = __get_handle_and_name(filename)
     else:
         f = sys.stdin
 
+    if patch:
+        pname = patch
+    elif not pname:
+        pname = filename
+
     if options.mail:
         try:
             msg = email.message_from_file(f)
@@ -192,16 +202,11 @@ def __import_file(filename, options, patch = None):
                  parse_mail(msg)
     else:
         message, author_name, author_email, author_date, diff = \
-                 parse_patch(f)
+                 parse_patch(f.read(), contains_diff = True)
 
     if filename:
         f.close()
 
-    if patch:
-        pname = patch
-    else:
-        pname = filename
-
     __create_patch(pname, message, author_name, author_email,
                    author_date, diff, options)
 
@@ -211,6 +216,9 @@ def __import_series(filename, options):
     applied = crt_series.get_applied()
 
     if filename:
+        if tarfile.is_tarfile(filename):
+            __import_tarfile(filename, options)
+            return
         f = file(filename)
         patchdir = os.path.dirname(filename)
     else:
@@ -264,6 +272,44 @@ def __import_url(url, options):
     urllib.urlretrieve(url, filename)
     __import_file(filename, options)
 
+def __import_tarfile(tar, options):
+    """Import patch series from a tar archive
+    """
+    import tempfile
+    import shutil
+
+    if not tarfile.is_tarfile(tar):
+        raise CmdException, "%s is not a tarfile!" % tar
+
+    t = tarfile.open(tar, 'r')
+    names = t.getnames()
+
+    # verify paths in the tarfile are safe
+    for n in names:
+        if n.startswith('/'):
+            raise CmdException, "Absolute path found in %s" % tar
+        if n.find("..") > -1:
+            raise CmdException, "Relative path found in %s" % tar
+
+    # find the series file
+    seriesfile = '';
+    for m in names:
+        if m.endswith('/series') or m == 'series':
+            seriesfile = m
+            break
+    if seriesfile == '':
+        raise CmdException, "no 'series' file found in %s" % tar
+
+    # unpack into a tmp dir
+    tmpdir = tempfile.mkdtemp('.stg')
+    t.extractall(tmpdir)
+
+    # apply the series
+    __import_series(os.path.join(tmpdir, seriesfile), options)
+
+    # cleanup the tmpdir
+    shutil.rmtree(tmpdir)
+
 def func(parser, options, args):
     """Import a GNU diff file as a new patch
     """
index 475a4ce..6ffb93e 100644 (file)
@@ -16,24 +16,21 @@ 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 stgit.commands.common import *
-from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'initialise the current branch for use with StGIT'
-usage = """%prog [options]
-
-Initialise the current GIT branch to be used as an StGIT stack. Note
-that you must already be in a GIT repository and .git/HEAD must point
-to a valid file in refs/heads/."""
-
-directory = DirectoryHasRepository()
+from stgit.commands import common
+from stgit.lib import stack
+
+help = 'Initialise the current branch for use with StGIT'
+kind = 'stack'
+usage = ['']
+description = """
+Initialise the current git branch to be used as an StGIT stack. The
+branch (and the git repository it is in) must already exist and
+contain at least one commit."""
+
+args = []
 options = []
 
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
     """Performs the repository initialisation
@@ -41,4 +38,4 @@ def func(parser, options, args):
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    crt_series.init()
+    stack.Stack.initialise(directory.repository)
index 52d55a5..3370e6c 100644 (file)
@@ -1,5 +1,8 @@
+# -*- coding: utf-8 -*-
+
 __copyright__ = """
 Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com>
+Copyright (C) 2008, Karl Hasselström <kha@treskal.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
@@ -15,133 +18,74 @@ 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, time
-from optparse import OptionParser, make_option
-from pydoc import pager
-from stgit.commands.common import *
-from stgit import stack, git
-from stgit.out import *
-from stgit.run import Run
-
-help = 'display the patch changelog'
-usage = """%prog [options] [patch]
-
-List all the current and past commit ids of the given patch. The
---graphical option invokes gitk instead of printing. The changelog
-commit messages have the form '<action> <new-patch-id>'. The <action>
-can be one of the following:
-
-  new     - new patch created
-  refresh - local changes were added to the patch
-  push    - the patch was cleanly pushed onto the stack
-  push(m) - the patch was pushed onto the stack with a three-way merge
-  push(f) - the patch was fast-forwarded
-  undo    - the patch boundaries were restored to the old values
-
-Note that only the diffs shown in the 'refresh', 'undo' and 'sync'
-actions are meaningful for the patch changes. The 'push' actions
-represent the changes to the entire base of the current
-patch. Conflicts reset the patch content and a subsequent 'refresh'
-will show the entire patch."""
-
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
-           make_option('-p', '--patch',
-                       help = 'show the refresh diffs',
-                       action = 'store_true'),
-           make_option('-n', '--number', type = 'int',
-                       help = 'limit the output to NUMBER commits'),
-           make_option('-f', '--full',
-                       help = 'show the full commit ids',
-                       action = 'store_true'),
-           make_option('-g', '--graphical',
-                       help = 'run gitk instead of printing',
-                       action = 'store_true')]
-
-def show_log(log, options):
-    """List the patch changelog
-    """
-    commit = git.get_commit(log)
-    if options.number != None:
-        n = options.number
-    else:
-        n = -1
-    diff_list = []
-    while commit:
-        if n == 0:
-            # limit the output
-            break
-
-        log = commit.get_log().split('\n')
-
-        cmd_rev = log[0].split()
-        if len(cmd_rev) >= 2:
-            cmd = cmd_rev[0]
-            rev = cmd_rev[1]
-        elif len(cmd_rev) == 1:
-            cmd = cmd_rev[0]
-            rev = ''
-        else:
-            cmd = rev = ''
-
-        if options.patch:
-            if cmd in ['refresh', 'undo', 'sync', 'edit']:
-                diff_list.append(git.pretty_commit(commit.get_id_hash()))
-
-                # limiter decrement
-                n -= 1
-        else:
-            if len(log) >= 3:
-                notes = log[2]
-            else:
-                notes = ''
-            author_name, author_email, author_date = \
-                         name_email_date(commit.get_author())
-            secs, tz = author_date.split()
-            date = '%s %s' % (time.ctime(int(secs)), tz)
-
-            if options.full:
-                out.stdout('%-7s %-40s %s' % (cmd[:7], rev[:40], date))
-            else:
-                out.stdout('%-8s [%-7s] %-28s  %s' % \
-                           (rev[:8], cmd[:7], notes[:28], date))
-
-            # limiter decrement
-            n -= 1
-
-        parent = commit.get_parent()
-        if parent:
-            commit = git.get_commit(parent)
-        else:
-            commit = None
-
-    if options.patch and diff_list:
-        pager('\n'.join(diff_list).rstrip())
+import os.path
+from optparse import make_option
+from stgit import argparse, run
+from stgit.argparse import opt
+from stgit.commands import common
+from stgit.lib import log
+from stgit.out import out
+
+help = 'Display the patch changelog'
+kind = 'stack'
+usage = ['[options] [<patches>]']
+description = """
+List the history of the patch stack: the stack log. If one or more
+patch names are given, limit the list to the log entries that touch
+the named patches.
+
+"stg undo" and "stg redo" let you step back and forth in the patch
+stack. "stg reset" lets you go directly to any state."""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
+options = [
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default one'),
+    opt('-d', '--diff', action = 'store_true',
+        short = 'Show the refresh diffs'),
+    opt('-n', '--number', type = 'int',
+        short = 'Limit the output to NUMBER commits'),
+    opt('-f', '--full', action = 'store_true',
+        short = 'Show the full commit ids'),
+    opt('-g', '--graphical', action = 'store_true',
+        short = 'Run gitk instead of printing')]
+
+directory = common.DirectoryHasRepositoryLib()
+
+def show_log(stacklog, pathlim, num, full, show_diff):
+    cmd = ['git', 'log']
+    if num != None and num > 0:
+        cmd.append('-%d' % num)
+    if show_diff:
+        cmd.append('-p')
+    elif not full:
+        cmd.append('--pretty=format:%h   %aD   %s')
+    run.Run(*(cmd + [stacklog.sha1, '--'] + pathlim)).run()
 
 def func(parser, options, args):
-    """Show the patch changelog
-    """
-    if len(args) == 0:
-        name = crt_series.get_current()
-        if not name:
-            raise CmdException, 'No patches applied'
-    elif len(args) == 1:
-        name = args[0]
-        if not name in crt_series.get_applied() + crt_series.get_unapplied() + \
-               crt_series.get_hidden():
-            raise CmdException, 'Unknown patch "%s"' % name
+    if options.branch:
+        stack = directory.repository.get_stack(options.branch)
     else:
-        parser.error('incorrect number of arguments')
-
-    patch = crt_series.get_patch(name)
-
-    log = patch.get_log()
-    if not log:
-        raise CmdException, 'No changelog for patch "%s"' % name
+        stack = directory.repository.current_stack
+    patches = common.parse_patches(args, list(stack.patchorder.all))
+    logref = log.log_ref(stack.name)
+    try:
+        logcommit = stack.repository.refs.get(logref)
+    except KeyError:
+        out.info('Log is empty')
+        return
+    stacklog = log.get_log_entry(stack.repository, logref, logcommit)
+    pathlim = [os.path.join('patches', pn) for pn in patches]
 
     if options.graphical:
-        # discard the exit codes generated by SIGINT, SIGKILL, SIGTERM
-        Run('gitk', log).returns([0, -2, -9, -15]).run()
+        for o in ['diff', 'number', 'full']:
+            if getattr(options, o):
+                parser.error('cannot combine --graphical and --%s' % o)
+        # Discard the exit codes generated by SIGINT, SIGKILL, and SIGTERM.
+        run.Run(*(['gitk', stacklog.simplified.sha1, '--'] + pathlim)
+                 ).returns([0, -2, -9, -15]).run()
     else:
-        show_log(log, options)
+        show_log(stacklog.simplified, pathlim,
+                 options.number, options.full, options.diff)
index 2230d2a..46e4b55 100644 (file)
@@ -17,27 +17,30 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 import sys, os, re, time, datetime, socket, smtplib, getpass
 import email, email.Utils, email.Header
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git, version, templates
+from stgit import argparse, stack, git, version, templates
 from stgit.config import config
+from stgit.run import Run
+from stgit.lib import git as gitlib
 
-
-help = 'send a patch or series of patches by e-mail'
-usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
-
+help = 'Send a patch or series of patches by e-mail'
+kind = 'patch'
+usage = [' [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
+description = r"""
 Send a patch or a range of patches by e-mail using the SMTP server
 specified by the 'stgit.smtpserver' configuration option, or the
-'--smtp-server' command line option. The From address and the e-mail
-format are generated from the template file passed as argument to
-'--template' (defaulting to '.git/patchmail.tmpl' or
-'~/.stgit/templates/patchmail.tmpl' or
+'--smtp-server' command line option. This option can also be an
+absolute path to 'sendmail' followed by command line arguments.
+
+The From address and the e-mail format are generated from the template
+file passed as argument to '--template' (defaulting to
+'.git/patchmail.tmpl' or '~/.stgit/templates/patchmail.tmpl' or
 '/usr/share/stgit/templates/patchmail.tmpl'). A patch can be sent as
-attachment using the --attach option in which case the 'mailattch.tmpl'
-template will be used instead of 'patchmail.tmpl'.
+attachment using the --attach option in which case the
+'mailattch.tmpl' template will be used instead of 'patchmail.tmpl'.
 
 The To/Cc/Bcc addresses can either be added to the template file or
 passed via the corresponding command line options. They can be e-mail
@@ -84,72 +87,63 @@ the following:
   %(commemail)s    - committer's e-mail
   %(commname)s     - committer's name
   %(diff)s         - unified diff of the patch
-  %(fromauth)s     - 'From: author\\n\\n' if different from sender
+  %(fromauth)s     - 'From: author\n\n' if different from sender
   %(longdescr)s    - the rest of the patch description, after the first line
   %(patch)s        - patch name
   %(prefix)s       - 'prefix ' string passed on the command line
   %(shortdescr)s   - the first line of the patch description"""
 
-directory = DirectoryHasRepository()
-options = [make_option('-a', '--all',
-                       help = 'e-mail all the applied patches',
-                       action = 'store_true'),
-           make_option('--to',
-                       help = 'add TO to the To: list',
-                       action = 'append'),
-           make_option('--cc',
-                       help = 'add CC to the Cc: list',
-                       action = 'append'),
-           make_option('--bcc',
-                       help = 'add BCC to the Bcc: list',
-                       action = 'append'),
-           make_option('--auto',
-                       help = 'automatically cc the patch signers',
-                       action = 'store_true'),
-           make_option('--noreply',
-                       help = 'do not send subsequent messages as replies',
-                       action = 'store_true'),
-           make_option('--unrelated',
-                       help = 'send patches without sequence numbering',
-                       action = 'store_true'),
-           make_option('--attach',
-                       help = 'send a patch as attachment',
-                       action = 'store_true'),
-           make_option('-v', '--version', metavar = 'VERSION',
-                       help = 'add VERSION to the [PATCH ...] prefix'),
-           make_option('--prefix', metavar = 'PREFIX',
-                       help = 'add PREFIX to the [... PATCH ...] prefix'),
-           make_option('-t', '--template', metavar = 'FILE',
-                       help = 'use FILE as the message template'),
-           make_option('-c', '--cover', metavar = 'FILE',
-                       help = 'send FILE as the cover message'),
-           make_option('-e', '--edit-cover',
-                       help = 'edit the cover message before sending',
-                       action = 'store_true'),
-           make_option('-E', '--edit-patches',
-                       help = 'edit each patch before sending',
-                       action = 'store_true'),
-           make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS',
-                       help = 'sleep for SECONDS between e-mails sending'),
-           make_option('--refid',
-                       help = 'use REFID as the reference id'),
-           make_option('--smtp-server', metavar = 'HOST[:PORT]',
-                       help = 'SMTP server to use for sending mail'),
-           make_option('-u', '--smtp-user', metavar = 'USER',
-                       help = 'username for SMTP authentication'),
-           make_option('-p', '--smtp-password', metavar = 'PASSWORD',
-                       help = 'username for SMTP authentication'),
-           make_option('-T', '--smtp-tls',
-                       help = 'use SMTP with TLS encryption',
-                       action = 'store_true'),
-           make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
-           make_option('-O', '--diff-opts',
-                       help = 'options to pass to git diff'),
-           make_option('-m', '--mbox',
-                       help = 'generate an mbox file instead of sending',
-                       action = 'store_true')]
-
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
+options = [
+    opt('-a', '--all', action = 'store_true',
+        short = 'E-mail all the applied patches'),
+    opt('--to', action = 'append',
+        short = 'Add TO to the To: list'),
+    opt('--cc', action = 'append',
+        short = 'Add CC to the Cc: list'),
+    opt('--bcc', action = 'append',
+        short = 'Add BCC to the Bcc: list'),
+    opt('--auto', action = 'store_true',
+        short = 'Automatically cc the patch signers'),
+    opt('--noreply', action = 'store_true',
+        short = 'Do not send subsequent messages as replies'),
+    opt('--unrelated', action = 'store_true',
+        short = 'Send patches without sequence numbering'),
+    opt('--attach', action = 'store_true',
+        short = 'Send a patch as attachment'),
+    opt('-v', '--version', metavar = 'VERSION',
+        short = 'Add VERSION to the [PATCH ...] prefix'),
+    opt('--prefix', metavar = 'PREFIX',
+        short = 'Add PREFIX to the [... PATCH ...] prefix'),
+    opt('-t', '--template', metavar = 'FILE',
+        short = 'Use FILE as the message template'),
+    opt('-c', '--cover', metavar = 'FILE',
+        short = 'Send FILE as the cover message'),
+    opt('-e', '--edit-cover', action = 'store_true',
+        short = 'Edit the cover message before sending'),
+    opt('-E', '--edit-patches', action = 'store_true',
+        short = 'Edit each patch before sending'),
+    opt('-s', '--sleep', type = 'int', metavar = 'SECONDS',
+        short = 'Sleep for SECONDS between e-mails sending'),
+    opt('--refid',
+        short = 'Use REFID as the reference id'),
+    opt('--smtp-server', metavar = 'HOST[:PORT] or "/path/to/sendmail -t -i"',
+        short = 'SMTP server or command to use for sending mail'),
+    opt('-u', '--smtp-user', metavar = 'USER',
+        short = 'Username for SMTP authentication'),
+    opt('-p', '--smtp-password', metavar = 'PASSWORD',
+        short = 'Password for SMTP authentication'),
+    opt('-T', '--smtp-tls', action = 'store_true',
+        short = 'Use SMTP with TLS encryption'),
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch'),
+    opt('-m', '--mbox', action = 'store_true',
+        short = 'Generate an mbox file instead of sending')
+    ] + argparse.diff_opts_option()
+
+directory = DirectoryHasRepository(log = False)
 
 def __get_sender():
     """Return the 'authname <authemail>' string as read from the
@@ -160,20 +154,25 @@ def __get_sender():
         try:
             sender = str(git.user())
         except git.GitException:
-            sender = str(git.author())
-
+            try:
+                sender = str(git.author())
+            except git.GitException:
+                pass
     if not sender:
-        raise CmdException, 'unknown sender details'
+        raise CmdException, ('Unknown sender name and e-mail; you should'
+                             ' for example set git config user.name and'
+                             ' user.email')
+    sender = email.Utils.parseaddr(sender)
+
+    return email.Utils.formataddr(address_or_alias(sender))
 
-    return address_or_alias(sender)
+def __addr_list(msg, header):
+    return [addr for name, addr in
+            email.Utils.getaddresses(msg.get_all(header, []))]
 
 def __parse_addresses(msg):
     """Return a two elements tuple: (from, [to])
     """
-    def __addr_list(msg, header):
-        return [name_addr[1] for name_addr in
-                email.Utils.getaddresses(msg.get_all(header, []))]
-
     from_addr_list = __addr_list(msg, 'From')
     if len(from_addr_list) == 0:
         raise CmdException, 'No "From" address'
@@ -183,10 +182,16 @@ def __parse_addresses(msg):
     if len(to_addr_list) == 0:
         raise CmdException, 'No "To/Cc/Bcc" addresses'
 
-    return (from_addr_list[0], to_addr_list)
+    return (from_addr_list[0], set(to_addr_list))
+
+def __send_message_sendmail(sendmail, msg):
+    """Send the message using the sendmail command.
+    """
+    cmd = sendmail.split()
+    Run(*cmd).raw_input(msg).discard_output()
 
-def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
-                   smtpuser, smtppassword, use_tls):
+def __send_message_smtp(smtpserver, from_addr, to_addr_list, msg,
+                        smtpuser, smtppassword, use_tls):
     """Send the message using the given SMTP server
     """
     try:
@@ -208,29 +213,47 @@ def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
         result = s.sendmail(from_addr, to_addr_list, msg)
         if len(result):
             print "mail server refused delivery for the following recipients: %s" % result
-        # give recipients a chance of receiving patches in the correct order
-        time.sleep(sleep)
     except Exception, err:
         raise CmdException, str(err)
 
     s.quit()
 
+def __send_message(smtpserver, from_addr, to_addr_list, msg,
+                   sleep, smtpuser, smtppassword, use_tls):
+    """Message sending dispatcher.
+    """
+    if smtpserver.startswith('/'):
+        # Use the sendmail tool
+        __send_message_sendmail(smtpserver, msg)
+    else:
+        # Use the SMTP server (we have host and port information)
+        __send_message_smtp(smtpserver, from_addr, to_addr_list, msg,
+                            smtpuser, smtppassword, use_tls)
+    # give recipients a chance of receiving patches in the correct order
+    time.sleep(sleep)
+
 def __build_address_headers(msg, options, extra_cc = []):
     """Build the address headers and check existing headers in the
     template.
     """
-    def __replace_header(header, addr):
-        if addr:
-            crt_addr = msg[header]
-            del msg[header]
-
-            if crt_addr:
-                msg[header] = address_or_alias(', '.join([crt_addr, addr]))
-            else:
-                msg[header] = address_or_alias(addr)
+    def __addr_pairs(msg, header, extra):
+        pairs = email.Utils.getaddresses(msg.get_all(header, []) + extra)
+        # remove pairs without an address and resolve the aliases
+        return [address_or_alias(p) for p in pairs if p[1]]
+
+    def __update_header(header, addr = '', ignore = ()):
+        addr_pairs = __addr_pairs(msg, header, [addr])
+        del msg[header]
+        # remove the duplicates and filter the addresses
+        addr_dict = dict((addr, email.Utils.formataddr((name, addr)))
+                         for name, addr in addr_pairs if addr not in ignore)
+        if addr_dict:
+            msg[header] = ', '.join(addr_dict.itervalues())
+        return set(addr_dict.iterkeys())
 
     to_addr = ''
     cc_addr = ''
+    extra_cc_addr = ''
     bcc_addr = ''
 
     autobcc = config.get('stgit.autobcc') or ''
@@ -238,26 +261,44 @@ def __build_address_headers(msg, options, extra_cc = []):
     if options.to:
         to_addr = ', '.join(options.to)
     if options.cc:
-        cc_addr = ', '.join(options.cc + extra_cc)
-        cc_addr = ', '.join(options.cc + extra_cc)
-    elif extra_cc:
-        cc_addr = ', '.join(extra_cc)
+        cc_addr = ', '.join(options.cc)
+    if extra_cc:
+        extra_cc_addr = ', '.join(extra_cc)
     if options.bcc:
         bcc_addr = ', '.join(options.bcc + [autobcc])
     elif autobcc:
         bcc_addr = autobcc
 
-    __replace_header('To', to_addr)
-    __replace_header('Cc', cc_addr)
-    __replace_header('Bcc', bcc_addr)
+    # if an address is on a header, ignore it from the rest
+    to_set = __update_header('To', to_addr)
+    cc_set = __update_header('Cc', cc_addr, to_set)
+    bcc_set = __update_header('Bcc', bcc_addr, to_set.union(cc_set))
+
+    # --auto generated addresses, don't include the sender
+    from_set = __update_header('From')
+    __update_header('Cc', extra_cc_addr, to_set.union(bcc_set).union(from_set))
+
+    # update other address headers
+    __update_header('Reply-To')
+    __update_header('Mail-Reply-To')
+    __update_header('Mail-Followup-To')
 
 def __get_signers_list(msg):
     """Return the address list generated from signed-off-by and
     acked-by lines in the message.
     """
     addr_list = []
-
-    r = re.compile('^(signed-off-by|acked-by|cc):\s+(.+)$', re.I)
+    tags = '%s|%s|%s|%s|%s|%s|%s' % (
+            'signed-off-by',
+            'acked-by',
+            'cc',
+            'reviewed-by',
+            'reported-by',
+            'tested-by',
+            'reported-and-tested-by')
+    regex = '^(%s):\s+(.+)$' % tags
+
+    r = re.compile(regex, re.I)
     for line in msg.split('\n'):
         m = r.match(line)
         if m:
@@ -276,7 +317,7 @@ def __build_extra_headers(msg, msg_id, ref_id = None):
         ref_id = '<%s>' % ref_id.strip(' \t\n<>')
         msg['In-Reply-To'] = ref_id
         msg['References'] = ref_id
-    msg['User-Agent'] = 'StGIT/%s' % version.version
+    msg['User-Agent'] = 'StGit/%s' % version.version
 
 def __encode_message(msg):
     # 7 or 8 bit encoding
@@ -347,11 +388,6 @@ def __build_cover(tmpl, patches, msg_id, options):
     else:
         number_str = ''
 
-    if options.diff_opts:
-        diff_flags = options.diff_opts.split()
-    else:
-        diff_flags = []
-
     tmpl_dict = {'sender':       sender,
                  # for backward template compatibility
                  'maintainer':   sender,
@@ -366,10 +402,10 @@ def __build_cover(tmpl, patches, msg_id, options):
                  'number':       number_str,
                  'shortlog':     stack.shortlog(crt_series.get_patch(p)
                                                 for p in patches),
-                 'diffstat':     git.diffstat(
-                     rev1 = git_id(crt_series, '%s//bottom' % patches[0]),
-                     rev2 = git_id(crt_series, '%s//top' % patches[-1]),
-                     diff_flags = diff_flags)}
+                 'diffstat':     gitlib.diffstat(git.diff(
+                     rev1 = git_id(crt_series, '%s^' % patches[0]),
+                     rev2 = git_id(crt_series, '%s' % patches[-1]),
+                     diff_flags = options.diff_flags))}
 
     try:
         msg_string = tmpl % tmpl_dict
@@ -408,8 +444,8 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
         options.edit_patches = True
 
     descr_lines = descr.split('\n')
-    short_descr = descr_lines[0].rstrip()
-    long_descr = '\n'.join(descr_lines[1:]).lstrip()
+    short_descr = descr_lines[0].strip()
+    long_descr = '\n'.join(l.rstrip() for l in descr_lines[1:]).lstrip('\n')
 
     authname = p.get_authname();
     authemail = p.get_authemail();
@@ -437,11 +473,6 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
             prefix_str = confprefix + ' '
         else:
             prefix_str = ''
-        
-    if options.diff_opts:
-        diff_flags = options.diff_opts.split()
-    else:
-        diff_flags = []
 
     total_nr_str = str(total_nr)
     patch_nr_str = str(patch_nr).zfill(len(total_nr_str))
@@ -450,6 +481,9 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
     else:
         number_str = ''
 
+    diff = git.diff(rev1 = git_id(crt_series, '%s^' % patch),
+                    rev2 = git_id(crt_series, '%s' % patch),
+                    diff_flags = options.diff_flags)
     tmpl_dict = {'patch':        patch,
                  'sender':       sender,
                  # for backward template compatibility
@@ -458,14 +492,8 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
                  'longdescr':    long_descr,
                  # for backward template compatibility
                  'endofheaders': '',
-                 'diff':         git.diff(
-                     rev1 = git_id(crt_series, '%s//bottom' % patch),
-                     rev2 = git_id(crt_series, '%s//top' % patch),
-                     diff_flags = diff_flags),
-                 'diffstat':     git.diffstat(
-                     rev1 = git_id(crt_series, '%s//bottom'%patch),
-                     rev2 = git_id(crt_series, '%s//top' % patch),
-                     diff_flags = diff_flags),
+                 'diff':         diff,
+                 'diffstat':     gitlib.diffstat(diff),
                  # for backward template compatibility
                  'date':         '',
                  'version':      version_str,
@@ -529,6 +557,9 @@ def func(parser, options, args):
     else:
         raise CmdException, 'Incorrect options. Unknown patches to send'
 
+    # early test for sender identity
+    __get_sender()
+
     out.start('Checking the validity of the patches')
     for p in patches:
         if crt_series.empty_patch(p):
index 6a8f086..151cfe9 100644 (file)
@@ -16,70 +16,90 @@ 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 stgit.commands.common import *
-from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'create a new patch and make it the topmost one'
-usage = """%prog [options] [name]
-
-Create a new, empty patch and make it the topmost one. If the
-'--message' option is not passed, an editor is invoked with the
-.git/patchdescr.tmpl, ~/.stgit/templates/patchdescr.tmpl or
-/usr/share/stgit/templates/patchdescr.tmpl file used a as template,
-together with generated lines. The local changes in the working tree
-are not included in the patch; an "stg refresh" command is needed for
-this.
-
-If no name is given for the new patch, one is generated from the first
-line of the commit message."""
-
-directory = DirectoryGotoToplevel()
-options = [make_option('-m', '--message',
-                       help = 'use MESSAGE as the patch description'),
-           make_option('-s', '--showpatch',
-                       help = 'show the patch content in the editor buffer',
-                       action = 'store_true'),
-           make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
-                       help = 'use "NAME <EMAIL>" as the author details'),
-           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')
-           ] + make_sign_options()
-
+from stgit import argparse, utils
+from stgit.commands import common
+from stgit.lib import git as gitlib, transaction
+from stgit.config import config
+
+help = 'Create a new, empty patch'
+kind = 'patch'
+usage = ['[options] [<name>]']
+description = """
+Create a new, empty patch on the current stack. The new patch is
+created on top of the currently applied patches, and is made the new
+top of the stack. Uncommitted changes in the work tree are not
+included in the patch -- that is handled by stglink:refresh[].
+
+The given name must be unique in the stack, and may only contain
+alphanumeric characters, dashes and underscores. If no name is given,
+one is generated from the first line of the patch's commit message.
+
+An editor will be launched to edit the commit message to be used for
+the patch, unless the '--message' flag already specified one. The
+'patchdescr.tmpl' template file (if available) is used to pre-fill the
+editor."""
+
+args = []
+options = (argparse.author_options()
+           + argparse.message_options(save_template = True)
+           + argparse.sign_options())
+
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
-    """Creates a new patch
-    """
+    """Create a new patch."""
+    stack = directory.repository.current_stack
+    if stack.repository.default_index.conflicts():
+        raise common.CmdException(
+            'Cannot create a new patch -- resolve conflicts first')
+
+    # Choose a name for the new patch -- or None, which means make one
+    # up later when we've gotten hold of the commit message.
     if len(args) == 0:
-        name = None # autogenerate a name
+        name = None
     elif len(args) == 1:
         name = args[0]
+        if stack.patches.exists(name):
+            raise common.CmdException('%s: patch already exists' % name)
     else:
         parser.error('incorrect number of arguments')
 
-    check_conflicts()
-    check_head_top_equal(crt_series)
+    cd = gitlib.CommitData(
+        tree = stack.head.data.tree, parents = [stack.head], message = '',
+        author = gitlib.Person.author(), committer = gitlib.Person.committer())
+
+    # Set patch commit message from commandline.
+    if options.message != None:
+        cd = cd.set_message(options.message)
 
-    if options.author:
-        options.authname, options.authemail = name_email(options.author)
+    # Modify author data.
+    cd = cd.set_author(options.author(cd.author))
 
-    crt_series.new_patch(name, message = options.message,
-                         show_patch = options.showpatch,
-                         author_name = options.authname,
-                         author_email = options.authemail,
-                         author_date = options.authdate,
-                         committer_name = options.commname,
-                         committer_email = options.commemail,
-                         sign_str = options.sign_str)
+    # Add Signed-off-by: or similar.
+    if options.sign_str != None:
+        sign_str = options.sign_str
+    else:
+        sign_str = config.get("stgit.autosign")
+
+    if sign_str != None:
+        cd = cd.set_message(
+            utils.add_sign_line(cd.message, sign_str,
+                                cd.committer.name, cd.committer.email))
+
+    if options.save_template:
+        options.save_template(cd.message)
+        return utils.STGIT_SUCCESS
+
+    # Let user edit the commit message manually.
+    if not options.message:
+        cd = cd.set_message(utils.edit_string(cd.message, '.stgit-new.txt'))
+    if name == None:
+        name = utils.make_patch_name(cd.message,
+                                     lambda name: stack.patches.exists(name))
+
+    # Write the new patch.
+    iw = stack.repository.default_iw
+    trans = transaction.StackTransaction(stack, 'new')
+    trans.patches[name] = stack.repository.commit(cd)
+    trans.applied.append(name)
+    return trans.run()
index 140699d..54fac21 100644 (file)
@@ -16,29 +16,30 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
 from pydoc import pager
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'show the applied patches modifying a file'
-usage = """%prog [options] [<files or dirs>]
+from stgit import argparse, stack, git
 
+help = 'Show the applied patches modifying a file'
+kind = 'stack'
+usage = ['[options] [<files or dirs>]']
+description = """
 Show the applied patches modifying the given files. Without arguments,
 it shows the patches affected by the local tree modifications. The
 '--diff' option also lists the patch log and the diff for the given
 files."""
 
-directory = DirectoryHasRepository()
-options = [make_option('-d', '--diff',
-                       help = 'show the diff for the given files',
-                       action = 'store_true'),
-           make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one')]
+args = [argparse.known_files]
+options = [
+    opt('-d', '--diff', action = 'store_true',
+        short = 'Show the diff for the given files'),
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch')]
+
+directory = DirectoryHasRepository(log = False)
 
 diff_tmpl = \
           '-------------------------------------------------------------------------------\n' \
index add2a33..ee08c01 100644 (file)
@@ -16,18 +16,17 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 from stgit.stack import Series
 
-
-help = 'import a patch from a different branch or a commit object'
-usage = """%prog [options] ([<patch1>] [<patch2>] [<patch3>..<patch4>])|<commit>
-
+help = 'Import a patch from a different branch or a commit object'
+kind = 'patch'
+usage = ['[options] ([<patch1>] [<patch2>] [<patch3>..<patch4>])|<commit>']
+description = """
 Import one or more patches from a different branch or a commit object
 into the current series. By default, the name of the imported patch is
 used as the name of the current patch. It can be overridden with the
@@ -35,28 +34,28 @@ used as the name of the current patch. It can be overridden with the
 option. The log and author information are those of the commit
 object."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-n', '--name',
-                       help = 'use NAME as the patch name'),
-           make_option('-B', '--ref-branch',
-                       help = 'pick patches from BRANCH'),
-           make_option('-r', '--reverse',
-                       help = 'reverse the commit object before importing',
-                       action = 'store_true'),
-           make_option('-p', '--parent', metavar = 'COMMITID',
-                       help = 'use COMMITID as parent'),
-           make_option('-x', '--expose',
-                       help = 'append the imported commit id to the patch log',
-                       action = 'store_true'),
-           make_option('--fold',
-                       help = 'fold the commit object into the current patch',
-                       action = 'store_true'),
-           make_option('--update',
-                       help = 'like fold but only update the current patch files',
-                       action = 'store_true'),
-           make_option('--unapplied',
-                       help = 'keep the patch unapplied',
-                       action = 'store_true')]
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
+options = [
+    opt('-n', '--name',
+        short = 'Use NAME as the patch name'),
+    opt('-B', '--ref-branch', args = [argparse.stg_branches],
+        short = 'Pick patches from BRANCH'),
+    opt('-r', '--reverse', action = 'store_true',
+        short = 'Reverse the commit object before importing'),
+    opt('-p', '--parent', metavar = 'COMMITID', args = [argparse.commit],
+        short = 'Use COMMITID as parent'),
+    opt('-x', '--expose', action = 'store_true',
+        short = 'Append the imported commit id to the patch log'),
+    opt('--fold', action = 'store_true',
+        short = 'Fold the commit object into the current patch'),
+    opt('--update', action = 'store_true',
+        short = 'Like fold but only update the current patch files'),
+    opt('--unapplied', action = 'store_true',
+        short = 'Keep the patch unapplied')]
+
+directory = DirectoryGotoToplevel(log = True)
 
 def __pick_commit(commit_id, patchname, options):
     """Pick a commit id.
@@ -65,6 +64,8 @@ def __pick_commit(commit_id, patchname, options):
 
     if options.name:
         patchname = options.name
+    if patchname:
+        patchname = find_patch_name(patchname, crt_series.patch_exists)
 
     if options.parent:
         parent = git_id(crt_series, options.parent)
@@ -83,12 +84,12 @@ def __pick_commit(commit_id, patchname, options):
 
         # try a direct git apply first
         if not git.apply_diff(bottom, top):
-            git.merge(bottom, git.get_head(), top, recursive = True)
+            git.merge_recursive(bottom, git.get_head(), top)
 
         out.done()
     elif options.update:
-        rev1 = git_id(crt_series, '//bottom')
-        rev2 = git_id(crt_series, '//top')
+        rev1 = git_id(crt_series, 'HEAD^')
+        rev2 = git_id(crt_series, 'HEAD')
         files = git.barefiles(rev1, rev2).split('\n')
 
         out.start('Updating with commit %s' % commit_id)
@@ -115,10 +116,8 @@ def __pick_commit(commit_id, patchname, options):
         patchname = newpatch.get_name()
 
         # find a patchlog to fork from
-        (refpatchname, refbranchname, refpatchid) = parse_rev(patchname)
-        if refpatchname and not refpatchid and \
-               (not refpatchid or refpatchid == 'top'):
-            # FIXME: should also support picking //top.old
+        refbranchname, refpatchname = parse_rev(patchname)
+        if refpatchname:
             if refbranchname:
                 # assume the refseries is OK, since we already resolved
                 # commit_str to a git_id
@@ -180,11 +179,16 @@ def func(parser, options, args):
         if options.parent:
             raise CmdException, '--parent can only be specified with one patch'
 
-    if (options.fold or options.update) and not crt_series.get_current():
+    if options.update and not crt_series.get_current():
         raise CmdException, 'No patches applied'
 
     if commit_id:
-        __pick_commit(commit_id, None, options)
+        # Try to guess a patch name if the argument was <branch>:<patch>
+        try:
+            patchname = args[0].split(':')[1]
+        except IndexError:
+            patchname = None
+        __pick_commit(commit_id, patchname, options)
     else:
         if options.unapplied:
             patches.reverse()
index dedf41b..2c78ac2 100644 (file)
@@ -17,35 +17,34 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'pop one or more patches from the stack'
-usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
+from stgit import argparse, stack, git
 
+help = 'Pop one or more patches from the stack'
+kind = 'stack'
+usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
+description = """
 Pop the topmost patch or a range of patches from the stack. The
 command fails if there are conflicts or local changes (and --keep was
 not specified).
 
 A series of pop and push operations are performed so that only the
 patches passed on the command line are popped from the stack. Some of
-the push operations may fail because of conflicts (push --undo would
+the push operations may fail because of conflicts ("stg undo" would
 revert the last push operation)."""
 
-directory = DirectoryGotoToplevel()
-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('-k', '--keep',
-                       help = 'keep the local changes',
-                       action = 'store_true')]
+args = [argparse.patch_range(argparse.applied_patches)]
+options = [
+    opt('-a', '--all', action = 'store_true',
+        short = 'Pop all the applied patches'),
+    opt('-n', '--number', type = 'int',
+        short = 'Pop the specified number of patches'),
+    opt('-k', '--keep', action = 'store_true',
+        short = 'Keep the local changes')]
 
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Pop the topmost patch from the stack
index bec7fd7..f6d1398 100644 (file)
@@ -16,18 +16,17 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
 from stgit.config import GitConfigException
-from stgit import stack, git
-
-
-help = 'pull the changes from the remote repository'
-usage = """%prog [options] [<repository>]
+from stgit import argparse, stack, git
 
+help = 'Pull changes from a remote repository'
+kind = 'stack'
+usage = ['[options] [<repository>]']
+description = """
 Pull the latest changes from the given remote repository (defaulting
 to branch.<name>.remote, or 'origin' if not set). This command works
 by popping all the patches from the stack, pulling the changes in the
@@ -38,13 +37,14 @@ resolved and the patch pushed again.
 
 Check the 'git fetch' documentation for the <repository> format."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-n', '--nopush',
-                       help = 'do not push the patches back after pulling',
-                       action = 'store_true'),
-           make_option('-m', '--merged',
-                       help = 'check for patches merged upstream',
-                       action = 'store_true')]
+args = [argparse.repo]
+options = [
+    opt('-n', '--nopush', action = 'store_true',
+        short = 'Do not push the patches back after pulling'),
+    opt('-m', '--merged', action = 'store_true',
+        short = 'Check for patches merged upstream')]
+
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Pull the changes from a remote repository
index 979835b..818e02d 100644 (file)
@@ -17,65 +17,44 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'push one or more patches onto of the stack'
-usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
+from stgit import argparse, stack, git
 
+help = 'Push one or more patches onto the stack'
+kind = 'stack'
+usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
+description = """
 Push one or more patches (defaulting to the first unapplied one) onto
 the stack. The 'push' operation allows patch reordering by commuting
-them with the three-way merge algorithm. If the result of the 'push'
-operation is not acceptable or if there are too many conflicts, the
-'--undo' option can be used to revert the last pushed patch. Conflicts
-raised during the push operation have to be fixed and the 'resolved'
-command run.
+them with the three-way merge algorithm. If there are conflicts while
+pushing a patch, those conflicts are written to the work tree, and the
+command halts. Conflicts raised during the push operation have to be
+fixed and the 'resolved' command run (alternatively, you may undo the
+conflicting push with 'stg undo').
 
 The command also notifies when the patch becomes empty (fully merged
 upstream) or is modified (three-way merged) by the 'push' operation."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-a', '--all',
-                       help = 'push all the unapplied patches',
-                       action = 'store_true'),
-           make_option('-n', '--number', type = 'int',
-                       help = 'push the specified number of patches'),
-           make_option('--reverse',
-                       help = 'push the patches in reverse order',
-                       action = 'store_true'),
-           make_option('-m', '--merged',
-                       help = 'check for patches merged upstream',
-                       action = 'store_true'),
-           make_option('--undo',
-                       help = 'undo the last patch pushing',
-                       action = 'store_true')]
+args = [argparse.patch_range(argparse.unapplied_patches)]
+options = [
+    opt('-a', '--all', action = 'store_true',
+        short = 'Push all the unapplied patches'),
+    opt('-n', '--number', type = 'int',
+        short = 'Push the specified number of patches'),
+    opt('--reverse', action = 'store_true',
+        short = 'Push the patches in reverse order'),
+    opt('-m', '--merged', action = 'store_true',
+        short = 'Check for patches merged upstream')]
 
+directory = DirectoryGotoToplevel(log = True)
 
 def func(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 CmdException, 'No patch to undo'
-
-        out.start('Undoing push of "%s"' % patch)
-        resolved_all()
-        if crt_series.undo_push():
-            out.done()
-        else:
-            out.done('patch unchanged')
-        print_crt_patch(crt_series)
-
-        return
-
     check_local_changes()
     check_conflicts()
     check_head_top_equal(crt_series)
index ec2e04c..a4bc6e7 100644 (file)
@@ -16,16 +16,15 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'move the stack base to another point in history'
-usage = """%prog [options] <new-base-id>
+from stgit import argparse, stack, git
 
+help = 'Move the stack base to another point in history'
+kind = 'stack'
+usage = ['[options] <new-base-id>']
+description = """
 Pop all patches from current stack, move the stack base to the given
 <new-base-id> and push the patches back.
 
@@ -38,16 +37,17 @@ the rebase by executing the following sequence:
 
 Or if you want to skip that patch:
 
-        $ stg push --undo
+        $ stg undo --hard
         $ stg push next-patch..top-patch"""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-n', '--nopush',
-                       help = 'do not push the patches back after rebasing',
-                       action = 'store_true'),
-           make_option('-m', '--merged',
-                       help = 'check for patches merged upstream',
-                       action = 'store_true')]
+args = [argparse.commit]
+options = [
+    opt('-n', '--nopush', action = 'store_true',
+        short = 'Do not push the patches back after rebasing'),
+    opt('-m', '--merged', action = 'store_true',
+        short = 'Check for patches merged upstream')]
+
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Rebase the current stack
diff --git a/stgit/commands/redo.py b/stgit/commands/redo.py
new file mode 100644 (file)
index 0000000..eb8b20f
--- /dev/null
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2008, Karl Hasselström <kha@treskal.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.argparse import opt
+from stgit.commands import common
+from stgit.lib import log, transaction
+
+help = 'Undo the last undo operation'
+kind = 'stack'
+usage = ['']
+description = """
+If the last command was an undo, reset the patch stack to the state it
+had before the undo. Consecutive invocations of "stg redo" will undo
+the effects of consecutive invocations of "stg undo".
+
+It is an error to run "stg redo" if the last command was not an
+undo."""
+
+args = []
+options = [
+    opt('-n', '--number', type = 'int', metavar = 'N', default = 1,
+        short = 'Undo the last N undos'),
+    opt('--hard', action = 'store_true',
+        short = 'Discard changes in your index/worktree')]
+
+directory = common.DirectoryHasRepositoryLib()
+
+def func(parser, options, args):
+    stack = directory.repository.current_stack
+    if options.number < 1:
+        raise common.CmdException('Bad number of undos to redo')
+    state = log.undo_state(stack, -options.number)
+    trans = transaction.StackTransaction(stack, 'redo %d' % options.number,
+                                         discard_changes = options.hard,
+                                         allow_bad_head = True)
+    try:
+        log.reset_stack(trans, stack.repository.default_iw, state)
+    except transaction.TransactionHalted:
+        pass
+    return trans.run(stack.repository.default_iw, allow_bad_head = True)
index 6e8ed0c..27cccc5 100644 (file)
@@ -1,6 +1,8 @@
+# -*- coding: utf-8 -*-
 
 __copyright__ = """
 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+Copyright (C) 2008, Karl Hasselström <kha@treskal.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
@@ -16,110 +18,238 @@ 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 stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git
-from stgit.config import config
-
-
-help = 'generate a new commit for the current patch'
-usage = """%prog [options] [<files or dirs>]
-
-Include the latest tree changes in the current patch. This command
-generates a new GIT commit object with the patch details, the previous
-one no longer being visible. The patch attributes like author,
-committer and description can be changed with the command line
-options. The '--force' option is useful when a commit object was
-created with a different tool but the changes need to be included in
-the current patch."""
-
-directory = DirectoryHasRepository()
-options = [make_option('-f', '--force',
-                       help = 'force the refresh even if HEAD and '\
-                       'top differ',
-                       action = 'store_true'),
-           make_option('--update',
-                       help = 'only update the current patch files',
-                       action = 'store_true'),
-           make_option('--undo',
-                       help = 'revert the commit generated by the last refresh',
-                       action = 'store_true'),
-           make_option('-a', '--annotate', metavar = 'NOTE',
-                       help = 'annotate the patch log entry'),
-           make_option('-p', '--patch',
-                       help = 'refresh (applied) PATCH instead of the top one')
-           ]
-
-def func(parser, options, args):
-    """Generate a new commit for the current or given patch.
-    """
-    args = git.ls_files(args)
-    directory.cd_to_topdir()
-
-    autoresolved = config.get('stgit.autoresolved')
-    if autoresolved != 'yes':
-        check_conflicts()
-
-    if options.patch:
-        if args or options.update:
-            raise CmdException, \
-                  'Only full refresh is available with the --patch option'
-        patch = options.patch
-        if not crt_series.patch_applied(patch):
-            raise CmdException, 'Patches "%s" not applied' % patch
+from stgit.argparse import opt
+from stgit.commands import common
+from stgit.lib import git, transaction, edit
+from stgit.out import out
+from stgit import argparse, utils
+
+help = 'Generate a new commit for the current patch'
+kind = 'patch'
+usage = ['[options] [<files or dirs>]']
+description = """
+Include the latest work tree and index changes in the current patch.
+This command generates a new git commit object for the patch; the old
+commit is no longer visible.
+
+You may optionally list one or more files or directories relative to
+the current working directory; if you do, only matching files will be
+updated.
+
+Behind the scenes, stg refresh first creates a new temporary patch
+with your updates, and then merges that patch into the patch you asked
+to have refreshed. If you asked to refresh a patch other than the
+topmost patch, there can be conflicts; in that case, the temporary
+patch will be left for you to take care of, for example with stg
+coalesce.
+
+The creation of the temporary patch is recorded in a separate entry in
+the patch stack log; this means that one undo step will undo the merge
+between the other patch and the temp patch, and two undo steps will
+additionally get rid of the temp patch."""
+
+args = [argparse.dirty_files]
+options = [
+    opt('-u', '--update', action = 'store_true',
+        short = 'Only update the current patch files'),
+    opt('-i', '--index', action = 'store_true',
+        short = 'Refresh from index instead of worktree', long = """
+        Instead of setting the patch top to the current contents of
+        the worktree, set it to the current contents of the index."""),
+    opt('-p', '--patch', args = [argparse.other_applied_patches,
+                                 argparse.unapplied_patches],
+        short = 'Refresh (applied) PATCH instead of the top patch'),
+    opt('-e', '--edit', action = 'store_true',
+        short = 'Invoke an editor for the patch description'),
+    ] + (argparse.message_options(save_template = False) +
+         argparse.sign_options() + argparse.author_options())
+
+directory = common.DirectoryHasRepositoryLib()
+
+def get_patch(stack, given_patch):
+    """Get the name of the patch we are to refresh."""
+    if given_patch:
+        patch_name = given_patch
+        if not stack.patches.exists(patch_name):
+            raise common.CmdException('%s: no such patch' % patch_name)
+        return patch_name
+    else:
+        if not stack.patchorder.applied:
+            raise common.CmdException(
+                'Cannot refresh top patch, because no patches are applied')
+        return stack.patchorder.applied[-1]
+
+def list_files(stack, patch_name, args, index, update):
+    """Figure out which files to update."""
+    if index:
+        # --index: Don't update the index.
+        return set()
+    paths = stack.repository.default_iw.changed_files(
+        stack.head.data.tree, args or [])
+    if update:
+        # --update: Restrict update to the paths that were already
+        # part of the patch.
+        paths &= stack.patches.get(patch_name).files()
+    return paths
+
+def write_tree(stack, paths, temp_index):
+    """Possibly update the index, and then write its tree.
+    @return: The written tree.
+    @rtype: L{Tree<stgit.git.Tree>}"""
+    def go(index):
+        if paths:
+            iw = git.IndexAndWorktree(index, stack.repository.default_worktree)
+            iw.update_index(paths)
+        return index.write_tree()
+    if temp_index:
+        index = stack.repository.temp_index()
+        try:
+            index.read_tree(stack.head)
+            return go(index)
+        finally:
+            index.delete()
+            stack.repository.default_iw.update_index(paths)
     else:
-        patch = crt_series.get_current()
-        if not patch:
-            raise CmdException, 'No patches applied'
-
-    if not options.force:
-        check_head_top_equal(crt_series)
-
-    if options.undo:
-        out.start('Undoing the refresh of "%s"' % patch)
-        crt_series.undo_refresh()
-        out.done()
-        return
-
-    files = [path for (stat, path) in git.tree_status(files = args, verbose = True)]
-
-    if files or not crt_series.head_top_equal():
-        if options.patch:
-            applied = crt_series.get_applied()
-            between = applied[:applied.index(patch):-1]
-            pop_patches(crt_series, between, keep = True)
-        elif options.update:
-            rev1 = git_id(crt_series, '//bottom')
-            rev2 = git_id(crt_series, '//top')
-            patch_files = git.barefiles(rev1, rev2).split('\n')
-            files = [f for f in files if f in patch_files]
-            if not files:
-                out.info('No modified files for updating patch "%s"' % patch)
-                return
-
-        out.start('Refreshing patch "%s"' % patch)
-
-        if autoresolved == 'yes':
-            resolved_all()
-        crt_series.refresh_patch(files = files,
-                                 backup = True, notes = options.annotate)
-
-        if crt_series.empty_patch(patch):
-            out.done('empty patch')
-        else:
-            out.done()
-
-        if options.patch:
-            between.reverse()
-            push_patches(crt_series, between)
-    elif options.annotate:
-        # only annotate the top log entry as there is no need to
-        # refresh the patch and generate a full commit
-        crt_series.log_patch(crt_series.get_patch(patch), None,
-                             notes = options.annotate)
+        return go(stack.repository.default_index)
+
+def make_temp_patch(stack, patch_name, paths, temp_index):
+    """Commit index to temp patch, in a complete transaction. If any path
+    limiting is in effect, use a temp index."""
+    tree = write_tree(stack, paths, temp_index)
+    commit = stack.repository.commit(git.CommitData(
+            tree = tree, parents = [stack.head],
+            message = 'Refresh of %s' % patch_name))
+    temp_name = utils.make_patch_name('refresh-temp', stack.patches.exists)
+    trans = transaction.StackTransaction(stack,
+                                         'refresh (create temporary patch)')
+    trans.patches[temp_name] = commit
+    trans.applied.append(temp_name)
+    return trans.run(stack.repository.default_iw,
+                     print_current_patch = False), temp_name
+
+def absorb_applied(trans, iw, patch_name, temp_name, edit_fun):
+    """Absorb the temp patch (C{temp_name}) into the given patch
+    (C{patch_name}), which must be applied. If the absorption
+    succeeds, call C{edit_fun} on the resulting
+    L{CommitData<stgit.lib.git.CommitData>} before committing it and
+    commit the return value.
+
+    @return: C{True} if we managed to absorb the temp patch, C{False}
+             if we had to leave it for the user to deal with."""
+    temp_absorbed = False
+    try:
+        # Pop any patch on top of the patch we're refreshing.
+        to_pop = trans.applied[trans.applied.index(patch_name)+1:]
+        if len(to_pop) > 1:
+            popped = trans.pop_patches(lambda pn: pn in to_pop)
+            assert not popped # no other patches were popped
+            trans.push_patch(temp_name, iw)
+        assert to_pop.pop() == temp_name
+
+        # Absorb the temp patch.
+        temp_cd = trans.patches[temp_name].data
+        assert trans.patches[patch_name] == temp_cd.parent
+        trans.patches[patch_name] = trans.stack.repository.commit(
+            edit_fun(trans.patches[patch_name].data.set_tree(temp_cd.tree)))
+        popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True)
+        assert not popped # the temp patch was topmost
+        temp_absorbed = True
+
+        # Push back any patch we were forced to pop earlier.
+        for pn in to_pop:
+            trans.push_patch(pn, iw)
+    except transaction.TransactionHalted:
+        pass
+    return temp_absorbed
+
+def absorb_unapplied(trans, iw, patch_name, temp_name, edit_fun):
+    """Absorb the temp patch (C{temp_name}) into the given patch
+    (C{patch_name}), which must be unapplied. If the absorption
+    succeeds, call C{edit_fun} on the resulting
+    L{CommitData<stgit.lib.git.CommitData>} before committing it and
+    commit the return value.
+
+    @param iw: Not used.
+    @return: C{True} if we managed to absorb the temp patch, C{False}
+             if we had to leave it for the user to deal with."""
+
+    # Pop the temp patch.
+    popped = trans.pop_patches(lambda pn: pn == temp_name)
+    assert not popped # the temp patch was topmost
+
+    # Try to create the new tree of the refreshed patch. (This is the
+    # same operation as pushing the temp patch onto the patch we're
+    # trying to refresh -- but we don't have a worktree to spill
+    # conflicts to, so if the simple merge doesn't succeed, we have to
+    # give up.)
+    patch_cd = trans.patches[patch_name].data
+    temp_cd = trans.patches[temp_name].data
+    new_tree = trans.stack.repository.simple_merge(
+        base = temp_cd.parent.data.tree,
+        ours = patch_cd.tree, theirs = temp_cd.tree)
+    if new_tree:
+        # It worked. Refresh the patch with the new tree, and delete
+        # the temp patch.
+        trans.patches[patch_name] = trans.stack.repository.commit(
+            edit_fun(patch_cd.set_tree(new_tree)))
+        popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True)
+        assert not popped # the temp patch was not applied
+        return True
     else:
-        out.info('Patch "%s" is already up to date' % patch)
+        # Nope, we couldn't create the new tree, so we'll just have to
+        # leave the temp patch for the user.
+        return False
+
+def absorb(stack, patch_name, temp_name, edit_fun):
+    """Absorb the temp patch into the target patch."""
+    trans = transaction.StackTransaction(stack, 'refresh')
+    iw = stack.repository.default_iw
+    f = { True: absorb_applied, False: absorb_unapplied
+          }[patch_name in trans.applied]
+    if f(trans, iw, patch_name, temp_name, edit_fun):
+        def info_msg(): pass
+    else:
+        def info_msg():
+            out.warn('The new changes did not apply cleanly to %s.'
+                     % patch_name, 'They were saved in %s.' % temp_name)
+    r = trans.run(iw)
+    info_msg()
+    return r
+
+def func(parser, options, args):
+    """Generate a new commit for the current or given patch."""
+
+    # Catch illegal argument combinations.
+    path_limiting = bool(args or options.update)
+    if options.index and path_limiting:
+        raise common.CmdException(
+            'Only full refresh is available with the --index option')
+
+    stack = directory.repository.current_stack
+    patch_name = get_patch(stack, options.patch)
+    paths = list_files(stack, patch_name, args, options.index, options.update)
+
+    # Make sure there are no conflicts in the files we want to
+    # refresh.
+    if stack.repository.default_index.conflicts() & paths:
+        raise common.CmdException(
+            'Cannot refresh -- resolve conflicts first')
+
+    # Commit index to temp patch, and absorb it into the target patch.
+    retval, temp_name = make_temp_patch(
+        stack, patch_name, paths, temp_index = path_limiting)
+    if retval != utils.STGIT_SUCCESS:
+        return retval
+    def edit_fun(cd):
+        cd, failed_diff = edit.auto_edit_patch(
+            stack.repository, cd, msg = options.message, contains_diff = False,
+            author = options.author, committer = lambda p: p,
+            sign_str = options.sign_str)
+        assert not failed_diff
+        if options.edit:
+            cd, failed_diff = edit.interactive_edit_patch(
+                stack.repository, cd, edit_diff = False,
+                diff_flags = [], replacement_diff = None)
+            assert not failed_diff
+        return cd
+    return absorb(stack, patch_name, temp_name, edit_fun)
index 1d7c43b..8a593ac 100644 (file)
@@ -16,24 +16,26 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'rename a patch in the series'
-usage = """%prog [options] [oldpatch] <newpatch>
-
-Rename <oldpatch> into <newpatch> in a series. If <oldpatch> is not given, the
-top-most patch will be renamed. """
-
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one')]
-
+from stgit import argparse, stack, git
+
+help = 'Rename a patch'
+kind = 'patch'
+usage = ['[options] [oldpatch] <newpatch>']
+description = """
+Rename <oldpatch> into <newpatch> in a series. If <oldpatch> is not
+given, the top-most patch will be renamed."""
+
+args = [argparse.applied_patches, argparse.unapplied_patches,
+        argparse.hidden_patches]
+options = [
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'use BRANCH instead of the default one')]
+
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Rename a patch in the series
index aa34792..37c4bab 100644 (file)
@@ -18,8 +18,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
@@ -27,15 +26,17 @@ from stgit.run import *
 from stgit import stack, git
 
 help = 'Fix StGit metadata if branch was modified with git commands'
-usage = """%prog [options]
-
+kind = 'stack'
+usage = ['']
+description = """
 If you modify an StGit stack (branch) with some git commands -- such
 as commit, pull, merge, and rebase -- you will leave the StGit
 metadata in an inconsistent state. In that situation, you have two
 options:
 
-  1. Use "git reset" or similar to undo the effect of the git
-     command(s).
+  1. Use "stg undo" to undo the effect of the git commands. (If you
+     know what you are doing and want more control, "git reset" or
+     similar will work too.)
 
   2. Use "stg repair". This will fix up the StGit metadata to
      accomodate the modifications to the branch. Specifically, it will
@@ -48,8 +49,8 @@ options:
        * However, merge commits cannot become patches; if you have
          committed a merge on top of your stack, "repair" will simply
          mark all patches below the merge unapplied, since they are no
-         longer reachable. If this is not what you want, use "git
-         reset" to get rid of the merge and run "stg repair" again.
+         longer reachable. If this is not what you want, use "stg
+         undo" to get rid of the merge and run "stg repair" again.
 
        * The applied patches are supposed to be precisely those that
          are reachable from the branch head. If you have used e.g.
@@ -68,9 +69,11 @@ NOTE: If using git commands on the stack was a mistake, running "stg
 repair" is _not_ what you want. In that case, what you want is option
 (1) above."""
 
-directory = DirectoryGotoToplevel()
+args = []
 options = []
 
+directory = DirectoryGotoToplevel(log = True)
+
 class Commit(object):
     def __init__(self, id):
         self.id = id
diff --git a/stgit/commands/reset.py b/stgit/commands/reset.py
new file mode 100644 (file)
index 0000000..7dfd4a0
--- /dev/null
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2008, Karl Hasselström <kha@treskal.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.argparse import opt
+from stgit.commands import common
+from stgit.lib import git, log, transaction
+from stgit.out import out
+from stgit import argparse
+
+help = 'Reset the patch stack to an earlier state'
+kind = 'stack'
+usage = ['[options] <state> [<patchnames>]']
+description = """
+Reset the patch stack to an earlier state. The state is specified with
+a commit id from a stack log; "stg log" lets you view this log, and
+"stg reset" lets you reset to any state you see in the log.
+
+If one or more patch names are given, reset only those patches, and
+leave the rest alone."""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
+options = [
+    opt('--hard', action = 'store_true',
+        short = 'Discard changes in your index/worktree')]
+
+directory = common.DirectoryHasRepositoryLib()
+
+def func(parser, options, args):
+    stack = directory.repository.current_stack
+    if len(args) >= 1:
+        ref, patches = args[0], args[1:]
+        state = log.get_log_entry(stack.repository, ref,
+                                  stack.repository.rev_parse(ref))
+    else:
+        raise common.CmdException('Wrong number of arguments')
+    trans = transaction.StackTransaction(stack, 'reset',
+                                         discard_changes = options.hard,
+                                         allow_bad_head = True)
+    try:
+        if patches:
+            log.reset_stack_partially(trans, stack.repository.default_iw,
+                                      state, patches)
+        else:
+            log.reset_stack(trans, stack.repository.default_iw, state)
+    except transaction.TransactionHalted:
+        pass
+    return trans.run(stack.repository.default_iw, allow_bad_head = not patches)
index 011db91..2ce7ec3 100644 (file)
@@ -17,32 +17,32 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git, basedir
+from stgit import argparse, stack, git, basedir
 from stgit.config import config, file_extensions
 from stgit.gitmergeonefile import interactive_merge
 
-
-help = 'mark a file conflict as solved'
-usage = """%prog [options] [<files...>]
-
+help = 'Mark a file conflict as solved'
+kind = 'wc'
+usage = ['[options] [<files...>]']
+description = """
 Mark a merge conflict as resolved. The conflicts can be seen with the
 'status' command, the corresponding files being prefixed with a
-'C'. This command also removes any <file>.{ancestor,current,patched}
-files."""
-
-directory = DirectoryHasRepository(needs_current_series = False)
-options = [make_option('-a', '--all',
-                       help = 'mark all conflicts as solved',
-                       action = 'store_true'),
-           make_option('-r', '--reset', metavar = '(ancestor|current|patched)',
-                       help = 'reset the file(s) to the given state'),
-           make_option('-i', '--interactive',
-                       help = 'run the interactive merging tool',
-                       action = 'store_true')]
+'C'."""
+
+args = [argparse.conflicting_files]
+options = [
+    opt('-a', '--all', action = 'store_true',
+        short = 'Mark all conflicts as solved'),
+    opt('-r', '--reset', metavar = '(ancestor|current|patched)',
+        args = [argparse.strings('ancestor', 'current', 'patched')],
+        short = 'Reset the file(s) to the given state'),
+    opt('-i', '--interactive', action = 'store_true',
+        short = 'Run the interactive merging tool')]
+
+directory = DirectoryHasRepository(needs_current_series = False, log = False)
 
 def func(parser, options, args):
     """Mark the conflict as resolved
@@ -77,18 +77,9 @@ def func(parser, options, args):
                 raise CmdException, 'No conflicts for "%s"' % filename
 
     # resolved
-    try:
+    if options.interactive:
         for filename in files:
-            if options.interactive:
-                interactive_merge(filename)
-            resolved(filename, options.reset)
-            del conflicts[conflicts.index(filename)]
-    finally:
-        # save or remove the conflicts file. Needs a finally clause to
-        # ensure that already solved conflicts are marked
-        if conflicts == []:
-            os.remove(os.path.join(basedir.get(), 'conflicts'))
-        else:
-            f = file(os.path.join(basedir.get(), 'conflicts'), 'w+')
-            f.writelines([line + '\n' for line in conflicts])
-            f.close()
+            interactive_merge(filename)
+            git.resolved([filename])
+    else:
+        git.resolved(files, options.reset)
diff --git a/stgit/commands/rm.py b/stgit/commands/rm.py
deleted file mode 100644 (file)
index 59d098b..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-
-__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 stgit.commands.common import *
-from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'remove files from the repository'
-usage = """%prog [options] <files...>
-
-Remove given files from the repository. The command doesn't remove the
-working copy of the file."""
-
-directory = DirectoryHasRepository()
-options = [make_option('-f', '--force',
-                       help = 'force removing even if the file exists',
-                       action = 'store_true')]
-
-
-def func(parser, options, args):
-    """Remove files from the repository
-    """
-    if len(args) < 1:
-        parser.error('incorrect number of arguments')
-
-    if not crt_series.get_current():
-        raise CmdException, 'No patches applied'
-
-    git.rm(args, options.force)
index e3467cc..5873801 100644 (file)
@@ -16,140 +16,134 @@ 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
-
-import stgit.commands.common
-from stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git
-
-
-help = 'print the patch series'
-usage = """%prog [options] [<patch-range>]
-
-Show all the patches in the series or just those in the given
-range. The applied patches are prefixed with a '+', the unapplied ones
-with a '-' and the hidden ones with a '!'. The current patch is
-prefixed with a '>'. Empty patches are prefixed with a '0'."""
-
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
-           make_option('-a', '--all',
-                       help = 'show all patches, including the hidden ones',
-                       action = 'store_true'),
-           make_option('-i', '--invisible',
-                       help = 'show the hidden patches only',
-                       action = 'store_true'),
-           make_option('-m', '--missing', metavar = 'BRANCH',
-                       help = 'show patches in BRANCH missing in current'),
-           make_option('-c', '--count',
-                       help = 'print the number of patches in the series',
-                       action = 'store_true'),
-           make_option('-d', '--description',
-                       help = 'show a short description for each patch',
-                       action = 'store_true'),
-           make_option('--author',
-                       help = 'show the author name for each patch',
-                       action = 'store_true'),
-           make_option('-e', '--empty',
-                       help = 'check whether patches are empty '
-                       '(much slower)',
-                       action = 'store_true'),
-           make_option('--showbranch',
-                       help = 'append the branch name to the listed patches',
-                       action = 'store_true'),
-           make_option('--noprefix',
-                       help = 'do not show the patch status prefix',
-                       action = 'store_true'),
-           make_option('-s', '--short',
-                       help = 'list just the patches around the topmost patch',
-                       action = 'store_true'),
-           make_option('-g', '--graphical',
-                       help = 'run gitk instead of printing',
-                       action = 'store_true')]
-
-
-def __get_description(patch):
+from stgit.argparse import opt
+from stgit.commands import common
+from stgit.commands.common import parse_patches
+from stgit.out import out
+from stgit.config import config
+from stgit import argparse
+
+help = 'Print the patch series'
+kind = 'stack'
+usage = ['[options] [<patch-range>]']
+description = """
+Show all the patches in the series, or just those in the given range,
+ordered from top to bottom.
+
+The applied patches are prefixed with a +++ (except the current patch,
+which is prefixed with a +>+), the unapplied patches with a +-+, and
+the hidden patches with a +!+.
+
+Empty patches are prefixed with a '0'."""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
+options = [
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch'),
+    opt('-a', '--all', action = 'store_true',
+        short = 'Show all patches, including the hidden ones'),
+    opt('-A', '--applied', action = 'store_true',
+        short = 'Show the applied patches only'),
+    opt('-U', '--unapplied', action = 'store_true',
+        short = 'Show the unapplied patches only'),
+    opt('-H', '--hidden', action = 'store_true',
+        short = 'Show the hidden patches only'),
+    opt('-m', '--missing', metavar = 'BRANCH',  args = [argparse.stg_branches],
+        short = 'Show patches in BRANCH missing in current'),
+    opt('-c', '--count', action = 'store_true',
+        short = 'Print the number of patches in the series'),
+    opt('-d', '--description', action = 'store_true',
+        short = 'Show a short description for each patch'),
+    opt('--author', action = 'store_true',
+        short = 'Show the author name for each patch'),
+    opt('-e', '--empty', action = 'store_true',
+        short = 'Check whether patches are empty', long = """
+        Before the +++, +>+, +-+, and +!+ prefixes, print a column
+        that contains either +0+ (for empty patches) or a space (for
+        non-empty patches)."""),
+    opt('--showbranch', action = 'store_true',
+        short = 'Append the branch name to the listed patches'),
+    opt('--noprefix', action = 'store_true',
+        short = 'Do not show the patch status prefix'),
+    opt('-s', '--short', action = 'store_true',
+        short = 'List just the patches around the topmost patch')]
+
+directory = common.DirectoryHasRepositoryLib()
+
+def __get_description(stack, patch):
     """Extract and return a patch's short description
     """
-    p = crt_series.get_patch(patch)
-    descr = (p.get_description() or '').strip()
+    cd = stack.patches.get(patch).commit.data
+    descr = cd.message.strip()
     descr_lines = descr.split('\n')
     return descr_lines[0].rstrip()
 
-def __get_author(patch):
+def __get_author(stack, patch):
     """Extract and return a patch's short description
     """
-    p = crt_series.get_patch(patch)
-    return p.get_authname();
+    cd = stack.patches.get(patch).commit.data
+    return cd.author.name
 
-def __print_patch(patch, branch_str, prefix, empty_prefix, length, options):
+def __print_patch(stack, patch, branch_str, prefix, length, options):
     """Print a patch name, description and various markers.
     """
     if options.noprefix:
         prefix = ''
-    elif options.empty and crt_series.empty_patch(patch):
-        prefix = empty_prefix
+    elif options.empty:
+        if stack.patches.get(patch).is_empty():
+            prefix = '0' + prefix
+        else:
+            prefix = ' ' + prefix
 
-    patch_str = patch + branch_str
+    patch_str = branch_str + patch
 
     if options.description or options.author:
         patch_str = patch_str.ljust(length)
 
     if options.description:
-        out.stdout(prefix + patch_str + ' # ' + __get_description(patch))
+        out.stdout(prefix + patch_str + ' # ' + __get_description(stack, patch))
     elif options.author:
-        out.stdout(prefix + patch_str + ' # ' + __get_author(patch))
+        out.stdout(prefix + patch_str + ' # ' + __get_author(stack, patch))
     else:
         out.stdout(prefix + patch_str)
 
 def func(parser, options, args):
     """Show the patch series
     """
-    global crt_series
-
     if options.all and options.short:
-        raise CmdException, 'combining --all and --short is meaningless'
-    
+        raise common.CmdException, 'combining --all and --short is meaningless'
+
+    stack = directory.repository.get_stack(options.branch)
+    if options.missing:
+        cmp_stack = stack
+        stack = directory.repository.get_stack(options.missing)
+
     # current series patches
-    if options.invisible:
-        applied = unapplied = []
-        hidden = crt_series.get_hidden()
+    applied = unapplied = hidden = ()
+    if options.applied or options.unapplied or options.hidden:
+        if options.all:
+            raise common.CmdException, \
+                '--all cannot be used with --applied/unapplied/hidden'
+        if options.applied:
+            applied = stack.patchorder.applied
+        if options.unapplied:
+            unapplied = stack.patchorder.unapplied
+        if options.hidden:
+            hidden = stack.patchorder.hidden
     elif options.all:
-        applied = crt_series.get_applied()
-        unapplied = crt_series.get_unapplied()
-        hidden = crt_series.get_hidden()
+        applied = stack.patchorder.applied
+        unapplied = stack.patchorder.unapplied
+        hidden = stack.patchorder.hidden
     else:
-        applied = crt_series.get_applied()
-        unapplied = crt_series.get_unapplied()
-        hidden = []
+        applied = stack.patchorder.applied
+        unapplied = stack.patchorder.unapplied
 
     if options.missing:
-        # switch the series, the one specified with --missing should
-        # become the current
-        cmp_series = crt_series
-        crt_series = stack.Series(options.missing)
-        stgit.commands.common.crt_series = crt_series
-
-        cmp_patches = applied + unapplied + hidden
-
-        # new current series patches
-        if options.invisible:
-            applied = unapplied = []
-            hidden = crt_series.get_hidden()
-        elif options.all:
-            applied = crt_series.get_applied()
-            unapplied = crt_series.get_unapplied()
-            hidden = crt_series.get_hidden()
-        else:
-            applied = crt_series.get_applied()
-            unapplied = crt_series.get_unapplied()
-            hidden = []
+        cmp_patches = cmp_stack.patchorder.all
     else:
-        cmp_patches = []
+        cmp_patches = ()
 
     # the filtering range covers the whole series
     if args:
@@ -186,38 +180,22 @@ def func(parser, options, args):
         return
 
     if options.showbranch:
-        branch_str = '@' + crt_series.get_name()
+        branch_str = stack.name + ':'
     else:
         branch_str = ''
 
-    if options.graphical:
-        if options.missing:
-            raise CmdException, '--graphical not supported with --missing'
-
-        gitk_args = []
-        if applied:
-            gitk_args.append('%s^..%s'
-                             % (git_id(crt_series, applied[0]),
-                                git_id(crt_series, applied[-1])))
-        for p in unapplied:
-            patch_id = git_id(crt_series, p)
-            gitk_args.append('%s^..%s' % (patch_id, patch_id))
-
-        # discard the exit codes generated by SIGINT, SIGKILL, SIGTERM
-        Run('gitk', *gitk_args).returns([0, -2, -9, -15]).run()
-    else:
-        max_len = 0
-        if len(patches) > 0:
-            max_len = max([len(i + branch_str) for i in patches])
+    max_len = 0
+    if len(patches) > 0:
+        max_len = max([len(i + branch_str) for i in patches])
 
-        if applied:
-            for p in applied[:-1]:
-                __print_patch(p, branch_str, '+ ', '0 ', max_len, options)
-            __print_patch(applied[-1], branch_str, '> ', '0>', max_len,
-                          options)
+    if applied:
+        for p in applied[:-1]:
+            __print_patch(stack, p, branch_str, '+ ', max_len, options)
+        __print_patch(stack, applied[-1], branch_str, '> ', max_len,
+                      options)
 
-        for p in unapplied:
-            __print_patch(p, branch_str, '- ', '0 ', max_len, options)
+    for p in unapplied:
+        __print_patch(stack, p, branch_str, '- ', max_len, options)
 
-        for p in hidden:
-            __print_patch(p, branch_str, '! ', '! ', max_len, options)
+    for p in hidden:
+        __print_patch(stack, p, branch_str, '! ', max_len, options)
index 72d1be3..895943a 100644 (file)
@@ -16,32 +16,31 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
 from pydoc import pager
-
+from stgit.argparse import opt
 from stgit.commands.common import *
-from stgit import git
-
-
-help = 'show the commit corresponding to a patch (or the current patch)'
-usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
+from stgit import argparse, git
 
-Show the commit log and the diff corresponding to the given
-patches. The output is similar to that generated by the 'git show'
-command."""
+help = 'Show the commit corresponding to a patch'
+kind = 'patch'
+usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
+description = """
+Show the commit log and the diff corresponding to the given patches.
+The output is similar to that generated by 'git show'."""
 
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
-           make_option('-a', '--applied',
-                       help = 'show the applied patches',
-                       action = 'store_true'),
-           make_option('-u', '--unapplied',
-                       help = 'show the unapplied patches',
-                       action = 'store_true'),
-           make_option('-O', '--show-opts',
-                       help = 'options to pass to "git show"')]
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
+options = [
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch'),
+    opt('-a', '--applied', action = 'store_true',
+        short = 'Show the applied patches'),
+    opt('-u', '--unapplied', action = 'store_true',
+        short = 'Show the unapplied patches'),
+    ] + argparse.diff_opts_option()
 
+directory = DirectoryHasRepository(log = False)
 
 def func(parser, options, args):
     """Show commit log and diff
@@ -52,23 +51,19 @@ def func(parser, options, args):
         patches = crt_series.get_unapplied()
     elif len(args) == 0:
         patches = ['HEAD']
+    elif '..' in ' '.join(args):
+        # patch ranges
+        applied = crt_series.get_applied()
+        unapplied = crt_series.get_unapplied()
+        patches = parse_patches(args, applied + unapplied + \
+                                crt_series.get_hidden(), len(applied))
     else:
-        if len(args) == 1 and args[0].find('..') == -1:
-            # single patch or commit id
-            patches = args
-        else:
-            applied = crt_series.get_applied()
-            unapplied = crt_series.get_unapplied()
-            patches = parse_patches(args, applied + unapplied + \
-                                    crt_series.get_hidden(), len(applied))
-
-    if options.show_opts:
-        show_flags = options.show_opts.split()
-    else:
-        show_flags = []
+        # individual patches or commit ids
+        patches = args
 
     commit_ids = [git_id(crt_series, patch) for patch in patches]
-    commit_str = '\n'.join([git.pretty_commit(commit_id, flags = show_flags)
+    commit_str = '\n'.join([git.pretty_commit(commit_id,
+                                              flags = options.diff_flags)
                             for commit_id in commit_ids])
     if commit_str:
         pager(commit_str)
index 7e5c955..d4561ed 100644 (file)
@@ -17,27 +17,43 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'send patches deeper down the stack'
-usage = """%prog [-t <target patch>] [-n] [<patches>]
-
-Pop all patches (or all patches including <target patch>), then
-push the specified <patches> (the current patch by default), and
-then push back into place the formerly-applied patches (unless -n
-is also given)."""
-
-directory = DirectoryGotoToplevel()
-options = [make_option('-n', '--nopush',
-                       help = 'do not push the patches back after sinking',
-                       action = 'store_true'),
-           make_option('-t', '--to', metavar = 'TARGET',
-                       help = 'sink patches below TARGET patch')]
+from stgit import argparse, stack, git
+
+help = 'Send patches deeper down the stack'
+kind = 'stack'
+usage = ['[-t <target patch>] [-n] [<patches>]']
+description = """
+This is the opposite operation of stglink:float[]: move the specified
+patches down the stack.  It is for example useful to group stable
+patches near the bottom of the stack, where they are less likely to be
+impacted by the push of another patch, and from where they can be more
+easily committed or pushed.
+
+If no patch is specified on command-line, the current patch gets sunk.
+By default patches are sunk to the bottom of the stack, but the '--to'
+option allows to place them under any applied patch.
+
+Sinking internally involves popping all patches (or all patches
+including <target patch>), then pushing the patches to sink, and then
+(unless '--nopush' is also given) pushing back into place the
+formerly-applied patches."""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
+options = [
+    opt('-n', '--nopush', action = 'store_true',
+        short = 'Do not push the patches back after sinking', long = """
+        Do not push back on the stack the formerly-applied patches.
+        Only the patches to sink are pushed."""),
+    opt('-t', '--to', metavar = 'TARGET', args = [argparse.applied_patches],
+        short = 'Sink patches below the TARGET patch', long = """
+        Specify a target patch to place the patches below, instead of
+        sinking them to the bottom of the stack.""")]
+
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Sink patches down the stack.
index 94d0b57..730b47c 100644 (file)
@@ -17,16 +17,15 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'show the tree status'
-usage = """%prog [options] [<files or dirs>]
+from stgit import argparse, stack, git
 
+help = 'Show the tree status'
+kind = 'wc'
+usage = ['[options] [<files or dirs>]']
+description = """
 Show the status of the whole working copy or the given files. The
 command also shows the files in the current directory which are not
 under revision control. The files are prefixed as follows:
@@ -37,35 +36,29 @@ under revision control. The files are prefixed as follows:
   C - conflict
   ? - unknown
 
-A 'refresh' command clears the status of the modified, new and deleted
-files."""
-
-directory = DirectoryHasRepository(needs_current_series = False)
-options = [make_option('-m', '--modified',
-                       help = 'show modified files only',
-                       action = 'store_true'),
-           make_option('-n', '--new',
-                       help = 'show new files only',
-                       action = 'store_true'),
-           make_option('-d', '--deleted',
-                       help = 'show deleted files only',
-                       action = 'store_true'),
-           make_option('-c', '--conflict',
-                       help = 'show conflict files only',
-                       action = 'store_true'),
-           make_option('-u', '--unknown',
-                       help = 'show unknown files only',
-                       action = 'store_true'),
-           make_option('-x', '--noexclude',
-                       help = 'do not exclude any files from listing',
-                       action = 'store_true'),
-           make_option('--reset',
-                       help = 'reset the current tree changes',
-                       action = 'store_true')]
-
-
-def status(files = None, modified = False, new = False, deleted = False,
-           conflict = False, unknown = False, noexclude = False):
+An 'stg refresh' command clears the status of the modified, new and
+deleted files."""
+
+args = [argparse.files]
+options = [
+    opt('-m', '--modified', action = 'store_true',
+        short = 'Show modified files only'),
+    opt('-n', '--new', action = 'store_true',
+        short = 'Show new files only'),
+    opt('-d', '--deleted', action = 'store_true',
+        short = 'Show deleted files only'),
+    opt('-c', '--conflict', action = 'store_true',
+        short = 'Show conflict files only'),
+    opt('-u', '--unknown', action = 'store_true',
+        short = 'Show unknown files only'),
+    opt('-x', '--noexclude', action = 'store_true',
+        short = 'Do not exclude any files from listing'),
+    opt('--reset', action = 'store_true',
+        short = 'Reset the current tree changes')]
+
+directory = DirectoryHasRepository(needs_current_series = False, log = False)
+
+def status(files, modified, new, deleted, conflict, unknown, noexclude):
     """Show the tree status
     """
     cache_files = git.tree_status(files,
@@ -104,9 +97,10 @@ def func(parser, options, args):
     directory.cd_to_topdir()
 
     if options.reset:
+        directory.log = True
         if args:
-            for f in args:
-                resolved(f)
+            conflicts = git.get_conflicts()
+            git.resolved([fn for fn in args if fn in conflicts])
             git.reset(args)
         else:
             resolved_all()
index a04ff82..ea949d6 100644 (file)
@@ -16,37 +16,34 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
 import stgit.commands.common
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'synchronise patches with a branch or a series'
-usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
+from stgit import argparse, stack, git
 
+help = 'Synchronise patches with a branch or a series'
+kind = 'patch'
+usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
+description = """
 For each of the specified patches perform a three-way merge with the
 same patch in the specified branch or series. The command can be used
 for keeping patches on several branches in sync. Note that the
 operation may fail for some patches because of conflicts. The patches
-in the series must apply cleanly.
-
-The sync operation can be reverted for individual patches with --undo."""
-
-directory = DirectoryGotoToplevel()
-options = [make_option('-a', '--all',
-                       help = 'synchronise all the applied patches',
-                       action = 'store_true'),
-           make_option('-B', '--ref-branch',
-                       help = 'syncronise patches with BRANCH'),
-           make_option('-s', '--series',
-                       help = 'syncronise patches with SERIES'),
-           make_option('--undo',
-                       help = 'undo the synchronisation of the current patch',
-                       action = 'store_true')]
+in the series must apply cleanly."""
+
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
+options = [
+    opt('-a', '--all', action = 'store_true',
+        short = 'Synchronise all the applied patches'),
+    opt('-B', '--ref-branch', args = [argparse.stg_branches],
+        short = 'Syncronise patches with BRANCH'),
+    opt('-s', '--series', args = [argparse.files],
+        short = 'Syncronise patches with SERIES')]
+
+directory = DirectoryGotoToplevel(log = True)
 
 def __check_all():
     check_local_changes()
@@ -57,7 +54,7 @@ def __branch_merge_patch(remote_series, pname):
     """Merge a patch from a remote branch into the current tree.
     """
     patch = remote_series.get_patch(pname)
-    git.merge(patch.get_bottom(), git.get_head(), patch.get_top())
+    git.merge_recursive(patch.get_bottom(), git.get_head(), patch.get_top())
 
 def __series_merge_patch(base, patchdir, pname):
     """Merge a patch file with the given StGIT patch.
@@ -68,18 +65,6 @@ def __series_merge_patch(base, patchdir, pname):
 def func(parser, options, args):
     """Synchronise a range of patches
     """
-    if options.undo:
-        if options.ref_branch or options.series:
-            raise CmdException, \
-                  '--undo cannot be specified with --ref-branch or --series'
-        __check_all()
-
-        out.start('Undoing the sync of "%s"' % crt_series.get_current())
-        crt_series.undo_refresh()
-        git.reset()
-        out.done()
-        return
-
     if options.ref_branch:
         remote_series = stack.Series(options.ref_branch)
         if options.ref_branch == crt_series.get_name():
@@ -157,9 +142,7 @@ def func(parser, options, args):
         bottom = patch.get_bottom()
         top = patch.get_top()
 
-        # reset the patch backup information. That's needed in case we
-        # undo the sync but there were no changes made
-        patch.set_bottom(bottom, backup = True)
+        # reset the patch backup information.
         patch.set_top(top, backup = True)
 
         # the actual merging (either from a branch or an external file)
index e7cb275..4ec37b4 100644 (file)
@@ -16,24 +16,23 @@ 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 stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git
-
-
-help = 'print the name of the top patch'
-usage = """%prog [options]
-
+from stgit.argparse import opt
+from stgit.commands import common
+from stgit.out import out
+from stgit import argparse
+
+help = 'Print the name of the top patch'
+kind = 'stack'
+usage = ['']
+description = """
 Print the name of the current (topmost) patch."""
 
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one')]
+args = []
+options = [
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch')]
 
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
     """Show the name of the topmost patch
@@ -41,8 +40,10 @@ def func(parser, options, args):
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    name = crt_series.get_current()
-    if name:
-        out.stdout(name)
+    stack = directory.repository.get_stack(options.branch)
+    applied = stack.patchorder.applied
+
+    if applied:
+        out.stdout(applied[-1])
     else:
-        raise CmdException, 'No patches applied'
+        raise common.CmdException, 'No patches applied'
diff --git a/stgit/commands/unapplied.py b/stgit/commands/unapplied.py
deleted file mode 100644 (file)
index d5bb43e..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-
-__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 stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git
-
-
-help = 'print the unapplied patches'
-usage = """%prog [options]
-
-List the patches from the series which are not pushed onto the stack.
-They are listed in the reverse order in which they were popped."""
-
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
-           make_option('-c', '--count',
-                       help = 'print the number of unapplied patches',
-                       action = 'store_true')]
-
-
-def func(parser, options, args):
-    """Show the unapplied patches
-    """
-    if len(args) != 0:
-        parser.error('incorrect number of arguments')
-
-    unapplied = crt_series.get_unapplied()
-
-    if options.count:
-        out.stdout(len(unapplied))
-    else:
-        for p in unapplied:
-            out.stdout(p)
index ba3448f..bcc8bac 100644 (file)
@@ -17,20 +17,21 @@ 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 stgit.commands.common import *
-from stgit.utils import *
+from stgit.argparse import opt
+from stgit.commands import common
+from stgit.lib import transaction
 from stgit.out import *
-from stgit import stack, git
-
-help = 'turn regular GIT commits into StGIT patches'
-usage = """%prog [<patchnames>] | -n NUM [<prefix>]] | -t <committish> [-x]
-
+from stgit import argparse, utils
+
+help = 'Turn regular git commits into StGit patches'
+kind = 'stack'
+usage = ['<patch-name-1> [<patch-name-2> ...]',
+         '-n NUM [<prefix>]',
+         '-t <committish> [-x]']
+description = """
 Take one or more git commits at the base of the current stack and turn
 them into StGIT patches. The new patches are created as applied patches
-at the bottom of the stack. This is the exact opposite of 'stg commit'.
+at the bottom of the stack. This is the opposite of 'stg commit'.
 
 By default, the number of patches to uncommit is determined by the
 number of patch names provided on the command line. First name is used
@@ -48,31 +49,32 @@ given commit should be uncommitted.
 Only commits with exactly one parent can be uncommitted; in other
 words, you can't uncommit a merge."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-n', '--number', type = 'int',
-                       help = 'uncommit the specified number of commits'),
-           make_option('-t', '--to',
-                       help = 'uncommit to the specified commit'),
-           make_option('-x', '--exclusive',
-                       help = 'exclude the commit specified by the --to option',
-                       action = 'store_true')]
+args = []
+options = [
+    opt('-n', '--number', type = 'int',
+        short = 'Uncommit the specified number of commits'),
+    opt('-t', '--to', args = [argparse.commit],
+        short = 'Uncommit to the specified commit'),
+    opt('-x', '--exclusive', action = 'store_true',
+        short = 'Exclude the commit specified by the --to option')]
+
+directory = common.DirectoryHasRepositoryLib()
 
 def func(parser, options, args):
     """Uncommit a number of patches.
     """
+    stack = directory.repository.current_stack
     if options.to:
         if options.number:
             parser.error('cannot give both --to and --number')
         if len(args) != 0:
             parser.error('cannot specify patch name with --to')
         patch_nr = patchnames = None
-        to_commit = git_id(crt_series, options.to)
+        to_commit = stack.repository.rev_parse(options.to)
     elif options.number:
         if options.number <= 0:
             parser.error('invalid value passed to --number')
-
         patch_nr = options.number
-
         if len(args) == 0:
             patchnames = None
         elif len(args) == 1:
@@ -88,53 +90,57 @@ def func(parser, options, args):
         patchnames = args
         patch_nr = len(patchnames)
 
-    if crt_series.get_protected():
-        raise CmdException, \
-              'This branch is protected. Uncommit is not permitted'
-
-    def get_commit(commit_id):
-        commit = git.Commit(commit_id)
+    def get_parent(c):
+        next = c.data.parents
         try:
-            parent, = commit.get_parents()
+            [next] = next
         except ValueError:
-            raise CmdException('Commit %s does not have exactly one parent'
-                               % commit_id)
-        return (commit, commit_id, parent)
+            raise common.CmdException(
+                'Trying to uncommit %s, which does not have exactly one parent'
+                % c.sha1)
+        return next
 
     commits = []
-    next_commit = crt_series.get_base()
+    next_commit = stack.base
     if patch_nr:
         out.start('Uncommitting %d patches' % patch_nr)
         for i in xrange(patch_nr):
-            commit, commit_id, parent = get_commit(next_commit)
-            commits.append((commit, commit_id, parent))
-            next_commit = parent
+            commits.append(next_commit)
+            next_commit = get_parent(next_commit)
     else:
         if options.exclusive:
-            out.start('Uncommitting to %s (exclusive)' % to_commit)
+            out.start('Uncommitting to %s (exclusive)' % to_commit.sha1)
         else:
-            out.start('Uncommitting to %s' % to_commit)
+            out.start('Uncommitting to %s' % to_commit.sha1)
         while True:
-            commit, commit_id, parent = get_commit(next_commit)
-            if commit_id == to_commit:
+            if next_commit == to_commit:
                 if not options.exclusive:
-                    commits.append((commit, commit_id, parent))
+                    commits.append(next_commit)
                 break
-            commits.append((commit, commit_id, parent))
-            next_commit = parent
+            commits.append(next_commit)
+            next_commit = get_parent(next_commit)
         patch_nr = len(commits)
 
-    for (commit, commit_id, parent), patchname in \
-        zip(commits, patchnames or [None for i in xrange(len(commits))]):
-        author_name, author_email, author_date = \
-                     name_email_date(commit.get_author())
-        crt_series.new_patch(patchname,
-                             can_edit = False, before_existing = True,
-                             commit = False,
-                             top = commit_id, bottom = parent,
-                             message = commit.get_log(),
-                             author_name = author_name,
-                             author_email = author_email,
-                             author_date = author_date)
-
+    taken_names = set(stack.patchorder.all)
+    if patchnames:
+        for pn in patchnames:
+            if pn in taken_names:
+                raise common.CmdException('Patch name "%s" already taken' % pn)
+            taken_names.add(pn)
+    else:
+        patchnames = []
+        for c in reversed(commits):
+            pn = utils.make_patch_name(c.data.message,
+                                       lambda pn: pn in taken_names)
+            patchnames.append(pn)
+            taken_names.add(pn)
+        patchnames.reverse()
+
+    trans = transaction.StackTransaction(stack, 'uncommit',
+                                         allow_conflicts = True,
+                                         allow_bad_head = True)
+    for commit, pn in zip(commits, patchnames):
+        trans.patches[pn] = commit
+    trans.applied = list(reversed(patchnames)) + trans.applied
+    trans.run(set_head = False)
     out.done()
diff --git a/stgit/commands/undo.py b/stgit/commands/undo.py
new file mode 100644 (file)
index 0000000..6a04363
--- /dev/null
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2008, Karl Hasselström <kha@treskal.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.argparse import opt
+from stgit.commands import common
+from stgit.lib import git, log, transaction
+from stgit.out import out
+
+help = 'Undo the last operation'
+kind = 'stack'
+usage = ['']
+description = """
+Reset the patch stack to the previous state. Consecutive invocations
+of "stg undo" will take you ever further into the past."""
+
+args = []
+options = [
+    opt('-n', '--number', type = 'int', metavar = 'N', default = 1,
+        short = 'Undo the last N commands'),
+    opt('--hard', action = 'store_true',
+        short = 'Discard changes in your index/worktree')]
+
+directory = common.DirectoryHasRepositoryLib()
+
+def func(parser, options, args):
+    stack = directory.repository.current_stack
+    if options.number < 1:
+        raise common.CmdException('Bad number of commands to undo')
+    state = log.undo_state(stack, options.number)
+    trans = transaction.StackTransaction(stack, 'undo %d' % options.number,
+                                         discard_changes = options.hard,
+                                         allow_bad_head = True)
+    try:
+        log.reset_stack(trans, stack.repository.default_iw, state)
+    except transaction.TransactionHalted:
+        pass
+    return trans.run(stack.repository.default_iw, allow_bad_head = True)
index 665d664..0c0832a 100644 (file)
@@ -16,23 +16,25 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os
-from optparse import OptionParser, make_option
-
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
-
-help = 'unhide a hidden patch in the series'
-usage = """%prog [options] <patch-range>
+from stgit import argparse, stack, git
 
+help = 'Unhide a hidden patch'
+kind = 'stack'
+usage = ['[options] <patch-range>']
+description = """
 Unhide a hidden range of patches so that they are shown in the plain
-'series' command output."""
+'stg series' command output."""
+
+args = [argparse.patch_range(argparse.hidden_patches)]
+options = [
+    opt('-b', '--branch', args = [argparse.stg_branches],
+        short = 'Use BRANCH instead of the default branch')]
 
-directory = DirectoryHasRepository()
-options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one')]
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Unhide a range of patches in the series
diff --git a/stgit/completion.py b/stgit/completion.py
new file mode 100644 (file)
index 0000000..38f0670
--- /dev/null
@@ -0,0 +1,150 @@
+import textwrap
+import stgit.commands
+from stgit import argparse
+import itertools
+
+def fun(name, *body):
+    return ['%s ()' % name, '{', list(body), '}']
+
+def fun_desc(name, desc, *body):
+    return ['# %s' % desc] + fun(name, *body)
+
+def flatten(stuff, sep):
+    r = stuff[0]
+    for s in stuff[1:]:
+        r.append(sep)
+        r.extend(s)
+    return r
+
+def write(f, stuff, indent = 0):
+    for s in stuff:
+        if isinstance(s, str):
+            f.write((' '*4*indent + s).rstrip() + '\n')
+        else:
+            write(f, s, indent + 1)
+
+def patch_list_fun(type):
+    return fun('_%s_patches' % type, 'local g=$(_gitdir)',
+               'test "$g" && cat "$g/patches/$(_current_branch)/%s"' % type)
+
+def file_list_fun(name, cmd):
+    return fun('_%s_files' % name, 'local g=$(_gitdir)',
+               'test "$g" && %s' % cmd)
+
+def ref_list_fun(name, prefix):
+    return fun(name, 'local g=$(_gitdir)',
+               ("test \"$g\" && git show-ref | grep ' %s/' | sed 's,.* %s/,,'"
+                % (prefix, prefix)))
+
+def util():
+    r = [fun_desc('_gitdir',
+                  "The path to .git, or empty if we're not in a repository.",
+                  'echo "$(git rev-parse --git-dir 2>/dev/null)"'),
+         fun_desc('_current_branch',
+                  "Name of the current branch, or empty if there isn't one.",
+                  'local b=$(git symbolic-ref HEAD 2>/dev/null)',
+                  'echo ${b#refs/heads/}'),
+         fun_desc('_other_applied_patches',
+                  'List of all applied patches except the current patch.',
+                  'local b=$(_current_branch)',
+                  'local g=$(_gitdir)',
+                  ('test "$g" && cat "$g/patches/$b/applied" | grep -v'
+                   ' "^$(tail -n 1 $g/patches/$b/applied 2> /dev/null)$"')),
+         fun('_patch_range', 'local patches="$1"', 'local cur="$2"',
+             'case "$cur" in', [
+                '*..*)', ['local pfx="${cur%..*}.."', 'cur="${cur#*..}"',
+                          'compgen -P "$pfx" -W "$patches" -- "$cur"', ';;'],
+                '*)', ['compgen -W "$patches" -- "$cur"', ';;']],
+             'esac'),
+         fun('_stg_branches',
+             'local g=$(_gitdir)', 'test "$g" && (cd $g/patches/ && echo *)'),
+         ref_list_fun('_all_branches', 'refs/heads'),
+         ref_list_fun('_tags', 'refs/tags'),
+         ref_list_fun('_remotes', 'refs/remotes')]
+    for type in ['applied', 'unapplied', 'hidden']:
+        r.append(patch_list_fun(type))
+    for name, cmd in [('conflicting',
+                       r"git ls-files --unmerged | sed 's/.*\t//g' | sort -u"),
+                      ('dirty', 'git diff-index --name-only HEAD'),
+                      ('unknown', 'git ls-files --others --exclude-standard'),
+                      ('known', 'git ls-files')]:
+        r.append(file_list_fun(name, cmd))
+    return flatten(r, '')
+
+def command_list(commands):
+    return ['_stg_commands="%s"\n' % ' '.join(sorted(commands.iterkeys()))]
+
+def command_fun(cmd, modname):
+    mod = stgit.commands.get_command(modname)
+    def cg(args, flags):
+        return argparse.compjoin(list(args) + [argparse.strings(*flags)]
+                                 ).command('$cur')
+    return fun(
+        '_stg_%s' % cmd,
+        'local flags="%s"' % ' '.join(sorted(
+                itertools.chain(
+                    ('--help',),
+                    (flag for opt in mod.options
+                     for flag in opt.flags if flag.startswith('--'))))),
+        'local prev="${COMP_WORDS[COMP_CWORD-1]}"',
+        'local cur="${COMP_WORDS[COMP_CWORD]}"',
+        'case "$prev" in', [
+            '%s) COMPREPLY=($(%s)) ;;' % ('|'.join(opt.flags), cg(opt.args, []))
+            for opt in mod.options if opt.args] + [
+            '*) COMPREPLY=($(%s)) ;;' % cg(mod.args, ['$flags'])],
+        'esac')
+
+def main_switch(commands):
+    return fun(
+        '_stg',
+        'local i',
+        'local c=1',
+        'local command',
+        '',
+        'while test $c -lt $COMP_CWORD; do', [
+            'if test $c == 1; then', [
+                'command="${COMP_WORDS[c]}"'],
+            'fi',
+            'c=$((++c))'],
+        'done',
+        '',
+        ('# Complete name of subcommand if the user has not finished'
+         ' typing it yet.'),
+        'if test $c -eq $COMP_CWORD -a -z "$command"; then', [
+            ('COMPREPLY=($(compgen -W "help version copyright $_stg_commands" --'
+             ' "${COMP_WORDS[COMP_CWORD]}"))'),
+            'return'],
+        'fi',
+        '',
+        '# Complete arguments to subcommands.',
+        'case "$command" in', [
+            'help) ', [
+            ('COMPREPLY=($(compgen -W "$_stg_commands" --'
+             ' "${COMP_WORDS[COMP_CWORD]}"))'),
+            'return ;;'],
+            'version) return ;;',
+            'copyright) return ;;'], [
+            '%s) _stg_%s ;;' % (cmd, cmd)
+            for cmd in sorted(commands.iterkeys())],
+        'esac')
+
+def install():
+    return ['complete -o bashdefault -o default -F _stg stg 2>/dev/null \\', [
+            '|| complete -o default -F _stg stg' ] ]
+
+def write_completion(f):
+    commands = stgit.commands.get_commands(allow_cached = False)
+    r = [["""# -*- shell-script -*-
+# bash completion script for StGit (automatically generated)
+#
+# To use these routines:
+#
+#    1. Copy this file to somewhere (e.g. ~/.stgit-completion.bash).
+#
+#    2. Add the following line to your .bashrc:
+#         . ~/.stgit-completion.bash"""]]
+    r += [util(), command_list(commands)]
+    for cmd, (modname, _, _) in sorted(commands.iteritems()):
+        r.append(command_fun(cmd, modname))
+    r += [main_switch(commands), install()]
+    write(f, flatten(r, ''))
index 5b47580..05ef624 100644 (file)
@@ -34,8 +34,6 @@ class GitConfig:
         'stgit.pullcmd':       'git pull',
         'stgit.fetchcmd':      'git fetch',
         'stgit.pull-policy':   'pull',
-        'stgit.merger':                'diff3 -L current -L ancestor -L patched -m -E ' \
-                               '"%(branch1)s" "%(ancestor)s" "%(branch2)s" > "%(output)s"',
         'stgit.autoimerge':    'no',
         'stgit.keeporig':      'yes',
         'stgit.keepoptimized': 'no',
index ea94ecc..4d01fc2 100644 (file)
@@ -43,7 +43,6 @@ class GRun(Run):
         """
         Run.__init__(self, 'git', *cmd)
 
-
 #
 # Classes
 #
@@ -154,14 +153,12 @@ def get_commit(id_hash):
 def get_conflicts():
     """Return the list of file conflicts
     """
-    conflicts_file = os.path.join(basedir.get(), '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
+    names = set()
+    for line in GRun('ls-files', '-z', '--unmerged'
+                     ).raw_output().split('\0')[:-1]:
+        stat, path = line.split('\t', 1)
+        names.add(path)
+    return list(names)
 
 def exclude_files():
     files = [os.path.join(basedir.get(), 'info', 'exclude')]
@@ -185,11 +182,13 @@ def ls_files(files, tree = 'HEAD', full_name = True):
     args.append('--')
     args.extend(files)
     try:
-        return GRun('ls-files', '--error-unmatch', *args).output_lines()
+        # use a set to avoid file names duplication due to different stages
+        fileset = set(GRun('ls-files', '--error-unmatch', *args).output_lines())
     except GitRunException:
         # just hide the details of the 'git ls-files' command we use
         raise GitException, \
             'Some of the given paths are either missing or not known to GIT'
+    return list(fileset)
 
 def parse_git_ls(output):
     """Parse the output of git diff-index, diff-files, etc. Doesn't handle
@@ -244,8 +243,6 @@ def tree_status(files = None, tree_id = 'HEAD', unknown = False,
 
     # conflicted files
     conflicts = get_conflicts()
-    if not conflicts:
-        conflicts = []
     cache_files += [('C', filename) for filename in conflicts
                     if not files or filename in files]
     reported_files = set(conflicts)
@@ -475,109 +472,6 @@ def rename_branch(from_name, to_name):
            and os.path.exists(os.path.join(reflog_dir, from_name)):
         rename(reflog_dir, from_name, to_name)
 
-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
-
-    if files:
-        try:
-            GRun('update-index', '--add', '--').xargs(files)
-        except GitRunException:
-            raise GitException, 'Unable to add file'
-
-def __copy_single(source, target, target2=''):
-    """Copy file or dir named 'source' to name target+target2"""
-
-    # "source" (file or dir) must match one or more git-controlled file
-    realfiles = GRun('ls-files', source).output_lines()
-    if len(realfiles) == 0:
-        raise GitException, '"%s" matches no git-controled files' % source
-
-    if os.path.isdir(source):
-        # physically copy the files, and record them to add them in one run
-        newfiles = []
-        re_string='^'+source+'/(.*)$'
-        prefix_regexp = re.compile(re_string)
-        for f in [f.strip() for f in realfiles]:
-            m = prefix_regexp.match(f)
-            if not m:
-                raise Exception, '"%s" does not match "%s"' % (f, re_string)
-            newname = target+target2+'/'+m.group(1)
-            if not os.path.exists(os.path.dirname(newname)):
-                os.makedirs(os.path.dirname(newname))
-            copyfile(f, newname)
-            newfiles.append(newname)
-
-        add(newfiles)
-    else: # files, symlinks, ...
-        newname = target+target2
-        copyfile(source, newname)
-        add([newname])
-
-
-def copy(filespecs, target):
-    if os.path.isdir(target):
-        # target is a directory: copy each entry on the command line,
-        # with the same name, into the target
-        target = target.rstrip('/')
-        
-        # first, check that none of the children of the target
-        # matching the command line aleady exist
-        for filespec in filespecs:
-            entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
-            if os.path.exists(entry):
-                raise GitException, 'Target "%s" already exists' % entry
-        
-        for filespec in filespecs:
-            filespec = filespec.rstrip('/')
-            basename = '/' + os.path.basename(filespec)
-            __copy_single(filespec, target, basename)
-
-    elif os.path.exists(target):
-        raise GitException, 'Target "%s" exists but is not a directory' % target
-    elif len(filespecs) != 1:
-        raise GitException, 'Cannot copy more than one file to non-directory'
-
-    else:
-        # at this point: len(filespecs)==1 and target does not exist
-
-        # check target directory
-        targetdir = os.path.dirname(target)
-        if targetdir != '' and not os.path.isdir(targetdir):
-            raise GitException, 'Target directory "%s" does not exist' % targetdir
-
-        __copy_single(filespecs[0].rstrip('/'), target)
-        
-
-def rm(files, force = False):
-    """Remove a file from the repository
-    """
-    if not force:
-        for f in files:
-            if os.path.exists(f):
-                raise GitException, '%s exists. Remove it first' %f
-        if files:
-            GRun('update-index', '--remove', '--').xargs(files)
-    else:
-        if files:
-            GRun('update-index', '--force-remove', '--').xargs(files)
-
 # Persons caching
 __user = None
 __author = None
@@ -724,77 +618,33 @@ def apply_diff(rev1, rev2, check_index = True, files = None):
 
     return True
 
-def merge(base, head1, head2, recursive = False):
+stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
+
+def merge_recursive(base, head1, head2):
     """Perform a 3-way merge between base, head1 and head2 into the
     local tree
     """
     refresh_index()
-
-    err_output = None
-    if recursive:
-        # this operation tracks renames but it is slower (used in
-        # general when pushing or picking patches)
-        try:
-            # discard output to mask the verbose prints of the tool
-            GRun('merge-recursive', base, '--', head1, head2
-                 ).discard_output()
-        except GitRunException, ex:
-            err_output = str(ex)
-            pass
-    else:
-        # the fast case where we don't track renames (used when the
-        # distance between base and heads is small, i.e. folding or
-        # synchronising patches)
-        try:
-            GRun('read-tree', '-u', '-m', '--aggressive',
-                 base, head1, head2).run()
-        except GitRunException:
-            raise GitException, 'read-tree failed (local changes maybe?)'
-
-    # check the index for unmerged entries
-    files = {}
-    stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
-
-    for line in GRun('ls-files', '--unmerged', '--stage', '-z'
-                     ).raw_output().split('\0'):
-        if not line:
-            continue
-
-        mode, hash, stage, path = stages_re.findall(line)[0]
-
-        if not path in files:
-            files[path] = {}
-            files[path]['1'] = ('', '')
-            files[path]['2'] = ('', '')
-            files[path]['3'] = ('', '')
-
-        files[path][stage] = (mode, hash)
-
-    if err_output and not files:
-        # if no unmerged files, there was probably a different type of
-        # error and we have to abort the merge
-        raise GitException, err_output
-
-    # merge the unmerged files
-    errors = False
-    for path in files:
-        # remove additional files that might be generated for some
-        # newer versions of GIT
-        for suffix in [base, head1, head2]:
-            if not suffix:
-                continue
-            fname = path + '~' + suffix
-            if os.path.exists(fname):
-                os.remove(fname)
-
-        stages = files[path]
-        if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
-                                 stages['3'][1], path, stages['1'][0],
-                                 stages['2'][0], stages['3'][0]) != 0:
-            errors = True
-
-    if errors:
-        raise GitException, 'GIT index merging failed (possible conflicts)'
+    p = GRun('merge-recursive', base, '--', head1, head2).env(
+        { 'GITHEAD_%s' % base: 'ancestor',
+          'GITHEAD_%s' % head1: 'current',
+          'GITHEAD_%s' % head2: 'patched'}).returns([0, 1])
+    output = p.output_lines()
+    if p.exitcode:
+        # There were conflicts
+        conflicts = [l.strip() for l in output if l.startswith('CONFLICT')]
+        out.info(*conflicts)
+
+        # try the interactive merge or stage checkout (if enabled)
+        for filename in get_conflicts():
+            if (gitmergeonefile.merge(filename)):
+                # interactive merge succeeded
+                resolved([filename])
+
+        # any conflicts left unsolved?
+        cn = len(get_conflicts())
+        if cn:
+            raise GitException, "%d conflict(s)" % cn
 
 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
          binary = True):
@@ -819,14 +669,6 @@ def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
     else:
         return ''
 
-# TODO: take another parameter representing a diff string as we
-# usually invoke git.diff() form the calling functions
-def diffstat(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = []):
-    """Return the diffstat between rev1 and rev2."""
-    return GRun('apply', '--stat', '--summary'
-               ).raw_input(diff(files, rev1, rev2, diff_flags = diff_flags)
-                          ).raw_output()
-
 def files(rev1, rev2, diff_flags = []):
     """Return the files modified between rev1 and rev2
     """
@@ -905,6 +747,17 @@ def reset(files = None, tree_id = None, check_out = True):
     if not files:
         __set_head(tree_id)
 
+def resolved(filenames, reset = None):
+    if reset:
+        stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset]
+        GRun('checkout-index', '--no-create', '--stage=%d' % stage,
+             '--stdin', '-z').input_nulterm(filenames).no_output()
+    GRun('update-index', '--add', '--').xargs(filenames)
+    for filename in filenames:
+        gitmergeonefile.clean_up(filename)
+        # update the access and modificatied times
+        os.utime(filename, None)
+
 def fetch(repository = 'origin', refspec = None):
     """Fetches changes from the remote repository, using 'git fetch'
     by default.
@@ -999,7 +852,7 @@ def apply_patch(filename = None, diff = None, base = None,
         top = commit(message = 'temporary commit used for applying a patch',
                      parents = [base])
         switch(orig_head)
-        merge(base, orig_head, top)
+        merge_recursive(base, orig_head, top)
 
 def clone(repository, local_dir):
     """Clone a remote repository. At the moment, just use the
index 55b62db..1fe226e 100644 (file)
@@ -33,7 +33,7 @@ class GitMergeException(StgException):
 #
 # Options
 #
-merger = ConfigOption('stgit', 'merger')
+autoimerge = ConfigOption('stgit', 'autoimerge')
 keeporig = ConfigOption('stgit', 'keeporig')
 
 #
@@ -48,274 +48,103 @@ def __str2none(x):
 class MRun(Run):
     exc = GitMergeException # use a custom exception class on errors
 
-def __checkout_files(orig_hash, file1_hash, file2_hash,
-                     path,
-                     orig_mode, file1_mode, file2_mode):
-    """Check out the files passed as arguments
+def __checkout_stages(filename):
+    """Check-out the merge stages in the index for the give file
     """
-    global orig, src1, src2
-
     extensions = file_extensions()
+    line = MRun('git', 'checkout-index', '--stage=all', '--', filename
+                ).output_one_line()
+    stages, path = line.split('\t')
+    stages = dict(zip(['ancestor', 'current', 'patched'],
+                      stages.split(' ')))
+
+    for stage, fn in stages.iteritems():
+        if stages[stage] == '.':
+            stages[stage] = None
+        else:
+            newname = filename + extensions[stage]
+            if os.path.exists(newname):
+                # remove the stage if it is already checked out
+                os.remove(newname)
+            os.rename(stages[stage], newname)
+            stages[stage] = newname
 
-    if orig_hash:
-        orig = path + extensions['ancestor']
-        tmp = MRun('git', 'unpack-file', orig_hash).output_one_line()
-        os.chmod(tmp, int(orig_mode, 8))
-        os.renames(tmp, orig)
-    if file1_hash:
-        src1 = path + extensions['current']
-        tmp = MRun('git', 'unpack-file', file1_hash).output_one_line()
-        os.chmod(tmp, int(file1_mode, 8))
-        os.renames(tmp, src1)
-    if file2_hash:
-        src2 = path + extensions['patched']
-        tmp = MRun('git', 'unpack-file', file2_hash).output_one_line()
-        os.chmod(tmp, int(file2_mode, 8))
-        os.renames(tmp, src2)
-
-    if file1_hash and not os.path.exists(path):
-        # the current file might be removed by GIT when it is a new
-        # file added in both branches. Just re-generate it
-        tmp = MRun('git', 'unpack-file', file1_hash).output_one_line()
-        os.chmod(tmp, int(file1_mode, 8))
-        os.renames(tmp, path)
-
-def __remove_files(orig_hash, file1_hash, file2_hash):
-    """Remove any temporary files
-    """
-    if orig_hash:
-        os.remove(orig)
-    if file1_hash:
-        os.remove(src1)
-    if file2_hash:
-        os.remove(src2)
-
-def __conflict(path):
-    """Write the conflict file for the 'path' variable and exit
-    """
-    append_string(os.path.join(basedir.get(), 'conflicts'), path)
-
+    return stages
 
-def interactive_merge(filename):
-    """Run the interactive merger on the given file. Note that the
-    index should not have any conflicts.
+def __remove_stages(filename):
+    """Remove the merge stages from the working directory
     """
     extensions = file_extensions()
+    for ext in extensions.itervalues():
+        fn = filename + ext
+        if os.path.isfile(fn):
+            os.remove(fn)
 
-    ancestor = filename + extensions['ancestor']
-    current = filename + extensions['current']
-    patched = filename + extensions['patched']
-
-    if os.path.isfile(ancestor):
-        three_way = True
-        files_dict = {'branch1': current,
-                      'ancestor': ancestor,
-                      'branch2': patched,
-                      'output': filename}
-        imerger = config.get('stgit.i3merge')
-    else:
-        three_way = False
-        files_dict = {'branch1': current,
-                      'branch2': patched,
-                      'output': filename}
-        imerger = config.get('stgit.i2merge')
-
-    if not imerger:
-        raise GitMergeException, 'No interactive merge command configured'
-
-    # check whether we have all the files for the merge
-    for fn in [filename, current, patched]:
-        if not os.path.isfile(fn):
-            raise GitMergeException, \
-                  'Cannot run the interactive merge: "%s" missing' % fn
-
-    mtime = os.path.getmtime(filename)
-
-    out.info('Trying the interactive %s merge'
-             % (three_way and 'three-way' or 'two-way'))
-
-    err = os.system(imerger % files_dict)
-    if err != 0:
-        raise GitMergeException, 'The interactive merge failed: %d' % err
-    if not os.path.isfile(filename):
-        raise GitMergeException, 'The "%s" file is missing' % filename
-    if mtime == os.path.getmtime(filename):
-        raise GitMergeException, 'The "%s" file was not modified' % filename
-
-
-#
-# Main algorithm
-#
-def merge(orig_hash, file1_hash, file2_hash,
-          path,
-          orig_mode, file1_mode, file2_mode):
-    """Three-way merge for one file algorithm
+def interactive_merge(filename):
+    """Run the interactive merger on the given file. Stages will be
+    removed according to stgit.keeporig. If successful and stages
+    kept, they will be removed via git.resolved().
     """
-    __checkout_files(orig_hash, file1_hash, file2_hash,
-                     path,
-                     orig_mode, file1_mode, file2_mode)
-
-    # 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 dealt with it)
-            if file1_hash == file2_hash:
-                if os.system('git update-index --cacheinfo %s %s %s'
-                             % (file1_mode, file1_hash, path)) != 0:
-                    out.error('git update-index failed')
-                    __conflict(path)
-                    return 1
-                if os.system('git checkout-index -u -f -- %s' % path):
-                    out.error('git checkout-index failed')
-                    __conflict(path)
-                    return 1
-                if file1_mode != file2_mode:
-                    out.error('File added in both, permissions conflict')
-                    __conflict(path)
-                    return 1
-            # 3-way merge
-            else:
-                merge_ok = os.system(str(merger) % {'branch1': src1,
-                                                    'ancestor': orig,
-                                                    'branch2': src2,
-                                                    'output': path }) == 0
-
-                if merge_ok:
-                    os.system('git update-index -- %s' % path)
-                    __remove_files(orig_hash, file1_hash, file2_hash)
-                    return 0
-                else:
-                    out.error('Three-way merge tool failed for file "%s"'
-                              % path)
-                    # reset the cache to the first branch
-                    os.system('git update-index --cacheinfo %s %s %s'
-                              % (file1_mode, file1_hash, path))
-
-                    if config.get('stgit.autoimerge') == 'yes':
-                        try:
-                            interactive_merge(path)
-                        except GitMergeException, ex:
-                            # interactive merge failed
-                            out.error(str(ex))
-                            if str(keeporig) != 'yes':
-                                __remove_files(orig_hash, file1_hash,
-                                               file2_hash)
-                            __conflict(path)
-                            return 1
-                        # successful interactive merge
-                        os.system('git update-index -- %s' % path)
-                        __remove_files(orig_hash, file1_hash, file2_hash)
-                        return 0
-                    else:
-                        # no interactive merge, just mark it as conflict
-                        if str(keeporig) != 'yes':
-                            __remove_files(orig_hash, file1_hash, file2_hash)
-                        __conflict(path)
-                        return 1
-
-        # 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(orig_hash, file1_hash, file2_hash)
-            return os.system('git update-index --remove -- %s' % path)
-        # file deleted in one and changed in the other
+    stages = __checkout_stages(filename)
+
+    try:
+        # Check whether we have all the files for the merge.
+        if not (stages['current'] and stages['patched']):
+            raise GitMergeException('Cannot run the interactive merge')
+
+        if stages['ancestor']:
+            three_way = True
+            files_dict = {'branch1': stages['current'],
+                          'ancestor': stages['ancestor'],
+                          'branch2': stages['patched'],
+                          'output': filename}
+            imerger = config.get('stgit.i3merge')
         else:
-            # Do something here - we must at least merge the entry in
-            # the cache, instead of leaving it in U(nmerged) state. In
-            # fact, stg resolved does not handle that.
-
-            # Do the same thing cogito does - remove the file in any case.
-            os.system('git update-index --remove -- %s' % path)
-
-            #if file1_hash:
-                ## file deleted upstream and changed in the patch. The
-                ## patch is probably going to move the changes
-                ## elsewhere.
-
-                #os.system('git update-index --remove -- %s' % path)
-            #else:
-                ## file deleted in the patch and changed upstream. We
-                ## could re-delete it, but for now leave it there -
-                ## and let the user check if he still wants to remove
-                ## the file.
-
-                ## reset the cache to the first branch
-                #os.system('git update-index --cacheinfo %s %s %s'
-                #          % (file1_mode, file1_hash, path))
-            __conflict(path)
-            return 1
-
-    # 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-index --add --cacheinfo %s %s %s'
-                             % (file1_mode, file1_hash, path)) != 0:
-                    out.error('git update-index failed')
-                    __conflict(path)
-                    return 1
-                if os.system('git checkout-index -u -f -- %s' % path):
-                    out.error('git checkout-index failed')
-                    __conflict(path)
-                    return 1
-                if file1_mode != file2_mode:
-                    out.error('File "%s" added in both, permissions conflict'
-                              % path)
-                    __conflict(path)
-                    return 1
-            # files added in both but different
-            else:
-                out.error('File "%s" added in branches but different' % path)
-                # reset the cache to the first branch
-                os.system('git update-index --cacheinfo %s %s %s'
-                          % (file1_mode, file1_hash, path))
-
-                if config.get('stgit.autoimerge') == 'yes':
-                    try:
-                        interactive_merge(path)
-                    except GitMergeException, ex:
-                        # interactive merge failed
-                        out.error(str(ex))
-                        if str(keeporig) != 'yes':
-                            __remove_files(orig_hash, file1_hash,
-                                           file2_hash)
-                        __conflict(path)
-                        return 1
-                    # successful interactive merge
-                    os.system('git update-index -- %s' % path)
-                    __remove_files(orig_hash, file1_hash, file2_hash)
-                    return 0
-                else:
-                    # no interactive merge, just mark it as conflict
-                    if str(keeporig) != 'yes':
-                        __remove_files(orig_hash, file1_hash, file2_hash)
-                    __conflict(path)
-                    return 1
-        # 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-index --add --cacheinfo %s %s %s'
-                         % (mode, obj, path)) != 0:
-                out.error('git update-index failed')
-                __conflict(path)
-                return 1
-            __remove_files(orig_hash, file1_hash, file2_hash)
-            return os.system('git checkout-index -u -f -- %s' % path)
+            three_way = False
+            files_dict = {'branch1': stages['current'],
+                          'branch2': stages['patched'],
+                          'output': filename}
+            imerger = config.get('stgit.i2merge')
+
+        if not imerger:
+            raise GitMergeException, 'No interactive merge command configured'
+
+        mtime = os.path.getmtime(filename)
+
+        out.start('Trying the interactive %s merge'
+                  % (three_way and 'three-way' or 'two-way'))
+        err = os.system(imerger % files_dict)
+        out.done()
+        if err != 0:
+            raise GitMergeException, 'The interactive merge failed'
+        if not os.path.isfile(filename):
+            raise GitMergeException, 'The "%s" file is missing' % filename
+        if mtime == os.path.getmtime(filename):
+            raise GitMergeException, 'The "%s" file was not modified' % filename
+    finally:
+        # keep the merge stages?
+        if str(keeporig) != 'yes':
+            __remove_stages(filename)
+
+def clean_up(filename):
+    """Remove merge conflict stages if they were generated.
+    """
+    if str(keeporig) == 'yes':
+        __remove_stages(filename)
 
-    # Unhandled case
-    out.error('Unhandled merge conflict: "%s" "%s" "%s" "%s" "%s" "%s" "%s"'
-              % (orig_hash, file1_hash, file2_hash,
-                 path,
-                 orig_mode, file1_mode, file2_mode))
-    __conflict(path)
-    return 1
+def merge(filename):
+    """Merge one file if interactive is allowed or check out the stages
+    if keeporig is set.
+    """
+    if str(autoimerge) == 'yes':
+        try:
+            interactive_merge(filename)
+        except GitMergeException, ex:
+            out.error(str(ex))
+            return False
+        return True
+
+    if str(keeporig) == 'yes':
+        __checkout_stages(filename)
+
+    return False
diff --git a/stgit/lib/__init__.py b/stgit/lib/__init__.py
new file mode 100644 (file)
index 0000000..45eb307
--- /dev/null
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2007, Karl Hasselström <kha@treskal.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/lib/edit.py b/stgit/lib/edit.py
new file mode 100644 (file)
index 0000000..c8d29f6
--- /dev/null
@@ -0,0 +1,99 @@
+"""This module contains utility functions for patch editing."""
+
+from stgit import utils
+from stgit.commands import common
+from stgit.lib import git
+
+def update_patch_description(repo, cd, text, contains_diff):
+    """Update the given L{CommitData<stgit.lib.git.CommitData>} with the
+    given text description, which may contain author name and time
+    stamp in addition to a new commit message. If C{contains_diff} is
+    true, it may also contain a replacement diff.
+
+    Return a pair: the new L{CommitData<stgit.lib.git.CommitData>};
+    and the diff text if it didn't apply, or C{None} otherwise."""
+    (message, authname, authemail, authdate, diff
+     ) = common.parse_patch(text, contains_diff)
+    a = cd.author
+    for val, setter in [(authname, 'set_name'), (authemail, 'set_email'),
+                        (git.Date.maybe(authdate), 'set_date')]:
+        if val != None:
+            a = getattr(a, setter)(val)
+    cd = cd.set_message(message).set_author(a)
+    failed_diff = None
+    if diff:
+        tree = repo.apply(cd.parent.data.tree, diff, quiet = False)
+        if tree == None:
+            failed_diff = diff
+        else:
+            cd = cd.set_tree(tree)
+    return cd, failed_diff
+
+def patch_desc(repo, cd, append_diff, diff_flags, replacement_diff):
+    """Return a description text for the patch, suitable for editing
+    and/or reimporting with L{update_patch_description()}.
+
+    @param cd: The L{CommitData<stgit.lib.git.CommitData>} to generate
+               a description of
+    @param append_diff: Whether to append the patch diff to the
+                        description
+    @type append_diff: C{bool}
+    @param diff_flags: Extra parameters to pass to C{git diff}
+    @param replacement_diff: Diff text to use; or C{None} if it should
+                             be computed from C{cd}
+    @type replacement_diff: C{str} or C{None}"""
+    desc = ['From: %s <%s>' % (cd.author.name, cd.author.email),
+            'Date: %s' % cd.author.date.isoformat(),
+            '',
+            cd.message]
+    if append_diff:
+        if replacement_diff:
+            diff = replacement_diff
+        else:
+            just_diff = repo.diff_tree(cd.parent.data.tree, cd.tree, diff_flags)
+            diff = '\n'.join([git.diffstat(just_diff), just_diff])
+        desc += ['---', '', diff]
+    return '\n'.join(desc)
+
+def interactive_edit_patch(repo, cd, edit_diff, diff_flags, replacement_diff):
+    """Edit the patch interactively. If C{edit_diff} is true, edit the
+    diff as well. If C{replacement_diff} is not C{None}, it contains a
+    diff to edit instead of the patch's real diff.
+
+    Return a pair: the new L{CommitData<stgit.lib.git.CommitData>};
+    and the diff text if it didn't apply, or C{None} otherwise."""
+    return update_patch_description(
+        repo, cd, utils.edit_string(
+            patch_desc(repo, cd, edit_diff, diff_flags, replacement_diff),
+            '.stgit-edit.' + ['txt', 'patch'][bool(edit_diff)]),
+        edit_diff)
+
+def auto_edit_patch(repo, cd, msg, contains_diff, author, committer, sign_str):
+    """Edit the patch noninteractively in a couple of ways:
+
+         - If C{msg} is not C{None}, parse it to find a replacement
+           message, and possibly also replacement author and
+           timestamp. If C{contains_diff} is true, also look for a
+           replacement diff.
+
+         - C{author} and C{committer} are two functions that take the
+           original L{Person<stgit.lib.git.Person>} value as argument,
+           and return the new one.
+
+         - C{sign_str}, if not C{None}, is a sign string to append to
+           the message.
+
+    Return a pair: the new L{CommitData<stgit.lib.git.CommitData>};
+    and the diff text if it didn't apply, or C{None} otherwise."""
+    if msg == None:
+        failed_diff = None
+    else:
+        cd, failed_diff = update_patch_description(repo, cd, msg, contains_diff)
+    a, c = author(cd.author), committer(cd.committer)
+    if (a, c) != (cd.author, cd.committer):
+        cd = cd.set_author(a).set_committer(c)
+    if sign_str != None:
+        cd = cd.set_message(utils.add_sign_line(
+                cd.message, sign_str, git.Person.committer().name,
+                git.Person.committer().email))
+    return cd, failed_diff
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
new file mode 100644 (file)
index 0000000..e2b4266
--- /dev/null
@@ -0,0 +1,912 @@
+"""A Python class hierarchy wrapping a git repository and its
+contents."""
+
+import os, os.path, re
+from datetime import datetime, timedelta, tzinfo
+
+from stgit import exception, run, utils
+from stgit.config import config
+
+class Immutable(object):
+    """I{Immutable} objects cannot be modified once created. Any
+    modification methods will return a new object, leaving the
+    original object as it was.
+
+    The reason for this is that we want to be able to represent git
+    objects, which are immutable, and want to be able to create new
+    git objects that are just slight modifications of other git
+    objects. (Such as, for example, modifying the commit message of a
+    commit object while leaving the rest of it intact. This involves
+    creating a whole new commit object that's exactly like the old one
+    except for the commit message.)
+
+    The L{Immutable} class doesn't actually enforce immutability --
+    that is up to the individual immutable subclasses. It just serves
+    as documentation."""
+
+class RepositoryException(exception.StgException):
+    """Base class for all exceptions due to failed L{Repository}
+    operations."""
+
+class BranchException(exception.StgException):
+    """Exception raised by failed L{Branch} operations."""
+
+class DateException(exception.StgException):
+    """Exception raised when a date+time string could not be parsed."""
+    def __init__(self, string, type):
+        exception.StgException.__init__(
+            self, '"%s" is not a valid %s' % (string, type))
+
+class DetachedHeadException(RepositoryException):
+    """Exception raised when HEAD is detached (that is, there is no
+    current branch)."""
+    def __init__(self):
+        RepositoryException.__init__(self, 'Not on any branch')
+
+class Repr(object):
+    """Utility class that defines C{__reps__} in terms of C{__str__}."""
+    def __repr__(self):
+        return str(self)
+
+class NoValue(object):
+    """A handy default value that is guaranteed to be distinct from any
+    real argument value."""
+    pass
+
+def make_defaults(defaults):
+    def d(val, attr, default_fun = lambda: None):
+        if val != NoValue:
+            return val
+        elif defaults != NoValue:
+            return getattr(defaults, attr)
+        else:
+            return default_fun()
+    return d
+
+class TimeZone(tzinfo, Repr):
+    """A simple time zone class for static offsets from UTC. (We have to
+    define our own since Python's standard library doesn't define any
+    time zone classes.)"""
+    def __init__(self, tzstring):
+        m = re.match(r'^([+-])(\d{2}):?(\d{2})$', tzstring)
+        if not m:
+            raise DateException(tzstring, 'time zone')
+        sign = int(m.group(1) + '1')
+        try:
+            self.__offset = timedelta(hours = sign*int(m.group(2)),
+                                      minutes = sign*int(m.group(3)))
+        except OverflowError:
+            raise DateException(tzstring, 'time zone')
+        self.__name = tzstring
+    def utcoffset(self, dt):
+        return self.__offset
+    def tzname(self, dt):
+        return self.__name
+    def dst(self, dt):
+        return timedelta(0)
+    def __str__(self):
+        return self.__name
+
+class Date(Immutable, Repr):
+    """Represents a timestamp used in git commits."""
+    def __init__(self, datestring):
+        # Try git-formatted date.
+        m = re.match(r'^(\d+)\s+([+-]\d\d:?\d\d)$', datestring)
+        if m:
+            try:
+                self.__time = datetime.fromtimestamp(int(m.group(1)),
+                                                     TimeZone(m.group(2)))
+            except ValueError:
+                raise DateException(datestring, 'date')
+            return
+
+        # Try iso-formatted date.
+        m = re.match(r'^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s+'
+                     + r'([+-]\d\d:?\d\d)$', datestring)
+        if m:
+            try:
+                self.__time = datetime(
+                    *[int(m.group(i + 1)) for i in xrange(6)],
+                    **{'tzinfo': TimeZone(m.group(7))})
+            except ValueError:
+                raise DateException(datestring, 'date')
+            return
+
+        raise DateException(datestring, 'date')
+    def __str__(self):
+        return self.isoformat()
+    def isoformat(self):
+        """Human-friendly ISO 8601 format."""
+        return '%s %s' % (self.__time.replace(tzinfo = None).isoformat(' '),
+                          self.__time.tzinfo)
+    @classmethod
+    def maybe(cls, datestring):
+        """Return a new object initialized with the argument if it contains a
+        value (otherwise, just return the argument)."""
+        if datestring in [None, NoValue]:
+            return datestring
+        return cls(datestring)
+
+class Person(Immutable, Repr):
+    """Represents an author or committer in a git commit object. Contains
+    name, email and timestamp."""
+    def __init__(self, name = NoValue, email = NoValue,
+                 date = NoValue, defaults = NoValue):
+        d = make_defaults(defaults)
+        self.__name = d(name, 'name')
+        self.__email = d(email, 'email')
+        self.__date = d(date, 'date')
+        assert isinstance(self.__date, Date) or self.__date in [None, NoValue]
+    name = property(lambda self: self.__name)
+    email = property(lambda self: self.__email)
+    name_email = property(lambda self: '%s <%s>' % (self.name, self.email))
+    date = property(lambda self: self.__date)
+    def set_name(self, name):
+        return type(self)(name = name, defaults = self)
+    def set_email(self, email):
+        return type(self)(email = email, defaults = self)
+    def set_date(self, date):
+        return type(self)(date = date, defaults = self)
+    def __str__(self):
+        return '%s %s' % (self.name_email, self.date)
+    @classmethod
+    def parse(cls, s):
+        m = re.match(r'^([^<]*)<([^>]*)>\s+(\d+\s+[+-]\d{4})$', s)
+        assert m
+        name = m.group(1).strip()
+        email = m.group(2)
+        date = Date(m.group(3))
+        return cls(name, email, date)
+    @classmethod
+    def user(cls):
+        if not hasattr(cls, '__user'):
+            cls.__user = cls(name = config.get('user.name'),
+                             email = config.get('user.email'))
+        return cls.__user
+    @classmethod
+    def author(cls):
+        if not hasattr(cls, '__author'):
+            cls.__author = cls(
+                name = os.environ.get('GIT_AUTHOR_NAME', NoValue),
+                email = os.environ.get('GIT_AUTHOR_EMAIL', NoValue),
+                date = Date.maybe(os.environ.get('GIT_AUTHOR_DATE', NoValue)),
+                defaults = cls.user())
+        return cls.__author
+    @classmethod
+    def committer(cls):
+        if not hasattr(cls, '__committer'):
+            cls.__committer = cls(
+                name = os.environ.get('GIT_COMMITTER_NAME', NoValue),
+                email = os.environ.get('GIT_COMMITTER_EMAIL', NoValue),
+                date = Date.maybe(
+                    os.environ.get('GIT_COMMITTER_DATE', NoValue)),
+                defaults = cls.user())
+        return cls.__committer
+
+class GitObject(Immutable, Repr):
+    """Base class for all git objects. One git object is represented by at
+    most one C{GitObject}, which makes it possible to compare them
+    using normal Python object comparison; it also ensures we don't
+    waste more memory than necessary."""
+
+class BlobData(Immutable, Repr):
+    """Represents the data contents of a git blob object."""
+    def __init__(self, string):
+        self.__string = str(string)
+    str = property(lambda self: self.__string)
+    def commit(self, repository):
+        """Commit the blob.
+        @return: The committed blob
+        @rtype: L{Blob}"""
+        sha1 = repository.run(['git', 'hash-object', '-w', '--stdin']
+                              ).raw_input(self.str).output_one_line()
+        return repository.get_blob(sha1)
+
+class Blob(GitObject):
+    """Represents a git blob object. All the actual data contents of the
+    blob object is stored in the L{data} member, which is a
+    L{BlobData} object."""
+    typename = 'blob'
+    default_perm = '100644'
+    def __init__(self, repository, sha1):
+        self.__repository = repository
+        self.__sha1 = sha1
+    sha1 = property(lambda self: self.__sha1)
+    def __str__(self):
+        return 'Blob<%s>' % self.sha1
+    @property
+    def data(self):
+        return BlobData(self.__repository.cat_object(self.sha1))
+
+class ImmutableDict(dict):
+    """A dictionary that cannot be modified once it's been created."""
+    def error(*args, **kwargs):
+        raise TypeError('Cannot modify immutable dict')
+    __delitem__ = error
+    __setitem__ = error
+    clear = error
+    pop = error
+    popitem = error
+    setdefault = error
+    update = error
+
+class TreeData(Immutable, Repr):
+    """Represents the data contents of a git tree object."""
+    @staticmethod
+    def __x(po):
+        if isinstance(po, GitObject):
+            perm, object = po.default_perm, po
+        else:
+            perm, object = po
+        return perm, object
+    def __init__(self, entries):
+        """Create a new L{TreeData} object from the given mapping from names
+        (strings) to either (I{permission}, I{object}) tuples or just
+        objects."""
+        self.__entries = ImmutableDict((name, self.__x(po))
+                                       for (name, po) in entries.iteritems())
+    entries = property(lambda self: self.__entries)
+    """Map from name to (I{permission}, I{object}) tuple."""
+    def set_entry(self, name, po):
+        """Create a new L{TreeData} object identical to this one, except that
+        it maps C{name} to C{po}.
+
+        @param name: Name of the changed mapping
+        @type name: C{str}
+        @param po: Value of the changed mapping
+        @type po: L{Blob} or L{Tree} or (C{str}, L{Blob} or L{Tree})
+        @return: The new L{TreeData} object
+        @rtype: L{TreeData}"""
+        e = dict(self.entries)
+        e[name] = self.__x(po)
+        return type(self)(e)
+    def del_entry(self, name):
+        """Create a new L{TreeData} object identical to this one, except that
+        it doesn't map C{name} to anything.
+
+        @param name: Name of the deleted mapping
+        @type name: C{str}
+        @return: The new L{TreeData} object
+        @rtype: L{TreeData}"""
+        e = dict(self.entries)
+        del e[name]
+        return type(self)(e)
+    def commit(self, repository):
+        """Commit the tree.
+        @return: The committed tree
+        @rtype: L{Tree}"""
+        listing = ''.join(
+            '%s %s %s\t%s\0' % (mode, obj.typename, obj.sha1, name)
+            for (name, (mode, obj)) in self.entries.iteritems())
+        sha1 = repository.run(['git', 'mktree', '-z']
+                              ).raw_input(listing).output_one_line()
+        return repository.get_tree(sha1)
+    @classmethod
+    def parse(cls, repository, s):
+        """Parse a raw git tree description.
+
+        @return: A new L{TreeData} object
+        @rtype: L{TreeData}"""
+        entries = {}
+        for line in s.split('\0')[:-1]:
+            m = re.match(r'^([0-7]{6}) ([a-z]+) ([0-9a-f]{40})\t(.*)$', line)
+            assert m
+            perm, type, sha1, name = m.groups()
+            entries[name] = (perm, repository.get_object(type, sha1))
+        return cls(entries)
+
+class Tree(GitObject):
+    """Represents a git tree object. All the actual data contents of the
+    tree object is stored in the L{data} member, which is a
+    L{TreeData} object."""
+    typename = 'tree'
+    default_perm = '040000'
+    def __init__(self, repository, sha1):
+        self.__sha1 = sha1
+        self.__repository = repository
+        self.__data = None
+    sha1 = property(lambda self: self.__sha1)
+    @property
+    def data(self):
+        if self.__data == None:
+            self.__data = TreeData.parse(
+                self.__repository,
+                self.__repository.run(['git', 'ls-tree', '-z', self.sha1]
+                                      ).raw_output())
+        return self.__data
+    def __str__(self):
+        return 'Tree<sha1: %s>' % self.sha1
+
+class CommitData(Immutable, Repr):
+    """Represents the data contents of a git commit object."""
+    def __init__(self, tree = NoValue, parents = NoValue, author = NoValue,
+                 committer = NoValue, message = NoValue, defaults = NoValue):
+        d = make_defaults(defaults)
+        self.__tree = d(tree, 'tree')
+        self.__parents = d(parents, 'parents')
+        self.__author = d(author, 'author', Person.author)
+        self.__committer = d(committer, 'committer', Person.committer)
+        self.__message = d(message, 'message')
+    tree = property(lambda self: self.__tree)
+    parents = property(lambda self: self.__parents)
+    @property
+    def parent(self):
+        assert len(self.__parents) == 1
+        return self.__parents[0]
+    author = property(lambda self: self.__author)
+    committer = property(lambda self: self.__committer)
+    message = property(lambda self: self.__message)
+    def set_tree(self, tree):
+        return type(self)(tree = tree, defaults = self)
+    def set_parents(self, parents):
+        return type(self)(parents = parents, defaults = self)
+    def add_parent(self, parent):
+        return type(self)(parents = list(self.parents or []) + [parent],
+                          defaults = self)
+    def set_parent(self, parent):
+        return self.set_parents([parent])
+    def set_author(self, author):
+        return type(self)(author = author, defaults = self)
+    def set_committer(self, committer):
+        return type(self)(committer = committer, defaults = self)
+    def set_message(self, message):
+        return type(self)(message = message, defaults = self)
+    def is_nochange(self):
+        return len(self.parents) == 1 and self.tree == self.parent.data.tree
+    def __str__(self):
+        if self.tree == None:
+            tree = None
+        else:
+            tree = self.tree.sha1
+        if self.parents == None:
+            parents = None
+        else:
+            parents = [p.sha1 for p in self.parents]
+        return ('CommitData<tree: %s, parents: %s, author: %s,'
+                ' committer: %s, message: "%s">'
+                ) % (tree, parents, self.author, self.committer, self.message)
+    def commit(self, repository):
+        """Commit the commit.
+        @return: The committed commit
+        @rtype: L{Commit}"""
+        c = ['git', 'commit-tree', self.tree.sha1]
+        for p in self.parents:
+            c.append('-p')
+            c.append(p.sha1)
+        env = {}
+        for p, v1 in ((self.author, 'AUTHOR'),
+                       (self.committer, 'COMMITTER')):
+            if p != None:
+                for attr, v2 in (('name', 'NAME'), ('email', 'EMAIL'),
+                                 ('date', 'DATE')):
+                    if getattr(p, attr) != None:
+                        env['GIT_%s_%s' % (v1, v2)] = str(getattr(p, attr))
+        sha1 = repository.run(c, env = env).raw_input(self.message
+                                                      ).output_one_line()
+        return repository.get_commit(sha1)
+    @classmethod
+    def parse(cls, repository, s):
+        """Parse a raw git commit description.
+        @return: A new L{CommitData} object
+        @rtype: L{CommitData}"""
+        cd = cls(parents = [])
+        lines = list(s.splitlines(True))
+        for i in xrange(len(lines)):
+            line = lines[i].strip()
+            if not line:
+                return cd.set_message(''.join(lines[i+1:]))
+            key, value = line.split(None, 1)
+            if key == 'tree':
+                cd = cd.set_tree(repository.get_tree(value))
+            elif key == 'parent':
+                cd = cd.add_parent(repository.get_commit(value))
+            elif key == 'author':
+                cd = cd.set_author(Person.parse(value))
+            elif key == 'committer':
+                cd = cd.set_committer(Person.parse(value))
+            else:
+                assert False
+        assert False
+
+class Commit(GitObject):
+    """Represents a git commit object. All the actual data contents of the
+    commit object is stored in the L{data} member, which is a
+    L{CommitData} object."""
+    typename = 'commit'
+    def __init__(self, repository, sha1):
+        self.__sha1 = sha1
+        self.__repository = repository
+        self.__data = None
+    sha1 = property(lambda self: self.__sha1)
+    @property
+    def data(self):
+        if self.__data == None:
+            self.__data = CommitData.parse(
+                self.__repository,
+                self.__repository.cat_object(self.sha1))
+        return self.__data
+    def __str__(self):
+        return 'Commit<sha1: %s, data: %s>' % (self.sha1, self.__data)
+
+class Refs(object):
+    """Accessor for the refs stored in a git repository. Will
+    transparently cache the values of all refs."""
+    def __init__(self, repository):
+        self.__repository = repository
+        self.__refs = None
+    def __cache_refs(self):
+        """(Re-)Build the cache of all refs in the repository."""
+        self.__refs = {}
+        for line in self.__repository.run(['git', 'show-ref']).output_lines():
+            m = re.match(r'^([0-9a-f]{40})\s+(\S+)$', line)
+            sha1, ref = m.groups()
+            self.__refs[ref] = sha1
+    def get(self, ref):
+        """Get the Commit the given ref points to. Throws KeyError if ref
+        doesn't exist."""
+        if self.__refs == None:
+            self.__cache_refs()
+        return self.__repository.get_commit(self.__refs[ref])
+    def exists(self, ref):
+        """Check if the given ref exists."""
+        try:
+            self.get(ref)
+        except KeyError:
+            return False
+        else:
+            return True
+    def set(self, ref, commit, msg):
+        """Write the sha1 of the given Commit to the ref. The ref may or may
+        not already exist."""
+        if self.__refs == None:
+            self.__cache_refs()
+        old_sha1 = self.__refs.get(ref, '0'*40)
+        new_sha1 = commit.sha1
+        if old_sha1 != new_sha1:
+            self.__repository.run(['git', 'update-ref', '-m', msg,
+                                   ref, new_sha1, old_sha1]).no_output()
+            self.__refs[ref] = new_sha1
+    def delete(self, ref):
+        """Delete the given ref. Throws KeyError if ref doesn't exist."""
+        if self.__refs == None:
+            self.__cache_refs()
+        self.__repository.run(['git', 'update-ref',
+                               '-d', ref, self.__refs[ref]]).no_output()
+        del self.__refs[ref]
+
+class ObjectCache(object):
+    """Cache for Python objects, for making sure that we create only one
+    Python object per git object. This reduces memory consumption and
+    makes object comparison very cheap."""
+    def __init__(self, create):
+        self.__objects = {}
+        self.__create = create
+    def __getitem__(self, name):
+        if not name in self.__objects:
+            self.__objects[name] = self.__create(name)
+        return self.__objects[name]
+    def __contains__(self, name):
+        return name in self.__objects
+    def __setitem__(self, name, val):
+        assert not name in self.__objects
+        self.__objects[name] = val
+
+class RunWithEnv(object):
+    def run(self, args, env = {}):
+        """Run the given command with an environment given by self.env.
+
+        @type args: list of strings
+        @param args: Command and argument vector
+        @type env: dict
+        @param env: Extra environment"""
+        return run.Run(*args).env(utils.add_dict(self.env, env))
+
+class RunWithEnvCwd(RunWithEnv):
+    def run(self, args, env = {}):
+        """Run the given command with an environment given by self.env, and
+        current working directory given by self.cwd.
+
+        @type args: list of strings
+        @param args: Command and argument vector
+        @type env: dict
+        @param env: Extra environment"""
+        return RunWithEnv.run(self, args, env).cwd(self.cwd)
+    def run_in_cwd(self, args):
+        """Run the given command with an environment given by self.env and
+        self.env_in_cwd, without changing the current working
+        directory.
+
+        @type args: list of strings
+        @param args: Command and argument vector"""
+        return RunWithEnv.run(self, args, self.env_in_cwd)
+
+class Repository(RunWithEnv):
+    """Represents a git repository."""
+    def __init__(self, directory):
+        self.__git_dir = directory
+        self.__refs = Refs(self)
+        self.__blobs = ObjectCache(lambda sha1: Blob(self, sha1))
+        self.__trees = ObjectCache(lambda sha1: Tree(self, sha1))
+        self.__commits = ObjectCache(lambda sha1: Commit(self, sha1))
+        self.__default_index = None
+        self.__default_worktree = None
+        self.__default_iw = None
+    env = property(lambda self: { 'GIT_DIR': self.__git_dir })
+    @classmethod
+    def default(cls):
+        """Return the default repository."""
+        try:
+            return cls(run.Run('git', 'rev-parse', '--git-dir'
+                               ).output_one_line())
+        except run.RunException:
+            raise RepositoryException('Cannot find git repository')
+    @property
+    def current_branch_name(self):
+        """Return the name of the current branch."""
+        return utils.strip_prefix('refs/heads/', self.head_ref)
+    @property
+    def default_index(self):
+        """An L{Index} object representing the default index file for the
+        repository."""
+        if self.__default_index == None:
+            self.__default_index = Index(
+                self, (os.environ.get('GIT_INDEX_FILE', None)
+                       or os.path.join(self.__git_dir, 'index')))
+        return self.__default_index
+    def temp_index(self):
+        """Return an L{Index} object representing a new temporary index file
+        for the repository."""
+        return Index(self, self.__git_dir)
+    @property
+    def default_worktree(self):
+        """A L{Worktree} object representing the default work tree."""
+        if self.__default_worktree == None:
+            path = os.environ.get('GIT_WORK_TREE', None)
+            if not path:
+                o = run.Run('git', 'rev-parse', '--show-cdup').output_lines()
+                o = o or ['.']
+                assert len(o) == 1
+                path = o[0]
+            self.__default_worktree = Worktree(path)
+        return self.__default_worktree
+    @property
+    def default_iw(self):
+        """An L{IndexAndWorktree} object representing the default index and
+        work tree for this repository."""
+        if self.__default_iw == None:
+            self.__default_iw = IndexAndWorktree(self.default_index,
+                                                 self.default_worktree)
+        return self.__default_iw
+    directory = property(lambda self: self.__git_dir)
+    refs = property(lambda self: self.__refs)
+    def cat_object(self, sha1):
+        return self.run(['git', 'cat-file', '-p', sha1]).raw_output()
+    def rev_parse(self, rev, discard_stderr = False):
+        try:
+            return self.get_commit(self.run(
+                    ['git', 'rev-parse', '%s^{commit}' % rev]
+                    ).discard_stderr(discard_stderr).output_one_line())
+        except run.RunException:
+            raise RepositoryException('%s: No such revision' % rev)
+    def get_blob(self, sha1):
+        return self.__blobs[sha1]
+    def get_tree(self, sha1):
+        return self.__trees[sha1]
+    def get_commit(self, sha1):
+        return self.__commits[sha1]
+    def get_object(self, type, sha1):
+        return { Blob.typename: self.get_blob,
+                 Tree.typename: self.get_tree,
+                 Commit.typename: self.get_commit }[type](sha1)
+    def commit(self, objectdata):
+        return objectdata.commit(self)
+    @property
+    def head_ref(self):
+        try:
+            return self.run(['git', 'symbolic-ref', '-q', 'HEAD']
+                            ).output_one_line()
+        except run.RunException:
+            raise DetachedHeadException()
+    def set_head_ref(self, ref, msg):
+        self.run(['git', 'symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()
+    def simple_merge(self, base, ours, theirs):
+        index = self.temp_index()
+        try:
+            result, index_tree = index.merge(base, ours, theirs)
+        finally:
+            index.delete()
+        return result
+    def apply(self, tree, patch_text, quiet):
+        """Given a L{Tree} and a patch, will either return the new L{Tree}
+        that results when the patch is applied, or None if the patch
+        couldn't be applied."""
+        assert isinstance(tree, Tree)
+        if not patch_text:
+            return tree
+        index = self.temp_index()
+        try:
+            index.read_tree(tree)
+            try:
+                index.apply(patch_text, quiet)
+                return index.write_tree()
+            except MergeException:
+                return None
+        finally:
+            index.delete()
+    def diff_tree(self, t1, t2, diff_opts):
+        """Given two L{Tree}s C{t1} and C{t2}, return the patch that takes
+        C{t1} to C{t2}.
+
+        @type diff_opts: list of strings
+        @param diff_opts: Extra diff options
+        @rtype: String
+        @return: Patch text"""
+        assert isinstance(t1, Tree)
+        assert isinstance(t2, Tree)
+        return self.run(['git', 'diff-tree', '-p'] + list(diff_opts)
+                        + [t1.sha1, t2.sha1]).raw_output()
+    def diff_tree_files(self, t1, t2):
+        """Given two L{Tree}s C{t1} and C{t2}, iterate over all files for
+        which they differ. For each file, yield a tuple with the old
+        file mode, the new file mode, the old blob, the new blob, the
+        status, the old filename, and the new filename. Except in case
+        of a copy or a rename, the old and new filenames are
+        identical."""
+        assert isinstance(t1, Tree)
+        assert isinstance(t2, Tree)
+        i = iter(self.run(['git', 'diff-tree', '-r', '-z'] + [t1.sha1, t2.sha1]
+                          ).raw_output().split('\0'))
+        try:
+            while True:
+                x = i.next()
+                if not x:
+                    continue
+                omode, nmode, osha1, nsha1, status = x[1:].split(' ')
+                fn1 = i.next()
+                if status[0] in ['C', 'R']:
+                    fn2 = i.next()
+                else:
+                    fn2 = fn1
+                yield (omode, nmode, self.get_blob(osha1),
+                       self.get_blob(nsha1), status, fn1, fn2)
+        except StopIteration:
+            pass
+
+class MergeException(exception.StgException):
+    """Exception raised when a merge fails for some reason."""
+
+class MergeConflictException(MergeException):
+    """Exception raised when a merge fails due to conflicts."""
+    def __init__(self, conflicts):
+        MergeException.__init__(self)
+        self.conflicts = conflicts
+
+class Index(RunWithEnv):
+    """Represents a git index file."""
+    def __init__(self, repository, filename):
+        self.__repository = repository
+        if os.path.isdir(filename):
+            # Create a temp index in the given directory.
+            self.__filename = os.path.join(
+                filename, 'index.temp-%d-%x' % (os.getpid(), id(self)))
+            self.delete()
+        else:
+            self.__filename = filename
+    env = property(lambda self: utils.add_dict(
+            self.__repository.env, { 'GIT_INDEX_FILE': self.__filename }))
+    def read_tree(self, tree):
+        self.run(['git', 'read-tree', tree.sha1]).no_output()
+    def write_tree(self):
+        """Write the index contents to the repository.
+        @return: The resulting L{Tree}
+        @rtype: L{Tree}"""
+        try:
+            return self.__repository.get_tree(
+                self.run(['git', 'write-tree']).discard_stderr(
+                    ).output_one_line())
+        except run.RunException:
+            raise MergeException('Conflicting merge')
+    def is_clean(self):
+        try:
+            self.run(['git', 'update-index', '--refresh']).discard_output()
+        except run.RunException:
+            return False
+        else:
+            return True
+    def apply(self, patch_text, quiet):
+        """In-index patch application, no worktree involved."""
+        try:
+            r = self.run(['git', 'apply', '--cached']).raw_input(patch_text)
+            if quiet:
+                r = r.discard_stderr()
+            r.no_output()
+        except run.RunException:
+            raise MergeException('Patch does not apply cleanly')
+    def apply_treediff(self, tree1, tree2, quiet):
+        """Apply the diff from C{tree1} to C{tree2} to the index."""
+        # Passing --full-index here is necessary to support binary
+        # files. It is also sufficient, since the repository already
+        # contains all involved objects; in other words, we don't have
+        # to use --binary.
+        self.apply(self.__repository.diff_tree(tree1, tree2, ['--full-index']),
+                   quiet)
+    def merge(self, base, ours, theirs, current = None):
+        """Use the index (and only the index) to do a 3-way merge of the
+        L{Tree}s C{base}, C{ours} and C{theirs}. The merge will either
+        succeed (in which case the first half of the return value is
+        the resulting tree) or fail cleanly (in which case the first
+        half of the return value is C{None}).
+
+        If C{current} is given (and not C{None}), it is assumed to be
+        the L{Tree} currently stored in the index; this information is
+        used to avoid having to read the right tree (either of C{ours}
+        and C{theirs}) into the index if it's already there. The
+        second half of the return value is the tree now stored in the
+        index, or C{None} if unknown. If the merge succeeded, this is
+        often the merge result."""
+        assert isinstance(base, Tree)
+        assert isinstance(ours, Tree)
+        assert isinstance(theirs, Tree)
+        assert current == None or isinstance(current, Tree)
+
+        # Take care of the really trivial cases.
+        if base == ours:
+            return (theirs, current)
+        if base == theirs:
+            return (ours, current)
+        if ours == theirs:
+            return (ours, current)
+
+        if current == theirs:
+            # Swap the trees. It doesn't matter since merging is
+            # symmetric, and will allow us to avoid the read_tree()
+            # call below.
+            ours, theirs = theirs, ours
+        if current != ours:
+            self.read_tree(ours)
+        try:
+            self.apply_treediff(base, theirs, quiet = True)
+            result = self.write_tree()
+            return (result, result)
+        except MergeException:
+            return (None, ours)
+    def delete(self):
+        if os.path.isfile(self.__filename):
+            os.remove(self.__filename)
+    def conflicts(self):
+        """The set of conflicting paths."""
+        paths = set()
+        for line in self.run(['git', 'ls-files', '-z', '--unmerged']
+                             ).raw_output().split('\0')[:-1]:
+            stat, path = line.split('\t', 1)
+            paths.add(path)
+        return paths
+
+class Worktree(object):
+    """Represents a git worktree (that is, a checked-out file tree)."""
+    def __init__(self, directory):
+        self.__directory = directory
+    env = property(lambda self: { 'GIT_WORK_TREE': '.' })
+    env_in_cwd = property(lambda self: { 'GIT_WORK_TREE': self.directory })
+    directory = property(lambda self: self.__directory)
+
+class CheckoutException(exception.StgException):
+    """Exception raised when a checkout fails."""
+
+class IndexAndWorktree(RunWithEnvCwd):
+    """Represents a git index and a worktree. Anything that an index or
+    worktree can do on their own are handled by the L{Index} and
+    L{Worktree} classes; this class concerns itself with the
+    operations that require both."""
+    def __init__(self, index, worktree):
+        self.__index = index
+        self.__worktree = worktree
+    index = property(lambda self: self.__index)
+    env = property(lambda self: utils.add_dict(self.__index.env,
+                                               self.__worktree.env))
+    env_in_cwd = property(lambda self: self.__worktree.env_in_cwd)
+    cwd = property(lambda self: self.__worktree.directory)
+    def checkout_hard(self, tree):
+        assert isinstance(tree, Tree)
+        self.run(['git', 'read-tree', '--reset', '-u', tree.sha1]
+                 ).discard_output()
+    def checkout(self, old_tree, new_tree):
+        # TODO: Optionally do a 3-way instead of doing nothing when we
+        # have a problem. Or maybe we should stash changes in a patch?
+        assert isinstance(old_tree, Tree)
+        assert isinstance(new_tree, Tree)
+        try:
+            self.run(['git', 'read-tree', '-u', '-m',
+                      '--exclude-per-directory=.gitignore',
+                      old_tree.sha1, new_tree.sha1]
+                     ).discard_output()
+        except run.RunException:
+            raise CheckoutException('Index/workdir dirty')
+    def merge(self, base, ours, theirs):
+        assert isinstance(base, Tree)
+        assert isinstance(ours, Tree)
+        assert isinstance(theirs, Tree)
+        try:
+            r = self.run(['git', 'merge-recursive', base.sha1, '--', ours.sha1,
+                          theirs.sha1],
+                         env = { 'GITHEAD_%s' % base.sha1: 'ancestor',
+                                 'GITHEAD_%s' % ours.sha1: 'current',
+                                 'GITHEAD_%s' % theirs.sha1: 'patched'})
+            r.returns([0, 1])
+            output = r.output_lines()
+            if r.exitcode:
+                # There were conflicts
+                conflicts = [l for l in output if l.startswith('CONFLICT')]
+                raise MergeConflictException(conflicts)
+        except run.RunException, e:
+            raise MergeException('Index/worktree dirty')
+    def changed_files(self, tree, pathlimits = []):
+        """Return the set of files in the worktree that have changed with
+        respect to C{tree}. The listing is optionally restricted to
+        those files that match any of the path limiters given.
+
+        The path limiters are relative to the current working
+        directory; the returned file names are relative to the
+        repository root."""
+        assert isinstance(tree, Tree)
+        return set(self.run_in_cwd(
+                ['git', 'diff-index', tree.sha1, '--name-only', '-z', '--']
+                + list(pathlimits)).raw_output().split('\0')[:-1])
+    def update_index(self, paths):
+        """Update the index with files from the worktree. C{paths} is an
+        iterable of paths relative to the root of the repository."""
+        cmd = ['git', 'update-index', '--remove']
+        self.run(cmd + ['-z', '--stdin']
+                 ).input_nulterm(paths).discard_output()
+
+class Branch(object):
+    """Represents a Git branch."""
+    def __init__(self, repository, name):
+        self.__repository = repository
+        self.__name = name
+        try:
+            self.head
+        except KeyError:
+            raise BranchException('%s: no such branch' % name)
+
+    name = property(lambda self: self.__name)
+    repository = property(lambda self: self.__repository)
+
+    def __ref(self):
+        return 'refs/heads/%s' % self.__name
+    @property
+    def head(self):
+        return self.__repository.refs.get(self.__ref())
+    def set_head(self, commit, msg):
+        self.__repository.refs.set(self.__ref(), commit, msg)
+
+    def set_parent_remote(self, name):
+        value = config.set('branch.%s.remote' % self.__name, name)
+    def set_parent_branch(self, name):
+        if config.get('branch.%s.remote' % self.__name):
+            # Never set merge if remote is not set to avoid
+            # possibly-erroneous lookups into 'origin'
+            config.set('branch.%s.merge' % self.__name, name)
+
+    @classmethod
+    def create(cls, repository, name, create_at = None):
+        """Create a new Git branch and return the corresponding
+        L{Branch} object."""
+        try:
+            branch = cls(repository, name)
+        except BranchException:
+            branch = None
+        if branch:
+            raise BranchException('%s: branch already exists' % name)
+
+        cmd = ['git', 'branch']
+        if create_at:
+            cmd.append(create_at.sha1)
+        repository.run(['git', 'branch', create_at.sha1]).discard_output()
+
+        return cls(repository, name)
+
+def diffstat(diff):
+    """Return the diffstat of the supplied diff."""
+    return run.Run('git', 'apply', '--stat', '--summary'
+                   ).raw_input(diff).raw_output()
diff --git a/stgit/lib/log.py b/stgit/lib/log.py
new file mode 100644 (file)
index 0000000..dfadd51
--- /dev/null
@@ -0,0 +1,524 @@
+r"""This module contains functions and classes for manipulating
+I{patch stack logs} (or just I{stack logs}).
+
+A stack log is a git branch. Each commit contains the complete state
+of the stack at the moment it was written; the most recent commit has
+the most recent state.
+
+For a branch C{I{foo}}, the stack log is stored in C{I{foo}.stgit}.
+Each log entry makes sure to have proper references to everything it
+needs, which means that it is safe against garbage collection -- you
+can even pull it from one repository to another.
+
+Stack log format (version 0)
+============================
+
+Version 0 was an experimental version of the stack log format; it is
+no longer supported.
+
+Stack log format (version 1)
+============================
+
+Commit message
+--------------
+
+The commit message is mostly for human consumption; in most cases it
+is just a subject line: the stg subcommand name and possibly some
+important command-line flag.
+
+An exception to this is log commits for undo and redo. Their subject
+line is "C{undo I{n}}" and "C{redo I{n}}"; the positive integer I{n}
+says how many steps were undone or redone.
+
+Tree
+----
+
+  - One blob, C{meta}, that contains the log data:
+
+      - C{Version:} I{n}
+
+        where I{n} must be 1. (Future versions of StGit might change
+        the log format; when this is done, this version number will be
+        incremented.)
+
+      - C{Previous:} I{sha1 or C{None}}
+
+        The commit of the previous log entry, or C{None} if this is
+        the first entry.
+
+      - C{Head:} I{sha1}
+
+        The current branch head.
+
+      - C{Applied:}
+
+        Marks the start of the list of applied patches. They are
+        listed in order, each on its own line: first one or more
+        spaces, then the patch name, then a colon, space, then the
+        patch's sha1.
+
+      - C{Unapplied:}
+
+        Same as C{Applied:}, but for the unapplied patches.
+
+      - C{Hidden:}
+
+        Same as C{Applied:}, but for the hidden patches.
+
+  - One subtree, C{patches}, that contains one blob per patch::
+
+      Bottom: <sha1 of patch's bottom tree>
+      Top:    <sha1 of patch's top tree>
+      Author: <author name and e-mail>
+      Date:   <patch timestamp>
+
+      <commit message>
+
+      ---
+
+      <patch diff>
+
+Following the message is a newline, three dashes, and another newline.
+Then come, each on its own line,
+
+Parents
+-------
+
+  - The first parent is the I{simplified log}, described below.
+
+  - The rest of the parents are just there to make sure that all the
+    commits referred to in the log entry -- patches, branch head,
+    previous log entry -- are ancestors of the log commit. (This is
+    necessary to make the log safe with regard to garbage collection
+    and pulling.)
+
+Simplified log
+--------------
+
+The simplified log is exactly like the full log, except that its only
+parent is the (simplified) previous log entry, if any. It's purpose is
+mainly ease of visualization."""
+
+import re
+from stgit.lib import git, stack as libstack
+from stgit import exception, utils
+from stgit.out import out
+import StringIO
+
+class LogException(exception.StgException):
+    pass
+
+class LogParseException(LogException):
+    pass
+
+def patch_file(repo, cd):
+    return repo.commit(git.BlobData(''.join(s + '\n' for s in [
+                    'Bottom: %s' % cd.parent.data.tree.sha1,
+                    'Top:    %s' % cd.tree.sha1,
+                    'Author: %s' % cd.author.name_email,
+                    'Date:   %s' % cd.author.date,
+                    '',
+                    cd.message,
+                    '',
+                    '---',
+                    '',
+                    repo.diff_tree(cd.parent.data.tree, cd.tree, ['-M']
+                                   ).strip()])))
+
+def log_ref(branch):
+    return 'refs/heads/%s.stgit' % branch
+
+class LogEntry(object):
+    __separator = '\n---\n'
+    __max_parents = 16
+    def __init__(self, repo, prev, head, applied, unapplied, hidden,
+                 patches, message):
+        self.__repo = repo
+        self.__prev = prev
+        self.__simplified = None
+        self.head = head
+        self.applied = applied
+        self.unapplied = unapplied
+        self.hidden = hidden
+        self.patches = patches
+        self.message = message
+    @property
+    def simplified(self):
+        if not self.__simplified:
+            self.__simplified = self.commit.data.parents[0]
+        return self.__simplified
+    @property
+    def prev(self):
+        if self.__prev != None and not isinstance(self.__prev, LogEntry):
+            self.__prev = self.from_commit(self.__repo, self.__prev)
+        return self.__prev
+    @property
+    def base(self):
+        if self.applied:
+            return self.patches[self.applied[0]].data.parent
+        else:
+            return self.head
+    @property
+    def top(self):
+        if self.applied:
+            return self.patches[self.applied[-1]]
+        else:
+            return self.head
+    all_patches = property(lambda self: (self.applied + self.unapplied
+                                         + self.hidden))
+    @classmethod
+    def from_stack(cls, prev, stack, message):
+        return cls(
+            repo = stack.repository,
+            prev = prev,
+            head = stack.head,
+            applied = list(stack.patchorder.applied),
+            unapplied = list(stack.patchorder.unapplied),
+            hidden = list(stack.patchorder.hidden),
+            patches = dict((pn, stack.patches.get(pn).commit)
+                           for pn in stack.patchorder.all),
+            message = message)
+    @staticmethod
+    def __parse_metadata(repo, metadata):
+        """Parse a stack log metadata string."""
+        if not metadata.startswith('Version:'):
+            raise LogParseException('Malformed log metadata')
+        metadata = metadata.splitlines()
+        version_str = utils.strip_prefix('Version:', metadata.pop(0)).strip()
+        try:
+            version = int(version_str)
+        except ValueError:
+            raise LogParseException(
+                'Malformed version number: %r' % version_str)
+        if version < 1:
+            raise LogException('Log is version %d, which is too old' % version)
+        if version > 1:
+            raise LogException('Log is version %d, which is too new' % version)
+        parsed = {}
+        for line in metadata:
+            if line.startswith(' '):
+                parsed[key].append(line.strip())
+            else:
+                key, val = [x.strip() for x in line.split(':', 1)]
+                if val:
+                    parsed[key] = val
+                else:
+                    parsed[key] = []
+        prev = parsed['Previous']
+        if prev == 'None':
+            prev = None
+        else:
+            prev = repo.get_commit(prev)
+        head = repo.get_commit(parsed['Head'])
+        lists = { 'Applied': [], 'Unapplied': [], 'Hidden': [] }
+        patches = {}
+        for lst in lists.keys():
+            for entry in parsed[lst]:
+                pn, sha1 = [x.strip() for x in entry.split(':')]
+                lists[lst].append(pn)
+                patches[pn] = repo.get_commit(sha1)
+        return (prev, head, lists['Applied'], lists['Unapplied'],
+                lists['Hidden'], patches)
+    @classmethod
+    def from_commit(cls, repo, commit):
+        """Parse a (full or simplified) stack log commit."""
+        message = commit.data.message
+        try:
+            perm, meta = commit.data.tree.data.entries['meta']
+        except KeyError:
+            raise LogParseException('Not a stack log')
+        (prev, head, applied, unapplied, hidden, patches
+         ) = cls.__parse_metadata(repo, meta.data.str)
+        lg = cls(repo, prev, head, applied, unapplied, hidden, patches, message)
+        lg.commit = commit
+        return lg
+    def __metadata_string(self):
+        e = StringIO.StringIO()
+        e.write('Version: 1\n')
+        if self.prev == None:
+            e.write('Previous: None\n')
+        else:
+            e.write('Previous: %s\n' % self.prev.commit.sha1)
+        e.write('Head: %s\n' % self.head.sha1)
+        for lst, title in [(self.applied, 'Applied'),
+                           (self.unapplied, 'Unapplied'),
+                           (self.hidden, 'Hidden')]:
+            e.write('%s:\n' % title)
+            for pn in lst:
+                e.write('  %s: %s\n' % (pn, self.patches[pn].sha1))
+        return e.getvalue()
+    def __parents(self):
+        """Return the set of parents this log entry needs in order to be a
+        descendant of all the commits it refers to."""
+        xp = set([self.head]) | set(self.patches[pn]
+                                    for pn in self.unapplied + self.hidden)
+        if self.applied:
+            xp.add(self.patches[self.applied[-1]])
+        if self.prev != None:
+            xp.add(self.prev.commit)
+            xp -= set(self.prev.patches.values())
+        return xp
+    def __tree(self, metadata):
+        if self.prev == None:
+            def pf(c):
+                return patch_file(self.__repo, c.data)
+        else:
+            prev_top_tree = self.prev.commit.data.tree
+            perm, prev_patch_tree = prev_top_tree.data.entries['patches']
+            # Map from Commit object to patch_file() results taken
+            # from the previous log entry.
+            c2b = dict((self.prev.patches[pn], pf) for pn, pf
+                       in prev_patch_tree.data.entries.iteritems())
+            def pf(c):
+                r = c2b.get(c, None)
+                if not r:
+                    r = patch_file(self.__repo, c.data)
+                return r
+        patches = dict((pn, pf(c)) for pn, c in self.patches.iteritems())
+        return self.__repo.commit(git.TreeData({
+                    'meta': self.__repo.commit(git.BlobData(metadata)),
+                    'patches': self.__repo.commit(git.TreeData(patches)) }))
+    def write_commit(self):
+        metadata = self.__metadata_string()
+        tree = self.__tree(metadata)
+        self.__simplified = self.__repo.commit(git.CommitData(
+                tree = tree, message = self.message,
+                parents = [prev.simplified for prev in [self.prev]
+                           if prev != None]))
+        parents = list(self.__parents())
+        while len(parents) >= self.__max_parents:
+            g = self.__repo.commit(git.CommitData(
+                    tree = tree, parents = parents[-self.__max_parents:],
+                    message = 'Stack log parent grouping'))
+            parents[-self.__max_parents:] = [g]
+        self.commit = self.__repo.commit(git.CommitData(
+                tree = tree, message = self.message,
+                parents = [self.simplified] + parents))
+
+def get_log_entry(repo, ref, commit):
+    try:
+        return LogEntry.from_commit(repo, commit)
+    except LogException, e:
+        raise LogException('While reading log from %s: %s' % (ref, e))
+
+def same_state(log1, log2):
+    """Check whether two log entries describe the same current state."""
+    s = [[lg.head, lg.applied, lg.unapplied, lg.hidden, lg.patches]
+         for lg in [log1, log2]]
+    return s[0] == s[1]
+
+def log_entry(stack, msg):
+    """Write a new log entry for the stack."""
+    ref = log_ref(stack.name)
+    try:
+        last_log_commit = stack.repository.refs.get(ref)
+    except KeyError:
+        last_log_commit = None
+    try:
+        if last_log_commit:
+            last_log = get_log_entry(stack.repository, ref, last_log_commit)
+        else:
+            last_log = None
+        new_log = LogEntry.from_stack(last_log, stack, msg)
+    except LogException, e:
+        out.warn(str(e), 'No log entry written.')
+        return
+    if last_log and same_state(last_log, new_log):
+        return
+    new_log.write_commit()
+    stack.repository.refs.set(ref, new_log.commit, msg)
+
+class Fakestack(object):
+    """Imitates a real L{Stack<stgit.lib.stack.Stack>}, but with the
+    topmost patch popped."""
+    def __init__(self, stack):
+        appl = list(stack.patchorder.applied)
+        unappl = list(stack.patchorder.unapplied)
+        hidd = list(stack.patchorder.hidden)
+        class patchorder(object):
+            applied = appl[:-1]
+            unapplied = [appl[-1]] + unappl
+            hidden = hidd
+            all = appl + unappl + hidd
+        self.patchorder = patchorder
+        class patches(object):
+            @staticmethod
+            def get(pn):
+                if pn == appl[-1]:
+                    class patch(object):
+                        commit = stack.patches.get(pn).old_commit
+                    return patch
+                else:
+                    return stack.patches.get(pn)
+        self.patches = patches
+        self.head = stack.head.data.parent
+        self.top = stack.top.data.parent
+        self.base = stack.base
+        self.name = stack.name
+        self.repository = stack.repository
+def compat_log_entry(msg):
+    """Write a new log entry. (Convenience function intended for use by
+    code not yet converted to the new infrastructure.)"""
+    repo = default_repo()
+    try:
+        stack = repo.get_stack(repo.current_branch_name)
+    except libstack.StackException, e:
+        out.warn(str(e), 'Could not write to stack log')
+    else:
+        if repo.default_index.conflicts() and stack.patchorder.applied:
+            log_entry(Fakestack(stack), msg)
+            log_entry(stack, msg + ' (CONFLICT)')
+        else:
+            log_entry(stack, msg)
+
+def delete_log(repo, branch):
+    ref = log_ref(branch)
+    if repo.refs.exists(ref):
+        repo.refs.delete(ref)
+
+def rename_log(repo, old_branch, new_branch, msg):
+    old_ref = log_ref(old_branch)
+    new_ref = log_ref(new_branch)
+    if repo.refs.exists(old_ref):
+        repo.refs.set(new_ref, repo.refs.get(old_ref), msg)
+        repo.refs.delete(old_ref)
+
+def copy_log(repo, src_branch, dst_branch, msg):
+    src_ref = log_ref(src_branch)
+    dst_ref = log_ref(dst_branch)
+    if repo.refs.exists(src_ref):
+        repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)
+
+def default_repo():
+    return libstack.Repository.default()
+
+def reset_stack(trans, iw, state):
+    """Reset the stack to a given previous state."""
+    for pn in trans.all_patches:
+        trans.patches[pn] = None
+    for pn in state.all_patches:
+        trans.patches[pn] = state.patches[pn]
+    trans.applied = state.applied
+    trans.unapplied = state.unapplied
+    trans.hidden = state.hidden
+    trans.base = state.base
+    trans.head = state.head
+
+def reset_stack_partially(trans, iw, state, only_patches):
+    """Reset the stack to a given previous state -- but only the given
+    patches, not anything else.
+
+    @param only_patches: Touch only these patches
+    @type only_patches: iterable"""
+    only_patches = set(only_patches)
+    patches_to_reset = set(state.all_patches) & only_patches
+    existing_patches = set(trans.all_patches)
+    original_applied_order = list(trans.applied)
+    to_delete = (existing_patches - patches_to_reset) & only_patches
+
+    # In one go, do all the popping we have to in order to pop the
+    # patches we're going to delete or modify.
+    def mod(pn):
+        if not pn in only_patches:
+            return False
+        if pn in to_delete:
+            return True
+        if trans.patches[pn] != state.patches.get(pn, None):
+            return True
+        return False
+    trans.pop_patches(mod)
+
+    # Delete and modify/create patches. We've previously popped all
+    # patches that we touch in this step.
+    trans.delete_patches(lambda pn: pn in to_delete)
+    for pn in patches_to_reset:
+        if pn in existing_patches:
+            if trans.patches[pn] == state.patches[pn]:
+                continue
+            else:
+                out.info('Resetting %s' % pn)
+        else:
+            if pn in state.hidden:
+                trans.hidden.append(pn)
+            else:
+                trans.unapplied.append(pn)
+            out.info('Resurrecting %s' % pn)
+        trans.patches[pn] = state.patches[pn]
+
+    # Push all the patches that we've popped, if they still
+    # exist.
+    pushable = set(trans.unapplied + trans.hidden)
+    for pn in original_applied_order:
+        if pn in pushable:
+            trans.push_patch(pn, iw)
+
+def undo_state(stack, undo_steps):
+    """Find the log entry C{undo_steps} steps in the past. (Successive
+    undo operations are supposed to "add up", so if we find other undo
+    operations along the way we have to add those undo steps to
+    C{undo_steps}.)
+
+    If C{undo_steps} is negative, redo instead of undo.
+
+    @return: The log entry that is the destination of the undo
+             operation
+    @rtype: L{LogEntry}"""
+    ref = log_ref(stack.name)
+    try:
+        commit = stack.repository.refs.get(ref)
+    except KeyError:
+        raise LogException('Log is empty')
+    log = get_log_entry(stack.repository, ref, commit)
+    while undo_steps != 0:
+        msg = log.message.strip()
+        um = re.match(r'^undo\s+(\d+)$', msg)
+        if undo_steps > 0:
+            if um:
+                undo_steps += int(um.group(1))
+            else:
+                undo_steps -= 1
+        else:
+            rm = re.match(r'^redo\s+(\d+)$', msg)
+            if um:
+                undo_steps += 1
+            elif rm:
+                undo_steps -= int(rm.group(1))
+            else:
+                raise LogException('No more redo information available')
+        if not log.prev:
+            raise LogException('Not enough undo information available')
+        log = log.prev
+    return log
+
+def log_external_mods(stack):
+    ref = log_ref(stack.name)
+    try:
+        log_commit = stack.repository.refs.get(ref)
+    except KeyError:
+        # No log exists yet.
+        log_entry(stack, 'start of log')
+        return
+    try:
+        log = get_log_entry(stack.repository, ref, log_commit)
+    except LogException:
+        # Something's wrong with the log, so don't bother.
+        return
+    if log.head == stack.head:
+        # No external modifications.
+        return
+    log_entry(stack, '\n'.join([
+                'external modifications', '',
+                'Modifications by tools other than StGit (e.g. git).']))
+
+def compat_log_external_mods():
+    try:
+        repo = default_repo()
+    except git.RepositoryException:
+        # No repository, so we couldn't log even if we wanted to.
+        return
+    try:
+        stack = repo.get_stack(repo.current_branch_name)
+    except exception.StgException:
+        # Stack doesn't exist, so we can't log.
+        return
+    log_external_mods(stack)
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
new file mode 100644 (file)
index 0000000..a72ee22
--- /dev/null
@@ -0,0 +1,265 @@
+"""A Python class hierarchy wrapping the StGit on-disk metadata."""
+
+import os.path
+from stgit import exception, utils
+from stgit.lib import git, stackupgrade
+from stgit.config import config
+
+class StackException(exception.StgException):
+    """Exception raised by L{stack} objects."""
+
+class Patch(object):
+    """Represents an StGit patch. This class is mainly concerned with
+    reading and writing the on-disk representation of a patch."""
+    def __init__(self, stack, name):
+        self.__stack = stack
+        self.__name = name
+    name = property(lambda self: self.__name)
+    @property
+    def __ref(self):
+        return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
+    @property
+    def __log_ref(self):
+        return self.__ref + '.log'
+    @property
+    def commit(self):
+        return self.__stack.repository.refs.get(self.__ref)
+    @property
+    def old_commit(self):
+        """Return the previous commit for this patch."""
+        fn = os.path.join(self.__compat_dir, 'top.old')
+        if not os.path.isfile(fn):
+            return None
+        return self.__stack.repository.get_commit(utils.read_string(fn))
+    @property
+    def __compat_dir(self):
+        return os.path.join(self.__stack.directory, 'patches', self.__name)
+    def __write_compat_files(self, new_commit, msg):
+        """Write files used by the old infrastructure."""
+        def write(name, val, multiline = False):
+            fn = os.path.join(self.__compat_dir, name)
+            if val:
+                utils.write_string(fn, val, multiline)
+            elif os.path.isfile(fn):
+                os.remove(fn)
+        def write_patchlog():
+            try:
+                old_log = [self.__stack.repository.refs.get(self.__log_ref)]
+            except KeyError:
+                old_log = []
+            cd = git.CommitData(tree = new_commit.data.tree, parents = old_log,
+                                message = '%s\t%s' % (msg, new_commit.sha1))
+            c = self.__stack.repository.commit(cd)
+            self.__stack.repository.refs.set(self.__log_ref, c, msg)
+            return c
+        d = new_commit.data
+        write('authname', d.author.name)
+        write('authemail', d.author.email)
+        write('authdate', d.author.date)
+        write('commname', d.committer.name)
+        write('commemail', d.committer.email)
+        write('description', d.message, multiline = True)
+        write('log', write_patchlog().sha1)
+        write('top', new_commit.sha1)
+        write('bottom', d.parent.sha1)
+        try:
+            old_top_sha1 = self.commit.sha1
+            old_bottom_sha1 = self.commit.data.parent.sha1
+        except KeyError:
+            old_top_sha1 = None
+            old_bottom_sha1 = None
+        write('top.old', old_top_sha1)
+        write('bottom.old', old_bottom_sha1)
+    def __delete_compat_files(self):
+        if os.path.isdir(self.__compat_dir):
+            for f in os.listdir(self.__compat_dir):
+                os.remove(os.path.join(self.__compat_dir, f))
+            os.rmdir(self.__compat_dir)
+        try:
+            # this compatibility log ref might not exist
+            self.__stack.repository.refs.delete(self.__log_ref)
+        except KeyError:
+            pass
+    def set_commit(self, commit, msg):
+        self.__write_compat_files(commit, msg)
+        self.__stack.repository.refs.set(self.__ref, commit, msg)
+    def delete(self):
+        self.__delete_compat_files()
+        self.__stack.repository.refs.delete(self.__ref)
+    def is_applied(self):
+        return self.name in self.__stack.patchorder.applied
+    def is_empty(self):
+        return self.commit.data.is_nochange()
+    def files(self):
+        """Return the set of files this patch touches."""
+        fs = set()
+        for (_, _, _, _, _, oldname, newname
+             ) in self.__stack.repository.diff_tree_files(
+            self.commit.data.tree, self.commit.data.parent.data.tree):
+            fs.add(oldname)
+            fs.add(newname)
+        return fs
+
+class PatchOrder(object):
+    """Keeps track of patch order, and which patches are applied.
+    Works with patch names, not actual patches."""
+    def __init__(self, stack):
+        self.__stack = stack
+        self.__lists = {}
+    def __read_file(self, fn):
+        return tuple(utils.read_strings(
+            os.path.join(self.__stack.directory, fn)))
+    def __write_file(self, fn, val):
+        utils.write_strings(os.path.join(self.__stack.directory, fn), val)
+    def __get_list(self, name):
+        if not name in self.__lists:
+            self.__lists[name] = self.__read_file(name)
+        return self.__lists[name]
+    def __set_list(self, name, val):
+        val = tuple(val)
+        if val != self.__lists.get(name, None):
+            self.__lists[name] = val
+            self.__write_file(name, val)
+    applied = property(lambda self: self.__get_list('applied'),
+                       lambda self, val: self.__set_list('applied', val))
+    unapplied = property(lambda self: self.__get_list('unapplied'),
+                         lambda self, val: self.__set_list('unapplied', val))
+    hidden = property(lambda self: self.__get_list('hidden'),
+                      lambda self, val: self.__set_list('hidden', val))
+    all = property(lambda self: self.applied + self.unapplied + self.hidden)
+    all_visible = property(lambda self: self.applied + self.unapplied)
+
+    @staticmethod
+    def create(stackdir):
+        """Create the PatchOrder specific files
+        """
+        utils.create_empty_file(os.path.join(stackdir, 'applied'))
+        utils.create_empty_file(os.path.join(stackdir, 'unapplied'))
+        utils.create_empty_file(os.path.join(stackdir, 'hidden'))
+
+class Patches(object):
+    """Creates L{Patch} objects. Makes sure there is only one such object
+    per patch."""
+    def __init__(self, stack):
+        self.__stack = stack
+        def create_patch(name):
+            p = Patch(self.__stack, name)
+            p.commit # raise exception if the patch doesn't exist
+            return p
+        self.__patches = git.ObjectCache(create_patch) # name -> Patch
+    def exists(self, name):
+        try:
+            self.get(name)
+            return True
+        except KeyError:
+            return False
+    def get(self, name):
+        return self.__patches[name]
+    def new(self, name, commit, msg):
+        assert not name in self.__patches
+        p = Patch(self.__stack, name)
+        p.set_commit(commit, msg)
+        self.__patches[name] = p
+        return p
+
+class Stack(git.Branch):
+    """Represents an StGit stack (that is, a git branch with some extra
+    metadata)."""
+    __repo_subdir = 'patches'
+
+    def __init__(self, repository, name):
+        git.Branch.__init__(self, repository, name)
+        self.__patchorder = PatchOrder(self)
+        self.__patches = Patches(self)
+        if not stackupgrade.update_to_current_format_version(repository, name):
+            raise StackException('%s: branch not initialized' % name)
+    patchorder = property(lambda self: self.__patchorder)
+    patches = property(lambda self: self.__patches)
+    @property
+    def directory(self):
+        return os.path.join(self.repository.directory, self.__repo_subdir, self.name)
+    @property
+    def base(self):
+        if self.patchorder.applied:
+            return self.patches.get(self.patchorder.applied[0]
+                                    ).commit.data.parent
+        else:
+            return self.head
+    @property
+    def top(self):
+        """Commit of the topmost patch, or the stack base if no patches are
+        applied."""
+        if self.patchorder.applied:
+            return self.patches.get(self.patchorder.applied[-1]).commit
+        else:
+            # When no patches are applied, base == head.
+            return self.head
+    def head_top_equal(self):
+        if not self.patchorder.applied:
+            return True
+        return self.head == self.patches.get(self.patchorder.applied[-1]).commit
+
+    def set_parents(self, remote, branch):
+        if remote:
+            self.set_parent_remote(remote)
+        if branch:
+            self.set_parent_branch(branch)
+
+    @classmethod
+    def initialise(cls, repository, name = None):
+        """Initialise a Git branch to handle patch series.
+
+        @param repository: The L{Repository} where the L{Stack} will be created
+        @param name: The name of the L{Stack}
+        """
+        if not name:
+            name = repository.current_branch_name
+        # make sure that the corresponding Git branch exists
+        git.Branch(repository, name)
+
+        dir = os.path.join(repository.directory, cls.__repo_subdir, name)
+        compat_dir = os.path.join(dir, 'patches')
+        if os.path.exists(dir):
+            raise StackException('%s: branch already initialized' % name)
+
+        # create the stack directory and files
+        utils.create_dirs(dir)
+        utils.create_dirs(compat_dir)
+        PatchOrder.create(dir)
+        config.set(stackupgrade.format_version_key(name),
+                   str(stackupgrade.FORMAT_VERSION))
+
+        return repository.get_stack(name)
+
+    @classmethod
+    def create(cls, repository, name,
+               create_at = None, parent_remote = None, parent_branch = None):
+        """Create and initialise a Git branch returning the L{Stack} object.
+
+        @param repository: The L{Repository} where the L{Stack} will be created
+        @param name: The name of the L{Stack}
+        @param create_at: The Git id used as the base for the newly created
+            Git branch
+        @param parent_remote: The name of the remote Git branch
+        @param parent_branch: The name of the parent Git branch
+        """
+        git.Branch.create(repository, name, create_at = create_at)
+        stack = cls.initialise(repository, name)
+        stack.set_parents(parent_remote, parent_branch)
+        return stack
+
+class Repository(git.Repository):
+    """A git L{Repository<git.Repository>} with some added StGit-specific
+    operations."""
+    def __init__(self, *args, **kwargs):
+        git.Repository.__init__(self, *args, **kwargs)
+        self.__stacks = {} # name -> Stack
+    @property
+    def current_stack(self):
+        return self.get_stack()
+    def get_stack(self, name = None):
+        if not name:
+            name = self.current_branch_name
+        if not name in self.__stacks:
+            self.__stacks[name] = Stack(self, name)
+        return self.__stacks[name]
diff --git a/stgit/lib/stackupgrade.py b/stgit/lib/stackupgrade.py
new file mode 100644 (file)
index 0000000..4b437dc
--- /dev/null
@@ -0,0 +1,105 @@
+import os.path
+from stgit import utils
+from stgit.out import out
+from stgit.config import config
+
+# The current StGit metadata format version.
+FORMAT_VERSION = 2
+
+def format_version_key(branch):
+    return 'branch.%s.stgit.stackformatversion' % branch
+
+def update_to_current_format_version(repository, branch):
+    """Update a potentially older StGit directory structure to the latest
+    version. Note: This function should depend as little as possible
+    on external functions that may change during a format version
+    bump, since it must remain able to process older formats."""
+
+    branch_dir = os.path.join(repository.directory, 'patches', branch)
+    key = format_version_key(branch)
+    old_key = 'branch.%s.stgitformatversion' % branch
+    def get_format_version():
+        """Return the integer format version number, or None if the
+        branch doesn't have any StGit metadata at all, of any version."""
+        fv = config.get(key)
+        ofv = config.get(old_key)
+        if fv:
+            # Great, there's an explicitly recorded format version
+            # number, which means that the branch is initialized and
+            # of that exact version.
+            return int(fv)
+        elif ofv:
+            # Old name for the version info: upgrade it.
+            config.set(key, ofv)
+            config.unset(old_key)
+            return int(ofv)
+        elif os.path.isdir(os.path.join(branch_dir, 'patches')):
+            # There's a .git/patches/<branch>/patches dirctory, which
+            # means this is an initialized version 1 branch.
+            return 1
+        elif os.path.isdir(branch_dir):
+            # There's a .git/patches/<branch> directory, which means
+            # this is an initialized version 0 branch.
+            return 0
+        else:
+            # The branch doesn't seem to be initialized at all.
+            return None
+    def set_format_version(v):
+        out.info('Upgraded branch %s to format version %d' % (branch, v))
+        config.set(key, '%d' % v)
+    def mkdir(d):
+        if not os.path.isdir(d):
+            os.makedirs(d)
+    def rm(f):
+        if os.path.exists(f):
+            os.remove(f)
+    def rm_ref(ref):
+        if repository.refs.exists(ref):
+            repository.refs.delete(ref)
+
+    # Update 0 -> 1.
+    if get_format_version() == 0:
+        mkdir(os.path.join(branch_dir, 'trash'))
+        patch_dir = os.path.join(branch_dir, 'patches')
+        mkdir(patch_dir)
+        refs_base = 'refs/patches/%s' % branch
+        for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
+                      + file(os.path.join(branch_dir, 'applied')).readlines()):
+            patch = patch.strip()
+            os.rename(os.path.join(branch_dir, patch),
+                      os.path.join(patch_dir, patch))
+            topfield = os.path.join(patch_dir, patch, 'top')
+            if os.path.isfile(topfield):
+                top = utils.read_string(topfield, False)
+            else:
+                top = None
+            if top:
+                repository.refs.set(refs_base + '/' + patch,
+                                    repository.get_commit(top), 'StGit upgrade')
+        set_format_version(1)
+
+    # Update 1 -> 2.
+    if get_format_version() == 1:
+        desc_file = os.path.join(branch_dir, 'description')
+        if os.path.isfile(desc_file):
+            desc = utils.read_string(desc_file)
+            if desc:
+                config.set('branch.%s.description' % branch, desc)
+            rm(desc_file)
+        rm(os.path.join(branch_dir, 'current'))
+        rm_ref('refs/bases/%s' % branch)
+        set_format_version(2)
+
+    # compatibility with the new infrastructure. The changes here do not
+    # affect the compatibility with the old infrastructure (format version 2)
+    if get_format_version() == 2:
+        hidden_file = os.path.join(branch_dir, 'hidden')
+        if not os.path.isfile(hidden_file):
+            utils.create_empty_file(hidden_file)
+
+    # Make sure we're at the latest version.
+    fv = get_format_version()
+    if not fv in [None, FORMAT_VERSION]:
+        raise StackException('Branch %s is at format version %d, expected %d'
+                             % (branch, fv, FORMAT_VERSION))
+    return fv != None # true if branch is initialized
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
new file mode 100644 (file)
index 0000000..54de127
--- /dev/null
@@ -0,0 +1,368 @@
+"""The L{StackTransaction} class makes it possible to make complex
+updates to an StGit stack in a safe and convenient way."""
+
+import atexit
+import itertools as it
+
+from stgit import exception, utils
+from stgit.utils import any, all
+from stgit.out import *
+from stgit.lib import git, log
+
+class TransactionException(exception.StgException):
+    """Exception raised when something goes wrong with a
+    L{StackTransaction}."""
+
+class TransactionHalted(TransactionException):
+    """Exception raised when a L{StackTransaction} stops part-way through.
+    Used to make a non-local jump from the transaction setup to the
+    part of the transaction code where the transaction is run."""
+
+def _print_current_patch(old_applied, new_applied):
+    def now_at(pn):
+        out.info('Now at patch "%s"' % pn)
+    if not old_applied and not new_applied:
+        pass
+    elif not old_applied:
+        now_at(new_applied[-1])
+    elif not new_applied:
+        out.info('No patch applied')
+    elif old_applied[-1] == new_applied[-1]:
+        pass
+    else:
+        now_at(new_applied[-1])
+
+class _TransPatchMap(dict):
+    """Maps patch names to sha1 strings."""
+    def __init__(self, stack):
+        dict.__init__(self)
+        self.__stack = stack
+    def __getitem__(self, pn):
+        try:
+            return dict.__getitem__(self, pn)
+        except KeyError:
+            return self.__stack.patches.get(pn).commit
+
+class StackTransaction(object):
+    """A stack transaction, used for making complex updates to an StGit
+    stack in one single operation that will either succeed or fail
+    cleanly.
+
+    The basic theory of operation is the following:
+
+      1. Create a transaction object.
+
+      2. Inside a::
+
+         try
+           ...
+         except TransactionHalted:
+           pass
+
+      block, update the transaction with e.g. methods like
+      L{pop_patches} and L{push_patch}. This may create new git
+      objects such as commits, but will not write any refs; this means
+      that in case of a fatal error we can just walk away, no clean-up
+      required.
+
+      (Some operations may need to touch your index and working tree,
+      though. But they are cleaned up when needed.)
+
+      3. After the C{try} block -- wheher or not the setup ran to
+      completion or halted part-way through by raising a
+      L{TransactionHalted} exception -- call the transaction's L{run}
+      method. This will either succeed in writing the updated state to
+      your refs and index+worktree, or fail without having done
+      anything."""
+    def __init__(self, stack, msg, discard_changes = False,
+                 allow_conflicts = False, allow_bad_head = False):
+        """Create a new L{StackTransaction}.
+
+        @param discard_changes: Discard any changes in index+worktree
+        @type discard_changes: bool
+        @param allow_conflicts: Whether to allow pre-existing conflicts
+        @type allow_conflicts: bool or function of L{StackTransaction}"""
+        self.__stack = stack
+        self.__msg = msg
+        self.__patches = _TransPatchMap(stack)
+        self.__applied = list(self.__stack.patchorder.applied)
+        self.__unapplied = list(self.__stack.patchorder.unapplied)
+        self.__hidden = list(self.__stack.patchorder.hidden)
+        self.__conflicting_push = None
+        self.__error = None
+        self.__current_tree = self.__stack.head.data.tree
+        self.__base = self.__stack.base
+        self.__discard_changes = discard_changes
+        self.__bad_head = None
+        self.__conflicts = None
+        if isinstance(allow_conflicts, bool):
+            self.__allow_conflicts = lambda trans: allow_conflicts
+        else:
+            self.__allow_conflicts = allow_conflicts
+        self.__temp_index = self.temp_index_tree = None
+        if not allow_bad_head:
+            self.__assert_head_top_equal()
+    stack = property(lambda self: self.__stack)
+    patches = property(lambda self: self.__patches)
+    def __set_applied(self, val):
+        self.__applied = list(val)
+    applied = property(lambda self: self.__applied, __set_applied)
+    def __set_unapplied(self, val):
+        self.__unapplied = list(val)
+    unapplied = property(lambda self: self.__unapplied, __set_unapplied)
+    def __set_hidden(self, val):
+        self.__hidden = list(val)
+    hidden = property(lambda self: self.__hidden, __set_hidden)
+    all_patches = property(lambda self: (self.__applied + self.__unapplied
+                                         + self.__hidden))
+    def __set_base(self, val):
+        assert (not self.__applied
+                or self.patches[self.applied[0]].data.parent == val)
+        self.__base = val
+    base = property(lambda self: self.__base, __set_base)
+    @property
+    def temp_index(self):
+        if not self.__temp_index:
+            self.__temp_index = self.__stack.repository.temp_index()
+            atexit.register(self.__temp_index.delete)
+        return self.__temp_index
+    @property
+    def top(self):
+        if self.__applied:
+            return self.__patches[self.__applied[-1]]
+        else:
+            return self.__base
+    def __get_head(self):
+        if self.__bad_head:
+            return self.__bad_head
+        else:
+            return self.top
+    def __set_head(self, val):
+        self.__bad_head = val
+    head = property(__get_head, __set_head)
+    def __assert_head_top_equal(self):
+        if not self.__stack.head_top_equal():
+            out.error(
+                'HEAD and top are not the same.',
+                'This can happen if you modify a branch with git.',
+                '"stg repair --help" explains more about what to do next.')
+            self.__abort()
+    def __checkout(self, tree, iw, allow_bad_head):
+        if not allow_bad_head:
+            self.__assert_head_top_equal()
+        if self.__current_tree == tree and not self.__discard_changes:
+            # No tree change, but we still want to make sure that
+            # there are no unresolved conflicts. Conflicts
+            # conceptually "belong" to the topmost patch, and just
+            # carrying them along to another patch is confusing.
+            if (self.__allow_conflicts(self) or iw == None
+                or not iw.index.conflicts()):
+                return
+            out.error('Need to resolve conflicts first')
+            self.__abort()
+        assert iw != None
+        if self.__discard_changes:
+            iw.checkout_hard(tree)
+        else:
+            iw.checkout(self.__current_tree, tree)
+        self.__current_tree = tree
+    @staticmethod
+    def __abort():
+        raise TransactionException(
+            'Command aborted (all changes rolled back)')
+    def __check_consistency(self):
+        remaining = set(self.all_patches)
+        for pn, commit in self.__patches.iteritems():
+            if commit == None:
+                assert self.__stack.patches.exists(pn)
+            else:
+                assert pn in remaining
+    def abort(self, iw = None):
+        # The only state we need to restore is index+worktree.
+        if iw:
+            self.__checkout(self.__stack.head.data.tree, iw,
+                            allow_bad_head = True)
+    def run(self, iw = None, set_head = True, allow_bad_head = False,
+            print_current_patch = True):
+        """Execute the transaction. Will either succeed, or fail (with an
+        exception) and do nothing."""
+        self.__check_consistency()
+        log.log_external_mods(self.__stack)
+        new_head = self.head
+
+        # Set branch head.
+        if set_head:
+            if iw:
+                try:
+                    self.__checkout(new_head.data.tree, iw, allow_bad_head)
+                except git.CheckoutException:
+                    # We have to abort the transaction.
+                    self.abort(iw)
+                    self.__abort()
+            self.__stack.set_head(new_head, self.__msg)
+
+        if self.__error:
+            if self.__conflicts:
+                out.error(*([self.__error] + self.__conflicts))
+            else:
+                out.error(self.__error)
+
+        # Write patches.
+        def write(msg):
+            for pn, commit in self.__patches.iteritems():
+                if self.__stack.patches.exists(pn):
+                    p = self.__stack.patches.get(pn)
+                    if commit == None:
+                        p.delete()
+                    else:
+                        p.set_commit(commit, msg)
+                else:
+                    self.__stack.patches.new(pn, commit, msg)
+            self.__stack.patchorder.applied = self.__applied
+            self.__stack.patchorder.unapplied = self.__unapplied
+            self.__stack.patchorder.hidden = self.__hidden
+            log.log_entry(self.__stack, msg)
+        old_applied = self.__stack.patchorder.applied
+        write(self.__msg)
+        if self.__conflicting_push != None:
+            self.__patches = _TransPatchMap(self.__stack)
+            self.__conflicting_push()
+            write(self.__msg + ' (CONFLICT)')
+        if print_current_patch:
+            _print_current_patch(old_applied, self.__applied)
+
+        if self.__error:
+            return utils.STGIT_CONFLICT
+        else:
+            return utils.STGIT_SUCCESS
+
+    def __halt(self, msg):
+        self.__error = msg
+        raise TransactionHalted(msg)
+
+    @staticmethod
+    def __print_popped(popped):
+        if len(popped) == 0:
+            pass
+        elif len(popped) == 1:
+            out.info('Popped %s' % popped[0])
+        else:
+            out.info('Popped %s -- %s' % (popped[-1], popped[0]))
+
+    def pop_patches(self, p):
+        """Pop all patches pn for which p(pn) is true. Return the list of
+        other patches that had to be popped to accomplish this. Always
+        succeeds."""
+        popped = []
+        for i in xrange(len(self.applied)):
+            if p(self.applied[i]):
+                popped = self.applied[i:]
+                del self.applied[i:]
+                break
+        popped1 = [pn for pn in popped if not p(pn)]
+        popped2 = [pn for pn in popped if p(pn)]
+        self.unapplied = popped1 + popped2 + self.unapplied
+        self.__print_popped(popped)
+        return popped1
+
+    def delete_patches(self, p, quiet = False):
+        """Delete all patches pn for which p(pn) is true. Return the list of
+        other patches that had to be popped to accomplish this. Always
+        succeeds."""
+        popped = []
+        all_patches = self.applied + self.unapplied + self.hidden
+        for i in xrange(len(self.applied)):
+            if p(self.applied[i]):
+                popped = self.applied[i:]
+                del self.applied[i:]
+                break
+        popped = [pn for pn in popped if not p(pn)]
+        self.unapplied = popped + [pn for pn in self.unapplied if not p(pn)]
+        self.hidden = [pn for pn in self.hidden if not p(pn)]
+        self.__print_popped(popped)
+        for pn in all_patches:
+            if p(pn):
+                s = ['', ' (empty)'][self.patches[pn].data.is_nochange()]
+                self.patches[pn] = None
+                if not quiet:
+                    out.info('Deleted %s%s' % (pn, s))
+        return popped
+
+    def push_patch(self, pn, iw = None):
+        """Attempt to push the named patch. If this results in conflicts,
+        halts the transaction. If index+worktree are given, spill any
+        conflicts to them."""
+        orig_cd = self.patches[pn].data
+        cd = orig_cd.set_committer(None)
+        oldparent = cd.parent
+        cd = cd.set_parent(self.top)
+        base = oldparent.data.tree
+        ours = cd.parent.data.tree
+        theirs = cd.tree
+        tree, self.temp_index_tree = self.temp_index.merge(
+            base, ours, theirs, self.temp_index_tree)
+        s = ''
+        merge_conflict = False
+        if not tree:
+            if iw == None:
+                self.__halt('%s does not apply cleanly' % pn)
+            try:
+                self.__checkout(ours, iw, allow_bad_head = False)
+            except git.CheckoutException:
+                self.__halt('Index/worktree dirty')
+            try:
+                iw.merge(base, ours, theirs)
+                tree = iw.index.write_tree()
+                self.__current_tree = tree
+                s = ' (modified)'
+            except git.MergeConflictException, e:
+                tree = ours
+                merge_conflict = True
+                self.__conflicts = e.conflicts
+                s = ' (conflict)'
+            except git.MergeException, e:
+                self.__halt(str(e))
+        cd = cd.set_tree(tree)
+        if any(getattr(cd, a) != getattr(orig_cd, a) for a in
+               ['parent', 'tree', 'author', 'message']):
+            comm = self.__stack.repository.commit(cd)
+            self.head = comm
+        else:
+            comm = None
+            s = ' (unmodified)'
+        if not merge_conflict and cd.is_nochange():
+            s = ' (empty)'
+        out.info('Pushed %s%s' % (pn, s))
+        def update():
+            if comm:
+                self.patches[pn] = comm
+            if pn in self.hidden:
+                x = self.hidden
+            else:
+                x = self.unapplied
+            del x[x.index(pn)]
+            self.applied.append(pn)
+        if merge_conflict:
+            # We've just caused conflicts, so we must allow them in
+            # the final checkout.
+            self.__allow_conflicts = lambda trans: True
+
+            # Save this update so that we can run it a little later.
+            self.__conflicting_push = update
+            self.__halt("%d merge conflict(s)" % len(self.__conflicts))
+        else:
+            # Update immediately.
+            update()
+
+    def reorder_patches(self, applied, unapplied, hidden, iw = None):
+        """Push and pop patches to attain the given ordering."""
+        common = len(list(it.takewhile(lambda (a, b): a == b,
+                                       zip(self.applied, applied))))
+        to_pop = set(self.applied[common:])
+        self.pop_patches(lambda pn: pn in to_pop)
+        for pn in applied[common:]:
+            self.push_patch(pn, iw)
+        assert self.applied == applied
+        assert set(self.unapplied + self.hidden) == set(unapplied + hidden)
+        self.unapplied = unapplied
+        self.hidden = hidden
index a03447f..e324179 100644 (file)
@@ -18,11 +18,11 @@ 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
+import sys, os, traceback
 
 import stgit.commands
 from stgit.out import *
+from stgit import argparse, run, utils
 
 #
 # The commands map
@@ -39,124 +39,22 @@ class Commands(dict):
         if not candidates:
             out.error('Unknown command: %s' % key,
                       'Try "%s help" for a list of supported commands' % prog)
-            sys.exit(1)
+            sys.exit(utils.STGIT_GENERAL_ERROR)
         elif len(candidates) > 1:
             out.error('Ambiguous command: %s' % key,
                       'Candidates are: %s' % ', '.join(candidates))
-            sys.exit(1)
+            sys.exit(utils.STGIT_GENERAL_ERROR)
 
         return candidates[0]
         
     def __getitem__(self, key):
-        """Return the command python module name based.
-        """
-        global prog
-
         cmd_mod = self.get(key) or self.get(self.canonical_cmd(key))
-            
-        __import__('stgit.commands.' + cmd_mod)
-        return getattr(stgit.commands, cmd_mod)
+        return stgit.commands.get_command(cmd_mod)
 
-commands = Commands({
-    'add':              'add',
-    'applied':          'applied',
-    'branch':           'branch',
-    'delete':           'delete',
-    'diff':             'diff',
-    'clean':            'clean',
-    'clone':            'clone',
-    'commit':           'commit',
-    'cp':              'copy',
-    'edit':             'edit',
-    'export':           'export',
-    'files':            'files',
-    'float':            'float',
-    'fold':             'fold',
-    'goto':             'goto',
-    'hide':             'hide',
-    'id':               'id',
-    'import':           'imprt',
-    'init':             'init',
-    'log':              'log',
-    'mail':             'mail',
-    'new':              'new',
-    'patches':          'patches',
-    'pick':             'pick',
-    'pop':              'pop',
-    'pull':             'pull',
-    'push':             'push',
-    'rebase':           'rebase',
-    'refresh':          'refresh',
-    'rename':           'rename',
-    'repair':           'repair',
-    'resolved':         'resolved',
-    'rm':               'rm',
-    'series':           'series',
-    'show':             'show',
-    'sink':             'sink',
-    'status':           'status',
-    'sync':             'sync',
-    'top':              'top',
-    'unapplied':        'unapplied',
-    'uncommit':         'uncommit',
-    'unhide':           'unhide'
-    })
+cmd_list = stgit.commands.get_commands()
+commands = Commands((cmd, mod) for cmd, (mod, kind, help)
+                    in cmd_list.iteritems())
 
-# classification: repository, stack, patch, working copy
-repocommands = (
-    'clone',
-    'id',
-    )
-stackcommands = (
-    'applied',
-    'branch',
-    'clean',
-    'commit',
-    'float',
-    'goto',
-    'hide',
-    'init',
-    'patches',
-    'pop',
-    'pull',
-    'push',
-    'rebase',
-    'repair',
-    'series',
-    'sink',
-    'top',
-    'unapplied',
-    'uncommit',
-    'unhide',
-    )
-patchcommands = (
-    'delete',
-    'edit',
-    'export',
-    'files',
-    'fold',
-    'import',
-    'log',
-    'mail',
-    'new',
-    'pick',
-    'refresh',
-    'rename',
-    'show',
-    'sync',
-    )
-wccommands = (
-    'add',
-    'cp',
-    'diff',
-    'resolved',
-    'rm',
-    'status',
-    )
-
-def _print_helpstring(cmd):
-    print '  ' + cmd + ' ' * (12 - len(cmd)) + commands[cmd].help
-    
 def print_help():
     print 'usage: %s <command> [options]' % os.path.basename(sys.argv[0])
     print
@@ -164,38 +62,13 @@ def print_help():
     print '  help        print the detailed command usage'
     print '  version     display version information'
     print '  copyright   display copyright information'
-    # unclassified commands if any
-    cmds = commands.keys()
-    cmds.sort()
-    for cmd in cmds:
-        if not cmd in repocommands and not cmd in stackcommands \
-               and not cmd in patchcommands and not cmd in wccommands:
-            _print_helpstring(cmd)
-    print
-
-    print 'Repository commands:'
-    for cmd in repocommands:
-        _print_helpstring(cmd)
-    print
-    
-    print 'Stack commands:'
-    for cmd in stackcommands:
-        _print_helpstring(cmd)
-    print
-
-    print 'Patch commands:'
-    for cmd in patchcommands:
-        _print_helpstring(cmd)
     print
-
-    print 'Working-copy commands:'
-    for cmd in wccommands:
-        _print_helpstring(cmd)
+    stgit.commands.pretty_command_list(cmd_list, sys.stdout)
 
 #
 # The main function (command dispatcher)
 #
-def main():
+def _main():
     """The main function
     """
     global prog
@@ -206,7 +79,7 @@ def main():
         print >> sys.stderr, 'usage: %s <command>' % prog
         print >> sys.stderr, \
               '  Try "%s --help" for a list of supported commands' % prog
-        sys.exit(1)
+        sys.exit(utils.STGIT_GENERAL_ERROR)
 
     cmd = sys.argv[1]
 
@@ -216,32 +89,31 @@ def main():
             sys.argv[2] = '--help'
         else:
             print_help()
-            sys.exit(0)
+            sys.exit(utils.STGIT_SUCCESS)
     if cmd == 'help':
         if len(sys.argv) == 3 and not sys.argv[2] in ['-h', '--help']:
             cmd = commands.canonical_cmd(sys.argv[2])
             if not cmd in commands:
                 out.error('%s help: "%s" command unknown' % (prog, cmd))
-                sys.exit(1)
+                sys.exit(utils.STGIT_GENERAL_ERROR)
 
             sys.argv[0] += ' %s' % cmd
             command = commands[cmd]
-            parser = OptionParser(usage = command.usage,
-                                  option_list = command.options)
+            parser = argparse.make_option_parser(command)
             from pydoc import pager
             pager(parser.format_help())
         else:
             print_help()
-        sys.exit(0)
+        sys.exit(utils.STGIT_SUCCESS)
     if cmd in ['-v', '--version', 'version']:
         from stgit.version import version
         print 'Stacked GIT %s' % version
         os.system('git --version')
         print 'Python version %s' % sys.version
-        sys.exit(0)
+        sys.exit(utils.STGIT_SUCCESS)
     if cmd in ['copyright']:
         print __copyright__
-        sys.exit(0)
+        sys.exit(utils.STGIT_SUCCESS)
 
     # re-build the command line arguments
     cmd = commands.canonical_cmd(cmd)
@@ -249,8 +121,7 @@ def main():
     del(sys.argv[1])
 
     command = commands[cmd]
-    usage = command.usage.split('\n')[0].strip()
-    parser = OptionParser(usage = usage, option_list = command.options)
+    parser = argparse.make_option_parser(command)
     options, args = parser.parse_args()
     directory = command.directory
 
@@ -265,7 +136,7 @@ def main():
         debug_level = int(os.environ.get('STGIT_DEBUG_LEVEL', 0))
     except ValueError:
         out.error('Invalid STGIT_DEBUG_LEVEL environment variable')
-        sys.exit(1)
+        sys.exit(utils.STGIT_GENERAL_ERROR)
 
     try:
         directory.setup()
@@ -278,14 +149,29 @@ def main():
             else:
                 command.crt_series = Series()
 
-        command.func(parser, options, args)
+        ret = command.func(parser, options, args)
     except (StgException, IOError, ParsingError, NoSectionError), err:
+        directory.write_log(cmd)
         out.error(str(err), title = '%s %s' % (prog, cmd))
         if debug_level > 0:
-            raise
-        else:
-            sys.exit(2)
+            traceback.print_exc()
+        sys.exit(utils.STGIT_COMMAND_ERROR)
+    except SystemExit:
+        # Triggered by the option parser when it finds bad commandline
+        # parameters.
+        sys.exit(utils.STGIT_COMMAND_ERROR)
     except KeyboardInterrupt:
-        sys.exit(1)
+        sys.exit(utils.STGIT_GENERAL_ERROR)
+    except:
+        out.error('Unhandled exception:')
+        traceback.print_exc()
+        sys.exit(utils.STGIT_BUG_ERROR)
 
-    sys.exit(0)
+    directory.write_log(cmd)
+    sys.exit(ret or utils.STGIT_SUCCESS)
+
+def main():
+    try:
+        _main()
+    finally:
+        run.finish_logging()
index d3c86b4..753c176 100644 (file)
@@ -17,10 +17,10 @@ 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
+import sys, textwrap
 
 class MessagePrinter(object):
-    def __init__(self):
+    def __init__(self, file = None):
         class Output(object):
             def __init__(self, write, flush):
                 self.write = write
@@ -49,6 +49,10 @@ class MessagePrinter(object):
                     self.at_start_of_line = False
             def tagged_lines(self, tag, lines):
                 tag += ': '
+                width = 79 - 2*self.level - len(tag)
+                lines = [wl for line in lines
+                         for wl in textwrap.wrap(line, width,
+                                                 break_long_words = False)]
                 for line in lines:
                     self.single_line(tag + line)
                     tag = ' '*len(tag)
@@ -64,9 +68,12 @@ class MessagePrinter(object):
                 self.new_line()
                 self.write(string)
                 self.at_start_of_line = string.endswith('\n')
-        self.__stderr = Output(sys.stderr.write, sys.stderr.flush)
-        self.__stdout = Output(sys.stdout.write, sys.stdout.flush)
-        if sys.stdout.isatty():
+        if file:
+            self.__stdout = self.__stderr = Output(file.write, file.flush)
+        else:
+            self.__stdout = Output(sys.stdout.write, sys.stdout.flush)
+            self.__stderr = Output(sys.stdout.write, sys.stdout.flush)
+        if file or sys.stdout.isatty():
             self.__out = self.__stdout
             self.__err = self.__stdout
         else:
index fa304d0..7493ed3 100644 (file)
@@ -27,12 +27,40 @@ class RunException(StgException):
     subprocess."""
     pass
 
-_all_log_modes = ['debug', 'profile']
-_log_mode = os.environ.get('STGIT_SUBPROCESS_LOG', '')
-if _log_mode and not _log_mode in _all_log_modes:
-    out.warn(('Unknown log mode "%s" specified in $STGIT_SUBPROCESS_LOG.'
-              % _log_mode),
-             'Valid values are: %s' % ', '.join(_all_log_modes))
+def get_log_mode(spec):
+    if not ':' in spec:
+        spec += ':'
+    (log_mode, outfile) = spec.split(':', 1)
+    all_log_modes = ['debug', 'profile']
+    if log_mode and not log_mode in all_log_modes:
+        out.warn(('Unknown log mode "%s" specified in $STGIT_SUBPROCESS_LOG.'
+                  % log_mode),
+                 'Valid values are: %s' % ', '.join(all_log_modes))
+    if outfile:
+        f = MessagePrinter(open(outfile, 'a'))
+    else:
+        f = out
+    return (log_mode, f)
+
+(_log_mode, _logfile) = get_log_mode(os.environ.get('STGIT_SUBPROCESS_LOG', ''))
+if _log_mode == 'profile':
+    _log_starttime = datetime.datetime.now()
+    _log_subproctime = 0.0
+
+def duration(t1, t2):
+    d = t2 - t1
+    return 86400*d.days + d.seconds + 1e-6*d.microseconds
+
+def finish_logging():
+    if _log_mode != 'profile':
+        return
+    ttime = duration(_log_starttime, datetime.datetime.now())
+    rtime = ttime - _log_subproctime
+    _logfile.info('Total time: %1.3f s' % ttime,
+                  'Time spent in subprocess calls: %1.3f s (%1.1f%%)'
+                  % (_log_subproctime, 100*_log_subproctime/ttime),
+                  'Remaining time: %1.3f s (%1.1f%%)'
+                  % (rtime, 100*rtime/ttime))
 
 class Run:
     exc = RunException
@@ -42,21 +70,32 @@ class Run:
             if type(c) != str:
                 raise Exception, 'Bad command: %r' % (cmd,)
         self.__good_retvals = [0]
-        self.__env = None
+        self.__env = self.__cwd = None
         self.__indata = None
         self.__discard_stderr = False
     def __log_start(self):
         if _log_mode == 'debug':
-            out.start('Running subprocess %s' % self.__cmd)
+            _logfile.start('Running subprocess %s' % self.__cmd)
+            if self.__cwd != None:
+                _logfile.info('cwd: %s' % self.__cwd)
+            if self.__env != None:
+                for k in sorted(self.__env.iterkeys()):
+                    if k not in os.environ or os.environ[k] != self.__env[k]:
+                        _logfile.info('%s: %s' % (k, self.__env[k]))
         elif _log_mode == 'profile':
-            out.start('Running subprocess %s' % self.__cmd[0])
+            _logfile.start('Running subprocess %s' % self.__cmd)
             self.__starttime = datetime.datetime.now()
     def __log_end(self, retcode):
+        global _log_subproctime, _log_starttime
         if _log_mode == 'debug':
-            out.done('return code: %d' % retcode)
+            _logfile.done('return code: %d' % retcode)
         elif _log_mode == 'profile':
-            duration = datetime.datetime.now() - self.__starttime
-            out.done('%1.3f s' % (duration.microseconds/1e6 + duration.seconds))
+            n = datetime.datetime.now()
+            d = duration(self.__starttime, n)
+            _logfile.done('%1.3f s' % d)
+            _log_subproctime += d
+            _logfile.info('Time since program start: %1.3f s'
+                          % duration(_log_starttime, n))
     def __check_exitcode(self):
         if self.__good_retvals == None:
             return
@@ -67,7 +106,7 @@ class Run:
         """Run with captured IO."""
         self.__log_start()
         try:
-            p = subprocess.Popen(self.__cmd, env = self.__env,
+            p = subprocess.Popen(self.__cmd, env = self.__env, cwd = self.__cwd,
                                  stdin = subprocess.PIPE,
                                  stdout = subprocess.PIPE,
                                  stderr = subprocess.PIPE)
@@ -85,7 +124,7 @@ class Run:
         assert self.__indata == None
         self.__log_start()
         try:
-            p = subprocess.Popen(self.__cmd, env = self.__env)
+            p = subprocess.Popen(self.__cmd, env = self.__env, cwd = self.__cwd)
             self.exitcode = p.wait()
         except OSError, e:
             raise self.exc('%s failed: %s' % (self.__cmd[0], e))
@@ -104,12 +143,18 @@ class Run:
         self.__env = dict(os.environ)
         self.__env.update(env)
         return self
+    def cwd(self, cwd):
+        self.__cwd = cwd
+        return self
     def raw_input(self, indata):
         self.__indata = indata
         return self
     def input_lines(self, lines):
         self.__indata = ''.join(['%s\n' % line for line in lines])
         return self
+    def input_nulterm(self, lines):
+        self.__indata = ''.join('%s\0' % line for line in lines)
+        return self
     def no_output(self):
         outdata = self.__run_io()
         if outdata:
index 802a382..9958e7a 100644 (file)
@@ -28,7 +28,7 @@ from stgit.run import *
 from stgit import git, basedir, templates
 from stgit.config import config
 from shutil import copyfile
-
+from stgit.lib import git as libgit, stackupgrade
 
 # stack exception class
 class StackException(StgException):
@@ -162,8 +162,6 @@ class Patch(StgitObject):
 
     def create(self):
         os.mkdir(self._dir())
-        self.create_empty_field('bottom')
-        self.create_empty_field('top')
 
     def delete(self, keep_log = False):
         if os.path.isdir(self._dir()):
@@ -198,47 +196,35 @@ class Patch(StgitObject):
 
     def __update_top_ref(self, ref):
         git.set_ref(self.__top_ref, ref)
+        self._set_field('top', ref)
+        self._set_field('bottom', git.get_commit(ref).get_parent())
 
     def __update_log_ref(self, ref):
         git.set_ref(self.__log_ref, ref)
 
-    def update_top_ref(self):
-        top = self.get_top()
-        if top:
-            self.__update_top_ref(top)
-
     def get_old_bottom(self):
-        return self._get_field('bottom.old')
+        return git.get_commit(self.get_old_top()).get_parent()
 
     def get_bottom(self):
-        return self._get_field('bottom')
-
-    def set_bottom(self, value, backup = False):
-        if backup:
-            curr = self._get_field('bottom')
-            self._set_field('bottom.old', curr)
-        self._set_field('bottom', value)
+        return git.get_commit(self.get_top()).get_parent()
 
     def get_old_top(self):
         return self._get_field('top.old')
 
     def get_top(self):
-        return self._get_field('top')
+        return git.rev_parse(self.__top_ref)
 
     def set_top(self, value, backup = False):
         if backup:
-            curr = self._get_field('top')
-            self._set_field('top.old', curr)
-        self._set_field('top', value)
+            curr_top = self.get_top()
+            self._set_field('top.old', curr_top)
+            self._set_field('bottom.old', git.get_commit(curr_top).get_parent())
         self.__update_top_ref(value)
 
     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)
+        if top:
             self.__update_top_ref(top)
             return True
         else:
@@ -296,9 +282,6 @@ class Patch(StgitObject):
         self._set_field('log', value)
         self.__update_log_ref(value)
 
-# The current StGIT metadata format version.
-FORMAT_VERSION = 2
-
 class PatchSet(StgitObject):
     def __init__(self, name = None):
         try:
@@ -366,7 +349,8 @@ class PatchSet(StgitObject):
     def is_initialised(self):
         """Checks if series is already initialised
         """
-        return bool(config.get(self.format_version_key()))
+        return config.get(stackupgrade.format_version_key(self.get_name())
+                          ) != None
 
 
 def shortlog(patches):
@@ -385,7 +369,8 @@ class Series(PatchSet):
 
         # Update the branch to the latest format version if it is
         # initialized, but don't touch it if it isn't.
-        self.update_to_current_format_version()
+        stackupgrade.update_to_current_format_version(
+            libgit.Repository.default(), self.get_name())
 
         self.__refs_base = 'refs/patches/%s' % self.get_name()
 
@@ -399,86 +384,6 @@ class Series(PatchSet):
         # trash directory
         self.__trash_dir = os.path.join(self._dir(), 'trash')
 
-    def format_version_key(self):
-        return 'branch.%s.stgit.stackformatversion' % self.get_name()
-
-    def update_to_current_format_version(self):
-        """Update a potentially older StGIT directory structure to the
-        latest version. Note: This function should depend as little as
-        possible on external functions that may change during a format
-        version bump, since it must remain able to process older formats."""
-
-        branch_dir = os.path.join(self._basedir(), 'patches', self.get_name())
-        def get_format_version():
-            """Return the integer format version number, or None if the
-            branch doesn't have any StGIT metadata at all, of any version."""
-            fv = config.get(self.format_version_key())
-            ofv = config.get('branch.%s.stgitformatversion' % self.get_name())
-            if fv:
-                # Great, there's an explicitly recorded format version
-                # number, which means that the branch is initialized and
-                # of that exact version.
-                return int(fv)
-            elif ofv:
-                # Old name for the version info, upgrade it
-                config.set(self.format_version_key(), ofv)
-                config.unset('branch.%s.stgitformatversion' % self.get_name())
-                return int(ofv)
-            elif os.path.isdir(os.path.join(branch_dir, 'patches')):
-                # There's a .git/patches/<branch>/patches dirctory, which
-                # means this is an initialized version 1 branch.
-                return 1
-            elif os.path.isdir(branch_dir):
-                # There's a .git/patches/<branch> directory, which means
-                # this is an initialized version 0 branch.
-                return 0
-            else:
-                # The branch doesn't seem to be initialized at all.
-                return None
-        def set_format_version(v):
-            out.info('Upgraded branch %s to format version %d' % (self.get_name(), v))
-            config.set(self.format_version_key(), '%d' % v)
-        def mkdir(d):
-            if not os.path.isdir(d):
-                os.makedirs(d)
-        def rm(f):
-            if os.path.exists(f):
-                os.remove(f)
-        def rm_ref(ref):
-            if git.ref_exists(ref):
-                git.delete_ref(ref)
-
-        # Update 0 -> 1.
-        if get_format_version() == 0:
-            mkdir(os.path.join(branch_dir, 'trash'))
-            patch_dir = os.path.join(branch_dir, 'patches')
-            mkdir(patch_dir)
-            refs_base = 'refs/patches/%s' % self.get_name()
-            for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
-                          + file(os.path.join(branch_dir, 'applied')).readlines()):
-                patch = patch.strip()
-                os.rename(os.path.join(branch_dir, patch),
-                          os.path.join(patch_dir, patch))
-                Patch(patch, patch_dir, refs_base).update_top_ref()
-            set_format_version(1)
-
-        # Update 1 -> 2.
-        if get_format_version() == 1:
-            desc_file = os.path.join(branch_dir, 'description')
-            if os.path.isfile(desc_file):
-                desc = read_string(desc_file)
-                if desc:
-                    config.set('branch.%s.description' % self.get_name(), desc)
-                rm(desc_file)
-            rm(os.path.join(branch_dir, 'current'))
-            rm_ref('refs/bases/%s' % self.get_name())
-            set_format_version(2)
-
-        # Make sure we're at the latest version.
-        if not get_format_version() in [None, FORMAT_VERSION]:
-            raise StackException('Branch %s is at format version %d, expected %d'
-                                 % (self.get_name(), get_format_version(), FORMAT_VERSION))
-
     def __patch_name_valid(self, name):
         """Raise an exception if the patch name is not valid.
         """
@@ -631,7 +536,8 @@ class Series(PatchSet):
         self.create_empty_field('applied')
         self.create_empty_field('unapplied')
 
-        config.set(self.format_version_key(), str(FORMAT_VERSION))
+        config.set(stackupgrade.format_version_key(self.get_name()),
+                   str(stackupgrade.FORMAT_VERSION))
 
     def rename(self, to_name):
         """Renames a series
@@ -762,6 +668,7 @@ class Series(PatchSet):
         config.remove_section('branch.%s.stgit' % self.get_name())
 
     def refresh_patch(self, files = None, message = None, edit = False,
+                      empty = False,
                       show_patch = False,
                       cache_update = True,
                       author_name = None, author_email = None,
@@ -803,9 +710,16 @@ class Series(PatchSet):
         if not bottom:
             bottom = patch.get_bottom()
 
+        if empty:
+            tree_id = git.get_commit(bottom).get_tree()
+        else:
+            tree_id = None
+
         commit_id = git.commit(files = files,
                                message = descr, parents = [bottom],
                                cache_update = cache_update,
+                               tree_id = tree_id,
+                               set_head = True,
                                allowempty = True,
                                author_name = author_name,
                                author_email = author_email,
@@ -813,7 +727,6 @@ class Series(PatchSet):
                                committer_name = committer_name,
                                committer_email = committer_email)
 
-        patch.set_bottom(bottom, backup = backup)
         patch.set_top(commit_id, backup = backup)
         patch.set_description(descr)
         patch.set_authname(author_name)
@@ -827,26 +740,6 @@ class Series(PatchSet):
 
         return commit_id
 
-    def undo_refresh(self):
-        """Undo the patch boundaries changes caused by 'refresh'
-        """
-        name = self.get_current()
-        assert(name)
-
-        patch = self.get_patch(name)
-        old_bottom = patch.get_old_bottom()
-        old_top = patch.get_old_top()
-
-        # the bottom of the patch is not changed by refresh. If the
-        # old_bottom is different, there wasn't any previous 'refresh'
-        # command (probably only a 'push')
-        if old_bottom != patch.get_bottom() or old_top == patch.get_top():
-            raise StackException, 'No undo information available'
-
-        git.reset(tree_id = old_top, check_out = False)
-        if patch.restore_old_boundaries():
-            self.log_patch(patch, 'undo')
-
     def new_patch(self, name, message = None, can_edit = True,
                   unapplied = False, show_patch = False,
                   top = None, bottom = None, commit = True,
@@ -927,11 +820,8 @@ class Series(PatchSet):
                                    committer_name = committer_name,
                                    committer_email = committer_email)
             # set the patch top to the new commit
-            patch.set_bottom(bottom)
             patch.set_top(commit_id)
         else:
-            assert top != bottom
-            patch.set_bottom(bottom)
             patch.set_top(top)
 
         self.log_patch(patch, 'new')
@@ -985,7 +875,6 @@ class Series(PatchSet):
             if head == bottom:
                 # reset the backup information. No logging since the
                 # patch hasn't changed
-                patch.set_bottom(head, backup = True)
                 patch.set_top(top, backup = True)
 
             else:
@@ -1013,7 +902,6 @@ class Series(PatchSet):
                                      committer_name = committer_name,
                                      committer_email = committer_email)
 
-                    patch.set_bottom(head, backup = True)
                     patch.set_top(top, backup = True)
 
                     self.log_patch(patch, 'push(f)')
@@ -1086,7 +974,6 @@ class Series(PatchSet):
         if head == bottom:
             # A fast-forward push. Just reset the backup
             # information. No need for logging
-            patch.set_bottom(bottom, backup = True)
             patch.set_top(top, backup = True)
 
             git.switch(top)
@@ -1109,11 +996,10 @@ class Series(PatchSet):
 
             # merge can fail but the patch needs to be pushed
             try:
-                git.merge(bottom, head, top, recursive = True)
+                git.merge_recursive(bottom, head, top)
             except git.GitException, ex:
                 out.error('The merge failed during "push".',
-                          'Use "refresh" after fixing the conflicts or'
-                          ' revert the operation with "push --undo".')
+                          'Revert the operation with "stg undo".')
 
         append_string(self.__applied_file, name)
 
@@ -1129,39 +1015,14 @@ class Series(PatchSet):
                 log = 'push'
             self.refresh_patch(bottom = head, cache_update = False, log = log)
         else:
-            # we store the correctly merged files only for
-            # tracking the conflict history. Note that the
-            # git.merge() operations should always leave the index
-            # in a valid state (i.e. only stage 0 files)
+            # we make the patch empty, with the merged state in the
+            # working tree.
             self.refresh_patch(bottom = head, cache_update = False,
-                               log = 'push(c)')
+                               empty = True, log = 'push(c)')
             raise StackException, str(ex)
 
         return modified
 
-    def undo_push(self):
-        name = self.get_current()
-        assert(name)
-
-        patch = self.get_patch(name)
-        old_bottom = patch.get_old_bottom()
-        old_top = patch.get_old_top()
-
-        # the top of the patch is changed by a push operation only
-        # together with the bottom (otherwise the top was probably
-        # modified by 'refresh'). If they are both unchanged, there
-        # was a fast forward
-        if old_bottom == patch.get_bottom() and old_top != patch.get_top():
-            raise StackException, 'No undo information available'
-
-        git.reset()
-        self.pop_patch(name)
-        ret = patch.restore_old_boundaries()
-        if ret:
-            self.log_patch(patch, 'undo')
-
-        return ret
-
     def pop_patch(self, name, keep = False):
         """Pops the top patch from the stack
         """
index a94257e..1fa96c2 100644 (file)
@@ -1,7 +1,7 @@
 """Common utility functions
 """
 
-import errno, optparse, os, os.path, re, sys
+import errno, os, os.path, re, sys
 from stgit.exception import *
 from stgit.config import config
 from stgit.out import *
@@ -170,25 +170,45 @@ def rename(basedir, file1, file2):
 class EditorException(StgException):
     pass
 
+def get_editor():
+    for editor in [os.environ.get('GIT_EDITOR'),
+                   config.get('stgit.editor'), # legacy
+                   config.get('core.editor'),
+                   os.environ.get('VISUAL'),
+                   os.environ.get('EDITOR'),
+                   'vi']:
+        if editor:
+            return editor
+
 def call_editor(filename):
     """Run the editor on the specified filename."""
-
-    # the editor
-    editor = config.get('stgit.editor')
-    if editor:
-        pass
-    elif 'EDITOR' in os.environ:
-        editor = os.environ['EDITOR']
-    else:
-        editor = 'vi'
-    editor += ' %s' % filename
-
-    out.start('Invoking the editor: "%s"' % editor)
-    err = os.system(editor)
+    cmd = '%s %s' % (get_editor(), filename)
+    out.start('Invoking the editor: "%s"' % cmd)
+    err = os.system(cmd)
     if err:
         raise EditorException, 'editor failed, exit code: %d' % err
     out.done()
 
+def edit_string(s, filename):
+    f = file(filename, 'w')
+    f.write(s)
+    f.close()
+    call_editor(filename)
+    f = file(filename)
+    s = f.read()
+    f.close()
+    os.remove(filename)
+    return s
+
+def find_patch_name(patchname, unacceptable):
+    """Find a patch name which is acceptable."""
+    if unacceptable(patchname):
+        suffix = 0
+        while unacceptable('%s-%d' % (patchname, suffix)):
+            suffix += 1
+        patchname = '%s-%d' % (patchname, suffix)
+    return patchname
+
 def patch_name_from_msg(msg):
     """Return a string to be used as a patch name. This is generated
     from the top line of the string passed as argument."""
@@ -209,12 +229,7 @@ def make_patch_name(msg, unacceptable, default_name = 'patch'):
     patchname = patch_name_from_msg(msg)
     if not patchname:
         patchname = default_name
-    if unacceptable(patchname):
-        suffix = 0
-        while unacceptable('%s-%d' % (patchname, suffix)):
-            suffix += 1
-        patchname = '%s-%d' % (patchname, suffix)
-    return patchname
+    return find_patch_name(patchname, unacceptable)
 
 # any and all functions are builtin in Python 2.5 and higher, but not
 # in 2.4.
@@ -231,21 +246,6 @@ if not 'all' in dir(__builtins__):
                 return False
         return True
 
-def make_sign_options():
-    def callback(option, opt_str, value, parser, sign_str):
-        if parser.values.sign_str not in [None, sign_str]:
-            raise optparse.OptionValueError(
-                '--ack and --sign were both specified')
-        parser.values.sign_str = sign_str
-    return [optparse.make_option('--sign', action = 'callback',
-                                 callback = callback, dest = 'sign_str',
-                                 callback_args = ('Signed-off-by',),
-                                 help = 'add Signed-off-by line'),
-            optparse.make_option('--ack', action = 'callback',
-                                 callback = callback, dest = 'sign_str',
-                                 callback_args = ('Acked-by',),
-                                 help = 'add Acked-by line')]
-
 def add_sign_line(desc, sign_str, name, email):
     if not sign_str:
         return desc
@@ -256,3 +256,38 @@ def add_sign_line(desc, sign_str, name, email):
     if not any(s in desc for s in ['\nSigned-off-by:', '\nAcked-by:']):
         desc = desc + '\n'
     return '%s\n%s\n' % (desc, sign_str)
+
+def parse_name_email(address):
+    """Return a tuple consisting of the name and email parsed from a
+    standard 'name <email>' or 'email (name)' string."""
+    address = re.sub(r'[\\"]', r'\\\g<0>', address)
+    str_list = re.findall(r'^(.*)\s*<(.*)>\s*$', address)
+    if not str_list:
+        str_list = re.findall(r'^(.*)\s*\((.*)\)\s*$', address)
+        if not str_list:
+            return None
+        return (str_list[0][1], str_list[0][0])
+    return str_list[0]
+
+def parse_name_email_date(address):
+    """Return a tuple consisting of the name, email and date parsed
+    from a 'name <email> date' string."""
+    address = re.sub(r'[\\"]', r'\\\g<0>', address)
+    str_list = re.findall('^(.*)\s*<(.*)>\s*(.*)\s*$', address)
+    if not str_list:
+        return None
+    return str_list[0]
+
+# Exit codes.
+STGIT_SUCCESS = 0        # everything's OK
+STGIT_GENERAL_ERROR = 1  # seems to be non-command-specific error
+STGIT_COMMAND_ERROR = 2  # seems to be a command that failed
+STGIT_CONFLICT = 3       # merge conflict, otherwise OK
+STGIT_BUG_ERROR = 4      # a bug in StGit
+
+def add_dict(d1, d2):
+    """Return a new dict with the contents of both d1 and d2. In case of
+    conflicting mappings, d2 takes precedence."""
+    d = dict(d1)
+    d.update(d2)
+    return d
index 15832ad..d57053d 100644 (file)
@@ -1,4 +1,63 @@
-version = '0.14.3'
+from stgit.exception import StgException
+from stgit import run, utils
+import os, os.path, re, sys
+
+class VersionUnavailable(StgException):
+    pass
+
+def git_describe_version():
+    path = sys.path[0]
+    try:
+        v = run.Run('git', 'describe', '--tags', '--abbrev=4'
+                    ).cwd(path).output_one_line()
+    except run.RunException, e:
+        raise VersionUnavailable(str(e))
+    if not re.match(r'^v[0-9]', v):
+        raise VersionUnavailable('%s: bad version' % v)
+    try:
+        dirty = run.Run('git', 'diff-index', '--name-only', 'HEAD'
+                        ).cwd(path).raw_output()
+    except run.RunException, e:
+        raise VersionUnavailable(str(e))
+    if dirty:
+        v += '-dirty'
+    return re.sub('-', '.', utils.strip_prefix('v', v))
+
+def builtin_version():
+    try:
+        import builtin_version as bv
+    except ImportError:
+        raise VersionUnavailable()
+    else:
+        return bv.version
+
+def _builtin_version_file(ext = 'py'):
+    return os.path.join(sys.path[0], 'stgit', 'builtin_version.%s' % ext)
+
+def write_builtin_version():
+    try:
+        v = git_describe_version()
+    except VersionUnavailable:
+        return
+    f = file(_builtin_version_file(), 'w')
+    f.write('# This file was generated automatically. Do not edit by hand.\n'
+            'version = %r\n' % v)
+
+def delete_builtin_version():
+    for ext in ['py', 'pyc', 'pyo']:
+        fn = _builtin_version_file(ext)
+        if os.path.exists(fn):
+            os.remove(fn)
+
+def get_version():
+    for v in [builtin_version, git_describe_version]:
+        try:
+            return v()
+        except VersionUnavailable:
+            pass
+    return 'unknown-version'
+
+version = get_version()
 
 # minimum version requirements
 git_min_ver = '1.5.2'
index ffb3c66..1e9510b 100644 (file)
--- a/t/README
+++ b/t/README
@@ -163,9 +163,9 @@ library for your script to use.
    yields success, test is considered a failure.
 
    This should _not_ be used for tests that succeed when their
-   commands fail -- use test_expect_success and shell negation (!) for
-   that. test_expect_failure is for cases when a test is known to be
-   broken.
+   commands fail -- use test_expect_success and one of general_error,
+   command_error, and conflict for that. test_expect_failure is for
+   cases when a test is known to be broken.
 
  - test_debug <script>
 
index 0eed3a4..3f7962a 100755 (executable)
@@ -18,30 +18,26 @@ test_expect_success 'Create a patch' \
    stg new foo -m "Add foo.txt" &&
    stg refresh'
 
-test_expect_success 'Old and new id with non-slashy branch' \
-  'stg id foo &&
-   stg id foo// &&
-   stg id foo/ &&
-   stg id foo//top &&
-   stg id foo/top &&
-   stg id foo@master &&
-   stg id foo@master//top &&
-   stg id foo@master/top'
+test_expect_success 'Try id with non-slashy branch' \
+  'stg id &&
+   stg id foo &&
+   stg id foo^ &&
+   stg id master:foo &&
+   stg id master:foo^'
 
 test_expect_success 'Clone branch to slashier name' \
   'stg branch --clone x/y/z'
 
-test_expect_success 'Try new form of id with slashy branch' \
+test_expect_success 'Try new id with slashy branch' \
   'stg id foo &&
-   stg id foo// &&
-   stg id foo//top &&
-   stg id foo@x/y/z &&
-   stg id foo@x/y/z//top'
+   stg id foo^ &&
+   stg id x/y/z:foo &&
+   stg id x/y/z:foo^'
 
 test_expect_success 'Try old id with slashy branch' '
-   ! stg id foo/ &&
-   ! stg id foo/top &&
-   ! stg id foo@x/y/z/top
+   command_error stg id foo/ &&
+   command_error stg id foo/top &&
+   command_error stg id foo@x/y/z/top
    '
 
 test_expect_success 'Create patch in slashy branch' \
@@ -51,11 +47,11 @@ test_expect_success 'Create patch in slashy branch' \
 
 test_expect_success 'Rename branches' \
   'stg branch --rename master goo/gaa &&
-   ! git show-ref --verify --quiet refs/heads/master &&
+   must_fail git show-ref --verify --quiet refs/heads/master &&
    stg branch --rename goo/gaa x1/x2/x3/x4 &&
-   ! git show-ref --verify --quiet refs/heads/goo/gaa &&
+   must_fail git show-ref --verify --quiet refs/heads/goo/gaa &&
    stg branch --rename x1/x2/x3/x4 servant &&
-   ! git show-ref --verify --quiet refs/heads/x1/x2/x3/x4
+   must_fail git show-ref --verify --quiet refs/heads/x1/x2/x3/x4
 '
 
 test_done
index a030739..ac92aa8 100755 (executable)
@@ -20,7 +20,7 @@ cat > expected.txt <<EOF
 EOF
 test_expect_success 'Run status on empty' '
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -29,7 +29,7 @@ EOF
 test_expect_success 'Status with an untracked file' '
     touch foo &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 rm -f foo
 
@@ -38,7 +38,7 @@ EOF
 test_expect_success 'Status with an empty directory' '
     mkdir foo &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -47,16 +47,16 @@ EOF
 test_expect_success 'Status with an untracked file in a subdir' '
     touch foo/bar &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
 A foo/bar
 EOF
 test_expect_success 'Status with an added file' '
-    stg add foo &&
+    git add foo &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -64,7 +64,7 @@ foo/bar
 EOF
 test_expect_success 'Status with an added file and -n option' '
     stg status -n > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -73,7 +73,7 @@ test_expect_success 'Status after refresh' '
     stg new -m "first patch" &&
     stg refresh &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -82,7 +82,7 @@ EOF
 test_expect_success 'Status after modification' '
     echo "wee" >> foo/bar &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -90,12 +90,12 @@ EOF
 test_expect_success 'Status after refresh' '
     stg new -m "second patch" && stg refresh &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 test_expect_success 'Add another file' '
     echo lajbans > fie &&
-    stg add fie &&
+    git add fie &&
     stg refresh
 '
 
@@ -110,12 +110,13 @@ cat > expected.txt <<EOF
 ? foo/bar.ancestor
 ? foo/bar.current
 ? foo/bar.patched
+A fie
 C foo/bar
 EOF
 test_expect_success 'Status after conflicting push' '
-    ! stg push &&
+    conflict_old stg push &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -123,7 +124,7 @@ C foo/bar
 EOF
 test_expect_success 'Status of file' '
     stg status foo/bar > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -131,32 +132,35 @@ C foo/bar
 EOF
 test_expect_success 'Status of dir' '
     stg status foo > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
+A fie
 EOF
 test_expect_success 'Status of other file' '
     stg status fie > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
+A fie
 M foo/bar
 EOF
 test_expect_success 'Status after resolving the push' '
     stg resolved -a &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
+A fie
 D foo/bar
 EOF
 test_expect_success 'Status after deleting a file' '
     rm foo/bar &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -165,10 +169,10 @@ EOF
 test_expect_success 'Status of disappeared newborn' '
     stg refresh &&
     touch foo/bar &&
-    stg add foo/bar &&
+    git add foo/bar &&
     rm foo/bar &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
 '
 
 cat > expected.txt <<EOF
@@ -179,7 +183,13 @@ test_expect_success 'Status after renaming a file' '
     git rm foo/bar &&
     git mv fie fay &&
     stg status > output.txt &&
-    diff -u expected.txt output.txt
+    test_cmp expected.txt output.txt
+'
+
+test_expect_success 'Status after renaming a file (with rename detection)' '
+    git config stgit.diff-opts -M &&
+    stg status > output.txt &&
+    test_cmp expected.txt output.txt
 '
 
 test_done
index 5a097a4..3fff3ee 100755 (executable)
@@ -26,7 +26,7 @@ test_expect_success \
 
 test_expect_success \
     'Try to create an stgit branch with a spurious patches/ entry' '
-    ! stg branch -c foo1
+    command_error stg branch -c foo1
 '
 
 test_expect_success \
@@ -43,7 +43,7 @@ test_expect_success \
 
 test_expect_success \
     'Try to create an stgit branch with an existing git branch by that name' '
-    ! stg branch -c foo2
+    command_error stg branch -c foo2
 '
 
 test_expect_success \
@@ -58,7 +58,7 @@ test_expect_success \
 test_expect_success \
     'Create an invalid refs/heads/ entry' '
     touch .git/refs/heads/foo3 &&
-    ! stg branch -c foo3
+    command_error stg branch -c foo3
 '
 
 test_expect_failure \
@@ -87,7 +87,7 @@ test_expect_success \
 
 test_expect_success \
     'Create branch down the stack, behind the conflict caused by the generated file' '
-    ! stg branch --create foo4 master^
+    command_error stg branch --create foo4 master^
 '
 
 test_expect_success \
index dd12132..d5d3aef 100755 (executable)
@@ -19,7 +19,7 @@ test_expect_success \
 
 test_expect_success \
     'Rename the current stgit branch' \
-    '! stg branch -r foo bar
+    'command_error stg branch -r foo bar
 '
 
 test_expect_success \
index b0087e9..1303b41 100755 (executable)
@@ -21,7 +21,7 @@ test_expect_success \
 test_expect_success \
     'Try to create a patch in a GIT branch' \
     '
-    ! stg new p0 -m "p0"
+    command_error stg new p0 -m "p0"
     '
 
 test_expect_success \
@@ -29,16 +29,16 @@ test_expect_success \
     '
     stg branch --clone foo &&
     stg new p1 -m "p1" &&
-    test $(stg applied -c) -eq 1
+    test $(stg series --applied -c) -eq 1
     '
 
 test_expect_success \
     'Clone the current StGIT branch' \
     '
     stg branch --clone bar &&
-    test $(stg applied -c) -eq 1 &&
+    test $(stg series --applied -c) -eq 1 &&
     stg new p2 -m "p2" &&
-    test $(stg applied -c) -eq 2
+    test $(stg series --applied -c) -eq 2
     '
 
 test_done
index 0be5d9b..826e41d 100755 (executable)
@@ -17,13 +17,13 @@ test_expect_success \
 test_expect_success \
     'Create a named patch' '
     stg new foo -m foobar &&
-    [ $(stg applied -c) -eq 1 ]
+    [ $(stg series --applied -c) -eq 1 ]
 '
 
 test_expect_success \
     'Create a patch without giving a name' '
     stg new -m yo &&
-    [ $(stg applied -c) -eq 2 ]
+    [ $(stg series --applied -c) -eq 2 ]
 '
 
 test_done
index 647c200..a591124 100755 (executable)
@@ -23,11 +23,11 @@ test_expect_success \
     stg clone foo bar &&
     (
         cd bar && stg new p1 -m p1 &&
-        printf "a\nc\n" > file && stg add file && stg refresh &&
+        printf "a\nc\n" > file && git add file && stg refresh &&
         stg new p2 -m p2 &&
         printf "a\nb\nc\n" > file && stg refresh &&
-        [ "$(echo $(stg applied))" = "p1 p2" ] &&
-        [ "$(echo $(stg unapplied))" = "" ]
+        [ "$(echo $(stg series --applied --noprefix))" = "p1 p2" ] &&
+        [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
     )
 '
 
@@ -36,7 +36,7 @@ test_expect_success \
     (
         cd foo &&
         GIT_DIR=../bar/.git git format-patch --stdout \
-          $(cd ../bar && stg id base@master)..HEAD | git am -3 -k
+          $(cd ../bar && stg id master:{base})..HEAD | git am -3 -k
     )
 '
 
@@ -49,16 +49,16 @@ test_expect_success \
 
 test_expect_success \
     'Attempt to push the first of those patches without --merged' \
-    "(cd bar && ! stg push
+    "(cd bar && conflict_old stg push
      )
 "
 
 test_expect_success \
     'Rollback the push' '
     (
-        cd bar && stg push --undo &&
-        [ "$(echo $(stg applied))" = "" ] &&
-        [ "$(echo $(stg unapplied))" = "p1 p2" ]
+        cd bar && stg undo --hard &&
+        [ "$(echo $(stg series --applied --noprefix))" = "" ] &&
+        [ "$(echo $(stg series --unapplied --noprefix))" = "p1 p2" ]
     )
 '
 
@@ -66,8 +66,8 @@ test_expect_success \
     'Push those patches while checking they were merged upstream' '
     (
         cd bar && stg push --merged --all
-        [ "$(echo $(stg applied))" = "p1 p2" ] &&
-        [ "$(echo $(stg unapplied))" = "" ]
+        [ "$(echo $(stg series --applied --noprefix))" = "p1 p2" ] &&
+        [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
     )
 '
 
index 805e805..d66ad26 100755 (executable)
@@ -30,7 +30,7 @@ test_expect_success \
     'Port those patches to orig tree' \
     '(cd foo &&
       GIT_DIR=../bar/.git git format-patch --stdout \
-          $(cd ../bar && stg id base@master)..HEAD |
+          $(cd ../bar && stg id master:{base})..HEAD |
       git am -3 -k
      )
     '
@@ -49,13 +49,13 @@ test_expect_success \
 
 test_expect_success \
     'Pull those patches applied upstream' \
-    "(cd bar && stg push --undo && stg push --all --merged
+    "(cd bar && stg undo && stg push --all --merged
      )
 "
 
 test_expect_success \
     'Check that all went well' \
-    "diff -u foo/file bar/file
+    "test_cmp foo/file bar/file
 "
 
 test_done
index edfa710..79439de 100755 (executable)
@@ -3,10 +3,10 @@
 # Copyright (c) 2006 Catalin Marinas
 #
 
-test_description='Exercise push --undo with missing files.
+test_description='Exercise stg undo with push of missing files.
 
 Test the case where a patch fails to be pushed because it modifies a
-missing file. The "push --undo" command has to be able to revert it.
+missing file. The "stg undo" command has to be able to revert it.
 '
 
 . ./test-lib.sh
@@ -21,7 +21,7 @@ test_expect_success \
        '
        stg new foo -m foo &&
        echo foo > test &&
-       stg add test &&
+       git add test &&
        stg refresh
        '
 
@@ -30,7 +30,7 @@ test_expect_success \
        '
        stg new bar -m bar &&
        echo bar > test &&
-       stg add test &&
+       git add test &&
        stg refresh
        '
 
@@ -43,28 +43,28 @@ test_expect_success \
 test_expect_success \
        'Push the second patch with conflict' \
        '
-       ! stg push bar
+       conflict_old stg push bar
        '
 
 test_expect_success \
        'Undo the previous push' \
        '
-       stg push --undo
+       stg undo --hard
        '
 
 test_expect_success \
        'Check the push after undo fails as well' \
        '
-       ! stg push bar
+       conflict_old stg push bar
        '
 
 test_expect_success \
        'Undo with disappeared newborn' \
        '
        touch newfile &&
-       stg add newfile &&
+       git add newfile &&
        rm newfile &&
-       stg push --undo
+       stg undo --hard
        '
 
 test_done
index 6e49b4d..e1ed577 100755 (executable)
@@ -12,22 +12,22 @@ test_expect_success \
     for i in 0 1 2 3 4 5 6 7 8 9; do
         stg new p$i -m p$i;
     done &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2 p3 p4 p5 p6 p7 p8 p9" ] &&
-    [ "$(echo $(stg unapplied))" = "" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2 p3 p4 p5 p6 p7 p8 p9" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
 '
 
 test_expect_success \
     'Pop half the patches' '
     stg pop -n 5 &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2 p3 p4" ] &&
-    [ "$(echo $(stg unapplied))" = "p5 p6 p7 p8 p9" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2 p3 p4" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p5 p6 p7 p8 p9" ]
 '
 
 test_expect_success \
     'Pop the remaining patches' '
     stg pop -a &&
-    [ "$(echo $(stg applied))" = "" ] &&
-    [ "$(echo $(stg unapplied))" = "p0 p1 p2 p3 p4 p5 p6 p7 p8 p9" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p0 p1 p2 p3 p4 p5 p6 p7 p8 p9" ]
 '
 
 test_done
diff --git a/t/t1203-push-conflict.sh b/t/t1203-push-conflict.sh
new file mode 100755 (executable)
index 0000000..96fee15
--- /dev/null
@@ -0,0 +1,70 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 David KÃ¥gedal
+#
+
+test_description='Exercise push conflicts.
+
+Test that the index has no modifications after a push with conflicts.
+'
+
+. ./test-lib.sh
+
+test_expect_success \
+       'Initialize the StGIT repository' \
+       'stg init
+'
+
+test_expect_success \
+       'Create the first patch' \
+       '
+       stg new foo -m foo &&
+       echo foo > test &&
+       echo fie > test2 &&
+       git add test test2 &&
+       stg refresh &&
+        stg pop
+       '
+
+test_expect_success \
+       'Create the second patch' \
+       '
+       stg new bar -m bar &&
+       echo bar > test &&
+       git add test &&
+       stg refresh
+       '
+
+test_expect_success \
+       'Push the first patch with conflict' \
+       '
+       conflict_old stg push foo
+       '
+
+test_expect_success \
+       'Show the, now empty, first patch' \
+       '
+       ! stg show foo | grep -q -e "^diff "
+       '
+
+test_expect_success \
+       'Check that the index has the non-conflict updates' \
+       '
+       git diff --cached --stat | grep -q -e "^ test2 | *1 "
+       '
+
+test_expect_success \
+       'Check that pop will fail while there are unmerged conflicts' \
+       '
+       command_error stg pop
+       '
+
+test_expect_success \
+       'Resolve the conflict' \
+       '
+       echo resolved > test &&
+       git add test &&
+       stg refresh
+       '
+
+test_done
index 40cd2a2..db473f2 100755 (executable)
@@ -8,11 +8,11 @@ test_expect_success 'Create a few patches' '
     for i in 0 1 2; do
         stg new p$i -m p$i &&
         echo "patch$i" >> patch$i.txt &&
-        stg add patch$i.txt &&
+        git add patch$i.txt &&
         stg refresh
     done &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
 '
 
 test_expect_success 'Make some non-conflicting local changes' '
@@ -21,8 +21,8 @@ test_expect_success 'Make some non-conflicting local changes' '
 
 test_expect_success 'Pop two patches, keeping local changes' '
     stg pop -n 2 --keep &&
-    [ "$(echo $(stg applied))" = "p0" ] &&
-    [ "$(echo $(stg unapplied))" = "p1 p2" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p0" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p1 p2" ] &&
     [ "$(echo $(ls patch?.txt))" = "patch0.txt" ] &&
     [ "$(echo $(cat patch0.txt))" = "patch0 local" ]
 '
@@ -34,8 +34,8 @@ test_expect_success 'Reset and push patches again' '
 
 test_expect_success 'Pop a patch without local changes' '
     stg pop --keep &&
-    [ "$(echo $(stg applied))" = "p0 p1" ] &&
-    [ "$(echo $(stg unapplied))" = "p2" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p2" ] &&
     [ "$(echo $(ls patch?.txt))" = "patch0.txt patch1.txt" ]
 '
 
index 54a5b89..f852762 100755 (executable)
@@ -9,11 +9,11 @@ test_expect_success 'Create some patches' '
         stg new p$i -m p$i &&
         echo x$i >> x.txt &&
         echo y$i >> foo/y.txt &&
-        stg add x.txt foo/y.txt &&
+        git add x.txt foo/y.txt &&
         stg refresh
     done &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
 '
 
 test_expect_success 'Fast-forward push from a subdir' '
@@ -33,7 +33,7 @@ test_expect_success 'Modifying push from a subdir' '
     [ "$(echo $(cat foo/y.txt))" = "y0 y1" ] &&
     stg new extra -m extra &&
     echo extra >> extra.txt &&
-    stg add extra.txt &&
+    git add extra.txt &&
     stg refresh &&
     cd foo &&
     stg push &&
@@ -47,7 +47,7 @@ test_expect_success 'Conflicting push from subdir' '
     [ "$(echo $(cat x.txt))" = "x0" ] &&
     [ "$(echo $(cat foo/y.txt))" = "y0" ] &&
     cd foo &&
-    ! stg push p2 &&
+    conflict_old stg push p2 &&
     cd .. &&
     [ "$(echo $(stg status --conflict))" = "foo/y.txt x.txt" ]
 '
@@ -57,12 +57,12 @@ test_expect_success 'Conflicting add/unknown file in subdir' '
     stg new foo -m foo &&
     mkdir d &&
     echo foo > d/test &&
-    stg add d/test &&
+    git add d/test &&
     stg refresh &&
     stg pop &&
     mkdir -p d &&
     echo bar > d/test &&
-    ! stg push foo &&
+    command_error stg push foo &&
     [ $(stg top) != "foo" ]
 '
 
diff --git a/t/t1206-push-hidden.sh b/t/t1206-push-hidden.sh
new file mode 100755 (executable)
index 0000000..20aa306
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+test_description='Test "stg push" with hidden patches'
+
+. ./test-lib.sh
+
+test_expect_success 'Initialize StGit stack' '
+    stg init &&
+    echo foo > foo.txt &&
+    git add foo.txt &&
+    stg new -m hidden-patch &&
+    stg refresh &&
+    stg pop &&
+    stg hide hidden-patch &&
+    test "$(echo $(stg series --all))" = "! hidden-patch"
+'
+
+test_expect_success 'Push an implicitly named hidden patch (should fail)' '
+    command_error stg push &&
+    test "$(echo $(stg series --all))" = "! hidden-patch"
+'
+
+test_expect_failure 'Push an explicitly named hidden patch (should work)' '
+    stg push hidden-patch &&
+    test "$(echo $(stg series --all))" = "> hidden-patch"
+'
+
+test_done
index 2e7ff21..43e0d04 100755 (executable)
@@ -19,7 +19,7 @@ test_expect_success \
        '
        stg new foo -m "Foo Patch" &&
        echo foo > test &&
-       stg add test &&
+       git add test &&
        stg refresh
        '
 
@@ -28,38 +28,38 @@ test_expect_success \
        '
        stg new bar -m "Bar Patch" &&
        echo bar > test &&
-       stg add test &&
+       git add test &&
        stg refresh
        '
 
 test_expect_success \
        'Commit the patches' \
        '
-       stg commit
+       stg commit --all
        '
 
 test_expect_success \
        'Uncommit the patches using names' \
        '
        stg uncommit bar foo &&
-       [ "$(stg id foo//top)" = "$(stg id bar//bottom)" ] &&
-       stg commit
+       [ "$(stg id foo)" = "$(stg id bar^)" ] &&
+       stg commit --all
        '
 
 test_expect_success \
        'Uncommit the patches using prefix' \
        '
        stg uncommit --number=2 foobar &&
-       [ "$(stg id foobar1//top)" = "$(stg id foobar2//bottom)" ] &&
-       stg commit
+       [ "$(stg id foobar1)" = "$(stg id foobar2^)" ] &&
+       stg commit --all
        '
 
 test_expect_success \
        'Uncommit the patches using auto names' \
        '
        stg uncommit --number=2 &&
-       [ "$(stg id foo-patch//top)" = "$(stg id bar-patch//bottom)" ] &&
-       stg commit
+       [ "$(stg id foo-patch)" = "$(stg id bar-patch^)" ] &&
+       stg commit --all
        '
 
 test_expect_success \
@@ -67,15 +67,31 @@ test_expect_success \
        '
        stg uncommit &&
        stg uncommit &&
-       [ "$(stg id foo-patch//top)" = "$(stg id bar-patch//bottom)" ] &&
-       stg commit
+       [ "$(stg id foo-patch)" = "$(stg id bar-patch^)" ] &&
+       stg commit --all
        '
 
 test_expect_success \
     'Uncommit the patches with --to' '
     stg uncommit --to HEAD^ &&
-    [ "$(stg id foo-patch//top)" = "$(stg id bar-patch//bottom)" ] &&
-    stg commit
+    [ "$(stg id foo-patch)" = "$(stg id bar-patch^)" ] &&
+    stg commit --all
+'
+
+test_expect_success 'Uncommit a commit with not precisely one parent' '
+    command_error stg uncommit -n 5  &&
+    [ "$(echo $(stg series))" = "" ]
+'
+
+# stg uncommit should work even when top != head, and should not touch
+# the head.
+test_expect_success 'Uncommit when top != head' '
+    stg new -m foo &&
+    git reset --hard HEAD^ &&
+    h=$(git rev-parse HEAD)
+    stg uncommit bar &&
+    test $(git rev-parse HEAD) = $h &&
+    test "$(echo $(stg series))" = "+ bar > foo"
 '
 
 test_done
index 5d9bdbd..8d5d4e5 100755 (executable)
@@ -5,7 +5,7 @@ test_description='Test the repair command.'
 
 test_expect_success \
     'Repair in a non-initialized repository' \
-    '! stg repair'
+    'command_error stg repair'
 
 test_expect_success \
     'Initialize the StGIT repository' \
@@ -20,7 +20,7 @@ test_expect_success \
     '
     stg new foo -m foo &&
     echo foo > foo.txt &&
-    stg add foo.txt &&
+    git add foo.txt &&
     stg refresh
     '
 
@@ -37,9 +37,9 @@ test_expect_success \
     '
 
 test_expect_success 'Turn one GIT commit into a patch' '
-    [ $(stg applied | wc -l) -eq 1 ] &&
+    [ $(stg series --applied -c) -eq 1 ] &&
     stg repair &&
-    [ $(stg applied | wc -l) -eq 2 ]
+    [ $(stg series --applied -c) -eq 2 ]
     '
 
 test_expect_success \
@@ -55,9 +55,9 @@ test_expect_success \
     '
 
 test_expect_success 'Turn three GIT commits into patches' '
-    [ $(stg applied | wc -l) -eq 2 ] &&
+    [ $(stg series --applied -c) -eq 2 ] &&
     stg repair &&
-    [ $(stg applied | wc -l) -eq 5 ]
+    [ $(stg series --applied -c) -eq 5 ]
     '
 
 test_expect_success \
@@ -72,9 +72,9 @@ test_expect_success \
     '
 
 test_expect_success 'Repair in the presence of a merge commit' '
-    [ $(stg applied | wc -l) -eq 5 ] &&
+    [ $(stg series --applied -c) -eq 5 ] &&
     stg repair &&
-    [ $(stg applied | wc -l) -eq 0 ]
+    [ $(stg series --applied -c) -eq 0 ]
 '
 
 test_done
index 910b23a..9fad3fa 100755 (executable)
@@ -21,39 +21,39 @@ test_expect_success 'Create five patches' '
     for i in 0 1 2 3 4; do
         stg new p$i -m p$i;
     done &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2 p3 p4" ] &&
-    [ "$(echo $(stg unapplied))" = "" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2 p3 p4" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
 '
 
 test_expect_success 'Pop two patches with git reset' '
     git reset --hard HEAD~2 &&
-    ! stg refresh &&
+    command_error stg refresh &&
     stg repair &&
     stg refresh &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "p3 p4" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p3 p4" ]
 '
 
 test_expect_success 'Create a new patch' '
     stg new q0 -m q0 &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2 q0" ] &&
-    [ "$(echo $(stg unapplied))" = "p3 p4" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2 q0" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p3 p4" ]
 '
 
 test_expect_success 'Go to an unapplied patch with with git reset' '
     git reset --hard $(stg id p3) &&
-    ! stg refresh &&
+    command_error stg refresh &&
     stg repair &&
     stg refresh &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "q0 p4" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "q0 p4" ]
 '
 
 test_expect_success 'Go back to below the stack base with git reset' '
     git reset --hard foo-tag &&
     stg repair &&
-    [ "$(echo $(stg applied))" = "" ] &&
-    [ "$(echo $(stg unapplied))" = "p0 p1 p2 p3 q0 p4" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p0 p1 p2 p3 q0 p4" ]
 '
 
 test_done
diff --git a/t/t1303-commit.sh b/t/t1303-commit.sh
new file mode 100755 (executable)
index 0000000..d53b9f2
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/sh
+test_description='Test stg commit'
+. ./test-lib.sh
+
+test_expect_success 'Initialize the StGIT repository' '
+    stg init
+'
+
+# stg commit with top != head should not succeed, since the committed
+# patches are poptentially lost.
+test_expect_success 'Commit when top != head (should fail)' '
+    stg new -m foo &&
+    git reset --hard HEAD^ &&
+    h=$(git rev-parse HEAD)
+    command_error stg commit &&
+    test $(git rev-parse HEAD) = $h &&
+    test "$(echo $(stg series))" = "> foo"
+'
+
+test_done
diff --git a/t/t1400-patch-history.sh b/t/t1400-patch-history.sh
deleted file mode 100755 (executable)
index 5b842d0..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2006 Catalin Marinas
-#
-
-test_description='Test the patch history generation.
-
-'
-
-. ./test-lib.sh
-
-test_expect_success \
-       'Initialize the StGIT repository' \
-       '
-       stg init
-       '
-
-test_expect_success \
-       'Create the first patch' \
-       '
-       stg new foo -m "Foo Patch" &&
-       echo foo > test && echo foo2 >> test &&
-       stg add test &&
-       stg refresh --annotate="foo notes"
-       '
-
-test_expect_success \
-       'Create the second patch' \
-       '
-       stg new bar -m "Bar Patch" &&
-       echo bar >> test &&
-       stg refresh
-       '
-
-test_expect_success \
-       'Check the "new" and "refresh" logs' \
-       '
-       stg log --full foo | grep -q -e "^new" &&
-       stg log --full foo | grep -q -e "^refresh" &&
-       stg log --full | grep -q -e "^new" &&
-       stg log --full | grep -q -e "^refresh"
-       '
-
-test_expect_success \
-       'Check the log annotation' \
-       '
-       stg log foo | grep -q -e    "\[refresh\] foo notes  " &&
-       stg log bar | grep -q -e    "\[refresh\]            " &&
-       stg refresh -p foo --annotate="foo notes 2" &&
-       stg log foo | grep -q -v -e "\[refresh\] foo notes  " &&
-       stg log foo | grep -q -e    "\[refresh\] foo notes 2"
-       '
-
-test_expect_success \
-       'Check the "push" log' \
-       '
-       stg pop &&
-       echo foo > test2 && stg add test2 && stg refresh &&
-       stg push &&
-       stg log --full | grep -q -e "^push    "
-       '
-
-test_expect_success \
-       'Check the "push(f)" log' \
-       '
-       stg pop &&
-       stg edit -m "Foo2 Patch" &&
-       stg push &&
-       stg log --full | grep -q -e "^push(f) "
-       '
-
-test_expect_success \
-       'Check the "push(m)" log' \
-       '
-       stg pop &&
-       echo foo2 > test && stg refresh &&
-       stg push &&
-       stg log --full | grep -q -e "^push(m) "
-       '
-
-test_expect_success \
-       'Check the "push(c)" log' \
-       '
-       echo bar > test && stg refresh &&
-       stg pop &&
-       echo foo > test && stg refresh &&
-       ! stg push &&
-       stg log --full | grep -q -e "^push(c) "
-       '
-
-test_expect_success \
-       'Check the push "undo" log' \
-       '
-       stg push --undo &&
-       stg log --full bar | grep -q -e "^undo    "
-       '
-
-test_expect_success \
-       'Check the refresh "undo" log' \
-       '
-       stg refresh --undo &&
-       stg log --full | grep -q -e "^undo    "
-       '
-
-test_done
index 814c9bd..e44af3a 100755 (executable)
@@ -12,45 +12,45 @@ test_description='Test floating a number of patches to the top of the stack
 test_expect_success \
        'Initialize the StGIT repository' \
        'stg init &&
-        stg new A -m "a" && echo A >a.txt && stg add a.txt && stg refresh &&
-        stg new B -m "b" && echo B >b.txt && stg add b.txt && stg refresh &&
-        stg new C -m "c" && echo C >c.txt && stg add c.txt && stg refresh &&
-        stg new D -m "d" && echo D >d.txt && stg add d.txt && stg refresh &&
-        stg new E -m "e" && echo E >e.txt && stg add e.txt && stg refresh &&
-        stg new F -m "f" && echo F >f.txt && stg add f.txt && stg refresh &&
-        stg new G -m "g" && echo G >g.txt && stg add g.txt && stg refresh &&
+        stg new A -m "a" && echo A >a.txt && git add a.txt && stg refresh &&
+        stg new B -m "b" && echo B >b.txt && git add b.txt && stg refresh &&
+        stg new C -m "c" && echo C >c.txt && git add c.txt && stg refresh &&
+        stg new D -m "d" && echo D >d.txt && git add d.txt && stg refresh &&
+        stg new E -m "e" && echo E >e.txt && git add e.txt && stg refresh &&
+        stg new F -m "f" && echo F >f.txt && git add f.txt && stg refresh &&
+        stg new G -m "g" && echo G >g.txt && git add g.txt && stg refresh &&
         stg pop &&
-        test "$(echo $(stg applied))" = "A B C D E F"
+        test "$(echo $(stg series --applied --noprefix))" = "A B C D E F"
        '
 
 test_expect_success \
        'Float A to top' \
        'stg float A &&
-        test "$(echo $(stg applied))" = "B C D E F A"
+        test "$(echo $(stg series --applied --noprefix))" = "B C D E F A"
        '
 test_expect_success \
        'Float A to top (noop)' \
        'stg float A &&
-        test "$(echo $(stg applied))" = "B C D E F A"
+        test "$(echo $(stg series --applied --noprefix))" = "B C D E F A"
        '
 test_expect_success \
        'Float B C to top' \
        'stg float B C &&
-        test "$(echo $(stg applied))" = "D E F A B C"
+        test "$(echo $(stg series --applied --noprefix))" = "D E F A B C"
        '
 test_expect_success \
        'Float E A to top' \
        'stg float E A &&
-        test "$(echo $(stg applied))" = "D F B C E A"
+        test "$(echo $(stg series --applied --noprefix))" = "D F B C E A"
        '
 test_expect_success \
        'Float E to top' \
        'stg float E &&
-        test "$(echo $(stg applied))" = "D F B C A E"
+        test "$(echo $(stg series --applied --noprefix))" = "D F B C A E"
        '
 test_expect_success \
        'Float G F to top' \
        'stg float G F &&
-        test "$(echo $(stg applied))" = "D B C A E G F"
+        test "$(echo $(stg series --applied --noprefix))" = "D B C A E G F"
        '
 test_done
index 2767c4c..516aa44 100755 (executable)
@@ -29,37 +29,37 @@ test_expect_success 'Initialize StGit stack' '
 '
 
 test_expect_success 'sink default without applied patches' '
-    ! stg sink
+    command_error stg sink
 '
 
 test_expect_success 'sink and reorder specified without applied patches' '
     stg sink p2 p1 &&
-    test "$(echo $(stg applied))" = "p2 p1"
+    test "$(echo $(stg series --applied --noprefix))" = "p2 p1"
 '
 
 test_expect_success 'sink patches to the bottom of the stack' '
     stg sink p4 p3 p2 &&
-    test "$(echo $(stg applied))" = "p4 p3 p2 p1"
+    test "$(echo $(stg series --applied --noprefix))" = "p4 p3 p2 p1"
 '
 
 test_expect_success 'sink current below a target' '
     stg sink --to=p2 &&
-    test "$(echo $(stg applied))" = "p4 p3 p1 p2"
+    test "$(echo $(stg series --applied --noprefix))" = "p4 p3 p1 p2"
 '
 
 test_expect_success 'bring patches forward' '
     stg sink --to=p2 p3 p4 &&
-    test "$(echo $(stg applied))" = "p1 p3 p4 p2"
+    test "$(echo $(stg series --applied --noprefix))" = "p1 p3 p4 p2"
 '
 
 test_expect_success 'sink specified patch below a target' '
     stg sink --to=p3 p2 &&
-    test "$(echo $(stg applied))" = "p1 p2 p3 p4"
+    test "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3 p4"
 '
 
 test_expect_success 'sink with conflict' '
-    ! stg sink --to=p2 p22 &&
-    test "$(echo $(stg applied))" = "p1 p22" &&
+    conflict_old stg sink --to=p2 p22 &&
+    test "$(echo $(stg series --applied --noprefix))" = "p1 p22" &&
     test "$(echo $(stg status -c))" = "f2"
 '
 
index df03d79..ef0b29d 100755 (executable)
@@ -12,34 +12,34 @@ test_expect_success \
     '
     stg new foo -m foo &&
     echo foo > foo.txt &&
-    stg add foo.txt &&
+    git add foo.txt &&
     stg refresh
     '
 
 test_expect_success \
     'Try to delete a non-existing patch' \
     '
-    [ $(stg applied | wc -l) -eq 1 ] &&
-    ! stg delete bar &&
-    [ $(stg applied | wc -l) -eq 1 ]
+    [ $(stg series --applied -c) -eq 1 ] &&
+    command_error stg delete bar &&
+    [ $(stg series --applied -c) -eq 1 ]
     '
 
 test_expect_success \
     'Try to delete the topmost patch while dirty' \
     '
     echo dirty >> foo.txt &&
-    [ $(stg applied | wc -l) -eq 1 ] &&
-    ! stg delete foo &&
-    [ $(stg applied | wc -l) -eq 1 ] &&
+    [ $(stg series --applied -c) -eq 1 ] &&
+    command_error stg delete foo &&
+    [ $(stg series --applied -c) -eq 1 ] &&
     git reset --hard
     '
 
 test_expect_success \
     'Delete the topmost patch' \
     '
-    [ $(stg applied | wc -l) -eq 1 ] &&
+    [ $(stg series --applied -c) -eq 1 ] &&
     stg delete foo &&
-    [ $(stg applied | wc -l) -eq 0 ]
+    [ $(stg series --applied -c) -eq 0 ]
     '
 
 test_expect_success \
@@ -47,7 +47,7 @@ test_expect_success \
     '
     stg new foo -m foo &&
     echo foo > foo.txt &&
-    stg add foo.txt &&
+    git add foo.txt &&
     stg refresh &&
     stg pop
     '
@@ -55,9 +55,9 @@ test_expect_success \
 test_expect_success \
     'Delete an unapplied patch' \
     '
-    [ $(stg unapplied | wc -l) -eq 1 ] &&
+    [ $(stg series --unapplied -c) -eq 1 ] &&
     stg delete foo &&
-    [ $(stg unapplied | wc -l) -eq 0 ]
+    [ $(stg series --unapplied -c) -eq 0 ]
     '
 
 test_expect_success \
@@ -65,20 +65,20 @@ test_expect_success \
     '
     stg new foo -m foo &&
     echo foo > foo.txt &&
-    stg add foo.txt &&
+    git add foo.txt &&
     stg refresh &&
     stg new bar -m bar &&
     echo bar > bar.txt &&
-    stg add bar.txt &&
+    git add bar.txt &&
     stg refresh
     '
 
 test_expect_success \
     'Try to delete a non-topmost applied patch' \
     '
-    [ $(stg applied | wc -l) -eq 2 ] &&
-    stg delete foo &&
-    [ $(stg applied | wc -l) -eq 2 ]
+    [ $(stg series --applied -c) -eq 2 ] &&
+    stg delete foo &&
+    [ $(stg series --applied -c) -eq 1 ]
     '
 
 test_expect_success \
@@ -87,23 +87,23 @@ test_expect_success \
     stg branch --create br &&
     stg new baz -m baz &&
     echo baz > baz.txt &&
-    stg add baz.txt &&
+    git add baz.txt &&
     stg refresh &&
     stg branch master &&
     stg new baz -m baz &&
     echo baz > baz.txt &&
-    stg add baz.txt &&
+    git add baz.txt &&
     stg refresh
     '
 
 test_expect_success \
     'Delete a patch in another branch' \
     '
-    [ $(stg applied | wc -l) -eq 3 ] &&
-    [ $(stg applied -b br | wc -l) -eq 1 ] &&
+    [ $(stg series --applied -c) -eq 2 ] &&
+    [ $(stg series --applied -b br -c) -eq 1 ] &&
     stg delete -b br baz &&
-    [ $(stg applied | wc -l) -eq 3 ] &&
-    [ $(stg applied -b br | wc -l) -eq 0 ]
+    [ $(stg series --applied -c) -eq 2 ] &&
+    [ $(stg series --applied -b br -c) -eq 0 ]
     '
 
 test_done
index 8eff308..cb7fb0d 100755 (executable)
@@ -12,7 +12,7 @@ test_expect_success \
     '
     stg new p0 -m p0 &&
     echo p0 > foo.txt &&
-    stg add foo.txt &&
+    git add foo.txt &&
     stg refresh &&
     for i in 1 2 3 4 5 6 7 8 9; do
         stg new p$i -m p$i &&
@@ -25,31 +25,31 @@ test_expect_success \
 test_expect_success \
     'Delete some patches' \
     '
-    [ "$(echo $(stg applied))" = "p0 p1 p2 p3 p4" ] &&
-    [ "$(echo $(stg unapplied))" = "p5 p6 p7 p8 p9" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2 p3 p4" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p5 p6 p7 p8 p9" ] &&
     stg delete p7 p6 p3 p4 &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "p5 p8 p9" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p5 p8 p9" ]
     '
 
 test_expect_success \
     'Delete some more patches, some of which do not exist' \
     '
-    [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "p5 p8 p9" ] &&
-    ! stg delete p7 p8 p2 p0 &&
-    [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "p5 p8 p9" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p5 p8 p9" ] &&
+    command_error stg delete p7 p8 p2 p0 &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p5 p8 p9" ]
     '
 
 test_expect_success \
     'Delete a range of patches' \
     '
-    [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "p5 p8 p9" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p5 p8 p9" ] &&
     stg delete p1..p8 &&
-    [ "$(echo $(stg applied))" = "p0" ] &&
-    [ "$(echo $(stg unapplied))" = "p9" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p9" ]
     '
 
 test_done
diff --git a/t/t1602-delete-spill.sh b/t/t1602-delete-spill.sh
new file mode 100755 (executable)
index 0000000..1ddec53
--- /dev/null
@@ -0,0 +1,47 @@
+#!/bin/sh
+test_description='Test "stg delete --spill"'
+. ./test-lib.sh
+
+test_expect_success 'Initialize the StGIT repository' '
+    stg init
+'
+
+test_expect_success 'Create five applied and three unapplied patches' '
+    for i in 0 1 2 3 4 5 6 7; do
+        echo $i >> foo &&
+        git add foo &&
+        git commit -m p$i
+    done
+    stg uncommit -n 8 &&
+    stg pop -n 3
+'
+
+test_expect_success 'Try to delete --spill an unapplied patch' '
+    command_error stg delete --spill p7 &&
+    test "$(echo $(stg series))" = "+ p0 + p1 + p2 + p3 > p4 - p5 - p6 - p7" &&
+    test "$(echo $(cat foo))" = "0 1 2 3 4" &&
+    test "$(echo $(git diff-files))" = ""
+'
+
+test_expect_success 'Try to delete --spill a non-top patch' '
+    command_error stg delete --spill p2 &&
+    test "$(echo $(stg series))" = "+ p0 + p1 + p2 + p3 > p4 - p5 - p6 - p7" &&
+    test "$(echo $(cat foo))" = "0 1 2 3 4" &&
+    test "$(echo $(git diff-files))" = ""
+'
+
+test_expect_success 'Delete --spill one patch' '
+    stg delete --spill p4 &&
+    test "$(echo $(stg series))" = "+ p0 + p1 + p2 > p3 - p5 - p6 - p7" &&
+    test "$(echo $(cat foo))" = "0 1 2 3 4" &&
+    test "$(echo $(git diff-files))" = ""
+'
+
+test_expect_success 'Delete --spill several patches' '
+    stg delete --spill p2 p3 p1 &&
+    test "$(echo $(stg series))" = "> p0 - p5 - p6 - p7" &&
+    test "$(echo $(cat foo))" = "0 1 2 3 4" &&
+    test "$(echo $(git diff-files))" = ""
+'
+
+test_done
index 618ebc7..8f2d44a 100755 (executable)
@@ -19,7 +19,7 @@ test_expect_success \
        '
        stg new foo -m "Foo Patch" &&
        echo foo > test &&
-       stg add test &&
+       git add test &&
        stg refresh
        '
 
diff --git a/t/t1701-goto-hidden.sh b/t/t1701-goto-hidden.sh
new file mode 100755 (executable)
index 0000000..a3c6e62
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+test_description='Test "stg goto" with hidden patches'
+
+. ./test-lib.sh
+
+test_expect_success 'Initialize StGit stack' '
+    stg init &&
+    echo foo > foo.txt &&
+    git add foo.txt &&
+    stg new -m hidden-patch &&
+    stg refresh &&
+    stg pop &&
+    stg hide hidden-patch &&
+    test "$(echo $(stg series --all))" = "! hidden-patch"
+'
+
+test_expect_success 'Refuse to go to a hidden patch' '
+    command_error stg goto hidden-patch &&
+    test "$(echo $(stg series --all))" = "! hidden-patch"
+'
+
+test_done
index 624e51c..ecaa593 100755 (executable)
@@ -91,4 +91,58 @@ test_expect_success \
     stg delete ..
     '
 
+test_expect_success \
+    'Apply a bzip2 patch created with "git diff"' \
+    '
+    bzip2 -c ../t1800-import/git-diff >../t1800-import/bzip2-git-diff &&
+    stg import ../t1800-import/bzip2-git-diff &&
+    [ $(git cat-file -p $(stg id) \
+        | grep -c "tree e96b1fba2160890ff600b675d7140d46b022b155") = 1 ] &&
+    rm ../t1800-import/bzip2-git-diff &&
+    stg delete .. 
+    '
+test_expect_success \
+    'Apply a bzip2 patch with a .bz2 suffix' \
+    '
+    bzip2 -c ../t1800-import/git-diff >../t1800-import/git-diff.bz2 &&
+    stg import ../t1800-import/git-diff.bz2 &&
+    [ $(git cat-file -p $(stg id) \
+        | grep -c "tree e96b1fba2160890ff600b675d7140d46b022b155") = 1 ] &&
+    rm ../t1800-import/git-diff.bz2 &&
+    stg delete .. 
+    '
+
+test_expect_success \
+    'Apply a gzip patch created with GNU diff' \
+    '
+    gzip -c ../t1800-import/gnu-diff >../t1800-import/gzip-gnu-diff &&
+    stg import ../t1800-import/gzip-gnu-diff &&
+    [ $(git cat-file -p $(stg id) \
+        | grep -c "tree e96b1fba2160890ff600b675d7140d46b022b155") = 1 ] &&
+    rm ../t1800-import/gzip-gnu-diff &&
+    stg delete ..
+    '
+test_expect_success \
+    'Apply a gzip patch with a .gz suffix' \
+    '
+    gzip -c ../t1800-import/gnu-diff >../t1800-import/gnu-diff.gz &&
+    stg import ../t1800-import/gnu-diff.gz &&
+    [ $(git cat-file -p $(stg id) \
+        | grep -c "tree e96b1fba2160890ff600b675d7140d46b022b155") = 1 ] &&
+    rm ../t1800-import/gnu-diff.gz &&
+    stg delete ..
+    '
+
+test_expect_success \
+    'apply a series from a tarball' \
+    '
+    rm -f jabberwocky.txt && touch jabberwocky.txt &&
+    git add jabberwocky.txt && git commit -m "empty file" jabberwocky.txt &&
+    (cd ../t1800-import; tar -cjf jabberwocky.tar.bz2 patches) &&
+    stg import --series ../t1800-import/jabberwocky.tar.bz2
+    [ $(git cat-file -p $(stg id) \
+        | grep -c "tree 2c33937252a21f1550c0bf21f1de534b68f69635") = 1 ] &&
+    rm ../t1800-import/jabberwocky.tar.bz2
+    '
+
 test_done
diff --git a/t/t1800-import/patches/attribution.patch b/t/t1800-import/patches/attribution.patch
new file mode 100644 (file)
index 0000000..2b7c8f9
--- /dev/null
@@ -0,0 +1,21 @@
+attribution
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    4 ++++
+ 1 files changed, 4 insertions(+), 0 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index 066d2e8..a9dd1f3 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -32,3 +32,7 @@ O frabjous day! Callooh! Callay!'
+   Did gyre and gimble in the wabe;
+ All mimsy were the borogoves,
+   And the mome raths outgrabe.
++
++      JABBERWOCKY
++      Lewis Carroll
++      (from Through the Looking-Glass and What Alice Found There, 1872) 
diff --git a/t/t1800-import/patches/delete-extra-lines.patch b/t/t1800-import/patches/delete-extra-lines.patch
new file mode 100644 (file)
index 0000000..e5b7a65
--- /dev/null
@@ -0,0 +1,22 @@
+delete extra lines
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    2 --
+ 1 files changed, 0 insertions(+), 2 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index 98cb716..066d2e8 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -28,8 +28,6 @@ He left it dead, and with its head
+ O frabjous day! Callooh! Callay!'
+   He chortled in his joy.
+-
+-
+ `Twas brillig, and the slithy toves
+   Did gyre and gimble in the wabe;
+ All mimsy were the borogoves,
diff --git a/t/t1800-import/patches/fifth-stanza.patch b/t/t1800-import/patches/fifth-stanza.patch
new file mode 100644 (file)
index 0000000..4f0e77c
--- /dev/null
@@ -0,0 +1,22 @@
+fifth stanza
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    5 +++++
+ 1 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index b1c2ad3..f1416dc 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -17,3 +17,8 @@ And, as in uffish thought he stood,
+   The Jabberwock, with eyes of flame,
+ Came whiffling through the tulgey wood,
+   And burbled as it came!
++
++One, two! One, two! And through and through
++  The vorpal blade went snicker-snack!
++He left it dead, and with its head
++  He went galumphing back.
diff --git a/t/t1800-import/patches/first-stanza.patch b/t/t1800-import/patches/first-stanza.patch
new file mode 100644 (file)
index 0000000..ee7818f
--- /dev/null
@@ -0,0 +1,18 @@
+first stanza
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    4 ++++
+ 1 files changed, 4 insertions(+), 0 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index e69de29..fba24dc 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -0,0 +1,4 @@
++`Twas brillig, and the slithy toves
++  Did gyre and gimble in the wabe:
++All mimsy were the borogoves,
++  And the mome raths outgrabe.
diff --git a/t/t1800-import/patches/fourth-stanza.patch b/t/t1800-import/patches/fourth-stanza.patch
new file mode 100644 (file)
index 0000000..eb2f8f2
--- /dev/null
@@ -0,0 +1,22 @@
+fourth stanza
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    5 +++++
+ 1 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index 6405f36..b1c2ad3 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -12,3 +12,8 @@ He took his vorpal sword in hand:
+   Long time the manxome foe he sought --
+ So rested he by the Tumtum tree,
+   And stood awhile in thought.
++
++And, as in uffish thought he stood,
++  The Jabberwock, with eyes of flame,
++Came whiffling through the tulgey wood,
++  And burbled as it came!
diff --git a/t/t1800-import/patches/second-stanza.patch b/t/t1800-import/patches/second-stanza.patch
new file mode 100644 (file)
index 0000000..bec1622
--- /dev/null
@@ -0,0 +1,22 @@
+second stanza
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    5 +++++
+ 1 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index fba24dc..9ed0b49 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -2,3 +2,8 @@
+   Did gyre and gimble in the wabe:
+ All mimsy were the borogoves,
+   And the mome raths outgrabe.
++
++"Beware the Jabberwock, my son!
++  The jaws that bite, the claws that catch!
++Beware the Jubjub bird, and shun
++  The frumious Bandersnatch!"
diff --git a/t/t1800-import/patches/series b/t/t1800-import/patches/series
new file mode 100644 (file)
index 0000000..5945c98
--- /dev/null
@@ -0,0 +1,10 @@
+# This series applies on GIT commit 6a8b6f6e2ecbcab26de7656b66b7f30eeba1ee96
+first-stanza.patch
+second-stanza.patch
+third-stanza.patch
+fourth-stanza.patch
+fifth-stanza.patch
+sixth-stanza.patch
+seventh-stanza.patch
+delete-extra-lines.patch
+attribution.patch
diff --git a/t/t1800-import/patches/seventh-stanza.patch b/t/t1800-import/patches/seventh-stanza.patch
new file mode 100644 (file)
index 0000000..555c200
--- /dev/null
@@ -0,0 +1,24 @@
+seventh stanza
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    7 +++++++
+ 1 files changed, 7 insertions(+), 0 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index bf732f5..98cb716 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -27,3 +27,10 @@ He left it dead, and with its head
+   Come to my arms, my beamish boy!
+ O frabjous day! Callooh! Callay!'
+   He chortled in his joy.
++
++
++
++`Twas brillig, and the slithy toves
++  Did gyre and gimble in the wabe;
++All mimsy were the borogoves,
++  And the mome raths outgrabe.
diff --git a/t/t1800-import/patches/sixth-stanza.patch b/t/t1800-import/patches/sixth-stanza.patch
new file mode 100644 (file)
index 0000000..2349b7e
--- /dev/null
@@ -0,0 +1,22 @@
+sixth stanza
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    5 +++++
+ 1 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index f1416dc..bf732f5 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -22,3 +22,8 @@ One, two! One, two! And through and through
+   The vorpal blade went snicker-snack!
+ He left it dead, and with its head
+   He went galumphing back.
++
++"And, has thou slain the Jabberwock?
++  Come to my arms, my beamish boy!
++O frabjous day! Callooh! Callay!'
++  He chortled in his joy.
diff --git a/t/t1800-import/patches/third-stanza.patch b/t/t1800-import/patches/third-stanza.patch
new file mode 100644 (file)
index 0000000..d942353
--- /dev/null
@@ -0,0 +1,22 @@
+third stanza
+
+From: Clark Williams <williams@redhat.com>
+
+
+---
+ jabberwocky.txt |    5 +++++
+ 1 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/jabberwocky.txt b/jabberwocky.txt
+index 9ed0b49..6405f36 100644
+--- a/jabberwocky.txt
++++ b/jabberwocky.txt
+@@ -7,3 +7,8 @@ All mimsy were the borogoves,
+   The jaws that bite, the claws that catch!
+ Beware the Jubjub bird, and shun
+   The frumious Bandersnatch!"
++
++He took his vorpal sword in hand:
++  Long time the manxome foe he sought --
++So rested he by the Tumtum tree,
++  And stood awhile in thought.
index cfdc6f3..cea6769 100755 (executable)
@@ -47,4 +47,37 @@ test_expect_success \
     [ "$t1" = "$t2" ]
     '
 
+test_expect_success \
+    'Check the To:, Cc: and Bcc: headers' \
+    '
+    stg mail --to=a@a --cc="b@b, c@c" --bcc=d@d $(stg top) -m \
+        -t ../../templates/patchmail.tmpl > mbox &&
+    test "$(cat mbox | grep -e "^To:")" = "To: a@a" &&
+    test "$(cat mbox | grep -e "^Cc:")" = "Cc: b@b, c@c" &&
+    test "$(cat mbox | grep -e "^Bcc:")" = "Bcc: d@d"
+    '
+
+test_expect_success \
+    'Check the --auto option' \
+    '
+    stg edit --sign &&
+    stg mail --to=a@a --cc="b@b, c@c" --bcc=d@d --auto $(stg top) -m \
+        -t ../../templates/patchmail.tmpl > mbox &&
+    test "$(cat mbox | grep -e "^To:")" = "To: a@a" &&
+    test "$(cat mbox | grep -e "^Cc:")" = \
+        "Cc: C O Mitter <committer@example.com>, b@b, c@c" &&
+    test "$(cat mbox | grep -e "^Bcc:")" = "Bcc: d@d"
+    '
+
+test_expect_success \
+    'Check the e-mail address duplicates' \
+    '
+    stg mail --to="a@a, b b <b@b>" --cc="b@b, c@c" \
+        --bcc="c@c, d@d, committer@example.com" --auto $(stg top) -m \
+        -t ../../templates/patchmail.tmpl > mbox &&
+    test "$(cat mbox | grep -e "^To:")" = "To: b b <b@b>, a@a" &&
+    test "$(cat mbox | grep -e "^Cc:")" = "Cc: c@c" &&
+    test "$(cat mbox | grep -e "^Bcc:")" = "Bcc: committer@example.com, d@d"
+    '
+
 test_done
index 484dbab..00ea7bd 100755 (executable)
@@ -18,39 +18,39 @@ test_expect_success \
     '
     stg new p1 -m p1 &&
     echo foo1 > foo1.txt &&
-    stg add foo1.txt &&
+    git add foo1.txt &&
     stg refresh &&
     stg new p2 -m p2 &&
     echo foo2 > foo2.txt &&
-    stg add foo2.txt &&
+    git add foo2.txt &&
     stg refresh &&
     stg new p3 -m p3 &&
     echo foo3 > foo3.txt &&
-    stg add foo3.txt &&
+    git add foo3.txt &&
     stg refresh &&
     stg export &&
     stg pop &&
-    [ "$(echo $(stg applied))" = "p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "p3" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p3" ]
     '
 
 test_expect_success \
     'Create a branch with empty patches' \
     '
-    stg branch -c foo base &&
+    stg branch -c foo {base} &&
     stg new p1 -m p1 &&
     stg new p2 -m p2 &&
     stg new p3 -m p3 &&
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
     '
 
 test_expect_success \
     'Synchronise second patch with the master branch' \
     '
     stg sync -B master p2 &&
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ] &&
     test $(cat foo2.txt) = "foo2"
     '
 
@@ -58,8 +58,8 @@ test_expect_success \
     'Synchronise the first two patches with the master branch' \
     '
     stg sync -B master -a &&
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ] &&
     test $(cat foo1.txt) = "foo1" &&
     test $(cat foo2.txt) = "foo2"
     '
@@ -68,8 +68,8 @@ test_expect_success \
     'Synchronise all the patches with the exported series' \
     '
     stg sync -s patches-master/series -a &&
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ] &&
     test $(cat foo1.txt) = "foo1" &&
     test $(cat foo2.txt) = "foo2" &&
     test $(cat foo3.txt) = "foo3"
@@ -79,20 +79,20 @@ test_expect_success \
     'Modify the master patches' \
     '
     stg branch master &&
-    [ "$(echo $(stg applied))" = "p1 p2" ] &&
-    [ "$(echo $(stg unapplied))" = "p3" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p3" ] &&
     stg goto p1 &&
     echo bar1 >> foo1.txt &&
     stg refresh &&
     stg goto p2 &&
     echo bar2 > bar2.txt &&
-    stg add bar2.txt &&
+    git add bar2.txt &&
     stg refresh &&
     stg goto p3 &&
     echo bar3 >> foo3.txt &&
     stg refresh &&
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ] &&
     stg export &&
     stg branch foo
     '
@@ -101,44 +101,44 @@ test_expect_success \
     'Synchronise second patch with the master branch' \
     '
     stg sync -B master p2 &&
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ] &&
     test $(cat bar2.txt) = "bar2"
     '
 
 test_expect_success \
     'Synchronise the first two patches with the master branch (to fail)' \
     '
-    ! stg sync -B master -a
+    conflict_old stg sync -B master -a
     '
 
 test_expect_success \
     'Restore the stack status after the failed sync' \
     '
-    [ "$(echo $(stg applied))" = "p1" ] &&
-    [ "$(echo $(stg unapplied))" = "p2 p3" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p2 p3" ] &&
     stg resolved -a &&
     stg refresh &&
     stg goto p3
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
     '
 
 test_expect_success \
     'Synchronise the third patch with the exported series (to fail)' \
     '
-    ! stg sync -s patches-master/series p3
+    conflict_old stg sync -s patches-master/series p3
     '
 
 test_expect_success \
     'Restore the stack status after the failed sync' \
     '
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ] &&
     stg resolved -a &&
     stg refresh &&
-    [ "$(echo $(stg applied))" = "p1 p2 p3" ] &&
-    [ "$(echo $(stg unapplied))" = "" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
     '
 
 test_done
index 07435f5..670c7c6 100755 (executable)
@@ -22,7 +22,7 @@ test_expect_success \
      git config branch.master.stgit.pull-policy fetch-rebase &&
      git config --list &&
      stg new c1 -m c1 &&
-     echo a > file && stg add file && stg refresh
+     echo a > file && git add file && stg refresh
     )
     '
 
@@ -30,7 +30,7 @@ test_expect_success \
     'Add non-rewinding commit upstream and pull it from clone' \
     '
     (cd upstream && stg new u1 -m u1 &&
-     echo a > file2 && stg add file2 && stg refresh) &&
+     echo a > file2 && git add file2 && stg refresh) &&
     (cd clone && stg pull) &&
     test -e clone/file2
     '
index 69b0fae..777ccb5 100755 (executable)
@@ -22,7 +22,7 @@ test_expect_success \
      git config branch.master.stgit.pull-policy pull &&
      git config --list &&
      stg new c1 -m c1 &&
-     echo a > file && stg add file && stg refresh
+     echo a > file && git add file && stg refresh
     )
     '
 
@@ -30,7 +30,7 @@ test_expect_success \
     'Add non-rewinding commit upstream and pull it from clone' \
     '
     (cd upstream && stg new u1 -m u1 &&
-     echo a > file2 && stg add file2 && stg refresh) &&
+     echo a > file2 && git add file2 && stg refresh) &&
     (cd clone && stg pull) &&
      test -e clone/file2
     '
@@ -43,7 +43,7 @@ test_expect_success \
     'Rewind/rewrite upstream commit and pull it from clone, without --merged' \
     '
     (cd upstream && echo b >> file2 && stg refresh) &&
-    (cd clone && ! stg pull)
+    (cd clone && conflict_old stg pull)
     '
 
 test_expect_success \
index 952ee7e..5619bda 100755 (executable)
@@ -16,14 +16,14 @@ test_expect_success \
     git config branch.stack.stgit.pull-policy rebase &&
     git config --list &&
     stg new c1 -m c1 &&
-    echo a > file && stg add file && stg refresh
+    echo a > file && git add file && stg refresh
     '
 
 test_expect_success \
     'Add non-rewinding commit in parent and pull the stack' \
     '
     stg branch parent && stg new u1 -m u1 &&
-    echo b > file2 && stg add file2 && stg refresh &&
+    echo b > file2 && git add file2 && stg refresh &&
     stg branch stack && stg pull &&
     test -e file2
     '
index ec2a104..adbf242 100755 (executable)
@@ -27,20 +27,20 @@ test_expect_success \
        'Rebase to previous commit' \
        '
        stg rebase master~1 &&
-       test `stg id base@stack` = `git rev-parse master~1` &&
-       test `stg applied | wc -l` = 1
+       test `stg id stack:{base}` = `git rev-parse master~1` &&
+       test `stg series --applied -c` = 1
        '
 
 test_expect_success \
        'Attempt rebase to non-existing commit' \
        '
-       ! stg rebase not-a-ref
+       command_error stg rebase not-a-ref
        '
 
 test_expect_success \
        'Check patches were re-applied' \
        '
-       test $(stg applied | wc -l) = 1
+       test $(stg series --applied -c) = 1
        '
 
 test_done
index 750e429..d731a11 100755 (executable)
@@ -4,11 +4,11 @@ test_description='Test the refresh command from a subdirectory'
 stg init
 
 test_expect_success 'Refresh from a subdirectory' '
-    stg new foo -m foo &&
+    stg new p0 -m p0 &&
     echo foo >> foo.txt &&
     mkdir bar &&
     echo bar >> bar/bar.txt &&
-    stg add foo.txt bar/bar.txt &&
+    git add foo.txt bar/bar.txt &&
     cd bar &&
     stg refresh &&
     cd .. &&
@@ -45,4 +45,31 @@ test_expect_success 'Refresh subdirectories recursively' '
     [ "$(stg status)" = "" ]
 '
 
+test_expect_success 'refresh -u' '
+    echo baz >> bar/baz.txt &&
+    stg new p1 -m p1 &&
+    git add bar/baz.txt &&
+    stg refresh --index &&
+    echo xyzzy >> foo.txt &&
+    echo xyzzy >> bar/bar.txt &&
+    echo xyzzy >> bar/baz.txt &&
+    stg refresh -u &&
+    test "$(echo $(stg status))" = "M bar/bar.txt M foo.txt"
+'
+
+test_expect_success 'refresh -u -p <subdir>' '
+    echo xyzzy >> bar/baz.txt &&
+    stg refresh -p p0 -u bar &&
+    test "$(echo $(stg status))" = "M bar/baz.txt M foo.txt"
+'
+
+test_expect_success 'refresh an unapplied patch' '
+    stg refresh -u &&
+    stg goto p0 &&
+    test "$(stg status)" = "M foo.txt" &&
+    stg refresh -p p1 &&
+    test "$(stg status)" = "" &&
+    test "$(echo $(stg files p1))" = "A bar/baz.txt M foo.txt"
+'
+
 test_done
index 3364c18..99fd29f 100755 (executable)
@@ -17,11 +17,28 @@ test_expect_success 'Initialize StGit stack' '
 '
 
 test_expect_success 'Clean empty patches' '
-    [ "$(echo $(stg applied))" = "e0 p0 e1" ] &&
-    [ "$(echo $(stg unapplied))" = "e2" ] &&
+    [ "$(echo $(stg series --applied --noprefix))" = "e0 p0 e1" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "e2" ] &&
     stg clean &&
-    [ "$(echo $(stg applied))" = "p0" ] &&
-    [ "$(echo $(stg unapplied))" = "" ]
+    [ "$(echo $(stg series --applied --noprefix))" = "p0" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
+'
+
+test_expect_success 'Create a conflict' '
+    stg new p1 -m p1 &&
+    echo bar > foo.txt &&
+    stg refresh &&
+    stg pop &&
+    stg new p2 -m p2
+    echo quux > foo.txt &&
+    stg refresh &&
+    conflict_old stg push
+'
+
+test_expect_success 'Make sure conflicting patches are preserved' '
+    stg clean &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p2 p1" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
 '
 
 test_done
diff --git a/t/t2600-coalesce.sh b/t/t2600-coalesce.sh
new file mode 100755 (executable)
index 0000000..9a043fd
--- /dev/null
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+test_description='Run "stg coalesce"'
+
+. ./test-lib.sh
+
+test_expect_success 'Initialize StGit stack' '
+    stg init &&
+    for i in 0 1 2 3; do
+        stg new p$i -m "foo $i" &&
+        echo "foo $i" >> foo.txt &&
+        git add foo.txt &&
+        stg refresh
+    done
+'
+
+test_expect_success 'Coalesce some patches' '
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ] &&
+    stg coalesce --name=q0 --message="wee woo" p1 p2 &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 q0 p3" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
+'
+
+test_expect_success 'Coalesce at stack top' '
+    stg coalesce --name=q1 --message="wee woo wham" q0 p3 &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p0 q1" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "" ]
+'
+
+cat > editor <<EOF
+#!/bin/sh
+echo "Editor was invoked" | tee editor-invoked
+EOF
+chmod a+x editor
+test_expect_success 'Coalesce with top != head' '
+    echo blahonga >> foo.txt &&
+    git commit -a -m "a new commit" &&
+    EDITOR=./editor command_error stg coalesce --name=r0 p0 q1 &&
+    test "$(echo $(stg series))" = "+ p0 > q1" &&
+    test ! -e editor-invoked
+'
+
+test_done
index ffac295..aad6d45 100755 (executable)
@@ -6,8 +6,10 @@ test_description='Run "stg refresh"'
 
 test_expect_success 'Initialize StGit stack' '
     stg init &&
-    echo expected.txt >> .git/info/exclude &&
+    echo expected*.txt >> .git/info/exclude &&
     echo patches.txt >> .git/info/exclude &&
+    echo show.txt >> .git/info/exclude &&
+    echo diff.txt >> .git/info/exclude &&
     stg new p0 -m "base" &&
     for i in 1 2 3; do
         echo base >> foo$i.txt &&
@@ -31,7 +33,7 @@ test_expect_success 'Refresh top patch' '
     stg status &&
     test -z "$(stg status)" &&
     stg patches foo3.txt > patches.txt &&
-    diff -u expected.txt patches.txt
+    test_cmp expected.txt patches.txt
 '
 
 cat > expected.txt <<EOF
@@ -45,7 +47,7 @@ test_expect_success 'Refresh middle patch' '
     stg status &&
     test -z "$(stg status)" &&
     stg patches foo2.txt > patches.txt &&
-    diff -u expected.txt patches.txt
+    test_cmp expected.txt patches.txt
 '
 
 cat > expected.txt <<EOF
@@ -59,7 +61,61 @@ test_expect_success 'Refresh bottom patch' '
     stg status &&
     test -z "$(stg status)" &&
     stg patches foo1.txt > patches.txt &&
-    diff -u expected.txt patches.txt
+    test_cmp expected.txt patches.txt
+'
+
+cat > expected.txt <<EOF
+p0
+p1
+p4
+EOF
+cat > expected2.txt <<EOF
+diff --git a/foo1.txt b/foo1.txt
+index 728535d..6f34984 100644
+--- a/foo1.txt
++++ b/foo1.txt
+@@ -1,3 +1,4 @@
+ base
+ foo 1
+ bar 1
++baz 1
+EOF
+cat > expected3.txt <<EOF
+diff --git a/foo1.txt b/foo1.txt
+index 6f34984..a80eb63 100644
+--- a/foo1.txt
++++ b/foo1.txt
+@@ -2,3 +2,4 @@ base
+ foo 1
+ bar 1
+ baz 1
++blah 1
+diff --git a/foo2.txt b/foo2.txt
+index 415c9f5..43168f2 100644
+--- a/foo2.txt
++++ b/foo2.txt
+@@ -1,3 +1,4 @@
+ base
+ foo 2
+ bar 2
++baz 2
+EOF
+test_expect_success 'Refresh --index' '
+    stg status &&
+    stg new p4 -m "refresh_index" &&
+    echo baz 1 >> foo1.txt &&
+    git add foo1.txt &&
+    echo blah 1 >> foo1.txt &&
+    echo baz 2 >> foo2.txt &&
+    stg refresh --index &&
+    stg patches foo1.txt > patches.txt &&
+    git diff HEAD^..HEAD > show.txt &&
+    stg diff > diff.txt &&
+    test_cmp expected.txt patches.txt &&
+    test_cmp expected2.txt show.txt &&
+    test_cmp expected3.txt diff.txt &&
+    stg new p5 -m "cleanup again" &&
+    stg refresh
 '
 
 test_expect_success 'Refresh moved files' '
index d42e90f..ed4bac4 100755 (executable)
@@ -29,18 +29,18 @@ EOF
 cat > expected2.txt <<EOF
 A 2.txt
 EOF
-test_expect_failure 'Add new file to non-top patch' '
+test_expect_success 'Add new file to non-top patch' '
     stg status > status1.txt &&
-    diff -u expected0.txt status1.txt &&
+    test_cmp expected0.txt status1.txt &&
     echo y > new.txt &&
     git add new.txt &&
     stg refresh -p p1 &&
     stg status > status2.txt &&
-    diff -u expected0.txt status2.txt &&
+    test_cmp expected0.txt status2.txt &&
     stg files p1 > files1.txt &&
-    diff -u expected1.txt files1.txt &&
+    test_cmp expected1.txt files1.txt &&
     stg files p2 > files2.txt &&
-    diff -u expected2.txt files2.txt
+    test_cmp expected2.txt files2.txt
 '
 
 test_done
diff --git a/t/t2702-refresh-rm.sh b/t/t2702-refresh-rm.sh
new file mode 100755 (executable)
index 0000000..0362cc6
--- /dev/null
@@ -0,0 +1,101 @@
+#!/bin/sh
+
+test_description='"stg refresh" with removed files'
+
+. ./test-lib.sh
+
+# Ignore our own temp files.
+cat >> .git/info/exclude <<EOF
+expected*.txt
+files*.txt
+status*.txt
+EOF
+
+reset () {
+    stg pop -a > /dev/null
+    git reset --hard > /dev/null
+}
+
+test_expect_success 'Initialize StGit stack' '
+    stg init &&
+    echo x > x.txt &&
+    echo y > y.txt &&
+    git add x.txt y.txt &&
+    git commit -m "Add some files"
+'
+
+cat > expected0.txt <<EOF
+D y.txt
+EOF
+printf '' > expected1.txt
+test_expect_success 'git rm a file' '
+    stg new -m p0 &&
+    git rm y.txt &&
+    stg status > status0.txt &&
+    test_cmp expected0.txt status0.txt &&
+    stg refresh &&
+    stg status > status1.txt &&
+    test_cmp expected1.txt status1.txt &&
+    stg files | sort > files.txt &&
+    test_cmp expected0.txt files.txt
+'
+
+reset
+
+cat > expected0.txt <<EOF
+D y.txt
+M x.txt
+EOF
+printf '' > expected1.txt
+test_expect_success 'git rm a file together with other changes' '
+    stg new -m p1 &&
+    echo x2 >> x.txt &&
+    git rm y.txt &&
+    stg status > status0.txt &&
+    test_cmp expected0.txt status0.txt &&
+    stg refresh &&
+    stg status > status1.txt &&
+    test_cmp expected1.txt status1.txt &&
+    stg files | sort > files.txt &&
+    test_cmp expected0.txt files.txt
+'
+
+reset
+
+cat > expected0.txt <<EOF
+D y.txt
+EOF
+printf '' > expected1.txt
+test_expect_success 'rm a file' '
+    stg new -m p2 &&
+    rm y.txt &&
+    stg status > status0.txt &&
+    test_cmp expected0.txt status0.txt &&
+    stg refresh &&
+    stg status > status1.txt &&
+    test_cmp expected1.txt status1.txt &&
+    stg files | sort > files.txt &&
+    test_cmp expected0.txt files.txt
+'
+
+reset
+
+cat > expected0.txt <<EOF
+D y.txt
+M x.txt
+EOF
+printf '' > expected1.txt
+test_expect_success 'rm a file together with other changes' '
+    stg new -m p3 &&
+    echo x2 >> x.txt &&
+    rm y.txt &&
+    stg status > status0.txt &&
+    test_cmp expected0.txt status0.txt &&
+    stg refresh &&
+    stg status > status1.txt &&
+    test_cmp expected1.txt status1.txt &&
+    stg files | sort > files.txt &&
+    test_cmp expected0.txt files.txt
+'
+
+test_done
diff --git a/t/t2800-goto-subdir.sh b/t/t2800-goto-subdir.sh
new file mode 100755 (executable)
index 0000000..28b8292
--- /dev/null
@@ -0,0 +1,59 @@
+#!/bin/sh
+
+test_description='Run "stg goto" in a subdirectory'
+
+. ./test-lib.sh
+
+test_expect_success 'Initialize StGit stack' '
+    stg init &&
+    echo expected1.txt >> .git/info/exclude &&
+    echo expected2.txt >> .git/info/exclude &&
+    echo actual.txt >> .git/info/exclude &&
+    mkdir foo &&
+    for i in 1 2 3; do
+        echo foo$i >> foo/bar &&
+        stg new p$i -m p$i &&
+        git add foo/bar &&
+        stg refresh
+    done
+'
+
+cat > expected1.txt <<EOF
+foo1
+EOF
+cat > expected2.txt <<EOF
+bar
+EOF
+test_expect_success 'Goto in subdirectory (just pop)' '
+    (cd foo && stg goto p1) &&
+    cat foo/bar > actual.txt &&
+    test_cmp expected1.txt actual.txt &&
+    ls foo > actual.txt &&
+    test_cmp expected2.txt actual.txt
+'
+
+test_expect_success 'Prepare conflicting goto' '
+    stg delete p2
+'
+
+cat > expected1.txt <<EOF
+foo1
+<<<<<<< current:foo/bar
+=======
+foo2
+foo3
+>>>>>>> patched:foo/bar
+EOF
+cat > expected2.txt <<EOF
+bar
+EOF
+test_expect_success 'Goto in subdirectory (conflicting push)' '
+    (cd foo && stg goto p3) ;
+    [ $? -eq 3 ] &&
+    cat foo/bar > actual.txt &&
+    test_cmp expected1.txt actual.txt &&
+    ls foo > actual.txt &&
+    test_cmp expected2.txt actual.txt
+'
+
+test_done
index 5f47f86..32900d0 100755 (executable)
@@ -13,7 +13,7 @@ Tests some parts of the stg rename command.'
 stg init
 
 test_expect_success 'Rename in empty' '
-   ! stg rename foo
+   command_error stg rename foo
 '
 
 test_expect_success 'Rename single top-most' '
@@ -23,7 +23,7 @@ test_expect_success 'Rename single top-most' '
 # bar
 
 test_expect_success 'Rename non-existing' '
-   ! stg rename neithersuchpatch norsuchpatch
+   command_error stg rename neithersuchpatch norsuchpatch
 '
 
 test_expect_success 'Rename with two arguments' '
@@ -33,15 +33,22 @@ test_expect_success 'Rename with two arguments' '
 # foo,baz
 
 test_expect_success 'Rename to existing name' '
-   ! stg rename foo baz
+   command_error stg rename foo baz
 '
 
 test_expect_success 'Rename to same name' '
-   ! stg rename foo foo
+   command_error stg rename foo foo
 '
 
 test_expect_success 'Rename top-most when others exist' '
    stg rename bar
 '
 
+test_expect_failure 'Rename hidden' '
+    stg pop &&
+    stg hide bar &&
+    stg rename bar pub &&
+    test "$(echo $(stg series --all))" = "> foo ! pub"
+'
+
 test_done
diff --git a/t/t3000-dirty-merge.sh b/t/t3000-dirty-merge.sh
new file mode 100755 (executable)
index 0000000..f0f79d5
--- /dev/null
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+test_description='Try a push that requires merging a file that is dirty'
+
+. ./test-lib.sh
+
+test_expect_success 'Initialize StGit stack with two patches' '
+    stg init &&
+    touch a &&
+    git add a &&
+    git commit -m a &&
+    echo 1 > a &&
+    git commit -a -m p1 &&
+    echo 2 > a &&
+    git commit -a -m p2 &&
+    stg uncommit -n 2
+'
+
+test_expect_success 'Pop one patch and update the other' '
+    stg goto p1 &&
+    echo 3 > a &&
+    stg refresh
+'
+
+test_expect_success 'Push with dirty worktree' '
+    echo 4 > a &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p2" ] &&
+    conflict stg goto p2 &&
+    [ "$(echo $(stg series --applied --noprefix))" = "p1" ] &&
+    [ "$(echo $(stg series --unapplied --noprefix))" = "p2" ] &&
+    [ "$(echo $(cat a))" = "4" ]
+'
+
+test_done
diff --git a/t/t3100-reset.sh b/t/t3100-reset.sh
new file mode 100755 (executable)
index 0000000..3024975
--- /dev/null
@@ -0,0 +1,160 @@
+#!/bin/sh
+
+test_description='Simple test cases for "stg reset"'
+
+. ./test-lib.sh
+
+# Ignore our own output files.
+cat > .git/info/exclude <<EOF
+/expected.txt
+EOF
+
+test_expect_success 'Initialize StGit stack with three patches' '
+    stg init &&
+    echo 000 >> a &&
+    git add a &&
+    git commit -m a &&
+    echo 111 >> a &&
+    git commit -a -m p1 &&
+    echo 222 >> a &&
+    git commit -a -m p2 &&
+    echo 333 >> a &&
+    git commit -a -m p3 &&
+    stg uncommit -n 3 &&
+    stg pop
+'
+
+cat > expected.txt <<EOF
+000
+111
+EOF
+test_expect_success 'Pop one patch ...' '
+    stg pop &&
+    test "$(echo $(stg series --all))" = "> p1 - p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo it' '
+    stg reset master.stgit^~1 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+333
+EOF
+test_expect_success 'Push one patch ...' '
+    stg push &&
+    test "$(echo $(stg series --all))" = "+ p1 + p2 > p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo it' '
+    stg reset master.stgit^~1 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+test_expect_success 'Commit one patch ...' '
+    stg commit &&
+    test "$(echo $(stg series --all))" = "> p2 - p3"
+'
+
+test_expect_success '... and undo it' '
+    stg reset master.stgit^~1 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2 - p3"
+'
+
+test_expect_success 'Hide a patch ...' '
+    stg hide p3 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2 ! p3"
+'
+
+test_expect_success '... undo the hiding ...' '
+    stg reset master.stgit^~1 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2 - p3"
+'
+
+test_expect_success '... unhide the patch ...' '
+    stg hide p3 && stg unhide p3 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2 - p3"
+'
+
+test_expect_success '... and undo the unhiding' '
+    stg reset master.stgit^~1 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2 ! p3" &&
+    stg unhide p3
+'
+
+cat > expected.txt <<EOF
+000
+111
+EOF
+test_expect_success 'Delete two patches ...' '
+    stg delete p2 p3 &&
+    test "$(echo $(stg series --all))" = "> p1" &&
+    test_cmp expected.txt a
+'
+
+test_expect_success '... and undo one of the deletions ...' '
+    stg reset master.stgit^~1 p3 &&
+    test "$(echo $(stg series --all))" = "> p1 - p3" &&
+    test_cmp expected.txt a
+'
+
+test_expect_success '... then undo the first undo ...' '
+    stg reset master.stgit^~1 &&
+    test "$(echo $(stg series --all))" = "> p1" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo the other deletion' '
+    stg reset master.stgit^~3 p2 &&
+    stg push p2 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+ggg
+EOF
+test_expect_success 'Refresh a patch ...' '
+    echo ggg >> a &&
+    stg refresh &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo the refresh' '
+    stg reset master.stgit^~2 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2" &&
+    test_cmp expected.txt a
+'
+
+test_done
diff --git a/t/t3101-reset-hard.sh b/t/t3101-reset-hard.sh
new file mode 100755 (executable)
index 0000000..2807ba3
--- /dev/null
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+test_description='Simple test cases for "stg reset"'
+
+. ./test-lib.sh
+
+# Ignore our own output files.
+cat > .git/info/exclude <<EOF
+/expected.txt
+/actual.txt
+EOF
+
+test_expect_success 'Initialize StGit stack with three patches' '
+    stg init &&
+    echo 000 >> a &&
+    git add a &&
+    git commit -m a &&
+    echo 111 >> a &&
+    git commit -a -m p1 &&
+    echo 222 >> a &&
+    git commit -a -m p2 &&
+    echo 333 >> a &&
+    git commit -a -m p3 &&
+    stg uncommit -n 3
+'
+
+cat > expected.txt <<EOF
+C a
+EOF
+test_expect_success 'Pop middle patch, creating a conflict' '
+    conflict_old stg pop p2 &&
+    stg status a > actual.txt &&
+    test_cmp expected.txt actual.txt &&
+    test "$(echo $(stg series))" = "+ p1 > p3 - p2"
+'
+
+test_expect_success 'Try to reset without --hard' '
+    command_error stg reset master.stgit^~1 &&
+    stg status a > actual.txt &&
+    test_cmp expected.txt actual.txt &&
+    test "$(echo $(stg series))" = "+ p1 > p3 - p2"
+'
+
+cat > expected.txt <<EOF
+EOF
+test_expect_success 'Try to reset with --hard' '
+    stg reset --hard master.stgit^~1 &&
+    stg status a > actual.txt &&
+    test_cmp expected.txt actual.txt &&
+    test "$(echo $(stg series))" = "> p1 - p3 - p2"
+'
+
+test_done
diff --git a/t/t3102-undo.sh b/t/t3102-undo.sh
new file mode 100755 (executable)
index 0000000..9373522
--- /dev/null
@@ -0,0 +1,81 @@
+#!/bin/sh
+
+test_description='Simple test cases for "stg undo"'
+
+. ./test-lib.sh
+
+# Ignore our own output files.
+cat > .git/info/exclude <<EOF
+/expected.txt
+EOF
+
+test_expect_success 'Initialize StGit stack with three patches' '
+    stg init &&
+    echo 000 >> a &&
+    git add a &&
+    git commit -m a &&
+    echo 111 >> a &&
+    git commit -a -m p1 &&
+    echo 222 >> a &&
+    git commit -a -m p2 &&
+    echo 333 >> a &&
+    git commit -a -m p3 &&
+    stg uncommit -n 3 &&
+    stg pop
+'
+
+cat > expected.txt <<EOF
+000
+111
+EOF
+test_expect_success 'Pop one patch ...' '
+    stg pop &&
+    test "$(echo $(stg series))" = "> p1 - p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo it' '
+    stg undo &&
+    test "$(echo $(stg series))" = "+ p1 > p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+EOF
+test_expect_success 'Pop two patches ...' '
+    stg pop &&
+    stg pop &&
+    test "$(echo $(stg series))" = "- p1 - p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo it' '
+    stg undo &&
+    stg undo &&
+    test "$(echo $(stg series))" = "+ p1 > p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success 'Undo past end of history' '
+    command_error stg undo -n 100 &&
+    test "$(echo $(stg series))" = "+ p1 > p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+test_done
diff --git a/t/t3103-undo-hard.sh b/t/t3103-undo-hard.sh
new file mode 100755 (executable)
index 0000000..599aa43
--- /dev/null
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+test_description='Simple test cases for "stg undo"'
+
+. ./test-lib.sh
+
+# Ignore our own output files.
+cat > .git/info/exclude <<EOF
+/expected.txt
+/actual.txt
+EOF
+
+test_expect_success 'Initialize StGit stack with three patches' '
+    stg init &&
+    echo 000 >> a &&
+    git add a &&
+    git commit -m a &&
+    echo 111 >> a &&
+    git commit -a -m p1 &&
+    echo 222 >> a &&
+    git commit -a -m p2 &&
+    echo 333 >> a &&
+    git commit -a -m p3 &&
+    stg uncommit -n 3
+'
+
+cat > expected.txt <<EOF
+C a
+EOF
+test_expect_success 'Pop middle patch, creating a conflict' '
+    conflict_old stg pop p2 &&
+    stg status a > actual.txt &&
+    test_cmp expected.txt actual.txt &&
+    test "$(echo $(stg series))" = "+ p1 > p3 - p2"
+'
+
+test_expect_success 'Try to undo without --hard' '
+    command_error stg undo &&
+    stg status a > actual.txt &&
+    test_cmp expected.txt actual.txt &&
+    test "$(echo $(stg series))" = "+ p1 > p3 - p2"
+'
+
+cat > expected.txt <<EOF
+EOF
+test_expect_success 'Try to undo with --hard' '
+    stg undo --hard &&
+    stg status a > actual.txt &&
+    test_cmp expected.txt actual.txt &&
+    test "$(echo $(stg series))" = "> p1 - p3 - p2"
+'
+
+test_done
diff --git a/t/t3104-redo.sh b/t/t3104-redo.sh
new file mode 100755 (executable)
index 0000000..030311d
--- /dev/null
@@ -0,0 +1,114 @@
+#!/bin/sh
+
+test_description='Simple test cases for "stg redo"'
+
+. ./test-lib.sh
+
+# Ignore our own output files.
+cat > .git/info/exclude <<EOF
+/expected.txt
+EOF
+
+test_expect_success 'Initialize StGit stack with three patches' '
+    stg init &&
+    echo 000 >> a &&
+    git add a &&
+    git commit -m a &&
+    echo 111 >> a &&
+    git commit -a -m p1 &&
+    echo 222 >> a &&
+    git commit -a -m p2 &&
+    echo 333 >> a &&
+    git commit -a -m p3 &&
+    stg uncommit -n 3
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success 'Pop one patch ...' '
+    stg pop &&
+    test "$(echo $(stg series))" = "+ p1 > p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+333
+EOF
+test_expect_success '... undo it ...' '
+    stg undo &&
+    test "$(echo $(stg series))" = "+ p1 + p2 > p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and redo' '
+    stg redo &&
+    test "$(echo $(stg series))" = "+ p1 > p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+EOF
+test_expect_success 'Pop three patches ...' '
+    stg push &&
+    stg pop &&
+    stg pop &&
+    stg pop &&
+    test "$(echo $(stg series))" = "- p1 - p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+333
+EOF
+test_expect_success '... undo it ...' '
+    stg undo &&
+    stg undo &&
+    stg undo &&
+    test "$(echo $(stg series))" = "+ p1 + p2 > p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+EOF
+test_expect_success '... redo the first two pops ...' '
+    stg redo -n 2 &&
+    test "$(echo $(stg series))" = "> p1 - p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+EOF
+test_expect_success '... and the remaining one' '
+    stg redo &&
+    test "$(echo $(stg series))" = "- p1 - p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+EOF
+test_expect_success 'Redo past end of history' '
+    command_error stg redo &&
+    test "$(echo $(stg series))" = "- p1 - p2 - p3" &&
+    test_cmp expected.txt a
+'
+
+test_done
diff --git a/t/t3105-undo-external-mod.sh b/t/t3105-undo-external-mod.sh
new file mode 100755 (executable)
index 0000000..f5aad64
--- /dev/null
@@ -0,0 +1,65 @@
+#!/bin/sh
+
+test_description='Undo external modifications of the stack'
+
+. ./test-lib.sh
+
+# Ignore our own output files.
+cat > .git/info/exclude <<EOF
+/expected.txt
+/head?.txt
+EOF
+
+test_expect_success 'Initialize StGit stack' '
+    stg init &&
+    echo 000 >> a &&
+    git add a &&
+    git commit -m p0 &&
+    echo 111 >> a &&
+    git add a &&
+    git commit -m p1 &&
+    stg uncommit -n 1
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success 'Make a git commit and turn it into a patch' '
+    git rev-parse HEAD > head0.txt &&
+    echo 222 >> a &&
+    git add a &&
+    git commit -m p2 &&
+    git rev-parse HEAD > head1.txt &&
+    stg repair &&
+    test "$(echo $(stg series))" = "+ p1 > p2" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success 'Undo the patchification' '
+    stg undo &&
+    git rev-parse HEAD > head2.txt &&
+    test_cmp head1.txt head2.txt &&
+    test "$(echo $(stg series))" = "> p1" &&
+    test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+EOF
+test_expect_success 'Undo the commit' '
+    stg undo &&
+    git rev-parse HEAD > head3.txt &&
+    test_cmp head0.txt head3.txt &&
+    test "$(echo $(stg series))" = "> p1" &&
+    test_cmp expected.txt a
+'
+
+test_done
diff --git a/t/t3300-edit.sh b/t/t3300-edit.sh
new file mode 100755 (executable)
index 0000000..ad3b23f
--- /dev/null
@@ -0,0 +1,215 @@
+#!/bin/sh
+test_description='Test "stg edit"'
+
+. ./test-lib.sh
+
+test_expect_success 'Setup' '
+    printf "000\n111\n222\n333\n" >> foo &&
+    git add foo &&
+    git commit -m "Initial commit" &&
+    sed -i "s/000/000xx/" foo &&
+    git commit -a -m "First change" &&
+    sed -i "s/111/111yy/" foo &&
+    git commit -a -m "Second change" &&
+    sed -i "s/222/222zz/" foo &&
+    git commit -a -m "Third change" &&
+    sed -i "s/333/333zz/" foo &&
+    git commit -a -m "Fourth change" &&
+    stg init &&
+    stg uncommit -n 4 p &&
+    stg pop -n 2 &&
+    stg hide p4 &&
+    test "$(echo $(stg series --all))" = "+ p1 > p2 - p3 ! p4"
+'
+
+# Commit parse functions.
+msg () { git cat-file -p $1 | sed '1,/^$/d' | tr '\n' / | sed 's,/*$,,' ; }
+auth () { git log -n 1 --pretty=format:"%an, %ae" $1 ; }
+date () { git log -n 1 --pretty=format:%ai $1 ; }
+
+test_expect_success 'Edit message of top patch' '
+    test "$(msg HEAD)" = "Second change" &&
+    stg edit p2 -m "Second change 2" &&
+    test "$(msg HEAD)" = "Second change 2"
+'
+
+test_expect_success 'Edit message of non-top patch' '
+    test "$(msg HEAD^)" = "First change" &&
+    stg edit p1 -m "First change 2" &&
+    test "$(msg HEAD^)" = "First change 2"
+'
+
+test_expect_success 'Edit message of unapplied patch' '
+    test "$(msg $(stg id p3))" = "Third change" &&
+    stg edit p3 -m "Third change 2" &&
+    test "$(msg $(stg id p3))" = "Third change 2"
+'
+
+test_expect_success 'Edit message of hidden patch' '
+    test "$(msg $(stg id p4))" = "Fourth change" &&
+    stg edit p4 -m "Fourth change 2" &&
+    test "$(msg $(stg id p4))" = "Fourth change 2"
+'
+
+test_expect_success 'Set patch message with --file <file>' '
+    test "$(msg HEAD)" = "Second change 2" &&
+    echo "Pride or Prejudice" > commitmsg &&
+    stg edit p2 -f commitmsg &&
+    test "$(msg HEAD)" = "Pride or Prejudice"
+'
+
+test_expect_success 'Set patch message with --file -' '
+    echo "Pride and Prejudice" | stg edit p2 -f - &&
+    test "$(msg HEAD)" = "Pride and Prejudice"
+'
+
+( printf 'From: A U Thor <author@example.com>\nDate: <omitted>'
+  printf '\n\nPride and Prejudice' ) > expected-tmpl
+omit_date () { sed "s/^Date:.*$/Date: <omitted>/" ; }
+
+test_expect_success 'Save template to file' '
+    stg edit --save-template saved-tmpl p2 &&
+    omit_date < saved-tmpl > saved-tmpl-d &&
+    test_cmp expected-tmpl saved-tmpl-d
+'
+
+test_expect_success 'Save template to stdout' '
+    stg edit --save-template - p2 > saved-tmpl2 &&
+    omit_date < saved-tmpl2 > saved-tmpl2-d &&
+    test_cmp expected-tmpl saved-tmpl2-d
+'
+
+# Test the various ways of invoking the interactive editor. The
+# preference order should be
+#
+#   1. GIT_EDITOR
+#   2. stgit.editor (legacy)
+#   3. core.editor
+#   4. VISUAL
+#   5. EDITOR
+#   6. vi
+
+mkeditor ()
+{
+    cat > "$1" <<EOF
+#!/bin/sh
+printf "\n$1" >> "\$1"
+EOF
+    chmod a+x "$1"
+}
+
+mkeditor vi
+test_expect_success 'Edit commit message interactively (vi)' '
+    m=$(msg HEAD) &&
+    PATH=.:$PATH stg edit p2 &&
+    test "$(msg HEAD)" = "$m/vi"
+'
+
+mkeditor e1
+test_expect_success 'Edit commit message interactively (EDITOR)' '
+    m=$(msg HEAD) &&
+    EDITOR=./e1 PATH=.:$PATH stg edit p2 &&
+    echo $m && echo $(msg HEAD) &&
+    test "$(msg HEAD)" = "$m/e1"
+'
+
+mkeditor e2
+test_expect_success 'Edit commit message interactively (VISUAL)' '
+    m=$(msg HEAD) &&
+    VISUAL=./e2 EDITOR=./e1 PATH=.:$PATH stg edit p2 &&
+    test "$(msg HEAD)" = "$m/e2"
+'
+
+mkeditor e3
+test_expect_success 'Edit commit message interactively (core.editor)' '
+    m=$(msg HEAD) &&
+    git config core.editor e3 &&
+    VISUAL=./e2 EDITOR=./e1 PATH=.:$PATH stg edit p2 &&
+    test "$(msg HEAD)" = "$m/e3"
+'
+
+mkeditor e4
+test_expect_success 'Edit commit message interactively (stgit.editor)' '
+    m=$(msg HEAD) &&
+    git config stgit.editor e4 &&
+    VISUAL=./e2 EDITOR=./e1 PATH=.:$PATH stg edit p2 &&
+    test "$(msg HEAD)" = "$m/e4"
+'
+
+mkeditor e5
+test_expect_success 'Edit commit message interactively (GIT_EDITOR)' '
+    m=$(msg HEAD) &&
+    GIT_EDITOR=./e5 VISUAL=./e2 EDITOR=./e1 PATH=.:$PATH stg edit p2 &&
+    test "$(msg HEAD)" = "$m/e5"
+'
+
+rm -f vi e1 e2 e3 e4 e5
+git config --unset core.editor
+git config --unset stgit.editor
+
+mkeditor twoliner
+test_expect_success 'Both noninterative and interactive editing' '
+    EDITOR=./twoliner stg edit -e -m "oneliner" p2 &&
+    test "$(msg HEAD)" = "oneliner/twoliner"
+'
+rm -f twoliner
+
+cat > diffedit <<EOF
+#!/bin/sh
+sed -i 's/111yy/111YY/' "\$1"
+EOF
+chmod a+x diffedit
+test_expect_success 'Edit patch diff' '
+    EDITOR=./diffedit stg edit -d p2 &&
+    test "$(grep 111 foo)" = "111YY"
+'
+rm -f diffedit
+
+test_expect_success 'Sign a patch' '
+    m=$(msg HEAD) &&
+    stg edit --sign p2 &&
+    test "$(msg HEAD)" = "$m//Signed-off-by: C O Mitter <committer@example.com>"
+'
+
+test_expect_success 'Acknowledge a patch' '
+    m=$(msg HEAD^) &&
+    stg edit --ack p1 &&
+    test "$(msg HEAD^)" = "$m//Acked-by: C O Mitter <committer@example.com>"
+'
+
+test_expect_success 'Set author' '
+    stg edit p2 --author "Jane Austin <jaustin@example.com>" &&
+    test "$(auth HEAD)" = "Jane Austin, jaustin@example.com"
+'
+
+test_expect_success 'Fail to set broken author' '
+    command_error stg edit p2 --author "No Mail Address" &&
+    test "$(auth HEAD)" = "Jane Austin, jaustin@example.com"
+'
+
+test_expect_success 'Set author name' '
+    stg edit p2 --authname "Jane Austen" &&
+    test "$(auth HEAD)" = "Jane Austen, jaustin@example.com"
+'
+
+test_expect_success 'Set author email' '
+    stg edit p2 --authemail "jausten@example.com" &&
+    test "$(auth HEAD)" = "Jane Austen, jausten@example.com"
+'
+
+test_expect_failure 'Set author date (RFC2822 format)' '
+    stg edit p2 --authdate "Wed, 10 Jul 2013 23:39:00 pm -0300" &&
+    test "$(date HEAD)" = "2013-07-10 23:39:00 -0300"
+'
+
+test_expect_failure 'Set author date (ISO 8601 format)' '
+    stg edit p2 --authdate "2013-01-28 22:30:00 -0300" &&
+    test "$(date HEAD)" = "2013-01-28 22:30:00 -0300"
+'
+
+test_expect_failure 'Fail to set invalid author date' '
+    command_error stg edit p2 --authdate "28 Jan 1813" &&
+    test "$(date HEAD)" = "2013-01-28 22:30:00 -0300"
+'
+
+test_done
diff --git a/t/t3400-pick.sh b/t/t3400-pick.sh
new file mode 100755 (executable)
index 0000000..3bd5c4f
--- /dev/null
@@ -0,0 +1,59 @@
+#!/bin/sh
+test_description='Test the pick command'
+
+. ./test-lib.sh
+
+test_expect_success \
+       'Initialize the StGIT repository' \
+       '
+       stg init &&
+       stg new A -m "a" && echo A > a && git add a && stg refresh &&
+       stg new B -m "b" && echo B > b && git add b && stg refresh &&
+       stg branch --clone foo &&
+       stg new C -m "c" && echo C > c && git add c && stg refresh &&
+       stg new D-foo -m "d" && echo D > d && git add d && stg refresh &&
+       stg branch master
+       '
+
+test_expect_success \
+       'Pick remote patch' \
+       '
+       stg pick foo:C &&
+       test "$(echo $(stg series --applied --noprefix))" = "A B C"
+       '
+
+test_expect_success \
+       'Pick --unapplied remote patch' \
+       '
+       stg pick --unapplied --ref-branch foo --name D D-foo &&
+       test "$(echo $(stg series --applied --noprefix))" = "A B C" &&
+       test "$(echo $(stg series --unapplied --noprefix))" = "D"
+       '
+
+test_expect_success \
+       'Pick local unapplied patch' \
+       '
+       stg pick D &&
+       test "$(echo $(stg series --applied --noprefix))" = "A B C D-0" &&
+       test "$(echo $(stg series --unapplied --noprefix))" = "D"
+       '
+
+test_expect_success \
+       'Pick --fold --reverse local patch' \
+       '
+       stg pick --fold --reverse D &&
+       stg refresh && stg clean &&
+       test "$(echo $(stg series --applied --noprefix))" = "A B C" &&
+       test "$(echo $(stg series --unapplied --noprefix))" = "D"
+       '
+
+test_expect_success \
+       'Pick --fold without applied patches' \
+       '
+       stg pop --all &&
+       stg pick --fold D &&
+       test "$(echo $(stg series --unapplied --noprefix))" = "A B C D" &&
+       test "$(echo $(stg status))" = "A d"
+       '
+
+test_done
index 8a308fb..b89c720 100755 (executable)
@@ -14,8 +14,8 @@ for ver in 0.12 0.8; do
 
     test_expect_success \
         "v$ver: Check the list of applied and unapplied patches" '
-        [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
-        [ "$(echo $(stg unapplied))" = "p3 p4" ]
+        [ "$(echo $(stg series --applied --noprefix))" = "p0 p1 p2" ] &&
+        [ "$(echo $(stg series --unapplied --noprefix))" = "p3 p4" ]
     '
 
     test_expect_success \
@@ -31,7 +31,7 @@ for ver in 0.12 0.8; do
 
     test_expect_success \
         "v$ver: Make sure the base ref is no longer there" '
-        ! git show-ref --verify --quiet refs/bases/master
+        must_fail git show-ref --verify --quiet refs/bases/master
     '
 
     cd ..
index 3d114a2..c1fb1b3 100644 (file)
@@ -4,14 +4,19 @@
 # Copyright (c) 2006 Yann Dirson - tuning for stgit
 #
 
+# Keep the original TERM for say_color
+ORIGINAL_TERM=$TERM
+
 # For repeatability, reset the environment to known value.
 LANG=C
 LC_ALL=C
 PAGER=cat
 TZ=UTC
-export LANG LC_ALL PAGER TZ
-EDITOR=:
-VISUAL=:
+TERM=dumb
+export LANG LC_ALL PAGER TERM TZ
+unset EDITOR
+unset VISUAL
+unset GIT_EDITOR
 unset AUTHOR_DATE
 unset AUTHOR_EMAIL
 unset AUTHOR_NAME
@@ -20,13 +25,14 @@ unset COMMIT_AUTHOR_NAME
 unset EMAIL
 unset GIT_ALTERNATE_OBJECT_DIRECTORIES
 unset GIT_AUTHOR_DATE
-#GIT_AUTHOR_EMAIL=author@example.com
-#GIT_AUTHOR_NAME='A U Thor'
+GIT_AUTHOR_EMAIL=author@example.com
+GIT_AUTHOR_NAME='A U Thor'
 unset GIT_COMMITTER_DATE
-#GIT_COMMITTER_EMAIL=committer@example.com
-#GIT_COMMITTER_NAME='C O Mitter'
+GIT_COMMITTER_EMAIL=committer@example.com
+GIT_COMMITTER_NAME='C O Mitter'
 unset GIT_DIFF_OPTS
 unset GIT_DIR
+unset GIT_WORK_TREE
 unset GIT_EXTERNAL_DIFF
 unset GIT_INDEX_FILE
 unset GIT_OBJECT_DIRECTORY
@@ -36,7 +42,7 @@ GIT_MERGE_VERBOSITY=5
 export GIT_MERGE_VERBOSITY
 export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME
 export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME
-export EDITOR VISUAL
+GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u}
 
 # Protect ourselves from common misconfiguration to export
 # CDPATH into the environment
@@ -57,19 +63,15 @@ esac
 # This test checks if command xyzzy does the right thing...
 # '
 # . ./test-lib.sh
-
-error () {
-       echo "* error: $*"
-       trap - exit
-       exit 1
-}
-
-say () {
-       echo "* $*"
-}
-
-test "${test_description}" != "" ||
-error "Test script did not set test_description."
+[ "x$ORIGINAL_TERM" != "xdumb" ] && (
+               TERM=$ORIGINAL_TERM &&
+               export TERM &&
+               [ -t 1 ] &&
+               tput bold >/dev/null 2>&1 &&
+               tput setaf 1 >/dev/null 2>&1 &&
+               tput sgr0 >/dev/null 2>&1
+       ) &&
+       color=t
 
 while test "$#" -ne 0
 do
@@ -79,16 +81,63 @@ do
        -i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
                immediate=t; shift ;;
        -h|--h|--he|--hel|--help)
-               echo "$test_description"
-               exit 0 ;;
+               help=t; shift ;;
        -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
                export STGIT_DEBUG_LEVEL="-1"
                verbose=t; shift ;;
+       -q|--q|--qu|--qui|--quie|--quiet)
+               quiet=t; shift ;;
+       --no-color)
+               color=; shift ;;
        *)
                break ;;
        esac
 done
 
+if test -n "$color"; then
+       say_color () {
+               (
+               TERM=$ORIGINAL_TERM
+               export TERM
+               case "$1" in
+                       error) tput bold; tput setaf 1;; # bold red
+                       skip)  tput bold; tput setaf 2;; # bold green
+                       pass)  tput setaf 2;;            # green
+                       info)  tput setaf 3;;            # brown
+                       *) test -n "$quiet" && return;;
+               esac
+               shift
+               echo "* $*"
+               tput sgr0
+               )
+       }
+else
+       say_color() {
+               test -z "$1" && test -n "$quiet" && return
+               shift
+               echo "* $*"
+       }
+fi
+
+error () {
+       say_color error "error: $*"
+       trap - exit
+       exit 1
+}
+
+say () {
+       say_color info "$*"
+}
+
+test "${test_description}" != "" ||
+error "Test script did not set test_description."
+
+if test "$help" = "t"
+then
+       echo "$test_description"
+       exit 0
+fi
+
 exec 5>&1
 if test "$verbose" = "t"
 then
@@ -99,8 +148,15 @@ fi
 
 test_failure=0
 test_count=0
+test_fixed=0
+test_broken=0
+
+die () {
+       echo >&5 "FATAL: Unexpected exit with code $?"
+       exit 1
+}
 
-trap 'echo >&5 "FATAL: Unexpected exit with code $?"; exit 1' exit
+trap 'die' exit
 
 test_tick () {
        if test -z "${test_tick+set}"
@@ -119,18 +175,29 @@ test_tick () {
 
 test_ok_ () {
        test_count=$(expr "$test_count" + 1)
-       say "  ok $test_count: $@"
+       say_color "" "  ok $test_count: $@"
 }
 
 test_failure_ () {
        test_count=$(expr "$test_count" + 1)
        test_failure=$(expr "$test_failure" + 1);
-       say "FAIL $test_count: $1"
+       say_color error "FAIL $test_count: $1"
        shift
        echo "$@" | sed -e 's/^/        /'
        test "$immediate" = "" || { trap - exit; exit 1; }
 }
 
+test_known_broken_ok_ () {
+       test_count=$(expr "$test_count" + 1)
+       test_fixed=$(($test_fixed+1))
+       say_color "" "  FIXED $test_count: $@"
+}
+
+test_known_broken_failure_ () {
+       test_count=$(expr "$test_count" + 1)
+       test_broken=$(($test_broken+1))
+       say_color skip "  still broken $test_count: $@"
+}
 
 test_debug () {
        test "$debug" = "" || eval "$1"
@@ -155,9 +222,9 @@ test_skip () {
        done
        case "$to_skip" in
        t)
-               say >&3 "skipping test: $@"
+               say_color skip >&3 "skipping test: $@"
                test_count=$(expr "$test_count" + 1)
-               say "skip $test_count: $1"
+               say_color skip "skip $test_count: $1"
                : true
                ;;
        *)
@@ -171,13 +238,13 @@ test_expect_failure () {
        error "bug in the test script: not 2 parameters to test-expect-failure"
        if ! test_skip "$@"
        then
-               say >&3 "expecting failure: $2"
+               say >&3 "checking known breakage: $2"
                test_run_ "$2"
-               if [ "$?" = 0 -a "$eval_ret" != 0 -a "$eval_ret" -lt 129 ]
+               if [ "$?" = 0 -a "$eval_ret" = 0 ]
                then
-                       test_ok_ "$1"
+                       test_known_broken_ok_ "$1"
                else
-                       test_failure_ "$@"
+                   test_known_broken_failure_ "$1"
                fi
        fi
        echo >&3 ""
@@ -217,26 +284,78 @@ test_expect_code () {
        echo >&3 ""
 }
 
-# Most tests can use the created repository, but some amy need to create more.
+# When running an StGit command that should exit with an error, use
+# these instead of testing for any non-zero exit code with !.
+exit_code () {
+       expected=$1
+       shift
+       "$@"
+       test $? -eq $expected
+}
+general_error () { exit_code 1 "$@" ; }
+command_error () { exit_code 2 "$@" ; }
+conflict () { exit_code 3 "$@" ; }
+
+# Old-infrastructure commands don't exit with the proper value on
+# conflicts. But we don't want half the tests to fail because of that,
+# so use this instead of "conflict" for them.
+conflict_old () { command_error "$@" ; }
+
+# Same thing, but for other commands that StGit where we just want to
+# make sure that they fail instead of crashing.
+must_fail () {
+        "$@"
+        test $? -gt 0 -a $? -le 129
+}
+
+# test_cmp is a helper function to compare actual and expected output.
+# You can use it like:
+#
+#      test_expect_success 'foo works' '
+#              echo expected >expected &&
+#              foo >actual &&
+#              test_cmp expected actual
+#      '
+#
+# This could be written as either "cmp" or "diff -u", but:
+# - cmp's output is not nearly as easy to read as diff -u
+# - not all diff versions understand "-u"
+
+test_cmp() {
+       $GIT_TEST_CMP "$@"
+}
+
+# Most tests can use the created repository, but some may need to create more.
 # Usage: test_create_repo <directory>
 test_create_repo () {
        test "$#" = 1 ||
        error "bug in the test script: not 1 parameter to test-create-repo"
-       owd=`pwd`
+       owd=$(pwd)
        repo="$1"
        mkdir "$repo"
        cd "$repo" || error "Cannot setup test environment"
-       git init >/dev/null 2>&1 ||
-       error "cannot run git init -- have you installed git-core?"
-       mkdir .git/info
-       echo "empty start" |
-       git commit-tree `git write-tree` >.git/refs/heads/master 2>&4 ||
-       error "cannot run git commit -- is your git-core functioning?"
+       git init >/dev/null 2>&1 || error "cannot run git init"
+       echo "empty start" | \
+           git commit-tree $(git write-tree) >.git/refs/heads/master 2>&4 || \
+           error "cannot run git commit"
+       mv .git/hooks .git/hooks-disabled
        cd "$owd"
 }
 
 test_done () {
        trap - exit
+
+       if test "$test_fixed" != 0
+       then
+               say_color pass "fixed $test_fixed known breakage(s)"
+       fi
+       if test "$test_broken" != 0
+       then
+               say_color error "still have $test_broken known breakage(s)"
+               msg="remaining $(($test_count-$test_broken)) test(s)"
+       else
+               msg="$test_count test(s)"
+       fi
        case "$test_failure" in
        0)
                # We could:
@@ -247,11 +366,11 @@ test_done () {
                # The Makefile provided will clean this test area so
                # we will leave things as they are.
 
-               say "passed all $test_count test(s)"
+               say_color pass "passed all $msg"
                exit 0 ;;
 
        *)
-               say "failed $test_failure among $test_count test(s)"
+               say_color error "failed $test_failure among $msg"
                exit 1 ;;
 
        esac
@@ -261,14 +380,17 @@ test_done () {
 # t/ subdirectory and are run in trash subdirectory.
 PATH=$(pwd)/..:$PATH
 HOME=$(pwd)/trash
-GIT_TEMPLATE_DIR=$(pwd)/../templates
 GIT_CONFIG=.git/config
-export PATH HOME GIT_TEMPLATE_DIR GIT_CONFIG
-
+export PATH HOME GIT_CONFIG
 
 # Test repository
 test=trash
-rm -fr "$test"
+rm -fr "$test" || {
+       trap - exit
+       echo >&5 "FATAL: Cannot prepare test area"
+       exit 1
+}
+
 test_create_repo $test
 cd "$test"
 
@@ -285,8 +407,8 @@ do
        done
        case "$to_skip" in
        t)
-               say >&3 "skipping test $this_test altogether"
-               say "skip all tests in $this_test"
+               say_color skip >&3 "skipping test $this_test altogether"
+               say_color skip "skip all tests in $this_test"
                test_done
        esac
 done
index 430b341..70d2df0 100644 (file)
@@ -11,7 +11,6 @@ Content-Disposition: inline
 
 %(fromauth)s%(longdescr)s
 ---
-
 %(diffstat)s
 --MIMEBOUNDARY
 Content-Type: text/plain; name=%(patch)s.patch
index e7f3481..11f2075 100644 (file)
@@ -4,6 +4,5 @@ From: %(authname)s <%(authemail)s>
 
 %(longdescr)s
 ---
-
 %(diffstat)s
 
index f5c35c2..7d4022a 100644 (file)
@@ -3,6 +3,5 @@ Subject: [%(prefix)sPATCH%(version)s%(number)s] %(shortdescr)s
 
 %(fromauth)s%(longdescr)s
 ---
-
 %(diffstat)s
 %(diff)s