package hudson.plugins.bitkeeper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Map;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import hudson.AbortException;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.scm.ChangeLogParser;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.util.FormValidation;
import hudson.util.VersionNumber;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.framework.io.ByteBuffer;
public class BitKeeperSCM extends SCM {
/**
* Source repository URL from which we pull.
*/
private final String parent;
/**
* Local name of the repository
*/
private final String localRepository;
/**
* Whether we should use 'bk pull' to update the local repository
* If not, we clean out the repo, and clone a fresh copy
*/
private final boolean usePull;
/**
* Specifies whether pull and clone commands should run in quiet mode
* By default, these commands print out all files that have changed.
* Since cloning can be quite verbose, turning on quiet mode can make the console
* output much more useful
*/
private final boolean quiet;
/**
* How many times to retry clone/pull operations before declaring the build a failure
*/
private final int maxAttempts = 9;
@DataBoundConstructor
public BitKeeperSCM(String parent, String localRepo, boolean usePull, boolean quiet) {
this.parent = parent;
this.localRepository = localRepo;
this.usePull = usePull;
this.quiet = quiet;
}
/**
* Gets the source repository path.
* Either URL or local file path.
*/
public String getParent() {
return parent;
}
/**
* Gets the local repository directory.
* Must be a local file path.
*/
public String getLocalRepository() {
return localRepository;
}
public boolean isUsePull() {
return usePull;
}
public boolean isQuiet() {
return quiet;
}
@Override
public FilePath getModuleRoot(FilePath workspace) {
return workspace.child(this.localRepository);
}
@Override
public boolean checkout(AbstractBuild build, Launcher launcher,
FilePath workspace, BuildListener listener, File changelogFile)
throws IOException, InterruptedException {
FilePath localRepo = workspace.child(localRepository);
if(this.usePull && localRepo.exists()) {
pullLocalRepo(build, launcher, listener, workspace);
} else {
cloneLocalRepo(build, launcher, listener, workspace);
}
saveChangelog(build, launcher, listener, changelogFile, localRepo);
String mostRecent =
this.getLatestChangeset(
build.getEnvironment(listener), launcher, workspace,
this.localRepository, listener
);
build.addAction(new BitKeeperTagAction(build, mostRecent));
return true;
}
private void pullLocalRepo(AbstractBuild build, Launcher launcher,
BuildListener listener, FilePath workspace)
throws IOException, InterruptedException, AbortException {
FilePath localRepo = workspace.child(localRepository);
PrintStream output = listener.getLogger();
ArrayList<String> args = new ArrayList<String>();
args.add(getDescriptor().getBkExe());
args.add("pull");
args.add("-u");
args.add("-c" + maxAttempts);
if(quiet) args.add("-q");
args.add(parent);
if(launcher.launch().cmds(args)
.envs(build.getEnvironment(listener)).stdout(output).pwd(localRepo).join() != 0)
{
listener.error("Failed to pull from " + parent);
throw new AbortException();
}
output.println("Pull completed");
}
private void saveChangelog(AbstractBuild build, Launcher launcher, BuildListener listener,
File changelogFile, FilePath localRepo)
throws IOException, InterruptedException, FileNotFoundException,
AbortException {
OutputStream changelog = null;
Run prevBuild = build.getPreviousBuild();
BitKeeperTagAction tagAction =
prevBuild == null ? null : prevBuild.getAction(BitKeeperTagAction.class);
String recentCset = tagAction == null ? null : tagAction.getCsetkey();
try {
changelog = new FileOutputStream(changelogFile);
if(recentCset == null || recentCset.equals("")) {
listener.error("No most recent changeset available for changelog");
return;
}
if(launcher.launch().cmds(getDescriptor().getBkExe(),
"changes",
"-v",
"-r" + recentCset + "..",
"-d$if(:CHANGESET:){U :USER:\n$each(:C:){C (:C:)\n}$each(:TAG:){T (:TAG:)\n}}$unless(:CHANGESET:){F :GFILE:\n}")
.envs(build.getEnvironment(listener)).stdout(changelog).pwd(localRepo).join() != 0)
{
listener.error("Failed to save changelog");
throw new AbortException();
}
} finally {
if(changelog != null)
changelog.close();
}
listener.getLogger().println("Changelog saved");
}
@Override
public ChangeLogParser createChangeLogParser() {
return new BitKeeperChangeLogParser();
}
@Override
public DescriptorImpl getDescriptor() {
return DescriptorImpl.DESCRIPTOR;
}
@Override
public boolean requiresWorkspaceForPolling() {
return false;
}
@Override
public boolean pollChanges(AbstractProject project, Launcher launcher,
FilePath workspace, TaskListener listener) throws IOException,
InterruptedException {
PrintStream output = listener.getLogger();
Run lastBuild = project.getLastBuild();
BitKeeperTagAction tagAction =
lastBuild == null ? null : lastBuild.getAction(BitKeeperTagAction.class);
String recentCset = tagAction == null ? null : tagAction.getCsetkey();
String cset =
this.getLatestChangeset(Collections.<String,String>emptyMap(), launcher, workspace, parent, listener);
return !(cset.equals(recentCset));
}
private String getLatestChangeset(Map<String, String> env, Launcher launcher,
FilePath workspace, String repository, TaskListener listener)
throws IOException, InterruptedException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if(launcher.launch().cmds(
getDescriptor().getBkExe(),"changes","-r+", "-d:CSETKEY:", "-D", repository)
.envs(env).stdout(baos).pwd(workspace).join()!=0) {
// dump the output from bk to assist trouble-shooting.
Util.copyStream(new ByteArrayInputStream(baos.toByteArray()),listener.getLogger());
listener.error("Failed to check the latest changeset");
throw new AbortException();
}
// obtain the current changeset
String rev = null;
for( String line : Util.tokenize(new String(baos.toByteArray(), "ASCII"),"\r\n") ) {
line = line.trim();
rev = line;
break;
}
if(rev==null) {
Util.copyStream(new ByteArrayInputStream(baos.toByteArray()),listener.getLogger());
listener.error("Failed to identify a revision");
throw new AbortException();
}
return rev;
}
private void cloneLocalRepo(AbstractBuild build, Launcher launcher,
TaskListener listener, FilePath workspace)
throws InterruptedException, IOException
{
FilePath localRepo = workspace.child(localRepository);
ArrayList<String> args = new ArrayList<String>();
args.add(getDescriptor().getBkExe());
args.add("clone");
if(quiet) args.add("-q");
args.add(parent);
args.add(localRepository);
PrintStream output = listener.getLogger();
int attempt = 0;
int result = 0;
do {
if(result != 0) {
Thread.sleep(30000);
listener.error("Retrying clone");
}
localRepo.deleteRecursive();
result = launcher.launch().cmds(args)
.envs(build.getEnvironment(listener)).stdout(output).pwd(workspace).join();
} while(++attempt < maxAttempts && result != 0);
if(result != 0) {
listener.error("Failed to clone after " + maxAttempts + " attempts from " + this.parent);
throw new AbortException();
}
output.println("New clone made");
}
public static final class DescriptorImpl extends SCMDescriptor<BitKeeperSCM> {
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
private String bkExe;
private DescriptorImpl() {
super(BitKeeperSCM.class, null);
load();
}
public String getDisplayName() {
return "BitKeeper";
}
/**
* Path to BitKeeper executable.
*/
public String getBkExe() {
if(bkExe==null) return "bk";
return bkExe;
}
@Override
public SCM newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return new BitKeeperSCM(
req.getParameter("bitkeeper.parent"),
req.getParameter("bitkeeper.localRepository"),
req.getParameter("bitkeeper.usePull")!=null,
req.getParameter("bitkeeper.quiet")!=null
);
}
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
bkExe = req.getParameter("bitkeeper.bkExe");
save();
return true;
}
public FormValidation doBkExeCheck(@QueryParameter String value) {
return FormValidation.validateExecutable(value, new FormValidation.FileValidator() {
@Override public FormValidation validate(File exe) {
ByteBuffer baos = new ByteBuffer();
try {
Hudson.getInstance().createLauncher(TaskListener.NULL).launch()
.cmds(getBkExe(), "version").stdout(baos).join();
Matcher m = VERSION_STRING.matcher(baos.toString());
if(m.find()) {
try {
if(new VersionNumber(m.group(1)).compareTo(V4_0_1)>=0) {
return FormValidation.ok(); // right version
} else {
return FormValidation.error("This bk is version "+m.group(1)+" but we need 4.0.1+");
}
} catch (IllegalArgumentException e) {
return FormValidation.warning("Hudson can't tell if this bk is 4.0.1 or later (detected version is %s)",m.group(1));
}
}
} catch (IOException e) {
// failed
} catch (InterruptedException e) {
// failed
}
return FormValidation.error("Unable to check bk version");
}
});
}
/**
* Pattern matcher for the version number.
*/
private static final Pattern VERSION_STRING = Pattern.compile("BitKeeper version is bk-([0-9.]+)");
private static final VersionNumber V4_0_1 = new VersionNumber("4.0.1");
}
}