all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
blob ac3b936334a860b2dda651869d31af1a0c81d38f 15897 bytes (raw)
name: lisp/vc/vc-timemachine.el 	 # note: path name is non-authoritative(*)

  1
  2
  3
  4
  5
  6
  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
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
 
;;; vc-timemachine.el --- Walk through revisions of a file  -*- lexical-binding:t -*-

;; Copyright (C) 2023 Free Software Foundation, Inc.

;; This program 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 of the License, or
;; (at your option) any later version.

;; This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Credits:
;;
;;  Peter Stiernström - wrote the original, git-only version
;;  John Yates        - refactored Peter's code for the VC environment

;;; Commentary:
;;
;; A timemachine buffer is a sliding read-only window over the distinct
;; revisions of a single file on a VCS branch.  To create a timemachine:
;;
;;    (vc-prefix-map ",")   vc-tm-revision-head
;;
;; Timemachine implements prefix-free minor mode vc-tm-mode:
;;
;;    "," . vc-tm-revision-head
;;    "~" . vc-tm-revision-select
;;    "g" . vc-tm-revision-i
;;    "n" . vc-tm-revision-next
;;    "p" . vc-tm-revision-previous
;;    "q" . vc-tm-quit
;;    "s" . vc-tm-revision-complete-subject
;;    "w" . vc-tm-abbreviated-revision-to-kill-ring
;;    "W" . vc-tm-revision-to-kill-ring
;;
;; To support timemachine functionality a VCS backend needs to implement:
;;
;;   - tm-revisions (file)
;;
;; And ideally:
;;
;;   - tm-map-line (file from-revision from-line to-revision from-is-older)
;;
;; For more details see the large comment at the front of vc.el.

;;; Todo:
;;
;; * implement missing blame
;; * implement missing show-commit
;; * vc-tm-create: when called from a revision buffer, should jump to that revision
;; * vc-tm--time-machine: confirm revision is present in tmbuf--branch-revisions

;;; Code:

(require 'vc)

(defgroup vc-timemachine nil
  "Time-machine functionality for VC backends."
  :group 'vc
  :version "30.1")

(defcustom vc-tm-date-format
  "%a %I:%M %p %Y-%m-%d"
  "Revision creation date format (emphasis on easy date comparison)."
  :type 'string
  :group 'vc-timemachine
  :version "30.1")

(defcustom vc-tm-echo-area t
  "When non-nil show revision details in the echo-area while navigating commits."
  :type 'boolean
  :group 'vc-timemachine
  :version "30.1")

(defcustom vc-tm-echo-area-detail 'subject
  "What to display when `vc-tm-echo-area` is t.
Available values are:
`ID`      : The revision's ID (commit hash)
`subject` : The revision's commit message subject line"
  :type '(radio (const :tag "Revision ID (commit hash)" commit)
                (const :tag "Revision message subject line" subject))
  :group 'vc-timemachine
  :version "30.1")

(defface vc-tm-echo-area-detail-face
  '((((class color) (background dark))
     :foreground "yellow")
    (((class color) (background light))
     :foreground "yellow4"))
  "Face to use when displaying details in the echo-area."
  :group 'vc-timemachine
  :version "30.1")

(defcustom vc-tm-echo-area-author t
  "Prepend author to echo-area details."
  :type 'boolean
  :group 'vc-timemachine
  :version "30.1")

(defface vc-tm-echo-area-author-face
  '((((class color) (background dark))
     :foreground "orange")
    (((class color) (background light))
     :foreground "DarkOrange4"))
  "Face to use when displaying author as part of details in the echo-area."
  :group 'vc-timemachine
  :version "30.1")

(defcustom vc-tm-abbreviation-length 12
  "Number of chars from full revision id to use for abbreviation."
  :type 'integer
  :group 'vc-timemachine
  :version "30.1")

(defcustom vc-tm-quit-to-invoking-buffer t
  "Switch to invoking buffer on ‘vc-tm-quit’."
  :type 'boolean
  :group 'vc-timemachine
  :version "30.1")

(defvar-local vc--time-machine nil
  "Cache a TM hint on various buffers.")
(put 'vc--time-machine 'permanent-local t)

(defvar-local tmbuf--abs-file nil
  "Absolute path to file being traversed by this time-machine.")
(put 'tmbuf--abs-file 'permanent-local t)
(defvar-local tmbuf--backend nil
  "The VC backend being used by this time-machine")
(put 'tmbuf--backend 'permanent-local t)
(defvar-local tmbuf--branch-index nil
  "Zero-base index into tmbuf--branch-revisions.")
(put 'tmbuf--branch-revisions 'permanent-local t)
(defvar-local tmbuf--branch-revisions nil
  "When non-nil, a vector of revision-info lists.")
(put 'tmbuf--branch-revisions 'permanent-local t)
(defvar-local tmbuf--source-buffer nil
  "A non-time-machine buffer for which this time-machine was created.")
(put 'tmbuf--source-buffer 'permanent-local t)

(defun vc-tm--time-machine ()
  "Return a valid time-machine for the current buffer."
  (if tmbuf--backend
      (current-buffer)
    (let ((revision vc-tm--revision) ;; caller could be a revision buffer
          (source (current-buffer)))
      (set-buffer (or (buffer-base-buffer) source))
      (vc-ensure-vc-buffer)

      (let* ((parent vc-parent-buffer)
             (abs-file (buffer-file-name))
             (backend  (vc-backend abs-file))
             (work-rev (vc-working-revision abs-file))
             (tmbuf vc--time-machine))

        ;; ensure that there is a revision with which to work.
        (unless revision
          (setq revision work-rev)
          (setq source (current-buffer)))

        ;; Validate any current time-machine buffer.
        (when tmbuf
          (with-current-buffer tmbuf
            (unless (and (equal abs-file tmbuf--abs-file)
                         (equal backend  tmbuf--backend)
                         ;; TODO: confirm that revision is in tmbuf--branch-revisions.
                         )
              ;; Discard an unvalidate TM buffer.
              (setq tmbuf nil))))

        ;; Create a fresh TM buffer if needed.
        (unless tmbuf
          (with-current-buffer (setq tmbuf (get-buffer-create "*nascent TM*"))
            (setq vc-parent-buffer parent)
            (setq vc--time-machine tmbuf)
            (setq vc-tm--revision revision)
            (setq tmbuf--abs-file abs-file)
            (setq tmbuf--backend backend)
            (setq tmbuf--source-buffer source)
            (setq tmbuf--branch-index 0)
            (setq tmbuf--branch-revisions
                  (with-temp-buffer
                    (prog2
                        (message "Enumerating revisions...")
                        (let* ((vec (cl-coerce (vc-call-backend backend 'tm-revisions abs-file) 'vector))
                               (branch (nreverse vec)))
                          branch)
                      (message "Enumerating revisions...done"))))))

        (set-buffer tmbuf)
        tmbuf))))

;;;###autoload
(defun vc-tm-revision-head ()
  "Show work file's current revision on checked-out branch."
  (interactive)
  (with-current-buffer (vc-tm--time-machine)
    (vc-tm--switch-to-revision 1)))

(defun vc-tm-revision-next ()
  "Show work file's next revision on checked-out branch."
  (interactive)
  (with-current-buffer (vc-tm--time-machine)
    (vc-tm--switch-to-revision tmbuf--branch-index)))

(defun vc-tm-revision-previous ()
  "Show work file's previous revision on checked-out branch."
  (interactive)
  (with-current-buffer (vc-tm--time-machine)
    (vc-tm--switch-to-revision (+ tmbuf--branch-index 2))))

(defun vc-tm-revision-i (number)
  "Show work file's N'th most recent revision on checked-out branch (1 being HEAD)."
  (interactive "nEnter revision position: ")
  (with-current-buffer (vc-tm--time-machine)
    (vc-tm--switch-to-revision number)))

(defun vc-tm-revision-complete-subject ()
  "Show work file's revision via subject completion on checked-out branch."
  (interactive)
  (let* ((s (completing-read
             "Commit subject: "
             (mapcar (apply-partially #'nth 2) tmbuf--branch-revisions))))
    (vc-tm--switch-to-revision
     (cl-loop for revision-number from 1
              for info across tmbuf--branch-revisions
              if (equal s (nth 2 info)) return revision-number
              finally (error "Subject not found")))))

(defun vc-tm--switch-to-revision (to-number)
  "Show work file's revision at position TO-NUMBER on checked-out branch."
  (let ((branch-length (length tmbuf--branch-revisions))
        (to-index (1- to-number))
        (calling-window-buffer (window-buffer))
        (cursor-win-pos))
    (with-current-buffer calling-window-buffer
      (setq cursor-win-pos (vc-tm--get-cursor-win-position)))
    (cond
     ((< to-number 1)
      (error "%s" "This is the HEAD revision; there are none newer"))
     ((>= to-index branch-length)
      (error "There is no revision %d (the oldest revision is %d)"
             to-number branch-length))
     (t
      (let* ((new-revision-info (vc-tm--tmbuf-revision-info to-index))
             (new-revision (car new-revision-info))
             (abbrev-rev (vc-tm--abbreviate new-revision))
             (date (nth 1 new-revision-info))
             (n-of-m (format " [%d/%d %s]" to-number branch-length date))
             ;; Use the file-name from new-revision-info to reveal renames.
             (file-name (file-name-nondirectory (nth 4 new-revision-info)))
             (tmbuf (current-buffer))
             (from-line (line-number-at-pos))
             (to-line from-line))
        (when vc-tm--revision
          (unless (= tmbuf--branch-index to-index)
            (setq to-line (vc-tm--map-line from-line to-index))))
        (vc-find-revision tmbuf--abs-file new-revision tmbuf--backend tmbuf)
        ;; Reuse timemachine windows, otherwise create them in some other-window.
        (if (eq calling-window-buffer tmbuf)
            (switch-to-buffer tmbuf)
          (switch-to-buffer-other-window tmbuf))
        (vc-tm-mode +1)
        (forward-line (- to-line (line-number-at-pos)))
        (vc-tm--set-cursor-win-position cursor-win-pos)
        (setq tmbuf--branch-index to-index)

        (rename-buffer (concat file-name " " abbrev-rev) t)
        (setq mode-line-buffer-identification
              (list (propertized-buffer-identification "%12b") n-of-m))

        (when vc-tm-echo-area
          (vc-tm--show-echo-area-details new-revision-info))))
      (vc-tm--erm-workaround))))


(defun vc-tm--map-line (from-line to-index)
  "Return a suggested new current-line after a revision jump."
  ;; Newer and older are first guesses; subsequently they may get swapped.
  (let* ((to-info (vc-tm--tmbuf-revision-info to-index))
         (to-revision (car to-info))
         (from-revision vc-tm--revision)
         (from-is-older (< tmbuf--branch-index to-index))
         (backend tmbuf--backend)
         (abs-file tmbuf--abs-file))
    (with-temp-buffer
      (vc-call-backend backend 'tm-map-line abs-file
                       from-revision from-line to-revision from-is-older))))

(defun vc-default-tm-map-line (_backend _rel-file _from-revision from-line
                                     _to-revision _from-is-older)
  "Default `map-line' implementation.
It merely returns FROM-LINE."
  from-line)

(defun vc-tm--show-echo-area-details (revision-info)
  "Show details for REVISION-INFO in echo-area."
  (let* ((date (nth 1 revision-info))
         (author (if vc-tm-show-author (concat " | " (nth 3 revision-info)) ""))
         (sha-or-subject (if (eq vc-tm-echo-area-detail 'commit) (car revision-info) (nth 2 revision-info))))
    (message "%s%s: %s"
             date
             (propertize author 'face 'vc-tm-echo-area-author-face)
             (propertize sha-or-subject 'face 'vc-tm-echo-area-detail-face))))

(defun vc-tm-format-date (date)
  "Return date formatted per the user's vc-tm-date-format."
  (format-time-string vc-tm-date-format (date-to-time date)))

(declare-function erm-reset-buffer "ext:enh-ruby-mode")

(defun vc-tm--erm-workaround ()
  "Workaround for enhanced ruby mode not detecting revision change."
  (when (eq major-mode 'enh-ruby-mode)
    (ignore-errors (erm-reset-buffer))))

(defun vc-tm--get-cursor-win-position ()
  "Return the cursor visual line number w.r.t. the current window first line."
  (let* ((win-point-min (save-excursion (move-to-window-line 0) (point)))
         (cur-pos (count-screen-lines win-point-min (point))))
    cur-pos))

(defun vc-tm--set-cursor-win-position (POS)
  "Set the cursor position to the POS visual line w.r.t. the window first line."
  (recenter POS))

(defun vc-tm--abbreviate (revision)
  "Return REVISION abbreviated to `vc-tm-abbreviation-length' chars."
  (if (length< revision vc-tm-abbreviation-length)
      revision
    (substring revision 0 vc-tm-abbreviation-length)))

(defun vc-tm-revision-to-kill-ring ()
  "Kill the current revisions abbreviated commit hash."
  (interactive)
  (let ((revision (vc-tm--tmbuf-revision)))
    (message revision)
    (kill-new revision)))

(defun vc-tm-abbreviated-revision-to-kill-ring ()
  "Kill the current revisions full commit hash."
  (interactive)
  (let ((revision (vc-tm--abbreviate (vc-tm--tmbuf-revision))))
    (message revision)
    (kill-new revision)))

;; (defun vc-tm-show-commit ()
;;   "Show commit for current revision."
;;   (interactive)
;;   (let ((rev (vc-tm--tmbuf-revision)))
;;     (if (fboundp 'magit-show-commit)
;;         (magit-show-commit rev)
;;       (message "You need to install magit to show commit"))))

;; (defun vc-tm-blame ()
;;   "Call ‘magit-blame’ on current revision."
;;   (interactive)
;;   (if (fboundp 'magit-blame)
;;       (let ((magit-buffer-revision (car tm--revision-info)))
;;      (magit-blame))
;;     (message "You need to install magit for blame capabilities")))

(defun vc-tm--tmbuf-revision (&optional index)
  "Return the unique revision id for this tmbuf's current revision."
  (car (vc-tm--tmbuf-revision-info index)))

(defun vc-tm--tmbuf-revision-info (&optional index)
  "Return the revision-info list for this tmbuf's current revision."
  (aref tmbuf--branch-revisions (or index tmbuf--branch-index)))

(defun vc-tm-quit ()
  "Exit the timemachine."
  (interactive)
  (let ((parent-buffer-name buffer-file-name))
    (kill-buffer)
    (let ((parent-buffer (find-buffer-visiting parent-buffer-name)))
      (when (and parent-buffer vc-tm-quit-to-invoking-buffer)
        (switch-to-buffer parent-buffer nil t)))))

(transient-define-prefix vc-tm-help ()
  "Show online help."
  ["Navigate"
   [("," "switch to HEAD revision" vc-tm-revision-head)
    ("~" "switch to selected revision id" vc-tm-revision-select)
    ("n" "switch to next revision" vc-tm-revision-next)
    ("p" "switch to previous revision" vc-tm-revision-previous)
    ("j" "switch to i'th revision" vc-tm-revision-nth)
    ("s" "switch to subject revision" vc-tm-revision-complete-subject)]]
  ["Revision ID to kill ring"
   [("w" "abbreviated revision id" vc-tm-abbreviated-revision-to-kill-ring)
    ("W" "full revision id" vc-tm-revision-to-kill-ring)]]
  ["Misc"
   [ ;; ("B" "blame current revision" vc-tm-blame)
;;  ("C" "view commit" vc-tm-show-commit)
    ("?" "show help" vc-tm-help)
    ("q" "quit" vc-tm-quit)]])

(define-minor-mode vc-tm-mode
  "VC Timemachine, feel the wings of history."
  :init-value nil
  :lighter " TM"
  :keymap
  '(("," . vc-tm-revision-head)
    ("~" . vc-tm-revision-select)
;;  ("B" . vc-tm-blame)
;;  ("C" . vc-tm-show-commit)
    ("j" . vc-tm-revision-nth)
    ("n" . vc-tm-revision-next)
    ("p" . vc-tm-revision-previous)
    ("q" . vc-tm-quit)
    ("s" . vc-tm-revision-complete-subject)
    ("S" . vc-tm-revision-complete-subject) ; in vc-mode "s" is vc-create-tag
    ("w" . vc-tm-abbreviated-revision-to-kill-ring)
    ("W" . vc-tm-revision-to-kill-ring)
    ("?" . vc-tm-help))
  :group 'vc-timemachine)

(provide 'vc-timemachine)

;;; vc-timemachine.el ends here

debug log:

solving ac3b936334 ...
found ac3b936334 in https://yhetil.org/emacs/CAJnXXoiJA971Qx4dfERLG9fjdVxWZAo7PdFRm0qhMO7rbsYZEg@mail.gmail.com/

applying [1/1] https://yhetil.org/emacs/CAJnXXoiJA971Qx4dfERLG9fjdVxWZAo7PdFRm0qhMO7rbsYZEg@mail.gmail.com/
diff --git a/lisp/vc/vc-timemachine.el b/lisp/vc/vc-timemachine.el
new file mode 100644
index 0000000000..ac3b936334

Checking patch lisp/vc/vc-timemachine.el...
Applied patch lisp/vc/vc-timemachine.el cleanly.

index at:
100644 ac3b936334a860b2dda651869d31af1a0c81d38f	lisp/vc/vc-timemachine.el

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.