config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

magit-submodule.el (31720B)


      1 ;;; magit-submodule.el --- Submodule support for Magit  -*- lexical-binding:t -*-
      2 
      3 ;; Copyright (C) 2008-2024 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 ;;; Code:
     24 
     25 (require 'magit)
     26 
     27 (defvar x-stretch-cursor)
     28 
     29 ;;; Options
     30 
     31 (defcustom magit-module-sections-hook
     32   '(magit-insert-modules-overview
     33     magit-insert-modules-unpulled-from-upstream
     34     magit-insert-modules-unpulled-from-pushremote
     35     magit-insert-modules-unpushed-to-upstream
     36     magit-insert-modules-unpushed-to-pushremote)
     37   "Hook run by `magit-insert-modules'.
     38 
     39 That function isn't part of `magit-status-sections-hook's default
     40 value, so you have to add it yourself for this hook to have any
     41 effect."
     42   :package-version '(magit . "2.11.0")
     43   :group 'magit-status
     44   :type 'hook)
     45 
     46 (defcustom magit-module-sections-nested t
     47   "Whether `magit-insert-modules' wraps inserted sections.
     48 
     49 If this is non-nil, then only a single top-level section
     50 is inserted.  If it is nil, then all sections listed in
     51 `magit-module-sections-hook' become top-level sections."
     52   :package-version '(magit . "2.11.0")
     53   :group 'magit-status
     54   :type 'boolean)
     55 
     56 (defcustom magit-submodule-list-mode-hook '(hl-line-mode)
     57   "Hook run after entering Magit-Submodule-List mode."
     58   :package-version '(magit . "2.9.0")
     59   :group 'magit-repolist
     60   :type 'hook
     61   :get 'magit-hook-custom-get
     62   :options '(hl-line-mode))
     63 
     64 (defcustom magit-submodule-list-columns
     65   '(("Path"     25 magit-modulelist-column-path   nil)
     66     ("Version"  25 magit-repolist-column-version
     67      ((:sort magit-repolist-version<)))
     68     ("Branch"   20 magit-repolist-column-branch   nil)
     69     ("B<U" 3 magit-repolist-column-unpulled-from-upstream
     70      ((:right-align t)
     71       (:sort <)))
     72     ("B>U" 3 magit-repolist-column-unpushed-to-upstream
     73      ((:right-align t)
     74       (:sort <)))
     75     ("B<P" 3 magit-repolist-column-unpulled-from-pushremote
     76      ((:right-align t)
     77       (:sort <)))
     78     ("B>P" 3 magit-repolist-column-unpushed-to-pushremote
     79      ((:right-align t)
     80       (:sort <)))
     81     ("B"   3 magit-repolist-column-branches
     82      ((:right-align t)
     83       (:sort <)))
     84     ("S"   3 magit-repolist-column-stashes
     85      ((:right-align t)
     86       (:sort <))))
     87   "List of columns displayed by `magit-list-submodules'.
     88 
     89 Each element has the form (HEADER WIDTH FORMAT PROPS).
     90 
     91 HEADER is the string displayed in the header.  WIDTH is the width
     92 of the column.  FORMAT is a function that is called with one
     93 argument, the repository identification (usually its basename),
     94 and with `default-directory' bound to the toplevel of its working
     95 tree.  It has to return a string to be inserted or nil.  PROPS is
     96 an alist that supports the keys `:right-align', `:pad-right' and
     97 `:sort'.
     98 
     99 The `:sort' function has a weird interface described in the
    100 docstring of `tabulated-list--get-sort'.  Alternatively `<' and
    101 `magit-repolist-version<' can be used as those functions are
    102 automatically replaced with functions that satisfy the interface.
    103 Set `:sort' to nil to inhibit sorting; if unspecified, then the
    104 column is sortable using the default sorter.
    105 
    106 You may wish to display a range of numeric columns using just one
    107 character per column and without any padding between columns, in
    108 which case you should use an appropriate HEADER, set WIDTH to 1,
    109 and set `:pad-right' to 0.  \"+\" is substituted for numbers higher
    110 than 9."
    111   :package-version '(magit . "2.8.0")
    112   :group 'magit-repolist
    113   :type `(repeat (list :tag "Column"
    114                        (string   :tag "Header Label")
    115                        (integer  :tag "Column Width")
    116                        (function :tag "Inserter Function")
    117                        (repeat   :tag "Properties"
    118                                  (list (choice :tag "Property"
    119                                                (const :right-align)
    120                                                (const :pad-right)
    121                                                (const :sort)
    122                                                (symbol))
    123                                        (sexp   :tag "Value"))))))
    124 
    125 (defcustom magit-submodule-list-sort-key '("Path" . nil)
    126   "Initial sort key for buffer created by `magit-list-submodules'.
    127 If nil, no additional sorting is performed.  Otherwise, this
    128 should be a cons cell (NAME . FLIP).  NAME is a string matching
    129 one of the column names in `magit-submodule-list-columns'.  FLIP,
    130 if non-nil, means to invert the resulting sort."
    131   :package-version '(magit . "3.2.0")
    132   :group 'magit-repolist
    133   :type '(choice (const nil)
    134                  (cons (string :tag "Column name")
    135                        (boolean :tag "Flip order"))))
    136 
    137 (defvar magit-submodule-list-format-path-functions nil)
    138 
    139 (defcustom magit-submodule-remove-trash-gitdirs nil
    140   "Whether `magit-submodule-remove' offers to trash module gitdirs.
    141 
    142 If this is nil, then that command does not offer to do so unless
    143 a prefix argument is used.  When this is t, then it does offer to
    144 do so even without a prefix argument.
    145 
    146 In both cases the action still has to be confirmed unless that is
    147 disabled using the option `magit-no-confirm'.  Doing the latter
    148 and also setting this variable to t will lead to tears."
    149   :package-version '(magit . "2.90.0")
    150   :group 'magit-commands
    151   :type 'boolean)
    152 
    153 ;;; Popup
    154 
    155 ;;;###autoload (autoload 'magit-submodule "magit-submodule" nil t)
    156 (transient-define-prefix magit-submodule ()
    157   "Act on a submodule."
    158   :man-page "git-submodule"
    159   ["Arguments"
    160    ("-f" "Force"            ("-f" "--force"))
    161    ("-r" "Recursive"        "--recursive")
    162    ("-N" "Do not fetch"     ("-N" "--no-fetch"))
    163    ("-C" "Checkout tip"     "--checkout")
    164    ("-R" "Rebase onto tip"  "--rebase")
    165    ("-M" "Merge tip"        "--merge")
    166    ("-U" "Use upstream tip" "--remote")]
    167   ["One module actions"
    168    ("a" magit-submodule-add)
    169    ("r" magit-submodule-register)
    170    ("p" magit-submodule-populate)
    171    ("u" magit-submodule-update)
    172    ("s" magit-submodule-synchronize)
    173    ("d" magit-submodule-unpopulate)
    174    ("k" "Remove" magit-submodule-remove)]
    175   ["Populated modules actions"
    176    ("l" "List modules"  magit-list-submodules)
    177    ("f" "Fetch modules" magit-fetch-modules)])
    178 
    179 (defun magit-submodule-arguments (&rest filters)
    180   (--filter (and (member it filters) it)
    181             (transient-args 'magit-submodule)))
    182 
    183 (defclass magit--git-submodule-suffix (transient-suffix)
    184   ())
    185 
    186 (cl-defmethod transient-format-description ((obj magit--git-submodule-suffix))
    187   (let ((value (delq nil (mapcar #'transient-infix-value transient--suffixes))))
    188     (replace-regexp-in-string
    189      "\\[--[^]]+\\]"
    190      (lambda (match)
    191        (format (propertize "[%s]" 'face 'transient-inactive-argument)
    192                (mapconcat (lambda (arg)
    193                             (propertize arg 'face
    194                                         (if (member arg value)
    195                                             'transient-argument
    196                                           'transient-inactive-argument)))
    197                           (save-match-data
    198                             (split-string (substring match 1 -1) "|"))
    199                           (propertize "|" 'face 'transient-inactive-argument))))
    200      (cl-call-next-method obj))))
    201 
    202 ;;;###autoload (autoload 'magit-submodule-add "magit-submodule" nil t)
    203 (transient-define-suffix magit-submodule-add (url &optional path name args)
    204   "Add the repository at URL as a module.
    205 
    206 Optional PATH is the path to the module relative to the root of
    207 the superproject.  If it is nil, then the path is determined
    208 based on the URL.  Optional NAME is the name of the module.  If
    209 it is nil, then PATH also becomes the name."
    210   :class 'magit--git-submodule-suffix
    211   :description "Add            git submodule add [--force]"
    212   (interactive
    213    (magit-with-toplevel
    214      (let* ((url (magit-read-string-ns "Add submodule (remote url)"))
    215             (path (let ((read-file-name-function
    216                          (if (or (eq read-file-name-function 'ido-read-file-name)
    217                                  (advice-function-member-p
    218                                   'ido-read-file-name
    219                                   read-file-name-function))
    220                              ;; The Ido variant doesn't work properly here.
    221                              #'read-file-name-default
    222                            read-file-name-function)))
    223                     (directory-file-name
    224                      (file-relative-name
    225                       (read-directory-name
    226                        "Add submodules at path: " nil nil nil
    227                        (and (string-match "\\([^./]+\\)\\(\\.git\\)?$" url)
    228                             (match-string 1 url))))))))
    229        (list url
    230              (directory-file-name path)
    231              (magit-submodule-read-name-for-path path)
    232              (magit-submodule-arguments "--force")))))
    233   (magit-submodule-add-1 url path name args))
    234 
    235 (defun magit-submodule-add-1 (url &optional path name args)
    236   (magit-with-toplevel
    237     (magit-submodule--maybe-reuse-gitdir name path)
    238     (magit-run-git-async "submodule" "add"
    239                          (and name (list "--name" name))
    240                          args "--" url path)
    241     (set-process-sentinel
    242      magit-this-process
    243      (lambda (process event)
    244        (when (memq (process-status process) '(exit signal))
    245          (if (> (process-exit-status process) 0)
    246              (magit-process-sentinel process event)
    247            (process-put process 'inhibit-refresh t)
    248            (magit-process-sentinel process event)
    249            (when (magit-git-version>= "2.12.0")
    250              (magit-call-git "submodule" "absorbgitdirs" path))
    251            (magit-refresh)))))))
    252 
    253 ;;;###autoload
    254 (defun magit-submodule-read-name-for-path (path &optional prefer-short)
    255   (let* ((path (directory-file-name (file-relative-name path)))
    256          (name (file-name-nondirectory path)))
    257     (push (if prefer-short path name) minibuffer-history)
    258     (magit-read-string-ns
    259      "Submodule name" nil (cons 'minibuffer-history 2)
    260      (or (--keep (pcase-let ((`(,var ,val) (split-string it "=")))
    261                    (and (equal val path)
    262                         (cadr (split-string var "\\."))))
    263                  (magit-git-lines "config" "--list" "-f" ".gitmodules"))
    264          (if prefer-short name path)))))
    265 
    266 ;;;###autoload (autoload 'magit-submodule-register "magit-submodule" nil t)
    267 (transient-define-suffix magit-submodule-register (modules)
    268   "Register MODULES.
    269 
    270 With a prefix argument act on all suitable modules.  Otherwise,
    271 if the region selects modules, then act on those.  Otherwise, if
    272 there is a module at point, then act on that.  Otherwise read a
    273 single module from the user."
    274   ;; This command and the underlying "git submodule init" do NOT
    275   ;; "initialize" modules.  They merely "register" modules in the
    276   ;; super-projects $GIT_DIR/config file, the purpose of which is to
    277   ;; allow users to change such values before actually initializing
    278   ;; the modules.
    279   :description "Register       git submodule init"
    280   (interactive
    281    (list (magit-module-confirm "Register" 'magit-module-no-worktree-p)))
    282   (magit-with-toplevel
    283     (magit-run-git-async "submodule" "init" "--" modules)))
    284 
    285 ;;;###autoload (autoload 'magit-submodule-populate "magit-submodule" nil t)
    286 (transient-define-suffix magit-submodule-populate (modules)
    287   "Create MODULES working directories, checking out the recorded commits.
    288 
    289 With a prefix argument act on all suitable modules.  Otherwise,
    290 if the region selects modules, then act on those.  Otherwise, if
    291 there is a module at point, then act on that.  Otherwise read a
    292 single module from the user."
    293   ;; This is the command that actually "initializes" modules.
    294   ;; A module is initialized when it has a working directory,
    295   ;; a gitlink, and a .gitmodules entry.
    296   :description "Populate       git submodule update --init"
    297   (interactive
    298    (list (magit-module-confirm "Populate" 'magit-module-no-worktree-p)))
    299   (magit-with-toplevel
    300     (magit-run-git-async "submodule" "update" "--init" "--" modules)))
    301 
    302 ;;;###autoload (autoload 'magit-submodule-update "magit-submodule" nil t)
    303 (transient-define-suffix magit-submodule-update (modules args)
    304   "Update MODULES by checking out the recorded commits.
    305 
    306 With a prefix argument act on all suitable modules.  Otherwise,
    307 if the region selects modules, then act on those.  Otherwise, if
    308 there is a module at point, then act on that.  Otherwise read a
    309 single module from the user."
    310   ;; Unlike `git-submodule's `update' command ours can only update
    311   ;; "initialized" modules by checking out other commits but not
    312   ;; "initialize" modules by creating the working directories.
    313   ;; To do the latter we provide the "setup" command.
    314   :class 'magit--git-submodule-suffix
    315   :description "Update         git submodule update [--force] [--no-fetch]
    316                      [--remote] [--recursive] [--checkout|--rebase|--merge]"
    317   (interactive
    318    (list (magit-module-confirm "Update" 'magit-module-worktree-p)
    319          (magit-submodule-arguments
    320           "--force" "--remote" "--recursive" "--checkout" "--rebase" "--merge"
    321           "--no-fetch")))
    322   (magit-with-toplevel
    323     (magit-run-git-async "submodule" "update" args "--" modules)))
    324 
    325 ;;;###autoload (autoload 'magit-submodule-synchronize "magit-submodule" nil t)
    326 (transient-define-suffix magit-submodule-synchronize (modules args)
    327   "Synchronize url configuration of MODULES.
    328 
    329 With a prefix argument act on all suitable modules.  Otherwise,
    330 if the region selects modules, then act on those.  Otherwise, if
    331 there is a module at point, then act on that.  Otherwise read a
    332 single module from the user."
    333   :class 'magit--git-submodule-suffix
    334   :description "Synchronize    git submodule sync [--recursive]"
    335   (interactive
    336    (list (magit-module-confirm "Synchronize" 'magit-module-worktree-p)
    337          (magit-submodule-arguments "--recursive")))
    338   (magit-with-toplevel
    339     (magit-run-git-async "submodule" "sync" args "--" modules)))
    340 
    341 ;;;###autoload (autoload 'magit-submodule-unpopulate "magit-submodule" nil t)
    342 (transient-define-suffix magit-submodule-unpopulate (modules args)
    343   "Remove working directories of MODULES.
    344 
    345 With a prefix argument act on all suitable modules.  Otherwise,
    346 if the region selects modules, then act on those.  Otherwise, if
    347 there is a module at point, then act on that.  Otherwise read a
    348 single module from the user."
    349   ;; Even though a package is "uninitialized" (it has no worktree)
    350   ;; the super-projects $GIT_DIR/config may never-the-less set the
    351   ;; module's url.  This may happen if you `deinit' and then `init'
    352   ;; to register (NOT initialize).  Because the purpose of `deinit'
    353   ;; is to remove the working directory AND to remove the url, this
    354   ;; command does not limit itself to modules that have no working
    355   ;; directory.
    356   :class 'magit--git-submodule-suffix
    357   :description "Unpopulate     git submodule deinit [--force]"
    358   (interactive
    359    (list (magit-module-confirm "Unpopulate")
    360          (magit-submodule-arguments "--force")))
    361   (magit-with-toplevel
    362     (magit-run-git-async "submodule" "deinit" args "--" modules)))
    363 
    364 ;;;###autoload
    365 (defun magit-submodule-remove (modules args trash-gitdirs)
    366   "Unregister MODULES and remove their working directories.
    367 
    368 For safety reasons, do not remove the gitdirs and if a module has
    369 uncommitted changes, then do not remove it at all.  If a module's
    370 gitdir is located inside the working directory, then move it into
    371 the gitdir of the superproject first.
    372 
    373 With the \"--force\" argument offer to remove dirty working
    374 directories and with a prefix argument offer to delete gitdirs.
    375 Both actions are very dangerous and have to be confirmed.  There
    376 are additional safety precautions in place, so you might be able
    377 to recover from making a mistake here, but don't count on it."
    378   (interactive
    379    (list (if-let ((modules (magit-region-values 'magit-module-section t)))
    380              (magit-confirm 'remove-modules nil "Remove %d modules" nil modules)
    381            (list (magit-read-module-path "Remove module")))
    382          (magit-submodule-arguments "--force")
    383          current-prefix-arg))
    384   (when (magit-git-version< "2.12.0")
    385     (error "This command requires Git v2.12.0"))
    386   (when magit-submodule-remove-trash-gitdirs
    387     (setq trash-gitdirs t))
    388   (magit-with-toplevel
    389     (when-let
    390         ((modified
    391           (seq-filter (lambda (module)
    392                         (let ((default-directory (file-name-as-directory
    393                                                   (expand-file-name module))))
    394                           (and (cddr (directory-files default-directory))
    395                                (magit-anything-modified-p))))
    396                       modules)))
    397       (if (member "--force" args)
    398           (if (magit-confirm 'remove-dirty-modules
    399                 "Remove dirty module %s"
    400                 "Remove %d dirty modules"
    401                 t modified)
    402               (dolist (module modified)
    403                 (let ((default-directory (file-name-as-directory
    404                                           (expand-file-name module))))
    405                   (magit-git "stash" "push"
    406                              "-m" "backup before removal of this module")))
    407             (setq modules (cl-set-difference modules modified :test #'equal)))
    408         (if (cdr modified)
    409             (message "Omitting %s modules with uncommitted changes: %s"
    410                      (length modified)
    411                      (mapconcat #'identity modified ", "))
    412           (message "Omitting module %s, it has uncommitted changes"
    413                    (car modified)))
    414         (setq modules (cl-set-difference modules modified :test #'equal))))
    415     (when modules
    416       (let ((alist
    417              (and trash-gitdirs
    418                   (--map (split-string it "\0")
    419                          (magit-git-lines "submodule" "foreach" "-q"
    420                                           "printf \"$sm_path\\0$name\n\"")))))
    421         (magit-git "submodule" "absorbgitdirs" "--" modules)
    422         (magit-git "submodule" "deinit" args "--" modules)
    423         (magit-git "rm" args "--" modules)
    424         (when (and trash-gitdirs
    425                    (magit-confirm 'trash-module-gitdirs
    426                      "Trash gitdir of module %s"
    427                      "Trash gitdirs of %d modules"
    428                      t modules))
    429           (dolist (module modules)
    430             (if-let ((name (cadr (assoc module alist))))
    431                 ;; Disregard if `magit-delete-by-moving-to-trash'
    432                 ;; is nil.  Not doing so would be too dangerous.
    433                 (delete-directory (convert-standard-filename
    434                                    (expand-file-name
    435                                     (concat "modules/" name)
    436                                     (magit-gitdir)))
    437                                   t t)
    438               (error "BUG: Weird module name and/or path for %s" module)))))
    439       (magit-refresh))))
    440 
    441 ;;; Sections
    442 
    443 ;;;###autoload
    444 (defun magit-insert-modules ()
    445   "Insert submodule sections.
    446 Hook `magit-module-sections-hook' controls which module sections
    447 are inserted, and option `magit-module-sections-nested' controls
    448 whether they are wrapped in an additional section."
    449   (when-let ((modules (magit-list-module-paths)))
    450     (if magit-module-sections-nested
    451         (magit-insert-section (modules nil t)
    452           (magit-insert-heading
    453             (format "%s (%s)"
    454                     (propertize "Modules"
    455                                 'font-lock-face 'magit-section-heading)
    456                     (length modules)))
    457           (magit-insert-section-body
    458             (magit--insert-modules)))
    459       (magit--insert-modules))))
    460 
    461 (defun magit--insert-modules (&optional _section)
    462   (magit-run-section-hook 'magit-module-sections-hook))
    463 
    464 ;;;###autoload
    465 (defun magit-insert-modules-overview ()
    466   "Insert sections for all modules.
    467 For each section insert the path and the output of `git describe --tags',
    468 or, failing that, the abbreviated HEAD commit hash."
    469   (when-let ((modules (magit-list-module-paths)))
    470     (magit-insert-section (modules nil t)
    471       (magit-insert-heading
    472         (format "%s (%s)"
    473                 (propertize "Modules overview"
    474                             'font-lock-face 'magit-section-heading)
    475                 (length modules)))
    476       (magit-insert-section-body
    477         (magit--insert-modules-overview)))))
    478 
    479 (defvar magit-modules-overview-align-numbers t)
    480 
    481 (defun magit--insert-modules-overview (&optional _section)
    482   (magit-with-toplevel
    483     (let* ((modules (magit-list-module-paths))
    484            (path-format (format "%%-%ds "
    485                                 (min (apply #'max (mapcar #'length modules))
    486                                      (/ (window-width) 2))))
    487            (branch-format (format "%%-%ds " (min 25 (/ (window-width) 3)))))
    488       (dolist (module modules)
    489         (let ((default-directory
    490                (expand-file-name (file-name-as-directory module))))
    491           (magit-insert-section (magit-module-section module t)
    492             (insert (propertize (format path-format module)
    493                                 'font-lock-face 'magit-diff-file-heading))
    494             (if (not (file-exists-p ".git"))
    495                 (insert "(unpopulated)")
    496               (insert
    497                (format
    498                 branch-format
    499                 (if-let ((branch (magit-get-current-branch)))
    500                     (propertize branch 'font-lock-face 'magit-branch-local)
    501                   (propertize "(detached)" 'font-lock-face 'warning))))
    502               (if-let ((desc (magit-git-string "describe" "--tags")))
    503                   (progn (when (and magit-modules-overview-align-numbers
    504                                     (string-match-p "\\`[0-9]" desc))
    505                            (insert ?\s))
    506                          (insert (propertize desc 'font-lock-face 'magit-tag)))
    507                 (when-let ((abbrev (magit-rev-format "%h")))
    508                   (insert (propertize abbrev 'font-lock-face 'magit-hash)))))
    509             (insert ?\n))))))
    510   (insert ?\n))
    511 
    512 (defvar-keymap magit-modules-section-map
    513   :doc "Keymap for `modules' sections."
    514   "<remap> <magit-visit-thing>" #'magit-list-submodules
    515   "<1>" (magit-menu-item "List %t" #'magit-list-submodules))
    516 
    517 (defvar-keymap magit-module-section-map
    518   :doc "Keymap for `module' sections."
    519   "C-j"        #'magit-submodule-visit
    520   "C-<return>" #'magit-submodule-visit
    521   "<remap> <magit-unstage-file>" #'magit-unstage
    522   "<remap> <magit-stage-file>"   #'magit-stage
    523   "<remap> <magit-visit-thing>"  #'magit-submodule-visit
    524   "<5>" (magit-menu-item "Module commands..." #'magit-submodule)
    525   "<4>" '(menu-item "--")
    526   "<3>" (magit-menu-item "Unstage %T" #'magit-unstage
    527                          '(:visible (eq (magit-diff-type) 'staged)))
    528   "<2>" (magit-menu-item "Stage %T" #'magit-stage
    529                          '(:visible (eq (magit-diff-type) 'unstaged)))
    530   "<1>" (magit-menu-item "Visit %s" #'magit-submodule-visit))
    531 
    532 (defun magit-submodule-visit (module &optional other-window)
    533   "Visit MODULE by calling `magit-status' on it.
    534 Offer to initialize MODULE if it's not checked out yet.
    535 With a prefix argument, visit in another window."
    536   (interactive (list (or (magit-section-value-if 'module)
    537                          (magit-read-module-path "Visit module"))
    538                      current-prefix-arg))
    539   (magit-with-toplevel
    540     (let ((path (expand-file-name module)))
    541       (cond
    542        ((file-exists-p (expand-file-name ".git" module))
    543         (magit-diff-visit-directory path other-window))
    544        ((y-or-n-p (format "Initialize submodule '%s' first?" module))
    545         (magit-run-git-async "submodule" "update" "--init" "--" module)
    546         (set-process-sentinel
    547          magit-this-process
    548          (lambda (process event)
    549            (let ((magit-process-raise-error t))
    550              (magit-process-sentinel process event))
    551            (when (and (eq (process-status      process) 'exit)
    552                       (=  (process-exit-status process) 0))
    553              (magit-diff-visit-directory path other-window)))))
    554        ((file-exists-p path)
    555         (dired-jump other-window (concat path "/.")))))))
    556 
    557 ;;;###autoload
    558 (defun magit-insert-modules-unpulled-from-upstream ()
    559   "Insert sections for modules that haven't been pulled from the upstream.
    560 These sections can be expanded to show the respective commits."
    561   (magit--insert-modules-logs "Modules unpulled from @{upstream}"
    562                               'modules-unpulled-from-upstream
    563                               "HEAD..@{upstream}"))
    564 
    565 ;;;###autoload
    566 (defun magit-insert-modules-unpulled-from-pushremote ()
    567   "Insert sections for modules that haven't been pulled from the push-remote.
    568 These sections can be expanded to show the respective commits."
    569   (magit--insert-modules-logs "Modules unpulled from @{push}"
    570                               'modules-unpulled-from-pushremote
    571                               "HEAD..@{push}"))
    572 
    573 ;;;###autoload
    574 (defun magit-insert-modules-unpushed-to-upstream ()
    575   "Insert sections for modules that haven't been pushed to the upstream.
    576 These sections can be expanded to show the respective commits."
    577   (magit--insert-modules-logs "Modules unmerged into @{upstream}"
    578                               'modules-unpushed-to-upstream
    579                               "@{upstream}..HEAD"))
    580 
    581 ;;;###autoload
    582 (defun magit-insert-modules-unpushed-to-pushremote ()
    583   "Insert sections for modules that haven't been pushed to the push-remote.
    584 These sections can be expanded to show the respective commits."
    585   (magit--insert-modules-logs "Modules unpushed to @{push}"
    586                               'modules-unpushed-to-pushremote
    587                               "@{push}..HEAD"))
    588 
    589 (defun magit--insert-modules-logs (heading type range)
    590   "For internal use, don't add to a hook."
    591   (unless (magit-ignore-submodules-p)
    592     (when-let ((modules (magit-list-module-paths)))
    593       (magit-insert-section ((eval type) nil t)
    594         (string-match "\\`\\(.+\\) \\([^ ]+\\)\\'" heading)
    595         (magit-insert-heading
    596           (propertize (match-string 1 heading)
    597                       'font-lock-face 'magit-section-heading)
    598           " "
    599           (propertize (match-string 2 heading)
    600                       'font-lock-face 'magit-branch-remote)
    601           ":")
    602         (magit-with-toplevel
    603           (dolist (module modules)
    604             (when (magit-module-worktree-p module)
    605               (let ((default-directory
    606                      (expand-file-name (file-name-as-directory module))))
    607                 (when (file-accessible-directory-p default-directory)
    608                   (magit-insert-section sec (magit-module-section module t)
    609                     (magit-insert-heading
    610                       (propertize module
    611                                   'font-lock-face 'magit-diff-file-heading)
    612                       ":")
    613                     (oset sec range range)
    614                     (magit-git-wash
    615                         (apply-partially #'magit-log-wash-log 'module)
    616                       "-c" "push.default=current" "log" "--oneline" range)
    617                     (when (> (point)
    618                              (oref sec content))
    619                       (delete-char -1))))))))
    620         (magit-cancel-section 'if-empty)
    621         (insert ?\n)))))
    622 
    623 ;;; List
    624 
    625 ;;;###autoload
    626 (defun magit-list-submodules ()
    627   "Display a list of the current repository's populated submodules."
    628   (interactive)
    629   (magit-submodule-list-setup magit-submodule-list-columns))
    630 
    631 (defvar-keymap magit-submodule-list-mode-map
    632   :doc "Local keymap for Magit-Submodule-List mode buffers."
    633   :parent magit-repolist-mode-map)
    634 
    635 (define-derived-mode magit-submodule-list-mode tabulated-list-mode "Modules"
    636   "Major mode for browsing a list of Git submodules."
    637   :group 'magit-repolist-mode
    638   (setq-local x-stretch-cursor nil)
    639   (setq tabulated-list-padding 0)
    640   (add-hook 'tabulated-list-revert-hook #'magit-submodule-list-refresh nil t)
    641   (setq imenu-prev-index-position-function
    642         #'magit-repolist--imenu-prev-index-position)
    643   (setq imenu-extract-index-name-function #'tabulated-list-get-id))
    644 
    645 (defvar-local magit-submodule-list-predicate nil)
    646 
    647 (defun magit-submodule-list-setup (columns &optional predicate)
    648   (magit-display-buffer
    649    (or (magit-get-mode-buffer 'magit-submodule-list-mode)
    650        (magit-generate-new-buffer 'magit-submodule-list-mode)))
    651   (magit-submodule-list-mode)
    652   (setq-local magit-repolist-columns columns)
    653   (setq-local magit-repolist-sort-key magit-submodule-list-sort-key)
    654   (setq-local magit-submodule-list-predicate predicate)
    655   (magit-repolist-setup-1)
    656   (magit-submodule-list-refresh))
    657 
    658 (defun magit-submodule-list-refresh ()
    659   (setq tabulated-list-entries
    660         (seq-keep
    661          (lambda (module)
    662            (let ((default-directory
    663                   (expand-file-name (file-name-as-directory module))))
    664              (and (file-exists-p ".git")
    665                   (or (not magit-submodule-list-predicate)
    666                       (funcall magit-submodule-list-predicate module))
    667                   (list module
    668                         (vconcat
    669                          (mapcar (pcase-lambda (`(,title ,width ,fn ,props))
    670                                    (or (funcall fn `((:path  ,module)
    671                                                      (:title ,title)
    672                                                      (:width ,width)
    673                                                      ,@props))
    674                                        ""))
    675                                  magit-repolist-columns))))))
    676          (magit-list-module-paths)))
    677   (message "Listing submodules...")
    678   (tabulated-list-init-header)
    679   (tabulated-list-print t)
    680   (message "Listing submodules...done"))
    681 
    682 (defun magit-modulelist-column-path (spec)
    683   "Insert the relative path of the submodule."
    684   (let ((path (cadr (assq :path spec))))
    685     (or (run-hook-with-args-until-success
    686          'magit-submodule-list-format-path-functions path)
    687         path)))
    688 
    689 ;;; Utilities
    690 
    691 (defun magit-submodule--maybe-reuse-gitdir (name path)
    692   (let ((gitdir (convert-standard-filename
    693                  (expand-file-name (concat "modules/" name)
    694                                    (magit-gitdir)))))
    695     (when (and (file-exists-p gitdir)
    696                (not (file-exists-p path)))
    697       (pcase (read-char-choice
    698               (concat
    699                gitdir " already exists.\n"
    700                "Type [u] to use the existing gitdir and create the working tree\n"
    701                "     [r] to rename the existing gitdir and clone again\n"
    702                "     [t] to trash the existing gitdir and clone again\n"
    703                "   [C-g] to abort ")
    704               '(?u ?r ?t))
    705         (?u (magit-submodule--restore-worktree (expand-file-name path) gitdir))
    706         (?r (rename-file gitdir (concat gitdir "-"
    707                                         (format-time-string "%F-%T"))))
    708         (?t (delete-directory gitdir t t))))))
    709 
    710 (defun magit-submodule--restore-worktree (worktree gitdir)
    711   (make-directory worktree t)
    712   (with-temp-file (expand-file-name ".git" worktree)
    713     (insert "gitdir: " (file-relative-name gitdir worktree) "\n"))
    714   (let ((default-directory worktree))
    715     (magit-call-git "reset" "--hard" "HEAD" "--")))
    716 
    717 ;;; _
    718 (provide 'magit-submodule)
    719 ;;; magit-submodule.el ends here