001/* 002 * ModeShape (http://www.modeshape.org) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.modeshape.connector.git; 017 018import java.io.IOException; 019import java.util.Collections; 020import java.util.Comparator; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Set; 024import org.eclipse.jgit.api.Git; 025import org.eclipse.jgit.api.ListBranchCommand; 026import org.eclipse.jgit.api.ListBranchCommand.ListMode; 027import org.eclipse.jgit.api.ListTagCommand; 028import org.eclipse.jgit.api.LogCommand; 029import org.eclipse.jgit.api.errors.GitAPIException; 030import org.eclipse.jgit.lib.ObjectId; 031import org.eclipse.jgit.lib.PersonIdent; 032import org.eclipse.jgit.lib.Ref; 033import org.eclipse.jgit.lib.Repository; 034import org.eclipse.jgit.revwalk.RevCommit; 035import org.eclipse.jgit.revwalk.RevWalk; 036import org.modeshape.schematic.document.Document; 037import org.modeshape.jcr.spi.federation.DocumentWriter; 038import org.modeshape.jcr.spi.federation.PageKey; 039import org.modeshape.jcr.spi.federation.PageWriter; 040 041/** 042 * 043 */ 044public abstract class GitFunction { 045 046 protected static final String DELIMITER = "/"; 047 protected static final String REMOTE_BRANCH_PREFIX = "refs/remotes/"; 048 protected static final String LOCAL_BRANCH_PREFIX = "refs/heads/"; 049 protected static final String TAG_PREFIX = "refs/tags/"; 050 protected static final int DEFAULT_PAGE_SIZE = 15; 051 052 protected static final Comparator<Ref> REVERSE_REF_COMPARATOR = new Comparator<Ref>() { 053 @Override 054 public int compare( Ref o1, 055 Ref o2 ) { 056 return 0 - o1.getName().compareTo(o2.getName()); 057 } 058 }; 059 060 protected final String name; 061 protected final GitConnector connector; 062 protected int pageSize = DEFAULT_PAGE_SIZE; 063 064 protected GitFunction( String name, 065 GitConnector connector ) { 066 this.name = name; 067 this.connector = connector; 068 } 069 070 /** 071 * Get the name of this function. 072 * 073 * @return the name; never null 074 */ 075 public String getName() { 076 return name; 077 } 078 079 public boolean isPaged() { 080 return false; 081 } 082 083 public abstract Document execute( Repository repository, 084 Git git, 085 CallSpecification spec, 086 DocumentWriter writer, 087 Values values ) throws GitAPIException, IOException; 088 089 private Set<String> remoteBranchPrefixes() { 090 Set<String> prefixes = new HashSet<String>(); 091 for (String remoteName : connector.remoteNames()) { 092 String prefix = remoteBranchPrefix(remoteName); 093 prefixes.add(prefix); 094 } 095 return prefixes; 096 } 097 098 private String remoteBranchPrefix( String remoteName ) { 099 return REMOTE_BRANCH_PREFIX + remoteName + "/"; 100 } 101 102 /** 103 * Obtain the name of the branch reference 104 * 105 * @param branchName 106 * @return the branch ref name 107 */ 108 protected String branchRefForName( String branchName ) { 109 String remoteName = connector.remoteName(); 110 return remoteName != null ? remoteBranchPrefix(remoteName) + branchName : LOCAL_BRANCH_PREFIX + branchName; 111 } 112 113 /** 114 * Obtain the name of the branch reference 115 * 116 * @param branchName 117 * @param remoteName the name of the remote 118 * @return the branch ref name 119 */ 120 protected String branchRefForName( String branchName, 121 String remoteName ) { 122 return remoteBranchPrefix(remoteName) + branchName; 123 } 124 125 /** 126 * Resolve the branch name, tag name, or commit ID into the appropriate ObjectId. Note that the branch names are assumed to be 127 * from the {@link GitConnector#remoteName() remote}. 128 * 129 * @param repository the Repository object; may not be null 130 * @param branchOrTagOrCommitId the branch name, tag name, or commit ID; may not be null 131 * @return the resolved ObjectId, or null if the supplied string does not resolve to an object ID 132 * @throws IOException if there is a problem reading the Git repository 133 */ 134 protected ObjectId resolveBranchOrTagOrCommitId( Repository repository, 135 String branchOrTagOrCommitId ) throws IOException { 136 ObjectId objId = repository.resolve(branchOrTagOrCommitId); 137 if (objId == null) { 138 for (String remoteName : connector.remoteNames()) { 139 String branchRef = branchRefForName(branchOrTagOrCommitId, remoteName); 140 objId = repository.resolve(branchRef); 141 if (objId != null) break; 142 } 143 } 144 return objId; 145 } 146 147 /** 148 * Add the names of the branches as children of the current node. 149 * 150 * @param git the Git object; may not be null 151 * @param spec the call specification; may not be null 152 * @param writer the document writer for the current node; may not be null 153 * @throws GitAPIException if there is a problem accessing the Git repository 154 */ 155 protected void addBranchesAsChildren( Git git, 156 CallSpecification spec, 157 DocumentWriter writer ) throws GitAPIException { 158 Set<String> remoteBranchPrefixes = remoteBranchPrefixes(); 159 if (remoteBranchPrefixes.isEmpty()) { 160 // Generate the child references to the LOCAL branches, which will be sorted by name ... 161 ListBranchCommand command = git.branchList(); 162 List<Ref> branches = command.call(); 163 // Reverse the sort of the branch names, since they might be version numbers ... 164 Collections.sort(branches, REVERSE_REF_COMPARATOR); 165 for (Ref ref : branches) { 166 String name = ref.getName(); 167 name = name.replace(GitFunction.LOCAL_BRANCH_PREFIX, ""); 168 writer.addChild(spec.childId(name), name); 169 } 170 return; 171 } 172 // There is at least one REMOTE branch, so generate the child references to the REMOTE branches, 173 // which will be sorted by name (by the command)... 174 ListBranchCommand command = git.branchList(); 175 command.setListMode(ListMode.REMOTE); 176 List<Ref> branches = command.call(); 177 // Reverse the sort of the branch names, since they might be version numbers ... 178 Collections.sort(branches, REVERSE_REF_COMPARATOR); 179 Set<String> uniqueNames = new HashSet<String>(); 180 for (Ref ref : branches) { 181 String name = ref.getName(); 182 if (uniqueNames.contains(name)) continue; 183 // We only want the branch if it matches one of the listed remotes ... 184 boolean skip = false; 185 for (String remoteBranchPrefix : remoteBranchPrefixes) { 186 if (name.startsWith(remoteBranchPrefix)) { 187 // Remove the prefix ... 188 name = name.replaceFirst(remoteBranchPrefix, ""); 189 break; 190 } 191 // Otherwise, it's a remote branch from a different remote that we don't want ... 192 skip = true; 193 } 194 if (skip) continue; 195 if (uniqueNames.add(name)) writer.addChild(spec.childId(name), name); 196 } 197 } 198 199 /** 200 * Add the names of the tags as children of the current node. 201 * 202 * @param git the Git object; may not be null 203 * @param spec the call specification; may not be null 204 * @param writer the document writer for the current node; may not be null 205 * @throws GitAPIException if there is a problem accessing the Git repository 206 */ 207 protected void addTagsAsChildren( Git git, 208 CallSpecification spec, 209 DocumentWriter writer ) throws GitAPIException { 210 // Generate the child references to the branches, which will be sorted by name (by the command). 211 ListTagCommand command = git.tagList(); 212 List<Ref> tags = command.call(); 213 // Reverse the sort of the branch names, since they might be version numbers ... 214 Collections.sort(tags, REVERSE_REF_COMPARATOR); 215 for (Ref ref : tags) { 216 String fullName = ref.getName(); 217 String name = fullName.replaceFirst(TAG_PREFIX, ""); 218 writer.addChild(spec.childId(name), name); 219 } 220 } 221 222 /** 223 * Add the first page of commits in the history names of the tags as children of the current node. 224 * 225 * @param git the Git object; may not be null 226 * @param spec the call specification; may not be null 227 * @param writer the document writer for the current node; may not be null 228 * @param pageSize the number of commits to include, and the number of commits that will be in the next page (if there are 229 * more commits) 230 * @throws GitAPIException if there is a problem accessing the Git repository 231 */ 232 protected void addCommitsAsChildren( Git git, 233 CallSpecification spec, 234 DocumentWriter writer, 235 int pageSize ) throws GitAPIException { 236 // Add commits in the log ... 237 LogCommand command = git.log(); 238 command.setSkip(0); 239 command.setMaxCount(pageSize); 240 241 // Add the first set of commits ... 242 int actual = 0; 243 String commitId = null; 244 for (RevCommit commit : command.call()) { 245 commitId = commit.getName(); 246 writer.addChild(spec.childId(commitId), commitId); 247 ++actual; 248 } 249 if (actual == pageSize) { 250 // We wrote the maximum number of commits, so there's (probably) another page ... 251 writer.addPage(spec.getId(), commitId, pageSize, PageWriter.UNKNOWN_TOTAL_SIZE); 252 } 253 } 254 255 /** 256 * Add an additional page of commits in the history names of the tags as children of the current node. 257 * 258 * @param git the Git object; may not be null 259 * @param repository the Repository object; may not be null 260 * @param spec the call specification; may not be null 261 * @param writer the page writer for the current node; may not be null 262 * @param pageKey the page key for this page; may not be null 263 * @throws GitAPIException if there is a problem accessing the Git repository 264 * @throws IOException if there is a problem reading the Git repository 265 */ 266 protected void addCommitsAsPageOfChildren( Git git, 267 Repository repository, 268 CallSpecification spec, 269 PageWriter writer, 270 PageKey pageKey ) throws GitAPIException, IOException { 271 RevWalk walker = new RevWalk(repository); 272 try { 273 // The offset is the ID of the last commit we read, so we'll need to skip the first commit 274 String lastCommitIdName = pageKey.getOffsetString(); 275 ObjectId lastCommitId = repository.resolve(lastCommitIdName); 276 int pageSize = (int)pageKey.getBlockSize(); 277 278 LogCommand command = git.log(); 279 command.add(lastCommitId); 280 command.setMaxCount(pageSize + 1); 281 // Add the first set of commits ... 282 int actual = 0; 283 String commitId = null; 284 for (RevCommit commit : command.call()) { 285 commitId = commit.getName(); 286 if (commitId.equals(lastCommitIdName)) continue; 287 writer.addChild(spec.childId(commitId), commitId); 288 ++actual; 289 } 290 if (actual == pageSize) { 291 assert commitId != null; 292 // We wrote the maximum number of commits, so there's (probably) another page ... 293 writer.addPage(pageKey.getParentId(), commitId, pageSize, PageWriter.UNKNOWN_TOTAL_SIZE); 294 } 295 } finally { 296 walker.dispose(); 297 } 298 } 299 300 protected boolean isQueryable( CallSpecification callSpec ) { 301 // by default, a git function does not return queryable content 302 return false; 303 } 304 305 protected String authorName( RevCommit commit ) { 306 PersonIdent authorIdent = commit.getAuthorIdent(); 307 return authorIdent != null ? authorIdent.getName() : "<unknown>"; 308 } 309 310 protected String commiterName( RevCommit commit ) { 311 PersonIdent committerIdent = commit.getCommitterIdent(); 312 return committerIdent != null ? committerIdent.getName() : "<unknown>"; 313 } 314 315 @Override 316 public String toString() { 317 return getName(); 318 } 319 320}