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/*
967N/A * Copyright 2010 Sun Microsystems, Inc. All rights reserved.
8N/A * Use is subject to license terms.
8N/A */
8N/Apackage org.opensolaris.opengrok.history;
8N/A
8N/Aimport java.io.BufferedReader;
8N/Aimport java.io.File;
1016N/Aimport java.io.FileNotFoundException;
8N/Aimport java.io.IOException;
8N/Aimport java.io.InputStream;
8N/Aimport java.io.InputStreamReader;
749N/Aimport java.text.DateFormat;
8N/Aimport java.text.ParseException;
8N/Aimport java.util.ArrayList;
25N/Aimport java.util.Date;
792N/Aimport java.util.List;
1481N/Aimport java.util.Map;
459N/Aimport java.util.logging.Level;
1327N/Aimport java.util.logging.Logger;
1327N/A
1470N/Aimport org.opensolaris.opengrok.configuration.Configuration;
50N/Aimport org.opensolaris.opengrok.configuration.RuntimeEnvironment;
574N/Aimport org.opensolaris.opengrok.util.Executor;
25N/A
8N/A/**
24N/A * Parse a stream of mercurial log comments.
8N/A */
770N/Aclass MercurialHistoryParser implements Executor.StreamHandler {
1470N/A private static final Logger logger =
1470N/A Logger.getLogger(MercurialHistoryParser.class.getName());
635N/A /** Prefix which identifies lines with the description of a commit. */
635N/A private static final String DESC_PREFIX = "description: ";
635N/A
792N/A private List<HistoryEntry> entries = new ArrayList<HistoryEntry>();
792N/A private final MercurialRepository repository;
792N/A private final String mydir;
355N/A
792N/A MercurialHistoryParser(MercurialRepository repository) {
792N/A this.repository = repository;
792N/A mydir = repository.getDirectoryName() + File.separator;
615N/A }
615N/A
792N/A /**
792N/A * Parse the history for the specified file or directory. If a changeset is
792N/A * specified, only return the history from the changeset right after the
792N/A * specified one.
792N/A *
792N/A * @param file the file or directory to get history for
792N/A * @param changeset the changeset right before the first one to fetch, or
792N/A * {@code null} if all changesets should be fetched
792N/A * @return history for the specified file or directory
792N/A * @throws HistoryException if an error happens when parsing the history
792N/A */
792N/A History parse(File file, String changeset) throws HistoryException {
1016N/A try {
1016N/A Executor executor = repository.getHistoryLogExecutor(file, changeset);
1016N/A int status = executor.exec(true, this);
1190N/A
1016N/A if (status != 0) {
1327N/A throw new HistoryException("Failed to get history for '" +
1327N/A file.getAbsolutePath() + "' - Exit code " + status);
1016N/A }
1016N/A } catch (IOException e) {
1327N/A throw new HistoryException("Failed to get history for '" +
1327N/A file.getAbsolutePath() + "'", e);
8N/A }
355N/A
792N/A // If a changeset to start from is specified, remove that changeset
792N/A // from the list, since only the ones following it should be returned.
792N/A // Also check that the specified changeset was found, otherwise throw
792N/A // an exception.
792N/A if (changeset != null) {
967N/A repository.removeAndVerifyOldestChangeset(entries, changeset);
792N/A }
792N/A
792N/A return new History(entries);
8N/A }
574N/A
574N/A /**
574N/A * Process the output from the hg log command and insert the HistoryEntries
574N/A * into the history field.
574N/A *
574N/A * @param input The output from the process
1481N/A * @throws IOException If an error occurs while reading the stream
574N/A */
1481N/A @SuppressWarnings("null")
815N/A @Override
574N/A public void processStream(InputStream input) throws IOException {
1470N/A Configuration cfg = RuntimeEnvironment.getConfig();
749N/A DateFormat df = repository.getDateFormat();
574N/A BufferedReader in = new BufferedReader(new InputStreamReader(input));
792N/A entries = new ArrayList<HistoryEntry>();
574N/A String s;
574N/A HistoryEntry entry = null;
1481N/A Map<String, String> srcRevMap = null;
1481N/A String uuid = repository.getSvnUUID();
1481N/A if (uuid != null) {
1481N/A srcRevMap = repository.getSrcRevisionMap();
1481N/A uuid = "@" + uuid;
1481N/A }
574N/A while ((s = in.readLine()) != null) {
574N/A if (s.startsWith("changeset:")) {
574N/A entry = new HistoryEntry();
635N/A entries.add(entry);
574N/A entry.setActive(true);
1481N/A s = s.substring("changeset:".length()).trim();
1481N/A if (uuid != null) {
1481N/A int idx = s.lastIndexOf(':');
1481N/A if (idx != -1) {
1481N/A entry.setOldRevision(srcRevMap.get(s.substring(idx+1)));
1481N/A s = s.substring(0, idx);
1481N/A }
1481N/A }
1481N/A entry.setRevision(s);
574N/A } else if (s.startsWith("user:") && entry != null) {
1481N/A s = s.substring("user:".length()).trim();
1481N/A if (uuid != null && s.endsWith(uuid)) {
1481N/A // strip off hgsubversion UUID since
1481N/A s = s.substring(0, s.length() - uuid.length());
1481N/A }
1481N/A entry.setAuthor(s);
574N/A } else if (s.startsWith("date:") && entry != null) {
574N/A Date date = new Date();
574N/A try {
574N/A date = df.parse(s.substring("date:".length()).trim());
574N/A } catch (ParseException pe) {
1470N/A logger.warning("Could not parse date " + s + ": "
1470N/A + pe.getMessage());
1327N/A logger.log(Level.FINE, "processStream", pe);
574N/A }
574N/A entry.setDate(date);
574N/A } else if (s.startsWith("files:") && entry != null) {
1479N/A // applies to directories only
1479N/A while ((s = in.readLine()) != null) {
1479N/A if (s.isEmpty()) {
1479N/A break;
1479N/A }
1479N/A File f = new File(mydir, s);
1479N/A try {
1479N/A entry.addFile(cfg.getPathRelativeToSourceRoot(f, 0));
1479N/A } catch (FileNotFoundException e) { // NOPMD
1479N/A // If the file is not located under the source root,
1479N/A // ignore it (bug #11664).
574N/A }
574N/A }
635N/A } else if (s.startsWith(DESC_PREFIX) && entry != null) {
635N/A entry.setMessage(decodeDescription(s));
574N/A }
574N/A }
574N/A }
635N/A
635N/A /**
635N/A * Decode a line with a description of a commit. The line is a sequence of
635N/A * XML character entities that need to be converted to single characters.
635N/A * This is to prevent problems if the log message contains one of the
635N/A * prefixes that {@link #processStream(InputStream)} is looking for (bug
635N/A * #405).
635N/A *
635N/A * This method is way too tolerant, and won't complain if the line has
635N/A * a different format than expected. It will return weird results, though.
635N/A *
635N/A * @param line the XML encoded line
635N/A * @return the decoded description
635N/A */
1463N/A private static String decodeDescription(String line) {
635N/A StringBuilder out = new StringBuilder();
635N/A int value = 0;
635N/A
635N/A // fetch the char values from the &#ddd; sequences
635N/A for (int i = DESC_PREFIX.length(); i < line.length(); i++) {
635N/A char ch = line.charAt(i);
635N/A if (Character.isDigit(ch)) {
635N/A value = value * 10 + Character.getNumericValue(ch);
635N/A } else if (ch == ';') {
635N/A out.append((char) value);
635N/A value = 0;
635N/A }
635N/A }
635N/A
635N/A assert value == 0 : "description did not end with a semi-colon";
635N/A
635N/A return out.toString();
635N/A }
8N/A}