File: grab.lisp

package info (click to toggle)
mcvs 1.0.13-8
  • links: PTS
  • area: main
  • in suites: sarge
  • size: 668 kB
  • ctags: 648
  • sloc: lisp: 5,091; ansic: 223; sh: 190; makefile: 58
file content (411 lines) | stat: -rw-r--r-- 15,151 bytes parent folder | download | duplicates (2)
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
;;; This source file is part of the Meta-CVS program, 
;;; which is distributed under the GNU license.
;;; Copyright 2002 Kaz Kylheku

(require "dirwalk")
(require "system")
(require "split")
(require "seqfuncs")
(require "mapping")
(require "types")
(require "chatter")
(require "options")
(require "checkout")
(require "remove")
(require "add")
(provide "grab")

(defun read-word-hash (&optional (byte-stream t))
  (let ((word-hash (make-hash-table :test #'equalp))
	token
	(state :junk))
    (labels ((new-token () 
	       (if token
		 (setf (fill-pointer token) 0)
		 (setf token (make-array '(8) 
					 :element-type 'character
					 :adjustable t
					 :fill-pointer 0))))
	     (save-token ()
	       (unless (gethash token word-hash)
		 (let ((copy (make-string (length token))))
		   (replace copy token)
		   (setf (gethash copy word-hash) copy)))))
      (do ((byte (read-byte byte-stream nil)
		 (read-byte byte-stream nil)))
	  ((null byte) (if (eq state :word) (save-token)) word-hash)
	(let ((ch (int-char byte)))
	  (ecase state
	    ((:junk)
	      (when (or (alpha-char-p ch) (digit-char-p ch)
			(char= ch #\_))
		(new-token)
		(vector-push-extend ch token)
		(setf state :word)))
	    ((:word)
		(cond
		  ((or (alpha-char-p ch) (digit-char-p ch)
		       (char= ch #\_) (char= ch #\-))
		     (vector-push-extend ch token))
		  (t (setf state :junk)
		     (save-token))))))))))

(defun word-hash-file (name)
  (with-open-file (s (parse-posix-namestring name)
		     :direction :input
		     :element-type 'unsigned-byte)
    (read-word-hash s)))

(defun correlate-word-hashes (hash-1 hash-2)
  (let ((hc-1 (hash-table-count hash-1))
	(hc-2 (hash-table-count hash-2)))
    (when (> hc-1 hc-2)
      (psetf hash-1 hash-2 hash-2 hash-1
	     hc-1 hc-2 hc-2 hc-1))
    (let ((common-count 0))
      (maphash #'(lambda (key element)
		   (declare (ignore key))
		   (when (gethash element hash-2)
		     (incf common-count)))
	       hash-1)
      (let ((total-count (- (+ hc-1 hc-2) common-count)))
	(if (zerop total-count)
	  0
	  (/ common-count total-count))))))

(defun correlate-paths (path-1 path-2)
  (let* ((split-1 (split-fields path-1 *path-sep*))
	 (split-2 (split-fields path-2 *path-sep*))
	 (longer (max (length split-1) (length split-2)))
	 (lcs-len (length (longest-common-subsequence split-1 split-2
						       :test #'string=))))
    (case (- longer lcs-len)
      ((0 1) 1)
      ((2) 95/100)
      ((3) 90/100)
      ((4) 85/100)
      (otherwise 80/100))))

(defun determine-common-words (common-hash added-or-removed-files)
  (dolist (file added-or-removed-files common-hash)
    (maphash #'(lambda (key value)
		 (declare (ignore value))
		 (let ((existing (gethash key common-hash)))
		   (if existing
		     (setf (gethash key common-hash) 
			   (1+ existing))
		     (setf (gethash key common-hash)
			   1))))
	     (second file))))

(defun eliminate-common-words (common-hash files threshold)
  (dolist (file files)
    (maphash #'(lambda (key value)
		 (declare (ignore value))
		 (let ((count (gethash key common-hash)))
		   (if (and count (>= count threshold))
		     (remhash key (second file)))))
	     (second file))))
		 
(defun determine-moved-files (added-files removed-files)
  (let (pairs moved-files taken-added taken-removed)
    (dolist (added added-files)
      (dolist (removed removed-files)
	(let ((correlation (correlate-word-hashes (second added) 
						  (second removed))))
	  (when (>= correlation 30/100)
	    (push (list added removed 
			(* correlation (correlate-paths (first added)
							(first removed))))
		  pairs)))))
    (setf pairs (sort pairs #'> :key #'third))
    (dolist (pair pairs)
      (unless (or (member (first pair) taken-added :test #'eq)
		  (member (second pair) taken-removed :test #'eq))
	(push (first pair) taken-added)
	(push (second pair) taken-removed)
	(push pair moved-files)))
    (values moved-files 
	    (set-difference added-files taken-added :test #'eq)
	    (set-difference removed-files taken-removed :test #'eq))))

(defun determine-moved-symlinks (added-symlinks removed-symlinks moved-files
				 stable-files)
  (let ((add-hash (make-hash-table :test #'equal))
	(remove-hash (make-hash-table :test #'equal))
	moved-symlinks taken-added taken-removed)
    (macrolet ((process-item (item path target hash)
		 `(unless (path-absolute-p ,target)
		    (multiple-value-bind (base dir) (basename ,path)
		      (declare (ignore base))
		      (if (null dir)
			(setf dir "."))
		      (multiple-value-bind (resolved-path out-of-bounds)
					   (canonicalize-path (path-cat 
								dir 
								,target))
			(unless out-of-bounds
			  (push ,item (gethash resolved-path ,hash))))))))
      (dolist (added added-symlinks)
	(destructuring-bind (path target) added
	  (process-item added path target add-hash)))
      (dolist (removed removed-symlinks)
	(with-slots (path target) removed
	  (process-item removed path target remove-hash)))
      (macrolet ((move-symlinks (source-name target-name)
		   `(let ((added-list (gethash ,target-name add-hash))
			  (removed-list (gethash ,source-name remove-hash))
			  (symlink-move-pairs))
		      (dolist (added added-list)
			(dolist (removed removed-list)
			  (push (list added removed (correlate-paths 
						      (first added)
						      (mapping-entry-path removed)))
				symlink-move-pairs)))
		      (setf symlink-move-pairs (sort symlink-move-pairs #'> :key #'third))
		      (dolist (pair symlink-move-pairs)
			(unless (or (member (first pair) taken-added :test #'eq)
				    (member (second pair) taken-removed :test #'eq))
			  (push (first pair) taken-added)
			  (push (second pair) taken-removed)
			  (push pair moved-symlinks))))))
	(dolist (file-move-pair moved-files)
	  (destructuring-bind ((target-name hash-2) 
			       (source-name hash-1 f-file) confidence)
			      file-move-pair
	  (declare (ignore hash-1 hash-2 f-file confidence))
	  (move-symlinks source-name target-name)))
	(dolist (entry stable-files)
	  (with-slots (path) entry
	    (move-symlinks path path)))))
    (values moved-symlinks
	    (set-difference added-symlinks taken-added :test #'eq)
	    (set-difference removed-symlinks taken-removed :test #'eq))))

(defun mcvs-grab (global-options command-options module subdir)
  (find-bind (:test #'string= :key #'first)
	     ((branch "r") (trunk "A"))
	     command-options
    (when (and branch trunk)
      (error "both -r and -A specified."))
    (when (and (not branch) (not trunk))
      (error "specify branch using -r or main trunk using -A."))
    (mcvs-checkout module subdir global-options 
		   `(("d" ,*this-dir*) ,@(if branch (list branch)))
		   :no-generate t)
    (in-sandbox-root-dir
      (let ((mapping (mapping-read *mcvs-map*))
	    invisible-old-paths old-paths
	    old-file-paths new-file-paths 
	    old-symlink-paths new-symlink-paths
	    added-files removed-files stable-files
	    added-symlinks removed-symlinks stable-symlinks)
	(chatter-info "Scanning directory structure.~%")
	(multiple-value-setq (old-paths invisible-old-paths)
			     (separate-if #'real-path-exists mapping
					  :key #'mapping-entry-path))
	(dolist (entry old-paths)
	  (with-slots (path) entry
	    (setf path (abstract-to-real-path path))))
	(multiple-value-setq (old-file-paths old-symlink-paths)
			     (separate :file old-paths
				       :test #'eq
				       :key #'mapping-entry-kind))

	(for-each-file-info (fi *this-dir*)
	  (let* ((path (canonicalize-path (file-name fi))))
	    (cond
	      ((regular-p fi)
		 (push path new-file-paths))
	      ((symlink-p fi)
	         (push path new-symlink-paths))
	      ((directory-p fi)
		(when (path-equal path *mcvs-dir*)
		  (skip))))))

	(multiple-value-setq (stable-files removed-files added-files) 
			     (intersection-difference old-file-paths 
						      new-file-paths
						      :key1 #'mapping-entry-path
						      :test #'equal))

	(multiple-value-setq (stable-symlinks removed-symlinks added-symlinks) 
			     (intersection-difference old-symlink-paths 
						      new-symlink-paths
						      :key1 #'mapping-entry-path
						      :test #'equal))


	(cond
	  ((or (null added-files) (null removed-files))
	     (setf added-files (mapcar #'(lambda (name) (list name)) added-files))
	     (setf removed-files (mapcar #'(lambda (entry) 
					     (with-slots (id path) entry
					       (list path nil id)))
					 removed-files)))
	  (t (chatter-terse "Analyzing ~a added file~:p.~%" (length added-files))
	     (setf added-files (mapcar #'(lambda (name) 
					(list name (word-hash-file name)))
				    added-files))

	     (chatter-terse "Analyzing ~a removed file~:p.~%" (length removed-files))
	     (setf removed-files (mapcar #'(lambda (entry)
					     (with-slots (id path) entry
					       (list path
						     (word-hash-file id) id)))
					 removed-files))
	     (let ((common-word-hash (make-hash-table :test #'equalp)))
	       (determine-common-words common-word-hash added-files)
	       (determine-common-words common-word-hash removed-files)
	       (let ((threshold (max 5 (* 1/5 (+ (length added-files)
						 (length removed-files))))))
		 (eliminate-common-words common-word-hash added-files threshold)
		 (eliminate-common-words common-word-hash removed-files threshold)))
	     (chatter-terse "Determining move candidates.~%")))

	(multiple-value-bind (moved-files added-files removed-files)
			     (if (or (null added-files) (null removed-files))
			       (values nil added-files removed-files)
			       (determine-moved-files added-files 
						      removed-files))
	  (when added-symlinks
	    (chatter-terse "Reading ~a added symbolic link~:p.~%" 
			   (length added-symlinks))
	    (setf added-symlinks (mapcar #'(lambda (path)
					     (list path (readlink path)))
					 added-symlinks)))

	  (multiple-value-bind (moved-symlinks added-symlinks removed-symlinks)
			       (if (or (null added-symlinks)
				       (null removed-symlinks))
				 (values nil added-symlinks removed-symlinks)
				 (determine-moved-symlinks added-symlinks 
							   removed-symlinks
							   moved-files
							   stable-files))

	    (let ((moved-hash (make-hash-table :test #'equal))
		  (all-hash (make-hash-table :test #'equal))
		  (mapping (mapping-read *mcvs-map*)))
	      (dolist (entry mapping)
		(setf (gethash (mapping-entry-id entry) all-hash) entry))
	      (when (or moved-files moved-symlinks)
		(dolist (pair moved-files)
		  (destructuring-bind ((target-name hash-2) 
				       (source-name hash-1 f-file) confidence)
				      pair
		    (declare (ignore hash-1 hash-2))
		    (chatter-terse "moving ~a -> ~a (confidence ~a%)~%" 
			    source-name target-name (round (* confidence 100)))
		    (setf (gethash f-file moved-hash) target-name)))
		(dolist (pair moved-symlinks)
		  (destructuring-bind ((target-name symlink-target)
				       source-entry confidence) pair
		    (declare (ignore symlink-target confidence))
		    (with-slots (id (source-name path)) source-entry
		      (chatter-terse "moving symlink ~a -> ~a~%"
				     source-name target-name)
		      (setf (gethash id moved-hash) target-name))))
		    
		(mapc #'(lambda (entry)
			  (with-slots (id path) entry
			    (let ((replacement (gethash id moved-hash)))
			      (if replacement
				(setf path 
				      (real-to-abstract-path replacement))))))
		      mapping)
		(dolist (entry mapping)
		  (with-slots (kind id path executable) entry
		    (when (and (gethash id moved-hash) (eq kind :file))
		      (unlink id)
		      (link (abstract-to-real-path path) id)
		      (setf executable (executable-p id))))))
	      (dolist (symlink-entry stable-symlinks)
		(with-slots (kind id path target) symlink-entry
		  (let ((map-entry (gethash id all-hash)))
		    (setf (mapping-entry-target map-entry)
			  (readlink path)))))
	      (dolist (pair moved-symlinks)
		(with-slots (kind id path target) (second pair)
		  (let ((map-entry (gethash id all-hash)))
		    (setf (mapping-entry-target map-entry)
			  (readlink (abstract-to-real-path 
				      (mapping-entry-path map-entry)))))))
	      (dolist (file-entry stable-files)
		(with-slots (kind id path) file-entry
		  (let ((map-entry (gethash id all-hash)))
		    (setf (mapping-entry-executable map-entry)
			  (executable-p path)))
		  (when (eq kind :file)
		    (unlink id)
		    (link path id))))
	      (mapping-write mapping *mcvs-map*)
	      (mapping-write mapping *mcvs-map-local*))
	    (when removed-files
	      (mcvs-remove nil (mapcar #'first removed-files) :no-sync t))
	    (when removed-symlinks
	      (mcvs-remove nil (mapcar #'mapping-entry-path removed-symlinks) 
			   :no-sync t))
	    (when added-files
	      (mcvs-add nil global-options 
			nil (mapcar #'first added-files)))
	    
	    (when added-symlinks
	      (mcvs-add nil global-options 
			nil (mapcar #'first added-symlinks)))))))))

(defun mcvs-grab-wrapper (global-options command-options args)
  (flet ((error ()
	   (error "specify module name, and optional subdirectory.")))
    (when (zerop (length args))
      (error))
    (destructuring-bind (module &optional subdir &rest superfluous) args
      (when superfluous
	(error))
      (mcvs-grab global-options command-options module subdir))))

(defconstant *grab-help*
"Syntax:

  mcvs grab { -A | -r branch-name } module-name [ subdirectory-path ]

Options:

  -A                Grab to the main trunk.
  -r branch-name    Grab to the specified branch.

Semantics:

  The grab command is a tool for incorporating external code streams
  into a Meta-CVS module.

  Grab works by comparing the contents of the current directory and its
  subdirectories, to the tip of the trunk or a branch of an existing
  Meta-CVS module. It produces a sandbox which contains a minimized set
  of local edits that are needed to make the branch or trunk in the repository
  look exactly like the current directory.

  These local edits have to be committed just like hand-made edits; the grab
  command itself has no effect on the contents of the repository, and does
  not change the local directory in any way other than by creating the MCVS
  subdirectory.

  If it was run with the wrong arguments, the recovery procedure is simply
  to recursively remove the MCVS subdirectory. Then it's possible to run grab
  again with different arguments, as necessary.

  If the subdirectory-path is specified, then grab will operate on 
  just that subdirectory of the module, making just that subtree look
  like the current directory. The result will be a partial sandbox
  containing local edits to just the visible part of the module.
  (See the help for the checkout command, which also takes a subdirectory path
  parameter to create a partial sandbox).

  Either the -A option or the -r option must be specified. This forces
  users to be explicitly clear about where they want the grab to go;
  the main trunk or a branch.

  Grab performs no merging whatsoever. Its job is to place a new document
  baseline at the tip of a code stream. Third party source tracking is
  performed by grabbing snapshots to a branch, and then merging that branch
  in the usual way. ")