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/*
1297N/A * Copyright (c) 2006, 2012, Oracle and/or its affiliates. All rights reserved.
8N/A */
8N/Apackage org.opensolaris.opengrok.history;
8N/A
65N/Aimport java.io.BufferedReader;
65N/Aimport java.io.ByteArrayInputStream;
262N/Aimport java.io.ByteArrayOutputStream;
8N/Aimport java.io.File;
24N/Aimport java.io.IOException;
8N/Aimport java.io.InputStream;
65N/Aimport java.io.InputStreamReader;
1421N/Aimport java.util.*;
416N/Aimport java.util.logging.Level;
95N/Aimport java.util.regex.Matcher;
95N/Aimport java.util.regex.Pattern;
1183N/A
416N/Aimport org.opensolaris.opengrok.OpenGrokLogger;
1421N/Aimport org.opensolaris.opengrok.configuration.RuntimeEnvironment;
516N/Aimport org.opensolaris.opengrok.util.Executor;
1195N/Aimport org.opensolaris.opengrok.util.IOUtils;
1297N/Aimport org.opensolaris.opengrok.web.Util;
8N/A
8N/A/**
65N/A * Access to a Mercurial repository.
574N/A *
8N/A */
290N/Apublic class MercurialRepository extends Repository {
815N/A private static final long serialVersionUID = 1L;
635N/A
1182N/A /** The property name used to obtain the client command for thisrepository. */
1190N/A public static final String CMD_PROPERTY_KEY =
1182N/A "org.opensolaris.opengrok.history.Mercurial";
1182N/A /** The command to use to access the repository if none was given explicitly */
1182N/A public static final String CMD_FALLBACK = "hg";
1182N/A
1412N/A /**
1412N/A * The boolean property and environment variable name to indicate
1182N/A * whether forest-extension in Mercurial adds repositories inside the
1412N/A * repositories.
1412N/A */
1182N/A public static final String NOFOREST_PROPERTY_KEY =
1182N/A "org.opensolaris.opengrok.history.mercurial.disableForest";
1182N/A
635N/A /** Template for formatting hg log output for files. */
1183N/A private static final String TEMPLATE = "changeset: {rev}:{node|short}\\n"
1183N/A + "{branches}{tags}{parents}\\n"
1183N/A + "user: {author}\\ndate: {date|isodate}\\n"
1183N/A + "description: {desc|strip|obfuscate}\\n";
635N/A
635N/A /** Template for formatting hg log output for directories. */
1190N/A private static final String DIR_TEMPLATE = TEMPLATE
1182N/A + "files: {files}{file_copies}\\n";
574N/A
664N/A public MercurialRepository() {
664N/A type = "Mercurial";
749N/A datePattern = "yyyy-MM-dd hh:mm ZZZZ";
664N/A }
664N/A
574N/A /**
574N/A * Get an executor to be used for retrieving the history log for the
574N/A * named file.
1190N/A *
574N/A * @param file The file to retrieve history for
792N/A * @param changeset the oldest changeset to return from the executor,
792N/A * or {@code null} if all changesets should be returned
574N/A * @return An Executor ready to be started
574N/A */
792N/A Executor getHistoryLogExecutor(File file, String changeset)
1016N/A throws HistoryException, IOException
792N/A {
1016N/A String abs = file.getCanonicalPath();
58N/A String filename = "";
50N/A if (abs.length() > directoryName.length()) {
50N/A filename = abs.substring(directoryName.length() + 1);
50N/A }
1190N/A
574N/A List<String> cmd = new ArrayList<String>();
1182N/A ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
1182N/A cmd.add(this.cmd);
574N/A cmd.add("log");
1412N/A if (!file.isDirectory()) { cmd.add("-f"); }
792N/A
792N/A if (changeset != null) {
792N/A cmd.add("-r");
792N/A String[] parts = changeset.split(":");
792N/A if (parts.length == 2) {
792N/A cmd.add("tip:" + parts[0]);
792N/A } else {
792N/A throw new HistoryException(
792N/A "Don't know how to parse changeset identifier: " +
792N/A changeset);
792N/A }
1190N/A }
792N/A
574N/A cmd.add("--template");
635N/A cmd.add(file.isDirectory() ? DIR_TEMPLATE : TEMPLATE);
574N/A cmd.add(filename);
1190N/A
664N/A return new Executor(cmd, new File(directoryName));
1190N/A }
1190N/A
815N/A @Override
1183N/A public InputStream getHistoryGet(String parent, String basename, String rev)
1183N/A {
65N/A InputStream ret = null;
65N/A
232N/A File directory = new File(directoryName);
232N/A
71N/A Process process = null;
355N/A InputStream in = null;
575N/A String revision = rev;
575N/A
575N/A if (rev.indexOf(':') != -1) {
575N/A revision = rev.substring(0, rev.indexOf(':'));
575N/A }
58N/A try {
1182N/A String filename = (new File(parent, basename)).getCanonicalPath()
1182N/A .substring(directoryName.length() + 1);
1182N/A ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
1182N/A String argv[] = {cmd, "cat", "-r", revision, filename};
71N/A process = Runtime.getRuntime().exec(argv, null, directory);
1190N/A
262N/A ByteArrayOutputStream out = new ByteArrayOutputStream();
262N/A byte[] buffer = new byte[32 * 1024];
355N/A in = process.getInputStream();
262N/A int len;
1190N/A
262N/A while ((len = in.read(buffer)) != -1) {
262N/A if (len > 0) {
262N/A out.write(buffer, 0, len);
262N/A }
65N/A }
1190N/A
1182N/A ret = new ByteArrayInputStream(out.toByteArray());
58N/A } catch (Exception exp) {
1190N/A OpenGrokLogger.getLogger().log(Level.SEVERE,
1183N/A "Failed to get history: " + exp.getClass().toString());
71N/A } finally {
1195N/A IOUtils.close(in);
71N/A // Clean up zombie-processes...
71N/A if (process != null) {
71N/A try {
71N/A process.exitValue();
75N/A } catch (IllegalThreadStateException exp) {
71N/A // the process is still running??? just kill it..
71N/A process.destroy();
71N/A }
71N/A }
24N/A }
1190N/A
8N/A return ret;
8N/A }
212N/A
95N/A /** Pattern used to extract author/revision from hg annotate. */
1238N/A private static final Pattern ANNOTATION_PATTERN =
1297N/A Pattern.compile("^\\s*(\\d+):");
95N/A
95N/A /**
95N/A * Annotate the specified file/revision.
95N/A *
95N/A * @param file file to annotate
95N/A * @param revision revision to annotate
95N/A * @return file annotation
95N/A */
815N/A @Override
459N/A public Annotation annotate(File file, String revision) throws IOException {
95N/A ArrayList<String> argv = new ArrayList<String>();
1182N/A ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
1182N/A argv.add(cmd);
95N/A argv.add("annotate");
95N/A argv.add("-n");
95N/A if (revision != null) {
95N/A argv.add("-r");
579N/A if (revision.indexOf(':') == -1) {
579N/A argv.add(revision);
579N/A } else {
575N/A argv.add(revision.substring(0, revision.indexOf(':')));
575N/A }
95N/A }
95N/A argv.add(file.getName());
95N/A ProcessBuilder pb = new ProcessBuilder(argv);
95N/A pb.directory(file.getParentFile());
355N/A Process process = null;
355N/A BufferedReader in = null;
355N/A Annotation ret = null;
1297N/A HashMap<String,HistoryEntry> revs = new HashMap<String,HistoryEntry>();
1297N/A
1297N/A // Construct hash map for history entries from history cache. This is
1297N/A // needed later to get user string for particular revision.
1303N/A try {
1297N/A History hist = HistoryGuru.getInstance().getHistory(file, false);
1297N/A for (HistoryEntry e : hist.getHistoryEntries()) {
1378N/A // Chop out the colon and all hexadecimal what follows.
1297N/A // This is because the whole changeset identification is
1297N/A // stored in history index while annotate only needs the
1297N/A // revision identifier.
1297N/A revs.put(e.getRevision().replaceFirst(":[a-f0-9]+", ""), e);
1297N/A }
1378N/A } catch (HistoryException he) {
1297N/A OpenGrokLogger.getLogger().log(Level.SEVERE,
1297N/A "Error: cannot get history for file " + file);
1378N/A return null;
1378N/A }
1297N/A
95N/A try {
355N/A process = pb.start();
355N/A in = new BufferedReader(new InputStreamReader(process.getInputStream()));
355N/A ret = new Annotation(file.getName());
347N/A String line;
347N/A int lineno = 0;
1182N/A Matcher matcher = ANNOTATION_PATTERN.matcher("");
347N/A while ((line = in.readLine()) != null) {
347N/A ++lineno;
1182N/A matcher.reset(line);
347N/A if (matcher.find()) {
1297N/A String rev = matcher.group(1);
1297N/A String author = "N/A";
1297N/A // Use the history index hash map to get the author.
1378N/A if (revs.get(rev) != null) {
1297N/A author = revs.get(rev).getAuthor();
1378N/A }
1297N/A ret.addLine(rev, Util.getEmail(author.trim()), true);
347N/A } else {
1190N/A OpenGrokLogger.getLogger().log(Level.SEVERE,
1190N/A "Error: did not find annotation in line "
1183N/A + lineno + ": [" + line + "]");
99N/A }
1190N/A }
95N/A } finally {
1195N/A IOUtils.close(in);
355N/A if (process != null) {
355N/A try {
355N/A process.exitValue();
355N/A } catch (IllegalThreadStateException e) {
355N/A // the process is still running??? just kill it..
355N/A process.destroy();
355N/A }
95N/A }
95N/A }
355N/A return ret;
95N/A }
95N/A
815N/A @Override
298N/A public boolean fileHasAnnotation(File file) {
176N/A return true;
176N/A }
176N/A
815N/A @Override
459N/A public void update() throws IOException {
664N/A File directory = new File(directoryName);
516N/A
516N/A List<String> cmd = new ArrayList<String>();
1182N/A ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
1182N/A cmd.add(this.cmd);
516N/A cmd.add("showconfig");
516N/A Executor executor = new Executor(cmd, directory);
516N/A if (executor.exec() != 0) {
516N/A throw new IOException(executor.getErrorString());
516N/A }
516N/A
516N/A if (executor.getOutputString().indexOf("paths.default=") != -1) {
516N/A cmd.clear();
1182N/A cmd.add(this.cmd);
516N/A cmd.add("pull");
516N/A cmd.add("-u");
516N/A executor = new Executor(cmd, directory);
516N/A if (executor.exec() != 0) {
516N/A throw new IOException(executor.getErrorString());
203N/A }
203N/A }
203N/A }
212N/A
815N/A @Override
212N/A public boolean fileHasHistory(File file) {
212N/A // Todo: is there a cheap test for whether mercurial has history
212N/A // available for a file?
212N/A // Otherwise, this is harmless, since mercurial's commands will just
212N/A // print nothing if there is no history.
212N/A return true;
212N/A }
1190N/A
314N/A @Override
314N/A boolean isRepositoryFor(File file) {
1412N/A if (file.isDirectory()) {
1412N/A File f = new File(file, ".hg");
1412N/A return f.exists() && f.isDirectory();
1412N/A }
1412N/A return false;
314N/A }
314N/A
314N/A @Override
314N/A boolean supportsSubRepositories() {
1183N/A String val = System.getenv(NOFOREST_PROPERTY_KEY);
1248N/A return !(val == null
1183N/A ? Boolean.getBoolean(NOFOREST_PROPERTY_KEY)
1183N/A : Boolean.parseBoolean(val));
314N/A }
435N/A
435N/A @Override
592N/A public boolean isWorking() {
1182N/A if (working == null) {
1182N/A ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
1248N/A working = checkCmd(cmd);
1182N/A }
1182N/A return working.booleanValue();
435N/A }
765N/A
765N/A @Override
765N/A boolean hasHistoryForDirectories() {
765N/A return true;
765N/A }
765N/A
765N/A @Override
765N/A History getHistory(File file) throws HistoryException {
792N/A return getHistory(file, null);
792N/A }
792N/A
792N/A @Override
792N/A History getHistory(File file, String sinceRevision)
792N/A throws HistoryException {
1421N/A RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1421N/A History result = new MercurialHistoryParser(this).parse(file, sinceRevision);
1421N/A // Assign tags to changesets they represent
1421N/A // We don't need to check if this repository supports tags, because we know it:-)
1421N/A if (env.isTagsEnabled()) {
1421N/A assignTagsInHistory(result);
1421N/A }
1421N/A return result;
1421N/A }
1421N/A
1421N/A /**
1421N/A * We need to create list of all tags prior to creation of HistoryEntries
1421N/A * per file.
1421N/A * @return true.
1421N/A */
1421N/A @Override
1421N/A boolean hasFileBasedTags() {
1421N/A return true;
1421N/A }
1421N/A
1421N/A @Override
1421N/A protected void buildTagList(File directory) {
1421N/A this.tagList = new TreeSet<TagEntry>();
1421N/A ArrayList<String> argv = new ArrayList<String>();
1421N/A ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
1421N/A argv.add(cmd);
1421N/A argv.add("tags");
1421N/A ProcessBuilder pb = new ProcessBuilder(argv);
1421N/A pb.directory(directory);
1421N/A Process process = null;
1421N/A BufferedReader in = null;
1421N/A
1421N/A try {
1421N/A process = pb.start();
1421N/A in = new BufferedReader(new InputStreamReader(process.getInputStream()));
1421N/A String line;
1421N/A while ((line = in.readLine()) != null) {
1421N/A String parts[] = line.split(" *");
1421N/A if (parts.length < 2) {
1421N/A throw new HistoryException("Tag line contains more than 2 columns: " + line);
1421N/A }
1421N/A // Grrr, how to parse tags with spaces inside?
1421N/A // This solution will loose multiple spaces;-/
1421N/A String tag = parts[0];
1421N/A for (int i = 1; i < parts.length - 1; ++i) {
1421N/A tag += " " + parts[i];
1421N/A }
1421N/A String revParts[] = parts[parts.length - 1].split(":");
1421N/A if (revParts.length != 2) {
1421N/A throw new HistoryException("Mercurial revision parsing error: " + parts[parts.length - 1]);
1421N/A }
1421N/A TagEntry tagEntry = new MercurialTagEntry(Integer.parseInt(revParts[0]), tag);
1421N/A // Reverse the order of the list
1421N/A this.tagList.add(tagEntry);
1421N/A }
1421N/A } catch (IOException e) {
1421N/A OpenGrokLogger.getLogger().log(Level.WARNING, "Failed to read tag list: {0}", e.getMessage());
1421N/A this.tagList = null;
1421N/A } catch (HistoryException e) {
1421N/A OpenGrokLogger.getLogger().log(Level.WARNING, "Failed to parse tag list: {0}", e.getMessage());
1421N/A this.tagList = null;
1421N/A }
1421N/A
1421N/A IOUtils.close(in);
1421N/A if (process != null) {
1421N/A try {
1421N/A process.exitValue();
1421N/A } catch (IllegalThreadStateException e) {
1421N/A // the process is still running??? just kill it..
1421N/A process.destroy();
1421N/A }
1421N/A }
765N/A }
8N/A}