View Javadoc
1   /*
2    * Copyright (C) 2008-2009, Google Inc.
3    * Copyright (C) 2008, Mike Ralphson <mike@abacus.co.uk>
4    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> and others
5    *
6    * This program and the accompanying materials are made available under the
7    * terms of the Eclipse Distribution License v. 1.0 which is available at
8    * https://www.eclipse.org/org/documents/edl-v10.php.
9    *
10   * SPDX-License-Identifier: BSD-3-Clause
11   */
12  
13  package org.eclipse.jgit.transport;
14  
15  import static java.nio.charset.StandardCharsets.UTF_8;
16  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
17  import static org.junit.Assert.assertEquals;
18  import static org.junit.Assert.assertNotNull;
19  import static org.junit.Assert.assertNull;
20  import static org.junit.Assert.assertThrows;
21  import static org.junit.Assert.assertTrue;
22  import static org.junit.Assert.fail;
23  
24  import java.io.ByteArrayInputStream;
25  import java.io.ByteArrayOutputStream;
26  import java.io.FileNotFoundException;
27  import java.io.IOException;
28  import java.net.URISyntaxException;
29  import java.util.Collections;
30  import java.util.Set;
31  
32  import org.eclipse.jgit.errors.MissingBundlePrerequisiteException;
33  import org.eclipse.jgit.errors.MissingObjectException;
34  import org.eclipse.jgit.errors.NotSupportedException;
35  import org.eclipse.jgit.errors.TransportException;
36  import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
37  import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
38  import org.eclipse.jgit.lib.Constants;
39  import org.eclipse.jgit.lib.NullProgressMonitor;
40  import org.eclipse.jgit.lib.ObjectId;
41  import org.eclipse.jgit.lib.ObjectInserter;
42  import org.eclipse.jgit.lib.ObjectReader;
43  import org.eclipse.jgit.lib.Ref;
44  import org.eclipse.jgit.lib.Repository;
45  import org.eclipse.jgit.revwalk.RevCommit;
46  import org.eclipse.jgit.revwalk.RevWalk;
47  import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
48  import org.junit.Test;
49  
50  public class BundleWriterTest extends SampleDataRepositoryTestCase {
51  
52  	@Test
53  	public void testEmptyBundleFails() throws Exception {
54  		Repository newRepo = createBareRepository();
55  		assertThrows(TransportException.class,
56  				() -> fetchFromBundle(newRepo, new byte[0]));
57  	}
58  
59  	@Test
60  	public void testNonBundleFails() throws Exception {
61  		Repository newRepo = createBareRepository();
62  		assertThrows(TransportException.class, () -> fetchFromBundle(newRepo,
63  				"Not a bundle file".getBytes(UTF_8)));
64  	}
65  
66  	@Test
67  	public void testGarbageBundleFails() throws Exception {
68  		Repository newRepo = createBareRepository();
69  		assertThrows(TransportException.class, () -> fetchFromBundle(newRepo,
70  				(TransportBundle.V2_BUNDLE_SIGNATURE + '\n' + "Garbage")
71  						.getBytes(UTF_8)));
72  	}
73  
74  	@Test
75  	public void testWriteSingleRef() throws Exception {
76  		// Create a tiny bundle, (well one of) the first commits only
77  		final byte[] bundle = makeBundle("refs/heads/firstcommit",
78  				"42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", null);
79  
80  		// Then we clone a new repo from that bundle and do a simple test. This
81  		// makes sure we could read the bundle we created.
82  		Repository newRepo = createBareRepository();
83  		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
84  		Ref advertisedRef = fetchResult
85  				.getAdvertisedRef("refs/heads/firstcommit");
86  
87  		// We expect first commit to appear by id
88  		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", advertisedRef
89  				.getObjectId().name());
90  		// ..and by name as the bundle created a new ref
91  		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", newRepo
92  				.resolve("refs/heads/firstcommit").name());
93  	}
94  
95  	@Test
96  	public void testWriteHEAD() throws Exception {
97  		byte[] bundle = makeBundle("HEAD",
98  				"42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", null);
99  
100 		Repository newRepo = createBareRepository();
101 		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
102 		Ref advertisedRef = fetchResult.getAdvertisedRef("HEAD");
103 
104 		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", advertisedRef
105 				.getObjectId().name());
106 	}
107 
108 	@Test
109 	public void testIncrementalBundle() throws Exception {
110 		byte[] bundle;
111 
112 		// Create a small bundle, an early commit
113 		bundle = makeBundle("refs/heads/aa", db.resolve("a").name(), null);
114 
115 		// Then we clone a new repo from that bundle and do a simple test. This
116 		// makes sure
117 		// we could read the bundle we created.
118 		Repository newRepo = createBareRepository();
119 		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
120 		Ref advertisedRef = fetchResult.getAdvertisedRef("refs/heads/aa");
121 
122 		assertEquals(db.resolve("a").name(), advertisedRef.getObjectId().name());
123 		assertEquals(db.resolve("a").name(), newRepo.resolve("refs/heads/aa")
124 				.name());
125 		assertNull(newRepo.resolve("refs/heads/a"));
126 
127 		// Next an incremental bundle
128 		try (RevWalk rw = new RevWalk(db)) {
129 			bundle = makeBundle("refs/heads/cc", db.resolve("c").name(),
130 					rw.parseCommit(db.resolve("a").toObjectId()));
131 			fetchResult = fetchFromBundle(newRepo, bundle);
132 			advertisedRef = fetchResult.getAdvertisedRef("refs/heads/cc");
133 			assertEquals(db.resolve("c").name(), advertisedRef.getObjectId().name());
134 			assertEquals(db.resolve("c").name(), newRepo.resolve("refs/heads/cc")
135 					.name());
136 			assertNull(newRepo.resolve("refs/heads/c"));
137 			assertNull(newRepo.resolve("refs/heads/a")); // still unknown
138 
139 			try {
140 				// Check that we actually needed the first bundle
141 				Repository newRepo2 = createBareRepository();
142 				fetchResult = fetchFromBundle(newRepo2, bundle);
143 				fail("We should not be able to fetch from bundle with prerequisites that are not fulfilled");
144 			} catch (MissingBundlePrerequisiteException e) {
145 				assertTrue(e.getMessage()
146 						.indexOf(db.resolve("refs/heads/a").name()) >= 0);
147 			}
148 		}
149 	}
150 
151 	@Test
152 	public void testAbortWrite() throws Exception {
153 		boolean caught = false;
154 		try {
155 			makeBundleWithCallback(
156 					"refs/heads/aa", db.resolve("a").name(), null, false);
157 		} catch (WriteAbortedException e) {
158 			caught = true;
159 		}
160 		assertTrue(caught);
161 	}
162 
163 	@Test
164 	public void testCustomObjectReader() throws Exception {
165 		String refName = "refs/heads/blob";
166 		String data = "unflushed data";
167 		ObjectId id;
168 		ByteArrayOutputStream out = new ByteArrayOutputStream();
169 		try (Repository repo = new InMemoryRepository(
170 					new DfsRepositoryDescription("repo"));
171 				ObjectInserter ins = repo.newObjectInserter();
172 				ObjectReader or = ins.newReader()) {
173 			id = ins.insert(OBJ_BLOB, Constants.encode(data));
174 			BundleWriter bw = new BundleWriter(or);
175 			bw.include(refName, id);
176 			bw.writeBundle(NullProgressMonitor.INSTANCE, out);
177 			assertNull(repo.exactRef(refName));
178 			try {
179 				repo.open(id, OBJ_BLOB);
180 				fail("We should not be able to open the unflushed blob");
181 			} catch (MissingObjectException e) {
182 				// Expected.
183 			}
184 		}
185 
186 		try (Repository repo = new InMemoryRepository(
187 					new DfsRepositoryDescription("copy"))) {
188 			fetchFromBundle(repo, out.toByteArray());
189 			Ref ref = repo.exactRef(refName);
190 			assertNotNull(ref);
191 			assertEquals(id, ref.getObjectId());
192 			assertEquals(data,
193 					new String(repo.open(id, OBJ_BLOB).getBytes(), UTF_8));
194 		}
195 	}
196 
197 	private static FetchResult fetchFromBundle(final Repository newRepo,
198 			final byte[] bundle) throws URISyntaxException,
199 			NotSupportedException, TransportException {
200 		final URIish uri = new URIish("in-memory://");
201 		final ByteArrayInputStream in = new ByteArrayInputStream(bundle);
202 		final RefSpec rs = new RefSpec("refs/heads/*:refs/heads/*");
203 		final Set<RefSpec> refs = Collections.singleton(rs);
204 		try (TransportBundleStream transport = new TransportBundleStream(
205 				newRepo, uri, in)) {
206 			return transport.fetch(NullProgressMonitor.INSTANCE, refs);
207 		}
208 	}
209 
210 	private byte[] makeBundle(final String name,
211 			final String anObjectToInclude, final RevCommit assume)
212 			throws FileNotFoundException, IOException {
213 		return makeBundleWithCallback(name, anObjectToInclude, assume, true);
214 	}
215 
216 	private byte[] makeBundleWithCallback(final String name,
217 			final String anObjectToInclude, final RevCommit assume,
218 			boolean value)
219 			throws FileNotFoundException, IOException {
220 		final BundleWriter bw;
221 
222 		bw = new BundleWriter(db);
223 		bw.setObjectCountCallback(new NaiveObjectCountCallback(value));
224 		bw.include(name, ObjectId.fromString(anObjectToInclude));
225 		if (assume != null)
226 			bw.assume(assume);
227 		final ByteArrayOutputStream out = new ByteArrayOutputStream();
228 		bw.writeBundle(NullProgressMonitor.INSTANCE, out);
229 		return out.toByteArray();
230 	}
231 
232 	private static class NaiveObjectCountCallback
233 			implements ObjectCountCallback {
234 		private final boolean value;
235 
236 		NaiveObjectCountCallback(boolean value) {
237 			this.value = value;
238 		}
239 
240 		@Override
241 		public void setObjectCount(long unused) throws WriteAbortedException {
242 			if (!value)
243 				throw new WriteAbortedException();
244 		}
245 	}
246 
247 }