stgit.el: Handle patch and branch names starting with hyphen
[stgit] / contrib / stgit.el
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
12 (when (< emacs-major-version 22)
13 (error "Emacs older than 22 is not supported by stgit.el"))
14
15 (require 'git nil t)
16 (require 'cl)
17 (require 'ewoc)
18 (require 'easymenu)
19 (require 'format-spec)
20
21 (defun stgit-set-default (symbol value)
22 "Set default value of SYMBOL to VALUE using `set-default' and
23 reload all StGit buffers."
24 (set-default symbol value)
25 (dolist (buf (buffer-list))
26 (with-current-buffer buf
27 (when (eq major-mode 'stgit-mode)
28 (stgit-reload)))))
29
30 (defgroup stgit nil
31 "A user interface for the StGit patch maintenance tool."
32 :group 'tools
33 :link '(function-link stgit)
34 :link '(url-link "http://www.procode.org/stgit/"))
35
36 (defcustom stgit-abbreviate-copies-and-renames t
37 "If non-nil, abbreviate copies and renames as \"dir/{old -> new}/file\"
38 instead of \"dir/old/file -> dir/new/file\"."
39 :type 'boolean
40 :group 'stgit
41 :set 'stgit-set-default)
42
43 (defcustom stgit-default-show-worktree t
44 "Set to non-nil to by default show the working tree in a new stgit buffer.
45
46 Use \\<stgit-mode-map>\\[stgit-toggle-worktree] to toggle the
47 this setting in an already-started StGit buffer."
48 :type 'boolean
49 :group 'stgit
50 :link '(variable-link stgit-show-worktree))
51
52 (defcustom stgit-find-copies-harder nil
53 "Try harder to find copied files when listing patches.
54
55 When not nil, runs git diff-tree with the --find-copies-harder
56 flag, which reduces performance."
57 :type 'boolean
58 :group 'stgit
59 :set 'stgit-set-default)
60
61 (defcustom stgit-show-worktree-mode 'center
62 "This variable controls where the \"Index\" and \"Work tree\"
63 will be shown on in the buffer.
64
65 It can be set to 'top (above all patches), 'center (show between
66 applied and unapplied patches), and 'bottom (below all patches)."
67 :type '(radio (const :tag "above all patches (top)" top)
68 (const :tag "between applied and unapplied patches (center)"
69 center)
70 (const :tag "below all patches (bottom)" bottom))
71 :group 'stgit
72 :link '(variable-link stgit-show-worktree)
73 :set 'stgit-set-default)
74
75 (defcustom stgit-patch-line-format "%s%m%-30n %e%d"
76 "The format string used to format patch lines.
77 The format string is passed to `format-spec' and the following
78 format characters are recognized:
79
80 %s - A '+', '-', '>' or space, depending on whether the patch is
81 applied, unapplied, top, or something else.
82
83 %m - An asterisk if the patch is marked, and a space otherwise.
84
85 %n - The patch name.
86
87 %e - The string \"(empty) \" if the patch is empty.
88
89 %d - The short patch description.
90
91 %D - The short patch description, or the patch name.
92
93 When `stgit-show-patch-names' is non-nil, the `stgit-noname-patch-line-format'
94 variable is used instead."
95 :type 'string
96 :group 'stgit
97 :set 'stgit-set-default)
98
99 (defcustom stgit-noname-patch-line-format "%s%m%e%D"
100 "The alternate format string used to format patch lines.
101 It has the same semantics as `stgit-patch-line-format', and the
102 display can be toggled between the two formats using
103 \\<stgit-mode-map>>\\[stgit-toggle-patch-names].
104
105 The alternate form is used when the patch name is hidden."
106 :type 'string
107 :group 'stgit
108 :set 'stgit-set-default)
109
110 (defcustom stgit-default-show-patch-names t
111 "If non-nil, default to showing patch names in a new stgit buffer.
112
113 Use \\<stgit-mode-map>\\[stgit-toggle-patch-names] to toggle the
114 this setting in an already-started StGit buffer."
115 :type 'boolean
116 :group 'stgit
117 :link '(variable-link stgit-show-patch-names))
118
119 (defcustom stgit-file-line-format " %-11s %-2m %n %c"
120 "The format string used to format file lines.
121 The format string is passed to `format-spec' and the following
122 format characters are recognized:
123
124 %s - A string describing the status of the file.
125
126 %m - Mode change information
127
128 %n - The file name.
129
130 %c - A description of file changes."
131 :type 'string
132 :group 'stgit
133 :set 'stgit-set-default)
134
135 (defface stgit-branch-name-face
136 '((t :inherit bold))
137 "The face used for the StGit branch name"
138 :group 'stgit)
139
140 (defface stgit-top-patch-face
141 '((((background dark)) (:weight bold :foreground "yellow"))
142 (((background light)) (:weight bold :foreground "purple"))
143 (t (:weight bold)))
144 "The face used for the top patch names"
145 :group 'stgit)
146
147 (defface stgit-applied-patch-face
148 '((((background dark)) (:foreground "light yellow"))
149 (((background light)) (:foreground "purple"))
150 (t ()))
151 "The face used for applied patch names"
152 :group 'stgit)
153
154 (defface stgit-unapplied-patch-face
155 '((((background dark)) (:foreground "gray80"))
156 (((background light)) (:foreground "orchid"))
157 (t ()))
158 "The face used for unapplied patch names"
159 :group 'stgit)
160
161 (defface stgit-description-face
162 '((((background dark)) (:foreground "tan"))
163 (((background light)) (:foreground "dark red")))
164 "The face used for StGit descriptions"
165 :group 'stgit)
166
167 (defface stgit-index-work-tree-title-face
168 '((((supports :slant italic)) :slant italic)
169 (t :inherit bold))
170 "StGit mode face used for the \"Index\" and \"Work tree\" titles"
171 :group 'stgit)
172
173 (defface stgit-unmerged-file-face
174 '((((class color) (background light)) (:foreground "red" :bold t))
175 (((class color) (background dark)) (:foreground "red" :bold t)))
176 "StGit mode face used for unmerged file status"
177 :group 'stgit)
178
179 (defface stgit-unknown-file-face
180 '((((class color) (background light)) (:foreground "goldenrod" :bold t))
181 (((class color) (background dark)) (:foreground "goldenrod" :bold t)))
182 "StGit mode face used for unknown file status"
183 :group 'stgit)
184
185 (defface stgit-ignored-file-face
186 '((((class color) (background light)) (:foreground "grey60"))
187 (((class color) (background dark)) (:foreground "grey40")))
188 "StGit mode face used for ignored files")
189
190 (defface stgit-file-permission-face
191 '((((class color) (background light)) (:foreground "green" :bold t))
192 (((class color) (background dark)) (:foreground "green" :bold t)))
193 "StGit mode face used for permission changes."
194 :group 'stgit)
195
196 (defface stgit-modified-file-face
197 '((((class color) (background light)) (:foreground "purple"))
198 (((class color) (background dark)) (:foreground "salmon")))
199 "StGit mode face used for modified file status"
200 :group 'stgit)
201
202 (defun stgit (dir)
203 "Manage StGit patches for the tree in DIR.
204
205 See `stgit-mode' for commands available."
206 (interactive "DDirectory: \n")
207 (switch-to-stgit-buffer (git-get-top-dir dir))
208 (stgit-reload))
209
210 (defun stgit-assert-mode ()
211 "Signal an error if not in an StGit buffer."
212 (assert (derived-mode-p 'stgit-mode) nil "Not an StGit buffer"))
213
214 (unless (fboundp 'git-get-top-dir)
215 (defun git-get-top-dir (dir)
216 "Retrieve the top-level directory of a git tree."
217 (let ((cdup (with-output-to-string
218 (with-current-buffer standard-output
219 (cd dir)
220 (unless (eq 0 (call-process "git" nil t nil
221 "rev-parse" "--show-cdup"))
222 (error "Cannot find top-level git tree for %s" dir))))))
223 (expand-file-name (concat (file-name-as-directory dir)
224 (car (split-string cdup "\n")))))))
225
226 (defun stgit-refresh-git-status (&optional dir)
227 "If it exists, refresh the `git-status' buffer belonging to
228 directory DIR or `default-directory'"
229 (when (and (fboundp 'git-find-status-buffer)
230 (fboundp 'git-refresh-status))
231 (let* ((top-dir (git-get-top-dir (or dir default-directory)))
232 (git-status-buffer (and top-dir (git-find-status-buffer top-dir))))
233 (when git-status-buffer
234 (with-current-buffer git-status-buffer
235 (git-refresh-status))))))
236
237 (defun stgit-find-buffer (dir)
238 "Return the buffer displaying StGit patches for DIR, or nil if none."
239 (setq dir (file-name-as-directory dir))
240 (let ((buffers (buffer-list)))
241 (while (and buffers
242 (not (with-current-buffer (car buffers)
243 (and (eq major-mode 'stgit-mode)
244 (string= default-directory dir)))))
245 (setq buffers (cdr buffers)))
246 (and buffers (car buffers))))
247
248 (defun switch-to-stgit-buffer (dir)
249 "Switch to a (possibly new) buffer displaying StGit patches for DIR."
250 (setq dir (file-name-as-directory dir))
251 (let ((buffer (stgit-find-buffer dir)))
252 (switch-to-buffer (or buffer
253 (create-stgit-buffer dir)))))
254
255 (defstruct (stgit-patch
256 (:conc-name stgit-patch->))
257 status name desc empty files-ewoc)
258
259 (defun stgit-patch-display-name (patch)
260 (let ((name (stgit-patch->name patch)))
261 (case name
262 (:index "Index")
263 (:work "Work Tree")
264 (t (symbol-name name)))))
265
266 (defun stgit-insert-without-trailing-whitespace (text)
267 "Insert TEXT in buffer using `insert', without trailing whitespace.
268 A newline is appended."
269 (unless (string-match "\\(.*?\\) *$" text)
270 (error))
271 (insert (match-string 1 text) ?\n))
272
273 (defun stgit-patch-pp (patch)
274 (let* ((status (stgit-patch->status patch))
275 (start (point))
276 (name (stgit-patch->name patch))
277 (face (cdr (assq status stgit-patch-status-face-alist)))
278 (fmt (if stgit-show-patch-names
279 stgit-patch-line-format
280 stgit-noname-patch-line-format))
281 (spec (format-spec-make
282 ?s (case status
283 ('applied "+")
284 ('top ">")
285 ('unapplied "-")
286 (t " "))
287 ?m (if (memq name stgit-marked-patches)
288 "*" " ")
289 ?n (propertize (stgit-patch-display-name patch)
290 'face face
291 'syntax-table (string-to-syntax "w"))
292 ?e (if (stgit-patch->empty patch) "(empty) " "")
293 ?d (propertize (or (stgit-patch->desc patch) "")
294 'face 'stgit-description-face)
295 ?D (propertize (let ((desc (stgit-patch->desc patch)))
296 (if (zerop (length desc))
297 (stgit-patch-display-name patch)
298 desc))
299 'face face)))
300 (text (format-spec fmt spec)))
301
302 (stgit-insert-without-trailing-whitespace text)
303 (put-text-property start (point) 'entry-type 'patch)
304 (when (memq name stgit-expanded-patches)
305 (stgit-insert-patch-files patch))
306 (put-text-property start (point) 'patch-data patch)))
307
308 (defun create-stgit-buffer (dir)
309 "Create a buffer for showing StGit patches.
310 Argument DIR is the repository path."
311 (let ((buf (create-file-buffer (concat dir "*stgit*")))
312 (inhibit-read-only t))
313 (with-current-buffer buf
314 (setq default-directory dir)
315 (stgit-mode)
316 (set (make-local-variable 'stgit-ewoc)
317 (ewoc-create #'stgit-patch-pp "Branch:\n\n" "--\n" t))
318 (setq buffer-read-only t))
319 buf))
320
321 (def-edebug-spec stgit-capture-output
322 (form body))
323 (defmacro stgit-capture-output (name &rest body)
324 "Capture StGit output and, if there was any output, show it in a window
325 at the end.
326 Returns nil if there was no output."
327 (declare (debug ([&or stringp null] body))
328 (indent 1))
329 `(let ((output-buf (get-buffer-create ,(or name "*StGit output*")))
330 (stgit-dir default-directory)
331 (inhibit-read-only t))
332 (with-current-buffer output-buf
333 (buffer-disable-undo)
334 (erase-buffer)
335 (setq default-directory stgit-dir)
336 (setq buffer-read-only t))
337 (let ((standard-output output-buf))
338 ,@body)
339 (with-current-buffer output-buf
340 (set-buffer-modified-p nil)
341 (setq buffer-read-only t)
342 (if (< (point-min) (point-max))
343 (display-buffer output-buf t)))))
344
345 (defun stgit-make-run-args (args)
346 "Return a copy of ARGS with its elements converted to strings."
347 (mapcar (lambda (x)
348 ;; don't use (format "%s" ...) to limit type errors
349 (cond ((stringp x) x)
350 ((integerp x) (number-to-string x))
351 ((symbolp x) (symbol-name x))
352 (t
353 (error "Bad element in stgit-make-run-args args: %S" x))))
354 args))
355
356 (defun stgit-run-silent (&rest args)
357 (setq args (stgit-make-run-args args))
358 (apply 'call-process "stg" nil standard-output nil args))
359
360 (defun stgit-run (&rest args)
361 (setq args (stgit-make-run-args args))
362 (let ((msgcmd (mapconcat #'identity args " ")))
363 (message "Running stg %s..." msgcmd)
364 (apply 'call-process "stg" nil standard-output nil args)
365 (message "Running stg %s...done" msgcmd)))
366
367 (defun stgit-run-git (&rest args)
368 (setq args (stgit-make-run-args args))
369 (let ((msgcmd (mapconcat #'identity args " ")))
370 (message "Running git %s..." msgcmd)
371 (apply 'call-process "git" nil standard-output nil args)
372 (message "Running git %s...done" msgcmd)))
373
374 (defun stgit-run-git-silent (&rest args)
375 (setq args (stgit-make-run-args args))
376 (apply 'call-process "git" nil standard-output nil args))
377
378 (defun stgit-index-empty-p ()
379 "Returns non-nil if the index contains no changes from HEAD."
380 (zerop (stgit-run-git-silent "diff-index" "--cached" "--quiet" "HEAD")))
381
382 (defun stgit-work-tree-empty-p ()
383 "Returns non-nil if the work tree contains no changes from index."
384 (zerop (stgit-run-git-silent "diff-files" "--quiet")))
385
386 (defvar stgit-index-node)
387 (defvar stgit-worktree-node)
388
389 (defvar stgit-did-advise nil
390 "Set to non-nil if appropriate (non-stgit) git functions have
391 been advised to update the stgit status when necessary.")
392
393 (defconst stgit-allowed-branch-name-re
394 ;; Disallow control characters, space, del, and "/:@^{}~" in
395 ;; "/"-separated parts; parts may not start with a period (.)
396 "^[^\0- ./:@^{}~\177][^\0- /:@^{}~\177]*\
397 \\(/[^\0- ./:@^{}~\177][^\0- /:@^{}~\177]*\\)*$"
398 "Regular expression that (new) branch names must match.")
399
400 (defun stgit-refresh-index ()
401 (when stgit-index-node
402 (ewoc-invalidate (car stgit-index-node) (cdr stgit-index-node))))
403
404 (defun stgit-refresh-worktree ()
405 (when stgit-worktree-node
406 (ewoc-invalidate (car stgit-worktree-node) (cdr stgit-worktree-node))))
407
408 (defun stgit-run-series-insert-index (ewoc)
409 (setq index-node (cons ewoc (ewoc-enter-last ewoc
410 (make-stgit-patch
411 :status 'index
412 :name :index
413 :desc nil
414 :empty nil)))
415 worktree-node (cons ewoc (ewoc-enter-last ewoc
416 (make-stgit-patch
417 :status 'work
418 :name :work
419 :desc nil
420 :empty nil)))))
421
422 (defun stgit-run-series (ewoc)
423 (setq stgit-index-node nil
424 stgit-worktree-node nil)
425 (let ((inserted-index (not stgit-show-worktree))
426 index-node
427 worktree-node
428 all-patchsyms)
429 (with-temp-buffer
430 (let* ((standard-output (current-buffer))
431 (exit-status (stgit-run-silent "series"
432 "--description" "--empty")))
433 (goto-char (point-min))
434 (if (not (zerop exit-status))
435 (cond ((looking-at "stg series: \\(.*\\)")
436 (setq inserted-index t)
437 (ewoc-set-hf ewoc (car (ewoc-get-hf ewoc))
438 (substitute-command-keys
439 "-- not initialized; run \\[stgit-init]")))
440 ((looking-at ".*")
441 (error "Error running stg: %s"
442 (match-string 0))))
443 (while (not (eobp))
444 (unless (looking-at
445 "\\([0 ]\\)\\([>+-]\\)\\( \\)\\([^ ]+\\) *[|#] \\(.*\\)")
446 (error "Syntax error in output from stg series"))
447 (let* ((state-str (match-string 2))
448 (state (cond ((string= state-str ">") 'top)
449 ((string= state-str "+") 'applied)
450 ((string= state-str "-") 'unapplied)))
451 (name (intern (match-string 4)))
452 (desc (match-string 5))
453 (empty (string= (match-string 1) "0")))
454 (unless inserted-index
455 (when (or (eq stgit-show-worktree-mode 'top)
456 (and (eq stgit-show-worktree-mode 'center)
457 (eq state 'unapplied)))
458 (setq inserted-index t)
459 (stgit-run-series-insert-index ewoc)))
460 (setq all-patchsyms (cons name all-patchsyms))
461 (ewoc-enter-last ewoc
462 (make-stgit-patch
463 :status state
464 :name name
465 :desc desc
466 :empty empty)))
467 (forward-line 1))))
468 (unless inserted-index
469 (stgit-run-series-insert-index ewoc)))
470 (setq stgit-index-node index-node
471 stgit-worktree-node worktree-node
472 stgit-marked-patches (intersection stgit-marked-patches
473 all-patchsyms))))
474
475 (defun stgit-current-branch ()
476 "Return the name of the current branch."
477 (substring (with-output-to-string
478 (stgit-run-silent "branch"))
479 0 -1))
480
481 (defun stgit-reload ()
482 "Update the contents of the StGit buffer."
483 (interactive)
484 (stgit-assert-mode)
485 (let ((inhibit-read-only t)
486 (curline (line-number-at-pos))
487 (curpatch (stgit-patch-name-at-point))
488 (curfile (stgit-patched-file-at-point)))
489 (ewoc-filter stgit-ewoc #'(lambda (x) nil))
490 (ewoc-set-hf stgit-ewoc
491 (concat "Branch: "
492 (propertize (stgit-current-branch)
493 'face 'stgit-branch-name-face)
494 "\n\n")
495 (if stgit-show-worktree
496 "--"
497 (propertize
498 (substitute-command-keys "--\n\"\\[stgit-toggle-worktree]\"\
499 shows the working tree\n")
500 'face 'stgit-description-face)))
501 (stgit-run-series stgit-ewoc)
502 (if curpatch
503 (stgit-goto-patch curpatch (and curfile (stgit-file->file curfile)))
504 (goto-line curline)))
505 (stgit-refresh-git-status))
506
507 (defconst stgit-file-status-code-strings
508 (mapcar (lambda (arg)
509 (cons (car arg)
510 (propertize (cadr arg) 'face (car (cddr arg)))))
511 '((add "Added" stgit-modified-file-face)
512 (copy "Copied" stgit-modified-file-face)
513 (delete "Deleted" stgit-modified-file-face)
514 (modify "Modified" stgit-modified-file-face)
515 (rename "Renamed" stgit-modified-file-face)
516 (mode-change "Mode change" stgit-modified-file-face)
517 (unmerged "Unmerged" stgit-unmerged-file-face)
518 (unknown "Unknown" stgit-unknown-file-face)
519 (ignore "Ignored" stgit-ignored-file-face)))
520 "Alist of code symbols to description strings")
521
522 (defconst stgit-patch-status-face-alist
523 '((applied . stgit-applied-patch-face)
524 (top . stgit-top-patch-face)
525 (unapplied . stgit-unapplied-patch-face)
526 (index . stgit-index-work-tree-title-face)
527 (work . stgit-index-work-tree-title-face))
528 "Alist of face to use for a given patch status")
529
530 (defun stgit-file-status-code-as-string (file)
531 "Return stgit status code for FILE as a string"
532 (let* ((code (assq (stgit-file->status file)
533 stgit-file-status-code-strings))
534 (score (stgit-file->cr-score file)))
535 (when code
536 (if (and score (/= score 100))
537 (format "%s %s" (cdr code)
538 (propertize (format "%d%%" score)
539 'face 'stgit-description-face))
540 (cdr code)))))
541
542 (defun stgit-file-status-code (str &optional score)
543 "Return stgit status code from git status string"
544 (let ((code (assoc str '(("A" . add)
545 ("C" . copy)
546 ("D" . delete)
547 ("I" . ignore)
548 ("M" . modify)
549 ("R" . rename)
550 ("T" . mode-change)
551 ("U" . unmerged)
552 ("X" . unknown)))))
553 (setq code (if code (cdr code) 'unknown))
554 (when (stringp score)
555 (if (> (length score) 0)
556 (setq score (string-to-number score))
557 (setq score nil)))
558 (if score (cons code score) code)))
559
560 (defconst stgit-file-type-strings
561 '((#o100 . "file")
562 (#o120 . "symlink")
563 (#o160 . "subproject"))
564 "Alist of names of file types")
565
566 (defun stgit-file-type-string (type)
567 "Return string describing file type TYPE (the high bits of file permission).
568 Cf. `stgit-file-type-strings' and `stgit-file-type-change-string'."
569 (let ((type-str (assoc type stgit-file-type-strings)))
570 (or (and type-str (cdr type-str))
571 (format "unknown type %o" type))))
572
573 (defun stgit-file-type-change-string (old-perm new-perm)
574 "Return string describing file type change from OLD-PERM to NEW-PERM.
575 Cf. `stgit-file-type-string'."
576 (let ((old-type (lsh old-perm -9))
577 (new-type (lsh new-perm -9)))
578 (cond ((= old-type new-type) "")
579 ((zerop new-type) "")
580 ((zerop old-type)
581 (if (= new-type #o100)
582 ""
583 (format "(%s)" (stgit-file-type-string new-type))))
584 (t (format "(%s -> %s)"
585 (stgit-file-type-string old-type)
586 (stgit-file-type-string new-type))))))
587
588 (defun stgit-file-mode-change-string (old-perm new-perm)
589 "Return string describing file mode change from OLD-PERM to NEW-PERM.
590 Cf. `stgit-file-type-change-string'."
591 (setq old-perm (logand old-perm #o777)
592 new-perm (logand new-perm #o777))
593 (if (or (= old-perm new-perm)
594 (zerop old-perm)
595 (zerop new-perm))
596 ""
597 (let* ((modified (logxor old-perm new-perm))
598 (not-x-modified (logand (logxor old-perm new-perm) #o666)))
599 (cond ((zerop modified) "")
600 ((and (zerop not-x-modified)
601 (or (and (eq #o111 (logand old-perm #o111))
602 (propertize "-x" 'face 'stgit-file-permission-face))
603 (and (eq #o111 (logand new-perm #o111))
604 (propertize "+x" 'face
605 'stgit-file-permission-face)))))
606 (t (concat (propertize (format "%o" old-perm)
607 'face 'stgit-file-permission-face)
608 (propertize " -> "
609 'face 'stgit-description-face)
610 (propertize (format "%o" new-perm)
611 'face 'stgit-file-permission-face)))))))
612
613 (defstruct (stgit-file
614 (:conc-name stgit-file->))
615 old-perm new-perm copy-or-rename cr-score cr-from cr-to status file)
616
617 (defun stgit-describe-copy-or-rename (file)
618 (let ((arrow (concat " " (propertize "->" 'face 'stgit-description-face) " "))
619 from to common-head common-tail)
620
621 (when stgit-abbreviate-copies-and-renames
622 (setq from (split-string (stgit-file->cr-from file) "/")
623 to (split-string (stgit-file->cr-to file) "/"))
624
625 (while (and from to (cdr from) (cdr to)
626 (string-equal (car from) (car to)))
627 (setq common-head (cons (car from) common-head)
628 from (cdr from)
629 to (cdr to)))
630 (setq common-head (nreverse common-head)
631 from (nreverse from)
632 to (nreverse to))
633 (while (and from to (cdr from) (cdr to)
634 (string-equal (car from) (car to)))
635 (setq common-tail (cons (car from) common-tail)
636 from (cdr from)
637 to (cdr to)))
638 (setq from (nreverse from)
639 to (nreverse to)))
640
641 (if (or common-head common-tail)
642 (concat (if common-head
643 (mapconcat #'identity common-head "/")
644 "")
645 (if common-head "/" "")
646 (propertize "{" 'face 'stgit-description-face)
647 (mapconcat #'identity from "/")
648 arrow
649 (mapconcat #'identity to "/")
650 (propertize "}" 'face 'stgit-description-face)
651 (if common-tail "/" "")
652 (if common-tail
653 (mapconcat #'identity common-tail "/")
654 ""))
655 (concat (stgit-file->cr-from file) arrow (stgit-file->cr-to file)))))
656
657 (defun stgit-file-pp (file)
658 (let ((start (point))
659 (spec (format-spec-make
660 ?s (stgit-file-status-code-as-string file)
661 ?m (stgit-file-mode-change-string
662 (stgit-file->old-perm file)
663 (stgit-file->new-perm file))
664 ?n (if (stgit-file->copy-or-rename file)
665 (stgit-describe-copy-or-rename file)
666 (stgit-file->file file))
667 ?c (propertize (stgit-file-type-change-string
668 (stgit-file->old-perm file)
669 (stgit-file->new-perm file))
670 'face 'stgit-description-face))))
671 (stgit-insert-without-trailing-whitespace
672 (format-spec stgit-file-line-format spec))
673 (add-text-properties start (point)
674 (list 'entry-type 'file
675 'file-data file))))
676
677 (defun stgit-find-copies-harder-diff-arg ()
678 "Return the flag to use with `git-diff' depending on the
679 `stgit-find-copies-harder' flag."
680 (if stgit-find-copies-harder "--find-copies-harder" "-C"))
681
682 (defun stgit-insert-ls-files (args file-flag)
683 (let ((start (point)))
684 (apply 'stgit-run-git
685 (append '("ls-files" "--exclude-standard" "-z") args))
686 (goto-char start)
687 (while (looking-at "\\([^\0]*\\)\0")
688 (let ((name-len (- (match-end 0) (match-beginning 0))))
689 (insert ":0 0 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 " file-flag "\0")
690 (forward-char name-len)))))
691
692 (defun stgit-process-files (callback)
693 (goto-char (point-min))
694 (when (looking-at "[0-9A-Fa-f]\\{40\\}\0")
695 (goto-char (match-end 0)))
696 (while (looking-at ":\\([0-7]+\\) \\([0-7]+\\) [0-9A-Fa-f]\\{40\\} [0-9A-Fa-f]\\{40\\} ")
697 (let ((old-perm (string-to-number (match-string 1) 8))
698 (new-perm (string-to-number (match-string 2) 8)))
699 (goto-char (match-end 0))
700 (let ((file
701 (cond ((looking-at
702 "\\([CR]\\)\\([0-9]*\\)\0\\([^\0]*\\)\0\\([^\0]*\\)\0")
703 (let* ((patch-status (stgit-patch->status patch))
704 (file-subexp (if (eq patch-status 'unapplied)
705 3
706 4))
707 (file (match-string file-subexp)))
708 (make-stgit-file
709 :old-perm old-perm
710 :new-perm new-perm
711 :copy-or-rename t
712 :cr-score (string-to-number (match-string 2))
713 :cr-from (match-string 3)
714 :cr-to (match-string 4)
715 :status (stgit-file-status-code
716 (match-string 1))
717 :file file)))
718 ((looking-at "\\([ABD-QS-Z]\\)\0\\([^\0]*\\)\0")
719 (make-stgit-file
720 :old-perm old-perm
721 :new-perm new-perm
722 :copy-or-rename nil
723 :cr-score nil
724 :cr-from nil
725 :cr-to nil
726 :status (stgit-file-status-code
727 (match-string 1))
728 :file (match-string 2))))))
729 (goto-char (match-end 0))
730 (funcall callback file)))))
731
732
733 (defun stgit-insert-patch-files (patch)
734 "Expand (show modification of) the patch PATCH after the line
735 at point."
736 (let* ((patchsym (stgit-patch->name patch))
737 (end (point-marker))
738 (args (list "-z" (stgit-find-copies-harder-diff-arg)))
739 (ewoc (ewoc-create #'stgit-file-pp nil nil t)))
740 (set-marker-insertion-type end t)
741 (setf (stgit-patch->files-ewoc patch) ewoc)
742 (with-temp-buffer
743 (let ((standard-output (current-buffer)))
744 (apply 'stgit-run-git
745 (cond ((eq patchsym :work)
746 (let (standard-output)
747 (stgit-run-git "update-index" "--refresh"))
748 `("diff-files" "-0" ,@args))
749 ((eq patchsym :index)
750 `("diff-index" ,@args "--cached" "HEAD"))
751 (t
752 `("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
753
754 (when (and (eq patchsym :work))
755 (when stgit-show-ignored
756 (stgit-insert-ls-files '("--ignored" "--others") "I"))
757 (when stgit-show-unknown
758 (stgit-insert-ls-files '("--directory" "--no-empty-directory"
759 "--others")
760 "X"))
761 (sort-regexp-fields nil ":[^\0]*\0\\([^\0]*\\)\0" "\\1"
762 (point-min) (point-max)))
763
764 (stgit-process-files (lambda (file) (ewoc-enter-last ewoc file)))
765
766 (unless (ewoc-nth ewoc 0)
767 (ewoc-set-hf ewoc ""
768 (concat " "
769 (propertize "<no files>"
770 'face 'stgit-description-face)
771 "\n")))))
772 (goto-char end)))
773
774 (defun stgit-find-file (&optional other-window)
775 (let* ((file (or (stgit-patched-file-at-point)
776 (error "No file at point")))
777 (filename (expand-file-name (stgit-file->file file))))
778 (unless (file-exists-p filename)
779 (error "File does not exist"))
780 (funcall (if other-window 'find-file-other-window 'find-file)
781 filename)
782 (when (eq (stgit-file->status file) 'unmerged)
783 (smerge-mode 1))))
784
785 (defun stgit-expand (&optional patches collapse)
786 "Show the contents of marked patches, or the patch at point.
787
788 See also `stgit-collapse'.
789
790 Non-interactively, operate on PATCHES, and collapse instead of
791 expand if COLLAPSE is not nil."
792 (interactive (list (stgit-patches-marked-or-at-point t)))
793 (stgit-assert-mode)
794 (let ((patches-diff (funcall (if collapse #'intersection #'set-difference)
795 patches stgit-expanded-patches)))
796 (setq stgit-expanded-patches
797 (if collapse
798 (set-difference stgit-expanded-patches patches-diff)
799 (append stgit-expanded-patches patches-diff)))
800 (ewoc-map #'(lambda (patch)
801 (memq (stgit-patch->name patch) patches-diff))
802 stgit-ewoc))
803 (move-to-column (stgit-goal-column)))
804
805 (defun stgit-collapse (&optional patches)
806 "Hide the contents of marked patches, or the patch at point.
807
808 See also `stgit-expand'."
809 (interactive (list (stgit-patches-marked-or-at-point t)))
810 (stgit-assert-mode)
811 (stgit-expand patches t))
812
813 (defun stgit-select-patch ()
814 (let ((patchname (stgit-patch-name-at-point)))
815 (stgit-expand (list patchname)
816 (memq patchname stgit-expanded-patches))))
817
818 (defun stgit-expand-directory (file)
819 (let* ((patch (stgit-patch-at-point))
820 (ewoc (stgit-patch->files-ewoc patch))
821 (node (ewoc-locate ewoc))
822 (filename (stgit-file->file file))
823 (start (make-marker))
824 (end (make-marker)))
825
826 (save-excursion
827 (forward-line 1)
828 (set-marker start (point))
829 (set-marker end (point))
830 (set-marker-insertion-type end t))
831
832 (assert (string-match "/$" filename))
833 ;; remove trailing "/"
834 (setf (stgit-file->file file) (substring filename 0 -1))
835 (ewoc-invalidate ewoc node)
836
837 (with-temp-buffer
838 (let ((standard-output (current-buffer)))
839 (stgit-insert-ls-files (list "--directory" "--others"
840 "--no-empty-directory" "--"
841 filename)
842 "X")
843 (stgit-process-files (lambda (f)
844 (setq node (ewoc-enter-after ewoc node f))))))
845
846 (let ((inhibit-read-only t))
847 (put-text-property start end 'patch-data patch))))
848
849 (defun stgit-select-file ()
850 (let* ((file (or (stgit-patched-file-at-point)
851 (error "No file at point")))
852 (filename (stgit-file->file file)))
853 (if (string-match "/$" filename)
854 (stgit-expand-directory file)
855 (stgit-find-file))))
856
857 (defun stgit-select ()
858 "With point on a patch, toggle showing files in the patch.
859
860 With point on a file, open the associated file. Opens the target
861 file for (applied) copies and renames."
862 (interactive)
863 (stgit-assert-mode)
864 (case (get-text-property (point) 'entry-type)
865 ('patch
866 (stgit-select-patch))
867 ('file
868 (stgit-select-file))
869 (t
870 (error "No patch or file on line"))))
871
872 (defun stgit-find-file-other-window ()
873 "Open file at point in other window"
874 (interactive)
875 (stgit-assert-mode)
876 (stgit-find-file t))
877
878 (defun stgit-find-file-merge ()
879 "Open file at point and merge it using `smerge-ediff'."
880 (interactive)
881 (stgit-assert-mode)
882 (stgit-find-file t)
883 (smerge-ediff))
884
885 (defun stgit-quit ()
886 "Hide the stgit buffer."
887 (interactive)
888 (stgit-assert-mode)
889 (bury-buffer))
890
891 (defun stgit-git-status ()
892 "Show status using `git-status'."
893 (interactive)
894 (stgit-assert-mode)
895 (unless (fboundp 'git-status)
896 (error "The stgit-git-status command requires git-status"))
897 (let ((dir default-directory))
898 (save-selected-window
899 (pop-to-buffer nil)
900 (git-status dir))))
901
902 (defun stgit-goal-column ()
903 "Return goal column for the current line"
904 (case (get-text-property (point) 'entry-type)
905 ('patch 2)
906 ('file 4)
907 (t 0)))
908
909 (defun stgit-next-line (&optional arg)
910 "Move cursor vertically down ARG lines"
911 (interactive "p")
912 (stgit-assert-mode)
913 (next-line arg)
914 (move-to-column (stgit-goal-column)))
915
916 (defun stgit-previous-line (&optional arg)
917 "Move cursor vertically up ARG lines"
918 (interactive "p")
919 (stgit-assert-mode)
920 (previous-line arg)
921 (move-to-column (stgit-goal-column)))
922
923 (defun stgit-next-patch (&optional arg)
924 "Move cursor down ARG patches."
925 (interactive "p")
926 (stgit-assert-mode)
927 (ewoc-goto-next stgit-ewoc (or arg 1))
928 (move-to-column goal-column))
929
930 (defun stgit-previous-patch (&optional arg)
931 "Move cursor up ARG patches."
932 (interactive "p")
933 (stgit-assert-mode)
934 (ewoc-goto-prev stgit-ewoc (or arg 1))
935 (move-to-column goal-column))
936
937 (defvar stgit-mode-hook nil
938 "Run after `stgit-mode' is setup.")
939
940 (defvar stgit-mode-map nil
941 "Keymap for StGit major mode.")
942
943 (unless stgit-mode-map
944 (let ((diff-map (make-sparse-keymap))
945 (toggle-map (make-sparse-keymap)))
946 (suppress-keymap diff-map)
947 (mapc (lambda (arg) (define-key diff-map (car arg) (cdr arg)))
948 '(("b" . stgit-diff-base)
949 ("c" . stgit-diff-combined)
950 ("m" . stgit-find-file-merge)
951 ("o" . stgit-diff-ours)
952 ("r" . stgit-diff-range)
953 ("t" . stgit-diff-theirs)))
954 (suppress-keymap toggle-map)
955 (mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
956 '(("n" . stgit-toggle-patch-names)
957 ("t" . stgit-toggle-worktree)
958 ("i" . stgit-toggle-ignored)
959 ("u" . stgit-toggle-unknown)))
960 (setq stgit-mode-map (make-keymap))
961 (suppress-keymap stgit-mode-map)
962 (mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
963 `((" " . stgit-mark-down)
964 ("m" . stgit-mark-down)
965 ("\d" . stgit-unmark-up)
966 ("u" . stgit-unmark-down)
967 ("?" . stgit-help)
968 ("h" . stgit-help)
969 ("\C-p" . stgit-previous-line)
970 ("\C-n" . stgit-next-line)
971 ([up] . stgit-previous-line)
972 ([down] . stgit-next-line)
973 ("p" . stgit-previous-patch)
974 ("n" . stgit-next-patch)
975 ("\M-{" . stgit-previous-patch)
976 ("\M-}" . stgit-next-patch)
977 ("s" . stgit-git-status)
978 ("g" . stgit-reload-or-repair)
979 ("r" . stgit-refresh)
980 ("\C-c\C-r" . stgit-rename)
981 ("e" . stgit-edit)
982 ("M" . stgit-move-patches)
983 ("S" . stgit-squash)
984 ("N" . stgit-new)
985 ("c" . stgit-new-and-refresh)
986 ("\C-c\C-c" . stgit-commit)
987 ("\C-c\C-u" . stgit-uncommit)
988 ("U" . stgit-revert)
989 ("R" . stgit-resolve-file)
990 ("\r" . stgit-select)
991 ("+" . stgit-expand)
992 ("-" . stgit-collapse)
993 ("o" . stgit-find-file-other-window)
994 ("i" . stgit-toggle-index)
995 (">" . stgit-push-next)
996 ("<" . stgit-pop-next)
997 ("P" . stgit-push-or-pop)
998 ("G" . stgit-goto)
999 ("=" . stgit-diff)
1000 ("D" . stgit-delete)
1001 ([?\C-/] . stgit-undo)
1002 ("\C-_" . stgit-undo)
1003 ([?\C-c ?\C-/] . stgit-redo)
1004 ("\C-c\C-_" . stgit-redo)
1005 ("B" . stgit-branch)
1006 ("\C-c\C-b" . stgit-rebase)
1007 ("t" . ,toggle-map)
1008 ("d" . ,diff-map)
1009 ("q" . stgit-quit))))
1010
1011 (let ((at-unmerged-file '(let ((file (stgit-patched-file-at-point)))
1012 (and file (eq (stgit-file->status file)
1013 'unmerged))))
1014 (patch-collapsed-p '(lambda (p) (not (memq p stgit-expanded-patches)))))
1015 (easy-menu-define stgit-menu stgit-mode-map
1016 "StGit Menu"
1017 `("StGit"
1018 ["Reload" stgit-reload-or-repair
1019 :help "Reload StGit status from disk"]
1020 ["Repair" stgit-repair
1021 :keys "\\[universal-argument] \\[stgit-reload-or-repair]"
1022 :help "Repair StGit metadata"]
1023 "-"
1024 ["Undo" stgit-undo t]
1025 ["Redo" stgit-redo t]
1026 "-"
1027 ["Git status" stgit-git-status :active (fboundp 'git-status)]
1028 "-"
1029 ["New patch" stgit-new-and-refresh
1030 :help "Create a new patch from changes in index or work tree"
1031 :active (not (and (stgit-index-empty-p) (stgit-work-tree-empty-p)))]
1032 ["New empty patch" stgit-new
1033 :help "Create a new, empty patch"]
1034 ["(Un)mark patch" stgit-toggle-mark
1035 :label (if (memq (stgit-patch-name-at-point nil t)
1036 stgit-marked-patches)
1037 "Unmark patch" "Mark patch")
1038 :active (stgit-patch-name-at-point nil t)]
1039 ["Expand/collapse patch"
1040 (let ((patches (stgit-patches-marked-or-at-point)))
1041 (if (member-if ,patch-collapsed-p patches)
1042 (stgit-expand patches)
1043 (stgit-collapse patches)))
1044 :label (if (member-if ,patch-collapsed-p
1045 (stgit-patches-marked-or-at-point))
1046 "Expand patches"
1047 "Collapse patches")
1048 :active (stgit-patches-marked-or-at-point)]
1049 ["Edit patch" stgit-edit
1050 :help "Edit patch comment"
1051 :active (stgit-patch-name-at-point nil t)]
1052 ["Rename patch" stgit-rename :active (stgit-patch-name-at-point nil t)]
1053 ["Push/pop patch" stgit-push-or-pop
1054 :label (if (subsetp (stgit-patches-marked-or-at-point nil t)
1055 (stgit-applied-patchsyms t))
1056 "Pop patches" "Push patches")]
1057 ["Delete patches" stgit-delete
1058 :active (stgit-patches-marked-or-at-point nil t)]
1059 "-"
1060 ["Move patches" stgit-move-patches
1061 :active stgit-marked-patches
1062 :help "Move marked patch(es) to point"]
1063 ["Squash patches" stgit-squash
1064 :active (> (length stgit-marked-patches) 1)
1065 :help "Merge marked patches into one"]
1066 "-"
1067 ["Refresh top patch" stgit-refresh
1068 :active (not (and (stgit-index-empty-p) (stgit-work-tree-empty-p)))
1069 :help "Refresh the top patch with changes in index or work tree"]
1070 ["Refresh this patch" (stgit-refresh t)
1071 :keys "\\[universal-argument] \\[stgit-refresh]"
1072 :help "Refresh marked patch with changes in index or work tree"
1073 :active (and (not (and (stgit-index-empty-p)
1074 (stgit-work-tree-empty-p)))
1075 (stgit-patch-name-at-point nil t))]
1076 "-"
1077 ["Find file" stgit-select
1078 :active (eq (get-text-property (point) 'entry-type) 'file)]
1079 ["Open file" stgit-find-file-other-window
1080 :active (eq (get-text-property (point) 'entry-type) 'file)]
1081 ["Toggle file index" stgit-toggle-index
1082 :active (and (eq (get-text-property (point) 'entry-type) 'file)
1083 (memq (stgit-patch-name-at-point) '(:work :index)))
1084 :label (if (eq (stgit-patch-name-at-point) :work)
1085 "Move change to index"
1086 "Move change to work tree")]
1087 "-"
1088 ["Show diff" stgit-diff
1089 :active (get-text-property (point) 'entry-type)]
1090 ["Show diff for range of applied patches" stgit-diff-range
1091 :active (= (length stgit-marked-patches) 1)]
1092 ("Merge"
1093 :active (stgit-git-index-unmerged-p)
1094 ["Combined diff" stgit-diff-combined
1095 :active (memq (stgit-patch-name-at-point nil nil) '(:work :index))]
1096 ["Diff against base" stgit-diff-base
1097 :help "Show diff against the common base"
1098 :active (memq (stgit-patch-name-at-point nil nil) '(:work :index))]
1099 ["Diff against ours" stgit-diff-ours
1100 :help "Show diff against our branch"
1101 :active (memq (stgit-patch-name-at-point nil nil) '(:work :index))]
1102 ["Diff against theirs" stgit-diff-theirs
1103 :help "Show diff against their branch"
1104 :active (memq (stgit-patch-name-at-point nil nil) '(:work :index))]
1105 "-"
1106 ["Interactive merge" stgit-find-file-merge
1107 :help "Interactively merge the file"
1108 :active ,at-unmerged-file]
1109 ["Resolve file" stgit-resolve-file
1110 :help "Mark file conflict as resolved"
1111 :active ,at-unmerged-file]
1112 )
1113 "-"
1114 ["Show index & work tree" stgit-toggle-worktree :style toggle
1115 :selected stgit-show-worktree]
1116 ["Show unknown files" stgit-toggle-unknown :style toggle
1117 :selected stgit-show-unknown :active stgit-show-worktree]
1118 ["Show ignored files" stgit-toggle-ignored :style toggle
1119 :selected stgit-show-ignored :active stgit-show-worktree]
1120 ["Show patch names" stgit-toggle-patch-names :style toggle
1121 :selected stgit-show-patch-names]
1122 "-"
1123 ["Switch branches" stgit-branch t
1124 :help "Switch to or create another branch"]
1125 ["Rebase branch" stgit-rebase t
1126 :help "Rebase the current branch"]
1127 ))))
1128
1129 ;; disable tool bar editing buttons
1130 (put 'stgit-mode 'mode-class 'special)
1131
1132 (defun stgit-mode ()
1133 "Major mode for interacting with StGit.
1134
1135 Start StGit using \\[stgit].
1136
1137 Basic commands:
1138 \\<stgit-mode-map>\
1139 \\[stgit-help] Show this help text
1140 \\[stgit-quit] Hide the StGit buffer
1141 \\[describe-bindings] Show all key bindings
1142
1143 \\[stgit-reload-or-repair] Reload the StGit buffer
1144 \\[universal-argument] \\[stgit-reload-or-repair] Repair StGit metadata
1145
1146 \\[stgit-undo] Undo most recent StGit operation
1147 \\[stgit-redo] Undo recent undo
1148
1149 \\[stgit-git-status] Run `git-status' (if available)
1150
1151 Movement commands:
1152 \\[stgit-previous-line] Move to previous line
1153 \\[stgit-next-line] Move to next line
1154 \\[stgit-previous-patch] Move to previous patch
1155 \\[stgit-next-patch] Move to next patch
1156
1157 \\[stgit-mark-down] Mark patch and move down
1158 \\[stgit-unmark-up] Unmark patch and move up
1159 \\[stgit-unmark-down] Unmark patch and move down
1160
1161 Commands for patches:
1162 \\[stgit-select] Toggle showing changed files in patch
1163 \\[stgit-refresh] Refresh patch with changes in index or work tree
1164 \\[stgit-diff] Show the patch log and diff
1165
1166 \\[stgit-expand] Show changes in marked patches
1167 \\[stgit-collapse] Hide changes in marked patches
1168
1169 \\[stgit-new-and-refresh] Create a new patch from index or work tree
1170 \\[stgit-new] Create a new, empty patch
1171
1172 \\[stgit-rename] Rename patch
1173 \\[stgit-edit] Edit patch description
1174 \\[stgit-delete] Delete patch(es)
1175
1176 \\[stgit-revert] Revert all changes in index or work tree
1177 \\[stgit-toggle-index] Toggle all changes between index and work tree
1178
1179 \\[stgit-push-next] Push next patch onto stack
1180 \\[stgit-pop-next] Pop current patch from stack
1181 \\[stgit-push-or-pop] Push or pop marked patches
1182 \\[stgit-goto] Make patch at point current by popping or pushing
1183
1184 \\[stgit-squash] Squash (meld together) patches
1185 \\[stgit-move-patches] Move marked patches to point
1186
1187 \\[stgit-commit] Commit patch(es)
1188 \\[stgit-uncommit] Uncommit patch(es)
1189
1190 Commands for files:
1191 \\[stgit-select] Open the file in this window
1192 \\[stgit-find-file-other-window] Open the file in another window
1193 \\[stgit-diff] Show the file's diff
1194
1195 \\[stgit-toggle-index] Toggle change between index and work tree
1196 \\[stgit-revert] Revert changes to file
1197
1198 Display commands:
1199 \\[stgit-toggle-patch-names] Toggle showing patch names
1200 \\[stgit-toggle-worktree] Toggle showing index and work tree
1201 \\[stgit-toggle-unknown] Toggle showing unknown files
1202 \\[stgit-toggle-ignored] Toggle showing ignored files
1203
1204 Commands for diffs:
1205 \\[stgit-diff] Show diff of patch or file
1206 \\[stgit-diff-range] Show diff for range of patches
1207 \\[stgit-diff-base] Show diff against the merge base
1208 \\[stgit-diff-ours] Show diff against our branch
1209 \\[stgit-diff-theirs] Show diff against their branch
1210
1211 With one prefix argument (e.g., \\[universal-argument] \\[stgit-diff]), \
1212 ignore space changes.
1213 With two prefix arguments (e.g., \\[universal-argument] \
1214 \\[universal-argument] \\[stgit-diff]), ignore all space changes.
1215
1216 Commands for merge conflicts:
1217 \\[stgit-find-file-merge] Resolve conflicts using `smerge-ediff'
1218 \\[stgit-resolve-file] Mark unmerged file as resolved
1219
1220 Commands for branches:
1221 \\[stgit-branch] Switch to or create another branch
1222 \\[stgit-rebase] Rebase the current branch
1223
1224 Customization variables:
1225 `stgit-abbreviate-copies-and-renames'
1226 `stgit-default-show-patch-names'
1227 `stgit-default-show-worktree'
1228 `stgit-find-copies-harder'
1229 `stgit-show-worktree-mode'
1230
1231 See also \\[customize-group] for the \"stgit\" group."
1232 (kill-all-local-variables)
1233 (buffer-disable-undo)
1234 (setq mode-name "StGit"
1235 major-mode 'stgit-mode
1236 goal-column 2)
1237 (use-local-map stgit-mode-map)
1238 (set (make-local-variable 'list-buffers-directory) default-directory)
1239 (set (make-local-variable 'stgit-marked-patches) nil)
1240 (set (make-local-variable 'stgit-expanded-patches) (list :work :index))
1241 (set (make-local-variable 'stgit-show-patch-names)
1242 stgit-default-show-patch-names)
1243 (set (make-local-variable 'stgit-show-worktree) stgit-default-show-worktree)
1244 (set (make-local-variable 'stgit-index-node) nil)
1245 (set (make-local-variable 'stgit-worktree-node) nil)
1246 (set (make-local-variable 'parse-sexp-lookup-properties) t)
1247 (set-variable 'truncate-lines 't)
1248 (add-hook 'after-save-hook 'stgit-update-stgit-for-buffer)
1249 (unless stgit-did-advise
1250 (stgit-advise)
1251 (setq stgit-did-advise t))
1252 (run-hooks 'stgit-mode-hook))
1253
1254 (defun stgit-advise-funlist (funlist)
1255 "Add advice to the functions in FUNLIST so we can refresh the
1256 stgit buffers as the git status of files change."
1257 (mapc (lambda (sym)
1258 (when (fboundp sym)
1259 (eval `(defadvice ,sym (after stgit-update-stgit-for-buffer)
1260 (stgit-update-stgit-for-buffer t)))
1261 (ad-activate sym)))
1262 funlist))
1263
1264 (defun stgit-advise ()
1265 "Add advice to appropriate (non-stgit) git functions so we can
1266 refresh the stgit buffers as the git status of files change."
1267 (mapc (lambda (arg)
1268 (let ((feature (car arg))
1269 (funlist (cdr arg)))
1270 (if (featurep feature)
1271 (stgit-advise-funlist funlist)
1272 (add-to-list 'after-load-alist
1273 `(,feature (stgit-advise-funlist
1274 (quote ,funlist)))))))
1275 '((vc-git vc-git-rename-file vc-git-revert vc-git-register)
1276 (git git-add-file git-checkout git-revert-file git-remove-file))))
1277
1278 (defun stgit-update-stgit-for-buffer (&optional refresh-index)
1279 "Refresh worktree status in any `stgit-mode' buffer that shows
1280 the status of the current buffer.
1281
1282 If REFRESH-INDEX is not-nil, also update the index."
1283 (let* ((dir (cond ((eq major-mode 'git-status-mode)
1284 default-directory)
1285 (buffer-file-name
1286 (file-name-directory
1287 (expand-file-name buffer-file-name)))))
1288 (gitdir (and dir (condition-case nil (git-get-top-dir dir)
1289 (error nil))))
1290 (buffer (and gitdir (stgit-find-buffer gitdir))))
1291 (when buffer
1292 (with-current-buffer buffer
1293 (stgit-refresh-worktree)
1294 (when refresh-index (stgit-refresh-index))))))
1295
1296 (defun stgit-add-mark (patchsym)
1297 "Mark the patch PATCHSYM."
1298 (setq stgit-marked-patches (cons patchsym stgit-marked-patches)))
1299
1300 (defun stgit-remove-mark (patchsym)
1301 "Unmark the patch PATCHSYM."
1302 (setq stgit-marked-patches (delq patchsym stgit-marked-patches)))
1303
1304 (defun stgit-clear-marks ()
1305 "Unmark all patches."
1306 (setq stgit-marked-patches '()))
1307
1308 (defun stgit-patch-at-point (&optional cause-error)
1309 (get-text-property (point) 'patch-data))
1310
1311 (defun stgit-patch-name-at-point (&optional cause-error only-patches)
1312 "Return the patch name on the current line as a symbol.
1313 If CAUSE-ERROR is not nil, signal an error if none found.
1314 If ONLY-PATCHES is not nil, only allow real patches, and not
1315 index or work tree."
1316 (let ((patch (stgit-patch-at-point)))
1317 (and patch
1318 only-patches
1319 (memq (stgit-patch->status patch) '(work index))
1320 (setq patch nil))
1321 (cond (patch
1322 (stgit-patch->name patch))
1323 (cause-error
1324 (error "No patch on this line")))))
1325
1326 (defun stgit-patched-file-at-point ()
1327 (get-text-property (point) 'file-data))
1328
1329 (defun stgit-patches-marked-or-at-point (&optional cause-error only-patches)
1330 "Return the symbols of the marked patches, or the patch on the current line.
1331 If CAUSE-ERRROR is not nil, signal an error if none found.
1332 If ONLY-PATCHES is not nil, do not include index or work tree."
1333 (if stgit-marked-patches
1334 stgit-marked-patches
1335 (let ((patch (stgit-patch-name-at-point nil only-patches)))
1336 (cond (patch (list patch))
1337 (cause-error (error "No patches marked or at this line"))
1338 (t nil)))))
1339
1340 (defun stgit-goto-patch (patchsym &optional file)
1341 "Move point to the line containing patch PATCHSYM.
1342 If that patch cannot be found, do nothing.
1343
1344 If the patch was found and FILE is not nil, instead move to that
1345 file's line. If FILE cannot be found, stay on the line of
1346 PATCHSYM."
1347 (let ((node (ewoc-nth stgit-ewoc 0)))
1348 (while (and node (not (eq (stgit-patch->name (ewoc-data node))
1349 patchsym)))
1350 (setq node (ewoc-next stgit-ewoc node)))
1351 (when (and node file)
1352 (let* ((file-ewoc (stgit-patch->files-ewoc (ewoc-data node)))
1353 (file-node (ewoc-nth file-ewoc 0)))
1354 (while (and file-node
1355 (not (equal (stgit-file->file (ewoc-data file-node))
1356 file)))
1357 (setq file-node (ewoc-next file-ewoc file-node)))
1358 (when file-node
1359 (ewoc-goto-node file-ewoc file-node)
1360 (move-to-column (stgit-goal-column))
1361 (setq node nil))))
1362 (when node
1363 (ewoc-goto-node stgit-ewoc node)
1364 (move-to-column goal-column))))
1365
1366 (defun stgit-init ()
1367 "Run stg init."
1368 (interactive)
1369 (stgit-assert-mode)
1370 (stgit-capture-output nil
1371 (stgit-run "init"))
1372 (stgit-reload))
1373
1374 (defun stgit-toggle-mark ()
1375 "Toggle mark on the patch under point."
1376 (interactive)
1377 (stgit-assert-mode)
1378 (if (memq (stgit-patch-name-at-point t t) stgit-marked-patches)
1379 (stgit-unmark)
1380 (stgit-mark)))
1381
1382 (defun stgit-mark ()
1383 "Mark the patch under point."
1384 (interactive)
1385 (stgit-assert-mode)
1386 (let* ((node (ewoc-locate stgit-ewoc))
1387 (patch (ewoc-data node))
1388 (name (stgit-patch->name patch)))
1389 (when (eq name :work)
1390 (error "Cannot mark the work tree"))
1391 (when (eq name :index)
1392 (error "Cannot mark the index"))
1393 (stgit-add-mark (stgit-patch->name patch))
1394 (let ((column (current-column)))
1395 (ewoc-invalidate stgit-ewoc node)
1396 (move-to-column column))))
1397
1398 (defun stgit-mark-down ()
1399 "Mark the patch under point and move to the next patch."
1400 (interactive)
1401 (stgit-mark)
1402 (stgit-next-patch))
1403
1404 (defun stgit-unmark ()
1405 "Remove mark from the patch on the current line."
1406 (interactive)
1407 (stgit-assert-mode)
1408 (let* ((node (ewoc-locate stgit-ewoc))
1409 (patch (ewoc-data node)))
1410 (stgit-remove-mark (stgit-patch->name patch))
1411 (let ((column (current-column)))
1412 (ewoc-invalidate stgit-ewoc node)
1413 (move-to-column column))))
1414
1415 (defun stgit-unmark-up ()
1416 "Remove mark from the patch on the previous line."
1417 (interactive)
1418 (stgit-assert-mode)
1419 (stgit-previous-patch)
1420 (stgit-unmark))
1421
1422 (defun stgit-unmark-down ()
1423 "Remove mark from the patch on the current line."
1424 (interactive)
1425 (stgit-assert-mode)
1426 (stgit-unmark)
1427 (stgit-next-patch))
1428
1429 (defun stgit-rename (name)
1430 "Rename the patch under point to NAME."
1431 (interactive (list
1432 (read-string "Patch name: "
1433 (symbol-name (stgit-patch-name-at-point t t)))))
1434 (stgit-assert-mode)
1435 (let ((old-patchsym (stgit-patch-name-at-point t t)))
1436 (stgit-capture-output nil
1437 (stgit-run "rename" "--" old-patchsym name))
1438 (let ((name-sym (intern name)))
1439 (when (memq old-patchsym stgit-expanded-patches)
1440 (setq stgit-expanded-patches
1441 (cons name-sym (delq old-patchsym stgit-expanded-patches))))
1442 (when (memq old-patchsym stgit-marked-patches)
1443 (setq stgit-marked-patches
1444 (cons name-sym (delq old-patchsym stgit-marked-patches))))
1445 (stgit-reload)
1446 (stgit-goto-patch name-sym))))
1447
1448 (defun stgit-reload-or-repair (repair)
1449 "Update the contents of the StGit buffer (`stgit-reload').
1450
1451 With a prefix argument, repair the StGit metadata if the branch
1452 was modified with git commands (`stgit-repair')."
1453 (interactive "P")
1454 (stgit-assert-mode)
1455 (if repair
1456 (stgit-repair)
1457 (stgit-reload)))
1458
1459 (defun stgit-repair ()
1460 "Run stg repair."
1461 (interactive)
1462 (stgit-assert-mode)
1463 (stgit-capture-output nil
1464 (stgit-run "repair"))
1465 (stgit-reload))
1466
1467 (defun stgit-available-branches (&optional all)
1468 "Returns a list of the names of the available stg branches as strings.
1469
1470 If ALL is not nil, also return non-stgit branches."
1471 (let ((output (with-output-to-string
1472 (stgit-run "branch" "--list")))
1473 (pattern (format "^>?\\s-+%c\\s-+\\(\\S-+\\)"
1474 (if all ?. ?s)))
1475 (start 0)
1476 result)
1477 (while (string-match pattern output start)
1478 (setq result (cons (match-string 1 output) result))
1479 (setq start (match-end 0)))
1480 result))
1481
1482 (defun stgit-branch (branch)
1483 "Switch to or create branch BRANCH."
1484 (interactive (list (completing-read "Switch to branch: "
1485 (stgit-available-branches))))
1486 (stgit-assert-mode)
1487 (when (cond ((equal branch (stgit-current-branch))
1488 (error "Branch is already current"))
1489 ((member branch (stgit-available-branches t))
1490 (stgit-capture-output nil (stgit-run "branch" "--" branch))
1491 t)
1492 ((not (string-match stgit-allowed-branch-name-re branch))
1493 (error "Invalid branch name"))
1494 ((yes-or-no-p (format "Create branch \"%s\"? " branch))
1495 (stgit-capture-output nil (stgit-run "branch" "--create" "--"
1496 branch))
1497 t))
1498 (stgit-reload)))
1499
1500 (defun stgit-available-refs (&optional omit-stgit)
1501 "Returns a list of the available git refs.
1502 If OMIT-STGIT is not nil, filter out \"resf/heads/*.stgit\"."
1503 (let* ((output (with-output-to-string
1504 (stgit-run-git-silent "for-each-ref" "--format=%(refname)"
1505 "refs/tags" "refs/heads"
1506 "refs/remotes")))
1507 (result (split-string output "\n" t)))
1508 (mapcar (lambda (s)
1509 (if (string-match "^refs/\\(heads\\|tags\\|remotes\\)/" s)
1510 (substring s (match-end 0))
1511 s))
1512 (if omit-stgit
1513 (delete-if (lambda (s)
1514 (string-match "^refs/heads/.*\\.stgit$" s))
1515 result)
1516 result))))
1517
1518 (defun stgit-parent-branch ()
1519 "Return the parent branch of the current stg branch as per
1520 git-config setting branch.<branch>.stgit.parentbranch."
1521 (let ((output (with-output-to-string
1522 (stgit-run-git-silent "config"
1523 (format "branch.%s.stgit.parentbranch"
1524 (stgit-current-branch))))))
1525 (when (string-match ".*" output)
1526 (match-string 0 output))))
1527
1528 (defun stgit-rebase (new-base)
1529 "Rebase the current branch to NEW-BASE.
1530
1531 Interactively, first ask which branch to rebase to. Defaults to
1532 what git-config branch.<branch>.stgit.parentbranch is set to."
1533 (interactive (list (completing-read "Rebase to: "
1534 (stgit-available-refs t)
1535 nil nil
1536 (stgit-parent-branch))))
1537 (stgit-assert-mode)
1538 (stgit-capture-output nil (stgit-run "rebase" "--" new-base))
1539 (stgit-reload))
1540
1541 (defun stgit-commit (count)
1542 "Run stg commit on COUNT commits.
1543 Interactively, the prefix argument is used as COUNT.
1544 A negative COUNT will uncommit instead."
1545 (interactive "p")
1546 (stgit-assert-mode)
1547 (if (< count 0)
1548 (stgit-uncommit (- count))
1549 (stgit-capture-output nil (stgit-run "commit" "-n" count))
1550 (stgit-reload)))
1551
1552 (defun stgit-uncommit (count)
1553 "Run stg uncommit on COUNT commits.
1554 Interactively, the prefix argument is used as COUNT.
1555 A negative COUNT will commit instead."
1556 (interactive "p")
1557 (stgit-assert-mode)
1558 (if (< count 0)
1559 (stgit-commit (- count))
1560 (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
1561 (stgit-reload)))
1562
1563 (defun stgit-neighbour-file ()
1564 "Return the file name of the next file after point, or the
1565 previous file if point is at the last file within a patch."
1566 (let ((old-point (point))
1567 neighbour-file)
1568 (and (zerop (forward-line 1))
1569 (let ((f (stgit-patched-file-at-point)))
1570 (and f (setq neighbour-file (stgit-file->file f)))))
1571 (goto-char old-point)
1572 (unless neighbour-file
1573 (and (zerop (forward-line -1))
1574 (let ((f (stgit-patched-file-at-point)))
1575 (and f (setq neighbour-file (stgit-file->file f)))))
1576 (goto-char old-point))
1577 neighbour-file))
1578
1579 (defun stgit-revert-file ()
1580 "Revert the file at point, which must be in the index or the
1581 working tree."
1582 (interactive)
1583 (stgit-assert-mode)
1584 (let* ((patched-file (or (stgit-patched-file-at-point)
1585 (error "No file on the current line")))
1586 (patch-name (stgit-patch-name-at-point))
1587 (file-status (stgit-file->status patched-file))
1588 (rm-file (cond ((stgit-file->copy-or-rename patched-file)
1589 (stgit-file->cr-to patched-file))
1590 ((eq file-status 'add)
1591 (stgit-file->file patched-file))))
1592 (co-file (cond ((eq file-status 'rename)
1593 (stgit-file->cr-from patched-file))
1594 ((not (memq file-status '(copy add)))
1595 (stgit-file->file patched-file))))
1596 (next-file (stgit-neighbour-file)))
1597
1598 (unless (memq patch-name '(:work :index))
1599 (error "No index or working tree file on this line"))
1600
1601 (when (eq file-status 'ignore)
1602 (error "Cannot revert ignored files"))
1603
1604 (when (eq file-status 'unknown)
1605 (error "Cannot revert unknown files"))
1606
1607 (let ((nfiles (+ (if rm-file 1 0) (if co-file 1 0))))
1608 (when (yes-or-no-p (format "Revert %d file%s? "
1609 nfiles
1610 (if (= nfiles 1) "" "s")))
1611 (stgit-capture-output nil
1612 (when rm-file
1613 (stgit-run-git "rm" "-f" "-q" "--" rm-file))
1614 (when co-file
1615 (stgit-run-git "checkout" "HEAD" co-file)))
1616 (stgit-reload)
1617 (stgit-goto-patch patch-name next-file)))))
1618
1619 (defun stgit-revert ()
1620 "Revert the change at point, which must be the index, the work
1621 tree, or a single change in either."
1622 (interactive)
1623 (stgit-assert-mode)
1624 (let ((patched-file (stgit-patched-file-at-point)))
1625 (if patched-file
1626 (stgit-revert-file)
1627 (let* ((patch-name (or (stgit-patch-name-at-point)
1628 (error "No patch or file at point")))
1629 (patch-desc (case patch-name
1630 (:index "index")
1631 (:work "work tree")
1632 (t (error (substitute-command-keys
1633 "Use \\[stgit-delete] to delete a patch"))))))
1634 (when (if (eq patch-name :work)
1635 (stgit-work-tree-empty-p)
1636 (stgit-index-empty-p))
1637 (error (format "There are no changes in the %s to revert"
1638 patch-desc)))
1639 (and (eq patch-name :index)
1640 (not (stgit-work-tree-empty-p))
1641 (error "Cannot revert index as work tree contains unstaged changes"))
1642
1643 (when (yes-or-no-p (format "Revert all changes in the %s? "
1644 patch-desc))
1645 (if (eq patch-name :index)
1646 (stgit-run-git-silent "reset" "--hard" "-q")
1647 (stgit-run-git-silent "checkout" "--" "."))
1648 (stgit-refresh-index)
1649 (stgit-refresh-worktree)
1650 (stgit-goto-patch patch-name))))))
1651
1652 (defun stgit-resolve-file ()
1653 "Resolve conflict in the file at point."
1654 (interactive)
1655 (stgit-assert-mode)
1656 (let* ((patched-file (stgit-patched-file-at-point))
1657 (patch (stgit-patch-at-point))
1658 (patch-name (and patch (stgit-patch->name patch)))
1659 (status (and patched-file (stgit-file->status patched-file))))
1660
1661 (unless (memq patch-name '(:work :index))
1662 (error "No index or working tree file on this line"))
1663
1664 (unless (eq status 'unmerged)
1665 (error "No conflict to resolve at the current line"))
1666
1667 (stgit-capture-output nil
1668 (stgit-move-change-to-index (stgit-file->file patched-file)))
1669
1670 (stgit-reload)))
1671
1672 (defun stgit-push-or-pop-patches (do-push npatches)
1673 "Push (if DO-PUSH is not nil) or pop (if DO-PUSH is nil)
1674 NPATCHES patches, or all patches if NPATCHES is t."
1675 (stgit-assert-mode)
1676 (stgit-capture-output nil
1677 (apply 'stgit-run
1678 (if do-push "push" "pop")
1679 (if (eq npatches t)
1680 '("--all")
1681 (list "-n" npatches))))
1682 (stgit-reload)
1683 (stgit-refresh-git-status))
1684
1685 (defun stgit-push-next (npatches)
1686 "Push the first unapplied patch.
1687 With numeric prefix argument, push that many patches."
1688 (interactive "p")
1689 (stgit-push-or-pop-patches t npatches))
1690
1691 (defun stgit-pop-next (npatches)
1692 "Pop the topmost applied patch.
1693 With numeric prefix argument, pop that many patches.
1694
1695 If NPATCHES is t, pop all patches."
1696 (interactive "p")
1697 (stgit-push-or-pop-patches nil npatches))
1698
1699 (defun stgit-applied-patches (&optional only-patches)
1700 "Return a list of the applied patches.
1701
1702 If ONLY-PATCHES is not nil, exclude index and work tree."
1703 (let ((states (if only-patches
1704 '(applied top)
1705 '(applied top index work)))
1706 result)
1707 (ewoc-map (lambda (patch)
1708 (when (memq (stgit-patch->status patch) states)
1709 (setq result (cons patch result)))
1710 nil)
1711 stgit-ewoc)
1712 result))
1713
1714 (defun stgit-applied-patchsyms (&optional only-patches)
1715 "Return a list of the symbols of the applied patches.
1716
1717 If ONLY-PATCHES is not nil, exclude index and work tree."
1718 (mapcar #'stgit-patch->name (stgit-applied-patches only-patches)))
1719
1720 (defun stgit-push-or-pop ()
1721 "Push or pop the marked patches."
1722 (interactive)
1723 (stgit-assert-mode)
1724 (let* ((patchsyms (stgit-patches-marked-or-at-point t t))
1725 (applied-syms (stgit-applied-patchsyms t))
1726 (unapplied (set-difference patchsyms applied-syms)))
1727 (stgit-capture-output nil
1728 (apply 'stgit-run
1729 (if unapplied "push" "pop")
1730 "--"
1731 (stgit-sort-patches (if unapplied unapplied patchsyms)))))
1732 (stgit-reload))
1733
1734 (defun stgit-goto-target ()
1735 "Return the goto target a point; either a patchsym, :top,
1736 or :bottom."
1737 (let ((patchsym (stgit-patch-name-at-point)))
1738 (cond ((memq patchsym '(:work :index)) nil)
1739 (patchsym)
1740 ((not (next-single-property-change (point) 'patch-data))
1741 :top)
1742 ((not (previous-single-property-change (point) 'patch-data))
1743 :bottom))))
1744
1745 (defun stgit-goto ()
1746 "Go to the patch on the current line.
1747
1748 Push or pop patches to make this patch topmost. Push or pop all
1749 patches if used on a line after or before all patches."
1750 (interactive)
1751 (stgit-assert-mode)
1752 (let ((patchsym (stgit-goto-target)))
1753 (unless patchsym
1754 (error "No patch to go to on this line"))
1755 (case patchsym
1756 (:top (stgit-push-or-pop-patches t t))
1757 (:bottom (stgit-push-or-pop-patches nil t))
1758 (t (stgit-capture-output nil
1759 (stgit-run "goto" "--" patchsym))
1760 (stgit-reload)))))
1761
1762 (defun stgit-id (patchsym)
1763 "Return the git commit id for PATCHSYM.
1764 If PATCHSYM is a keyword, returns PATCHSYM unmodified."
1765 (if (keywordp patchsym)
1766 patchsym
1767 (let ((result (with-output-to-string
1768 (stgit-run-silent "id" "--" patchsym))))
1769 (unless (string-match "^\\([0-9A-Fa-f]\\{40\\}\\)$" result)
1770 (error "Cannot find commit id for %s" patchsym))
1771 (match-string 1 result))))
1772
1773 (defun stgit-whitespace-diff-arg (arg)
1774 (when (numberp arg)
1775 (cond ((> arg 4) "--ignore-all-space")
1776 ((> arg 1) "--ignore-space-change"))))
1777
1778 (defun stgit-show-patch (unmerged-stage ignore-whitespace)
1779 "Show the patch on the current line.
1780
1781 UNMERGED-STAGE is the argument to `git-diff' that that selects
1782 which stage to diff against in the case of unmerged files."
1783 (let ((space-arg (stgit-whitespace-diff-arg ignore-whitespace))
1784 (patch-name (stgit-patch-name-at-point t)))
1785 (stgit-capture-output "*StGit patch*"
1786 (case (get-text-property (point) 'entry-type)
1787 ('file
1788 (let* ((patched-file (stgit-patched-file-at-point))
1789 (patch-id (let ((id (stgit-id patch-name)))
1790 (if (and (eq id :index)
1791 (eq (stgit-file->status patched-file)
1792 'unmerged))
1793 :work
1794 id)))
1795 (args (append (and space-arg (list space-arg))
1796 (and (stgit-file->cr-from patched-file)
1797 (list (stgit-find-copies-harder-diff-arg)))
1798 (cond ((eq patch-id :index)
1799 '("--cached"))
1800 ((eq patch-id :work)
1801 (list unmerged-stage))
1802 (t
1803 (list (concat patch-id "^") patch-id)))
1804 '("--")
1805 (if (stgit-file->copy-or-rename patched-file)
1806 (list (stgit-file->cr-from patched-file)
1807 (stgit-file->cr-to patched-file))
1808 (list (stgit-file->file patched-file))))))
1809 (apply 'stgit-run-git "diff" args)))
1810 ('patch
1811 (let* ((patch-id (stgit-id patch-name)))
1812 (if (or (eq patch-id :index) (eq patch-id :work))
1813 (apply 'stgit-run-git "diff"
1814 (stgit-find-copies-harder-diff-arg)
1815 (append (and space-arg (list space-arg))
1816 (if (eq patch-id :index)
1817 '("--cached")
1818 (list unmerged-stage))))
1819 (let ((args (append '("show" "-O" "--patch-with-stat" "-O" "-M")
1820 (and space-arg (list "-O" space-arg))
1821 '("--")
1822 (list (stgit-patch-name-at-point)))))
1823 (apply 'stgit-run args)))))
1824 (t
1825 (error "No patch or file at point")))
1826 (with-current-buffer standard-output
1827 (goto-char (point-min))
1828 (diff-mode)))))
1829
1830 (defmacro stgit-define-diff (name diff-arg &optional unmerged-action)
1831 `(defun ,name (&optional ignore-whitespace)
1832 ,(format "Show the patch on the current line.
1833
1834 %sWith a prefix argument, ignore whitespace. With a prefix argument
1835 greater than four (e.g., \\[universal-argument] \
1836 \\[universal-argument] \\[%s]), ignore all whitespace."
1837 (if unmerged-action
1838 (format "For unmerged files, %s.\n\n" unmerged-action)
1839 "")
1840 name)
1841 (interactive "p")
1842 (stgit-assert-mode)
1843 (stgit-show-patch ,diff-arg ignore-whitespace)))
1844
1845 (stgit-define-diff stgit-diff
1846 "--ours" nil)
1847 (stgit-define-diff stgit-diff-ours
1848 "--ours"
1849 "diff against our branch")
1850 (stgit-define-diff stgit-diff-theirs
1851 "--theirs"
1852 "diff against their branch")
1853 (stgit-define-diff stgit-diff-base
1854 "--base"
1855 "diff against the merge base")
1856 (stgit-define-diff stgit-diff-combined
1857 "--cc"
1858 "show a combined diff")
1859
1860 (defun stgit-diff-range (&optional ignore-whitespace)
1861 "Show diff for the range of patches between point and the marked patch.
1862
1863 With a prefix argument, ignore whitespace. With a prefix argument
1864 greater than four (e.g., \\[universal-argument] \
1865 \\[universal-argument] \\[stgit-diff-range]), ignore all whitespace."
1866 (interactive "p")
1867 (stgit-assert-mode)
1868 (unless (= (length stgit-marked-patches) 1)
1869 (error "Need exactly one patch marked"))
1870 (let* ((patches (stgit-sort-patches (cons (stgit-patch-name-at-point t t)
1871 stgit-marked-patches)
1872 t))
1873 (first-patch (car patches))
1874 (second-patch (if (cdr patches) (cadr patches) first-patch))
1875 (whitespace-arg (stgit-whitespace-diff-arg ignore-whitespace))
1876 (applied (stgit-applied-patchsyms t)))
1877 (unless (and (memq first-patch applied) (memq second-patch applied))
1878 (error "Can only show diff range for applied patches"))
1879 (stgit-capture-output (format "*StGit diff %s..%s*"
1880 first-patch second-patch)
1881 (apply 'stgit-run-git (append '("diff" "--patch-with-stat")
1882 (and whitespace-arg (list whitespace-arg))
1883 (list (format "%s^" (stgit-id first-patch))
1884 (stgit-id second-patch))))
1885 (with-current-buffer standard-output
1886 (goto-char (point-min))
1887 (diff-mode)))))
1888
1889 (defun stgit-move-change-to-index (file &optional force)
1890 "Copies the work tree state of FILE to index, using git add or git rm.
1891
1892 If FORCE is not nil, use --force."
1893 (let ((op (if (or (file-exists-p file) (file-symlink-p file))
1894 '("add") '("rm" "-q"))))
1895 (stgit-capture-output "*git output*"
1896 (apply 'stgit-run-git (append op (and force '("--force"))
1897 '("--") (list file))))))
1898
1899 (defun stgit-remove-change-from-index (file)
1900 "Unstages the change in FILE from the index"
1901 (stgit-capture-output "*git output*"
1902 (stgit-run-git "reset" "-q" "--" file)))
1903
1904 (defun stgit-git-index-unmerged-p ()
1905 (let (result)
1906 (with-output-to-string
1907 (setq result (not (zerop (stgit-run-git-silent "diff-index" "--cached"
1908 "--diff-filter=U"
1909 "--quiet" "HEAD")))))
1910 result))
1911
1912 (defun stgit-file-toggle-index ()
1913 "Move modified file in or out of the index.
1914
1915 Leaves the point where it is, but moves the mark to where the
1916 file ended up. You can then jump to the file with \
1917 \\[exchange-point-and-mark]."
1918 (interactive)
1919 (stgit-assert-mode)
1920 (let* ((patched-file (or (stgit-patched-file-at-point)
1921 (error "No file on the current line")))
1922 (patched-status (stgit-file->status patched-file)))
1923 (when (eq patched-status 'unmerged)
1924 (error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
1925 (let* ((patch (stgit-patch-at-point))
1926 (patch-name (stgit-patch->name patch))
1927 (mark-file (if (eq patched-status 'rename)
1928 (stgit-file->cr-to patched-file)
1929 (stgit-file->file patched-file)))
1930 (point-file (if (eq patched-status 'rename)
1931 (stgit-file->cr-from patched-file)
1932 (stgit-neighbour-file))))
1933
1934 (cond ((eq patch-name :work)
1935 (stgit-move-change-to-index (stgit-file->file patched-file)
1936 (eq patched-status 'ignore)))
1937 ((eq patch-name :index)
1938 (stgit-remove-change-from-index (stgit-file->file patched-file)))
1939 (t
1940 (error "Can only move files between working tree and index")))
1941 (stgit-refresh-worktree)
1942 (stgit-refresh-index)
1943 (stgit-goto-patch (if (eq patch-name :index) :work :index) mark-file)
1944 (push-mark nil t t)
1945 (stgit-goto-patch patch-name point-file))))
1946
1947 (defun stgit-toggle-index ()
1948 "Move change in or out of the index.
1949
1950 Works on index and work tree, as well as files in either.
1951
1952 Leaves the point where it is, but moves the mark to where the
1953 file ended up. You can then jump to the file with \
1954 \\[exchange-point-and-mark]."
1955 (interactive)
1956 (stgit-assert-mode)
1957 (if (stgit-patched-file-at-point)
1958 (stgit-file-toggle-index)
1959 (let ((patch-name (stgit-patch-name-at-point)))
1960 (unless (memq patch-name '(:index :work))
1961 (error "Can only move changes between working tree and index"))
1962 (when (stgit-git-index-unmerged-p)
1963 (error "Resolve unmerged changes with \\[stgit-resolve-file] first"))
1964 (if (if (eq patch-name :index)
1965 (stgit-index-empty-p)
1966 (stgit-work-tree-empty-p))
1967 (message "No changes to be moved")
1968 (stgit-capture-output nil
1969 (if (eq patch-name :work)
1970 (stgit-run-git "add" "--update")
1971 (stgit-run-git "reset" "--mixed" "-q")))
1972 (stgit-refresh-worktree)
1973 (stgit-refresh-index))
1974 (stgit-goto-patch (if (eq patch-name :index) :work :index)))))
1975
1976 (defun stgit-edit ()
1977 "Edit the patch on the current line."
1978 (interactive)
1979 (stgit-assert-mode)
1980 (let ((patchsym (stgit-patch-name-at-point t t))
1981 (edit-buf (get-buffer-create "*StGit edit*"))
1982 (dir default-directory))
1983 (log-edit 'stgit-confirm-edit t nil edit-buf)
1984 (set (make-local-variable 'stgit-edit-patchsym) patchsym)
1985 (setq default-directory dir)
1986 (let ((standard-output edit-buf))
1987 (save-excursion
1988 (stgit-run-silent "edit" "--save-template=-" "--" patchsym)))))
1989
1990 (defun stgit-confirm-edit ()
1991 (interactive)
1992 (let ((file (make-temp-file "stgit-edit-")))
1993 (write-region (point-min) (point-max) file)
1994 (stgit-capture-output nil
1995 (stgit-run "edit" "-f" file "--" stgit-edit-patchsym))
1996 (with-current-buffer log-edit-parent-buffer
1997 (stgit-reload))))
1998
1999 (defun stgit-new (add-sign &optional refresh)
2000 "Create a new patch.
2001 With a prefix argument, include a \"Signed-off-by:\" line at the
2002 end of the patch."
2003 (interactive "P")
2004 (stgit-assert-mode)
2005 (let ((edit-buf (get-buffer-create "*StGit edit*"))
2006 (dir default-directory))
2007 (log-edit 'stgit-confirm-new t nil edit-buf)
2008 (setq default-directory dir)
2009 (set (make-local-variable 'stgit-refresh-after-new) refresh)
2010 (when add-sign
2011 (save-excursion
2012 (let ((standard-output (current-buffer)))
2013 (stgit-run-silent "new" "--sign" "--save-template=-"))))))
2014
2015 (defun stgit-confirm-new ()
2016 (interactive)
2017 (let ((file (make-temp-file "stgit-edit-"))
2018 (refresh stgit-refresh-after-new))
2019 (write-region (point-min) (point-max) file)
2020 (stgit-capture-output nil
2021 (stgit-run "new" "-f" file))
2022 (with-current-buffer log-edit-parent-buffer
2023 (if refresh
2024 (stgit-refresh)
2025 (stgit-reload)))))
2026
2027 (defun stgit-new-and-refresh (add-sign)
2028 "Create a new patch and refresh it with the current changes.
2029
2030 With a prefix argument, include a \"Signed-off-by:\" line at the
2031 end of the patch.
2032
2033 This works just like running `stgit-new' followed by `stgit-refresh'."
2034 (interactive "P")
2035 (stgit-assert-mode)
2036 (stgit-new add-sign t))
2037
2038 (defun stgit-create-patch-name (description)
2039 "Create a patch name from a long description"
2040 (let ((patch ""))
2041 (while (> (length description) 0)
2042 (cond ((string-match "\\`[a-zA-Z_-]+" description)
2043 (setq patch (downcase (concat patch
2044 (match-string 0 description))))
2045 (setq description (substring description (match-end 0))))
2046 ((string-match "\\` +" description)
2047 (setq patch (concat patch "-"))
2048 (setq description (substring description (match-end 0))))
2049 ((string-match "\\`[^a-zA-Z_-]+" description)
2050 (setq description (substring description (match-end 0))))))
2051 (cond ((= (length patch) 0)
2052 "patch")
2053 ((> (length patch) 20)
2054 (substring patch 0 20))
2055 (t patch))))
2056
2057 (defun stgit-delete (patchsyms &optional spill-p)
2058 "Delete the patches in PATCHSYMS.
2059 Interactively, delete the marked patches, or the patch at point.
2060
2061 With a prefix argument, or SPILL-P, spill the patch contents to
2062 the work tree and index."
2063 (interactive (list (stgit-patches-marked-or-at-point t t)
2064 current-prefix-arg))
2065 (stgit-assert-mode)
2066 (unless patchsyms
2067 (error "No patches to delete"))
2068 (when (memq :index patchsyms)
2069 (error "Cannot delete the index"))
2070 (when (memq :work patchsyms)
2071 (error "Cannot delete the work tree"))
2072
2073 (let ((npatches (length patchsyms)))
2074 (when (yes-or-no-p (format "Really delete %d patch%s%s? "
2075 npatches
2076 (if (= 1 npatches) "" "es")
2077 (if spill-p
2078 " (spilling contents to index)"
2079 "")))
2080 (let ((args (append (when spill-p '("--spill"))
2081 '("--")
2082 patchsyms)))
2083 (stgit-capture-output nil
2084 (apply 'stgit-run "delete" args))
2085 (stgit-reload)))))
2086
2087 (defun stgit-move-patches-target ()
2088 "Return the patchsym indicating a target patch for
2089 `stgit-move-patches'.
2090
2091 This is either the first unmarked patch at or after point, or one
2092 of :top and :bottom if the point is after or before the applied
2093 patches."
2094
2095 (save-excursion
2096 (let (result)
2097 (while (not result)
2098 (let ((patchsym (stgit-patch-name-at-point)))
2099 (cond ((memq patchsym '(:work :index)) (setq result :top))
2100 (patchsym (if (memq patchsym stgit-marked-patches)
2101 (stgit-next-patch)
2102 (setq result patchsym)))
2103 ((re-search-backward "^>" nil t) (setq result :top))
2104 (t (setq result :bottom)))))
2105 result)))
2106
2107 (defun stgit-sort-patches (patchsyms &optional allow-duplicates)
2108 "Returns the list of patches in PATCHSYMS sorted according to
2109 their position in the patch series, bottommost first.
2110
2111 PATCHSYMS must not contain duplicate entries, unless
2112 ALLOW-DUPLICATES is not nil."
2113 (let (sorted-patchsyms
2114 (series (with-output-to-string
2115 (with-current-buffer standard-output
2116 (stgit-run-silent "series" "--noprefix"))))
2117 start)
2118 (while (string-match "^\\(.+\\)" series start)
2119 (let ((patchsym (intern (match-string 1 series))))
2120 (when (memq patchsym patchsyms)
2121 (setq sorted-patchsyms (cons patchsym sorted-patchsyms))))
2122 (setq start (match-end 0)))
2123 (setq sorted-patchsyms (nreverse sorted-patchsyms))
2124
2125 (unless allow-duplicates
2126 (unless (= (length patchsyms) (length sorted-patchsyms))
2127 (error "Internal error")))
2128
2129 sorted-patchsyms))
2130
2131 (defun stgit-move-patches (patchsyms target-patch)
2132 "Move the patches in PATCHSYMS to below TARGET-PATCH.
2133 If TARGET-PATCH is :bottom or :top, move the patches to the
2134 bottom or top of the stack, respectively.
2135
2136 Interactively, move the marked patches to where the point is."
2137 (interactive (list stgit-marked-patches
2138 (stgit-move-patches-target)))
2139 (stgit-assert-mode)
2140 (unless patchsyms
2141 (error "Need at least one patch to move"))
2142
2143 (unless target-patch
2144 (error "Point not at a patch"))
2145
2146 ;; need to have patchsyms sorted by position in the stack
2147 (let ((sorted-patchsyms (stgit-sort-patches patchsyms)))
2148 (stgit-capture-output nil
2149 (if (eq target-patch :top)
2150 (apply 'stgit-run "float" "--" sorted-patchsyms)
2151 (apply 'stgit-run
2152 "sink"
2153 (append (unless (eq target-patch :bottom)
2154 (list "--to" target-patch))
2155 '("--")
2156 sorted-patchsyms)))))
2157 (stgit-reload))
2158
2159 (defun stgit-squash (patchsyms)
2160 "Squash the patches in PATCHSYMS.
2161 Interactively, squash the marked patches.
2162
2163 Unless there are any conflicts, the patches will be merged into
2164 one patch, which will occupy the same spot in the series as the
2165 deepest patch had before the squash."
2166 (interactive (list stgit-marked-patches))
2167 (stgit-assert-mode)
2168 (when (< (length patchsyms) 2)
2169 (error "Need at least two patches to squash"))
2170 (let ((stgit-buffer (current-buffer))
2171 (edit-buf (get-buffer-create "*StGit edit*"))
2172 (dir default-directory)
2173 (sorted-patchsyms (stgit-sort-patches patchsyms)))
2174 (log-edit 'stgit-confirm-squash t nil edit-buf)
2175 (set (make-local-variable 'stgit-patchsyms) sorted-patchsyms)
2176 (setq default-directory dir)
2177 (let ((result (let ((standard-output edit-buf))
2178 (save-excursion
2179 (apply 'stgit-run-silent "squash"
2180 "--save-template=-" "--" sorted-patchsyms)))))
2181
2182 ;; stg squash may have reordered the patches or caused conflicts
2183 (with-current-buffer stgit-buffer
2184 (stgit-reload))
2185
2186 (unless (eq 0 result)
2187 (fundamental-mode)
2188 (rename-buffer "*StGit error*")
2189 (resize-temp-buffer-window)
2190 (switch-to-buffer-other-window stgit-buffer)
2191 (error "stg squash failed")))))
2192
2193 (defun stgit-confirm-squash ()
2194 (interactive)
2195 (let ((file (make-temp-file "stgit-edit-")))
2196 (write-region (point-min) (point-max) file)
2197 (stgit-capture-output nil
2198 (apply 'stgit-run "squash" "-f" file "--" stgit-patchsyms))
2199 (with-current-buffer log-edit-parent-buffer
2200 (stgit-clear-marks)
2201 ;; Go to first marked patch and stay there
2202 (goto-char (point-min))
2203 (re-search-forward (concat "^[>+-]\\*") nil t)
2204 (move-to-column goal-column)
2205 (let ((pos (point)))
2206 (stgit-reload)
2207 (goto-char pos)))))
2208
2209 (defun stgit-help ()
2210 "Display help for the StGit mode."
2211 (interactive)
2212 (describe-function 'stgit-mode))
2213
2214 (defun stgit-undo-or-redo (redo hard)
2215 "Run stg undo or, if REDO is non-nil, stg redo.
2216
2217 If HARD is non-nil, use the --hard flag."
2218 (stgit-assert-mode)
2219 (let ((cmd (if redo "redo" "undo")))
2220 (stgit-capture-output nil
2221 (if arg
2222 (when (or (and (stgit-index-empty-p)
2223 (stgit-work-tree-empty-p))
2224 (y-or-n-p (format "Hard %s may overwrite index/work tree changes. Continue? "
2225 cmd)))
2226 (stgit-run cmd "--hard"))
2227 (stgit-run cmd))))
2228 (stgit-reload))
2229
2230 (defun stgit-undo (&optional arg)
2231 "Run stg undo.
2232 With prefix argument, run it with the --hard flag.
2233
2234 See also `stgit-redo'."
2235 (interactive "P")
2236 (stgit-undo-or-redo nil arg))
2237
2238 (defun stgit-redo (&optional arg)
2239 "Run stg redo.
2240 With prefix argument, run it with the --hard flag.
2241
2242 See also `stgit-undo'."
2243 (interactive "P")
2244 (stgit-undo-or-redo t arg))
2245
2246 (defun stgit-refresh (&optional arg)
2247 "Run stg refresh.
2248 If the index contains any changes, only refresh from index.
2249
2250 With prefix argument, refresh the marked patch or the patch under point."
2251 (interactive "P")
2252 (stgit-assert-mode)
2253 (let ((patchargs (if arg
2254 (let ((patches (stgit-patches-marked-or-at-point nil t)))
2255 (when (> (length patches) 1)
2256 (error "Too many patches marked"))
2257 (cons "-p" patches))
2258 nil)))
2259 (unless (stgit-index-empty-p)
2260 (setq patchargs (cons "--index" patchargs)))
2261 (stgit-capture-output nil
2262 (apply 'stgit-run "refresh" patchargs))
2263 (stgit-refresh-git-status))
2264 (stgit-reload))
2265
2266 (defvar stgit-show-worktree nil
2267 "If nil, inhibit showing work tree and index in the stgit buffer.
2268
2269 See also `stgit-show-worktree-mode'.")
2270
2271 (defvar stgit-show-ignored nil
2272 "If nil, inhibit showing files ignored by git.")
2273
2274 (defvar stgit-show-unknown nil
2275 "If nil, inhibit showing files not registered with git.")
2276
2277 (defvar stgit-show-patch-names t
2278 "If nil, inhibit showing patch names.")
2279
2280 (defun stgit-toggle-worktree (&optional arg)
2281 "Toggle the visibility of the work tree.
2282 With ARG, show the work tree if ARG is positive.
2283
2284 Its initial setting is controlled by `stgit-default-show-worktree'.
2285
2286 `stgit-show-worktree-mode' controls where on screen the index and
2287 work tree will show up."
2288 (interactive)
2289 (stgit-assert-mode)
2290 (setq stgit-show-worktree
2291 (if (numberp arg)
2292 (> arg 0)
2293 (not stgit-show-worktree)))
2294 (stgit-reload))
2295
2296 (defun stgit-toggle-ignored (&optional arg)
2297 "Toggle the visibility of files ignored by git in the work
2298 tree. With ARG, show these files if ARG is positive.
2299
2300 Use \\[stgit-toggle-worktree] to show the work tree."
2301 (interactive)
2302 (stgit-assert-mode)
2303 (setq stgit-show-ignored
2304 (if (numberp arg)
2305 (> arg 0)
2306 (not stgit-show-ignored)))
2307 (stgit-reload))
2308
2309 (defun stgit-toggle-unknown (&optional arg)
2310 "Toggle the visibility of files not registered with git in the
2311 work tree. With ARG, show these files if ARG is positive.
2312
2313 Use \\[stgit-toggle-worktree] to show the work tree."
2314 (interactive)
2315 (stgit-assert-mode)
2316 (setq stgit-show-unknown
2317 (if (numberp arg)
2318 (> arg 0)
2319 (not stgit-show-unknown)))
2320 (stgit-reload))
2321
2322 (defun stgit-toggle-patch-names (&optional arg)
2323 "Toggle the visibility of patch names. With ARG, show patch names
2324 if ARG is positive.
2325
2326 The initial setting is controlled by `stgit-default-show-patch-names'."
2327 (interactive)
2328 (stgit-assert-mode)
2329 (setq stgit-show-patch-names
2330 (if (numberp arg)
2331 (> arg 0)
2332 (not stgit-show-patch-names)))
2333 (stgit-reload))
2334
2335 (provide 'stgit)