config

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

magit-submodule.el (31865B)


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