/* * Copyright (C) 2010, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.notes; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; import static org.eclipse.jgit.lib.FileMode.REGULAR_FILE; import java.io.IOException; import java.util.Iterator; import java.util.NoSuchElementException; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectInserter.Formatter; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.TreeFormatter; /** * A note tree holding only notes, with no subtrees. * * The leaf bucket contains on average less than 256 notes, all of whom share * the same leading prefix. If a notes branch has less than 256 notes, the top * level tree of the branch should be a LeafBucket. Once a notes branch has more * than 256 notes, the root should be a {@link FanoutBucket} and the LeafBucket * will appear only as a cell of a FanoutBucket. * * Entries within the LeafBucket are stored sorted by ObjectId, and lookup is * performed using binary search. As the entry list should contain fewer than * 256 elements, the average number of compares to find an element should be * less than 8 due to the O(log N) lookup behavior. * * A LeafBucket must be parsed from a tree object by {@link NoteParser}. */ class LeafBucket extends InMemoryNoteBucket { static final int MAX_SIZE = 256; /** All note blobs in this bucket, sorted sequentially. */ private Note[] notes; /** Number of items in {@link #notes}. */ private int cnt; LeafBucket(int prefixLen) { super(prefixLen); notes = new Note[4]; } private int search(AnyObjectId objId) { int low = 0; int high = cnt; while (low < high) { int mid = (low + high) >>> 1; int cmp = objId.compareTo(notes[mid]); if (cmp < 0) high = mid; else if (cmp == 0) return mid; else low = mid + 1; } return -(low + 1); } @Override Note getNote(AnyObjectId objId, ObjectReader or) { int idx = search(objId); return 0 <= idx ? notes[idx] : null; } Note get(int index) { return notes[index]; } int size() { return cnt; } @Override Iterator iterator(AnyObjectId objId, ObjectReader reader) { return new Iterator<>() { private int idx; @Override public boolean hasNext() { return idx < cnt; } @Override public Note next() { if (hasNext()) { return notes[idx++]; } throw new NoSuchElementException(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } @Override int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException { return cnt; } @Override InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData, ObjectReader or) throws IOException { int p = search(noteOn); if (0 <= p) { if (noteData != null) { notes[p].setData(noteData.copy()); return this; } System.arraycopy(notes, p + 1, notes, p, cnt - p - 1); cnt--; return 0 < cnt ? this : null; } else if (noteData != null) { if (shouldSplit()) { return split().set(noteOn, noteData, or); } growIfFull(); p = -(p + 1); if (p < cnt) { System.arraycopy(notes, p, notes, p + 1, cnt - p); } notes[p] = new Note(noteOn, noteData.copy()); cnt++; return this; } else { return this; } } @Override ObjectId writeTree(ObjectInserter inserter) throws IOException { return inserter.insert(build()); } @Override ObjectId getTreeId() { try (Formatter f = new ObjectInserter.Formatter()) { return f.idFor(build()); } } private TreeFormatter build() { byte[] nameBuf = new byte[OBJECT_ID_STRING_LENGTH]; int nameLen = OBJECT_ID_STRING_LENGTH - prefixLen; TreeFormatter fmt = new TreeFormatter(treeSize(nameLen)); NonNoteEntry e = nonNotes; for (int i = 0; i < cnt; i++) { Note n = notes[i]; n.copyTo(nameBuf, 0); while (e != null && e.pathCompare(nameBuf, prefixLen, nameLen, REGULAR_FILE) < 0) { e.format(fmt); e = e.next; } fmt.append(nameBuf, prefixLen, nameLen, REGULAR_FILE, n.getData()); } for (; e != null; e = e.next) e.format(fmt); return fmt; } private int treeSize(int nameLen) { int sz = cnt * TreeFormatter.entrySize(REGULAR_FILE, nameLen); for (NonNoteEntry e = nonNotes; e != null; e = e.next) sz += e.treeEntrySize(); return sz; } void parseOneEntry(AnyObjectId noteOn, AnyObjectId noteData) { growIfFull(); notes[cnt++] = new Note(noteOn, noteData.copy()); } @Override InMemoryNoteBucket append(Note note) { if (shouldSplit()) { return split().append(note); } growIfFull(); notes[cnt++] = note; return this; } private void growIfFull() { if (notes.length == cnt) { Note[] n = new Note[notes.length * 2]; System.arraycopy(notes, 0, n, 0, cnt); notes = n; } } private boolean shouldSplit() { return MAX_SIZE <= cnt && prefixLen + 2 < OBJECT_ID_STRING_LENGTH; } FanoutBucket split() { FanoutBucket n = new FanoutBucket(prefixLen); for (int i = 0; i < cnt; i++) n.append(notes[i]); n.nonNotes = nonNotes; return n; } }