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 javax.jcr.RepositoryException; 020import org.eclipse.jgit.api.Git; 021import org.eclipse.jgit.api.errors.GitAPIException; 022import org.eclipse.jgit.lib.ObjectId; 023import org.eclipse.jgit.lib.ObjectLoader; 024import org.eclipse.jgit.lib.Repository; 025import org.eclipse.jgit.revwalk.RevCommit; 026import org.eclipse.jgit.revwalk.RevWalk; 027import org.eclipse.jgit.treewalk.TreeWalk; 028import org.eclipse.jgit.treewalk.filter.PathFilter; 029import org.modeshape.jcr.JcrLexicon; 030import org.modeshape.jcr.api.value.DateTime; 031import org.modeshape.jcr.spi.federation.DocumentWriter; 032import org.modeshape.jcr.spi.federation.PageKey; 033import org.modeshape.jcr.spi.federation.PageWriter; 034import org.modeshape.jcr.value.BinaryValue; 035import org.modeshape.schematic.document.Document; 036 037/** 038 * A function that returns the file and directory structure within a particular commit. The structure of this area of the 039 * repository is as follows: 040 * 041 * <pre> 042 * /tree/{branchOrTagOrObjectId}/{filesAndFolders}/... 043 * </pre> 044 */ 045public class GitTree extends GitFunction implements PageableGitFunction { 046 047 protected static final String JCR_CONTENT = "jcr:content"; 048 protected static final String JCR_CONTENT_SUFFIX = "/" + JCR_CONTENT; 049 050 protected static final String NAME = "tree"; 051 protected static final String ID = "/tree"; 052 053 protected static Object referenceToTree( ObjectId commitId, 054 String branchOrTagOrCommitId, 055 Values values ) { 056 return values.referenceTo(ID + DELIMITER + branchOrTagOrCommitId); 057 } 058 059 public GitTree( GitConnector connector ) { 060 super(NAME, connector); 061 } 062 063 @Override 064 public Document execute( Repository repository, 065 Git git, 066 CallSpecification spec, 067 DocumentWriter writer, 068 Values values ) throws GitAPIException, IOException { 069 if (spec.parameterCount() == 0) { 070 // This is the top-level "/branches" node 071 writer.setPrimaryType(GitLexicon.TREES); 072 073 // Generate the child references to the branches and tags. Branches are likely used more often, so list them first... 074 addBranchesAsChildren(git, spec, writer); 075 addTagsAsChildren(git, spec, writer); 076 addCommitsAsChildren(git, spec, writer, pageSize); 077 078 } else if (spec.parameterCount() == 1) { 079 // This is a particular branch/tag/commit node ... 080 String branchOrTagOrObjectId = spec.parameter(0); 081 ObjectId objId = resolveBranchOrTagOrCommitId(repository, branchOrTagOrObjectId); 082 RevWalk walker = new RevWalk(repository); 083 walker.setRetainBody(true); // we need to parse the commit for the top-level 084 try { 085 RevCommit commit = walker.parseCommit(objId); 086 087 // could happen if not enough permissions, for example 088 if (commit != null) { 089 // Add the properties for this node ... 090 String committer = commiterName(commit); 091 String author = authorName(commit); 092 DateTime committed = values.dateFrom(commit.getCommitTime()); 093 writer.setPrimaryType(GitLexicon.FOLDER); 094 writer.addProperty(JcrLexicon.CREATED, committed); 095 writer.addProperty(JcrLexicon.CREATED_BY, committer); 096 writer.addProperty(GitLexicon.OBJECT_ID, objId.name()); 097 writer.addProperty(GitLexicon.AUTHOR, author); 098 writer.addProperty(GitLexicon.COMMITTER, committer); 099 writer.addProperty(GitLexicon.COMMITTED, committed); 100 writer.addProperty(GitLexicon.TITLE, commit.getShortMessage()); 101 writer.addProperty(GitLexicon.HISTORY, GitHistory.referenceToHistory(objId, branchOrTagOrObjectId, values)); 102 writer.addProperty(GitLexicon.DETAIL, GitCommitDetails.referenceToCommit(objId, values)); 103 104 // Add the top-level children of the directory ... 105 addInformationForPath(repository, writer, commit, "", spec, values); 106 } else { 107 connector.getLogger().warn(GitI18n.cannotReadCommit, objId); 108 109 } 110 } finally { 111 walker.dispose(); 112 } 113 114 } else { 115 // This is a folder or file within the directory structure ... 116 String branchOrTagOrObjectId = spec.parameter(0); 117 String path = spec.parametersAsPath(1); 118 ObjectId objId = resolveBranchOrTagOrCommitId(repository, branchOrTagOrObjectId); 119 RevWalk walker = new RevWalk(repository); 120 walker.setRetainBody(true); 121 try { 122 // Get the commit information ... 123 RevCommit commit = walker.parseCommit(objId); 124 125 if (commit != null) { 126 // Add the top-level children of the directory ... 127 addInformationForPath(repository, writer, commit, path, spec, values); 128 } 129 } finally { 130 walker.dispose(); 131 } 132 } 133 return writer.document(); 134 } 135 136 protected void addInformationForPath( Repository repository, 137 DocumentWriter writer, 138 RevCommit commit, 139 String path, 140 CallSpecification spec, 141 Values values ) throws GitAPIException, IOException { 142 // Make sure the path is in the canonical form we need ... 143 if (path.startsWith("/")) { 144 if (path.length() == 1) path = ""; 145 else path = path.substring(1); 146 } 147 148 // Now see if we're actually referring to the "jcr:content" node ... 149 boolean isContentNode = false; 150 if (path.endsWith(JCR_CONTENT_SUFFIX)) { 151 isContentNode = true; 152 path = path.substring(0, path.length() - JCR_CONTENT_SUFFIX.length()); 153 } 154 155 // Create the TreeWalk that we'll use to navigate the files/directories ... 156 final TreeWalk tw = new TreeWalk(repository); 157 tw.addTree(commit.getTree()); 158 if ("".equals(path)) { 159 // This is the top-level directory, so we don't need to pre-walk to find anything ... 160 tw.setRecursive(false); 161 while (tw.next()) { 162 String childName = tw.getNameString(); 163 String childId = spec.childId(childName); 164 writer.addChild(childId, childName); 165 } 166 } else { 167 // We need to first find our path *before* we can walk the children ... 168 PathFilter filter = PathFilter.create(path); 169 tw.setFilter(filter); 170 while (tw.next()) { 171 if (filter.isDone(tw)) { 172 break; 173 } else if (tw.isSubtree()) { 174 tw.enterSubtree(); 175 } 176 } 177 // Now that the TreeWalk is the in right location given by the 'path', we can get the 178 if (tw.isSubtree()) { 179 // The object at the 'path' is a directory, so go into it ... 180 tw.enterSubtree(); 181 182 // Find the commit in which this folder was last modified ... 183 // This may not be terribly efficient, but it seems to work faster on subsequent runs ... 184 writer.setPrimaryType(GitLexicon.FOLDER); 185 186 // Add folder-related properties ... 187 String committer = commiterName(commit); 188 String author = authorName(commit); 189 DateTime committed = values.dateFrom(commit.getCommitTime()); 190 writer.addProperty(JcrLexicon.CREATED, committed); 191 writer.addProperty(JcrLexicon.CREATED_BY, committer); 192 writer.addProperty(GitLexicon.OBJECT_ID, commit.getId().name()); 193 writer.addProperty(GitLexicon.AUTHOR, author); 194 writer.addProperty(GitLexicon.COMMITTER, committer); 195 writer.addProperty(GitLexicon.COMMITTED, committed); 196 writer.addProperty(GitLexicon.TITLE, commit.getShortMessage()); 197 198 // And now walk the contents of the directory ... 199 while (tw.next()) { 200 String childName = tw.getNameString(); 201 String childId = spec.childId(childName); 202 writer.addChild(childId, childName); 203 } 204 } else { 205 // The path specifies a file (or a content node) ... 206 207 if (isContentNode) { 208 writer.setPrimaryType(GitLexicon.RESOURCE); 209 210 // Add file-related properties ... 211 String committer = commiterName(commit); 212 String author = authorName(commit); 213 DateTime committed = values.dateFrom(commit.getCommitTime()); 214 215 writer.addProperty(JcrLexicon.LAST_MODIFIED, committed); 216 writer.addProperty(JcrLexicon.LAST_MODIFIED_BY, committer); 217 writer.addProperty(GitLexicon.OBJECT_ID, commit.getId().name()); 218 writer.addProperty(GitLexicon.AUTHOR, author); 219 writer.addProperty(GitLexicon.COMMITTER, committer); 220 writer.addProperty(GitLexicon.COMMITTED, committed); 221 writer.addProperty(GitLexicon.TITLE, commit.getShortMessage()); 222 // Create the BinaryValue ... 223 ObjectId fileObjectId = tw.getObjectId(0); 224 ObjectLoader fileLoader = repository.open(fileObjectId); 225 // we'll always create an external binary which will be resolved by the connector when required 226 BinaryValue value = new GitBinaryValue(fileObjectId, fileLoader, connector.getSourceName(), name, 227 connector.getMimeTypeDetector()); 228 writer.addProperty(JcrLexicon.DATA, value); 229 if (connector.includeMimeType()) { 230 try { 231 String filename = spec.parameter(spec.parameterCount() - 1); // the last is 'jcr:content' 232 String mimeType = value.getMimeType(filename); 233 if (mimeType != null) writer.addProperty(JcrLexicon.MIMETYPE, mimeType); 234 } catch (RepositoryException | IOException e) { 235 // do nothing 236 connector.getLogger().debug("cannot determine mime-type information for objectID '{0}'", fileObjectId); 237 } 238 } 239 } else { 240 writer.setPrimaryType(GitLexicon.FILE); 241 242 // Add file-related properties ... 243 String committer = commiterName(commit); 244 String author = authorName(commit); 245 DateTime committed = values.dateFrom(commit.getCommitTime()); 246 247 writer.addProperty(JcrLexicon.CREATED, committed); 248 writer.addProperty(JcrLexicon.CREATED_BY, committer); 249 writer.addProperty(GitLexicon.OBJECT_ID, commit.getId().name()); 250 writer.addProperty(GitLexicon.AUTHOR, author); 251 writer.addProperty(GitLexicon.COMMITTER, committer); 252 writer.addProperty(GitLexicon.COMMITTED, committed); 253 writer.addProperty(GitLexicon.TITLE, commit.getShortMessage()); 254 255 // Add the "jcr:content" child node ... 256 String childId = spec.childId(JCR_CONTENT); 257 writer.addChild(childId, JCR_CONTENT); 258 } 259 } 260 } 261 } 262 263 @Override 264 public boolean isPaged() { 265 return true; 266 } 267 268 @Override 269 public Document execute( Repository repository, 270 Git git, 271 CallSpecification spec, 272 PageWriter writer, 273 Values values, 274 PageKey pageKey ) throws GitAPIException, IOException { 275 if (spec.parameterCount() != 0) return null; 276 addCommitsAsPageOfChildren(git, repository, spec, writer, pageKey); 277 return writer.document(); 278 } 279}