/
pacfiles-mode.el
273 lines (246 loc) · 11.5 KB
/
pacfiles-mode.el
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
;;; pacfiles-mode.el --- The pacnew and pacsave merging tool -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2023 Carlos G. Cordero
;;
;; Author: Carlos G. Cordero <http://github/UndeadKernel>
;; Maintainer: Carlos G. Cordero <pacfiles@binarycharly.com>
;; Created: Oct 11, 2018
;; Modified: May 03, 2023
;; Version: 1.2
;; Keywords: files pacman arch pacnew pacsave update linux
;; URL: https://github.com/UndeadKernel/pacfiles-mode
;; Package-Requires: ((emacs "26.1"))
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; `pacfiles-mode' is an Emacs major mode to manage `.pacnew` and `.pacsave`
;; files left by Arch's pacman. To merge files, *pacfiles-mode* automatically
;; creates an Ediff merge session that a user can interact with. After finishing
;; the Ediff merge session, *pacfiles-mode* cleans up the mess that Ediff leaves
;; behind. *pacfiles-mode* also takes care of keeping the correct permissions of
;; merged files, and requests passwords (with TRAMP) to act as root when needed.
;;
;; Start the major mode using the command `pacfiles' or `pacfiles/start'.
;;
;;; Code:
(require 'pacfiles-buttons)
(require 'pacfiles-utils)
(require 'pacfiles-win)
(require 'pacfiles-automerge)
(require 'cl-lib)
(require 'ediff)
(require 'outline)
(require 'time-date)
(defgroup pacfiles nil "Options that relate to ‘pacfiles-mode’."
:group 'applications)
(defcustom pacfiles-updates-search-command
"find /etc -name '*.pacnew' -o -name '*.pacsave' 2>/dev/null"
"Command to find .pacnew files."
:type '(string)
:group 'pacfiles)
(defvar pacfiles--merge-search-command
(concat "find " (shell-quote-argument pacfiles-merge-file-tmp-location) " -name '*.pacmerge' 2>/dev/null")
"Command to search for temporarily merged files.")
(defvar pacfiles--ediff-conf '()
"Alist that stores ediff variables and its values.")
;;;###autoload
(defalias 'pacfiles 'pacfiles-start)
;;;###autoload
(defun pacfiles-start ()
"Find and manage pacman backup files in an Arch-based GNU/Linux system."
(interactive)
;; Save the current window configuration so that it can be restored when we are finished.
(pacfiles--push-window-conf)
;; Save ediff varaibles that we modify to later restore them to the uers's value.
(pacfiles--save-ediff-conf)
(let ((buffer (get-buffer-create pacfiles--files-buffer-name)))
(display-buffer buffer '(pacfiles--display-buffer-fullscreen))
(with-current-buffer buffer
(pacfiles-mode)
(pacfiles-revert-buffer t t))))
;;;###autoload
(defun pacfiles-quit ()
"Quit ‘pacfiles-mode’ and restore the previous window and ediff configuration."
(interactive)
(pacfiles--restore-ediff-conf)
;; Kill buffers we create which start with '*pacfiles:'
(kill-matching-buffers "^\\*pacfiles:.*" t t)
(pacfiles--pop-window-conf))
;; Main function that displays the contents of the PACFILES buffer.
;;;###autoload
(defun pacfiles-revert-buffer (&optional _ignore-auto noconfirm)
"Populate the ‘pacfiles-mode’ buffer with .pacnew and .pacsave files.
Ignore IGNORE-AUTO but take into account NOCONFIRM."
(interactive)
(with-current-buffer (get-buffer-create pacfiles--files-buffer-name)
(when (or noconfirm
(y-or-n-p (format "Reload list of backup pacman files? ")))
(run-hooks 'before-revert-hook)
;; The actual revert mechanism starts here
(let ((inhibit-read-only t)
(files (mapcar #'pacfiles--set-remote-path-maybe
(split-string (shell-command-to-string
pacfiles-updates-search-command) "\n" t)))
(merged-files (mapcar #'pacfiles--set-remote-path-maybe
(split-string (shell-command-to-string
pacfiles--merge-search-command) "\n" t)))
(pacnew-alist (list))
(pacsave-alist (list)))
(delete-region (point-min) (point-max))
(insert "* PACFILES MODE" "\n")
;; Split .pacnew and .pacsave files
(dolist (file files)
;; Associate each FILE in FILES with a file to hold the merge
(let ((merge-file
(pacfiles--set-remote-path-maybe
(pacfiles--calculate-merge-file file pacfiles-merge-file-tmp-location))))
(cond
((string-match-p ".pacnew" file)
(push (cons file merge-file) pacnew-alist))
((string-match-p ".pacsave" file)
(push (cons file merge-file) pacsave-alist))
(t (user-error (format "Cannot process file %s" file))))))
;; --- Process .pacnew files ---
(insert "\n\n" "** PACNEW files" "\n")
(insert "\n" "*** pending" "\n")
;; Display the .pacnew files that need merging
(pacfiles--insert-pending-files pacnew-alist merged-files)
(insert "\n" "*** merged" "\n")
;; Display .pacnew files that have an associated merge file.
(pacfiles--insert-merged-files pacnew-alist merged-files)
;; --- Process .pacsave files ---
(insert "\n\n" "** PACSAVE files" "\n")
(insert "\n" "*** pending" "\n")
;; Display the .pacsave files that need merging
(pacfiles--insert-pending-files pacsave-alist merged-files)
(insert "\n" "*** merged" "\n")
(pacfiles--insert-merged-files pacsave-alist merged-files)
(insert "\n\n")
(pacfiles--insert-footer-buttons))))
(goto-char 0))
;;;###autoload
(defun pacfiles-revert-buffer-no-confirm ()
"Revert the pacfiles list buffer without asking for confirmation."
(interactive)
(pacfiles-revert-buffer t t))
(defun pacfiles--insert-pending-files (files-alist merged-files)
"Insert files in FILES-ALIST if their `cdr' is not in MERGED-FILES.
The FILE-TYPE specifies which type of update file we are processing."
;; Keep files in FILES-ALIST which don't have a cdr in MERGED-FILES.
(let ((pending-alist (cl-remove-if (lambda (i) (member (cdr i) merged-files)) files-alist)))
(if (null pending-alist)
(insert (propertize "--- no pending files ---\n" 'font-lock-face 'font-lock-comment-face))
(dolist (file-pair pending-alist)
(pacfiles--insert-merge-button file-pair)
(pacfiles--insert-automerge-button-maybe file-pair)
(pacfiles--insert-diff-button (car file-pair))
(pacfiles--insert-delete-button file-pair)
(insert (car file-pair) " ")
(pacfiles--insert-days-old (car file-pair))
(insert "\n")))))
(defun pacfiles--insert-merged-files (files-alist merged-files)
"Insert files in FILES-ALIST that have an associated file in MERGED-FILES."
(let ((merged-alist (cl-remove-if-not (lambda (i) (member (cdr i) merged-files)) files-alist)))
(if (null merged-alist)
(insert (propertize "--- no merged files ---\n" 'font-lock-face 'font-lock-comment-face))
(dolist (file-pair merged-alist)
(pacfiles--insert-apply-button file-pair)
(pacfiles--insert-view-merge-button file-pair)
(pacfiles--insert-discard-button file-pair)
(insert (car file-pair) " ")
;; calculate how many days old is the merged file
(pacfiles--insert-days-created (cdr file-pair))
(insert "\n")))))
(defun pacfiles--insert-days-old (file)
"Insert how many days passed between FILE and FILE without its extension.
If REVERSE-ORDER is non-nil, calculate the time difference as
\(FILE without extension\) - FILE."
(let* ((base-file (file-name-sans-extension file))
(time-base-file
(time-to-seconds (file-attribute-modification-time (file-attributes base-file))))
(time-file
(time-to-seconds (file-attribute-modification-time (file-attributes file))))
(reverse-time (< time-file time-base-file)))
(when (file-exists-p base-file)
(insert
(propertize
(format "(%.1f %s)"
(time-to-number-of-days
(cond (reverse-time (time-subtract time-base-file time-file))
(t (time-subtract time-file time-base-file))))
(if reverse-time "day[s] old" "day[s] ahead"))
'font-lock-face 'font-lock-warning-face)))))
(defun pacfiles--insert-days-created (file)
"Insert the number of days since FILE was created."
(if (file-exists-p file)
(let ((time-file (file-attribute-modification-time (file-attributes file))))
(insert
(propertize
(format "(%.1f day[s] since created)" (time-to-number-of-days (time-since time-file)))
'font-lock-face 'font-lock-string-face)))
(error "File '%s' dosn't exist" file)))
(defun pacfiles--save-ediff-conf ()
"Save ediff variables we modify with the user's current values.
We restore the saved variables after ‘pacfiles-mode’ quits."
(require 'ediff)
(let ((vars-to-save
'(ediff-autostore-merges ediff-keep-variants ediff-window-setup-function
ediff-before-setup-hook ediff-quit-hook ediff-cleanup-hook ediff-quit-merge-hook
ediff-quit-hook ediff-split-window-function)))
(dolist (var vars-to-save)
(push (pacfiles--var-to-cons var) pacfiles--ediff-conf))))
(defun pacfiles--change-ediff-conf ()
"Change ediff's configuration variables to fit ‘pacfiles-mode’."
(setq ediff-autostore-merges nil
ediff-keep-variants t
ediff-window-setup-function #'ediff-setup-windows-plain
ediff-split-window-function #'split-window-horizontally)
(add-hook 'ediff-before-setup-hook #'pacfiles--push-window-conf)
(add-hook 'ediff-quit-hook #'pacfiles--pop-window-conf t)
(add-hook 'ediff-cleanup-hook #'pacfiles--clean-after-ediff)
(remove-hook 'ediff-quit-merge-hook #'ediff-maybe-save-and-delete-merge)
(add-hook 'ediff-quit-hook (lambda () (pacfiles-revert-buffer t t))))
(defun pacfiles--restore-ediff-conf ()
"Restore the ediff variables saved by `pacfiles--save-ediff-conf'."
(dolist (pair pacfiles--ediff-conf)
(pacfiles--cons-to-var pair))
(setq pacfiles--ediff-conf '()))
(defvar pacfiles-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "q") #'pacfiles-quit)
(define-key map (kbd "g") #'pacfiles-revert-buffer-no-confirm)
(define-key map (kbd "r") #'pacfiles-revert-buffer-no-confirm)
(define-key map (kbd "TAB") #'outline-toggle-children)
(define-key map (kbd "C-c C-p") #'outline-previous-heading)
(define-key map (kbd "C-c C-n") #'outline-next-heading)
(define-key map (kbd "n") #'forward-button)
(define-key map (kbd "p") #'backward-button)
map)
"Keymap for ‘pacfiles-mode’.")
;; Tell emacs that, when creating new buffers, pacfiles-mode should not be used
;; ... as the major mode.
(put 'pacfiles-mode 'mode-class 'special)
;;;###autoload
(define-derived-mode pacfiles-mode outline-mode "pacfiles"
:syntax-table nil
:abbrev-table nil
"Major mode for managing .pacnew and .pacsave files."
;; If the buffer is not the one we create, do nothing and error out.
(unless (string= (buffer-name) pacfiles--files-buffer-name)
(user-error "Use the command `pacfiles' instead of `pacfiles-mode' to start pacfiles-mode"))
;; The buffer shall not be edited.
(read-only-mode)
;; No edits... no undo.
(buffer-disable-undo)
;; Disable showing parents locally by letting the mode think it's disabled.
(setq-local show-paren-mode nil)
;; Set the function used when reverting pacfile-mode buffers.
(setq-local revert-buffer-function #'pacfiles-revert-buffer)
;; configure ediff
(pacfiles--change-ediff-conf)
;; configure outline-mode
(setq-local outline-blank-line t))
(provide 'pacfiles-mode)
;;; pacfiles-mode.el ends here