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>
5    * and other copyright owners as documented in the project's IP log.
6    *
7    * This program and the accompanying materials are made available
8    * under the terms of the Eclipse Distribution License v1.0 which
9    * accompanies this distribution, is reproduced below, and is
10   * available at http://www.eclipse.org/org/documents/edl-v10.php
11   *
12   * All rights reserved.
13   *
14   * Redistribution and use in source and binary forms, with or
15   * without modification, are permitted provided that the following
16   * conditions are met:
17   *
18   * - Redistributions of source code must retain the above copyright
19   *   notice, this list of conditions and the following disclaimer.
20   *
21   * - Redistributions in binary form must reproduce the above
22   *   copyright notice, this list of conditions and the following
23   *   disclaimer in the documentation and/or other materials provided
24   *   with the distribution.
25   *
26   * - Neither the name of the Eclipse Foundation, Inc. nor the
27   *   names of its contributors may be used to endorse or promote
28   *   products derived from this software without specific prior
29   *   written permission.
30   *
31   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
32   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
33   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
34   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
35   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
36   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
37   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
38   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
39   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
40   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
41   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
42   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
43   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44   */
45  
46  package org.eclipse.jgit.transport;
47  
48  import static java.nio.charset.StandardCharsets.UTF_8;
49  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
50  import static org.junit.Assert.assertEquals;
51  import static org.junit.Assert.assertNotNull;
52  import static org.junit.Assert.assertNull;
53  import static org.junit.Assert.assertTrue;
54  import static org.junit.Assert.fail;
55  
56  import java.io.ByteArrayInputStream;
57  import java.io.ByteArrayOutputStream;
58  import java.io.FileNotFoundException;
59  import java.io.IOException;
60  import java.net.URISyntaxException;
61  import java.util.Collections;
62  import java.util.Set;
63  
64  import org.eclipse.jgit.errors.MissingBundlePrerequisiteException;
65  import org.eclipse.jgit.errors.MissingObjectException;
66  import org.eclipse.jgit.errors.NotSupportedException;
67  import org.eclipse.jgit.errors.TransportException;
68  import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
69  import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
70  import org.eclipse.jgit.lib.Constants;
71  import org.eclipse.jgit.lib.NullProgressMonitor;
72  import org.eclipse.jgit.lib.ObjectId;
73  import org.eclipse.jgit.lib.ObjectInserter;
74  import org.eclipse.jgit.lib.ObjectReader;
75  import org.eclipse.jgit.lib.Ref;
76  import org.eclipse.jgit.lib.Repository;
77  import org.eclipse.jgit.revwalk.RevCommit;
78  import org.eclipse.jgit.revwalk.RevWalk;
79  import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
80  import org.junit.Rule;
81  import org.junit.Test;
82  import org.junit.rules.ExpectedException;
83  
84  public class BundleWriterTest extends SampleDataRepositoryTestCase {
85  
86  	@Rule
87  	public ExpectedException thrown = ExpectedException.none();
88  
89  	@Test
90  	public void testEmptyBundleFails() throws Exception {
91  		Repository newRepo = createBareRepository();
92  		thrown.expect(TransportException.class);
93  		fetchFromBundle(newRepo, new byte[0]);
94  	}
95  
96  	@Test
97  	public void testNonBundleFails() throws Exception {
98  		Repository newRepo = createBareRepository();
99  		thrown.expect(TransportException.class);
100 		fetchFromBundle(newRepo, "Not a bundle file".getBytes(UTF_8));
101 	}
102 
103 	@Test
104 	public void testGarbageBundleFails() throws Exception {
105 		Repository newRepo = createBareRepository();
106 		thrown.expect(TransportException.class);
107 		fetchFromBundle(newRepo,
108 				(TransportBundle.V2_BUNDLE_SIGNATURE + '\n' + "Garbage")
109 						.getBytes(UTF_8));
110 	}
111 
112 	@Test
113 	public void testWriteSingleRef() throws Exception {
114 		// Create a tiny bundle, (well one of) the first commits only
115 		final byte[] bundle = makeBundle("refs/heads/firstcommit",
116 				"42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", null);
117 
118 		// Then we clone a new repo from that bundle and do a simple test. This
119 		// makes sure we could read the bundle we created.
120 		Repository newRepo = createBareRepository();
121 		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
122 		Ref advertisedRef = fetchResult
123 				.getAdvertisedRef("refs/heads/firstcommit");
124 
125 		// We expect first commit to appear by id
126 		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", advertisedRef
127 				.getObjectId().name());
128 		// ..and by name as the bundle created a new ref
129 		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", newRepo
130 				.resolve("refs/heads/firstcommit").name());
131 	}
132 
133 	@Test
134 	public void testWriteHEAD() throws Exception {
135 		byte[] bundle = makeBundle("HEAD",
136 				"42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", null);
137 
138 		Repository newRepo = createBareRepository();
139 		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
140 		Ref advertisedRef = fetchResult.getAdvertisedRef("HEAD");
141 
142 		assertEquals("42e4e7c5e507e113ebbb7801b16b52cf867b7ce1", advertisedRef
143 				.getObjectId().name());
144 	}
145 
146 	@Test
147 	public void testIncrementalBundle() throws Exception {
148 		byte[] bundle;
149 
150 		// Create a small bundle, an early commit
151 		bundle = makeBundle("refs/heads/aa", db.resolve("a").name(), null);
152 
153 		// Then we clone a new repo from that bundle and do a simple test. This
154 		// makes sure
155 		// we could read the bundle we created.
156 		Repository newRepo = createBareRepository();
157 		FetchResult fetchResult = fetchFromBundle(newRepo, bundle);
158 		Ref advertisedRef = fetchResult.getAdvertisedRef("refs/heads/aa");
159 
160 		assertEquals(db.resolve("a").name(), advertisedRef.getObjectId().name());
161 		assertEquals(db.resolve("a").name(), newRepo.resolve("refs/heads/aa")
162 				.name());
163 		assertNull(newRepo.resolve("refs/heads/a"));
164 
165 		// Next an incremental bundle
166 		try (RevWalk rw = new RevWalk(db)) {
167 			bundle = makeBundle("refs/heads/cc", db.resolve("c").name(),
168 					rw.parseCommit(db.resolve("a").toObjectId()));
169 			fetchResult = fetchFromBundle(newRepo, bundle);
170 			advertisedRef = fetchResult.getAdvertisedRef("refs/heads/cc");
171 			assertEquals(db.resolve("c").name(), advertisedRef.getObjectId().name());
172 			assertEquals(db.resolve("c").name(), newRepo.resolve("refs/heads/cc")
173 					.name());
174 			assertNull(newRepo.resolve("refs/heads/c"));
175 			assertNull(newRepo.resolve("refs/heads/a")); // still unknown
176 
177 			try {
178 				// Check that we actually needed the first bundle
179 				Repository newRepo2 = createBareRepository();
180 				fetchResult = fetchFromBundle(newRepo2, bundle);
181 				fail("We should not be able to fetch from bundle with prerequisites that are not fulfilled");
182 			} catch (MissingBundlePrerequisiteException e) {
183 				assertTrue(e.getMessage()
184 						.indexOf(db.resolve("refs/heads/a").name()) >= 0);
185 			}
186 		}
187 	}
188 
189 	@Test
190 	public void testAbortWrite() throws Exception {
191 		boolean caught = false;
192 		try {
193 			makeBundleWithCallback(
194 					"refs/heads/aa", db.resolve("a").name(), null, false);
195 		} catch (WriteAbortedException e) {
196 			caught = true;
197 		}
198 		assertTrue(caught);
199 	}
200 
201 	@Test
202 	public void testCustomObjectReader() throws Exception {
203 		String refName = "refs/heads/blob";
204 		String data = "unflushed data";
205 		ObjectId id;
206 		ByteArrayOutputStream out = new ByteArrayOutputStream();
207 		try (Repository repo = new InMemoryRepository(
208 					new DfsRepositoryDescription("repo"));
209 				ObjectInserter ins = repo.newObjectInserter();
210 				ObjectReader or = ins.newReader()) {
211 			id = ins.insert(OBJ_BLOB, Constants.encode(data));
212 			BundleWriter bw = new BundleWriter(or);
213 			bw.include(refName, id);
214 			bw.writeBundle(NullProgressMonitor.INSTANCE, out);
215 			assertNull(repo.exactRef(refName));
216 			try {
217 				repo.open(id, OBJ_BLOB);
218 				fail("We should not be able to open the unflushed blob");
219 			} catch (MissingObjectException e) {
220 				// Expected.
221 			}
222 		}
223 
224 		try (Repository repo = new InMemoryRepository(
225 					new DfsRepositoryDescription("copy"))) {
226 			fetchFromBundle(repo, out.toByteArray());
227 			Ref ref = repo.exactRef(refName);
228 			assertNotNull(ref);
229 			assertEquals(id, ref.getObjectId());
230 			assertEquals(data,
231 					new String(repo.open(id, OBJ_BLOB).getBytes(), UTF_8));
232 		}
233 	}
234 
235 	private static FetchResult fetchFromBundle(final Repository newRepo,
236 			final byte[] bundle) throws URISyntaxException,
237 			NotSupportedException, TransportException {
238 		final URIish uri = new URIish("in-memory://");
239 		final ByteArrayInputStream in = new ByteArrayInputStream(bundle);
240 		final RefSpec rs = new RefSpec("refs/heads/*:refs/heads/*");
241 		final Set<RefSpec> refs = Collections.singleton(rs);
242 		try (TransportBundleStream transport = new TransportBundleStream(
243 				newRepo, uri, in)) {
244 			return transport.fetch(NullProgressMonitor.INSTANCE, refs);
245 		}
246 	}
247 
248 	private byte[] makeBundle(final String name,
249 			final String anObjectToInclude, final RevCommit assume)
250 			throws FileNotFoundException, IOException {
251 		return makeBundleWithCallback(name, anObjectToInclude, assume, true);
252 	}
253 
254 	private byte[] makeBundleWithCallback(final String name,
255 			final String anObjectToInclude, final RevCommit assume,
256 			boolean value)
257 			throws FileNotFoundException, IOException {
258 		final BundleWriter bw;
259 
260 		bw = new BundleWriter(db);
261 		bw.setObjectCountCallback(new NaiveObjectCountCallback(value));
262 		bw.include(name, ObjectId.fromString(anObjectToInclude));
263 		if (assume != null)
264 			bw.assume(assume);
265 		final ByteArrayOutputStream out = new ByteArrayOutputStream();
266 		bw.writeBundle(NullProgressMonitor.INSTANCE, out);
267 		return out.toByteArray();
268 	}
269 
270 	private static class NaiveObjectCountCallback
271 			implements ObjectCountCallback {
272 		private final boolean value;
273 
274 		NaiveObjectCountCallback(boolean value) {
275 			this.value = value;
276 		}
277 
278 		@Override
279 		public void setObjectCount(long unused) throws WriteAbortedException {
280 			if (!value)
281 				throw new WriteAbortedException();
282 		}
283 	}
284 
285 }