chiark / gitweb /
Merge branch 'stable'
authorCatalin Marinas <catalin.marinas@gmail.com>
Thu, 17 Apr 2008 20:50:14 +0000 (21:50 +0100)
committerCatalin Marinas <catalin.marinas@gmail.com>
Thu, 17 Apr 2008 20:50:14 +0000 (21:50 +0100)
65 files changed:
Documentation/stg-cp.txt [deleted file]
Documentation/tutorial.txt
contrib/Makefile [new file with mode: 0644]
contrib/stgit-completion.bash
contrib/stgit.el [new file with mode: 0644]
examples/gitconfig
setup.py
stgit/commands/add.py [deleted file]
stgit/commands/applied.py
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/diff.py
stgit/commands/edit.py
stgit/commands/export.py
stgit/commands/files.py
stgit/commands/goto.py
stgit/commands/imprt.py
stgit/commands/mail.py
stgit/commands/pick.py
stgit/commands/refresh.py
stgit/commands/resolved.py
stgit/commands/rm.py [deleted file]
stgit/commands/show.py
stgit/commands/status.py
stgit/commands/sync.py
stgit/commands/unapplied.py
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/run.py
stgit/stack.py
stgit/utils.py
t/t0002-status.sh
t/t1200-push-modified.sh
t/t1202-push-undo.sh
t/t1203-push-conflict.sh [new file with mode: 0755]
t/t1204-pop-keep.sh
t/t1205-push-subdir.sh
t/t1300-uncommit.sh
t/t1301-repair.sh
t/t1400-patch-history.sh
t/t1500-float.sh
t/t1600-delete-one.sh
t/t1601-delete-many.sh
t/t1700-goto-top.sh
t/t2000-sync.sh
t/t2100-pull-policy-fetch.sh
t/t2101-pull-policy-pull.sh
t/t2102-pull-policy-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/t2800-goto-subdir.sh [new file with mode: 0755]
t/t3000-dirty-merge.sh [new file with mode: 0755]

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 2b8e4e782aa827de3be31f43af312ab2a03401d8..6eaa623bf69fb475a0fa9256c68e822fd4b314e6 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/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 b3b23d497dd45f0942c5ffb2e324052d5083c8b2..b02eb64d1ced5e70feae395b67e6714c7a00c5bb 100644 (file)
@@ -18,6 +18,7 @@ _stg_commands="
     diff
     clean
     clone
+    coalesce
     commit
     cp
     edit
@@ -238,6 +239,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 ;;
diff --git a/contrib/stgit.el b/contrib/stgit.el
new file mode 100644 (file)
index 0000000..339ef13
--- /dev/null
@@ -0,0 +1,375 @@
+;; 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 dir)
+  (stgit-refresh))
+
+(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))
+
+(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-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-"))
+        (patch (stgit-create-patch-name
+                (buffer-substring (point-min)
+                                  (save-excursion (goto-char (point-min))
+                                                  (end-of-line)
+                                                  (point))))))
+    (write-region (point-min) (point-max) file)
+    (stgit-capture-output nil
+      (stgit-run "new" "-m" "placeholder" patch)
+      (stgit-run "edit" "-f" file patch))
+    (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-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 52d2a696d5d0c004fa13c7327136f75f44655650..c16f78671c77712d29f07fe48e259f13a3ff6254 100644 (file)
        # 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
index d90cf5bd8802bff07650da3dcc92407498b17bbd..3be087cbba164b395515e4b6dd388221a7073449 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -60,7 +60,7 @@ setup(name = 'stgit',
       description = 'Stacked GIT',
       long_description = 'Push/pop utility on top of GIT',
       scripts = ['stg'],
-      packages = ['stgit', 'stgit.commands'],
+      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']),
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)
index 45d09262626094f537426d87cdb979ec43ce91c3..522425b475bd4f6df9beee12cd2186932c2aff3f 100644 (file)
@@ -16,25 +16,21 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os
-from optparse import OptionParser, make_option
-
-from stgit.commands.common import *
-from stgit.utils import *
+from optparse import make_option
 from stgit.out import *
-from stgit import stack, git
+from stgit.commands import common
 
 
 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
+List the patches from the series which have already been 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()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
+                       help = 'use BRANCH instead of the default branch'),
            make_option('-c', '--count',
                        help = 'print the number of applied patches',
                        action = 'store_true')]
@@ -46,10 +42,13 @@ def func(parser, options, args):
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    applied = crt_series.get_applied()
+    if options.branch:
+        s = directory.repository.get_stack(options.branch)
+    else:
+        s = directory.repository.current_stack
 
     if options.count:
-        out.stdout(len(applied))
+        out.stdout(len(s.patchorder.applied))
     else:
-        for p in applied:
-            out.stdout(p)
+        for pn in s.patchorder.applied:
+            out.stdout(pn)
index c703418df57e781db0247ccfa603b32e3b3dd80c..889c1dc356b480b081c0533052e0fedf4970aa82 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, 'stg clean')
+    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..291a537
--- /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 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')
+           ] + utils.make_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, 'stg coalesce')
+    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.applied)
+                                          + list(stack.patchorder.unapplied)))
+    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..bff94ceff9a01b5858f4c48923ec0f6f4f7fe15b 100644 (file)
@@ -15,53 +15,82 @@ 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.applied)
+                                       + list(stack.patchorder.unapplied)))
+    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.applied
+                                 + stack.patchorder.unapplied) if pn in args]
+        bad = set(args) - set(patches)
+        if bad:
+            raise common.CmdException('Bad 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
+    trans = transaction.StackTransaction(stack, 'stg commit')
+    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..5a1952bad85a158beca8f74067a11ae2475a2e24 100644 (file)
@@ -27,7 +27,7 @@ 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
 
 # Command exception class
 class CmdException(StgException):
@@ -129,7 +129,7 @@ def check_head_top_equal(crt_series):
    more about what to do next.""")
 
 def check_conflicts():
-    if os.path.exists(os.path.join(basedir.get(), 'conflicts')):
+    if git.get_conflicts():
         raise CmdException, \
               'Unsolved conflicts. Please resolve them first or\n' \
               '  revert the changes with "status --reset"'
@@ -145,29 +145,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
@@ -289,12 +269,13 @@ 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)
+    address = re.sub(r'[\\"]', r'\\\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
+            raise CmdException('Incorrect "name <email>"/"email (name)"'
+                               ' string: %s' % address)
         return ( str_list[0][1], str_list[0][0] )
 
     return str_list[0]
@@ -303,7 +284,7 @@ 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)
+    address = re.sub(r'[\\"]', r'\\\g<0>', address)
     str_list = re.findall('^(.*)\s*<(.*)>\s*(.*)\s*$', address)
     if not str_list:
         raise CmdException, 'Incorrect "name <email> date" string: %s' % address
@@ -482,11 +463,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 +542,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 1425518d5a21fbab30c5705dea0c42d8f8ef10c3..fd6be34377280e8027460bbcea230885ada75ec1 100644 (file)
@@ -46,12 +46,10 @@ 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')
+           ] + make_diff_opts_option()
 
 def func(parser, options, args):
     """Show the tree diff
@@ -83,16 +81,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),
+                        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 a4d8f963f2a288169ecb326a1f4b6f899c32fac3..7daf156ffe402442485048329cca627d72ddb283 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 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,29 +47,19 @@ 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('-e', '--edit', action = 'store_true',
+                       help = 'invoke interactive editor'),
            make_option('--author', metavar = '"NAME <EMAIL>"',
                        help = 'replae the author details with "NAME <EMAIL>"'),
            make_option('--authname',
@@ -84,161 +72,143 @@ options = [make_option('-d', '--diff',
                        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)
+           ] + (utils.make_sign_options() + utils.make_message_options()
+                + utils.make_diff_opts_option())
 
-    if options.diff_opts:
-        if not options.diff:
-            raise CmdException, '--diff-opts only available with --diff'
-        diff_flags = options.diff_opts.split()
+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)
+        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.
+    if options.author != None:
+        options.authname, options.authemail = common.name_email(options.author)
+    for p, f, val in [('author', 'name', options.authname),
+                      ('author', 'email', options.authemail),
+                      ('author', 'date', gitlib.Date.maybe(options.authdate)),
+                      ('committer', 'name', options.commname),
+                      ('committer', 'email', options.commemail)]:
+        if val != None:
+            cd = getattr(cd, 'set_' + p)(
+                getattr(getattr(cd, p), 'set_' + f)(val))
+
+    # 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, 'stg edit')
+    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 913172914feff00911a3415c1c1e6f1b5ab4c6d5..50f6f671a979ef7bca12fa2900ada64679546670 100644 (file)
@@ -64,11 +64,10 @@ 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')
+           ] + make_diff_opts_option()
 
 
 def func(parser, options, args):
@@ -89,11 +88,6 @@ 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()
     if len(args) != 0:
         patches = parse_patches(args, applied)
@@ -144,11 +138,13 @@ def func(parser, options, args):
         long_descr = reduce(lambda x, y: x + '\n' + y,
                             descr_lines[1:], '').strip()
 
+        diff = git.diff(rev1 = patch.get_bottom(),
+                        rev2 = patch.get_top(),
+                        diff_flags = options.diff_flags)
         tmpl_dict = {'description': patch.get_description().rstrip(),
                      'shortdescr': short_descr,
                      'longdescr': long_descr,
-                     'diffstat': git.diffstat(rev1 = patch.get_bottom(),
-                                              rev2 = patch.get_top()),
+                     'diffstat': git.diffstat(diff),
                      'authname': patch.get_authname(),
                      'authemail': patch.get_authemail(),
                      'authdate': patch.get_authdate(),
@@ -178,9 +174,7 @@ def func(parser, options, args):
             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 4550251d471c4ce67cd72a6a8cbd8c24060b7c18..b43b12f87457411b7082250ec9346489213089b8 100644 (file)
@@ -40,11 +40,10 @@ options = [make_option('-s', '--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')
+           ] + make_diff_opts_option()
 
 
 def func(parser, options, args):
@@ -61,13 +60,9 @@ def func(parser, options, args):
     rev2 = git_id(crt_series, '%s//top' % 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..fe13e497c50f006c453c81887b3c8c2716a101dd 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,26 @@ 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, 'stg 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
     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 1c21a745280a4539a434d01a10a5b003226ea66d..4a4b7929f3ed3c02d56148e8cb2fe1f11668edac 100644 (file)
@@ -192,7 +192,7 @@ 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()
index 54ab5c913860e10712855edc90260e94d82b8bcc..b4d4e18acce6d4ef5ba4b66db60c96404f9822f2 100644 (file)
@@ -27,7 +27,7 @@ from stgit.config import config
 
 
 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
@@ -84,7 +84,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
@@ -144,11 +144,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')
+           ] + make_diff_opts_option()
 
 
 def __get_sender():
@@ -361,9 +360,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(
+                 'diffstat':     git.diffstat(git.diff(
                      rev1 = git_id(crt_series, '%s//bottom' % patches[0]),
-                     rev2 = git_id(crt_series, '%s//top' % patches[-1]))}
+                     rev2 = git_id(crt_series, '%s//top' % patches[-1])))}
 
     try:
         msg_string = tmpl % tmpl_dict
@@ -431,11 +430,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 +438,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//bottom' % patch),
+                    rev2 = git_id(crt_series, '%s//top' % patch),
+                    diff_flags = options.diff_flags)
     tmpl_dict = {'patch':        patch,
                  'sender':       sender,
                  # for backward template compatibility
@@ -452,13 +449,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 f9ee7c22bb2e0dfd6c8893b47934a598750105e9..1f7c84b981cf5d72db3de047593fd5f145f380d2 100644 (file)
@@ -83,7 +83,7 @@ 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:
index 6e8ed0c313f87e3ec9bd3a1f80cf6a1b1f19eb80..4695c6277ec5ea2cda7269b519e4cc5cbfe8b196 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,9 +94,10 @@ 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]
@@ -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 72d1be387f02678011ba3fb67202cfd1d4f2e673..b77a9c8740e08309ed79a7929c271c7050ec698a 100644 (file)
@@ -38,9 +38,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')
+           ] + make_diff_opts_option()
 
 
 def func(parser, options, args):
@@ -62,13 +61,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 20614b0fdd2af39b8503625e4d2b8617d046b7af..6da45160d5a8100a66d8a7a5c0f9a497fccaabba 100644 (file)
@@ -59,16 +59,14 @@ options = [make_option('-m', '--modified',
            make_option('-x', '--noexclude',
                        help = 'do not exclude any files from listing',
                        action = 'store_true'),
-           make_option('-O', '--diff-opts',
-                       help = 'options to pass to git-diff'),
            make_option('--reset',
                        help = 'reset the current tree changes',
-                       action = 'store_true')]
+                       action = 'store_true')
+           ] + make_diff_opts_option()
 
 
-def status(files = None, modified = False, new = False, deleted = False,
-           conflict = False, unknown = False, noexclude = False,
-           diff_flags = []):
+def status(files, modified, new, deleted, conflict, unknown, noexclude,
+           diff_flags):
     """Show the tree status
     """
     cache_files = git.tree_status(files,
@@ -109,18 +107,13 @@ 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()
             git.reset()
     else:
-        if options.diff_opts:
-            diff_flags = options.diff_opts.split()
-        else:
-            diff_flags = []
-
         status(args, options.modified, options.new, options.deleted,
                options.conflict, options.unknown, options.noexclude,
-               diff_flags = diff_flags)
+               options.diff_flags)
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 d5bb43e3004f1b7aabd71dacaf1319703e440415..770220763b2b3755456fee253b2de0607edc4999 100644 (file)
@@ -16,13 +16,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 optparse import make_option
 from stgit.out import *
-from stgit import stack, git
+from stgit.commands import common
 
 
 help = 'print the unapplied patches'
@@ -31,9 +27,9 @@ 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()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
+                       help = 'use BRANCH instead of the default branch'),
            make_option('-c', '--count',
                        help = 'print the number of unapplied patches',
                        action = 'store_true')]
@@ -45,10 +41,13 @@ def func(parser, options, args):
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    unapplied = crt_series.get_unapplied()
+    if options.branch:
+        s = directory.repository.get_stack(options.branch)
+    else:
+        s = directory.repository.current_stack
 
     if options.count:
-        out.stdout(len(unapplied))
+        out.stdout(len(s.patchorder.unapplied))
     else:
-        for p in unapplied:
-            out.stdout(p)
+        for pn in s.patchorder.unapplied:
+            out.stdout(pn)
index ba3448fd7c52f7831eae95bc796337aa70dc9099..272c5dbc9f4189020b23bb6cd2666e8fd98e15ee 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,55 @@ 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)
         else:
             out.start('Uncommitting to %s' % to_commit)
         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.applied + stack.patchorder.unapplied)
+    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, 'stg uncommit')
+    for commit, pn in zip(commits, patchnames):
+        trans.patches[pn] = commit
+    trans.applied = list(reversed(patchnames)) + trans.applied
+    trans.run()
     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 deb5efcfe7654896a3abcd12e36a5711726a0743..4dc4dcfef738ed3dcaef3b7c1072899af9b1c1d0 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 = None, 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 tree_status(files = None, tree_id = 'HEAD', unknown = False,
                   noexclude = True, verbose = False, diff_flags = []):
@@ -226,8 +225,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)
@@ -446,109 +443,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
@@ -695,77 +589,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):
@@ -790,12 +640,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
@@ -875,6 +722,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.
@@ -969,7 +827,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 4c90e1a7833e0aae510a1d5870c232fc7cbabc7e..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..c5b048f
--- /dev/null
@@ -0,0 +1,543 @@
+import os, os.path, re
+from datetime import datetime, timedelta, tzinfo
+
+from stgit import exception, run, utils
+from stgit.config import config
+
+class RepositoryException(exception.StgException):
+    pass
+
+class DateException(exception.StgException):
+    def __init__(self, string, type):
+        exception.StgException.__init__(
+            self, '"%s" is not a valid %s' % (string, type))
+
+class DetachedHeadException(RepositoryException):
+    def __init__(self):
+        RepositoryException.__init__(self, 'Not on any branch')
+
+class Repr(object):
+    def __repr__(self):
+        return str(self)
+
+class NoValue(object):
+    pass
+
+def make_defaults(defaults):
+    def d(val, attr):
+        if val != NoValue:
+            return val
+        elif defaults != NoValue:
+            return getattr(defaults, attr)
+        else:
+            return None
+    return d
+
+class TimeZone(tzinfo, Repr):
+    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(Repr):
+    """Immutable."""
+    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):
+        if datestring in [None, NoValue]:
+            return datestring
+        return cls(datestring)
+
+class Person(Repr):
+    """Immutable."""
+    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 Tree(Repr):
+    """Immutable."""
+    def __init__(self, sha1):
+        self.__sha1 = sha1
+    sha1 = property(lambda self: self.__sha1)
+    def __str__(self):
+        return 'Tree<%s>' % self.sha1
+
+class Commitdata(Repr):
+    """Immutable."""
+    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')
+        self.__committer = d(committer, '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)
+    @classmethod
+    def parse(cls, repository, s):
+        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(Repr):
+    """Immutable."""
+    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):
+    def __init__(self, repository):
+        self.__repository = repository
+        self.__refs = None
+    def __cache_refs(self):
+        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):
+        """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):
+        try:
+            self.get(ref)
+        except KeyError:
+            return False
+        else:
+            return True
+    def set(self, ref, commit, msg):
+        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):
+        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."""
+    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 = {}):
+        return run.Run(*args).env(utils.add_dict(self.env, env))
+
+class RunWithEnvCwd(RunWithEnv):
+    def run(self, args, env = {}):
+        return RunWithEnv.run(self, args, env).cwd(self.cwd)
+
+class Repository(RunWithEnv):
+    def __init__(self, directory):
+        self.__git_dir = directory
+        self.__refs = Refs(self)
+        self.__trees = ObjectCache(lambda sha1: Tree(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 default_index(self):
+        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 Index(self, self.__git_dir)
+    @property
+    def default_worktree(self):
+        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):
+        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):
+        try:
+            return self.get_commit(self.run(
+                    ['git', 'rev-parse', '%s^{commit}' % rev]
+                    ).output_one_line())
+        except run.RunException:
+            raise RepositoryException('%s: No such revision' % rev)
+    def get_tree(self, sha1):
+        return self.__trees[sha1]
+    def get_commit(self, sha1):
+        return self.__commits[sha1]
+    def commit(self, commitdata):
+        c = ['git', 'commit-tree', commitdata.tree.sha1]
+        for p in commitdata.parents:
+            c.append('-p')
+            c.append(p.sha1)
+        env = {}
+        for p, v1 in ((commitdata.author, 'AUTHOR'),
+                       (commitdata.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 = self.run(c, env = env).raw_input(commitdata.message
+                                                ).output_one_line()
+        return self.get_commit(sha1)
+    @property
+    def head(self):
+        try:
+            return self.run(['git', 'symbolic-ref', '-q', 'HEAD']
+                            ).output_one_line()
+        except run.RunException:
+            raise DetachedHeadException()
+    def set_head(self, ref, msg):
+        self.run(['git', 'symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()
+    def simple_merge(self, base, ours, theirs):
+        """Given three trees, tries to do an in-index merge in a temporary
+        index with a temporary index. Returns the result tree, or None if
+        the merge failed (due to conflicts)."""
+        assert isinstance(base, Tree)
+        assert isinstance(ours, Tree)
+        assert isinstance(theirs, Tree)
+
+        # Take care of the really trivial cases.
+        if base == ours:
+            return theirs
+        if base == theirs:
+            return ours
+        if ours == theirs:
+            return ours
+
+        index = self.temp_index()
+        try:
+            index.merge(base, ours, theirs)
+            try:
+                return index.write_tree()
+            except MergeException:
+                return None
+        finally:
+            index.delete()
+    def apply(self, tree, patch_text):
+        """Given a tree and a patch, will either return the new 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)
+                return index.write_tree()
+            except MergeException:
+                return None
+        finally:
+            index.delete()
+    def diff_tree(self, t1, t2, diff_opts):
+        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):
+    pass
+
+class MergeConflictException(MergeException):
+    pass
+
+class Index(RunWithEnv):
+    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 merge(self, base, ours, theirs):
+        """In-index merge, no worktree involved."""
+        self.run(['git', 'read-tree', '-m', '-i', '--aggressive',
+                  base.sha1, ours.sha1, theirs.sha1]).no_output()
+    def apply(self, patch_text):
+        """In-index patch application, no worktree involved."""
+        try:
+            self.run(['git', 'apply', '--cached']
+                     ).raw_input(patch_text).no_output()
+        except run.RunException:
+            raise MergeException('Patch does not apply cleanly')
+    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):
+    def __init__(self, directory):
+        self.__directory = directory
+    env = property(lambda self: { 'GIT_WORK_TREE': '.' })
+    directory = property(lambda self: self.__directory)
+
+class CheckoutException(exception.StgException):
+    pass
+
+class IndexAndWorktree(RunWithEnvCwd):
+    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()
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
new file mode 100644 (file)
index 0000000..3de3776
--- /dev/null
@@ -0,0 +1,175 @@
+import os.path
+from stgit import exception, utils
+from stgit.lib import git, stackupgrade
+
+class Patch(object):
+    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)
+        self.__stack.repository.refs.delete(self.__log_ref)
+    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))
+
+class Patches(object):
+    """Creates Patch objects."""
+    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(object):
+    def __init__(self, repository, name):
+        self.__repository = repository
+        self.__name = name
+        try:
+            self.head
+        except KeyError:
+            raise exception.StgException('%s: no such branch' % name)
+        self.__patchorder = PatchOrder(self)
+        self.__patches = Patches(self)
+        if not stackupgrade.update_to_current_format_version(repository, name):
+            raise exception.StgException('%s: branch not initialized' % name)
+    name = property(lambda self: self.__name)
+    repository = property(lambda self: self.__repository)
+    patchorder = property(lambda self: self.__patchorder)
+    patches = property(lambda self: self.__patches)
+    @property
+    def directory(self):
+        return os.path.join(self.__repository.directory, 'patches', self.__name)
+    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)
+    @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
+
+class Repository(git.Repository):
+    def __init__(self, *args, **kwargs):
+        git.Repository.__init__(self, *args, **kwargs)
+        self.__stacks = {} # name -> Stack
+    @property
+    def current_branch(self):
+        return utils.strip_leading('refs/heads/', self.head)
+    @property
+    def current_stack(self):
+        return self.get_stack(self.current_branch)
+    def get_stack(self, 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..96ccb79
--- /dev/null
@@ -0,0 +1,98 @@
+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)
+
+    # 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..1ece01e
--- /dev/null
@@ -0,0 +1,217 @@
+from stgit import exception, utils
+from stgit.utils import any, all
+from stgit.out import *
+from stgit.lib import git
+
+class TransactionException(exception.StgException):
+    pass
+
+class TransactionHalted(TransactionException):
+    pass
+
+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):
+    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):
+    def __init__(self, stack, msg):
+        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.__error = None
+        self.__current_tree = self.__stack.head.data.tree
+        self.__base = self.__stack.base
+    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_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)
+    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:
+            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)
+        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):
+        self.__check_consistency()
+        new_head = self.__head
+
+        # Set branch 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
+
+        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."""
+        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."""
+        popped = []
+        all_patches = self.applied + self.unapplied
+        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.__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.__stack.repository.simple_merge(base, ours, theirs)
+        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)'
+        del self.unapplied[self.unapplied.index(pn)]
+        self.applied.append(pn)
+        out.info('Pushed %s%s' % (pn, s))
+        if merge_conflict:
+            self.__halt('Merge conflict')
index a03447fc6b4aee33632a10fbc8bd5f0cce46eb44..663fdec35e7564071ccac54e3bfc319061f7c690 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 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,14 @@ 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,7 +90,6 @@ commands = Commands({
     'rename':           'rename',
     'repair':           'repair',
     'resolved':         'resolved',
-    'rm':               'rm',
     'series':           'series',
     'show':             'show',
     'sink':             'sink',
@@ -111,6 +110,7 @@ stackcommands = (
     'applied',
     'branch',
     'clean',
+    'coalesce',
     'commit',
     'float',
     'goto',
@@ -146,11 +146,8 @@ patchcommands = (
     'sync',
     )
 wccommands = (
-    'add',
-    'cp',
     'diff',
     'resolved',
-    'rm',
     'status',
     )
 
@@ -206,7 +203,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 +213,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 +229,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 +262,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 +275,17 @@ 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 KeyboardInterrupt:
-        sys.exit(1)
+        sys.exit(utils.STGIT_GENERAL_ERROR)
+    except:
+        out.error('Unhandled exception:')
+        traceback.print_exc()
+        sys.exit(utils.STGIT_BUG_ERROR)
 
-    sys.exit(0)
+    sys.exit(ret or utils.STGIT_SUCCESS)
index fa304d039159d95730d0e96e540736a5d69ad443..0b79729d2a5c84657fb17bd02c722ff6c699dfd2 100644 (file)
@@ -42,12 +42,18 @@ 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)
+            if self.__cwd != None:
+                out.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]:
+                        out.info('%s: %s' % (k, self.__env[k]))
         elif _log_mode == 'profile':
             out.start('Running subprocess %s' % self.__cmd[0])
             self.__starttime = datetime.datetime.now()
@@ -67,7 +73,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 +91,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 +110,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..cd523826153ba336b7c1b63056a3cae461d02fb8 100644 (file)
@@ -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."""
@@ -256,3 +263,81 @@ 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 make_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 make_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"')]
+
+# 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 strip_leading(prefix, s):
+    """Strip leading prefix from a string. Blow up if the prefix isn't
+    there."""
+    assert s.startswith(prefix)
+    return s[len(prefix):]
+
+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 43e1ca08e4deba373408c9914cd04f85c545b3eb..0a70f15c31389e8934a672ddb7e036c2680e28b4 100755 (executable)
@@ -54,7 +54,7 @@ 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
 '
@@ -95,7 +95,7 @@ test_expect_success 'Status after refresh' '
 
 test_expect_success 'Add another file' '
     echo lajbans > fie &&
-    stg add fie &&
+    git add fie &&
     stg refresh
 '
 
@@ -110,6 +110,7 @@ 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' '
@@ -135,6 +136,7 @@ test_expect_success 'Status of dir' '
 '
 
 cat > expected.txt <<EOF
+A fie
 EOF
 test_expect_success 'Status of other file' '
     stg status fie > output.txt &&
@@ -142,6 +144,7 @@ test_expect_success 'Status of other file' '
 '
 
 cat > expected.txt <<EOF
+A fie
 M foo/bar
 EOF
 test_expect_success 'Status after resolving the push' '
@@ -151,6 +154,7 @@ test_expect_success 'Status after resolving the push' '
 '
 
 cat > expected.txt <<EOF
+A fie
 D foo/bar
 EOF
 test_expect_success 'Status after deleting a file' '
@@ -165,7 +169,7 @@ 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
index cfec6960a29fba3793fdd1d57f2923893bfcab2c..ba4f70c43714979f287064d6d6581493ef8f1964 100755 (executable)
@@ -23,7 +23,7 @@ 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" ] &&
index edfa7105e2fced7b2491ebf11288e3b756c09459..b602643f9018509bc605de1199afe47bd178453e 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
        '
 
@@ -62,7 +62,7 @@ test_expect_success \
        'Undo with disappeared newborn' \
        '
        touch newfile &&
-       stg add newfile &&
+       git add newfile &&
        rm newfile &&
        stg push --undo
        '
diff --git a/t/t1203-push-conflict.sh b/t/t1203-push-conflict.sh
new file mode 100755 (executable)
index 0000000..72bd49f
--- /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' \
+       '
+       ! 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' \
+       '
+       ! stg pop
+       '
+
+test_expect_success \
+       'Resolve the conflict' \
+       '
+       echo resolved > test &&
+       git add test &&
+       stg refresh
+       '
+
+test_done
index 40cd2a2aa33bc16f6fb3f010a47d3352c80effd3..35f4ec0b094fc6406df9b759676d892a73383e92 100755 (executable)
@@ -8,7 +8,7 @@ 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" ] &&
index 54a5b896e199d8529e99fe994d30d5ee266e63f0..175d36d2888987b85505de8810183217e63b3ba2 100755 (executable)
@@ -9,7 +9,7 @@ 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" ] &&
@@ -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 &&
@@ -57,7 +57,7 @@ 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 &&
index 2e7ff211745d413bb2fee81080407444202fbea0..a906d135d9c1ee077c1f7042390b3297044e4196 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,14 +28,14 @@ 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 \
@@ -43,7 +43,7 @@ test_expect_success \
        '
        stg uncommit bar foo &&
        [ "$(stg id foo//top)" = "$(stg id bar//bottom)" ] &&
-       stg commit
+       stg commit --all
        '
 
 test_expect_success \
@@ -51,7 +51,7 @@ test_expect_success \
        '
        stg uncommit --number=2 foobar &&
        [ "$(stg id foobar1//top)" = "$(stg id foobar2//bottom)" ] &&
-       stg commit
+       stg commit --all
        '
 
 test_expect_success \
@@ -59,7 +59,7 @@ test_expect_success \
        '
        stg uncommit --number=2 &&
        [ "$(stg id foo-patch//top)" = "$(stg id bar-patch//bottom)" ] &&
-       stg commit
+       stg commit --all
        '
 
 test_expect_success \
@@ -68,14 +68,19 @@ test_expect_success \
        stg uncommit &&
        stg uncommit &&
        [ "$(stg id foo-patch//top)" = "$(stg id bar-patch//bottom)" ] &&
-       stg commit
+       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 commit --all
+'
+
+test_expect_success 'Uncommit a commit with not precisely one parent' '
+    stg uncommit -n 5 ; [ $? = 2 ] &&
+    [ "$(echo $(stg series))" = "" ]
 '
 
 test_done
index 5d9bdbdc4b8e408eb997a9d2f38fd65302ede1f7..b555b93ac11c38393d68de68a3bea142394bde43 100755 (executable)
@@ -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
     '
 
index 5b842d0b116246398a29d4766d4185c3ed98a63c..879b1a5a03bceb66d9d97b31f24bcbc5b313de49 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"
        '
 
@@ -55,7 +55,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    "
        '
index 814c9bd2981f3099345c920f7dc4fba00205f355..778fde47570a4c4da7c41250e5d3608bc4fb47be 100755 (executable)
@@ -12,13 +12,13 @@ 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"
        '
index df03d79c860db85377e3bac80eab8c1aca83f6ab..3052b3a9426b994ec236a4a912083c0214ed175b 100755 (executable)
@@ -12,7 +12,7 @@ test_expect_success \
     '
     stg new foo -m foo &&
     echo foo > foo.txt &&
-    stg add foo.txt &&
+    git add foo.txt &&
     stg refresh
     '
 
@@ -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
     '
@@ -65,11 +65,11 @@ 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
     '
 
@@ -87,12 +87,12 @@ 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
     '
 
index 8eff308ebfa7e7477096522ac041b42a24df27f1..30b0a1dd0a33defbe04a31aeb3550aaaedc8434c 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 &&
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
        '
 
index 484dbabb75702d2c38ca0b73e84c0a1f3d9930a4..e489603f3fa4d7d64c31cd52e17f8fd193242911 100755 (executable)
@@ -18,15 +18,15 @@ 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 &&
@@ -86,7 +86,7 @@ test_expect_success \
     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 &&
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..ce4b5c89542623e3cdff6baaef14afa9c6617f4a 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
     '
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 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..ad8f892e624107e5c7104d4d77cd18dcbdba05ca 100755 (executable)
@@ -24,4 +24,21 @@ test_expect_success 'Clean empty patches' '
     [ "$(echo $(stg unapplied))" = "" ]
 '
 
+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 &&
+    ! stg push
+'
+
+test_expect_success 'Make sure conflicting patches are preserved' '
+    stg clean &&
+    [ "$(echo $(stg applied))" = "p0 p2 p1" ] &&
+    [ "$(echo $(stg unapplied))" = "" ]
+'
+
 test_done
diff --git a/t/t2600-coalesce.sh b/t/t2600-coalesce.sh
new file mode 100755 (executable)
index 0000000..f13a309
--- /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 applied))" = "p0 p1 p2 p3" ] &&
+    [ "$(echo $(stg unapplied))" = "" ] &&
+    stg coalesce --name=q0 --message="wee woo" p1 p2 &&
+    [ "$(echo $(stg applied))" = "p0 q0 p3" ] &&
+    [ "$(echo $(stg unapplied))" = "" ]
+'
+
+test_expect_success 'Coalesce at stack top' '
+    stg coalesce --name=q1 --message="wee woo wham" q0 p3 &&
+    [ "$(echo $(stg applied))" = "p0 q1" ] &&
+    [ "$(echo $(stg unapplied))" = "" ]
+'
+
+test_done
index 2e7901c19a92e0aab1f1d19e8212e34890be150b..9eae85ddbf770ad7d6360b3510142695921cc31d 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 &&
@@ -62,4 +64,57 @@ test_expect_success 'Refresh bottom patch' '
     diff -u 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 &&
+    diff -u expected.txt patches.txt &&
+    diff -u expected2.txt show.txt &&
+    diff -u expected3.txt diff.txt &&
+    stg new p5 -m "cleanup again" &&
+    stg refresh
+'
 test_done
diff --git a/t/t2800-goto-subdir.sh b/t/t2800-goto-subdir.sh
new file mode 100755 (executable)
index 0000000..fcad7da
--- /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 &&
+    diff -u expected1.txt actual.txt &&
+    ls foo > actual.txt &&
+    diff -u 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 &&
+    diff -u expected1.txt actual.txt &&
+    ls foo > actual.txt &&
+    diff -u expected2.txt actual.txt
+'
+
+test_done
diff --git a/t/t3000-dirty-merge.sh b/t/t3000-dirty-merge.sh
new file mode 100755 (executable)
index 0000000..d87bba1
--- /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 applied))" = "p1" ] &&
+    [ "$(echo $(stg unapplied))" = "p2" ] &&
+    ! stg goto p2 &&
+    [ "$(echo $(stg applied))" = "p1" ] &&
+    [ "$(echo $(stg unapplied))" = "p2" ] &&
+    [ "$(echo $(cat a))" = "4" ]
+'
+
+test_done