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}