fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 23h ago
864 lines · 34.4 KB Raw Blame History
1 ;;; magit-files.el --- Finding files -*- lexical-binding:t -*-
2
3 ;; Copyright (C) 2008-2026 The Magit Project Contributors
4
5 ;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
6 ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
7
8 ;; SPDX-License-Identifier: GPL-3.0-or-later
9
10 ;; Magit is free software: you can redistribute it and/or modify it
11 ;; under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14 ;;
15 ;; Magit is distributed in the hope that it will be useful, but WITHOUT
16 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
18 ;; License for more details.
19 ;;
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with Magit. If not, see <https://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This library implements support for finding blobs, staged files,
26 ;; and Git configuration files. It also implements modes useful in
27 ;; buffers visiting files and blobs, and the commands used by those
28 ;; modes.
29
30 ;;; Code:
31
32 (require 'magit)
33
34 (require 'bookmark)
35
36 (declare-function ediff-quit "ediff-util" (reverse-default-keep-variants))
37
38 ;;; Find Blob
39
40 (define-obsolete-variable-alias 'magit-find-file-hook
41 'magit-find-blob-hook "Magit 4.6.0")
42
43 (define-obsolete-variable-alias 'magit-find-index-hook
44 'magit-find-blob-hook "Magit 4.6.0")
45
46 (defvar magit-find-blob-hook (list #'magit-blob-mode))
47
48 (defvar-local magit-buffer-blob-oid--init nil)
49 (defvar-local magit-buffer--volatile nil)
50 (put 'magit-buffer--volatile 'permanent-local t)
51
52 ;;;###autoload
53 (defun magit-find-file (rev file)
54 "View FILE from REV.
55 Switch to a buffer visiting blob REV:FILE, creating one if none
56 already exists. If prior to calling this command the current
57 buffer and/or cursor position is about the same file, then go
58 to the line and column corresponding to that location."
59 (interactive (magit-find-file-read-args "Find file"))
60 (pop-to-buffer-same-window (magit-find-file-noselect rev file)))
61
62 ;;;###autoload
63 (defun magit-find-file-other-window (rev file)
64 "View FILE from REV, in another window.
65 Switch to a buffer visiting blob REV:FILE, creating one if none
66 already exists. If prior to calling this command the current
67 buffer and/or cursor position is about the same file, then go to
68 the line and column corresponding to that location."
69 (interactive (magit-find-file-read-args "Find file in other window"))
70 (switch-to-buffer-other-window (magit-find-file-noselect rev file)))
71
72 ;;;###autoload
73 (defun magit-find-file-other-frame (rev file)
74 "View FILE from REV, in another frame.
75 Switch to a buffer visiting blob REV:FILE, creating one if none
76 already exists. If prior to calling this command the current
77 buffer and/or cursor position is about the same file, then go to
78 the line and column corresponding to that location."
79 (interactive (magit-find-file-read-args "Find file in other frame"))
80 (switch-to-buffer-other-frame (magit-find-file-noselect rev file)))
81
82 (defun magit-find-file-read-args (prompt)
83 (let* ((pseudo-revs '("{worktree}" "{index}"))
84 (rev (magit-completing-read "Find file from revision"
85 (append pseudo-revs
86 (magit-list-refnames nil t))
87 nil 'any nil 'magit-revision-history
88 (or (magit-branch-or-commit-at-point)
89 (magit-get-current-branch)))))
90 (list rev
91 (magit-read-file-from-rev (if (member rev pseudo-revs) "HEAD" rev)
92 prompt))))
93
94 (defun magit-find-file-noselect (rev file &optional no-restore-position volatile)
95 "Read FILE from REV into a buffer and return the buffer.
96 REV is a revision or one of \"{worktree}\" or \"{index}\".
97 Non-interactively REV can also be a blob object."
98 (let* ((rev (pcase rev
99 ('nil "{worktree}")
100 ((and "{index}"
101 (guard (length> (magit--file-index-stages file) 1)))
102 "{worktree}")
103 (rev rev)))
104 (topdir (magit-toplevel))
105 (file (expand-file-name file topdir))
106 (buffer
107 (cond-let
108 ((equal rev "{worktree}")
109 (let ((revert-without-query
110 (if (and$ (find-buffer-visiting file)
111 (buffer-local-value 'auto-revert-mode $))
112 (cons "." revert-without-query)
113 revert-without-query)))
114 (find-file-noselect file volatile)))
115 ((not topdir)
116 (error "%s is not inside a Git repository" file))
117 ([defdir (file-name-directory file)]
118 [rev (magit--abbrev-if-oid rev)]
119 (unless (file-in-directory-p file topdir)
120 (error "%s is not inside Git repository %s" file topdir))
121 (with-current-buffer
122 (magit--get-blob-buffer rev file volatile)
123 (if (magit-blob-p rev)
124 (setq magit-buffer-blob-oid--init (magit-rev-parse rev))
125 (setq magit-buffer-revision rev))
126 (setq magit-buffer-file-name file)
127 (setq default-directory
128 (if (file-exists-p defdir) defdir topdir))
129 (setq-local revert-buffer-function #'magit--revert-blob-buffer)
130 (setq-local buffer-read-only t)
131 (setq-local read-only-mode--state t)
132 (magit--refresh-blob-buffer)
133 (current-buffer)))
134 ((error "Unexpected error")))))
135 (when (and (not no-restore-position)
136 (equal (magit-buffer-file-name) file))
137 (let ((pos (magit-find-file--position)))
138 (with-current-buffer buffer
139 (apply #'magit-find-file--restore-position pos))))
140 buffer))
141
142 (defun magit--get-blob-buffer (obj file &optional volatile)
143 ;; If OBJ is a commit, it is assumed to be abbreviated.
144 ;; FILE is assumed to be relative to the top-level.
145 (cond-let
146 ([buf (if (magit-blob-p obj)
147 (magit--find-buffer 'magit-buffer-blob-oid (magit-rev-parse obj)
148 'magit-buffer-file-name file)
149 (magit--find-buffer 'magit-buffer-revision obj
150 'magit-buffer-file-name file))]
151 (with-current-buffer buf
152 (when (and (not volatile) magit-buffer--volatile)
153 (setq magit-buffer--volatile nil)
154 (rename-buffer (magit--blob-buffer-name obj file))
155 (magit--blob-cache-remove buf)))
156 buf)
157 ([buf (get-buffer-create (magit--blob-buffer-name obj file volatile))]
158 (with-current-buffer buf
159 (setq magit-buffer--volatile volatile)
160 (magit--blob-cache-put buf))
161 (buffer-enable-undo buf)
162 buf)))
163
164 (defun magit--blob-buffer-name (obj file &optional volatile relative)
165 (format "%s%s.~%s~"
166 (if volatile " " "")
167 (or (if relative (file-relative-name file (magit-toplevel)) file)
168 (and (magit-blob-p obj) "{blob}"))
169 (subst-char-in-string ?/ ?_ obj)))
170
171 (defun magit--revert-blob-buffer (_ignore-auto _noconfirm)
172 (let ((pos (magit-find-file--position)))
173 (magit--refresh-blob-buffer t)
174 (apply #'magit-find-file--restore-position pos)))
175
176 (defun magit--refresh-blob-buffer (&optional force)
177 (let ((old-blob-oid magit-buffer-blob-oid))
178 (cond
179 (magit-buffer-revision
180 (setq magit-buffer-revision-oid
181 (magit-commit-oid magit-buffer-revision t))
182 (setq magit-buffer-blob-oid
183 (magit-blob-oid magit-buffer-revision magit-buffer-file-name)))
184 (magit-buffer-blob-oid--init
185 (setq magit-buffer-blob-oid magit-buffer-blob-oid--init)
186 (setq magit-buffer-blob-oid--init nil)))
187 (when (or force (not (equal old-blob-oid magit-buffer-blob-oid)))
188 (let ((inhibit-read-only t))
189 (erase-buffer)
190 (save-excursion
191 (magit--insert-blob-contents magit-buffer-revision
192 (magit-file-relative-name))))
193 (magit--blob-normal-mode))))
194
195 (defun magit--blob-normal-mode ()
196 (let ((buffer-file-name magit-buffer-file-name)
197 (buffer-name (buffer-name)))
198 ;; `font-lock-mode' contains a hardcoded condition that prevents it
199 ;; from being enabled in hidden buffers. Use a fake `buffer-name'
200 ;; to trick it into believing the buffer is not hidden.
201 (if (eq (aref buffer-name 0) ?\s)
202 (letrec ((adv (lambda (fn &optional buffer)
203 (let ((name (funcall fn buffer)))
204 (if (equal name buffer-name)
205 (substring name 1)
206 name)))))
207 (advice-add 'buffer-name :around adv)
208 (unwind-protect
209 (normal-mode (not enable-local-variables))
210 (advice-remove 'buffer-name adv)))
211 ;; We want `normal-mode' to respect nil `enable-local-variables'.
212 ;; The FIND-FILE argument wasn't designed for our use case,
213 ;; so we have to use this strange invocation to achieve that.
214 (normal-mode (not enable-local-variables)))
215 ;; Set this here, so we don't have to make it permanent-local, which
216 ;; we probably shouldn't do, because for other packages changing the.
217 ;; If the user calls `normal-mode', this value will be discarded.
218 (setq-local bookmark-make-record-function #'magit--make-blob-bookmark)
219 (set-buffer-modified-p nil)
220 (run-hooks 'magit-find-blob-hook)))
221
222 (defun magit-find-file--position ()
223 (list (or magit-buffer-revision-oid magit-buffer-revision "{worktree}")
224 magit-buffer-blob-oid
225 (line-number-at-pos)
226 (current-column)))
227
228 (defun magit-find-file--restore-position (before blob line col)
229 (let ((file (magit-file-relative-name))
230 (rev (or magit-buffer-revision-oid magit-buffer-revision "{worktree}")))
231 (goto-char (point-min))
232 (forward-line
233 (1-
234 (pcase (list before rev)
235 ((guard (equal magit-buffer-blob-oid blob)) line)
236 ('("{worktree}" "{worktree}") line)
237 ('("{worktree}" "{index}")
238 (magit-diff-visit--offset line file "-R"))
239 (`("{worktree}" ,_)
240 (magit-diff-visit--offset line file "-R" rev))
241 ('("{index}" "{worktree}")
242 (magit-diff-visit--offset line file))
243 ('("{index}" "{index}")
244 (magit-diff-visit--offset line (list blob magit-buffer-blob-oid file)))
245 (`("{index}" ,_)
246 (magit-diff-visit--offset line file "-R" "--cached"))
247 (`(,_ "{worktree}")
248 (magit-diff-visit--offset line file before))
249 (`(,_ "{index}")
250 (magit-diff-visit--offset line file "--cached"))
251 (_
252 (magit-diff-visit--offset line file before rev)))))
253 (move-to-column col)))
254
255 (defvar magit--blob-cache-limit 100
256 "Limit number of volatile blob buffers to be kept alive.
257 If nil, only use `magit--blob-cache-timeout'.")
258 (defvar magit--blob-cache-timeout 1800
259 "Limit age, since last access, of volatile blob buffers to be kept alive.
260 Age is tracked in seconds. If nil, only use `magit--blob-cache-limit'.")
261 (defvar magit--blob-cache-interval 600
262 "Seconds between volatile blob buffer pruning runs.")
263 (defvar magit--blob-cache-timer nil)
264 (defvar magit--blob-cache nil)
265
266 (defun magit--blob-cache-put (buffer)
267 (if-let ((elt (assq buffer magit--blob-cache)))
268 (setcdr elt (current-time))
269 (push (cons buffer (current-time)) magit--blob-cache))
270 (magit--blob-cache-start))
271
272 (defun magit--blob-cache-remove (buffer)
273 (and$ (assq buffer magit--blob-cache)
274 (delq $ magit--blob-cache)))
275
276 (defun magit--blob-cache-start ()
277 (when (and magit--blob-cache-interval
278 (not magit--blob-cache-timer))
279 (setq magit--blob-cache-timer
280 (run-with-timer magit--blob-cache-interval nil
281 #'magit--blob-cache-prune))))
282
283 (defun magit--blob-cache-prune ()
284 (when magit--blob-cache-timer
285 (cancel-timer magit--blob-cache-timer)
286 (setq magit--blob-cache-timer nil))
287 (pcase-let
288 ((`(,active ,rest)
289 (magit--separate
290 (pcase-lambda (`(,buffer))
291 (or (get-buffer-window buffer t)
292 (not (eq (buffer-local-value 'magit-buffer--volatile buffer) t))))
293 magit--blob-cache)))
294 (when magit--blob-cache-timeout
295 (setq rest
296 (seq-filter (pcase-lambda (`(,buffer . ,time))
297 (cond
298 ((not (buffer-live-p buffer)) nil)
299 ((time-less-p (time-subtract (current-time) time)
300 magit--blob-cache-timeout)
301 buffer)
302 (t (kill-buffer buffer) nil)))
303 rest)))
304 (when-let* ((_ magit--blob-cache-limit)
305 (ceiling (- magit--blob-cache-limit (length active)))
306 (_ (length> rest ceiling)))
307 (let ((sorted (compat-call sort rest
308 :key (##float-time (cdr %))
309 :lessp #'<
310 :reverse t)))
311 (dolist (kill (nthcdr ceiling sorted))
312 (kill-buffer (car kill)))
313 (setq rest (ntake ceiling sorted))))
314 (setq magit--blob-cache (nconc active rest)))
315 (magit--blob-cache-start))
316
317 (defun magit--blob-cache-zap ()
318 (pcase-dolist (`(,buffer) magit--blob-cache)
319 (kill-buffer buffer))
320 (setq magit--blob-cache nil))
321
322 (defun magit--inhibit-in-blob-buffers (fn &rest args)
323 "Prevent the advised function from doing anything in Magit blob buffers."
324 (unless magit-buffer-blob-oid
325 (apply fn args)))
326 ;; See bb8a65269d.
327 (advice-add 'global-diff-hl-mode-enable-in-buffer ; Emacs >= 30
328 :around #'magit--inhibit-in-blob-buffers)
329 (advice-add 'global-diff-hl-mode-enable-in-buffers ; Emacs < 30
330 :around #'magit--inhibit-in-blob-buffers)
331 ;; See 234a787b8c.
332 (advice-add 'eglot--maybe-activate-editing-mode
333 :around #'magit--inhibit-in-blob-buffers)
334 ;; See https://github.com/doomemacs/doomemacs/pull/6309.
335 (advice-add 'lsp
336 :around #'magit--inhibit-in-blob-buffers)
337
338 ;;; Blob Bookmarks
339
340 (defun magit--make-blob-bookmark ()
341 (let* ((obj (or magit-buffer-revision
342 magit-buffer-blob-oid))
343 (bookmark (cons nil (bookmark-make-record-default 'no-file))))
344 (bookmark-prop-set bookmark 'handler #'magit--handle-blob-bookmark)
345 (bookmark-prop-set bookmark 'revision obj)
346 (bookmark-prop-set bookmark 'filename magit-buffer-file-name)
347 ;; Determine buffer name anew to avoid using volatile name.
348 ;; These are offered to the user when they name the bookmark.
349 (bookmark-prop-set
350 bookmark 'defaults
351 (list (magit--blob-buffer-name obj magit-buffer-file-name nil t)
352 (magit--blob-buffer-name obj magit-buffer-file-name)))
353 bookmark))
354
355 (defun magit--handle-blob-bookmark (bookmark)
356 (pop-to-buffer-same-window
357 (magit-find-file-noselect (bookmark-prop-get bookmark 'revision)
358 (bookmark-prop-get bookmark 'filename)
359 t)))
360
361 ;;; Update Index
362
363 (defun magit-update-index ()
364 "Update the index with the contents of the current buffer.
365 The current buffer has to be visiting a file in the index, which
366 is done using `magit-find-index-noselect'."
367 (interactive)
368 (let ((file (magit-file-relative-name)))
369 (unless (equal magit-buffer-revision "{index}")
370 (user-error "%s isn't visiting the index" file))
371 (if (y-or-n-p (format "Update index with contents of %s?" (buffer-name)))
372 (let ((index (make-temp-name
373 (expand-file-name "magit-update-index-" (magit-gitdir))))
374 (buffer (current-buffer)))
375 (magit-run-before-change-functions file "un-/stage")
376 (unwind-protect
377 (progn
378 (let ((coding-system-for-write buffer-file-coding-system))
379 (with-temp-file index
380 (insert-buffer-substring buffer)))
381 (magit-with-toplevel
382 (magit-call-git
383 "update-index" "--cacheinfo"
384 (substring (magit-git-string "ls-files" "-s" file)
385 0 6)
386 (magit-git-string "hash-object" "-t" "blob" "-w"
387 (concat "--path=" file)
388 "--" (magit-convert-filename-for-git index))
389 file)))
390 (ignore-errors (delete-file index)))
391 (set-buffer-modified-p nil)
392 (magit-run-after-apply-functions file "un-/stage"))
393 (message "Abort")))
394 (when$ (magit-get-mode-buffer 'magit-status-mode)
395 (with-current-buffer $
396 (magit-refresh)))
397 t)
398
399 ;;; Find Config File
400
401 (defun magit-find-git-config-file (filename &optional wildcards)
402 "Edit a file located in the current repository's git directory.
403
404 When \".git\", located at the root of the working tree, is a
405 regular file, then that makes it cumbersome to open a file
406 located in the actual git directory.
407
408 This command is like `find-file', except that it temporarily
409 binds `default-directory' to the actual git directory, while
410 reading the FILENAME."
411 (interactive
412 (let ((default-directory (magit-gitdir)))
413 (find-file-read-args "Find file: "
414 (confirm-nonexistent-file-or-buffer))))
415 (find-file filename wildcards))
416
417 (defun magit-find-git-config-file-other-window (filename &optional wildcards)
418 "Edit a file located in the current repo's git directory, in another window.
419
420 When \".git\", located at the root of the working tree, is a
421 regular file, then that makes it cumbersome to open a file
422 located in the actual git directory.
423
424 This command is like `find-file-other-window', except that it
425 temporarily binds `default-directory' to the actual git
426 directory, while reading the FILENAME."
427 (interactive
428 (let ((default-directory (magit-gitdir)))
429 (find-file-read-args "Find file in other window: "
430 (confirm-nonexistent-file-or-buffer))))
431 (find-file-other-window filename wildcards))
432
433 (defun magit-find-git-config-file-other-frame (filename &optional wildcards)
434 "Edit a file located in the current repo's git directory, in another frame.
435
436 When \".git\", located at the root of the working tree, is a
437 regular file, then that makes it cumbersome to open a file
438 located in the actual git directory.
439
440 This command is like `find-file-other-frame', except that it
441 temporarily binds `default-directory' to the actual git
442 directory, while reading the FILENAME."
443 (interactive
444 (let ((default-directory (magit-gitdir)))
445 (find-file-read-args "Find file in other frame: "
446 (confirm-nonexistent-file-or-buffer))))
447 (find-file-other-frame filename wildcards))
448
449 ;;; File Dispatch
450
451 ;;;###autoload(autoload 'magit-file-dispatch "magit" nil t)
452 (transient-define-prefix magit-file-dispatch ()
453 "Invoke a Magit command that acts on the visited file.
454 When invoked outside a file-visiting buffer, then fall back
455 to `magit-dispatch'."
456 :info-manual "(magit) Minor Mode for Buffers Visiting Files"
457 [:if magit-file-relative-name
458 ["File actions"
459 (" s" "Stage" magit-file-stage :if-not-derived dired-mode)
460 (" s" "Stage" magit-dired-stage :if-derived dired-mode)
461 (" u" "Unstage" magit-file-unstage :if-not-derived dired-mode)
462 (" u" "Unstage" magit-dired-unstage :if-derived dired-mode)
463 (", x" "Untrack" magit-file-untrack)
464 (", r" "Rename" magit-file-rename)
465 (", k" "Delete" magit-file-delete)
466 (", c" "Checkout" magit-file-checkout)]
467 ["Inspect"
468 ("D" "Diff..." magit-diff)
469 ("d" "Diff" magit-diff-buffer-file)]
470 [""
471 ("L" "Log..." magit-log)
472 ("l" "Log" magit-log-buffer-file :if-not-derived dired-mode)
473 ("l" "Log" magit-dired-log :if-derived dired-mode)
474 ("t" "Trace" magit-log-trace-definition)
475 ("M" "Merged" magit-log-merged :level 7)]
476 [""
477 ("B" "Blame..." magit-blame)
478 ("b" "Blame" magit-blame-addition)
479 ("r" "...removal" magit-blame-removal)
480 ("f" "...reverse" magit-blame-reverse)
481 ("m" "Blame echo" magit-blame-echo)
482 ("q" "Quit blame" magit-blame-quit)]
483 ["Navigate"
484 ("p" "Prev blob" magit-blob-previous)
485 ("n" "Next blob" magit-blob-next)
486 ("v" "Goto blob" magit-find-file)
487 ("V" "Goto file" magit-blob-visit-file)
488 ("g" "Goto status" magit-status-here)
489 ("G" "Goto magit" magit-display-repository-buffer)]
490 ["More actions"
491 ("c" "Commit" magit-commit)
492 ("e" "Edit line" magit-edit-line-commit)]]
493 [:if-not magit-file-relative-name
494 ["File actions"
495 ("s" "Stage" magit-stage-files)
496 ("u" "Unstage" magit-unstage-files)
497 ("x" "Untrack" magit-file-untrack)
498 ("r" "Rename" magit-file-rename)
499 ("k" "Delete" magit-file-delete)
500 ("c" "Checkout" magit-file-checkout)]
501 ["Navigate"
502 ("g" "Goto status" magit-status-here :if-not-mode magit-status-mode)
503 ("G" "Goto magit" magit-display-repository-buffer)]])
504
505 ;;; Blob Mode
506
507 (defvar-keymap magit-blob-mode-map
508 :doc "Keymap for `magit-blob-mode'."
509 "<remap> <read-only-mode>" #'magit-blob-mode
510 "g" #'revert-buffer
511 "p" #'magit-blob-previous
512 "n" #'magit-blob-next
513 "b" #'magit-blame-addition
514 "r" #'magit-blame-removal
515 "f" #'magit-blame-reverse
516 "q" #'magit-bury-or-kill-buffer)
517
518 (define-minor-mode magit-blob-mode
519 "Enable key bindings, which are useful in blob-visiting buffers.
520
521 \\{magit-blob-mode-map}
522 When the user disables `read-only-mode', these bindings would conflict
523 with bindings for `self-insert-command'. To avoid this conflict,
524 `read-only-mode' is remapped to `magit-blob-mode' and disabling the
525 latter disables both modes. Likewise, enabling it, also enables
526 `read-only-mode'.
527
528 When this mode is disabled, many of the commands, for which it would
529 single-character bindings, are accessible via \\[magit-file-dispatch]."
530 :package-version '(magit . "2.3.0")
531 ;; Don't actually call `read-only-mode'. Because
532 ;; that could enable the incompatible `view-mode'.
533 (setq-local buffer-read-only magit-blob-mode)
534 (setq-local read-only-mode--state magit-blob-mode))
535
536 (defun magit-bury-buffer (&optional kill-buffer)
537 "Bury the current buffer, or with a prefix argument kill it.
538
539 If the buffer is used by an Ediff session, refuse to kill or bury just
540 that buffer. That former would break the session and the latter makes
541 little sense in this context. Instead offer to quit the whole session."
542 (interactive "P")
543 (cond ((bound-and-true-p ediff-this-buffer-ediff-sessions)
544 (ediff-quit nil))
545 (kill-buffer (kill-buffer))
546 ((bury-buffer))))
547
548 (defun magit-bury-or-kill-buffer (&optional bury-buffer)
549 "Bury the current buffer if displayed in multiple windows, else kill it.
550
551 With a prefix argument only bury the buffer even if it is only
552 displayed in a single window.
553
554 If the buffer is used by an Ediff session, refuse to kill or bury just
555 that buffer. That former would break the session and the latter makes
556 little sense in this context. Instead offer to quit the whole session."
557 (interactive "P")
558 (cond ((bound-and-true-p ediff-this-buffer-ediff-sessions)
559 (ediff-quit nil))
560 ((or bury-buffer (cdr (get-buffer-window-list nil nil t)))
561 (bury-buffer))
562 ((kill-buffer))))
563
564 (defun magit-kill-this-buffer ()
565 "Kill the current buffer."
566 (interactive)
567 (kill-buffer))
568
569 (transient-define-suffix magit-blob-previous ()
570 "Visit the previous blob which modified the current file."
571 :inapt-if-not (##and$ (magit-buffer-file-name)
572 (magit-blob-ancestor (magit-buffer-revision) $))
573 (interactive)
574 (cond-let
575 [[rev (or magit-buffer-revision "{worktree}")]
576 [file (magit-buffer-file-name)]]
577 ((not file)
578 (user-error "Buffer isn't visiting a file or blob"))
579 ([prev (magit-blob-ancestor rev file)]
580 (apply #'magit-blob-visit prev))
581 ((user-error "You have reached the beginning of time"))))
582
583 (transient-define-suffix magit-blob-next ()
584 "Visit the next blob which modified the current file."
585 :inapt-if-nil 'magit-buffer-file-name
586 (interactive)
587 (cond-let
588 [[rev (or magit-buffer-revision "{worktree}")]
589 [file (magit-buffer-file-name)]]
590 ((not file)
591 (user-error "Buffer isn't visiting a file or blob"))
592 ([next (magit-blob-successor rev file)]
593 (apply #'magit-blob-visit next))
594 ((user-error "You have reached the end of time"))))
595
596 ;;;###autoload
597 (defun magit-blob-visit-file ()
598 "View the file from the worktree corresponding to the current blob.
599 When visiting a blob or the version from the index, then go to
600 the same location in the respective file in the working tree."
601 (interactive)
602 (if-let ((file (magit-file-relative-name)))
603 (pop-to-buffer-same-window (magit-find-file-noselect "{worktree}" file))
604 (user-error "Not visiting a blob")))
605
606 (defun magit-blob-visit (rev file)
607 (magit-find-file rev file)
608 (unless (member rev '("{worktree}" "{index}"))
609 (apply #'message "%s (%s %s ago)"
610 (magit-rev-format "%s" rev)
611 (magit--age (magit-rev-format "%ct" rev)))))
612
613 (defun magit-blob-ancestor (rev file)
614 (pcase rev
615 ((and "{worktree}" (guard (magit-anything-staged-p nil file)))
616 (list "{index}" file))
617 ((or "{worktree}" "{index}")
618 (list (magit-rev-abbrev "HEAD") file))
619 (_ (nth (if rev 1 0)
620 (magit-with-toplevel
621 (seq-partition
622 (magit-git-lines "log" "-2" "--format=%h" "--name-only"
623 "--follow" (or rev "HEAD") "--" file)
624 2))))))
625
626 (defun magit-blob-successor (rev file)
627 (pcase rev
628 ("{worktree}" nil)
629 ("{index}" (list "{worktree}" file))
630 (_ (let ((lines (magit-with-toplevel
631 (magit-git-lines "log" "--format=%h" "--name-only"
632 "--follow" "HEAD" "--" file))))
633 (catch 'found
634 (while lines
635 (if (equal (nth 2 lines) rev)
636 (throw 'found (list (nth 0 lines) (nth 1 lines)))
637 (setq lines (nthcdr 2 lines))))
638 (list (if (magit-anything-staged-p nil file) "{index}" "{worktree}")
639 file))))))
640
641 ;;; File Commands
642
643 ;;;###autoload
644 (defun magit-file-stage ()
645 "Stage all changes to the file being visited in the current buffer."
646 (interactive)
647 (unless buffer-file-name
648 (user-error "Not visiting a file"))
649 (magit-with-toplevel
650 (magit-stage-1 (and (magit-file-ignored-p buffer-file-name)
651 (if (y-or-n-p "Visited file is ignored; stage anyway?")
652 "--force"
653 (user-error "Abort")))
654 (list (magit-file-relative-name)))))
655
656 ;;;###autoload
657 (defun magit-file-unstage ()
658 "Unstage all changes to the file being visited in the current buffer."
659 (interactive)
660 (unless buffer-file-name
661 (user-error "Not visiting a file"))
662 (magit-with-toplevel
663 (magit-unstage-1 (list (magit-file-relative-name)))))
664
665 ;;;###autoload
666 (defun magit-file-untrack (files &optional force)
667 "Untrack the selected FILES or one file read in the minibuffer.
668
669 With a prefix argument FORCE do so even when the files have
670 staged as well as unstaged changes."
671 (interactive (list (or (if-let ((files (magit-region-values 'file t)))
672 (if (magit-file-tracked-p (car files))
673 (magit-confirm-files 'untrack files "Untrack")
674 (user-error "Already untracked"))
675 (list (magit-read-tracked-file "Untrack file" t))))
676 current-prefix-arg))
677 (magit-with-toplevel
678 (magit-run-git "rm" "--cached" (and force "--force") "--" files)))
679
680 ;;;###autoload
681 (defun magit-file-rename (file newname)
682 "Rename or move FILE to NEWNAME.
683 NEWNAME may be a file or directory name. If FILE isn't tracked in
684 Git, fallback to using `rename-file'."
685 (interactive
686 (let* ((file (magit-read-file "Rename file" nil t))
687 (path (expand-file-name file (magit-toplevel))))
688 (list path (expand-file-name
689 (read-file-name (format "Move %s to destination: " file)
690 (file-name-directory path))))))
691 (let ((oldbuf (get-file-buffer file))
692 (dstdir (file-name-directory newname))
693 (dstfile (if (directory-name-p newname)
694 (concat newname (file-name-nondirectory file))
695 newname)))
696 (when (and oldbuf (buffer-modified-p oldbuf))
697 (user-error "Save %s before moving it" file))
698 (when (file-exists-p dstfile)
699 (user-error "%s already exists" dstfile))
700 (unless (file-exists-p dstdir)
701 (user-error "Destination directory %s does not exist" dstdir))
702 (if (magit-file-tracked-p file)
703 (magit-call-git "mv"
704 (magit-convert-filename-for-git file)
705 (magit-convert-filename-for-git newname))
706 (rename-file file newname current-prefix-arg))
707 (when oldbuf
708 (with-current-buffer oldbuf
709 (let ((buffer-read-only buffer-read-only))
710 (set-visited-file-name dstfile nil t))
711 (if (fboundp 'vc-refresh-state)
712 (vc-refresh-state)
713 (with-no-warnings
714 (vc-find-file-hook))))))
715 (magit-refresh))
716
717 ;;;###autoload
718 (defun magit-file-delete (files &optional force)
719 "Delete the selected FILES or one file read in the minibuffer.
720
721 With a prefix argument FORCE do so even when the files have
722 uncommitted changes. When the files aren't being tracked in
723 Git, then fallback to using `delete-file'."
724 (interactive (list (if-let ((files (magit-region-values 'file t)))
725 (magit-confirm-files 'delete files "Delete")
726 (list (magit-read-file "Delete file" nil t)))
727 current-prefix-arg))
728 (let ((args (and force (list "--force"))))
729 (when-let ((dirs (seq-filter #'file-directory-p files)))
730 (if (yes-or-no-p (format "Recursively delete %s %S:"
731 (if (length= dirs 1) "directory" "directories")
732 (string-join dirs ",")))
733 (push "-r" args)
734 (user-error "Abort")))
735 (if (magit-file-tracked-p (car files))
736 (magit-call-git "rm" args "--" files)
737 (let ((topdir (magit-toplevel)))
738 (dolist (file files)
739 (setq file (expand-file-name file topdir))
740 (if (file-directory-p file)
741 (delete-directory file t t)
742 (delete-file file t))))))
743 (magit-refresh))
744
745 ;;;###autoload
746 (defun magit-file-checkout (rev file)
747 "Checkout FILE from REV."
748 (interactive
749 (let ((rev (magit-read-branch-or-commit
750 "Checkout from revision" magit-buffer-revision)))
751 (list rev (magit-read-file-from-rev rev "Checkout file" nil t))))
752 (magit-with-toplevel
753 (magit-run-git "checkout" rev "--" file)))
754
755 ;;; Read File
756
757 (defvar magit-read-file-hist nil)
758
759 (defun magit-read-file-from-rev (rev prompt &optional default include-dirs)
760 (let ((files (magit-revision-files rev)))
761 (when include-dirs
762 (setq files (sort (nconc files (magit-revision-directories rev))
763 #'string<)))
764 (magit-completing-read
765 prompt files nil t nil 'magit-read-file-hist
766 (car (member (or default (magit-current-file)) files)))))
767
768 (defun magit-read-file (prompt &optional tracked-only include-dirs)
769 (let* ((files (nconc (magit-list-files)
770 (and (not tracked-only)
771 (magit-untracked-files))))
772 (choices
773 (if include-dirs
774 (let (dirs)
775 (dolist (file files)
776 (while (and file
777 (setq file (file-name-parent-directory file))
778 (not (equal file "./")))
779 (if (member file dirs)
780 (setq file nil)
781 (push file dirs))))
782 (sort (nconc dirs files) #'string<))
783 files)))
784 (magit-completing-read
785 prompt choices nil t nil nil
786 (car (member (or (magit-section-value-if '(file submodule))
787 (magit-file-relative-name nil tracked-only))
788 choices)))))
789
790 (defun magit-read-tracked-file (prompt &optional include-dirs)
791 (magit-read-file prompt t include-dirs))
792
793 (defun magit-read-unmerged-file (&optional prompt)
794 (let ((current (magit-current-file))
795 (unmerged (magit-unmerged-files)))
796 (unless unmerged
797 (user-error "There are no unresolved conflicts"))
798 (magit-completing-read (or prompt "Resolve file")
799 unmerged nil t nil nil
800 (car (member current unmerged)))))
801
802 (defun magit-read-file-choice (prompt files &optional error default)
803 "Read file from FILES.
804
805 If FILES has only one member, return that instead of prompting.
806 If FILES has no members, give a user error. ERROR can be given
807 to provide a more informative error.
808
809 If DEFAULT is non-nil, use this as the default value instead of
810 `magit-current-file'."
811 (pcase (length files)
812 (0 (user-error (or error "No file choices")))
813 (1 (car files))
814 (_ (magit-completing-read
815 prompt files nil t nil 'magit-read-file-hist
816 (car (member (or default (magit-current-file)) files))))))
817
818 (defun magit-read-changed-file (rev-or-range prompt &optional default)
819 (magit-read-file-choice
820 prompt
821 (magit-changed-files rev-or-range)
822 default
823 (concat "No file changed in " rev-or-range)))
824
825 ;;; _
826
827 (define-obsolete-function-alias 'magit-stage-buffer-file
828 'magit-file-stage "Magit 4.3.2")
829
830 (define-obsolete-function-alias 'magit-unstage-buffer-file
831 'magit-file-unstage "Magit 4.3.2")
832
833 (define-obsolete-function-alias 'magit-find-file-noselect-1
834 'magit-find-file-noselect "Magit 4.4.0")
835
836 (defun magit-find-file--internal (rev file display)
837 (declare (obsolete magit-find-file-noselect "Magit 4.6.0"))
838 (let ((buf (magit-find-file-noselect rev file)))
839 (funcall display buf)
840 buf))
841
842 (defun magit-find-file-index-noselect (file)
843 "Read FILE from the index into a buffer and return the buffer."
844 (declare (obsolete magit-find-file-noselect "Magit 4.6.0"))
845 (magit-find-file-noselect "{index}" file t))
846
847 (provide 'magit-files)
848 ;; Local Variables:
849 ;; read-symbol-shorthands: (
850 ;; ("and$" . "cond-let--and$")
851 ;; ("thread$" . "cond-let--thread$")
852 ;; ("when$" . "cond-let--when$")
853 ;; ("and-let*" . "cond-let--and-let*")
854 ;; ("and-let" . "cond-let--and-let")
855 ;; ("if-let*" . "cond-let--if-let*")
856 ;; ("if-let" . "cond-let--if-let")
857 ;; ("when-let*" . "cond-let--when-let*")
858 ;; ("when-let" . "cond-let--when-let")
859 ;; ("while-let*" . "cond-let--while-let*")
860 ;; ("while-let" . "cond-let--while-let")
861 ;; ("match-string" . "match-string")
862 ;; ("match-str" . "match-string-no-properties"))
863 ;; End:
864 ;;; magit-files.el ends here