config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

magit-bisect.el (12398B)


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