source: cl-darcs/tags/0.2.0/apply-patch.lisp

Last change on this file was 101, checked in by Magnus Henoch, 17 years ago

Tag 0.2.0

File size: 12.6 KB
Line 
1;;; Copyright (C) 2006 Magnus Henoch
2;;;
3;;; This program is free software; you can redistribute it and/or
4;;; modify it under the terms of the GNU General Public License as
5;;; published by the Free Software Foundation; either version 2 of the
6;;; License, or (at your option) any later version.
7;;;
8;;; This program is distributed in the hope that it will be useful,
9;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
10;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11;;; General Public License for more details.
12;;;
13;;; You should have received a copy of the GNU General Public License
14;;; along with this program; if not, write to the Free Software
15;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
16
17(in-package :darcs)
18
19(defmacro with-file-patching ((instreamvar outstreamvar filename) &body body)
20  "Open FILENAME for patching.
21Bind INSTREAMVAR to a stream that reads from FILENAME.
22Bind OUTSTREAMVAR to a stream that writes to a temporary file.
23If BODY finishes normally, overwrite FILENAME with the temporary file."
24  (let ((files-copied-gensym (gensym))
25        (filename-gensym (gensym)))
26    `(let ((,files-copied-gensym nil)
27           (,filename-gensym ,filename))
28       (restart-case
29           ;; Open the file to patch for reading.
30           (with-open-stream (,instreamvar
31                              (make-instance 'unreadable-stream
32                                             :base-stream
33                                             (open ,filename-gensym
34                                                   :direction :input
35                                                   :if-does-not-exist :error
36                                                   :element-type '(unsigned-byte 8))
37                                             :haskellish-lines t))
38             ;; Open a temporary file for writing.
39             (with-temp-file (,outstreamvar :element-type '(unsigned-byte 8))
40               (progn ,@body)
41
42               (close ,instreamvar)
43               (close ,outstreamvar)
44
45               (setf ,files-copied-gensym t)
46               ;; Copy the temporary file over the original.
47               (fad:copy-file (pathname ,outstreamvar) ,filename :overwrite t)))
48
49         ;; Until the temporary file is copied over the original, we can
50         ;; retry as many times we want.
51         ;; XXX: how can we enter a new version of the function?
52         ;; (retry-patch ()
53;;         :test (lambda (c) (declare (ignore c)) (not ,files-copied-gensym))
54;;         :report (lambda (stream)
55;;                   (format stream "Revert changes to ~A and retry patch" ,filename-gensym))
56;;         ,retry-form)
57         
58         (ignore-patch ()
59           :report (lambda (stream)
60                     (format stream "Ignore patch to ~A" ,filename-gensym))
61           nil)))))
62
63(defgeneric apply-patch (patch repodir)
64  (:documentation "Apply PATCH to working copy in REPODIR."))
65
66(defmethod apply-patch :around (patch repodir)
67  "Offer a RETRY restart for all patches.
68In some cases, the patch might be applied twice."
69  (restart-case
70      (call-next-method)
71    (retry ()
72      :report (lambda (stream)
73                (format stream "Retry patch ~A (possibly non-idempotent)" patch))
74      (apply-patch patch repodir))
75    (ignore ()
76      :report (lambda (stream)
77                (format stream "Ignore patch ~A" patch))
78      nil)))
79
80(defmethod apply-patch ((patch named-patch) repodir)
81  "Apply PATCH in REPODIR.
82That is, simply apply the patch contained in the named patch."
83  (dformat "~&Applying ~A: \"~A\"."
84           (patchinfo-date (named-patch-patchinfo patch))
85           (patchinfo-name (named-patch-patchinfo patch)))
86  (apply-patch (named-patch-patch patch) repodir))
87
88(defmethod apply-patch ((patch composite-patch) repodir)
89  (apply-patch-list (patches patch) repodir))
90
91(defmethod apply-patch ((patch change-pref-patch) repodir)
92  ;; Maybe we're applying the patch to a pristine directory, in which
93  ;; case we don't care about preferences.
94  (when (has-prefs-dir repodir)
95    (with-accessors ((pref change-pref-which)
96                     (from change-pref-from)
97                     (to change-pref-to)) patch
98      (let ((old-value (or (get-pref repodir pref) "")))
99        (unless (string= from old-value)
100          (warn
101           "While changing pref ~S to ~S, expected old value to be ~S, but it was ~S."
102           pref to from old-value))
103        (set-pref repodir pref to)))))
104
105(defmethod apply-patch ((patch add-file-patch) repodir)
106  "Create a file in REPODIR, by PATCH."
107  (let ((new-file (merge-pathnames
108                   (patch-filename patch) repodir)))
109    (dformat "~&Creating file ~A." new-file)
110    (open new-file :direction :probe
111          :if-does-not-exist :create
112          :if-exists :error)))
113
114(defmethod apply-patch ((patch binary-patch) repodir)
115  "Apply a binary patch in REPODIR."
116  (let ((file (merge-pathnames (patch-filename patch) repodir)))
117    ;; Check that the file matches the old content.
118    (with-open-file (in file
119                        :direction :input :if-does-not-exist :error
120                        :element-type '(unsigned-byte 8))
121      (when (or (/= (file-length in) (length (binary-oldhex patch)))
122                (let ((bytes (make-array (file-length in)
123                                         :element-type '(unsigned-byte 8))))
124                  (read-sequence bytes in)
125                  (not (equalp bytes (binary-oldhex patch)))))
126        (cerror "Write new contents to ~A anyway."
127                "Contents of ~A don't match patch." file)))
128    ;; Overwrite with new content.
129    (with-open-file (out file
130                         :direction :output :if-exists :supersede
131                         :element-type '(unsigned-byte 8))
132      (write-sequence (binary-newhex patch) out))))
133
134(defmethod apply-patch ((patch rm-file-patch) repodir)
135  "Delete a file in REPODIR.  File should be empty.
136If it's not, a warning will be signalled."
137  (let ((the-file (merge-pathnames
138                   (patch-filename patch) repodir)))
139    (with-open-file (in the-file :direction :input
140                        :if-does-not-exist :error)
141      (let ((length (file-length in)))
142      (unless (zerop length)
143        (warn "File ~A is not empty (contains ~A bytes)." the-file length))))
144    (dformat "~&Deleting file ~A." the-file)
145    (delete-file the-file)))
146
147(defmethod apply-patch ((patch add-dir-patch) repodir)
148  "Create a directory in REPODIR."
149  (let ((new-dir (merge-pathnames (patch-directory patch) repodir)))
150    (dformat "~&Creating directory ~A." new-dir)
151    (make-dir new-dir)))
152
153(defmethod apply-patch ((patch rm-dir-patch) repodir)
154  "Delete a directory in REPODIR.  Directory must be empty."
155  (let ((dir-to-remove (merge-pathnames (patch-directory patch) repodir)))
156    (dformat "~&Deleting directory ~A." dir-to-remove)
157    (delete-dir dir-to-remove)))
158
159(defmethod apply-patch ((patch move-patch) repodir)
160  "Move a file in REPODIR."
161  (let ((from (merge-pathnames (patch-move-from patch) repodir))
162        (to (merge-pathnames (patch-move-to patch) repodir)))
163;;     (fad:copy-file from to :overwrite nil)
164;;     (delete-file from)
165
166    ;; This seems to be an easier method, which works even if we're
167    ;; moving a directory.
168    #+sbcl (sb-ext:run-program "mv" (list (namestring from) (namestring to))
169                               :search t)
170    #+clisp (let ((result (ext:run-program "mv" :arguments (list (namestring from) (namestring to)))))
171              (unless (eql result 0)
172                (error "Couldn't move ~A to ~A." from to)))))
173
174(defmethod apply-patch ((patch token-replace-patch) repodir)
175  "Apply a token replace patch to a file in REPODIR."
176  (let ((filename (merge-pathnames (patch-filename patch) repodir))
177        (old-regexp (cl-ppcre:create-scanner
178                     (format nil "(^|[^~A])~A($|[^~A])"
179                             (token-regexp patch)
180                             (old-token patch)
181                             (token-regexp patch))))
182        (new-regexp (cl-ppcre:create-scanner
183                     (format nil "(^|[^~A])~A($|[^~A])"
184                             (token-regexp patch)
185                             (new-token patch)
186                             (token-regexp patch))))
187        (replacement (format nil "\\1~A\\2" (new-token patch))))
188    (dformat "~&Patching ~A with ~A." filename patch)
189    (with-file-patching (in out filename)
190      (let ((file-empty t))
191        (flet ((maybe-terpri ()
192                 ;; Unless we're writing the first line, we have to
193                 ;; terminate the previous one.
194                 (if file-empty
195                     (setf file-empty nil)
196                     (write-byte 10 out))))
197          (loop
198             (let ((line (read-binary-line in nil :eof)))
199               (when (eql line :eof)
200                 (return))
201               (maybe-terpri)
202
203               (setf line (bytes-to-string line))
204               (when (cl-ppcre:scan new-regexp line)
205                 (cerror "Ignore" "While replacing ~S with ~S, found ~S before patching: ~S."
206                         (old-token patch) (new-token patch) (new-token patch) line))
207               
208               (let ((patched-line (cl-ppcre:regex-replace-all old-regexp line replacement)))
209                 (write-sequence (string-to-bytes patched-line) out)))))))))
210     
211(defmethod apply-patch ((patch hunk-patch) repodir)
212  "Apply a single hunk patch to REPODIR."
213  ;; This is just a special case of having several hunks in a row.
214  (apply-hunk-list (list patch) repodir))
215
216(defun apply-patch-list (patches repodir)
217  "Apply a list of patches, attempting to optimize for adjacent hunks."
218  (dformat "~&Looking for adjacent hunks...")
219  (loop while patches
220     do
221     (etypecase (car patches)
222       (hunk-patch
223        (let ((filename (patch-filename (car patches)))
224              (line-number 0))
225          (loop while (and (typep (car patches) 'hunk-patch)
226                           (equal (patch-filename (car patches)) filename)
227                           (>= (hunk-line-number (car patches)) line-number))
228             collect (car patches) into hunks
229             do (setf line-number (+
230                                   (hunk-line-number (car patches))
231                                   (length (hunk-new-lines (car patches)))))
232             (setf patches (cdr patches))
233             finally (loop
234                        (restart-case
235                            (progn
236                              (apply-hunk-list hunks repodir)
237                              (return))
238                          (retry-hunks ()
239                            :report (lambda (stream)
240                                      (format stream "Retry patch ~A to ~A" hunks filename))))))))
241       (patch
242        (apply-patch (car patches) repodir)
243        (setf patches (cdr patches))))))
244
245(defun apply-hunk-list (hunks repodir)
246  "Apply HUNKS to REPODIR.
247HUNKS is assumed to be a list of HUNK-PATCHes, each acting on the
248same file."
249  ;; Darcs' idea of a line is a string of characters
250  ;; terminated by a newline or end-of-file.  Thus, if a
251  ;; file ends with a newline, it has a last line with
252  ;; zero characters.
253  (let* ((filename (merge-pathnames
254                    (patch-filename (car hunks))
255                    repodir)))
256    (dformat "~&Patching ~A with ~A." filename hunks)
257    (with-file-patching (in out filename)
258      (let ((line-number 1) (file-empty t))
259        (flet ((maybe-terpri ()
260                 ;; Unless we're writing the first line, we have to
261                 ;; terminate the previous one.
262                 (if file-empty
263                     (setf file-empty nil)
264                     (write-byte 10 out))))
265          (dolist (hunk hunks)
266            ;; Lines not touched by the hunks are just output.
267            (loop while (< line-number (hunk-line-number hunk))
268               do (let ((line (read-binary-line in nil :eof)))
269                    ;; See if we are skipping more than we have.
270                    (when (and (eql line :eof)
271                               (/= line-number (1- (hunk-line-number hunk))))
272                      (error "Hunk starts at line ~A, but file is shorter."
273                             (hunk-line-number hunk)))
274
275                    (maybe-terpri)
276                    (unless (eql line :eof)
277                      (write-sequence line out))
278
279                    (incf line-number)))
280
281            ;; Start by removing lines...
282            (loop for old on (hunk-old-lines hunk)
283               do
284                 (let ((line (read-binary-line in nil :eof)))
285                   (cond
286                     ((and (eql line :eof) 
287                           (= (length old) 1)
288                           (= (length (car old)) 0))
289                      ;; Sometimes, the file is empty, but the patch
290                      ;; wants to remove one empty line.  That's an
291                      ;; effect of different views of what a line is,
292                      ;; so let it pass.
293                      )
294                     ((eql line :eof)
295                      (error "Hunk ~A too long (looking for ~S)." 
296                             hunk (bytes-to-string (car old))))
297                     ;; Note that equalp would do case-insensitive
298                     ;; comparison if these were not byte arrays.
299                     ((not (equalp (car old) line))
300                      (error "Hunk ~A: text ~S doesn't match file text ~S."
301                             hunk (bytes-to-string (car old))
302                             (bytes-to-string line)))
303                     (t
304                      ;; ...which in this context means not writing
305                      ;; them to the temporary file.
306                      ))))
307           
308            ;; Now, let's add lines.  The obvious way to do it would
309            ;; be to print the lines to output, increasing the line
310            ;; counter for every line.  However, in some circumstances
311            ;; there are two subsequent hunks modifying the same line
312            ;; (i.e. s/A/B/ and s/B/C/ are expected to have the same
313            ;; result as s/A/C/), so we unread the lines instead.
314            (dolist (new (reverse (hunk-new-lines hunk)))
315              (unread-line in new)))
316
317          ;; And output the lines after all hunks
318          (loop for line = (read-binary-line in nil :eof)
319             until (eql line :eof)
320             do (maybe-terpri)
321             (write-sequence line out)))))))
322
323(defmethod apply-patch ((patch merger-patch) repodir)
324  "Apply a merger patch to REPODIR."
325  (dformat "~&Applying merger patch ~A" patch)
326  ;; I'll gladly admit to not understanding what this is supposed to
327  ;; do.  This is a simplified version of merger_equivalent.
328  (let ((undo (merger-undo patch)))
329    (when (null undo)
330      (error "Don't know how to undo ~A." patch))
331
332    (apply-patch undo repodir)
333   
334    ;; After this comes "glump".  As long as version is "0.0", it
335    ;; doesn't do anything.
336    (assert (string= (merger-version patch) "0.0"))))
Note: See TracBrowser for help on using the repository browser.