CVSSCM.java 44.6 KB
Newer Older
K
kohsuke 已提交
1 2 3
package hudson.scm;

import hudson.FilePath;
K
kohsuke 已提交
4
import hudson.FilePath.FileCallable;
K
kohsuke 已提交
5 6 7 8
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import static hudson.Util.fixEmpty;
9
import hudson.model.AbstractBuild;
10
import hudson.model.AbstractModelObject;
11
import hudson.model.AbstractProject;
K
kohsuke 已提交
12 13 14 15
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Hudson;
16 17
import hudson.model.Job;
import hudson.model.LargeText;
K
kohsuke 已提交
18
import hudson.model.ModelObject;
K
kohsuke 已提交
19
import hudson.model.Run;
20
import hudson.model.TaskListener;
K
kohsuke 已提交
21
import hudson.org.apache.tools.ant.taskdefs.cvslib.ChangeLogTask;
K
kohsuke 已提交
22 23
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
K
kohsuke 已提交
24
import hudson.util.ArgumentListBuilder;
25
import hudson.util.ByteBuffer;
K
kohsuke 已提交
26 27
import hudson.util.ForkOutputStream;
import hudson.util.FormFieldValidator;
K
kohsuke 已提交
28
import hudson.util.StreamTaskListener;
K
kohsuke 已提交
29 30 31 32 33 34
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;
K
kohsuke 已提交
35

K
kohsuke 已提交
36
import javax.servlet.ServletException;
37
import javax.servlet.ServletOutputStream;
K
kohsuke 已提交
38
import javax.servlet.http.HttpServletResponse;
K
kohsuke 已提交
39
import java.io.BufferedOutputStream;
K
kohsuke 已提交
40 41 42 43 44
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
K
kohsuke 已提交
45
import java.io.FileOutputStream;
K
kohsuke 已提交
46 47 48
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
K
kohsuke 已提交
49 50
import java.io.OutputStream;
import java.io.PrintWriter;
K
kohsuke 已提交
51
import java.io.Reader;
K
kohsuke 已提交
52
import java.io.Serializable;
K
kohsuke 已提交
53
import java.io.StringWriter;
54
import java.lang.ref.WeakReference;
55
import java.text.DateFormat;
K
kohsuke 已提交
56
import java.util.ArrayList;
K
kohsuke 已提交
57
import java.util.Collections;
58
import java.util.Date;
59
import java.util.Enumeration;
K
kohsuke 已提交
60 61
import java.util.HashMap;
import java.util.HashSet;
K
kohsuke 已提交
62
import java.util.List;
K
kohsuke 已提交
63
import java.util.Locale;
K
kohsuke 已提交
64
import java.util.Map;
65
import java.util.Map.Entry;
K
kohsuke 已提交
66
import java.util.Set;
K
kohsuke 已提交
67
import java.util.StringTokenizer;
68
import java.util.TimeZone;
K
kohsuke 已提交
69
import java.util.TreeSet;
K
kohsuke 已提交
70 71 72 73 74 75 76 77 78 79
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * CVS.
 *
 * <p>
 * I couldn't call this class "CVS" because that would cause the view folder name
 * to collide with CVS control files.
 *
K
kohsuke 已提交
80 81 82 83
 * <p>
 * This object gets shipped to the remote machine to perform some of the work,
 * so it implements {@link Serializable}.
 *
K
kohsuke 已提交
84 85
 * @author Kohsuke Kawaguchi
 */
K
kohsuke 已提交
86
public class CVSSCM extends AbstractCVSFamilySCM implements Serializable {
K
kohsuke 已提交
87 88 89 90 91 92 93 94 95
    /**
     * CVSSCM connection string.
     */
    private String cvsroot;

    /**
     * Module names.
     *
     * This could be a whitespace-separate list of multiple modules.
K
kohsuke 已提交
96
     * Modules could be either directories or files. 
K
kohsuke 已提交
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
     */
    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;
    }

K
kohsuke 已提交
147 148 149
    /**
     * Branch to build. Null to indicate the trunk.
     */
K
kohsuke 已提交
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
    public String getBranch() {
        return branch;
    }

    public String getCvsRsh() {
        return cvsRsh;
    }

    public boolean getCanUseUpdate() {
        return canUseUpdate;
    }

    public boolean isFlatten() {
        return flatten;
    }

166
    public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath dir, TaskListener listener) throws IOException, InterruptedException {
167
        List<String> changedFiles = update(true, launcher, dir, listener, new Date());
K
kohsuke 已提交
168 169 170 171

        return changedFiles!=null && !changedFiles.isEmpty();
    }

172
    private void configureDate(ArgumentListBuilder cmd, Date date) { // #192
173 174 175
        DateFormat df = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.US);
        df.setTimeZone(TimeZone.getTimeZone("UTC")); // #209
        cmd.add("-D", df.format(date));
176 177
    }

178
    public boolean checkout(AbstractBuild build, Launcher launcher, FilePath dir, BuildListener listener, File changelogFile) throws IOException, InterruptedException {
K
kohsuke 已提交
179 180
        List<String> changedFiles = null; // files that were affected by update. null this is a check out

K
kohsuke 已提交
181
        if(canUseUpdate && isUpdatable(dir)) {
182
            changedFiles = update(false, launcher, dir, listener, build.getTimestamp().getTime());
K
kohsuke 已提交
183 184 185 186 187 188
            if(changedFiles==null)
                return false;   // failed
        } else {
            dir.deleteContents();

            ArgumentListBuilder cmd = new ArgumentListBuilder();
189
            cmd.add(getDescriptor().getCvsExe(),debugLogging?"-t":"-Q","-z9","-d",cvsroot,"co");
K
kohsuke 已提交
190 191 192 193
            if(branch!=null)
                cmd.add("-r",branch);
            if(flatten)
                cmd.add("-d",dir.getName());
194
            configureDate(cmd, build.getTimestamp().getTime());
K
kohsuke 已提交
195 196 197 198 199 200 201 202
            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);
K
kohsuke 已提交
203 204 205 206 207 208 209 210 211 212 213 214 215 216
        final OutputStream os = new RemoteOutputStream(new FileOutputStream(archiveFile));
        
        build.getProject().getWorkspace().act(new FileCallable<Void>() {
            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);

K
kohsuke 已提交
217 218 219 220 221
                        if(!mf.exists())
                            // directory doesn't exist. This happens if a directory that was checked out
                            // didn't include any file.
                            continue;

K
kohsuke 已提交
222 223 224
                        if(!mf.isDirectory()) {
                            // this module is just a file, say "foo/bar.txt".
                            // to record "foo/CVS/*", we need to start by archiving "foo".
225 226 227 228
                            int idx = m.lastIndexOf('/');
                            if(idx==-1)
                                throw new Error("Kohsuke probe: m="+m);
                            m = m.substring(0, idx);
K
kohsuke 已提交
229 230 231 232
                            mf = mf.getParentFile();
                        }
                        archive(mf,m,zos);
                    }
K
kohsuke 已提交
233
                }
K
kohsuke 已提交
234 235
                zos.close();
                return null;
K
kohsuke 已提交
236
            }
K
kohsuke 已提交
237
        });
K
kohsuke 已提交
238 239 240 241 242 243 244 245 246 247

        // 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.
     */
248
    private static File getArchiveFile(AbstractBuild build) {
K
kohsuke 已提交
249 250 251
        return new File(build.getRootDir(),"workspace.zip");
    }

K
kohsuke 已提交
252 253 254 255 256 257
    /**
     * Archives all the CVS-controlled files in {@code dir}.
     *
     * @param relPath
     *      The path name in ZIP to store this directory with.
     */
K
kohsuke 已提交
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
    private void archive(File dir,String relPath,ZipOutputStream zos) throws IOException {
        Set<String> knownFiles = new HashSet<String>();
        // 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<String> 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.
     */
K
kohsuke 已提交
317
    private List<String> update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener, Date date) throws IOException, InterruptedException {
K
kohsuke 已提交
318 319 320 321

        List<String> changedFileNames = new ArrayList<String>();    // file names relative to the workspace

        ArgumentListBuilder cmd = new ArgumentListBuilder();
322
        cmd.add(getDescriptor().getCvsExe(),"-q","-z9");
K
kohsuke 已提交
323 324 325
        if(dryRun)
            cmd.add("-n");
        cmd.add("update","-PdC");
326 327 328 329
        if (branch != null) {
            cmd.add("-r", branch);
        }
        configureDate(cmd, date);
K
kohsuke 已提交
330 331 332 333 334 335 336 337 338 339

        if(flatten) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            if(!run(launcher,cmd,listener,workspace,
                new ForkOutputStream(baos,listener.getLogger())))
                return null;

            parseUpdateOutput("",baos, changedFileNames);
        } else {
J
jglick 已提交
340
            @SuppressWarnings("unchecked") // StringTokenizer oddly has the wrong type
K
kohsuke 已提交
341 342
            final Set<String> moduleNames = new TreeSet(Collections.list(new StringTokenizer(module)));

343
            // Add in any existing CVS dirs, in case project checked out its own.
K
kohsuke 已提交
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
            moduleNames.addAll(workspace.act(new FileCallable<Set<String>>() {
                public Set<String> 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);
J
jglick 已提交
360 361
                            }
                        }
362
                    }
K
kohsuke 已提交
363
                    return moduleNames;
364
                }
K
kohsuke 已提交
365 366
            }));

367
            for (String moduleName : moduleNames) {
K
kohsuke 已提交
368 369
                // capture the output during update
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
K
kohsuke 已提交
370 371 372 373 374 375 376 377 378 379 380 381 382
                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('/'));
                }
K
kohsuke 已提交
383

K
kohsuke 已提交
384 385
                if(!run(launcher,actualCmd,listener,
                    modulePath,
K
kohsuke 已提交
386 387 388 389 390
                    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.
K
kohsuke 已提交
391
                parseUpdateOutput(baseName+'/',baos, changedFileNames);
K
kohsuke 已提交
392 393 394 395 396 397 398 399 400 401
            }
        }

        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] (.+)");

402 403
    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");
K
kohsuke 已提交
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442

    /**
     * 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<String> 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"
     */
K
kohsuke 已提交
443 444 445 446 447 448 449 450 451 452 453 454 455 456
    private boolean isUpdatable(FilePath dir) throws IOException, InterruptedException {
        return dir.act(new FileCallable<Boolean>() {
            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;
                }
K
kohsuke 已提交
457
            }
K
kohsuke 已提交
458
        });
K
kohsuke 已提交
459 460 461
    }

    private boolean isUpdatableModule(File module) {
K
kohsuke 已提交
462 463 464 465
        if(!module.isDirectory())
            // module is a file, like "foo/bar.txt". Then CVS information is "foo/CVS".
            module = module.getParentFile();

K
kohsuke 已提交
466 467 468 469 470 471 472 473 474 475 476
        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 {
477 478 479 480 481 482
            File tag = new File(cvs,"Tag");
            if (tag.exists()) {
                try {
                    Reader r = new FileReader(tag);
                    try {
                        String s = new BufferedReader(r).readLine();
K
kohsuke 已提交
483
                        return s != null && s.startsWith("D");
484 485 486 487 488 489 490
                    } finally {
                        r.close();
                    }
                } catch (IOException e) {
                    return false;
                }
            }
K
kohsuke 已提交
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
        }

        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;
        }
    }

K
kohsuke 已提交
516 517

    /**
518
     * Used to communicate the result of the detection in {@link CVSSCM#calcChangeLog(AbstractBuild, List, File, BuildListener)}
K
kohsuke 已提交
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
     */
    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;
    }

K
kohsuke 已提交
546 547 548 549 550 551 552 553 554 555 556 557 558
    /**
     * Computes the changelog into an XML file.
     *
     * <p>
     * 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.
     */
559
    private boolean calcChangeLog(AbstractBuild build, final List<String> changedFiles, File changelogFile, final BuildListener listener) throws InterruptedException {
K
kohsuke 已提交
560 561 562 563 564 565 566 567 568
        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");

K
kohsuke 已提交
569 570
        FilePath baseDir = build.getProject().getWorkspace();
       final String cvspassFile = getDescriptor().getCvspassFile();
K
kohsuke 已提交
571 572

        try {
K
kohsuke 已提交
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632
            // 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<ChangeLogResult>() {
                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) {
K
kohsuke 已提交
633
                // non-fatal error must have occurred, such as cvs changelog parsing error.s
K
kohsuke 已提交
634
                listener.getLogger().print(result.errorOutput);
K
kohsuke 已提交
635 636
            }
            return true;
K
kohsuke 已提交
637
        } catch( BuildExceptionWithLog e ) {
K
kohsuke 已提交
638
            // capture output from the task for diagnosis
K
kohsuke 已提交
639
            listener.getLogger().print(e.errorOutput);
K
kohsuke 已提交
640
            // then report an error
K
kohsuke 已提交
641 642
            BuildException x = (BuildException) e.getCause();
            PrintWriter w = listener.error(x.getMessage());
K
kohsuke 已提交
643
            w.println("Working directory is "+baseDir);
K
kohsuke 已提交
644
            x.printStackTrace(w);
K
kohsuke 已提交
645 646 647 648 649 650
            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
K
kohsuke 已提交
651 652 653
        } catch( IOException e ) {
            e.printStackTrace(listener.error("Failed to detect changlog"));
            return true;
K
kohsuke 已提交
654 655 656 657
        }
    }

    public DescriptorImpl getDescriptor() {
K
kohsuke 已提交
658
        return DescriptorImpl.DESCRIPTOR;
K
kohsuke 已提交
659 660
    }

K
kohsuke 已提交
661
    public void buildEnvVars(Map<String,String> env) {
K
kohsuke 已提交
662 663
        if(cvsRsh!=null)
            env.put("CVS_RSH",cvsRsh);
K
kohsuke 已提交
664
        String cvspass = getDescriptor().getCvspassFile();
K
kohsuke 已提交
665 666 667 668 669
        if(cvspass.length()!=0)
            env.put("CVS_PASSFILE",cvspass);
    }

    public static final class DescriptorImpl extends Descriptor<SCM> implements ModelObject {
K
kohsuke 已提交
670 671
        static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

672 673 674 675 676
        /**
         * Path to <tt>.cvspass</tt>. Null to default.
         */
        private String cvsPassFile;

677 678 679 680 681
        /**
         * Path to cvs executable. Null to just use "cvs".
         */
        private String cvsExe;

682 683 684 685 686 687 688 689 690 691
        /**
         * Copy-on-write.
         */
        private volatile Map<String,RepositoryBrowser> browsers = new HashMap<String,RepositoryBrowser>();

        class RepositoryBrowser {
            String diffURL;
            String browseURL;
        }

K
kohsuke 已提交
692 693
        DescriptorImpl() {
            super(CVSSCM.class);
694
            load();
K
kohsuke 已提交
695 696
        }

697 698 699 700
        protected void convert(Map<String, Object> oldPropertyBag) {
            cvsPassFile = (String)oldPropertyBag.get("cvspass");
        }

K
kohsuke 已提交
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716
        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() {
717
            String value = cvsPassFile;
K
kohsuke 已提交
718 719 720 721 722
            if(value==null)
                value = "";
            return value;
        }

723 724 725 726 727
        public String getCvsExe() {
            if(cvsExe==null)    return "cvs";
            else                return cvsExe;
        }

K
kohsuke 已提交
728
        public void setCvspassFile(String value) {
729
            cvsPassFile = value;
K
kohsuke 已提交
730 731 732 733 734 735 736
            save();
        }

        /**
         * Gets the URL that shows the diff.
         */
        public String getDiffURL(String cvsRoot, String pathName, String oldRev, String newRev) {
737 738 739
            RepositoryBrowser b = browsers.get(cvsRoot);
            if(b==null)   return null;
            return b.diffURL.replaceAll("%%P",pathName).replace("%%r",oldRev).replace("%%R",newRev);
K
kohsuke 已提交
740 741 742

        }

743
        public boolean configure( StaplerRequest req ) {
744 745
            cvsPassFile = fixEmpty(req.getParameter("cvs_cvspass").trim());
            cvsExe = fixEmpty(req.getParameter("cvs_exe").trim());
K
kohsuke 已提交
746

747
            Map<String,RepositoryBrowser> browsers = new HashMap<String, RepositoryBrowser>();
K
kohsuke 已提交
748 749 750 751 752
            int i=0;
            while(true) {
                String root = req.getParameter("cvs_repobrowser_cvsroot" + i);
                if(root==null)  break;

753 754 755 756 757
                RepositoryBrowser rb = new RepositoryBrowser();
                rb.browseURL = req.getParameter("cvs_repobrowser"+i);
                rb.diffURL = req.getParameter("cvs_repobrowser_diff"+i);
                browsers.put(root,rb);

K
kohsuke 已提交
758 759
                i++;
            }
760
            this.browsers = browsers;
K
kohsuke 已提交
761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792

            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();
        }

793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822
        /**
         * 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();
        }

K
kohsuke 已提交
823 824 825 826 827
        /**
         * Displays "cvs --version" for trouble shooting.
         */
        public void doVersion(StaplerRequest req, StaplerResponse rsp) throws IOException {
            rsp.setContentType("text/plain");
828 829 830 831 832 833 834 835 836 837 838 839
            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);
K
kohsuke 已提交
840
                w.close();
841
            }
K
kohsuke 已提交
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
        }

        /**
         * Checks the entry to the CVSROOT field.
         * <p>
         * 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
K
kohsuke 已提交
933
         * cvs does some tty magic to disable echo back or whatever.
K
kohsuke 已提交
934 935 936 937 938 939 940 941 942 943 944 945 946 947 948
         */
        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(
949
                new String[]{getCvsExe(), "-d",cvsroot,"login"}, new String[0],
K
kohsuke 已提交
950 951 952 953 954 955 956 957 958
                new ByteArrayInputStream((password+"\n").getBytes()),
                rsp.getOutputStream());
            proc.join();
        }
    }

    /**
     * Action for a build that performs the tagging.
     */
K
kohsuke 已提交
959
    public final class TagAction extends AbstractModelObject implements Action {
960
        private final AbstractBuild build;
K
kohsuke 已提交
961 962 963 964

        /**
         * If non-null, that means the build is already tagged.
         */
K
kohsuke 已提交
965
        private volatile String tagName;
K
kohsuke 已提交
966 967 968 969 970

        /**
         * If non-null, that means the tagging is in progress
         * (asynchronously.)
         */
K
kohsuke 已提交
971
        private transient volatile TagWorkerThread workerThread;
K
kohsuke 已提交
972

K
kohsuke 已提交
973 974 975 976 977
        /**
         * Hold the log of "cvs tag" operation.
         */
        private transient WeakReference<LargeText> log;

978
        public TagAction(AbstractBuild build) {
K
kohsuke 已提交
979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001
            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;
        }

1002
        public AbstractBuild getBuild() {
K
kohsuke 已提交
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
            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 {
K
kohsuke 已提交
1023 1024
            Map<AbstractBuild,String> tagSet = new HashMap<AbstractBuild,String>();

K
kohsuke 已提交
1025
            String name = req.getParameter("name");
K
kohsuke 已提交
1026 1027
            if(isInvalidTag(name)) {
                sendError("No valid tag name given",req,rsp);
K
kohsuke 已提交
1028 1029 1030
                return;
            }

K
kohsuke 已提交
1031 1032 1033 1034 1035
            tagSet.put(build,name);

            if(req.getParameter("upstream")!=null) {
                // tag all upstream builds
                Enumeration e = req.getParameterNames();
1036
                Map<AbstractProject, Integer> upstreams = build.getUpstreamBuilds(); // TODO: define them at AbstractBuild level
K
kohsuke 已提交
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049

                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.'
1050
                    Job p = Hudson.getInstance().getItemByFullName(upName,Job.class);
K
kohsuke 已提交
1051 1052 1053 1054

                    Run build = p.getBuildByNumber(upstreams.get(p));
                    tagSet.put((AbstractBuild) build,tag);
                }
K
kohsuke 已提交
1055 1056
            }

K
kohsuke 已提交
1057 1058
            new TagWorkerThread(tagSet).start();

K
kohsuke 已提交
1059 1060 1061
            doIndex(req,rsp);
        }

K
kohsuke 已提交
1062 1063 1064 1065
        private boolean isInvalidTag(String name) {
            return name==null || name.length()==0;
        }

K
kohsuke 已提交
1066 1067 1068 1069 1070 1071 1072 1073 1074
        /**
         * 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);
        }

K
kohsuke 已提交
1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089
        /**
         * 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);
            }
        }

K
kohsuke 已提交
1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115
        /**
         * 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();
1116
                    cmd.add(getDescriptor().getCvsExe(),"tag");
K
kohsuke 已提交
1117 1118 1119 1120 1121 1122 1123 1124
                    if(isDir) {
                        cmd.add("-R");
                    }
                    cmd.add(tagName);
                    if(!isDir) {
                        cmd.add(path.getName());
                        path = path.getParent();
                    }
K
kohsuke 已提交
1125

K
kohsuke 已提交
1126 1127 1128 1129 1130
                    if(!CVSSCM.this.run(new Launcher.LocalLauncher(listener),cmd,listener, path)) {
                        listener.getLogger().println("tagging failed");
                        return;
                    }
                }
K
kohsuke 已提交
1131

K
kohsuke 已提交
1132 1133 1134
                // completed successfully
                onTagCompleted(tagName);
                build.save();
K
kohsuke 已提交
1135

K
kohsuke 已提交
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146
            } 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()));
                }
K
kohsuke 已提交
1147
            }
K
kohsuke 已提交
1148
        }
K
kohsuke 已提交
1149

K
kohsuke 已提交
1150 1151 1152 1153 1154 1155 1156 1157
        /**
         * Atomically set the tag name and then be done with {@link TagWorkerThread}.
         */
        private synchronized void onTagCompleted(String tagName) {
            this.tagName = tagName;
            this.workerThread = null;
        }
    }
K
kohsuke 已提交
1158

K
kohsuke 已提交
1159 1160
    public static final class TagWorkerThread extends Thread {
        // StringWriter is synchronized
K
kohsuke 已提交
1161 1162
        private final ByteBuffer log = new ByteBuffer();
        private final LargeText text = new LargeText(log,false);
K
kohsuke 已提交
1163
        private final Map<AbstractBuild,String> tagSet;
K
kohsuke 已提交
1164

K
kohsuke 已提交
1165 1166 1167
        public TagWorkerThread(Map<AbstractBuild,String> tagSet) {
            this.tagSet = tagSet;
        }
K
kohsuke 已提交
1168

K
kohsuke 已提交
1169 1170 1171 1172
        public String getLog() {
            // this method can be invoked from another thread.
            return log.toString();
        }
K
kohsuke 已提交
1173

K
kohsuke 已提交
1174 1175 1176
        public synchronized void start() {
            for (Entry<AbstractBuild, String> e : tagSet.entrySet()) {
                TagAction ta = e.getKey().getAction(TagAction.class);
K
kohsuke 已提交
1177
                if(ta!=null) {
K
kohsuke 已提交
1178
                    ta.workerThread = this;
K
kohsuke 已提交
1179 1180
                    ta.log = new WeakReference<LargeText>(text);
                }
K
kohsuke 已提交
1181
            }
K
kohsuke 已提交
1182

K
kohsuke 已提交
1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193
            super.start();
        }

        public void run() {
            TaskListener listener = new StreamTaskListener(log);

            for (Entry<AbstractBuild, String> 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;
K
kohsuke 已提交
1194
                }
K
kohsuke 已提交
1195 1196 1197
                listener.getLogger().println("Tagging "+e.getKey()+" to "+e.getValue());
                ta.perform(e.getValue(), listener);
                listener.getLogger().println();
K
kohsuke 已提交
1198
            }
K
kohsuke 已提交
1199 1200

            listener.getLogger().println("Completed");
K
kohsuke 已提交
1201
            text.markAsComplete();
K
kohsuke 已提交
1202 1203
        }
    }
K
kohsuke 已提交
1204 1205 1206 1207 1208 1209 1210 1211

    /**
     * Temporary hack for assisting trouble-shooting.
     *
     * <p>
     * Setting this property to true would cause <tt>cvs log</tt> to dump a lot of messages.
     */
    public static boolean debugLogging = false;
K
kohsuke 已提交
1212 1213

    private static final long serialVersionUID = 1L;
K
kohsuke 已提交
1214
}