/* * Copyright 2015-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.util.versioncontrol; import com.facebook.buck.log.Logger; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.MoreMaps; import com.facebook.buck.util.ObjectMappers; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProcessExecutorFactory; import com.facebook.buck.util.ProcessExecutorParams; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.annotation.Nullable; public class HgCmdLineInterface implements VersionControlCmdLineInterface { private static final Logger LOG = Logger.get(VersionControlCmdLineInterface.class); private static final Map<String, String> HG_ENVIRONMENT_VARIABLES = ImmutableMap.of( // Set HGPLAIN to prevent user-defined Hg aliases from interfering with the expected behavior. "HGPLAIN", "1"); /** * Path to the rawmanifest.py Mercurial extenions used to transfer the manifest to Buck. We can't * use PackagedResource here because we need to get the raw manifest from the AutoSparse * ProjectFileSystemDelegate, which should not have access to the parent ProjectFileSystem. */ private static final String PATH_TO_RAWMANIFEST_PY = System.getProperty( "buck.path_to_rawmanifest_py", // Fall back on this value when running Buck from an IDE. new File("src/com/facebook/buck/util/versioncontrol/rawmanifest.py").getAbsolutePath()); private static final Pattern HG_REVISION_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9]+$"); private static final String HG_CMD_TEMPLATE = "{hg}"; private static final String REVISION_ID_TEMPLATE = "{revision}"; private static final String PATH_TEMPLATE = "{path}"; private static final ImmutableList<String> ROOT_COMMAND = ImmutableList.of(HG_CMD_TEMPLATE, "root"); private static final ImmutableList<String> CURRENT_REVISION_ID_COMMAND = ImmutableList.of(HG_CMD_TEMPLATE, "log", "-l", "1", "--template", "{node|short}"); // -mardu: Track modified, added, deleted, unknown private static final ImmutableList<String> CHANGED_FILES_COMMAND = ImmutableList.of(HG_CMD_TEMPLATE, "status", "-mardu", "-0", "--rev", REVISION_ID_TEMPLATE); private static final ImmutableList<String> SPARSE_IMPORT_COMMAND = ImmutableList.of( HG_CMD_TEMPLATE, "sparse", "-Tjson", "--import-rules", PATH_TEMPLATE, "--traceback"); private static final ImmutableList<String> RAW_MANIFEST_COMMAND = ImmutableList.of( HG_CMD_TEMPLATE, "--config", "extensions.rawmanifest=" + PATH_TO_RAWMANIFEST_PY, "rawmanifest", "-d", "-o", PATH_TEMPLATE); private static final ImmutableList<String> FAST_STATS_COMMAND = ImmutableList.of( HG_CMD_TEMPLATE, "log", "--rev", ". + ancestor(.,remote/master)", "--template", "{node|short} {date|hgdate} {remotebookmarks}\\n"); private ProcessExecutorFactory processExecutorFactory; private final Path projectRoot; private final String hgCmd; private final ImmutableMap<String, String> environment; @Nullable private Optional<Path> hgRoot; public HgCmdLineInterface( ProcessExecutorFactory processExecutorFactory, Path projectRoot, String hgCmd, ImmutableMap<String, String> environment) { this.processExecutorFactory = processExecutorFactory; this.projectRoot = projectRoot; this.hgCmd = hgCmd; this.environment = MoreMaps.merge(environment, HG_ENVIRONMENT_VARIABLES); } @Override public boolean isSupportedVersionControlSystem() { return true; } public String currentRevisionId() throws VersionControlCommandFailedException, InterruptedException { return validateRevisionId(executeCommand(CURRENT_REVISION_ID_COMMAND)); } @Override public String diffBetweenRevisions(String baseRevision, String tipRevision) throws VersionControlCommandFailedException, InterruptedException { validateRevisionId(baseRevision); validateRevisionId(tipRevision); File temp = null; try { temp = File.createTempFile("diff", ".tmp"); // Command: hg export -r "base::tip - base" executeCommand( ImmutableList.of( HG_CMD_TEMPLATE, "export", "-o", temp.toString(), "--rev", baseRevision + "::" + tipRevision + " - " + baseRevision)); return new String(Files.readAllBytes(temp.toPath())); } catch (IOException e) { LOG.debug(e.getMessage()); throw new VersionControlCommandFailedException(e.getMessage()); } finally { if (temp != null) { temp.delete(); } } } @Override public ImmutableSet<String> changedFiles(String fromRevisionId) throws VersionControlCommandFailedException, InterruptedException { String hgChangedFilesString = executeCommand( replaceTemplateValue(CHANGED_FILES_COMMAND, REVISION_ID_TEMPLATE, fromRevisionId)); return Arrays.stream(hgChangedFilesString.split("\0")) .filter(s -> !s.isEmpty()) .collect(MoreCollectors.toImmutableSet()); } @Override public FastVersionControlStats fastVersionControlStats() throws InterruptedException, VersionControlCommandFailedException { String output = executeCommand(FAST_STATS_COMMAND, false); String[] lines = output.split("\n"); switch (lines.length) { case 1: return parseFastStats(lines[0], lines[0]); case 2: return parseFastStats(lines[0], lines[1]); } throw new VersionControlCommandFailedException( String.format( "Unexpected number of lines output from '%s':\n%s", FAST_STATS_COMMAND.stream().collect(Collectors.joining(" ")), output)); } private FastVersionControlStats parseFastStats( String currentRevisionLine, String baseRevisionLine) throws VersionControlCommandFailedException { String numberOfWordsMismatchFormat = String.format( "Unexpected number of words output from '%s', expected 3 or more:\n%%s", FAST_STATS_COMMAND.stream().collect(Collectors.joining(" "))); String[] currentRevisionWords = currentRevisionLine.split(" ", 4); if (currentRevisionWords.length < 3) { throw new VersionControlCommandFailedException( String.format(numberOfWordsMismatchFormat, currentRevisionLine)); } String[] baseRevisionWords = baseRevisionLine.split(" ", 4); if (baseRevisionWords.length < 3) { throw new VersionControlCommandFailedException( String.format(numberOfWordsMismatchFormat, baseRevisionLine)); } return FastVersionControlStats.of( currentRevisionWords[0], baseRevisionWords.length == 4 ? ImmutableSet.copyOf(baseRevisionWords[3].split(" ")) : ImmutableSet.of(), baseRevisionWords[0], Long.valueOf(baseRevisionWords[1])); } public String extractRawManifest() throws VersionControlCommandFailedException, InterruptedException { try { Path hgmanifestDir = Files.createTempDirectory("hgmanifest"); hgmanifestDir.toFile().deleteOnExit(); Path hgmanifestOutput = hgmanifestDir.resolve("manifest.raw"); executeCommand( replaceTemplateValue(RAW_MANIFEST_COMMAND, PATH_TEMPLATE, hgmanifestOutput.toString())); return hgmanifestOutput.toString(); } catch (IOException e) { throw new VersionControlCommandFailedException("Unable to load hg manifest"); } } @Nullable public Path getHgRoot() throws InterruptedException { if (hgRoot == null) { try { hgRoot = Optional.of(Paths.get(executeCommand(ROOT_COMMAND))); } catch (VersionControlCommandFailedException e) { hgRoot = Optional.empty(); } } return hgRoot.orElse(null); } public SparseSummary exportHgSparseRules(Path exportFile) throws VersionControlCommandFailedException, InterruptedException { String json = executeCommand( replaceTemplateValue(SPARSE_IMPORT_COMMAND, PATH_TEMPLATE, exportFile.toString())); try (JsonParser parser = ObjectMappers.createParser(json)) { return ObjectMappers.READER .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) .readValue(parser, SparseSummary.class); } catch (IOException e) { throw new VersionControlCommandFailedException("Unable to parse sparse summary output"); } } private String executeCommand(Iterable<String> command) throws VersionControlCommandFailedException, InterruptedException { return executeCommand(command, true); } private String executeCommand(Iterable<String> command, boolean cleanOutput) throws VersionControlCommandFailedException, InterruptedException { command = replaceTemplateValue(command, HG_CMD_TEMPLATE, hgCmd); String commandString = commandAsString(command); LOG.debug("Executing command: " + commandString); ProcessExecutorParams processExecutorParams = ProcessExecutorParams.builder() .setCommand(command) .setDirectory(projectRoot) .setEnvironment(environment) .build(); ProcessExecutor.Result result; try (PrintStream stdout = new PrintStream(new ByteArrayOutputStream()); PrintStream stderr = new PrintStream(new ByteArrayOutputStream())) { ProcessExecutor processExecutor = processExecutorFactory.createProcessExecutor(stdout, stderr); result = processExecutor.launchAndExecute(processExecutorParams); } catch (IOException e) { throw new VersionControlCommandFailedException(e); } Optional<String> resultString = result.getStdout(); if (!resultString.isPresent()) { throw new VersionControlCommandFailedException( "Received no output from launched process for command: " + commandString); } if (result.getExitCode() != 0) { throw new VersionControlCommandFailedException( result.getMessageForUnexpectedResult(commandString)); } if (cleanOutput) { return cleanResultString(resultString.get()); } else { return resultString.get(); } } private static String validateRevisionId(String revisionId) throws VersionControlCommandFailedException { Matcher revisionIdMatcher = HG_REVISION_ID_PATTERN.matcher(revisionId); if (!revisionIdMatcher.matches()) { throw new VersionControlCommandFailedException(revisionId + " is not a valid revision ID."); } return revisionId; } private static Iterable<String> replaceTemplateValue( Iterable<String> values, final String template, final String replacement) { return StreamSupport.stream(values.spliterator(), false) .map(text -> text.contains(template) ? text.replace(template, replacement) : text) .collect(MoreCollectors.toImmutableList()); } private static String commandAsString(Iterable<String> command) { return Joiner.on(" ").join(command); } private static String cleanResultString(String result) { return result.trim().replace("\'", "").replace("\n", ""); } }