org.opensolaris.opengrok.history.BitKeeperRepository.java Source code

Java tutorial

Introduction

Here is the source code for org.opensolaris.opengrok.history.BitKeeperRepository.java

Source

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

package org.opensolaris.opengrok.history;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.jrcs.rcs.InvalidVersionNumberException;
import org.apache.commons.jrcs.rcs.Version;
import org.opensolaris.opengrok.configuration.RuntimeEnvironment;
import org.opensolaris.opengrok.logger.LoggerFactory;
import org.opensolaris.opengrok.util.Executor;

/**
 * Access to a BitKeeper repository.
 *
 * @author James Service  {@literal <jas2701@googlemail.com>}
 */
public class BitKeeperRepository extends Repository {

    private static final Logger LOGGER = LoggerFactory.getLogger(BitKeeperRepository.class);

    private static final long serialVersionUID = 1L;
    /**
     * The property name used to obtain the client command for this repository.
     */
    public static final String CMD_PROPERTY_KEY = "org.opensolaris.opengrok.history.BitKeeper";
    /**
     * The command to use to access the repository if none was given explicitly.
     */
    public static final String CMD_FALLBACK = "bk";
    /**
     * The output format specification for log commands.
     */
    private static final String LOG_DSPEC = "D :DPN:\\t:REV:\\t:D_: :T: GMT:TZ:\\t:USER:$if(:RENAME:){\\t:DPN|PARENT:}\\n$each(:C:){C (:C:)\\n}";
    /**
     * The output format specification for tags commands. Versions 7.3 and greater.
     */
    private static final String TAG_DSPEC = "D :REV:\\t:D_: :T: GMT:TZ:\\n$each(:TAGS:){T (:TAGS:)\\n}";
    /**
     * The output format specification for tags commands. Versions 7.2 and less.
     */
    private static final String TAG_DSPEC_OLD = "D :REV:\\t:D_: :T: GMT:TZ:\\n$each(:TAG:){T (:TAG:)\\n}";
    /**
     * The output format specification for tags commands. Versions 7.2 and less.
     */
    private static final Version NEW_DSPEC_VERSION = new Version(7, 3);
    /*
     * Using a dspec not only makes it easier to parse, but also means we don't get tripped up by any system-wide
     * non-default dspecs on the box we are running on.
     */
    /**
     * Pattern to parse a version number from output of bk --version.
     */
    private static final Pattern VERSION_PATTERN = Pattern.compile("BitKeeper version is .*-(\\d(\\.\\d)*)");

    /**
     * The version of the BitKeeper executable. This affects the correct dspec to use for tags.
     */
    private Version version = null;

    /**
     * Constructor to construct the thing to be constructed.
     */
    public BitKeeperRepository() {
        type = "BitKeeper";
        datePatterns = new String[] { "yyyy-MM-dd HH:mm:ss z" };

        ignoredDirs.add(".bk");
    }

    /**
     * Updates working and version member variables by running bk --version.
     */
    private void ensureVersion() {
        if (working == null) {
            ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
            final Executor exec = new Executor(new String[] { RepoCommand, "--version" });
            if (exec.exec(false) == 0) {
                working = Boolean.TRUE;
                final Matcher matcher = VERSION_PATTERN.matcher(exec.getOutputString());
                if (matcher.find()) {
                    try {
                        version = new Version(matcher.group(1));
                    } catch (final InvalidVersionNumberException e) {
                        assert false : "Failed to parse a version number.";
                    }
                }
            } else {
                working = Boolean.FALSE;
            }
            if (version == null) {
                version = new Version(0, 0);
            }
        }
    }

    /**
     * Returns whether file represents a BitKeeper repository. A BitKeeper repository has a folder named .bk at its
     * source root.
     *
     * @return ret a boolean denoting whether it is or not
     */
    @Override
    boolean isRepositoryFor(File file) {
        if (file.isDirectory()) {
            final File f = new File(file, ".bk");
            return f.exists() && f.isDirectory();
        }
        return false;
    }

    /**
     * Returns whether the BitKeeper command is working.
     *
     * @return working a boolean denoting whether it is or not
     */
    @Override
    public boolean isWorking() {
        ensureVersion();
        return working.booleanValue();
    }

    /**
     * Returns the version of the BitKeeper executable.
     *
     * @return version a Version object
     */
    public Version getVersion() {
        ensureVersion();
        return version;
    }

    /**
     * Implementation of abstract method determineBranch. BitKeeper doesn't really have branches as such.
     *
     * @return null
     */
    @Override
    String determineBranch() throws IOException {
        return null;
    }

    /**
     * Return the first listed pull parent of this repository BitKeeper can have multiple push parents and pul parents.
     *
     * @return parent a string denoting the parent, or null.
     */
    @Override
    String determineParent() throws IOException {
        final File directory = new File(directoryName);

        final ArrayList<String> argv = new ArrayList<String>();
        ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
        argv.add(RepoCommand);
        argv.add("parent");
        argv.add("-1il");

        final Executor executor = new Executor(argv, directory);
        final int rc = executor.exec(false);
        final String parent = executor.getOutputString().trim();
        if (rc == 0) {
            return parent;
        } else if (parent.equals("This repository has no pull parent.")) {
            return null;
        } else {
            throw new IOException(executor.getErrorString());
        }
    }

    /* History Stuff */
    /*
     * BitKeeper has independent revisions for its individual files like CVS, but also provides changesets, which is an
     * atomic commit of a group of deltas to files. Changesets have their own revision numbers.
     *
     * When constructing a history then, we therefore have a choice of whether to go by file revisions, or changeset
     * revisions. It seemed like doing it by changeset revisions would be both a) more difficult, and b) not in tune
     * with how BitKeeper is actually used (although, in the interest of full disclosure, I have only been using it for
     * a month).
     */

    /**
     * Returns whether BitKeeper has history for its directories.
     *
     * @return false
     */
    @Override
    boolean hasHistoryForDirectories() {
        return false;
    }

    /**
     * Returns whether BitKeeper has history for a file.
     *
     * @return ret a boolean denoting whether it does or not
     */
    @Override
    public boolean fileHasHistory(File file) {
        final File absolute = file.getAbsoluteFile();
        final File directory = absolute.getParentFile();
        final String basename = absolute.getName();

        final ArrayList<String> argv = new ArrayList<String>();
        ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
        argv.add(RepoCommand);
        argv.add("files");
        argv.add(basename);

        final Executor executor = new Executor(argv, directory);
        if (executor.exec(true) != 0) {
            LOGGER.log(Level.SEVERE, "Failed to check file: {0}", executor.getErrorString());
            return false;
        }

        return executor.getOutputString().trim().equals(basename);
    }

    /**
     * Construct a History for a file in this repository.
     *
     * @param file a file in the repository
     * @return history a history object
     */
    @Override
    History getHistory(File file) throws HistoryException {
        return getHistory(file, null);
    }

    /**
     * Construct a History for a file in this repository.
     *
     * @param file a file in the repository
     * @param sinceRevision omit history from before, and including, this revision
     * @return history a history object
     */
    @Override
    History getHistory(File file, String sinceRevision) throws HistoryException {
        final File absolute = file.getAbsoluteFile();
        final File directory = absolute.getParentFile();
        final String basename = absolute.getName();

        final ArrayList<String> argv = new ArrayList<String>();
        ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
        argv.add(RepoCommand);
        argv.add("log");
        if (sinceRevision != null) {
            argv.add("-r" + sinceRevision + "..");
        }
        argv.add("-d" + LOG_DSPEC);
        argv.add(basename);

        final Executor executor = new Executor(argv, directory);
        final BitKeeperHistoryParser parser = new BitKeeperHistoryParser(datePatterns[0]);
        if (executor.exec(true, parser) != 0) {
            throw new HistoryException(executor.getErrorString());
        }

        final RuntimeEnvironment env = RuntimeEnvironment.getInstance();
        final History history = parser.getHistory();

        // Assign tags to changesets they represent
        // We don't need to check if this repository supports tags,
        // because we know it :-)
        if (env.isTagsEnabled()) {
            assignTagsInHistory(history);
        }

        return history;
    }

    /**
     * Return an InputStream of the content of a given file at a given revision.
     *
     * @param parent the directory the file is in
     * @param basename the basename of the file
     * @param revision revision, or null for latest
     * @return output an input stream
     */
    @Override
    public InputStream getHistoryGet(String parent, String basename, String revision) {
        final File directory = new File(parent).getAbsoluteFile();

        final ArrayList<String> argv = new ArrayList<String>();
        ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
        argv.add(RepoCommand);
        argv.add("get");
        argv.add("-p");
        if (revision != null) {
            argv.add("-r" + revision);
        }
        argv.add(basename);

        final Executor executor = new Executor(argv, directory);
        if (executor.exec(true) != 0) {
            LOGGER.log(Level.SEVERE, "Failed to get history: {0}", executor.getErrorString());
            return null;
        }

        return executor.getOutputStream();
    }

    /* Annotation Stuff */

    /**
     * Returns whether BitKeeper has annotation for a file. It does if it has history for the file.
     *
     * @return ret a boolean denoting whether it does or not
     */
    @Override
    public boolean fileHasAnnotation(File file) {
        return fileHasHistory(file);
    }

    /**
     * Annotate the specified file/revision. The options `-aur` to `bk annotate` specify that bitkeeper will output the
     * last user to edit the line, the last revision the line was edited, and then the line itself, each separated by a
     * hard tab.
     *
     * @param file file to annotate
     * @param revision revision to annotate, or null for latest
     * @return annotation file annotation
     */
    @Override
    public Annotation annotate(File file, String revision) throws IOException {
        final File absolute = file.getCanonicalFile();
        final File directory = absolute.getParentFile();
        final String basename = absolute.getName();

        final ArrayList<String> argv = new ArrayList<String>();
        ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
        argv.add(RepoCommand);
        argv.add("annotate");
        argv.add("-aur");
        if (revision != null) {
            argv.add("-r" + revision);
        }
        argv.add(basename);

        final Executor executor = new Executor(argv, directory);
        final BitKeeperAnnotationParser parser = new BitKeeperAnnotationParser(basename);
        if (executor.exec(true, parser) != 0) {
            throw new IOException(executor.getErrorString());
        } else {
            return parser.getAnnotation();
        }
    }

    /* Tag Stuff */

    /**
     * Returns whether a set of tags should be constructed up front. BitKeeper tags changesets, not files, so yes.
     *
     * @return true
     */
    @Override
    boolean hasFileBasedTags() {
        return true;
    }

    /**
     * Returns the version of the BitKeeper executable.
     *
     * @return version a Version object
     */
    private String getTagDspec() {
        if (NEW_DSPEC_VERSION.compareVersions(getVersion()) <= 0) {
            return TAG_DSPEC;
        } else {
            return TAG_DSPEC_OLD;
        }
    }

    /**
     * Constructs a set of tags up front.
     *
     * @param directory the repository directory
     */
    @Override
    public void buildTagList(File directory) {
        final ArrayList<String> argv = new ArrayList<String>();
        argv.add("bk");
        argv.add("tags");
        argv.add("-d" + getTagDspec());

        final Executor executor = new Executor(argv, directory);
        final BitKeeperTagParser parser = new BitKeeperTagParser(datePatterns[0]);
        executor.exec(true, parser);
        tagList = parser.getEntries();
    }

    /* Update Stuff */

    @Override
    public void update() {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}