stgit.el: Bugfix stgit-unmark-down on the last patch
[stgit] / contrib / stgit.el
CommitLineData
3a59f3db
KH
1;; stgit.el: An emacs mode for StGit
2;;
3;; Copyright (C) 2007 David Kågedal <davidk@lysator.liu.se>
4;;
5;; To install: put this file on the load-path and place the following
6;; in your .emacs file:
7;;
8;; (require 'stgit)
9;;
10;; To start: `M-x stgit'
11
0f076fe6
GH
12(require 'git nil t)
13
56d81fe5 14(defun stgit (dir)
a53347d9 15 "Manage StGit patches for the tree in DIR."
56d81fe5 16 (interactive "DDirectory: \n")
52144ce5 17 (switch-to-stgit-buffer (git-get-top-dir dir))
1f0bf00f 18 (stgit-reload))
56d81fe5 19
074a4fb0
GH
20(unless (fboundp 'git-get-top-dir)
21 (defun git-get-top-dir (dir)
22 "Retrieve the top-level directory of a git tree."
23 (let ((cdup (with-output-to-string
24 (with-current-buffer standard-output
25 (cd dir)
26 (unless (eq 0 (call-process "git" nil t nil
27 "rev-parse" "--show-cdup"))
28 (error "cannot find top-level git tree for %s." dir))))))
29 (expand-file-name (concat (file-name-as-directory dir)
30 (car (split-string cdup "\n")))))))
31
32(defun stgit-refresh-git-status (&optional dir)
33 "If it exists, refresh the `git-status' buffer belonging to
34directory DIR or `default-directory'"
35 (when (and (fboundp 'git-find-status-buffer)
36 (fboundp 'git-refresh-status))
37 (let* ((top-dir (git-get-top-dir (or dir default-directory)))
38 (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
39 (when git-status-buffer
40 (with-current-buffer git-status-buffer
41 (git-refresh-status))))))
52144ce5 42
56d81fe5 43(defun switch-to-stgit-buffer (dir)
a53347d9 44 "Switch to a (possibly new) buffer displaying StGit patches for DIR."
56d81fe5
DK
45 (setq dir (file-name-as-directory dir))
46 (let ((buffers (buffer-list)))
47 (while (and buffers
48 (not (with-current-buffer (car buffers)
49 (and (eq major-mode 'stgit-mode)
50 (string= default-directory dir)))))
51 (setq buffers (cdr buffers)))
52 (switch-to-buffer (if buffers
53 (car buffers)
54 (create-stgit-buffer dir)))))
55
56(defun create-stgit-buffer (dir)
57 "Create a buffer for showing StGit patches.
58Argument DIR is the repository path."
59 (let ((buf (create-file-buffer (concat dir "*stgit*")))
60 (inhibit-read-only t))
61 (with-current-buffer buf
62 (setq default-directory dir)
63 (stgit-mode)
64 (setq buffer-read-only t))
65 buf))
66
67(defmacro stgit-capture-output (name &rest body)
a53347d9 68 "Capture StGit output and show it in a window at the end."
34afb86c
DK
69 `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
70 (stgit-dir default-directory)
71 (inhibit-read-only t))
56d81fe5 72 (with-current-buffer output-buf
34afb86c
DK
73 (erase-buffer)
74 (setq default-directory stgit-dir)
75 (setq buffer-read-only t))
56d81fe5
DK
76 (let ((standard-output output-buf))
77 ,@body)
34afb86c
DK
78 (with-current-buffer output-buf
79 (set-buffer-modified-p nil)
80 (setq buffer-read-only t)
81 (if (< (point-min) (point-max))
82 (display-buffer output-buf t)))))
56d81fe5
DK
83(put 'stgit-capture-output 'lisp-indent-function 1)
84
9aecd505 85(defun stgit-run-silent (&rest args)
56d81fe5
DK
86 (apply 'call-process "stg" nil standard-output nil args))
87
9aecd505
DK
88(defun stgit-run (&rest args)
89 (let ((msgcmd (mapconcat #'identity args " ")))
90 (message "Running stg %s..." msgcmd)
91 (apply 'call-process "stg" nil standard-output nil args)
92 (message "Running stg %s...done" msgcmd)))
93
378a003d
GH
94(defun stgit-run-git (&rest args)
95 (let ((msgcmd (mapconcat #'identity args " ")))
96 (message "Running git %s..." msgcmd)
97 (apply 'call-process "git" nil standard-output nil args)
98 (message "Running git %s...done" msgcmd)))
99
1f60181a
GH
100(defun stgit-run-git-silent (&rest args)
101 (apply 'call-process "git" nil standard-output nil args))
102
1f0bf00f 103(defun stgit-reload ()
a53347d9 104 "Update the contents of the StGit buffer."
56d81fe5
DK
105 (interactive)
106 (let ((inhibit-read-only t)
107 (curline (line-number-at-pos))
108 (curpatch (stgit-patch-at-point)))
109 (erase-buffer)
110 (insert "Branch: ")
9aecd505
DK
111 (stgit-run-silent "branch")
112 (stgit-run-silent "series" "--description")
6df83d42 113 (stgit-rescan)
56d81fe5
DK
114 (if curpatch
115 (stgit-goto-patch curpatch)
074a4fb0
GH
116 (goto-line curline)))
117 (stgit-refresh-git-status))
56d81fe5 118
8f40753a
GH
119(defgroup stgit nil
120 "A user interface for the StGit patch maintenance tool."
121 :group 'tools)
122
07f464e0
DK
123(defface stgit-description-face
124 '((((background dark)) (:foreground "tan"))
125 (((background light)) (:foreground "dark red")))
8f40753a
GH
126 "The face used for StGit descriptions"
127 :group 'stgit)
07f464e0
DK
128
129(defface stgit-top-patch-face
130 '((((background dark)) (:weight bold :foreground "yellow"))
131 (((background light)) (:weight bold :foreground "purple"))
132 (t (:weight bold)))
8f40753a
GH
133 "The face used for the top patch names"
134 :group 'stgit)
07f464e0
DK
135
136(defface stgit-applied-patch-face
137 '((((background dark)) (:foreground "light yellow"))
138 (((background light)) (:foreground "purple"))
139 (t ()))
8f40753a
GH
140 "The face used for applied patch names"
141 :group 'stgit)
07f464e0
DK
142
143(defface stgit-unapplied-patch-face
144 '((((background dark)) (:foreground "gray80"))
145 (((background light)) (:foreground "orchid"))
146 (t ()))
8f40753a
GH
147 "The face used for unapplied patch names"
148 :group 'stgit)
07f464e0 149
1f60181a
GH
150(defface stgit-modified-file-face
151 '((((class color) (background light)) (:foreground "purple"))
152 (((class color) (background dark)) (:foreground "salmon")))
153 "StGit mode face used for modified file status"
154 :group 'stgit)
155
156(defface stgit-unmerged-file-face
157 '((((class color) (background light)) (:foreground "red" :bold t))
158 (((class color) (background dark)) (:foreground "red" :bold t)))
159 "StGit mode face used for unmerged file status"
160 :group 'stgit)
161
162(defface stgit-unknown-file-face
163 '((((class color) (background light)) (:foreground "goldenrod" :bold t))
164 (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
165 "StGit mode face used for unknown file status"
166 :group 'stgit)
167
a6d9a852
GH
168(defface stgit-file-permission-face
169 '((((class color) (background light)) (:foreground "green" :bold t))
170 (((class color) (background dark)) (:foreground "green" :bold t)))
171 "StGit mode face used for permission changes."
172 :group 'stgit)
173
1f60181a
GH
174(defcustom stgit-expand-find-copies-harder
175 nil
176 "Try harder to find copied files when listing patches.
177
178When not nil, runs git diff-tree with the --find-copies-harder
179flag, which reduces performance."
180 :type 'boolean
181 :group 'stgit)
182
183(defconst stgit-file-status-code-strings
184 (mapcar (lambda (arg)
185 (cons (car arg)
a6d9a852
GH
186 (propertize (cadr arg) 'face (car (cddr arg)))))
187 '((add "Added" stgit-modified-file-face)
188 (copy "Copied" stgit-modified-file-face)
189 (delete "Deleted" stgit-modified-file-face)
190 (modify "Modified" stgit-modified-file-face)
191 (rename "Renamed" stgit-modified-file-face)
192 (mode-change "Mode change" stgit-modified-file-face)
193 (unmerged "Unmerged" stgit-unmerged-file-face)
194 (unknown "Unknown" stgit-unknown-file-face)))
1f60181a
GH
195 "Alist of code symbols to description strings")
196
197(defun stgit-file-status-code-as-string (code)
198 "Return stgit status code as string"
a6d9a852
GH
199 (let ((str (assq (if (consp code) (car code) code)
200 stgit-file-status-code-strings)))
201 (when str
202 (format "%-11s "
203 (if (and str (consp code) (/= (cdr code) 100))
204 (format "%s %s" (cdr str)
205 (propertize (format "%d%%" (cdr code))
206 'face 'stgit-description-face))
207 (cdr str))))))
1f60181a 208
a6d9a852 209(defun stgit-file-status-code (str &optional score)
1f60181a
GH
210 "Return stgit status code from git status string"
211 (let ((code (assoc str '(("A" . add)
212 ("C" . copy)
213 ("D" . delete)
214 ("M" . modify)
215 ("R" . rename)
216 ("T" . mode-change)
217 ("U" . unmerged)
218 ("X" . unknown)))))
a6d9a852
GH
219 (setq code (if code (cdr code) 'unknown))
220 (when (stringp score)
221 (if (> (length score) 0)
222 (setq score (string-to-number score))
223 (setq score nil)))
224 (if score (cons code score) code)))
225
226(defconst stgit-file-type-strings
227 '((#o100 . "file")
228 (#o120 . "symlink")
229 (#o160 . "subproject"))
230 "Alist of names of file types")
231
232(defun stgit-file-type-string (type)
233 (let ((type-str (assoc type stgit-file-type-strings)))
234 (or (and type-str (cdr type-str))
235 (format "unknown type %o" type))))
236
237(defun stgit-file-type-change-string (old-perm new-perm)
238 (let ((old-type (lsh old-perm -9))
239 (new-type (lsh new-perm -9)))
240 (cond ((= old-type new-type) "")
241 ((zerop new-type) "")
242 ((zerop old-type)
243 (if (= new-type #o100)
244 ""
245 (format " (%s)" (stgit-file-type-string new-type))))
246 (t (format " (%s -> %s)"
247 (stgit-file-type-string old-type)
248 (stgit-file-type-string new-type))))))
249
250(defun stgit-file-mode-change-string (old-perm new-perm)
251 (setq old-perm (logand old-perm #o777)
252 new-perm (logand new-perm #o777))
253 (if (or (= old-perm new-perm)
254 (zerop old-perm)
255 (zerop new-perm))
256 ""
257 (let* ((modified (logxor old-perm new-perm))
258 (not-x-modified (logand (logxor old-perm new-perm) #o666)))
259 (cond ((zerop modified) "")
260 ((and (zerop not-x-modified)
261 (or (and (eq #o111 (logand old-perm #o111))
262 (propertize "-x" 'face 'stgit-file-permission-face))
263 (and (eq #o111 (logand new-perm #o111))
264 (propertize "+x" 'face
265 'stgit-file-permission-face)))))
266 (t (concat (propertize (format "%o" old-perm)
267 'face 'stgit-file-permission-face)
268 (propertize " -> "
269 'face 'stgit-description-face)
270 (propertize (format "%o" new-perm)
271 'face 'stgit-file-permission-face)))))))
1f60181a 272
378a003d
GH
273(defun stgit-expand-patch (patchsym)
274 (save-excursion
275 (forward-line)
1f60181a
GH
276 (let* ((start (point))
277 (result (with-output-to-string
278 (stgit-run-git "diff-tree" "-r" "-z"
279 (if stgit-expand-find-copies-harder
280 "--find-copies-harder"
281 "-C")
282 (stgit-id (symbol-name patchsym))))))
283 (let (mstart)
a6d9a852 284 (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]*\\)\\)"
1f60181a 285 result mstart)
a6d9a852
GH
286 (let ((copy-or-rename (match-string 4 result))
287 (old-perm (read (format "#o%s" (match-string 1 result))))
288 (new-perm (read (format "#o%s" (match-string 2 result))))
1f60181a 289 (line-start (point))
a6d9a852
GH
290 status
291 change
1f60181a
GH
292 properties)
293 (insert " ")
294 (if copy-or-rename
a6d9a852
GH
295 (let ((cr-score (match-string 5 result))
296 (cr-from-file (match-string 6 result))
297 (cr-to-file (match-string 7 result)))
298 (setq status (stgit-file-status-code copy-or-rename
299 cr-score)
300 properties (list 'stgit-old-file cr-from-file
301 'stgit-new-file cr-to-file)
302 change (concat
303 cr-from-file
304 (propertize " -> "
305 'face 'stgit-description-face)
306 cr-to-file)))
307 (setq status (stgit-file-status-code (match-string 8 result))
308 properties (list 'stgit-file (match-string 9 result))
309 change (match-string 9 result)))
310
311 (let ((mode-change (stgit-file-mode-change-string old-perm
312 new-perm)))
313 (insert (format "%-12s" (stgit-file-status-code-as-string
314 status))
315 mode-change
316 (if (> (length mode-change) 0) " " "")
317 change
318 (propertize (stgit-file-type-change-string old-perm
319 new-perm)
320 'face 'stgit-description-face)
321 ?\n))
1f60181a
GH
322 (add-text-properties line-start (point) properties))
323 (setq mstart (match-end 0))))
324 (when (= start (point))
325 (insert " <no files>\n"))
326 (put-text-property start (point) 'stgit-patchsym patchsym))))
378a003d 327
6df83d42
DK
328(defun stgit-rescan ()
329 "Rescan the status buffer."
07f464e0 330 (save-excursion
6df83d42
DK
331 (let ((marked ()))
332 (goto-char (point-min))
333 (while (not (eobp))
334 (cond ((looking-at "Branch: \\(.*\\)")
335 (put-text-property (match-beginning 1) (match-end 1)
336 'face 'bold))
8ee1e4b4 337 ((looking-at "\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
6df83d42
DK
338 (let ((state (match-string 1))
339 (patchsym (intern (match-string 3))))
340 (put-text-property
341 (match-beginning 3) (match-end 3) 'face
342 (cond ((string= state ">") 'stgit-top-patch-face)
343 ((string= state "+") 'stgit-applied-patch-face)
344 ((string= state "-") 'stgit-unapplied-patch-face)))
345 (put-text-property (match-beginning 4) (match-end 4)
346 'face 'stgit-description-face)
347 (when (memq patchsym stgit-marked-patches)
348 (replace-match "*" nil nil nil 2)
378a003d
GH
349 (setq marked (cons patchsym marked)))
350 (when (memq patchsym stgit-expanded-patches)
351 (stgit-expand-patch patchsym))
352 ))
ad80ce22
DK
353 ((or (looking-at "stg series: Branch \".*\" not initialised")
354 (looking-at "stg series: .*: branch not initialized"))
1c2426dc
DK
355 (forward-line 1)
356 (insert "Run M-x stgit-init to initialise")))
6df83d42
DK
357 (forward-line 1))
358 (setq stgit-marked-patches (nreverse marked)))))
07f464e0 359
378a003d
GH
360(defun stgit-select ()
361 "Expand or collapse the current entry"
362 (interactive)
363 (let ((curpatch (stgit-patch-at-point)))
364 (if (not curpatch)
365 (let ((patched-file (stgit-patched-file-at-point)))
366 (unless patched-file
367 (error "No patch or file on the current line"))
368 (let ((filename (expand-file-name (cdr patched-file))))
369 (unless (file-exists-p filename)
370 (error "File does not exist"))
371 (find-file filename)))
372 (setq curpatch (intern curpatch))
373 (setq stgit-expanded-patches
374 (if (memq curpatch stgit-expanded-patches)
375 (delq curpatch stgit-expanded-patches)
376 (cons curpatch stgit-expanded-patches)))
377 (stgit-reload))))
378
379(defun stgit-find-file-other-window ()
380 "Open file at point in other window"
381 (interactive)
382 (let ((patched-file (stgit-patched-file-at-point)))
383 (unless patched-file
384 (error "No file on the current line"))
385 (let ((filename (expand-file-name (cdr patched-file))))
386 (unless (file-exists-p filename)
387 (error "File does not exist"))
388 (find-file-other-window filename))))
389
83327d53 390(defun stgit-quit ()
a53347d9 391 "Hide the stgit buffer."
83327d53
GH
392 (interactive)
393 (bury-buffer))
394
0f076fe6 395(defun stgit-git-status ()
a53347d9 396 "Show status using `git-status'."
0f076fe6
GH
397 (interactive)
398 (unless (fboundp 'git-status)
399 (error "stgit-git-status requires git-status"))
400 (let ((dir default-directory))
401 (save-selected-window
402 (pop-to-buffer nil)
403 (git-status dir))))
404
378a003d
GH
405(defun stgit-next-line (&optional arg try-vscroll)
406 "Move cursor vertically down ARG lines"
407 (interactive "p\np")
408 (next-line arg try-vscroll)
409 (when (looking-at " \\S-")
410 (forward-char 2)))
411
412(defun stgit-previous-line (&optional arg try-vscroll)
413 "Move cursor vertically up ARG lines"
414 (interactive "p\np")
415 (previous-line arg try-vscroll)
416 (when (looking-at " \\S-")
417 (forward-char 2)))
418
419(defun stgit-next-patch (&optional arg)
420 "Move cursor down ARG patches"
421 (interactive "p")
422 (unless arg
423 (setq arg 1))
424 (if (< arg 0)
425 (stgit-previous-patch (- arg))
426 (while (not (zerop arg))
427 (setq arg (1- arg))
428 (while (progn (stgit-next-line)
429 (not (stgit-patch-at-point)))))))
430
431(defun stgit-previous-patch (&optional arg)
432 "Move cursor up ARG patches"
433 (interactive "p")
434 (unless arg
435 (setq arg 1))
436 (if (< arg 0)
437 (stgit-next-patch (- arg))
438 (while (not (zerop arg))
439 (setq arg (1- arg))
440 (while (progn (stgit-previous-line)
441 (not (stgit-patch-at-point)))))))
442
56d81fe5
DK
443(defvar stgit-mode-hook nil
444 "Run after `stgit-mode' is setup.")
445
446(defvar stgit-mode-map nil
447 "Keymap for StGit major mode.")
448
449(unless stgit-mode-map
450 (setq stgit-mode-map (make-keymap))
451 (suppress-keymap stgit-mode-map)
022a3664
GH
452 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
453 '((" " . stgit-mark)
3dccdc9b 454 ("m" . stgit-mark)
9b151b27
GH
455 ("\d" . stgit-unmark-up)
456 ("u" . stgit-unmark-down)
022a3664
GH
457 ("?" . stgit-help)
458 ("h" . stgit-help)
378a003d
GH
459 ("p" . stgit-previous-line)
460 ("n" . stgit-next-line)
461 ("\C-p" . stgit-previous-patch)
462 ("\C-n" . stgit-next-patch)
463 ("\M-{" . stgit-previous-patch)
464 ("\M-}" . stgit-next-patch)
0f076fe6 465 ("s" . stgit-git-status)
022a3664
GH
466 ("g" . stgit-reload)
467 ("r" . stgit-refresh)
468 ("\C-c\C-r" . stgit-rename)
469 ("e" . stgit-edit)
470 ("c" . stgit-coalesce)
471 ("N" . stgit-new)
472 ("R" . stgit-repair)
473 ("C" . stgit-commit)
474 ("U" . stgit-uncommit)
378a003d
GH
475 ("\r" . stgit-select)
476 ("o" . stgit-find-file-other-window)
022a3664
GH
477 (">" . stgit-push-next)
478 ("<" . stgit-pop-next)
479 ("P" . stgit-push-or-pop)
480 ("G" . stgit-goto)
481 ("=" . stgit-show)
482 ("D" . stgit-delete)
483 ([(control ?/)] . stgit-undo)
83327d53
GH
484 ("\C-_" . stgit-undo)
485 ("q" . stgit-quit))))
56d81fe5
DK
486
487(defun stgit-mode ()
488 "Major mode for interacting with StGit.
489Commands:
490\\{stgit-mode-map}"
491 (kill-all-local-variables)
492 (buffer-disable-undo)
493 (setq mode-name "StGit"
494 major-mode 'stgit-mode
495 goal-column 2)
496 (use-local-map stgit-mode-map)
497 (set (make-local-variable 'list-buffers-directory) default-directory)
6df83d42 498 (set (make-local-variable 'stgit-marked-patches) nil)
378a003d 499 (set (make-local-variable 'stgit-expanded-patches) nil)
2870f8b8 500 (set-variable 'truncate-lines 't)
56d81fe5
DK
501 (run-hooks 'stgit-mode-hook))
502
6df83d42
DK
503(defun stgit-add-mark (patch)
504 (let ((patchsym (intern patch)))
505 (setq stgit-marked-patches (cons patchsym stgit-marked-patches))))
506
507(defun stgit-remove-mark (patch)
508 (let ((patchsym (intern patch)))
509 (setq stgit-marked-patches (delq patchsym stgit-marked-patches))))
510
e6b1fdae
DK
511(defun stgit-clear-marks ()
512 (setq stgit-marked-patches '()))
513
6df83d42
DK
514(defun stgit-marked-patches ()
515 "Return the names of the marked patches."
516 (mapcar 'symbol-name stgit-marked-patches))
517
378a003d
GH
518(defun stgit-patch-at-point (&optional cause-error allow-file)
519 "Return the patch name on the current line.
520If CAUSE-ERROR is not nil, signal an error if none found.
521If ALLOW-FILE is not nil, also handle when point is on a file of
522a patch."
523 (or (and allow-file
524 (let ((patchsym (get-text-property (point) 'stgit-patchsym)))
525 (and patchsym
526 (symbol-name patchsym))))
527 (save-excursion
528 (beginning-of-line)
529 (and (looking-at "[>+-][ *]\\([^ ]*\\)")
530 (match-string-no-properties 1)))
531 (and cause-error
532 (error "No patch on this line"))))
533
1f60181a
GH
534(defun stgit-patched-file-at-point (&optional both-files)
535 "Returns a cons of the patchsym and file name at point. For
536copies and renames, return the new file if the patch is either
537applied. If BOTH-FILES is non-nil, return a cons of the old and
538the new file names instead of just one name."
539 (let ((patchsym (get-text-property (point) 'stgit-patchsym))
540 (file (get-text-property (point) 'stgit-file)))
541 (cond ((not patchsym) nil)
542 (file (cons patchsym file))
543 (both-files
544 (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
545 (get-text-property (point) 'stgit-new-file))))
546 (t
547 (let ((file-sym (save-excursion
548 (stgit-previous-patch)
549 (unless (equal (stgit-patch-at-point)
550 (symbol-name patchsym))
551 (error "Cannot find the %s patch" patchsym))
552 (beginning-of-line)
553 (if (= (char-after) ?-)
554 'stgit-old-file
555 'stgit-new-file))))
556 (cons patchsym (get-text-property (point) file-sym)))))))
56d81fe5 557
7755d7f1
KH
558(defun stgit-patches-marked-or-at-point ()
559 "Return the names of the marked patches, or the patch on the current line."
560 (if stgit-marked-patches
561 (stgit-marked-patches)
562 (let ((patch (stgit-patch-at-point)))
563 (if patch
564 (list patch)
565 '()))))
566
56d81fe5 567(defun stgit-goto-patch (patch)
a53347d9 568 "Move point to the line containing PATCH."
56d81fe5
DK
569 (let ((p (point)))
570 (goto-char (point-min))
6df83d42 571 (if (re-search-forward (concat "^[>+-][ *]" (regexp-quote patch) " ") nil t)
56d81fe5
DK
572 (progn (move-to-column goal-column)
573 t)
574 (goto-char p)
575 nil)))
576
1c2426dc 577(defun stgit-init ()
a53347d9 578 "Run stg init."
1c2426dc
DK
579 (interactive)
580 (stgit-capture-output nil
b0424080 581 (stgit-run "init"))
1f0bf00f 582 (stgit-reload))
1c2426dc 583
6df83d42 584(defun stgit-mark ()
a53347d9 585 "Mark the patch under point."
6df83d42 586 (interactive)
018fa1ac 587 (let ((patch (stgit-patch-at-point t)))
6df83d42 588 (stgit-add-mark patch)
1f0bf00f 589 (stgit-reload))
378a003d 590 (stgit-next-patch))
6df83d42 591
9b151b27 592(defun stgit-unmark-up ()
a53347d9 593 "Remove mark from the patch on the previous line."
6df83d42 594 (interactive)
378a003d 595 (stgit-previous-patch)
018fa1ac 596 (stgit-remove-mark (stgit-patch-at-point t))
9b151b27
GH
597 (stgit-reload))
598
599(defun stgit-unmark-down ()
a53347d9 600 "Remove mark from the patch on the current line."
9b151b27 601 (interactive)
018fa1ac 602 (stgit-remove-mark (stgit-patch-at-point t))
1288eda2
GH
603 (stgit-reload)
604 (stgit-next-patch))
6df83d42 605
56d81fe5 606(defun stgit-rename (name)
018fa1ac
GH
607 "Rename the patch under point to NAME."
608 (interactive (list (read-string "Patch name: " (stgit-patch-at-point t))))
609 (let ((old-name (stgit-patch-at-point t)))
56d81fe5
DK
610 (stgit-capture-output nil
611 (stgit-run "rename" old-name name))
378a003d
GH
612 (let ((old-name-sym (intern old-name))
613 (name-sym (intern name)))
614 (when (memq old-name-sym stgit-expanded-patches)
615 (setq stgit-expanded-patches
616 (cons name-sym (delq old-name-sym stgit-expanded-patches))))
617 (when (memq old-name-sym stgit-marked-patches)
618 (setq stgit-marked-patches
619 (cons name-sym (delq old-name-sym stgit-marked-patches)))))
1f0bf00f 620 (stgit-reload)
56d81fe5
DK
621 (stgit-goto-patch name)))
622
26201d96 623(defun stgit-repair ()
a53347d9 624 "Run stg repair."
26201d96
DK
625 (interactive)
626 (stgit-capture-output nil
b0424080 627 (stgit-run "repair"))
1f0bf00f 628 (stgit-reload))
26201d96 629
c4aad9a7
DK
630(defun stgit-commit ()
631 "Run stg commit."
632 (interactive)
633 (stgit-capture-output nil (stgit-run "commit"))
1f0bf00f 634 (stgit-reload))
c4aad9a7
DK
635
636(defun stgit-uncommit (arg)
637 "Run stg uncommit. Numeric arg determines number of patches to uncommit."
638 (interactive "p")
639 (stgit-capture-output nil (stgit-run "uncommit" "-n" (number-to-string arg)))
1f0bf00f 640 (stgit-reload))
c4aad9a7 641
0b661144
DK
642(defun stgit-push-next (npatches)
643 "Push the first unapplied patch.
644With numeric prefix argument, push that many patches."
645 (interactive "p")
646 (stgit-capture-output nil (stgit-run "push" "-n"
647 (number-to-string npatches)))
074a4fb0
GH
648 (stgit-reload)
649 (stgit-refresh-git-status))
56d81fe5 650
0b661144
DK
651(defun stgit-pop-next (npatches)
652 "Pop the topmost applied patch.
653With numeric prefix argument, pop that many patches."
654 (interactive "p")
655 (stgit-capture-output nil (stgit-run "pop" "-n" (number-to-string npatches)))
074a4fb0
GH
656 (stgit-reload)
657 (stgit-refresh-git-status))
56d81fe5 658
f9182fca
KH
659(defun stgit-applied-at-point ()
660 "Is the patch on the current line applied?"
661 (save-excursion
662 (beginning-of-line)
663 (looking-at "[>+]")))
664
665(defun stgit-push-or-pop ()
a53347d9 666 "Push or pop the patch on the current line."
f9182fca 667 (interactive)
018fa1ac 668 (let ((patch (stgit-patch-at-point t))
f9182fca
KH
669 (applied (stgit-applied-at-point)))
670 (stgit-capture-output nil
b0424080 671 (stgit-run (if applied "pop" "push") patch))
1f0bf00f 672 (stgit-reload)))
f9182fca 673
c7adf5ef 674(defun stgit-goto ()
a53347d9 675 "Go to the patch on the current line."
c7adf5ef 676 (interactive)
018fa1ac 677 (let ((patch (stgit-patch-at-point t)))
c7adf5ef 678 (stgit-capture-output nil
b0424080 679 (stgit-run "goto" patch))
1f0bf00f 680 (stgit-reload)))
c7adf5ef 681
378a003d
GH
682(defun stgit-id (patch)
683 "Return the git commit id for PATCH"
684 (let ((result (with-output-to-string
685 (stgit-run-silent "id" patch))))
686 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
687 (error "Cannot find commit id for %s" patch))
688 (match-string 1 result)))
689
56d81fe5 690(defun stgit-show ()
a53347d9 691 "Show the patch on the current line."
56d81fe5
DK
692 (interactive)
693 (stgit-capture-output "*StGit patch*"
378a003d
GH
694 (let ((patch (stgit-patch-at-point)))
695 (if (not patch)
1f60181a 696 (let ((patched-file (stgit-patched-file-at-point t)))
378a003d
GH
697 (unless patched-file
698 (error "No patch or file at point"))
699 (let ((id (stgit-id (symbol-name (car patched-file)))))
700 (with-output-to-temp-buffer "*StGit diff*"
1f60181a
GH
701 (if (consp (cdr patched-file))
702 ;; two files (copy or rename)
703 (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
704 (cadr patched-file) (cddr patched-file))
705 ;; just one file
706 (stgit-run-git "diff" (concat id "^") id "--"
707 (cdr patched-file)))
378a003d
GH
708 (with-current-buffer standard-output
709 (diff-mode)))))
710 (stgit-run "show" (stgit-patch-at-point))
711 (with-current-buffer standard-output
712 (goto-char (point-min))
713 (diff-mode))))))
0663524d 714
0bca35c8 715(defun stgit-edit ()
a53347d9 716 "Edit the patch on the current line."
0bca35c8 717 (interactive)
018fa1ac 718 (let ((patch (stgit-patch-at-point t))
0780be79 719 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
720 (dir default-directory))
721 (log-edit 'stgit-confirm-edit t nil edit-buf)
722 (set (make-local-variable 'stgit-edit-patch) patch)
723 (setq default-directory dir)
724 (let ((standard-output edit-buf))
9aecd505 725 (stgit-run-silent "edit" "--save-template=-" patch))))
0bca35c8
DK
726
727(defun stgit-confirm-edit ()
728 (interactive)
729 (let ((file (make-temp-file "stgit-edit-")))
730 (write-region (point-min) (point-max) file)
731 (stgit-capture-output nil
732 (stgit-run "edit" "-f" file stgit-edit-patch))
733 (with-current-buffer log-edit-parent-buffer
1f0bf00f 734 (stgit-reload))))
0bca35c8 735
aa04f831
GH
736(defun stgit-new (add-sign)
737 "Create a new patch.
738With a prefix argument, include a \"Signed-off-by:\" line at the
739end of the patch."
740 (interactive "P")
c5d45b92
GH
741 (let ((edit-buf (get-buffer-create "*StGit edit*"))
742 (dir default-directory))
743 (log-edit 'stgit-confirm-new t nil edit-buf)
aa04f831
GH
744 (setq default-directory dir)
745 (when add-sign
746 (save-excursion
747 (let ((standard-output (current-buffer)))
748 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
64c097a0
DK
749
750(defun stgit-confirm-new ()
751 (interactive)
27b0f9e4 752 (let ((file (make-temp-file "stgit-edit-")))
64c097a0
DK
753 (write-region (point-min) (point-max) file)
754 (stgit-capture-output nil
27b0f9e4 755 (stgit-run "new" "-f" file))
64c097a0 756 (with-current-buffer log-edit-parent-buffer
1f0bf00f 757 (stgit-reload))))
64c097a0
DK
758
759(defun stgit-create-patch-name (description)
760 "Create a patch name from a long description"
761 (let ((patch ""))
762 (while (> (length description) 0)
763 (cond ((string-match "\\`[a-zA-Z_-]+" description)
764 (setq patch (downcase (concat patch (match-string 0 description))))
765 (setq description (substring description (match-end 0))))
766 ((string-match "\\` +" description)
767 (setq patch (concat patch "-"))
768 (setq description (substring description (match-end 0))))
769 ((string-match "\\`[^a-zA-Z_-]+" description)
770 (setq description (substring description (match-end 0))))))
771 (cond ((= (length patch) 0)
772 "patch")
773 ((> (length patch) 20)
774 (substring patch 0 20))
775 (t patch))))
0bca35c8 776
7755d7f1 777(defun stgit-delete (patch-names)
a53347d9 778 "Delete the named patches."
7755d7f1
KH
779 (interactive (list (stgit-patches-marked-or-at-point)))
780 (if (zerop (length patch-names))
781 (error "No patches to delete")
782 (when (yes-or-no-p (format "Really delete %d patches? "
783 (length patch-names)))
784 (stgit-capture-output nil
785 (apply 'stgit-run "delete" patch-names))
1f0bf00f 786 (stgit-reload))))
7755d7f1 787
ea0def18 788(defun stgit-coalesce (patch-names)
a53347d9 789 "Run stg coalesce on the named patches."
ea0def18 790 (interactive (list (stgit-marked-patches)))
0780be79 791 (let ((edit-buf (get-buffer-create "*StGit edit*"))
ea0def18
DK
792 (dir default-directory))
793 (log-edit 'stgit-confirm-coalesce t nil edit-buf)
794 (set (make-local-variable 'stgit-patches) patch-names)
795 (setq default-directory dir)
796 (let ((standard-output edit-buf))
9aecd505 797 (apply 'stgit-run-silent "coalesce" "--save-template=-" patch-names))))
ea0def18
DK
798
799(defun stgit-confirm-coalesce ()
800 (interactive)
801 (let ((file (make-temp-file "stgit-edit-")))
802 (write-region (point-min) (point-max) file)
803 (stgit-capture-output nil
804 (apply 'stgit-run "coalesce" "-f" file stgit-patches))
805 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
806 (stgit-clear-marks)
807 ;; Go to first marked patch and stay there
808 (goto-char (point-min))
809 (re-search-forward (concat "^[>+-]\\*") nil t)
810 (move-to-column goal-column)
811 (let ((pos (point)))
1f0bf00f 812 (stgit-reload)
e6b1fdae 813 (goto-char pos)))))
ea0def18 814
0663524d
KH
815(defun stgit-help ()
816 "Display help for the StGit mode."
817 (interactive)
818 (describe-function 'stgit-mode))
3a59f3db 819
83e51dbf
DK
820(defun stgit-undo (&optional arg)
821 "Run stg undo.
822With prefix argument, run it with the --hard flag."
823 (interactive "P")
824 (stgit-capture-output nil
825 (if arg
826 (stgit-run "undo" "--hard")
827 (stgit-run "undo")))
1f0bf00f 828 (stgit-reload))
83e51dbf 829
4d73c4d8
DK
830(defun stgit-refresh (&optional arg)
831 "Run stg refresh.
a53347d9 832With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8
DK
833 (interactive "P")
834 (let ((patchargs (if arg
b0424080
GH
835 (let ((patches (stgit-patches-marked-or-at-point)))
836 (cond ((null patches)
837 (error "no patch to update"))
838 ((> (length patches) 1)
839 (error "too many patches selected"))
840 (t
841 (cons "-p" patches))))
842 nil)))
4d73c4d8 843 (stgit-capture-output nil
074a4fb0
GH
844 (apply 'stgit-run "refresh" patchargs))
845 (stgit-refresh-git-status))
4d73c4d8
DK
846 (stgit-reload))
847
3a59f3db 848(provide 'stgit)