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