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.ByteArrayOutputStream;
019import java.io.IOException;
020import java.util.Collections;
021import java.util.List;
022import org.eclipse.jgit.api.Git;
023import org.eclipse.jgit.api.errors.GitAPIException;
024import org.eclipse.jgit.diff.DiffEntry;
025import org.eclipse.jgit.diff.DiffFormatter;
026import org.eclipse.jgit.errors.CorruptObjectException;
027import org.eclipse.jgit.errors.IncorrectObjectTypeException;
028import org.eclipse.jgit.errors.MissingObjectException;
029import org.eclipse.jgit.lib.ObjectId;
030import org.eclipse.jgit.lib.Repository;
031import org.eclipse.jgit.revwalk.RevCommit;
032import org.eclipse.jgit.revwalk.RevWalk;
033import org.eclipse.jgit.treewalk.TreeWalk;
034import org.modeshape.schematic.document.Document;
035import org.modeshape.jcr.spi.federation.DocumentWriter;
036import org.modeshape.jcr.spi.federation.PageKey;
037import org.modeshape.jcr.spi.federation.PageWriter;
038
039/**
040 * A {@link GitFunction} that returns the information about a particular commit. The structure of this area of the repository is
041 * as follows:
042 * 
043 * <pre>
044 *   /commit/{branchOrTagNameOrObjectId}
045 * </pre>
046 */
047public class GitCommitDetails extends GitFunction implements PageableGitFunction {
048
049    /**
050     * The name of the character set that is used when building the patch difference for a commit.
051     */
052    public static final String DIFF_CHARSET_NAME = "UTF-8";
053
054    protected static final String NAME = "commit";
055    protected static final String ID = "/commit";
056
057    protected static Object referenceToCommit( ObjectId id,
058                                               Values values ) {
059        return values.referenceTo(ID + DELIMITER + id.getName());
060    }
061
062    protected static Object[] referencesToCommits( ObjectId[] ids,
063                                                   Values values ) {
064        int size = ids.length;
065        Object[] results = new Object[size];
066        for (int i = 0; i != size; ++i) {
067            results[i] = referenceToCommit(ids[i], values);
068        }
069        return results;
070    }
071
072    public GitCommitDetails( GitConnector connector ) {
073        super(NAME, connector);
074    }
075
076    @Override
077    public Document execute( Repository repository,
078                             Git git,
079                             CallSpecification spec,
080                             DocumentWriter writer,
081                             Values values ) throws GitAPIException, IOException {
082        if (spec.parameterCount() == 0) {
083            // This is the top-level "/commit" node
084            writer.setPrimaryType(GitLexicon.DETAILS);
085
086            // Generate the child references to the branches, tags, and commits in the history ...
087            addBranchesAsChildren(git, spec, writer);
088            addTagsAsChildren(git, spec, writer);
089            addCommitsAsChildren(git, spec, writer, pageSize);
090
091        } else if (spec.parameterCount() == 1) {
092            // This is the top-level "/commit/{branchOrTagNameOrObjectId}" node
093            writer.setPrimaryType(GitLexicon.DETAILED_COMMIT);
094
095            // Add the properties describing this commit ...
096            RevWalk walker = new RevWalk(repository);
097            walker.setRetainBody(true);
098            try {
099                String branchOrTagOrCommitId = spec.parameter(0);
100                ObjectId objId = resolveBranchOrTagOrCommitId(repository, branchOrTagOrCommitId);
101                RevCommit commit = walker.parseCommit(objId);
102                writer.addProperty(GitLexicon.OBJECT_ID, objId.name());
103                writer.addProperty(GitLexicon.AUTHOR, authorName(commit));
104                writer.addProperty(GitLexicon.COMMITTER, commiterName(commit));
105                writer.addProperty(GitLexicon.COMMITTED, values.dateFrom(commit.getCommitTime()));
106                writer.addProperty(GitLexicon.TITLE, commit.getShortMessage());
107                writer.addProperty(GitLexicon.MESSAGE, commit.getFullMessage().trim());// removes trailing whitespace
108                writer.addProperty(GitLexicon.PARENTS, GitCommitDetails.referencesToCommits(commit.getParents(), values));
109                writer.addProperty(GitLexicon.TREE, GitTree.referenceToTree(objId, objId.name(), values));
110
111                // Compute the difference between the commit and it's parent(s), and generate the diff/patch file ...
112                List<DiffEntry> differences = computeDifferences(commit, walker, repository);
113                String patchFile = computePatch(differences, repository);
114                writer.addProperty(GitLexicon.DIFF, patchFile);
115
116            } finally {
117                walker.dispose();
118            }
119        } else {
120            return null;
121        }
122
123        return writer.document();
124    }
125
126    @Override
127    public boolean isPaged() {
128        return true;
129    }
130
131    @Override
132    public Document execute( Repository repository,
133                             Git git,
134                             CallSpecification spec,
135                             PageWriter writer,
136                             Values values,
137                             PageKey pageKey ) throws GitAPIException, IOException {
138        if (spec.parameterCount() != 0) return null;
139        addCommitsAsPageOfChildren(git, repository, spec, writer, pageKey);
140        return writer.document();
141    }
142
143    protected List<DiffEntry> computeDifferences( RevCommit commit,
144                                                  RevWalk walker,
145                                                  Repository repository )
146        throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException {
147        // Set up the tree walk to obtain the difference between the commit and it's parent(s) ...
148        TreeWalk tw = new TreeWalk(repository);
149        tw.setRecursive(true);
150        tw.addTree(commit.getTree());
151
152        RevCommit[] parents = commit.getParents();
153
154        for (RevCommit parent : parents) {
155            RevCommit parentCommit = walker.parseCommit(parent);
156            tw.addTree(parentCommit.getTree());
157            //if there are multiple parents, we can't really have a multiple-way diff so we'll only look at the first parent
158            if (parents.length > 1) {
159                connector.getLogger().warn(GitI18n.commitWithMultipleParents, commit.getName(), parentCommit.getName());
160                break;
161            }
162        }
163
164        if (tw.getTreeCount() == 1) {
165            connector.getLogger().warn(GitI18n.commitWithSingleParent, commit.getName(), tw.getObjectId(0).name());
166            return Collections.emptyList();
167        }
168
169        // Now process the diff of each file ...
170        return DiffEntry.scan(tw);
171    }
172
173    protected String computePatch( Iterable<DiffEntry> entries,
174                                   Repository repository ) throws IOException {
175        ByteArrayOutputStream output = new ByteArrayOutputStream();
176        DiffFormatter formatter = new DiffFormatter(output);
177        formatter.setRepository(repository);
178        for (DiffEntry entry : entries) {
179            formatter.format(entry);
180        }
181        return output.toString(DIFF_CHARSET_NAME);
182    }
183
184}