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  		addRepoToClose(newRepo);
84  		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
85  		Ref advertisedRef = fetchResult
86  				.getAdvertisedRef("refs/heads/firstcommit");
87  
88  		// We expect first commit to appear by id
89  		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", advertisedRef
90  				.getObjectId().name());
91  		// ..and by name as the bundle created a new ref
92  		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", newRepo
93  				.resolve("refs/heads/firstcommit").name());
94  	}
95  
96  	@Test
97  	public void testWriteHEAD() throws Exception {
98  		byte[] bundle = makeBundle("HEAD",
99  				"42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", null);
100 
101 		Repository newRepo = createBareRepository();
102 		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
103 		Ref advertisedRef = fetchResult.getAdvertisedRef("HEAD");
104 
105 		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", advertisedRef
106 				.getObjectId().name());
107 	}
108 
109 	@Test
110 	public void testIncrementalBundle() throws Exception {
111 		byte[] bundle;
112 
113 		// Create a small bundle, an early commit
114 		bundle = makeBundle("refs/heads/aa", db.resolve("a").name(), null);
115 
116 		// Then we clone a new repo from that bundle and do a simple test. This
117 		// makes sure
118 		// we could read the bundle we created.
119 		Repository newRepo = createBareRepository();
120 		addRepoToClose(newRepo);
121 		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
122 		Ref advertisedRef = fetchResult.getAdvertisedRef("refs/heads/aa");
123 
124 		assertEquals(db.resolve("a").name(), advertisedRef.getObjectId().name());
125 		assertEquals(db.resolve("a").name(), newRepo.resolve("refs/heads/aa")
126 				.name());
127 		assertNull(newRepo.resolve("refs/heads/a"));
128 
129 		// Next an incremental bundle
130 		try (RevWalk rw = new RevWalk(db)) {
131 			bundle = makeBundle("refs/heads/cc", db.resolve("c").name(),
132 					rw.parseCommit(db.resolve("a").toObjectId()));
133 			fetchResult = fetchFromBundle(newRepo, bundle);
134 			advertisedRef = fetchResult.getAdvertisedRef("refs/heads/cc");
135 			assertEquals(db.resolve("c").name(), advertisedRef.getObjectId().name());
136 			assertEquals(db.resolve("c").name(), newRepo.resolve("refs/heads/cc")
137 					.name());
138 			assertNull(newRepo.resolve("refs/heads/c"));
139 			assertNull(newRepo.resolve("refs/heads/a")); // still unknown
140 
141 			try {
142 				// Check that we actually needed the first bundle
143 				Repository newRepo2 = createBareRepository();
144 				fetchResult = fetchFromBundle(newRepo2, bundle);
145 				fail("We should not be able to fetch from bundle with prerequisites that are not fulfilled");
146 			} catch (MissingBundlePrerequisiteException e) {
147 				assertTrue(e.getMessage()
148 						.indexOf(db.resolve("refs/heads/a").name()) >= 0);
149 			}
150 		}
151 	}
152 
153 	@Test
154 	public void testAbortWrite() throws Exception {
155 		boolean caught = false;
156 		try {
157 			makeBundleWithCallback(
158 					"refs/heads/aa", db.resolve("a").name(), null, false);
159 		} catch (WriteAbortedException e) {
160 			caught = true;
161 		}
162 		assertTrue(caught);
163 	}
164 
165 	@Test
166 	public void testCustomObjectReader() throws Exception {
167 		String refName = "refs/heads/blob";
168 		String data = "unflushed data";
169 		ObjectId id;
170 		ByteArrayOutputStream out = new ByteArrayOutputStream();
171 		try (Repository repo = new InMemoryRepository(
172 					new DfsRepositoryDescription("repo"));
173 				ObjectInserter ins = repo.newObjectInserter();
174 				ObjectReader or = ins.newReader()) {
175 			id = ins.insert(OBJ_BLOB, Constants.encode(data));
176 			BundleWriter bw = new BundleWriter(or);
177 			bw.include(refName, id);
178 			bw.writeBundle(NullProgressMonitor.INSTANCE, out);
179 			assertNull(repo.exactRef(refName));
180 			try {
181 				repo.open(id, OBJ_BLOB);
182 				fail("We should not be able to open the unflushed blob");
183 			} catch (MissingObjectException e) {
184 				// Expected.
185 			}
186 		}
187 
188 		try (Repository repo = new InMemoryRepository(
189 					new DfsRepositoryDescription("copy"))) {
190 			fetchFromBundle(repo, out.toByteArray());
191 			Ref ref = repo.exactRef(refName);
192 			assertNotNull(ref);
193 			assertEquals(id, ref.getObjectId());
194 			assertEquals(data,
195 					new String(repo.open(id, OBJ_BLOB).getBytes(), UTF_8));
196 		}
197 	}
198 
199 	private static FetchResult fetchFromBundle(final Repository newRepo,
200 			final byte[] bundle) throws URISyntaxException,
201 			NotSupportedException, TransportException {
202 		final URIish uri = new URIish("in-memory://");
203 		final ByteArrayInputStream in = new ByteArrayInputStream(bundle);
204 		final RefSpec rs = new RefSpec("refs/heads/*:refs/heads/*");
205 		final Set<RefSpec> refs = Collections.singleton(rs);
206 		try (TransportBundleStream transport = new TransportBundleStream(
207 				newRepo, uri, in)) {
208 			return transport.fetch(NullProgressMonitor.INSTANCE, refs);
209 		}
210 	}
211 
212 	private byte[] makeBundle(final String name,
213 			final String anObjectToInclude, final RevCommit assume)
214 			throws FileNotFoundException, IOException {
215 		return makeBundleWithCallback(name, anObjectToInclude, assume, true);
216 	}
217 
218 	private byte[] makeBundleWithCallback(final String name,
219 			final String anObjectToInclude, final RevCommit assume,
220 			boolean value)
221 			throws FileNotFoundException, IOException {
222 		final BundleWriter bw;
223 
224 		bw = new BundleWriter(db);
225 		bw.setObjectCountCallback(new NaiveObjectCountCallback(value));
226 		bw.include(name, ObjectId.fromString(anObjectToInclude));
227 		if (assume != null)
228 			bw.assume(assume);
229 		final ByteArrayOutputStream out = new ByteArrayOutputStream();
230 		bw.writeBundle(NullProgressMonitor.INSTANCE, out);
231 		return out.toByteArray();
232 	}
233 
234 	private static class NaiveObjectCountCallback
235 			implements ObjectCountCallback {
236 		private final boolean value;
237 
238 		NaiveObjectCountCallback(boolean value) {
239 			this.value = value;
240 		}
241 
242 		@Override
243 		public void setObjectCount(long unused) throws WriteAbortedException {
244 			if (!value)
245 				throw new WriteAbortedException();
246 		}
247 	}
248 
249 }