Browse code
(init)
Ed Langley authored on 01/05/2019 10:06:48
Showing 3 changed files
Showing 3 changed files
2 | 2 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,18 @@ |
1 |
+;;; -*- Mode:Lisp; Syntax:ANSI-Common-Lisp; Package: ASDF-USER -*- |
|
2 |
+(in-package :asdf-user) |
|
3 |
+ |
|
4 |
+(defsystem :cl-git |
|
5 |
+ :description "" |
|
6 |
+ :author "Ed L <edward@elangley.org>" |
|
7 |
+ :license "MIT" |
|
8 |
+ :pathname #-fw.dev nil #+fw.dev #p"PROJECTS:cl-git;" |
|
9 |
+ :depends-on (:alexandria |
|
10 |
+ :split-sequence |
|
11 |
+ :cl-dot |
|
12 |
+ :chipz |
|
13 |
+ :data-lens |
|
14 |
+ :fwoar-lisputils |
|
15 |
+ :fwoar-lisputils/bin-parser |
|
16 |
+ :serapeum |
|
17 |
+ :uiop) |
|
18 |
+ :components ((:file "cl-git"))) |
0 | 19 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,393 @@ |
1 |
+(defpackage :fwoar.cl-git |
|
2 |
+ (:use :cl ) |
|
3 |
+ (:export )) |
|
4 |
+(in-package :fwoar.cl-git) |
|
5 |
+ |
|
6 |
+(defclass repository () |
|
7 |
+ ((%root :initarg :root :reader root))) |
|
8 |
+ |
|
9 |
+(defun repository (root) |
|
10 |
+ (fw.lu:new 'repository root)) |
|
11 |
+ |
|
12 |
+(defun get-local-branches (root) |
|
13 |
+ (mapcar (data-lens:juxt #'pathname-name |
|
14 |
+ (alexandria:compose #'serapeum:trim-whitespace |
|
15 |
+ #'alexandria:read-file-into-string)) |
|
16 |
+ (uiop:directory* |
|
17 |
+ (merge-pathnames ".git/refs/heads/*" |
|
18 |
+ root)))) |
|
19 |
+ |
|
20 |
+(defun loose-object-path (sha) |
|
21 |
+ (let ((obj-path (fwoar.string-utils:insert-at 2 #\/ sha))) |
|
22 |
+ (merge-pathnames obj-path ".git/objects/"))) |
|
23 |
+ |
|
24 |
+(defclass pack () |
|
25 |
+ ((%pack :initarg :pack :reader pack-file) |
|
26 |
+ (%index :initarg :index :reader index-file))) |
|
27 |
+ |
|
28 |
+(defun pack (index pack) |
|
29 |
+ (fw.lu:new 'pack index pack)) |
|
30 |
+ |
|
31 |
+(defun pack-files (repo) |
|
32 |
+ (mapcar 'pack |
|
33 |
+ (uiop:directory* |
|
34 |
+ (merge-pathnames ".git/objects/pack/*.idx" |
|
35 |
+ repo)) |
|
36 |
+ (uiop:directory* |
|
37 |
+ (merge-pathnames ".git/objects/pack/*.pack" |
|
38 |
+ repo)))) |
|
39 |
+ |
|
40 |
+(defun find-object-in-pack-files (repo id) |
|
41 |
+ (dolist (pack-file (pack-files repo)) |
|
42 |
+ (multiple-value-bind (pack mid) (find-pack-containing pack-file id) |
|
43 |
+ (when pack |
|
44 |
+ (return-from find-object-in-pack-files |
|
45 |
+ (values pack mid)))))) |
|
46 |
+ |
|
47 |
+(defun edges-in-fanout (toc s sha) |
|
48 |
+ (let* ((fanout-offset (getf toc :fanout))) |
|
49 |
+ (file-position s (+ fanout-offset (* 4 (1- (elt sha 0))))) |
|
50 |
+ (destructuring-bind ((_ . cur) (__ . next)) (fwoar.bin-parser:extract '((cur 4 fwoar.bin-parser:be->int) |
|
51 |
+ (next 4 fwoar.bin-parser:be->int)) |
|
52 |
+ s) |
|
53 |
+ (declare (ignore _ __)) |
|
54 |
+ (values cur next)))) |
|
55 |
+ |
|
56 |
+(defun find-sha-between-terms (toc s start end sha) |
|
57 |
+ (unless (>= start end) |
|
58 |
+ (let* ((sha-offset (getf toc :shas)) |
|
59 |
+ (mid (floor (+ start end) |
|
60 |
+ 2))) |
|
61 |
+ (file-position s (+ sha-offset (* 20 mid))) |
|
62 |
+ (let ((sha-at-mid (read-bytes 20 'fwoar.bin-parser:byte-array-to-hex-string s))) |
|
63 |
+ (cond ((string< sha sha-at-mid) |
|
64 |
+ (find-sha-between-terms toc s start mid sha)) |
|
65 |
+ ((string> sha sha-at-mid) |
|
66 |
+ (find-sha-between-terms toc s (1+ mid) end sha)) |
|
67 |
+ (t mid)))))) |
|
68 |
+ |
|
69 |
+(defun find-pack-containing (pack-file id) |
|
70 |
+ (with-open-file (s (index-file pack-file) :element-type '(unsigned-byte 8)) |
|
71 |
+ (let ((binary-sha (ironclad:hex-string-to-byte-array id)) |
|
72 |
+ (toc (idx-toc s))) |
|
73 |
+ (multiple-value-bind (_ end) (edges-in-fanout toc s binary-sha) |
|
74 |
+ (declare (ignore _)) |
|
75 |
+ (let ((midpoint (find-sha-between-terms toc s 0 end id))) |
|
76 |
+ (and midpoint |
|
77 |
+ (values pack-file |
|
78 |
+ midpoint))))))) |
|
79 |
+ |
|
80 |
+(defun extract-object-from-pack (pack obj-number) |
|
81 |
+ (with-open-file (s (index-file pack) :element-type '(unsigned-byte 8)) |
|
82 |
+ (with-open-file (p (pack-file pack) :element-type '(unsigned-byte 8)) |
|
83 |
+ (let* ((toc (idx-toc s)) |
|
84 |
+ (offset-offset (getf toc :4-byte-offsets))) |
|
85 |
+ (file-position s (+ offset-offset (* 4 obj-number))) |
|
86 |
+ (let ((object-offset-in-pack (read-bytes 4 'fwoar.bin-parser:be->int s))) |
|
87 |
+ (file-position p object-offset-in-pack) |
|
88 |
+ (read-object-from-pack p)))))) |
|
89 |
+ |
|
90 |
+(defun extract-loose-object (repo id) |
|
91 |
+ (with-open-file (s (object repo id) |
|
92 |
+ :element-type '(unsigned-byte 8)) |
|
93 |
+ (chipz:decompress nil (chipz:make-dstate 'chipz:zlib) |
|
94 |
+ s))) |
|
95 |
+ |
|
96 |
+(defun extract-object (repo id) |
|
97 |
+ (if (object repo id) |
|
98 |
+ (extract-loose-object repo id) |
|
99 |
+ (data-lens.lenses:view *object-data-lens* |
|
100 |
+ (multiple-value-call 'extract-object-from-pack |
|
101 |
+ (find-object-in-pack-files (root repo) id))))) |
|
102 |
+ |
|
103 |
+(defparameter *object-data-lens* |
|
104 |
+ (data-lens.lenses:make-alist-lens :object-data)) |
|
105 |
+ |
|
106 |
+(defun turn-read-object-to-string (object) |
|
107 |
+ (data-lens.lenses:over *object-data-lens* 'babel:octets-to-string object)) |
|
108 |
+ |
|
109 |
+(defgeneric branches (repository) |
|
110 |
+ (:method ((repository repository)) |
|
111 |
+ (get-local-branches (root repository)))) |
|
112 |
+ |
|
113 |
+(defgeneric branch (repository name) |
|
114 |
+ (:method ((repository repository) name) |
|
115 |
+ (second |
|
116 |
+ (find name (get-local-branches (root repository)) |
|
117 |
+ :test 'equal |
|
118 |
+ :key 'car)))) |
|
119 |
+ |
|
120 |
+(defgeneric object (repository id) |
|
121 |
+ (:method ((repository repository) id) |
|
122 |
+ (car |
|
123 |
+ (uiop:directory* |
|
124 |
+ (merge-pathnames (loose-object-path (serapeum:concat id "*")) |
|
125 |
+ (root repository)))))) |
|
126 |
+ |
|
127 |
+(defun fanout-table (s) |
|
128 |
+ (coerce (alexandria:assoc-value |
|
129 |
+ (fwoar.bin-parser:extract '((head 4) |
|
130 |
+ (version 4) |
|
131 |
+ (fanout-table #.(* 4 256) batch-4)) |
|
132 |
+ s) |
|
133 |
+ 'fanout-table) |
|
134 |
+ 'vector)) |
|
135 |
+ |
|
136 |
+(defun batch-4 (bytes) |
|
137 |
+ (mapcar 'fwoar.bin-parser:be->int |
|
138 |
+ (serapeum:batches bytes 4))) |
|
139 |
+ |
|
140 |
+(defun batch-20 (bytes) |
|
141 |
+ (serapeum:batches bytes 20)) |
|
142 |
+ |
|
143 |
+(defun get-object-size (bytes) |
|
144 |
+ (let ((first (elt bytes 0)) |
|
145 |
+ (rest (subseq bytes 1))) |
|
146 |
+ (logior (ash (fwoar.bin-parser:be->int rest) 4) |
|
147 |
+ (logand first 15)))) |
|
148 |
+ |
|
149 |
+(defun get-object-type (bytes) |
|
150 |
+ (let ((first (elt bytes 0))) |
|
151 |
+ (ldb (byte 3 4) |
|
152 |
+ first))) |
|
153 |
+ |
|
154 |
+(serapeum:defalias ->sha-string |
|
155 |
+ (<>1 (data-lens:over 'fwoar.bin-parser:byte-array-to-hex-string) |
|
156 |
+ batch-20)) |
|
157 |
+ |
|
158 |
+(defun get-shas-before (fanout-table first-sha-byte s) |
|
159 |
+ (let ((num-before (elt fanout-table first-sha-byte)) |
|
160 |
+ (num-total (alexandria:last-elt fanout-table))) |
|
161 |
+ (values (fwoar.bin-parser:extract (list (list 'shas (* 20 num-before) '->sha-string)) |
|
162 |
+ s) |
|
163 |
+ (- num-total num-before)))) |
|
164 |
+ |
|
165 |
+(defun advance-past-crcs (obj-count s) |
|
166 |
+ (file-position s |
|
167 |
+ (+ (file-position s) |
|
168 |
+ (* 4 obj-count)))) |
|
169 |
+ |
|
170 |
+(defun object-offset (object-number s) |
|
171 |
+ (file-position s |
|
172 |
+ (+ (file-position s) |
|
173 |
+ (* (1- object-number) |
|
174 |
+ 4))) |
|
175 |
+ (fwoar.bin-parser:extract '((offset 4 fwoar.bin-parser:be->int)) |
|
176 |
+ s)) |
|
177 |
+ |
|
178 |
+(defmacro sym->plist (&rest syms) |
|
179 |
+ `(list ,@(loop for sym in syms |
|
180 |
+ append (list (alexandria:make-keyword sym) |
|
181 |
+ sym)))) |
|
182 |
+ |
|
183 |
+(defun idx-toc (idx-stream) |
|
184 |
+ (let* ((object-count (progn (file-position idx-stream 1028) |
|
185 |
+ (let ((buf (make-array 4))) |
|
186 |
+ (read-sequence buf idx-stream) |
|
187 |
+ (fwoar.bin-parser:be->int buf)))) |
|
188 |
+ (signature 0) |
|
189 |
+ (version 4) |
|
190 |
+ (fanout 8) |
|
191 |
+ (shas (+ fanout |
|
192 |
+ (* 4 256))) |
|
193 |
+ (packed-crcs (+ shas |
|
194 |
+ (* 20 object-count))) |
|
195 |
+ (4-byte-offsets (+ packed-crcs |
|
196 |
+ (* 4 object-count))) |
|
197 |
+ (8-byte-offsets-pro (+ 4-byte-offsets |
|
198 |
+ (* object-count 4))) |
|
199 |
+ (pack-sha (- (file-length idx-stream) |
|
200 |
+ 40)) |
|
201 |
+ (8-byte-offsets (when (/= 8-byte-offsets-pro pack-sha) |
|
202 |
+ 8-byte-offsets-pro)) |
|
203 |
+ (idx-sha (- (file-length idx-stream) |
|
204 |
+ 20))) |
|
205 |
+ (values (sym->plist signature |
|
206 |
+ version |
|
207 |
+ fanout |
|
208 |
+ shas |
|
209 |
+ packed-crcs |
|
210 |
+ 4-byte-offsets |
|
211 |
+ 8-byte-offsets |
|
212 |
+ pack-sha |
|
213 |
+ idx-sha) |
|
214 |
+ object-count))) |
|
215 |
+ |
|
216 |
+(defun read-bytes (count format stream) |
|
217 |
+ (let ((seq (make-array count))) |
|
218 |
+ (read-sequence seq stream) |
|
219 |
+ (funcall format |
|
220 |
+ seq))) |
|
221 |
+ |
|
222 |
+(defun collect-data (idx-toc s num) |
|
223 |
+ (let ((sha-idx (getf idx-toc :shas)) |
|
224 |
+ (crc-idx (getf idx-toc :packed-crcs)) |
|
225 |
+ (4-byte-offsets-idx (getf idx-toc :4-byte-offsets)) |
|
226 |
+ (8-byte-offsets-idx (getf idx-toc :8-byte-offsets))) |
|
227 |
+ (values num |
|
228 |
+ (progn |
|
229 |
+ (file-position s (+ sha-idx (* num 20))) |
|
230 |
+ (read-bytes 20 'fwoar.bin-parser:byte-array-to-hex-string s)) |
|
231 |
+ (progn |
|
232 |
+ (file-position s (+ crc-idx (* num 4))) |
|
233 |
+ (read-bytes 4 'identity s)) |
|
234 |
+ (progn |
|
235 |
+ (file-position s (+ 4-byte-offsets-idx (* num 4))) |
|
236 |
+ (read-bytes 4 'fwoar.bin-parser:be->int s))))) |
|
237 |
+ |
|
238 |
+(defun object-type->sym (object-type) |
|
239 |
+ (ecase object-type |
|
240 |
+ (1 :commit) |
|
241 |
+ (2 :tree) |
|
242 |
+ (3 :blob) |
|
243 |
+ (4 :tag) |
|
244 |
+ (6 :ofs-delta) |
|
245 |
+ (7 :ref-delta))) |
|
246 |
+ |
|
247 |
+(defun read-object-metadata-from-pack (s) |
|
248 |
+ (let* ((metadata (fwoar.bin-parser:extract-high s)) |
|
249 |
+ (type (get-object-type metadata)) |
|
250 |
+ (size (get-object-size metadata))) |
|
251 |
+ (values (cons :type (object-type->sym type)) |
|
252 |
+ (cons :decompressed-size size)))) |
|
253 |
+ |
|
254 |
+(defun read-object-from-pack (s) |
|
255 |
+ (let* ((metadata (fwoar.bin-parser:extract-high s)) |
|
256 |
+ (type (get-object-type metadata)) |
|
257 |
+ (size (get-object-size metadata)) |
|
258 |
+ (object-data (chipz:decompress nil (chipz:make-dstate 'chipz:zlib) s))) |
|
259 |
+ (list (cons :type (object-type->sym type)) |
|
260 |
+ (cons :decompressed-size size) |
|
261 |
+ (cons :object-data object-data) |
|
262 |
+ (cons :raw-data object-data)))) |
|
263 |
+ |
|
264 |
+(defun get-first-commits-from-pack (idx pack n) |
|
265 |
+ (let ((toc (idx-toc idx)) |
|
266 |
+ (result ())) |
|
267 |
+ (dotimes (i n (reverse result)) |
|
268 |
+ (multiple-value-bind (_ sha __ offset) (collect-data toc idx i) |
|
269 |
+ (declare (ignore _ __)) |
|
270 |
+ (file-position pack offset) |
|
271 |
+ (push `((:sha . ,sha) |
|
272 |
+ ,@(multiple-value-list |
|
273 |
+ (read-object-metadata-from-pack pack)) |
|
274 |
+ (:offset . ,offset)) |
|
275 |
+ result)) |
|
276 |
+ ))) |
|
277 |
+ |
|
278 |
+(defmacro inspect- (s form) |
|
279 |
+ `(let ((result ,form)) |
|
280 |
+ (format ,s "~&~s (~{~s~^ ~})~%~4t~s~%" |
|
281 |
+ ',form |
|
282 |
+ ,(typecase form |
|
283 |
+ (list `(list ',(car form) ,@(cdr form))) |
|
284 |
+ (t `(list ,form))) |
|
285 |
+ result) |
|
286 |
+ result)) |
|
287 |
+ |
|
288 |
+(defun inspect-* (fn) |
|
289 |
+ (lambda (&rest args) |
|
290 |
+ (declare (dynamic-extent args)) |
|
291 |
+ (inspect- *trace-output* |
|
292 |
+ (apply fn args)))) |
|
293 |
+ |
|
294 |
+(defun partition (char string &key from-end) |
|
295 |
+ (let ((pos (position char string :from-end from-end))) |
|
296 |
+ (if pos |
|
297 |
+ (list (subseq string 0 pos) |
|
298 |
+ (subseq string (1+ pos))) |
|
299 |
+ (list string |
|
300 |
+ nil)))) |
|
301 |
+ |
|
302 |
+(defun partition-subseq (subseq string &key from-end) |
|
303 |
+ (let ((pos (search subseq string :from-end from-end))) |
|
304 |
+ (if pos |
|
305 |
+ (list (subseq string 0 pos) |
|
306 |
+ (subseq string (+ (length subseq) pos))) |
|
307 |
+ (list string |
|
308 |
+ nil)))) |
|
309 |
+ |
|
310 |
+(defun split-object (object-data) |
|
311 |
+ (destructuring-bind (head tail) |
|
312 |
+ (partition 0 |
|
313 |
+ object-data) |
|
314 |
+ (destructuring-bind (type length) |
|
315 |
+ (partition #\space |
|
316 |
+ (babel:octets-to-string head :encoding :latin1)) |
|
317 |
+ (values tail |
|
318 |
+ (list type |
|
319 |
+ (parse-integer length)))))) |
|
320 |
+ |
|
321 |
+(defclass git-object () |
|
322 |
+ ()) |
|
323 |
+(defclass commit (git-object) |
|
324 |
+ ()) |
|
325 |
+ |
|
326 |
+(defun parse-commit (commit) |
|
327 |
+ (destructuring-bind (metadata message) |
|
328 |
+ (partition-subseq #(#\newline #\newline) |
|
329 |
+ commit #+(or)(babel:octets-to-string commit :encoding :latin1)) |
|
330 |
+ (values message |
|
331 |
+ (fwoar.string-utils:split #\newline metadata)))) |
|
332 |
+ |
|
333 |
+(defclass git-graph () |
|
334 |
+ ((%repo :initarg :repo :reader repo) |
|
335 |
+ (%depth :initarg :depth :reader depth) |
|
336 |
+ (%branches :reader branches) |
|
337 |
+ (%node-cache :reader node-cache :initform (make-hash-table :test 'equal)) |
|
338 |
+ (%edge-cache :reader edge-cache :initform (make-hash-table :test 'equal)))) |
|
339 |
+ |
|
340 |
+(defmethod initialize-instance :after ((object git-graph) &key) |
|
341 |
+ (setf (slot-value object '%branches) |
|
342 |
+ (fw.lu:alist-string-hash-table |
|
343 |
+ (funcall (data-lens:over |
|
344 |
+ (<>1 (data-lens:applying #'cons) |
|
345 |
+ (data-lens:transform-head |
|
346 |
+ (serapeum:op (subseq _1 0 (min (length _1) 7)))) |
|
347 |
+ #'reverse)) |
|
348 |
+ (branches (repo object)))))) |
|
349 |
+ |
|
350 |
+(defun git-graph (repo) |
|
351 |
+ (fw.lu:new 'git-graph repo)) |
|
352 |
+ |
|
353 |
+(defun get-commit-parents (repository commit) |
|
354 |
+ (map 'list |
|
355 |
+ (serapeum:op (second (partition #\space _))) |
|
356 |
+ (remove-if-not (lambda (it) |
|
357 |
+ (serapeum:string-prefix-p "parent" it)) |
|
358 |
+ (nth-value 1 (parse-commit |
|
359 |
+ (split-object |
|
360 |
+ (chipz:decompress nil (chipz:make-dstate 'chipz:zlib) |
|
361 |
+ (object repository |
|
362 |
+ commit)))))))) |
|
363 |
+ |
|
364 |
+(defmethod cl-dot:graph-object-node ((graph git-graph) (commit string)) |
|
365 |
+ (alexandria:ensure-gethash commit |
|
366 |
+ (node-cache graph) |
|
367 |
+ (make-instance 'cl-dot:node |
|
368 |
+ :attributes `(:label ,(gethash #1=(subseq commit 0 7) |
|
369 |
+ (branches graph) |
|
370 |
+ #1#))))) |
|
371 |
+ |
|
372 |
+(defmethod cl-dot:graph-object-points-to ((graph git-graph) (commit string)) |
|
373 |
+ (mapcar (lambda (c) |
|
374 |
+ (setf (gethash (list commit c) |
|
375 |
+ (edge-cache graph)) |
|
376 |
+ t) |
|
377 |
+ c) |
|
378 |
+ (remove-if (lambda (it) |
|
379 |
+ (gethash (list commit it) |
|
380 |
+ (edge-cache graph))) |
|
381 |
+ (mapcar (serapeum:op (subseq _ 0 7)) |
|
382 |
+ (get-commit-parents (repo graph) commit) |
|
383 |
+ #+nil |
|
384 |
+ (loop |
|
385 |
+ for cur = (list commit) then parents |
|
386 |
+ for parents = (let ((f (get-commit-parents (repo graph) (car cur)))) |
|
387 |
+ f) |
|
388 |
+ until (or (not parents) |
|
389 |
+ (cdr parents)) |
|
390 |
+ finally (return (or parents |
|
391 |
+ (when (not (equal commit (car cur))) |
|
392 |
+ cur)))))))) |
|
393 |
+ |