magit-bisect.el 7.77 KB
Newer Older
1
;;; magit-bisect.el --- bisect support for Magit  -*- lexical-binding: t -*-
2

Jonas Bernoulli's avatar
Jonas Bernoulli committed
3
;; Copyright (C) 2011-2018  The Magit Project Contributors
4
;;
5 6
;; You should have received a copy of the AUTHORS.md file which
;; lists all contributors.  If not, see http://magit.vc/authors.
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

;; Author: Jonas Bernoulli <jonas@bernoul.li>
;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>

;; Magit is free software; you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; Magit is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
;; License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with Magit.  If not, see http://www.gnu.org/licenses.

;;; Commentary:

;; Use a binary search to find the commit that introduced a bug.

;;; Code:

(require 'magit)

32 33
;;; Options

34 35
(defcustom magit-bisect-show-graph t
  "Whether to use `--graph' in the log showing commits yet to be bisected."
36
  :package-version '(magit . "2.8.0")
37 38 39
  :group 'magit-status
  :type 'boolean)

40
(defface magit-bisect-good
41
  '((t :foreground "DarkOliveGreen"))
42 43 44 45
  "Face for good bisect revisions."
  :group 'magit-faces)

(defface magit-bisect-skip
46
  '((t :foreground "DarkGoldenrod"))
47 48 49 50
  "Face for skipped bisect revisions."
  :group 'magit-faces)

(defface magit-bisect-bad
51
  '((t :foreground "IndianRed4"))
52 53 54
  "Face for bad bisect revisions."
  :group 'magit-faces)

55 56
;;; Commands

Jonas Bernoulli's avatar
Jonas Bernoulli committed
57
;;;###autoload (autoload 'magit-bisect-popup "magit-bisect" nil t)
58 59 60
(magit-define-popup magit-bisect-popup
  "Popup console for bisect commands."
  :man-page "git-bisect"
61 62 63 64 65 66 67
  :actions            '((?B "Start"        magit-bisect-start)
                        (?s "Start script" magit-bisect-run))
  :sequence-actions   '((?b "Bad"          magit-bisect-bad)
                        (?g "Good"         magit-bisect-good)
                        (?k "Skip"         magit-bisect-skip)
                        (?r "Reset"        magit-bisect-reset)
                        (?s "Run script"   magit-bisect-run))
68
  :sequence-predicate 'magit-bisect-in-progress-p)
69 70 71 72 73 74 75 76 77 78

;;;###autoload
(defun magit-bisect-start (bad good)
  "Start a bisect session.

Bisecting a bug means to find the commit that introduced it.
This command starts such a bisect session by asking for a know
good and a bad commit.  To move the session forward use the
other actions from the bisect popup (\
\\<magit-status-mode-map>\\[magit-bisect-popup])."
79 80 81
  (interactive (if (magit-bisect-in-progress-p)
                   (user-error "Already bisecting")
                 (magit-bisect-start-read-args)))
82
  (magit-git-bisect "start" (list bad good) t))
83

84 85 86 87
(defun magit-bisect-start-read-args ()
  (let  ((b (magit-read-branch-or-commit "Start bisect with bad revision")))
    (list b (magit-read-other-branch-or-commit "Good revision" b))))

88 89
;;;###autoload
(defun magit-bisect-reset ()
90
  "After bisecting, cleanup bisection state and return to original `HEAD'."
91
  (interactive)
92 93 94
  (magit-confirm 'reset-bisect)
  (magit-run-git "bisect" "reset")
  (ignore-errors (delete-file (magit-git-dir "BISECT_CMD_OUTPUT"))))
95 96 97 98 99 100 101

;;;###autoload
(defun magit-bisect-good ()
  "While bisecting, mark the current commit as good.
Use this after you have asserted that the commit does not contain
the bug in question."
  (interactive)
102
  (magit-git-bisect "good"))
103 104 105 106 107 108 109

;;;###autoload
(defun magit-bisect-bad ()
  "While bisecting, mark the current commit as bad.
Use this after you have asserted that the commit does contain the
bug in question."
  (interactive)
110
  (magit-git-bisect "bad"))
111 112 113 114 115 116 117

;;;###autoload
(defun magit-bisect-skip ()
  "While bisecting, skip the current commit.
Use this if for some reason the current commit is not a good one
to test.  This command lets Git choose a different one."
  (interactive)
118
  (magit-git-bisect "skip"))
119 120

;;;###autoload
121 122 123 124 125 126 127 128 129 130 131
(defun magit-bisect-run (cmdline &optional bad good)
  "Bisect automatically by running commands after each step.

Unlike `git bisect run' this can be used before bisecting has
begun.  In that case it behaves like `git bisect start; git
bisect run'."
  (interactive (let ((args (and (not (magit-bisect-in-progress-p))
                                (magit-bisect-start-read-args))))
                 (cons (read-shell-command "Bisect shell command: ") args)))
  (when (and bad good)
    (magit-bisect-start bad good))
132
  (magit-git-bisect "run" (list shell-file-name shell-command-switch cmdline)))
133

134
(defun magit-git-bisect (subcommand &optional args no-assert)
135 136
  (unless (or no-assert (magit-bisect-in-progress-p))
    (user-error "Not bisecting"))
137
  (magit-with-toplevel
138 139
    (magit-run-git-with-logfile
     (magit-git-dir "BISECT_CMD_OUTPUT") "bisect" subcommand args)))
140

141 142
;;; Sections

143 144 145 146
(defun magit-bisect-in-progress-p ()
  (file-exists-p (magit-git-dir "BISECT_LOG")))

(defun magit-insert-bisect-output ()
147
  "While bisecting, insert section with output from `git bisect'."
148
  (when (magit-bisect-in-progress-p)
149 150 151 152 153 154 155 156 157 158 159 160
    (let* ((lines
            (or (magit-file-lines (magit-git-dir "BISECT_CMD_OUTPUT"))
                (list "Bisecting: (no saved bisect output)"
                      "It appears you have invoked `git bisect' from a shell."
                      "There is nothing wrong with that, we just cannot display"
                      "anything useful here.  Consult the shell output instead.")))
           (done-re "^\\([a-z0-9]\\{40\\}\\) is the first bad commit$")
           (bad-line (or (and (string-match done-re (car lines))
                              (pop lines))
                         (--first (string-match done-re it) lines))))
      (magit-insert-section ((eval (if bad-line 'commit 'bisect-output))
                             (and bad-line (match-string 1 bad-line)))
161
        (magit-insert-heading
162
          (propertize (or bad-line (pop lines))
163 164 165 166 167 168
                      'face 'magit-section-heading))
        (dolist (line lines)
          (insert line "\n"))))
    (insert "\n")))

(defun magit-insert-bisect-rest ()
169
  "While bisecting, insert section visualizing the bisect state."
170 171 172 173 174
  (when (magit-bisect-in-progress-p)
    (magit-insert-section (bisect-view)
      (magit-insert-heading "Bisect Rest:")
      (magit-git-wash (apply-partially 'magit-log-wash-log 'bisect-vis)
        "bisect" "visualize" "git" "log"
175
        "--format=%h%d%x00%s" "--decorate=full"
176
        (and magit-bisect-show-graph "--graph")))))
177 178

(defun magit-insert-bisect-log ()
179
  "While bisecting, insert section logging bisect progress."
180 181 182
  (when (magit-bisect-in-progress-p)
    (magit-insert-section (bisect-log)
      (magit-insert-heading "Bisect Log:")
183 184
      (magit-git-wash #'magit-wash-bisect-log "bisect" "log")
      (insert ?\n))))
185

186
(defun magit-wash-bisect-log (_args)
187 188 189 190 191 192 193 194
  (let (beg)
    (while (progn (setq beg (point-marker))
                  (re-search-forward "^\\(git bisect [^\n]+\n\\)" nil t))
      (magit-bind-match-strings (heading) nil
        (magit-delete-match)
        (save-restriction
          (narrow-to-region beg (point))
          (goto-char (point-min))
195
          (magit-insert-section (bisect-item heading t)
196
            (insert (propertize heading 'face 'magit-section-secondary-heading))
197
            (magit-insert-heading)
198
            (magit-wash-sequence
199
             (apply-partially 'magit-log-wash-rev 'bisect-log
200 201 202 203 204 205
                              (magit-abbrev-length)))
            (insert ?\n)))))
    (when (re-search-forward
           "# first bad commit: \\[\\([a-z0-9]\\{40\\}\\)\\] [^\n]+\n" nil t)
      (magit-bind-match-strings (hash) nil
        (magit-delete-match)
206
        (magit-insert-section (bisect-item)
207
          (insert hash " is the first bad commit\n"))))))
208 209 210

(provide 'magit-bisect)
;;; magit-bisect.el ends here