/*
* 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 java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListBranchCommand;
import org.eclipse.jgit.api.ListBranchCommand.ListMode;
import org.eclipse.jgit.api.ListTagCommand;
import org.eclipse.jgit.api.LogCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.modeshape.schematic.document.Document;
import org.modeshape.jcr.spi.federation.DocumentWriter;
import org.modeshape.jcr.spi.federation.PageKey;
import org.modeshape.jcr.spi.federation.PageWriter;
/**
*
*/
public abstract class GitFunction {
protected static final String DELIMITER = "/";
protected static final String REMOTE_BRANCH_PREFIX = "refs/remotes/";
protected static final String LOCAL_BRANCH_PREFIX = "refs/heads/";
protected static final String TAG_PREFIX = "refs/tags/";
protected static final int DEFAULT_PAGE_SIZE = 15;
protected static final Comparator<Ref> REVERSE_REF_COMPARATOR = new Comparator<Ref>() {
@Override
public int compare( Ref o1,
Ref o2 ) {
return 0 - o1.getName().compareTo(o2.getName());
}
};
protected final String name;
protected final GitConnector connector;
protected int pageSize = DEFAULT_PAGE_SIZE;
protected GitFunction( String name,
GitConnector connector ) {
this.name = name;
this.connector = connector;
}
/**
* Get the name of this function.
*
* @return the name; never null
*/
public String getName() {
return name;
}
public boolean isPaged() {
return false;
}
public abstract Document execute( Repository repository,
Git git,
CallSpecification spec,
DocumentWriter writer,
Values values ) throws GitAPIException, IOException;
private Set<String> remoteBranchPrefixes() {
Set<String> prefixes = new HashSet<String>();
for (String remoteName : connector.remoteNames()) {
String prefix = remoteBranchPrefix(remoteName);
prefixes.add(prefix);
}
return prefixes;
}
private String remoteBranchPrefix( String remoteName ) {
return REMOTE_BRANCH_PREFIX + remoteName + "/";
}
/**
* Obtain the name of the branch reference
*
* @param branchName
* @return the branch ref name
*/
protected String branchRefForName( String branchName ) {
String remoteName = connector.remoteName();
return remoteName != null ? remoteBranchPrefix(remoteName) + branchName : LOCAL_BRANCH_PREFIX + branchName;
}
/**
* Obtain the name of the branch reference
*
* @param branchName
* @param remoteName the name of the remote
* @return the branch ref name
*/
protected String branchRefForName( String branchName,
String remoteName ) {
return remoteBranchPrefix(remoteName) + branchName;
}
/**
* Resolve the branch name, tag name, or commit ID into the appropriate ObjectId. Note that the branch names are assumed to be
* from the {@link GitConnector#remoteName() remote}.
*
* @param repository the Repository object; may not be null
* @param branchOrTagOrCommitId the branch name, tag name, or commit ID; may not be null
* @return the resolved ObjectId, or null if the supplied string does not resolve to an object ID
* @throws IOException if there is a problem reading the Git repository
*/
protected ObjectId resolveBranchOrTagOrCommitId( Repository repository,
String branchOrTagOrCommitId ) throws IOException {
ObjectId objId = repository.resolve(branchOrTagOrCommitId);
if (objId == null) {
for (String remoteName : connector.remoteNames()) {
String branchRef = branchRefForName(branchOrTagOrCommitId, remoteName);
objId = repository.resolve(branchRef);
if (objId != null) break;
}
}
return objId;
}
/**
* Add the names of the branches as children of the current node.
*
* @param git the Git object; may not be null
* @param spec the call specification; may not be null
* @param writer the document writer for the current node; may not be null
* @throws GitAPIException if there is a problem accessing the Git repository
*/
protected void addBranchesAsChildren( Git git,
CallSpecification spec,
DocumentWriter writer ) throws GitAPIException {
Set<String> remoteBranchPrefixes = remoteBranchPrefixes();
if (remoteBranchPrefixes.isEmpty()) {
// Generate the child references to the LOCAL branches, which will be sorted by name ...
ListBranchCommand command = git.branchList();
List<Ref> branches = command.call();
// Reverse the sort of the branch names, since they might be version numbers ...
Collections.sort(branches, REVERSE_REF_COMPARATOR);
for (Ref ref : branches) {
String name = ref.getName();
name = name.replace(GitFunction.LOCAL_BRANCH_PREFIX, "");
writer.addChild(spec.childId(name), name);
}
return;
}
// There is at least one REMOTE branch, so generate the child references to the REMOTE branches,
// which will be sorted by name (by the command)...
ListBranchCommand command = git.branchList();
command.setListMode(ListMode.REMOTE);
List<Ref> branches = command.call();
// Reverse the sort of the branch names, since they might be version numbers ...
Collections.sort(branches, REVERSE_REF_COMPARATOR);
Set<String> uniqueNames = new HashSet<String>();
for (Ref ref : branches) {
String name = ref.getName();
if (uniqueNames.contains(name)) continue;
// We only want the branch if it matches one of the listed remotes ...
boolean skip = false;
for (String remoteBranchPrefix : remoteBranchPrefixes) {
if (name.startsWith(remoteBranchPrefix)) {
// Remove the prefix ...
name = name.replaceFirst(remoteBranchPrefix, "");
break;
}
// Otherwise, it's a remote branch from a different remote that we don't want ...
skip = true;
}
if (skip) continue;
if (uniqueNames.add(name)) writer.addChild(spec.childId(name), name);
}
}
/**
* Add the names of the tags as children of the current node.
*
* @param git the Git object; may not be null
* @param spec the call specification; may not be null
* @param writer the document writer for the current node; may not be null
* @throws GitAPIException if there is a problem accessing the Git repository
*/
protected void addTagsAsChildren( Git git,
CallSpecification spec,
DocumentWriter writer ) throws GitAPIException {
// Generate the child references to the branches, which will be sorted by name (by the command).
ListTagCommand command = git.tagList();
List<Ref> tags = command.call();
// Reverse the sort of the branch names, since they might be version numbers ...
Collections.sort(tags, REVERSE_REF_COMPARATOR);
for (Ref ref : tags) {
String fullName = ref.getName();
String name = fullName.replaceFirst(TAG_PREFIX, "");
writer.addChild(spec.childId(name), name);
}
}
/**
* Add the first page of commits in the history names of the tags as children of the current node.
*
* @param git the Git object; may not be null
* @param spec the call specification; may not be null
* @param writer the document writer for the current node; may not be null
* @param pageSize the number of commits to include, and the number of commits that will be in the next page (if there are
* more commits)
* @throws GitAPIException if there is a problem accessing the Git repository
*/
protected void addCommitsAsChildren( Git git,
CallSpecification spec,
DocumentWriter writer,
int pageSize ) throws GitAPIException {
// Add commits in the log ...
LogCommand command = git.log();
command.setSkip(0);
command.setMaxCount(pageSize);
// Add the first set of commits ...
int actual = 0;
String commitId = null;
for (RevCommit commit : command.call()) {
commitId = commit.getName();
writer.addChild(spec.childId(commitId), commitId);
++actual;
}
if (actual == pageSize) {
// We wrote the maximum number of commits, so there's (probably) another page ...
writer.addPage(spec.getId(), commitId, pageSize, PageWriter.UNKNOWN_TOTAL_SIZE);
}
}
/**
* Add an additional page of commits in the history names of the tags as children of the current node.
*
* @param git the Git object; may not be null
* @param repository the Repository object; may not be null
* @param spec the call specification; may not be null
* @param writer the page writer for the current node; may not be null
* @param pageKey the page key for this page; may not be null
* @throws GitAPIException if there is a problem accessing the Git repository
* @throws IOException if there is a problem reading the Git repository
*/
protected void addCommitsAsPageOfChildren( Git git,
Repository repository,
CallSpecification spec,
PageWriter writer,
PageKey pageKey ) throws GitAPIException, IOException {
RevWalk walker = new RevWalk(repository);
try {
// The offset is the ID of the last commit we read, so we'll need to skip the first commit
String lastCommitIdName = pageKey.getOffsetString();
ObjectId lastCommitId = repository.resolve(lastCommitIdName);
int pageSize = (int)pageKey.getBlockSize();
LogCommand command = git.log();
command.add(lastCommitId);
command.setMaxCount(pageSize + 1);
// Add the first set of commits ...
int actual = 0;
String commitId = null;
for (RevCommit commit : command.call()) {
commitId = commit.getName();
if (commitId.equals(lastCommitIdName)) continue;
writer.addChild(spec.childId(commitId), commitId);
++actual;
}
if (actual == pageSize) {
assert commitId != null;
// We wrote the maximum number of commits, so there's (probably) another page ...
writer.addPage(pageKey.getParentId(), commitId, pageSize, PageWriter.UNKNOWN_TOTAL_SIZE);
}
} finally {
walker.dispose();
}
}
protected boolean isQueryable( CallSpecification callSpec ) {
// by default, a git function does not return queryable content
return false;
}
protected String authorName( RevCommit commit ) {
PersonIdent authorIdent = commit.getAuthorIdent();
return authorIdent != null ? authorIdent.getName() : "<unknown>";
}
protected String commiterName( RevCommit commit ) {
PersonIdent committerIdent = commit.getCommitterIdent();
return committerIdent != null ? committerIdent.getName() : "<unknown>";
}
@Override
public String toString() {
return getName();
}
}