chiark / gitweb /
t2800: Adapt to changes in git's conflict markers
[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)
e558a4ab
GH
68 "Capture StGit output and, if there was any output, show it in a window
69at the end.
70Returns nil if there was no output."
94baef5a
DK
71 (declare (debug ([&or stringp null] body))
72 (indent 1))
34afb86c
DK
73 `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
74 (stgit-dir default-directory)
75 (inhibit-read-only t))
56d81fe5 76 (with-current-buffer output-buf
34afb86c
DK
77 (erase-buffer)
78 (setq default-directory stgit-dir)
79 (setq buffer-read-only t))
56d81fe5
DK
80 (let ((standard-output output-buf))
81 ,@body)
34afb86c
DK
82 (with-current-buffer output-buf
83 (set-buffer-modified-p nil)
84 (setq buffer-read-only t)
85 (if (< (point-min) (point-max))
86 (display-buffer output-buf t)))))
56d81fe5 87
d51722b7
GH
88(defun stgit-make-run-args (args)
89 "Return a copy of ARGS with its elements converted to strings."
90 (mapcar (lambda (x)
91 ;; don't use (format "%s" ...) to limit type errors
92 (cond ((stringp x) x)
93 ((integerp x) (number-to-string x))
94 ((symbolp x) (symbol-name x))
95 (t
96 (error "Bad element in stgit-make-run-args args: %S" x))))
97 args))
98
9aecd505 99(defun stgit-run-silent (&rest args)
d51722b7 100 (setq args (stgit-make-run-args args))
56d81fe5
DK
101 (apply 'call-process "stg" nil standard-output nil args))
102
9aecd505 103(defun stgit-run (&rest args)
d51722b7 104 (setq args (stgit-make-run-args args))
9aecd505
DK
105 (let ((msgcmd (mapconcat #'identity args " ")))
106 (message "Running stg %s..." msgcmd)
107 (apply 'call-process "stg" nil standard-output nil args)
108 (message "Running stg %s...done" msgcmd)))
109
378a003d 110(defun stgit-run-git (&rest args)
d51722b7 111 (setq args (stgit-make-run-args args))
378a003d
GH
112 (let ((msgcmd (mapconcat #'identity args " ")))
113 (message "Running git %s..." msgcmd)
114 (apply 'call-process "git" nil standard-output nil args)
115 (message "Running git %s...done" msgcmd)))
116
1f60181a 117(defun stgit-run-git-silent (&rest args)
d51722b7 118 (setq args (stgit-make-run-args args))
1f60181a
GH
119 (apply 'call-process "git" nil standard-output nil args))
120
1f0bf00f 121(defun stgit-reload ()
a53347d9 122 "Update the contents of the StGit buffer."
56d81fe5
DK
123 (interactive)
124 (let ((inhibit-read-only t)
125 (curline (line-number-at-pos))
126 (curpatch (stgit-patch-at-point)))
127 (erase-buffer)
128 (insert "Branch: ")
9aecd505 129 (stgit-run-silent "branch")
bce79a6a 130 (stgit-run-silent "series" "--description" "--empty")
6df83d42 131 (stgit-rescan)
56d81fe5
DK
132 (if curpatch
133 (stgit-goto-patch curpatch)
074a4fb0
GH
134 (goto-line curline)))
135 (stgit-refresh-git-status))
56d81fe5 136
8f40753a
GH
137(defgroup stgit nil
138 "A user interface for the StGit patch maintenance tool."
139 :group 'tools)
140
07f464e0
DK
141(defface stgit-description-face
142 '((((background dark)) (:foreground "tan"))
143 (((background light)) (:foreground "dark red")))
8f40753a
GH
144 "The face used for StGit descriptions"
145 :group 'stgit)
07f464e0
DK
146
147(defface stgit-top-patch-face
148 '((((background dark)) (:weight bold :foreground "yellow"))
149 (((background light)) (:weight bold :foreground "purple"))
150 (t (:weight bold)))
8f40753a
GH
151 "The face used for the top patch names"
152 :group 'stgit)
07f464e0
DK
153
154(defface stgit-applied-patch-face
155 '((((background dark)) (:foreground "light yellow"))
156 (((background light)) (:foreground "purple"))
157 (t ()))
8f40753a
GH
158 "The face used for applied patch names"
159 :group 'stgit)
07f464e0
DK
160
161(defface stgit-unapplied-patch-face
162 '((((background dark)) (:foreground "gray80"))
163 (((background light)) (:foreground "orchid"))
164 (t ()))
8f40753a
GH
165 "The face used for unapplied patch names"
166 :group 'stgit)
07f464e0 167
1f60181a
GH
168(defface stgit-modified-file-face
169 '((((class color) (background light)) (:foreground "purple"))
170 (((class color) (background dark)) (:foreground "salmon")))
171 "StGit mode face used for modified file status"
172 :group 'stgit)
173
174(defface stgit-unmerged-file-face
175 '((((class color) (background light)) (:foreground "red" :bold t))
176 (((class color) (background dark)) (:foreground "red" :bold t)))
177 "StGit mode face used for unmerged file status"
178 :group 'stgit)
179
180(defface stgit-unknown-file-face
181 '((((class color) (background light)) (:foreground "goldenrod" :bold t))
182 (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
183 "StGit mode face used for unknown file status"
184 :group 'stgit)
185
a6d9a852
GH
186(defface stgit-file-permission-face
187 '((((class color) (background light)) (:foreground "green" :bold t))
188 (((class color) (background dark)) (:foreground "green" :bold t)))
189 "StGit mode face used for permission changes."
190 :group 'stgit)
191
1f60181a
GH
192(defcustom stgit-expand-find-copies-harder
193 nil
194 "Try harder to find copied files when listing patches.
195
196When not nil, runs git diff-tree with the --find-copies-harder
197flag, which reduces performance."
198 :type 'boolean
199 :group 'stgit)
200
201(defconst stgit-file-status-code-strings
202 (mapcar (lambda (arg)
203 (cons (car arg)
a6d9a852
GH
204 (propertize (cadr arg) 'face (car (cddr arg)))))
205 '((add "Added" stgit-modified-file-face)
206 (copy "Copied" stgit-modified-file-face)
207 (delete "Deleted" stgit-modified-file-face)
208 (modify "Modified" stgit-modified-file-face)
209 (rename "Renamed" stgit-modified-file-face)
210 (mode-change "Mode change" stgit-modified-file-face)
211 (unmerged "Unmerged" stgit-unmerged-file-face)
212 (unknown "Unknown" stgit-unknown-file-face)))
1f60181a
GH
213 "Alist of code symbols to description strings")
214
215(defun stgit-file-status-code-as-string (code)
216 "Return stgit status code as string"
a6d9a852
GH
217 (let ((str (assq (if (consp code) (car code) code)
218 stgit-file-status-code-strings)))
219 (when str
220 (format "%-11s "
221 (if (and str (consp code) (/= (cdr code) 100))
222 (format "%s %s" (cdr str)
223 (propertize (format "%d%%" (cdr code))
224 'face 'stgit-description-face))
225 (cdr str))))))
1f60181a 226
a6d9a852 227(defun stgit-file-status-code (str &optional score)
1f60181a
GH
228 "Return stgit status code from git status string"
229 (let ((code (assoc str '(("A" . add)
230 ("C" . copy)
231 ("D" . delete)
232 ("M" . modify)
233 ("R" . rename)
234 ("T" . mode-change)
235 ("U" . unmerged)
236 ("X" . unknown)))))
a6d9a852
GH
237 (setq code (if code (cdr code) 'unknown))
238 (when (stringp score)
239 (if (> (length score) 0)
240 (setq score (string-to-number score))
241 (setq score nil)))
242 (if score (cons code score) code)))
243
244(defconst stgit-file-type-strings
245 '((#o100 . "file")
246 (#o120 . "symlink")
247 (#o160 . "subproject"))
248 "Alist of names of file types")
249
250(defun stgit-file-type-string (type)
47271f41
GH
251 "Return string describing file type TYPE (the high bits of file permission).
252Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
a6d9a852
GH
253 (let ((type-str (assoc type stgit-file-type-strings)))
254 (or (and type-str (cdr type-str))
255 (format "unknown type %o" type))))
256
257(defun stgit-file-type-change-string (old-perm new-perm)
47271f41
GH
258 "Return string describing file type change from OLD-PERM to NEW-PERM.
259Cf. `stgit-file-type-string'."
a6d9a852
GH
260 (let ((old-type (lsh old-perm -9))
261 (new-type (lsh new-perm -9)))
262 (cond ((= old-type new-type) "")
263 ((zerop new-type) "")
264 ((zerop old-type)
265 (if (= new-type #o100)
266 ""
267 (format " (%s)" (stgit-file-type-string new-type))))
268 (t (format " (%s -> %s)"
269 (stgit-file-type-string old-type)
270 (stgit-file-type-string new-type))))))
271
272(defun stgit-file-mode-change-string (old-perm new-perm)
47271f41
GH
273 "Return string describing file mode change from OLD-PERM to NEW-PERM.
274Cf. `stgit-file-type-change-string'."
a6d9a852
GH
275 (setq old-perm (logand old-perm #o777)
276 new-perm (logand new-perm #o777))
277 (if (or (= old-perm new-perm)
278 (zerop old-perm)
279 (zerop new-perm))
280 ""
281 (let* ((modified (logxor old-perm new-perm))
282 (not-x-modified (logand (logxor old-perm new-perm) #o666)))
283 (cond ((zerop modified) "")
284 ((and (zerop not-x-modified)
285 (or (and (eq #o111 (logand old-perm #o111))
286 (propertize "-x" 'face 'stgit-file-permission-face))
287 (and (eq #o111 (logand new-perm #o111))
288 (propertize "+x" 'face
289 'stgit-file-permission-face)))))
290 (t (concat (propertize (format "%o" old-perm)
291 'face 'stgit-file-permission-face)
292 (propertize " -> "
293 'face 'stgit-description-face)
294 (propertize (format "%o" new-perm)
295 'face 'stgit-file-permission-face)))))))
1f60181a 296
378a003d 297(defun stgit-expand-patch (patchsym)
47271f41
GH
298 "Expand (show modification of) the patch with name PATCHSYM (a
299symbol) at point.
300`stgit-expand-find-copies-harder' controls how hard to try to
301find copied files."
378a003d
GH
302 (save-excursion
303 (forward-line)
1f60181a
GH
304 (let* ((start (point))
305 (result (with-output-to-string
306 (stgit-run-git "diff-tree" "-r" "-z"
307 (if stgit-expand-find-copies-harder
308 "--find-copies-harder"
309 "-C")
d51722b7 310 (stgit-id patchsym)))))
1f60181a 311 (let (mstart)
a6d9a852 312 (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 313 result mstart)
a6d9a852
GH
314 (let ((copy-or-rename (match-string 4 result))
315 (old-perm (read (format "#o%s" (match-string 1 result))))
316 (new-perm (read (format "#o%s" (match-string 2 result))))
1f60181a 317 (line-start (point))
a6d9a852
GH
318 status
319 change
1f60181a
GH
320 properties)
321 (insert " ")
322 (if copy-or-rename
a6d9a852
GH
323 (let ((cr-score (match-string 5 result))
324 (cr-from-file (match-string 6 result))
325 (cr-to-file (match-string 7 result)))
326 (setq status (stgit-file-status-code copy-or-rename
327 cr-score)
328 properties (list 'stgit-old-file cr-from-file
329 'stgit-new-file cr-to-file)
330 change (concat
331 cr-from-file
332 (propertize " -> "
333 'face 'stgit-description-face)
334 cr-to-file)))
335 (setq status (stgit-file-status-code (match-string 8 result))
336 properties (list 'stgit-file (match-string 9 result))
337 change (match-string 9 result)))
338
339 (let ((mode-change (stgit-file-mode-change-string old-perm
340 new-perm)))
341 (insert (format "%-12s" (stgit-file-status-code-as-string
342 status))
343 mode-change
344 (if (> (length mode-change) 0) " " "")
345 change
346 (propertize (stgit-file-type-change-string old-perm
347 new-perm)
348 'face 'stgit-description-face)
349 ?\n))
1f60181a
GH
350 (add-text-properties line-start (point) properties))
351 (setq mstart (match-end 0))))
352 (when (= start (point))
353 (insert " <no files>\n"))
d51722b7 354 (put-text-property start (point) 'stgit-file-patchsym patchsym))))
378a003d 355
acc5652f
DK
356(defun stgit-collapse-patch (patchsym)
357 "Collapse the patch with name PATCHSYM after the line at point."
358 (save-excursion
359 (forward-line)
360 (let ((start (point)))
361 (while (eq (get-text-property (point) 'stgit-file-patchsym) patchsym)
362 (forward-line))
363 (delete-region start (point)))))
364
6df83d42
DK
365(defun stgit-rescan ()
366 "Rescan the status buffer."
07f464e0 367 (save-excursion
3a1cf814
GH
368 (let ((marked ())
369 found-any)
6df83d42
DK
370 (goto-char (point-min))
371 (while (not (eobp))
372 (cond ((looking-at "Branch: \\(.*\\)")
373 (put-text-property (match-beginning 1) (match-end 1)
374 'face 'bold))
bce79a6a 375 ((looking-at "\\([0 ]\\)\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
3a1cf814 376 (setq found-any t)
bce79a6a
GH
377 (let ((empty (match-string 1))
378 (state (match-string 2))
379 (patchsym (intern (match-string 4))))
6df83d42 380 (put-text-property
bce79a6a 381 (match-beginning 4) (match-end 4) 'face
6df83d42
DK
382 (cond ((string= state ">") 'stgit-top-patch-face)
383 ((string= state "+") 'stgit-applied-patch-face)
384 ((string= state "-") 'stgit-unapplied-patch-face)))
bce79a6a 385 (put-text-property (match-beginning 5) (match-end 5)
6df83d42
DK
386 'face 'stgit-description-face)
387 (when (memq patchsym stgit-marked-patches)
bce79a6a
GH
388 (save-excursion
389 (replace-match "*" nil nil nil 3))
378a003d 390 (setq marked (cons patchsym marked)))
d51722b7
GH
391 (put-text-property (match-beginning 0) (match-end 0)
392 'stgit-patchsym patchsym)
378a003d
GH
393 (when (memq patchsym stgit-expanded-patches)
394 (stgit-expand-patch patchsym))
bce79a6a
GH
395 (when (equal "0" empty)
396 (save-excursion
397 (goto-char (match-beginning 5))
398 (insert "(empty) ")))
399 (delete-char 1)
378a003d 400 ))
ad80ce22
DK
401 ((or (looking-at "stg series: Branch \".*\" not initialised")
402 (looking-at "stg series: .*: branch not initialized"))
3a1cf814 403 (setq found-any t)
1c2426dc
DK
404 (forward-line 1)
405 (insert "Run M-x stgit-init to initialise")))
6df83d42 406 (forward-line 1))
3a1cf814
GH
407 (setq stgit-marked-patches (nreverse marked))
408 (unless found-any
409 (insert "\n "
410 (propertize "no patches in series"
411 'face 'stgit-description-face))))))
07f464e0 412
acc5652f
DK
413(defun stgit-select-file ()
414 (let ((patched-file (stgit-patched-file-at-point)))
415 (unless patched-file
416 (error "No patch or file on the current line"))
417 (let ((filename (expand-file-name (cdr patched-file))))
418 (unless (file-exists-p filename)
419 (error "File does not exist"))
420 (find-file filename))))
421
422(defun stgit-toggle-patch-file-list (curpath)
423 (let ((inhibit-read-only t))
424 (if (memq curpatch stgit-expanded-patches)
425 (save-excursion
426 (setq stgit-expanded-patches (delq curpatch stgit-expanded-patches))
427 (stgit-collapse-patch curpatch))
428 (progn
429 (setq stgit-expanded-patches (cons curpatch stgit-expanded-patches))
430 (stgit-expand-patch curpatch)))))
431
378a003d
GH
432(defun stgit-select ()
433 "Expand or collapse the current entry"
434 (interactive)
435 (let ((curpatch (stgit-patch-at-point)))
acc5652f
DK
436 (if curpatch
437 (stgit-toggle-patch-file-list curpatch)
438 (stgit-select-file))))
439
378a003d
GH
440
441(defun stgit-find-file-other-window ()
442 "Open file at point in other window"
443 (interactive)
444 (let ((patched-file (stgit-patched-file-at-point)))
445 (unless patched-file
446 (error "No file on the current line"))
447 (let ((filename (expand-file-name (cdr patched-file))))
448 (unless (file-exists-p filename)
449 (error "File does not exist"))
450 (find-file-other-window filename))))
451
83327d53 452(defun stgit-quit ()
a53347d9 453 "Hide the stgit buffer."
83327d53
GH
454 (interactive)
455 (bury-buffer))
456
0f076fe6 457(defun stgit-git-status ()
a53347d9 458 "Show status using `git-status'."
0f076fe6
GH
459 (interactive)
460 (unless (fboundp 'git-status)
df283a8b 461 (error "The stgit-git-status command requires git-status"))
0f076fe6
GH
462 (let ((dir default-directory))
463 (save-selected-window
464 (pop-to-buffer nil)
465 (git-status dir))))
466
58f72f16
GH
467(defun stgit-goal-column ()
468 "Return goal column for the current line"
469 (cond ((get-text-property (point) 'stgit-file-patchsym) 4)
470 ((get-text-property (point) 'stgit-patchsym) 2)
471 (t 0)))
472
473(defun stgit-next-line (&optional arg)
378a003d 474 "Move cursor vertically down ARG lines"
58f72f16
GH
475 (interactive "p")
476 (next-line arg)
477 (move-to-column (stgit-goal-column)))
378a003d 478
58f72f16 479(defun stgit-previous-line (&optional arg)
378a003d 480 "Move cursor vertically up ARG lines"
58f72f16
GH
481 (interactive "p")
482 (previous-line arg)
483 (move-to-column (stgit-goal-column)))
378a003d
GH
484
485(defun stgit-next-patch (&optional arg)
486 "Move cursor down ARG patches"
487 (interactive "p")
488 (unless arg
489 (setq arg 1))
490 (if (< arg 0)
491 (stgit-previous-patch (- arg))
492 (while (not (zerop arg))
493 (setq arg (1- arg))
494 (while (progn (stgit-next-line)
495 (not (stgit-patch-at-point)))))))
496
497(defun stgit-previous-patch (&optional arg)
498 "Move cursor up ARG patches"
499 (interactive "p")
500 (unless arg
501 (setq arg 1))
502 (if (< arg 0)
503 (stgit-next-patch (- arg))
504 (while (not (zerop arg))
505 (setq arg (1- arg))
506 (while (progn (stgit-previous-line)
507 (not (stgit-patch-at-point)))))))
508
56d81fe5
DK
509(defvar stgit-mode-hook nil
510 "Run after `stgit-mode' is setup.")
511
512(defvar stgit-mode-map nil
513 "Keymap for StGit major mode.")
514
515(unless stgit-mode-map
516 (setq stgit-mode-map (make-keymap))
517 (suppress-keymap stgit-mode-map)
022a3664
GH
518 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
519 '((" " . stgit-mark)
3dccdc9b 520 ("m" . stgit-mark)
9b151b27
GH
521 ("\d" . stgit-unmark-up)
522 ("u" . stgit-unmark-down)
022a3664
GH
523 ("?" . stgit-help)
524 ("h" . stgit-help)
58f72f16
GH
525 ("\C-p" . stgit-previous-line)
526 ("\C-n" . stgit-next-line)
527 ([up] . stgit-previous-line)
528 ([down] . stgit-next-line)
529 ("p" . stgit-previous-patch)
530 ("n" . stgit-next-patch)
378a003d
GH
531 ("\M-{" . stgit-previous-patch)
532 ("\M-}" . stgit-next-patch)
0f076fe6 533 ("s" . stgit-git-status)
022a3664
GH
534 ("g" . stgit-reload)
535 ("r" . stgit-refresh)
536 ("\C-c\C-r" . stgit-rename)
537 ("e" . stgit-edit)
7cc45294 538 ("M" . stgit-move-patches)
594aa463 539 ("S" . stgit-squash)
022a3664
GH
540 ("N" . stgit-new)
541 ("R" . stgit-repair)
542 ("C" . stgit-commit)
543 ("U" . stgit-uncommit)
378a003d
GH
544 ("\r" . stgit-select)
545 ("o" . stgit-find-file-other-window)
022a3664
GH
546 (">" . stgit-push-next)
547 ("<" . stgit-pop-next)
548 ("P" . stgit-push-or-pop)
549 ("G" . stgit-goto)
550 ("=" . stgit-show)
551 ("D" . stgit-delete)
552 ([(control ?/)] . stgit-undo)
83327d53 553 ("\C-_" . stgit-undo)
adeef6bc
GH
554 ("B" . stgit-branch)
555 ("q" . stgit-quit))))
56d81fe5
DK
556
557(defun stgit-mode ()
558 "Major mode for interacting with StGit.
559Commands:
560\\{stgit-mode-map}"
561 (kill-all-local-variables)
562 (buffer-disable-undo)
563 (setq mode-name "StGit"
564 major-mode 'stgit-mode
565 goal-column 2)
566 (use-local-map stgit-mode-map)
567 (set (make-local-variable 'list-buffers-directory) default-directory)
6df83d42 568 (set (make-local-variable 'stgit-marked-patches) nil)
378a003d 569 (set (make-local-variable 'stgit-expanded-patches) nil)
2870f8b8 570 (set-variable 'truncate-lines 't)
56d81fe5
DK
571 (run-hooks 'stgit-mode-hook))
572
d51722b7
GH
573(defun stgit-add-mark (patchsym)
574 "Mark the patch PATCHSYM."
980ccd21
GH
575 (setq stgit-marked-patches (cons patchsym stgit-marked-patches))
576 (save-excursion
577 (when (stgit-goto-patch patchsym)
578 (move-to-column 1)
579 (let ((inhibit-read-only t))
580 (insert-and-inherit ?*)
581 (delete-char 1)))))
6df83d42 582
d51722b7
GH
583(defun stgit-remove-mark (patchsym)
584 "Unmark the patch PATCHSYM."
980ccd21
GH
585 (setq stgit-marked-patches (delq patchsym stgit-marked-patches))
586 (save-excursion
587 (when (stgit-goto-patch patchsym)
588 (move-to-column 1)
589 (let ((inhibit-read-only t))
590 (insert-and-inherit ? )
591 (delete-char 1)))))
6df83d42 592
e6b1fdae 593(defun stgit-clear-marks ()
47271f41 594 "Unmark all patches."
e6b1fdae
DK
595 (setq stgit-marked-patches '()))
596
378a003d 597(defun stgit-patch-at-point (&optional cause-error allow-file)
d51722b7 598 "Return the patch name on the current line as a symbol.
378a003d
GH
599If CAUSE-ERROR is not nil, signal an error if none found.
600If ALLOW-FILE is not nil, also handle when point is on a file of
601a patch."
d51722b7
GH
602 (or (get-text-property (point) 'stgit-patchsym)
603 (and allow-file
604 (get-text-property (point) 'stgit-file-patchsym))
605 (when cause-error
606 (error "No patch on this line"))))
378a003d 607
1f60181a
GH
608(defun stgit-patched-file-at-point (&optional both-files)
609 "Returns a cons of the patchsym and file name at point. For
610copies and renames, return the new file if the patch is either
611applied. If BOTH-FILES is non-nil, return a cons of the old and
612the new file names instead of just one name."
d51722b7 613 (let ((patchsym (get-text-property (point) 'stgit-file-patchsym))
1f60181a
GH
614 (file (get-text-property (point) 'stgit-file)))
615 (cond ((not patchsym) nil)
616 (file (cons patchsym file))
617 (both-files
618 (cons patchsym (cons (get-text-property (point) 'stgit-old-file)
619 (get-text-property (point) 'stgit-new-file))))
620 (t
621 (let ((file-sym (save-excursion
622 (stgit-previous-patch)
d51722b7
GH
623 (unless (eq (stgit-patch-at-point)
624 patchsym)
1f60181a
GH
625 (error "Cannot find the %s patch" patchsym))
626 (beginning-of-line)
627 (if (= (char-after) ?-)
628 'stgit-old-file
629 'stgit-new-file))))
630 (cons patchsym (get-text-property (point) file-sym)))))))
56d81fe5 631
7755d7f1 632(defun stgit-patches-marked-or-at-point ()
d51722b7 633 "Return the symbols of the marked patches, or the patch on the current line."
7755d7f1 634 (if stgit-marked-patches
d51722b7 635 stgit-marked-patches
7755d7f1
KH
636 (let ((patch (stgit-patch-at-point)))
637 (if patch
638 (list patch)
639 '()))))
640
d51722b7
GH
641(defun stgit-goto-patch (patchsym)
642 "Move point to the line containing patch PATCHSYM.
643If that patch cannot be found, return nil."
644 (let ((p (text-property-any (point-min) (point-max)
645 'stgit-patchsym patchsym)))
646 (when p
56d81fe5 647 (goto-char p)
d51722b7 648 (move-to-column goal-column))))
56d81fe5 649
1c2426dc 650(defun stgit-init ()
a53347d9 651 "Run stg init."
1c2426dc
DK
652 (interactive)
653 (stgit-capture-output nil
b0424080 654 (stgit-run "init"))
1f0bf00f 655 (stgit-reload))
1c2426dc 656
6df83d42 657(defun stgit-mark ()
a53347d9 658 "Mark the patch under point."
6df83d42 659 (interactive)
018fa1ac 660 (let ((patch (stgit-patch-at-point t)))
980ccd21 661 (stgit-add-mark patch))
378a003d 662 (stgit-next-patch))
6df83d42 663
9b151b27 664(defun stgit-unmark-up ()
a53347d9 665 "Remove mark from the patch on the previous line."
6df83d42 666 (interactive)
378a003d 667 (stgit-previous-patch)
980ccd21 668 (stgit-remove-mark (stgit-patch-at-point t)))
9b151b27
GH
669
670(defun stgit-unmark-down ()
a53347d9 671 "Remove mark from the patch on the current line."
9b151b27 672 (interactive)
018fa1ac 673 (stgit-remove-mark (stgit-patch-at-point t))
1288eda2 674 (stgit-next-patch))
6df83d42 675
56d81fe5 676(defun stgit-rename (name)
018fa1ac 677 "Rename the patch under point to NAME."
d51722b7
GH
678 (interactive (list (read-string "Patch name: "
679 (symbol-name (stgit-patch-at-point t)))))
680 (let ((old-patchsym (stgit-patch-at-point t)))
56d81fe5 681 (stgit-capture-output nil
d51722b7
GH
682 (stgit-run "rename" old-patchsym name))
683 (let ((name-sym (intern name)))
684 (when (memq old-patchsym stgit-expanded-patches)
378a003d 685 (setq stgit-expanded-patches
d51722b7
GH
686 (cons name-sym (delq old-patchsym stgit-expanded-patches))))
687 (when (memq old-patchsym stgit-marked-patches)
378a003d 688 (setq stgit-marked-patches
d51722b7
GH
689 (cons name-sym (delq old-patchsym stgit-marked-patches))))
690 (stgit-reload)
691 (stgit-goto-patch name-sym))))
56d81fe5 692
26201d96 693(defun stgit-repair ()
a53347d9 694 "Run stg repair."
26201d96
DK
695 (interactive)
696 (stgit-capture-output nil
b0424080 697 (stgit-run "repair"))
1f0bf00f 698 (stgit-reload))
26201d96 699
adeef6bc
GH
700(defun stgit-available-branches ()
701 "Returns a list of the available stg branches"
702 (let ((output (with-output-to-string
703 (stgit-run "branch" "--list")))
704 (start 0)
705 result)
706 (while (string-match "^>?\\s-+s\\s-+\\(\\S-+\\)" output start)
707 (setq result (cons (match-string 1 output) result))
708 (setq start (match-end 0)))
709 result))
710
711(defun stgit-branch (branch)
712 "Switch to branch BRANCH."
713 (interactive (list (completing-read "Switch to branch: "
714 (stgit-available-branches))))
715 (stgit-capture-output nil (stgit-run "branch" "--" branch))
716 (stgit-reload))
717
41c1c59c
GH
718(defun stgit-commit (count)
719 "Run stg commit on COUNT commits.
720Interactively, the prefix argument is used as COUNT."
721 (interactive "p")
722 (stgit-capture-output nil (stgit-run "commit" "-n" count))
1f0bf00f 723 (stgit-reload))
c4aad9a7 724
41c1c59c
GH
725(defun stgit-uncommit (count)
726 "Run stg uncommit on COUNT commits.
727Interactively, the prefix argument is used as COUNT."
c4aad9a7 728 (interactive "p")
41c1c59c 729 (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
1f0bf00f 730 (stgit-reload))
c4aad9a7 731
0b661144
DK
732(defun stgit-push-next (npatches)
733 "Push the first unapplied patch.
734With numeric prefix argument, push that many patches."
735 (interactive "p")
d51722b7 736 (stgit-capture-output nil (stgit-run "push" "-n" npatches))
074a4fb0
GH
737 (stgit-reload)
738 (stgit-refresh-git-status))
56d81fe5 739
0b661144
DK
740(defun stgit-pop-next (npatches)
741 "Pop the topmost applied patch.
742With numeric prefix argument, pop that many patches."
743 (interactive "p")
d51722b7 744 (stgit-capture-output nil (stgit-run "pop" "-n" npatches))
074a4fb0
GH
745 (stgit-reload)
746 (stgit-refresh-git-status))
56d81fe5 747
f9182fca
KH
748(defun stgit-applied-at-point ()
749 "Is the patch on the current line applied?"
750 (save-excursion
751 (beginning-of-line)
752 (looking-at "[>+]")))
753
754(defun stgit-push-or-pop ()
a53347d9 755 "Push or pop the patch on the current line."
f9182fca 756 (interactive)
d51722b7 757 (let ((patchsym (stgit-patch-at-point t))
f9182fca
KH
758 (applied (stgit-applied-at-point)))
759 (stgit-capture-output nil
d51722b7 760 (stgit-run (if applied "pop" "push") patchsym))
1f0bf00f 761 (stgit-reload)))
f9182fca 762
c7adf5ef 763(defun stgit-goto ()
a53347d9 764 "Go to the patch on the current line."
c7adf5ef 765 (interactive)
d51722b7 766 (let ((patchsym (stgit-patch-at-point t)))
c7adf5ef 767 (stgit-capture-output nil
d51722b7 768 (stgit-run "goto" patchsym))
1f0bf00f 769 (stgit-reload)))
c7adf5ef 770
d51722b7
GH
771(defun stgit-id (patchsym)
772 "Return the git commit id for PATCHSYM."
378a003d 773 (let ((result (with-output-to-string
d51722b7 774 (stgit-run-silent "id" patchsym))))
378a003d 775 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
d51722b7 776 (error "Cannot find commit id for %s" patchsym))
378a003d
GH
777 (match-string 1 result)))
778
56d81fe5 779(defun stgit-show ()
a53347d9 780 "Show the patch on the current line."
56d81fe5
DK
781 (interactive)
782 (stgit-capture-output "*StGit patch*"
d51722b7
GH
783 (let ((patchsym (stgit-patch-at-point)))
784 (if (not patchsym)
1f60181a 785 (let ((patched-file (stgit-patched-file-at-point t)))
378a003d
GH
786 (unless patched-file
787 (error "No patch or file at point"))
d51722b7 788 (let ((id (stgit-id (car patched-file))))
5f1b0013
GH
789 (if (consp (cdr patched-file))
790 ;; two files (copy or rename)
791 (stgit-run-git "diff" "-C" "-C" (concat id "^") id "--"
792 (cadr patched-file) (cddr patched-file))
793 ;; just one file
794 (stgit-run-git "diff" (concat id "^") id "--"
795 (cdr patched-file)))))
68048d11 796 (stgit-run "show" "-O" "--patch-with-stat" "-O" "-M" patchsym))
5f1b0013
GH
797 (with-current-buffer standard-output
798 (goto-char (point-min))
799 (diff-mode)))))
0663524d 800
0bca35c8 801(defun stgit-edit ()
a53347d9 802 "Edit the patch on the current line."
0bca35c8 803 (interactive)
d51722b7 804 (let ((patchsym (stgit-patch-at-point t))
0780be79 805 (edit-buf (get-buffer-create "*StGit edit*"))
0bca35c8
DK
806 (dir default-directory))
807 (log-edit 'stgit-confirm-edit t nil edit-buf)
d51722b7 808 (set (make-local-variable 'stgit-edit-patchsym) patchsym)
0bca35c8
DK
809 (setq default-directory dir)
810 (let ((standard-output edit-buf))
d51722b7 811 (stgit-run-silent "edit" "--save-template=-" patchsym))))
0bca35c8
DK
812
813(defun stgit-confirm-edit ()
814 (interactive)
815 (let ((file (make-temp-file "stgit-edit-")))
816 (write-region (point-min) (point-max) file)
817 (stgit-capture-output nil
d51722b7 818 (stgit-run "edit" "-f" file stgit-edit-patchsym))
0bca35c8 819 (with-current-buffer log-edit-parent-buffer
1f0bf00f 820 (stgit-reload))))
0bca35c8 821
aa04f831
GH
822(defun stgit-new (add-sign)
823 "Create a new patch.
824With a prefix argument, include a \"Signed-off-by:\" line at the
825end of the patch."
826 (interactive "P")
c5d45b92
GH
827 (let ((edit-buf (get-buffer-create "*StGit edit*"))
828 (dir default-directory))
829 (log-edit 'stgit-confirm-new t nil edit-buf)
aa04f831
GH
830 (setq default-directory dir)
831 (when add-sign
832 (save-excursion
833 (let ((standard-output (current-buffer)))
834 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
64c097a0
DK
835
836(defun stgit-confirm-new ()
837 (interactive)
27b0f9e4 838 (let ((file (make-temp-file "stgit-edit-")))
64c097a0
DK
839 (write-region (point-min) (point-max) file)
840 (stgit-capture-output nil
27b0f9e4 841 (stgit-run "new" "-f" file))
64c097a0 842 (with-current-buffer log-edit-parent-buffer
1f0bf00f 843 (stgit-reload))))
64c097a0
DK
844
845(defun stgit-create-patch-name (description)
846 "Create a patch name from a long description"
847 (let ((patch ""))
848 (while (> (length description) 0)
849 (cond ((string-match "\\`[a-zA-Z_-]+" description)
8439f657
GH
850 (setq patch (downcase (concat patch
851 (match-string 0 description))))
64c097a0
DK
852 (setq description (substring description (match-end 0))))
853 ((string-match "\\` +" description)
854 (setq patch (concat patch "-"))
855 (setq description (substring description (match-end 0))))
856 ((string-match "\\`[^a-zA-Z_-]+" description)
857 (setq description (substring description (match-end 0))))))
858 (cond ((= (length patch) 0)
859 "patch")
860 ((> (length patch) 20)
861 (substring patch 0 20))
862 (t patch))))
0bca35c8 863
9008e45b 864(defun stgit-delete (patchsyms &optional spill-p)
d51722b7 865 "Delete the patches in PATCHSYMS.
9008e45b
GH
866Interactively, delete the marked patches, or the patch at point.
867
868With a prefix argument, or SPILL-P, spill the patch contents to
869the work tree and index."
870 (interactive (list (stgit-patches-marked-or-at-point)
871 current-prefix-arg))
e7231e4f
GH
872 (unless patchsyms
873 (error "No patches to delete"))
d51722b7 874 (let ((npatches (length patchsyms)))
9008e45b 875 (when (yes-or-no-p (format "Really delete %d patch%s%s? "
e7231e4f 876 npatches
9008e45b
GH
877 (if (= 1 npatches) "" "es")
878 (if spill-p
879 " (spilling contents to index)"
880 "")))
881 (let ((args (if spill-p
882 (cons "--spill" patchsyms)
883 patchsyms)))
884 (stgit-capture-output nil
885 (apply 'stgit-run "delete" args))
886 (stgit-reload)))))
d51722b7 887
7cc45294
GH
888(defun stgit-move-patches-target ()
889 "Return the patchsym indicating a target patch for
890`stgit-move-patches'.
891
892This is either the patch at point, or one of :top and :bottom, if
893the point is after or before the applied patches."
894
895 (let ((patchsym (stgit-patch-at-point)))
896 (cond (patchsym patchsym)
897 ((save-excursion (re-search-backward "^>" nil t)) :top)
898 (t :bottom))))
899
95369f6c
GH
900(defun stgit-sort-patches (patchsyms)
901 "Returns the list of patches in PATCHSYMS sorted according to
902their position in the patch series, bottommost first.
903
904PATCHSYMS may not contain duplicate entries."
905 (let (sorted-patchsyms
906 (series (with-output-to-string
907 (with-current-buffer standard-output
908 (stgit-run-silent "series" "--noprefix"))))
909 start)
910 (while (string-match "^\\(.+\\)" series start)
911 (let ((patchsym (intern (match-string 1 series))))
912 (when (memq patchsym patchsyms)
913 (setq sorted-patchsyms (cons patchsym sorted-patchsyms))))
914 (setq start (match-end 0)))
915 (setq sorted-patchsyms (nreverse sorted-patchsyms))
916
917 (unless (= (length patchsyms) (length sorted-patchsyms))
918 (error "Internal error"))
919
920 sorted-patchsyms))
921
7cc45294
GH
922(defun stgit-move-patches (patchsyms target-patch)
923 "Move the patches in PATCHSYMS to below TARGET-PATCH.
924If TARGET-PATCH is :bottom or :top, move the patches to the
925bottom or top of the stack, respectively.
926
927Interactively, move the marked patches to where the point is."
928 (interactive (list stgit-marked-patches
929 (stgit-move-patches-target)))
930 (unless patchsyms
931 (error "Need at least one patch to move"))
932
933 (unless target-patch
934 (error "Point not at a patch"))
935
936 (if (eq target-patch :top)
937 (stgit-capture-output nil
938 (apply 'stgit-run "float" patchsyms))
939
940 ;; need to have patchsyms sorted by position in the stack
95369f6c 941 (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))
7cc45294
GH
942 (while sorted-patchsyms
943 (setq sorted-patchsyms
944 (and (stgit-capture-output nil
945 (if (eq target-patch :bottom)
946 (stgit-run "sink" "--" (car sorted-patchsyms))
947 (stgit-run "sink" "--to" target-patch "--"
948 (car sorted-patchsyms))))
949 (cdr sorted-patchsyms))))))
950 (stgit-reload))
951
594aa463
KH
952(defun stgit-squash (patchsyms)
953 "Squash the patches in PATCHSYMS.
693d179b
GH
954Interactively, squash the marked patches.
955
956Unless there are any conflicts, the patches will be merged into
957one patch, which will occupy the same spot in the series as the
958deepest patch had before the squash."
d51722b7
GH
959 (interactive (list stgit-marked-patches))
960 (when (< (length patchsyms) 2)
594aa463 961 (error "Need at least two patches to squash"))
32d7545d
GH
962 (let ((stgit-buffer (current-buffer))
963 (edit-buf (get-buffer-create "*StGit edit*"))
693d179b
GH
964 (dir default-directory)
965 (sorted-patchsyms (stgit-sort-patches patchsyms)))
594aa463 966 (log-edit 'stgit-confirm-squash t nil edit-buf)
693d179b 967 (set (make-local-variable 'stgit-patchsyms) sorted-patchsyms)
ea0def18 968 (setq default-directory dir)
32d7545d
GH
969 (let ((result (let ((standard-output edit-buf))
970 (apply 'stgit-run-silent "squash"
971 "--save-template=-" sorted-patchsyms))))
972
973 ;; stg squash may have reordered the patches or caused conflicts
974 (with-current-buffer stgit-buffer
975 (stgit-reload))
976
977 (unless (eq 0 result)
978 (fundamental-mode)
979 (rename-buffer "*StGit error*")
980 (resize-temp-buffer-window)
981 (switch-to-buffer-other-window stgit-buffer)
982 (error "stg squash failed")))))
ea0def18 983
594aa463 984(defun stgit-confirm-squash ()
ea0def18
DK
985 (interactive)
986 (let ((file (make-temp-file "stgit-edit-")))
987 (write-region (point-min) (point-max) file)
988 (stgit-capture-output nil
594aa463 989 (apply 'stgit-run "squash" "-f" file stgit-patchsyms))
ea0def18 990 (with-current-buffer log-edit-parent-buffer
e6b1fdae
DK
991 (stgit-clear-marks)
992 ;; Go to first marked patch and stay there
993 (goto-char (point-min))
994 (re-search-forward (concat "^[>+-]\\*") nil t)
995 (move-to-column goal-column)
996 (let ((pos (point)))
1f0bf00f 997 (stgit-reload)
e6b1fdae 998 (goto-char pos)))))
ea0def18 999
0663524d
KH
1000(defun stgit-help ()
1001 "Display help for the StGit mode."
1002 (interactive)
1003 (describe-function 'stgit-mode))
3a59f3db 1004
83e51dbf
DK
1005(defun stgit-undo (&optional arg)
1006 "Run stg undo.
1007With prefix argument, run it with the --hard flag."
1008 (interactive "P")
1009 (stgit-capture-output nil
1010 (if arg
1011 (stgit-run "undo" "--hard")
1012 (stgit-run "undo")))
1f0bf00f 1013 (stgit-reload))
83e51dbf 1014
4d73c4d8
DK
1015(defun stgit-refresh (&optional arg)
1016 "Run stg refresh.
a53347d9 1017With prefix argument, refresh the marked patch or the patch under point."
4d73c4d8
DK
1018 (interactive "P")
1019 (let ((patchargs (if arg
b0424080
GH
1020 (let ((patches (stgit-patches-marked-or-at-point)))
1021 (cond ((null patches)
df283a8b 1022 (error "No patch to update"))
b0424080 1023 ((> (length patches) 1)
df283a8b 1024 (error "Too many patches selected"))
b0424080
GH
1025 (t
1026 (cons "-p" patches))))
1027 nil)))
4d73c4d8 1028 (stgit-capture-output nil
074a4fb0
GH
1029 (apply 'stgit-run "refresh" patchargs))
1030 (stgit-refresh-git-status))
4d73c4d8
DK
1031 (stgit-reload))
1032
3a59f3db 1033(provide 'stgit)