/* * 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 */ /* * Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved. */ package org.opensolaris.opengrok.history; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.opensolaris.opengrok.configuration.Configuration; import org.opensolaris.opengrok.util.Executor; import org.opensolaris.opengrok.util.IOUtils; import org.opensolaris.opengrok.util.StringUtils; /** * Access to a Git repository. */ public class GitRepository extends Repository { private static final Logger logger = Logger.getLogger(GitRepository.class.getName()); 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 = Configuration.PROPERTY_KEY_PREFIX + "history.git"; /** The command to use to access the repository if none was given explicitly */ public static final String CMD_FALLBACK = "git"; /** git blame command */ private static final String BLAME = "blame"; /** the UUID of the bridged SVN repository */ private String svnUUID = null; /** the svnRev2gitHash map - since git automat. appends it to the log * message, we don't need it yet. So for now it is here just for * completeness. */ private Map oldRevMap; /** LM date of the corresponding file (cached map refresh indicator) */ private long lastModOldRev; /** * Create a new instance of type {@code git}. */ public GitRepository() { type = "git"; datePattern = "EEE MMM dd hh:mm:ss yyyy ZZZZ"; } /** * Get path of the requested file given a commit hash. * Useful for tracking the path when file has changed its location. * * @param fileName name of the file to retrieve the path * @param revision commit hash to track the path of the file * @return full path of the file on success; null string on failure */ private String getCorrectPath(String fileName, String revision) throws IOException { List cmd = new ArrayList(); String path = ""; ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); cmd.add(this.cmd); cmd.add(BLAME); cmd.add("-l"); cmd.add("-C"); cmd.add(fileName); File directory = new File(directoryName); Executor exec = new Executor(cmd, directory); int status = exec.exec(); if (status != 0) { logger.warning("Failed to get blame list in resolving correct path"); return path; } BufferedReader in = new BufferedReader(exec.getOutputReader()); try { String pattern = "^\\W*" + revision + " (.+?) .*$"; Pattern commitPattern = Pattern.compile(pattern); String line = ""; Matcher matcher = commitPattern.matcher(line); while ((line = in.readLine()) != null) { matcher.reset(line); if (matcher.find()) { path = matcher.group(1); break; } } } finally { in.close(); } return path; } /** * Get the mapping of this repository based revisions to the revisions of * the non-hybrid repository. Key is the full git revision aka 'hash', the * corresponding value is the revision string in the non-hybrid repository. * @return an empty map, if this is a native git repository or is not * a hybrid (like git-svn), the fully populated map otherwise. */ Map getSrcRevisionMap() { if (oldRevMap != null) { return oldRevMap; } File f = new File(directoryName, ".git" + File.separator + "svn" + File.separator + "refs" + File.separator + "remotes" + File.separator + "git-svn" + File.separator + ".rev_map." + getSvnUUID()); if (! f.exists()) { oldRevMap = Collections.emptyMap(); return oldRevMap; } if (oldRevMap != null) { long t = f.lastModified(); if (t <= lastModOldRev) { return oldRevMap; } lastModOldRev = t; } Map m = new HashMap<>(); try (DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(f)))) { final char[] HEX = "0123456789abcdef".toCharArray(); // NH40 => an uint32_t and a String of 40 hex characters (sha1) long rev; final byte[] hex_uuid = new byte[20]; final char[] hash = new char[hex_uuid.length * 2]; long count = f.length() / (4 + hex_uuid.length); int i, k; byte b; for (; count > 0; count--) { rev = 0xFFFFFFFFL & dis.readInt(); dis.read(hex_uuid, 0, hex_uuid.length); for (i=0, k=0; i < hex_uuid.length; i++) { b = hex_uuid[i]; hash[k++] = HEX[(b >> 4) & 0xF]; hash[k++] = HEX[(b & 0xF)]; } m.put(new String(hash), Long.toString(rev, 10)); } // sometimes the last record is a placeholder - the hash // is made of zeros, only. k = 0; for (i=0; i < hex_uuid.length; i++) { if (hex_uuid[i] != 0) { k++; break; } } if (k == 0) { m.remove(new String(hash)); } } catch (IOException e) { logger.warning(e.getLocalizedMessage()); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "getSrcRevisionMap()", e); } } oldRevMap = m; return oldRevMap; } /** * Get the UUID assigned by git-svn to the repository. * @return {@code null} if this is not a subversion hybrid repository, * the git-svn UUID otherwise. */ public String getSvnUUID() { if (svnUUID == null) { File f = new File(directoryName, ".git" + File.separator + "svn" + File.separator + ".metadata"); if (f.exists()) { try (BufferedReader br = new BufferedReader(new FileReader(f))){ String s; boolean svn_rem = false; while ((s = br.readLine()) != null) { if (! svn_rem) { if (s.trim().equals("[svn-remote \"svn\"]")) { svn_rem = true; } continue; } s = s.trim(); if (s.length() == 43 && s.startsWith("uuid = ")) { svnUUID = s.substring(7); break; } } } catch (IOException e) { logger.warning(e.getLocalizedMessage()); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "getSvnUUID()", e); } } } if (svnUUID == null) { svnUUID = ""; } } return svnUUID.isEmpty() ? null : svnUUID; } /** * {@inheritDoc} */ @Override public void setDirectoryName(String directoryName) { if (!StringUtils.isSame(this.directoryName, directoryName)) { // should not happen twice, but if, make sure we flush the cache svnUUID = null; oldRevMap = null; } super.setDirectoryName(directoryName); } /** * Get an executor to be used for retrieving the history log for the * named file. * * @param file The file to retrieve history for (canonical path incl. source * root). * @return An Executor ready to be started */ Executor getHistoryLogExecutor(final File file, String sinceRevision) throws IOException { String abs = file.getCanonicalPath(); String filename = ""; if (abs.length() > directoryName.length()) { filename = abs.substring(directoryName.length() + 1); } List cmd = new ArrayList(); ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); cmd.add(this.cmd); cmd.add("log"); cmd.add("--name-only"); cmd.add("--pretty=fuller"); if (sinceRevision != null) { cmd.add(sinceRevision + ".."); } if (filename.length() > 0) { cmd.add(filename); } return new Executor(cmd, new File(getDirectoryName())); } /** * Create a {@code Reader} that reads an {@code InputStream} using the * correct character encoding. * * @param input a stream with the output from a log or blame command * @return a reader that reads the input * @throws IOException if the reader cannot be created */ @SuppressWarnings("static-method") Reader newLogReader(InputStream input) throws IOException { // Bug #17731: Git always encodes the log output using UTF-8 (unless // overridden by i18n.logoutputencoding, but let's assume that hasn't // been done for now). Create a reader that uses UTF-8 instead of the // platform's default encoding. return new InputStreamReader(input, "UTF-8"); } /** * {@inheritDoc} */ @SuppressWarnings("resource") @Override public InputStream getHistoryGet(String parent, String basename, String rev) { InputStream ret = null; File directory = new File(directoryName); ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; Process process = null; InputStream in = null; try { String filename = (new File(parent, basename)).getCanonicalPath() .substring(directoryName.length() + 1); ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); String argv[] = {cmd, "show", rev + ":" + filename}; process = Runtime.getRuntime().exec(argv, null, directory); in = process.getInputStream(); int len; boolean error = true; while ((len = in.read(buffer)) != -1) { error = false; if (len > 0) { output.write(buffer, 0, len); } } if (error) { process.destroy(); String path = getCorrectPath(filename, rev); argv[2] = rev + ":" + path; process = Runtime.getRuntime().exec(argv, null, directory); in = process.getInputStream(); while ((len = in.read(buffer)) != -1) { if (len > 0) { output.write(buffer, 0, len); } } } ret = new ByteArrayInputStream(output.toByteArray()); } catch (Exception exp) { logger.warning("Failed to get history: " + exp.getMessage()); logger.log(Level.FINE, "getHistoryGet", exp); } finally { // Clean up zombie-processes... if (process != null) { try { process.exitValue(); } catch (IllegalThreadStateException exp) { // the process is still running??? just kill it.. process.destroy(); } } IOUtils.close(in); } return ret; } /** Pattern used to extract author/revision from git blame. */ private static final Pattern BLAME_PATTERN = Pattern.compile("^\\W*(\\w+).+?\\((\\D+).*$"); /** * {@inheritDoc} */ @Override public Annotation annotate(File file, String revision) throws IOException { List cmd = new ArrayList(); ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); cmd.add(this.cmd); cmd.add(BLAME); cmd.add("-l"); if (revision != null) { cmd.add(revision); } cmd.add(file.getName()); Executor exec = new Executor(cmd, file.getParentFile()); int status = exec.exec(); // File might have changed its location if (status != 0) { cmd.clear(); ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); cmd.add(this.cmd); cmd.add(BLAME); cmd.add("-l"); cmd.add("-C"); cmd.add(file.getName()); exec = new Executor(cmd, file.getParentFile()); status = exec.exec(); if (status != 0) { logger.warning("Failed to get blame list"); } BufferedReader in = new BufferedReader(exec.getOutputReader()); try { String pattern = "^\\W*" + revision + " (.+?) .*$"; Pattern commitPattern = Pattern.compile(pattern); String line = ""; Matcher matcher = commitPattern.matcher(line); while ((line = in.readLine()) != null) { matcher.reset(line); if (matcher.find()) { String filepath = matcher.group(1); cmd.clear(); ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); cmd.add(this.cmd); cmd.add(BLAME); cmd.add("-l"); if (revision != null) { cmd.add(revision); } cmd.add("--"); cmd.add(filepath); File directory = new File(directoryName); exec = new Executor(cmd, directory); status = exec.exec(); if (status != 0) { logger.warning("Failed to get blame details for modified file path"); } break; } } } finally { in.close(); } } if (status != 0) { logger.warning("Failed to get annotations for '" + file.getAbsolutePath() + "' - Exit code " + status); } return parseAnnotation(newLogReader(exec.getOutputStream()), file.getName()); } /** * Parse the given input for annotation infos. * @param input data to parse * @param fileName name of the file associated with the given input (used * for reporting, only) * @return a annotion which may or may not contain all reqiuired information. * @throws IOException */ private static Annotation parseAnnotation(Reader input, String fileName) throws IOException { BufferedReader in = new BufferedReader(input); Annotation ret = new Annotation(fileName); String line = ""; int lineno = 0; Matcher matcher = BLAME_PATTERN.matcher(line); while ((line = in.readLine()) != null) { ++lineno; matcher.reset(line); if (matcher.find()) { String rev = matcher.group(1); String author = matcher.group(2).trim(); ret.addLine(rev, author); } else { logger.log(Level.WARNING, "Did not find annotation in line {0} [{1}] of {2}", new Object[] { String.valueOf(lineno), line, fileName }); } } return ret; } /** * {@inheritDoc} */ @Override public boolean fileHasAnnotation(File file) { return true; } /** * {@inheritDoc} */ @Override public void update() throws IOException { File directory = new File(getDirectoryName()); List cmd = new ArrayList(); ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); cmd.add(this.cmd); cmd.add("config"); cmd.add("--list"); Executor executor = new Executor(cmd, directory); if (executor.exec() != 0) { throw new IOException(executor.getErrorString()); } if (executor.getOutputString().indexOf("remote.origin.url=") != -1) { cmd.clear(); cmd.add(this.cmd); cmd.add("pull"); cmd.add("-n"); cmd.add("-q"); if (executor.exec() != 0) { throw new IOException(executor.getErrorString()); } } } /** * {@inheritDoc} */ @Override public boolean fileHasHistory(File file) { // Todo: is there a cheap test for whether Git has history // available for a file? // Otherwise, this is harmless, since Git's commands will just // print nothing if there is no history. return true; } /** * {@inheritDoc} */ @Override public boolean isRepositoryFor(File file) { if (file.isDirectory()) { File f = new File(file, ".git"); return f.exists() && f.isDirectory(); } return false; } /** * {@inheritDoc} */ @Override public boolean isWorking() { if (working == null) { ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); working = checkCmd(cmd, "--help"); } return working.booleanValue(); } /** * {@inheritDoc} */ @Override public boolean hasHistoryForDirectories() { return true; } /** * {@inheritDoc} */ @Override public History getHistory(File file) throws HistoryException { return getHistory(file, null); } /** * {@inheritDoc} */ @Override public History getHistory(File file, String sinceRevision) throws HistoryException { return new GitHistoryParser().parse(file, this, sinceRevision); } }