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