chiark / gitweb /
Merge branch 'stable'
[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"))
df283a8b 28 (error "Cannot find top-level git tree for %s" dir))))))
074a4fb0
GH
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)
47271f41
GH
233 "Return string describing file type TYPE (the high bits of file permission).
234Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
a6d9a852
GH
235 (let ((type-str (assoc type stgit-file-type-strings)))
236 (or (and type-str (cdr type-str))
237 (format "unknown type %o" type))))
238
239(defun stgit-file-type-change-string (old-perm new-perm)
47271f41
GH
240 "Return string describing file type change from OLD-PERM to NEW-PERM.
241Cf. `stgit-file-type-string'."
a6d9a852
GH
242 (let ((old-type (lsh old-perm -9))
243 (new-type (lsh new-perm -9)))
244 (cond ((= old-type new-type) "")
245 ((zerop new-type) "")
246 ((zerop old-type)
247 (if (= new-type #o100)
248 ""
249 (format " (%s)" (stgit-file-type-string new-type))))
250 (t (format " (%s -> %s)"
251 (stgit-file-type-string old-type)
252 (stgit-file-type-string new-type))))))
253
254(defun stgit-file-mode-change-string (old-perm new-perm)
47271f41
GH
255 "Return string describing file mode change from OLD-PERM to NEW-PERM.
256Cf. `stgit-file-type-change-string'."
a6d9a852
GH
257 (setq old-perm (logand old-perm #o777)
258 new-perm (logand new-perm #o777))
259 (if (or (= old-perm new-perm)
260 (zerop old-perm)
261 (zerop new-perm))
262 ""
263 (let* ((modified (logxor old-perm new-perm))
264 (not-x-modified (logand (logxor old-perm new-perm) #o666)))
265 (cond ((zerop modified) "")
266 ((and (zerop not-x-modified)
267 (or (and (eq #o111 (logand old-perm #o111))
268 (propertize "-x" 'face 'stgit-file-permission-face))
269 (and (eq #o111 (logand new-perm #o111))
270 (propertize "+x" 'face
271 'stgit-file-permission-face)))))
272 (t (concat (propertize (format "%o" old-perm)
273 'face 'stgit-file-permission-face)
274 (propertize " -> "
275 'face 'stgit-description-face)
276 (propertize (format "%o" new-perm)
277 'face 'stgit-file-permission-face)))))))
1f60181a 278
378a003d 279(defun stgit-expand-patch (patchsym)
47271f41
GH
280 "Expand (show modification of) the patch with name PATCHSYM (a
281symbol) at point.
282`stgit-expand-find-copies-harder' controls how hard to try to
283find copied files."
378a003d
GH
284 (save-excursion
285 (forward-line)
1f60181a
GH
286 (let* ((start (point))
287 (result (with-output-to-string
288 (stgit-run-git "diff-tree" "-r" "-z"
289 (if stgit-expand-find-copies-harder
290 "--find-copies-harder"
291 "-C")
292 (stgit-id (symbol-name patchsym))))))
293 (let (mstart)
a6d9a852 294 (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 295 result mstart)
a6d9a852
GH
296 (let ((copy-or-rename (match-string 4 result))
297 (old-perm (read (format "#o%s" (match-string 1 result))))
298 (new-perm (read (format "#o%s" (match-string 2 result))))
1f60181a 299 (line-start (point))
a6d9a852
GH
300 status
301 change
1f60181a
GH
302 properties)
303 (insert " ")
304 (if copy-or-rename
a6d9a852
GH
305 (let ((cr-score (match-string 5 result))
306 (cr-from-file (match-string 6 result))
307 (cr-to-file (match-string 7 result)))
308 (setq status (stgit-file-status-code copy-or-rename
309 cr-score)
310 properties (list 'stgit-old-file cr-from-file
311 'stgit-new-file cr-to-file)
312 change (concat
313 cr-from-file
314 (propertize " -> "
315 'face 'stgit-description-face)
316 cr-to-file)))
317 (setq status (stgit-file-status-code (match-string 8 result))
318 properties (list 'stgit-file (match-string 9 result))
319 change (match-string 9 result)))
320
321 (let ((mode-change (stgit-file-mode-change-string old-perm
322 new-perm)))
323 (insert (format "%-12s" (stgit-file-status-code-as-string
324 status))
325 mode-change
326 (if (> (length mode-change) 0) " " "")
327 change
328 (propertize (stgit-file-type-change-string old-perm
329 new-perm)
330 'face 'stgit-description-face)
331 ?\n))
1f60181a
GH
332 (add-text-properties line-start (point) properties))
333 (setq mstart (match-end 0))))
334 (when (= start (point))
335 (insert " <no files>\n"))
336 (put-text-property start (point) 'stgit-patchsym patchsym))))
378a003d 337
6df83d42
DK
338(defun stgit-rescan ()
339 "Rescan the status buffer."
07f464e0 340 (save-excursion
6df83d42
DK
341 (let ((marked ()))
342 (goto-char (point-min))
343 (while (not (eobp))
344 (cond ((looking-at "Branch: \\(.*\\)")
345 (put-text-property (match-beginning 1) (match-end 1)
346 'face 'bold))
8ee1e4b4 347 ((looking-at "\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
6df83d42
DK
348 (let ((state (match-string 1))
349 (patchsym (intern (match-string 3))))
350 (put-text-property
351 (match-beginning 3) (match-end 3) 'face
352 (cond ((string= state ">") 'stgit-top-patch-face)
353 ((string= state "+") 'stgit-applied-patch-face)
354 ((string= state "-") 'stgit-unapplied-patch-face)))
355 (put-text-property (match-beginning 4) (match-end 4)
356 'face 'stgit-description-face)
357 (when (memq patchsym stgit-marked-patches)
358 (replace-match "*" nil nil nil 2)
378a003d
GH
359 (setq marked (cons patchsym marked)))
360 (when (memq patchsym stgit-expanded-patches)
361 (stgit-expand-patch patchsym))
362 ))
ad80ce22
DK
363 ((or (looking-at "stg series: Branch \".*\" not initialised")
364 (looking-at "stg series: .*: branch not initialized"))
1c2426dc
DK
365 (forward-line 1)
366 (insert "Run M-x stgit-init to initialise")))
6df83d42
DK
367 (forward-line 1))
368 (setq stgit-marked-patches (nreverse marked)))))
07f464e0 369
378a003d
GH
370(defun stgit-select ()
371 "Expand or collapse the current entry"
372 (interactive)
373 (let ((curpatch (stgit-patch-at-point)))
374 (if (not curpatch)
375 (let ((patched-file (stgit-patched-file-at-point)))
376 (unless patched-file
377 (error "No patch or file on the current line"))
378 (let ((filename (expand-file-name (cdr patched-file))))
379 (unless (file-exists-p filename)
380 (error "File does not exist"))
381 (find-file filename)))
382 (setq curpatch (intern curpatch))
383 (setq stgit-expanded-patches
384 (if (memq curpatch stgit-expanded-patches)
385 (delq curpatch stgit-expanded-patches)
386 (cons curpatch stgit-expanded-patches)))
387 (stgit-reload))))
388
389(defun stgit-find-file-other-window ()
390 "Open file at point in other window"
391 (interactive)
392 (let ((patched-file (stgit-patched-file-at-point)))
393 (unless patched-file
394 (error "No file on the current line"))
395 (let ((filename (expand-file-name (cdr patched-file))))
396 (unless (file-exists-p filename)
397 (error "File does not exist"))
398 (find-file-other-window filename))))
399
83327d53 400(defun stgit-quit ()
a53347d9 401 "Hide the stgit buffer."
83327d53
GH
402 (interactive)
403 (bury-buffer))
404
0f076fe6 405(defun stgit-git-status ()
a53347d9 406 "Show status using `git-status'."
0f076fe6
GH
407 (interactive)
408 (unless (fboundp 'git-status)
df283a8b 409 (error "The stgit-git-status command requires git-status"))
0f076fe6
GH
410 (let ((dir default-directory))
411 (save-selected-window
412 (pop-to-buffer nil)
413 (git-status dir))))
414
378a003d
GH
415(defun stgit-next-line (&optional arg try-vscroll)
416 "Move cursor vertically down ARG lines"
417 (interactive "p\np")
418 (next-line arg try-vscroll)
419 (when (looking-at " \\S-")
420 (forward-char 2)))
421
422(defun stgit-previous-line (&optional arg try-vscroll)
423 "Move cursor vertically up ARG lines"
424 (interactive "p\np")
425 (previous-line arg try-vscroll)
426 (when (looking-at " \\S-")
427 (forward-char 2)))
428
429(defun stgit-next-patch (&optional arg)
430 "Move cursor down ARG patches"
431 (interactive "p")
432 (unless arg
433 (setq arg 1))
434 (if (< arg 0)
435 (stgit-previous-patch (- arg))
436 (while (not (zerop arg))
437 (setq arg (1- arg))
438 (while (progn (stgit-next-line)
439 (not (stgit-patch-at-point)))))))
440
441(defun stgit-previous-patch (&optional arg)
442 "Move cursor up ARG patches"
443 (interactive "p")
444 (unless arg
445 (setq arg 1))
446 (if (< arg 0)
447 (stgit-next-patch (- arg))
448 (while (not (zerop arg))
449 (setq arg (1- arg))
450 (while (progn (stgit-previous-line)
451 (not (stgit-patch-at-point)))))))
452
56d81fe5
DK
453(defvar stgit-mode-hook nil
454 "Run after `stgit-mode' is setup.")
455
456(defvar stgit-mode-map nil
457 "Keymap for StGit major mode.")
458
459(unless stgit-mode-map
460 (setq stgit-mode-map (make-keymap))
461 (suppress-keymap stgit-mode-map)
022a3664
GH
462 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
463 '((" " . stgit-mark)
3dccdc9b 464 ("m" . stgit-mark)
9b151b27
GH
465 ("\d" . stgit-unmark-up)
466 ("u" . stgit-unmark-down)
022a3664
GH
467 ("?" . stgit-help)
468 ("h" . stgit-help)
378a003d
GH
469 ("p" . stgit-previous-line)
470 ("n" . stgit-next-line)
471 ("\C-p" . stgit-previous-patch)
472 ("\C-n" . stgit-next-patch)
473 ("\M-{" . stgit-previous-patch)
474 ("\M-}" . stgit-next-patch)
0f076fe6 475 ("s" . stgit-git-status)
022a3664
GH
476 ("g" . stgit-reload)
477 ("r" . stgit-refresh)
478 ("\C-c\C-r" . stgit-rename)
479 ("e" . stgit-edit)
480 ("c" . stgit-coalesce)
481 ("N" . stgit-new)
482 ("R" . stgit-repair)
483 ("C" . stgit-commit)
484 ("U" . stgit-uncommit)
378a003d
GH
485 ("\r" . stgit-select)
486 ("o" . stgit-find-file-other-window)
022a3664
GH
487 (">" . stgit-push-next)
488 ("<" . stgit-pop-next)
489 ("P" . stgit-push-or-pop)
490 ("G" . stgit-goto)
491 ("=" . stgit-show)
492 ("D" . stgit-delete)
493 ([(control ?/)] . stgit-undo)
83327d53
GH
494 ("\C-_" . stgit-undo)
495 ("q" . stgit-quit))))
56d81fe5
DK
496
497(defun stgit-mode ()
498 "Major mode for interacting with StGit.
499Commands:
500\\{stgit-mode-map}"
501 (kill-all-local-variables)
502 (buffer-disable-undo)
503 (setq mode-name "StGit"
504 major-mode 'stgit-mode
505 goal-column 2)
506 (use-local-map stgit-mode-map)
507 (set (make-local-variable 'list-buffers-directory) default-directory)
6df83d42 508 (set (make-local-variable 'stgit-marked-patches) nil)
378a003d 509 (set (make-local-variable 'stgit-expanded-patches) nil)
2870f8b8 510 (set-variable 'truncate-lines 't)
56d81fe5
DK
511 (run-hooks 'stgit-mode-hook))
512
6df83d42 513(defun stgit-add-mark (patch)
47271f41 514 "Mark the patch named PATCH."
6df83d42
DK
515 (let ((patchsym (intern patch)))
516 (setq stgit-marked-patches (cons patchsym stgit-marked-patches))))
517
518(defun stgit-remove-mark (patch)
47271f41 519 "Unmark the patch named PATCH."
6df83d42
DK
520 (let ((patchsym (intern patch)))
521 (setq stgit-marked-patches (delq patchsym stgit-marked-patches))))
522
e6b1fdae 523(defun stgit-clear-marks ()
47271f41 524 "Unmark all patches."
e6b1fdae
DK
525 (setq stgit-marked-patches '()))
526
6df83d42
DK
527(defun stgit-marked-patches ()
528 "Return the names of the marked patches."
529 (mapcar 'symbol-name stgit-marked-patches))
530
378a003d
GH
531(defun stgit-patch-at-point (&optional cause-error allow-file)
532 "Return the patch name on the current line.
533If CAUSE-ERROR is not nil, signal an error if none found.
534If ALLOW-FILE is not nil, also handle when point is on a file of
535a patch."
536 (or (and allow-file
537 (let ((patchsym (get-text-property (point) 'stgit-patchsym)))
538 (and patchsym
539 (symbol-name patchsym))))
540 (save-excursion
541 (beginning-of-line)
542 (and (looking-at "[>+-][ *]\\([^ ]*\\)")
543 (match-string-no-properties 1)))
544 (and cause-error
545 (error "No patch on this line"))))
546
1f60181a
GH
547(defun stgit-patched-file-at-point (&optional both-files)
548 "Returns a cons of the patchsym and file name at point. For
549copies and renames, return the new file if the patch is either
550applied. If BOTH-FILES is non-nil, return a cons of the old and
551the new file names instead of just one name."
552 (let ((patchsym (get-text-property (point) 'stgit-patchsym))
553 (file (get-text-property (point) 'stgit-file)))
554 (cond ((not patchsym) nil)
555 (file (cons patchsym file))
556 (both-files
557 (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
558 (get-text-property (point) 'stgit-new-file))))
559 (t
560 (let ((file-sym (save-excursion
561 (stgit-previous-patch)
562 (unless (equal (stgit-patch-at-point)
563 (symbol-name patchsym))
564 (error "Cannot find the %s patch" patchsym))
565 (beginning-of-line)
566 (if (= (char-after) ?-)
567 'stgit-old-file
568 'stgit-new-file))))
569 (cons patchsym (get-text-property (point) file-sym)))))))
56d81fe5 570
7755d7f1
KH
571(defun stgit-patches-marked-or-at-point ()
572 "Return the names of the marked patches, or the patch on the current line."
573 (if stgit-marked-patches
574 (stgit-marked-patches)
575 (let ((patch (stgit-patch-at-point)))
576 (if patch
577 (list patch)
578 '()))))
579
56d81fe5 580(defun stgit-goto-patch (patch)
a53347d9 581 "Move point to the line containing PATCH."
56d81fe5
DK
582 (let ((p (point)))
583 (goto-char (point-min))
8439f657
GH
584 (if (re-search-forward (concat "^[>+-][ *]" (regexp-quote patch) " ")
585 nil t)
56d81fe5
DK
586 (progn (move-to-column goal-column)
587 t)
588 (goto-char p)
589 nil)))
590
1c2426dc 591(defun stgit-init ()
a53347d9 592 "Run stg init."
1c2426dc
DK
593 (interactive)
594 (stgit-capture-output nil
b0424080 595 (stgit-run "init"))
1f0bf00f 596 (stgit-reload))
1c2426dc 597
6df83d42 598(defun stgit-mark ()
a53347d9 599 "Mark the patch under point."
6df83d42 600 (interactive)
018fa1ac 601 (let ((patch (stgit-patch-at-point t)))
6df83d42 602 (stgit-add-mark patch)
1f0bf00f 603 (stgit-reload))
378a003d 604 (stgit-next-patch))
6df83d42 605
9b151b27 606(defun stgit-unmark-up ()
a53347d9 607 "Remove mark from the patch on the previous line."
6df83d42 608 (interactive)
378a003d 609 (stgit-previous-patch)
018fa1ac 610 (stgit-remove-mark (stgit-patch-at-point t))
9b151b27
GH
611 (stgit-reload))
612
613(defun stgit-unmark-down ()
a53347d9 614 "Remove mark from the patch on the current line."
9b151b27 615 (interactive)
018fa1ac 616 (stgit-remove-mark (stgit-patch-at-point t))
1288eda2
GH
617 (stgit-reload)
618 (stgit-next-patch))
6df83d42 619
56d81fe5 620(defun stgit-rename (name)
018fa1ac
GH
621 "Rename the patch under point to NAME."
622 (interactive (list (read-string "Patch name: " (stgit-patch-at-point t))))
623 (let ((old-name (stgit-patch-at-point t)))
56d81fe5
DK
624 (stgit-capture-output nil
625 (stgit-run "rename" old-name name))
378a003d
GH
626 (let ((old-name-sym (intern old-name))
627 (name-sym (intern name)))
628 (when (memq old-name-sym stgit-expanded-patches)
629 (setq stgit-expanded-patches
630 (cons name-sym (delq old-name-sym stgit-expanded-patches))))
631 (when (memq old-name-sym stgit-marked-patches)
632 (setq stgit-marked-patches
633 (cons name-sym (delq old-name-sym stgit-marked-patches)))))
1f0bf00f 634 (stgit-reload)
56d81fe5
DK
635 (stgit-goto-patch name)))
636
26201d96 637(defun stgit-repair ()
a53347d9 638 "Run stg repair."
26201d96
DK
639 (interactive)
640 (stgit-capture-output nil
b0424080 641 (stgit-run "repair"))
1f0bf00f 642 (stgit-reload))
26201d96 643
c4aad9a7
DK
644(defun stgit-commit ()
645 "Run stg commit."
646 (interactive)
647 (stgit-capture-output nil (stgit-run "commit"))
1f0bf00f 648 (stgit-reload))
c4aad9a7
DK
649
650(defun stgit-uncommit (arg)
651 "Run stg uncommit. Numeric arg determines number of patches to uncommit."
652 (interactive "p")
653 (stgit-capture-output nil (stgit-run "uncommit" "-n" (number-to-string arg)))
1f0bf00f 654 (stgit-reload))
c4aad9a7 655
0b661144
DK
656(defun stgit-push-next (npatches)
657 "Push the first unapplied patch.
658With numeric prefix argument, push that many patches."
659 (interactive "p")
660 (stgit-capture-output nil (stgit-run "push" "-n"
661 (number-to-string npatches)))
074a4fb0
GH
662 (stgit-reload)
663 (stgit-refresh-git-status))
56d81fe5 664
0b661144
DK
665(defun stgit-pop-next (npatches)
666 "Pop the topmost applied patch.
667With numeric prefix argument, pop that many patches."
668 (interactive "p")
669 (stgit-capture-output nil (stgit-run "pop" "-n" (number-to-string npatches)))
074a4fb0
GH
670 (stgit-reload)
671 (stgit-refresh-git-status))
56d81fe5 672
f9182fca
KH
673(defun stgit-applied-at-point ()
674 "Is the patch on the current line applied?"
675 (save-excursion
676 (beginning-of-line)
677 (looking-at "[>+]")))
678
679(defun stgit-push-or-pop ()
a53347d9 680 "Push or pop the patch on the current line."
f9182fca 681 (interactive)
018fa1ac 682 (let ((patch (stgit-patch-at-point t))
f9182fca
KH
683 (applied (stgit-applied-at-point)))
684 (stgit-capture-output nil
b0424080 685 (stgit-run (if applied "pop" "push") patch))
1f0bf00f 686 (stgit-reload)))
f9182fca 687
c7adf5ef 688(defun stgit-goto ()
a53347d9 689 "Go to the patch on the current line."
c7adf5ef 690 (interactive)
018fa1ac 691 (let ((patch (stgit-patch-at-point t)))
c7adf5ef 692 (stgit-capture-output nil
b0424080 693 (stgit-run "goto" patch))
1f0bf00f 694 (stgit-reload)))
c7adf5ef 695
378a003d
GH
696(defun stgit-id (patch)
697 "Return the git commit id for PATCH"
698 (let ((result (with-output-to-string
699 (stgit-run-silent "id" patch))))
700 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
701 (error "Cannot find commit id for %s" patch))
702 (match-string 1 result)))
703
56d81fe5 704(defun stgit-show ()
a53347d9 705 "Show the patch on the current line."
56d81fe5
DK
706 (interactive)
707 (stgit-capture-output "*StGit patch*"
378a003d
GH
708 (let ((patch (stgit-patch-at-point)))
709 (if (not patch)
1f60181a 710 (let ((patched-file (stgit-patched-file-at-point t)))
378a003d
GH
711 (unless patched-file
712 (error "No patch or file at point"))
713 (let ((id (stgit-id (symbol-name (car patched-file)))))
714 (with-output-to-temp-buffer "*StGit diff*"
1f60181a
GH
715 (if (consp (cdr patched-file))
716 ;; two files (copy or rename)
717 (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
718 (cadr patched-file) (cddr patched-file))
719 ;; just one file
720 (stgit-run-git "diff" (concat id "^") id "--"
721 (cdr patched-file)))
378a003d
GH
722 (with-current-buffer standard-output
723 (diff-mode)))))
724 (stgit-run "show" (stgit-patch-at-point))
725 (with-current-buffer standard-output
726 (goto-char (point-min))
727 (diff-mode))))))
0663524d 728
0bca35c8 729(defun stgit-edit ()
a53347d9 730 "Edit the patch on the current line."
0bca35c8 731 (interactive)
018fa1ac 732 (let ((patch (stgit-patch-at-point t))
0780be79 733 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
734 (dir default-directory))
735 (log-edit 'stgit-confirm-edit t nil edit-buf)
736 (set (make-local-variable 'stgit-edit-patch) patch)
737 (setq default-directory dir)
738 (let ((standard-output edit-buf))
9aecd505 739 (stgit-run-silent "edit" "--save-template=-" patch))))
0bca35c8
DK
740
741(defun stgit-confirm-edit ()
742 (interactive)
743 (let ((file (make-temp-file "stgit-edit-")))
744 (write-region (point-min) (point-max) file)
745 (stgit-capture-output nil
746 (stgit-run "edit" "-f" file stgit-edit-patch))
747 (with-current-buffer log-edit-parent-buffer
1f0bf00f 748 (stgit-reload))))
0bca35c8 749
aa04f831
GH
750(defun stgit-new (add-sign)
751 "Create a new patch.
752With a prefix argument, include a \"Signed-off-by:\" line at the
753end of the patch."
754 (interactive "P")
c5d45b92
GH
755 (let ((edit-buf (get-buffer-create "*StGit edit*"))
756 (dir default-directory))
757 (log-edit 'stgit-confirm-new t nil edit-buf)
aa04f831
GH
758 (setq default-directory dir)
759 (when add-sign
760 (save-excursion
761 (let ((standard-output (current-buffer)))
762 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
64c097a0
DK
763
764(defun stgit-confirm-new ()
765 (interactive)
27b0f9e4 766 (let ((file (make-temp-file "stgit-edit-")))
64c097a0
DK
767 (write-region (point-min) (point-max) file)
768 (stgit-capture-output nil
27b0f9e4 769 (stgit-run "new" "-f" file))
64c097a0 770 (with-current-buffer log-edit-parent-buffer
1f0bf00f 771 (stgit-reload))))
64c097a0
DK
772
773(defun stgit-create-patch-name (description)
774 "Create a patch name from a long description"
775 (let ((patch ""))
776 (while (> (length description) 0)
777 (cond ((string-match "\\`[a-zA-Z_-]+" description)
8439f657
GH
778 (setq patch (downcase (concat patch
779 (match-string 0 description))))
64c097a0
DK
780 (setq description (substring description (match-end 0))))
781 ((string-match "\\` +" description)
782 (setq patch (concat patch "-"))
783 (setq description (substring description (match-end 0))))
784 ((string-match "\\`[^a-zA-Z_-]+" description)
785 (setq description (substring description (match-end 0))))))
786 (cond ((= (length patch) 0)
787 "patch")
788 ((> (length patch) 20)
789 (substring patch 0 20))
790 (t patch))))
0bca35c8 791
7755d7f1 792(defun stgit-delete (patch-names)
a53347d9 793 "Delete the named patches."
7755d7f1
KH
794 (interactive (list (stgit-patches-marked-or-at-point)))
795 (if (zerop (length patch-names))
796 (error "No patches to delete")
797 (when (yes-or-no-p (format "Really delete %d patches? "
798 (length patch-names)))
799 (stgit-capture-output nil
800 (apply 'stgit-run "delete" patch-names))
1f0bf00f 801 (stgit-reload))))
7755d7f1 802
ea0def18 803(defun stgit-coalesce (patch-names)
a53347d9 804 "Run stg coalesce on the named patches."
ea0def18 805 (interactive (list (stgit-marked-patches)))
0780be79 806 (let ((edit-buf (get-buffer-create "*StGit edit*"))
ea0def18
DK
807 (dir default-directory))
808 (log-edit 'stgit-confirm-coalesce t nil edit-buf)
809 (set (make-local-variable 'stgit-patches) patch-names)
810 (setq default-directory dir)
811 (let ((standard-output edit-buf))
9aecd505 812 (apply 'stgit-run-silent "coalesce" "--save-template=-" patch-names))))
ea0def18
DK
813
814(defun stgit-confirm-coalesce ()
815 (interactive)
816 (let ((file (make-temp-file "stgit-edit-")))
817 (write-region (point-min) (point-max) file)
818 (stgit-capture-output nil
819 (apply 'stgit-run "coalesce" "-f" file stgit-patches))
820 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
821 (stgit-clear-marks)
822 ;; Go to first marked patch and stay there
823 (goto-char (point-min))
824 (re-search-forward (concat "^[>+-]\\*") nil t)
825 (move-to-column goal-column)
826 (let ((pos (point)))
1f0bf00f 827 (stgit-reload)
e6b1fdae 828 (goto-char pos)))))
ea0def18 829
0663524d
KH
830(defun stgit-help ()
831 "Display help for the StGit mode."
832 (interactive)
833 (describe-function 'stgit-mode))
3a59f3db 834
83e51dbf
DK
835(defun stgit-undo (&optional arg)
836 "Run stg undo.
837With prefix argument, run it with the --hard flag."
838 (interactive "P")
839 (stgit-capture-output nil
840 (if arg
841 (stgit-run "undo" "--hard")
842 (stgit-run "undo")))
1f0bf00f 843 (stgit-reload))
83e51dbf 844
4d73c4d8
DK
845(defun stgit-refresh (&optional arg)
846 "Run stg refresh.
a53347d9 847With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8
DK
848 (interactive "P")
849 (let ((patchargs (if arg
b0424080
GH
850 (let ((patches (stgit-patches-marked-or-at-point)))
851 (cond ((null patches)
df283a8b 852 (error "No patch to update"))
b0424080 853 ((> (length patches) 1)
df283a8b 854 (error "Too many patches selected"))
b0424080
GH
855 (t
856 (cons "-p" patches))))
857 nil)))
4d73c4d8 858 (stgit-capture-output nil
074a4fb0
GH
859 (apply 'stgit-run "refresh" patchargs))
860 (stgit-refresh-git-status))
4d73c4d8
DK
861 (stgit-reload))
862
3a59f3db 863(provide 'stgit)