/* * Copyright 2017 ThoughtWorks, 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.thoughtworks.go.domain.materials.svn; import com.thoughtworks.go.config.materials.svn.SvnMaterial; import com.thoughtworks.go.domain.materials.Modification; import com.thoughtworks.go.domain.materials.Modifications; import com.thoughtworks.go.domain.materials.SCMCommand; import com.thoughtworks.go.domain.materials.ValidationBean; import com.thoughtworks.go.util.SvnLogXmlParser; import com.thoughtworks.go.util.command.*; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.input.SAXBuilder; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.text.ParseException; import java.util.HashMap; import java.util.List; import java.util.Set; import static com.thoughtworks.go.util.ExceptionUtils.bomb; import static com.thoughtworks.go.util.ExceptionUtils.bombIf; import static com.thoughtworks.go.util.command.CommandLine.createCommandLine; public class SvnCommand extends SCMCommand implements Subversion { private UrlArgument repositoryUrl; private StringArgument userName; private PasswordArgument password; private boolean checkExternals; private static final Logger LOG = Logger.getLogger(SvnCommand.class); public static final String SVN_DATE_FORMAT_IN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; public static final String SVN_DATE_FORMAT_OUT = "yyyy-MM-dd'T'HH:mm:ss.SSS"; private static final String ERR_SVN_NOT_FOUND = "Failed to find 'svn' on your PATH. Please ensure 'svn' is executable by the Go Server and on the Go Agents where this material will be used."; private final SvnLogXmlParser svnLogXmlParser; private transient final static ThreadLocal<SAXBuilder> saxBuilderThreadLocal = new ThreadLocal<>(); public SvnCommand(String materialFingerprint, String repositoryUrl) { this(materialFingerprint, repositoryUrl, null, null, false); } public SvnCommand(String materialFingerprint, String url, String userName, String password, boolean checkExternals) { super(materialFingerprint); this.repositoryUrl = url == null ? null : new UrlArgument(url); this.checkExternals = checkExternals; this.userName = new StringArgument(userName); this.password = new PasswordArgument(password); this.svnLogXmlParser = new SvnLogXmlParser(); } public ValidationBean checkConnection() { CommandLine command = buildSvnLogCommandForLatestOne(); try { executeCommand(command); return ValidationBean.valid(); } catch (Exception e) { try{ version(); LOG.error("failed to connect to " + getUrlForDisplay(), e); return ValidationBean.notValid("svn: Malformed URL " + getUrlForDisplay() + " : \n" + e.getMessage()); } catch (Exception exp){ return ValidationBean.notValid(ERR_SVN_NOT_FOUND); } } } public List<SvnExternal> getAllExternalURLs() { CommandLine svnExternalCommand = svn(true) .withArgs("propget", "--non-interactive", "svn:externals", "-R") .withArg(repositoryUrl); ConsoleResult result = executeCommand(svnExternalCommand); String svnExternalConsoleOut = result.outputAsString(); SvnInfo remoteInfo = remoteInfo(new SAXBuilder()); String repoUrl = remoteInfo.getUrl(); String repoRoot = remoteInfo.getRoot(); List<SvnExternal> svnExternalList = null; try { svnExternalList = new SvnExternalParser().parse(svnExternalConsoleOut, repoUrl, repoRoot); } catch (RuntimeException e) { throw (RuntimeException) result.smudgedException(e); } return svnExternalList; } public UrlArgument getUrl() { return repositoryUrl; } CommandLine buildSvnLogCommandForLatestOne() { return svn(true) .withArgs("log", "--non-interactive", "--xml", "-v", "--limit", "1") .withArg(repositoryUrl); } public List<Modification> latestModification() { CommandLine command = buildSvnLogCommandForLatestOne(); ConsoleResult result = executeCommand(command); String output = result.outputAsString(); try { return parseSvnLog(output); } catch (Exception e) { throw bomb(result.smudgedException(e)); } } public List<Modification> modificationsSince(SubversionRevision subversionRevision) { CommandLine command = svn(true) .withArgs("log", "--non-interactive", "--xml", "-v", "-r", "HEAD:" + subversionRevision.getRevision()) .withArg(repositoryUrl); ConsoleResult result = executeCommand(command); String output = result.outputAsString(); try { List<Modification> modifications = parseSvnLog(output); modifications = Modifications.filterOutRevision(modifications, subversionRevision); return modifications; } catch (Exception e) { LOG.error("Error parsing svn log output", result.smudgedException(e)); throw bomb(e); } } private List<Modification> parseSvnLog(String output) { SAXBuilder builder = getBuilder(); SvnInfo svnInfo = remoteInfo(builder); return svnLogXmlParser.parse(output, svnInfo.getPath(), builder); } private SAXBuilder getBuilder() { SAXBuilder saxBuilder = saxBuilderThreadLocal.get(); if(saxBuilder == null){ saxBuilder = new SAXBuilder(); saxBuilderThreadLocal.set(saxBuilder); } return saxBuilder; } public SvnInfo remoteInfo(SAXBuilder builder) { SvnInfo svnInfo = new SvnInfo(); svnInfo.parse(executeCommand(svn(true) .withArgs("info", "--xml", "--non-interactive") .withArg(repositoryUrl)).outputAsString(), builder); return svnInfo; } public SvnInfo workingDirInfo(File workingDir) throws IOException { SvnInfo svnInfo = new SvnInfo(); svnInfo.parse(executeCommand(svn(false).withArgs("info", "--xml", "--non-interactive").withArg(workingDir.getCanonicalPath())).outputForDisplayAsString(), getBuilder()); return svnInfo; } public SubversionRevision checkoutTo(ConsoleOutputStreamConsumer outputStreamConsumer, File targetFolder, SubversionRevision revision) { CommandLine command = svn(true) .withArgs("checkout", "--non-interactive", "-r", revision.getRevision()) .withArg(repositoryUrl) .withArg(targetFolder.getAbsolutePath()); executeCommand(command, outputStreamConsumer); bombIf(!targetFolder.exists(), "Folder was not created or does not exist.. something broken"); return null; } public void updateTo(ConsoleOutputStreamConsumer outputStreamConsumer, File workingFolder, SubversionRevision targetRevision) { CommandLine command = svn(true).withArgs("update", "--non-interactive", "-r", targetRevision.getRevision(), workingFolder.getAbsolutePath()); executeCommand(command, outputStreamConsumer); } public void cleanupAndRevert(ConsoleOutputStreamConsumer outputStreamConsumer, File workingFolder) { CommandLine command = svn(false).withArgs("cleanup", workingFolder.getAbsolutePath()); executeCommand(command, outputStreamConsumer); command = svn(false).withArgs("revert", "--recursive", workingFolder.getAbsolutePath()); executeCommand(command, outputStreamConsumer); } public String workingRepositoryUrl(File workingFolder) throws IOException { return workingDirInfo(workingFolder).getUrl(); } public String getUrlForDisplay() { return repositoryUrl.forDisplay(); } public String getUserName() { return userName.forCommandline(); } public String getPassword() { return password == null ? null : password.forDisplay(); } public boolean isCheckExternals() { return checkExternals; } public String version() { CommandLine svn = createCommandLine("svn").withArgs("--version"); return executeCommand(svn).outputAsString(); } ConsoleResult executeCommand(CommandLine svnCmd) { return runOrBomb(svnCmd); } private int executeCommand(CommandLine svnCmd, ConsoleOutputStreamConsumer outputStreamConsumer) { int returnValue = run(svnCmd, outputStreamConsumer); if (returnValue != 0) { throw new RuntimeException("Failed to run " + svnCmd.toStringForDisplay()); } return returnValue; } private CommandLine svn(boolean needAuth) { CommandLine line = svnExecutable(); if (needAuth) { addCredentials(line, userName, password); } return line; } private CommandLine svnExecutable() { return createCommandLine("svn").withEncoding("UTF-8"); } private void addCredentials(CommandLine line, StringArgument svnUserName, PasswordArgument svnPassword) { if (!StringUtils.isBlank(svnUserName.forCommandline())) { line.withArgs("--username", svnUserName.forCommandline()); if (!StringUtils.isBlank(svnPassword.forCommandline())) { line.withArg("--password"); line.withArg(svnPassword); } line.withNonArgSecret(svnPassword); } } public void add(ConsoleOutputStreamConsumer output, File file) { CommandLine line = svn(false).withArgs("add", file.getAbsolutePath()); executeCommand(line, output); } public void commit(ConsoleOutputStreamConsumer output, File workingDir, String message) { CommandLine line = svn(true).withArgs("commit", "--non-interactive", "-m", message, workingDir.getAbsolutePath()); executeCommand(line, output); } public void propset(File workingDir, String propName, String propValue) { CommandLine line = svn(true).withArgs("propset", "--non-interactive", propName, propValue, "."); line.setWorkingDir(workingDir); executeCommand(line); } public HashMap<String, String> createUrlToRemoteUUIDMap(Set<SvnMaterial> svnMaterials) { HashMap<String, String> urlToUUIDMap = new HashMap<>(); for (SvnMaterial svnMaterial : svnMaterials) { CommandLine command = svnExecutable().withArgs("info", "--xml"); addCredentials(command, new StringArgument(svnMaterial.getUserName()), new PasswordArgument(svnMaterial.getPassword())); final String queryUrl = svnMaterial.getUrl(); command.withArg(queryUrl); ConsoleResult consoleResult = null; try { consoleResult = executeCommand(command); urlToUUIDMap.putAll(svnLogXmlParser.parseInfoToGetUUID(consoleResult.outputAsString(), queryUrl, getBuilder())); } catch (RuntimeException e) { LOG.warn("Failed to map UUID to URL. SVN post-commit will not work for materials with URL " + queryUrl, e); } } return urlToUUIDMap; } static class SvnInfo { private String path = ""; private String encodedUrl = ""; private String root = ""; private static final String ENCODING = "UTF-8"; public void parse(String xmlOutput, SAXBuilder builder) { try { Document document = builder.build(new StringReader(xmlOutput)); parseDOMTree(document); } catch (Exception e) { bomb("Unable to parse svn info output: " + xmlOutput, e); } } private void parseDOMTree(Document document) throws ParseException, UnsupportedEncodingException { Element infoElement = document.getRootElement(); Element entryElement = infoElement.getChild("entry"); String encodedUrl = entryElement.getChildTextTrim("url"); Element repositoryElement = entryElement.getChild("repository"); String root = repositoryElement.getChildTextTrim("root"); String encodedPath = StringUtils.replace(encodedUrl, root, ""); this.path = URLDecoder.decode(encodedPath, ENCODING); this.root = root; this.encodedUrl = encodedUrl; } public String getPath() { return path; } public String getUrl() { return encodedUrl; } public String getRoot() { return root; } } }