config

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

magit-submodule.el (31924B)


      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 args)
    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   :class 'magit--git-submodule-suffix
    299   :description "Populate       git submodule update --init [--recursive]"
    300   (interactive
    301    (list (magit-module-confirm "Populate" 'magit-module-no-worktree-p)
    302          (magit-submodule-arguments "--recursive")))
    303   (magit-with-toplevel
    304     (magit-run-git-async "submodule" "update" "--init" args "--" modules)))
    305 
    306 ;;;###autoload (autoload 'magit-submodule-update "magit-submodule" nil t)
    307 (transient-define-suffix magit-submodule-update (modules args)
    308   "Update MODULES by checking out the recorded commits.
    309 
    310 With a prefix argument act on all suitable modules.  Otherwise,
    311 if the region selects modules, then act on those.  Otherwise, if
    312 there is a module at point, then act on that.  Otherwise read a
    313 single module from the user."
    314   ;; Unlike `git-submodule's `update' command ours can only update
    315   ;; "initialized" modules by checking out other commits but not
    316   ;; "initialize" modules by creating the working directories.
    317   ;; To do the latter we provide the "setup" command.
    318   :class 'magit--git-submodule-suffix
    319   :description "Update         git submodule update [--force] [--no-fetch]
    320                      [--remote] [--recursive] [--checkout|--rebase|--merge]"
    321   (interactive
    322    (list (magit-module-confirm "Update" 'magit-module-worktree-p)
    323          (magit-submodule-arguments
    324           "--force" "--remote" "--recursive" "--checkout" "--rebase" "--merge"
    325           "--no-fetch")))
    326   (magit-with-toplevel
    327     (magit-run-git-async "submodule" "update" args "--" modules)))
    328 
    329 ;;;###autoload (autoload 'magit-submodule-synchronize "magit-submodule" nil t)
    330 (transient-define-suffix magit-submodule-synchronize (modules args)
    331   "Synchronize url configuration of MODULES.
    332 
    333 With a prefix argument act on all suitable modules.  Otherwise,
    334 if the region selects modules, then act on those.  Otherwise, if
    335 there is a module at point, then act on that.  Otherwise read a
    336 single module from the user."
    337   :class 'magit--git-submodule-suffix
    338   :description "Synchronize    git submodule sync [--recursive]"
    339   (interactive
    340    (list (magit-module-confirm "Synchronize" 'magit-module-worktree-p)
    341          (magit-submodule-arguments "--recursive")))
    342   (magit-with-toplevel
    343     (magit-run-git-async "submodule" "sync" args "--" modules)))
    344 
    345 ;;;###autoload (autoload 'magit-submodule-unpopulate "magit-submodule" nil t)
    346 (transient-define-suffix magit-submodule-unpopulate (modules args)
    347   "Remove working directories of MODULES.
    348 
    349 With a prefix argument act on all suitable modules.  Otherwise,
    350 if the region selects modules, then act on those.  Otherwise, if
    351 there is a module at point, then act on that.  Otherwise read a
    352 single module from the user."
    353   ;; Even when a submodule is "uninitialized" (it has no worktree)
    354   ;; the super-project's $GIT_DIR/config may never-the-less set the
    355   ;; module's url.  This may happen if you `deinit' and then `init'
    356   ;; to register (NOT initialize).  Because the purpose of `deinit'
    357   ;; is to remove the working directory AND to remove the url, this
    358   ;; command does not limit itself to modules that have no working
    359   ;; directory.
    360   :class 'magit--git-submodule-suffix
    361   :description "Unpopulate     git submodule deinit [--force]"
    362   (interactive
    363    (list (magit-module-confirm "Unpopulate")
    364          (magit-submodule-arguments "--force")))
    365   (magit-with-toplevel
    366     (magit-run-git-async "submodule" "deinit" args "--" modules)))
    367 
    368 ;;;###autoload
    369 (defun magit-submodule-remove (modules args trash-gitdirs)
    370   "Unregister MODULES and remove their working directories.
    371 
    372 For safety reasons, do not remove the gitdirs and if a module has
    373 uncommitted changes, then do not remove it at all.  If a module's
    374 gitdir is located inside the working directory, then move it into
    375 the gitdir of the superproject first.
    376 
    377 With the \"--force\" argument offer to remove dirty working
    378 directories and with a prefix argument offer to delete gitdirs.
    379 Both actions are very dangerous and have to be confirmed.  There
    380 are additional safety precautions in place, so you might be able
    381 to recover from making a mistake here, but don't count on it."
    382   (interactive
    383    (list (if-let ((modules (magit-region-values 'magit-module-section t)))
    384              (magit-confirm 'remove-modules nil "Remove %d modules" nil modules)
    385            (list (magit-read-module-path "Remove module")))
    386          (magit-submodule-arguments "--force")
    387          current-prefix-arg))
    388   (when (magit-git-version< "2.12.0")
    389     (error "This command requires Git v2.12.0"))
    390   (when magit-submodule-remove-trash-gitdirs
    391     (setq trash-gitdirs t))
    392   (magit-with-toplevel
    393     (when-let
    394         ((modified
    395           (seq-filter (lambda (module)
    396                         (let ((default-directory (file-name-as-directory
    397                                                   (expand-file-name module))))
    398                           (and (cddr (directory-files default-directory))
    399                                (magit-anything-modified-p))))
    400                       modules)))
    401       (if (member "--force" args)
    402           (if (magit-confirm 'remove-dirty-modules
    403                 "Remove dirty module %s"
    404                 "Remove %d dirty modules"
    405                 t modified)
    406               (dolist (module modified)
    407                 (let ((default-directory (file-name-as-directory
    408                                           (expand-file-name module))))
    409                   (magit-git "stash" "push"
    410                              "-m" "backup before removal of this module")))
    411             (setq modules (cl-set-difference modules modified :test #'equal)))
    412         (if (cdr modified)
    413             (message "Omitting %s modules with uncommitted changes: %s"
    414                      (length modified)
    415                      (string-join modified ", "))
    416           (message "Omitting module %s, it has uncommitted changes"
    417                    (car modified)))
    418         (setq modules (cl-set-difference modules modified :test #'equal))))
    419     (when modules
    420       (let ((alist
    421              (and trash-gitdirs
    422                   (--map (split-string it "\0")
    423                          (magit-git-lines "submodule" "foreach" "-q"
    424                                           "printf \"$sm_path\\0$name\n\"")))))
    425         (magit-git "submodule" "absorbgitdirs" "--" modules)
    426         (magit-git "submodule" "deinit" args "--" modules)
    427         (magit-git "rm" args "--" modules)
    428         (when (and trash-gitdirs
    429                    (magit-confirm 'trash-module-gitdirs
    430                      "Trash gitdir of module %s"
    431                      "Trash gitdirs of %d modules"
    432                      t modules))
    433           (dolist (module modules)
    434             (if-let ((name (cadr (assoc module alist))))
    435                 ;; Disregard if `magit-delete-by-moving-to-trash'
    436                 ;; is nil.  Not doing so would be too dangerous.
    437                 (delete-directory (convert-standard-filename
    438                                    (expand-file-name
    439                                     (concat "modules/" name)
    440                                     (magit-gitdir)))
    441                                   t t)
    442               (error "BUG: Weird module name and/or path for %s" module)))))
    443       (magit-refresh))))
    444 
    445 ;;; Sections
    446 
    447 ;;;###autoload
    448 (defun magit-insert-modules ()
    449   "Insert submodule sections.
    450 Hook `magit-module-sections-hook' controls which module sections
    451 are inserted, and option `magit-module-sections-nested' controls
    452 whether they are wrapped in an additional section."
    453   (when-let ((modules (magit-list-module-paths)))
    454     (if magit-module-sections-nested
    455         (magit-insert-section (modules nil t)
    456           (magit-insert-heading
    457             (format "%s (%s)"
    458                     (propertize "Modules"
    459                                 'font-lock-face 'magit-section-heading)
    460                     (length modules)))
    461           (magit-insert-section-body
    462             (magit--insert-modules)))
    463       (magit--insert-modules))))
    464 
    465 (defun magit--insert-modules (&optional _section)
    466   (magit-run-section-hook 'magit-module-sections-hook))
    467 
    468 ;;;###autoload
    469 (defun magit-insert-modules-overview ()
    470   "Insert sections for all modules.
    471 For each section insert the path and the output of `git describe --tags',
    472 or, failing that, the abbreviated HEAD commit hash."
    473   (when-let ((modules (magit-list-module-paths)))
    474     (magit-insert-section (modules nil t)
    475       (magit-insert-heading
    476         (format "%s (%s)"
    477                 (propertize "Modules overview"
    478                             'font-lock-face 'magit-section-heading)
    479                 (length modules)))
    480       (magit-insert-section-body
    481         (magit--insert-modules-overview)))))
    482 
    483 (defvar magit-modules-overview-align-numbers t)
    484 
    485 (defun magit--insert-modules-overview (&optional _section)
    486   (magit-with-toplevel
    487     (let* ((modules (magit-list-module-paths))
    488            (path-format (format "%%-%ds "
    489                                 (min (apply #'max (mapcar #'length modules))
    490                                      (/ (window-width) 2))))
    491            (branch-format (format "%%-%ds " (min 25 (/ (window-width) 3)))))
    492       (dolist (module modules)
    493         (let ((default-directory
    494                (expand-file-name (file-name-as-directory module))))
    495           (magit-insert-section (module module t)
    496             (insert (propertize (format path-format module)
    497                                 'font-lock-face 'magit-diff-file-heading))
    498             (if (not (file-exists-p ".git"))
    499                 (insert "(unpopulated)")
    500               (insert
    501                (format
    502                 branch-format
    503                 (if-let ((branch (magit-get-current-branch)))
    504                     (propertize branch 'font-lock-face 'magit-branch-local)
    505                   (propertize "(detached)" 'font-lock-face 'warning))))
    506               (if-let ((desc (magit-git-string "describe" "--tags")))
    507                   (progn (when (and magit-modules-overview-align-numbers
    508                                     (string-match-p "\\`[0-9]" desc))
    509                            (insert ?\s))
    510                          (insert (propertize desc 'font-lock-face 'magit-tag)))
    511                 (when-let ((abbrev (magit-rev-format "%h")))
    512                   (insert (propertize abbrev 'font-lock-face 'magit-hash)))))
    513             (insert ?\n))))))
    514   (insert ?\n))
    515 
    516 (defvar-keymap magit-modules-section-map
    517   :doc "Keymap for `modules' sections."
    518   "<remap> <magit-visit-thing>" #'magit-list-submodules
    519   "<1>" (magit-menu-item "List %t" #'magit-list-submodules))
    520 
    521 (defvar-keymap magit-module-section-map
    522   :doc "Keymap for `module' sections."
    523   "C-j"        #'magit-submodule-visit
    524   "C-<return>" #'magit-submodule-visit
    525   "<remap> <magit-unstage-file>" #'magit-unstage
    526   "<remap> <magit-stage-file>"   #'magit-stage
    527   "<remap> <magit-visit-thing>"  #'magit-submodule-visit
    528   "<5>" (magit-menu-item "Module commands..." #'magit-submodule)
    529   "<4>" '(menu-item "--")
    530   "<3>" (magit-menu-item "Unstage %T" #'magit-unstage
    531                          '(:visible (eq (magit-diff-type) 'staged)))
    532   "<2>" (magit-menu-item "Stage %T" #'magit-stage
    533                          '(:visible (eq (magit-diff-type) 'unstaged)))
    534   "<1>" (magit-menu-item "Visit %s" #'magit-submodule-visit))
    535 
    536 (defun magit-submodule-visit (module &optional other-window)
    537   "Visit MODULE by calling `magit-status' on it.
    538 Offer to initialize MODULE if it's not checked out yet.
    539 With a prefix argument, visit in another window."
    540   (interactive (list (or (magit-section-value-if 'module)
    541                          (magit-read-module-path "Visit module"))
    542                      current-prefix-arg))
    543   (magit-with-toplevel
    544     (let ((path (expand-file-name module)))
    545       (cond
    546        ((file-exists-p (expand-file-name ".git" module))
    547         (magit-diff-visit-directory path other-window))
    548        ((y-or-n-p (format "Initialize submodule '%s' first?" module))
    549         (magit-run-git-async "submodule" "update" "--init" "--" module)
    550         (set-process-sentinel
    551          magit-this-process
    552          (lambda (process event)
    553            (let ((magit-process-raise-error t))
    554              (magit-process-sentinel process event))
    555            (when (and (eq (process-status      process) 'exit)
    556                       (=  (process-exit-status process) 0))
    557              (magit-diff-visit-directory path other-window)))))
    558        ((file-exists-p path)
    559         (dired-jump other-window (concat path "/.")))))))
    560 
    561 ;;;###autoload
    562 (defun magit-insert-modules-unpulled-from-upstream ()
    563   "Insert sections for modules that haven't been pulled from the upstream.
    564 These sections can be expanded to show the respective commits."
    565   (magit--insert-modules-logs "Modules unpulled from @{upstream}"
    566                               'modules-unpulled-from-upstream
    567                               "HEAD..@{upstream}"))
    568 
    569 ;;;###autoload
    570 (defun magit-insert-modules-unpulled-from-pushremote ()
    571   "Insert sections for modules that haven't been pulled from the push-remote.
    572 These sections can be expanded to show the respective commits."
    573   (magit--insert-modules-logs "Modules unpulled from @{push}"
    574                               'modules-unpulled-from-pushremote
    575                               "HEAD..@{push}"))
    576 
    577 ;;;###autoload
    578 (defun magit-insert-modules-unpushed-to-upstream ()
    579   "Insert sections for modules that haven't been pushed to the upstream.
    580 These sections can be expanded to show the respective commits."
    581   (magit--insert-modules-logs "Modules unmerged into @{upstream}"
    582                               'modules-unpushed-to-upstream
    583                               "@{upstream}..HEAD"))
    584 
    585 ;;;###autoload
    586 (defun magit-insert-modules-unpushed-to-pushremote ()
    587   "Insert sections for modules that haven't been pushed to the push-remote.
    588 These sections can be expanded to show the respective commits."
    589   (magit--insert-modules-logs "Modules unpushed to @{push}"
    590                               'modules-unpushed-to-pushremote
    591                               "@{push}..HEAD"))
    592 
    593 (defun magit--insert-modules-logs (heading type range)
    594   "For internal use, don't add to a hook."
    595   (when-let (((not (magit-ignore-submodules-p)))
    596              (modules (magit-list-module-paths)))
    597     (magit-insert-section ((eval type) nil t)
    598       (string-match "\\`\\(.+\\) \\([^ ]+\\)\\'" heading)
    599       (magit-insert-heading
    600         (propertize (match-string 1 heading)
    601                     'font-lock-face 'magit-section-heading)
    602         " "
    603         (propertize (match-string 2 heading)
    604                     'font-lock-face 'magit-branch-remote)
    605         ":")
    606       (dolist (module modules)
    607         (when-let* ((default-directory (expand-file-name module))
    608                     ((file-exists-p (expand-file-name ".git")))
    609                     (lines (magit-git-lines "-c" "push.default=current"
    610                                             "log" "--oneline" range))
    611                     (count (length lines))
    612                     ((> count 0)))
    613           (magit-insert-section
    614               ( module module t
    615                 :range range)
    616             (magit-insert-heading count
    617               (propertize module 'font-lock-face 'magit-diff-file-heading))
    618             (dolist (line lines)
    619               (string-match magit-log-module-re line)
    620               (let ((rev (match-string 1 line))
    621                     (msg (match-string 2 line)))
    622                 (magit-insert-section (module-commit rev t)
    623                   (insert (propertize rev 'font-lock-face 'magit-hash) " "
    624                           (funcall magit-log-format-message-function rev msg)
    625                           "\n")))))))
    626       (magit-cancel-section 'if-empty)
    627       (insert ?\n))))
    628 
    629 ;;; List
    630 
    631 ;;;###autoload
    632 (defun magit-list-submodules ()
    633   "Display a list of the current repository's populated submodules."
    634   (interactive)
    635   (magit-submodule-list-setup magit-submodule-list-columns))
    636 
    637 (defvar-keymap magit-submodule-list-mode-map
    638   :doc "Local keymap for Magit-Submodule-List mode buffers."
    639   :parent magit-repolist-mode-map)
    640 
    641 (define-derived-mode magit-submodule-list-mode tabulated-list-mode "Modules"
    642   "Major mode for browsing a list of Git submodules."
    643   :interactive nil
    644   :group 'magit-repolist
    645   (setq-local x-stretch-cursor nil)
    646   (setq tabulated-list-padding 0)
    647   (add-hook 'tabulated-list-revert-hook #'magit-submodule-list-refresh nil t)
    648   (setq imenu-prev-index-position-function
    649         #'magit-repolist--imenu-prev-index-position)
    650   (setq imenu-extract-index-name-function #'tabulated-list-get-id))
    651 
    652 (defvar-local magit-submodule-list-predicate nil)
    653 
    654 (defun magit-submodule-list-setup (columns &optional predicate)
    655   (magit-display-buffer
    656    (or (magit-get-mode-buffer 'magit-submodule-list-mode)
    657        (magit-generate-new-buffer 'magit-submodule-list-mode)))
    658   (magit-submodule-list-mode)
    659   (setq-local magit-repolist-columns columns)
    660   (setq-local magit-repolist-sort-key magit-submodule-list-sort-key)
    661   (setq-local magit-submodule-list-predicate predicate)
    662   (magit-repolist-setup-1)
    663   (magit-submodule-list-refresh))
    664 
    665 (defun magit-submodule-list-refresh ()
    666   (setq tabulated-list-entries
    667         (seq-keep
    668          (lambda (module)
    669            (let ((default-directory
    670                   (expand-file-name (file-name-as-directory module))))
    671              (and (file-exists-p ".git")
    672                   (or (not magit-submodule-list-predicate)
    673                       (funcall magit-submodule-list-predicate module))
    674                   (list module
    675                         (vconcat
    676                          (mapcar (pcase-lambda (`(,title ,width ,fn ,props))
    677                                    (or (funcall fn `((:path  ,module)
    678                                                      (:title ,title)
    679                                                      (:width ,width)
    680                                                      ,@props))
    681                                        ""))
    682                                  magit-repolist-columns))))))
    683          (magit-list-module-paths)))
    684   (message "Listing submodules...")
    685   (tabulated-list-init-header)
    686   (tabulated-list-print t)
    687   (message "Listing submodules...done"))
    688 
    689 (defun magit-modulelist-column-path (spec)
    690   "Insert the relative path of the submodule."
    691   (let ((path (cadr (assq :path spec))))
    692     (or (run-hook-with-args-until-success
    693          'magit-submodule-list-format-path-functions path)
    694         path)))
    695 
    696 ;;; Utilities
    697 
    698 (defun magit-submodule--maybe-reuse-gitdir (name path)
    699   (let ((gitdir (convert-standard-filename
    700                  (expand-file-name (concat "modules/" name)
    701                                    (magit-gitdir)))))
    702     (when (and (file-exists-p gitdir)
    703                (not (file-exists-p path)))
    704       (pcase (read-char-choice
    705               (concat
    706                gitdir " already exists.\n"
    707                "Type [u] to use the existing gitdir and create the working tree\n"
    708                "     [r] to rename the existing gitdir and clone again\n"
    709                "     [t] to trash the existing gitdir and clone again\n"
    710                "   [C-g] to abort ")
    711               '(?u ?r ?t))
    712         (?u (magit-submodule--restore-worktree (expand-file-name path) gitdir))
    713         (?r (rename-file gitdir (concat gitdir "-"
    714                                         (format-time-string "%F-%T"))))
    715         (?t (delete-directory gitdir t t))))))
    716 
    717 (defun magit-submodule--restore-worktree (worktree gitdir)
    718   (make-directory worktree t)
    719   (with-temp-file (expand-file-name ".git" worktree)
    720     (insert "gitdir: " (file-relative-name gitdir worktree) "\n"))
    721   (let ((default-directory worktree))
    722     (magit-call-git "reset" "--hard" "HEAD" "--")))
    723 
    724 ;;; _
    725 (provide 'magit-submodule)
    726 ;;; magit-submodule.el ends here