/*
* 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.IOException;
import javax.jcr.RepositoryException;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.modeshape.jcr.JcrLexicon;
import org.modeshape.jcr.api.value.DateTime;
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.value.BinaryValue;
import org.modeshape.schematic.document.Document;
/**
* A function that returns the file and directory structure within a particular commit. The structure of this area of the
* repository is as follows:
*
* <pre>
* /tree/{branchOrTagOrObjectId}/{filesAndFolders}/...
* </pre>
*/
public class GitTree extends GitFunction implements PageableGitFunction {
protected static final String JCR_CONTENT = "jcr:content";
protected static final String JCR_CONTENT_SUFFIX = "/" + JCR_CONTENT;
protected static final String NAME = "tree";
protected static final String ID = "/tree";
protected static Object referenceToTree( ObjectId commitId,
String branchOrTagOrCommitId,
Values values ) {
return values.referenceTo(ID + DELIMITER + branchOrTagOrCommitId);
}
public GitTree( GitConnector connector ) {
super(NAME, connector);
}
@Override
public Document execute( Repository repository,
Git git,
CallSpecification spec,
DocumentWriter writer,
Values values ) throws GitAPIException, IOException {
if (spec.parameterCount() == 0) {
// This is the top-level "/branches" node
writer.setPrimaryType(GitLexicon.TREES);
// Generate the child references to the branches and tags. Branches are likely used more often, so list them first...
addBranchesAsChildren(git, spec, writer);
addTagsAsChildren(git, spec, writer);
addCommitsAsChildren(git, spec, writer, pageSize);
} else if (spec.parameterCount() == 1) {
// This is a particular branch/tag/commit node ...
String branchOrTagOrObjectId = spec.parameter(0);
ObjectId objId = resolveBranchOrTagOrCommitId(repository, branchOrTagOrObjectId);
RevWalk walker = new RevWalk(repository);
walker.setRetainBody(true); // we need to parse the commit for the top-level
try {
RevCommit commit = walker.parseCommit(objId);
// could happen if not enough permissions, for example
if (commit != null) {
// Add the properties for this node ...
String committer = commiterName(commit);
String author = authorName(commit);
DateTime committed = values.dateFrom(commit.getCommitTime());
writer.setPrimaryType(GitLexicon.FOLDER);
writer.addProperty(JcrLexicon.CREATED, committed);
writer.addProperty(JcrLexicon.CREATED_BY, committer);
writer.addProperty(GitLexicon.OBJECT_ID, objId.name());
writer.addProperty(GitLexicon.AUTHOR, author);
writer.addProperty(GitLexicon.COMMITTER, committer);
writer.addProperty(GitLexicon.COMMITTED, committed);
writer.addProperty(GitLexicon.TITLE, commit.getShortMessage());
writer.addProperty(GitLexicon.HISTORY, GitHistory.referenceToHistory(objId, branchOrTagOrObjectId, values));
writer.addProperty(GitLexicon.DETAIL, GitCommitDetails.referenceToCommit(objId, values));
// Add the top-level children of the directory ...
addInformationForPath(repository, writer, commit, "", spec, values);
} else {
connector.getLogger().warn(GitI18n.cannotReadCommit, objId);
}
} finally {
walker.dispose();
}
} else {
// This is a folder or file within the directory structure ...
String branchOrTagOrObjectId = spec.parameter(0);
String path = spec.parametersAsPath(1);
ObjectId objId = resolveBranchOrTagOrCommitId(repository, branchOrTagOrObjectId);
RevWalk walker = new RevWalk(repository);
walker.setRetainBody(true);
try {
// Get the commit information ...
RevCommit commit = walker.parseCommit(objId);
if (commit != null) {
// Add the top-level children of the directory ...
addInformationForPath(repository, writer, commit, path, spec, values);
}
} finally {
walker.dispose();
}
}
return writer.document();
}
protected void addInformationForPath( Repository repository,
DocumentWriter writer,
RevCommit commit,
String path,
CallSpecification spec,
Values values ) throws GitAPIException, IOException {
// Make sure the path is in the canonical form we need ...
if (path.startsWith("/")) {
if (path.length() == 1) path = "";
else path = path.substring(1);
}
// Now see if we're actually referring to the "jcr:content" node ...
boolean isContentNode = false;
if (path.endsWith(JCR_CONTENT_SUFFIX)) {
isContentNode = true;
path = path.substring(0, path.length() - JCR_CONTENT_SUFFIX.length());
}
// Create the TreeWalk that we'll use to navigate the files/directories ...
final TreeWalk tw = new TreeWalk(repository);
tw.addTree(commit.getTree());
if ("".equals(path)) {
// This is the top-level directory, so we don't need to pre-walk to find anything ...
tw.setRecursive(false);
while (tw.next()) {
String childName = tw.getNameString();
String childId = spec.childId(childName);
writer.addChild(childId, childName);
}
} else {
// We need to first find our path *before* we can walk the children ...
PathFilter filter = PathFilter.create(path);
tw.setFilter(filter);
while (tw.next()) {
if (filter.isDone(tw)) {
break;
} else if (tw.isSubtree()) {
tw.enterSubtree();
}
}
// Now that the TreeWalk is the in right location given by the 'path', we can get the
if (tw.isSubtree()) {
// The object at the 'path' is a directory, so go into it ...
tw.enterSubtree();
// Find the commit in which this folder was last modified ...
// This may not be terribly efficient, but it seems to work faster on subsequent runs ...
writer.setPrimaryType(GitLexicon.FOLDER);
// Add folder-related properties ...
String committer = commiterName(commit);
String author = authorName(commit);
DateTime committed = values.dateFrom(commit.getCommitTime());
writer.addProperty(JcrLexicon.CREATED, committed);
writer.addProperty(JcrLexicon.CREATED_BY, committer);
writer.addProperty(GitLexicon.OBJECT_ID, commit.getId().name());
writer.addProperty(GitLexicon.AUTHOR, author);
writer.addProperty(GitLexicon.COMMITTER, committer);
writer.addProperty(GitLexicon.COMMITTED, committed);
writer.addProperty(GitLexicon.TITLE, commit.getShortMessage());
// And now walk the contents of the directory ...
while (tw.next()) {
String childName = tw.getNameString();
String childId = spec.childId(childName);
writer.addChild(childId, childName);
}
} else {
// The path specifies a file (or a content node) ...
if (isContentNode) {
writer.setPrimaryType(GitLexicon.RESOURCE);
// Add file-related properties ...
String committer = commiterName(commit);
String author = authorName(commit);
DateTime committed = values.dateFrom(commit.getCommitTime());
writer.addProperty(JcrLexicon.LAST_MODIFIED, committed);
writer.addProperty(JcrLexicon.LAST_MODIFIED_BY, committer);
writer.addProperty(GitLexicon.OBJECT_ID, commit.getId().name());
writer.addProperty(GitLexicon.AUTHOR, author);
writer.addProperty(GitLexicon.COMMITTER, committer);
writer.addProperty(GitLexicon.COMMITTED, committed);
writer.addProperty(GitLexicon.TITLE, commit.getShortMessage());
// Create the BinaryValue ...
ObjectId fileObjectId = tw.getObjectId(0);
ObjectLoader fileLoader = repository.open(fileObjectId);
// we'll always create an external binary which will be resolved by the connector when required
BinaryValue value = new GitBinaryValue(fileObjectId, fileLoader, connector.getSourceName(), name,
connector.getMimeTypeDetector());
writer.addProperty(JcrLexicon.DATA, value);
if (connector.includeMimeType()) {
try {
String filename = spec.parameter(spec.parameterCount() - 1); // the last is 'jcr:content'
String mimeType = value.getMimeType(filename);
if (mimeType != null) writer.addProperty(JcrLexicon.MIMETYPE, mimeType);
} catch (RepositoryException | IOException e) {
// do nothing
connector.getLogger().debug("cannot determine mime-type information for objectID '{0}'", fileObjectId);
}
}
} else {
writer.setPrimaryType(GitLexicon.FILE);
// Add file-related properties ...
String committer = commiterName(commit);
String author = authorName(commit);
DateTime committed = values.dateFrom(commit.getCommitTime());
writer.addProperty(JcrLexicon.CREATED, committed);
writer.addProperty(JcrLexicon.CREATED_BY, committer);
writer.addProperty(GitLexicon.OBJECT_ID, commit.getId().name());
writer.addProperty(GitLexicon.AUTHOR, author);
writer.addProperty(GitLexicon.COMMITTER, committer);
writer.addProperty(GitLexicon.COMMITTED, committed);
writer.addProperty(GitLexicon.TITLE, commit.getShortMessage());
// Add the "jcr:content" child node ...
String childId = spec.childId(JCR_CONTENT);
writer.addChild(childId, JCR_CONTENT);
}
}
}
}
@Override
public boolean isPaged() {
return true;
}
@Override
public Document execute( Repository repository,
Git git,
CallSpecification spec,
PageWriter writer,
Values values,
PageKey pageKey ) throws GitAPIException, IOException {
if (spec.parameterCount() != 0) return null;
addCommitsAsPageOfChildren(git, repository, spec, writer, pageKey);
return writer.document();
}
}