chiark / gitweb /
Merge branch 'proposed'
authorCatalin Marinas <catalin.marinas@gmail.com>
Thu, 1 Jan 2009 21:51:22 +0000 (21:51 +0000)
committerCatalin Marinas <catalin.marinas@gmail.com>
Thu, 1 Jan 2009 21:51:22 +0000 (21:51 +0000)
Documentation/SubmittingPatches [new file with mode: 0644]
contrib/stgit.el
stgit/argparse.py
stgit/commands/files.py
stgit/commands/mail.py
stgit/commands/series.py
stgit/completion.py

diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
new file mode 100644 (file)
index 0000000..ec2d3d6
--- /dev/null
@@ -0,0 +1,419 @@
+Checklist (and a short version for the impatient):
+
+        Commits:
+
+        - Make commits of logical units.
+        - Check for unnecessary whitespace with "git diff --check"
+          before committing.
+        - Do not check in commented out code or unneeded files.
+        - Provide a meaningful commit message.
+        - The first line of the commit message should be a short
+          description and should skip the full stop.
+        - If you want your work included in StGit, add a
+          "Signed-off-by: Your Name <you@example.com>" line to the
+          commit message (or just use the option "-s" when
+          committing) to confirm that you agree to the Developer's
+          Certificate of Origin.
+        - Make sure that you have tests for the bug you are fixing.
+        - Make sure that the test suite passes after your commit.
+
+        Patch:
+
+        - Preferably use "stg mail" to send patches. The first time,
+          it's a good idea to try to mail the patches to yourself to
+          see that everything works.
+        - Do not PGP sign your patch.
+        - Do not attach your patch, but read in the mail.
+          body, unless you cannot teach your mailer to
+          leave the formatting of the patch alone.
+        - Be careful doing cut & paste into your mailer, not to
+          corrupt whitespaces.
+        - Provide additional information (which is unsuitable for the
+          commit message) between the "---" and the diffstat. (The -E
+          option to stg mail lets you edit the message before you send
+          it out.)
+        - If you change, add, or remove a command line option or
+          make some other user interface change, the associated
+          documentation should be updated as well.
+        - If your name is not writable in ASCII, make sure that
+          you send off a message in the correct encoding.
+        - Send the patch to the list (git@vger.kernel.org) and the
+          maintainer (catalin.marinas@gmail.com) if (and only if) the
+          patch is ready for inclusion.
+
+
+Long version:
+
+
+1. Make separate commits for logically separate changes.
+
+   Unless your patch is really trivial, you should not be sending out
+   a patch that was generated between your working tree and your
+   commit head. Instead, always make a commit with complete commit
+   message and generate a series of patches from your repository. It
+   is a good discipline.
+
+   Describe the technical detail of the change(s).
+
+   If your description starts to get too long, that's a sign that you
+   probably need to split up your commit to finer grained pieces.
+
+   Oh, another thing. I am picky about whitespaces. Please run git
+   diff --check on your changes before you commit.
+
+
+2. Generate your patch using Git tools out of your commits.
+
+   Git based diff tools (Git, Cogito, and StGit included) generate
+   unidiff which is the preferred format.
+
+   You do not have to be afraid to use -M option to "git diff" and
+   friends, if your patch involves file renames. The receiving end can
+   handle them just fine.
+
+   Please make sure your patch does not include any extra files which
+   do not belong in a patch submission. Make sure to review your patch
+   after generating it, to ensure accuracy. Before sending out, please
+   make sure it cleanly applies to the "master" branch head. If you
+   are preparing a work based on some other branch, that is fine, but
+   please mark it as such.
+
+
+3. Sending your patches.
+
+   StGit patches should be sent to the Git mailing list
+   (git@vger.kernel.org), and preferably CCed to the StGit maintainer
+   (catalin.marinas@gmail.com). The recipients need to be able to read
+   and comment on the changes you are submitting. It is important for
+   a developer to be able to "quote" your changes, using standard
+   e-mail tools, so that they may comment on specific portions of your
+   code. For this reason, all patches should be submitted "inline".
+   WARNING: Be wary of your MUAs word-wrap corrupting your patch. Do
+   not cut-n-paste your patch; you can lose tabs that way if you are
+   not careful.
+
+   It is a common convention to prefix your subject line with [StGit
+   PATCH]. This lets people easily distinguish patches to StGit from
+   other e-mail discussions and patches meant for Git itself. Use of
+   additional markers after PATCH and the closing bracket to mark the
+   nature of the patch is also encouraged. E.g. [PATCH/RFC] is often
+   used when the patch is not ready to be applied but it is for
+   discussion, [PATCH v2], [PATCH v3] etc. are often seen when you are
+   sending an update to what you have previously sent.
+
+   "stg mail" command follows the best current practice to format the
+   body of an e-mail message. At the beginning of the patch should
+   come your commit message, ending with the Signed-off-by: lines, and
+   a line that consists of three dashes, followed by the diffstat
+   information and the patch itself. If you are forwarding a patch
+   from somebody else, optionally, at the beginning of the e-mail
+   message just before the commit message starts, you can put a
+   "From:" line to name that person.
+
+   You often want to add additional explanation about the patch, other
+   than the commit message itself. Place such "cover letter" material
+   between the three dash lines and the diffstat. If you have comments
+   about a whole series of patches, you can include them in a separate
+   cover mail message (the -e option to stg mail).
+
+   Do not attach the patch as a MIME attachment, compressed or not. Do
+   not let your e-mail client send quoted-printable. Do not let your
+   e-mail client send format=flowed which would destroy whitespaces in
+   your patches. Many popular e-mail applications will not always
+   transmit a MIME attachment as plain text, making it impossible to
+   comment on your code. A MIME attachment also takes a bit more time
+   to process. This does not decrease the likelihood of your
+   MIME-attached change being accepted, but it makes it more likely
+   that it will be postponed.
+
+   Exception: If your mailer is mangling patches then someone may ask
+   you to re-send them using MIME, that is OK.
+
+   Do not PGP sign your patch, at least for now. Most likely, your
+   maintainer or other people on the list would not have your PGP key
+   and would not bother obtaining it anyway. Your patch is not judged
+   by who you are; a good patch from an unknown origin has a far
+   better chance of being accepted than a patch from a known,
+   respected origin that is done poorly or does incorrect things.
+
+
+4. Sign your work
+
+   To improve tracking of who did what, we've borrowed the "sign-off"
+   procedure from the Git and Linux kernel projects on patches that
+   are being emailed around. Although StGit is a lot smaller project
+   it is a good discipline to follow it.
+
+   The sign-off is a simple line at the end of the explanation for the
+   patch, which certifies that you wrote it or otherwise have the
+   right to pass it on as a open-source patch. The rules are pretty
+   simple: if you can certify the below:
+
+        Developer's Certificate of Origin 1.1
+
+        By making a contribution to this project, I certify that:
+
+        (a) The contribution was created in whole or in part by me and
+            I have the right to submit it under the open source
+            license indicated in the file; or
+
+        (b) The contribution is based upon previous work that, to the
+            best of my knowledge, is covered under an appropriate open
+            source license and I have the right under that license to
+            submit that work with modifications, whether created in
+            whole or in part by me, under the same open source license
+            (unless I am permitted to submit under a different
+            license), as indicated in the file; or
+
+        (c) The contribution was provided directly to me by some other
+            person who certified (a), (b) or (c) and I have not
+            modified it.
+
+        (d) I understand and agree that this project and the
+            contribution are public and that a record of the
+            contribution (including all personal information I submit
+            with it, including my sign-off) is maintained indefinitely
+            and may be redistributed consistent with this project or
+            the open source license(s) involved.
+
+   then you just add a line saying
+
+       Signed-off-by: Random J Developer <random@developer.example.org>
+
+   This line can be automatically added by StGit by any command that
+   accepts the --sign option.
+
+   Notice that you can place your own Signed-off-by: line when
+   forwarding somebody else's patch with the above rules for D-C-O.
+   Indeed you are encouraged to do so. Do not forget to place an
+   in-body "From: " line at the beginning to properly attribute the
+   change to its true author (see (2) above).
+
+   Also notice that a real name is used in the Signed-off-by: line.
+   Please don't hide your real name.
+
+   Some people also put extra tags at the end.
+
+   "Acked-by:" says that the patch was reviewed by a person who is
+   more familiar with the issues and the area the patch attempts to
+   modify. "Tested-by:" says the patch was tested by the person and
+   found to have the desired effect.
+
+
+------------------------------------------------
+MUA specific hints
+
+Some of patches I receive or pick up from the list share common
+patterns of breakage.  Please make sure your MUA is set up
+properly not to corrupt whitespaces.  Here are two common ones
+I have seen:
+
+* Empty context lines that do not have _any_ whitespace.
+
+* Non empty context lines that have one extra whitespace at the
+  beginning.
+
+One test you could do yourself if your MUA is set up correctly is:
+
+* Send the patch to yourself, exactly the way you would, except
+  To: and Cc: lines, which would not contain the list and
+  maintainer address.
+
+* Save that patch to a file in UNIX mailbox format.  Call it say
+  a.patch.
+
+* Try to apply to the tip of the "master" branch from the
+  public repository:
+
+    $ git fetch http://homepage.ntlworld.com/cmarinas/stgit.git master:test-apply
+    $ git checkout test-apply
+    $ git reset --hard
+    $ stg init
+    $ stg import -M a.patch
+
+If it does not apply correctly, there can be various reasons.
+
+* Your patch itself does not apply cleanly.  That is _bad_ but
+  does not have much to do with your MUA.  Please rebase the
+  patch appropriately.
+
+* Your MUA corrupted your patch; "stg import" would complain that
+  the patch does not apply.
+
+* Check the imported patch with e.g. "stg show". If it isn't exactly
+  what you would want to see in the commit log message, it is very
+  likely that the maintainer would end up hand editing the log
+  message when he applies your patch. Things like "Hi, this is my
+  first patch.\n", if you really want to put in the patch e-mail,
+  should come after the three-dash line that signals the end of the
+  commit message.
+
+
+Pine
+----
+
+(Johannes Schindelin)
+
+I don't know how many people still use pine, but for those poor
+souls it may be good to mention that the quell-flowed-text is
+needed for recent versions.
+
+... the "no-strip-whitespace-before-send" option, too. AFAIK it
+was introduced in 4.60.
+
+(Linus Torvalds)
+
+And 4.58 needs at least this.
+
+---
+diff-tree 8326dd8350be64ac7fc805f6563a1d61ad10d32c (from e886a61f76edf5410573e92e38ce22974f9c40f1)
+Author: Linus Torvalds <torvalds@g5.osdl.org>
+Date:   Mon Aug 15 17:23:51 2005 -0700
+
+    Fix pine whitespace-corruption bug
+
+    There's no excuse for unconditionally removing whitespace from
+    the pico buffers on close.
+
+diff --git a/pico/pico.c b/pico/pico.c
+--- a/pico/pico.c
++++ b/pico/pico.c
+@@ -219,7 +219,9 @@ PICO *pm;
+            switch(pico_all_done){      /* prepare for/handle final events */
+              case COMP_EXIT :          /* already confirmed */
+                packheader();
++#if 0
+                stripwhitespace();
++#endif
+                c |= COMP_EXIT;
+                break;
+
+
+(Daniel Barkalow)
+
+> A patch to SubmittingPatches, MUA specific help section for
+> users of Pine 4.63 would be very much appreciated.
+
+Ah, it looks like a recent version changed the default behavior to do the
+right thing, and inverted the sense of the configuration option. (Either
+that or Gentoo did it.) So you need to set the
+"no-strip-whitespace-before-send" option, unless the option you have is
+"strip-whitespace-before-send", in which case you should avoid checking
+it.
+
+
+Thunderbird
+-----------
+
+(A Large Angry SCM)
+
+Here are some hints on how to successfully submit patches inline using
+Thunderbird.
+
+This recipe appears to work with the current [*1*] Thunderbird from Suse.
+
+The following Thunderbird extensions are needed:
+        AboutConfig 0.5
+                http://aboutconfig.mozdev.org/
+        External Editor 0.7.2
+                http://globs.org/articles.php?lng=en&pg=8
+
+1) Prepare the patch as a text file using your method of choice.
+
+2) Before opening a compose window, use Edit->Account Settings to
+uncheck the "Compose messages in HTML format" setting in the
+"Composition & Addressing" panel of the account to be used to send the
+patch. [*2*]
+
+3) In the main Thunderbird window, _before_ you open the compose window
+for the patch, use Tools->about:config to set the following to the
+indicated values:
+        mailnews.send_plaintext_flowed  => false
+        mailnews.wraplength             => 0
+
+4) Open a compose window and click the external editor icon.
+
+5) In the external editor window, read in the patch file and exit the
+editor normally.
+
+6) Back in the compose window: Add whatever other text you wish to the
+message, complete the addressing and subject fields, and press send.
+
+7) Optionally, undo the about:config/account settings changes made in
+steps 2 & 3.
+
+
+[Footnotes]
+*1* Version 1.0 (20041207) from the MozillaThunderbird-1.0-5 rpm of Suse
+9.3 professional updates.
+
+*2* It may be possible to do this with about:config and the following
+settings but I haven't tried, yet.
+        mail.html_compose                       => false
+        mail.identity.default.compose_html      => false
+        mail.identity.id?.compose_html          => false
+
+(Lukas Sandström)
+
+There is a script in contrib/thunderbird-patch-inline which can help
+you include patches with Thunderbird in an easy way. To use it, do the
+steps above and then use the script as the external editor.
+
+Gnus
+----
+
+'|' in the *Summary* buffer can be used to pipe the current
+message to an external program, and this is a handy way to drive
+"git am".  However, if the message is MIME encoded, what is
+piped into the program is the representation you see in your
+*Article* buffer after unwrapping MIME.  This is often not what
+you would want for two reasons.  It tends to screw up non ASCII
+characters (most notably in people's names), and also
+whitespaces (fatal in patches).  Running 'C-u g' to display the
+message in raw form before using '|' to run the pipe can work
+this problem around.
+
+
+KMail
+-----
+
+This should help you to submit patches inline using KMail.
+
+1) Prepare the patch as a text file.
+
+2) Click on New Mail.
+
+3) Go under "Options" in the Composer window and be sure that
+"Word wrap" is not set.
+
+4) Use Message -> Insert file... and insert the patch.
+
+5) Back in the compose window: add whatever other text you wish to the
+message, complete the addressing and subject fields, and press send.
+
+
+Gmail
+-----
+
+Submitting properly formatted patches via Gmail is simple now that
+IMAP support is available. First, edit your ~/.gitconfig to specify your
+account settings:
+
+[imap]
+        folder = "[Gmail]/Drafts"
+        host = imaps://imap.gmail.com
+        user = user@gmail.com
+        pass = p4ssw0rd
+        port = 993
+        sslverify = false
+
+Next, ensure that your Gmail settings are correct. In "Settings" the
+"Use Unicode (UTF-8) encoding for outgoing messages" should be checked.
+
+Once your commits are ready to send to the mailing list, run the following
+command to send the patch emails to your Gmail Drafts folder.
+
+        $ git format-patch -M --stdout origin/master | git imap-send
+
+Go to your Gmail account, open the Drafts folder, find the patch email, fill
+in the To: and CC: fields and send away!
index d0f19c38b2c7b81fc7d79289dd951da64ae6dc40..21ef28a14f26d74ed280745100af6206b1961062 100644 (file)
@@ -9,25 +9,39 @@
 ;;
 ;; To start: `M-x stgit'
 
+(require 'git nil t)
+
 (defun stgit (dir)
-  "Manage stgit patches"
+  "Manage StGit patches for the tree in DIR."
   (interactive "DDirectory: \n")
   (switch-to-stgit-buffer (git-get-top-dir dir))
-  (stgit-refresh))
-
-(defun git-get-top-dir (dir)
-  "Retrieve the top-level directory of a git tree."
-  (let ((cdup (with-output-to-string
-                (with-current-buffer standard-output
-                  (cd dir)
-                  (unless (eq 0 (call-process "git" nil t nil
-                                              "rev-parse" "--show-cdup"))
-                    (error "cannot find top-level git tree for %s." dir))))))
-    (expand-file-name (concat (file-name-as-directory dir)
-                              (car (split-string cdup "\n"))))))
+  (stgit-reload))
+
+(unless (fboundp 'git-get-top-dir)
+  (defun git-get-top-dir (dir)
+    "Retrieve the top-level directory of a git tree."
+    (let ((cdup (with-output-to-string
+                  (with-current-buffer standard-output
+                    (cd dir)
+                    (unless (eq 0 (call-process "git" nil t nil
+                                                "rev-parse" "--show-cdup"))
+                      (error "Cannot find top-level git tree for %s" dir))))))
+      (expand-file-name (concat (file-name-as-directory dir)
+                                (car (split-string cdup "\n")))))))
+
+(defun stgit-refresh-git-status (&optional dir)
+  "If it exists, refresh the `git-status' buffer belonging to
+directory DIR or `default-directory'"
+  (when (and (fboundp 'git-find-status-buffer)
+             (fboundp 'git-refresh-status))
+    (let* ((top-dir (git-get-top-dir (or dir default-directory)))
+           (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
+      (when git-status-buffer
+        (with-current-buffer git-status-buffer
+          (git-refresh-status))))))
 
 (defun switch-to-stgit-buffer (dir)
-  "Switch to a (possibly new) buffer displaying StGit patches for DIR"
+  "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
@@ -51,7 +65,7 @@ (defun create-stgit-buffer (dir)
     buf))
 
 (defmacro stgit-capture-output (name &rest body)
-  "Capture StGit output and show it in a window at the end"
+  "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))
@@ -68,46 +82,258 @@ (defmacro stgit-capture-output (name &rest body)
            (display-buffer output-buf t)))))
 (put 'stgit-capture-output 'lisp-indent-function 1)
 
-(defun stgit-run (&rest args)
+(defun stgit-run-silent (&rest args)
   (apply 'call-process "stg" nil standard-output nil args))
 
-(defun stgit-refresh ()
-  "Update the contents of the stgit buffer"
+(defun stgit-run (&rest args)
+  (let ((msgcmd (mapconcat #'identity args " ")))
+    (message "Running stg %s..." msgcmd)
+    (apply 'call-process "stg" nil standard-output nil args)
+    (message "Running stg %s...done" msgcmd)))
+
+(defun stgit-run-git (&rest args)
+  (let ((msgcmd (mapconcat #'identity args " ")))
+    (message "Running git %s..." msgcmd)
+    (apply 'call-process "git" nil standard-output nil args)
+    (message "Running git %s...done" msgcmd)))
+
+(defun stgit-run-git-silent (&rest args)
+  (apply 'call-process "git" nil standard-output nil args))
+
+(defun stgit-reload ()
+  "Update the contents of the StGit buffer."
   (interactive)
   (let ((inhibit-read-only t)
         (curline (line-number-at-pos))
         (curpatch (stgit-patch-at-point)))
     (erase-buffer)
     (insert "Branch: ")
-    (stgit-run "branch")
-    (stgit-run "series" "--description")
+    (stgit-run-silent "branch")
+    (stgit-run-silent "series" "--description")
     (stgit-rescan)
     (if curpatch
         (stgit-goto-patch curpatch)
-      (goto-line curline))))
+      (goto-line curline)))
+  (stgit-refresh-git-status))
+
+(defgroup stgit nil
+  "A user interface for the StGit patch maintenance tool."
+  :group 'tools)
 
 (defface stgit-description-face
   '((((background dark)) (:foreground "tan"))
     (((background light)) (:foreground "dark red")))
-  "The face used for StGit desriptions")
+  "The face used for StGit descriptions"
+  :group 'stgit)
 
 (defface stgit-top-patch-face
   '((((background dark)) (:weight bold :foreground "yellow"))
     (((background light)) (:weight bold :foreground "purple"))
     (t (:weight bold)))
-  "The face used for the top patch names")
+  "The face used for the top patch names"
+  :group 'stgit)
 
 (defface stgit-applied-patch-face
   '((((background dark)) (:foreground "light yellow"))
     (((background light)) (:foreground "purple"))
     (t ()))
-  "The face used for applied patch names")
+  "The face used for applied patch names"
+  :group 'stgit)
 
 (defface stgit-unapplied-patch-face
   '((((background dark)) (:foreground "gray80"))
     (((background light)) (:foreground "orchid"))
     (t ()))
-  "The face used for unapplied patch names")
+  "The face used for unapplied patch names"
+  :group 'stgit)
+
+(defface stgit-modified-file-face
+  '((((class color) (background light)) (:foreground "purple"))
+    (((class color) (background dark)) (:foreground "salmon")))
+  "StGit mode face used for modified file status"
+  :group 'stgit)
+
+(defface stgit-unmerged-file-face
+  '((((class color) (background light)) (:foreground "red" :bold t))
+    (((class color) (background dark)) (:foreground "red" :bold t)))
+  "StGit mode face used for unmerged file status"
+  :group 'stgit)
+
+(defface stgit-unknown-file-face
+  '((((class color) (background light)) (:foreground "goldenrod" :bold t))
+    (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
+  "StGit mode face used for unknown file status"
+  :group 'stgit)
+
+(defface stgit-file-permission-face
+  '((((class color) (background light)) (:foreground "green" :bold t))
+    (((class color) (background dark)) (:foreground "green" :bold t)))
+  "StGit mode face used for permission changes."
+  :group 'stgit)
+
+(defcustom stgit-expand-find-copies-harder
+  nil
+  "Try harder to find copied files when listing patches.
+
+When not nil, runs git diff-tree with the --find-copies-harder
+flag, which reduces performance."
+  :type 'boolean
+  :group 'stgit)
+
+(defconst stgit-file-status-code-strings
+  (mapcar (lambda (arg)
+            (cons (car arg)
+                  (propertize (cadr arg) 'face (car (cddr arg)))))
+          '((add         "Added"       stgit-modified-file-face)
+            (copy        "Copied"      stgit-modified-file-face)
+            (delete      "Deleted"     stgit-modified-file-face)
+            (modify      "Modified"    stgit-modified-file-face)
+            (rename      "Renamed"     stgit-modified-file-face)
+            (mode-change "Mode change" stgit-modified-file-face)
+            (unmerged    "Unmerged"    stgit-unmerged-file-face)
+            (unknown     "Unknown"     stgit-unknown-file-face)))
+  "Alist of code symbols to description strings")
+
+(defun stgit-file-status-code-as-string (code)
+  "Return stgit status code as string"
+  (let ((str (assq (if (consp code) (car code) code)
+                   stgit-file-status-code-strings)))
+    (when str
+      (format "%-11s  "
+              (if (and str (consp code) (/= (cdr code) 100))
+                  (format "%s %s" (cdr str)
+                          (propertize (format "%d%%" (cdr code))
+                                      'face 'stgit-description-face))
+                (cdr str))))))
+
+(defun stgit-file-status-code (str &optional score)
+  "Return stgit status code from git status string"
+  (let ((code (assoc str '(("A" . add)
+                           ("C" . copy)
+                           ("D" . delete)
+                           ("M" . modify)
+                           ("R" . rename)
+                           ("T" . mode-change)
+                           ("U" . unmerged)
+                           ("X" . unknown)))))
+    (setq code (if code (cdr code) 'unknown))
+    (when (stringp score)
+      (if (> (length score) 0)
+          (setq score (string-to-number score))
+        (setq score nil)))
+    (if score (cons code score) code)))
+
+(defconst stgit-file-type-strings
+  '((#o100 . "file")
+    (#o120 . "symlink")
+    (#o160 . "subproject"))
+  "Alist of names of file types")
+
+(defun stgit-file-type-string (type)
+  "Return string describing file type TYPE (the high bits of file permission).
+Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
+  (let ((type-str (assoc type stgit-file-type-strings)))
+    (or (and type-str (cdr type-str))
+       (format "unknown type %o" type))))
+
+(defun stgit-file-type-change-string (old-perm new-perm)
+  "Return string describing file type change from OLD-PERM to NEW-PERM.
+Cf. `stgit-file-type-string'."
+  (let ((old-type (lsh old-perm -9))
+        (new-type (lsh new-perm -9)))
+    (cond ((= old-type new-type) "")
+          ((zerop new-type) "")
+          ((zerop old-type)
+           (if (= new-type #o100)
+               ""
+             (format "   (%s)" (stgit-file-type-string new-type))))
+          (t (format "   (%s -> %s)"
+                     (stgit-file-type-string old-type)
+                     (stgit-file-type-string new-type))))))
+
+(defun stgit-file-mode-change-string (old-perm new-perm)
+  "Return string describing file mode change from OLD-PERM to NEW-PERM.
+Cf. `stgit-file-type-change-string'."
+  (setq old-perm (logand old-perm #o777)
+        new-perm (logand new-perm #o777))
+  (if (or (= old-perm new-perm)
+          (zerop old-perm)
+          (zerop new-perm))
+      ""
+    (let* ((modified       (logxor old-perm new-perm))
+          (not-x-modified (logand (logxor old-perm new-perm) #o666)))
+      (cond ((zerop modified) "")
+            ((and (zerop not-x-modified)
+                  (or (and (eq #o111 (logand old-perm #o111))
+                           (propertize "-x" 'face 'stgit-file-permission-face))
+                      (and (eq #o111 (logand new-perm #o111))
+                           (propertize "+x" 'face
+                                       'stgit-file-permission-face)))))
+            (t (concat (propertize (format "%o" old-perm)
+                                   'face 'stgit-file-permission-face)
+                       (propertize " -> "
+                                   'face 'stgit-description-face)
+                       (propertize (format "%o" new-perm)
+                                   'face 'stgit-file-permission-face)))))))
+
+(defun stgit-expand-patch (patchsym)
+  "Expand (show modification of) the patch with name PATCHSYM (a
+symbol) at point.
+`stgit-expand-find-copies-harder' controls how hard to try to
+find copied files."
+  (save-excursion
+    (forward-line)
+    (let* ((start (point))
+           (result (with-output-to-string
+                     (stgit-run-git "diff-tree" "-r" "-z"
+                                    (if stgit-expand-find-copies-harder
+                                        "--find-copies-harder"
+                                      "-C")
+                                    (stgit-id (symbol-name patchsym))))))
+      (let (mstart)
+        (while (string-match "\0:\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} \\(\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\\|\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\\)"
+                             result mstart)
+          (let ((copy-or-rename (match-string 4 result))
+                (old-perm       (read (format "#o%s" (match-string 1 result))))
+                (new-perm       (read (format "#o%s" (match-string 2 result))))
+                (line-start (point))
+                status
+                change
+                properties)
+            (insert "    ")
+            (if copy-or-rename
+                (let ((cr-score       (match-string 5 result))
+                      (cr-from-file   (match-string 6 result))
+                      (cr-to-file     (match-string 7 result)))
+                  (setq status (stgit-file-status-code copy-or-rename
+                                                       cr-score)
+                        properties (list 'stgit-old-file cr-from-file
+                                         'stgit-new-file cr-to-file)
+                        change (concat
+                                cr-from-file
+                                (propertize " -> "
+                                            'face 'stgit-description-face)
+                                cr-to-file)))
+              (setq status (stgit-file-status-code (match-string 8 result))
+                    properties (list 'stgit-file (match-string 9 result))
+                    change (match-string 9 result)))
+
+            (let ((mode-change (stgit-file-mode-change-string old-perm
+                                                              new-perm)))
+              (insert (format "%-12s" (stgit-file-status-code-as-string
+                                       status))
+                      mode-change
+                      (if (> (length mode-change) 0) " " "")
+                      change
+                      (propertize (stgit-file-type-change-string old-perm
+                                                                 new-perm)
+                                  'face 'stgit-description-face)
+                      ?\n))
+            (add-text-properties line-start (point) properties))
+          (setq mstart (match-end 0))))
+      (when (= start (point))
+        (insert "    <no files>\n"))
+      (put-text-property start (point) 'stgit-patchsym patchsym))))
 
 (defun stgit-rescan ()
   "Rescan the status buffer."
@@ -130,7 +356,10 @@ (defun stgit-rescan ()
                                     'face 'stgit-description-face)
                  (when (memq patchsym stgit-marked-patches)
                    (replace-match "*" nil nil nil 2)
-                   (setq marked (cons patchsym marked)))))
+                   (setq marked (cons patchsym marked)))
+                 (when (memq patchsym stgit-expanded-patches)
+                   (stgit-expand-patch patchsym))
+                 ))
               ((or (looking-at "stg series: Branch \".*\" not initialised")
                    (looking-at "stg series: .*: branch not initialized"))
                (forward-line 1)
@@ -138,6 +367,89 @@ (defun stgit-rescan ()
         (forward-line 1))
       (setq stgit-marked-patches (nreverse marked)))))
 
+(defun stgit-select ()
+  "Expand or collapse the current entry"
+  (interactive)
+  (let ((curpatch (stgit-patch-at-point)))
+    (if (not curpatch)
+        (let ((patched-file (stgit-patched-file-at-point)))
+          (unless patched-file
+            (error "No patch or file on the current line"))
+          (let ((filename (expand-file-name (cdr patched-file))))
+            (unless (file-exists-p filename)
+              (error "File does not exist"))
+            (find-file filename)))
+      (setq curpatch (intern curpatch))
+      (setq stgit-expanded-patches
+            (if (memq curpatch stgit-expanded-patches)
+                (delq curpatch stgit-expanded-patches)
+              (cons curpatch stgit-expanded-patches)))
+      (stgit-reload))))
+
+(defun stgit-find-file-other-window ()
+  "Open file at point in other window"
+  (interactive)
+  (let ((patched-file (stgit-patched-file-at-point)))
+    (unless patched-file
+      (error "No file on the current line"))
+    (let ((filename (expand-file-name (cdr patched-file))))
+      (unless (file-exists-p filename)
+        (error "File does not exist"))
+      (find-file-other-window filename))))
+
+(defun stgit-quit ()
+  "Hide the stgit buffer."
+  (interactive)
+  (bury-buffer))
+
+(defun stgit-git-status ()
+  "Show status using `git-status'."
+  (interactive)
+  (unless (fboundp 'git-status)
+    (error "The stgit-git-status command requires git-status"))
+  (let ((dir default-directory))
+    (save-selected-window
+      (pop-to-buffer nil)
+      (git-status dir))))
+
+(defun stgit-next-line (&optional arg try-vscroll)
+  "Move cursor vertically down ARG lines"
+  (interactive "p\np")
+  (next-line arg try-vscroll)
+  (when (looking-at "  \\S-")
+    (forward-char 2)))
+
+(defun stgit-previous-line (&optional arg try-vscroll)
+  "Move cursor vertically up ARG lines"
+  (interactive "p\np")
+  (previous-line arg try-vscroll)
+  (when (looking-at "  \\S-")
+    (forward-char 2)))
+
+(defun stgit-next-patch (&optional arg)
+  "Move cursor down ARG patches"
+  (interactive "p")
+  (unless arg
+    (setq arg 1))
+  (if (< arg 0)
+      (stgit-previous-patch (- arg))
+    (while (not (zerop arg))
+      (setq arg (1- arg))
+      (while (progn (stgit-next-line)
+                    (not (stgit-patch-at-point)))))))
+
+(defun stgit-previous-patch (&optional arg)
+  "Move cursor up ARG patches"
+  (interactive "p")
+  (unless arg
+    (setq arg 1))
+  (if (< arg 0)
+      (stgit-next-patch (- arg))
+    (while (not (zerop arg))
+      (setq arg (1- arg))
+      (while (progn (stgit-previous-line)
+                    (not (stgit-patch-at-point)))))))
+
 (defvar stgit-mode-hook nil
   "Run after `stgit-mode' is setup.")
 
@@ -147,28 +459,40 @@ (defvar stgit-mode-map nil
 (unless stgit-mode-map
   (setq stgit-mode-map (make-keymap))
   (suppress-keymap stgit-mode-map)
-  (define-key stgit-mode-map " "   'stgit-mark)
-  (define-key stgit-mode-map "\d" 'stgit-unmark)
-  (define-key stgit-mode-map "?"   'stgit-help)
-  (define-key stgit-mode-map "h"   'stgit-help)
-  (define-key stgit-mode-map "p"   'previous-line)
-  (define-key stgit-mode-map "n"   'next-line)
-  (define-key stgit-mode-map "g"   'stgit-refresh)
-  (define-key stgit-mode-map "r"   'stgit-rename)
-  (define-key stgit-mode-map "e"   'stgit-edit)
-  (define-key stgit-mode-map "c"   'stgit-coalesce)
-  (define-key stgit-mode-map "N"   'stgit-new)
-  (define-key stgit-mode-map "R"   'stgit-repair)
-  (define-key stgit-mode-map "C"   'stgit-commit)
-  (define-key stgit-mode-map "U"   'stgit-uncommit)
-  (define-key stgit-mode-map ">"   'stgit-push-next)
-  (define-key stgit-mode-map "<"   'stgit-pop-next)
-  (define-key stgit-mode-map "P"   'stgit-push-or-pop)
-  (define-key stgit-mode-map "G"   'stgit-goto)
-  (define-key stgit-mode-map "="   'stgit-show)
-  (define-key stgit-mode-map "D"   'stgit-delete)
-  (define-key stgit-mode-map [(control ?/)] 'stgit-undo)
-  (define-key stgit-mode-map "\C-_" 'stgit-undo))
+  (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
+        '((" " .        stgit-mark)
+          ("m" .        stgit-mark)
+          ("\d" .       stgit-unmark-up)
+          ("u" .        stgit-unmark-down)
+          ("?" .        stgit-help)
+          ("h" .        stgit-help)
+          ("p" .        stgit-previous-line)
+          ("n" .        stgit-next-line)
+          ("\C-p" .     stgit-previous-patch)
+          ("\C-n" .     stgit-next-patch)
+          ("\M-{" .     stgit-previous-patch)
+          ("\M-}" .     stgit-next-patch)
+          ("s" .        stgit-git-status)
+          ("g" .        stgit-reload)
+          ("r" .        stgit-refresh)
+          ("\C-c\C-r" . stgit-rename)
+          ("e" .        stgit-edit)
+          ("c" .        stgit-coalesce)
+          ("N" .        stgit-new)
+          ("R" .        stgit-repair)
+          ("C" .        stgit-commit)
+          ("U" .        stgit-uncommit)
+          ("\r" .       stgit-select)
+          ("o" .        stgit-find-file-other-window)
+          (">" .        stgit-push-next)
+          ("<" .        stgit-pop-next)
+          ("P" .        stgit-push-or-pop)
+          ("G" .        stgit-goto)
+          ("=" .        stgit-show)
+          ("D" .        stgit-delete)
+          ([(control ?/)] . stgit-undo)
+          ("\C-_" .     stgit-undo)
+          ("q" . stgit-quit))))
 
 (defun stgit-mode ()
   "Major mode for interacting with StGit.
@@ -182,28 +506,67 @@ (defun stgit-mode ()
   (use-local-map stgit-mode-map)
   (set (make-local-variable 'list-buffers-directory) default-directory)
   (set (make-local-variable 'stgit-marked-patches) nil)
+  (set (make-local-variable 'stgit-expanded-patches) nil)
   (set-variable 'truncate-lines 't)
   (run-hooks 'stgit-mode-hook))
 
 (defun stgit-add-mark (patch)
+  "Mark the patch named PATCH."
   (let ((patchsym (intern patch)))
     (setq stgit-marked-patches (cons patchsym stgit-marked-patches))))
 
 (defun stgit-remove-mark (patch)
+  "Unmark the patch named PATCH."
   (let ((patchsym (intern patch)))
     (setq stgit-marked-patches (delq patchsym stgit-marked-patches))))
 
+(defun stgit-clear-marks ()
+  "Unmark all patches."
+  (setq 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-patch-at-point (&optional cause-error allow-file)
+  "Return the patch name on the current line.
+If CAUSE-ERROR is not nil, signal an error if none found.
+If ALLOW-FILE is not nil, also handle when point is on a file of
+a patch."
+  (or (and allow-file
+           (let ((patchsym (get-text-property (point) 'stgit-patchsym)))
+             (and patchsym
+                  (symbol-name patchsym))))
+      (save-excursion
+        (beginning-of-line)
+        (and (looking-at "[>+-][ *]\\([^ ]*\\)")
+             (match-string-no-properties 1)))
+      (and cause-error
+           (error "No patch on this line"))))
+
+(defun stgit-patched-file-at-point (&optional both-files)
+  "Returns a cons of the patchsym and file name at point. For
+copies and renames, return the new file if the patch is either
+applied. If BOTH-FILES is non-nil, return a cons of the old and
+the new file names instead of just one name."
+  (let ((patchsym (get-text-property (point) 'stgit-patchsym))
+        (file     (get-text-property (point) 'stgit-file)))
+    (cond ((not patchsym) nil)
+          (file (cons patchsym file))
+          (both-files
+           (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
+                                (get-text-property (point) 'stgit-new-file))))
+          (t
+           (let ((file-sym (save-excursion
+                             (stgit-previous-patch)
+                             (unless (equal (stgit-patch-at-point)
+                                            (symbol-name patchsym))
+                               (error "Cannot find the %s patch" patchsym))
+                             (beginning-of-line)
+                             (if (= (char-after) ?-)
+                                 'stgit-old-file 
+                               'stgit-new-file))))
+             (cons patchsym (get-text-property (point) file-sym)))))))
 
 (defun stgit-patches-marked-or-at-point ()
   "Return the names of the marked patches, or the patch on the current line."
@@ -215,67 +578,80 @@ (defun stgit-patches-marked-or-at-point ()
         '()))))
 
 (defun stgit-goto-patch (patch)
-  "Move point to the line containing 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)
+    (if (re-search-forward (concat "^[>+-][ *]" (regexp-quote patch) " ")
+                           nil t)
         (progn (move-to-column goal-column)
                t)
       (goto-char p)
       nil)))
 
 (defun stgit-init ()
-  "Run stg init"
+  "Run stg init."
   (interactive)
   (stgit-capture-output nil
-   (stgit-run "init"))
-  (stgit-refresh))
+    (stgit-run "init"))
+  (stgit-reload))
 
 (defun stgit-mark ()
-  "Mark the patch under point"
+  "Mark the patch under point."
   (interactive)
-  (let ((patch (stgit-patch-at-point)))
+  (let ((patch (stgit-patch-at-point t)))
     (stgit-add-mark patch)
-    (stgit-refresh))
-  (next-line))
+    (stgit-reload))
+  (stgit-next-patch))
 
-(defun stgit-unmark ()
-  "Mark the patch on the previous line"
+(defun stgit-unmark-up ()
+  "Remove mark from the patch on the previous line."
   (interactive)
-  (forward-line -1)
-  (let ((patch (stgit-patch-at-point)))
-    (stgit-remove-mark patch)
-    (stgit-refresh)))
+  (stgit-previous-patch)
+  (stgit-remove-mark (stgit-patch-at-point t))
+  (stgit-reload))
+
+(defun stgit-unmark-down ()
+  "Remove mark from the patch on the current line."
+  (interactive)
+  (stgit-remove-mark (stgit-patch-at-point t))
+  (stgit-reload)
+  (stgit-next-patch))
 
 (defun stgit-rename (name)
-  "Rename the patch under point"
-  (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"))
+  "Rename the patch under point to NAME."
+  (interactive (list (read-string "Patch name: " (stgit-patch-at-point t))))
+  (let ((old-name (stgit-patch-at-point t)))
     (stgit-capture-output nil
       (stgit-run "rename" old-name name))
-    (stgit-refresh)
+    (let ((old-name-sym (intern old-name))
+          (name-sym (intern name)))
+      (when (memq old-name-sym stgit-expanded-patches)
+        (setq stgit-expanded-patches
+            (cons name-sym (delq old-name-sym stgit-expanded-patches))))
+      (when (memq old-name-sym stgit-marked-patches)
+        (setq stgit-marked-patches
+            (cons name-sym (delq old-name-sym stgit-marked-patches)))))
+    (stgit-reload)
     (stgit-goto-patch name)))
 
 (defun stgit-repair ()
-  "Run stg repair"
+  "Run stg repair."
   (interactive)
   (stgit-capture-output nil
-   (stgit-run "repair"))
-  (stgit-refresh))
+    (stgit-run "repair"))
+  (stgit-reload))
 
 (defun stgit-commit ()
   "Run stg commit."
   (interactive)
   (stgit-capture-output nil (stgit-run "commit"))
-  (stgit-refresh))
+  (stgit-reload))
 
 (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))
+  (stgit-reload))
 
 (defun stgit-push-next (npatches)
   "Push the first unapplied patch.
@@ -283,14 +659,16 @@ (defun stgit-push-next (npatches)
   (interactive "p")
   (stgit-capture-output nil (stgit-run "push" "-n"
                                        (number-to-string npatches)))
-  (stgit-refresh))
+  (stgit-reload)
+  (stgit-refresh-git-status))
 
 (defun stgit-pop-next (npatches)
   "Pop the topmost applied patch.
 With numeric prefix argument, pop that many patches."
   (interactive "p")
   (stgit-capture-output nil (stgit-run "pop" "-n" (number-to-string npatches)))
-  (stgit-refresh))
+  (stgit-reload)
+  (stgit-refresh-git-status))
 
 (defun stgit-applied-at-point ()
   "Is the patch on the current line applied?"
@@ -299,42 +677,66 @@ (defun stgit-applied-at-point ()
     (looking-at "[>+]")))
 
 (defun stgit-push-or-pop ()
-  "Push or pop the patch on the current line"
+  "Push or pop the patch on the current line."
   (interactive)
-  (let ((patch (stgit-patch-at-point))
+  (let ((patch (stgit-patch-at-point t))
         (applied (stgit-applied-at-point)))
     (stgit-capture-output nil
-       (stgit-run (if applied "pop" "push") patch))
-    (stgit-refresh)))
+      (stgit-run (if applied "pop" "push") patch))
+    (stgit-reload)))
 
 (defun stgit-goto ()
-  "Go to the patch on the current line"
+  "Go to the patch on the current line."
   (interactive)
-  (let ((patch (stgit-patch-at-point)))
+  (let ((patch (stgit-patch-at-point t)))
     (stgit-capture-output nil
-       (stgit-run "goto" patch))
-    (stgit-refresh)))
+      (stgit-run "goto" patch))
+    (stgit-reload)))
+
+(defun stgit-id (patch)
+  "Return the git commit id for PATCH"
+  (let ((result (with-output-to-string
+                  (stgit-run-silent "id" patch))))
+    (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
+      (error "Cannot find commit id for %s" patch))
+    (match-string 1 result)))
 
 (defun stgit-show ()
-  "Show the patch on the current line"
+  "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))))
+    (let ((patch (stgit-patch-at-point)))
+      (if (not patch)
+          (let ((patched-file (stgit-patched-file-at-point t)))
+            (unless patched-file
+              (error "No patch or file at point"))
+            (let ((id (stgit-id (symbol-name (car patched-file)))))
+              (with-output-to-temp-buffer "*StGit diff*"
+                (if (consp (cdr patched-file))
+                    ;; two files (copy or rename)
+                    (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
+                                   (cadr patched-file) (cddr patched-file))
+                  ;; just one file
+                  (stgit-run-git "diff" (concat id "^") id "--"
+                                 (cdr patched-file)))
+                (with-current-buffer standard-output
+                  (diff-mode)))))
+        (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"
+  "Edit the patch on the current line."
   (interactive)
-  (let ((patch (stgit-patch-at-point))
+  (let ((patch (stgit-patch-at-point t))
         (edit-buf (get-buffer-create "*StGit edit*"))
         (dir default-directory))
     (log-edit 'stgit-confirm-edit t nil edit-buf)
     (set (make-local-variable 'stgit-edit-patch) patch)
     (setq default-directory dir)
     (let ((standard-output edit-buf))
-      (stgit-run "edit" "--save-template=-" patch))))
+      (stgit-run-silent "edit" "--save-template=-" patch))))
 
 (defun stgit-confirm-edit ()
   (interactive)
@@ -343,13 +745,21 @@ (defun stgit-confirm-edit ()
     (stgit-capture-output nil
       (stgit-run "edit" "-f" file stgit-edit-patch))
     (with-current-buffer log-edit-parent-buffer
-      (stgit-refresh))))
+      (stgit-reload))))
 
-(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-new (add-sign)
+  "Create a new patch.
+With a prefix argument, include a \"Signed-off-by:\" line at the
+end of the patch."
+  (interactive "P")
+  (let ((edit-buf (get-buffer-create "*StGit edit*"))
+        (dir default-directory))
+    (log-edit 'stgit-confirm-new t nil edit-buf)
+    (setq default-directory dir)
+    (when add-sign
+      (save-excursion
+        (let ((standard-output (current-buffer)))
+          (stgit-run-silent "new" "--sign" "--save-template=-"))))))
 
 (defun stgit-confirm-new ()
   (interactive)
@@ -358,14 +768,15 @@ (defun stgit-confirm-new ()
     (stgit-capture-output nil
       (stgit-run "new" "-f" file))
     (with-current-buffer log-edit-parent-buffer
-      (stgit-refresh))))
+      (stgit-reload))))
 
 (defun stgit-create-patch-name (description)
   "Create a patch name from a long description"
   (let ((patch ""))
     (while (> (length description) 0)
       (cond ((string-match "\\`[a-zA-Z_-]+" description)
-             (setq patch (downcase (concat patch (match-string 0 description))))
+             (setq patch (downcase (concat patch
+                                           (match-string 0 description))))
              (setq description (substring description (match-end 0))))
             ((string-match "\\` +" description)
              (setq patch (concat patch "-"))
@@ -379,7 +790,7 @@ (defun stgit-create-patch-name (description)
           (t patch))))
 
 (defun stgit-delete (patch-names)
-  "Delete the named patches"
+  "Delete the named patches."
   (interactive (list (stgit-patches-marked-or-at-point)))
   (if (zerop (length patch-names))
       (error "No patches to delete")
@@ -387,10 +798,10 @@ (defun stgit-delete (patch-names)
                                (length patch-names)))
       (stgit-capture-output nil
         (apply 'stgit-run "delete" patch-names))
-      (stgit-refresh))))
+      (stgit-reload))))
 
 (defun stgit-coalesce (patch-names)
-  "Run stg coalesce on the named patches"
+  "Run stg coalesce on the named patches."
   (interactive (list (stgit-marked-patches)))
   (let ((edit-buf (get-buffer-create "*StGit edit*"))
         (dir default-directory))
@@ -398,7 +809,7 @@ (defun stgit-coalesce (patch-names)
     (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))))
+      (apply 'stgit-run-silent "coalesce" "--save-template=-" patch-names))))
 
 (defun stgit-confirm-coalesce ()
   (interactive)
@@ -407,7 +818,14 @@ (defun stgit-confirm-coalesce ()
     (stgit-capture-output nil
       (apply 'stgit-run "coalesce" "-f" file stgit-patches))
     (with-current-buffer log-edit-parent-buffer
-      (stgit-refresh))))
+      (stgit-clear-marks)
+      ;; Go to first marked patch and stay there
+      (goto-char (point-min))
+      (re-search-forward (concat "^[>+-]\\*") nil t)
+      (move-to-column goal-column)
+      (let ((pos (point)))
+        (stgit-reload)
+        (goto-char pos)))))
 
 (defun stgit-help ()
   "Display help for the StGit mode."
@@ -422,6 +840,24 @@ (defun stgit-undo (&optional arg)
     (if arg
         (stgit-run "undo" "--hard")
       (stgit-run "undo")))
-  (stgit-refresh))
+  (stgit-reload))
+
+(defun stgit-refresh (&optional arg)
+  "Run stg refresh.
+With prefix argument, refresh the marked patch or the patch under point."
+  (interactive "P")
+  (let ((patchargs (if arg
+                       (let ((patches (stgit-patches-marked-or-at-point)))
+                         (cond ((null patches)
+                                (error "No patch to update"))
+                               ((> (length patches) 1)
+                                (error "Too many patches selected"))
+                               (t
+                                (cons "-p" patches))))
+                     nil)))
+    (stgit-capture-output nil
+      (apply 'stgit-run "refresh" patchargs))
+    (stgit-refresh-git-status))
+  (stgit-reload))
 
 (provide 'stgit)
index 0a5f66a7f95fdd6ebb76dcb236bd4d733e4890ce..418a506d08a3ecf540cfea95931b82fb6ecfeab7 100644 (file)
@@ -37,7 +37,7 @@ class opt(object):
         return optparse.make_option(*self.pargs, **kwargs)
     def metavar(self):
         o = self.get_option()
-        if not o.nargs:
+        if not o.takes_value():
             return None
         if o.metavar:
             return o.metavar
index 46d43c150a58ce7919a4958e423a11d9306fca0a..84925afc5b3eab71242a9cf3b9a12549a4d6e50e 100644 (file)
@@ -59,9 +59,12 @@ def func(parser, options, args):
     rev2 = git_id(crt_series, '%s' % patch)
 
     if options.stat:
-        out.stdout_raw(gitlib.diffstat(git.diff(rev1 = rev1, rev2 = rev2)) + '\n')
+        output = gitlib.diffstat(git.diff(rev1 = rev1, rev2 = rev2))
     elif options.bare:
-        out.stdout_raw(git.barefiles(rev1, rev2) + '\n')
+        output = git.barefiles(rev1, rev2)
     else:
-        out.stdout_raw(git.files(rev1, rev2, diff_flags = options.diff_flags)
-                       + '\n')
+        output = git.files(rev1, rev2, diff_flags = options.diff_flags)
+    if output:
+        if not output.endswith('\n'):
+            output += '\n'
+        out.stdout_raw(output)
index 2dd88c359523ced5b0faa9c57a6ec85e11f333d5..cab896bad5c50e9a4cbf699027e71ddee0ade512 100644 (file)
@@ -154,9 +154,14 @@ def __get_sender():
         try:
             sender = str(git.user())
         except git.GitException:
-            sender = str(git.author())
+            try:
+                sender = str(git.author())
+            except git.GitException:
+                pass
     if not sender:
-        raise CmdException, 'unknown sender details'
+        raise CmdException, ('Unknown sender name and e-mail; you should'
+                             ' for example set git config user.name and'
+                             ' user.email')
     sender = email.Utils.parseaddr(sender)
 
     return email.Utils.formataddr(address_or_alias(sender))
@@ -551,6 +556,9 @@ def func(parser, options, args):
     else:
         raise CmdException, 'Incorrect options. Unknown patches to send'
 
+    # early test for sender identity
+    __get_sender()
+
     out.start('Checking the validity of the patches')
     for p in patches:
         if crt_series.empty_patch(p):
index 95196d3b3843eed48a15b0957ae80bb58cf36630..587380131d2893a4ff5a57325d59a2d484c5bb10 100644 (file)
@@ -27,10 +27,14 @@ help = 'Print the patch series'
 kind = 'stack'
 usage = ['[options] [<patch-range>]']
 description = """
-Show all the patches in the series or just those in the given
-range. The applied patches are prefixed with a '+', the unapplied ones
-with a '-' and the hidden ones with a '!'. The current patch is
-prefixed with a '>'. Empty patches are prefixed with a '0'."""
+Show all the patches in the series, or just those in the given range,
+ordered from top to bottom.
+
+The applied patches are prefixed with a +++ (except the current patch,
+which is prefixed with a +>+), the unapplied patches with a +-+, and
+the hidden patches with a +!+.
+
+Empty patches are prefixed with a '0'."""
 
 args = [argparse.patch_range(argparse.applied_patches,
                              argparse.unapplied_patches,
@@ -55,7 +59,10 @@ options = [
     opt('--author', action = 'store_true',
         short = 'Show the author name for each patch'),
     opt('-e', '--empty', action = 'store_true',
-        short = 'Check whether patches are empty'),
+        short = 'Check whether patches are empty', long = """
+        Before the +++, +>+, +-+, and +!+ prefixes, print a column
+        that contains either +0+ (for empty patches) or a space (for
+        non-empty patches)."""),
     opt('--showbranch', action = 'store_true',
         short = 'Append the branch name to the listed patches'),
     opt('--noprefix', action = 'store_true',
@@ -79,13 +86,16 @@ def __get_author(stack, patch):
     cd = stack.patches.get(patch).commit.data
     return cd.author.name
 
-def __print_patch(stack, patch, branch_str, prefix, empty_prefix, length, options):
+def __print_patch(stack, patch, branch_str, prefix, length, options):
     """Print a patch name, description and various markers.
     """
     if options.noprefix:
         prefix = ''
-    elif options.empty and stack.patches.get(patch).is_empty():
-        prefix = empty_prefix
+    elif options.empty:
+        if stack.patches.get(patch).is_empty():
+            prefix = '0' + prefix
+        else:
+            prefix = ' ' + prefix
 
     patch_str = branch_str + patch
 
@@ -180,12 +190,12 @@ def func(parser, options, args):
 
     if applied:
         for p in applied[:-1]:
-            __print_patch(stack, p, branch_str, '+ ', '0 ', max_len, options)
-        __print_patch(stack, applied[-1], branch_str, '> ', '0>', max_len,
+            __print_patch(stack, p, branch_str, '+ ', max_len, options)
+        __print_patch(stack, applied[-1], branch_str, '> ', max_len,
                       options)
 
     for p in unapplied:
-        __print_patch(stack, p, branch_str, '- ', '0 ', max_len, options)
+        __print_patch(stack, p, branch_str, '- ', max_len, options)
 
     for p in hidden:
-        __print_patch(stack, p, branch_str, '! ', '! ', max_len, options)
+        __print_patch(stack, p, branch_str, '! ', max_len, options)
index e461e3bfe3b984e8406af18ac66d84ed13c4dfb3..affc8c6ed22f17ce1d45d47a5546b3654a479352 100644 (file)
@@ -1,6 +1,7 @@
 import textwrap
 import stgit.commands
 from stgit import argparse
+import itertools
 
 def fun(name, *body):
     return ['%s ()' % name, '{', list(body), '}']
@@ -81,8 +82,10 @@ def command_fun(cmd, modname):
     return fun(
         '_stg_%s' % cmd,
         'local flags="%s"' % ' '.join(sorted(
-                flag for opt in mod.options
-                for flag in opt.flags if flag.startswith('--'))),
+                itertools.chain(
+                    ('--help',),
+                    (flag for opt in mod.options
+                     for flag in opt.flags if flag.startswith('--'))))),
         'local prev="${COMP_WORDS[COMP_CWORD-1]}"',
         'local cur="${COMP_WORDS[COMP_CWORD]}"',
         'case "$prev" in', [