├── .gitignore ├── README.md ├── backups-mode.el ├── bm-utilities.el ├── fallback ├── .nosearch └── backup-walker.el └── scripts └── show-orphaned.sh /.gitignore: -------------------------------------------------------------------------------- 1 | deploy.sh 2 | *~ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # backups-mode for emacs 3 | 4 | ## Inspiration 5 | The inspiration for this came after reading [John Siracusa's review of the Document Model](http://arstechnica.com/apple/reviews/2011/07/mac-os-x-10-7.ars/7#document-model) new to Mac OS X Lion. This new framework on the Mac allows for the following: 6 | 7 | * All documents are automatically saved. This includes when the user closes the document or quits the application. 8 | * Old versions of the document are automatically stored and accessible. 9 | * The user has the ability to manually save a verion of the document. 10 | * Old versions can be viewed, diffed, and reverted. 11 | * Reverting an old version saves the current file as a version and copies the selected version to become the current file. 12 | 13 | So I set out to emulate these features in emacs. emacs already has its own rudimentary version control system that can be turned on simply by configuring emacs. It can also be configured to keep those backup files tucked away in a central directory. It can also be configured to automatically save (without prompting) your files when you kill the buffer or quit emacs. In the event that emacs crashes, you can also restore your file from an autosave file emacs creates for you. So it has all of that out of the box. 14 | 15 | What it doesn't have (or at least I couldn't find) is the ability to easily find, view, diff, and revert those versioned backup files. That is where *backups-mode.el* comes into play. While editing any file-based buffer in emacs, you can now do two extra things. You can list all backups and you can explicily save a version. 16 | 17 | ## Disclaimer 18 | Using the emacs version control functionality should not be a replacement for a proper version control system such as cvs, svn, git, mercurial, and the like. So if you are developing a project or typing a manuscript, you'll want to use one of those systems. I find this useful for the less important stuff such as blog posts or exploritory programming. 19 | 20 | ## Installation 21 | git clone git@github.com:chadbraunduin/backups-mode.git 22 | cd backups-mode 23 | # copy the necessary files to your emacs load-path 24 | cp backups-mode.el bm-utilities.el backup-walker.el ~/.emacs.d/ 25 | # this assumes ~/.emacs.d/ is in your emacs load-path 26 | # add the following to .emacs 27 | (require 'backups-mode) 28 | (backups-minor-mode) 29 | 30 | ## Additional configuration 31 | ;; putting this in your .emacs will allow you to change version control settings. The following are the default settings found in backups-mode.el. 32 | (setq backup-by-copying t 33 | delete-old-versions t 34 | kept-new-versions 6 35 | kept-old-versions 2 36 | version-control t) 37 | [emacs documentation](http://www.gnu.org/software/emacs/elisp/html_node/Numbered-Backups.html) will explain these options. 38 | 39 | If you are using Windows, you'll want to set these objects to be Windows specific: 40 | 41 | * last-modified-date-command-function 42 | * unknown-last-modified-date 43 | 44 | By default, backups are saved to "~/.emacs.d/backups" and tramp file backups are saved to "~/.emacs.d/tramp-backups". These defaults can be changed by setting: 45 | 46 | * emacs-directory 47 | * backup-directory 48 | * tramp-backup-directory 49 | 50 | ### Key mapping 51 | The backups-minor-mode keymap is defined in variable named backups-minor-mode-keymap. So you can make individual key changes like so: 52 | 53 | (define-key backups-minor-mode-keymap (kbd "C-c b") nil) 54 | (define-key backups-minor-mode-keymap (kbd "C-c l") 'list-backups) 55 | 56 | Or redifine the entire keymap like so: 57 | 58 | (setq backups-minor-mode-keymap (let ((map (make-sparse-keymap))) 59 | (define-key map (kbd "C-c sv") 'save-version) 60 | (define-key map (kbd "C-c lb") 'list-backups) 61 | (define-key map (kbd "C-c kb") 'kill-buffer-prompt) 62 | (when (backup-walker-p) 63 | (define-key map (kbd "C-c bw") 'backup-walker-start)) 64 | map)) 65 | 66 | ### My .emacs 67 | As an example, here's the configuration from my .emacs file. 68 | 69 | (require 'backups-mode) 70 | (defvar backup-directory "~/.emacs-backups/backups/") 71 | (defvar tramp-backup-directory "~/.emacs-backups/tramp-backups/") 72 | (backups-minor-mode) 73 | ;; keep all versions forever 74 | (setq delete-old-versions 1) 75 | 76 | ## Commands 77 | While editing any file-based emacs buffer, there are some additional commands: 78 | 79 | * save-version 80 | * This will version the previous saved copy of the file. 81 | * By default, this command can be done with control-c v ("\C-cv") 82 | 83 | * list-backups 84 | * This will open a new buffer in backups-mode which will list all backups of the file. 85 | * By default, this command can be done with control-c b ("\C-cb") 86 | 87 | * backup-walker 88 | * This will open a new buffer in backup-walker mode which will let you sequentially move through backup diffs. 89 | * By default, this command can be done with control-c w ("\C-cw") 90 | 91 | * kill-buffer-prompt 92 | * This will allow the user to close a buffer without saving any changes 93 | * By default, this command can be done with control-c k ("\C-ck") 94 | 95 | While in the backups-mode buffer, these are the commands: 96 | 97 | * View Backup 98 | * This is done by aligning the cursor to a file's line, and hitting \[enter\]. 99 | * Backup files will be opened read-only. 100 | 101 | * Revert Backup 102 | * This is done by aligning the cursor to a file's line, and typing "R". 103 | * Reverting will save the current file as a version, then replace the current file with the chosen backup. 104 | 105 | * Diff 2 Files 106 | * This is done by aligning the cursor to a file's line, and typing "d". This will mark that line as first file to diff. 107 | * Then, you align the cursor to another file's line and type "d". This will run the "diff-function" command on the two selected files. 108 | 109 | * Purge (delete) Backups 110 | * This is similar to Dired mode's batch delete, except Backups Mode uses "p" instead of "d" to mark backups for deletion. The choice of letters is because "d" is already being used to diff backups. 111 | * After marking one-to-many backups, "x" deletes all the marked backups. 112 | 113 | ## Backup Walker 114 | I've adapted lewang's [backup-walker](https://github.com/lewang/backup-walker) project. Backup Walker gives you a different view on your backups. Whereas Backups Mode lists all of your backups, Backup Walker starts with a diff of the current file and the previous backup. You can then sequentially move through diffs of consecutive backup files. Often, if you know what you are looking for, Backup Walker can be more efficient than Backups Mode. 115 | 116 | My adapted version of backup-walker is found in the "fallback" directory. Backups-mode will only use that version if it cannot find another version of backup-walker in your load-path. So a more up-to-date version will override what I have saved as a fallback. 117 | 118 | ## Cleanup 119 | The problem with creating N backup files per file is that over time you'll have generated a lot of backup files. Some of these backup files may even be orphaned if the original file is moved or deleted. I've taken two approaches for this problem: 120 | 121 | ### For local files 122 | For local files, I've created the script "show-orphaned.sh" (found in the scripts directory). It goes through my backups directory and displays all orphaned backups. I've created a @daily crontab job to remove all of the orphaned backups. 123 | 124 | @daily /home/chadbraunduin/.emacs-backups/backups/show-orphaned.sh | xargs -r /bin/rm -f 125 | 126 | ### For tramp files 127 | For tramp files, we cannot assume to be able to access the original file. Therefore, I've taken a more crude approach with tramp backups. I've scheduled a @daily crontab job that removes any tramp backups that have not be accessed in the past 180 days (roughly 6 months). 128 | 129 | @daily find /home/chadbraunduin/.emacs-backups/tramp-backups/ -type f -name "*.*~" -atime +180 | xargs -r /bin/rm -f 130 | 131 | ## rsnapshot configuration 132 | I use rsnapshot for rsync backups to an external drive. I've decided I do not care to backup these emacs generated backup files. Therefore, I've added these two lines to /etc/rsnapshot.conf: 133 | 134 | exclude /home/chadbraunduin/.emacs-backups/backups/*.*~ 135 | exclude /home/chadbraunduin/.emacs-backups/tramp-backups/*.*~ 136 | 137 | ## Bugs and TODOs 138 | As with most projects, this is still a work in progress. The known issues are: 139 | 140 | * I am planning on adapting this project to work with git-mode work-in-progress backups (WIP). 141 | * A [bug](https://github.com/chadbraunduin/backups-mode/issues/1) using this on Mac Os X has been reported. This is related to use the function copy-file. 142 | * I have not tested it in Windows, yet. I believe configuration changes will be necessary to make it work in that environment. Since I only use emacs in Linux, this is not a personal priority for me. 143 | -------------------------------------------------------------------------------- /backups-mode.el: -------------------------------------------------------------------------------- 1 | ;;; backups-mode.el --- major mode for autosaving files and listing, viewing, and reverting Emacs generated backups 2 | ;; Copyright (C) 2011 by Chad Braun-Duin 3 | 4 | ;; This program is free software: you can redistribute it and/or modify 5 | ;; it under the terms of the GNU General Public License as published by 6 | ;; the Free Software Foundation, either version 3 of the License, or 7 | ;; (at your option) any later version. 8 | 9 | ;; This program is distributed in the hope that it will be useful, 10 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | ;; GNU General Public License for more details. 13 | 14 | ;; You should have received a copy of the GNU General Public License 15 | ;; along with this program. If not, see . 16 | 17 | ;; Author: Chad Braun-Duin 18 | ;; Maintainer: Chad Braun-Duin 19 | ;; Created: 27 Aug 2011 20 | ;; Version: 2.0 21 | ;; Last-Updated: 12 Oct 2011 22 | ;; By: Chad Braun-Duin 23 | ;; URL: https://github.com/chadbraunduin/backups-mode 24 | ;; Keywords: backup, backups, autosaving, autosave, diff 25 | ;; Compatibility: Emacs 23+ 26 | 27 | ;;; Commentary: 28 | ;; The purpose of these commands and this new major mode is to loosely approximate the autosave-with-versions API 29 | ;; recently instituted by Apple for Mac OS X Lion. 30 | 31 | ;;; Installation 32 | ;; git clone git@github.com:chadbraunduin/backups-mode.git 33 | ;; cd backups-mode 34 | ;; # copy to your emacs load-path 35 | ;; cp backups-mode.el bm-utilities.el backup-walker.el ~/.emacs.d/ 36 | ;; # this assumes ~/.emacs.d/ is in your emacs load-path 37 | ;; # add the following to .emacs 38 | ;; (require 'backups-mode) 39 | ;; (backups-minor-mode) 40 | 41 | ;;; Addtional Configuration 42 | ;; putting this in your .emacs will allow you to change version control settings. These are the default settings found in backups-mode.el. 43 | ;; (setq backup-by-copying t 44 | ;; delete-old-versions t 45 | ;; kept-new-versions 6 46 | ;; kept-old-versions 2 47 | ;; version-control t) 48 | ;; Documentation on these settings can be found here: http://www.gnu.org/software/emacs/elisp/html_node/Numbered-Backups.html 49 | 50 | ;;; My Personal Configuration 51 | ;; As an example, here's the configuration from my .emacs file 52 | ;; (require 'backups-mode) 53 | ;; (defvar backup-directory "~/.emacs-backups/backups/") 54 | ;; (defvar tramp-backup-directory "~/.emacs-backups/tramp-backups/") 55 | ;; (backups-minor-mode) 56 | ;; keep all versions forever 57 | ;; (setq delete-old-versions 1) 58 | 59 | ;;; Usage 60 | ;; While editing a file-based buffer there are two new commands and some changes to note. 61 | ;; Now, whenever you kill a buffer or kill emacs, all file-based buffers will be saved without prompting. 62 | ;; New Commands while editing a file: 63 | ;; save-version (\C-cv) will backup the previously saved version of the file. 64 | ;; list-backups (\C-cb) will open a backups-mode buffer. 65 | ;; The backups-mode buffer will list all backups Emacs has created for the file and will allow you these options: 66 | ;; view-backup () will open a backup file read-only. 67 | ;; revert-backup (R) will backup the current file then replace the current file with the backup you've chosen. 68 | ;; diff 2 files (d + d) You can choose from the current file or any backup files and diff two of them. 69 | ;; purge backups (p [+ p] + x) You can delete backups in a batch fashion by marking backups then executing a deletion of all the marked backups. 70 | 71 | 72 | ;;; Code: 73 | 74 | (provide 'backups-mode) 75 | (load "bm-utilities.el") 76 | 77 | (eval-when-compile (require 'cl)) 78 | (eval-and-compile (require 'diff)) 79 | 80 | (defun backup-walker-p () 81 | (fboundp 'backup-walker-start)) 82 | 83 | ;;; global variables and .emacs configuation default values 84 | (defvar backups-mode-hook nil) 85 | (defvar last-modified-date-command-function 'nix-last-modified-date-command) ;; platform specific way of getting last modified date 86 | (defun nix-last-modified-date-command (file-name) (concat "date -r " file-name " +\"%x %r\"")) 87 | (defvar unknown-last-modified-date "date:") ;; platform specific output for unknown last modified date 88 | (defvar backup-files-function 'bm-backup-files) 89 | 90 | (defvar backups-minor-mode-keymap (let ((map (make-sparse-keymap))) 91 | (define-key map (kbd "C-c v") 'save-version) 92 | (define-key map (kbd "C-c b") 'list-backups) 93 | (define-key map (kbd "C-c k") 'kill-buffer-prompt) 94 | (when (backup-walker-p) 95 | (define-key map (kbd "C-c w") 'backup-walker-start)) 96 | map)) 97 | 98 | (define-minor-mode backups-minor-mode 99 | "Turns on emacs backups and keybindings to access the backups" 100 | "" 101 | :lighter " backups" 102 | :global t 103 | :keymap backups-minor-mode-keymap) 104 | 105 | ;; autosave configuration section 106 | (defvar emacs-directory "~/.emacs.d/") 107 | 108 | (defvar backup-directory (concat emacs-directory "backups/")) 109 | (make-directory backup-directory t) 110 | (setq backup-directory-alist `((".*" . ,backup-directory))) 111 | (setq auto-save-list-file-prefix (concat backup-directory ".auto-saves-")) 112 | (setq auto-save-file-name-transforms `((".*" ,backup-directory t))) 113 | 114 | (defvar tramp-backup-directory (concat emacs-directory "tramp-backups/")) 115 | (make-directory tramp-backup-directory t) 116 | (setq tramp-backup-directory-alist `((".*" . ,tramp-backup-directory))) 117 | 118 | ;; this next line turns on emacs version control with backups 119 | (setq backup-by-copying t 120 | delete-old-versions t 121 | kept-new-versions 6 122 | kept-old-versions 2 123 | version-control t) 124 | 125 | (defadvice kill-buffer (around kill-buffer) 126 | "Always save before killing a file buffer" 127 | (when (and (buffer-modified-p) 128 | (buffer-file-name) 129 | (file-exists-p (buffer-file-name))) 130 | (save-buffer)) 131 | ad-do-it) 132 | (ad-activate 'kill-buffer) 133 | 134 | (defadvice save-buffers-kill-emacs (around save-buffers-kill-emacs) 135 | "Always save before killing emacs" 136 | (save-some-buffers t) 137 | ad-do-it) 138 | (ad-activate 'save-buffers-kill-emacs) 139 | 140 | (defun kill-buffer-prompt () 141 | "Allows one to kill a buffer without saving it. 142 | This is necessary since once you start backups-mode all file based buffers 143 | are saved automatically when they are killed" 144 | (interactive) 145 | (if (and (buffer-modified-p) (buffer-file-name) (file-exists-p (buffer-file-name)) (y-or-n-p "Save buffer?")) 146 | (save-buffer) 147 | (set-buffer-modified-p nil)) 148 | (kill-buffer)) 149 | 150 | ;; commands you can run from any file 151 | (defun save-version () 152 | "Make the most recently saved version of the file a backup" 153 | (interactive) 154 | (set-buffer-modified-p t) 155 | (save-buffer 16)) ;; archive a copy of the previous version 156 | 157 | (defun list-backups () 158 | "Lists all saved backups in a buffer whose major mode is backups-mode" 159 | (interactive) 160 | (let ((original-file (buffer-file-name))) 161 | (if original-file 162 | (list-backups-from-file original-file) 163 | (princ "No backups for a non-file buffer")))) 164 | 165 | (defun* list-backups-from-file (original-file &key data) 166 | (setq first-config (current-window-configuration)) 167 | (bm-switch-to-window (format "*Backups: %s*" (buffer-name (get-file-buffer original-file))) 168 | 'backups-major-mode-p) 169 | (backups-mode) ;; switch to backups-mode 170 | (erase-buffer) 171 | (setq second-config (current-window-configuration)) 172 | 173 | (unless (assq :original-file data) 174 | (push `(:original-file . ,original-file) data)) 175 | (push `(:backups . ,(bm-get-sorted-backups original-file backup-files-function)) data) 176 | (push `(:backups-buffer . ,(current-buffer)) data) 177 | (push `(:marked-for-purging . ,(list)) data) 178 | (unless (assq :first-config data) 179 | (push `(:first-config . ,first-config) data)) 180 | (if (assq :second-config data) 181 | (setcdr (assq :second-config data) second-config) 182 | (push `(:second-config . ,second-config) data)) 183 | 184 | (make-variable-buffer-local 'backups-mode-data-alist) 185 | (setq backups-mode-data-alist data) 186 | 187 | (make-variable-buffer-local 'first-diff-index) 188 | (setq first-diff-index nil) 189 | 190 | ;; do pretty print here 191 | (insert (format "%s\n" original-file)) 192 | (insert 193 | (apply 'concat (mapcar 194 | (lambda (file) 195 | (let* ((version (bm-get-version file)) 196 | (version (if version (number-to-string version) "current")) 197 | (last-modified-date (or (bm-get-last-modified-date file) (concat "unknown" "\t")))) 198 | (format " %-6s\t%s" 199 | (propertize version 'face 'font-lock-keyword-face) 200 | last-modified-date))) 201 | (bm-get-backups data)))) 202 | ;; move the cursor to the top 203 | (goto-char 1) 204 | (forward-line) 205 | (set-buffer-modified-p nil)) 206 | 207 | ;;; backups-mode map and methods 208 | (defun backups-mode-map () 209 | "Keymap for backups major mode" 210 | (let ((map (make-sparse-keymap))) 211 | (suppress-keymap map) 212 | (when (backup-walker-p) 213 | (define-key map "w" (lambda () 214 | (interactive) 215 | (backup-walker-start (bm-get-original-file backups-mode-data-alist) 216 | :index (1- (bm-get-index-number (line-number-at-pos))) 217 | :data backups-mode-data-alist)))) 218 | (define-key map (kbd "") 'view-backup) 219 | (define-key map "r" (lambda () (interactive) (princ bm-revert-message))) 220 | (define-key map "R" 'revert-backup) 221 | (define-key map "d" 'diff-version) 222 | (define-key map "p" 'mark-backup-for-purge) 223 | (define-key map "x" 'purge-backups) 224 | (define-key map "q" (lambda () (interactive) (bm-kill-all-buffers backups-mode-data-alist))) ;; quit buffer and cleanup all other buffers opened up in the process 225 | (define-key map [remap next-line] 'forward-line) 226 | (define-key map [remap previous-line] (lambda () (interactive) (previous-line) (beginning-of-line))) 227 | map)) 228 | 229 | (defun backups-mode () 230 | "Major mode for viewing and reverting backup files" 231 | (interactive) 232 | (kill-all-local-variables) 233 | (use-local-map (backups-mode-map)) 234 | (buffer-disable-undo) 235 | (setq header-line-format (concat (when (backup-walker-p) " backup-walker,") 236 | " view backup, + diff, revert,

+ delete, quit")) 237 | (setq major-mode 'backups-mode) 238 | (setq mode-name "Backups-list") 239 | (run-hooks 'backups-mode-hook)) 240 | 241 | (define-minor-mode view-backup-mode () 242 | "Minor mode for viewing a single backup file" 243 | " Backup-file" 244 | '(("q" . (lambda () (interactive) (bm-kill-popup-buffer backups-mode-data-alist))) 245 | ("d" . diff-with-current) 246 | ("r" . (lambda () (interactive) (princ bm-revert-message))) 247 | ("R" . (lambda () (interactive) (bm-revert-backup-from-file (bm-get-original-file backups-mode-data-alist) 248 | (buffer-file-name) 249 | backups-mode-data-alist)))) 250 | :init-value nil) 251 | 252 | (define-minor-mode diff-backup-mode () 253 | "Minor mode for viewing a backup diff" 254 | "" 255 | '(("q" . (lambda () (interactive) (bm-kill-popup-buffer backups-mode-data-alist))) 256 | ("1" . (lambda () (interactive) (view-backup-from-diff (cdr (assoc :first-file-name backups-mode-data-alist))))) 257 | ("2" . (lambda () (interactive) (view-backup-from-diff (cdr (assoc :second-file-name backups-mode-data-alist)))))) 258 | :init-value nil) 259 | 260 | ;;; backups-mode private methods 261 | (defun bm-get-file-name-from-index (index) 262 | (bm-get-file-name (nth index (bm-get-backups backups-mode-data-alist)))) 263 | 264 | (defun bm-get-index-number (line-number) 265 | (- line-number 2)) 266 | 267 | (defun bm-get-line-number (index) 268 | (+ index 2)) 269 | 270 | ;;; backups-mode commands 271 | 272 | ;; view commands 273 | (defun view-backup () 274 | "View a single backup file in a popup window. 275 | If you choose to view the current version of the file, 276 | this will close backups-mode and move the user back to the current file." 277 | (interactive) 278 | (let ((index (bm-get-index-number (line-number-at-pos)))) 279 | (cond ((zerop index) 280 | (princ bm-current-file-message)) 281 | ((and (> index 0) (< index (length (bm-get-backups backups-mode-data-alist)))) 282 | (bm-open-file-read-only (bm-get-file-name-from-index index))) 283 | (t (princ bm-no-file-message))))) 284 | 285 | (defun view-backup-from-diff (filename) 286 | "View a single backup file" 287 | (interactive) 288 | (let ((original-file (bm-get-original-file backups-mode-data-alist))) 289 | (if (equal filename original-file) 290 | (princ bm-current-file-message) 291 | (bm-open-file-read-only filename)))) 292 | 293 | (defun bm-open-file-read-only (filename) 294 | (setq ro-buffer (find-file-noselect filename)) 295 | (setq current-config (current-window-configuration)) 296 | (let* ((orig-data (copy-alist backups-mode-data-alist))) 297 | (bm-switch-to-window ro-buffer 'backups-minor-mode-p) 298 | (view-backup-mode t) 299 | (bm-rename-buffer filename orig-data) 300 | (setq backups-mode-data-alist orig-data) 301 | (setq header-line-format (format " diff with current, revert, quit")))) 302 | 303 | 304 | ;; revert commands 305 | (defun revert-backup () 306 | "Save the current file as a version then replace it with 307 | the chosen backup." 308 | (interactive) 309 | (let ((index (bm-get-index-number (line-number-at-pos)))) 310 | (cond ((zerop index) 311 | (princ "Cannot revert current buffer")) 312 | ((and (> index 0) (< index (length (bm-get-backups backups-mode-data-alist)))) 313 | (bm-revert-backup-from-file (bm-get-original-file backups-mode-data-alist) 314 | (bm-get-file-name-from-index index) 315 | backups-mode-data-alist)) 316 | (t (princ bm-no-file-message))))) 317 | 318 | (defun bm-revert-backup-from-file (orig-file-name backup-file-name data) 319 | (let* ((temp-backup-file-name (concat backup-file-name "#temp#")) 320 | (orig-buffer-name (buffer-name (get-file-buffer orig-file-name)))) 321 | ;; using a temp file is necessary since saving the buffer may delete the backup file before it can be restored 322 | (bm-kill-all-buffers data) 323 | (copy-file backup-file-name temp-backup-file-name t) 324 | (when orig-buffer-name 325 | (switch-to-buffer orig-buffer-name) 326 | (save-buffer) ;; first, save the buffer. This is so the current changes become a saved version 327 | (save-version) ;; save a version of the current buffer 328 | (kill-buffer)) ;; kill the original buffer 329 | (copy-file temp-backup-file-name orig-file-name t) ;; move the temp file to become the current file 330 | (delete-file temp-backup-file-name) 331 | (find-file orig-file-name))) 332 | 333 | ;; diff commands 334 | (defun diff-version () 335 | "Diff two versions of the file." 336 | (interactive) 337 | (let* ((line-number (line-number-at-pos)) 338 | (index (bm-get-index-number line-number)) 339 | (orig-buffer-name (buffer-name (get-file-buffer (bm-get-original-file backups-mode-data-alist))))) 340 | (if (and (>= index 0) (< index (length (bm-get-backups backups-mode-data-alist)))) 341 | (progn 342 | (cond ((eq first-diff-index index) 343 | (beginning-of-line) 344 | (delete-char 1) 345 | (insert " ") 346 | (setq first-diff-index nil) 347 | (beginning-of-line)) 348 | (first-diff-index 349 | (goto-line (bm-get-line-number first-diff-index)) 350 | (delete-char 1) 351 | (insert " ") 352 | (goto-line line-number) 353 | (progn 354 | (when (and 355 | (zerop first-diff-index) 356 | (get-buffer orig-buffer-name) 357 | (buffer-modified-p (get-buffer orig-buffer-name))) 358 | (let ((backups-mode-buffer-name (buffer-name))) 359 | (switch-to-buffer orig-buffer-name) 360 | (save-buffer) 361 | (switch-to-buffer backups-mode-buffer-name))) 362 | (let ((first-file-name (bm-get-file-name-from-index first-diff-index)) 363 | (second-file-name (bm-get-file-name-from-index index))) 364 | (setq first-diff-index nil) 365 | (set-buffer-modified-p nil) 366 | (bm-diff-files first-file-name second-file-name)))) 367 | (t 368 | (setq first-diff-index index) 369 | (beginning-of-line) 370 | (insert "d") 371 | (delete-char 1) 372 | (forward-line))) 373 | (set-buffer-modified-p nil)) 374 | (princ bm-no-file-message))) ) 375 | 376 | (defun diff-with-current () 377 | "diff the current backup buffer with the current version of the file" 378 | (interactive) 379 | (let ((first-file-name (bm-get-original-file backups-mode-data-alist)) 380 | (second-file-name (buffer-file-name))) 381 | (bm-diff-files first-file-name second-file-name))) 382 | 383 | (defun bm-diff-files (first-file-name second-file-name) 384 | (setq diff-buffer (diff-no-select first-file-name second-file-name)) 385 | (setq current-config (current-window-configuration)) 386 | (with-current-buffer diff-buffer 387 | (diff-backup-mode t)) ;; must set minor mode before switching to diff buffer 388 | (let* ((orig-data (copy-alist backups-mode-data-alist))) 389 | (bm-switch-to-window diff-buffer 'backups-minor-mode-p) 390 | (push `(:first-file-name . ,first-file-name) orig-data) 391 | (push `(:second-file-name . ,second-file-name) orig-data) 392 | (setq backups-mode-data-alist orig-data) 393 | (setq header-line-format " quit, <1> view first, <2> view second"))) 394 | 395 | 396 | ;; deletion 397 | (defun mark-backup-for-purge () 398 | "mark backups for batch deletion" 399 | (interactive) 400 | (let ((index (bm-get-index-number (line-number-at-pos))) 401 | (marked (bm-get-marked-for-purging backups-mode-data-alist))) 402 | (cond ((zerop index) 403 | (princ "Cannot mark the current file for purging")) 404 | ((and (>= index 0) (< index (length (bm-get-backups backups-mode-data-alist)))) 405 | (if (memq index marked) 406 | (progn 407 | (beginning-of-line) 408 | (delete-char 1) 409 | (insert " ") 410 | (setcdr (assq :marked-for-purging backups-mode-data-alist) 411 | (delq index marked)) 412 | (beginning-of-line)) 413 | (progn 414 | (beginning-of-line) 415 | (insert "p") 416 | (delete-char 1) 417 | (setcdr (assq :marked-for-purging backups-mode-data-alist) 418 | (push index marked)) 419 | (forward-line))) 420 | (set-buffer-modified-p nil)) 421 | (t 422 | (princ bm-no-file-message))))) 423 | 424 | (defun purge-backups () 425 | "Purge (delete) backups" 426 | (interactive) 427 | (let ((marked (bm-get-marked-for-purging backups-mode-data-alist))) 428 | (cond ((zerop (length marked)) 429 | (princ "No backups marked to purge")) 430 | ((y-or-n-p "Purge the marked backups") 431 | (mapc 432 | (lambda (index) 433 | (let* ((file-name (bm-get-file-name-from-index index)) 434 | (buf (get-file-buffer file-name))) 435 | (when (buffer-live-p buf) 436 | (kill-buffer buf)) 437 | (delete-file file-name))) 438 | marked) 439 | (list-backups-from-file (bm-get-original-file backups-mode-data-alist) 440 | :data backups-mode-data-alist))))) 441 | 442 | ;; also require lewang's backup-walker if it exists 443 | ;; use the most up-to-date version first 444 | ;; fallback to a bundled version 445 | (or (require 'backup-walker nil 'noerror) 446 | (let ((load-path 447 | (cons (expand-file-name "fallback" 448 | (file-name-directory load-file-name)) 449 | load-path))) 450 | (require 'backup-walker))) 451 | 452 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 453 | ;;; backups-mode.el ends here 454 | -------------------------------------------------------------------------------- /bm-utilities.el: -------------------------------------------------------------------------------- 1 | 2 | ;; functions common to both backups-mode and backup-walker 3 | (defun bm-kill-all-buffers (data) 4 | (let* ((minor-buffers (bm-other-buffers 'backups-minor-mode-p)) 5 | (backups-buffer (cdr (assq :backups-buffer data))) 6 | (walking-buffer (cdr (assq :walking-buffer data))) 7 | (first-config (bm-get-first-config data))) 8 | 9 | ;; first, cleanup all minor mode open buffers 10 | (bm-kill-open-buffers minor-buffers) 11 | 12 | ;; then, cleanup the backups-buffer 13 | (when (buffer-live-p backups-buffer) 14 | (kill-buffer backups-buffer)) 15 | 16 | ;; then, cleanup the backup-walker buffer 17 | (when (buffer-live-p walking-buffer) 18 | (kill-buffer walking-buffer)) 19 | 20 | ;; finally, reset the window configuration 21 | (set-window-configuration 22 | (bm-get-first-config data)))) 23 | 24 | (defun bm-kill-open-buffers (buffers) 25 | (mapc (lambda (buffer) 26 | (when (buffer-live-p buffer) 27 | (kill-buffer buffer))) 28 | buffers)) 29 | 30 | (defun bm-kill-popup-buffer (data) 31 | (let ((second-config (bm-get-second-config data)) 32 | others) 33 | (kill-buffer) 34 | (setq others (bm-other-buffers 'backups-minor-mode-p)) 35 | (if others 36 | (switch-to-buffer (car others)) 37 | (set-window-configuration second-config)))) 38 | 39 | (defun bm-other-buffers (pred) 40 | (filter (lambda (buf) 41 | (with-current-buffer buf 42 | (funcall pred))) 43 | (buffer-list))) 44 | 45 | (defun bm-switch-to-window (buffer-or-name pred) 46 | (let ((similar-window (get-window-with-predicate (make-backups-window-p pred))) 47 | (window-count (length (window-list))) 48 | width 49 | height) 50 | (if (and similar-window (window-live-p similar-window)) 51 | (select-window similar-window) 52 | (if (>= window-count 4) 53 | (other-window 1) ;; don't split if 4+ windows exist 54 | (progn 55 | (select-window (get-largest-window)) 56 | (setq width (window-width)) 57 | (setq height (window-height)) 58 | (split-window nil nil 59 | (> (/ width height) 2.1))))) ;; heuristic to popup vertically or horizontally 60 | (switch-to-buffer buffer-or-name))) 61 | 62 | (defun backups-major-mode-p () 63 | (and (or (eq major-mode 'backups-mode) 64 | (eq major-mode 'backup-walker-mode)) 65 | (not diff-backup-mode))) 66 | 67 | (defun backups-minor-mode-p () 68 | (or diff-backup-mode 69 | view-backup-mode 70 | backup-walker-minor-mode)) 71 | 72 | (defun make-backups-window-p (pred) 73 | (lexical-let ((pred pred)) 74 | (lambda (window) 75 | (set-buffer (window-buffer window)) 76 | (funcall pred)))) 77 | 78 | ;;; list-backups helper methods 79 | (defun bm-backup-files (original-file) 80 | (let* ((backup-file (file-name-sans-versions 81 | (make-backup-file-name (expand-file-name original-file)))) 82 | (backup-directory (file-name-directory backup-file))) 83 | (mapcar 84 | (lambda (f) (concat backup-directory f)) 85 | (file-name-all-completions 86 | (file-name-nondirectory backup-file) 87 | backup-directory)))) 88 | 89 | (defun bm-get-sorted-backups (original-file backup-files-function) 90 | (flet ((file-sort-p (file-name1 file-name2) 91 | (let ((version1 (bm-make-version-number file-name1)) 92 | (version2 (bm-make-version-number file-name2))) 93 | (> version1 version2)))) 94 | (mapcar 'bm-make-file 95 | (cons original-file (sort 96 | (funcall backup-files-function original-file) 97 | 'file-sort-p))))) 98 | 99 | (defun bm-make-version-number (file-name) 100 | (let ((try-version-index (string-match "~[0-9]+~$" file-name))) 101 | (when try-version-index 102 | (bm-full-version-number file-name try-version-index)))) 103 | 104 | (defun bm-full-version-number (file-name start &optional number-str) 105 | (let* ((number-str (or number-str "")) 106 | (number (string-to-number number-str))) 107 | (if (< start (length file-name)) 108 | (let ((current-char (substring file-name (+ 1 start) (+ 2 start)))) 109 | (cond ((equal current-char "0") (bm-full-version-number file-name (+ 1 start) (concat number-str current-char))) 110 | ((equal (string-to-number current-char) 0) number) 111 | (t (bm-full-version-number file-name (+ 1 start) (concat number-str current-char))))) 112 | number))) 113 | 114 | ;;; file structure methods 115 | (defun bm-make-file (file-name) 116 | (flet ((make-last-modified-date 117 | (file-name) 118 | (let ((last-modified-date 119 | (shell-command-to-string 120 | (funcall last-modified-date-command-function file-name)))) 121 | (when (not (equal (car (split-string last-modified-date)) unknown-last-modified-date)) 122 | last-modified-date)))) 123 | (list 124 | (bm-make-version-number file-name) 125 | (make-last-modified-date file-name) 126 | file-name))) 127 | 128 | (defun bm-get-version (file) 129 | (nth 0 file)) 130 | 131 | (defun bm-get-last-modified-date (file) 132 | (nth 1 file)) 133 | 134 | (defun bm-get-file-name (file) 135 | (nth 2 file)) 136 | 137 | ;; data-alist helper methods 138 | (defun bm-get-original-file (data) 139 | (cdr (assq :original-file data))) 140 | 141 | (defun bm-get-backups (data) 142 | (cdr (assq :backups data))) 143 | 144 | (defun bm-get-marked-for-purging (data) 145 | (cdr (assq :marked-for-purging data))) 146 | 147 | (defun bm-get-first-config (data) 148 | (cdr (assq :first-config data))) 149 | 150 | (defun bm-get-second-config (data) 151 | (cdr (assq :second-config data))) 152 | 153 | ;; generic helper functions 154 | (defun filter (condp lst) 155 | (delq nil 156 | (mapcar (lambda (x) (and (funcall condp x) x)) lst))) 157 | 158 | (defun diff-no-select (old new &optional switches no-async) 159 | (save-window-excursion 160 | (ediff old new switches)) 161 | ;; (get-buffer-create "*Diff*") 162 | ) 163 | 164 | (defun bm-rename-buffer (filename data) 165 | (rename-buffer (concat (file-name-nondirectory (bm-get-original-file data)) 166 | " " 167 | (number-to-string (bm-make-version-number filename))))) 168 | 169 | ;;; variables common to both backups-mode and backup-walker 170 | ;; minor mode variables 171 | (defvar backup-walker-minor-mode nil "non-nil if backup walker minor mode is enabled") 172 | (make-variable-buffer-local 'backup-walker-minor-mode) 173 | 174 | (defvar view-backup-mode nil "non-nil if viewing a backup from backups-mode") 175 | (make-variable-buffer-local 'view-backup-mode) 176 | 177 | (defvar diff-backup-mode nil "non-nil if diffing a backup from backups-mode") 178 | (make-variable-buffer-local 'diff-backup-mode) 179 | 180 | ;; common string literals 181 | (defvar bm-revert-message "Use a capital R to revert") 182 | (defvar bm-current-file-message "Cannot view the current file in read-only mode") 183 | (defvar bm-no-file-message "No file on this line") 184 | -------------------------------------------------------------------------------- /fallback/.nosearch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadbraunduin/backups-mode/4b591badf7b12905a2da8c822899283eb583bd57/fallback/.nosearch -------------------------------------------------------------------------------- /fallback/backup-walker.el: -------------------------------------------------------------------------------- 1 | ;;; backup-walker.el --- quickly traverse all backups of a file 2 | 3 | ;; this file is not part of Emacs 4 | 5 | ;; Copyright (C) 2011 Le Wang 6 | ;; Author: Le Wang 7 | ;; Maintainer: Chad Braun-Duin 8 | ;; Description: quickly traverse all backups of a file 9 | 10 | ;; Created: Wed Sep 7 19:38:05 2011 (+0800) 11 | ;; Version: 2.0 12 | ;; Last-Updated: Web Oct 12 22:54 2011 (+0800) 13 | ;; By: Chad Braun-Duin 14 | ;; URL: https://github.com/chadbraunduin/backups-mode 15 | ;; Keywords: backup 16 | ;; Compatibility: Emacs 23+ 17 | 18 | ;;; Installation: 19 | 20 | ;; 21 | ;; add to ~/.emacs.el 22 | ;; 23 | ;; (require 'backup-walker) 24 | ;; 25 | ;; This installation is unnecessary if you've already put this version of 26 | ;; backup-walker.el and bm-utilities.el in your emacs load-path and have 27 | ;; required and started backups-mode 28 | 29 | ;;; Commentary (from lewang): 30 | 31 | ;; I never delete backups. They are versioned in their own directory, happy 32 | ;; and safe. My fingers skip to C-x C-s whenever I pause to think about 33 | ;; anything. Even when I'm working with VCS, I save far more often than I 34 | ;; commit. 35 | ;; 36 | ;; This package helps me traverse those backups if I'm looking for something. 37 | ;; 38 | ;; The typical workflow is: 39 | ;; 40 | ;; 1) I'm in a buffer and realize I need to check some backups. 41 | ;; 42 | ;; C-cw 43 | ;; 44 | ;; 2) I press

to go backwards in history until I see something 45 | ;; interesting. Then I press to bring it up. OOPs this isn't 46 | ;; it, I go back to the backup-walker window and find the right file. 47 | ;; 48 | ;; 3) I get what I need from the backup, go back to backup-walker, and press 49 | ;; and kill all open backups. 50 | ;; 51 | ;; 4) the end. 52 | ;; 53 | ;; Additionally, note that all the diff-mode facilities are available in the 54 | ;; `backup-walker' buffer. 55 | ;; 56 | 57 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 58 | ;; 59 | ;; This program is free software; you can redistribute it and/or 60 | ;; modify it under the terms of the GNU General Public License as 61 | ;; published by the Free Software Foundation; either version 3, or 62 | ;; (at your option) any later version. 63 | ;; 64 | ;; This program is distributed in the hope that it will be useful, 65 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 66 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 67 | ;; General Public License for more details. 68 | ;; 69 | ;; You should have received a copy of the GNU General Public License 70 | ;; along with this program; see the file COPYING. If not, write to 71 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 72 | ;; Floor, Boston, MA 02110-1301, USA. 73 | ;; 74 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 75 | 76 | ;;; Code: 77 | 78 | (provide 'backup-walker) 79 | (load "bm-utilities.el") 80 | 81 | (eval-when-compile (require 'cl)) 82 | (eval-and-compile (require 'diff)) 83 | 84 | (defun backups-mode-p () 85 | (fboundp 'backups-mode-start)) 86 | 87 | (defvar backup-walker-ro-map (make-sparse-keymap)) 88 | (define-key backup-walker-ro-map [(n)] 'backup-walker-next) 89 | (define-key backup-walker-ro-map [(p)] 'backup-walker-previous) 90 | (define-key backup-walker-ro-map [(q)] 'backup-walker-quit) 91 | (define-key backup-walker-ro-map [(return)] 'backup-walker-show-file-in-other-window) 92 | (when (backups-mode-p) 93 | (define-key backup-walker-ro-map [(b)] (lambda () 94 | (interactive) 95 | (list-backups-from-file 96 | (cdr (assq :original-file backup-walker-data-alist)) 97 | :data backup-walker-data-alist)))) 98 | 99 | (define-derived-mode backup-walker-mode diff-mode "Diff backup-walker" 100 | "major mode for traversing versioned backups. Use 101 | `backup-walker-start' as entry point." 102 | (run-hooks 'view-mode-hook) ; diff-mode sets up this hook to 103 | ; remove its read-only overrides. 104 | (add-to-list 'minor-mode-overriding-map-alist `(buffer-read-only . ,backup-walker-ro-map))) 105 | 106 | 107 | (defun backup-walker-minor-mode (&optional arg) 108 | "purposefully made non-interactive, because this mode should only be used by code" 109 | (setq arg (cond ((or (null arg) 110 | (eq arg 'toggle)) 111 | (if backup-walker-minor-mode 112 | nil 113 | t)) 114 | ((> arg 0) 115 | t) 116 | ((<= arg 0) 117 | nil))) 118 | (setq backup-walker-minor-mode arg) 119 | (force-mode-line-update) 120 | (if backup-walker-minor-mode 121 | (let ((index (cdr (assq :index backup-walker-data-alist))) 122 | (suffixes (cdr (assq :backup-suffix-list backup-walker-data-alist)))) 123 | (setq header-line-format (backup-walker-get-key-help-common index suffixes (concat "viewing " (propertize (int-to-string 124 | (backup-walker-get-version 125 | (nth index suffixes))) 126 | 'face 'font-lock-keyword-face) 127 | ", "))) 128 | (add-to-list 'minor-mode-overriding-map-alist `(buffer-read-only . ,backup-walker-ro-map)) 129 | (when (backups-mode-p) 130 | (define-key backup-walker-ro-map "r" (lambda () (interactive) (princ bm-revert-message))) 131 | (define-key backup-walker-ro-map "R" (lambda () (interactive) 132 | (bm-revert-backup-from-file (cdr (assq :original-file backup-walker-data-alist)) 133 | (buffer-file-name) 134 | backup-walker-data-alist))))) 135 | (setq header-line-format nil) 136 | (delq `(buffer-read-only . ,backup-walker-ro-map) minor-mode-overriding-map-alist)) 137 | backup-walker-minor-mode) 138 | 139 | (add-minor-mode 'backup-walker-minor-mode " walker" nil nil nil) 140 | 141 | (defvar backup-walker-data-alist nil "") 142 | (make-variable-buffer-local 'backup-walker-data-alist) 143 | 144 | (defsubst backup-walker-get-version (fn &optional start) 145 | "return version number given backup" 146 | (if start 147 | (string-to-int 148 | (substring fn 149 | (string-match "[[:digit:]]+" fn start) 150 | (match-end 0))) 151 | (backup-walker-get-version fn (length (file-name-sans-versions fn))))) 152 | 153 | (defsubst backup-walker-get-key-help-common (index suffixes current-file-string) 154 | (concat 155 | (if (eq index 0) 156 | (if (eq major-mode 'backup-walker-mode) 157 | (concat (propertize "current" 'face 'font-lock-keyword-face) 158 | ", ") 159 | "") 160 | (concat (propertize " " 'face 'italic) 161 | (propertize (int-to-string (backup-walker-get-version (nth (1- index) suffixes))) 162 | 'face 'font-lock-keyword-face) 163 | ", ")) 164 | current-file-string 165 | (if (nth (1+ index) suffixes) 166 | (concat (propertize "

" 'face 'italic) 167 | (propertize (int-to-string 168 | (backup-walker-get-version (nth (1+ index) suffixes))) 169 | 'face 'font-lock-keyword-face) 170 | ", ") 171 | "") 172 | (when backup-walker-minor-mode 173 | " revert, ") 174 | (propertize "" 'face 'italic) 175 | " quit")) 176 | 177 | ;; TODO: We can actually compute the correct new point by diffing and 178 | ;; interpreting results. So far, it seems overkill. 179 | (defsubst backup-walker-move (index-cons index suffixes new-index) 180 | "internal function used by backup-walker-{next,previous}" 181 | (cond 182 | ((eq major-mode 'backup-walker-mode) 183 | (setcdr index-cons new-index) 184 | (backup-walker-refresh)) 185 | (backup-walker-minor-mode 186 | (let* ((prefix (cdr (assq :backup-prefix backup-walker-data-alist))) 187 | (file-name (concat prefix (nth new-index suffixes))) 188 | (alist (copy-alist backup-walker-data-alist)) 189 | (buf (find-file-noselect file-name))) 190 | (setcdr (assq :index alist) new-index) 191 | (with-current-buffer buf 192 | (setq backup-walker-data-alist alist) 193 | (backup-walker-minor-mode 1)) 194 | (switch-to-buffer buf) 195 | (bm-rename-buffer file-name backup-walker-data-alist))))) 196 | 197 | (defun backup-walker-get-sorted-backups (filename) 198 | "Return version sorted list of backups of the form: 199 | (prefix (list of suffixes))" 200 | ;; `make-backup-file-name' will get us the right directory for 201 | ;; ordinary or numeric backups. It might create a directory for 202 | ;; backups as a side-effect, according to `backup-directory-alist'. 203 | (let* ((filename (file-name-sans-versions 204 | (make-backup-file-name (expand-file-name filename)))) 205 | (file (file-name-nondirectory filename)) 206 | (dir (file-name-directory filename)) 207 | (comp (file-name-all-completions file dir)) 208 | (prefix-len (length file))) 209 | (cons filename (mapcar 210 | (lambda (f) 211 | (substring (cdr f) prefix-len)) 212 | (sort 213 | (mapcar (lambda (f) 214 | (cons (backup-walker-get-version f prefix-len) 215 | f)) 216 | comp) 217 | (lambda (f1 f2) 218 | (not (< (car f1) (car f2))))))))) 219 | 220 | 221 | (defun backup-walker-refresh () 222 | (let* ((index (cdr (assq :index backup-walker-data-alist))) 223 | (suffixes (cdr (assq :backup-suffix-list backup-walker-data-alist))) 224 | (prefix (cdr (assq :backup-prefix backup-walker-data-alist))) 225 | (right-file (concat prefix (nth index suffixes))) 226 | (right-version (format "%i" (backup-walker-get-version right-file))) 227 | diff-buf left-file left-version) 228 | ;; (debug) 229 | (if (eq index 0) 230 | (setq left-file (cdr (assq :original-file backup-walker-data-alist)) 231 | left-version "orig") 232 | (setq left-file (concat prefix (nth (1- index) suffixes)) 233 | left-version (format "%i" (backup-walker-get-version left-file)))) 234 | (setq diff-buf (diff-no-select left-file right-file nil 'noasync)) 235 | ;; (debug) 236 | (setq buffer-read-only nil) 237 | (delete-region (point-min) (point-max)) 238 | (insert-buffer-substring diff-buf) 239 | (goto-char (point-min)) 240 | (set-buffer-modified-p nil) 241 | (setq buffer-read-only t) 242 | (force-mode-line-update) 243 | (setq header-line-format 244 | (concat (when (backups-mode-p) 245 | " backups-mode, ") 246 | (backup-walker-get-key-help-common index 247 | suffixes 248 | (concat (propertize "" 'face 'italic) 249 | " opens " 250 | (propertize (propertize (int-to-string (backup-walker-get-version right-file)) 251 | 'face 'font-lock-keyword-face)) 252 | ", ")))) 253 | (kill-buffer diff-buf))) 254 | 255 | ;;;###autoload 256 | (defun* backup-walker-start (original-file &key (index 0) data) 257 | "start walking with the latest backup 258 | with universal arg, ask for a file-name." 259 | (interactive (list (if (and current-prefix-arg (listp current-prefix-arg)) 260 | (read-file-name 261 | "Original file: " 262 | nil 263 | buffer-file-name 264 | t 265 | (file-name-nondirectory buffer-file-name)) 266 | (or buffer-file-name 267 | (error "buffer has no file name"))))) 268 | (unless (and version-control 269 | (not (eq version-control 'never))) 270 | (error "version-control must be enabled for backup-walker to function.")) 271 | (let ((first-config (current-window-configuration)) 272 | (backups (backup-walker-get-sorted-backups original-file)) 273 | buf) 274 | 275 | (if (null (cdr backups)) 276 | (error "no backups found.") 277 | 278 | (unless (assq :original-file data) 279 | (push `(:original-file . ,original-file) data)) 280 | (push `(:backup-prefix . ,(car backups)) data) 281 | (push `(:backup-suffix-list . ,(cdr backups)) data) 282 | (push `(:index . ,(+ 0)) data) 283 | (unless (assq :first-config data) 284 | (push `(:first-config . ,first-config) data)) 285 | 286 | (setq buf (get-buffer-create (format "*Walking: %s*" (buffer-name (get-file-buffer original-file))))) 287 | (with-current-buffer buf 288 | (push `(:walking-buffer . ,buf) data) 289 | (backup-walker-mode)) 290 | (bm-switch-to-window buf 'backups-major-mode-p) 291 | (buffer-disable-undo) 292 | (setq second-config (current-window-configuration)) 293 | (if (assq :second-config data) 294 | (setcdr (assq :second-config data) second-config) 295 | (push `(:second-config . ,second-config) data)) 296 | (setq backup-walker-data-alist data) 297 | (backup-walker-refresh) 298 | (when (> index 0) 299 | (backup-walker-previous index)) 300 | buf))) 301 | 302 | 303 | (defun backup-walker-next (arg) 304 | "move to a more recent backup 305 | with ARG, move ARG times" 306 | (interactive "p") 307 | (cond ((< arg 0) 308 | (backup-walker-previous (- arg))) 309 | ((> arg 0) 310 | (let* ((index-cons (assq :index backup-walker-data-alist)) 311 | (index (cdr index-cons)) 312 | (suffixes (cdr (assq :backup-suffix-list backup-walker-data-alist))) 313 | (new-index (- index arg))) 314 | (if (< new-index 0) 315 | (signal 'args-out-of-range (list (format "not enough newer backups, max is %i" index))) 316 | (backup-walker-move index-cons index suffixes new-index)))))) 317 | 318 | (defun backup-walker-previous (arg) 319 | "move to a less recent backup 320 | with ARG move ARG times" 321 | (interactive "p") 322 | (cond ((< arg 0) 323 | (backup-walker-next (- arg))) 324 | ((> arg 0) 325 | (let* ((index-cons (assq :index backup-walker-data-alist)) 326 | (index (cdr index-cons)) 327 | (suffixes (cdr (assq :backup-suffix-list backup-walker-data-alist))) 328 | (max-movement (- (1- (length suffixes)) index)) 329 | (new-index (+ index arg))) 330 | (if (> arg max-movement) 331 | (signal 'args-out-of-range (list (format "not enough older backups, max is %i" max-movement))) 332 | (backup-walker-move index-cons index suffixes new-index)))))) 333 | 334 | (defun backup-walker-show-file-in-other-window () 335 | "open the current backup " 336 | (interactive) 337 | (unless (eq major-mode 'backup-walker-mode) 338 | (error "this is not a backup walker buffer.")) 339 | (let* ((index (cdr (assq :index backup-walker-data-alist))) 340 | (suffixes (cdr (assq :backup-suffix-list backup-walker-data-alist))) 341 | (prefix (cdr (assq :backup-prefix backup-walker-data-alist))) 342 | (file-name (concat prefix (nth index suffixes))) 343 | (walking-buf (current-buffer)) 344 | (alist (copy-alist backup-walker-data-alist)) 345 | (buf (find-file-noselect file-name))) 346 | (with-current-buffer buf 347 | (setq backup-walker-data-alist alist) 348 | (backup-walker-minor-mode 1)) 349 | (bm-switch-to-window buf 'backups-minor-mode-p) 350 | (bm-rename-buffer file-name backup-walker-data-alist))) 351 | 352 | (defun backup-walker-blame (line) 353 | "find out where a certain line came into existance 354 | show the backup *JUST BEFORE* this line was born, since that is 355 | usually more interesting." 356 | (interactive (list (read-string "line: " nil 'backup-walker-blame))) 357 | (cond 358 | (backup-walker-minor-mode 359 | (let* ((my-index (cdr (assq :index backup-walker-data-alist))) 360 | (walking-buf (cdr (assq :walking-buffer backup-walker-data-alist)))) 361 | (with-current-buffer walking-buf 362 | (setcdr (assq :index backup-walker-data-alist) my-index) 363 | (backup-walker-refresh)) 364 | (switch-to-buffer walking-buf) 365 | (backup-walker-blame line))) 366 | ((eq major-mode 'backup-walker-mode) 367 | (let* ((index-cons (assq :index backup-walker-data-alist)) 368 | (old-index (cdr index-cons)) 369 | (is-unified (member "-u" diff-switches)) 370 | (search-str (format "-%s" line)) 371 | found) 372 | (condition-case err 373 | (while (not found) 374 | (let ((suffixes (cdr (assq :backup-suffix-list backup-walker-data-alist))) 375 | (index (cdr (assq :index backup-walker-data-alist)))) 376 | (when (not is-unified) 377 | (diff-context->unified (point-min) (point-max))) 378 | (message "searching %s" (nth index suffixes)) 379 | (if (search-forward search-str nil t) 380 | (setq found t) 381 | (backup-walker-previous 1)))) 382 | ('args-out-of-range 383 | (setcdr index-cons old-index) 384 | (backup-walker-refresh) 385 | (message "input was not found in backup history"))))) 386 | (t 387 | (error "not sure what you want me to do.")))) 388 | 389 | (defun backup-walker-quit (arg) 390 | "quit backup-walker session. 391 | Also, kill all associated backup buffers." 392 | (interactive "P") 393 | (cond (backup-walker-minor-mode 394 | (bm-kill-popup-buffer backup-walker-data-alist)) 395 | ((eq major-mode 'backup-walker-mode) 396 | (bm-kill-all-buffers backup-walker-data-alist)) 397 | (t 398 | (error "I don't know how to quit you.")))) 399 | 400 | 401 | ;; also require chadbraunduin's backups-mode if it exists 402 | (require 'backups-mode nil 'noerror) 403 | 404 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 405 | ;; backup-walker.el ends here 406 | -------------------------------------------------------------------------------- /scripts/show-orphaned.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | backups_directory="/home/chadbraunduin/.emacs-backups/backups" 4 | cd "$backups_directory" 5 | for backup in *.*~ 6 | do 7 | orig_file=$backup 8 | orig_file="${orig_file//!//}" 9 | orig_file="${orig_file/%[.][~][0-9]*[~]/}" 10 | orig_file="${orig_file/%[~]/}" 11 | if [ ! -f $orig_file ] 12 | then 13 | echo "$backups_directory/$backup" 14 | fi 15 | done 16 | --------------------------------------------------------------------------------