All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.scm.CVSSCM Maven / Gradle / Ivy

package hudson.scm;

import hudson.FilePath;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import static hudson.Util.fixEmpty;
import hudson.model.Action;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.ModelObject;
import hudson.model.Project;
import hudson.model.Result;
import hudson.model.StreamBuildListener;
import hudson.model.TaskListener;
import hudson.org.apache.tools.ant.taskdefs.cvslib.ChangeLogTask;
import hudson.util.ArgumentListBuilder;
import hudson.util.ForkOutputStream;
import hudson.util.FormFieldValidator;
import java.util.Collections;
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.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Set;
import java.util.TreeSet;
import java.util.HashSet;
import java.util.HashMap;
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. * * @author Kohsuke Kawaguchi */ public class CVSSCM extends AbstractCVSFamilySCM { /** * CVSSCM connection string. */ private String cvsroot; /** * Module names. * * This could be a whitespace-separate list of multiple modules. */ 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(Project project, Launcher launcher, FilePath dir, TaskListener listener) throws IOException { List changedFiles = update(true, launcher, dir, listener); return changedFiles!=null && !changedFiles.isEmpty(); } public boolean checkout(Build build, Launcher launcher, FilePath dir, BuildListener listener, File changelogFile) throws IOException { List changedFiles = null; // files that were affected by update. null this is a check out if(canUseUpdate && isUpdatable(dir.getLocal())) { changedFiles = update(false,launcher,dir,listener); if(changedFiles==null) return false; // failed } else { dir.deleteContents(); ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add("cvs","-Q","-z9","-d",cvsroot,"co"); if(branch!=null) cmd.add("-r",branch); if(flatten) cmd.add("-d",dir.getName()); cmd.addTokenized(module); if(!run(launcher,cmd,listener, flatten ? dir.getParent() : dir)) return false; } // archive the workspace to support later tagging // TODO: doing this partially remotely would be faster File archiveFile = getArchiveFile(build); ZipOutputStream zos = new ZipOutputStream(archiveFile); if(flatten) { archive(build.getProject().getWorkspace().getLocal(), module, zos); } else { StringTokenizer tokens = new StringTokenizer(module); while(tokens.hasMoreTokens()) { String m = tokens.nextToken(); archive(new File(build.getProject().getWorkspace().getLocal(),m),m,zos); } } zos.close(); // 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(Build build) { return new File(build.getRootDir(),"workspace.zip"); } 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. */ public List update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException { List changedFileNames = new ArrayList(); // file names relative to the workspace ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add("cvs","-q","-z9"); if(dryRun) cmd.add("-n"); cmd.add("update","-PdC"); 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 Set moduleNames = new TreeSet(Collections.list(new StringTokenizer(module))); // Add in any existing CVS dirs, in case project checked out its own. File[] subdirs = workspace.getLocal().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); } } } for (String moduleName : moduleNames) { // capture the output during update ByteArrayOutputStream baos = new ByteArrayOutputStream(); if(!run(launcher,cmd,listener, new FilePath(workspace, moduleName), 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(moduleName+'/',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(File dir) { 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) { 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 { if(new File(cvs,"Tag").exists()) 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; } } /** * 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(Build build, List changedFiles, File changelogFile, final BuildListener listener) { 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"); 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()); File baseDir = build.getProject().getWorkspace().getLocal(); task.setDir(baseDir); if(DESCRIPTOR.getCvspassFile().length()!=0) task.setPassfile(new File(DESCRIPTOR.getCvspassFile())); task.setCvsRoot(cvsroot); task.setCvsRsh(cvsRsh); task.setFailOnError(true); task.setDestfile(changelogFile); task.setBranch(branch); task.setStart(build.getPreviousBuild().getTimestamp().getTime()); task.setEnd(build.getTimestamp().getTime()); 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(baseDir,filePath).getParentFile().exists()) task.addFile(filePath); } } else { // fallback if(!flatten) task.setPackage(module); } try { task.execute(); if(hadError[0]) { // non-fatal error must have occurred, such as cvs changelog parsing error.s listener.getLogger().print(errorOutput); } return true; } catch( BuildException e ) { // capture output from the task for diagnosis listener.getLogger().print(errorOutput); // then report an error PrintWriter w = listener.error(e.getMessage()); w.println("Working directory is "+baseDir); e.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 } } public DescriptorImpl getDescriptor() { return DESCRIPTOR; } public void buildEnvVars(Map env) { if(cvsRsh!=null) env.put("CVS_RSH",cvsRsh); String cvspass = DESCRIPTOR.getCvspassFile(); if(cvspass.length()!=0) env.put("CVS_PASSFILE",cvspass); } static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); public static final class DescriptorImpl extends Descriptor implements ModelObject { /** * Path to .cvspass. Null to default. */ private String cvsPassFile; /** * 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 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 ) { setCvspassFile(req.getParameter("cvs_cvspass")); 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(); } /** * Displays "cvs --version" for trouble shooting. */ public void doVersion(StaplerRequest req, StaplerResponse rsp) throws IOException { rsp.setContentType("text/plain"); Proc proc = Hudson.getInstance().createLauncher(TaskListener.NULL).launch( new String[]{"cvs", "--version"}, new String[0], rsp.getOutputStream(), FilePath.RANDOM); proc.join(); } /** * 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 ecoback 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[]{"cvs", "-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 implements Action { private final Build build; /** * If non-null, that means the build is already tagged. */ private String tagName; /** * If non-null, that means the tagging is in progress * (asynchronously.) */ private transient TagWorkerThread workerThread; public TagAction(Build 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 Build 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 { String name = req.getParameter("name"); if(name==null || name.length()==0) { // invalid tag name doIndex(req,rsp); return; } if(workerThread==null) { workerThread = new TagWorkerThread(name); workerThread.start(); } doIndex(req,rsp); } /** * 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); } public final class TagWorkerThread extends Thread { private final String tagName; // StringWriter is synchronized private final StringWriter log = new StringWriter(); public TagWorkerThread(String tagName) { this.tagName = tagName; } public String getLog() { // this method can be invoked from another thread. return log.toString(); } public String getTagName() { return tagName; } public void run() { BuildListener listener = new StreamBuildListener(log); Result result = Result.FAILURE; File destdir = null; listener.started(); 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(); ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add("cvs","tag","-R",tagName); if(!CVSSCM.this.run(new Launcher(listener),cmd,listener,new FilePath(destdir).child(m))) { listener.getLogger().println("tagging failed"); return; } } // completed successfully synchronized(TagAction.this) { TagAction.this.tagName = this.tagName; TagAction.this.workerThread = null; } 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())); } listener.finished(result); } } } } /** * 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; }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy