package hudson.plugins.bazaar;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath.FileCallable;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Launcher.LocalLauncher;
import hudson.Launcher.ProcStarter;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import hudson.scm.ChangeLogParser;
import hudson.scm.PollingResult;
import hudson.scm.PollingResult.Change;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.scm.SCMRevisionState;
import hudson.util.ArgumentListBuilder;
import hudson.util.FormValidation;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
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;
/**
* Bazaar SCM.
*
* @author Trond Norbye
*/
public class BazaarSCM extends SCM implements Serializable {
/**
* Source repository URL from which we pull.
*/
private final String source;
private final boolean clean;
@DataBoundConstructor
public BazaarSCM(String source, boolean clean) {
this.source = source;
this.clean = clean;
}
/**
* Gets the source repository path.
* Either URL or local file path.
* @return
*/
public String getSource() {
return source;
}
/**
* True if we want clean check out each time. This means deleting everything in the workspace
* @return
*/
public boolean isClean() {
return clean;
}
private String getRevid(Launcher launcher, TaskListener listener, String root)
throws InterruptedException {
String rev = null;
try {
if (launcher == null) {
/* Running for a VM or whathaveyou: make a launcher on master
* todo grab a launcher on 'any slave'
*/
launcher = new LocalLauncher(listener);
}
PrintStream output = listener.getLogger();
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
final String bzr_cmd = getDescriptor().getBzrExe();
ProcStarter starter = launcher.launch();
starter = starter.cmds(bzr_cmd, "revision-info", "-d", root);
// The launcher should already have the right vars!
// starter = starter.envs(EnvVars.masterEnvVars);
starter = starter.stdout(stdout);
starter = starter.stderr(stderr);
// not needed without workspaces : -d starter = starter.pwd(workspace);
final int ret = starter.join();
final String info_output = "bzr revision-info -d " + root + " returned " + ret + ". Command output: \"" + stdout.toString() + "\" stderr: \"" + stderr.toString() + "\"";
if (ret != 0) {
logger.warning(info_output);
} else {
String[] infos = stdout.toString().split("\\s");
rev = infos[1];
}
// output.printf("info result: %s\n", info_output);
} catch (IOException e) {
StringWriter w = new StringWriter();
e.printStackTrace(new PrintWriter(w));
logger.log(Level.WARNING, "Failed to poll repository: ", e);
}
if (rev == null) {
logger.log(Level.WARNING, "Failed to get revision id for: {0}", root);
}
return rev;
}
private void getLog(Launcher launcher, FilePath workspace, String oldver, String newver, File changeLog) throws InterruptedException {
try {
int ret;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String version = "revid:" + oldver + "..revid:" + newver;
if ((ret = launcher.launch().cmds(getDescriptor().getBzrExe(), "log", "-v", "-r", version, "--long", "--show-ids")
.envs(EnvVars.masterEnvVars).stdout(baos).pwd(workspace).join()) != 0) {
logger.log(Level.WARNING, "bzr log -v -r returned {0}", ret);
} else {
FileOutputStream fos = new FileOutputStream(changeLog);
fos.write(baos.toByteArray());
fos.close();
}
} catch (IOException e) {
StringWriter w = new StringWriter();
e.printStackTrace(new PrintWriter(w));
logger.log(Level.WARNING, "Failed to poll repository: ", e);
}
}
@Override
protected PollingResult compareRemoteRevisionWith(
AbstractProject<?, ?> project, Launcher launcher, FilePath workspace,
TaskListener listener, SCMRevisionState baseline) throws
IOException, InterruptedException {
PrintStream output = listener.getLogger();
output.printf("Getting current remote revision...");
String upstream = getRevid(launcher, listener, source);
output.println(upstream);
final BazaarRevisionState remote = (upstream == null) ? null : new BazaarRevisionState(upstream);
final Change change;
output.printf("Baseline is %s.\n", baseline);
if ((baseline == SCMRevisionState.NONE)
// appears that other instances of None occur - its not a singleton.
// so do a (fugly) class check.
|| (baseline.getClass() != BazaarRevisionState.class)
|| (!remote.rev_id.equals(((BazaarRevisionState)baseline).rev_id)))
change = Change.SIGNIFICANT;
else
change = Change.NONE;
return new PollingResult(baseline,remote,change);
}
@Override
public boolean requiresWorkspaceForPolling() {
return false;
}
@Override
public SCMRevisionState calcRevisionsFromBuild(AbstractBuild<?, ?> build,
Launcher launcher, TaskListener listener) throws IOException,
InterruptedException {
PrintStream output = listener.getLogger();
output.println("Getting local revision...");
String local = getRevid(launcher, listener, build.getWorkspace().getRemote());
output.println(local);
return local == null ? null : new BazaarRevisionState(local);
}
/** for old hudsons - delete at will **/
@Override
public boolean pollChanges(AbstractProject project, Launcher launcher,
FilePath workspace, TaskListener listener) throws IOException,
InterruptedException {
PrintStream output = listener.getLogger();
output.println("Getting upstream revision...");
String upstream = getRevid(launcher, listener, source);
output.println(upstream);
output.println("Getting local revision...");
String local = getRevid(launcher, listener, workspace.getRemote());
output.println(local);
return ! upstream.equals(local);
}
@Override
public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException {
boolean canUpdate = workspace.act(new FileCallable<Boolean>() {
private static final long serialVersionUID = 1L;
public Boolean invoke(File ws, VirtualChannel channel) throws IOException {
File file = new File(ws, ".bzr");
return file.exists();
}
});
if (canUpdate && !clean) {
return update(build, launcher, workspace, listener, changelogFile);
} else {
return clone(build, launcher, workspace, listener, changelogFile);
}
}
/**
* Updates the current workspace.
*/
private boolean update(AbstractBuild<?, ?> build, Launcher launcher,
FilePath workspace, BuildListener listener, File changelogFile)
throws InterruptedException, IOException {
try {
String oldid = getRevid(launcher, listener, workspace.getRemote());
if (launcher.launch().cmds(getDescriptor().getBzrExe(), "pull", "--overwrite", source)
.envs(build.getEnvironment(listener)).stdout(listener.getLogger()).pwd(workspace).join() != 0) {
listener.error("Failed to pull");
return false;
}
String newid = getRevid(launcher, listener, workspace.getRemote());
getLog(launcher, workspace, oldid, newid, changelogFile);
} catch (IOException e) {
listener.error("Failed to pull");
return false;
}
return true;
}
/**
* Start from scratch and clone the whole repository.
*/
private boolean clone(AbstractBuild<?, ?> build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws InterruptedException {
try {
workspace.deleteRecursive();
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to clean the workspace"));
return false;
}
ArgumentListBuilder args = new ArgumentListBuilder();
args.add(getDescriptor().getBzrExe(), "branch");
args.add(source, workspace.getRemote());
try {
if (launcher.launch().cmds(args).envs(build.getEnvironment(listener)).stdout(listener.getLogger()).join() != 0) {
listener.error("Failed to clone " + source);
return false;
}
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to clone " + source));
return false;
}
return createEmptyChangeLog(changelogFile, listener, "changelog");
}
@Override
public void buildEnvVars(AbstractBuild build, Map<String, String> env) {
}
@Override
public ChangeLogParser createChangeLogParser() {
return new BazaarChangeLogParser();
}
@Override
public DescriptorImpl getDescriptor() {
return DescriptorImpl.DESCRIPTOR;
}
public static final class DescriptorImpl extends SCMDescriptor<BazaarSCM> {
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
private String bzrExe;
private transient String version;
private DescriptorImpl() {
super(BazaarSCM.class, null);
load();
}
public String getDisplayName() {
return "Bazaar";
}
/**
* Path to bazaar executable.
* @return
*/
public String getBzrExe() {
return (bzrExe == null) ? "bzr" : bzrExe;
}
@Override
public SCM newInstance(StaplerRequest req, JSONObject formData) throws FormException {
BazaarSCM scm = req.bindJSON(BazaarSCM.class, formData);
return scm;
}
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
bzrExe = req.getParameter("bazaar.bzrExe");
version = null;
save();
return true;
}
public FormValidation doBzrExeCheck(@QueryParameter final String value) throws IOException, ServletException {
return FormValidation.validateExecutable(value, new FormValidation.FileValidator() {
@Override public FormValidation validate(File exe) {
try {
ByteBuffer baos = new ByteBuffer();
if (Hudson.getInstance().createLauncher(TaskListener.NULL).launch()
.cmds(getBzrExe(), "--version").stdout(baos).join() == 0) {
return FormValidation.ok();
} else {
return FormValidation.warning("Could not locate the executable in path");
}
} catch (IOException e) {
// failed
} catch (InterruptedException e) {
// failed
}
return FormValidation.error("Unable to check bazaar version");
}
});
}
/**
* UUID version string.
* This appears to be used for snapshot builds. See issue #1683
*/
private static final Pattern UUID_VERSION_STRING = Pattern.compile("\\(version ([0-9a-f]+)");
}
private static final long serialVersionUID = 1L;
private static final Logger logger = Logger.getLogger(BazaarSCM.class.getName());
}