View Javadoc
1   /*
2    * Copyright (C) 2019 Google LLC and others
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  
11  package org.eclipse.jgit.internal.storage.file;
12  
13  import static org.eclipse.jgit.lib.Ref.UNDEFINED_UPDATE_INDEX;
14  import static org.eclipse.jgit.lib.Ref.Storage.NEW;
15  import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
16  
17  import java.io.File;
18  import java.io.IOException;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.HashSet;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.TreeSet;
26  import java.util.concurrent.locks.Lock;
27  import java.util.concurrent.locks.ReentrantLock;
28  import java.util.stream.Collectors;
29  
30  import org.eclipse.jgit.annotations.NonNull;
31  import org.eclipse.jgit.errors.MissingObjectException;
32  import org.eclipse.jgit.events.RefsChangedEvent;
33  import org.eclipse.jgit.internal.storage.reftable.MergedReftable;
34  import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate;
35  import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase;
36  import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
37  import org.eclipse.jgit.lib.BatchRefUpdate;
38  import org.eclipse.jgit.lib.Constants;
39  import org.eclipse.jgit.lib.ObjectId;
40  import org.eclipse.jgit.lib.ObjectIdRef;
41  import org.eclipse.jgit.lib.PersonIdent;
42  import org.eclipse.jgit.lib.Ref;
43  import org.eclipse.jgit.lib.RefDatabase;
44  import org.eclipse.jgit.lib.RefRename;
45  import org.eclipse.jgit.lib.RefUpdate;
46  import org.eclipse.jgit.lib.ReflogEntry;
47  import org.eclipse.jgit.lib.ReflogReader;
48  import org.eclipse.jgit.lib.Repository;
49  import org.eclipse.jgit.lib.SymbolicRef;
50  import org.eclipse.jgit.revwalk.RevObject;
51  import org.eclipse.jgit.revwalk.RevTag;
52  import org.eclipse.jgit.revwalk.RevWalk;
53  import org.eclipse.jgit.transport.ReceiveCommand;
54  import org.eclipse.jgit.util.FileUtils;
55  import org.eclipse.jgit.util.RefList;
56  import org.eclipse.jgit.util.RefMap;
57  
58  /**
59   * Implements RefDatabase using reftable for storage.
60   *
61   * This class is threadsafe.
62   */
63  public class FileReftableDatabase extends RefDatabase {
64  	private final ReftableDatabase reftableDatabase;
65  
66  	private final FileRepository fileRepository;
67  
68  	private final FileReftableStack reftableStack;
69  
70  	FileReftableDatabase(FileRepository repo) throws IOException {
71  		this(repo, new File(new File(repo.getDirectory(), Constants.REFTABLE),
72  				Constants.TABLES_LIST));
73  	}
74  
75  	FileReftableDatabase(FileRepository repo, File refstackName) throws IOException {
76  		this.fileRepository = repo;
77  		this.reftableStack = new FileReftableStack(refstackName,
78  			new File(fileRepository.getDirectory(), Constants.REFTABLE),
79  			() -> fileRepository.fireEvent(new RefsChangedEvent()),
80  			() -> fileRepository.getConfig());
81  		this.reftableDatabase = new ReftableDatabase() {
82  
83  			@Override
84  			public MergedReftable openMergedReftable() throws IOException {
85  				return reftableStack.getMergedReftable();
86  			}
87  		};
88  	}
89  
90  	ReflogReader getReflogReader(String refname) throws IOException {
91  		return reftableDatabase.getReflogReader(refname);
92  	}
93  
94  	/**
95  	 * @param repoDir
96  	 * @return whether the given repo uses reftable for refdb storage.
97  	 */
98  	public static boolean isReftable(File repoDir) {
99  		return new File(repoDir, Constants.REFTABLE).isDirectory();
100 	}
101 
102 	/** {@inheritDoc} */
103 	@Override
104 	public boolean hasFastTipsWithSha1() throws IOException {
105 		return reftableDatabase.hasFastTipsWithSha1();
106 	}
107 
108 	/**
109 	 * Runs a full compaction for GC purposes.
110 	 * @throws IOException on I/O errors
111 	 */
112 	public void compactFully() throws IOException {
113 		Lock l = reftableDatabase.getLock();
114 		l.lock();
115 		try {
116 			reftableStack.compactFully();
117 			reftableDatabase.clearCache();
118 		} finally {
119 			l.unlock();
120 		}
121 	}
122 
123 	private ReentrantLock getLock() {
124 		return reftableDatabase.getLock();
125 	}
126 
127 	/** {@inheritDoc} */
128 	@Override
129 	public boolean performsAtomicTransactions() {
130 		return true;
131 	}
132 
133 	/** {@inheritDoc} */
134 	@NonNull
135 	@Override
136 	public BatchRefUpdate newBatchUpdate() {
137 		return new FileReftableBatchRefUpdate(this, fileRepository);
138 	}
139 
140 	/** {@inheritDoc} */
141 	@Override
142 	public RefUpdate newUpdate(String refName, boolean detach)
143 			throws IOException {
144 		boolean detachingSymbolicRef = false;
145 		Ref ref = exactRef(refName);
146 
147 		if (ref == null) {
148 			ref = new ObjectIdRef.Unpeeled(NEW, refName, null);
149 		} else {
150 			detachingSymbolicRef = detach && ref.isSymbolic();
151 		}
152 
153 		RefUpdate update = new FileReftableRefUpdate(ref);
154 		if (detachingSymbolicRef) {
155 			update.setDetachingSymbolicRef();
156 		}
157 		return update;
158 	}
159 
160 	/** {@inheritDoc} */
161 	@Override
162 	public Ref exactRef(String name) throws IOException {
163 		return reftableDatabase.exactRef(name);
164 	}
165 
166 	/** {@inheritDoc} */
167 	@Override
168 	public List<Ref> getRefs() throws IOException {
169 		return super.getRefs();
170 	}
171 
172 	/** {@inheritDoc} */
173 	@Override
174 	public Map<String, Ref> getRefs(String prefix) throws IOException {
175 		List<Ref> refs = reftableDatabase.getRefsByPrefix(prefix);
176 		RefList.Builder<Ref> builder = new RefList.Builder<>(refs.size());
177 		for (Ref r : refs) {
178 			builder.add(r);
179 		}
180 		return new RefMap(prefix, builder.toRefList(), RefList.emptyList(),
181 				RefList.emptyList());
182 	}
183 
184 	/** {@inheritDoc} */
185 	@Override
186 	public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
187 			throws IOException {
188 		return reftableDatabase.getRefsByPrefixWithExclusions(include, excludes);
189 	}
190 
191 	/** {@inheritDoc} */
192 	@Override
193 	public List<Ref> getAdditionalRefs() throws IOException {
194 		return Collections.emptyList();
195 	}
196 
197 	/** {@inheritDoc} */
198 	@Override
199 	public Ref peel(Ref ref) throws IOException {
200 		Ref oldLeaf = ref.getLeaf();
201 		if (oldLeaf.isPeeled() || oldLeaf.getObjectId() == null) {
202 			return ref;
203 		}
204 		return recreate(ref, doPeel(oldLeaf), hasVersioning());
205 
206 	}
207 
208 	private Ref doPeel(Ref leaf) throws IOException {
209 		try (RevWalk rw = new RevWalk(fileRepository)) {
210 			RevObject obj = rw.parseAny(leaf.getObjectId());
211 			if (obj instanceof RevTag) {
212 				return new ObjectIdRef.PeeledTag(leaf.getStorage(),
213 						leaf.getName(), leaf.getObjectId(), rw.peel(obj).copy(),
214 						hasVersioning() ? leaf.getUpdateIndex()
215 								: UNDEFINED_UPDATE_INDEX);
216 			}
217 			return new ObjectIdRef.PeeledNonTag(leaf.getStorage(),
218 					leaf.getName(), leaf.getObjectId(),
219 					hasVersioning() ? leaf.getUpdateIndex()
220 							: UNDEFINED_UPDATE_INDEX);
221 
222 		}
223 	}
224 
225 	private static Ref recreate(Ref old, Ref leaf, boolean hasVersioning) {
226 		if (old.isSymbolic()) {
227 			Ref dst = recreate(old.getTarget(), leaf, hasVersioning);
228 			return new SymbolicRef(old.getName(), dst,
229 					hasVersioning ? old.getUpdateIndex()
230 							: UNDEFINED_UPDATE_INDEX);
231 		}
232 		return leaf;
233 	}
234 
235 	private class FileRefRename extends RefRename {
236 		FileRefRename(RefUpdate src, RefUpdate dst) {
237 			super(src, dst);
238 		}
239 
240 		void writeRename(ReftableWriter w) throws IOException {
241 			long idx = reftableDatabase.nextUpdateIndex();
242 			w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin();
243 			List<Ref> refs = new ArrayList<>(3);
244 
245 			Ref dest = destination.getRef();
246 			Ref head = exactRef(Constants.HEAD);
247 			if (head != null && head.isSymbolic()
248 					&& head.getLeaf().getName().equals(source.getName())) {
249 				head = new SymbolicRef(Constants.HEAD, dest, idx);
250 				refs.add(head);
251 			}
252 
253 			ObjectId objId = source.getRef().getObjectId();
254 
255 			// XXX should we check if the source is a Tag vs. NonTag?
256 			refs.add(new ObjectIdRef.PeeledNonTag(Ref.Storage.NEW,
257 					destination.getName(), objId));
258 			refs.add(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, source.getName(),
259 					null));
260 
261 			w.sortAndWriteRefs(refs);
262 			PersonIdent who = destination.getRefLogIdent();
263 			if (who == null) {
264 				who = new PersonIdent(fileRepository);
265 			}
266 
267 			if (!destination.getRefLogMessage().isEmpty()) {
268 				List<String> refnames = refs.stream().map(r -> r.getName())
269 						.collect(Collectors.toList());
270 				Collections.sort(refnames);
271 				for (String s : refnames) {
272 					ObjectId old = (Constants.HEAD.equals(s)
273 							|| s.equals(source.getName())) ? objId
274 									: ObjectId.zeroId();
275 					ObjectId newId = (Constants.HEAD.equals(s)
276 							|| s.equals(destination.getName())) ? objId
277 									: ObjectId.zeroId();
278 
279 					w.writeLog(s, idx, who, old, newId,
280 							destination.getRefLogMessage());
281 				}
282 			}
283 		}
284 
285 		@Override
286 		protected RefUpdate.Result doRename() throws IOException {
287 			Ref src = exactRef(source.getName());
288 			if (exactRef(destination.getName()) != null || src == null
289 					|| !source.getOldObjectId().equals(src.getObjectId())) {
290 				return RefUpdate.Result.LOCK_FAILURE;
291 			}
292 
293 			if (src.isSymbolic()) {
294 				// We could support this, but this is easier and compatible.
295 				return RefUpdate.Result.IO_FAILURE;
296 			}
297 
298 			if (!addReftable(this::writeRename)) {
299 				return RefUpdate.Result.LOCK_FAILURE;
300 			}
301 
302 			return RefUpdate.Result.RENAMED;
303 		}
304 	}
305 
306 	/** {@inheritDoc} */
307 	@Override
308 	public RefRename newRename(String fromName, String toName)
309 			throws IOException {
310 		RefUpdate src = newUpdate(fromName, true);
311 		RefUpdate dst = newUpdate(toName, true);
312 		return new FileRefRename(src, dst);
313 	}
314 
315 	/** {@inheritDoc} */
316 	@Override
317 	public boolean isNameConflicting(String name) throws IOException {
318 		return reftableDatabase.isNameConflicting(name, new TreeSet<>(),
319 				new HashSet<>());
320 	}
321 
322 	/** {@inheritDoc} */
323 	@Override
324 	public void close() {
325 		reftableStack.close();
326 	}
327 
328 	/** {@inheritDoc} */
329 	@Override
330 	public void create() throws IOException {
331 		FileUtils.mkdir(
332 				new File(fileRepository.getDirectory(), Constants.REFTABLE),
333 				true);
334 	}
335 
336 	private boolean addReftable(FileReftableStack.Writer w) throws IOException {
337 		if (!reftableStack.addReftable(w)) {
338 			reftableStack.reload();
339 			reftableDatabase.clearCache();
340 			return false;
341 		}
342 		reftableDatabase.clearCache();
343 
344 		return true;
345 	}
346 
347 	private class FileReftableBatchRefUpdate extends ReftableBatchRefUpdate {
348 		FileReftableBatchRefUpdate(FileReftableDatabase db,
349 				Repository repository) {
350 			super(db, db.reftableDatabase, db.getLock(), repository);
351 		}
352 
353 		@Override
354 		protected void applyUpdates(List<Ref> newRefs,
355 				List<ReceiveCommand> pending) throws IOException {
356 			if (!addReftable(rw -> write(rw, newRefs, pending))) {
357 				for (ReceiveCommand c : pending) {
358 					if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
359 						c.setResult(RefUpdate.Result.LOCK_FAILURE);
360 					}
361 				}
362 			}
363 		}
364 	}
365 
366 	private class FileReftableRefUpdate extends RefUpdate {
367 		FileReftableRefUpdate(Ref ref) {
368 			super(ref);
369 		}
370 
371 		@Override
372 		protected RefDatabase getRefDatabase() {
373 			return FileReftableDatabase.this;
374 		}
375 
376 		@Override
377 		protected Repository getRepository() {
378 			return FileReftableDatabase.this.fileRepository;
379 		}
380 
381 		@Override
382 		protected void unlock() {
383 			// nop.
384 		}
385 
386 		private RevWalk rw;
387 
388 		private Ref dstRef;
389 
390 		@Override
391 		public Result update(RevWalk walk) throws IOException {
392 			try {
393 				rw = walk;
394 				return super.update(walk);
395 			} finally {
396 				rw = null;
397 			}
398 		}
399 
400 		@Override
401 		protected boolean tryLock(boolean deref) throws IOException {
402 			dstRef = getRef();
403 			if (deref) {
404 				dstRef = dstRef.getLeaf();
405 			}
406 
407 			Ref derefed = exactRef(dstRef.getName());
408 			if (derefed != null) {
409 				setOldObjectId(derefed.getObjectId());
410 			}
411 
412 			return true;
413 		}
414 
415 		void writeUpdate(ReftableWriter w) throws IOException {
416 			Ref newRef = null;
417 			if (rw != null && !ObjectId.zeroId().equals(getNewObjectId())) {
418 				RevObject obj = rw.parseAny(getNewObjectId());
419 				if (obj instanceof RevTag) {
420 					newRef = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED,
421 							dstRef.getName(), getNewObjectId(),
422 							rw.peel(obj).copy());
423 				}
424 			}
425 			if (newRef == null) {
426 				newRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED,
427 						dstRef.getName(), getNewObjectId());
428 			}
429 
430 			long idx = reftableDatabase.nextUpdateIndex();
431 			w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
432 					.writeRef(newRef);
433 
434 			ObjectId oldId = getOldObjectId();
435 			if (oldId == null) {
436 				oldId = ObjectId.zeroId();
437 			}
438 			w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId,
439 					getNewObjectId(), getRefLogMessage());
440 		}
441 
442 		@Override
443 		public PersonIdent getRefLogIdent() {
444 			PersonIdent who = super.getRefLogIdent();
445 			if (who == null) {
446 				who = new PersonIdent(getRepository());
447 			}
448 			return who;
449 		}
450 
451 		void writeDelete(ReftableWriter w) throws IOException {
452 			Ref newRef = new ObjectIdRef.Unpeeled(Ref.Storage.NEW,
453 					dstRef.getName(), null);
454 			long idx = reftableDatabase.nextUpdateIndex();
455 			w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
456 					.writeRef(newRef);
457 
458 			ObjectId oldId = ObjectId.zeroId();
459 			Ref old = exactRef(dstRef.getName());
460 			if (old != null) {
461 				old = old.getLeaf();
462 				if (old.getObjectId() != null) {
463 					oldId = old.getObjectId();
464 				}
465 			}
466 
467 			w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId,
468 					ObjectId.zeroId(), getRefLogMessage());
469 		}
470 
471 		@Override
472 		protected Result doUpdate(Result desiredResult) throws IOException {
473 			if (isRefLogIncludingResult()) {
474 				setRefLogMessage(
475 						getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$
476 						false);
477 			}
478 
479 			if (!addReftable(this::writeUpdate)) {
480 				return Result.LOCK_FAILURE;
481 			}
482 
483 			return desiredResult;
484 		}
485 
486 		@Override
487 		protected Result doDelete(Result desiredResult) throws IOException {
488 
489 			if (isRefLogIncludingResult()) {
490 				setRefLogMessage(
491 						getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$
492 						false);
493 			}
494 
495 			if (!addReftable(this::writeDelete)) {
496 				return Result.LOCK_FAILURE;
497 			}
498 
499 			return desiredResult;
500 		}
501 
502 		void writeLink(ReftableWriter w) throws IOException {
503 			long idx = reftableDatabase.nextUpdateIndex();
504 			w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
505 					.writeRef(dstRef);
506 
507 			ObjectId beforeId = ObjectId.zeroId();
508 			Ref before = exactRef(dstRef.getName());
509 			if (before != null) {
510 				before = before.getLeaf();
511 				if (before.getObjectId() != null) {
512 					beforeId = before.getObjectId();
513 				}
514 			}
515 
516 			Ref after = dstRef.getLeaf();
517 			ObjectId afterId = ObjectId.zeroId();
518 			if (after.getObjectId() != null) {
519 				afterId = after.getObjectId();
520 			}
521 
522 			w.writeLog(dstRef.getName(), idx, getRefLogIdent(), beforeId,
523 					afterId, getRefLogMessage());
524 		}
525 
526 		@Override
527 		protected Result doLink(String target) throws IOException {
528 			if (isRefLogIncludingResult()) {
529 				setRefLogMessage(
530 						getRefLogMessage() + ": " + Result.FORCED.toString(), //$NON-NLS-1$
531 						false);
532 			}
533 
534 			boolean exists = exactRef(getName()) != null;
535 			dstRef = new SymbolicRef(getName(),
536 					new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null),
537 					reftableDatabase.nextUpdateIndex());
538 
539 			if (!addReftable(this::writeLink)) {
540 				return Result.LOCK_FAILURE;
541 			}
542 			// XXX unclear if we should support FORCED here. Baseclass says
543 			// NEW is OK ?
544 			return exists ? Result.FORCED : Result.NEW;
545 		}
546 	}
547 
548 	private static void writeConvertTable(Repository repo, ReftableWriter w,
549 			boolean writeLogs) throws IOException {
550 		int size = 0;
551 		List<Ref> refs = repo.getRefDatabase().getRefs();
552 		if (writeLogs) {
553 			for (Ref r : refs) {
554 				ReflogReader rlr = repo.getReflogReader(r.getName());
555 				if (rlr != null) {
556 					size = Math.max(rlr.getReverseEntries().size(), size);
557 				}
558 			}
559 		}
560 		// We must use 1 here, nextUpdateIndex() on the empty stack is 1.
561 		w.setMinUpdateIndex(1).setMaxUpdateIndex(size + 1).begin();
562 
563 		// The spec says to write the logs in the first table, and put refs in a
564 		// separate table, but this complicates the compaction (when we can we drop
565 		// deletions? Can we compact the .log table and the .ref table together?)
566 		try (RevWalk rw = new RevWalk(repo)) {
567 			List<Ref> toWrite = new ArrayList<>(refs.size());
568 			for (Ref r : refs) {
569 				toWrite.add(refForWrite(rw, r));
570 			}
571 			w.sortAndWriteRefs(toWrite);
572 		}
573 
574 		if (writeLogs) {
575 			for (Ref r : refs) {
576 				long idx = size;
577 				ReflogReader reader = repo.getReflogReader(r.getName());
578 				if (reader == null) {
579 					continue;
580 				}
581 				for (ReflogEntry e : reader.getReverseEntries()) {
582 					w.writeLog(r.getName(), idx, e.getWho(), e.getOldId(),
583 							e.getNewId(), e.getComment());
584 					idx--;
585 				}
586 			}
587 		}
588 	}
589 
590 	private static Ref refForWrite(RevWalk rw, Ref r) throws IOException {
591 		if (r.isSymbolic()) {
592 			return new SymbolicRef(r.getName(), new ObjectIdRef.Unpeeled(NEW,
593 					r.getTarget().getName(), null));
594 		}
595 		ObjectId newId = r.getObjectId();
596 		RevObject peel = null;
597 		try {
598 			RevObject obj = rw.parseAny(newId);
599 			if (obj instanceof RevTag) {
600 				peel = rw.peel(obj);
601 			}
602 		} catch (MissingObjectException e) {
603 			/* ignore this error and copy the dangling object ID into reftable too. */
604 		}
605 		if (peel != null) {
606 				return new ObjectIdRef.PeeledTag(PACKED, r.getName(), newId,
607 						peel.copy());
608 			}
609 
610 		return new ObjectIdRef.PeeledNonTag(PACKED, r.getName(), newId);
611 	}
612 
613 	/**
614 	 * @param repo
615 	 *            the repository
616 	 * @param writeLogs
617 	 *            whether to write reflogs
618 	 * @return a reftable based RefDB from an existing repository.
619 	 * @throws IOException
620 	 *             on IO error
621 	 */
622 	public static FileReftableDatabase convertFrom(FileRepository repo,
623 			boolean writeLogs) throws IOException {
624 		FileReftableDatabase newDb = null;
625 		File reftableList = null;
626 		try {
627 			File reftableDir = new File(repo.getDirectory(),
628 					Constants.REFTABLE);
629 			reftableList = new File(reftableDir, Constants.TABLES_LIST);
630 			if (!reftableDir.isDirectory()) {
631 				reftableDir.mkdir();
632 			}
633 
634 			try (FileReftableStack stack = new FileReftableStack(reftableList,
635 					reftableDir, null, () -> repo.getConfig())) {
636 				stack.addReftable(rw -> writeConvertTable(repo, rw, writeLogs));
637 			}
638 			reftableList = null;
639 		} finally {
640 			if (reftableList != null) {
641 				reftableList.delete();
642 			}
643 		}
644 		return newDb;
645 	}
646 }