package hudson.plugins.mercurial; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.AbortException; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.matrix.MatrixRun; import hudson.model.AbstractBuild; import hudson.model.Action; import hudson.model.Actionable; import hudson.model.Computer; import hudson.model.Item; import hudson.model.Job; import hudson.model.Node; import hudson.model.ParameterDefinition; import hudson.model.ParametersDefinitionProperty; import hudson.model.Run; import hudson.model.StringParameterDefinition; import hudson.model.TaskListener; import hudson.plugins.mercurial.browser.HgBrowser; import hudson.plugins.mercurial.browser.HgWeb; import hudson.scm.ChangeLogParser; import hudson.scm.PollingResult; import hudson.scm.PollingResult.Change; import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.scm.SCMRevisionState; import hudson.util.ArgumentListBuilder; import hudson.util.ForkOutputStream; import hudson.util.ListBoxModel; import hudson.util.LogTaskListener; import hudson.util.VersionNumber; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.io.Serializable; import java.net.MalformedURLException; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import static java.util.logging.Level.FINE; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.jenkinsci.plugins.multiplescms.MultiSCMRevisionState; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; /** * Mercurial SCM. */ public class MercurialSCM extends SCM implements Serializable { // Environment vars names to be exposed private static final String ENV_MERCURIAL_REVISION = "MERCURIAL_REVISION"; private static final String ENV_MERCURIAL_REVISION_SHORT = "MERCURIAL_REVISION_SHORT"; private static final String ENV_MERCURIAL_REVISION_NUMBER = "MERCURIAL_REVISION_NUMBER"; private static final String ENV_MERCURIAL_REVISION_BRANCH = "MERCURIAL_REVISION_BRANCH"; private static final String ENV_MERCURIAL_REPOSITORY_URL = "MERCURIAL_REPOSITORY_URL"; // old fields are left so that old config data can be read in, but // they are deprecated. transient so that they won't show up in XML // when writing back @Deprecated private transient boolean forest; /** * Name of selected installation, if any. */ private String installation; /** * Source repository URL from which we pull. */ private final String source; /** * Prefixes of files within the repository which we're dependent on. * Storing as member variable so as to only parse the dependencies string once. * Will be either null (use whole repo), or nonempty list of subdir names. */ private transient Set<String> _modules; // Same thing, but not parsed for jelly. private String modules = ""; public enum RevisionType { BRANCH() { @Override public String getDisplayName() { return "Branch"; } }, TAG() { @Override public String getDisplayName() { return "Tag"; } }, CHANGESET() { @Override public String getDisplayName() { return "Changeset"; } }, REVSET() { @Override public String getDisplayName() { return "Revset"; } }; public abstract String getDisplayName(); } private RevisionType revisionType = RevisionType.BRANCH; /** * Revision to follow. */ private String revision = "default"; @Deprecated private String branch; /** Slash-separated subdirectory of the workspace in which the repository will be kept; null for top level. */ private String subdir; private boolean clean; private HgBrowser browser; private String credentialsId; private boolean disableChangeLog; @DataBoundConstructor public MercurialSCM(String source) { this.source = Util.fixEmptyAndTrim(source); } @Deprecated public MercurialSCM(String installation, String source, String branch, String modules, String subdir, HgBrowser browser, boolean clean) { this(installation, source, branch, modules, subdir, browser, clean, null); } @Deprecated public MercurialSCM(String installation, String source, String branch, String modules, String subdir, HgBrowser browser, boolean clean, String credentialsId) { this(installation, source, RevisionType.BRANCH, branch, modules, subdir, browser, clean, credentialsId); } @Deprecated public MercurialSCM(String installation, String source, @NonNull RevisionType revisionType, @NonNull String revision, String modules, String subdir, HgBrowser browser, boolean clean, String credentialsId) { this(installation, source, revisionType, revision, modules, subdir, browser, clean, credentialsId, false); } @Deprecated public MercurialSCM(String installation, String source, @NonNull RevisionType revisionType, @NonNull String revision, String modules, String subdir, HgBrowser browser, boolean clean, String credentialsId, boolean disableChangeLog) { this(source); setInstallation(installation); setModules(modules); setSubdir(subdir); setClean(clean); setRevisionType(revisionType); setRevision(revision); setBrowser(browser); setCredentialsId(credentialsId); setDisableChangeLog(disableChangeLog); } private void parseModules() { if (modules.trim().length() > 0) { _modules = new HashSet<String>(); // split by commas and whitespace, except "\ " for (String r : modules.split("(?<!\\\\)[ \\r\\n,]+")) { if (r.length() == 0) { // initial spaces should be ignored continue; } // now replace "\ " to " ". r = r.replaceAll("\\\\ ", " "); // Strip leading slashes while (r.startsWith("/")) { r = r.substring(1); } // Use unix file path separators r = r.replace('\\', '/'); _modules.add(r); } } else { _modules = null; } } private Object readResolve() { if (revisionType == null) { revisionType = RevisionType.BRANCH; assert revision == null; revision = branch == null ? "default" : branch; branch = null; } parseModules(); return this; } public String getInstallation() { return installation; } @DataBoundSetter public final void setInstallation(String installation) { this.installation = installation; } /** * Gets the source repository path. * Either URL or local file path. */ public String getSource() { return source; } private String getSource(EnvVars env) { return env.expand(source); } @Override public String getKey() { String base = "hg " + getSource(new EnvVars()); if (revisionType == RevisionType.CHANGESET) { return base; } else { return base + " " + revision; } } public String getCredentialsId() { return credentialsId; } @DataBoundSetter public final void setCredentialsId(String credentialsId) { this.credentialsId = credentialsId; } public boolean isDisableChangeLog() { return disableChangeLog; } @DataBoundSetter public final void setDisableChangeLog(boolean disableChangeLog) { this.disableChangeLog = disableChangeLog; } @CheckForNull StandardUsernameCredentials getCredentials(Job<?,?> owner, EnvVars env) { if (credentialsId != null) { for (StandardUsernameCredentials c : availableCredentials(owner, getSource(env))) { if (c.getId().equals(credentialsId)) { return c; } } } return null; } public @NonNull RevisionType getRevisionType() { return revisionType; } @DataBoundSetter public final void setRevisionType(@NonNull RevisionType revisionType) { this.revisionType = revisionType; } public @NonNull String getRevision() { return revision; } @DataBoundSetter public final void setRevision(@NonNull String revision) { this.revision = Util.fixEmpty(revision) == null ? "default" : revision; } @Deprecated public String getBranch() { if (revisionType != RevisionType.BRANCH) { throw new IllegalStateException(); } return revision; } /** * Same as {@link #getRevision()} but with <em>default</em> values of parameters expanded. */ private String getRevisionExpanded(Job<?,?> project, EnvVars env) { ParametersDefinitionProperty params = project.getProperty(ParametersDefinitionProperty.class); if (params != null) { for (ParameterDefinition param : params.getParameterDefinitions()) { if (param instanceof StringParameterDefinition) { String dflt = ((StringParameterDefinition) param).getDefaultValue(); if (dflt != null) { env.put(param.getName(), dflt); } } } } return getRevision(env); } private String getRevision(EnvVars env) { return env.expand(revision); } public String getSubdir() { return subdir; } @DataBoundSetter public final void setSubdir(String subdir) { this.subdir = Util.fixEmptyAndTrim(subdir); } private String getSubdir(EnvVars env) { return env.expand(subdir); } private FilePath workspace2Repo(FilePath workspace, EnvVars env) { return subdir != null ? workspace.child(env.expand(subdir)) : workspace; } public HgBrowser getBrowser() { return browser; } @DataBoundSetter public final void setBrowser(HgBrowser browser) { this.browser = browser; } @Override public RepositoryBrowser<?> guessBrowser() { try { return new HgWeb(getSource(new EnvVars())); } catch (MalformedURLException x) { LOGGER.log(Level.FINE, null, x); // OK, could just be a local directory path return null; } } /** * True if we want clean check out each time. This means deleting everything in the repository checkout * (except <tt>.hg</tt>) */ public boolean isClean() { return clean; } @DataBoundSetter public final void setClean(boolean clean) { this.clean = clean; } @Override public SCMRevisionState calcRevisionsFromBuild(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { // tag action is added during checkout, so this shouldn't be called, but just in case. EnvVars env = build.getEnvironment(listener); //TODO: fall-back to the master's workspace? if (workspace == null) { throw new IOException("Workspace is not specified"); } final Node nodeWithTheWorkspace = workspaceToNode(workspace); if (nodeWithTheWorkspace == null) { throw new IOException("Cannot find a node for the specified workspace"); } HgExe hg = new HgExe(findInstallation(getInstallation()), getCredentials(build.getParent(), env), launcher, nodeWithTheWorkspace, listener, env); try { String tip = hg.tip(workspace2Repo(workspace, env), null); String rev = hg.tipNumber(workspace2Repo(workspace, env), null); String branch = revisionType != RevisionType.BRANCH ? hg.branch(workspace2Repo(workspace, env), null) : null; return tip != null && rev != null ? new MercurialTagAction(tip, rev, getSubdir(env), branch) : null; } finally { hg.close(); } } @Override public boolean requiresWorkspaceForPolling() { MercurialInstallation mercurialInstallation = findInstallation(installation); return mercurialInstallation == null || !(mercurialInstallation.isUseCaches() || mercurialInstallation.isUseSharing() ); } @Override public PollingResult compareRemoteRevisionWith(Job<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState _baseline) throws IOException, InterruptedException { final Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { throw new IOException("Jenkins instance is not ready"); } if (!(_baseline instanceof MercurialTagAction)) { throw new IOException("SCM revision state is not a Mercurial one"); } MercurialTagAction baseline = (MercurialTagAction)_baseline; PrintStream output = listener.getLogger(); EnvVars env = project.getEnvironment(jenkins, listener); StandardUsernameCredentials credentials = getCredentials(project, env); if (!requiresWorkspaceForPolling()) { launcher = jenkins.createLauncher(listener); CachedRepo possiblyCachedRepo = cachedSource(Jenkins.getInstance(), env, launcher, listener, true, credentials); if (possiblyCachedRepo == null) { throw new IOException("Could not use cache to poll for changes. See error messages above for more details"); } FilePath repositoryCache = new FilePath(new File(possiblyCachedRepo.getRepoLocation())); return compare(launcher, listener, baseline, output, jenkins, repositoryCache, project); } // TODO do canUpdate check similar to in checkout, and possibly return INCOMPARABLE try { // Get the list of changed files. Node node = workspaceToNode(workspace); FilePath repository = workspace2Repo(workspace, env); pull(launcher, repository, listener, node, getRevisionExpanded(project, env), credentials, env); return compare(launcher, listener, baseline, output, node, repository, project); } catch(IOException e) { if (causedByMissingHg(e)) { listener.error(Messages.MercurialSCM_failed_to_compare_with_remote_repository()); throw new AbortException("Failed to compare with remote repository"); } throw new IOException("Failed to compare with remote repository", e); } } PollingResult compare(Launcher launcher, TaskListener listener, MercurialTagAction baseline, PrintStream output, Node node, FilePath repository, Job<?,?> project) throws IOException, InterruptedException { Change change = null; for (ChangeComparator s : ChangeComparator.all()) { Change c = s.compare(this, launcher, listener, baseline, output, node, repository, project); if (c != null) { if (change == null || c.compareTo(change) > 0) { change = c; } } } if (change != null) { return new PollingResult(change); } EnvVars env = project.getEnvironment(node, listener); HgExe hg = new HgExe(findInstallation(getInstallation()), getCredentials(project, env), launcher, node, listener, env); try { String _revision = getRevisionExpanded(project, env); String remote = hg.tip(repository, _revision); String rev = hg.tipNumber(repository, _revision); String branch = revisionType != RevisionType.BRANCH ? hg.branch(repository, _revision) : null; if (remote == null) { throw new IOException("failed to find ID of branch head"); } if (rev == null) { throw new IOException("failed to find revision of branch head"); } if (remote.equals(baseline.id)) { // shortcut return new PollingResult(baseline, new MercurialTagAction(remote, rev, getSubdir(env), branch), Change.NONE); } Set<String> changedFileNames = parseStatus(hg.popen(repository, listener, false, new ArgumentListBuilder("status", "--rev", baseline.id, "--rev", remote))); MercurialTagAction cur = new MercurialTagAction(remote, rev, getSubdir(env), branch); return new PollingResult(baseline,cur,computeDegreeOfChanges(changedFileNames,output)); } finally { hg.close(); } } static Set<String> parseStatus(String status) { Set<String> result = new HashSet<String>(); Matcher m = Pattern.compile("(?m)^[ARM] (.+)").matcher(status); while (m.find()) { result.add(m.group(1)); } return result; } private int pull(Launcher launcher, FilePath repository, TaskListener listener, Node node, String revision, StandardUsernameCredentials credentials, EnvVars env) throws IOException, InterruptedException { HgExe hg = new HgExe(findInstallation(getInstallation()), credentials, launcher, node, listener, env); try { ArgumentListBuilder cmd = hg.seed(true); cmd.add("pull"); if (revisionType == RevisionType.BRANCH || revisionType == RevisionType.CHANGESET) { // does not work for tags cmd.add("--rev", revision); } CachedRepo cachedSource = cachedSource(node, env, launcher, listener, true, credentials); if (cachedSource != null) { cmd.add(cachedSource.getRepoLocation()); } return HgExe.joinWithPossibleTimeout( hg.launch(cmd).pwd(repository), true, listener); } finally { hg.close(); } } private Change computeDegreeOfChanges(Set<String> changedFileNames, PrintStream output) { LOGGER.log(FINE, "Changed file names: {0}", changedFileNames); if (changedFileNames.isEmpty()) { return Change.NONE; } Set<String> depchanges = dependentChanges(changedFileNames); LOGGER.log(FINE, "Dependent changed file names: {0}", depchanges); if (depchanges.isEmpty()) { output.println(Messages.MercurialSCM_non_dependent_changes_detected()); return Change.INSIGNIFICANT; } output.println(Messages.MercurialSCM_dependent_changes_detected()); return Change.SIGNIFICANT; } /** * Filter out the given file name list by picking up changes that are in the modules we care about. */ private Set<String> dependentChanges(Set<String> changedFileNames) { Set<String> affecting = new HashSet<String>(); for (String changedFile : changedFileNames) { if (changedFile.matches("[.]hg(ignore|tags)")) { continue; } if (_modules == null) { affecting.add(changedFile); continue; } String unixChangedFile = changedFile.replace('\\', '/'); for (String dependency : _modules) { if (unixChangedFile.startsWith(dependency)) { affecting.add(changedFile); break; } } } return affecting; } public static @CheckForNull MercurialInstallation findInstallation(String name) { for (MercurialInstallation inst : MercurialInstallation.allInstallations()) { if (inst.getName().equals(name)) { return inst; } } return null; } @Override public void checkout(Run<?, ?> build, Launcher launcher, FilePath workspace, final TaskListener listener, File changelogFile, SCMRevisionState baseline) throws IOException, InterruptedException { MercurialInstallation mercurialInstallation = findInstallation(installation); final boolean jobShouldUseSharing = mercurialInstallation != null && mercurialInstallation.isUseSharing(); Node node = workspaceToNode(workspace); FilePath repository = workspace2Repo(workspace, build.getEnvironment(listener)); boolean canReuseExistingWorkspace; try { canReuseExistingWorkspace = canReuseWorkspace(repository, node, jobShouldUseSharing, build, launcher, listener); } catch(IOException e) { if (causedByMissingHg(e)) { listener.error("Failed to determine whether workspace can be reused because hg could not be found;" + " check that you've properly configured your Mercurial installation"); } else { e.printStackTrace(listener.error("Failed to determine whether workspace can be reused")); } throw new AbortException("Failed to determine whether workspace can be reused"); } String revToBuild = getRevToBuild(build, workspace, build.getEnvironment(listener)); StandardUsernameCredentials credentials = getCredentials(build.getParent(), build.getEnvironment(listener)); if (canReuseExistingWorkspace) { update(build, launcher, repository, node, listener, revToBuild, credentials); } else { clone(build, launcher, repository, node, listener, revToBuild, credentials); } if (changelogFile != null) { try { determineChanges(build, launcher, listener, changelogFile, repository, node, revToBuild, baseline); } catch (IOException e) { listener.error("Failed to capture change log"); e.printStackTrace(listener.getLogger()); throw new AbortException("Failed to capture change log"); } } } private boolean canReuseWorkspace(FilePath repo, Node node, boolean jobShouldUseSharing, Run<?,?> build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { boolean jobUsesSharing = new FilePath(repo, ".hg/sharedpath").exists(); if (jobShouldUseSharing != jobUsesSharing) { return false; } else if(jobUsesSharing) { return true; } if (!new FilePath(repo, ".hg/hgrc").exists()) { return false; } HgExe hg = new HgExe(findInstallation(getInstallation()), getCredentials(build.getParent(), build.getEnvironment(listener)), launcher, node, listener, build.getEnvironment(listener)); try { String upstream = hg.config(repo, "paths.default"); EnvVars env = build.getEnvironment(listener); if (HgExe.pathEquals(getSource(env), upstream)) { return true; } listener.error( "Workspace reports paths.default as " + upstream + "\nwhich looks different than " + getSource(env) + "\nso falling back to fresh clone rather than incremental update"); return false; } finally { hg.close(); } } private void determineChanges(Run<?, ?> build, Launcher launcher, TaskListener listener, @Nonnull File changelogFile, FilePath repository, Node node, String revToBuild, SCMRevisionState baseline) throws IOException, InterruptedException { if (isDisableChangeLog()) { createEmptyChangeLog(changelogFile, listener, "changelog"); return; } MercurialTagAction prevTag = (MercurialTagAction) baseline; if (prevTag == null) { listener.getLogger().println("WARN: Revision data for previous build unavailable; unable to determine change log"); createEmptyChangeLog(changelogFile, listener, "changelog"); return; } EnvVars env = build.getEnvironment(listener); MercurialInstallation inst = findInstallation(getInstallation()); StandardUsernameCredentials credentials = getCredentials(build.getParent(), env); HgExe hg = new HgExe(inst, credentials, launcher, node, listener, env); try { ArgumentListBuilder logCommand = hg.seed(true).add("log", "--rev", prevTag.getId(), "--template", "exists\\n"); int exitCode = hg.launch(logCommand).pwd(repository).join(); if(exitCode != 0) { listener.error("Previously built revision " + prevTag.getId() + " is not known in this clone; unable to determine change log"); createEmptyChangeLog(changelogFile, listener, "changelog"); return; } // calc changelog final FileOutputStream os = new FileOutputStream(changelogFile); try { os.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".getBytes("UTF-8")); try { os.write("<changesets>\n".getBytes("UTF-8")); ArgumentListBuilder args = hg.seed(false); args.add("log"); args.add("--template", MercurialChangeSet.CHANGELOG_TEMPLATE); if(revisionType == RevisionType.REVSET) { args.add("--rev", "ancestors(" + revToBuild + ") and not ancestors(" + prevTag.getId() + ")"); } else { args.add("--rev", "ancestors('" + revToBuild.replace("'", "\\'") + "') and not ancestors(" + prevTag.getId() + ")"); } args.add("--encoding", "UTF-8"); args.add("--encodingmode", "replace"); ByteArrayOutputStream errorLog = new ByteArrayOutputStream(); int r = hg.launch(args).stdout(new ForkOutputStream(os, errorLog)).pwd(repository).join(); if(r!=0) { Util.copyStream(new ByteArrayInputStream(errorLog.toByteArray()), listener.getLogger()); throw new IOException("Failure detected while running hg log to determine change log"); } } finally { os.write("</changesets>".getBytes("UTF-8")); } } finally { os.close(); } } finally { hg.close(); } } private void update(Run<?, ?> build, Launcher launcher, FilePath repository, Node node, TaskListener listener, String toRevision, StandardUsernameCredentials credentials) throws IOException, InterruptedException { HgExe hg = new HgExe(findInstallation(getInstallation()), credentials, launcher, node, listener, build.getEnvironment(listener)); EnvVars env = build.getEnvironment(listener); try { int pullExitCode; try { pullExitCode = pull(launcher, repository, listener, node, toRevision, credentials, env); } catch (IOException e) { if (causedByMissingHg(e)) { listener.error("Failed to pull because hg could not be found;" + " check that you've properly configured your Mercurial installation"); } else { e.printStackTrace(listener.error("Failed to pull")); } throw new AbortException("Failed to pull"); } if (pullExitCode != 0) { listener.error("Failed to pull"); throw new AbortException("Failed to pull"); } int updateExitCode; try { updateExitCode = hg.run("update", "--clean", "--rev", toRevision).pwd(repository).join(); } catch (IOException e) { listener.error("Failed to update"); e.printStackTrace(listener.getLogger()); throw new AbortException("Failed to update"); } if (updateExitCode != 0) { listener.error("Failed to update"); throw new AbortException("Failed to update"); } if (build.getNumber() % 100 == 0) { CachedRepo cachedSource = cachedSource(node, env, launcher, listener, true, credentials); if (cachedSource != null && !cachedSource.isUseSharing()) { // Periodically recreate hardlinks to the cache to save disk space. hg.run("--config", "extensions.relink=", "relink", cachedSource.getRepoLocation()).pwd(repository).join(); // ignore failures } } if(clean) { if (hg.cleanAll().pwd(repository).join() != 0) { listener.error("Failed to clean unversioned files"); throw new AbortException("Failed to clean unversioned files"); } } String tip = hg.tip(repository, null); String rev = hg.tipNumber(repository, null); String branch = revisionType != RevisionType.BRANCH ? hg.branch(repository, null) : null; if (tip != null && rev != null) { build.addAction(new MercurialTagAction(tip, rev, getSubdir(env), branch)); } } finally { hg.close(); } } private String getRevToBuild(Run<?, ?> build, FilePath workspace, EnvVars env) { String revToBuild = getRevision(env); if (build instanceof MatrixRun) { MatrixRun matrixRun = (MatrixRun) build; MercurialTagAction parentRevision = null; final Jenkins jenkins = Jenkins.getInstance(); if (jenkins != null && jenkins.getPlugin("multiple-scms") != null) { MultiSCMRevisionState parentRevisions = matrixRun.getParentBuild().getAction(MultiSCMRevisionState.class); if (parentRevisions != null) { SCMRevisionState _parentRevisions = parentRevisions.get(this, workspace, (MatrixRun) build); if (_parentRevisions instanceof MercurialTagAction) { parentRevision = (MercurialTagAction)_parentRevisions; } // otherwise fall-back to the default behavior } if (parentRevisions == null) { parentRevision = matrixRun.getParentBuild().getAction(MercurialTagAction.class); } } else { parentRevision = matrixRun.getParentBuild().getAction(MercurialTagAction.class); } if (parentRevision != null && parentRevision.getId() != null) { revToBuild = parentRevision.getId(); } } return revToBuild; } /** * Start from scratch and clone the whole repository. */ private void clone(Run<?, ?> build, Launcher launcher, FilePath repository, Node node, TaskListener listener, String toRevision, StandardUsernameCredentials credentials) throws InterruptedException, IOException { try { repository.deleteRecursive(); } catch (IOException e) { e.printStackTrace(listener.error("Failed to clean the repository checkout")); throw new AbortException("Failed to clean the repository checkout"); } EnvVars env = build.getEnvironment(listener); HgExe hg = new HgExe(findInstallation(getInstallation()), credentials, launcher, node, listener, env); try { ArgumentListBuilder args = hg.seed(true); CachedRepo cachedSource = cachedSource(node, env, launcher, listener, false, credentials); if (cachedSource != null) { if (cachedSource.isUseSharing()) { args.add("--config", "extensions.share="); args.add("share"); args.add("--noupdate"); args.add(cachedSource.getRepoLocation()); if (new VersionNumber(hg.version()).compareTo(new VersionNumber("3.3")) >= 0) { args.add("-B"); } } else { args.add("clone"); args.add("--noupdate"); args.add(cachedSource.getRepoLocation()); } } else { args.add("clone"); if (revisionType == RevisionType.BRANCH || revisionType == RevisionType.CHANGESET) { args.add("--rev", toRevision); } args.add("--noupdate"); args.add(getSource(env)); } args.add(repository.getRemote()); repository.mkdirs(); int cloneExitCode; try { cloneExitCode = hg.launch(args).join(); } catch (IOException e) { if (causedByMissingHg(e)) { listener.error("Failed to clone " + getSource(env) + " because hg could not be found;" + " check that you've properly configured your Mercurial installation"); } else { e.printStackTrace(listener.error(Messages.MercurialSCM_failed_to_clone(getSource(env)))); } throw new AbortException(Messages.MercurialSCM_failed_to_clone(getSource(env))); } if(cloneExitCode!=0) { listener.error(Messages.MercurialSCM_failed_to_clone(getSource(env))); throw new AbortException(Messages.MercurialSCM_failed_to_clone(getSource(env))); } if (cachedSource != null && !cachedSource.isUseSharing()) { FilePath hgrc = repository.child(".hg/hgrc"); if (hgrc.exists()) { String hgrcText = hgrc.readToString(); if (!hgrcText.contains(cachedSource.getRepoLocation())) { listener.error(".hg/hgrc did not contain " + cachedSource.getRepoLocation() + " as expected:\n" + hgrcText); throw new AbortException(".hg/hgrc did not contain " + cachedSource.getRepoLocation() + " as expected:\n" + hgrcText); } hgrc.write(hgrcText.replace(cachedSource.getRepoLocation(), getSource(env)), null); } // Passing --rev disables hardlinks, so we need to recreate them: hg.run("--config", "extensions.relink=", "relink", cachedSource.getRepoLocation()) .pwd(repository).join(); // ignore failures } ArgumentListBuilder upArgs = hg.seed(true); upArgs.add("update"); upArgs.add("--rev", toRevision); if (hg.launch(upArgs).pwd(repository).join() != 0) { throw new AbortException("Failed to update " + getSource(env) + " to rev " + toRevision); } String tip = hg.tip(repository, null); String rev = hg.tipNumber(repository, null); String branch = revisionType != RevisionType.BRANCH ? hg.branch(repository, null) : null; if (tip != null && rev != null) { build.addAction(new MercurialTagAction(tip, rev, getSubdir(env), branch)); } } finally { hg.close(); } } @Override public void buildEnvVars(AbstractBuild<?,?> build, Map<String, String> env) { buildEnvVarsFromActionable(build, env); } void buildEnvVarsFromActionable(Actionable build, Map<String, String> env) { MercurialTagAction a = findTag(build, new EnvVars(env)); if (a != null) { env.put(ENV_MERCURIAL_REVISION, a.id); env.put(ENV_MERCURIAL_REVISION_SHORT, a.getShortId()); env.put(ENV_MERCURIAL_REVISION_NUMBER, a.rev); if (revisionType != RevisionType.BRANCH) env.put(ENV_MERCURIAL_REVISION_BRANCH, a.getBranch()); env.put(ENV_MERCURIAL_REPOSITORY_URL, this.getSource()); } } private MercurialTagAction findTag(Actionable build, EnvVars env) { for (Action action : build.getActions()) { if (action instanceof MercurialTagAction) { MercurialTagAction tag = (MercurialTagAction) action; // JENKINS-12162: differentiate plugins in different subdirs String ourSubDir = getSubdir( env ); String tagSubDir = tag.getSubdir( ); if ((ourSubDir == null && tagSubDir == null) || (ourSubDir != null && ourSubDir.equals(tagSubDir))) { return tag; } } } return null; } @Override public ChangeLogParser createChangeLogParser() { return new MercurialChangeLogParser(_modules); } @Override public FilePath getModuleRoot(FilePath workspace, AbstractBuild build) { if ( build != null ) { try { EnvVars env = build.getEnvironment(new LogTaskListener(LOGGER, Level.INFO)); return workspace2Repo(workspace, env); } catch (IOException ex) { Logger.getLogger(MercurialSCM.class.getName()).log(Level.SEVERE, null, ex); } catch (InterruptedException ex) { Logger.getLogger(MercurialSCM.class.getName()).log(Level.SEVERE, null, ex); } } EnvVars env = new EnvVars( ); return workspace2Repo(workspace, env); } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } public String getModules() { return modules; } @DataBoundSetter public final void setModules(String modules) { this.modules = Util.fixNull(modules); parseModules(); } private boolean causedByMissingHg(IOException e) { String message = e.getMessage(); return message != null && message.startsWith("Cannot run program") && message.endsWith("No such file or directory"); } private @CheckForNull CachedRepo cachedSource(Node node, EnvVars env, Launcher launcher, TaskListener listener, boolean useTimeout, StandardUsernameCredentials credentials) throws InterruptedException { MercurialInstallation inst = findInstallation(installation); if (inst == null || !inst.isUseCaches()) { return null; } try { FilePath cache = Cache.fromURL(getSource(env), credentials, inst.getMasterCacheRoot()).repositoryCache(inst, node, launcher, listener, useTimeout); if (cache != null) { return new CachedRepo(cache.getRemote(), inst.isUseSharing()); } else { listener.error("Failed to use repository cache for " + getSource(env)); return null; } } catch (InterruptedException x) { throw x; } catch (Exception x) { x.printStackTrace(listener.error("Failed to use repository cache for " + getSource(env))); return null; } } private static class CachedRepo { private final String repoLocation; private final boolean useSharing; private CachedRepo(String repoLocation, boolean useSharing) { this.repoLocation = repoLocation; this.useSharing = useSharing; } public String getRepoLocation() { return repoLocation; } public boolean isUseSharing() { return useSharing; } } @CheckForNull private static Node workspaceToNode(FilePath workspace) { // TODO https://trello.com/c/doFFMdUm/46-filepath-getcomputer Jenkins j = Jenkins.getInstance(); if (j != null && workspace.isRemote()) { for (Computer c : j.getComputers()) { if (c.getChannel() == workspace.getChannel()) { Node n = c.getNode(); if (n != null) { return n; } } } } return j; } private static List<? extends StandardUsernameCredentials> availableCredentials(Job<?,?> owner, String source) { // TODO implement support for SSHUserPrivateKey return CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, owner, null, URIRequirementBuilder.fromUri(source).build()); } @Extension public static final class DescriptorImpl extends SCMDescriptor<MercurialSCM> { private String hgExe; public DescriptorImpl() { super(HgBrowser.class); load(); } public String getDisplayName() { return "Mercurial"; } /** * Path to mercurial executable. */ public String getHgExe() { if (hgExe == null) { return "hg"; } return hgExe; } @Override public boolean isApplicable(Job project) { return true; } @Override public SCM newInstance(StaplerRequest req, JSONObject formData) throws FormException { return super.newInstance(req, formData); } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { hgExe = req.getParameter("mercurial.hgExe"); save(); return true; } public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Job<?,?> owner, @QueryParameter String source) { if (owner == null || !owner.hasPermission(Item.EXTENDED_READ)) { return new ListBoxModel(); } return new StandardUsernameListBoxModel() .withEmptySelection() .withAll(availableCredentials(owner, new EnvVars( ).expand( source ))); } } private static final long serialVersionUID = 1L; private static final Logger LOGGER = Logger.getLogger(MercurialSCM.class.getName()); }