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.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressWarnings;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.Action;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.plugins.mercurial.browser.HgBrowser;
import hudson.scm.RepositoryBrowser;
import hudson.scm.RepositoryBrowsers;
import hudson.scm.SCM;
import hudson.util.ArgumentListBuilder;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import hudson.util.VersionNumber;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.CheckForNull;
import jenkins.model.Jenkins;
import jenkins.scm.api.SCMFile;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMHeadEvent;
import jenkins.scm.api.SCMHeadObserver;
import jenkins.scm.api.SCMProbe;
import jenkins.scm.api.SCMProbeStat;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMSource;
import jenkins.scm.api.SCMSourceCriteria;
import jenkins.scm.api.SCMSourceDescriptor;
import jenkins.scm.api.SCMSourceOwner;
import jenkins.scm.api.metadata.PrimaryInstanceMetadataAction;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
public final class MercurialSCMSource extends SCMSource {
private final String installation;
private final String source;
private final String credentialsId;
private final String branchPattern;
private final String modules;
private final String subdir;
private final HgBrowser browser;
private final boolean clean;
@DataBoundConstructor
public MercurialSCMSource(String id, String installation, String source, String credentialsId, String branchPattern, String modules, String subdir, HgBrowser browser, boolean clean) {
super(id);
this.installation = installation;
this.source = source;
this.credentialsId = credentialsId;
this.branchPattern = branchPattern;
this.modules = modules;
this.subdir = subdir;
this.browser = browser;
this.clean = clean;
}
public String getInstallation() {
return installation;
}
public String getSource() {
return source;
}
public String getCredentialsId() {
return credentialsId;
}
public String getBranchPattern() {
return branchPattern;
}
public String getModules() {
return modules;
}
public String getSubdir() {
return subdir;
}
public HgBrowser getBrowser() {
// Could default to HgWeb the way MercurialSCM does, but probably unnecessary.
return browser;
}
public boolean isClean() {
return clean;
}
@Override
protected void retrieve(@edu.umd.cs.findbugs.annotations.CheckForNull SCMSourceCriteria criteria,
@NonNull SCMHeadObserver observer,
@edu.umd.cs.findbugs.annotations.CheckForNull SCMHeadEvent<?> event,
@NonNull final TaskListener listener) throws IOException, InterruptedException {
MercurialInstallation inst = MercurialSCM.findInstallation(installation);
if (inst == null) {
listener.error("No configured Mercurial installation");
return;
}
if (!inst.isUseCaches()) {
listener.error("Mercurial installation " + installation + " does not support caches");
return;
}
final Node node = Jenkins.getInstance();
if (node == null) { // Should not happen BTW
listener.error("Cannot retrieve the Jenkins master node");
return;
}
Launcher launcher = node.createLauncher(listener);
StandardUsernameCredentials credentials = getCredentials();
final FilePath cache = Cache.fromURL(source, credentials, inst.getMasterCacheRoot()).repositoryCache(inst, node, launcher, listener, true);
if (cache == null) {
listener.error("Could not use caches, not fetching branch heads");
return;
}
final HgExe hg = new HgExe(inst, credentials, launcher, node, listener, new EnvVars());
try {
String heads = hg.popen(cache, listener, true, new ArgumentListBuilder("heads", "--template", "{node} {branch}\\n"));
Pattern p = Pattern.compile(Util.fixNull(branchPattern).length() == 0 ? ".+" : branchPattern);
for (String line : heads.split("\r?\n")) {
final String[] nodeBranch = line.split(" ", 2);
final String name = nodeBranch[1];
if (p.matcher(name).matches()) {
listener.getLogger().println("Found branch " + name);
SCMHead branch = new SCMHead(name);
if (criteria != null) {
SCMProbe probe = new SCMProbe() {
@NonNull
@Override
public SCMProbeStat stat(@NonNull String path) throws IOException {
try {
String files = hg.popen(cache, listener, true,
new ArgumentListBuilder("locate", "-r", nodeBranch[0], "-I", "path:" + path));
if (StringUtils.isBlank(files)) {
return SCMProbeStat.fromType(SCMFile.Type.NONEXISTENT);
}
return SCMProbeStat.fromType(SCMFile.Type.REGULAR_FILE);
} catch (InterruptedException e) {
throw new IOException(e);
}
}
@Override
public void close() throws IOException {
}
@Override
public String name() {
return name;
}
@Override
public long lastModified() {
return 0;
}
};
if (criteria.isHead(probe, listener)) {
listener.getLogger().println("Met criteria");
} else {
listener.getLogger().println("Does not meet criteria");
continue;
}
}
observer.observe(branch, new MercurialRevision(branch, nodeBranch[0]));
} else {
listener.getLogger().println("Ignoring branch " + name);
}
}
} finally {
hg.close();
}
}
@SuppressWarnings("DB_DUPLICATE_BRANCHES")
@Override public SCM build(SCMHead head, SCMRevision revision) {
String rev = revision == null ? head.getName() : ((MercurialRevision) revision).hash;
return new MercurialSCM(installation, source, revision == null ? MercurialSCM.RevisionType.BRANCH : MercurialSCM.RevisionType.CHANGESET, rev, modules, subdir, browser, clean, credentialsId, false);
}
private @CheckForNull StandardUsernameCredentials getCredentials() {
if (credentialsId != null) {
for (StandardUsernameCredentials c : availableCredentials(getOwner(), source)) {
if (c.getId().equals(credentialsId)) {
return c;
}
}
}
return null;
}
@NonNull
@Override
protected List<Action> retrieveActions(@NonNull SCMHead head,
@edu.umd.cs.findbugs.annotations.CheckForNull SCMHeadEvent event,
@NonNull TaskListener listener) throws IOException, InterruptedException {
// TODO for Mercurial 2.4+ check for the bookmark called @ and resolve that to determine the primary
if ("default".equals(head.getName())) {
return Collections.<Action>singletonList(new PrimaryInstanceMetadataAction());
}
return Collections.emptyList();
}
private static List<? extends StandardUsernameCredentials> availableCredentials(@CheckForNull SCMSourceOwner owner, @CheckForNull String source) {
return CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, owner, null, URIRequirementBuilder.fromUri(source).build());
}
@Extension public static final class DescriptorImpl extends SCMSourceDescriptor {
@Override public String getDisplayName() {
return "Mercurial";
}
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner owner, @QueryParameter String source) {
if (owner == null || !owner.hasPermission(Item.EXTENDED_READ)) {
return new ListBoxModel();
}
return new StandardUsernameListBoxModel()
.withEmptySelection()
.withAll(availableCredentials(owner, source));
}
public FormValidation doCheckBranchPattern(@QueryParameter String value) {
try {
Pattern.compile(value);
return FormValidation.ok();
} catch (PatternSyntaxException x) {
return FormValidation.error(x.getDescription());
}
}
public List<Descriptor<RepositoryBrowser<?>>> getBrowserDescriptors() {
return RepositoryBrowsers.filter(HgBrowser.class);
}
}
/*package*/ static final class MercurialRevision extends SCMRevision {
private final String hash;
MercurialRevision(SCMHead branch, String hash) {
super(branch);
this.hash = hash;
}
@Override public boolean equals(Object obj) {
return obj instanceof MercurialRevision && ((MercurialRevision) obj).hash.equals(hash);
}
@Override public int hashCode() {
return hash.hashCode();
}
@Override public String toString() {
return getHead().getName() + ":" + hash;
}
}
}