fordon_greeman / voidfiles

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