blob: 867d522e0850d34b287011710994f3f6bbb59801 [file] [log] [blame]
/*
* Copyright (C) 2022, 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.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_COMMIT_DATA;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_OID_FANOUT;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_OID_LOOKUP;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_LOOKUP_WIDTH;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.COMMIT_GRAPH_MAGIC;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.NB;
import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.io.SilentFileInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The loader returns the representation of the commit-graph file content.
*/
public class CommitGraphLoader {
private final static Logger LOG = LoggerFactory
.getLogger(CommitGraphLoader.class);
/**
* Open an existing commit-graph file for reading.
* <p>
* The format of the file will be automatically detected and a proper access
* implementation for that format will be constructed and returned to the
* caller. The file may or may not be held open by the returned instance.
*
* @param graphFile
* existing commit-graph to read.
* @return a copy of the commit-graph file in memory
* @throws FileNotFoundException
* the file does not exist.
* @throws CommitGraphFormatException
* commit-graph file's format is different from we expected.
* @throws java.io.IOException
* the file exists but could not be read due to security errors
* or unexpected data corruption.
*/
public static CommitGraph open(File graphFile) throws FileNotFoundException,
CommitGraphFormatException, IOException {
try (SilentFileInputStream fd = new SilentFileInputStream(graphFile)) {
try {
return read(fd);
} catch (CommitGraphFormatException fe) {
throw fe;
} catch (IOException ioe) {
throw new IOException(MessageFormat.format(
JGitText.get().unreadableCommitGraph,
graphFile.getAbsolutePath()), ioe);
}
}
}
/**
* Read an existing commit-graph file from a buffered stream.
* <p>
* The format of the file will be automatically detected and a proper access
* implementation for that format will be constructed and returned to the
* caller. The file may or may not be held open by the returned instance.
*
* @param fd
* stream to read the commit-graph file from. The stream must be
* buffered as some small IOs are performed against the stream.
* The caller is responsible for closing the stream.
*
* @return a copy of the commit-graph file in memory
* @throws CommitGraphFormatException
* the commit-graph file's format is different from we expected.
* @throws java.io.IOException
* the stream cannot be read.
*/
public static CommitGraph read(InputStream fd)
throws CommitGraphFormatException, IOException {
byte[] hdr = new byte[8];
IO.readFully(fd, hdr, 0, hdr.length);
int magic = NB.decodeInt32(hdr, 0);
if (magic != COMMIT_GRAPH_MAGIC) {
throw new CommitGraphFormatException(
JGitText.get().notACommitGraph);
}
// Read the hash version (1 byte)
// 1 => SHA-1
// 2 => SHA-256 nonsupport now
int hashVersion = hdr[5];
if (hashVersion != 1) {
throw new CommitGraphFormatException(
JGitText.get().incorrectOBJECT_ID_LENGTH);
}
// Check commit-graph version
int v = hdr[4];
if (v != 1) {
throw new CommitGraphFormatException(MessageFormat.format(
JGitText.get().unsupportedCommitGraphVersion,
Integer.valueOf(v)));
}
// Read the number of "chunkOffsets" (1 byte)
int numberOfChunks = hdr[6];
// hdr[7] is the number of base commit-graphs, which is not supported in
// current version
byte[] lookupBuffer = new byte[CHUNK_LOOKUP_WIDTH
* (numberOfChunks + 1)];
IO.readFully(fd, lookupBuffer, 0, lookupBuffer.length);
List<ChunkSegment> chunks = new ArrayList<>(numberOfChunks + 1);
for (int i = 0; i <= numberOfChunks; i++) {
// chunks[numberOfChunks] is just a marker, in order to record the
// length of the last chunk.
int id = NB.decodeInt32(lookupBuffer, i * 12);
long offset = NB.decodeInt64(lookupBuffer, i * 12 + 4);
chunks.add(new ChunkSegment(id, offset));
}
boolean readChangedPathFilters;
try {
readChangedPathFilters = SystemReader.getInstance()
.getJGitConfig()
.getBoolean(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION,
ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS, false);
} catch (ConfigInvalidException e) {
// Use the default value if, for some reason, the config couldn't be read.
readChangedPathFilters = false;
}
CommitGraphBuilder builder = CommitGraphBuilder.builder();
for (int i = 0; i < numberOfChunks; i++) {
long chunkOffset = chunks.get(i).offset;
int chunkId = chunks.get(i).id;
long len = chunks.get(i + 1).offset - chunkOffset;
if (len > Integer.MAX_VALUE - 8) { // http://stackoverflow.com/a/8381338
throw new CommitGraphFormatException(
JGitText.get().commitGraphFileIsTooLargeForJgit);
}
byte buffer[] = new byte[(int) len];
IO.readFully(fd, buffer, 0, buffer.length);
switch (chunkId) {
case CHUNK_ID_OID_FANOUT:
builder.addOidFanout(buffer);
break;
case CHUNK_ID_OID_LOOKUP:
builder.addOidLookUp(buffer);
break;
case CHUNK_ID_COMMIT_DATA:
builder.addCommitData(buffer);
break;
case CHUNK_ID_EXTRA_EDGE_LIST:
builder.addExtraList(buffer);
break;
case CHUNK_ID_BLOOM_FILTER_INDEX:
if (readChangedPathFilters) {
builder.addBloomFilterIndex(buffer);
}
break;
case CHUNK_ID_BLOOM_FILTER_DATA:
if (readChangedPathFilters) {
builder.addBloomFilterData(buffer);
}
break;
default:
LOG.warn(MessageFormat.format(
JGitText.get().commitGraphChunkUnknown,
Integer.toHexString(chunkId)));
}
}
return builder.build();
}
private static class ChunkSegment {
final int id;
final long offset;
private ChunkSegment(int id, long offset) {
this.id = id;
this.offset = offset;
}
}
}