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}