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