fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 22h ago
340 lines · 12.9 KB Raw Blame History
1 ;;; magit-bisect.el --- Bisect 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 ;; Use a binary search to find the commit that introduced a bug.
26
27 ;;; Code:
28
29 (require 'magit)
30
31 ;;; Options
32
33 (defcustom magit-bisect-show-graph t
34 "Whether to use \"--graph\" in the log showing commits yet to be bisected."
35 :package-version '(magit . "2.8.0")
36 :group 'magit-status
37 :type 'boolean)
38
39 (defface magit-bisect-good
40 '((t :foreground "DarkOliveGreen"))
41 "Face for good bisect revisions."
42 :group 'magit-faces)
43
44 (defface magit-bisect-skip
45 '((t :foreground "DarkGoldenrod"))
46 "Face for skipped bisect revisions."
47 :group 'magit-faces)
48
49 (defface magit-bisect-bad
50 '((t :foreground "IndianRed4"))
51 "Face for bad bisect revisions."
52 :group 'magit-faces)
53
54 ;;; Commands
55
56 ;;;###autoload(autoload 'magit-bisect "magit-bisect" nil t)
57 (transient-define-prefix magit-bisect ()
58 "Narrow in on the commit that introduced a bug."
59 :man-page "git-bisect"
60 [:class transient-subgroups
61 :if-not magit-bisect-in-progress-p
62 ["Arguments"
63 ("-n" "Don't checkout commits" "--no-checkout")
64 ("-p" "Follow only first parent of a merge" "--first-parent"
65 :if (##magit-git-version>= "2.29"))
66 (magit-bisect:--term-old :level 6)
67 (magit-bisect:--term-new :level 6)]
68 ["Actions"
69 ("B" "Start" magit-bisect-start)
70 ("s" "Start script" magit-bisect-run)]]
71 ["Actions"
72 :if magit-bisect-in-progress-p
73 ("B" "Bad" magit-bisect-bad)
74 ("g" "Good" magit-bisect-good)
75 ("m" "Mark" magit-bisect-mark :level 6)
76 ("k" "Skip" magit-bisect-skip)
77 ("r" "Reset" magit-bisect-reset)
78 ("s" "Run script" magit-bisect-run)])
79
80 (transient-define-argument magit-bisect:--term-old ()
81 :description "Old/good term"
82 :class 'transient-option
83 :key "=o"
84 :argument "--term-old=")
85
86 (transient-define-argument magit-bisect:--term-new ()
87 :description "New/bad term"
88 :class 'transient-option
89 :key "=n"
90 :argument "--term-new=")
91
92 ;;;###autoload
93 (defun magit-bisect-start (bad good args)
94 "Start a bisect session.
95
96 Bisecting a bug means to find the commit that introduced it.
97 This command starts such a bisect session by asking for a known
98 good and a known bad commit. To move the session forward use the
99 other actions from the bisect transient command (\
100 \\<magit-status-mode-map>\\[magit-bisect])."
101 (interactive (if (magit-bisect-in-progress-p)
102 (user-error "Already bisecting")
103 (magit-bisect-start-read-args)))
104 (magit-bisect-start--assert bad good args)
105 (magit-repository-local-set 'bisect--first-parent
106 (transient-arg-value "--first-parent" args))
107 (magit-git-bisect "start" (list args bad good) t))
108
109 (defun magit-bisect-start-read-args ()
110 (let ((args (transient-args 'magit-bisect))
111 (revs (magit-region-values '(commit branch) t)))
112 (or (and revs
113 (let ((a (car revs))
114 (b (last revs)))
115 (cond ((magit-rev-ancestor-p a b) (list b a args))
116 ((magit-rev-ancestor-p b a) (list a b args)))))
117 (let ((bad (magit-read-branch-or-commit
118 (format "Start bisect with %s revision"
119 (or (transient-arg-value "--term-new=" args)
120 "bad")))))
121 (list bad
122 (magit-read-other-branch-or-commit
123 (format "%s revision"
124 (or (transient-arg-value "--term-old=" args)
125 "Good"))
126 bad)
127 args)))))
128
129 (defun magit-bisect-start--assert (bad good args)
130 (unless (magit-merge-base bad good)
131 (user-error
132 "%s `%s' or merge-base has to be an ancestor of %s `%s')"
133 (or (transient-arg-value "--term-old=" args) "Good")
134 good
135 (or (transient-arg-value "--term-new=" args) "bad")
136 bad))
137 (when (magit-anything-modified-p)
138 (user-error "Cannot bisect with uncommitted changes")))
139
140 ;;;###autoload
141 (defun magit-bisect-reset ()
142 "After bisecting, cleanup bisection state and return to original `HEAD'."
143 (interactive)
144 (magit-confirm 'reset-bisect)
145 (magit-run-git "bisect" "reset")
146 (magit-repository-local-delete 'bisect--first-parent)
147 (ignore-errors
148 (delete-file (expand-file-name "BISECT_CMD_OUTPUT" (magit-gitdir)))))
149
150 ;;;###autoload
151 (defun magit-bisect-good ()
152 "While bisecting, mark the current commit as good.
153 Use this after you have asserted that the commit does not contain
154 the bug in question."
155 (interactive)
156 (magit-git-bisect (or (cadr (magit-bisect-terms))
157 (user-error "Not bisecting"))))
158
159 ;;;###autoload
160 (defun magit-bisect-bad ()
161 "While bisecting, mark the current commit as bad.
162 Use this after you have asserted that the commit does contain the
163 bug in question."
164 (interactive)
165 (magit-git-bisect (or (car (magit-bisect-terms))
166 (user-error "Not bisecting"))))
167
168 ;;;###autoload
169 (defun magit-bisect-mark ()
170 "While bisecting, mark the current commit with a bisect term.
171 During a bisect using alternate terms, commits can still be
172 marked with `magit-bisect-good' and `magit-bisect-bad', as those
173 commands map to the correct term (\"good\" to --term-old's value
174 and \"bad\" to --term-new's). However, in some cases, it can be
175 difficult to keep that mapping straight in your head; this
176 command provides an interface that exposes the underlying terms."
177 (interactive)
178 (magit-git-bisect
179 (pcase-let ((`(,term-new ,term-old) (or (magit-bisect-terms)
180 (user-error "Not bisecting"))))
181 (pcase (read-char-choice
182 (format "Mark HEAD as %s ([n]ew) or %s ([o]ld)"
183 term-new term-old)
184 (list ?n ?o))
185 (?n term-new)
186 (?o term-old)))))
187
188 ;;;###autoload
189 (defun magit-bisect-skip ()
190 "While bisecting, skip the current commit.
191 Use this if for some reason the current commit is not a good one
192 to test. This command lets Git choose a different one."
193 (interactive)
194 (magit-git-bisect "skip"))
195
196 ;;;###autoload
197 (defun magit-bisect-run (cmdline &optional bad good args)
198 "Bisect automatically by running commands after each step.
199
200 Unlike \"git bisect run\" this can be used before bisecting has begun.
201 In that case it behaves like \"git bisect start; git bisect run\"."
202 (interactive (let ((args (and (not (magit-bisect-in-progress-p))
203 (magit-bisect-start-read-args))))
204 (cons (read-shell-command "Bisect shell command: ") args)))
205 (when (and bad good)
206 (magit-bisect-start--assert bad good args)
207 ;; Avoid `magit-git-bisect' because it's asynchronous, but the
208 ;; next "git bisect run" call requires the bisect to be started.
209 (magit-with-toplevel
210 (magit-process-git
211 (list :file (expand-file-name "BISECT_CMD_OUTPUT" (magit-gitdir)))
212 "bisect" "start" bad good args)
213 (magit-refresh)))
214 (with-connection-local-variables
215 (magit-git-bisect "run" (list shell-file-name
216 shell-command-switch cmdline))))
217
218 (defun magit-git-bisect (subcommand &optional args no-assert)
219 (unless (or no-assert (magit-bisect-in-progress-p))
220 (user-error "Not bisecting"))
221 (message "Bisecting...")
222 (magit-with-toplevel
223 (magit-run-git-async "bisect" subcommand args))
224 (set-process-sentinel
225 magit-this-process
226 (lambda (process event)
227 (when (memq (process-status process) '(exit signal))
228 (if (> (process-exit-status process) 0)
229 (magit-process-sentinel process event)
230 (process-put process 'inhibit-refresh t)
231 (magit-process-sentinel process event)
232 (when (buffer-live-p (process-buffer process))
233 (with-current-buffer (process-buffer process)
234 (when-let* ((section (magit-section-at))
235 (output (buffer-substring-no-properties
236 (oref section content)
237 (oref section end))))
238 (with-temp-file
239 (expand-file-name "BISECT_CMD_OUTPUT" (magit-gitdir))
240 (insert output)))))
241 (magit-refresh))
242 (message "Bisecting...done")))))
243
244 ;;; Sections
245
246 (defun magit-bisect-in-progress-p ()
247 (file-exists-p (expand-file-name "BISECT_LOG" (magit-gitdir))))
248
249 (defun magit-bisect-terms ()
250 (magit-file-lines (expand-file-name "BISECT_TERMS" (magit-gitdir))))
251
252 (defun magit-insert-bisect-output ()
253 "While bisecting, insert section with output from \"git bisect\"."
254 (when (magit-bisect-in-progress-p)
255 (let* ((lines
256 (or (magit-file-lines
257 (expand-file-name "BISECT_CMD_OUTPUT" (magit-gitdir)))
258 (list "Bisecting: (no saved bisect output)"
259 "It appears you have invoked \"git bisect\" from a shell."
260 "There is nothing wrong with that, we just cannot display"
261 "anything useful here. Consult the shell output instead.")))
262 (done-re "^\\([a-z0-9]\\{40,\\}\\) is the first bad commit$")
263 (bad-line (or (and (string-match done-re (car lines))
264 (pop lines))
265 (seq-find (##string-match done-re %) lines))))
266 (magit-insert-section ((eval (if bad-line 'commit 'bisect-output))
267 (and bad-line (match-str 1 bad-line)))
268 (magit-insert-heading
269 (propertize (or bad-line (pop lines))
270 'font-lock-face 'magit-section-heading))
271 (dolist (line lines)
272 (insert line "\n"))))
273 (insert "\n")))
274
275 (defun magit-insert-bisect-rest ()
276 "While bisecting, insert section visualizing the bisect state."
277 (when (magit-bisect-in-progress-p)
278 (magit-insert-section (bisect-view)
279 (magit-insert-heading t "Bisect Rest")
280 (magit-git-wash (apply-partially #'magit-log-wash-log 'bisect-vis)
281 "bisect" "visualize" "git" "log"
282 "--format=%h%x00%D%x00%s" "--decorate=full"
283 (and magit-bisect-show-graph "--graph")
284 (and (magit-repository-local-get 'bisect--first-parent)
285 "--first-parent")))))
286
287 (defun magit-insert-bisect-log ()
288 "While bisecting, insert section logging bisect progress."
289 (when (magit-bisect-in-progress-p)
290 (magit-insert-section (bisect-log)
291 (magit-insert-heading t "Bisect Log")
292 (magit-git-wash #'magit-wash-bisect-log "bisect" "log")
293 (insert ?\n))))
294
295 (defun magit-wash-bisect-log (_args)
296 (let (beg)
297 (while (progn (setq beg (point-marker))
298 (re-search-forward
299 "^\\(\\(?:git bisect\\|# status:\\) [^\n]+\n\\)" nil t))
300 (if (string-prefix-p "# status:" (match-str 1))
301 (magit-delete-match)
302 (magit-bind-match-strings (heading) nil
303 (magit-delete-match)
304 (save-restriction
305 (narrow-to-region beg (point))
306 (goto-char (point-min))
307 (magit-insert-section (bisect-item heading t)
308 (magit-insert-heading
309 (propertize heading 'font-lock-face
310 'magit-section-secondary-heading))
311 (magit-wash-sequence
312 (apply-partially #'magit-log-wash-rev 'bisect-log
313 (magit-abbrev-length)))
314 (insert ?\n))))))
315 (when (re-search-forward
316 "# first bad commit: \\[\\([a-z0-9]\\{40,\\}\\)\\] [^\n]+\n" nil t)
317 (magit-bind-match-strings (hash) nil
318 (magit-delete-match)
319 (magit-insert-section (bisect-item)
320 (insert hash " is the first bad commit\n"))))))
321
322 ;;; _
323 (provide 'magit-bisect)
324 ;; Local Variables:
325 ;; read-symbol-shorthands: (
326 ;; ("and$" . "cond-let--and$")
327 ;; ("thread$" . "cond-let--thread$")
328 ;; ("when$" . "cond-let--when$")
329 ;; ("and-let*" . "cond-let--and-let*")
330 ;; ("and-let" . "cond-let--and-let")
331 ;; ("if-let*" . "cond-let--if-let*")
332 ;; ("if-let" . "cond-let--if-let")
333 ;; ("when-let*" . "cond-let--when-let*")
334 ;; ("when-let" . "cond-let--when-let")
335 ;; ("while-let*" . "cond-let--while-let*")
336 ;; ("while-let" . "cond-let--while-let")
337 ;; ("match-string" . "match-string")
338 ;; ("match-str" . "match-string-no-properties"))
339 ;; End:
340 ;;; magit-bisect.el ends here