View Javadoc
1   /*
2    * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3    * Copyright (C) 2008-2010, Google Inc.
4    * Copyright (C) 2006-2010, Robin Rosenberg <robin.rosenberg@dewire.com>
5    * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> and others
6    *
7    * This program and the accompanying materials are made available under the
8    * terms of the Eclipse Distribution License v. 1.0 which is available at
9    * https://www.eclipse.org/org/documents/edl-v10.php.
10   *
11   * SPDX-License-Identifier: BSD-3-Clause
12   */
13  
14  package org.eclipse.jgit.internal.storage.file;
15  
16  import static java.util.stream.Collectors.toList;
17  
18  import java.io.File;
19  import java.io.FileInputStream;
20  import java.io.FileNotFoundException;
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.text.MessageFormat;
25  import java.text.ParseException;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Objects;
32  import java.util.Set;
33  
34  import org.eclipse.jgit.annotations.Nullable;
35  import org.eclipse.jgit.api.errors.JGitInternalException;
36  import org.eclipse.jgit.attributes.AttributesNode;
37  import org.eclipse.jgit.attributes.AttributesNodeProvider;
38  import org.eclipse.jgit.errors.ConfigInvalidException;
39  import org.eclipse.jgit.events.IndexChangedEvent;
40  import org.eclipse.jgit.internal.JGitText;
41  import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateHandle;
42  import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateRepository;
43  import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
44  import org.eclipse.jgit.lib.BaseRepositoryBuilder;
45  import org.eclipse.jgit.lib.BatchRefUpdate;
46  import org.eclipse.jgit.lib.ConfigConstants;
47  import org.eclipse.jgit.lib.Constants;
48  import org.eclipse.jgit.lib.CoreConfig.HideDotFiles;
49  import org.eclipse.jgit.lib.CoreConfig.SymLinks;
50  import org.eclipse.jgit.lib.NullProgressMonitor;
51  import org.eclipse.jgit.lib.ObjectId;
52  import org.eclipse.jgit.lib.ProgressMonitor;
53  import org.eclipse.jgit.lib.Ref;
54  import org.eclipse.jgit.lib.RefDatabase;
55  import org.eclipse.jgit.lib.RefUpdate;
56  import org.eclipse.jgit.lib.ReflogEntry;
57  import org.eclipse.jgit.lib.ReflogReader;
58  import org.eclipse.jgit.lib.Repository;
59  import org.eclipse.jgit.lib.StoredConfig;
60  import org.eclipse.jgit.revwalk.RevWalk;
61  import org.eclipse.jgit.storage.file.FileBasedConfig;
62  import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
63  import org.eclipse.jgit.storage.pack.PackConfig;
64  import org.eclipse.jgit.transport.ReceiveCommand;
65  import org.eclipse.jgit.util.FileUtils;
66  import org.eclipse.jgit.util.IO;
67  import org.eclipse.jgit.util.RawParseUtils;
68  import org.eclipse.jgit.util.StringUtils;
69  import org.eclipse.jgit.util.SystemReader;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  /**
74   * Represents a Git repository. A repository holds all objects and refs used for
75   * managing source code (could by any type of file, but source code is what
76   * SCM's are typically used for).
77   *
78   * In Git terms all data is stored in GIT_DIR, typically a directory called
79   * .git. A work tree is maintained unless the repository is a bare repository.
80   * Typically the .git directory is located at the root of the work dir.
81   *
82   * <ul>
83   * <li>GIT_DIR
84   * 	<ul>
85   * 		<li>objects/ - objects</li>
86   * 		<li>refs/ - tags and heads</li>
87   * 		<li>config - configuration</li>
88   * 		<li>info/ - more configurations</li>
89   * 	</ul>
90   * </li>
91   * </ul>
92   * <p>
93   * This class is thread-safe.
94   * <p>
95   * This implementation only handles a subtly undocumented subset of git features.
96   */
97  public class FileRepository extends Repository {
98  	private static final Logger LOG = LoggerFactory
99  			.getLogger(FileRepository.class);
100 	private static final String UNNAMED = "Unnamed repository; edit this file to name it for gitweb."; //$NON-NLS-1$
101 
102 	private final FileBasedConfig repoConfig;
103 	private RefDatabase refs;
104 	private final ObjectDirectory objectDatabase;
105 
106 	private final Object snapshotLock = new Object();
107 
108 	// protected by snapshotLock
109 	private FileSnapshot snapshot;
110 
111 	/**
112 	 * Construct a representation of a Git repository.
113 	 * <p>
114 	 * The work tree, object directory, alternate object directories and index
115 	 * file locations are deduced from the given git directory and the default
116 	 * rules by running
117 	 * {@link org.eclipse.jgit.storage.file.FileRepositoryBuilder}. This
118 	 * constructor is the same as saying:
119 	 *
120 	 * <pre>
121 	 * new FileRepositoryBuilder().setGitDir(gitDir).build()
122 	 * </pre>
123 	 *
124 	 * @param gitDir
125 	 *            GIT_DIR (the location of the repository metadata).
126 	 * @throws java.io.IOException
127 	 *             the repository appears to already exist but cannot be
128 	 *             accessed.
129 	 * @see FileRepositoryBuilder
130 	 */
131 	public FileRepository(File gitDir) throws IOException {
132 		this(new FileRepositoryBuilder().setGitDir(gitDir).setup());
133 	}
134 
135 	/**
136 	 * A convenience API for {@link #FileRepository(File)}.
137 	 *
138 	 * @param gitDir
139 	 *            GIT_DIR (the location of the repository metadata).
140 	 * @throws java.io.IOException
141 	 *             the repository appears to already exist but cannot be
142 	 *             accessed.
143 	 * @see FileRepositoryBuilder
144 	 */
145 	public FileRepository(String gitDir) throws IOException {
146 		this(new File(gitDir));
147 	}
148 
149 	/**
150 	 * Create a repository using the local file system.
151 	 *
152 	 * @param options
153 	 *            description of the repository's important paths.
154 	 * @throws java.io.IOException
155 	 *             the user configuration file or repository configuration file
156 	 *             cannot be accessed.
157 	 */
158 	public FileRepository(BaseRepositoryBuilder options) throws IOException {
159 		super(options);
160 		StoredConfig userConfig = null;
161 		try {
162 			userConfig = SystemReader.getInstance().getUserConfig();
163 		} catch (ConfigInvalidException e) {
164 			LOG.error(e.getMessage(), e);
165 			throw new IOException(e.getMessage(), e);
166 		}
167 		repoConfig = new FileBasedConfig(userConfig, getFS().resolve(
168 				getDirectory(), Constants.CONFIG),
169 				getFS());
170 		loadRepoConfig();
171 
172 		repoConfig.addChangeListener(this::fireEvent);
173 
174 		final long repositoryFormatVersion = getConfig().getLong(
175 				ConfigConstants.CONFIG_CORE_SECTION, null,
176 				ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
177 
178 		String reftype = repoConfig.getString(
179 				ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
180 				ConfigConstants.CONFIG_KEY_REF_STORAGE);
181 		if (repositoryFormatVersion >= 1 && reftype != null) {
182 			if (StringUtils.equalsIgnoreCase(reftype,
183 					ConfigConstants.CONFIG_REF_STORAGE_REFTABLE)) {
184 				refs = new FileReftableDatabase(this);
185 			} else if (StringUtils.equalsIgnoreCase(reftype,
186 					ConfigConstants.CONFIG_REFSTORAGE_REFTREE)) {
187 				refs = new RefTreeDatabase(this, new RefDirectory(this));
188 			} else {
189 				throw new IOException(JGitText.get().unknownRepositoryFormat);
190 			}
191 		} else {
192 			refs = new RefDirectory(this);
193 		}
194 
195 		objectDatabase = new ObjectDirectory(repoConfig, //
196 				options.getObjectDirectory(), //
197 				options.getAlternateObjectDirectories(), //
198 				getFS(), //
199 				new File(getDirectory(), Constants.SHALLOW));
200 
201 		if (objectDatabase.exists()) {
202 			if (repositoryFormatVersion > 1)
203 				throw new IOException(MessageFormat.format(
204 						JGitText.get().unknownRepositoryFormat2,
205 						Long.valueOf(repositoryFormatVersion)));
206 		}
207 
208 		if (!isBare()) {
209 			snapshot = FileSnapshot.save(getIndexFile());
210 		}
211 	}
212 
213 	private void loadRepoConfig() throws IOException {
214 		try {
215 			repoConfig.load();
216 		} catch (ConfigInvalidException e) {
217 			throw new IOException(JGitText.get().unknownRepositoryFormat, e);
218 		}
219 	}
220 
221 	/**
222 	 * {@inheritDoc}
223 	 * <p>
224 	 * Create a new Git repository initializing the necessary files and
225 	 * directories.
226 	 */
227 	@Override
228 	public void create(boolean bare) throws IOException {
229 		final FileBasedConfig cfg = getConfig();
230 		if (cfg.getFile().exists()) {
231 			throw new IllegalStateException(MessageFormat.format(
232 					JGitText.get().repositoryAlreadyExists, getDirectory()));
233 		}
234 		FileUtils.mkdirs(getDirectory(), true);
235 		HideDotFiles hideDotFiles = getConfig().getEnum(
236 				ConfigConstants.CONFIG_CORE_SECTION, null,
237 				ConfigConstants.CONFIG_KEY_HIDEDOTFILES,
238 				HideDotFiles.DOTGITONLY);
239 		if (hideDotFiles != HideDotFiles.FALSE && !isBare()
240 				&& getDirectory().getName().startsWith(".")) //$NON-NLS-1$
241 			getFS().setHidden(getDirectory(), true);
242 		refs.create();
243 		objectDatabase.create();
244 
245 		FileUtils.mkdir(new File(getDirectory(), "branches")); //$NON-NLS-1$
246 		FileUtils.mkdir(new File(getDirectory(), "hooks")); //$NON-NLS-1$
247 
248 		RefUpdate head = updateRef(Constants.HEAD);
249 		head.disableRefLog();
250 		head.link(Constants.R_HEADS + Constants.MASTER);
251 
252 		final boolean fileMode;
253 		if (getFS().supportsExecute()) {
254 			File tmp = File.createTempFile("try", "execute", getDirectory()); //$NON-NLS-1$ //$NON-NLS-2$
255 
256 			getFS().setExecute(tmp, true);
257 			final boolean on = getFS().canExecute(tmp);
258 
259 			getFS().setExecute(tmp, false);
260 			final boolean off = getFS().canExecute(tmp);
261 			FileUtils.delete(tmp);
262 
263 			fileMode = on && !off;
264 		} else {
265 			fileMode = false;
266 		}
267 
268 		SymLinks symLinks = SymLinks.FALSE;
269 		if (getFS().supportsSymlinks()) {
270 			File tmp = new File(getDirectory(), "tmplink"); //$NON-NLS-1$
271 			try {
272 				getFS().createSymLink(tmp, "target"); //$NON-NLS-1$
273 				symLinks = null;
274 				FileUtils.delete(tmp);
275 			} catch (IOException e) {
276 				// Normally a java.nio.file.FileSystemException
277 			}
278 		}
279 		if (symLinks != null)
280 			cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null,
281 					ConfigConstants.CONFIG_KEY_SYMLINKS, symLinks.name()
282 							.toLowerCase(Locale.ROOT));
283 		cfg.setInt(ConfigConstants.CONFIG_CORE_SECTION, null,
284 				ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
285 		cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
286 				ConfigConstants.CONFIG_KEY_FILEMODE, fileMode);
287 		if (bare)
288 			cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
289 					ConfigConstants.CONFIG_KEY_BARE, true);
290 		cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
291 				ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, !bare);
292 		if (SystemReader.getInstance().isMacOS())
293 			// Java has no other way
294 			cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
295 					ConfigConstants.CONFIG_KEY_PRECOMPOSEUNICODE, true);
296 		if (!bare) {
297 			File workTree = getWorkTree();
298 			if (!getDirectory().getParentFile().equals(workTree)) {
299 				cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null,
300 						ConfigConstants.CONFIG_KEY_WORKTREE, getWorkTree()
301 								.getAbsolutePath());
302 				LockFile dotGitLockFile = new LockFile(new File(workTree,
303 						Constants.DOT_GIT));
304 				try {
305 					if (dotGitLockFile.lock()) {
306 						dotGitLockFile.write(Constants.encode(Constants.GITDIR
307 								+ getDirectory().getAbsolutePath()));
308 						dotGitLockFile.commit();
309 					}
310 				} finally {
311 					dotGitLockFile.unlock();
312 				}
313 			}
314 		}
315 		cfg.save();
316 	}
317 
318 	/**
319 	 * Get the directory containing the objects owned by this repository
320 	 *
321 	 * @return the directory containing the objects owned by this repository.
322 	 */
323 	public File getObjectsDirectory() {
324 		return objectDatabase.getDirectory();
325 	}
326 
327 	/** {@inheritDoc} */
328 	@Override
329 	public ObjectDirectory getObjectDatabase() {
330 		return objectDatabase;
331 	}
332 
333 	/** {@inheritDoc} */
334 	@Override
335 	public RefDatabase getRefDatabase() {
336 		return refs;
337 	}
338 
339 	/** {@inheritDoc} */
340 	@Override
341 	public String getIdentifier() {
342 		File directory = getDirectory();
343 		if (directory != null) {
344 			return directory.getPath();
345 		}
346 		throw new IllegalStateException();
347 	}
348 
349 	/** {@inheritDoc} */
350 	@Override
351 	public FileBasedConfig getConfig() {
352 		try {
353 			SystemReader.getInstance().getUserConfig();
354 			if (repoConfig.isOutdated()) {
355 				loadRepoConfig();
356 			}
357 		} catch (IOException | ConfigInvalidException e) {
358 			throw new RuntimeException(e);
359 		}
360 		return repoConfig;
361 	}
362 
363 	/** {@inheritDoc} */
364 	@Override
365 	@Nullable
366 	public String getGitwebDescription() throws IOException {
367 		String d;
368 		try {
369 			d = RawParseUtils.decode(IO.readFully(descriptionFile()));
370 		} catch (FileNotFoundException err) {
371 			return null;
372 		}
373 		if (d != null) {
374 			d = d.trim();
375 			if (d.isEmpty() || UNNAMED.equals(d)) {
376 				return null;
377 			}
378 		}
379 		return d;
380 	}
381 
382 	/** {@inheritDoc} */
383 	@Override
384 	public void setGitwebDescription(@Nullable String description)
385 			throws IOException {
386 		String old = getGitwebDescription();
387 		if (Objects.equals(old, description)) {
388 			return;
389 		}
390 
391 		File path = descriptionFile();
392 		LockFile lock = new LockFile(path);
393 		if (!lock.lock()) {
394 			throw new IOException(MessageFormat.format(JGitText.get().lockError,
395 					path.getAbsolutePath()));
396 		}
397 		try {
398 			String d = description;
399 			if (d != null) {
400 				d = d.trim();
401 				if (!d.isEmpty()) {
402 					d += '\n';
403 				}
404 			} else {
405 				d = ""; //$NON-NLS-1$
406 			}
407 			lock.write(Constants.encode(d));
408 			lock.commit();
409 		} finally {
410 			lock.unlock();
411 		}
412 	}
413 
414 	private File descriptionFile() {
415 		return new File(getDirectory(), "description"); //$NON-NLS-1$
416 	}
417 
418 	/**
419 	 * {@inheritDoc}
420 	 * <p>
421 	 * Objects known to exist but not expressed by {@code #getAllRefs()}.
422 	 * <p>
423 	 * When a repository borrows objects from another repository, it can
424 	 * advertise that it safely has that other repository's references, without
425 	 * exposing any other details about the other repository. This may help a
426 	 * client trying to push changes avoid pushing more than it needs to.
427 	 */
428 	@Override
429 	public Set<ObjectId> getAdditionalHaves() {
430 		return getAdditionalHaves(null);
431 	}
432 
433 	/**
434 	 * Objects known to exist but not expressed by {@code #getAllRefs()}.
435 	 * <p>
436 	 * When a repository borrows objects from another repository, it can
437 	 * advertise that it safely has that other repository's references, without
438 	 * exposing any other details about the other repository. This may help a
439 	 * client trying to push changes avoid pushing more than it needs to.
440 	 *
441 	 * @param skips
442 	 *            Set of AlternateHandle Ids already seen
443 	 *
444 	 * @return unmodifiable collection of other known objects.
445 	 */
446 	private Set<ObjectId> getAdditionalHaves(Set<AlternateHandle.Id> skips) {
447 		HashSet<ObjectId> r = new HashSet<>();
448 		skips = objectDatabase.addMe(skips);
449 		for (AlternateHandle d : objectDatabase.myAlternates()) {
450 			if (d instanceof AlternateRepository && !skips.contains(d.getId())) {
451 				FileRepository repo;
452 
453 				repo = ((AlternateRepository) d).repository;
454 				for (Ref ref : repo.getAllRefs().values()) {
455 					if (ref.getObjectId() != null)
456 						r.add(ref.getObjectId());
457 					if (ref.getPeeledObjectId() != null)
458 						r.add(ref.getPeeledObjectId());
459 				}
460 				r.addAll(repo.getAdditionalHaves(skips));
461 			}
462 		}
463 		return r;
464 	}
465 
466 	/**
467 	 * Add a single existing pack to the list of available pack files.
468 	 *
469 	 * @param pack
470 	 *            path of the pack file to open.
471 	 * @throws java.io.IOException
472 	 *             index file could not be opened, read, or is not recognized as
473 	 *             a Git pack file index.
474 	 */
475 	public void openPack(File pack) throws IOException {
476 		objectDatabase.openPack(pack);
477 	}
478 
479 	/** {@inheritDoc} */
480 	@Override
481 	public void scanForRepoChanges() throws IOException {
482 		getRefDatabase().getRefs(); // This will look for changes to refs
483 		detectIndexChanges();
484 	}
485 
486 	/** Detect index changes. */
487 	private void detectIndexChanges() {
488 		if (isBare()) {
489 			return;
490 		}
491 
492 		File indexFile = getIndexFile();
493 		synchronized (snapshotLock) {
494 			if (snapshot == null) {
495 				snapshot = FileSnapshot.save(indexFile);
496 				return;
497 			}
498 			if (!snapshot.isModified(indexFile)) {
499 				return;
500 			}
501 		}
502 		notifyIndexChanged(false);
503 	}
504 
505 	/** {@inheritDoc} */
506 	@Override
507 	public void notifyIndexChanged(boolean internal) {
508 		synchronized (snapshotLock) {
509 			snapshot = FileSnapshot.save(getIndexFile());
510 		}
511 		fireEvent(new IndexChangedEvent(internal));
512 	}
513 
514 	/** {@inheritDoc} */
515 	@Override
516 	public ReflogReader getReflogReader(String refName) throws IOException {
517 		if (refs instanceof FileReftableDatabase) {
518 			// Cannot use findRef: reftable stores log data for deleted or renamed
519 			// branches.
520 			return ((FileReftableDatabase)refs).getReflogReader(refName);
521 		}
522 
523 		// TODO: use exactRef here, which offers more predictable and therefore preferable
524 		// behavior.
525 		Ref ref = findRef(refName);
526 		if (ref == null) {
527 			return null;
528 		}
529 		return new ReflogReaderImpl(this, ref.getName());
530 	}
531 
532 	/** {@inheritDoc} */
533 	@Override
534 	public AttributesNodeProvider createAttributesNodeProvider() {
535 		return new AttributesNodeProviderImpl(this);
536 	}
537 
538 	/**
539 	 * Implementation a {@link AttributesNodeProvider} for a
540 	 * {@link FileRepository}.
541 	 *
542 	 * @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
543 	 *
544 	 */
545 	static class AttributesNodeProviderImpl implements
546 			AttributesNodeProvider {
547 
548 		private AttributesNode infoAttributesNode;
549 
550 		private AttributesNode globalAttributesNode;
551 
552 		/**
553 		 * Constructor.
554 		 *
555 		 * @param repo
556 		 *            {@link Repository} that will provide the attribute nodes.
557 		 */
558 		protected AttributesNodeProviderImpl(Repository repo) {
559 			infoAttributesNode = new InfoAttributesNode(repo);
560 			globalAttributesNode = new GlobalAttributesNode(repo);
561 		}
562 
563 		@Override
564 		public AttributesNode getInfoAttributesNode() throws IOException {
565 			if (infoAttributesNode instanceof InfoAttributesNode)
566 				infoAttributesNode = ((InfoAttributesNode) infoAttributesNode)
567 						.load();
568 			return infoAttributesNode;
569 		}
570 
571 		@Override
572 		public AttributesNode getGlobalAttributesNode() throws IOException {
573 			if (globalAttributesNode instanceof GlobalAttributesNode)
574 				globalAttributesNode = ((GlobalAttributesNode) globalAttributesNode)
575 						.load();
576 			return globalAttributesNode;
577 		}
578 
579 		static void loadRulesFromFile(AttributesNode r, File attrs)
580 				throws FileNotFoundException, IOException {
581 			if (attrs.exists()) {
582 				try (FileInputStream in = new FileInputStream(attrs)) {
583 					r.parse(in);
584 				}
585 			}
586 		}
587 
588 	}
589 
590 	private boolean shouldAutoDetach() {
591 		return getConfig().getBoolean(ConfigConstants.CONFIG_GC_SECTION,
592 				ConfigConstants.CONFIG_KEY_AUTODETACH, true);
593 	}
594 
595 	/** {@inheritDoc} */
596 	@Override
597 	public void autoGC(ProgressMonitor monitor) {
598 		GC gc = new GC(this);
599 		gc.setPackConfig(new PackConfig(this));
600 		gc.setProgressMonitor(monitor);
601 		gc.setAuto(true);
602 		gc.setBackground(shouldAutoDetach());
603 		try {
604 			gc.gc();
605 		} catch (ParseException | IOException e) {
606 			throw new JGitInternalException(JGitText.get().gcFailed, e);
607 		}
608 	}
609 
610 	/**
611 	 * Converts the RefDatabase from reftable to RefDirectory. This operation is
612 	 * not atomic.
613 	 *
614 	 * @param writeLogs
615 	 *            whether to write reflogs
616 	 * @param backup
617 	 *            whether to rename or delete the old storage files. If set to
618 	 *            {@code true}, the reftable list is left in {@code refs.old},
619 	 *            and the {@code reftable/} dir is left alone. If set to
620 	 *            {@code false}, the {@code reftable/} dir is removed, and
621 	 *            {@code refs} file is removed.
622 	 * @throws IOException
623 	 *             on IO problem
624 	 */
625 	void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException {
626 		List<Ref> all = refs.getRefs();
627 		File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
628 		if (packedRefs.exists()) {
629 			throw new IOException(MessageFormat.format(JGitText.get().fileAlreadyExists,
630 				packedRefs.getName()));
631 		}
632 
633 		File refsFile = new File(getDirectory(), "refs"); //$NON-NLS-1$
634 		File refsHeadsFile = new File(refsFile, "heads");//$NON-NLS-1$
635 		File headFile = new File(getDirectory(), Constants.HEAD);
636 		FileReftableDatabase oldDb = (FileReftableDatabase) refs;
637 
638 		// Remove the dummy files that ensure compatibility with older git
639 		// versions (see convertToReftable). First make room for refs/heads/
640 		refsHeadsFile.delete();
641 		// RefDirectory wants to create the refs/ directory from scratch, so
642 		// remove that too.
643 		refsFile.delete();
644 		// remove HEAD so its previous invalid value doesn't cause issues.
645 		headFile.delete();
646 
647 		// This is not atomic, but there is no way to instantiate a RefDirectory
648 		// that is disconnected from the current repo.
649 		RefDirectory refDir = new RefDirectory(this);
650 		refs = refDir;
651 		refs.create();
652 
653 		ReflogWriter logWriter = refDir.newLogWriter(true);
654 		List<Ref> symrefs = new ArrayList<>();
655 		BatchRefUpdate bru = refs.newBatchUpdate();
656 		for (Ref r : all) {
657 			if (r.isSymbolic()) {
658 				symrefs.add(r);
659 			} else {
660 				bru.addCommand(new ReceiveCommand(ObjectId.zeroId(),
661 						r.getObjectId(), r.getName()));
662 			}
663 
664 			if (writeLogs) {
665 				List<ReflogEntry> logs = oldDb.getReflogReader(r.getName())
666 					.getReverseEntries();
667 				Collections.reverse(logs);
668 				for (ReflogEntry e : logs) {
669 					logWriter.log(r.getName(), e);
670 				}
671 			}
672 		}
673 
674 		try (RevWalkvwalk/RevWalk.html#RevWalk">RevWalk rw = new RevWalk(this)) {
675 			bru.execute(rw, NullProgressMonitor.INSTANCE);
676 		}
677 
678 		List<String> failed = new ArrayList<>();
679 		for (ReceiveCommand cmd : bru.getCommands()) {
680 			if (cmd.getResult() != ReceiveCommand.Result.OK) {
681 				failed.add(cmd.getRefName() + ": " + cmd.getResult()); //$NON-NLS-1$
682 			}
683 		}
684 
685 		if (!failed.isEmpty()) {
686 			throw new IOException(String.format("%s: %s", //$NON-NLS-1$
687 					JGitText.get().failedToConvert,
688 					StringUtils.join(failed, ", "))); //$NON-NLS-1$
689 		}
690 
691 		for (Ref s : symrefs) {
692 			RefUpdate up = refs.newUpdate(s.getName(), false);
693 			up.setForceUpdate(true);
694 			RefUpdate.Result res = up.link(s.getTarget().getName());
695 			if (res != RefUpdate.Result.NEW
696 					&& res != RefUpdate.Result.NO_CHANGE) {
697 				throw new IOException(
698 						String.format("ref %s: %s", s.getName(), res)); //$NON-NLS-1$
699 			}
700 		}
701 
702 		if (!backup) {
703 			File reftableDir = new File(getDirectory(), Constants.REFTABLE);
704 			FileUtils.delete(reftableDir,
705 					FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
706 		}
707 		repoConfig.unset(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
708 				ConfigConstants.CONFIG_KEY_REF_STORAGE);
709 		repoConfig.save();
710 	}
711 
712 	/**
713 	 * Converts the RefDatabase from RefDirectory to reftable. This operation is
714 	 * not atomic.
715 	 *
716 	 * @param writeLogs
717 	 *            whether to write reflogs
718 	 * @param backup
719 	 *            whether to rename or delete the old storage files. If set to
720 	 *            {@code true}, the loose refs are left in {@code refs.old}, the
721 	 *            packed-refs in {@code packed-refs.old} and reflogs in
722 	 *            {@code refs.old/}. HEAD is left in {@code HEAD.old} and also
723 	 *            {@code .log} is appended to additional refs. If set to
724 	 *            {@code false}, the {@code refs/} and {@code logs/} directories
725 	 *            and {@code HEAD} and additional symbolic refs are removed.
726 	 * @throws IOException
727 	 *             on IO problem
728 	 */
729 	@SuppressWarnings("nls")
730 	void convertToReftable(boolean writeLogs, boolean backup)
731 			throws IOException {
732 		File reftableDir = new File(getDirectory(), Constants.REFTABLE);
733 		File headFile = new File(getDirectory(), Constants.HEAD);
734 		if (reftableDir.exists() && reftableDir.listFiles().length > 0) {
735 			throw new IOException(JGitText.get().reftableDirExists);
736 		}
737 
738 		// Ignore return value, as it is tied to temporary newRefs file.
739 		FileReftableDatabase.convertFrom(this, writeLogs);
740 
741 		File refsFile = new File(getDirectory(), "refs");
742 
743 		// non-atomic: remove old data.
744 		File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
745 		File logsDir = new File(getDirectory(), Constants.LOGS);
746 
747 		List<String> additional = getRefDatabase().getAdditionalRefs().stream()
748 				.map(Ref::getName).collect(toList());
749 		additional.add(Constants.HEAD);
750 		if (backup) {
751 			FileUtils.rename(refsFile, new File(getDirectory(), "refs.old"));
752 			if (packedRefs.exists()) {
753 				FileUtils.rename(packedRefs, new File(getDirectory(),
754 						Constants.PACKED_REFS + ".old"));
755 			}
756 			if (logsDir.exists()) {
757 				FileUtils.rename(logsDir,
758 						new File(getDirectory(), Constants.LOGS + ".old"));
759 			}
760 			for (String r : additional) {
761 				FileUtils.rename(new File(getDirectory(), r),
762 					new File(getDirectory(), r + ".old"));
763 			}
764 		} else {
765 			FileUtils.delete(packedRefs, FileUtils.SKIP_MISSING);
766 			FileUtils.delete(headFile);
767 			FileUtils.delete(logsDir, FileUtils.RECURSIVE);
768 			FileUtils.delete(refsFile, FileUtils.RECURSIVE);
769 			for (String r : additional) {
770 				new File(getDirectory(), r).delete();
771 			}
772 		}
773 
774 		FileUtils.mkdir(refsFile, true);
775 
776 		// By putting in a dummy HEAD, old versions of Git still detect a repo
777 		// (that they can't read)
778 		try (OutputStream os = new FileOutputStream(headFile)) {
779 			os.write(Constants.encodeASCII("ref: refs/heads/.invalid"));
780 		}
781 
782 		// Some tools might write directly into .git/refs/heads/BRANCH. By
783 		// putting a file here, this fails spectacularly.
784 		FileUtils.createNewFile(new File(refsFile, "heads"));
785 
786 		repoConfig.setString(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
787 				ConfigConstants.CONFIG_KEY_REF_STORAGE,
788 				ConfigConstants.CONFIG_REF_STORAGE_REFTABLE);
789 		repoConfig.setLong(ConfigConstants.CONFIG_CORE_SECTION, null,
790 				ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1);
791 		repoConfig.save();
792 		refs.close();
793 		refs = new FileReftableDatabase(this);
794 	}
795 
796 	/**
797 	 * Converts between ref storage formats.
798 	 *
799 	 * @param format
800 	 *            the format to convert to, either "reftable" or "refdir"
801 	 * @param writeLogs
802 	 *            whether to write reflogs
803 	 * @param backup
804 	 *            whether to make a backup of the old data
805 	 * @throws IOException
806 	 *             on I/O problems.
807 	 */
808 	public void convertRefStorage(String format, boolean writeLogs,
809 			boolean backup) throws IOException {
810 		if (format.equals("reftable")) { //$NON-NLS-1$
811 			if (refs instanceof RefDirectory) {
812 				convertToReftable(writeLogs, backup);
813 			}
814 		} else if (format.equals("refdir")) {//$NON-NLS-1$
815 			if (refs instanceof FileReftableDatabase) {
816 				convertToPackedRefs(writeLogs, backup);
817 			}
818 		} else {
819 			throw new IOException(MessageFormat
820 					.format(JGitText.get().unknownRefStorageFormat, format));
821 		}
822 	}
823 }