/* * Copyright 2016 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.config.materials.svn; import com.thoughtworks.go.config.PasswordEncrypter; import com.thoughtworks.go.config.materials.PasswordAwareMaterial; import com.thoughtworks.go.config.materials.ScmMaterial; import com.thoughtworks.go.config.materials.ScmMaterialConfig; import com.thoughtworks.go.config.materials.SubprocessExecutionContext; import com.thoughtworks.go.domain.MaterialInstance; import com.thoughtworks.go.domain.materials.*; import com.thoughtworks.go.domain.materials.svn.*; import com.thoughtworks.go.security.GoCipher; import com.thoughtworks.go.util.FileUtil; import com.thoughtworks.go.util.GoConstants; import com.thoughtworks.go.util.StringUtil; import com.thoughtworks.go.util.command.ConsoleOutputStreamConsumer; import com.thoughtworks.go.util.command.UrlArgument; import org.apache.log4j.Logger; import org.bouncycastle.crypto.InvalidCipherTextException; import javax.annotation.PostConstruct; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import static com.thoughtworks.go.util.ExceptionUtils.bomb; import static com.thoughtworks.go.util.ExceptionUtils.bombIfNull; import static com.thoughtworks.go.util.FileUtil.createParentFolderIfNotExist; import static java.lang.String.format; /** * @understands configuration for subversion */ public class SvnMaterial extends ScmMaterial implements PasswordEncrypter, PasswordAwareMaterial { private static final Logger LOGGER = Logger.getLogger(SvnMaterial.class); private UrlArgument url; private String userName; private String password; private String encryptedPassword; private boolean checkExternals; private transient Subversion svnLazyLoaded; private final GoCipher goCipher; public static final String TYPE = "SvnMaterial"; private SvnMaterial(GoCipher goCipher) { super("SvnMaterial"); this.goCipher = goCipher; } public SvnMaterial(String url, String userName, String password, boolean checkExternals) { this(url, userName, password, checkExternals, new GoCipher()); } public SvnMaterial(Subversion svn) { this(svn.getUrl().forCommandline(), svn.getUserName(), svn.getPassword(), svn.isCheckExternals()); this.svnLazyLoaded = svn; } public SvnMaterial(String url, String userName, String password, boolean checkExternals, GoCipher goCipher) { this(goCipher); bombIfNull(url, "null url"); setUrl(url); this.userName = userName; setPassword(password); this.checkExternals = checkExternals; } public SvnMaterial(String url, String userName, String password, boolean checkExternals, String folder) { this(url, userName, password, checkExternals); this.folder = folder; } public SvnMaterial(SvnMaterialConfig config) { this(config.getUrl(), config.getUserName(), config.getPassword(), config.isCheckExternals(), config.getGoCipher()); this.autoUpdate = config.getAutoUpdate(); this.filter = config.rawFilter(); this.invertFilter = config.getInvertFilter(); this.folder = config.getFolder(); this.name = config.getName(); } @Override public MaterialConfig config() { return new SvnMaterialConfig(url, userName, getPassword(), checkExternals, goCipher, autoUpdate, filter, invertFilter, folder, name); } private Subversion svn() { if (svnLazyLoaded == null || !svnLazyLoaded.getUrl().equals(url)) { svnLazyLoaded = new SvnCommand(getFingerprint(), url.forCommandline(), userName, getPassword(), checkExternals); } return svnLazyLoaded; } public List<Modification> latestModification(File baseDir, final SubprocessExecutionContext execCtx) { return svn().latestModification(); } public List<Modification> modificationsSince(File workingDirectory, Revision revision, final SubprocessExecutionContext execCtx) { return svn().modificationsSince(new SubversionRevision(revision.getRevision())); } public MaterialInstance createMaterialInstance() { return new SvnMaterialInstance(url.forCommandline(), userName, UUID.randomUUID().toString(), checkExternals); } @Override protected void appendCriteria(Map parameters) { parameters.put(ScmMaterialConfig.URL, url.forCommandline()); parameters.put(ScmMaterialConfig.USERNAME, userName); parameters.put("checkExternals", checkExternals); } @Override protected void appendAttributes(Map parameters) { parameters.put(ScmMaterialConfig.URL, url); parameters.put(ScmMaterialConfig.USERNAME, userName); parameters.put("checkExternals", checkExternals); } public void updateTo(ConsoleOutputStreamConsumer outputStreamConsumer, File baseDir, RevisionContext revisionContext, final SubprocessExecutionContext execCtx) { Revision revision = revisionContext.getLatestRevision(); File workingDir = execCtx.isServer() ? baseDir : workingdir(baseDir); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Updating to revision: " + revision + " in workingdirectory " + workingDir); } outputStreamConsumer.stdOutput(format("[%s] Start updating %s at revision %s from %s", GoConstants.PRODUCT_NAME, updatingTarget(), revision.getRevision(), url)); boolean shouldDoFreshCheckout = !workingDir.isDirectory() || isRepositoryChanged(workingDir); if (shouldDoFreshCheckout) { freshCheckout(outputStreamConsumer, new SubversionRevision(revision), workingDir); } else { cleanupAndUpdate(outputStreamConsumer, new SubversionRevision(revision), workingDir); } LOGGER.debug("done with update"); outputStreamConsumer.stdOutput(format("[%s] Done.\n", GoConstants.PRODUCT_NAME)); } public boolean isRepositoryChanged(File workingFolder) { try { File file = new File(workingFolder, ".svn"); if (workingFolder.isDirectory() && file.exists() && file.isDirectory()) { String workingUrl = svn().workingRepositoryUrl(workingFolder); return !MaterialUrl.sameUrl(url.toString(), workingUrl); } else { return true; } } catch (IOException e) { return true; } } public void freshCheckout(ConsoleOutputStreamConsumer outputStreamConsumer, SubversionRevision revision, File workingFolder) { if (workingFolder.isDirectory()) { FileUtil.deleteFolder(workingFolder); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Checking out to revision " + revision + " in " + workingFolder); } createParentFolderIfNotExist(workingFolder); svn().checkoutTo(outputStreamConsumer, workingFolder, revision); } public void cleanupAndUpdate(ConsoleOutputStreamConsumer outputStreamConsumer, SubversionRevision revision, File workingFolder) { try { svn().cleanupAndRevert(outputStreamConsumer, workingFolder); } catch (Exception e) { String message = "Failed to do cleanup and revert in " + workingFolder.getAbsolutePath(); LOGGER.error(message); LOGGER.debug(message, e); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Updating to revision " + revision + " on " + workingFolder); } svn().updateTo(outputStreamConsumer, workingFolder, revision); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } if (!super.equals(o)) { return false; } SvnMaterial that = (SvnMaterial) o; if (checkExternals != that.checkExternals) { return false; } if (url != null ? !url.equals(that.url) : that.url != null) { return false; } if (userName != null ? !userName.equals(that.userName) : that.userName != null) { return false; } return true; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + (url != null ? url.hashCode() : 0); result = 31 * result + (userName != null ? userName.hashCode() : 0); result = 31 * result + (checkExternals ? 1 : 0); return result; } protected String getLocation() { return url == null ? null : url.forDisplay(); } public String getTypeForDisplay() { return "Subversion"; } @Override public Map<String, Object> getAttributes(boolean addSecureFields) { Map<String, Object> materialMap = new HashMap<>(); materialMap.put("type", "svn"); Map<String, Object> configurationMap = new HashMap<>(); if (addSecureFields) { configurationMap.put("url", url.forCommandline()); } else { configurationMap.put("url", url.forDisplay()); } configurationMap.put("username", userName); if (addSecureFields) { configurationMap.put("password", getPassword()); } configurationMap.put("check-externals", checkExternals); materialMap.put("svn-configuration", configurationMap); return materialMap; } public Class getInstanceType() { return SvnMaterialInstance.class; } public ValidationBean checkConnection(final SubprocessExecutionContext execCtx) { return svn().checkConnection(); } public String getUrl() { return url == null ? null : url.forCommandline(); } @Override protected UrlArgument getUrlArgument() { return url; } public String getLongDescription() { return String.format("URL: %s, Username: %s, CheckExternals: %s", url.forDisplay(), userName, checkExternals); } public String getUserName() { return userName; } public String getPassword() { return currentPassword(); } public void setUrl(String url) { this.url = new UrlArgument(url); } public void setPassword(String password) { resetPassword(password); } public boolean isCheckExternals() { return checkExternals; } private String folderFor(String folderForExternal) { return getFolder() == null ? folderForExternal : getFolder() + "/" + folderForExternal; } public void add(ConsoleOutputStreamConsumer outputStreamConsumer, File file) { svn().add(outputStreamConsumer, file); } public void commit(ConsoleOutputStreamConsumer outputStreamConsumer, File workingDir, String message) { svn().commit(outputStreamConsumer, workingDir, message); } @Override public boolean matches(String name, String regex) { if (!regex.startsWith("/")) { regex = "/" + regex; } return name.matches(regex); } @Override public String toString() { return "SvnMaterial{" + "url=" + url + ", userName='" + userName + '\'' + ", checkExternals=" + checkExternals + '}'; } /** * @deprecated used only in tests - we need to disentangle this */ public static SvnMaterial createSvnMaterialWithMock(Subversion svn) { return new SvnMaterial(svn); } private void resetPassword(String passwordToSet) { if (StringUtil.isBlank(passwordToSet)) { encryptedPassword = null; } setPasswordIfNotBlank(passwordToSet); } private void setPasswordIfNotBlank(String password) { if (StringUtil.isBlank(password)) { return; } try { this.encryptedPassword = this.goCipher.encrypt(password); } catch (Exception e) { bomb("Password encryption failed. Please verify your cipher key.", e); } this.password = null; } public String getEncryptedPassword() { return encryptedPassword; } @PostConstruct public void ensureEncrypted() { setPasswordIfNotBlank(password); } public String currentPassword() { try { return StringUtil.isBlank(encryptedPassword) ? null : this.goCipher.decrypt(encryptedPassword); } catch (InvalidCipherTextException e) { throw new RuntimeException("Could not decrypt the password to get the real password", e); } } }