package hudson.scm;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import static hudson.Util.fixEmpty;
import hudson.model.AbstractBuild;
import hudson.model.AbstractModelObject;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.Job;
import hudson.model.LargeText;
import hudson.model.ModelObject;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.org.apache.tools.ant.taskdefs.cvslib.ChangeLogTask;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
import hudson.util.ArgumentListBuilder;
import hudson.util.ByteBuffer;
import hudson.util.ForkOutputStream;
import hudson.util.FormFieldValidator;
import hudson.util.StreamTaskListener;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.lang.ref.WeakReference;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* CVS.
*
*
* I couldn't call this class "CVS" because that would cause the view folder name
* to collide with CVS control files.
*
*
* This object gets shipped to the remote machine to perform some of the work,
* so it implements {@link Serializable}.
*
* @author Kohsuke Kawaguchi
*/
public class CVSSCM extends AbstractCVSFamilySCM implements Serializable {
/**
* CVSSCM connection string.
*/
private String cvsroot;
/**
* Module names.
*
* This could be a whitespace-separate list of multiple modules.
* Modules could be either directories or files.
*/
private String module;
private String branch;
private String cvsRsh;
private boolean canUseUpdate;
/**
* True to avoid creating a sub-directory inside the workspace.
* (Works only when there's just one module.)
*/
private boolean flatten;
public CVSSCM(String cvsroot, String module,String branch,String cvsRsh,boolean canUseUpdate, boolean flatten) {
this.cvsroot = cvsroot;
this.module = module.trim();
this.branch = nullify(branch);
this.cvsRsh = nullify(cvsRsh);
this.canUseUpdate = canUseUpdate;
this.flatten = flatten && module.indexOf(' ')==-1;
}
public String getCvsRoot() {
return cvsroot;
}
/**
* If there are multiple modules, return the module directory of the first one.
* @param workspace
*/
public FilePath getModuleRoot(FilePath workspace) {
if(flatten)
return workspace;
int idx = module.indexOf(' ');
if(idx>=0) return workspace.child(module.substring(0,idx));
else return workspace.child(module);
}
public ChangeLogParser createChangeLogParser() {
return new CVSChangeLogParser();
}
public String getAllModules() {
return module;
}
/**
* Branch to build. Null to indicate the trunk.
*/
public String getBranch() {
return branch;
}
public String getCvsRsh() {
return cvsRsh;
}
public boolean getCanUseUpdate() {
return canUseUpdate;
}
public boolean isFlatten() {
return flatten;
}
public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath dir, TaskListener listener) throws IOException, InterruptedException {
List changedFiles = update(true, launcher, dir, listener, new Date());
return changedFiles!=null && !changedFiles.isEmpty();
}
private void configureDate(ArgumentListBuilder cmd, Date date) { // #192
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.US);
df.setTimeZone(TimeZone.getTimeZone("UTC")); // #209
cmd.add("-D", df.format(date));
}
public boolean checkout(AbstractBuild build, Launcher launcher, FilePath dir, BuildListener listener, File changelogFile) throws IOException, InterruptedException {
List changedFiles = null; // files that were affected by update. null this is a check out
if(canUseUpdate && isUpdatable(dir)) {
changedFiles = update(false, launcher, dir, listener, build.getTimestamp().getTime());
if(changedFiles==null)
return false; // failed
} else {
dir.deleteContents();
ArgumentListBuilder cmd = new ArgumentListBuilder();
cmd.add(getDescriptor().getCvsExe(),debugLogging?"-t":"-Q","-z9","-d",cvsroot,"co");
if(branch!=null)
cmd.add("-r",branch);
if(flatten)
cmd.add("-d",dir.getName());
configureDate(cmd, build.getTimestamp().getTime());
cmd.addTokenized(module);
if(!run(launcher,cmd,listener, flatten ? dir.getParent() : dir))
return false;
}
// archive the workspace to support later tagging
File archiveFile = getArchiveFile(build);
final OutputStream os = new RemoteOutputStream(new FileOutputStream(archiveFile));
build.getProject().getWorkspace().act(new FileCallable() {
public Void invoke(File ws, VirtualChannel channel) throws IOException {
ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os));
if(flatten) {
archive(ws, module, zos);
} else {
StringTokenizer tokens = new StringTokenizer(module);
while(tokens.hasMoreTokens()) {
String m = tokens.nextToken();
File mf = new File(ws, m);
if(!mf.exists())
// directory doesn't exist. This happens if a directory that was checked out
// didn't include any file.
continue;
if(!mf.isDirectory()) {
// this module is just a file, say "foo/bar.txt".
// to record "foo/CVS/*", we need to start by archiving "foo".
int idx = m.lastIndexOf('/');
if(idx==-1)
throw new Error("Kohsuke probe: m="+m);
m = m.substring(0, idx);
mf = mf.getParentFile();
}
archive(mf,m,zos);
}
}
zos.close();
return null;
}
});
// contribute the tag action
build.getActions().add(new TagAction(build));
return calcChangeLog(build, changedFiles, changelogFile, listener);
}
/**
* Returns the file name used to archive the build.
*/
private static File getArchiveFile(AbstractBuild build) {
return new File(build.getRootDir(),"workspace.zip");
}
/**
* Archives all the CVS-controlled files in {@code dir}.
*
* @param relPath
* The path name in ZIP to store this directory with.
*/
private void archive(File dir,String relPath,ZipOutputStream zos) throws IOException {
Set knownFiles = new HashSet();
// see http://www.monkey.org/openbsd/archive/misc/9607/msg00056.html for what Entries.Log is for
parseCVSEntries(new File(dir,"CVS/Entries"),knownFiles);
parseCVSEntries(new File(dir,"CVS/Entries.Log"),knownFiles);
parseCVSEntries(new File(dir,"CVS/Entries.Extra"),knownFiles);
boolean hasCVSdirs = !knownFiles.isEmpty();
knownFiles.add("CVS");
File[] files = dir.listFiles();
if(files==null)
throw new IOException("No such directory exists. Did you specify the correct branch?: "+dir);
for( File f : files ) {
String name = relPath+'/'+f.getName();
if(f.isDirectory()) {
if(hasCVSdirs && !knownFiles.contains(f.getName())) {
// not controlled in CVS. Skip.
// but also make sure that we archive CVS/*, which doesn't have CVS/CVS
continue;
}
archive(f,name,zos);
} else {
if(!dir.getName().equals("CVS"))
// we only need to archive CVS control files, not the actual workspace files
continue;
zos.putNextEntry(new ZipEntry(name));
FileInputStream fis = new FileInputStream(f);
Util.copyStream(fis,zos);
fis.close();
zos.closeEntry();
}
}
}
/**
* Parses the CVS/Entries file and adds file/directory names to the list.
*/
private void parseCVSEntries(File entries, Set knownFiles) throws IOException {
if(!entries.exists())
return;
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(entries)));
String line;
while((line=in.readLine())!=null) {
String[] tokens = line.split("/+");
if(tokens==null || tokens.length<2) continue; // invalid format
knownFiles.add(tokens[1]);
}
in.close();
}
/**
* Updates the workspace as well as locate changes.
*
* @return
* List of affected file names, relative to the workspace directory.
* Null if the operation failed.
*/
private List update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener, Date date) throws IOException, InterruptedException {
List changedFileNames = new ArrayList(); // file names relative to the workspace
ArgumentListBuilder cmd = new ArgumentListBuilder();
cmd.add(getDescriptor().getCvsExe(),"-q","-z9");
if(dryRun)
cmd.add("-n");
cmd.add("update","-PdC");
if (branch != null) {
cmd.add("-r", branch);
}
configureDate(cmd, date);
if(flatten) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if(!run(launcher,cmd,listener,workspace,
new ForkOutputStream(baos,listener.getLogger())))
return null;
parseUpdateOutput("",baos, changedFileNames);
} else {
@SuppressWarnings("unchecked") // StringTokenizer oddly has the wrong type
final Set moduleNames = new TreeSet(Collections.list(new StringTokenizer(module)));
// Add in any existing CVS dirs, in case project checked out its own.
moduleNames.addAll(workspace.act(new FileCallable>() {
public Set invoke(File ws, VirtualChannel channel) throws IOException {
File[] subdirs = ws.listFiles();
if (subdirs != null) {
SUBDIR: for (File s : subdirs) {
if (new File(s, "CVS").isDirectory()) {
String top = s.getName();
for (String mod : moduleNames) {
if (mod.startsWith(top + "/")) {
// #190: user asked to check out foo/bar foo/baz quux
// Our top-level dirs are "foo" and "quux".
// Do not add "foo" to checkout or we will check out foo/*!
continue SUBDIR;
}
}
moduleNames.add(top);
}
}
}
return moduleNames;
}
}));
for (String moduleName : moduleNames) {
// capture the output during update
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FilePath modulePath = new FilePath(workspace, moduleName);
ArgumentListBuilder actualCmd = cmd;
String baseName = moduleName;
if(!modulePath.isDirectory()) {
// updating just one file, like "foo/bar.txt".
// run update command from "foo" directory with "bar.txt" as the command line argument
actualCmd = cmd.clone();
actualCmd.add(modulePath.getName());
modulePath = modulePath.getParent();
baseName = baseName.substring(0,baseName.lastIndexOf('/'));
}
if(!run(launcher,actualCmd,listener,
modulePath,
new ForkOutputStream(baos,listener.getLogger())))
return null;
// we'll run one "cvs log" command with workspace as the base,
// so use path names that are relative to moduleName.
parseUpdateOutput(baseName+'/',baos, changedFileNames);
}
}
return changedFileNames;
}
// see http://www.network-theory.co.uk/docs/cvsmanual/cvs_153.html for the output format.
// we don't care '?' because that's not in the repository
private static final Pattern UPDATE_LINE = Pattern.compile("[UPARMC] (.+)");
private static final Pattern REMOVAL_LINE = Pattern.compile("cvs (server|update): `?(.+?)'? is no longer in the repository");
//private static final Pattern NEWDIRECTORY_LINE = Pattern.compile("cvs server: New directory `(.+)' -- ignored");
/**
* Parses the output from CVS update and list up files that might have been changed.
*
* @param result
* list of file names whose changelog should be checked. This may include files
* that are no longer present. The path names are relative to the workspace,
* hence "String", not {@link File}.
*/
private void parseUpdateOutput(String baseName, ByteArrayOutputStream output, List result) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(
new ByteArrayInputStream(output.toByteArray())));
String line;
while((line=in.readLine())!=null) {
Matcher matcher = UPDATE_LINE.matcher(line);
if(matcher.matches()) {
result.add(baseName+matcher.group(1));
continue;
}
matcher= REMOVAL_LINE.matcher(line);
if(matcher.matches()) {
result.add(baseName+matcher.group(2));
continue;
}
// this line is added in an attempt to capture newly created directories in the repository,
// but it turns out that this line always hit if the workspace is missing a directory
// that the server has, even if that directory contains nothing in it
//matcher= NEWDIRECTORY_LINE.matcher(line);
//if(matcher.matches()) {
// result.add(baseName+matcher.group(1));
//}
}
}
/**
* Returns true if we can use "cvs update" instead of "cvs checkout"
*/
private boolean isUpdatable(FilePath dir) throws IOException, InterruptedException {
return dir.act(new FileCallable() {
public Boolean invoke(File dir, VirtualChannel channel) throws IOException {
if(flatten) {
return isUpdatableModule(dir);
} else {
StringTokenizer tokens = new StringTokenizer(module);
while(tokens.hasMoreTokens()) {
File module = new File(dir,tokens.nextToken());
if(!isUpdatableModule(module))
return false;
}
return true;
}
}
});
}
private boolean isUpdatableModule(File module) {
if(!module.isDirectory())
// module is a file, like "foo/bar.txt". Then CVS information is "foo/CVS".
module = module.getParentFile();
File cvs = new File(module,"CVS");
if(!cvs.exists())
return false;
// check cvsroot
if(!checkContents(new File(cvs,"Root"),cvsroot))
return false;
if(branch!=null) {
if(!checkContents(new File(cvs,"Tag"),'T'+branch))
return false;
} else {
File tag = new File(cvs,"Tag");
if (tag.exists()) {
try {
Reader r = new FileReader(tag);
try {
String s = new BufferedReader(r).readLine();
return s != null && s.startsWith("D");
} finally {
r.close();
}
} catch (IOException e) {
return false;
}
}
}
return true;
}
/**
* Returns true if the contents of the file is equal to the given string.
*
* @return false in all the other cases.
*/
private boolean checkContents(File file, String contents) {
try {
Reader r = new FileReader(file);
try {
String s = new BufferedReader(r).readLine();
if (s == null) return false;
return s.trim().equals(contents.trim());
} finally {
r.close();
}
} catch (IOException e) {
return false;
}
}
/**
* Used to communicate the result of the detection in {@link CVSSCM#calcChangeLog(AbstractBuild, List, File, BuildListener)}
*/
class ChangeLogResult implements Serializable {
boolean hadError;
String errorOutput;
public ChangeLogResult(boolean hadError, String errorOutput) {
this.hadError = hadError;
if(hadError)
this.errorOutput = errorOutput;
}
private static final long serialVersionUID = 1L;
}
/**
* Used to propagate {@link BuildException} and error log at the same time.
*/
class BuildExceptionWithLog extends RuntimeException {
final String errorOutput;
public BuildExceptionWithLog(BuildException cause, String errorOutput) {
super(cause);
this.errorOutput = errorOutput;
}
private static final long serialVersionUID = 1L;
}
/**
* Computes the changelog into an XML file.
*
*
* When we update the workspace, we'll compute the changelog by using its output to
* make it faster. In general case, we'll fall back to the slower approach where
* we check all files in the workspace.
*
* @param changedFiles
* Files whose changelog should be checked for updates.
* This is provided if the previous operation is update, otherwise null,
* which means we have to fall back to the default slow computation.
*/
private boolean calcChangeLog(AbstractBuild build, final List changedFiles, File changelogFile, final BuildListener listener) throws InterruptedException {
if(build.getPreviousBuild()==null || (changedFiles!=null && changedFiles.isEmpty())) {
// nothing to compare against, or no changes
// (note that changedFiles==null means fallback, so we have to run cvs log.
listener.getLogger().println("$ no changes detected");
return createEmptyChangeLog(changelogFile,listener, "changelog");
}
listener.getLogger().println("$ computing changelog");
FilePath baseDir = build.getProject().getWorkspace();
final String cvspassFile = getDescriptor().getCvspassFile();
try {
// range of time for detecting changes
final Date startTime = build.getPreviousBuild().getTimestamp().getTime();
final Date endTime = build.getTimestamp().getTime();
final OutputStream out = new RemoteOutputStream(new FileOutputStream(changelogFile));
ChangeLogResult result = baseDir.act(new FileCallable() {
public ChangeLogResult invoke(File ws, VirtualChannel channel) throws IOException {
final StringWriter errorOutput = new StringWriter();
final boolean[] hadError = new boolean[1];
ChangeLogTask task = new ChangeLogTask() {
public void log(String msg, int msgLevel) {
// send error to listener. This seems like the route in which the changelog task
// sends output
if(msgLevel==org.apache.tools.ant.Project.MSG_ERR) {
hadError[0] = true;
errorOutput.write(msg);
errorOutput.write('\n');
return;
}
if(debugLogging) {
listener.getLogger().println(msg);
}
}
};
task.setProject(new org.apache.tools.ant.Project());
task.setDir(ws);
if(cvspassFile.length()!=0)
task.setPassfile(new File(cvspassFile));
task.setCvsRoot(cvsroot);
task.setCvsRsh(cvsRsh);
task.setFailOnError(true);
task.setDeststream(new BufferedOutputStream(out));
task.setBranch(branch);
task.setStart(startTime);
task.setEnd(endTime);
if(changedFiles!=null) {
// if the directory doesn't exist, cvs changelog will die, so filter them out.
// this means we'll lose the log of those changes
for (String filePath : changedFiles) {
if(new File(ws,filePath).getParentFile().exists())
task.addFile(filePath);
}
} else {
// fallback
if(!flatten)
task.setPackage(module);
}
try {
task.execute();
} catch (BuildException e) {
throw new BuildExceptionWithLog(e,errorOutput.toString());
}
return new ChangeLogResult(hadError[0],errorOutput.toString());
}
});
if(result.hadError) {
// non-fatal error must have occurred, such as cvs changelog parsing error.s
listener.getLogger().print(result.errorOutput);
}
return true;
} catch( BuildExceptionWithLog e ) {
// capture output from the task for diagnosis
listener.getLogger().print(e.errorOutput);
// then report an error
BuildException x = (BuildException) e.getCause();
PrintWriter w = listener.error(x.getMessage());
w.println("Working directory is "+baseDir);
x.printStackTrace(w);
return false;
} catch( RuntimeException e ) {
// an user reported a NPE inside the changeLog task.
// we don't want a bug in Ant to prevent a build.
e.printStackTrace(listener.error(e.getMessage()));
return true; // so record the message but continue
} catch( IOException e ) {
e.printStackTrace(listener.error("Failed to detect changlog"));
return true;
}
}
public DescriptorImpl getDescriptor() {
return DescriptorImpl.DESCRIPTOR;
}
public void buildEnvVars(Map env) {
if(cvsRsh!=null)
env.put("CVS_RSH",cvsRsh);
String cvspass = getDescriptor().getCvspassFile();
if(cvspass.length()!=0)
env.put("CVS_PASSFILE",cvspass);
}
public static final class DescriptorImpl extends Descriptor implements ModelObject {
static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
/**
* Path to .cvspass. Null to default.
*/
private String cvsPassFile;
/**
* Path to cvs executable. Null to just use "cvs".
*/
private String cvsExe;
/**
* Copy-on-write.
*/
private volatile Map browsers = new HashMap();
class RepositoryBrowser {
String diffURL;
String browseURL;
}
DescriptorImpl() {
super(CVSSCM.class);
load();
}
protected void convert(Map oldPropertyBag) {
cvsPassFile = (String)oldPropertyBag.get("cvspass");
}
public String getDisplayName() {
return "CVS";
}
public SCM newInstance(StaplerRequest req) {
return new CVSSCM(
req.getParameter("cvs_root"),
req.getParameter("cvs_module"),
req.getParameter("cvs_branch"),
req.getParameter("cvs_rsh"),
req.getParameter("cvs_use_update")!=null,
req.getParameter("cvs_legacy")==null
);
}
public String getCvspassFile() {
String value = cvsPassFile;
if(value==null)
value = "";
return value;
}
public String getCvsExe() {
if(cvsExe==null) return "cvs";
else return cvsExe;
}
public void setCvspassFile(String value) {
cvsPassFile = value;
save();
}
/**
* Gets the URL that shows the diff.
*/
public String getDiffURL(String cvsRoot, String pathName, String oldRev, String newRev) {
RepositoryBrowser b = browsers.get(cvsRoot);
if(b==null) return null;
return b.diffURL.replaceAll("%%P",pathName).replace("%%r",oldRev).replace("%%R",newRev);
}
public boolean configure( StaplerRequest req ) {
cvsPassFile = fixEmpty(req.getParameter("cvs_cvspass").trim());
cvsExe = fixEmpty(req.getParameter("cvs_exe").trim());
Map browsers = new HashMap();
int i=0;
while(true) {
String root = req.getParameter("cvs_repobrowser_cvsroot" + i);
if(root==null) break;
RepositoryBrowser rb = new RepositoryBrowser();
rb.browseURL = req.getParameter("cvs_repobrowser"+i);
rb.diffURL = req.getParameter("cvs_repobrowser_diff"+i);
browsers.put(root,rb);
i++;
}
this.browsers = browsers;
save();
return true;
}
//
// web methods
//
public void doCvsPassCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
// this method can be used to check if a file exists anywhere in the file system,
// so it should be protected.
new FormFieldValidator(req,rsp,true) {
protected void check() throws IOException, ServletException {
String v = fixEmpty(request.getParameter("value"));
if(v==null) {
// default.
ok();
} else {
File cvsPassFile = new File(v);
if(cvsPassFile.exists()) {
ok();
} else {
error("No such file exists");
}
}
}
}.process();
}
/**
* Checks if cvs executable exists.
*/
public void doCvsExeCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
// this method can be used to check if a file exists anywhere in the file system,
// so it should be protected.
new FormFieldValidator(req,rsp,true) {
protected void check() throws IOException, ServletException {
String cvsExe = fixEmpty(request.getParameter("value"));
if(cvsExe==null) {
ok();
return;
}
if(cvsExe.indexOf(File.separatorChar)>=0) {
// this is full path
if(new File(cvsExe).exists()) {
ok();
} else {
error("There's no such file: "+cvsExe);
}
} else {
// can't really check
ok();
}
}
}.process();
}
/**
* Displays "cvs --version" for trouble shooting.
*/
public void doVersion(StaplerRequest req, StaplerResponse rsp) throws IOException {
rsp.setContentType("text/plain");
ServletOutputStream os = rsp.getOutputStream();
try {
Proc proc = Hudson.getInstance().createLauncher(TaskListener.NULL).launch(
new String[]{getCvsExe(), "--version"}, new String[0], os, null);
proc.join();
} catch (IOException e) {
PrintWriter w = new PrintWriter(os);
w.println("Failed to launch "+getCvsExe());
String msg = Util.getWin32ErrorMessage(e);
if(msg!=null)
w.println(msg);
e.printStackTrace(w);
w.close();
}
}
/**
* Checks the entry to the CVSROOT field.
*
* Also checks if .cvspass file contains the entry for this.
*/
public void doCvsrootCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
new FormFieldValidator(req,rsp,false) {
protected void check() throws IOException, ServletException {
String v = fixEmpty(request.getParameter("value"));
if(v==null) {
error("CVSROOT is mandatory");
return;
}
// CVSROOT format isn't really that well defined. So it's hard to check this rigorously.
if(v.startsWith(":pserver") || v.startsWith(":ext")) {
if(!CVSROOT_PSERVER_PATTERN.matcher(v).matches()) {
error("Invalid CVSROOT string");
return;
}
// I can't really test if the machine name exists, either.
// some cvs, such as SOCKS-enabled cvs can resolve host names that Hudson might not
// be able to. If :ext is used, all bets are off anyway.
}
// check .cvspass file to see if it has entry.
// CVS handles authentication only if it's pserver.
if(v.startsWith(":pserver")) {
String cvspass = getCvspassFile();
File passfile;
if(cvspass.equals("")) {
passfile = new File(new File(System.getProperty("user.home")),".cvspass");
} else {
passfile = new File(cvspass);
}
if(passfile.exists()) {
// It's possible that we failed to locate the correct .cvspass file location,
// so don't report an error if we couldn't locate this file.
//
// if this is explicitly specified, then our system config page should have
// reported an error.
if(!scanCvsPassFile(passfile, v)) {
error("It doesn't look like this CVSROOT has its password set." +
" Would you like to set it now?");
return;
}
}
}
// all tests passed so far
ok();
}
}.process();
}
/**
* Checks if the given pserver CVSROOT value exists in the pass file.
*/
private boolean scanCvsPassFile(File passfile, String cvsroot) throws IOException {
cvsroot += ' ';
String cvsroot2 = "/1 "+cvsroot; // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5006835
BufferedReader in = new BufferedReader(new FileReader(passfile));
try {
String line;
while((line=in.readLine())!=null) {
// "/1 " version always have the port number in it, so examine a much with
// default port 2401 left out
int portIndex = line.indexOf(":2401/");
String line2 = "";
if(portIndex>=0)
line2 = line.substring(0,portIndex+1)+line.substring(portIndex+5); // leave '/'
if(line.startsWith(cvsroot) || line.startsWith(cvsroot2) || line2.startsWith(cvsroot2))
return true;
}
return false;
} finally {
in.close();
}
}
private static final Pattern CVSROOT_PSERVER_PATTERN =
Pattern.compile(":(ext|pserver):[^@:]+@[^:]+:(\\d+:)?.+");
/**
* Runs cvs login command.
*
* TODO: this apparently doesn't work. Probably related to the fact that
* cvs does some tty magic to disable echo back or whatever.
*/
public void doPostPassword(StaplerRequest req, StaplerResponse rsp) throws IOException {
if(!Hudson.adminCheck(req,rsp))
return;
String cvsroot = req.getParameter("cvsroot");
String password = req.getParameter("password");
if(cvsroot==null || password==null) {
rsp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
rsp.setContentType("text/plain");
Proc proc = Hudson.getInstance().createLauncher(TaskListener.NULL).launch(
new String[]{getCvsExe(), "-d",cvsroot,"login"}, new String[0],
new ByteArrayInputStream((password+"\n").getBytes()),
rsp.getOutputStream());
proc.join();
}
}
/**
* Action for a build that performs the tagging.
*/
public final class TagAction extends AbstractModelObject implements Action {
private final AbstractBuild build;
/**
* If non-null, that means the build is already tagged.
*/
private volatile String tagName;
/**
* If non-null, that means the tagging is in progress
* (asynchronously.)
*/
private transient volatile TagWorkerThread workerThread;
/**
* Hold the log of "cvs tag" operation.
*/
private transient WeakReference log;
public TagAction(AbstractBuild build) {
this.build = build;
}
public String getIconFileName() {
return "save.gif";
}
public String getDisplayName() {
return "Tag this build";
}
public String getUrlName() {
return "tagBuild";
}
public String getTagName() {
return tagName;
}
public TagWorkerThread getWorkerThread() {
return workerThread;
}
public AbstractBuild getBuild() {
return build;
}
public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
req.setAttribute("build",build);
req.getView(this,chooseAction()).forward(req,rsp);
}
private synchronized String chooseAction() {
if(tagName!=null)
return "alreadyTagged.jelly";
if(workerThread!=null)
return "inProgress.jelly";
return "tagForm.jelly";
}
/**
* Invoked to actually tag the workspace.
*/
public synchronized void doSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
Map tagSet = new HashMap();
String name = req.getParameter("name");
if(isInvalidTag(name)) {
sendError("No valid tag name given",req,rsp);
return;
}
tagSet.put(build,name);
if(req.getParameter("upstream")!=null) {
// tag all upstream builds
Enumeration e = req.getParameterNames();
Map upstreams = build.getUpstreamBuilds(); // TODO: define them at AbstractBuild level
while(e.hasMoreElements()) {
String upName = (String) e.nextElement();
if(!upName.startsWith("upstream."))
continue;
String tag = req.getParameter(upName);
if(isInvalidTag(tag)) {
sendError("No valid tag name given for "+upName,req,rsp);
return;
}
upName = upName.substring(9); // trim off 'upstream.'
Job p = Hudson.getInstance().getItemByFullName(upName,Job.class);
Run build = p.getBuildByNumber(upstreams.get(p));
tagSet.put((AbstractBuild) build,tag);
}
}
new TagWorkerThread(tagSet).start();
doIndex(req,rsp);
}
private boolean isInvalidTag(String name) {
return name==null || name.length()==0;
}
/**
* Clears the error status.
*/
public synchronized void doClearError(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
if(workerThread!=null && !workerThread.isAlive())
workerThread = null;
doIndex(req,rsp);
}
/**
* Handles incremental log output.
*/
public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException {
if(log==null) {
rsp.setStatus(HttpServletResponse.SC_OK);
} else {
LargeText text = log.get();
if(text!=null)
text.doProgressText(req,rsp);
else
rsp.setStatus(HttpServletResponse.SC_OK);
}
}
/**
* Performs tagging.
*/
public void perform(String tagName, TaskListener listener) {
File destdir = null;
try {
destdir = Util.createTempDir();
// unzip the archive
listener.getLogger().println("expanding the workspace archive into "+destdir);
Expand e = new Expand();
e.setProject(new org.apache.tools.ant.Project());
e.setDest(destdir);
e.setSrc(getArchiveFile(build));
e.setTaskType("unzip");
e.execute();
// run cvs tag command
listener.getLogger().println("tagging the workspace");
StringTokenizer tokens = new StringTokenizer(CVSSCM.this.module);
while(tokens.hasMoreTokens()) {
String m = tokens.nextToken();
FilePath path = new FilePath(destdir).child(m);
boolean isDir = path.isDirectory();
ArgumentListBuilder cmd = new ArgumentListBuilder();
cmd.add(getDescriptor().getCvsExe(),"tag");
if(isDir) {
cmd.add("-R");
}
cmd.add(tagName);
if(!isDir) {
cmd.add(path.getName());
path = path.getParent();
}
if(!CVSSCM.this.run(new Launcher.LocalLauncher(listener),cmd,listener, path)) {
listener.getLogger().println("tagging failed");
return;
}
}
// completed successfully
onTagCompleted(tagName);
build.save();
} catch (Throwable e) {
e.printStackTrace(listener.fatalError(e.getMessage()));
} finally {
try {
if(destdir!=null) {
listener.getLogger().println("cleaning up "+destdir);
Util.deleteRecursive(destdir);
}
} catch (IOException e) {
e.printStackTrace(listener.fatalError(e.getMessage()));
}
}
}
/**
* Atomically set the tag name and then be done with {@link TagWorkerThread}.
*/
private synchronized void onTagCompleted(String tagName) {
this.tagName = tagName;
this.workerThread = null;
}
}
public static final class TagWorkerThread extends Thread {
// StringWriter is synchronized
private final ByteBuffer log = new ByteBuffer();
private final LargeText text = new LargeText(log,false);
private final Map tagSet;
public TagWorkerThread(Map tagSet) {
this.tagSet = tagSet;
}
public String getLog() {
// this method can be invoked from another thread.
return log.toString();
}
public synchronized void start() {
for (Entry e : tagSet.entrySet()) {
TagAction ta = e.getKey().getAction(TagAction.class);
if(ta!=null) {
ta.workerThread = this;
ta.log = new WeakReference(text);
}
}
super.start();
}
public void run() {
TaskListener listener = new StreamTaskListener(log);
for (Entry e : tagSet.entrySet()) {
TagAction ta = e.getKey().getAction(TagAction.class);
if(ta==null) {
listener.error(e.getKey()+" doesn't have CVS tag associated with it. Skipping");
continue;
}
listener.getLogger().println("Tagging "+e.getKey()+" to "+e.getValue());
ta.perform(e.getValue(), listener);
listener.getLogger().println();
}
listener.getLogger().println("Completed");
text.markAsComplete();
}
}
/**
* Temporary hack for assisting trouble-shooting.
*
*
* Setting this property to true would cause cvs log to dump a lot of messages.
*/
public static boolean debugLogging = false;
private static final long serialVersionUID = 1L;
}