8N/A/*
8N/A * CDDL HEADER START
8N/A *
8N/A * The contents of this file are subject to the terms of the
8N/A * Common Development and Distribution License (the "License").
8N/A * You may not use this file except in compliance with the License.
8N/A *
8N/A * See LICENSE.txt included in this distribution for the specific
8N/A * language governing permissions and limitations under the License.
8N/A *
8N/A * When distributing Covered Code, include this CDDL HEADER in each
8N/A * file and include the License file at LICENSE.txt.
8N/A * If applicable, add the following below this CDDL HEADER, with the
8N/A * fields enclosed by brackets "[]" replaced with your own identifying
8N/A * information: Portions Copyright [yyyy] [name of copyright owner]
8N/A *
8N/A * CDDL HEADER END
8N/A */
8N/A
8N/A/*
1241N/A * Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
8N/A */
8N/Apackage org.opensolaris.opengrok.history;
8N/A
95N/Aimport java.io.File;
459N/Aimport java.io.IOException;
8N/Aimport java.io.InputStream;
749N/Aimport java.text.DateFormat;
749N/Aimport java.text.SimpleDateFormat;
771N/Aimport java.util.ArrayList;
771N/Aimport java.util.List;
749N/Aimport java.util.Locale;
783N/Aimport java.util.logging.Logger;
1481N/Aimport java.util.logging.Level;
1327N/A
1241N/Aimport org.opensolaris.opengrok.configuration.RuntimeEnvironment;
1182N/Aimport org.opensolaris.opengrok.util.Executor;
8N/A
8N/A/**
1190N/A * An interface for an external repository.
8N/A *
8N/A * @author Trond Norbye
8N/A */
664N/Apublic abstract class Repository extends RepositoryInfo {
232N/A
1461N/A private static final long serialVersionUID = 2089170589187502060L;
1327N/A private static final Logger logger =
1327N/A Logger.getLogger(Repository.class.getName());
1241N/A /**
1241N/A * The command with which to access the external repository. Can be
1241N/A * {@code null} if the repository isn't accessed via a CLI, or if it
1241N/A * hasn't been initialized by {@link #ensureCommand} yet.
1241N/A */
1241N/A protected String cmd;
1241N/A
1461N/A /**
1461N/A * Check if the repository supports {@code getHistory()} requests for
1473N/A * the given file. For performance reasons subclasses should use some meta
1473N/A * information instead of actually hitting the repository to make that
1473N/A * decision.
1473N/A * @param file canonical path incl. source root.
1461N/A *
1461N/A * @return {@code true} if the repository can get history for the given file.
1461N/A */
1473N/A public abstract boolean fileHasHistory(File file);
765N/A
765N/A /**
765N/A * Check if the repository supports {@code getHistory()} requests for
765N/A * whole directories at once.
765N/A *
765N/A * @return {@code true} if the repository can get history for directories
765N/A */
1473N/A public abstract boolean hasHistoryForDirectories();
765N/A
765N/A /**
1473N/A * Get the history log for the specified file or directory for all changesets.
1473N/A * @param file the file to get the history for (canonical path incl. source
1473N/A * root) .
765N/A * @return history log for file
765N/A * @throws HistoryException on error accessing the history
1473N/A * @see #getHistory(File, String)
765N/A */
1473N/A public abstract History getHistory(File file) throws HistoryException;
765N/A
65N/A /**
1473N/A *
771N/A * Get the history after a specified revision.
771N/A * </p>
771N/A *
771N/A * <p>
771N/A * The default implementation first fetches the full history and then
771N/A * throws away the oldest revisions. This is not efficient, so subclasses
771N/A * should override it in order to get good performance. Once every subclass
771N/A * has implemented a more efficient method, the default implementation
771N/A * should be removed and made abstract.
771N/A * </p>
771N/A *
1473N/A * @param file the file to get the history for (canonical path incl. source
1473N/A * root).
771N/A * @param sinceRevision the revision right before the first one to return,
771N/A * or {@code null} to return the full history
771N/A * @return partial history for file
771N/A * @throws HistoryException on error accessing the history
1473N/A * @see #getHistoryGet(String, String, String)
771N/A */
1473N/A public History getHistory(File file, String sinceRevision)
1473N/A throws HistoryException
1473N/A {
783N/A
783N/A // If we want an incremental history update and get here, warn that
783N/A // it may be slow.
783N/A if (sinceRevision != null) {
1327N/A logger.warning("Incremental history retrieval is not implemented for "
1327N/A + getClass().getSimpleName()
1327N/A + ". Falling back to slower full history retrieval.");
783N/A }
783N/A
771N/A History history = getHistory(file);
771N/A
771N/A if (sinceRevision == null) {
771N/A return history;
771N/A }
771N/A
771N/A List<HistoryEntry> partial = new ArrayList<HistoryEntry>();
771N/A for (HistoryEntry entry : history.getHistoryEntries()) {
967N/A partial.add(entry);
771N/A if (sinceRevision.equals(entry.getRevision())) {
967N/A // Found revision right before the first one to return.
967N/A break;
771N/A }
771N/A }
771N/A
967N/A removeAndVerifyOldestChangeset(partial, sinceRevision);
967N/A history.setHistoryEntries(partial);
967N/A return history;
967N/A }
967N/A
967N/A /**
967N/A * Remove the oldest changeset from a list (assuming sorted with most
967N/A * recent changeset first) and verify that it is the changeset we expected
967N/A * to find there.
967N/A *
967N/A * @param entries a list of {@code HistoryEntry} objects
967N/A * @param revision the revision we expect the oldest entry to have
967N/A * @throws HistoryException if the oldest entry was not the one we expected
967N/A */
1461N/A @SuppressWarnings("static-method")
1473N/A void removeAndVerifyOldestChangeset(List<HistoryEntry> entries,
1473N/A String revision) throws HistoryException
1473N/A {
967N/A HistoryEntry entry =
1473N/A entries.isEmpty() ? null : entries.remove(entries.size() - 1);
967N/A
967N/A // TODO We should check more thoroughly that the changeset is the one
967N/A // we expected it to be, since some SCMs may change the revision
967N/A // numbers so that identical revision numbers does not always mean
967N/A // identical changesets. We could for example get the cached changeset
967N/A // and compare more fields, like author and date.
967N/A if (entry == null || !revision.equals(entry.getRevision())) {
1473N/A throw new HistoryException("Cached revision '" + revision
1473N/A + "' not found in the repository");
967N/A }
771N/A }
771N/A
771N/A /**
1473N/A * Get an input stream that can be used to read a speciffic version of a
65N/A * named file.
1473N/A * @param parent The name of the directory (canonical path incl. source root)
1473N/A * containing the file.
1473N/A * @param basename the name of The file to get.
1473N/A * @param rev The revision to get.
1467N/A * @return An input stream containing the correct revision or {@code null}
1467N/A * on error.
65N/A */
1473N/A public abstract InputStream getHistoryGet(
1473N/A String parent, String basename, String rev);
65N/A
65N/A /**
1473N/A * Checks whether this parser is able to annotate files. For performance
1473N/A * reasons subclasses should use some meta information instead of actually
1473N/A * hitting the repository to make that decision.
1473N/A * @param file canonical path incl. source root.
1473N/A * @return <code>true</code> if annotation is supported.
176N/A */
1473N/A public abstract boolean fileHasAnnotation(File file);
176N/A
176N/A /**
95N/A * Annotate the specified revision of a file.
95N/A *
1473N/A * @param file the file to annotate (canonical path incl. source root).
1182N/A * @param revision revision of the file. Either {@code null} or a none-empty
1182N/A * string.
1467N/A * @return an <code>Annotation</code> object or {@code null} on error
1462N/A * @throws IOException if an error occurs
95N/A */
1473N/A protected abstract Annotation annotate(File file, String revision)
1473N/A throws IOException;
95N/A
95N/A /**
65N/A * Create a history log cache for all of the files in this repository.
766N/A * {@code getHistory()} is used to fetch the history for the entire
766N/A * repository. If {@code hasHistoryForDirectories()} returns {@code false},
766N/A * this method is a no-op.
232N/A *
771N/A * @param cache the cache instance in which to store the history log
771N/A * @param sinceRevision if non-null, incrementally update the cache with
771N/A * all revisions after the specified revision; otherwise, create the full
771N/A * history starting with the initial revision
771N/A *
615N/A * @throws HistoryException on error
65N/A */
771N/A final void createCache(HistoryCache cache, String sinceRevision)
1473N/A throws HistoryException
1473N/A {
435N/A if (!isWorking()) {
435N/A return;
435N/A }
238N/A
238N/A // If we don't have a directory parser, we can't create the cache
238N/A // this way. Just give up and return.
766N/A if (!hasHistoryForDirectories()) {
1461N/A Logger.getLogger(getClass().getName()).info("Skipping creation of "
1461N/A + "history cache for '" + getDirectoryName() + "', since "
1461N/A + "retrieval of history for directories is not implemented for "
1461N/A + "this repository type.");
238N/A return;
238N/A }
238N/A
232N/A File directory = new File(getDirectoryName());
969N/A
969N/A History history;
969N/A try {
969N/A history = getHistory(directory, sinceRevision);
969N/A } catch (HistoryException he) {
969N/A if (sinceRevision == null) {
969N/A // Failed to get full history, so fail.
969N/A throw he;
969N/A }
1182N/A // Failed to get partial history. This may have been caused
1182N/A // by changes in the revision numbers since the last update
1182N/A // (bug #14724) so we'll try to regenerate the cache from
1182N/A // scratch instead.
1327N/A logger.info("Failed to get partial history. Attempting to " +
1327N/A "recreate the history cache from scratch (" + he.getMessage() + ")");
1481N/A if (logger.isLoggable(Level.FINE)) {
1481N/A logger.log(Level.FINE, "createCache()", he);
1481N/A }
1182N/A history = null;
969N/A }
969N/A
969N/A if (sinceRevision != null && history == null) {
969N/A // Failed to get partial history, now get full history instead.
969N/A history = getHistory(directory);
969N/A // Got full history successfully. Clear the history cache so that
969N/A // we can recreate it from scratch.
969N/A cache.clear(this);
969N/A }
771N/A
743N/A if (history != null) {
743N/A cache.store(history, this);
232N/A }
232N/A }
1190N/A
203N/A /**
203N/A * Update the content in this repository by pulling the changes from the
203N/A * upstream repository..
203N/A * @throws Exception if an error occurs.
203N/A */
459N/A abstract void update() throws IOException;
1190N/A
314N/A /**
314N/A * Check if this it the right repository type for the given file.
1190N/A *
1473N/A * @param file File to check if this is a repository for. If it is a relative
1473N/A * path, it gets interpreted relative to the current working directory.
1473N/A * So to be sure, it should be a canonical path incl. source root.
1473N/A * @return {@code true} if this is the correct repository for this
1473N/A * file/directory.
314N/A */
1473N/A public abstract boolean isRepositoryFor(File file);
1190N/A
314N/A /**
1473N/A * Check whether this repository supports sub reporitories (a.k.a. forests).
1190N/A *
1473N/A * @return {@code true} if this repository supports sub repositories.
314N/A */
1461N/A @SuppressWarnings({ "PMD.EmptyMethodInAbstractClassShouldBeAbstract", "static-method" })
1473N/A public boolean supportsSubRepositories() {
314N/A return false;
314N/A }
749N/A
1461N/A /**
1461N/A * Get the date format to be used to parse dates from repository command
1461N/A * output.
1461N/A * @return a new {@link DateFormat} instance.
1461N/A * @see #setDatePattern(String)
1461N/A */
749N/A public DateFormat getDateFormat() {
1182N/A return new SimpleDateFormat(datePattern, Locale.US);
1182N/A }
1190N/A
1473N/A /**
1473N/A * Check, whether the given command executes succesfully (i.e return code
1473N/A * == 0).
1473N/A * @param args 0 .. command to execute, 1..n command line args
1473N/A * @return {@code true} on succes.
1473N/A */
1473N/A protected static Boolean checkCmd(String... args) {
1182N/A Executor exec = new Executor(args);
1182N/A return Boolean.valueOf(exec.exec(false) == 0);
749N/A }
1241N/A
1241N/A /**
1241N/A * Set the name of the external client command that should be used to
1241N/A * access the repository wrt. the given parameters. Does nothing, if this
1241N/A * repository's <var>cmd</var> has already been set (i.e. has a
1241N/A * non-{@code null} value).
1241N/A *
1241N/A * @param propertyKey property key to lookup the corresponding system property.
1241N/A * @param fallbackCommand the command to use, if lookup fails.
1241N/A * @return the command to use.
1241N/A * @see #cmd
1241N/A */
1241N/A protected String ensureCommand(String propertyKey, String fallbackCommand) {
1241N/A if (cmd != null) {
1241N/A return cmd;
1241N/A }
1470N/A cmd = RuntimeEnvironment.getConfig()
1241N/A .getRepoCmd(this.getClass().getCanonicalName());
1241N/A if (cmd == null) {
1241N/A cmd = System.getProperty(propertyKey, fallbackCommand);
1470N/A RuntimeEnvironment.getConfig()
1241N/A .setRepoCmd(this.getClass().getCanonicalName(), cmd);
1241N/A }
1241N/A return cmd;
1241N/A }
8N/A}