stgit.el: Add stgit-line-format
[stgit] / contrib / stgit.el
index 86602be..f08b007 100644 (file)
@@ -14,6 +14,7 @@
 
 (require 'git nil t)
 (require 'cl)
 
 (require 'git nil t)
 (require 'cl)
+(require 'comint)
 (require 'ewoc)
 (require 'easymenu)
 (require 'format-spec)
 (require 'ewoc)
 (require 'easymenu)
 (require 'format-spec)
@@ -43,12 +44,30 @@ instead of \"dir/old/file -> dir/new/file\"."
 (defcustom stgit-default-show-worktree t
   "Set to non-nil to by default show the working tree in a new stgit buffer.
 
 (defcustom stgit-default-show-worktree t
   "Set to non-nil to by default show the working tree in a new stgit buffer.
 
-Use \\<stgit-mode-map>\\[stgit-toggle-worktree] to toggle the
-this setting in an already-started StGit buffer."
+Use \\<stgit-mode-map>\\[stgit-toggle-worktree] to toggle this \
+setting in an already-started StGit buffer."
   :type 'boolean
   :group 'stgit
   :link '(variable-link stgit-show-worktree))
 
   :type 'boolean
   :group 'stgit
   :link '(variable-link stgit-show-worktree))
 
+(defcustom stgit-default-show-unknown nil
+  "Set to non-nil to by default show unknown files a new stgit buffer.
+
+Use \\<stgit-mode-map>\\[stgit-toggle-unknown] to toggle this \
+setting in an already-started StGit buffer."
+  :type 'boolean
+  :group 'stgit
+  :link '(variable-link stgit-show-unknown))
+
+(defcustom stgit-default-show-ignored nil
+  "Set to non-nil to by default show ignored files a new stgit buffer.
+
+Use \\<stgit-mode-map>\\[stgit-toggle-ignored] to toggle this \
+setting in an already-started StGit buffer."
+  :type 'boolean
+  :group 'stgit
+  :link '(variable-link stgit-show-ignored))
+
 (defcustom stgit-find-copies-harder nil
   "Try harder to find copied files when listing patches.
 
 (defcustom stgit-find-copies-harder nil
   "Try harder to find copied files when listing patches.
 
@@ -99,8 +118,8 @@ variable is used instead."
 (defcustom stgit-noname-patch-line-format "%s%m%e%D"
   "The alternate format string used to format patch lines.
 It has the same semantics as `stgit-patch-line-format', and the
 (defcustom stgit-noname-patch-line-format "%s%m%e%D"
   "The alternate format string used to format patch lines.
 It has the same semantics as `stgit-patch-line-format', and the
-display can be toggled between the two formats using
-\\<stgit-mode-map>>\\[stgit-toggle-patch-names].
+display can be toggled between the two formats using \
+\\<stgit-mode-map>\\[stgit-toggle-patch-names].
 
 The alternate form is used when the patch name is hidden."
   :type 'string
 
 The alternate form is used when the patch name is hidden."
   :type 'string
@@ -110,8 +129,8 @@ The alternate form is used when the patch name is hidden."
 (defcustom stgit-default-show-patch-names t
   "If non-nil, default to showing patch names in a new stgit buffer.
 
 (defcustom stgit-default-show-patch-names t
   "If non-nil, default to showing patch names in a new stgit buffer.
 
-Use \\<stgit-mode-map>\\[stgit-toggle-patch-names] to toggle the
-this setting in an already-started StGit buffer."
+Use \\<stgit-mode-map>\\[stgit-toggle-patch-names] \
+to toggle the this setting in an already-started StGit buffer."
   :type 'boolean
   :group 'stgit
   :link '(variable-link stgit-show-patch-names))
   :type 'boolean
   :group 'stgit
   :link '(variable-link stgit-show-patch-names))
@@ -270,14 +289,19 @@ A newline is appended."
     (error))
   (insert (match-string 1 text) ?\n))
 
     (error))
   (insert (match-string 1 text) ?\n))
 
+(defun stgit-line-format ()
+  "Return the current line format; one of
+`stgit-patch-line-format' and `stgit-noname-patch-line-format'"
+  (if stgit-show-patch-names
+      stgit-patch-line-format
+    stgit-noname-patch-line-format))
+
 (defun stgit-patch-pp (patch)
   (let* ((status (stgit-patch->status patch))
          (start (point))
          (name (stgit-patch->name patch))
          (face (cdr (assq status stgit-patch-status-face-alist)))
 (defun stgit-patch-pp (patch)
   (let* ((status (stgit-patch->status patch))
          (start (point))
          (name (stgit-patch->name patch))
          (face (cdr (assq status stgit-patch-status-face-alist)))
-         (fmt (if stgit-show-patch-names
-                  stgit-patch-line-format
-                stgit-noname-patch-line-format))
+         (fmt (stgit-line-format))
          (spec (format-spec-make
                 ?s (case status
                      ('applied "+")
          (spec (format-spec-make
                 ?s (case status
                      ('applied "+")
@@ -733,10 +757,12 @@ Cf. `stgit-file-type-change-string'."
 (defun stgit-insert-patch-files (patch)
   "Expand (show modification of) the patch PATCH after the line
 at point."
 (defun stgit-insert-patch-files (patch)
   "Expand (show modification of) the patch PATCH after the line
 at point."
-  (let* ((patchsym (stgit-patch->name patch))
-         (end      (point-marker))
-         (args     (list "-z" (stgit-find-copies-harder-diff-arg)))
-         (ewoc     (ewoc-create #'stgit-file-pp nil nil t)))
+  (let* ((patchsym     (stgit-patch->name patch))
+         (end          (point-marker))
+         (args         (list "-z" (stgit-find-copies-harder-diff-arg)))
+         (ewoc         (ewoc-create #'stgit-file-pp nil nil t))
+         (show-ignored stgit-show-ignored)
+         (show-unknown stgit-show-unknown))
     (set-marker-insertion-type end t)
     (setf (stgit-patch->files-ewoc patch) ewoc)
     (with-temp-buffer
     (set-marker-insertion-type end t)
     (setf (stgit-patch->files-ewoc patch) ewoc)
     (with-temp-buffer
@@ -752,9 +778,9 @@ at point."
                       `("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
 
         (when (and (eq patchsym :work))
                       `("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
 
         (when (and (eq patchsym :work))
-          (when stgit-show-ignored
+          (when show-ignored
             (stgit-insert-ls-files '("--ignored" "--others") "I"))
             (stgit-insert-ls-files '("--ignored" "--others") "I"))
-          (when stgit-show-unknown
+          (when show-unknown
             (stgit-insert-ls-files '("--directory" "--no-empty-directory"
                                      "--others")
                                    "X"))
             (stgit-insert-ls-files '("--directory" "--no-empty-directory"
                                      "--others")
                                    "X"))
@@ -843,6 +869,8 @@ See also `stgit-expand'."
         (stgit-process-files (lambda (f)
                                (setq node (ewoc-enter-after ewoc node f))))))
 
         (stgit-process-files (lambda (f)
                                (setq node (ewoc-enter-after ewoc node f))))))
 
+    (move-to-column (stgit-goal-column))
+
     (let ((inhibit-read-only t))
       (put-text-property start end 'patch-data patch))))
 
     (let ((inhibit-read-only t))
       (put-text-property start end 'patch-data patch))))
 
@@ -943,7 +971,6 @@ file for (applied) copies and renames."
 (unless stgit-mode-map
   (let ((diff-map   (make-sparse-keymap))
         (toggle-map (make-sparse-keymap)))
 (unless stgit-mode-map
   (let ((diff-map   (make-sparse-keymap))
         (toggle-map (make-sparse-keymap)))
-    (suppress-keymap diff-map)
     (mapc (lambda (arg) (define-key diff-map (car arg) (cdr arg)))
           '(("b" .        stgit-diff-base)
             ("c" .        stgit-diff-combined)
     (mapc (lambda (arg) (define-key diff-map (car arg) (cdr arg)))
           '(("b" .        stgit-diff-base)
             ("c" .        stgit-diff-combined)
@@ -951,7 +978,6 @@ file for (applied) copies and renames."
             ("o" .        stgit-diff-ours)
             ("r" .        stgit-diff-range)
             ("t" .        stgit-diff-theirs)))
             ("o" .        stgit-diff-ours)
             ("r" .        stgit-diff-range)
             ("t" .        stgit-diff-theirs)))
-    (suppress-keymap toggle-map)
     (mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
           '(("n" .        stgit-toggle-patch-names)
             ("t" .        stgit-toggle-worktree)
     (mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
           '(("n" .        stgit-toggle-patch-names)
             ("t" .        stgit-toggle-worktree)
@@ -1006,7 +1032,8 @@ file for (applied) copies and renames."
             ("\C-c\C-b" . stgit-rebase)
             ("t" .        ,toggle-map)
             ("d" .        ,diff-map)
             ("\C-c\C-b" . stgit-rebase)
             ("t" .        ,toggle-map)
             ("d" .        ,diff-map)
-            ("q" .        stgit-quit))))
+            ("q" .        stgit-quit)
+            ("!" .        stgit-execute))))
 
   (let ((at-unmerged-file '(let ((file (stgit-patched-file-at-point)))
                              (and file (eq (stgit-file->status file)
 
   (let ((at-unmerged-file '(let ((file (stgit-patched-file-at-point)))
                              (and file (eq (stgit-file->status file)
@@ -1148,6 +1175,8 @@ Basic commands:
 
 \\[stgit-git-status]   Run `git-status' (if available)
 
 
 \\[stgit-git-status]   Run `git-status' (if available)
 
+\\[stgit-execute]      Run an stg shell command
+
 Movement commands:
 \\[stgit-previous-line]        Move to previous line
 \\[stgit-next-line]    Move to next line
 Movement commands:
 \\[stgit-previous-line]        Move to previous line
 \\[stgit-next-line]    Move to next line
@@ -1223,7 +1252,9 @@ Commands for branches:
 
 Customization variables:
 `stgit-abbreviate-copies-and-renames'
 
 Customization variables:
 `stgit-abbreviate-copies-and-renames'
+`stgit-default-show-ignored'
 `stgit-default-show-patch-names'
 `stgit-default-show-patch-names'
+`stgit-default-show-unknown'
 `stgit-default-show-worktree'
 `stgit-find-copies-harder'
 `stgit-show-worktree-mode'
 `stgit-default-show-worktree'
 `stgit-find-copies-harder'
 `stgit-show-worktree-mode'
@@ -1235,15 +1266,17 @@ See also \\[customize-group] for the \"stgit\" group."
         major-mode 'stgit-mode
         goal-column 2)
   (use-local-map stgit-mode-map)
         major-mode 'stgit-mode
         goal-column 2)
   (use-local-map stgit-mode-map)
-  (set (make-local-variable 'list-buffers-directory) default-directory)
-  (set (make-local-variable 'stgit-marked-patches) nil)
-  (set (make-local-variable 'stgit-expanded-patches) (list :work :index))
-  (set (make-local-variable 'stgit-show-patch-names)
-       stgit-default-show-patch-names)
-  (set (make-local-variable 'stgit-show-worktree) stgit-default-show-worktree)
-  (set (make-local-variable 'stgit-index-node) nil)
-  (set (make-local-variable 'stgit-worktree-node) nil)
-  (set (make-local-variable 'parse-sexp-lookup-properties) t)
+  (mapc (lambda (x) (set (make-local-variable (car x)) (cdr x)))
+        `((list-buffers-directory       . ,default-directory)
+          (parse-sexp-lookup-properties . t)
+          (stgit-expanded-patches       . (:work :index))
+          (stgit-index-node             . nil)
+          (stgit-worktree-node          . nil)
+          (stgit-marked-patches         . nil)
+          (stgit-show-ignored           . ,stgit-default-show-ignored)
+          (stgit-show-patch-names       . ,stgit-default-show-patch-names)
+          (stgit-show-unknown           . ,stgit-default-show-unknown)
+          (stgit-show-worktree          . ,stgit-default-show-worktree)))
   (set-variable 'truncate-lines 't)
   (add-hook 'after-save-hook 'stgit-update-stgit-for-buffer)
   (unless stgit-did-advise
   (set-variable 'truncate-lines 't)
   (add-hook 'after-save-hook 'stgit-update-stgit-for-buffer)
   (unless stgit-did-advise
@@ -1272,15 +1305,53 @@ refresh the stgit buffers as the git status of files change."
               (add-to-list 'after-load-alist
                            `(,feature (stgit-advise-funlist
                                        (quote ,funlist)))))))
               (add-to-list 'after-load-alist
                            `(,feature (stgit-advise-funlist
                                        (quote ,funlist)))))))
+        ;; lists of (<feature> <function> <function> ...) to be advised
         '((vc-git vc-git-rename-file vc-git-revert vc-git-register)
         '((vc-git vc-git-rename-file vc-git-revert vc-git-register)
-          (git    git-add-file git-checkout git-revert-file git-remove-file))))
+          (git    git-add-file git-checkout git-revert-file git-remove-file)
+          (dired  dired-delete-file))))
+
+(defvar stgit-pending-refresh-buffers nil
+  "Alist of (cons `buffer' `refresh-index') of buffers that need
+to be refreshed. `refresh-index' is non-nil if both work tree
+and index need to be refreshed.")
+
+(defun stgit-run-pending-refreshs ()
+  "Run all pending stgit buffer updates as posted by `stgit-post-refresh'."
+  (let ((buffers stgit-pending-refresh-buffers)
+        (stgit-inhibit-messages t))
+    (setq stgit-pending-refresh-buffers nil)
+    (while buffers
+      (let* ((elem (car buffers))
+             (buffer (car elem))
+             (refresh-index (cdr elem)))
+        (when (buffer-name buffer)
+          (with-current-buffer buffer
+            (stgit-refresh-worktree)
+            (when refresh-index (stgit-refresh-index)))))
+      (setq buffers (cdr buffers)))))
+
+(defun stgit-post-refresh (buffer refresh-index)
+  "Update worktree status in BUFFER when Emacs becomes idle. If
+REFRESH-INDEX is non-nil, also update the index."
+  (unless stgit-pending-refresh-buffers
+    (run-with-idle-timer 0.1 nil 'stgit-run-pending-refreshs))
+  (let ((elem (assq buffer stgit-pending-refresh-buffers)))
+    (if elem
+        ;; if buffer is already present, set its refresh-index flag if
+        ;; necessary
+        (when refresh-index
+          (setcdr elem t))
+      ;; new entry
+      (setq stgit-pending-refresh-buffers
+            (cons (cons buffer refresh-index)
+                  stgit-pending-refresh-buffers)))))
 
 (defun stgit-update-stgit-for-buffer (&optional refresh-index)
 
 (defun stgit-update-stgit-for-buffer (&optional refresh-index)
-  "Refresh worktree status in any `stgit-mode' buffer that shows
-the status of the current buffer.
+  "When Emacs becomes idle, refresh worktree status in any
+`stgit-mode' buffer that shows the status of the current buffer.
 
 
-If REFRESH-INDEX is not-nil, also update the index."
-  (let* ((dir (cond ((eq major-mode 'git-status-mode)
+If REFRESH-INDEX is non-nil, also update the index."
+  (let* ((dir (cond ((derived-mode-p 'stgit-status-mode 'dired-mode)
                      default-directory)
                     (buffer-file-name
                      (file-name-directory
                      default-directory)
                     (buffer-file-name
                      (file-name-directory
@@ -1289,9 +1360,7 @@ If REFRESH-INDEX is not-nil, also update the index."
                             (error nil))))
         (buffer (and gitdir (stgit-find-buffer gitdir))))
     (when buffer
                             (error nil))))
         (buffer (and gitdir (stgit-find-buffer gitdir))))
     (when buffer
-      (with-current-buffer buffer
-        (stgit-refresh-worktree)
-        (when refresh-index (stgit-refresh-index))))))
+      (stgit-post-refresh buffer refresh-index))))
 
 (defun stgit-add-mark (patchsym)
   "Mark the patch PATCHSYM."
 
 (defun stgit-add-mark (patchsym)
   "Mark the patch PATCHSYM."
@@ -1492,9 +1561,16 @@ If ALL is not nil, also return non-stgit branches."
               ((not (string-match stgit-allowed-branch-name-re branch))
                (error "Invalid branch name"))
               ((yes-or-no-p (format "Create branch \"%s\"? " branch))
               ((not (string-match stgit-allowed-branch-name-re branch))
                (error "Invalid branch name"))
               ((yes-or-no-p (format "Create branch \"%s\"? " branch))
-               (stgit-capture-output nil (stgit-run "branch" "--create" "--"
-                                                    branch))
-               t))
+               (let ((branch-point (completing-read
+                                    "Branch from (default current branch): "
+                                    (stgit-available-branches))))
+                 (stgit-capture-output nil
+                   (apply 'stgit-run
+                          `("branch" "--create" "--"
+                            ,branch
+                            ,@(unless (zerop (length branch-point))
+                                (list branch-point)))))
+                 t)))
     (stgit-reload)))
 
 (defun stgit-available-refs (&optional omit-stgit)
     (stgit-reload)))
 
 (defun stgit-available-refs (&optional omit-stgit)
@@ -2211,6 +2287,75 @@ deepest patch had before the squash."
   (interactive)
   (describe-function 'stgit-mode))
 
   (interactive)
   (describe-function 'stgit-mode))
 
+(defun stgit-execute-process-sentinel (process sentinel)
+  (let (old-sentinel stgit-buf)
+    (with-current-buffer (process-buffer process)
+      (setq old-sentinel old-process-sentinel
+            stgit-buf    stgit-buffer))
+    (and (memq (process-status process) '(exit signal))
+         (buffer-live-p stgit-buf)
+         (with-current-buffer stgit-buf
+           (stgit-reload)))
+    (funcall old-sentinel process sentinel)))
+
+(defun stgit-execute-process-filter (process output)
+  (with-current-buffer (process-buffer process)
+    (let* ((old-point (point))
+           (pmark     (process-mark process))
+           (insert-at (marker-position pmark))
+           (at-pmark  (= insert-at old-point)))
+      (goto-char insert-at)
+      (insert-before-markers output)
+      (comint-carriage-motion insert-at (point))
+      (set-marker pmark (point))
+      (unless at-pmark
+        (goto-char old-point)))))
+
+(defun stgit-execute ()
+  "Prompt for an stg command to execute in a shell.
+
+The names of any marked patches or the patch at point are
+inserted in the command to be executed.
+
+If the command ends in an ampersand, run it asynchronously.
+
+When the command has finished, reload the stgit buffer."
+  (interactive)
+  (stgit-assert-mode)
+  (let* ((patches (stgit-patches-marked-or-at-point nil t))
+         (patch-names (mapcar 'symbol-name patches))
+         (hyphens (find-if (lambda (s) (string-match "^-" s)) patch-names))
+         (defaultcmd (if patches
+                         (concat "stg  "
+                                 (and hyphens "-- ")
+                                 (mapconcat 'identity patch-names " "))
+                       "stg "))
+         (cmd (read-from-minibuffer "Shell command: " (cons defaultcmd 5)
+                                    nil nil 'shell-command-history))
+         (async (string-match "&[ \t]*\\'" cmd))
+         (buffer (get-buffer-create
+                  (if async
+                      "*Async Shell Command*"
+                    "*Shell Command Output*"))))
+    ;; cannot use minibuffer as stgit-reload would overwrite it; if we
+    ;; show the buffer, shell-command will not use the minibuffer
+    (display-buffer buffer)
+    (shell-command cmd)
+    (if async
+        (let ((old-buffer (current-buffer)))
+          (with-current-buffer buffer
+            (let ((process (get-buffer-process buffer)))
+              (set (make-local-variable 'old-process-sentinel)
+                   (process-sentinel process))
+              (set (make-local-variable 'stgit-buffer)
+                   old-buffer)
+              (set-process-filter process 'stgit-execute-process-filter)
+              (set-process-sentinel process 'stgit-execute-process-sentinel))))
+      (with-current-buffer buffer
+        (comint-carriage-motion (point-min) (point-max)))
+      (shrink-window-if-larger-than-buffer (get-buffer-window buffer))
+      (stgit-reload))))
+
 (defun stgit-undo-or-redo (redo hard)
   "Run stg undo or, if REDO is non-nil, stg redo.
 
 (defun stgit-undo-or-redo (redo hard)
   "Run stg undo or, if REDO is non-nil, stg redo.
 
@@ -2297,6 +2442,8 @@ work tree will show up."
   "Toggle the visibility of files ignored by git in the work
 tree. With ARG, show these files if ARG is positive.
 
   "Toggle the visibility of files ignored by git in the work
 tree. With ARG, show these files if ARG is positive.
 
+Its initial setting is controlled by `stgit-default-show-ignored'.
+
 Use \\[stgit-toggle-worktree] to show the work tree."
   (interactive)
   (stgit-assert-mode)
 Use \\[stgit-toggle-worktree] to show the work tree."
   (interactive)
   (stgit-assert-mode)
@@ -2310,6 +2457,8 @@ Use \\[stgit-toggle-worktree] to show the work tree."
   "Toggle the visibility of files not registered with git in the
 work tree. With ARG, show these files if ARG is positive.
 
   "Toggle the visibility of files not registered with git in the
 work tree. With ARG, show these files if ARG is positive.
 
+Its initial setting is controlled by `stgit-default-show-unknown'.
+
 Use \\[stgit-toggle-worktree] to show the work tree."
   (interactive)
   (stgit-assert-mode)
 Use \\[stgit-toggle-worktree] to show the work tree."
   (interactive)
   (stgit-assert-mode)