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