blob: 9f65ee20749d7c9a34a71fd507d25efd98d63873 [file] [log] [blame]
/*
* Copyright (C) 2021, Tencent.
*
* 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.internal.storage.commitgraph;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.internal.storage.file.GC;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.NB;
import org.junit.Before;
import org.junit.Test;
public class CommitGraphWriterTest extends RepositoryTestCase {
private TestRepository<FileRepository> tr;
private ByteArrayOutputStream os;
private CommitGraphWriter writer;
private RevWalk walk;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
os = new ByteArrayOutputStream();
tr = new TestRepository<>(db, new RevWalk(db), mockSystemReader);
walk = new RevWalk(db);
mockSystemReader.setJGitConfig(new MockConfig());
}
@Test
public void testWriteInEmptyRepo() throws Exception {
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
writer = new CommitGraphWriter(
GraphCommits.fromWalk(m, Collections.emptySet(), walk));
writer.write(m, os);
assertEquals(0, os.size());
}
@Test
public void testWriterWithExtraEdgeList() throws Exception {
RevCommit root = commit();
RevCommit a = commit(root);
RevCommit b = commit(root);
RevCommit c = commit(root);
RevCommit tip = commit(a, b, c);
Set<ObjectId> wants = Collections.singleton(tip);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
assertEquals(5, graphCommits.size());
byte[] data = os.toByteArray();
assertTrue(data.length > 0);
byte[] headers = new byte[8];
System.arraycopy(data, 0, headers, 0, 8);
assertArrayEquals(new byte[] { 'C', 'G', 'P', 'H', 1, 1, 6, 0 },
headers);
assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT,
NB.decodeInt32(data, 8));
assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP,
NB.decodeInt32(data, 20));
assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA,
NB.decodeInt32(data, 32));
assertEquals(CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST,
NB.decodeInt32(data, 44));
assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX,
NB.decodeInt32(data, 56));
assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA,
NB.decodeInt32(data, 68));
}
@Test
public void testWriterWithoutExtraEdgeList() throws Exception {
RevCommit root = commit();
RevCommit a = commit(root);
RevCommit b = commit(root);
RevCommit tip = commit(a, b);
Set<ObjectId> wants = Collections.singleton(tip);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
assertEquals(4, graphCommits.size());
byte[] data = os.toByteArray();
assertTrue(data.length > 0);
byte[] headers = new byte[8];
System.arraycopy(data, 0, headers, 0, 8);
assertArrayEquals(new byte[] { 'C', 'G', 'P', 'H', 1, 1, 5, 0 },
headers);
assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT,
NB.decodeInt32(data, 8));
assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP,
NB.decodeInt32(data, 20));
assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA,
NB.decodeInt32(data, 32));
assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX,
NB.decodeInt32(data, 44));
assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA,
NB.decodeInt32(data, 56));
}
@Test
public void testProgressMonitor() throws Exception {
RevCommit root = commit();
RevCommit a = commit(root);
RevCommit b = commit(root);
RevCommit tip = commit(a, b);
Set<ObjectId> wants = Collections.singleton(tip);
NonNestedTasksProgressMonitor nonNested = new NonNestedTasksProgressMonitor();
GraphCommits graphCommits = GraphCommits.fromWalk(nonNested, wants,
walk);
writer = new CommitGraphWriter(graphCommits, true);
writer.write(nonNested, os);
}
private static class NonNestedTasksProgressMonitor
implements ProgressMonitor {
boolean inTask;
@Override
public void start(int totalTasks) {
// empty
}
@Override
public void beginTask(String title, int totalWork) {
assertFalse("Previous monitoring task is not closed", inTask);
inTask = true;
}
@Override
public void update(int completed) {
// empty
}
@Override
public void endTask() {
assertTrue("Closing task that wasn't started", inTask);
inTask = false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public void showDuration(boolean enabled) {
// empty
}
}
static HashSet<String> changedPathStrings(byte[] data) {
int oidf_offset = -1;
int bidx_offset = -1;
int bdat_offset = -1;
for (int i = 8; i < data.length - 4; i += 12) {
switch (NB.decodeInt32(data, i)) {
case CommitGraphConstants.CHUNK_ID_OID_FANOUT:
oidf_offset = (int) NB.decodeInt64(data, i + 4);
break;
case CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX:
bidx_offset = (int) NB.decodeInt64(data, i + 4);
break;
case CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA:
bdat_offset = (int) NB.decodeInt64(data, i + 4);
break;
}
}
assertTrue(oidf_offset > 0);
assertTrue(bidx_offset > 0);
assertTrue(bdat_offset > 0);
bdat_offset += 12; // skip version, hash count, bits per entry
int commit_count = NB.decodeInt32(data, oidf_offset + 255 * 4);
int[] changed_path_length_cumuls = new int[commit_count];
for (int i = 0; i < commit_count; i++) {
changed_path_length_cumuls[i] = NB.decodeInt32(data,
bidx_offset + i * 4);
}
HashSet<String> changed_paths = new HashSet<>();
for (int i = 0; i < commit_count; i++) {
int prior_cumul = i == 0 ? 0 : changed_path_length_cumuls[i - 1];
String changed_path = "";
for (int j = prior_cumul; j < changed_path_length_cumuls[i]; j++) {
changed_path += data[bdat_offset + j] + ",";
}
changed_paths.add(changed_path);
}
return changed_paths;
}
/**
* Expected value generated using the following:
*
* <pre>
* # apply into git-repo: https://lore.kernel.org/git/cover.1684790529.git.jonathantanmy@google.com/
* (cd git-repo; make)
* git-repo/bin-wrappers/git init tested
* (cd tested; touch foo.txt; mkdir -p onedir/twodir; touch onedir/twodir/bar.txt)
* git-repo/bin-wrappers/git -C tested add foo.txt onedir
* git-repo/bin-wrappers/git -C tested commit -m first_commit
* (cd tested; mv foo.txt foo-new.txt; mv onedir/twodir/bar.txt onedir/twodir/bar-new.txt)
* git-repo/bin-wrappers/git -C tested add foo-new.txt onedir
* git-repo/bin-wrappers/git -C tested commit -a -m second_commit
* git-repo/bin-wrappers/git -C tested maintenance run
* git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths
* (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph)
* </pre>
*
* @throws Exception
*/
@Test
public void testChangedPathFilterRootAndNested() throws Exception {
RevBlob emptyBlob = tr.blob(new byte[] {});
RevCommit root = tr.commit(tr.tree(tr.file("foo.txt", emptyBlob),
tr.file("onedir/twodir/bar.txt", emptyBlob)));
RevCommit tip = tr.commit(tr.tree(tr.file("foo-new.txt", emptyBlob),
tr.file("onedir/twodir/bar-new.txt", emptyBlob)), root);
Set<ObjectId> wants = Collections.singleton(tip);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
assertThat(changedPaths, containsInAnyOrder(
"109,-33,2,60,20,79,-11,116,",
"119,69,63,-8,0,"));
}
/**
* Expected value generated using the following:
*
* <pre>
* git -C git-repo checkout todo get version number when it is merged
* (cd git-repo; make)
* git-repo/bin-wrappers/git init tested
* (cd tested; mkdir -p onedir/twodir; touch onedir/twodir/a.txt; touch onedir/twodir/b.txt)
* git-repo/bin-wrappers/git -C tested add onedir
* git-repo/bin-wrappers/git -C tested commit -m first_commit
* (cd tested; mv onedir/twodir/a.txt onedir/twodir/c.txt; mv onedir/twodir/b.txt onedir/twodir/d.txt)
* git-repo/bin-wrappers/git -C tested add onedir
* git-repo/bin-wrappers/git -C tested commit -a -m second_commit
* git-repo/bin-wrappers/git -C tested maintenance run
* git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths
* (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph)
* </pre>
*
* @throws Exception
*/
@Test
public void testChangedPathFilterOverlappingNested() throws Exception {
RevBlob emptyBlob = tr.blob(new byte[] {});
RevCommit root = tr
.commit(tr.tree(tr.file("onedir/twodir/a.txt", emptyBlob),
tr.file("onedir/twodir/b.txt", emptyBlob)));
RevCommit tip = tr
.commit(tr.tree(tr.file("onedir/twodir/c.txt", emptyBlob),
tr.file("onedir/twodir/d.txt", emptyBlob)), root);
Set<ObjectId> wants = Collections.singleton(tip);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
assertThat(changedPaths, containsInAnyOrder("61,30,23,-24,1,",
"-58,-51,-46,60,29,-121,113,90,"));
}
/**
* Expected value generated using the following:
*
* <pre>
* git -C git-repo checkout todo get version number when it is merged
* (cd git-repo; make)
* git-repo/bin-wrappers/git init tested
* (cd tested; touch 你好)
* git-repo/bin-wrappers/git -C tested add 你好
* git-repo/bin-wrappers/git -C tested commit -m first_commit
* git-repo/bin-wrappers/git -C tested maintenance run
* git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths
* (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph)
* </pre>
*
* @throws Exception
*/
@Test
public void testChangedPathFilterHighBit() throws Exception {
RevBlob emptyBlob = tr.blob(new byte[] {});
// tr.file encodes using UTF-8
RevCommit root = tr.commit(tr.tree(tr.file("你好", emptyBlob)));
Set<ObjectId> wants = Collections.singleton(root);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
assertThat(changedPaths, containsInAnyOrder("16,16,"));
}
@Test
public void testChangedPathFilterEmptyChange() throws Exception {
RevCommit root = commit();
Set<ObjectId> wants = Collections.singleton(root);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
assertThat(changedPaths, containsInAnyOrder("0,"));
}
@Test
public void testChangedPathFilterManyChanges() throws Exception {
RevBlob emptyBlob = tr.blob(new byte[] {});
DirCacheEntry[] entries = new DirCacheEntry[513];
for (int i = 0; i < entries.length; i++) {
entries[i] = tr.file(i + ".txt", emptyBlob);
}
RevCommit root = tr.commit(tr.tree(entries));
Set<ObjectId> wants = Collections.singleton(root);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
assertThat(changedPaths, containsInAnyOrder("-1,"));
}
@Test
public void testReuseBloomFilters() throws Exception {
RevBlob emptyBlob = tr.blob(new byte[] {});
RevCommit root = tr.commit(tr.tree(tr.file("foo.txt", emptyBlob),
tr.file("onedir/twodir/bar.txt", emptyBlob)));
tr.branch("master").update(root);
db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_COMMIT_GRAPH, true);
db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
ConfigConstants.CONFIG_KEY_WRITE_COMMIT_GRAPH, true);
db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
ConfigConstants.CONFIG_KEY_WRITE_CHANGED_PATHS, true);
GC gc = new GC(db);
gc.gc().get();
RevCommit tip = tr.commit(tr.tree(tr.file("foo-new.txt", emptyBlob),
tr.file("onedir/twodir/bar-new.txt", emptyBlob)), root);
Set<ObjectId> wants = Collections.singleton(tip);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
writer = new CommitGraphWriter(graphCommits, true);
CommitGraphWriter.Stats stats = writer.write(m, os);
assertEquals(1, stats.getChangedPathFiltersReused());
assertEquals(1, stats.getChangedPathFiltersComputed());
// Expected strings are the same as in
// #testChangedPathFilterRootAndNested
HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
assertThat(changedPaths, containsInAnyOrder(
"109,-33,2,60,20,79,-11,116,",
"119,69,63,-8,0,"));
}
RevCommit commit(RevCommit... parents) throws Exception {
return tr.commit(parents);
}
private static final class MockConfig extends FileBasedConfig {
private MockConfig() {
super(null, null);
}
@Override
public void load() throws IOException, ConfigInvalidException {
// Do nothing
}
@Override
public void save() throws IOException {
// Do nothing
}
@Override
public boolean isOutdated() {
return false;
}
@Override
public String toString() {
return "MockConfig";
}
@Override
public boolean getBoolean(final String section, final String name,
final boolean defaultValue) {
if (section.equals(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION)
&& name.equals(
ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS)) {
return true;
}
return defaultValue;
}
}
}