/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.connector.git; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.jcr.NamespaceRegistry; import javax.jcr.RepositoryException; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.modeshape.schematic.document.Document; import org.modeshape.jcr.RepositoryConfiguration; import org.modeshape.jcr.api.nodetype.NodeTypeManager; import org.modeshape.jcr.cache.DocumentStoreException; import org.modeshape.jcr.spi.federation.Connector; import org.modeshape.jcr.spi.federation.DocumentWriter; import org.modeshape.jcr.spi.federation.PageKey; import org.modeshape.jcr.spi.federation.PageWriter; import org.modeshape.jcr.spi.federation.Pageable; import org.modeshape.jcr.spi.federation.ReadOnlyConnector; import org.modeshape.jcr.value.binary.ExternalBinaryValue; /** * A read-only {@link Connector} that accesses the content in a local Git repository that is a clone of a remote repository. * <p> * This connector has several properties that must be configured via the {@link RepositoryConfiguration}: * <ul> * <li><strong><code>directoryPath</code></strong> - The path to the folder that is or contains the <code>.git</code> data * structure is to be accessed by this connector.</li> * <li><strong><code>remoteName</code></strong> - The alias used by the local Git repository for the remote repository. The * default is the "<code>origin</code>". If the value contains commas, the value contains an ordered list of remote aliases that * should be searched; the first one to match an existing remote will be used.</li> * <li><strong><code>queryableBranches</code></strong> - An array with the names of the branches that should be queryable by the * repository. By default, only the master branch is queryable.</li> * </ul> * </p> * <p> * The connector results in the following structure: * </p> * <table cellspacing="0" cellpadding="1" border="1"> * <tr> * <th>Path</th> * <th>Description</th> * </tr> * <tr> * <td><code>/branches/{branchName}</code></td> * <td>The list of branches.</td> * </tr> * <tr> * <td><code>/tags/{tagName}</code></td> * <td>The list of tags.</td> * </tr> * <tr> * <td><code>/commits/{branchOrTagNameOrCommit}/{objectId}</code></td> * <td>The history of commits on the branch, tag or object ID name "<code>{branchOrTagNameOrCommit}</code>", where " * <code>{objectId}</code>" is the object ID of the commit.</td> * </tr> * <tr> * <td><code>/commit/{branchOrTagNameOrCommit}</code></td> * <td>The information about a particular branch, tag or commit "<code>{branchOrTagNameOrCommit}</code>".</td> * </tr> * <tr> * <td><code>/tree/{branchOrTagOrObjectId}/{filesAndFolders}/...</code></td> * <td>The structure of the directories and files in the specified branch, tag or commit "<code>{branchOrTagNameOrCommit}</code>". * </td> * </tr> * </table> */ public class GitConnector extends ReadOnlyConnector implements Pageable { private static final boolean DEFAULT_INCLUDE_MIME_TYPE = false; private static final String GIT_DIRECTORY_NAME = ".git"; private static final String GIT_CND_PATH = "org/modeshape/connector/git/git.cnd"; /** * The string path for a {@link File} object that represents the top-level directory of the local Git repository. This is set * via reflection and is required for this connector. */ private String directoryPath; /** * The optional string value representing the name of the remote that serves as the primary remote repository. By default this * is "origin". This is set via reflection. */ private String remoteName; /** * The optional string value representing the name of the remote that serves as the primary remote repository. By default this * is "origin". This is set via reflection. */ private List<String> parsedRemoteNames; /** * The optional boolean value specifying whether the connector should set the "jcr:mimeType" property on the "jcr:content" * child node under each "git:file" node. By default this is '{@value GitConnector#DEFAULT_INCLUDE_MIME_TYPE}'. This is set * via reflection. */ private boolean includeMimeType = DEFAULT_INCLUDE_MIME_TYPE; private Repository repository; private Git git; private Map<String, GitFunction> functions; private Map<String, PageableGitFunction> pageableFunctions; private Values values; @Override public void initialize( NamespaceRegistry registry, NodeTypeManager nodeTypeManager ) throws RepositoryException, IOException { super.initialize(registry, nodeTypeManager); // Verify the local git repository exists ... File dir = new File(directoryPath); if (!dir.exists() || !dir.isDirectory()) { throw new RepositoryException(GitI18n.directoryDoesNotExist.text(dir.getAbsolutePath())); } if (!dir.canRead()) { throw new RepositoryException(GitI18n.directoryCannotBeRead.text(dir.getAbsolutePath())); } File gitDir = dir; if (!GIT_DIRECTORY_NAME.equals(gitDir.getName())) { gitDir = new File(dir, ".git"); if (!gitDir.exists() || !gitDir.isDirectory()) { throw new RepositoryException(GitI18n.directoryDoesNotExist.text(gitDir.getAbsolutePath())); } if (!gitDir.canRead()) { throw new RepositoryException(GitI18n.directoryCannotBeRead.text(gitDir.getAbsolutePath())); } } values = new Values(factories(), getContext().getBinaryStore()); // Set up the repository instance. We expect it to exist, and will use it as a "bare" repository (meaning // that no working directory will be used nor needs to exist) ... repository = new FileRepositoryBuilder().setGitDir(gitDir).setMustExist(true).setBare().build(); git = new Git(repository); parsedRemoteNames = new ArrayList<String>(); if (this.remoteName != null) { // Make sure the remote exists ... Set<String> remoteNames = repository.getConfig().getSubsections("remote"); String remoteName = null; for (String desiredName : this.remoteName.split(",")) { desiredName = desiredName.trim(); if (remoteNames.contains(desiredName)) { remoteName = desiredName; parsedRemoteNames.add(desiredName); break; } } if (remoteName == null) { throw new RepositoryException(GitI18n.remoteDoesNotExist.text(this.remoteName, gitDir.getAbsolutePath())); } this.remoteName = remoteName; } // Register the different functions ... functions = new HashMap<String, GitFunction>(); pageableFunctions = new HashMap<String, PageableGitFunction>(); register(new GitRoot(this), new GitBranches(this), new GitTags(this), new GitHistory(this), new GitCommitDetails(this), new GitTree(this)); // Register the Git-specific node types ... InputStream cndStream = getClass().getClassLoader().getResourceAsStream(GIT_CND_PATH); nodeTypeManager.registerNodeTypes(cndStream, true); } private void register( GitFunction... functions ) { for (GitFunction function : functions) { this.functions.put(function.getName(), function); if (function instanceof PageableGitFunction) { this.pageableFunctions.put(function.getName(), (PageableGitFunction)function); } } } protected DocumentWriter newDocumentWriter( String id ) { return super.newDocument(id); } protected boolean includeMimeType() { return includeMimeType; } @Override public void shutdown() { repository = null; git = null; functions = null; } @Override public Document getDocumentById( String id ) { CallSpecification callSpec = new CallSpecification(id); GitFunction function = functions.get(callSpec.getFunctionName()); if (function == null) return null; try { // Set up the document writer ... DocumentWriter writer = newDocument(id); String parentId = callSpec.getParentId(); assert parentId != null; writer.setParent(parentId); // check if the document should be indexed or not, based on the global connector setting and the specific function if (!this.isQueryable() || !function.isQueryable(callSpec)) { writer.setNotQueryable(); } // Now call the function ... Document doc = function.execute(repository, git, callSpec, writer, values); // Log the result ... getLogger().trace("ID={0},result={1}", id, doc); return doc; } catch (Throwable e) { throw new DocumentStoreException(id, e); } } @Override public Document getChildren( PageKey pageKey ) { String id = pageKey.getParentId(); CallSpecification callSpec = new CallSpecification(id); PageableGitFunction function = pageableFunctions.get(callSpec.getFunctionName()); if (function == null) return null; try { // Set up the document writer ... PageWriter writer = newPageDocument(pageKey); // Now call the function ... return function.execute(repository, git, callSpec, writer, values, pageKey); } catch (Throwable e) { throw new DocumentStoreException(id, e); } } @Override public Document getChildReference( String parentKey, String childKey ) { // The child key always contains the path to the child, so therefore we can always use it to create the // child reference document ... CallSpecification callSpec = new CallSpecification(childKey); return newChildReference(childKey, callSpec.lastParameter()); } @Override public String getDocumentId( String path ) { // Our paths are basically used as IDs ... return path; } @Override public Collection<String> getDocumentPathsById( String id ) { // Our paths are basically used as IDs, so the ID is the path ... return Collections.singletonList(id); } @Override public boolean hasDocument( String id ) { Document doc = getDocumentById(id); return doc != null; } @Override public ExternalBinaryValue getBinaryValue( String id ) { try { ObjectId fileObjectId = ObjectId.fromString(id); ObjectLoader fileLoader = repository.open(fileObjectId); return new GitBinaryValue(fileObjectId, fileLoader, getSourceName(), null, getMimeTypeDetector()); } catch (IOException e) { throw new DocumentStoreException(id, e); } } protected final String remoteName() { return remoteName; } protected final List<String> remoteNames() { return parsedRemoteNames; } }