mirror of https://github.com/Chizi123/.emacs.d.git

Chizi123
2018-11-18 8f6f2705a38e2515b6c57fda12c5be29fb9a798f
commit | author | age
5cb5f7 1 ;;; magit-files.el --- finding files  -*- lexical-binding: t -*-
C 2
3 ;; Copyright (C) 2010-2018  The Magit Project Contributors
4 ;;
5 ;; You should have received a copy of the AUTHORS.md file which
6 ;; lists all contributors.  If not, see http://magit.vc/authors.
7
8 ;; Author: Jonas Bernoulli <jonas@bernoul.li>
9 ;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
10
11 ;; Magit is free software; you can redistribute it and/or modify it
12 ;; under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 3, or (at your option)
14 ;; any later version.
15 ;;
16 ;; Magit is distributed in the hope that it will be useful, but WITHOUT
17 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
18 ;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
19 ;; License for more details.
20 ;;
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with Magit.  If not, see http://www.gnu.org/licenses.
23
24 ;;; Commentary:
25
26 ;; This library implements support for finding blobs, staged files,
27 ;; and Git configuration files.  It also implements modes useful in
28 ;; buffers visiting files and blobs, and the commands used by those
29 ;; modes.
30
31 ;;; Code:
32
33 (eval-when-compile
34   (require 'subr-x))
35
36 (require 'magit)
37
38 ;;; Find Blob
39
40 (defvar magit-find-file-hook nil)
41 (add-hook 'magit-find-file-hook #'magit-blob-mode)
42
43 ;;;###autoload
44 (defun magit-find-file (rev file)
45   "View FILE from REV.
46 Switch to a buffer visiting blob REV:FILE,
47 creating one if none already exists."
48   (interactive (magit-find-file-read-args "Find file"))
49   (switch-to-buffer (magit-find-file-noselect rev file)))
50
51 ;;;###autoload
52 (defun magit-find-file-other-window (rev file)
53   "View FILE from REV, in another window.
54 Like `magit-find-file', but create a new window or reuse an
55 existing one."
56   (interactive (magit-find-file-read-args "Find file in other window"))
57   (switch-to-buffer-other-window (magit-find-file-noselect rev file)))
58
59 (defun magit-find-file-read-args (prompt)
60   (let  ((rev (magit-read-branch-or-commit "Find file from revision")))
61     (list rev (magit-read-file-from-rev rev prompt))))
62
63 (defun magit-find-file-noselect (rev file)
64   "Read FILE from REV into a buffer and return the buffer.
65 FILE must be relative to the top directory of the repository."
66   (magit-find-file-noselect-1 rev file 'magit-find-file-hook))
67
68 (defun magit-find-file-noselect-1 (rev file hookvar &optional revert)
69   "Read FILE from REV into a buffer and return the buffer.
70 FILE must be relative to the top directory of the repository.
71 An empty REV stands for index."
72   (let ((topdir (magit-toplevel)))
73     (when (file-name-absolute-p file)
74       (setq file (file-relative-name file topdir)))
75     (with-current-buffer (magit-get-revision-buffer-create rev file)
76       (when (or (not magit-buffer-file-name)
77                 (if (eq revert 'ask-revert)
78                     (y-or-n-p (format "%s already exists; revert it? "
79                                       (buffer-name))))
80                 revert)
81         (setq magit-buffer-revision
82               (if (string= rev "") "{index}" (magit-rev-format "%H" rev)))
83         (setq magit-buffer-refname rev)
84         (setq magit-buffer-file-name (expand-file-name file topdir))
85         (setq default-directory
86               (let ((dir (file-name-directory magit-buffer-file-name)))
87                 (if (file-exists-p dir) dir topdir)))
88         (setq-local revert-buffer-function #'magit-revert-rev-file-buffer)
89         (revert-buffer t t)
90         (run-hooks hookvar))
91       (current-buffer))))
92
93 (defun magit-get-revision-buffer-create (rev file)
94   (magit-get-revision-buffer rev file t))
95
96 (defun magit-get-revision-buffer (rev file &optional create)
97   (funcall (if create 'get-buffer-create 'get-buffer)
98            (format "%s.~%s~" file (if (equal rev "") "index"
99                                     (subst-char-in-string ?/ ?_ rev)))))
100
101 (defun magit-revert-rev-file-buffer (_ignore-auto noconfirm)
102   (when (or noconfirm
103             (and (not (buffer-modified-p))
104                  (catch 'found
105                    (dolist (regexp revert-without-query)
106                      (when (string-match regexp magit-buffer-file-name)
107                        (throw 'found t)))))
108             (yes-or-no-p (format "Revert buffer from git %s? "
109                                  (if (equal magit-buffer-refname "") "{index}"
110                                    (concat "revision " magit-buffer-refname)))))
111     (let* ((inhibit-read-only t)
112            (default-directory (magit-toplevel))
113            (file (file-relative-name magit-buffer-file-name))
114            (coding-system-for-read (or coding-system-for-read 'undecided)))
115       (erase-buffer)
116       (magit-git-insert "cat-file" "-p" (concat magit-buffer-refname ":" file))
117       (setq buffer-file-coding-system last-coding-system-used))
118     (let ((buffer-file-name magit-buffer-file-name)
119           (after-change-major-mode-hook
120            (remq 'global-diff-hl-mode-enable-in-buffers
121                  after-change-major-mode-hook)))
122       (normal-mode t))
123     (setq buffer-read-only t)
124     (set-buffer-modified-p nil)
125     (goto-char (point-min))))
126
127 ;;; Find Index
128
129 (defvar magit-find-index-hook nil)
130
131 (defun magit-find-file-index-noselect (file &optional revert)
132   "Read FILE from the index into a buffer and return the buffer.
133 FILE must to be relative to the top directory of the repository."
134   (magit-find-file-noselect-1 "" file 'magit-find-index-hook
135                               (or revert 'ask-revert)))
136
137 (defun magit-update-index ()
138   "Update the index with the contents of the current buffer.
139 The current buffer has to be visiting a file in the index, which
140 is done using `magit-find-index-noselect'."
141   (interactive)
142   (let ((file (magit-file-relative-name)))
143     (unless (equal magit-buffer-refname "")
144       (user-error "%s isn't visiting the index" file))
145     (if (y-or-n-p (format "Update index with contents of %s" (buffer-name)))
146         (let ((index (make-temp-file "index"))
147               (buffer (current-buffer)))
148           (when magit-wip-before-change-mode
149             (magit-wip-commit-before-change (list file) " before un-/stage"))
150           (let ((coding-system-for-write buffer-file-coding-system))
151             (with-temp-file index
152               (insert-buffer-substring buffer)))
153           (magit-with-toplevel
154             (magit-call-git "update-index" "--cacheinfo"
155                             (substring (magit-git-string "ls-files" "-s" file)
156                                        0 6)
157                             (magit-git-string "hash-object" "-t" "blob" "-w"
158                                               (concat "--path=" file)
159                                               "--" index)
160                             file))
161           (set-buffer-modified-p nil)
162           (when magit-wip-after-apply-mode
163             (magit-wip-commit-after-apply (list file) " after un-/stage")))
164       (message "Abort")))
165   (--when-let (magit-mode-get-buffer 'magit-status-mode)
166     (with-current-buffer it (magit-refresh)))
167   t)
168
169 ;;; Find Config File
170
171 (defun magit-find-git-config-file (filename &optional wildcards)
172   "Edit a file located in the current repository's git directory.
173
174 When \".git\", located at the root of the working tree, is a
175 regular file, then that makes it cumbersome to open a file
176 located in the actual git directory.
177
178 This command is like `find-file', except that it temporarily
179 binds `default-directory' to the actual git directory, while
180 reading the FILENAME."
181   (interactive
182    (let ((default-directory (magit-git-dir)))
183      (find-file-read-args "Find file: "
184                           (confirm-nonexistent-file-or-buffer))))
185   (find-file filename wildcards))
186
187 (defun magit-find-git-config-file-other-window (filename &optional wildcards)
188   "Edit a file located in the current repository's git directory, in another window.
189
190 When \".git\", located at the root of the working tree, is a
191 regular file, then that makes it cumbersome to open a file
192 located in the actual git directory.
193
194 This command is like `find-file-other-window', except that it
195 temporarily binds `default-directory' to the actual git
196 directory, while reading the FILENAME."
197   (interactive
198    (let ((default-directory (magit-git-dir)))
199      (find-file-read-args "Find file in other window: "
200                           (confirm-nonexistent-file-or-buffer))))
201   (find-file-other-window filename wildcards))
202
203 (defun magit-find-git-config-file-other-frame (filename &optional wildcards)
204   "Edit a file located in the current repository's git directory, in another frame.
205
206 When \".git\", located at the root of the working tree, is a
207 regular file, then that makes it cumbersome to open a file
208 located in the actual git directory.
209
210 This command is like `find-file-other-frame', except that it
211 temporarily binds `default-directory' to the actual git
212 directory, while reading the FILENAME."
213   (interactive
214    (let ((default-directory (magit-git-dir)))
215      (find-file-read-args "Find file in other frame: "
216                           (confirm-nonexistent-file-or-buffer))))
217   (find-file-other-frame filename wildcards))
218
219 ;;; File Mode
220
221 (defvar magit-file-mode-map
222   (let ((map (make-sparse-keymap)))
223     (define-key map "\C-xg"    'magit-status)
224     (define-key map "\C-x\M-g" 'magit-dispatch-popup)
225     (define-key map "\C-c\M-g" 'magit-file-popup)
226     map)
227   "Keymap for `magit-file-mode'.")
228
229 ;;;###autoload (autoload 'magit-file-popup "magit" nil t)
230 (magit-define-popup magit-file-popup
231   "Popup console for Magit commands in file-visiting buffers."
232   :actions '((?s "Stage"     magit-stage-file)
233              (?D "Diff..."   magit-diff-buffer-file-popup)
234              (?L "Log..."    magit-log-buffer-file-popup)
235              (?B "Blame..."  magit-blame-popup) nil
236              (?u "Unstage"   magit-unstage-file)
237              (?d "Diff"      magit-diff-buffer-file)
238              (?l "Log"       magit-log-buffer-file)
239              (?b "Blame"     magit-blame-addition)
240              (?p "Prev blob" magit-blob-previous)
241              (?c "Commit"    magit-commit-popup) nil
242              (?t "Trace"     magit-log-trace-definition)
243              (?r (lambda ()
244                    (with-current-buffer magit-pre-popup-buffer
245                      (and (not buffer-file-name)
246                           (propertize "...removal" 'face 'default))))
247                  magit-blame-removal)
248              (?n "Next blob" magit-blob-next)
249              (?e "Edit line" magit-edit-line-commit)
250              nil nil
251              (?f (lambda ()
252                    (with-current-buffer magit-pre-popup-buffer
253                      (and (not buffer-file-name)
254                           (propertize "...reverse" 'face 'default))))
255                  magit-blame-reverse)
256              nil)
257   :max-action-columns 5)
258
259 (defvar magit-file-mode-lighter "")
260
261 (define-minor-mode magit-file-mode
262   "Enable some Magit features in a file-visiting buffer.
263
264 Currently this only adds the following key bindings.
265 \n\\{magit-file-mode-map}"
266   :package-version '(magit . "2.2.0")
267   :lighter magit-file-mode-lighter
268   :keymap  magit-file-mode-map)
269
270 (defun magit-file-mode-turn-on ()
271   (and buffer-file-name
272        (magit-inside-worktree-p t)
273        (magit-file-mode)))
274
275 ;;;###autoload
276 (define-globalized-minor-mode global-magit-file-mode
277   magit-file-mode magit-file-mode-turn-on
278   :package-version '(magit . "2.13.0")
279   :link '(info-link "(magit)Minor Mode for Buffers Visiting Files")
280   :group 'magit-essentials
281   :group 'magit-modes
282   :init-value t)
283 ;; Unfortunately `:init-value t' only sets the value of the mode
284 ;; variable but does not cause the mode function to be called, and we
285 ;; cannot use `:initialize' to call that explicitly because the option
286 ;; is defined before the functions, so we have to do it here.
287 (cl-eval-when (load)
288   (when global-magit-file-mode
289     (global-magit-file-mode 1)))
290
291 ;;; Blob Mode
292
293 (defvar magit-blob-mode-map
294   (let ((map (make-sparse-keymap)))
295     (cond ((featurep 'jkl)
296            (define-key map "i" 'magit-blob-previous)
297            (define-key map "k" 'magit-blob-next)
298            (define-key map "j" 'magit-blame-addition)
299            (define-key map "l" 'magit-blame-removal)
300            (define-key map "f" 'magit-blame-reverse))
301           (t
302            (define-key map "p" 'magit-blob-previous)
303            (define-key map "n" 'magit-blob-next)
304            (define-key map "b" 'magit-blame-addition)
305            (define-key map "r" 'magit-blame-removal)
306            (define-key map "f" 'magit-blame-reverse)))
307     (define-key map "q" 'magit-kill-this-buffer)
308     map)
309   "Keymap for `magit-blob-mode'.")
310
311 (define-minor-mode magit-blob-mode
312   "Enable some Magit features in blob-visiting buffers.
313
314 Currently this only adds the following key bindings.
315 \n\\{magit-blob-mode-map}"
316   :package-version '(magit . "2.3.0"))
317
318 (defun magit-blob-next ()
319   "Visit the next blob which modified the current file."
320   (interactive)
321   (if magit-buffer-file-name
322       (magit-blob-visit (or (magit-blob-successor magit-buffer-revision
323                                                   magit-buffer-file-name)
324                             magit-buffer-file-name)
325                         (line-number-at-pos))
326     (if (buffer-file-name (buffer-base-buffer))
327         (user-error "You have reached the end of time")
328       (user-error "Buffer isn't visiting a file or blob"))))
329
330 (defun magit-blob-previous ()
331   "Visit the previous blob which modified the current file."
332   (interactive)
333   (if-let ((file (or magit-buffer-file-name
334                      (buffer-file-name (buffer-base-buffer)))))
335       (--if-let (magit-blob-ancestor magit-buffer-revision file)
336           (magit-blob-visit it (line-number-at-pos))
337         (user-error "You have reached the beginning of time"))
338     (user-error "Buffer isn't visiting a file or blob")))
339
340 (defun magit-blob-visit (blob-or-file line)
341   (if (stringp blob-or-file)
342       (find-file blob-or-file)
343     (pcase-let ((`(,rev ,file) blob-or-file))
344       (magit-find-file rev file)
345       (apply #'message "%s (%s %s ago)"
346              (magit-rev-format "%s" rev)
347              (magit--age (magit-rev-format "%ct" rev)))))
348   (goto-char (point-min))
349   (forward-line (1- line)))
350
351 (defun magit-blob-ancestor (rev file)
352   (let ((lines (magit-with-toplevel
353                  (magit-git-lines "log" "-2" "--format=%H" "--name-only"
354                                   "--follow" (or rev "HEAD") "--" file))))
355     (if rev (cddr lines) (butlast lines 2))))
356
357 (defun magit-blob-successor (rev file)
358   (let ((lines (magit-with-toplevel
359                  (magit-git-lines "log" "--format=%H" "--name-only" "--follow"
360                                   "HEAD" "--" file))))
361     (catch 'found
362       (while lines
363         (if (equal (nth 2 lines) rev)
364             (throw 'found (list (nth 0 lines) (nth 1 lines)))
365           (setq lines (nthcdr 2 lines)))))))
366
367 ;;; File Commands
368
369 (defun magit-file-rename (file newname)
370   "Rename the FILE to NEWNAME.
371 If FILE isn't tracked in Git, fallback to using `rename-file'."
372   (interactive
373    (let* ((file (magit-read-file "Rename file"))
374           (dir (file-name-directory file))
375           (newname (read-file-name (format "Rename %s to file: " file)
376                                    (and dir (expand-file-name dir)))))
377      (list (expand-file-name file (magit-toplevel))
378            (expand-file-name newname))))
379   (if (magit-file-tracked-p (magit-convert-filename-for-git file))
380       (let ((oldbuf (get-file-buffer file)))
381         (when (and oldbuf (buffer-modified-p oldbuf))
382           (user-error "Save %s before moving it" file))
383         (when (file-exists-p newname)
384           (user-error "%s already exists" newname))
385         (magit-run-git "mv"
386                        (magit-convert-filename-for-git file)
387                        (magit-convert-filename-for-git newname))
388         (when oldbuf
389           (with-current-buffer oldbuf
390             (let ((buffer-read-only buffer-read-only))
391               (set-visited-file-name newname))
392             (if (fboundp 'vc-refresh-state)
393                 (vc-refresh-state)
394               (with-no-warnings
395                 (vc-find-file-hook))))))
396     (rename-file file newname current-prefix-arg)
397     (magit-refresh)))
398
399 (defun magit-file-untrack (files &optional force)
400   "Untrack the selected FILES or one file read in the minibuffer.
401
402 With a prefix argument FORCE do so even when the files have
403 staged as well as unstaged changes."
404   (interactive (list (or (--if-let (magit-region-values 'file t)
405                              (progn
406                                (unless (magit-file-tracked-p (car it))
407                                  (user-error "Already untracked"))
408                                (magit-confirm-files 'untrack it "Untrack"))
409                            (list (magit-read-tracked-file "Untrack file"))))
410                      current-prefix-arg))
411   (magit-run-git "rm" "--cached" (and force "--force") "--" files))
412
413 (defun magit-file-delete (files &optional force)
414   "Delete the selected FILES or one file read in the minibuffer.
415
416 With a prefix argument FORCE do so even when the files have
417 uncommitted changes.  When the files aren't being tracked in
418 Git, then fallback to using `delete-file'."
419   (interactive (list (--if-let (magit-region-values 'file t)
420                          (magit-confirm-files 'delete it "Delete")
421                        (list (magit-read-file "Delete file")))
422                      current-prefix-arg))
423   (if (magit-file-tracked-p (car files))
424       (magit-call-git "rm" (and force "--force") "--" files)
425     (let ((topdir (magit-toplevel)))
426       (dolist (file files)
427         (delete-file (expand-file-name file topdir) t))))
428   (magit-refresh))
429
430 ;;;###autoload
431 (defun magit-file-checkout (rev file)
432   "Checkout FILE from REV."
433   (interactive
434    (let ((rev (magit-read-branch-or-commit
435                "Checkout from revision" magit-buffer-revision)))
436      (list rev (magit-read-file-from-rev rev "Checkout file"))))
437   (magit-with-toplevel
438     (magit-run-git "checkout" rev "--" file)))
439
440 ;;; Read File
441
442 (defvar magit-read-file-hist nil)
443
444 (defun magit-read-file-from-rev (rev prompt &optional default)
445   (let ((files (magit-revision-files rev)))
446     (magit-completing-read
447      prompt files nil t nil 'magit-read-file-hist
448      (car (member (or default (magit-current-file)) files)))))
449
450 (defun magit-read-file (prompt &optional tracked-only)
451   (let ((choices (nconc (magit-list-files)
452                         (unless tracked-only (magit-untracked-files)))))
453     (magit-completing-read
454      prompt choices nil t nil nil
455      (car (member (or (magit-section-value-if '(file submodule))
456                       (magit-file-relative-name nil tracked-only))
457                   choices)))))
458
459 (defun magit-read-tracked-file (prompt)
460   (magit-read-file prompt t))
461
462 (defun magit-read-file-choice (prompt files &optional error default)
463   "Read file from FILES.
464
465 If FILES has only one member, return that instead of prompting.
466 If FILES has no members, give a user error.  ERROR can be given
467 to provide a more informative error.
468
469 If DEFAULT is non-nil, use this as the default value instead of
470 `magit-current-file'."
471   (pcase (length files)
472     (0 (user-error (or error "No file choices")))
473     (1 (car files))
474     (_ (magit-completing-read
475         prompt files nil t nil 'magit-read-file-hist
476         (car (member (or default (magit-current-file)) files))))))
477
478 (defun magit-read-changed-file (rev-or-range prompt &optional default)
479   (magit-read-file-choice
480    prompt
481    (magit-changed-files rev-or-range)
482    default
483    (concat "No file changed in " rev-or-range)))
484
485 (defun magit-read-files (prompt initial-contents)
486   (mapconcat 'identity
487              (completing-read-multiple (or prompt "File,s: ")
488                                        (magit-list-files)
489                                        nil nil initial-contents) ","))
490
491 ;;; _
492 (provide 'magit-files)
493 ;;; magit-files.el ends here