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