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