dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

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