/* * Copyright (C) 2009, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.storage.file; import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.ObjectToPack; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.pack.PackWriter; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectDatabase; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; /** * Traditional file system based {@link org.eclipse.jgit.lib.ObjectDatabase}. *

* This is the classical object database representation for a Git repository, * where objects are stored loose by hashing them into directories by their * {@link org.eclipse.jgit.lib.ObjectId}, or are stored in compressed containers * known as {@link org.eclipse.jgit.internal.storage.file.Pack}s. *

* Optionally an object database can reference one or more alternates; other * ObjectDatabase instances that are searched in addition to the current * database. *

* Databases are divided into two halves: a half that is considered to be fast * to search (the {@code PackFile}s), and a half that is considered to be slow * to search (loose objects). When alternates are present the fast half is fully * searched (recursively through all alternates) before the slow half is * considered. */ public class ObjectDirectory extends FileObjectDatabase { /** Maximum number of candidates offered as resolutions of abbreviation. */ private static final int RESOLVE_ABBREV_LIMIT = 256; private final AlternateHandle handle = new AlternateHandle(this); private final Config config; private final File objects; private final File infoDirectory; private final LooseObjects loose; private final PackDirectory packed; private final PackDirectory preserved; private final File alternatesFile; private final FS fs; private final AtomicReference alternates; private final File shallowFile; private FileSnapshot shallowFileSnapshot = FileSnapshot.DIRTY; private Set shallowCommitsIds; /** * Initialize a reference to an on-disk object directory. * * @param cfg * configuration this directory consults for write settings. * @param dir * the location of the objects directory. * @param alternatePaths * a list of alternate object directories * @param fs * the file system abstraction which will be necessary to perform * certain file system operations. * @param shallowFile * file which contains IDs of shallow commits, null if shallow * commits handling should be turned off * @throws java.io.IOException * an alternate object cannot be opened. */ public ObjectDirectory(final Config cfg, final File dir, File[] alternatePaths, FS fs, File shallowFile) throws IOException { config = cfg; objects = dir; infoDirectory = new File(objects, "info"); //$NON-NLS-1$ File packDirectory = new File(objects, "pack"); //$NON-NLS-1$ File preservedDirectory = new File(packDirectory, "preserved"); //$NON-NLS-1$ alternatesFile = new File(objects, Constants.INFO_ALTERNATES); loose = new LooseObjects(objects); packed = new PackDirectory(config, packDirectory); preserved = new PackDirectory(config, preservedDirectory); this.fs = fs; this.shallowFile = shallowFile; alternates = new AtomicReference<>(); if (alternatePaths != null) { AlternateHandle[] alt; alt = new AlternateHandle[alternatePaths.length]; for (int i = 0; i < alternatePaths.length; i++) alt[i] = openAlternate(alternatePaths[i]); alternates.set(alt); } } /** {@inheritDoc} */ @Override public final File getDirectory() { return loose.getDirectory(); } /** *

Getter for the field packDirectory.

* * @return the location of the pack directory. */ public final File getPackDirectory() { return packed.getDirectory(); } /** *

Getter for the field preservedDirectory.

* * @return the location of the preserved directory. */ public final File getPreservedDirectory() { return preserved.getDirectory(); } /** {@inheritDoc} */ @Override public boolean exists() { return fs.exists(objects); } /** {@inheritDoc} */ @Override public void create() throws IOException { loose.create(); FileUtils.mkdir(infoDirectory); packed.create(); } /** {@inheritDoc} */ @Override public ObjectDirectoryInserter newInserter() { return new ObjectDirectoryInserter(this, config); } /** * Create a new inserter that inserts all objects as pack files, not loose * objects. * * @return new inserter. */ public PackInserter newPackInserter() { return new PackInserter(this); } /** {@inheritDoc} */ @Override public void close() { loose.close(); packed.close(); // Fully close all loaded alternates and clear the alternate list. AlternateHandle[] alt = alternates.get(); if (alt != null && alternates.compareAndSet(alt, null)) { for(AlternateHandle od : alt) od.close(); } } /** {@inheritDoc} */ @Override public Collection getPacks() { return packed.getPacks(); } /** * {@inheritDoc} *

* Add a single existing pack to the list of available pack files. */ @Override public Pack openPack(File pack) throws IOException { PackFile pf; try { pf = new PackFile(pack); } catch (IllegalArgumentException e) { throw new IOException( MessageFormat.format(JGitText.get().notAValidPack, pack), e); } String p = pf.getName(); // TODO(nasserg): See if PackFile can do these checks instead if (p.length() != 50 || !p.startsWith("pack-") //$NON-NLS-1$ || !pf.getPackExt().equals(PACK)) { throw new IOException( MessageFormat.format(JGitText.get().notAValidPack, pack)); } PackFile bitmapIdx = pf.create(BITMAP_INDEX); Pack res = new Pack(pack, bitmapIdx.exists() ? bitmapIdx : null); packed.insert(res); return res; } /** {@inheritDoc} */ @Override public String toString() { return "ObjectDirectory[" + getDirectory() + "]"; //$NON-NLS-1$ //$NON-NLS-2$ } /** {@inheritDoc} */ @Override public boolean has(AnyObjectId objectId) { return loose.hasCached(objectId) || hasPackedOrLooseInSelfOrAlternate(objectId) || (restoreFromSelfOrAlternate(objectId, null) && hasPackedOrLooseInSelfOrAlternate(objectId)); } private boolean hasPackedOrLooseInSelfOrAlternate(AnyObjectId objectId) { return hasPackedInSelfOrAlternate(objectId, null) || hasLooseInSelfOrAlternate(objectId, null); } private boolean hasPackedInSelfOrAlternate(AnyObjectId objectId, Set skips) { if (hasPackedObject(objectId)) { return true; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { if (alt.db.hasPackedInSelfOrAlternate(objectId, skips)) { return true; } } } return false; } private boolean hasLooseInSelfOrAlternate(AnyObjectId objectId, Set skips) { if (loose.has(objectId)) { return true; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { if (alt.db.hasLooseInSelfOrAlternate(objectId, skips)) { return true; } } } return false; } boolean hasPackedObject(AnyObjectId objectId) { return packed.has(objectId); } @Override void resolve(Set matches, AbbreviatedObjectId id) throws IOException { resolve(matches, id, null); } private void resolve(Set matches, AbbreviatedObjectId id, Set skips) throws IOException { if (!packed.resolve(matches, id, RESOLVE_ABBREV_LIMIT)) return; if (!loose.resolve(matches, id, RESOLVE_ABBREV_LIMIT)) return; skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { alt.db.resolve(matches, id, skips); if (matches.size() > RESOLVE_ABBREV_LIMIT) { return; } } } } @Override ObjectLoader openObject(WindowCursor curs, AnyObjectId objectId) throws IOException { ObjectLoader ldr = openObjectWithoutRestoring(curs, objectId); if (ldr == null && restoreFromSelfOrAlternate(objectId, null)) { ldr = openObjectWithoutRestoring(curs, objectId); } return ldr; } private ObjectLoader openObjectWithoutRestoring(WindowCursor curs, AnyObjectId objectId) throws IOException { if (loose.hasCached(objectId)) { ObjectLoader ldr = openLooseObject(curs, objectId); if (ldr != null) { return ldr; } } ObjectLoader ldr = openPackedFromSelfOrAlternate(curs, objectId, null); if (ldr != null) { return ldr; } return openLooseFromSelfOrAlternate(curs, objectId, null); } private ObjectLoader openPackedFromSelfOrAlternate(WindowCursor curs, AnyObjectId objectId, Set skips) { ObjectLoader ldr = openPackedObject(curs, objectId); if (ldr != null) { return ldr; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { ldr = alt.db.openPackedFromSelfOrAlternate(curs, objectId, skips); if (ldr != null) { return ldr; } } } return null; } private ObjectLoader openLooseFromSelfOrAlternate(WindowCursor curs, AnyObjectId objectId, Set skips) throws IOException { ObjectLoader ldr = openLooseObject(curs, objectId); if (ldr != null) { return ldr; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { ldr = alt.db.openLooseFromSelfOrAlternate(curs, objectId, skips); if (ldr != null) { return ldr; } } } return null; } ObjectLoader openPackedObject(WindowCursor curs, AnyObjectId objectId) { return packed.open(curs, objectId); } @Override ObjectLoader openLooseObject(WindowCursor curs, AnyObjectId id) throws IOException { return loose.open(curs, id); } @Override long getObjectSize(WindowCursor curs, AnyObjectId id) throws IOException { long sz = getObjectSizeWithoutRestoring(curs, id); if (0 > sz && restoreFromSelfOrAlternate(id, null)) { sz = getObjectSizeWithoutRestoring(curs, id); } return sz; } private long getObjectSizeWithoutRestoring(WindowCursor curs, AnyObjectId id) throws IOException { if (loose.hasCached(id)) { long len = loose.getSize(curs, id); if (0 <= len) { return len; } } long len = getPackedSizeFromSelfOrAlternate(curs, id, null); if (0 <= len) { return len; } return getLooseSizeFromSelfOrAlternate(curs, id, null); } private long getPackedSizeFromSelfOrAlternate(WindowCursor curs, AnyObjectId id, Set skips) { long len = packed.getSize(curs, id); if (0 <= len) { return len; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { len = alt.db.getPackedSizeFromSelfOrAlternate(curs, id, skips); if (0 <= len) { return len; } } } return -1; } private long getLooseSizeFromSelfOrAlternate(WindowCursor curs, AnyObjectId id, Set skips) throws IOException { long len = loose.getSize(curs, id); if (0 <= len) { return len; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { len = alt.db.getLooseSizeFromSelfOrAlternate(curs, id, skips); if (0 <= len) { return len; } } } return -1; } @Override void selectObjectRepresentation(PackWriter packer, ObjectToPack otp, WindowCursor curs) throws IOException { selectObjectRepresentation(packer, otp, curs, null); } private void selectObjectRepresentation(PackWriter packer, ObjectToPack otp, WindowCursor curs, Set skips) throws IOException { packed.selectRepresentation(packer, otp, curs); skips = addMe(skips); for (AlternateHandle h : myAlternates()) { if (!skips.contains(h.getId())) { h.db.selectObjectRepresentation(packer, otp, curs, skips); } } } private boolean restoreFromSelfOrAlternate(AnyObjectId objectId, Set skips) { if (restoreFromSelf(objectId)) { return true; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { if (alt.db.restoreFromSelfOrAlternate(objectId, skips)) { return true; } } } return false; } private boolean restoreFromSelf(AnyObjectId objectId) { Pack preservedPack = preserved.getPack(objectId); if (preservedPack == null) { return false; } PackFile preservedFile = new PackFile(preservedPack.getPackFile()); // Restore the index last since the set will be considered for use once // the index appears. for (PackExt ext : PackExt.values()) { if (!INDEX.equals(ext)) { restore(preservedFile.create(ext)); } } restore(preservedFile.create(INDEX)); return true; } private boolean restore(PackFile preservedPack) { PackFile restored = preservedPack .createForDirectory(packed.getDirectory()); try { Files.createLink(restored.toPath(), preservedPack.toPath()); } catch (IOException e) { return false; } return true; } @Override InsertLooseObjectResult insertUnpackedObject(File tmp, ObjectId id, boolean createDuplicate) throws IOException { // If the object is already in the repository, remove temporary file. // if (loose.hasCached(id)) { FileUtils.delete(tmp, FileUtils.RETRY); return InsertLooseObjectResult.EXISTS_LOOSE; } if (!createDuplicate && has(id)) { FileUtils.delete(tmp, FileUtils.RETRY); return InsertLooseObjectResult.EXISTS_PACKED; } return loose.insert(tmp, id); } @Override Config getConfig() { return config; } @Override FS getFS() { return fs; } @Override Set getShallowCommits() throws IOException { if (shallowFile == null || !shallowFile.isFile()) return Collections.emptySet(); if (shallowFileSnapshot == null || shallowFileSnapshot.isModified(shallowFile)) { shallowCommitsIds = new HashSet<>(); try (BufferedReader reader = open(shallowFile)) { String line; while ((line = reader.readLine()) != null) { try { shallowCommitsIds.add(ObjectId.fromString(line)); } catch (IllegalArgumentException ex) { throw new IOException(MessageFormat .format(JGitText.get().badShallowLine, line), ex); } } } shallowFileSnapshot = FileSnapshot.save(shallowFile); } return shallowCommitsIds; } void closeAllPackHandles(File packFile) { // if the packfile already exists (because we are rewriting a // packfile for the same set of objects maybe with different // PackConfig) then make sure we get rid of all handles on the file. // Windows will not allow for rename otherwise. if (packFile.exists()) { for (Pack p : packed.getPacks()) { if (packFile.getPath().equals(p.getPackFile().getPath())) { p.close(); break; } } } } AlternateHandle[] myAlternates() { AlternateHandle[] alt = alternates.get(); if (alt == null) { synchronized (alternates) { alt = alternates.get(); if (alt == null) { try { alt = loadAlternates(); } catch (IOException e) { alt = new AlternateHandle[0]; } alternates.set(alt); } } } return alt; } Set addMe(Set skips) { if (skips == null) { skips = new HashSet<>(); } skips.add(handle.getId()); return skips; } private AlternateHandle[] loadAlternates() throws IOException { final List l = new ArrayList<>(4); try (BufferedReader br = open(alternatesFile)) { String line; while ((line = br.readLine()) != null) { l.add(openAlternate(line)); } } return l.toArray(new AlternateHandle[0]); } private static BufferedReader open(File f) throws IOException, FileNotFoundException { return Files.newBufferedReader(f.toPath(), UTF_8); } private AlternateHandle openAlternate(String location) throws IOException { final File objdir = fs.resolve(objects, location); return openAlternate(objdir); } private AlternateHandle openAlternate(File objdir) throws IOException { final File parent = objdir.getParentFile(); if (FileKey.isGitRepository(parent, fs)) { FileKey key = FileKey.exact(parent, fs); FileRepository db = (FileRepository) RepositoryCache.open(key); return new AlternateRepository(db); } ObjectDirectory db = new ObjectDirectory(config, objdir, null, fs, null); return new AlternateHandle(db); } /** * Compute the location of a loose object file. */ @Override public File fileFor(AnyObjectId objectId) { return loose.fileFor(objectId); } static class AlternateHandle { static class Id { String alternateId; public Id(File object) { try { this.alternateId = object.getCanonicalPath(); } catch (Exception e) { alternateId = null; } } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o == null || !(o instanceof Id)) { return false; } Id aId = (Id) o; return Objects.equals(alternateId, aId.alternateId); } @Override public int hashCode() { if (alternateId == null) { return 1; } return alternateId.hashCode(); } } final ObjectDirectory db; AlternateHandle(ObjectDirectory db) { this.db = db; } void close() { db.close(); } public Id getId(){ return db.getAlternateId(); } } static class AlternateRepository extends AlternateHandle { final FileRepository repository; AlternateRepository(FileRepository r) { super(r.getObjectDatabase()); repository = r; } @Override void close() { repository.close(); } } /** {@inheritDoc} */ @Override public ObjectDatabase newCachedDatabase() { return newCachedFileObjectDatabase(); } CachedObjectDirectory newCachedFileObjectDatabase() { return new CachedObjectDirectory(this); } AlternateHandle.Id getAlternateId() { return new AlternateHandle.Id(objects); } }