+ (stgit-assert-mode)
+ (let* ((patchsyms (stgit-patches-marked-or-at-point t t))
+ (applied-syms (stgit-applied-patchsyms t))
+ (unapplied (set-difference patchsyms applied-syms)))
+ (stgit-capture-output nil
+ (apply 'stgit-run
+ (if unapplied "push" "pop")
+ "--"
+ (stgit-sort-patches (if unapplied unapplied patchsyms)))))
+ (stgit-reload))
+
+(defun stgit-goto-target ()
+ "Return the goto target a point; either a patchsym, :top,
+or :bottom."
+ (let ((patchsym (stgit-patch-name-at-point)))
+ (cond ((memq patchsym '(:work :index)) nil)
+ (patchsym)
+ ((not (next-single-property-change (point) 'patch-data))
+ :top)
+ ((not (previous-single-property-change (point) 'patch-data))
+ :bottom))))
+
+(defun stgit-goto ()
+ "Go to the patch on the current line.
+
+Push or pop patches to make this patch topmost. Push or pop all
+patches if used on a line after or before all patches."
+ (interactive)
+ (stgit-assert-mode)
+ (let ((patchsym (stgit-goto-target)))
+ (unless patchsym
+ (error "No patch to go to on this line"))
+ (case patchsym
+ (:top (stgit-push-or-pop-patches t t))
+ (:bottom (stgit-push-or-pop-patches nil t))
+ (t (stgit-capture-output nil
+ (stgit-run "goto" patchsym))
+ (stgit-reload)))))
+
+(defun stgit-id (patchsym)
+ "Return the git commit id for PATCHSYM.
+If PATCHSYM is a keyword, returns PATCHSYM unmodified."
+ (if (keywordp patchsym)
+ patchsym
+ (let ((result (with-output-to-string
+ (stgit-run-silent "id" patchsym))))
+ (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
+ (error "Cannot find commit id for %s" patchsym))
+ (match-string 1 result))))
+
+(defun stgit-whitespace-diff-arg (arg)
+ (when (numberp arg)
+ (cond ((> arg 4) "--ignore-all-space")
+ ((> arg 1) "--ignore-space-change"))))
+
+(defun stgit-show-patch (unmerged-stage ignore-whitespace)
+ "Show the patch on the current line.
+
+UNMERGED-STAGE is the argument to `git-diff' that that selects
+which stage to diff against in the case of unmerged files."
+ (let ((space-arg (stgit-whitespace-diff-arg ignore-whitespace))
+ (patch-name (stgit-patch-name-at-point t)))
+ (stgit-capture-output "*StGit patch*"
+ (case (get-text-property (point) 'entry-type)
+ ('file
+ (let* ((patched-file (stgit-patched-file-at-point))
+ (patch-id (let ((id (stgit-id patch-name)))
+ (if (and (eq id :index)
+ (eq (stgit-file->status patched-file)
+ 'unmerged))
+ :work
+ id)))
+ (args (append (and space-arg (list space-arg))
+ (and (stgit-file->cr-from patched-file)
+ (list (stgit-find-copies-harder-diff-arg)))
+ (cond ((eq patch-id :index)
+ '("--cached"))
+ ((eq patch-id :work)
+ (list unmerged-stage))
+ (t
+ (list (concat patch-id "^") patch-id)))
+ '("--")
+ (if (stgit-file->copy-or-rename patched-file)
+ (list (stgit-file->cr-from patched-file)
+ (stgit-file->cr-to patched-file))
+ (list (stgit-file->file patched-file))))))
+ (apply 'stgit-run-git "diff" args)))
+ ('patch
+ (let* ((patch-id (stgit-id patch-name)))
+ (if (or (eq patch-id :index) (eq patch-id :work))
+ (apply 'stgit-run-git "diff"
+ (stgit-find-copies-harder-diff-arg)
+ (append (and space-arg (list space-arg))
+ (if (eq patch-id :index)
+ '("--cached")
+ (list unmerged-stage))))
+ (let ((args (append '("show" "-O" "--patch-with-stat" "-O" "-M")
+ (and space-arg (list "-O" space-arg))
+ (list (stgit-patch-name-at-point)))))
+ (apply 'stgit-run args)))))
+ (t
+ (error "No patch or file at point")))
+ (with-current-buffer standard-output
+ (goto-char (point-min))
+ (diff-mode)))))
+
+(defmacro stgit-define-diff (name diff-arg &optional unmerged-action)
+ `(defun ,name (&optional ignore-whitespace)
+ ,(format "Show the patch on the current line.
+
+%sWith a prefix argument, ignore whitespace. With a prefix argument
+greater than four (e.g., \\[universal-argument] \
+\\[universal-argument] \\[%s]), ignore all whitespace."
+ (if unmerged-action
+ (format "For unmerged files, %s.\n\n" unmerged-action)
+ "")
+ name)
+ (interactive "p")
+ (stgit-assert-mode)
+ (stgit-show-patch ,diff-arg ignore-whitespace)))
+
+(stgit-define-diff stgit-diff
+ "--ours" nil)
+(stgit-define-diff stgit-diff-ours
+ "--ours"
+ "diff against our branch")
+(stgit-define-diff stgit-diff-theirs
+ "--theirs"
+ "diff against their branch")
+(stgit-define-diff stgit-diff-base
+ "--base"
+ "diff against the merge base")
+(stgit-define-diff stgit-diff-combined
+ "--cc"
+ "show a combined diff")
+
+(defun stgit-move-change-to-index (file &optional force)
+ "Copies the work tree state of FILE to index, using git add or git rm.
+
+If FORCE is not nil, use --force."
+ (let ((op (if (or (file-exists-p file) (file-symlink-p file))
+ '("add") '("rm" "-q"))))
+ (stgit-capture-output "*git output*"
+ (apply 'stgit-run-git (append op (and force '("--force"))
+ '("--") (list file))))))
+
+(defun stgit-remove-change-from-index (file)
+ "Unstages the change in FILE from the index"
+ (stgit-capture-output "*git output*"
+ (stgit-run-git "reset" "-q" "--" file)))
+
+(defun stgit-git-index-unmerged-p ()
+ (let (result)
+ (with-output-to-string
+ (setq result (not (zerop (stgit-run-git-silent "diff-index" "--cached"
+ "--diff-filter=U"
+ "--quiet" "HEAD")))))
+ result))
+
+(defun stgit-file-toggle-index ()
+ "Move modified file in or out of the index.
+
+Leaves the point where it is, but moves the mark to where the
+file ended up. You can then jump to the file with \
+\\[exchange-point-and-mark]."
+ (interactive)
+ (stgit-assert-mode)
+ (let* ((patched-file (or (stgit-patched-file-at-point)
+ (error "No file on the current line")))
+ (patched-status (stgit-file->status patched-file)))
+ (when (eq patched-status 'unmerged)
+ (error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
+ (let* ((patch (stgit-patch-at-point))
+ (patch-name (stgit-patch->name patch))
+ (mark-file (if (eq patched-status 'rename)
+ (stgit-file->cr-to patched-file)
+ (stgit-file->file patched-file)))
+ (point-file (if (eq patched-status 'rename)
+ (stgit-file->cr-from patched-file)
+ (stgit-neighbour-file))))
+
+ (cond ((eq patch-name :work)
+ (stgit-move-change-to-index (stgit-file->file patched-file)
+ (eq patched-status 'ignore)))
+ ((eq patch-name :index)
+ (stgit-remove-change-from-index (stgit-file->file patched-file)))
+ (t
+ (error "Can only move files between working tree and index")))
+ (stgit-refresh-worktree)
+ (stgit-refresh-index)
+ (stgit-goto-patch (if (eq patch-name :index) :work :index) mark-file)
+ (push-mark nil t t)
+ (stgit-goto-patch patch-name point-file))))
+
+(defun stgit-toggle-index ()
+ "Move change in or out of the index.
+
+Works on index and work tree, as well as files in either.
+
+Leaves the point where it is, but moves the mark to where the
+file ended up. You can then jump to the file with \
+\\[exchange-point-and-mark]."
+ (interactive)
+ (stgit-assert-mode)
+ (if (stgit-patched-file-at-point)
+ (stgit-file-toggle-index)
+ (let ((patch-name (stgit-patch-name-at-point)))
+ (unless (memq patch-name '(:index :work))
+ (error "Can only move changes between working tree and index"))
+ (when (stgit-git-index-unmerged-p)
+ (error "Resolve unmerged changes with \\[stgit-resolve-file] first"))
+ (if (if (eq patch-name :index)
+ (stgit-index-empty-p)
+ (stgit-work-tree-empty-p))
+ (message "No changes to be moved")
+ (stgit-capture-output nil
+ (if (eq patch-name :work)
+ (stgit-run-git "add" "--update")
+ (stgit-run-git "reset" "--mixed" "-q")))
+ (stgit-refresh-worktree)
+ (stgit-refresh-index))
+ (stgit-goto-patch (if (eq patch-name :index) :work :index)))))
+
+(defun stgit-edit ()
+ "Edit the patch on the current line."
+ (interactive)
+ (stgit-assert-mode)
+ (let ((patchsym (stgit-patch-name-at-point t t))
+ (edit-buf (get-buffer-create "*StGit edit*"))
+ (dir default-directory))
+ (log-edit 'stgit-confirm-edit t nil edit-buf)
+ (set (make-local-variable 'stgit-edit-patchsym) patchsym)
+ (setq default-directory dir)
+ (let ((standard-output edit-buf))
+ (save-excursion
+ (stgit-run-silent "edit" "--save-template=-" patchsym)))))
+
+(defun stgit-confirm-edit ()
+ (interactive)
+ (let ((file (make-temp-file "stgit-edit-")))
+ (write-region (point-min) (point-max) file)
+ (stgit-capture-output nil
+ (stgit-run "edit" "-f" file stgit-edit-patchsym))
+ (with-current-buffer log-edit-parent-buffer
+ (stgit-reload))))
+
+(defun stgit-new (add-sign &optional refresh)
+ "Create a new patch.
+With a prefix argument, include a \"Signed-off-by:\" line at the
+end of the patch."
+ (interactive "P")
+ (stgit-assert-mode)
+ (let ((edit-buf (get-buffer-create "*StGit edit*"))
+ (dir default-directory))
+ (log-edit 'stgit-confirm-new t nil edit-buf)
+ (setq default-directory dir)
+ (set (make-local-variable 'stgit-refresh-after-new) refresh)
+ (when add-sign
+ (save-excursion
+ (let ((standard-output (current-buffer)))
+ (stgit-run-silent "new" "--sign" "--save-template=-"))))))
+
+(defun stgit-confirm-new ()
+ (interactive)
+ (let ((file (make-temp-file "stgit-edit-"))
+ (refresh stgit-refresh-after-new))
+ (write-region (point-min) (point-max) file)
+ (stgit-capture-output nil
+ (stgit-run "new" "-f" file))
+ (with-current-buffer log-edit-parent-buffer
+ (if refresh
+ (stgit-refresh)
+ (stgit-reload)))))
+
+(defun stgit-new-and-refresh (add-sign)
+ "Create a new patch and refresh it with the current changes.
+
+With a prefix argument, include a \"Signed-off-by:\" line at the
+end of the patch.
+
+This works just like running `stgit-new' followed by `stgit-refresh'."
+ (interactive "P")
+ (stgit-assert-mode)
+ (stgit-new add-sign t))
+
+(defun stgit-create-patch-name (description)
+ "Create a patch name from a long description"
+ (let ((patch ""))
+ (while (> (length description) 0)
+ (cond ((string-match "\\`[a-zA-Z_-]+" description)
+ (setq patch (downcase (concat patch
+ (match-string 0 description))))
+ (setq description (substring description (match-end 0))))
+ ((string-match "\\` +" description)
+ (setq patch (concat patch "-"))
+ (setq description (substring description (match-end 0))))
+ ((string-match "\\`[^a-zA-Z_-]+" description)
+ (setq description (substring description (match-end 0))))))
+ (cond ((= (length patch) 0)
+ "patch")
+ ((> (length patch) 20)
+ (substring patch 0 20))
+ (t patch))))
+
+(defun stgit-delete (patchsyms &optional spill-p)
+ "Delete the patches in PATCHSYMS.
+Interactively, delete the marked patches, or the patch at point.
+
+With a prefix argument, or SPILL-P, spill the patch contents to
+the work tree and index."
+ (interactive (list (stgit-patches-marked-or-at-point t t)
+ current-prefix-arg))
+ (stgit-assert-mode)
+ (unless patchsyms
+ (error "No patches to delete"))
+ (when (memq :index patchsyms)
+ (error "Cannot delete the index"))
+ (when (memq :work patchsyms)
+ (error "Cannot delete the work tree"))
+
+ (let ((npatches (length patchsyms)))
+ (when (yes-or-no-p (format "Really delete %d patch%s%s? "
+ npatches
+ (if (= 1 npatches) "" "es")
+ (if spill-p
+ " (spilling contents to index)"
+ "")))
+ (let ((args (if spill-p
+ (cons "--spill" patchsyms)
+ patchsyms)))
+ (stgit-capture-output nil
+ (apply 'stgit-run "delete" args))
+ (stgit-reload)))))
+
+(defun stgit-move-patches-target ()
+ "Return the patchsym indicating a target patch for
+`stgit-move-patches'.
+
+This is either the first unmarked patch at or after point, or one
+of :top and :bottom if the point is after or before the applied
+patches."
+
+ (save-excursion
+ (let (result)
+ (while (not result)
+ (let ((patchsym (stgit-patch-name-at-point)))
+ (cond ((memq patchsym '(:work :index)) (setq result :top))
+ (patchsym (if (memq patchsym stgit-marked-patches)
+ (stgit-next-patch)
+ (setq result patchsym)))
+ ((re-search-backward "^>" nil t) (setq result :top))
+ (t (setq result :bottom)))))
+ result)))
+
+(defun stgit-sort-patches (patchsyms &optional allow-duplicates)
+ "Returns the list of patches in PATCHSYMS sorted according to
+their position in the patch series, bottommost first.
+
+PATCHSYMS must not contain duplicate entries, unless
+ALLOW-DUPLICATES is not nil."
+ (let (sorted-patchsyms
+ (series (with-output-to-string
+ (with-current-buffer standard-output
+ (stgit-run-silent "series" "--noprefix"))))
+ start)
+ (while (string-match "^\\(.+\\)" series start)
+ (let ((patchsym (intern (match-string 1 series))))
+ (when (memq patchsym patchsyms)
+ (setq sorted-patchsyms (cons patchsym sorted-patchsyms))))
+ (setq start (match-end 0)))
+ (setq sorted-patchsyms (nreverse sorted-patchsyms))
+
+ (unless allow-duplicates
+ (unless (= (length patchsyms) (length sorted-patchsyms))
+ (error "Internal error")))
+
+ sorted-patchsyms))
+
+(defun stgit-move-patches (patchsyms target-patch)
+ "Move the patches in PATCHSYMS to below TARGET-PATCH.
+If TARGET-PATCH is :bottom or :top, move the patches to the
+bottom or top of the stack, respectively.
+
+Interactively, move the marked patches to where the point is."
+ (interactive (list stgit-marked-patches
+ (stgit-move-patches-target)))
+ (stgit-assert-mode)
+ (unless patchsyms
+ (error "Need at least one patch to move"))
+
+ (unless target-patch
+ (error "Point not at a patch"))
+
+ ;; need to have patchsyms sorted by position in the stack
+ (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))