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