View Javadoc
1   package org.eclipse.jgit.internal.storage.dfs;
2   
3   import java.io.ByteArrayOutputStream;
4   import java.io.FileNotFoundException;
5   import java.io.IOException;
6   import java.nio.ByteBuffer;
7   import java.util.ArrayList;
8   import java.util.Collection;
9   import java.util.HashMap;
10  import java.util.List;
11  import java.util.Map;
12  import java.util.Objects;
13  import java.util.concurrent.ConcurrentHashMap;
14  import java.util.concurrent.ConcurrentMap;
15  import java.util.concurrent.atomic.AtomicInteger;
16  import java.util.concurrent.locks.ReadWriteLock;
17  import java.util.concurrent.locks.ReentrantReadWriteLock;
18  
19  import org.eclipse.jgit.annotations.Nullable;
20  import org.eclipse.jgit.internal.storage.pack.PackExt;
21  import org.eclipse.jgit.lib.BatchRefUpdate;
22  import org.eclipse.jgit.lib.ObjectId;
23  import org.eclipse.jgit.lib.ObjectIdRef;
24  import org.eclipse.jgit.lib.ProgressMonitor;
25  import org.eclipse.jgit.lib.Ref;
26  import org.eclipse.jgit.lib.Ref.Storage;
27  import org.eclipse.jgit.lib.RefDatabase;
28  import org.eclipse.jgit.revwalk.RevObject;
29  import org.eclipse.jgit.revwalk.RevTag;
30  import org.eclipse.jgit.revwalk.RevWalk;
31  import org.eclipse.jgit.transport.ReceiveCommand;
32  import org.eclipse.jgit.util.RefList;
33  
34  /**
35   * Git repository stored entirely in the local process memory.
36   * <p>
37   * This implementation builds on the DFS repository by storing all reference and
38   * object data in the local process. It is not very efficient and exists only
39   * for unit testing and small experiments.
40   * <p>
41   * The repository is thread-safe. Memory used is released only when this object
42   * is garbage collected. Closing the repository has no impact on its memory.
43   */
44  public class InMemoryRepository extends DfsRepository {
45  	/** Builder for in-memory repositories. */
46  	public static class Builder
47  			extends DfsRepositoryBuilder<Builder, InMemoryRepository> {
48  		@Override
49  		public InMemoryRepository build() throws IOException {
50  			return new InMemoryRepository(this);
51  		}
52  	}
53  
54  	static final AtomicInteger packId = new AtomicInteger();
55  
56  	private final DfsObjDatabase objdb;
57  	private final RefDatabase refdb;
58  	private String gitwebDescription;
59  	private boolean performsAtomicTransactions = true;
60  
61  	/**
62  	 * Initialize a new in-memory repository.
63  	 *
64  	 * @param repoDesc
65  	 *            description of the repository.
66  	 */
67  	public InMemoryRepository(DfsRepositoryDescription repoDesc) {
68  		this(new Builder().setRepositoryDescription(repoDesc));
69  	}
70  
71  	InMemoryRepository(Builder builder) {
72  		super(builder);
73  		objdb = new MemObjDatabase(this);
74  		refdb = new MemRefDatabase();
75  	}
76  
77  	@Override
78  	public DfsObjDatabase getObjectDatabase() {
79  		return objdb;
80  	}
81  
82  	@Override
83  	public RefDatabase getRefDatabase() {
84  		return refdb;
85  	}
86  
87  	/**
88  	 * Enable (or disable) the atomic reference transaction support.
89  	 * <p>
90  	 * Useful for testing atomic support enabled or disabled.
91  	 *
92  	 * @param atomic
93  	 */
94  	public void setPerformsAtomicTransactions(boolean atomic) {
95  		performsAtomicTransactions = atomic;
96  	}
97  
98  	@Override
99  	@Nullable
100 	public String getGitwebDescription() {
101 		return gitwebDescription;
102 	}
103 
104 	@Override
105 	public void setGitwebDescription(@Nullable String d) {
106 		gitwebDescription = d;
107 	}
108 
109 	private class MemObjDatabase extends DfsObjDatabase {
110 		private List<DfsPackDescription> packs = new ArrayList<>();
111 
112 		MemObjDatabase(DfsRepository repo) {
113 			super(repo, new DfsReaderOptions());
114 		}
115 
116 		@Override
117 		protected synchronized List<DfsPackDescription> listPacks() {
118 			return packs;
119 		}
120 
121 		@Override
122 		protected DfsPackDescription newPack(PackSource source) {
123 			int id = packId.incrementAndGet();
124 			DfsPackDescription desc = new MemPack(
125 					"pack-" + id + "-" + source.name(), //$NON-NLS-1$ //$NON-NLS-2$
126 					getRepository().getDescription());
127 			return desc.setPackSource(source);
128 		}
129 
130 		@Override
131 		protected synchronized void commitPackImpl(
132 				Collection<DfsPackDescription> desc,
133 				Collection<DfsPackDescription> replace) {
134 			List<DfsPackDescription> n;
135 			n = new ArrayList<>(desc.size() + packs.size());
136 			n.addAll(desc);
137 			n.addAll(packs);
138 			if (replace != null)
139 				n.removeAll(replace);
140 			packs = n;
141 		}
142 
143 		@Override
144 		protected void rollbackPack(Collection<DfsPackDescription> desc) {
145 			// Do nothing. Pack is not recorded until commitPack.
146 		}
147 
148 		@Override
149 		protected ReadableChannel openFile(DfsPackDescription desc, PackExt ext)
150 				throws FileNotFoundException, IOException {
151 			MemPack memPack = (MemPack) desc;
152 			byte[] file = memPack.fileMap.get(ext);
153 			if (file == null)
154 				throw new FileNotFoundException(desc.getFileName(ext));
155 			return new ByteArrayReadableChannel(file);
156 		}
157 
158 		@Override
159 		protected DfsOutputStream writeFile(
160 				DfsPackDescription desc, final PackExt ext) throws IOException {
161 			final MemPack memPack = (MemPack) desc;
162 			return new Out() {
163 				@Override
164 				public void flush() {
165 					memPack.fileMap.put(ext, getData());
166 				}
167 			};
168 		}
169 	}
170 
171 	private static class MemPack extends DfsPackDescription {
172 		final Map<PackExt, byte[]>
173 				fileMap = new HashMap<>();
174 
175 		MemPack(String name, DfsRepositoryDescription repoDesc) {
176 			super(repoDesc, name);
177 		}
178 	}
179 
180 	private abstract static class Out extends DfsOutputStream {
181 		private final ByteArrayOutputStream dst = new ByteArrayOutputStream();
182 
183 		private byte[] data;
184 
185 		@Override
186 		public void write(byte[] buf, int off, int len) {
187 			data = null;
188 			dst.write(buf, off, len);
189 		}
190 
191 		@Override
192 		public int read(long position, ByteBuffer buf) {
193 			byte[] d = getData();
194 			int n = Math.min(buf.remaining(), d.length - (int) position);
195 			if (n == 0)
196 				return -1;
197 			buf.put(d, (int) position, n);
198 			return n;
199 		}
200 
201 		byte[] getData() {
202 			if (data == null)
203 				data = dst.toByteArray();
204 			return data;
205 		}
206 
207 		@Override
208 		public abstract void flush();
209 
210 		@Override
211 		public void close() {
212 			flush();
213 		}
214 
215 	}
216 
217 	private static class ByteArrayReadableChannel implements ReadableChannel {
218 		private final byte[] data;
219 
220 		private int position;
221 
222 		private boolean open = true;
223 
224 		ByteArrayReadableChannel(byte[] buf) {
225 			data = buf;
226 		}
227 
228 		@Override
229 		public int read(ByteBuffer dst) {
230 			int n = Math.min(dst.remaining(), data.length - position);
231 			if (n == 0)
232 				return -1;
233 			dst.put(data, position, n);
234 			position += n;
235 			return n;
236 		}
237 
238 		@Override
239 		public void close() {
240 			open = false;
241 		}
242 
243 		@Override
244 		public boolean isOpen() {
245 			return open;
246 		}
247 
248 		@Override
249 		public long position() {
250 			return position;
251 		}
252 
253 		@Override
254 		public void position(long newPosition) {
255 			position = (int) newPosition;
256 		}
257 
258 		@Override
259 		public long size() {
260 			return data.length;
261 		}
262 
263 		@Override
264 		public int blockSize() {
265 			return 0;
266 		}
267 
268 		@Override
269 		public void setReadAheadBytes(int b) {
270 			// Unnecessary on a byte array.
271 		}
272 	}
273 
274 	/**
275 	 * A ref database storing all refs in-memory.
276 	 * <p>
277 	 * This class is protected (and not private) to facilitate testing using
278 	 * subclasses of InMemoryRepository.
279 	 */
280     protected class MemRefDatabase extends DfsRefDatabase {
281 		private final ConcurrentMap<String, Ref> refs = new ConcurrentHashMap<>();
282 		private final ReadWriteLock lock = new ReentrantReadWriteLock(true /* fair */);
283 
284 		/**
285 		 * Initialize a new in-memory ref database.
286 		 */
287 		protected MemRefDatabase() {
288 			super(InMemoryRepository.this);
289 		}
290 
291 		@Override
292 		public boolean performsAtomicTransactions() {
293 			return performsAtomicTransactions;
294 		}
295 
296 		@Override
297 		public BatchRefUpdate newBatchUpdate() {
298 			return new BatchRefUpdate(this) {
299 				@Override
300 				public void execute(RevWalk walk, ProgressMonitor monitor)
301 						throws IOException {
302 					if (performsAtomicTransactions() && isAtomic()) {
303 						try {
304 							lock.writeLock().lock();
305 							batch(getCommands());
306 						} finally {
307 							lock.writeLock().unlock();
308 						}
309 					} else {
310 						super.execute(walk, monitor);
311 					}
312 				}
313 			};
314 		}
315 
316 		@Override
317 		protected RefCache scanAllRefs() throws IOException {
318 			RefList.Builder<Ref> ids = new RefList.Builder<>();
319 			RefList.Builder<Ref> sym = new RefList.Builder<>();
320 			try {
321 				lock.readLock().lock();
322 				for (Ref ref : refs.values()) {
323 					if (ref.isSymbolic())
324 						sym.add(ref);
325 					ids.add(ref);
326 				}
327 			} finally {
328 				lock.readLock().unlock();
329 			}
330 			ids.sort();
331 			sym.sort();
332 			objdb.getCurrentPackList().markDirty();
333 			return new RefCache(ids.toRefList(), sym.toRefList());
334 		}
335 
336 		private void batch(List<ReceiveCommand> cmds) {
337 			// Validate that the target exists in a new RevWalk, as the RevWalk
338 			// from the RefUpdate might be reading back unflushed objects.
339 			Map<ObjectId, ObjectId> peeled = new HashMap<>();
340 			try (RevWalk rw = new RevWalk(getRepository())) {
341 				for (ReceiveCommand c : cmds) {
342 					if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
343 						ReceiveCommand.abort(cmds);
344 						return;
345 					}
346 
347 					if (!ObjectId.zeroId().equals(c.getNewId())) {
348 						try {
349 							RevObject o = rw.parseAny(c.getNewId());
350 							if (o instanceof RevTag) {
351 								peeled.put(o, rw.peel(o).copy());
352 							}
353 						} catch (IOException e) {
354 							c.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT);
355 							ReceiveCommand.abort(cmds);
356 							return;
357 						}
358 					}
359 				}
360 			}
361 
362 			// Check all references conform to expected old value.
363 			for (ReceiveCommand c : cmds) {
364 				Ref r = refs.get(c.getRefName());
365 				if (r == null) {
366 					if (c.getType() != ReceiveCommand.Type.CREATE) {
367 						c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
368 						ReceiveCommand.abort(cmds);
369 						return;
370 					}
371 				} else {
372 					ObjectId objectId = r.getObjectId();
373 					if (r.isSymbolic() || objectId == null
374 							|| !objectId.equals(c.getOldId())) {
375 						c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
376 						ReceiveCommand.abort(cmds);
377 						return;
378 					}
379 				}
380 			}
381 
382 			// Write references.
383 			for (ReceiveCommand c : cmds) {
384 				if (c.getType() == ReceiveCommand.Type.DELETE) {
385 					refs.remove(c.getRefName());
386 					c.setResult(ReceiveCommand.Result.OK);
387 					continue;
388 				}
389 
390 				ObjectId p = peeled.get(c.getNewId());
391 				Ref r;
392 				if (p != null) {
393 					r = new ObjectIdRef.PeeledTag(Storage.PACKED,
394 							c.getRefName(), c.getNewId(), p);
395 				} else {
396 					r = new ObjectIdRef.PeeledNonTag(Storage.PACKED,
397 							c.getRefName(), c.getNewId());
398 				}
399 				refs.put(r.getName(), r);
400 				c.setResult(ReceiveCommand.Result.OK);
401 			}
402 			clearCache();
403 		}
404 
405 		@Override
406 		protected boolean compareAndPut(Ref oldRef, Ref newRef)
407 				throws IOException {
408 			try {
409 				lock.writeLock().lock();
410 				ObjectId id = newRef.getObjectId();
411 				if (id != null) {
412 					try (RevWalk rw = new RevWalk(getRepository())) {
413 						// Validate that the target exists in a new RevWalk, as the RevWalk
414 						// from the RefUpdate might be reading back unflushed objects.
415 						rw.parseAny(id);
416 					}
417 				}
418 				String name = newRef.getName();
419 				if (oldRef == null)
420 					return refs.putIfAbsent(name, newRef) == null;
421 
422 				Ref cur = refs.get(name);
423 				if (cur != null) {
424 					if (eq(cur, oldRef))
425 						return refs.replace(name, cur, newRef);
426 				}
427 
428 				if (oldRef.getStorage() == Storage.NEW)
429 					return refs.putIfAbsent(name, newRef) == null;
430 
431 				return false;
432 			} finally {
433 				lock.writeLock().unlock();
434 			}
435 		}
436 
437 		@Override
438 		protected boolean compareAndRemove(Ref oldRef) throws IOException {
439 			try {
440 				lock.writeLock().lock();
441 				String name = oldRef.getName();
442 				Ref cur = refs.get(name);
443 				if (cur != null && eq(cur, oldRef))
444 					return refs.remove(name, cur);
445 				else
446 					return false;
447 			} finally {
448 				lock.writeLock().unlock();
449 			}
450 		}
451 
452 		private boolean eq(Ref a, Ref b) {
453 			if (!Objects.equals(a.getName(), b.getName()))
454 				return false;
455 			if (a.isSymbolic() != b.isSymbolic())
456 				return false;
457 			if (a.isSymbolic())
458 				return Objects.equals(a.getTarget().getName(), b.getTarget().getName());
459 			else
460 				return Objects.equals(a.getObjectId(), b.getObjectId());
461 		}
462 	}
463 }