git.fiddlerwoaroof.com
Browse code

(init)

Ed Langley authored on 01/05/2019 10:06:48
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
+