dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

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