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