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