CommitGraph: implement commit-graph writer

Teach JGit to write a commit-graph formatted file by walking commit
graph from specified commit objects.

See: https://git-scm.com/docs/commit-graph-format/2.21.0

Bug: 574368
Change-Id: I34f9f28f8729080c275f86215ebf30b2d05af41d
Signed-off-by: kylezhao <kylezhao@tencent.com>
This commit is contained in:
kylezhao 2021-07-06 20:20:14 +08:00
parent 1d5a6c77a6
commit cf70e7cbe4
9 changed files with 800 additions and 0 deletions

View File

@ -0,0 +1,113 @@
/*
* 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.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
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);
}
@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);
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, 4, 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));
}
@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);
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, 3, 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));
}
RevCommit commit(RevCommit... parents) throws Exception {
return tr.commit(parents);
}
}

View File

@ -0,0 +1,119 @@
/*
* 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.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
import org.junit.Test;
public class GraphCommitsTest extends RepositoryTestCase {
private TestRepository<FileRepository> tr;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
tr = new TestRepository<>(db, new RevWalk(db), mockSystemReader);
}
@Test
public void testEmptyRepo() throws Exception {
try (RevWalk rw = new RevWalk(db)) {
GraphCommits commits = GraphCommits.fromWalk(
NullProgressMonitor.INSTANCE, Collections.emptySet(), rw);
assertEquals(0, commits.size());
assertEquals(0, commits.getExtraEdgeCnt());
assertFalse(commits.iterator().hasNext());
}
}
@Test
public void testRepoWithoutExtraEdges() throws Exception {
try (RevWalk rw = new RevWalk(db)) {
RevCommit root = commit();
RevCommit a = commit(root);
RevCommit b = commit(root);
RevCommit tip = commit(a, b);
GraphCommits commits = GraphCommits.fromWalk(
NullProgressMonitor.INSTANCE, Collections.singleton(tip),
rw);
assertEquals(4, commits.size());
assertEquals(0, commits.getExtraEdgeCnt());
verifyOrderOfLookup(List.of(root, a, b, tip), commits);
}
}
@Test
public void testRepoWithExtraEdges() throws Exception {
try (RevWalk rw = new RevWalk(db)) {
RevCommit root = commit();
RevCommit a = commit(root);
RevCommit b = commit(root);
RevCommit c = commit(root);
RevCommit tip = commit(a, b, c);
GraphCommits commits = GraphCommits.fromWalk(
NullProgressMonitor.INSTANCE, Collections.singleton(tip),
rw);
assertEquals(5, commits.size());
assertEquals(2, commits.getExtraEdgeCnt());
verifyOrderOfLookup(List.of(root, a, b, c, tip), commits);
}
}
@Test
public void testCommitNotInLookup() throws Exception {
try (RevWalk rw = new RevWalk(db)) {
RevCommit a = commit();
RevCommit b = commit(a);
GraphCommits commits = GraphCommits.fromWalk(
NullProgressMonitor.INSTANCE, Collections.singleton(a), rw);
assertEquals(1, commits.size());
assertEquals(0, commits.getExtraEdgeCnt());
Iterator<RevCommit> iterator = commits.iterator();
assertEquals(a, iterator.next());
assertFalse(iterator.hasNext());
assertThrows(MissingObjectException.class,
() -> commits.getOidPosition(b));
}
}
private void verifyOrderOfLookup(List<RevCommit> commits,
GraphCommits graphCommits) {
commits = new ArrayList<>(commits);
Collections.sort(commits);
Iterator<RevCommit> expected = commits.iterator();
Iterator<RevCommit> actual = graphCommits.iterator();
while (expected.hasNext()) {
assertEquals(expected.next(), actual.next());
}
assertFalse(actual.hasNext());
}
RevCommit commit(RevCommit... parents) throws Exception {
return tr.commit(parents);
}
}

View File

@ -143,11 +143,13 @@ collisionOn=Collision on {0}
commandClosedStderrButDidntExit=Command {0} closed stderr stream but didn''t exit within timeout {1} seconds
commandRejectedByHook=Rejected by "{0}" hook.\n{1}
commandWasCalledInTheWrongState=Command {0} was called in the wrong state
commitGraphWritingCancelled=commit-graph writing was canceled
commitMessageNotSpecified=commit message not specified
commitOnRepoWithoutHEADCurrentlyNotSupported=Commit on repo without HEAD currently not supported
commitAmendOnInitialNotPossible=Amending is not possible on initial commit.
commitsHaveAlreadyBeenMarkedAsStart=Commits have already been marked as walk starts.
compressingObjects=Compressing objects
computingCommitGeneration=Computing commit-graph generation numbers
configSubsectionContainsNewline=config subsection name contains newline
configSubsectionContainsNullByte=config subsection name contains byte 0x00
configValueContainsNullByte=config value contains byte 0x00
@ -328,6 +330,7 @@ fileModeNotSetForPath=FileMode not set for path {0}
filterExecutionFailed=Execution of filter command ''{0}'' on file ''{1}'' failed
filterExecutionFailedRc=Execution of filter command ''{0}'' on file ''{1}'' failed with return code ''{2}'', message on stderr: ''{3}''
filterRequiresCapability=filter requires server to advertise that capability
findingCommitsForCommitGraph=Finding commits for commit-graph
findingGarbage=Finding garbage
flagIsDisposed={0} is disposed.
flagNotFromThis={0} not from this.
@ -839,6 +842,7 @@ writerAlreadyInitialized=Writer already initialized
writeTimedOut=Write timed out after {0} ms
writingNotPermitted=Writing not permitted
writingNotSupported=Writing {0} not supported.
writingOutCommitGraph=Writing out commit-graph in {0} passes
writingObjects=Writing objects
wrongDecompressedLength=wrong decompressed length
wrongRepositoryState=Wrong Repository State: {0}

View File

@ -171,11 +171,13 @@ public static JGitText get() {
/***/ public String commandClosedStderrButDidntExit;
/***/ public String commandRejectedByHook;
/***/ public String commandWasCalledInTheWrongState;
/***/ public String commitGraphWritingCancelled;
/***/ public String commitMessageNotSpecified;
/***/ public String commitOnRepoWithoutHEADCurrentlyNotSupported;
/***/ public String commitAmendOnInitialNotPossible;
/***/ public String commitsHaveAlreadyBeenMarkedAsStart;
/***/ public String compressingObjects;
/***/ public String computingCommitGeneration;
/***/ public String configSubsectionContainsNewline;
/***/ public String configSubsectionContainsNullByte;
/***/ public String configValueContainsNullByte;
@ -356,6 +358,7 @@ public static JGitText get() {
/***/ public String filterExecutionFailed;
/***/ public String filterExecutionFailedRc;
/***/ public String filterRequiresCapability;
/***/ public String findingCommitsForCommitGraph;
/***/ public String findingGarbage;
/***/ public String flagIsDisposed;
/***/ public String flagNotFromThis;
@ -867,6 +870,7 @@ public static JGitText get() {
/***/ public String writeTimedOut;
/***/ public String writingNotPermitted;
/***/ public String writingNotSupported;
/***/ public String writingOutCommitGraph;
/***/ public String writingObjects;
/***/ public String wrongDecompressedLength;
/***/ public String wrongRepositoryState;

View File

@ -0,0 +1,55 @@
/*
* 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;
/**
* Constants relating to commit-graph.
*/
class CommitGraphConstants {
static final int COMMIT_GRAPH_MAGIC = 0x43475048; /* "CGPH" */
static final int CHUNK_ID_OID_FANOUT = 0x4f494446; /* "OIDF" */
static final int CHUNK_ID_OID_LOOKUP = 0x4f49444c; /* "OIDL" */
static final int CHUNK_ID_COMMIT_DATA = 0x43444154; /* "CDAT" */
static final int CHUNK_ID_EXTRA_EDGE_LIST = 0x45444745; /* "EDGE" */
/**
* First 4 bytes describe the chunk id. Value 0 is a terminating label.
* Other 8 bytes provide the byte-offset in current file for chunk to start.
*/
static final int CHUNK_LOOKUP_WIDTH = 12;
/**
* First 8 bytes are for the positions of the first two parents of the ith
* commit. The next 8 bytes store the generation number of the commit and
* the commit time in seconds since EPOCH.
*/
static final int COMMIT_DATA_WIDTH = 16;
/** Mask to make the last edgeValue into position */
static final int GRAPH_EDGE_LAST_MASK = 0x7fffffff;
/** EdgeValue & GRAPH_LAST_EDGE != 0 means it is the last edgeValue */
static final int GRAPH_LAST_EDGE = 0x80000000;
/** EdgeValue == GRAPH_NO_PARENT means it has no parents */
static final int GRAPH_NO_PARENT = 0x70000000;
/**
* EdgeValue & GRAPH_EXTRA_EDGES_NEEDED != 0 means its other parents are in
* Chunk Extra Edge List
*/
static final int GRAPH_EXTRA_EDGES_NEEDED = 0x80000000;
}

View File

@ -0,0 +1,335 @@
/*
* 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.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_DATA_WIDTH;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.COMMIT_GRAPH_MAGIC;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.GRAPH_EXTRA_EDGES_NEEDED;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.GRAPH_LAST_EDGE;
import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.GRAPH_NO_PARENT;
import static org.eclipse.jgit.lib.Constants.COMMIT_GENERATION_NOT_COMPUTED;
import static org.eclipse.jgit.lib.Constants.COMMIT_GENERATION_UNKNOWN;
import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.io.CancellableDigestOutputStream;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.util.NB;
/**
* Writes a commit-graph formatted file.
*/
public class CommitGraphWriter {
private static final int COMMIT_GRAPH_VERSION_GENERATED = 1;
private static final int OID_HASH_VERSION = 1;
private static final int GRAPH_FANOUT_SIZE = 4 * 256;
private static final int GENERATION_NUMBER_MAX = 0x3FFFFFFF;
private final int hashsz;
private final GraphCommits graphCommits;
/**
* Create commit-graph writer for these commits.
*
* @param graphCommits
* the commits which will be writen to the commit-graph.
*/
public CommitGraphWriter(@NonNull GraphCommits graphCommits) {
this.graphCommits = graphCommits;
this.hashsz = OBJECT_ID_LENGTH;
}
/**
* Write commit-graph to the supplied stream.
*
* @param monitor
* progress monitor to report the number of items written.
* @param commitGraphStream
* output stream of commit-graph data. The stream should be
* buffered by the caller. The caller is responsible for closing
* the stream.
* @throws IOException
*/
public void write(@NonNull ProgressMonitor monitor,
@NonNull OutputStream commitGraphStream) throws IOException {
if (graphCommits.size() == 0) {
return;
}
List<ChunkHeader> chunks = createChunks();
long writeCount = 256 + 2 * graphCommits.size()
+ graphCommits.getExtraEdgeCnt();
monitor.beginTask(
MessageFormat.format(JGitText.get().writingOutCommitGraph,
Integer.valueOf(chunks.size())),
(int) writeCount);
try (CancellableDigestOutputStream out = new CancellableDigestOutputStream(
monitor, commitGraphStream)) {
writeHeader(out, chunks.size());
writeChunkLookup(out, chunks);
writeChunks(monitor, out, chunks);
writeCheckSum(out);
} catch (InterruptedIOException e) {
throw new IOException(JGitText.get().commitGraphWritingCancelled);
} finally {
monitor.endTask();
}
}
private List<ChunkHeader> createChunks() {
List<ChunkHeader> chunks = new ArrayList<>();
chunks.add(new ChunkHeader(CHUNK_ID_OID_FANOUT, GRAPH_FANOUT_SIZE));
chunks.add(new ChunkHeader(CHUNK_ID_OID_LOOKUP,
hashsz * graphCommits.size()));
chunks.add(new ChunkHeader(CHUNK_ID_COMMIT_DATA,
(hashsz + 16) * graphCommits.size()));
if (graphCommits.getExtraEdgeCnt() > 0) {
chunks.add(new ChunkHeader(CHUNK_ID_EXTRA_EDGE_LIST,
graphCommits.getExtraEdgeCnt() * 4));
}
return chunks;
}
private void writeHeader(CancellableDigestOutputStream out, int numChunks)
throws IOException {
byte[] headerBuffer = new byte[8];
NB.encodeInt32(headerBuffer, 0, COMMIT_GRAPH_MAGIC);
byte[] buff = { (byte) COMMIT_GRAPH_VERSION_GENERATED,
(byte) OID_HASH_VERSION, (byte) numChunks, (byte) 0 };
System.arraycopy(buff, 0, headerBuffer, 4, 4);
out.write(headerBuffer, 0, 8);
out.flush();
}
private void writeChunkLookup(CancellableDigestOutputStream out,
List<ChunkHeader> chunks) throws IOException {
int numChunks = chunks.size();
long chunkOffset = 8 + (numChunks + 1) * CHUNK_LOOKUP_WIDTH;
byte[] buffer = new byte[CHUNK_LOOKUP_WIDTH];
for (ChunkHeader chunk : chunks) {
NB.encodeInt32(buffer, 0, chunk.id);
NB.encodeInt64(buffer, 4, chunkOffset);
out.write(buffer);
chunkOffset += chunk.size;
}
NB.encodeInt32(buffer, 0, 0);
NB.encodeInt64(buffer, 4, chunkOffset);
out.write(buffer);
}
private void writeChunks(ProgressMonitor monitor,
CancellableDigestOutputStream out, List<ChunkHeader> chunks)
throws IOException {
for (ChunkHeader chunk : chunks) {
int chunkId = chunk.id;
switch (chunkId) {
case CHUNK_ID_OID_FANOUT:
writeFanoutTable(out);
break;
case CHUNK_ID_OID_LOOKUP:
writeOidLookUp(out);
break;
case CHUNK_ID_COMMIT_DATA:
writeCommitData(monitor, out);
break;
case CHUNK_ID_EXTRA_EDGE_LIST:
writeExtraEdges(out);
break;
}
}
}
private void writeCheckSum(CancellableDigestOutputStream out)
throws IOException {
out.write(out.getDigest());
out.flush();
}
private void writeFanoutTable(CancellableDigestOutputStream out)
throws IOException {
byte[] tmp = new byte[4];
int[] fanout = new int[256];
for (RevCommit c : graphCommits) {
fanout[c.getFirstByte() & 0xff]++;
}
for (int i = 1; i < fanout.length; i++) {
fanout[i] += fanout[i - 1];
}
for (int n : fanout) {
NB.encodeInt32(tmp, 0, n);
out.write(tmp, 0, 4);
out.getWriteMonitor().update(1);
}
}
private void writeOidLookUp(CancellableDigestOutputStream out)
throws IOException {
byte[] tmp = new byte[4 + hashsz];
for (RevCommit c : graphCommits) {
c.copyRawTo(tmp, 0);
out.write(tmp, 0, hashsz);
out.getWriteMonitor().update(1);
}
}
private void writeCommitData(ProgressMonitor monitor,
CancellableDigestOutputStream out) throws IOException {
int[] generations = computeGenerationNumbers(monitor);
int num = 0;
byte[] tmp = new byte[hashsz + COMMIT_DATA_WIDTH];
int i = 0;
for (RevCommit commit : graphCommits) {
int edgeValue;
int[] packedDate = new int[2];
ObjectId treeId = commit.getTree();
treeId.copyRawTo(tmp, 0);
RevCommit[] parents = commit.getParents();
if (parents.length == 0) {
edgeValue = GRAPH_NO_PARENT;
} else {
RevCommit parent = parents[0];
edgeValue = graphCommits.getOidPosition(parent);
}
NB.encodeInt32(tmp, hashsz, edgeValue);
if (parents.length == 1) {
edgeValue = GRAPH_NO_PARENT;
} else if (parents.length == 2) {
RevCommit parent = parents[1];
edgeValue = graphCommits.getOidPosition(parent);
} else if (parents.length > 2) {
edgeValue = GRAPH_EXTRA_EDGES_NEEDED | num;
num += parents.length - 1;
}
NB.encodeInt32(tmp, hashsz + 4, edgeValue);
packedDate[0] = 0; // commitTime is an int in JGit now
packedDate[0] |= generations[i] << 2;
packedDate[1] = commit.getCommitTime();
NB.encodeInt32(tmp, hashsz + 8, packedDate[0]);
NB.encodeInt32(tmp, hashsz + 12, packedDate[1]);
out.write(tmp);
out.getWriteMonitor().update(1);
i++;
}
}
private int[] computeGenerationNumbers(ProgressMonitor monitor)
throws MissingObjectException {
int[] generations = new int[graphCommits.size()];
monitor.beginTask(JGitText.get().computingCommitGeneration,
graphCommits.size());
for (RevCommit cmit : graphCommits) {
monitor.update(1);
int generation = generations[graphCommits.getOidPosition(cmit)];
if (generation != COMMIT_GENERATION_NOT_COMPUTED
&& generation != COMMIT_GENERATION_UNKNOWN) {
continue;
}
Stack<RevCommit> commitStack = new Stack<>();
commitStack.push(cmit);
while (!commitStack.empty()) {
int maxGeneration = 0;
boolean allParentComputed = true;
RevCommit current = commitStack.peek();
RevCommit parent;
for (int i = 0; i < current.getParentCount(); i++) {
parent = current.getParent(i);
generation = generations[graphCommits
.getOidPosition(parent)];
if (generation == COMMIT_GENERATION_NOT_COMPUTED
|| generation == COMMIT_GENERATION_UNKNOWN) {
allParentComputed = false;
commitStack.push(parent);
break;
} else if (generation > maxGeneration) {
maxGeneration = generation;
}
}
if (allParentComputed) {
RevCommit commit = commitStack.pop();
generation = maxGeneration + 1;
if (generation > GENERATION_NUMBER_MAX) {
generation = GENERATION_NUMBER_MAX;
}
generations[graphCommits
.getOidPosition(commit)] = generation;
}
}
}
monitor.endTask();
return generations;
}
private void writeExtraEdges(CancellableDigestOutputStream out)
throws IOException {
byte[] tmp = new byte[4];
for (RevCommit commit : graphCommits) {
RevCommit[] parents = commit.getParents();
if (parents.length > 2) {
int edgeValue;
for (int n = 1; n < parents.length; n++) {
RevCommit parent = parents[n];
edgeValue = graphCommits.getOidPosition(parent);
if (n == parents.length - 1) {
edgeValue |= GRAPH_LAST_EDGE;
}
NB.encodeInt32(tmp, 0, edgeValue);
out.write(tmp);
out.getWriteMonitor().update(1);
}
}
}
}
private static class ChunkHeader {
final int id;
final long size;
public ChunkHeader(int id, long size) {
this.id = id;
this.size = size;
}
}
}

View File

@ -0,0 +1,147 @@
/*
* 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 java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdOwnerMap;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.BlockList;
/**
* The commits which are used by the commit-graph writer to:
* <ul>
* <li>List commits in SHA1 order.</li>
* <li>Get the position of a specific SHA1 in the list.</li>
* </ul>
*/
public class GraphCommits implements Iterable<RevCommit> {
/**
* Prepare and create the commits for
* {@link org.eclipse.jgit.internal.storage.commitgraph.CommitGraphWriter}
* from the RevWalk.
*
* @param pm
* progress monitor.
* @param wants
* the list of wanted objects, writer walks commits starting at
* these. Must not be {@code null}.
* @param walk
* the RevWalk to use. Must not be {@code null}.
* @return the commits' collection which are used by the commit-graph
* writer. Never null.
* @throws IOException
*/
public static GraphCommits fromWalk(ProgressMonitor pm,
@NonNull Set<? extends ObjectId> wants, @NonNull RevWalk walk)
throws IOException {
walk.reset();
walk.sort(RevSort.NONE);
walk.setRetainBody(false);
for (ObjectId id : wants) {
RevObject o = walk.parseAny(id);
if (o instanceof RevCommit) {
walk.markStart((RevCommit) o);
}
}
List<RevCommit> commits = new BlockList<>();
RevCommit c;
pm.beginTask(JGitText.get().findingCommitsForCommitGraph,
ProgressMonitor.UNKNOWN);
while ((c = walk.next()) != null) {
pm.update(1);
commits.add(c);
}
pm.endTask();
return new GraphCommits(commits);
}
private final List<RevCommit> sortedCommits;
private final ObjectIdOwnerMap<CommitWithPosition> commitPosMap;
private final int extraEdgeCnt;
/**
* Initialize the GraphCommits.
*
* @param commits
* list of commits with their headers already parsed.
*/
private GraphCommits(List<RevCommit> commits) {
Collections.sort(commits); // sorted by name
sortedCommits = commits;
commitPosMap = new ObjectIdOwnerMap<>();
int cnt = 0;
for (int i = 0; i < commits.size(); i++) {
RevCommit c = sortedCommits.get(i);
if (c.getParentCount() > 2) {
cnt += c.getParentCount() - 1;
}
commitPosMap.add(new CommitWithPosition(c, i));
}
this.extraEdgeCnt = cnt;
}
int getOidPosition(RevCommit c) throws MissingObjectException {
CommitWithPosition commitWithPosition = commitPosMap.get(c);
if (commitWithPosition == null) {
throw new MissingObjectException(c, Constants.OBJ_COMMIT);
}
return commitWithPosition.position;
}
RevCommit getCommit(int oidPos) {
if (oidPos < 0 || oidPos >= sortedCommits.size()) {
return null;
}
return sortedCommits.get(oidPos);
}
int getExtraEdgeCnt() {
return extraEdgeCnt;
}
int size() {
return sortedCommits.size();
}
/** {@inheritDoc} */
@Override
public Iterator<RevCommit> iterator() {
return sortedCommits.iterator();
}
private static class CommitWithPosition extends ObjectIdOwnerMap.Entry {
final int position;
CommitWithPosition(AnyObjectId id, int position) {
super(id);
this.position = position;
}
}
}

View File

@ -886,4 +886,10 @@ public final class ConfigConstants {
*/
public static final String CONFIG_KEY_ABBREV = "abbrev";
/**
* The "writeCommitGraph" key
*
* @since 6.5
*/
public static final String CONFIG_KEY_WRITE_COMMIT_GRAPH = "writeCommitGraph";
}

View File

@ -754,6 +754,23 @@ public static byte[] encode(String str) {
*/
public static final int INFINITE_DEPTH = 0x7fffffff;
/**
* We use ({@value}) as generation number for commits not in the
* commit-graph file.
*
* @since 6.5
*/
public static int COMMIT_GENERATION_UNKNOWN = Integer.MAX_VALUE;
/**
* If a commit-graph file was written by a version of Git that did not
* compute generation numbers, then those commits will have generation
* number represented by ({@value}).
*
* @since 6.5
*/
public static int COMMIT_GENERATION_NOT_COMPUTED = 0;
private Constants() {
// Hide the default constructor
}