Skip to content

Commit 4449bff

Browse files
committed
RefAdvancer: helper to find when a branch enters a pack
For the bitmap calculation in midx, we need to follow a tip until it enters a pack. Add a helper to advance tips until they find their entry point into a pack. Change-Id: I54a7df7dd51be104a31008b3bf2949df6a6a6964
1 parent 0f88fca commit 4449bff

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright (C) 2026, Google LLC.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Distribution License v. 1.0 which is available at
6+
* https://www.eclipse.org/org/documents/edl-v10.php.
7+
*
8+
* SPDX-License-Identifier: BSD-3-Clause
9+
*/
10+
package org.eclipse.jgit.internal.storage.dfs;
11+
12+
import static org.junit.Assert.assertEquals;
13+
import static org.junit.Assert.assertTrue;
14+
15+
import java.io.IOException;
16+
import java.util.HashSet;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Set;
20+
import java.util.stream.Collectors;
21+
22+
import org.eclipse.jgit.junit.TestRepository;
23+
import org.eclipse.jgit.lib.ObjectId;
24+
import org.eclipse.jgit.revwalk.RevCommit;
25+
import org.junit.Before;
26+
import org.junit.Test;
27+
28+
public class RefAdvancerWalkTest {
29+
private static final String MAIN = "refs/heads/main";
30+
31+
private InMemoryRepository db;
32+
33+
private TestRepository<InMemoryRepository> git;
34+
35+
private Set<RevCommit> commitsInMidx;
36+
37+
private Map<String, RevCommit> commitByLetter;
38+
39+
@Before
40+
public void setUp() throws Exception {
41+
db = new InMemoryRepository(
42+
new DfsRepositoryDescription("ref advance"));
43+
git = new TestRepository<>(db);
44+
setupRepo();
45+
}
46+
47+
/**
48+
* <pre>
49+
* tipMergeBeforeMidx -> H
50+
* |
51+
* |
52+
* F G <- tipStraight
53+
* |\ |
54+
* | \|
55+
* tipMergeMidxCommits -> D E
56+
* |\ |
57+
* +--------+
58+
* | B C <-- tipIn
59+
* | | / |
60+
* | |/ |
61+
* | A |
62+
* | midx|
63+
* +--------+
64+
* </pre>
65+
*/
66+
private void setupRepo() throws Exception {
67+
RevCommit a = commitToMain();
68+
RevCommit b = commitToMain();
69+
RevCommit c = commit(a);
70+
RevCommit d = commitToMain(c);
71+
RevCommit e = commit(c);
72+
/* unused */ commitToMain();
73+
RevCommit g = commit(e);
74+
RevCommit h = commitToMain();
75+
76+
commitsInMidx = new HashSet<>();
77+
commitsInMidx.add(a);
78+
commitsInMidx.add(b);
79+
commitsInMidx.add(c);
80+
81+
commitByLetter = Map.of("a", a, "b", b, "c", c, "d", d, "e", e, "g", g,
82+
"h", h);
83+
84+
}
85+
86+
@Test
87+
public void singleWant_linearHistory() throws Exception {
88+
runTest("g", Set.of("c"));
89+
}
90+
91+
@Test
92+
public void singleWant_alreadyInMidx() throws Exception {
93+
runTest("c", Set.of("c"));
94+
}
95+
96+
@Test
97+
public void singleWant_mergeCommitsInMidx() throws Exception {
98+
runTest("d", Set.of("b", "c"));
99+
}
100+
101+
@Test
102+
public void singleWant_mergeBeforeMidx() throws Exception {
103+
runTest("h", Set.of("b", "c"));
104+
}
105+
106+
@Test
107+
public void manyWant_mergeBeforeMidx() throws Exception {
108+
runTest(Set.of("h", "c", "d", "g"), Set.of("b", "c"));
109+
}
110+
111+
private void runTest(String want, Set<String> expectedTips)
112+
throws IOException {
113+
runTest(Set.of(want), expectedTips);
114+
}
115+
116+
private void runTest(Set<String> want, Set<String> expectedTips)
117+
throws IOException {
118+
RefAdvancerWalk advancer = new RefAdvancerWalk(db,
119+
commitsInMidx::contains);
120+
List<ObjectId> wants = want.stream().map(commitByLetter::get)
121+
.collect(Collectors.toUnmodifiableList());
122+
Set<RevCommit> tipsInMidx = advancer.advance(wants);
123+
124+
Set<RevCommit> expected = expectedTips.stream().map(commitByLetter::get)
125+
.collect(Collectors.toUnmodifiableSet());
126+
assertEquals(expected.size(), tipsInMidx.size());
127+
assertTrue(tipsInMidx.containsAll(expected));
128+
}
129+
130+
private static int commitCounter = 0;
131+
132+
private RevCommit commitToMain() throws Exception {
133+
int i = commitCounter++;
134+
return git.branch(MAIN).commit()
135+
.add("xx" + i, git.blob("content #" + i)).create();
136+
}
137+
138+
private RevCommit commitToMain(RevCommit... extraParent) throws Exception {
139+
int i = commitCounter++;
140+
TestRepository<InMemoryRepository>.CommitBuilder commit = git
141+
.branch(MAIN).commit();
142+
for (RevCommit p : extraParent) {
143+
commit.parent(p);
144+
}
145+
146+
return commit.add("xx" + i, git.blob("content #" + i)).create();
147+
}
148+
149+
private RevCommit commit(RevCommit parent) throws Exception {
150+
int i = commitCounter++;
151+
return git.commit().parent(parent)
152+
.add("cc" + i, git.blob("out of main content #" + i)).create();
153+
}
154+
155+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (C) 2026, Google LLC.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Distribution License v. 1.0 which is available at
6+
* https://www.eclipse.org/org/documents/edl-v10.php.
7+
*
8+
* SPDX-License-Identifier: BSD-3-Clause
9+
*/
10+
package org.eclipse.jgit.internal.storage.dfs;
11+
12+
import java.io.IOException;
13+
import java.util.HashSet;
14+
import java.util.List;
15+
import java.util.Set;
16+
17+
import org.eclipse.jgit.errors.StopWalkException;
18+
import org.eclipse.jgit.lib.ObjectId;
19+
import org.eclipse.jgit.revwalk.RevCommit;
20+
import org.eclipse.jgit.revwalk.RevObject;
21+
import org.eclipse.jgit.revwalk.RevSort;
22+
import org.eclipse.jgit.revwalk.RevWalk;
23+
import org.eclipse.jgit.revwalk.filter.RevFilter;
24+
25+
/**
26+
* Walk from some commits and find where to they enter a pack
27+
*/
28+
class RefAdvancerWalk {
29+
30+
private final DfsRepository db;
31+
32+
private final InPackPredicate includeP;
33+
34+
/**
35+
* True when the commit is in the pack
36+
*/
37+
@FunctionalInterface
38+
interface InPackPredicate {
39+
boolean test(RevCommit c) throws IOException;
40+
}
41+
42+
RefAdvancerWalk(DfsRepository db, InPackPredicate include) {
43+
this.db = db;
44+
this.includeP = include;
45+
}
46+
47+
private RevWalk createRevWalk() {
48+
RevWalk rw = new RevWalk(db);
49+
rw.sort(RevSort.COMMIT_TIME_DESC);
50+
rw.setRevFilter(new FirstInPack(includeP));
51+
return rw;
52+
}
53+
54+
/**
55+
* Advance the tips to their first commit inside the pack
56+
*
57+
* @param allTips
58+
* tips of interesting refs
59+
* @return first commit(s) where the tips enter the pack. A tips may
60+
* translate into 0 commits (it doesn't enter the pack in its
61+
* history), 1 commit (a linear history) or n commits (merges lead
62+
* to multiple histories into the pack). A tip already inside the
63+
* pack is returned as it is.
64+
* @throws IOException
65+
* error browsing history
66+
*/
67+
Set<RevCommit> advance(List<ObjectId> allTips) throws IOException {
68+
Set<RevCommit> tipsInMidx = new HashSet<>(allTips.size());
69+
try (RevWalk rw = createRevWalk()) {
70+
for (ObjectId tip : allTips) {
71+
RevObject tipObject = rw.parseAny(tip);
72+
if (!(tipObject instanceof RevCommit tipCommit)) {
73+
continue;
74+
}
75+
76+
rw.markStart(tipCommit);
77+
RevCommit inPack;
78+
while ((inPack = rw.next()) != null) {
79+
tipsInMidx.add(inPack);
80+
}
81+
}
82+
}
83+
return tipsInMidx;
84+
}
85+
86+
private static class FirstInPack extends RevFilter {
87+
88+
private final InPackPredicate isInPack;
89+
90+
FirstInPack(InPackPredicate isInPack) {
91+
this.isInPack = isInPack;
92+
}
93+
94+
@Override
95+
public boolean include(RevWalk walker, RevCommit cmit)
96+
throws StopWalkException, IOException {
97+
if (!isInPack.test(cmit)) {
98+
return false;
99+
}
100+
101+
for (RevCommit p : cmit.getParents()) {
102+
walker.markUninteresting(p);
103+
}
104+
return true;
105+
}
106+
107+
@Override
108+
public RevFilter clone() {
109+
return this;
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)