/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* See LICENSE.txt included in this distribution for the specific
* language governing permissions and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at LICENSE.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright (c) 2008, 2014, Oracle and/or its affiliates. All rights reserved.
*/
package org.opensolaris.opengrok.history;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.opensolaris.opengrok.OpenGrokLogger;
import org.opensolaris.opengrok.configuration.RuntimeEnvironment;
import org.opensolaris.opengrok.util.Executor;
/**
* An interface for an external repository.
*
* @author Trond Norbye
*/
public abstract class Repository extends RepositoryInfo {
/**
* The command with which to access the external repository. Can be
* {@code null} if the repository isn't accessed via a CLI, or if it
* hasn't been initialized by {@link #ensureCommand} yet.
*/
protected String cmd;
/**
* List of <revision, tags> pairs for repositories which display tags
* only for files changed by the tagged commit.
*/
protected TreeSet<TagEntry> tagList = null;
abstract boolean fileHasHistory(File file);
/**
* Check if the repository supports {@code getHistory()} requests for
* whole directories at once.
*
* @return {@code true} if the repository can get history for directories
*/
abstract boolean hasHistoryForDirectories();
/**
* Get the history log for the specified file or directory.
* @param file the file to get the history for
* @return history log for file
* @throws HistoryException on error accessing the history
*/
abstract History getHistory(File file) throws HistoryException;
/**
* <p>
* Get the history after a specified revision.
* </p>
*
* <p>
* The default implementation first fetches the full history and then
* throws away the oldest revisions. This is not efficient, so subclasses
* should override it in order to get good performance. Once every subclass
* has implemented a more efficient method, the default implementation
* should be removed and made abstract.
* </p>
*
* @param file the file to get the history for
* @param sinceRevision the revision right before the first one to return,
* or {@code null} to return the full history
* @return partial history for file
* @throws HistoryException on error accessing the history
*/
History getHistory(File file, String sinceRevision)
throws HistoryException {
// If we want an incremental history update and get here, warn that
// it may be slow.
if (sinceRevision != null) {
Logger logger = OpenGrokLogger.getLogger();
logger.log(Level.WARNING,
"Incremental history retrieval is not implemented for {0}.",
getClass().getSimpleName());
logger.log(Level.WARNING,
"Falling back to slower full history retrieval.");
}
History history = getHistory(file);
if (sinceRevision == null) {
return history;
}
List<HistoryEntry> partial = new ArrayList<HistoryEntry>();
for (HistoryEntry entry : history.getHistoryEntries()) {
partial.add(entry);
if (sinceRevision.equals(entry.getRevision())) {
// Found revision right before the first one to return.
break;
}
}
removeAndVerifyOldestChangeset(partial, sinceRevision);
history.setHistoryEntries(partial);
return history;
}
/**
* Remove the oldest changeset from a list (assuming sorted with most
* recent changeset first) and verify that it is the changeset we expected
* to find there.
*
* @param entries a list of {@code HistoryEntry} objects
* @param revision the revision we expect the oldest entry to have
* @throws HistoryException if the oldest entry was not the one we expected
*/
void removeAndVerifyOldestChangeset(List<HistoryEntry> entries,
String revision)
throws HistoryException {
HistoryEntry entry =
entries.isEmpty() ? null : entries.remove(entries.size() - 1);
// TODO We should check more thoroughly that the changeset is the one
// we expected it to be, since some SCMs may change the revision
// numbers so that identical revision numbers does not always mean
// identical changesets. We could for example get the cached changeset
// and compare more fields, like author and date.
if (entry == null || !revision.equals(entry.getRevision())) {
throw new HistoryException("Cached revision '" + revision +
"' not found in the repository");
}
}
/**
* Get an input stream that I may use to read a specific version of a
* named file.
* @param parent the name of the directory containing the file
* @param basename the name of the file to get
* @param rev the revision to get
* @return An input stream containing the correct revision.
*/
abstract InputStream getHistoryGet(
String parent, String basename, String rev);
/**
* Checks whether this parser can annotate files.
*
* @return <code>true</code> if annotation is supported
*/
abstract boolean fileHasAnnotation(File file);
/**
* Returns if this repository tags only files changed in last commit, i.e.
* if we need to prepare list of repository-wide tags prior to creation of
* file history entries.
* @return True if we need tag list creation prior to file parsing,
* false by default.
*/
boolean hasFileBasedTags() {
return false;
}
TreeSet<TagEntry> getTagList() {
return this.tagList;
}
/**
* Assign tags to changesets they represent
* The complete list of tags must be pre-built using {@code getTagList()}.
* Then this function squeeze all tags to changesets which actually exist
* in the history of given file.
* Must be implemented repository-specific.
* @see getTagList
* @param hist History we want to assign tags to.
*/
void assignTagsInHistory(History hist) throws HistoryException {
if (hist == null) {
return;
}
if (this.getTagList() == null) {
throw new HistoryException("Tag list was not created before assigning tags to changesets!");
}
Iterator<TagEntry> it = this.getTagList().descendingIterator();
TagEntry lastTagEntry = null;
// Go through all commits of given file
for (HistoryEntry ent : hist.getHistoryEntries()) {
// Assign all tags created since the last revision
// Revision in this HistoryEntry must be already specified!
// TODO is there better way to do this? We need to "repeat"
// last element returned by call to next()
while (lastTagEntry != null || it.hasNext()) {
if (lastTagEntry == null) {
lastTagEntry = it.next();
}
if (lastTagEntry.compareTo(ent) >= 0) {
if (ent.getTags() == null) {
ent.setTags(lastTagEntry.getTags());
} else {
ent.setTags(ent.getTags() + ", " + lastTagEntry.getTags());
}
} else {
break;
}
if (it.hasNext()) {
lastTagEntry = it.next();
} else {
lastTagEntry = null;
}
}
}
}
/**
* Create internal list of all tags in this repository.
* @param directory
*/
protected void buildTagList(File directory) {
this.tagList = null;
}
/**
* Annotate the specified revision of a file.
*
* @param file the file to annotate
* @param revision revision of the file. Either {@code null} or a none-empty
* string.
* @return an <code>Annotation</code> object
* @throws java.io.IOException if an error occurs
*/
abstract Annotation annotate(File file, String revision) throws IOException;
/**
* Return revision for annotate view.
*
* @param history_revision full revision
* @return revision string suitable for matching into annotation
*/
protected String getRevisionForAnnotate(String history_revision) {
return history_revision;
}
/**
* Create a history log cache for all files in this repository.
* {@code getHistory()} is used to fetch the history for the entire
* repository. If {@code hasHistoryForDirectories()} returns {@code false},
* this method is a no-op.
*
* @param cache the cache instance in which to store the history log
* @param sinceRevision if non-null, incrementally update the cache with
* all revisions after the specified revision; otherwise, create the full
* history starting with the initial revision
*
* @throws HistoryException on error
*/
final void createCache(HistoryCache cache, String sinceRevision)
throws HistoryException {
if (!isWorking()) {
return;
}
// If we don't have a directory parser, we can't create the cache
// this way. Just give up and return.
if (!hasHistoryForDirectories()) {
Logger.getLogger(getClass().getName()).log(
Level.INFO,
"Skipping creation of history cache for {0}, since retrieval " +
"of history for directories is not implemented for this " +
"repository type.", getDirectoryName());
return;
}
File directory = new File(getDirectoryName());
History history;
try {
history = getHistory(directory, sinceRevision);
} catch (HistoryException he) {
if (sinceRevision == null) {
// Failed to get full history, so fail.
throw he;
}
// Failed to get partial history. This may have been caused
// by changes in the revision numbers since the last update
// (bug #14724) so we'll try to regenerate the cache from
// scratch instead.
OpenGrokLogger.getLogger().log(Level.INFO,
"Failed to get partial history. Attempting to " +
"recreate the history cache from scratch.", he);
history = null;
}
if (sinceRevision != null && history == null) {
// Failed to get partial history, now get full history instead.
history = getHistory(directory);
// Got full history successfully. Clear the history cache so that
// we can recreate it from scratch.
cache.clear(this);
}
// We need to refresh list of tags for incremental reindex.
RuntimeEnvironment env = RuntimeEnvironment.getInstance();
if (env.isTagsEnabled() && this.hasFileBasedTags()) {
this.buildTagList(new File(this.directoryName));
}
if (history != null) {
cache.store(history, this);
}
}
/**
* Update the content in this repository by pulling the changes from the
* upstream repository..
* @throws IOException if an error occurs.
*/
abstract void update() throws IOException;
/**
* Check if this it the right repository type for the given file.
*
* @param file File to check if this is a repository for.
* @return true if this is the correct repository for this file/directory.
*/
abstract boolean isRepositoryFor(File file);
/**
* Returns true if this repository supports sub repositories (a.k.a. forests).
*
* @return true if this repository supports sub repositories
*/
@SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract")
boolean supportsSubRepositories() {
return false;
}
public DateFormat getDateFormat() {
return new SimpleDateFormat(datePattern, Locale.US);
}
static Boolean checkCmd(String... args) {
Executor exec = new Executor(args);
return Boolean.valueOf(exec.exec(false) == 0);
}
/**
* Set the name of the external client command that should be used to
* access the repository wrt. the given parameters. Does nothing, if this
* repository's <var>cmd</var> has already been set (i.e. has a
* non-{@code null} value).
*
* @param propertyKey property key to lookup the corresponding system property.
* @param fallbackCommand the command to use, if lookup fails.
* @return the command to use.
* @see #cmd
*/
protected String ensureCommand(String propertyKey, String fallbackCommand) {
if (cmd != null) {
return cmd;
}
cmd = RuntimeEnvironment.getInstance()
.getRepoCmd(this.getClass().getCanonicalName());
if (cmd == null) {
cmd = System.getProperty(propertyKey, fallbackCommand);
RuntimeEnvironment.getInstance()
.setRepoCmd(this.getClass().getCanonicalName(), cmd);
}
return cmd;
}
}