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/*
1421N/A * Copyright (c) 2008, 2012, 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;
1421N/Aimport java.util.*;
783N/Aimport java.util.logging.Level;
783N/Aimport java.util.logging.Logger;
783N/Aimport org.opensolaris.opengrok.OpenGrokLogger;
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
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;
1421N/A
1421N/A /**
1421N/A * List of <revision, tags> pairs for repositories which display tags
1421N/A * only for files changed by the tagged commit.
1421N/A */
1421N/A protected TreeSet<TagEntry> tagList = null;
1241N/A
231N/A 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 */
765N/A abstract boolean hasHistoryForDirectories();
765N/A
765N/A /**
765N/A * Get the history log for the specified file or directory.
765N/A * @param file the file to get the history for
765N/A * @return history log for file
765N/A * @throws HistoryException on error accessing the history
765N/A */
765N/A abstract History getHistory(File file) throws HistoryException;
765N/A
65N/A /**
771N/A * <p>
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 *
771N/A * @param file the file to get the history for
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
771N/A */
771N/A History getHistory(File file, String sinceRevision)
771N/A throws HistoryException {
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) {
1190N/A Logger logger = OpenGrokLogger.getLogger();
1054N/A logger.log(Level.WARNING,
783N/A "Incremental history retrieval is not implemented for {0}.",
783N/A getClass().getSimpleName());
1054N/A logger.log(Level.WARNING,
783N/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 */
967N/A void removeAndVerifyOldestChangeset(List<HistoryEntry> entries,
967N/A String revision)
967N/A throws HistoryException {
967N/A HistoryEntry entry =
967N/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())) {
967N/A throw new HistoryException("Cached revision '" + revision +
967N/A "' not found in the repository");
967N/A }
771N/A }
771N/A
771N/A /**
65N/A * Get an input stream that I may use to read a speciffic version of a
65N/A * named file.
65N/A * @param parent the name of the directory containing the file
65N/A * @param basename the name of the file to get
65N/A * @param rev the revision to get
65N/A * @return An input stream containing the correct revision.
65N/A */
231N/A abstract InputStream getHistoryGet(
231N/A String parent, String basename, String rev);
65N/A
65N/A /**
176N/A * Checks whether this parser can annotate files.
176N/A *
176N/A * @return <code>true</code> if annotation is supported
176N/A */
298N/A abstract boolean fileHasAnnotation(File file);
1421N/A
1421N/A /**
1421N/A * Returns if this repository tags only files changed in last commit, i.e.
1421N/A * if we need to prepare list of repository-wide tags prior to creation of
1421N/A * file history entries.
1421N/A * @return True if we need tag list creation prior to file parsing,
1421N/A * false by default.
1421N/A */
1421N/A boolean hasFileBasedTags() {
1421N/A return false;
1421N/A }
1421N/A
1421N/A TreeSet<TagEntry> getTagList() {
1421N/A return this.tagList;
1421N/A }
1421N/A
1421N/A /**
1421N/A * Assign tags to changesets they represent
1421N/A * The complete list of tags must be pre-built using {@code getTagList()}.
1421N/A * Then this function squeeze all tags to changesets which actually exist
1421N/A * in the history of given file.
1421N/A * Must be implemented repository-specific.
1421N/A * @see getTagList
1421N/A * @param hist History we want to assign tags to.
1421N/A */
1421N/A void assignTagsInHistory(History hist) throws HistoryException {
1421N/A if (hist == null) {
1421N/A return;
1421N/A }
1421N/A if (this.getTagList() == null) {
1421N/A throw new HistoryException("Tag list was not created before assigning tags to changesets!");
1421N/A }
1421N/A Iterator<TagEntry> it = this.getTagList().descendingIterator();
1421N/A TagEntry lastTagEntry = null;
1421N/A // Go through all commits of given file
1421N/A for (HistoryEntry ent : hist.getHistoryEntries()) {
1421N/A // Assign all tags created since the last revision
1421N/A // Revision in this HistoryEntry must be already specified!
1421N/A // TODO is there better way to do this? We need to "repeat"
1421N/A // last element returned by call to next()
1421N/A while (lastTagEntry != null || it.hasNext()) {
1421N/A if (lastTagEntry == null) {
1421N/A lastTagEntry = it.next();
1421N/A }
1421N/A if (lastTagEntry.compareTo(ent) >= 0) {
1421N/A if (ent.getTags() == null) {
1421N/A ent.setTags(lastTagEntry.getTags());
1421N/A } else {
1421N/A ent.setTags(ent.getTags() + ", " + lastTagEntry.getTags());
1421N/A }
1421N/A } else {
1421N/A break;
1421N/A }
1421N/A if (it.hasNext()) {
1421N/A lastTagEntry = it.next();
1421N/A } else {
1421N/A lastTagEntry = null;
1421N/A }
1421N/A }
1421N/A }
1421N/A }
1421N/A
1421N/A /**
1421N/A * Create internal list of all tags in this repository.
1421N/A * @param directory
1421N/A */
1421N/A protected void buildTagList(File directory) {
1421N/A this.tagList = null;
1421N/A }
176N/A
176N/A /**
95N/A * Annotate the specified revision of a file.
95N/A *
95N/A * @param file the file to annotate
1182N/A * @param revision revision of the file. Either {@code null} or a none-empty
1182N/A * string.
95N/A * @return an <code>Annotation</code> object
459N/A * @throws java.io.IOException if an error occurs
95N/A */
459N/A abstract Annotation annotate(File file, String revision) 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)
771N/A throws HistoryException {
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()) {
1400N/A Logger.getLogger(getClass().getName()).log(
1400N/A Level.INFO,
1400N/A "Skipping creation of history cache for {0}, since retrieval " +
1400N/A "of history for directories is not implemented for this " +
1400N/A "repository type.", getDirectoryName());
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.
1182N/A OpenGrokLogger.getLogger().log(Level.INFO,
1182N/A "Failed to get partial history. Attempting to " +
1182N/A "recreate the history cache from scratch.", he);
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 *
314N/A * @param file File to check if this is a repository for.
314N/A * @return true if this is the correct repository for this file/directory.
314N/A */
314N/A abstract boolean isRepositoryFor(File file);
1190N/A
314N/A /**
314N/A * Returns true if this repository supports sub reporitories (a.k.a. forests).
1190N/A *
314N/A * @return true if this repository supports sub repositories
314N/A */
436N/A @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract")
314N/A boolean supportsSubRepositories() {
314N/A return false;
314N/A }
749N/A
749N/A public DateFormat getDateFormat() {
1182N/A return new SimpleDateFormat(datePattern, Locale.US);
1182N/A }
1190N/A
1248N/A 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 }
1241N/A cmd = RuntimeEnvironment.getInstance()
1241N/A .getRepoCmd(this.getClass().getCanonicalName());
1241N/A if (cmd == null) {
1241N/A cmd = System.getProperty(propertyKey, fallbackCommand);
1241N/A RuntimeEnvironment.getInstance()
1241N/A .setRepoCmd(this.getClass().getCanonicalName(), cmd);
1241N/A }
1241N/A return cmd;
1241N/A }
8N/A}