KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > hudson > scm > CVSSCM


1 package hudson.scm;
2
3 import hudson.FilePath;
4 import hudson.FilePath.FileCallable;
5 import hudson.Launcher;
6 import hudson.Proc;
7 import hudson.Util;
8 import static hudson.Util.fixEmpty;
9 import hudson.model.AbstractBuild;
10 import hudson.model.AbstractModelObject;
11 import hudson.model.AbstractProject;
12 import hudson.model.Action;
13 import hudson.model.BuildListener;
14 import hudson.model.Descriptor;
15 import hudson.model.Hudson;
16 import hudson.model.Job;
17 import hudson.model.LargeText;
18 import hudson.model.ModelObject;
19 import hudson.model.Run;
20 import hudson.model.TaskListener;
21 import hudson.org.apache.tools.ant.taskdefs.cvslib.ChangeLogTask;
22 import hudson.remoting.RemoteOutputStream;
23 import hudson.remoting.VirtualChannel;
24 import hudson.util.ArgumentListBuilder;
25 import hudson.util.ByteBuffer;
26 import hudson.util.ForkOutputStream;
27 import hudson.util.FormFieldValidator;
28 import hudson.util.StreamTaskListener;
29 import org.apache.tools.ant.BuildException;
30 import org.apache.tools.ant.taskdefs.Expand;
31 import org.apache.tools.zip.ZipEntry;
32 import org.apache.tools.zip.ZipOutputStream;
33 import org.kohsuke.stapler.StaplerRequest;
34 import org.kohsuke.stapler.StaplerResponse;
35
36 import javax.servlet.ServletException JavaDoc;
37 import javax.servlet.http.HttpServletResponse JavaDoc;
38 import java.io.BufferedOutputStream JavaDoc;
39 import java.io.BufferedReader JavaDoc;
40 import java.io.ByteArrayInputStream JavaDoc;
41 import java.io.ByteArrayOutputStream JavaDoc;
42 import java.io.File JavaDoc;
43 import java.io.FileInputStream JavaDoc;
44 import java.io.FileOutputStream JavaDoc;
45 import java.io.FileReader JavaDoc;
46 import java.io.IOException JavaDoc;
47 import java.io.InputStreamReader JavaDoc;
48 import java.io.OutputStream JavaDoc;
49 import java.io.PrintWriter JavaDoc;
50 import java.io.Reader JavaDoc;
51 import java.io.Serializable JavaDoc;
52 import java.io.StringWriter JavaDoc;
53 import java.lang.ref.WeakReference JavaDoc;
54 import java.text.DateFormat JavaDoc;
55 import java.util.ArrayList JavaDoc;
56 import java.util.Collections JavaDoc;
57 import java.util.Date JavaDoc;
58 import java.util.Enumeration JavaDoc;
59 import java.util.HashMap JavaDoc;
60 import java.util.HashSet JavaDoc;
61 import java.util.List JavaDoc;
62 import java.util.Locale JavaDoc;
63 import java.util.Map JavaDoc;
64 import java.util.Map.Entry;
65 import java.util.Set JavaDoc;
66 import java.util.StringTokenizer JavaDoc;
67 import java.util.TimeZone JavaDoc;
68 import java.util.TreeSet JavaDoc;
69 import java.util.regex.Matcher JavaDoc;
70 import java.util.regex.Pattern JavaDoc;
71
72 /**
73  * CVS.
74  *
75  * <p>
76  * I couldn't call this class "CVS" because that would cause the view folder name
77  * to collide with CVS control files.
78  *
79  * <p>
80  * This object gets shipped to the remote machine to perform some of the work,
81  * so it implements {@link Serializable}.
82  *
83  * @author Kohsuke Kawaguchi
84  */

85 public class CVSSCM extends AbstractCVSFamilySCM implements Serializable JavaDoc {
86     /**
87      * CVSSCM connection string.
88      */

89     private String JavaDoc cvsroot;
90
91     /**
92      * Module names.
93      *
94      * This could be a whitespace-separate list of multiple modules.
95      * Modules could be either directories or files.
96      */

97     private String JavaDoc module;
98
99     private String JavaDoc branch;
100
101     private String JavaDoc cvsRsh;
102
103     private boolean canUseUpdate;
104
105     /**
106      * True to avoid creating a sub-directory inside the workspace.
107      * (Works only when there's just one module.)
108      */

109     private boolean flatten;
110
111
112     public CVSSCM(String JavaDoc cvsroot, String JavaDoc module,String JavaDoc branch,String JavaDoc cvsRsh,boolean canUseUpdate, boolean flatten) {
113         this.cvsroot = cvsroot;
114         this.module = module.trim();
115         this.branch = nullify(branch);
116         this.cvsRsh = nullify(cvsRsh);
117         this.canUseUpdate = canUseUpdate;
118         this.flatten = flatten && module.indexOf(' ')==-1;
119     }
120
121     private String JavaDoc compression() {
122         // CVS 1.11.22 manual:
123
// If the access method is omitted, then if the repository starts with
124
// `/', then `:local:' is assumed. If it does not start with `/' then
125
// either `:ext:' or `:server:' is assumed.
126
boolean local = cvsroot.startsWith("/") || cvsroot.startsWith(":local:") || cvsroot.startsWith(":fork:");
127         // For local access, compression is senseless. For remote, use z3:
128
// http://root.cern.ch/root/CVS.html#checkout
129
return local ? "-z0" : "-z3";
130     }
131
132     public String JavaDoc getCvsRoot() {
133         return cvsroot;
134     }
135
136     /**
137      * If there are multiple modules, return the module directory of the first one.
138      * @param workspace
139      */

140     public FilePath getModuleRoot(FilePath workspace) {
141         if(flatten)
142             return workspace;
143
144         int idx = module.indexOf(' ');
145         if(idx>=0) return workspace.child(module.substring(0,idx));
146         else return workspace.child(module);
147     }
148
149     public ChangeLogParser createChangeLogParser() {
150         return new CVSChangeLogParser();
151     }
152
153     public String JavaDoc getAllModules() {
154         return module;
155     }
156
157     /**
158      * Branch to build. Null to indicate the trunk.
159      */

160     public String JavaDoc getBranch() {
161         return branch;
162     }
163
164     public String JavaDoc getCvsRsh() {
165         return cvsRsh;
166     }
167
168     public boolean getCanUseUpdate() {
169         return canUseUpdate;
170     }
171
172     public boolean isFlatten() {
173         return flatten;
174     }
175
176     public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath dir, TaskListener listener) throws IOException JavaDoc, InterruptedException JavaDoc {
177         List JavaDoc<String JavaDoc> changedFiles = update(true, launcher, dir, listener, new Date JavaDoc());
178
179         return changedFiles!=null && !changedFiles.isEmpty();
180     }
181
182     private void configureDate(ArgumentListBuilder cmd, Date JavaDoc date) { // #192
183
DateFormat JavaDoc df = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.US);
184         df.setTimeZone(TimeZone.getTimeZone("UTC")); // #209
185
cmd.add("-D", df.format(date));
186     }
187
188     public boolean checkout(AbstractBuild build, Launcher launcher, FilePath dir, BuildListener listener, File JavaDoc changelogFile) throws IOException JavaDoc, InterruptedException JavaDoc {
189         List JavaDoc<String JavaDoc> changedFiles = null; // files that were affected by update. null this is a check out
190

191         if(canUseUpdate && isUpdatable(dir)) {
192             changedFiles = update(false, launcher, dir, listener, build.getTimestamp().getTime());
193             if(changedFiles==null)
194                 return false; // failed
195
} else {
196             if(!checkout(launcher,dir,listener,build.getTimestamp().getTime()))
197                 return false;
198         }
199
200         // archive the workspace to support later tagging
201
File JavaDoc archiveFile = getArchiveFile(build);
202         final OutputStream os = new RemoteOutputStream(new FileOutputStream JavaDoc(archiveFile));
203         
204         build.getProject().getWorkspace().act(new FileCallable<Void JavaDoc>() {
205             public Void JavaDoc invoke(File JavaDoc ws, VirtualChannel channel) throws IOException JavaDoc {
206                 ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream JavaDoc(os));
207
208                 if(flatten) {
209                     archive(ws, module, zos,true);
210                 } else {
211                     StringTokenizer JavaDoc tokens = new StringTokenizer JavaDoc(module);
212                     while(tokens.hasMoreTokens()) {
213                         String JavaDoc m = tokens.nextToken();
214                         File JavaDoc mf = new File JavaDoc(ws, m);
215
216                         if(!mf.exists())
217                             // directory doesn't exist. This happens if a directory that was checked out
218
// didn't include any file.
219
continue;
220
221                         if(!mf.isDirectory()) {
222                             // this module is just a file, say "foo/bar.txt".
223
// to record "foo/CVS/*", we need to start by archiving "foo".
224
int idx = m.lastIndexOf('/');
225                             if(idx==-1)
226                                 throw new Error JavaDoc("Kohsuke probe: m="+m);
227                             m = m.substring(0, idx);
228                             mf = mf.getParentFile();
229                         }
230                         archive(mf,m,zos,true);
231                     }
232                 }
233                 zos.close();
234                 return null;
235             }
236         });
237
238         // contribute the tag action
239
build.getActions().add(new TagAction(build));
240
241         return calcChangeLog(build, changedFiles, changelogFile, listener);
242     }
243
244     public boolean checkout(Launcher launcher, FilePath dir, TaskListener listener) throws IOException JavaDoc, InterruptedException JavaDoc {
245         Date JavaDoc now = new Date JavaDoc();
246         if(canUseUpdate && isUpdatable(dir)) {
247             return update(false, launcher, dir, listener, now)!=null;
248         } else {
249             return checkout(launcher,dir,listener, now);
250         }
251     }
252
253     private boolean checkout(Launcher launcher, FilePath dir, TaskListener listener, Date JavaDoc dt) throws IOException JavaDoc, InterruptedException JavaDoc {
254         dir.deleteContents();
255
256         ArgumentListBuilder cmd = new ArgumentListBuilder();
257         cmd.add(getDescriptor().getCvsExe(),debugLogging?"-t":"-Q",compression(),"-d",cvsroot,"co");
258         if(branch!=null)
259             cmd.add("-r",branch);
260         if(flatten)
261             cmd.add("-d",dir.getName());
262         configureDate(cmd,dt);
263         cmd.addTokenized(module);
264
265         return run(launcher,cmd,listener, flatten ? dir.getParent() : dir);
266     }
267
268     /**
269      * Returns the file name used to archive the build.
270      */

271     private static File JavaDoc getArchiveFile(AbstractBuild build) {
272         return new File JavaDoc(build.getRootDir(),"workspace.zip");
273     }
274
275     /**
276      * Archives all the CVS-controlled files in {@code dir}.
277      *
278      * @param relPath
279      * The path name in ZIP to store this directory with.
280      */

281     private void archive(File JavaDoc dir,String JavaDoc relPath,ZipOutputStream zos, boolean isRoot) throws IOException JavaDoc {
282         Set JavaDoc<String JavaDoc> knownFiles = new HashSet JavaDoc<String JavaDoc>();
283         // see http://www.monkey.org/openbsd/archive/misc/9607/msg00056.html for what Entries.Log is for
284
parseCVSEntries(new File JavaDoc(dir,"CVS/Entries"),knownFiles);
285         parseCVSEntries(new File JavaDoc(dir,"CVS/Entries.Log"),knownFiles);
286         parseCVSEntries(new File JavaDoc(dir,"CVS/Entries.Extra"),knownFiles);
287         boolean hasCVSdirs = !knownFiles.isEmpty();
288         knownFiles.add("CVS");
289
290         File JavaDoc[] files = dir.listFiles();
291         if(files==null) {
292             if(isRoot)
293                 throw new IOException JavaDoc("No such directory exists. Did you specify the correct branch/tag?: "+dir);
294             else
295                 throw new IOException JavaDoc("No such directory exists. Looks like someone is modifying the workspace concurrently: "+dir);
296         }
297         for( File JavaDoc f : files ) {
298             String JavaDoc name = relPath+'/'+f.getName();
299             if(f.isDirectory()) {
300                 if(hasCVSdirs && !knownFiles.contains(f.getName())) {
301                     // not controlled in CVS. Skip.
302
// but also make sure that we archive CVS/*, which doesn't have CVS/CVS
303
continue;
304                 }
305                 archive(f,name,zos,false);
306             } else {
307                 if(!dir.getName().equals("CVS"))
308                     // we only need to archive CVS control files, not the actual workspace files
309
continue;
310                 zos.putNextEntry(new ZipEntry(name));
311                 FileInputStream JavaDoc fis = new FileInputStream JavaDoc(f);
312                 Util.copyStream(fis,zos);
313                 fis.close();
314                 zos.closeEntry();
315             }
316         }
317     }
318
319     /**
320      * Parses the CVS/Entries file and adds file/directory names to the list.
321      */

322     private void parseCVSEntries(File JavaDoc entries, Set JavaDoc<String JavaDoc> knownFiles) throws IOException JavaDoc {
323         if(!entries.exists())
324             return;
325
326         BufferedReader JavaDoc in = new BufferedReader JavaDoc(new InputStreamReader JavaDoc(new FileInputStream JavaDoc(entries)));
327         String JavaDoc line;
328         while((line=in.readLine())!=null) {
329             String JavaDoc[] tokens = line.split("/+");
330             if(tokens==null || tokens.length<2) continue; // invalid format
331
knownFiles.add(tokens[1]);
332         }
333         in.close();
334     }
335
336     /**
337      * Updates the workspace as well as locate changes.
338      *
339      * @return
340      * List of affected file names, relative to the workspace directory.
341      * Null if the operation failed.
342      */

343     private List JavaDoc<String JavaDoc> update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener, Date JavaDoc date) throws IOException JavaDoc, InterruptedException JavaDoc {
344
345         List JavaDoc<String JavaDoc> changedFileNames = new ArrayList JavaDoc<String JavaDoc>(); // file names relative to the workspace
346

347         ArgumentListBuilder cmd = new ArgumentListBuilder();
348         cmd.add(getDescriptor().getCvsExe(),"-q",compression());
349         if(dryRun)
350             cmd.add("-n");
351         cmd.add("update","-PdC");
352         if (branch != null) {
353             cmd.add("-r", branch);
354         }
355         configureDate(cmd, date);
356
357         if(flatten) {
358             ByteArrayOutputStream JavaDoc baos = new ByteArrayOutputStream JavaDoc();
359
360             if(!run(launcher,cmd,listener,workspace,
361                 new ForkOutputStream(baos,listener.getLogger())))
362                 return null;
363
364             parseUpdateOutput("",baos, changedFileNames);
365         } else {
366             @SuppressWarnings JavaDoc("unchecked") // StringTokenizer oddly has the wrong type
367
final Set JavaDoc<String JavaDoc> moduleNames = new TreeSet JavaDoc(Collections.list(new StringTokenizer JavaDoc(module)));
368
369             // Add in any existing CVS dirs, in case project checked out its own.
370
moduleNames.addAll(workspace.act(new FileCallable<Set JavaDoc<String JavaDoc>>() {
371                 public Set JavaDoc<String JavaDoc> invoke(File JavaDoc ws, VirtualChannel channel) throws IOException JavaDoc {
372                     File JavaDoc[] subdirs = ws.listFiles();
373                     if (subdirs != null) {
374                         SUBDIR: for (File JavaDoc s : subdirs) {
375                             if (new File JavaDoc(s, "CVS").isDirectory()) {
376                                 String JavaDoc top = s.getName();
377                                 for (String JavaDoc mod : moduleNames) {
378                                     if (mod.startsWith(top + "/")) {
379                                         // #190: user asked to check out foo/bar foo/baz quux
380
// Our top-level dirs are "foo" and "quux".
381
// Do not add "foo" to checkout or we will check out foo/*!
382
continue SUBDIR;
383                                     }
384                                 }
385                                 moduleNames.add(top);
386                             }
387                         }
388                     }
389                     return moduleNames;
390                 }
391             }));
392
393             for (String JavaDoc moduleName : moduleNames) {
394                 // capture the output during update
395
ByteArrayOutputStream JavaDoc baos = new ByteArrayOutputStream JavaDoc();
396                 FilePath modulePath = new FilePath(workspace, moduleName);
397
398                 ArgumentListBuilder actualCmd = cmd;
399                 String JavaDoc baseName = moduleName;
400
401                 if(!modulePath.isDirectory()) {
402                     // updating just one file, like "foo/bar.txt".
403
// run update command from "foo" directory with "bar.txt" as the command line argument
404
actualCmd = cmd.clone();
405                     actualCmd.add(modulePath.getName());
406                     modulePath = modulePath.getParent();
407                     int slash = baseName.lastIndexOf('/');
408                     if (slash > 0) {
409                         baseName = baseName.substring(0, slash);
410                     }
411                 }
412
413                 if(!run(launcher,actualCmd,listener,
414                     modulePath,
415                     new ForkOutputStream(baos,listener.getLogger())))
416                     return null;
417
418                 // we'll run one "cvs log" command with workspace as the base,
419
// so use path names that are relative to moduleName.
420
parseUpdateOutput(baseName+'/',baos, changedFileNames);
421             }
422         }
423
424         return changedFileNames;
425     }
426
427     // see http://www.network-theory.co.uk/docs/cvsmanual/cvs_153.html for the output format.
428
// we don't care '?' because that's not in the repository
429
private static final Pattern JavaDoc UPDATE_LINE = Pattern.compile("[UPARMC] (.+)");
430
431     private static final Pattern JavaDoc REMOVAL_LINE = Pattern.compile("cvs (server|update): `?(.+?)'? is no longer in the repository");
432     //private static final Pattern NEWDIRECTORY_LINE = Pattern.compile("cvs server: New directory `(.+)' -- ignored");
433

434     /**
435      * Parses the output from CVS update and list up files that might have been changed.
436      *
437      * @param result
438      * list of file names whose changelog should be checked. This may include files
439      * that are no longer present. The path names are relative to the workspace,
440      * hence "String", not {@link File}.
441      */

442     private void parseUpdateOutput(String JavaDoc baseName, ByteArrayOutputStream JavaDoc output, List JavaDoc<String JavaDoc> result) throws IOException JavaDoc {
443         BufferedReader JavaDoc in = new BufferedReader JavaDoc(new InputStreamReader JavaDoc(
444             new ByteArrayInputStream JavaDoc(output.toByteArray())));
445         String JavaDoc line;
446         while((line=in.readLine())!=null) {
447             Matcher JavaDoc matcher = UPDATE_LINE.matcher(line);
448             if(matcher.matches()) {
449                 result.add(baseName+matcher.group(1));
450                 continue;
451             }
452
453             matcher= REMOVAL_LINE.matcher(line);
454             if(matcher.matches()) {
455                 result.add(baseName+matcher.group(2));
456                 continue;
457             }
458
459             // this line is added in an attempt to capture newly created directories in the repository,
460
// but it turns out that this line always hit if the workspace is missing a directory
461
// that the server has, even if that directory contains nothing in it
462
//matcher= NEWDIRECTORY_LINE.matcher(line);
463
//if(matcher.matches()) {
464
// result.add(baseName+matcher.group(1));
465
//}
466
}
467     }
468
469     /**
470      * Returns true if we can use "cvs update" instead of "cvs checkout"
471      */

472     private boolean isUpdatable(FilePath dir) throws IOException JavaDoc, InterruptedException JavaDoc {
473         return dir.act(new FileCallable<Boolean JavaDoc>() {
474             public Boolean JavaDoc invoke(File JavaDoc dir, VirtualChannel channel) throws IOException JavaDoc {
475                 if(flatten) {
476                     return isUpdatableModule(dir);
477                 } else {
478                     StringTokenizer JavaDoc tokens = new StringTokenizer JavaDoc(module);
479                     while(tokens.hasMoreTokens()) {
480                         File JavaDoc module = new File JavaDoc(dir,tokens.nextToken());
481                         if(!isUpdatableModule(module))
482                             return false;
483                     }
484                     return true;
485                 }
486             }
487         });
488     }
489
490     private boolean isUpdatableModule(File JavaDoc module) {
491         if(!module.isDirectory())
492             // module is a file, like "foo/bar.txt". Then CVS information is "foo/CVS".
493
module = module.getParentFile();
494
495         File JavaDoc cvs = new File JavaDoc(module,"CVS");
496         if(!cvs.exists())
497             return false;
498
499         // check cvsroot
500
if(!checkContents(new File JavaDoc(cvs,"Root"),cvsroot))
501             return false;
502         if(branch!=null) {
503             if(!checkContents(new File JavaDoc(cvs,"Tag"),'T'+branch))
504                 return false;
505         } else {
506             File JavaDoc tag = new File JavaDoc(cvs,"Tag");
507             if (tag.exists()) {
508                 try {
509                     Reader JavaDoc r = new FileReader JavaDoc(tag);
510                     try {
511                         String JavaDoc s = new BufferedReader JavaDoc(r).readLine();
512                         return s != null && s.startsWith("D");
513                     } finally {
514                         r.close();
515                     }
516                 } catch (IOException JavaDoc e) {
517                     return false;
518                 }
519             }
520         }
521
522         return true;
523     }
524
525     /**
526      * Returns true if the contents of the file is equal to the given string.
527      *
528      * @return false in all the other cases.
529      */

530     private boolean checkContents(File JavaDoc file, String JavaDoc contents) {
531         try {
532             Reader JavaDoc r = new FileReader JavaDoc(file);
533             try {
534                 String JavaDoc s = new BufferedReader JavaDoc(r).readLine();
535                 if (s == null) return false;
536                 return s.trim().equals(contents.trim());
537             } finally {
538                 r.close();
539             }
540         } catch (IOException JavaDoc e) {
541             return false;
542         }
543     }
544
545
546     /**
547      * Used to communicate the result of the detection in {@link CVSSCM#calcChangeLog(AbstractBuild, List, File, BuildListener)}
548      */

549     class ChangeLogResult implements Serializable JavaDoc {
550         boolean hadError;
551         String JavaDoc errorOutput;
552
553         public ChangeLogResult(boolean hadError, String JavaDoc errorOutput) {
554             this.hadError = hadError;
555             if(hadError)
556                 this.errorOutput = errorOutput;
557         }
558         private static final long serialVersionUID = 1L;
559     }
560
561     /**
562      * Used to propagate {@link BuildException} and error log at the same time.
563      */

564     class BuildExceptionWithLog extends RuntimeException JavaDoc {
565         final String JavaDoc errorOutput;
566
567         public BuildExceptionWithLog(BuildException cause, String JavaDoc errorOutput) {
568             super(cause);
569             this.errorOutput = errorOutput;
570         }
571
572         private static final long serialVersionUID = 1L;
573     }
574
575     /**
576      * Computes the changelog into an XML file.
577      *
578      * <p>
579      * When we update the workspace, we'll compute the changelog by using its output to
580      * make it faster. In general case, we'll fall back to the slower approach where
581      * we check all files in the workspace.
582      *
583      * @param changedFiles
584      * Files whose changelog should be checked for updates.
585      * This is provided if the previous operation is update, otherwise null,
586      * which means we have to fall back to the default slow computation.
587      */

588     private boolean calcChangeLog(AbstractBuild build, final List JavaDoc<String JavaDoc> changedFiles, File JavaDoc changelogFile, final BuildListener listener) throws InterruptedException JavaDoc {
589         if(build.getPreviousBuild()==null || (changedFiles!=null && changedFiles.isEmpty())) {
590             // nothing to compare against, or no changes
591
// (note that changedFiles==null means fallback, so we have to run cvs log.
592
listener.getLogger().println("$ no changes detected");
593             return createEmptyChangeLog(changelogFile,listener, "changelog");
594         }
595
596         listener.getLogger().println("$ computing changelog");
597
598         FilePath baseDir = build.getProject().getWorkspace();
599         final String JavaDoc cvspassFile = getDescriptor().getCvspassFile();
600         final String JavaDoc cvsExe = getDescriptor().getCvsExe();
601
602         try {
603             // range of time for detecting changes
604
final Date JavaDoc startTime = build.getPreviousBuild().getTimestamp().getTime();
605             final Date JavaDoc endTime = build.getTimestamp().getTime();
606             final OutputStream out = new RemoteOutputStream(new FileOutputStream JavaDoc(changelogFile));
607
608             ChangeLogResult result = baseDir.act(new FileCallable<ChangeLogResult>() {
609                 public ChangeLogResult invoke(File JavaDoc ws, VirtualChannel channel) throws IOException JavaDoc {
610                     final StringWriter JavaDoc errorOutput = new StringWriter JavaDoc();
611                     final boolean[] hadError = new boolean[1];
612
613                     ChangeLogTask task = new ChangeLogTask() {
614                         public void log(String JavaDoc msg, int msgLevel) {
615                             // send error to listener. This seems like the route in which the changelog task
616
// sends output
617
if(msgLevel==org.apache.tools.ant.Project.MSG_ERR) {
618                                 hadError[0] = true;
619                                 errorOutput.write(msg);
620                                 errorOutput.write('\n');
621                                 return;
622                             }
623                             if(debugLogging) {
624                                 listener.getLogger().println(msg);
625                             }
626                         }
627                     };
628                     task.setProject(new org.apache.tools.ant.Project());
629                     task.setCvsExe(cvsExe);
630                     task.setDir(ws);
631                     if(cvspassFile.length()!=0)
632                         task.setPassfile(new File JavaDoc(cvspassFile));
633                     task.setCvsRoot(cvsroot);
634                     task.setCvsRsh(cvsRsh);
635                     task.setFailOnError(true);
636                     task.setDeststream(new BufferedOutputStream JavaDoc(out));
637                     task.setBranch(branch);
638                     task.setStart(startTime);
639                     task.setEnd(endTime);
640                     if(changedFiles!=null) {
641                         // if the directory doesn't exist, cvs changelog will die, so filter them out.
642
// this means we'll lose the log of those changes
643
for (String JavaDoc filePath : changedFiles) {
644                             if(new File JavaDoc(ws,filePath).getParentFile().exists())
645                                 task.addFile(filePath);
646                         }
647                     } else {
648                         // fallback
649
if(!flatten)
650                             task.setPackage(module);
651                     }
652
653                     try {
654                         task.execute();
655                     } catch (BuildException e) {
656                         throw new BuildExceptionWithLog(e,errorOutput.toString());
657                     }
658
659                     return new ChangeLogResult(hadError[0],errorOutput.toString());
660                 }
661             });
662
663             if(result.hadError) {
664                 // non-fatal error must have occurred, such as cvs changelog parsing error.s
665
listener.getLogger().print(result.errorOutput);
666             }
667             return true;
668         } catch( BuildExceptionWithLog e ) {
669             // capture output from the task for diagnosis
670
listener.getLogger().print(e.errorOutput);
671             // then report an error
672
BuildException x = (BuildException) e.getCause();
673             PrintWriter JavaDoc w = listener.error(x.getMessage());
674             w.println("Working directory is "+baseDir);
675             x.printStackTrace(w);
676             return false;
677         } catch( RuntimeException JavaDoc e ) {
678             // an user reported a NPE inside the changeLog task.
679
// we don't want a bug in Ant to prevent a build.
680
e.printStackTrace(listener.error(e.getMessage()));
681             return true; // so record the message but continue
682
} catch( IOException JavaDoc e ) {
683             e.printStackTrace(listener.error("Failed to detect changlog"));
684             return true;
685         }
686     }
687
688     public DescriptorImpl getDescriptor() {
689         return DescriptorImpl.DESCRIPTOR;
690     }
691
692     public void buildEnvVars(Map JavaDoc<String JavaDoc,String JavaDoc> env) {
693         if(cvsRsh!=null)
694             env.put("CVS_RSH",cvsRsh);
695         String JavaDoc cvspass = getDescriptor().getCvspassFile();
696         if(cvspass.length()!=0)
697             env.put("CVS_PASSFILE",cvspass);
698     }
699
700     public static final class DescriptorImpl extends Descriptor<SCM> implements ModelObject {
701         static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
702
703         /**
704          * Path to <tt>.cvspass</tt>. Null to default.
705          */

706         private String JavaDoc cvsPassFile;
707
708         /**
709          * Path to cvs executable. Null to just use "cvs".
710          */

711         private String JavaDoc cvsExe;
712
713         /**
714          * Copy-on-write.
715          */

716         private volatile Map JavaDoc<String JavaDoc,RepositoryBrowser> browsers = new HashMap JavaDoc<String JavaDoc,RepositoryBrowser>();
717
718         class RepositoryBrowser {
719             String JavaDoc diffURL;
720             String JavaDoc browseURL;
721         }
722
723         DescriptorImpl() {
724             super(CVSSCM.class);
725             load();
726         }
727
728         protected void convert(Map JavaDoc<String JavaDoc, Object JavaDoc> oldPropertyBag) {
729             cvsPassFile = (String JavaDoc)oldPropertyBag.get("cvspass");
730         }
731
732         public String JavaDoc getDisplayName() {
733             return "CVS";
734         }
735
736         public SCM newInstance(StaplerRequest req) {
737             return new CVSSCM(
738                 req.getParameter("cvs_root"),
739                 req.getParameter("cvs_module"),
740                 req.getParameter("cvs_branch"),
741                 req.getParameter("cvs_rsh"),
742                 req.getParameter("cvs_use_update")!=null,
743                 req.getParameter("cvs_legacy")==null
744             );
745         }
746
747         public String JavaDoc getCvspassFile() {
748             String JavaDoc value = cvsPassFile;
749             if(value==null)
750                 value = "";
751             return value;
752         }
753
754         public String JavaDoc getCvsExe() {
755             if(cvsExe==null) return "cvs";
756             else return cvsExe;
757         }
758
759         public void setCvspassFile(String JavaDoc value) {
760             cvsPassFile = value;
761             save();
762         }
763
764         /**
765          * Gets the URL that shows the diff.
766          */

767         public String JavaDoc getDiffURL(String JavaDoc cvsRoot, String JavaDoc pathName, String JavaDoc oldRev, String JavaDoc newRev) {
768             RepositoryBrowser b = browsers.get(cvsRoot);
769             if(b==null) return null;
770             return b.diffURL.replaceAll("%%P",pathName).replace("%%r",oldRev).replace("%%R",newRev);
771
772         }
773
774         public boolean configure( StaplerRequest req ) {
775             cvsPassFile = fixEmpty(req.getParameter("cvs_cvspass").trim());
776             cvsExe = fixEmpty(req.getParameter("cvs_exe").trim());
777
778             Map JavaDoc<String JavaDoc,RepositoryBrowser> browsers = new HashMap JavaDoc<String JavaDoc, RepositoryBrowser>();
779             int i=0;
780             while(true) {
781                 String JavaDoc root = req.getParameter("cvs_repobrowser_cvsroot" + i);
782                 if(root==null) break;
783
784                 RepositoryBrowser rb = new RepositoryBrowser();
785                 rb.browseURL = req.getParameter("cvs_repobrowser"+i);
786                 rb.diffURL = req.getParameter("cvs_repobrowser_diff"+i);
787                 browsers.put(root,rb);
788
789                 i++;
790             }
791             this.browsers = browsers;
792
793             save();
794
795             return true;
796         }
797
798     //
799
// web methods
800
//
801

802         public void doCvsPassCheck(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
803             // this method can be used to check if a file exists anywhere in the file system,
804
// so it should be protected.
805
new FormFieldValidator(req,rsp,true) {
806                 protected void check() throws IOException JavaDoc, ServletException JavaDoc {
807                     String JavaDoc v = fixEmpty(request.getParameter("value"));
808                     if(v==null) {
809                         // default.
810
ok();
811                     } else {
812                         File JavaDoc cvsPassFile = new File JavaDoc(v);
813
814                         if(cvsPassFile.exists()) {
815                             ok();
816                         } else {
817                             error("No such file exists");
818                         }
819                     }
820                 }
821             }.process();
822         }
823
824         /**
825          * Checks if cvs executable exists.
826          */

827         public void doCvsExeCheck(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
828             // this method can be used to check if a file exists anywhere in the file system,
829
// so it should be protected.
830
new FormFieldValidator(req,rsp,true) {
831                 protected void check() throws IOException JavaDoc, ServletException JavaDoc {
832                     String JavaDoc cvsExe = fixEmpty(request.getParameter("value"));
833                     if(cvsExe==null) {
834                         ok();
835                         return;
836                     }
837
838                     if(cvsExe.indexOf(File.separatorChar)>=0) {
839                         // this is full path
840
if(new File JavaDoc(cvsExe).exists()) {
841                             ok();
842                         } else {
843                             error("There's no such file: "+cvsExe);
844                         }
845                     } else {
846                         // can't really check
847
ok();
848                     }
849
850                 }
851             }.process();
852         }
853
854         /**
855          * Displays "cvs --version" for trouble shooting.
856          */

857         public void doVersion(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
858             ByteBuffer baos = new ByteBuffer();
859             try {
860                 Proc proc = Hudson.getInstance().createLauncher(TaskListener.NULL).launch(
861                     new String JavaDoc[]{getCvsExe(), "--version"}, new String JavaDoc[0], baos, null);
862                 proc.join();
863                 rsp.setContentType("text/plain");
864                 baos.writeTo(rsp.getOutputStream());
865             } catch (IOException JavaDoc e) {
866                 req.setAttribute("error",e);
867                 rsp.forward(this,"versionCheckError",req);
868             }
869         }
870
871         /**
872          * Checks the entry to the CVSROOT field.
873          * <p>
874          * Also checks if .cvspass file contains the entry for this.
875          */

876         public void doCvsrootCheck(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
877             new FormFieldValidator(req,rsp,false) {
878                 protected void check() throws IOException JavaDoc, ServletException JavaDoc {
879                     String JavaDoc v = fixEmpty(request.getParameter("value"));
880                     if(v==null) {
881                         error("CVSROOT is mandatory");
882                         return;
883                     }
884
885                     Matcher JavaDoc m = CVSROOT_PSERVER_PATTERN.matcher(v);
886
887                     // CVSROOT format isn't really that well defined. So it's hard to check this rigorously.
888
if(v.startsWith(":pserver") || v.startsWith(":ext")) {
889                         if(!m.matches()) {
890                             error("Invalid CVSROOT string");
891                             return;
892                         }
893                         // I can't really test if the machine name exists, either.
894
// some cvs, such as SOCKS-enabled cvs can resolve host names that Hudson might not
895
// be able to. If :ext is used, all bets are off anyway.
896
}
897
898                     // check .cvspass file to see if it has entry.
899
// CVS handles authentication only if it's pserver.
900
if(v.startsWith(":pserver")) {
901                         if(m.group(2)==null) {// if password is not specified in CVSROOT
902
String JavaDoc cvspass = getCvspassFile();
903                             File JavaDoc passfile;
904                             if(cvspass.equals("")) {
905                                 passfile = new File JavaDoc(new File JavaDoc(System.getProperty("user.home")),".cvspass");
906                             } else {
907                                 passfile = new File JavaDoc(cvspass);
908                             }
909
910                             if(passfile.exists()) {
911                                 // It's possible that we failed to locate the correct .cvspass file location,
912
// so don't report an error if we couldn't locate this file.
913
//
914
// if this is explicitly specified, then our system config page should have
915
// reported an error.
916
if(!scanCvsPassFile(passfile, v)) {
917                                     error("It doesn't look like this CVSROOT has its password set." +
918                                         " Would you like to set it now?");
919                                     return;
920                                 }
921                             }
922                         }
923                     }
924
925                     // all tests passed so far
926
ok();
927                 }
928             }.process();
929         }
930
931         /**
932          * Checks if the given pserver CVSROOT value exists in the pass file.
933          */

934         private boolean scanCvsPassFile(File JavaDoc passfile, String JavaDoc cvsroot) throws IOException JavaDoc {
935             cvsroot += ' ';
936             String JavaDoc cvsroot2 = "/1 "+cvsroot; // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5006835
937
BufferedReader JavaDoc in = new BufferedReader JavaDoc(new FileReader JavaDoc(passfile));
938             try {
939                 String JavaDoc line;
940                 while((line=in.readLine())!=null) {
941                     // "/1 " version always have the port number in it, so examine a much with
942
// default port 2401 left out
943
int portIndex = line.indexOf(":2401/");
944                     String JavaDoc line2 = "";
945                     if(portIndex>=0)
946                         line2 = line.substring(0,portIndex+1)+line.substring(portIndex+5); // leave '/'
947

948                     if(line.startsWith(cvsroot) || line.startsWith(cvsroot2) || line2.startsWith(cvsroot2))
949                         return true;
950                 }
951                 return false;
952             } finally {
953                 in.close();
954             }
955         }
956
957         private static final Pattern JavaDoc CVSROOT_PSERVER_PATTERN =
958             Pattern.compile(":(ext|pserver):[^@:]+(:[^@:]+)?@[^:]+:(\\d+:)?.+");
959
960         /**
961          * Runs cvs login command.
962          *
963          * TODO: this apparently doesn't work. Probably related to the fact that
964          * cvs does some tty magic to disable echo back or whatever.
965          */

966         public void doPostPassword(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc {
967             if(!Hudson.adminCheck(req,rsp))
968                 return;
969
970             String JavaDoc cvsroot = req.getParameter("cvsroot");
971             String JavaDoc password = req.getParameter("password");
972
973             if(cvsroot==null || password==null) {
974                 rsp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
975                 return;
976             }
977
978             rsp.setContentType("text/plain");
979             Proc proc = Hudson.getInstance().createLauncher(TaskListener.NULL).launch(
980                 new String JavaDoc[]{getCvsExe(), "-d",cvsroot,"login"}, new String JavaDoc[0],
981                 new ByteArrayInputStream JavaDoc((password+"\n").getBytes()),
982                 rsp.getOutputStream());
983             proc.join();
984         }
985     }
986
987     /**
988      * Action for a build that performs the tagging.
989      */

990     public final class TagAction extends AbstractModelObject implements Action {
991         private final AbstractBuild build;
992
993         /**
994          * If non-null, that means the build is already tagged.
995          */

996         private volatile String JavaDoc tagName;
997
998         /**
999          * If non-null, that means the tagging is in progress
1000         * (asynchronously.)
1001         */

1002        private transient volatile TagWorkerThread workerThread;
1003
1004        /**
1005         * Hold the log of "cvs tag" operation.
1006         */

1007        private transient WeakReference JavaDoc<LargeText> log;
1008
1009        public TagAction(AbstractBuild build) {
1010            this.build = build;
1011        }
1012
1013        public String JavaDoc getIconFileName() {
1014            return "save.gif";
1015        }
1016
1017        public String JavaDoc getDisplayName() {
1018            return "Tag this build";
1019        }
1020
1021        public String JavaDoc getUrlName() {
1022            return "tagBuild";
1023        }
1024
1025        public String JavaDoc getTagName() {
1026            return tagName;
1027        }
1028
1029        public TagWorkerThread getWorkerThread() {
1030            return workerThread;
1031        }
1032
1033        public AbstractBuild getBuild() {
1034            return build;
1035        }
1036
1037        public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
1038            req.setAttribute("build",build);
1039            req.getView(this,chooseAction()).forward(req,rsp);
1040        }
1041
1042        private synchronized String JavaDoc chooseAction() {
1043            if(tagName!=null)
1044                return "alreadyTagged.jelly";
1045            if(workerThread!=null)
1046                return "inProgress.jelly";
1047            return "tagForm.jelly";
1048        }
1049
1050        /**
1051         * Invoked to actually tag the workspace.
1052         */

1053        public synchronized void doSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
1054            Map JavaDoc<AbstractBuild,String JavaDoc> tagSet = new HashMap JavaDoc<AbstractBuild,String JavaDoc>();
1055
1056            String JavaDoc name = req.getParameter("name");
1057            if(isInvalidTag(name)) {
1058                sendError("No valid tag name given",req,rsp);
1059                return;
1060            }
1061
1062            tagSet.put(build,name);
1063
1064            if(req.getParameter("upstream")!=null) {
1065                // tag all upstream builds
1066
Enumeration JavaDoc e = req.getParameterNames();
1067                Map JavaDoc<AbstractProject, Integer JavaDoc> upstreams = build.getUpstreamBuilds(); // TODO: define them at AbstractBuild level
1068

1069                while(e.hasMoreElements()) {
1070                    String JavaDoc upName = (String JavaDoc) e.nextElement();
1071                    if(!upName.startsWith("upstream."))
1072                        continue;
1073
1074                    String JavaDoc tag = req.getParameter(upName);
1075                    if(isInvalidTag(tag)) {
1076                        sendError("No valid tag name given for "+upName,req,rsp);
1077                        return;
1078                    }
1079
1080                    upName = upName.substring(9); // trim off 'upstream.'
1081
Job p = Hudson.getInstance().getItemByFullName(upName,Job.class);
1082
1083                    Run build = p.getBuildByNumber(upstreams.get(p));
1084                    tagSet.put((AbstractBuild) build,tag);
1085                }
1086            }
1087
1088            new TagWorkerThread(tagSet).start();
1089
1090            doIndex(req,rsp);
1091        }
1092
1093        private boolean isInvalidTag(String JavaDoc name) {
1094            return name==null || name.length()==0;
1095        }
1096
1097        /**
1098         * Clears the error status.
1099         */

1100        public synchronized void doClearError(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
1101            if(workerThread!=null && !workerThread.isAlive())
1102                workerThread = null;
1103            doIndex(req,rsp);
1104        }
1105
1106        /**
1107         * Handles incremental log output.
1108         */

1109        public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc {
1110            if(log==null) {
1111                rsp.setStatus(HttpServletResponse.SC_OK);
1112            } else {
1113                LargeText text = log.get();
1114                if(text!=null)
1115                    text.doProgressText(req,rsp);
1116                else
1117                    rsp.setStatus(HttpServletResponse.SC_OK);
1118            }
1119        }
1120
1121        /**
1122         * Performs tagging.
1123         */

1124        public void perform(String JavaDoc tagName, TaskListener listener) {
1125            File JavaDoc destdir = null;
1126            try {
1127                destdir = Util.createTempDir();
1128
1129                // unzip the archive
1130
listener.getLogger().println("expanding the workspace archive into "+destdir);
1131                Expand e = new Expand();
1132                e.setProject(new org.apache.tools.ant.Project());
1133                e.setDest(destdir);
1134                e.setSrc(getArchiveFile(build));
1135                e.setTaskType("unzip");
1136                e.execute();
1137
1138                // run cvs tag command
1139
listener.getLogger().println("tagging the workspace");
1140                StringTokenizer JavaDoc tokens = new StringTokenizer JavaDoc(CVSSCM.this.module);
1141                while(tokens.hasMoreTokens()) {
1142                    String JavaDoc m = tokens.nextToken();
1143                    FilePath path = new FilePath(destdir).child(m);
1144                    boolean isDir = path.isDirectory();
1145
1146                    ArgumentListBuilder cmd = new ArgumentListBuilder();
1147                    cmd.add(getDescriptor().getCvsExe(),"tag");
1148                    if(isDir) {
1149                        cmd.add("-R");
1150                    }
1151                    cmd.add(tagName);
1152                    if(!isDir) {
1153                        cmd.add(path.getName());
1154                        path = path.getParent();
1155                    }
1156
1157                    if(!CVSSCM.this.run(new Launcher.LocalLauncher(listener),cmd,listener, path)) {
1158                        listener.getLogger().println("tagging failed");
1159                        return;
1160                    }
1161                }
1162
1163                // completed successfully
1164
onTagCompleted(tagName);
1165                build.save();
1166
1167            } catch (Throwable JavaDoc e) {
1168                e.printStackTrace(listener.fatalError(e.getMessage()));
1169            } finally {
1170                try {
1171                    if(destdir!=null) {
1172                        listener.getLogger().println("cleaning up "+destdir);
1173                        Util.deleteRecursive(destdir);
1174                    }
1175                } catch (IOException JavaDoc e) {
1176                    e.printStackTrace(listener.fatalError(e.getMessage()));
1177                }
1178            }
1179        }
1180
1181        /**
1182         * Atomically set the tag name and then be done with {@link TagWorkerThread}.
1183         */

1184        private synchronized void onTagCompleted(String JavaDoc tagName) {
1185            this.tagName = tagName;
1186            this.workerThread = null;
1187        }
1188    }
1189
1190    public static final class TagWorkerThread extends Thread JavaDoc {
1191        // StringWriter is synchronized
1192
private final ByteBuffer log = new ByteBuffer();
1193        private final LargeText text = new LargeText(log,false);
1194        private final Map JavaDoc<AbstractBuild,String JavaDoc> tagSet;
1195
1196        public TagWorkerThread(Map JavaDoc<AbstractBuild,String JavaDoc> tagSet) {
1197            this.tagSet = tagSet;
1198        }
1199
1200        public String JavaDoc getLog() {
1201            // this method can be invoked from another thread.
1202
return log.toString();
1203        }
1204
1205        public synchronized void start() {
1206            for (Entry<AbstractBuild, String JavaDoc> e : tagSet.entrySet()) {
1207                TagAction ta = e.getKey().getAction(TagAction.class);
1208                if(ta!=null) {
1209                    ta.workerThread = this;
1210                    ta.log = new WeakReference JavaDoc<LargeText>(text);
1211                }
1212            }
1213
1214            super.start();
1215        }
1216
1217        public void run() {
1218            TaskListener listener = new StreamTaskListener(log);
1219
1220            for (Entry<AbstractBuild, String JavaDoc> e : tagSet.entrySet()) {
1221                TagAction ta = e.getKey().getAction(TagAction.class);
1222                if(ta==null) {
1223                    listener.error(e.getKey()+" doesn't have CVS tag associated with it. Skipping");
1224                    continue;
1225                }
1226                listener.getLogger().println("Tagging "+e.getKey()+" to "+e.getValue());
1227                try {
1228                    e.getKey().keepLog();
1229                } catch (IOException JavaDoc x) {
1230                    x.printStackTrace(listener.error("Failed to mark "+e.getKey()+" for keep"));
1231                }
1232                ta.perform(e.getValue(), listener);
1233                listener.getLogger().println();
1234            }
1235
1236            listener.getLogger().println("Completed");
1237            text.markAsComplete();
1238        }
1239    }
1240
1241    /**
1242     * Temporary hack for assisting trouble-shooting.
1243     *
1244     * <p>
1245     * Setting this property to true would cause <tt>cvs log</tt> to dump a lot of messages.
1246     */

1247    public static boolean debugLogging = false;
1248
1249    private static final long serialVersionUID = 1L;
1250}
1251
Popular Tags