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