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 package org.eclipse.jgit.internal.storage.reftable;
45
46 import static java.lang.Math.log;
47 import static org.eclipse.jgit.internal.storage.reftable.BlockWriter.padBetweenBlocks;
48 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_FOOTER_LEN;
49 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_HEADER_LEN;
50 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_HEADER_MAGIC;
51 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.INDEX_BLOCK_TYPE;
52 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.LOG_BLOCK_TYPE;
53 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.MAX_BLOCK_SIZE;
54 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.MAX_RESTARTS;
55 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.OBJ_BLOCK_TYPE;
56 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.REF_BLOCK_TYPE;
57 import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.VERSION_1;
58 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
59
60 import java.io.IOException;
61 import java.io.OutputStream;
62 import java.util.ArrayList;
63 import java.util.Collection;
64 import java.util.Collections;
65 import java.util.HashSet;
66 import java.util.Iterator;
67 import java.util.List;
68 import java.util.Set;
69 import java.util.zip.CRC32;
70
71 import org.eclipse.jgit.annotations.Nullable;
72 import org.eclipse.jgit.internal.storage.reftable.BlockWriter.DeleteLogEntry;
73 import org.eclipse.jgit.internal.storage.reftable.BlockWriter.Entry;
74 import org.eclipse.jgit.internal.storage.reftable.BlockWriter.IndexEntry;
75 import org.eclipse.jgit.internal.storage.reftable.BlockWriter.LogEntry;
76 import org.eclipse.jgit.internal.storage.reftable.BlockWriter.ObjEntry;
77 import org.eclipse.jgit.internal.storage.reftable.BlockWriter.RefEntry;
78 import org.eclipse.jgit.lib.AbbreviatedObjectId;
79 import org.eclipse.jgit.lib.AnyObjectId;
80 import org.eclipse.jgit.lib.ObjectId;
81 import org.eclipse.jgit.lib.ObjectIdOwnerMap;
82 import org.eclipse.jgit.lib.ObjectIdSubclassMap;
83 import org.eclipse.jgit.lib.PersonIdent;
84 import org.eclipse.jgit.lib.Ref;
85 import org.eclipse.jgit.util.LongList;
86 import org.eclipse.jgit.util.NB;
87
88
89
90
91
92
93
94 public class ReftableWriter {
95 private ReftableConfig config;
96 private int refBlockSize;
97 private int logBlockSize;
98 private int restartInterval;
99 private int maxIndexLevels;
100 private boolean alignBlocks;
101 private boolean indexObjects;
102
103 private long minUpdateIndex;
104 private long maxUpdateIndex;
105
106 private ReftableOutputStream out;
107 private ObjectIdSubclassMap<RefList> obj2ref;
108
109 private BlockWriter cur;
110 private Section refs;
111 private Section objs;
112 private Section logs;
113 private int objIdLen;
114 private Stats stats;
115
116
117 public ReftableWriter() {
118 this(new ReftableConfig());
119 }
120
121
122
123
124
125
126
127 public ReftableWriter(ReftableConfig cfg) {
128 config = cfg;
129 }
130
131
132
133
134
135
136 public ReftableWriter setConfig(ReftableConfig cfg) {
137 this.config = cfg != null ? cfg : new ReftableConfig();
138 return this;
139 }
140
141
142
143
144
145
146
147
148 public ReftableWriter setMinUpdateIndex(long min) {
149 minUpdateIndex = min;
150 return this;
151 }
152
153
154
155
156
157
158
159
160
161 public ReftableWriter setMaxUpdateIndex(long max) {
162 maxUpdateIndex = max;
163 return this;
164 }
165
166
167
168
169
170
171
172
173
174
175
176 public ReftableWriter begin(OutputStream os) throws IOException {
177 refBlockSize = config.getRefBlockSize();
178 logBlockSize = config.getLogBlockSize();
179 restartInterval = config.getRestartInterval();
180 maxIndexLevels = config.getMaxIndexLevels();
181 alignBlocks = config.isAlignBlocks();
182 indexObjects = config.isIndexObjects();
183
184 if (refBlockSize <= 0) {
185 refBlockSize = 4 << 10;
186 } else if (refBlockSize > MAX_BLOCK_SIZE) {
187 throw new IllegalArgumentException();
188 }
189 if (logBlockSize <= 0) {
190 logBlockSize = 2 * refBlockSize;
191 }
192 if (restartInterval <= 0) {
193 restartInterval = refBlockSize < (60 << 10) ? 16 : 64;
194 }
195
196 out = new ReftableOutputStream(os, refBlockSize, alignBlocks);
197 refs = new Section(REF_BLOCK_TYPE);
198 if (indexObjects) {
199 obj2ref = new ObjectIdSubclassMap<>();
200 }
201 writeFileHeader();
202 return this;
203 }
204
205
206
207
208
209
210
211
212
213
214 public ReftableWriter sortAndWriteRefs(Collection<Ref> refsToPack)
215 throws IOException {
216 Iterator<RefEntry> itr = refsToPack.stream()
217 .map(r -> new RefEntry(r, maxUpdateIndex - minUpdateIndex))
218 .sorted(Entry::compare)
219 .iterator();
220 while (itr.hasNext()) {
221 RefEntry entry = itr.next();
222 long blockPos = refs.write(entry);
223 indexRef(entry.ref, blockPos);
224 }
225 return this;
226 }
227
228
229
230
231
232
233
234
235
236
237
238 public void writeRef(Ref ref) throws IOException {
239 writeRef(ref, maxUpdateIndex);
240 }
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255 public void writeRef(Ref ref, long updateIndex) throws IOException {
256 if (updateIndex < minUpdateIndex) {
257 throw new IllegalArgumentException();
258 }
259 long d = updateIndex - minUpdateIndex;
260 long blockPos = refs.write(new RefEntry(ref, d));
261 indexRef(ref, blockPos);
262 }
263
264 private void indexRef(Ref ref, long blockPos) {
265 if (indexObjects && !ref.isSymbolic()) {
266 indexId(ref.getObjectId(), blockPos);
267 indexId(ref.getPeeledObjectId(), blockPos);
268 }
269 }
270
271 private void indexId(ObjectId id, long blockPos) {
272 if (id != null) {
273 RefList l = obj2ref.get(id);
274 if (l == null) {
275 l = new RefList(id);
276 obj2ref.add(l);
277 }
278 l.addBlock(blockPos);
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 public void writeLog(String ref, long updateIndex, PersonIdent who,
307 ObjectId oldId, ObjectId newId, @Nullable String message)
308 throws IOException {
309 String msg = message != null ? message : "";
310 beginLog();
311 logs.write(new LogEntry(ref, updateIndex, who, oldId, newId, msg));
312 }
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333 public void deleteLog(String ref, long updateIndex) throws IOException {
334 beginLog();
335 logs.write(new DeleteLogEntry(ref, updateIndex));
336 }
337
338 private void beginLog() throws IOException {
339 if (logs == null) {
340 finishRefAndObjSections();
341 out.flushFileHeader();
342 out.setBlockSize(logBlockSize);
343 logs = new Section(LOG_BLOCK_TYPE);
344 }
345 }
346
347
348
349
350
351
352
353 public long estimateTotalBytes() {
354 long bytes = out.size();
355 if (bytes == 0) {
356 bytes += FILE_HEADER_LEN;
357 }
358 if (cur != null) {
359 long curBlockPos = out.size();
360 int sz = cur.currentSize();
361 bytes += sz;
362
363 IndexBuilder idx = null;
364 if (cur.blockType() == REF_BLOCK_TYPE) {
365 idx = refs.idx;
366 } else if (cur.blockType() == LOG_BLOCK_TYPE) {
367 idx = logs.idx;
368 }
369 if (idx != null && shouldHaveIndex(idx)) {
370 if (idx == refs.idx) {
371 bytes += out.estimatePadBetweenBlocks(sz);
372 }
373 bytes += idx.estimateBytes(curBlockPos);
374 }
375 }
376 bytes += FILE_FOOTER_LEN;
377 return bytes;
378 }
379
380
381
382
383
384
385
386
387 public ReftableWriter finish() throws IOException {
388 finishRefAndObjSections();
389 finishLogSection();
390 writeFileFooter();
391 out.finishFile();
392
393 stats = new Stats(this, out);
394 out = null;
395 obj2ref = null;
396 cur = null;
397 refs = null;
398 objs = null;
399 logs = null;
400 return this;
401 }
402
403 private void finishRefAndObjSections() throws IOException {
404 if (cur != null && cur.blockType() == REF_BLOCK_TYPE) {
405 refs.finishSectionMaybeWriteIndex();
406 if (indexObjects && !obj2ref.isEmpty() && refs.idx.bytes > 0) {
407 writeObjBlocks();
408 }
409 obj2ref = null;
410 }
411 }
412
413 private void writeObjBlocks() throws IOException {
414 List<RefList> sorted = sortById(obj2ref);
415 obj2ref = null;
416 objIdLen = shortestUniqueAbbreviation(sorted);
417
418 out.padBetweenBlocksToNextBlock();
419 objs = new Section(OBJ_BLOCK_TYPE);
420 objs.entryCnt = sorted.size();
421 for (RefList l : sorted) {
422 objs.write(new ObjEntry(objIdLen, l, l.blockPos));
423 }
424 objs.finishSectionMaybeWriteIndex();
425 }
426
427 private void finishLogSection() throws IOException {
428 if (cur != null && cur.blockType() == LOG_BLOCK_TYPE) {
429 logs.finishSectionMaybeWriteIndex();
430 }
431 }
432
433 private boolean shouldHaveIndex(IndexBuilder idx) {
434 int threshold;
435 if (idx == refs.idx && alignBlocks) {
436 threshold = 4;
437 } else {
438 threshold = 1;
439 }
440 return idx.entries.size() + (cur != null ? 1 : 0) > threshold;
441 }
442
443 private void writeFileHeader() {
444 byte[] hdr = new byte[FILE_HEADER_LEN];
445 encodeHeader(hdr);
446 out.write(hdr, 0, FILE_HEADER_LEN);
447 }
448
449 private void encodeHeader(byte[] hdr) {
450 System.arraycopy(FILE_HEADER_MAGIC, 0, hdr, 0, 4);
451 int bs = alignBlocks ? refBlockSize : 0;
452 NB.encodeInt32(hdr, 4, (VERSION_1 << 24) | bs);
453 NB.encodeInt64(hdr, 8, minUpdateIndex);
454 NB.encodeInt64(hdr, 16, maxUpdateIndex);
455 }
456
457 private void writeFileFooter() {
458 int ftrLen = FILE_FOOTER_LEN;
459 byte[] ftr = new byte[ftrLen];
460 encodeHeader(ftr);
461
462 NB.encodeInt64(ftr, 24, indexPosition(refs));
463 NB.encodeInt64(ftr, 32, (firstBlockPosition(objs) << 5) | objIdLen);
464 NB.encodeInt64(ftr, 40, indexPosition(objs));
465 NB.encodeInt64(ftr, 48, firstBlockPosition(logs));
466 NB.encodeInt64(ftr, 56, indexPosition(logs));
467
468 CRC32 crc = new CRC32();
469 crc.update(ftr, 0, ftrLen - 4);
470 NB.encodeInt32(ftr, ftrLen - 4, (int) crc.getValue());
471
472 out.write(ftr, 0, ftrLen);
473 }
474
475 private static long firstBlockPosition(@Nullable Section s) {
476 return s != null ? s.firstBlockPosition : 0;
477 }
478
479 private static long indexPosition(@Nullable Section s) {
480 return s != null && s.idx != null ? s.idx.rootPosition : 0;
481 }
482
483
484 public Stats getStats() {
485 return stats;
486 }
487
488
489 public static class Stats {
490 private final int refBlockSize;
491 private final int logBlockSize;
492 private final int restartInterval;
493
494 private final long minUpdateIndex;
495 private final long maxUpdateIndex;
496
497 private final long refCnt;
498 private final long objCnt;
499 private final int objIdLen;
500 private final long logCnt;
501 private final long refBytes;
502 private final long objBytes;
503 private final long logBytes;
504 private final long paddingUsed;
505 private final long totalBytes;
506
507 private final int refIndexSize;
508 private final int refIndexLevels;
509 private final int objIndexSize;
510 private final int objIndexLevels;
511
512 Stats(ReftableWriter w, ReftableOutputStream o) {
513 refBlockSize = w.refBlockSize;
514 logBlockSize = w.logBlockSize;
515 restartInterval = w.restartInterval;
516
517 minUpdateIndex = w.minUpdateIndex;
518 maxUpdateIndex = w.maxUpdateIndex;
519 paddingUsed = o.paddingUsed();
520 totalBytes = o.size();
521
522 refCnt = w.refs.entryCnt;
523 refBytes = w.refs.bytes;
524
525 objCnt = w.objs != null ? w.objs.entryCnt : 0;
526 objBytes = w.objs != null ? w.objs.bytes : 0;
527 objIdLen = w.objIdLen;
528
529 logCnt = w.logs != null ? w.logs.entryCnt : 0;
530 logBytes = w.logs != null ? w.logs.bytes : 0;
531
532 IndexBuilder refIdx = w.refs.idx;
533 refIndexSize = refIdx.bytes;
534 refIndexLevels = refIdx.levels;
535
536 IndexBuilder objIdx = w.objs != null ? w.objs.idx : null;
537 objIndexSize = objIdx != null ? objIdx.bytes : 0;
538 objIndexLevels = objIdx != null ? objIdx.levels : 0;
539 }
540
541
542 public int refBlockSize() {
543 return refBlockSize;
544 }
545
546
547 public int logBlockSize() {
548 return logBlockSize;
549 }
550
551
552 public int restartInterval() {
553 return restartInterval;
554 }
555
556
557 public long minUpdateIndex() {
558 return minUpdateIndex;
559 }
560
561
562 public long maxUpdateIndex() {
563 return maxUpdateIndex;
564 }
565
566
567 public long refCount() {
568 return refCnt;
569 }
570
571
572 public long objCount() {
573 return objCnt;
574 }
575
576
577 public long logCount() {
578 return logCnt;
579 }
580
581
582 public long refBytes() {
583 return refBytes;
584 }
585
586
587 public long objBytes() {
588 return objBytes;
589 }
590
591
592 public long logBytes() {
593 return logBytes;
594 }
595
596
597 public long totalBytes() {
598 return totalBytes;
599 }
600
601
602 public long paddingBytes() {
603 return paddingUsed;
604 }
605
606
607 public int refIndexSize() {
608 return refIndexSize;
609 }
610
611
612 public int refIndexLevels() {
613 return refIndexLevels;
614 }
615
616
617 public int objIndexSize() {
618 return objIndexSize;
619 }
620
621
622 public int objIndexLevels() {
623 return objIndexLevels;
624 }
625
626
627
628
629
630
631 public int objIdLength() {
632 return objIdLen;
633 }
634 }
635
636 private static List<RefList> sortById(ObjectIdSubclassMap<RefList> m) {
637 List<RefList> s = new ArrayList<>(m.size());
638 for (RefList l : m) {
639 s.add(l);
640 }
641 Collections.sort(s);
642 return s;
643 }
644
645 private static int shortestUniqueAbbreviation(List<RefList> in) {
646
647 int bytes = Math.max(2, (int) (log(in.size()) / log(8)));
648 Set<AbbreviatedObjectId> tmp = new HashSet<>((int) (in.size() * 0.75f));
649 retry: for (;;) {
650 int hexLen = bytes * 2;
651 for (ObjectId id : in) {
652 AbbreviatedObjectId a = id.abbreviate(hexLen);
653 if (!tmp.add(a)) {
654 if (++bytes >= OBJECT_ID_LENGTH) {
655 return OBJECT_ID_LENGTH;
656 }
657 tmp.clear();
658 continue retry;
659 }
660 }
661 return bytes;
662 }
663 }
664
665 private static class RefList extends ObjectIdOwnerMap.Entry {
666 final LongList blockPos = new LongList(2);
667
668 RefList(AnyObjectId id) {
669 super(id);
670 }
671
672 void addBlock(long pos) {
673 if (!blockPos.contains(pos)) {
674 blockPos.add(pos);
675 }
676 }
677 }
678
679 private class Section {
680 final IndexBuilder idx;
681 final long firstBlockPosition;
682
683 long entryCnt;
684 long bytes;
685
686 Section(byte keyType) {
687 idx = new IndexBuilder(keyType);
688 firstBlockPosition = out.size();
689 }
690
691 long write(BlockWriter.Entry entry) throws IOException {
692 if (cur == null) {
693 beginBlock(entry);
694 } else if (!cur.tryAdd(entry)) {
695 flushCurBlock();
696 if (cur.padBetweenBlocks()) {
697 out.padBetweenBlocksToNextBlock();
698 }
699 beginBlock(entry);
700 }
701 entryCnt++;
702 return out.size();
703 }
704
705 private void beginBlock(BlockWriter.Entry entry)
706 throws BlockSizeTooSmallException {
707 byte blockType = entry.blockType();
708 int bs = out.bytesAvailableInBlock();
709 cur = new BlockWriter(blockType, idx.keyType, bs, restartInterval);
710 cur.mustAdd(entry);
711 }
712
713 void flushCurBlock() throws IOException {
714 idx.entries.add(new IndexEntry(cur.lastKey(), out.size()));
715 cur.writeTo(out);
716 }
717
718 void finishSectionMaybeWriteIndex() throws IOException {
719 flushCurBlock();
720 cur = null;
721 if (shouldHaveIndex(idx)) {
722 idx.writeIndex();
723 }
724 bytes = out.size() - firstBlockPosition;
725 }
726 }
727
728 private class IndexBuilder {
729 final byte keyType;
730 List<IndexEntry> entries = new ArrayList<>();
731 long rootPosition;
732 int bytes;
733 int levels;
734
735 IndexBuilder(byte kt) {
736 keyType = kt;
737 }
738
739 int estimateBytes(long curBlockPos) {
740 BlockWriter b = new BlockWriter(
741 INDEX_BLOCK_TYPE, keyType,
742 MAX_BLOCK_SIZE,
743 Math.max(restartInterval, entries.size() / MAX_RESTARTS));
744 try {
745 for (Entry e : entries) {
746 b.mustAdd(e);
747 }
748 if (cur != null) {
749 b.mustAdd(new IndexEntry(cur.lastKey(), curBlockPos));
750 }
751 } catch (BlockSizeTooSmallException e) {
752 return b.currentSize();
753 }
754 return b.currentSize();
755 }
756
757 void writeIndex() throws IOException {
758 if (padBetweenBlocks(keyType)) {
759 out.padBetweenBlocksToNextBlock();
760 }
761 long startPos = out.size();
762 writeMultiLevelIndex(entries);
763 bytes = (int) (out.size() - startPos);
764 entries = null;
765 }
766
767 private void writeMultiLevelIndex(List<IndexEntry> keys)
768 throws IOException {
769 levels = 1;
770 while (maxIndexLevels == 0 || levels < maxIndexLevels) {
771 keys = writeOneLevel(keys);
772 if (keys == null) {
773 return;
774 }
775 levels++;
776 }
777
778
779
780 BlockWriter b = new BlockWriter(
781 INDEX_BLOCK_TYPE, keyType,
782 MAX_BLOCK_SIZE,
783 Math.max(restartInterval, keys.size() / MAX_RESTARTS));
784 for (Entry e : keys) {
785 b.mustAdd(e);
786 }
787 rootPosition = out.size();
788 b.writeTo(out);
789 }
790
791 private List<IndexEntry> writeOneLevel(List<IndexEntry> keys)
792 throws IOException {
793 Section thisLevel = new Section(keyType);
794 for (Entry e : keys) {
795 thisLevel.write(e);
796 }
797 if (!thisLevel.idx.entries.isEmpty()) {
798 thisLevel.flushCurBlock();
799 if (cur.padBetweenBlocks()) {
800 out.padBetweenBlocksToNextBlock();
801 }
802 cur = null;
803 return thisLevel.idx.entries;
804 }
805
806
807 rootPosition = out.size();
808 cur.writeTo(out);
809 cur = null;
810 return null;
811 }
812 }
813 }