/*
* 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.");
}
}