dotemacs

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

magit-submodule.el (31114B)


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