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;
1481N/Aimport java.io.FileReader;
1479N/Aimport java.io.FileWriter;
24N/Aimport java.io.IOException;
8N/Aimport java.io.InputStream;
65N/Aimport java.io.InputStreamReader;
58N/Aimport java.util.ArrayList;
1481N/Aimport java.util.Collections;
1297N/Aimport java.util.HashMap;
1481N/Aimport java.util.Map;
516N/Aimport java.util.List;
1481N/Aimport java.util.logging.Level;
1327N/Aimport java.util.logging.Logger;
95N/Aimport java.util.regex.Matcher;
95N/Aimport java.util.regex.Pattern;
1183N/A
1462N/Aimport org.opensolaris.opengrok.configuration.Configuration;
1467N/Aimport org.opensolaris.opengrok.configuration.RuntimeEnvironment;
516N/Aimport org.opensolaris.opengrok.util.Executor;
1195N/Aimport org.opensolaris.opengrok.util.IOUtils;
1481N/Aimport org.opensolaris.opengrok.util.StringUtils;
1297N/Aimport org.opensolaris.opengrok.web.Util;
8N/A
8N/A/**
65N/A * Access to a Mercurial repository.
8N/A */
290N/Apublic class MercurialRepository extends Repository {
1473N/A private static final Logger logger =
1473N/A Logger.getLogger(MercurialRepository.class.getName());
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 =
1462N/A Configuration.PROPERTY_KEY_PREFIX + "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
1182N/A /** The boolean property and environment variable name to indicate
1182N/A * whether forest-extension in Mercurial adds repositories inside the
1182N/A * repositories. */
1182N/A public static final String NOFOREST_PROPERTY_KEY =
1462N/A Configuration.PROPERTY_KEY_PREFIX + "history.mercurial.disableForest";
1182N/A
635N/A /** Template for formatting hg log output for files. */
1481N/A private static final String[] TEMPLATE = {
1481N/A "changeset: {rev}:{node|short}\\n"
1183N/A + "{branches}{tags}{parents}\\n"
1463N/A + "user: {author}\\ndate: {date|rfc3339date}\\n"
1481N/A + "description: {desc|strip|obfuscate}\\n",
1481N/A
1481N/A "changeset: {rev}:{node|short}:{node}\\n"
1481N/A + "{branches}{tags}{parents}\\n"
1481N/A + "user: {author}\\ndate: {date|rfc3339date}\\n"
1481N/A + "description: {desc|strip|obfuscate}\\n"
1481N/A };
635N/A
635N/A /** Template for formatting hg log output for directories. */
1481N/A private static final String[] DIR_TEMPLATE = {
1481N/A TEMPLATE[0] + "files:\\n{files}\\n",
1481N/A TEMPLATE[1] + "files:\\n{files}\\n",
1481N/A };
1481N/A private static File[] STYLEFILE = new File[2];
1481N/A private static String DATE_PATTERN;
1481N/A
1481N/A // 0 .. normal repo, 1 .. bridged repo, i.e "cloned" from a different type
1481N/A private int style;
1481N/A private String svnUUID = null;
1481N/A
1481N/A private Map<String, String> oldRevMap;
1481N/A private long lastModOldRev;
1479N/A
1461N/A /**
1462N/A * Create a new instance of type {@code Mercurial}.
1461N/A */
664N/A public MercurialRepository() {
664N/A type = "Mercurial";
1481N/A if (DATE_PATTERN == null) {
1467N/A if (RuntimeEnvironment.isOldJVM()) {
1481N/A TEMPLATE[0] = TEMPLATE[0].replace("rfc3339date", "isodate");
1481N/A TEMPLATE[1] = TEMPLATE[1].replace("rfc3339date", "isodate");
1481N/A DIR_TEMPLATE[0] = DIR_TEMPLATE[0].replace("rfc3339date", "isodate");
1481N/A DIR_TEMPLATE[1] = DIR_TEMPLATE[1].replace("rfc3339date", "isodate");
1481N/A DATE_PATTERN = "yyyy-MM-dd hh:mm ZZZZ";
1467N/A } else {
1481N/A DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ssXXX";
1467N/A }
1467N/A }
1481N/A datePattern = DATE_PATTERN;
664N/A }
664N/A
1481N/A private static void checkStyleFile(int style) throws IOException {
1481N/A if (STYLEFILE[style] == null || !STYLEFILE[style].canRead()) {
1479N/A FileWriter out = null;
1479N/A String EOL = System.getProperty("line.separator");
1479N/A try {
1481N/A File f = File.createTempFile("hgstyle" + style, ".tmp", null);
1479N/A f.deleteOnExit();
1479N/A out = new FileWriter(f, false);
1479N/A out.write("changeset = \"");
1481N/A out.write(DIR_TEMPLATE[style]);
1479N/A out.write('"');
1479N/A out.write(EOL);
1479N/A // actually mercurial needs to provide a way to escape certain
1479N/A // chars in filenames, to get this 100% correct
1479N/A out.write("file = \"{file}\\n\"");
1479N/A out.write(EOL);
1479N/A out.flush();
1481N/A STYLEFILE[style] = f.getAbsoluteFile();
1479N/A } finally {
1479N/A IOUtils.close(out);
1479N/A }
1479N/A }
1479N/A }
1479N/A
1481N/A // package scope: MercurialHistoryParser.java is the only consumer
1481N/A /**
1481N/A * Get the mapping of this repository based revisions to the revisions of
1481N/A * the non-hybrid repository. Key is the full hg revision aka
1481N/A * 'node', the corresponding value is the revision string in the non-hybrid
1481N/A * repository.
1481N/A * @return an empty map, if this is a native mercurial repository or is not
1481N/A * a hybrid (like hgsubversion), the fully populated map otherwise.
1481N/A */
1481N/A Map<String,String> getSrcRevisionMap() {
1481N/A if (oldRevMap != null && style != 1) {
1481N/A return oldRevMap;
1481N/A }
1481N/A File f = new File(directoryName, ".hg" + File.separator + "svn"
1481N/A + File.separator + "rev_map");
1481N/A if (! f.exists()) {
1481N/A oldRevMap = Collections.emptyMap();
1481N/A return oldRevMap;
1481N/A }
1481N/A if (oldRevMap != null) {
1481N/A long t = f.lastModified();
1481N/A if (t <= lastModOldRev) {
1481N/A return oldRevMap;
1481N/A }
1481N/A lastModOldRev = t;
1481N/A }
1481N/A Map<String, String> m = new HashMap<>();
1481N/A try (BufferedReader br = new BufferedReader(new FileReader(f))) {
1481N/A String line;
1481N/A while ((line = br.readLine()) != null) {
1481N/A int idx = line.indexOf(' ');
1481N/A if (idx > 0) {
1481N/A m.put(line.substring(idx+1).trim(), line.substring(0, idx));
1481N/A }
1481N/A }
1481N/A } catch (IOException e) {
1481N/A logger.warning(e.getLocalizedMessage());
1481N/A if (logger.isLoggable(Level.FINE)) {
1481N/A logger.log(Level.FINE, "getSrcRevisionMap()", e);
1481N/A }
1481N/A }
1481N/A oldRevMap = m;
1481N/A return oldRevMap;
1481N/A }
1481N/A
1481N/A /**
1481N/A * Get the UUID assigned by hgsubversion to the repository.
1481N/A * @return {@code null} if this is not a subversion hybrid repository,
1481N/A * the hgsubversion UUID otherwise.
1481N/A */
1481N/A public String getSvnUUID() {
1481N/A if (svnUUID == null) {
1481N/A File f = new File(directoryName, ".hg" + File.separator + "svn"
1481N/A + File.separator + "uuid");
1481N/A if (f.exists()) {
1481N/A try (BufferedReader br = new BufferedReader(new FileReader(f))){
1481N/A String s = br.readLine();
1481N/A svnUUID = s.trim();
1481N/A } catch (IOException e) {
1481N/A logger.warning(e.getLocalizedMessage());
1481N/A if (logger.isLoggable(Level.FINE)) {
1481N/A logger.log(Level.FINE, "getSvnUUID()", e);
1481N/A }
1481N/A }
1481N/A }
1481N/A if (svnUUID == null) {
1481N/A svnUUID = "";
1481N/A style = 0;
1481N/A } else {
1481N/A style = 1;
1481N/A }
1481N/A }
1481N/A return svnUUID.isEmpty() ? null : svnUUID;
1481N/A }
1481N/A
1481N/A /**
1481N/A * {@inheritDoc}
1481N/A */
1481N/A @Override
1481N/A public void setDirectoryName(String directoryName) {
1481N/A if (this.directoryName != null
1481N/A && !StringUtils.isSame(this.directoryName, directoryName))
1481N/A {
1481N/A throw new IllegalArgumentException("Software bug: directory is "
1481N/A + "already set. (" + this.directoryName + " != "
1481N/A + directoryName + ")");
1481N/A }
1481N/A super.setDirectoryName(directoryName);
1481N/A getSvnUUID(); // trigger style adjustments
1481N/A }
1481N/A
574N/A /**
574N/A * Get an executor to be used for retrieving the history log for the
1479N/A * named file. When indexing it gets usually called twice: 1st without any
1479N/A * -r option, 2nd with -r $currentRev:tip.
1190N/A *
1473N/A * @param file The file to retrieve history for (canonical path incl. source
1473N/A * root).
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)
1473N/A throws HistoryException, IOException
792N/A {
1016N/A String abs = file.getCanonicalPath();
58N/A String filename = "";
1467N/A if (!abs.startsWith(directoryName)) {
1481N/A throw new HistoryException("File '" + file
1481N/A + "' doesn't belong to me (" + directoryName + ")");
1467N/A }
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");
822N/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(":");
1481N/A if (parts.length == 2 || (parts.length == 3 && style == 1)) {
792N/A cmd.add("tip:" + parts[0]);
792N/A } else {
1481N/A throw new HistoryException("Don't know how to parse changeset "
1481N/A + changeset + " (" + file + " " + parts.length + " "
1481N/A + getSvnUUID() + " " + directoryName + ")");
792N/A }
1190N/A }
792N/A
1479N/A if (file.isDirectory()) {
1481N/A checkStyleFile(style);
1479N/A cmd.add("--style");
1481N/A cmd.add(STYLEFILE[style].getPath());
1479N/A } else {
1479N/A cmd.add("--template");
1481N/A cmd.add(TEMPLATE[style]);
1479N/A }
574N/A cmd.add(filename);
1190N/A
664N/A return new Executor(cmd, new File(directoryName));
1190N/A }
1190N/A
1461N/A /**
1461N/A * {@inheritDoc}
1461N/A */
1461N/A @SuppressWarnings("resource")
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 {
1467N/A String filename = (new File(parent, basename)).getCanonicalPath();
1467N/A if (!filename.startsWith(directoryName)) {
1467N/A return null;
1467N/A }
1467N/A filename = filename.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) {
1327N/A logger.warning("Failed to get history: " + exp.getMessage());
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 /**
1473N/A * {@inheritDoc}
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()) {
1473N/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 }
1435N/A } catch (HistoryException he) {
1327N/A logger.warning("Cannot get history for file '" + file + "'");
1435N/A return null;
1435N/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("");
1384N/A HashMap<String, String> authorMap = new HashMap<String, String>();
1481N/A String uuid = getSvnUUID();
1481N/A if (uuid != null) {
1481N/A uuid = "@" + uuid;
1481N/A }
347N/A while ((line = in.readLine()) != null) {
347N/A ++lineno;
1182N/A matcher.reset(line);
1384N/A String lastRev = "";
1384N/A String author = "N/A";
347N/A if (matcher.find()) {
1297N/A String rev = matcher.group(1);
1384N/A if (!lastRev.equals(rev)) {
1384N/A // Use the history index hash map to get the author.
1384N/A HistoryEntry he = revs.get(rev);
1384N/A if (he != null) {
1384N/A author = he.getAuthor();
1384N/A // no different strings for the same author
1384N/A String tmp = authorMap.get(author);
1384N/A if (tmp == null) {
1481N/A // hgsubversioned entries have no e-mail
1481N/A tmp = (uuid != null)
1481N/A ? author
1481N/A : Util.getEmail(author);
1384N/A authorMap.put(author, tmp);
1384N/A }
1384N/A author = tmp;
1384N/A }
1384N/A lastRev = rev;
1435N/A }
1384N/A ret.addLine(rev, author);
347N/A } else {
1327N/A logger.warning("Did not find annotation in line "
1327N/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
1461N/A /**
1461N/A * {@inheritDoc}
1461N/A */
815N/A @Override
298N/A public boolean fileHasAnnotation(File file) {
176N/A return true;
176N/A }
176N/A
1461N/A /**
1461N/A * {@inheritDoc}
1461N/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
1461N/A /**
1461N/A * {@inheritDoc}
1461N/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
1473N/A /**
1473N/A * {@inheritDoc}
1473N/A */
314N/A @Override
1473N/A public boolean isRepositoryFor(File file) {
1473N/A if (file.isDirectory()) {
1473N/A File f = new File(file, ".hg");
1473N/A return f.exists() && f.isDirectory();
1473N/A }
1473N/A return false;
314N/A }
314N/A
1473N/A /**
1473N/A * {@inheritDoc}
1473N/A */
314N/A @Override
1473N/A public 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
1461N/A /**
1461N/A * {@inheritDoc}
1461N/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
1473N/A /**
1473N/A * {@inheritDoc}
1473N/A */
765N/A @Override
1473N/A public boolean hasHistoryForDirectories() {
765N/A return true;
765N/A }
765N/A
1473N/A /**
1473N/A * {@inheritDoc}
1473N/A */
765N/A @Override
1473N/A public History getHistory(File file) throws HistoryException {
792N/A return getHistory(file, null);
792N/A }
792N/A
1473N/A /**
1473N/A * {@inheritDoc}
1473N/A */
792N/A @Override
1473N/A public History getHistory(File file, String sinceRevision)
1473N/A throws HistoryException
1467N/A {
1467N/A try {
1473N/A if (file == null
1473N/A || !file.getCanonicalPath().startsWith(getDirectoryName()))
1473N/A {
1467N/A return new History();
1467N/A }
1467N/A } catch (IOException e) {
1467N/A return new History();
1467N/A }
792N/A return new MercurialHistoryParser(this).parse(file, sinceRevision);
765N/A }
8N/A}