chiark / gitweb /
Merge branch 'stable'
authorCatalin Marinas <catalin.marinas@gmail.com>
Fri, 19 Sep 2008 21:52:26 +0000 (22:52 +0100)
committerCatalin Marinas <catalin.marinas@gmail.com>
Fri, 19 Sep 2008 21:52:26 +0000 (22:52 +0100)
Conflicts:

t/t1501-sink.sh

108 files changed:
Documentation/Makefile
Documentation/stg-cp.txt [deleted file]
Documentation/tutorial.txt
INSTALL
Makefile
contrib/Makefile [new file with mode: 0644]
contrib/diffcol.sh
contrib/stgit-completion.bash
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
stgit/argparse.py [new file with mode: 0644]
stgit/commands/add.py [deleted file]
stgit/commands/applied.py [deleted file]
stgit/commands/clean.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/goto.py
stgit/commands/id.py
stgit/commands/imprt.py
stgit/commands/init.py
stgit/commands/mail.py
stgit/commands/new.py
stgit/commands/pick.py
stgit/commands/refresh.py
stgit/commands/resolved.py
stgit/commands/rm.py [deleted file]
stgit/commands/series.py
stgit/commands/show.py
stgit/commands/status.py
stgit/commands/sync.py
stgit/commands/top.py
stgit/commands/unapplied.py [deleted file]
stgit/commands/uncommit.py
stgit/config.py
stgit/git.py
stgit/gitmergeonefile.py
stgit/lib/__init__.py [new file with mode: 0644]
stgit/lib/git.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
t/t1500-float.sh
t/t1501-sink.sh
t/t1600-delete-one.sh
t/t1601-delete-many.sh
t/t1700-goto-top.sh
t/t1701-goto-hidden.sh [new file with mode: 0755]
t/t1800-import.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/t3300-edit.sh [new file with mode: 0755]
t/t4000-upgrade.sh
t/test-lib.sh
templates/mailattch.tmpl
templates/patchexport.tmpl
templates/patchmail.tmpl

index 2e5d23cd32be1766fec455deb9e39116ec314b7d..c53c4afa38b8f05cec9257898b9124f2db0437ef 100644 (file)
@@ -1,7 +1,6 @@
 MAN1_TXT=$(wildcard stg*.txt)
-MAN7_TXT=
 
-DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT) $(MAN7_TXT))
+DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT))
 
 ARTICLES = 
 # with their own formatting rules.
@@ -11,12 +10,11 @@ DOC_HTML += $(patsubst %,%.html,$(ARTICLES) $(SP_ARTICLES))
 DOC_PDF += $(patsubst %,%.pdf,$(ARTICLES) $(SP_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 +35,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 +57,12 @@ 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
 
 %.html : %.txt
        $(ASCIIDOC) -b xhtml11 -d manpage -f asciidoc.conf $(ASCIIDOC_EXTRA) $<
 
-%.1 %.7 : %.xml
+%.1 : %.xml
        xmlto -m callouts.xsl man $<
 
 %.xml : %.txt
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].
index b040d290dabc003454f531cde7fb45f67bb7e848..5d2e50ab816e6589d189fcdff4e8620f3e3bac81 100644 (file)
@@ -123,18 +123,22 @@ You can view modified files that have already been saved into a patch:
   stg files
 
 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:
+already exist in the working directory (it also notices if you remove
+them), but you have to tell StGIT explicitly if you add or rename a
+file:
 
-  stg add [<file>*]
-  stg rm [<file>*]
+  git add new-file
 
-To record the renaming of a file in your new patch, issue both of these
-commands:
+to add a file, and
+
+  mv old-file new-file
+  git add new-file
+
+or simply
+
+  git mv old-file new-file
 
-  stg rm <oldfilename>
-  stg add <newfilename>
+to move a file.
 
 
 Stack manipulation: managing multiple patches
diff --git a/INSTALL b/INSTALL
index 8d2bebd9d1824f1b7af5cfe6fbd11f9cbfde6d74..3da4efdce26a61e7af0265973183b7717afaeb80 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 c890b8ed0a786b0be48661c7b9837d1eb6d0ebdb..9322fe0954abb664cf57a5d494bd98c0b9c89990 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,19 +1,32 @@
-PREFIX ?= $(HOME)
+prefix ?= $(HOME)
 DESTDIR        ?= /
 PYTHON ?= python
 
+TEST_PATCHES ?= ..
+
 all:
        $(PYTHON) setup.py build
 
 install:
-       $(PYTHON) setup.py install --prefix=$(PREFIX) --root=$(DESTDIR) --force
+       $(PYTHON) setup.py install --prefix=$(prefix) --root=$(DESTDIR) --force
 
 doc:
        cd Documentation && $(MAKE) all
 
+install-doc:
+       $(MAKE) -C Documentation install
+
+install-html:
+       $(MAKE) -C Documentation install-html
+
 test:
        cd t && $(MAKE) all
 
+test_patches:
+       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); \
@@ -26,4 +39,4 @@ clean:
 tags:
        ctags -e -R stgit/*
 
-.PHONY: all install doc test clean
+.PHONY: all install doc install-doc install-html test test_patches clean
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 ea9109d5bfe2ddd5cb127f5e338186f679352e55..eecc87a1e419c117938e17871c1b11d601770d63 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 
 # Code copied from Quilt (http://savannah.nongnu.org/projects/quilt)
 #
index 8d49e218097bf17ab6c1f3a1d588c745d10c0c2b..1467c2834c51e7bfbc3889ccabaef2fd7f8f41c8 100644 (file)
 #         . ~/.stgit-completion.bash
 
 _stg_commands="
-    add
-    applied
     branch
     delete
     diff
     clean
     clone
+    coalesce
     commit
-    cp
     edit
     export
     files
@@ -43,14 +41,12 @@ _stg_commands="
     rename
     repair
     resolved
-    rm
     series
     show
     sink
     status
     sync
     top
-    unapplied
     uncommit
     unhide
 "
@@ -238,6 +234,7 @@ _stg ()
         # repository commands
         id)     _stg_patches $command _all_patches ;;
         # stack commands
+        coalesce) _stg_patches $command _applied_patches ;;
         float)  _stg_patches $command _all_patches ;;
         goto)   _stg_patches $command _all_other_patches ;;
         hide)   _stg_patches $command _unapplied_patches ;;
@@ -262,8 +259,6 @@ _stg ()
         # 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 ;;
diff --git a/contrib/stgit.el b/contrib/stgit.el
new file mode 100644 (file)
index 0000000..5aaf311
--- /dev/null
@@ -0,0 +1,401 @@
+;; 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'
+
+(defun stgit (dir)
+  "Manage stgit patches"
+  (interactive "DDirectory: \n")
+  (switch-to-stgit-buffer (git-get-top-dir dir))
+  (stgit-refresh))
+
+(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 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-run (&rest args)
+  (apply 'call-process "stg" nil standard-output nil args))
+
+(defun stgit-refresh ()
+  "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 "branch")
+    (stgit-run "series" "--description")
+    (stgit-rescan)
+    (if curpatch
+        (stgit-goto-patch curpatch)
+      (goto-line curline))))
+
+(defface stgit-description-face
+  '((((background dark)) (:foreground "tan"))
+    (((background light)) (:foreground "dark red")))
+  "The face used for StGit desriptions")
+
+(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")
+
+(defface stgit-applied-patch-face
+  '((((background dark)) (:foreground "light yellow"))
+    (((background light)) (:foreground "purple"))
+    (t ()))
+  "The face used for applied patch names")
+
+(defface stgit-unapplied-patch-face
+  '((((background dark)) (:foreground "gray80"))
+    (((background light)) (:foreground "orchid"))
+    (t ()))
+  "The face used for unapplied patch names")
+
+(defun stgit-rescan ()
+  "Rescan the status buffer."
+  (save-excursion
+    (let ((marked ()))
+      (goto-char (point-min))
+      (while (not (eobp))
+        (cond ((looking-at "Branch: \\(.*\\)")
+               (put-text-property (match-beginning 1) (match-end 1)
+                                  'face 'bold))
+              ((looking-at "\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
+               (let ((state (match-string 1))
+                     (patchsym (intern (match-string 3))))
+                 (put-text-property
+                  (match-beginning 3) (match-end 3) '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 4) (match-end 4)
+                                    'face 'stgit-description-face)
+                 (when (memq patchsym stgit-marked-patches)
+                   (replace-match "*" nil nil nil 2)
+                   (setq marked (cons patchsym marked))))))
+        (forward-line 1))
+      (setq stgit-marked-patches (nreverse marked)))))
+
+(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)
+  (define-key stgit-mode-map " "   'stgit-mark)
+  (define-key stgit-mode-map "\d" 'stgit-unmark)
+  (define-key stgit-mode-map "?"   'stgit-help)
+  (define-key stgit-mode-map "h"   'stgit-help)
+  (define-key stgit-mode-map "p"   'previous-line)
+  (define-key stgit-mode-map "n"   'next-line)
+  (define-key stgit-mode-map "g"   'stgit-refresh)
+  (define-key stgit-mode-map "r"   'stgit-rename)
+  (define-key stgit-mode-map "e"   'stgit-edit)
+  (define-key stgit-mode-map "c"   'stgit-coalesce)
+  (define-key stgit-mode-map "N"   'stgit-new)
+  (define-key stgit-mode-map "R"   'stgit-repair)
+  (define-key stgit-mode-map "C"   'stgit-commit)
+  (define-key stgit-mode-map "U"   'stgit-uncommit)
+  (define-key stgit-mode-map ">"   'stgit-push-next)
+  (define-key stgit-mode-map "<"   'stgit-pop-next)
+  (define-key stgit-mode-map "P"   'stgit-push-or-pop)
+  (define-key stgit-mode-map "G"   'stgit-goto)
+  (define-key stgit-mode-map "="   'stgit-show)
+  (define-key stgit-mode-map "D"   'stgit-delete))
+
+(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-variable 'truncate-lines 't)
+  (run-hooks 'stgit-mode-hook))
+
+(defun stgit-add-mark (patch)
+  (let ((patchsym (intern patch)))
+    (setq stgit-marked-patches (cons patchsym stgit-marked-patches))))
+
+(defun stgit-remove-mark (patch)
+  (let ((patchsym (intern patch)))
+    (setq stgit-marked-patches (delq patchsym stgit-marked-patches))))
+
+(defun stgit-marked-patches ()
+  "Return the names of the marked patches."
+  (mapcar 'symbol-name stgit-marked-patches))
+
+(defun stgit-patch-at-point ()
+  "Return the patch name on the current line"
+  (save-excursion
+    (beginning-of-line)
+    (if (looking-at "[>+-][ *]\\([^ ]*\\)")
+        (match-string-no-properties 1)
+      nil)))
+
+(defun stgit-patches-marked-or-at-point ()
+  "Return the names 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 (patch)
+  "Move point to the line containing PATCH"
+  (let ((p (point)))
+    (goto-char (point-min))
+    (if (re-search-forward (concat "^[>+-][ *]" (regexp-quote patch) " ") nil t)
+        (progn (move-to-column goal-column)
+               t)
+      (goto-char p)
+      nil)))
+
+(defun stgit-mark ()
+  "Mark the patch under point"
+  (interactive)
+  (let ((patch (stgit-patch-at-point)))
+    (stgit-add-mark patch)
+    (stgit-refresh))
+  (next-line))
+
+(defun stgit-unmark ()
+  "Mark the patch on the previous line"
+  (interactive)
+  (forward-line -1)
+  (let ((patch (stgit-patch-at-point)))
+    (stgit-remove-mark patch)
+    (stgit-refresh)))
+
+(defun stgit-rename (name)
+  "Rename the patch under point"
+  (interactive (list (read-string "Patch name: " (stgit-patch-at-point))))
+  (let ((old-name (stgit-patch-at-point)))
+    (unless old-name
+      (error "No patch on this line"))
+    (stgit-capture-output nil
+      (stgit-run "rename" old-name name))
+    (stgit-refresh)
+    (stgit-goto-patch name)))
+
+(defun stgit-repair ()
+  "Run stg repair"
+  (interactive)
+  (stgit-capture-output nil
+   (stgit-run "repair"))
+  (stgit-refresh))
+
+(defun stgit-commit ()
+  "Run stg commit."
+  (interactive)
+  (stgit-capture-output nil (stgit-run "commit"))
+  (stgit-refresh))
+
+(defun stgit-uncommit (arg)
+  "Run stg uncommit. Numeric arg determines number of patches to uncommit."
+  (interactive "p")
+  (stgit-capture-output nil (stgit-run "uncommit" "-n" (number-to-string arg)))
+  (stgit-refresh))
+
+(defun stgit-push-next ()
+  "Push the first unapplied patch"
+  (interactive)
+  (stgit-capture-output nil (stgit-run "push"))
+  (stgit-refresh))
+
+(defun stgit-pop-next ()
+  "Pop the topmost applied patch"
+  (interactive)
+  (stgit-capture-output nil (stgit-run "pop"))
+  (stgit-refresh))
+
+(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 ((patch (stgit-patch-at-point))
+        (applied (stgit-applied-at-point)))
+    (stgit-capture-output nil
+       (stgit-run (if applied "pop" "push") patch))
+    (stgit-refresh)))
+
+(defun stgit-goto ()
+  "Go to the patch on the current line"
+  (interactive)
+  (let ((patch (stgit-patch-at-point)))
+    (stgit-capture-output nil
+       (stgit-run "goto" patch))
+    (stgit-refresh)))
+
+(defun stgit-show ()
+  "Show the patch on the current line"
+  (interactive)
+  (stgit-capture-output "*StGit patch*"
+    (stgit-run "show" (stgit-patch-at-point))
+    (with-current-buffer standard-output
+      (goto-char (point-min))
+      (diff-mode))))
+
+(defun stgit-edit ()
+  "Edit the patch on the current line"
+  (interactive)
+  (let ((patch (stgit-patch-at-point))
+        (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-patch) patch)
+    (setq default-directory dir)
+    (let ((standard-output edit-buf))
+      (stgit-run "edit" "--save-template=-" patch))))
+
+(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-patch))
+    (with-current-buffer log-edit-parent-buffer
+      (stgit-refresh))))
+
+(defun stgit-new ()
+  "Create a new patch"
+  (interactive)
+  (let ((edit-buf (get-buffer-create "*StGit edit*")))
+    (log-edit 'stgit-confirm-new t nil edit-buf)))
+
+(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-refresh))))
+
+(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 (patch-names)
+  "Delete the named patches"
+  (interactive (list (stgit-patches-marked-or-at-point)))
+  (if (zerop (length patch-names))
+      (error "No patches to delete")
+    (when (yes-or-no-p (format "Really delete %d patches? "
+                               (length patch-names)))
+      (stgit-capture-output nil
+        (apply 'stgit-run "delete" patch-names))
+      (stgit-refresh))))
+
+(defun stgit-coalesce (patch-names)
+  "Run stg coalesce on the named patches"
+  (interactive (list (stgit-marked-patches)))
+  (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-patches) patch-names)
+    (setq default-directory dir)
+    (let ((standard-output edit-buf))
+      (apply 'stgit-run "coalesce" "--save-template=-" patch-names))))
+
+(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-patches))
+    (with-current-buffer log-edit-parent-buffer
+      (stgit-refresh))))
+
+(defun stgit-help ()
+  "Display help for the StGit mode."
+  (interactive)
+  (describe-function 'stgit-mode))
+
+(provide 'stgit)
index 56886805a4abbfc5f3c9023667e3d262b888392d..795c77bb5989dec6eab46dbac71b1fa8b2e59a2d 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 b7a6629c60cfa8570d52b762d4b7e962f9b78f4b..9efc0898eb01fa8e09f8b0acc6190482515a2b84 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 10b3715737e995456f1e336cc65e4c5fe60d3e65..81854d3573155549b96dfbbc7f3a00cb2d5847c3 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',
+                                     'contrib/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/stgit/argparse.py b/stgit/argparse.py
new file mode 100644 (file)
index 0000000..4999272
--- /dev/null
@@ -0,0 +1,106 @@
+"""Utility functions for command-line option parsing."""
+
+import optparse, sys
+from stgit import utils
+from stgit.config import config
+
+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 [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 message_options():
+    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 (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)
+    m = optparse.make_option
+    return [m('-m', '--message', action = 'callback', callback = msg_callback,
+              dest = 'message', type = 'string',
+              help = 'use MESSAGE instead of invoking the editor'),
+            m('-f', '--file', action = 'callback', callback = file_callback,
+              dest = 'message', type = 'string', metavar = 'FILE',
+              help = 'use FILE instead of invoking the editor'),
+            m('--save-template', action = 'callback', callback = templ_callback,
+              metavar = 'FILE', dest = 'save_template', type = 'string',
+              help = 'save the message template to FILE and exit')]
+
+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 [optparse.make_option(
+        '-O', '--diff-opts', dest = 'diff_flags',
+        default = (config.get('stgit.diff-opts') or '').split(),
+        action = 'callback', callback = diff_opts_callback,
+        type = 'string', metavar = 'OPTIONS',
+        help = '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 ([optparse.make_option(
+                '--%s' % person, metavar = '"NAME <EMAIL>"', type = 'string',
+                action = 'callback', callback = full_callback, dest = person,
+                default = lambda p: p, help = 'set the %s details' % person)]
+            + [optparse.make_option(
+                '--%s%s' % (short, f), metavar = f.upper(), type = 'string',
+                action = 'callback', callback = short_callback, dest = person,
+                callback_args = (f,), help = 'set the %s %s' % (person, f))
+               for f in ['name', 'email', 'date']])
+
+def author_committer_options():
+    return person_opts('author', 'auth') + person_opts('committer', 'comm')
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 c703418df57e781db0247ccfa603b32e3b3dd80c..a5effb61cbc70c46724c1ba84c6955f4c26a9f73 100644 (file)
@@ -15,14 +15,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, os
-from optparse import OptionParser, make_option
-
-from stgit.commands.common import *
-from stgit.utils import *
+from optparse import make_option
 from stgit.out import *
-from stgit import stack, git
-
+from stgit.commands import common
+from stgit.lib import transaction
 
 help = 'delete the empty patches in the series'
 usage = """%prog [options]
@@ -31,7 +27,7 @@ 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()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-a', '--applied',
                        help = 'delete the empty applied patches',
                        action = 'store_true'),
@@ -40,18 +36,23 @@ options = [make_option('-a', '--applied',
                        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)
+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 +60,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)
diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
new file mode 100644 (file)
index 0000000..2d672bb
--- /dev/null
@@ -0,0 +1,122 @@
+# -*- 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 optparse import make_option
+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'
+usage = """%prog [options] <patches>
+
+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."""
+
+directory = common.DirectoryHasRepositoryLib()
+options = [make_option('-n', '--name', help = 'name of coalesced patch')
+           ] + argparse.message_options()
+
+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 e56f5a056d9c0d3b313aa59edd48d2906d48179d..df7fa67857330658e61b9349520458775f9d3434 100644 (file)
@@ -15,53 +15,85 @@ 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 optparse import make_option
+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]
+usage = """%prog [<patchnames>] | -n NUM | --all
 
-Merge the applied patches into the base of the current stack and
-remove them from the series while advancing the base.
+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.
 
-Use this command only if you want to permanently store the applied
-patches and no longer manage them 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.
 
-directory = DirectoryGotoToplevel()
-options = []
+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."""
 
+directory = common.DirectoryHasRepositoryLib()
+options = [make_option('-n', '--number', type = 'int',
+                       help = 'commit the specified number of patches'),
+           make_option('-a', '--all', action = 'store_true',
+                       help = 'commit all applied patches')]
 
 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 384038753ceb727fd3eeb417e46a4f992afbc83b..3cecec074a24880e6dc4e20097a91703b79b4066 100644 (file)
@@ -19,120 +19,84 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os, os.path, re
-from optparse import OptionParser, make_option
-
 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
 
 # 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 resolve them first'
+                           ' or revert the changes with "status --reset"')
 
 def print_crt_patch(crt_series, branch = None):
     if not branch:
@@ -145,29 +109,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 +172,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,29 +232,19 @@ 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 = parse_name_email(address)
+    if p:
+        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
@@ -482,11 +418,11 @@ 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):
+    """Parse the input text and return (description, authname,
     authemail, authdate, diff)
     """
-    descr, diff = __split_descr_diff(fobj.read())
+    descr, diff = __split_descr_diff(text)
     descr, authname, authemail, authdate = __parse_description(descr)
 
     # we don't yet have an agreed place for the creation date.
@@ -561,3 +497,11 @@ 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
+    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 1696cb9c5497efae8a0a51955f4b5226147cd916..c5d3754a2c24a781d542c309d812394564766c73 100644 (file)
@@ -16,67 +16,46 @@ 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 optparse import make_option
 
+from stgit.commands import common
+from stgit.lib import transaction
 
 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.
+Delete the patches passed as arguments.
 
 Note that the 'delete' operation is irreversible."""
 
-directory = DirectoryGotoToplevel()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-b', '--branch',
                        help = 'use BRANCH instead of the default one')]
 
 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)))
     else:
         parser.error('No patches specified')
-
-    applied = []
-
-    # 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 791b1cecc67f4b2055fe7593feec0b5590f27327..4f3e2c45e06afe464ac557a63b864f0436585330 100644 (file)
@@ -23,35 +23,29 @@ from pydoc import pager
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
-
+from stgit import argparse, stack, git
 
 help = 'show the tree diff'
 usage = """%prog [options] [<files or dirs>]
 
 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'
+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.
 
-If neither bottom nor top are given but a '//' is present, the command
-shows the specified patch (defaulting to the current one)."""
+rev = '[branch:](<patch>|{base}) | <tree-ish>'
+"""
 
 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')]
-
+                       action = 'store_true')
+           ] + argparse.diff_opts_option()
 
 def func(parser, options, args):
     """Show the tree diff
@@ -63,17 +57,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 +68,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(git.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 36d2e01461dd61a23ce8097087c95feb7221897a..92447a8c80a445e3bb62115ca1b3c987a31c5934 100644 (file)
@@ -18,14 +18,12 @@ 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 optparse import make_option
 
-from stgit.commands.common import *
-from stgit.utils import *
+from stgit import argparse, git, utils
+from stgit.commands import common
+from stgit.lib import git as gitlib, transaction
 from stgit.out import *
-from stgit import stack, git
-
 
 help = 'edit a patch description or diff'
 usage = """%prog [options] [<patch>]
@@ -49,196 +47,150 @@ separator:
   Diff text
 
 Command-line options can be used to modify specific information
-without invoking the editor.
+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 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.
-"""
+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."""
 
-directory = DirectoryGotoToplevel()
+directory = common.DirectoryHasRepositoryLib()
 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()
+           make_option('-e', '--edit', action = 'store_true',
+                       help = 'invoke interactive editor'),
+           ] + (argparse.sign_options() + argparse.message_options()
+                + argparse.author_committer_options()
+                + argparse.diff_opts_option())
+
+def patch_diff(repository, cd, diff, diff_flags):
+    if diff:
+        diff = repository.diff_tree(cd.parent.data.tree, cd.tree, diff_flags)
+        return '\n'.join([git.diffstat(diff), diff])
     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)
+        return None
+
+def patch_description(cd, diff):
+    """Generate a string containing the description to edit."""
+
+    desc = ['From: %s <%s>' % (cd.author.name, cd.author.email),
+            'Date: %s' % cd.author.date.isoformat(),
+            '',
+            cd.message]
+    if diff:
+        desc += ['---',
+                 '',
+                diff]
+    return '\n'.join(desc)
+
+def patch_desc(repository, cd, failed_diff, diff, diff_flags):
+    return patch_description(cd, failed_diff or patch_diff(
+            repository, cd, diff, diff_flags))
+
+def update_patch_description(repository, cd, text):
+    message, authname, authemail, authdate, diff = common.parse_patch(text)
+    cd = (cd.set_message(message)
+            .set_author(cd.author.set_name(authname)
+                                 .set_email(authemail)
+                                 .set_date(gitlib.Date.maybe(authdate))))
+    failed_diff = None
+    if diff:
+        tree = repository.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 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)
-    else:
-        __edit_update_patch(pname, options)
+        parser.error('Cannot edit more than one patch')
+
+    cd = orig_cd = stack.patches.get(patchname).commit.data
 
-    if pname != crt_pname:
-        # Push the patches back
-        between.reverse()
-        push_patches(crt_series, between)
+    # Read patch from user-provided description.
+    if options.message == None:
+        failed_diff = None
+    else:
+        cd, failed_diff = update_patch_description(stack.repository, cd,
+                                                   options.message)
+
+    # Modify author and committer data.
+    a, c = options.author(cd.author), options.committer(cd.committer)
+    if (a, c) != (cd.author, cd.committer):
+        cd = cd.set_author(a).set_committer(c)
+
+    # Add Signed-off-by: or similar.
+    if options.sign_str != None:
+        cd = cd.set_message(utils.add_sign_line(
+                cd.message, options.sign_str, gitlib.Person.committer().name,
+                gitlib.Person.committer().email))
+
+    if options.save_template:
+        options.save_template(
+            patch_desc(stack.repository, cd, failed_diff,
+                       options.diff, options.diff_flags))
+        return utils.STGIT_SUCCESS
+
+    # Let user edit the patch manually.
+    if cd == orig_cd or options.edit:
+        fn = '.stgit-edit.' + ['txt', 'patch'][bool(options.diff)]
+        cd, failed_diff = update_patch_description(
+            stack.repository, cd, utils.edit_string(
+                patch_desc(stack.repository, cd, failed_diff,
+                           options.diff, options.diff_flags),
+                fn))
+
+    def failed():
+        fn = '.stgit-failed.patch'
+        f = file(fn, 'w')
+        f.write(patch_desc(stack.repository, cd, failed_diff,
+                           options.diff, options.diff_flags))
+        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:
+        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 c4fb7e3c578b4950e89933428e7fcce8342023aa..fb373a96b5d50ce365075f7c30b554197ff9494c 100644 (file)
@@ -18,14 +18,13 @@ 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
+import os
+from optparse import make_option
 
+from stgit.commands import common
+from stgit import argparse, git, templates
+from stgit.out import out
+from stgit.lib import git as gitlib
 
 help = 'exports patches to a directory'
 usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
@@ -49,7 +48,7 @@ file:
   %(commemail)s   - committer's e-mail
 """
 
-directory = DirectoryHasRepository()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-d', '--dir',
                        help = 'export patches to DIR instead of the default'),
            make_option('-p', '--patch',
@@ -64,20 +63,20 @@ options = [make_option('-d', '--dir',
                        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')]
-
+                       action = 'store_true')
+           ] + argparse.diff_opts_option()
 
 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 +88,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 +113,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 +130,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': git.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 +158,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 +171,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 cc9ed309a57e7c817ea95bdbe0e66dac6c76e849..cdb0b526f9bdafb739b3894930a257f05e975de0 100644 (file)
@@ -22,11 +22,10 @@ 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 import argparse, stack, git
 
 help = 'show the files modified by a patch (or the current patch)'
-usage = """%prog [options] [<patch>]
+usage = """%prog [options] [[<branch>:]<patch>]
 
 List the files modified by the given patch (defaulting to the current
 one). Passing the '--stat' option shows the diff statistics for the
@@ -38,36 +37,28 @@ 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')]
-
+                       action = 'store_true')
+           ] + argparse.diff_opts_option()
 
 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')
 
-    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) + '\n')
+        out.stdout_raw(git.diffstat(git.diff(rev1 = rev1, rev2 = rev2)) + '\n')
     elif options.bare:
         out.stdout_raw(git.barefiles(rev1, rev2) + '\n')
     else:
-        if options.diff_opts:
-            diff_flags = options.diff_opts.split()
-        else:
-            diff_flags = []
-
-        out.stdout_raw(git.files(rev1, rev2, diff_flags = diff_flags) + '\n')
+        out.stdout_raw(git.files(rev1, rev2, diff_flags = options.diff_flags)
+                       + '\n')
index 84b840b52fd2044b87f358ef4303d0268cc798de..99ae6c0a17ea6e48c6de25d744701e6985e5476c 100644 (file)
@@ -15,13 +15,9 @@ 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
-
+from stgit.commands import common
+from stgit.lib import transaction
 
 help = 'push or pop patches to the given one'
 usage = """%prog [options] <name>
@@ -30,38 +26,28 @@ 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."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-k', '--keep',
-                       help = 'keep the local changes when popping patches',
-                       action = 'store_true')]
-
+directory = common.DirectoryHasRepositoryLib()
+options = []
 
 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 94b0229558b9cb471f0b8cd42442fd1bd1b16cb3..3819acc62b80aaf3a5ad13fd188bbaaefc679c5a 100644 (file)
@@ -15,28 +15,24 @@ 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.out import out
+from stgit.commands import common
+from stgit.lib import stack
 
 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')]
+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."""
 
+directory = common.DirectoryHasRepositoryLib()
+options = []
 
 def func(parser, options, args):
     """Show the applied patches
@@ -48,4 +44,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 cd44d3fa4af913dc44706f7523bd655092cc6001..5fc83e935d241f1ad00a26b74447cb5337defd62 100644 (file)
@@ -23,8 +23,7 @@ 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 import argparse, stack, git
 
 help = 'import a GNU diff file as a new patch'
 usage = """%prog [options] [<file>|<url>]
@@ -88,7 +87,7 @@ options = [make_option('-m', '--mail',
                        help = 'use COMMNAME as the committer name'),
            make_option('--commemail',
                        help = 'use COMMEMAIL as the committer e-mail')
-           ] + make_sign_options()
+           ] + argparse.sign_options()
 
 
 def __strip_patch_name(name):
@@ -175,14 +174,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 +219,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())
 
     if filename:
         f.close()
 
-    if patch:
-        pname = patch
-    else:
-        pname = filename
-
     __create_patch(pname, message, author_name, author_email,
                    author_date, diff, options)
 
index 475a4ce59f44a893b092caf5228690196a6ab5a4..b68acd746cb5f7bbb0a72eb3fdb4b4f95ffa66e4 100644 (file)
@@ -16,13 +16,8 @@ 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
-
+from stgit.commands import common
+from stgit.lib import stack
 
 help = 'initialise the current branch for use with StGIT'
 usage = """%prog [options]
@@ -31,14 +26,13 @@ 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()
+directory = common.DirectoryHasRepositoryLib()
 options = []
 
-
 def func(parser, options, args):
     """Performs the repository initialisation
     """
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    crt_series.init()
+    stack.Stack.initialise(directory.repository)
index 05c905b05002f73aac85b85a5ce88941dbad68b6..521c8eb26fe83b1f98f810522c2f64fdfe93c3cf 100644 (file)
@@ -22,22 +22,25 @@ from optparse import OptionParser, make_option
 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
 
 
 help = 'send a patch or series of patches by e-mail'
-usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
+usage = r"""%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
 
 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,7 +87,7 @@ 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
@@ -133,8 +136,9 @@ options = [make_option('-a', '--all',
                        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('--smtp-server',
+                       metavar = 'HOST[:PORT] or "/path/to/sendmail -t -i"',
+                       help = 'SMTP server or command to use for sending mail'),
            make_option('-u', '--smtp-user', metavar = 'USER',
                        help = 'username for SMTP authentication'),
            make_option('-p', '--smtp-password', metavar = 'PASSWORD',
@@ -144,12 +148,10 @@ options = [make_option('-a', '--all',
                        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')]
-
+                       action = 'store_true')
+           ] + argparse.diff_opts_option()
 
 def __get_sender():
     """Return the 'authname <authemail>' string as read from the
@@ -185,8 +187,14 @@ def __parse_addresses(msg):
 
     return (from_addr_list[0], to_addr_list)
 
-def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
-                   smtpuser, smtppassword, use_tls):
+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_smtp(smtpserver, from_addr, to_addr_list, msg,
+                        smtpuser, smtppassword, use_tls):
     """Send the message using the given SMTP server
     """
     try:
@@ -208,13 +216,25 @@ 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.
@@ -361,9 +381,9 @@ 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]))}
+                 'diffstat':     git.diffstat(git.diff(
+                     rev1 = git_id(crt_series, '%s^' % patches[0]),
+                     rev2 = git_id(crt_series, '%s' % patches[-1])))}
 
     try:
         msg_string = tmpl % tmpl_dict
@@ -402,8 +422,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();
@@ -431,11 +451,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))
@@ -444,6 +459,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
@@ -452,13 +470,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':         diff,
+                 'diffstat':     git.diffstat(diff),
                  # for backward template compatibility
                  'date':         '',
                  'version':      version_str,
index 6a8f08630a548f9996227155694e791ecb597fdf..4a58d52ef1fdab23f4fe456d01e3313c56bb57c6 100644 (file)
@@ -16,13 +16,12 @@ 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
+from optparse import make_option
 
+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 patch and make it the topmost one'
 usage = """%prog [options] [name]
@@ -38,48 +37,65 @@ 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()
-
+directory = common.DirectoryHasRepositoryLib()
+options = (argparse.author_committer_options()
+           + argparse.message_options() + argparse.sign_options())
 
 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())
 
-    if options.author:
-        options.authname, options.authemail = name_email(options.author)
+    # Set patch commit message from commandline.
+    if options.message != None:
+        cd = cd.set_message(options.message)
 
-    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)
+    # Modify author and committer data.
+    cd = (cd.set_author(options.author(cd.author))
+            .set_committer(options.committer(cd.committer)))
+
+    # 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 add2a33b12a77adce5bb56d4ed380896778f8648..44b461e7f4a9ecbac9e859b2ed33fe91831eae02 100644 (file)
@@ -83,12 +83,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 +115,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
index 6e8ed0c313f87e3ec9bd3a1f80cf6a1b1f19eb80..73e4ee020eaa3104ad09b09f33dde384cace1253 100644 (file)
@@ -31,11 +31,9 @@ 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."""
+one no longer being visible. 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',
@@ -45,6 +43,9 @@ options = [make_option('-f', '--force',
            make_option('--update',
                        help = 'only update the current patch files',
                        action = 'store_true'),
+           make_option('--index',
+                       help = 'use the current contents of the index instead of looking at the working directory',
+                       action = 'store_true'),
            make_option('--undo',
                        help = 'revert the commit generated by the last refresh',
                        action = 'store_true'),
@@ -76,6 +77,14 @@ def func(parser, options, args):
         if not patch:
             raise CmdException, 'No patches applied'
 
+    if options.index:
+        if args or options.update:
+            raise CmdException, \
+                  'Only full refresh is available with the --index option'
+        if options.patch:
+            raise CmdException, \
+                  '--patch is not compatible with the --index option'
+
     if not options.force:
         check_head_top_equal(crt_series)
 
@@ -85,16 +94,17 @@ def func(parser, options, args):
         out.done()
         return
 
-    files = [path for (stat, path) in git.tree_status(files = args, verbose = True)]
+    if not options.index:
+        files = [path for (stat, path) in git.tree_status(files = args, verbose = True)]
 
-    if files or not crt_series.head_top_equal():
+    if options.index or 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')
+            rev1 = git_id(crt_series, 'HEAD^')
+            rev2 = git_id(crt_series, 'HEAD')
             patch_files = git.barefiles(rev1, rev2).split('\n')
             files = [f for f in files if f in patch_files]
             if not files:
@@ -105,8 +115,13 @@ def func(parser, options, args):
 
         if autoresolved == 'yes':
             resolved_all()
-        crt_series.refresh_patch(files = files,
-                                 backup = True, notes = options.annotate)
+
+        if options.index:
+            crt_series.refresh_patch(cache_update = False,
+                                     backup = True, notes = options.annotate)
+        else:
+            crt_series.refresh_patch(files = files,
+                                     backup = True, notes = options.annotate)
 
         if crt_series.empty_patch(patch):
             out.done('empty patch')
index 011db9133a5f77912ccae79527295e96f11ba22f..4ee75b818756b6215d222bf27f05faaad2176ddf 100644 (file)
@@ -31,8 +31,7 @@ usage = """%prog [options] [<files...>]
 
 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."""
+'C'."""
 
 directory = DirectoryHasRepository(needs_current_series = False)
 options = [make_option('-a', '--all',
@@ -77,18 +76,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 e3467cc1a539bb46e73afa81c4a4e7d821e35762..f705d0071e7b38bb894700c8679a74fc46a5f288 100644 (file)
@@ -16,15 +16,12 @@ 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
+from optparse import make_option
 
+from stgit.commands import common
+from stgit.commands.common import parse_patches
+from stgit.out import out
+from stgit.config import config
 
 help = 'print the patch series'
 usage = """%prog [options] [<patch-range>]
@@ -34,13 +31,20 @@ 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()
+directory = common.DirectoryHasRepositoryLib()
+
 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',
+           make_option('-A', '--applied',
+                       help = 'show the applied patches only',
+                       action = 'store_true'),
+           make_option('-U', '--unapplied',
+                       help = 'show the unapplied patches only',
+                       action = 'store_true'),
+           make_option('-H', '--hidden',
                        help = 'show the hidden patches only',
                        action = 'store_true'),
            make_option('-m', '--missing', metavar = 'BRANCH',
@@ -66,90 +70,78 @@ options = [make_option('-b', '--branch',
                        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):
+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, empty_prefix, length, options):
     """Print a patch name, description and various markers.
     """
     if options.noprefix:
         prefix = ''
-    elif options.empty and crt_series.empty_patch(patch):
+    elif options.empty and stack.patches.get(patch).is_empty():
         prefix = empty_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 +178,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, '+ ', '0 ', max_len, options)
+        __print_patch(stack, applied[-1], branch_str, '> ', '0>', 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, '- ', '0 ', 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 72d1be387f02678011ba3fb67202cfd1d4f2e673..36361d6c884c2573df4b9094459593696d91a8ae 100644 (file)
@@ -20,8 +20,7 @@ from optparse import OptionParser, make_option
 from pydoc import pager
 
 from stgit.commands.common import *
-from stgit import git
-
+from stgit import argparse, git
 
 help = 'show the commit corresponding to a patch (or the current patch)'
 usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
@@ -38,10 +37,8 @@ options = [make_option('-b', '--branch',
                        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"')]
-
+                       action = 'store_true')
+           ] + argparse.diff_opts_option()
 
 def func(parser, options, args):
     """Show commit log and diff
@@ -62,13 +59,9 @@ def func(parser, options, args):
             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 = []
-
     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 94d0b57ac0ef2fb75c6e2ce9750d2c5bebf8d7a2..a84ff6c6d33362a1f1ff13772eb3a25d2f3ce575 100644 (file)
@@ -64,8 +64,7 @@ options = [make_option('-m', '--modified',
                        action = 'store_true')]
 
 
-def status(files = None, modified = False, new = False, deleted = False,
-           conflict = False, unknown = False, noexclude = False):
+def status(files, modified, new, deleted, conflict, unknown, noexclude):
     """Show the tree status
     """
     cache_files = git.tree_status(files,
@@ -105,8 +104,8 @@ def func(parser, options, args):
 
     if options.reset:
         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 a04ff8231020b0adf400d3f3dcb572205ff5f700..0e2c18fbf5c320032f31b8e9c92acc01ecf5f869 100644 (file)
@@ -57,7 +57,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.
@@ -159,7 +159,6 @@ def func(parser, options, args):
 
         # 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)
         patch.set_top(top, backup = True)
 
         # the actual merging (either from a branch or an external file)
index e7cb275f849e372099cd59f4e25b0108484bac80..c8fbba4ab154d902d26a3ceb2869f335e9dd6a51 100644 (file)
@@ -16,33 +16,30 @@ 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 optparse import make_option
 
+from stgit.commands import common
+from stgit.out import out
 
 help = 'print the name of the top patch'
 usage = """%prog [options]
 
 Print the name of the current (topmost) patch."""
 
-directory = DirectoryHasRepository()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-b', '--branch',
                        help = 'use BRANCH instead of the default one')]
 
-
 def func(parser, options, args):
     """Show the name of the topmost patch
     """
     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 ba3448fd7c52f7831eae95bc796337aa70dc9099..c21306b4705576e9e0831233db7fb82d4f6b9590 100644 (file)
@@ -17,20 +17,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 optparse import make_option
+from stgit.commands import common
+from stgit.lib import transaction
 from stgit.out import *
-from stgit import stack, git
+from stgit import utils
 
 help = 'turn regular GIT commits into StGIT patches'
 usage = """%prog [<patchnames>] | -n NUM [<prefix>]] | -t <committish> [-x]
 
 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,7 +46,7 @@ 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()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-n', '--number', type = 'int',
                        help = 'uncommit the specified number of commits'),
            make_option('-t', '--to',
@@ -60,19 +58,18 @@ options = [make_option('-n', '--number', type = 'int',
 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 +85,56 @@ 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)
+    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()
index 89344454c181fc5fb6448213bad28954b6691204..9bfdd52e29dd890ea989fbef50b0cb7c4e7757eb 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 35579d4223f03f6160ce305681bd8c823cad8d1b..ee31ecd07a5a7d17d1a9bba364658e97c0cdaedc 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,12 +669,9 @@ 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):
-    """Return the diffstat between rev1 and rev2."""
-    return GRun('apply', '--stat', '--summary'
-                ).raw_input(diff(files, rev1, rev2)).raw_output()
+def diffstat(diff):
+    """Return the diffstat of the supplied diff."""
+    return GRun('apply', '--stat', '--summary').raw_input(diff).raw_output()
 
 def files(rev1, rev2, diff_flags = []):
     """Return the files modified between rev1 and rev2
@@ -904,6 +751,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.
@@ -998,7 +856,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 c1af2f85f82c315e89593cadbdcf3e9aef8344a2..1fe226e0fcb1bb824764b84609f12a975d19ba19 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/git.py b/stgit/lib/git.py
new file mode 100644 (file)
index 0000000..2386e27
--- /dev/null
@@ -0,0 +1,845 @@
+"""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)
+    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> %s' % (self.name, self.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)
+
+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()
+
+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."""
+
+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):
+        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': '.' })
+    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))
+    cwd = property(lambda self: self.__worktree.directory)
+    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.discard_output()
+        except run.RunException, e:
+            if r.exitcode == 1:
+                raise MergeConflictException()
+            else:
+                raise MergeException('Index/worktree dirty')
+    def changed_files(self):
+        return self.run(['git', 'diff-files', '--name-only']).output_lines()
+    def update_index(self, files):
+        self.run(['git', 'update-index', '--remove', '-z', '--stdin']
+                 ).input_nulterm(files).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)
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
new file mode 100644 (file)
index 0000000..1059955
--- /dev/null
@@ -0,0 +1,240 @@
+"""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 __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)
+        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()
+
+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
+    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..cbfca55
--- /dev/null
@@ -0,0 +1,310 @@
+"""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
+
+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, allow_conflicts = False):
+        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.__error = None
+        self.__current_tree = self.__stack.head.data.tree
+        self.__base = self.__stack.base
+        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
+    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)
+    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
+    def __checkout(self, tree, iw):
+        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()
+        if self.__current_tree == tree:
+            # 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
+        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.__applied + self.__unapplied + self.__hidden)
+        for pn, commit in self.__patches.iteritems():
+            if commit == None:
+                assert self.__stack.patches.exists(pn)
+            else:
+                assert pn in remaining
+    @property
+    def __head(self):
+        if self.__applied:
+            return self.__patches[self.__applied[-1]]
+        else:
+            return self.__base
+    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)
+    def run(self, iw = None, set_head = True):
+        """Execute the transaction. Will either succeed, or fail (with an
+        exception) and do nothing."""
+        self.__check_consistency()
+        new_head = self.__head
+
+        # Set branch head.
+        if set_head:
+            if iw:
+                try:
+                    self.__checkout(new_head.data.tree, iw)
+                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:
+            out.error(self.__error)
+
+        # Write patches.
+        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, self.__msg)
+            else:
+                self.__stack.patches.new(pn, commit, self.__msg)
+        _print_current_patch(self.__stack.patchorder.applied, self.__applied)
+        self.__stack.patchorder.applied = self.__applied
+        self.__stack.patchorder.unapplied = self.__unapplied
+        self.__stack.patchorder.hidden = self.__hidden
+
+        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):
+        """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
+                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)
+        s = ['', ' (empty)'][cd.is_nochange()]
+        oldparent = cd.parent
+        cd = cd.set_parent(self.__head)
+        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)
+        merge_conflict = False
+        if not tree:
+            if iw == None:
+                self.__halt('%s does not apply cleanly' % pn)
+            try:
+                self.__checkout(ours, iw)
+            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:
+                tree = ours
+                merge_conflict = True
+                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']):
+            self.patches[pn] = self.__stack.repository.commit(cd)
+        else:
+            s = ' (unmodified)'
+        if pn in self.hidden:
+            x = self.hidden
+        else:
+            x = self.unapplied
+        del x[x.index(pn)]
+        self.applied.append(pn)
+        out.info('Pushed %s%s' % (pn, s))
+        if merge_conflict:
+            # We've just caused conflicts, so we must allow them in
+            # the final checkout.
+            self.__allow_conflicts = lambda trans: True
+
+            self.__halt('Merge conflict')
+
+    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 a03447fc6b4aee33632a10fbc8bd5f0cce46eb44..a98fe49d90f26560a2667d432010c1a2320d1c7c 100644 (file)
@@ -18,11 +18,12 @@ 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
+import sys, os, traceback
 from optparse import OptionParser
 
 import stgit.commands
 from stgit.out import *
+from stgit import run, utils
 
 #
 # The commands map
@@ -39,11 +40,11 @@ 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]
         
@@ -58,15 +59,13 @@ class Commands(dict):
         return getattr(stgit.commands, cmd_mod)
 
 commands = Commands({
-    'add':              'add',
-    'applied':          'applied',
     'branch':           'branch',
     'delete':           'delete',
     'diff':             'diff',
     'clean':            'clean',
     'clone':            'clone',
+    'coalesce':         'coalesce',
     'commit':           'commit',
-    'cp':              'copy',
     'edit':             'edit',
     'export':           'export',
     'files':            'files',
@@ -90,14 +89,12 @@ commands = Commands({
     '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'
     })
@@ -108,9 +105,9 @@ repocommands = (
     'id',
     )
 stackcommands = (
-    'applied',
     'branch',
     'clean',
+    'coalesce',
     'commit',
     'float',
     'goto',
@@ -125,7 +122,6 @@ stackcommands = (
     'series',
     'sink',
     'top',
-    'unapplied',
     'uncommit',
     'unhide',
     )
@@ -146,11 +142,8 @@ patchcommands = (
     'sync',
     )
 wccommands = (
-    'add',
-    'cp',
     'diff',
     'resolved',
-    'rm',
     'status',
     )
 
@@ -195,7 +188,7 @@ def print_help():
 #
 # The main function (command dispatcher)
 #
-def main():
+def _main():
     """The main function
     """
     global prog
@@ -206,7 +199,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,13 +209,13 @@ 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]
@@ -232,16 +225,16 @@ def main():
             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)
@@ -265,7 +258,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 +271,27 @@ def main():
             else:
                 command.crt_series = Series()
 
-        command.func(parser, options, args)
+        ret = command.func(parser, options, args)
     except (StgException, IOError, ParsingError, NoSectionError), err:
         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(ret or utils.STGIT_SUCCESS)
 
-    sys.exit(0)
+def main():
+    try:
+        _main()
+    finally:
+        run.finish_logging()
index d3c86b4cff9012c51a68f47547c933d9b0499593..753c17642f4c42ea471207408d14fd2a440f2e09 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 fa304d039159d95730d0e96e540736a5d69ad443..7493ed321f094198a36ceb1e225d95bf6b020a5e 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 802a382ad0f7609c136d660daee6c05e28c68c1a..74c2c108f3519f160d9d8d64d52e500f4284abdd 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)
@@ -927,11 +840,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 +895,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 +922,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 +994,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 +1016,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 "push --undo".')
 
         append_string(self.__applied_file, name)
 
@@ -1129,12 +1035,10 @@ 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
index 3a480c0e6d9d9a2c0323fca3aaa789fd96068f08..979fd3bbfa176b6c5df2d5418c7cc1bcdd954629 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 *
@@ -175,12 +175,8 @@ def call_editor(filename):
 
     # the editor
     editor = config.get('stgit.editor')
-    if editor:
-        pass
-    elif 'EDITOR' in os.environ:
-        editor = os.environ['EDITOR']
-    else:
-        editor = 'vi'
+    if not editor:
+        editor = os.environ.get('EDITOR', 'vi')
     editor += ' %s' % filename
 
     out.start('Invoking the editor: "%s"' % editor)
@@ -189,6 +185,17 @@ def call_editor(filename):
         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 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."""
@@ -231,21 +238,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 +248,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 15832ad86bfb5b5c8767660f02879ce6ad6ee066..d57053d90afbce3740c72b777a811790f48e2452 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 ffb3c66e8d4cf72683f0fa056185ee1a50963bff..1e9510b76744ccdccb3d5ac8c0a337693e0ba454 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 0eed3a44088c1d7adede76ab90d090d04b86fba6..3f7962ac13681b01aba91f923f24e9731345c1c6 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 a0307394699caa485bf5cf8f8a550802dd750ec4..ac92aa8a059a5273ec83a78a479ca4f5486137b3 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 5a097a405058360ca175ca5af11b3dca5ebed36c..3fff3ee855e8cd540e91b56923ae1a5fbbcdc435 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 dd121326417434b127fd5a265d429f10670494dc..d5d3aefeab699a1c6648cfb36d0b912c31f92b96 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 b0087e91a3bdd48d95295f04ebdaf05d98502e8f..1303b418404487466a3672174893ce946eef1f89 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 0be5d9b21f6f9b4f2684f16544ee8b969a849a0e..826e41d85b78ee0c9d75bb4d7f4e5811d28f9ffa 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 647c20063b4ef49d18700ddef0739c61f133df58..35c9bdda24b09eeadfdcfce199eac97870f6a540 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,7 +49,7 @@ 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
      )
 "
 
@@ -57,8 +57,8 @@ test_expect_success \
     'Rollback the push' '
     (
         cd bar && stg push --undo &&
-        [ "$(echo $(stg applied))" = "" ] &&
-        [ "$(echo $(stg unapplied))" = "p1 p2" ]
+        [ "$(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 805e805c9cd34e231a711f899a9a2d2b38b0e806..3a7efc1f434e40fb703a30963602c375363e876d 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
      )
     '
@@ -55,7 +55,7 @@ test_expect_success \
 
 test_expect_success \
     'Check that all went well' \
-    "diff -u foo/file bar/file
+    "test_cmp foo/file bar/file
 "
 
 test_done
index edfa7105e2fced7b2491ebf11288e3b756c09459..544fe8d1a2d5f1ca24bb3163aedf24f0c24f61c2 100755 (executable)
@@ -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,7 +43,7 @@ test_expect_success \
 test_expect_success \
        'Push the second patch with conflict' \
        '
-       ! stg push bar
+       conflict_old stg push bar
        '
 
 test_expect_success \
@@ -55,14 +55,14 @@ test_expect_success \
 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
        '
index 6e49b4d28ffbbe69b0a6cd21ab203b65636719f7..e1ed5777472918ced5f3c6dfd8bf8858a2d8c9f0 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 40cd2a2aa33bc16f6fb3f010a47d3352c80effd3..db473f2a4faffb77033e8e20fe90e35d96114f0a 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 54a5b896e199d8529e99fe994d30d5ee266e63f0..f852762c2efa4a2317c1ee3a99d16036a826d10f 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 2e7ff211745d413bb2fee81080407444202fbea0..43e0d04b4fa29724750cd911e18bba22b4ba158a 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 5d9bdbdc4b8e408eb997a9d2f38fd65302ede1f7..8d5d4e50746cbdafc8d58a95b0f672029e5d1a25 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 910b23abf589997b5d05167ec9ae7e0abb8c99d4..9fad3fa892b72b8b32548df00917cee46d82939f 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
index 5b842d0b116246398a29d4766d4185c3ed98a63c..13cd1e338e0cfc576288a7c04507a554bd06e5d8 100755 (executable)
@@ -20,7 +20,7 @@ test_expect_success \
        '
        stg new foo -m "Foo Patch" &&
        echo foo > test && echo foo2 >> test &&
-       stg add test &&
+       git add test &&
        stg refresh --annotate="foo notes"
        '
 
@@ -35,9 +35,7 @@ test_expect_success \
 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"
        '
 
@@ -55,7 +53,7 @@ test_expect_success \
        'Check the "push" log' \
        '
        stg pop &&
-       echo foo > test2 && stg add test2 && stg refresh &&
+       echo foo > test2 && git add test2 && stg refresh &&
        stg push &&
        stg log --full | grep -q -e "^push    "
        '
@@ -84,7 +82,7 @@ test_expect_success \
        echo bar > test && stg refresh &&
        stg pop &&
        echo foo > test && stg refresh &&
-       ! stg push &&
+       conflict_old stg push &&
        stg log --full | grep -q -e "^push(c) "
        '
 
index 814c9bd2981f3099345c920f7dc4fba00205f355..e44af3a5676d1e1a2c15d117247d574031cfc87c 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 2767c4cbe0fb2be1de4cc64d48ce16ca35030066..516aa44cfb3301a5602ea54d10ca014bc86daf8b 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 df03d79c860db85377e3bac80eab8c1aca83f6ab..ef0b29d27c8a59a5febe64227e846b95ddb2b629 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 8eff308ebfa7e7477096522ac041b42a24df27f1..cb7fb0d6dd3d1d5a480902cdc487e3c2e635a775 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
index 618ebc7cfc7b15b6b647481cbee10da89035731b..8f2d44a0eb526dcec31559cd61791dce45f26828 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 624e51c7b53fe045cfabcaf8aa3e6658154bc249..9c317411f0dc2aaead640031850d4c4b9cafb57c 100755 (executable)
@@ -91,4 +91,46 @@ 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_done
index 484dbabb75702d2c38ca0b73e84c0a1f3d9930a4..00ea7bd8d5b86623865f77d825cd5b0390a437c8 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 07435f5011ad05ac3397ff5bb8cf05dd8ebb8baf..670c7c6fe765038b0fb2d9f1b68a5dff8688dbd3 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 69b0faef62160227bb585a6b0f47cf7c1709b76a..777ccb5f83b95a15b430a75654054c5b5afcd81b 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 952ee7e90ffdaef61a4c23cec9719327b4908874..5619bda9f8ca2e03ee9d1995ecd9748691db5140 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 ec2a104fc2e089a5d9653faa852cf416b987c909..adbf242722e090fdfe4ccdb164d694745012c9b9 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 750e4291bb675662eed035c6b7e73557783792f8..92c1cc86577b12f6ff77f955fe6ae3874765757c 100755 (executable)
@@ -8,7 +8,7 @@ test_expect_success 'Refresh from a subdirectory' '
     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 .. &&
index 3364c188f034ae9f6e2229f95c69fc16f3e2d51d..99fd29ff1ecc32924997efcb511a5764608ef5cc 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..ef5bf99
--- /dev/null
@@ -0,0 +1,31 @@
+#!/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))" = "" ]
+'
+
+test_done
index ffac29597aaf9f691dbd81d5691adbf9e398af99..aad6d4500577b1dc11c4dc38b9921b3dceffa38d 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 d42e90f4a9b35dbd667900ea61bd31df79b700b1..6fb735212cb838e14246bfd19421218aa5497b36 100755 (executable)
@@ -31,16 +31,16 @@ A 2.txt
 EOF
 test_expect_failure '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 5f47f864473422d580e5fbc4b34bdc5cf0160454..32900d0a0852bf1887f677840078b222fdb64d41 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/t3300-edit.sh b/t/t3300-edit.sh
new file mode 100755 (executable)
index 0000000..5772e48
--- /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\n" >> "\$1"
+EOF
+    chmod a+x "$1"
+}
+
+mkeditor vi
+test_expect_failure '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_failure '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_failure '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_failure '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_failure '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
index 8a308fbf7ef53b24fa1ad0cbfbe5a6c04ad75cbd..b89c7202b93bb814d461bc90875a86c9bc027aaa 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 3d114a2da293be2fde1075e96cc79109cf9ed8f7..ad8da684b2c924d5456f29cf906cd99bd4181202 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
+TERM=dumb
+export LANG LC_ALL PAGER TERM TZ
 EDITOR=:
 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
@@ -37,6 +43,7 @@ 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 +64,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 +82,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 +149,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 +176,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 +223,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 +239,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 +285,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 +367,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 +381,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 +408,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 430b341d01b87466457d5c2e905fa2853a00b8a5..70d2df02a9cfb190bb0723269378379a06f0362d 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 e7f3481a23600c969a3cdf9bf793e8239546c1d1..11f2075e097c8a7bc648f1ef38f15504c5e81c2d 100644 (file)
@@ -4,6 +4,5 @@ From: %(authname)s <%(authemail)s>
 
 %(longdescr)s
 ---
-
 %(diffstat)s
 
index f5c35c25821e981fc9f34fd95760377fc07e58f8..7d4022a2da8c59d03c8d5d7eacd420bf1c52f57c 100644 (file)
@@ -3,6 +3,5 @@ Subject: [%(prefix)sPATCH%(version)s%(number)s] %(shortdescr)s
 
 %(fromauth)s%(longdescr)s
 ---
-
 %(diffstat)s
 %(diff)s