/*************************GO-LICENSE-START********************************* * Copyright 2014 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. *************************GO-LICENSE-END***********************************/ package com.thoughtworks.go.config.materials.mercurial; 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.mercurial.HgCommand; import com.thoughtworks.go.domain.materials.mercurial.HgMaterialInstance; import com.thoughtworks.go.domain.materials.svn.MaterialUrl; import com.thoughtworks.go.util.FileUtil; import com.thoughtworks.go.util.GoConstants; import com.thoughtworks.go.util.command.*; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.log4j.Logger; import java.io.File; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.thoughtworks.go.util.ExceptionUtils.bomb; import static com.thoughtworks.go.util.ExceptionUtils.bombIfFailedToRunCommandLine; import static com.thoughtworks.go.util.FileUtil.createParentFolderIfNotExist; import static java.lang.String.format; /** * @understands configuration for mercurial version control */ public class HgMaterial extends ScmMaterial { private static final Pattern HG_VERSION_PATTERN = Pattern.compile(".*\\(.*\\s+(\\d(\\.\\d)+.*)\\)"); private static final Logger LOGGER = Logger.getLogger(HgMaterial.class); private HgUrlArgument url; //TODO: use iBatis to set the type for us, and we can get rid of this field. public static final String TYPE = "HgMaterial"; private static final String ERROR_OLD_VERSION = "Please install Mercurial Version 1.0 or above." + " The current installed hg is "; private static final String ERR_NO_HG_INSTALLED = "Failed to find 'hg' on your PATH. Please ensure 'hg' is executable by the Go Server and on the Go Agents where this material will be used."; private final String HG_DEFAULT_BRANCH = "default"; private HgMaterial() { super(TYPE); } public HgMaterial(String url, String folder) { this(); this.url = new HgUrlArgument(url); this.folder = folder; } public HgMaterial(HgMaterialConfig config) { this(config.getUrl(), config.getFolder()); this.autoUpdate = config.getAutoUpdate(); this.filter = config.rawFilter(); this.invertFilter = config.getInvertFilter(); this.name = config.getName(); } @Override public MaterialConfig config() { return new HgMaterialConfig(url, autoUpdate, filter, invertFilter, folder, name); } public List<Modification> latestModification(File baseDir, final SubprocessExecutionContext execCtx) { HgCommand hgCommand = getHg(baseDir); return hgCommand.latestOneModificationAsModifications(); } public List<Modification> modificationsSince(File baseDir, Revision revision, final SubprocessExecutionContext execCtx) { return getHg(baseDir).modificationsSince(revision); } public MaterialInstance createMaterialInstance() { return new HgMaterialInstance(url.forCommandline(), UUID.randomUUID().toString()); } @Override protected void appendCriteria(Map<String, Object> parameters) { parameters.put(ScmMaterialConfig.URL, url.forCommandline()); } @Override protected void appendAttributes(Map<String, Object> parameters) { parameters.put("url", url); } private HgCommand getHg(File baseDir) { InMemoryStreamConsumer output = ProcessOutputStreamConsumer.inMemoryConsumer(); HgCommand hgCommand = null; try { hgCommand = hg(baseDir, output); } catch (Exception e) { bomb(e.getMessage() + " " + output.getStdError(), e); } return hgCommand; } public void updateTo(ConsoleOutputStreamConsumer outputStreamConsumer, File baseDir, RevisionContext revisionContext, final SubprocessExecutionContext execCtx) { Revision revision = revisionContext.getLatestRevision(); try { outputStreamConsumer.stdOutput(format("[%s] Start updating %s at revision %s from %s", GoConstants.PRODUCT_NAME, updatingTarget(), revision.getRevision(), url.forDisplay())); File workingDir = execCtx.isServer() ? baseDir : workingdir(baseDir); hg(workingDir, outputStreamConsumer).updateTo(revision, outputStreamConsumer); outputStreamConsumer.stdOutput(format("[%s] Done.\n", GoConstants.PRODUCT_NAME)); } catch (Exception e) { bomb(e); } } public void add(File baseDir, ProcessOutputStreamConsumer outputStreamConsumer, File file) throws Exception { hg(baseDir, outputStreamConsumer).add(outputStreamConsumer, file); } public void commit(File baseDir, ProcessOutputStreamConsumer consumer, String comment, String username) throws Exception { hg(baseDir, consumer).commit(consumer, comment, username); } public void push(File baseDir, ProcessOutputStreamConsumer consumer) throws Exception { hg(baseDir, consumer).push(consumer); } boolean isVersionOnedotZeorOrHigher(String hgout) { String hgVersion = parseHgVersion(hgout); Float aFloat = NumberUtils.createFloat(hgVersion.subSequence(0, 3).toString()); return aFloat >= 1; } private String parseHgVersion(String hgOut) { String[] lines = hgOut.split("\n"); String firstLine = lines[0]; Matcher m = HG_VERSION_PATTERN.matcher(firstLine); if (m.matches()) { return m.group(1); } else { throw bomb("can not parse hgout : " + hgOut); } } public ValidationBean checkConnection(final SubprocessExecutionContext execCtx) { HgCommand hgCommand = new HgCommand(null, null, null, null, secrets()); try { hgCommand.checkConnection(url); return ValidationBean.valid(); } catch (Exception e) { String message = null; try { message = hgCommand.version(); return handleException(e, message); } catch (Exception ex) { return ValidationBean.notValid(ERR_NO_HG_INSTALLED); } } } ValidationBean handleException(Exception e, String hgVersionConsoleOut) { ValidationBean defaultResponse = ValidationBean.notValid( "Repository " + url.forDisplay() + " not found!" + " : \n" + e.getMessage()); try { boolean olderThanHg10 = !isVersionOnedotZeorOrHigher(hgVersionConsoleOut); if (olderThanHg10) { return ValidationBean.notValid(ERROR_OLD_VERSION + hgVersionConsoleOut); } else { return defaultResponse; } } catch (Exception e1) { LOGGER.debug("Problem validating HG", e); return defaultResponse; } } private HgCommand hg(File workingFolder, ConsoleOutputStreamConsumer outputStreamConsumer) throws Exception { HgCommand hgCommand = new HgCommand(getFingerprint(), workingFolder, getBranch(), getUrl(), secrets()); if (!isHgRepository(workingFolder) || isRepositoryChanged(hgCommand)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Invalid hg working copy or repository changed. Delete folder: " + workingFolder); } FileUtil.deleteFolder(workingFolder); } if (!workingFolder.exists()) { createParentFolderIfNotExist(workingFolder); int returnValue = hgCommand.clone(outputStreamConsumer, url); bombIfFailedToRunCommandLine(returnValue, "Failed to run hg clone command"); } return hgCommand; } private List<SecretString> secrets() { SecretString secretSubstitution = new SecretString() { @Override public String replaceSecretInfo(String line) { return line.replace(url.forCommandline(), url.forDisplay()); } }; return Arrays.asList(secretSubstitution); } private boolean isHgRepository(File workingFolder) { return new File(workingFolder, ".hg").isDirectory(); } private boolean isRepositoryChanged(HgCommand hgCommand) { ConsoleResult result = hgCommand.workingRepositoryUrl(); return !MaterialUrl.sameUrl(url.defaultRemoteUrl(), new HgUrlArgument(result.outputAsString()).defaultRemoteUrl()); } public String getUserName() { return null; } public String getPassword() { return null; } @Override public String getEncryptedPassword() { return null; } public boolean isCheckExternals() { return false; } public String getUrl() { return url.forCommandline(); } public UrlArgument getUrlArgument() { return url; } public String getLongDescription() { return String.format("URL: %s", url.forDisplay()); } @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; } HgMaterial that = (HgMaterial) o; if (url != null ? !url.equals(that.url) : that.url != null) { return false; } return true; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + (url != null ? url.hashCode() : 0); return result; } protected String getLocation() { return getUrlArgument().forDisplay(); } public String getTypeForDisplay() { return "Mercurial"; } @Override public String getShortRevision(String revision) { if (revision == null) return null; if (revision.length()<12) return revision; return revision.substring(0,12); } @Override public Map<String, Object> getAttributes(boolean addSecureFields) { Map<String, Object> materialMap = new HashMap<>(); materialMap.put("type", "mercurial"); Map<String, Object> configurationMap = new HashMap<>(); if (addSecureFields) { configurationMap.put("url", url.forCommandline()); } else { configurationMap.put("url", url.forDisplay()); } materialMap.put("mercurial-configuration", configurationMap); return materialMap; } public Class getInstanceType() { return HgMaterialInstance.class; } @Override public String toString() { return "HgMaterial{" + "url=" + url + '}'; } String getBranch() { return getBranchFromUrl(url.forCommandline()); } private String getBranchFromUrl(String url) { String[] componentsOfUrl = StringUtils.split(url.toString(), HgUrlArgument.DOUBLE_HASH); if (componentsOfUrl.length > 1) { return componentsOfUrl[1]; } return HG_DEFAULT_BRANCH; } }