/* * 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, 2011, 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.ArrayList; import java.util.List; import java.util.Locale; import java.util.logging.Logger; import java.util.logging.Level; 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 { private static final long serialVersionUID = 2089170589187502060L; private static final Logger logger = Logger.getLogger(Repository.class.getName()); /** * 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; /** * Check if the repository supports {@code getHistory()} requests for * the given file. For performance reasons subclasses should use some meta * information instead of actually hitting the repository to make that * decision. * @param file canonical path incl. source root. * * @return {@code true} if the repository can get history for the given file. */ public 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 */ public abstract boolean hasHistoryForDirectories(); /** * Get the history log for the specified file or directory for all changesets. * @param file the file to get the history for (canonical path incl. source * root) . * @return history log for file * @throws HistoryException on error accessing the history * @see #getHistory(File, String) */ public abstract History getHistory(File file) throws HistoryException; /** * * Get the history after a specified revision. *

* *

* 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. *

* * @param file the file to get the history for (canonical path incl. source * root). * @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 * @see #getHistoryGet(String, String, String) */ public 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.warning("Incremental history retrieval is not implemented for " + getClass().getSimpleName() + ". Falling back to slower full history retrieval."); } History history = getHistory(file); if (sinceRevision == null) { return history; } List partial = new ArrayList(); 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 */ @SuppressWarnings("static-method") void removeAndVerifyOldestChangeset(List 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 can be used to read a speciffic version of a * named file. * @param parent The name of the directory (canonical path incl. source root) * 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 or {@code null} * on error. */ public abstract InputStream getHistoryGet( String parent, String basename, String rev); /** * Checks whether this parser is able to annotate files. For performance * reasons subclasses should use some meta information instead of actually * hitting the repository to make that decision. * @param file canonical path incl. source root. * @return true if annotation is supported. */ public abstract boolean fileHasAnnotation(File file); /** * Annotate the specified revision of a file. * * @param file the file to annotate (canonical path incl. source root). * @param revision revision of the file. Either {@code null} or a none-empty * string. * @return an Annotation object or {@code null} on error * @throws IOException if an error occurs */ protected abstract Annotation annotate(File file, String revision) throws IOException; /** * Create a history log cache for all of the 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()).info("Skipping creation of " + "history cache for '" + getDirectoryName() + "', since " + "retrieval of history for directories is not implemented for " + "this repository type."); 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. logger.info("Failed to get partial history. Attempting to " + "recreate the history cache from scratch (" + he.getMessage() + ")"); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "createCache()", 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); } if (history != null) { cache.store(history, this); } } /** * Update the content in this repository by pulling the changes from the * upstream repository.. * @throws Exception 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. If it is a relative * path, it gets interpreted relative to the current working directory. * So to be sure, it should be a canonical path incl. source root. * @return {@code true} if this is the correct repository for this * file/directory. */ public abstract boolean isRepositoryFor(File file); /** * Check whether this repository supports sub reporitories (a.k.a. forests). * * @return {@code true} if this repository supports sub repositories. */ @SuppressWarnings({ "PMD.EmptyMethodInAbstractClassShouldBeAbstract", "static-method" }) public boolean supportsSubRepositories() { return false; } /** * Get the date format to be used to parse dates from repository command * output. * @return a new {@link DateFormat} instance. * @see #setDatePattern(String) */ public DateFormat getDateFormat() { return new SimpleDateFormat(datePattern, Locale.US); } /** * Check, whether the given command executes succesfully (i.e return code * == 0). * @param args 0 .. command to execute, 1..n command line args * @return {@code true} on succes. */ protected 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 cmd 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.getConfig() .getRepoCmd(this.getClass().getCanonicalName()); if (cmd == null) { cmd = System.getProperty(propertyKey, fallbackCommand); RuntimeEnvironment.getConfig() .setRepoCmd(this.getClass().getCanonicalName(), cmd); } return cmd; } }