View Javadoc

1   /*
2   Karma core - Core of the Karma application
3   Copyright (C) 2004  Toolforge <www.toolforge.nl>
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, write to the Free Software
17  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  */
19  package nl.toolforge.karma.core.vc.cvsimpl;
20  
21  import net.sf.sillyexceptions.OutOfTheBlueException;
22  import nl.toolforge.core.util.file.MyFileUtils;
23  import nl.toolforge.karma.core.ErrorCode;
24  import nl.toolforge.karma.core.KarmaRuntimeException;
25  import nl.toolforge.karma.core.Version;
26  import nl.toolforge.karma.core.cmd.Command;
27  import nl.toolforge.karma.core.cmd.CommandResponse;
28  import nl.toolforge.karma.core.history.ModuleHistory;
29  import nl.toolforge.karma.core.history.ModuleHistoryEvent;
30  import nl.toolforge.karma.core.history.ModuleHistoryException;
31  import nl.toolforge.karma.core.history.ModuleHistoryFactory;
32  import nl.toolforge.karma.core.location.Location;
33  import nl.toolforge.karma.core.module.Module;
34  import nl.toolforge.karma.core.vc.Authenticator;
35  import nl.toolforge.karma.core.vc.Authenticators;
36  import nl.toolforge.karma.core.vc.DevelopmentLine;
37  import nl.toolforge.karma.core.vc.PatchLine;
38  import nl.toolforge.karma.core.vc.Runner;
39  import nl.toolforge.karma.core.vc.SymbolicName;
40  import nl.toolforge.karma.core.vc.VersionControlException;
41  import nl.toolforge.karma.core.vc.VersionControlSystem;
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  import org.netbeans.lib.cvsclient.Client;
45  import org.netbeans.lib.cvsclient.admin.StandardAdminHandler;
46  import org.netbeans.lib.cvsclient.command.CommandException;
47  import org.netbeans.lib.cvsclient.command.GlobalOptions;
48  import org.netbeans.lib.cvsclient.command.add.AddCommand;
49  import org.netbeans.lib.cvsclient.command.checkout.CheckoutCommand;
50  import org.netbeans.lib.cvsclient.command.commit.CommitCommand;
51  import org.netbeans.lib.cvsclient.command.importcmd.ImportCommand;
52  import org.netbeans.lib.cvsclient.command.log.LogInformation;
53  import org.netbeans.lib.cvsclient.command.log.RlogCommand;
54  import org.netbeans.lib.cvsclient.command.tag.TagCommand;
55  import org.netbeans.lib.cvsclient.command.update.UpdateCommand;
56  import org.netbeans.lib.cvsclient.connection.AuthenticationException;
57  import org.netbeans.lib.cvsclient.connection.Connection;
58  import org.netbeans.lib.cvsclient.connection.ConnectionFactory;
59  import org.netbeans.lib.cvsclient.connection.PServerConnection;
60  import org.netbeans.lib.cvsclient.event.CVSListener;
61  
62  import java.io.File;
63  import java.io.IOException;
64  import java.util.ArrayList;
65  import java.util.Collection;
66  import java.util.Date;
67  import java.util.Hashtable;
68  import java.util.Iterator;
69  import java.util.List;
70  import java.util.Map;
71  import java.util.StringTokenizer;
72  
73  /***
74   * <p>Runner class for CVS. Executes stuff on a CVS repository.
75   * <p/>
76   * <p>TODO : the CVSRunner could be made multi-threaded, to use bandwidth to a remote repository much better ...
77   *
78   * @author D.A. Smedes
79   * @version $Id: CVSRunner.java,v 1.33 2004/11/16 22:31:57 asmedes Exp $
80   */
81  public final class CVSRunner implements Runner {
82  
83    static {
84  
85      // As per the recommendation ...
86      //
87      System.setProperty("javacvs.multiple_commands_warning", "false");
88    }
89  
90    private CVSListener listener = null; // The listener that receives events from this runner.
91  
92    private static Log logger = LogFactory.getLog(CVSRunner.class);
93  
94    private GlobalOptions globalOptions = new GlobalOptions();
95    private Connection connection = null;
96  
97    private CVSRepository location = null;
98  
99    private boolean isExt = false;
100 
101   /***
102    * Constructs a runner to fire commands on a CVS repository. A typical client for a <code>CVSRunner</code> instance is
103    * a {@link Command} implementation, as that one knows what to fire away on CVS. The runner is instantiated with a
104    * <code>location</code> and a <code>manifest</code>. The location must be a <code>CVSLocationImpl</code> instance,
105    * reprenting a CVS repository. The manifest is required because it determines the base point from where CVS commands
106    * will be run; modules are checked out in a directory structure, relative to
107    * {@link nl.toolforge.karma.core.manifest.Manifest#getModuleBaseDirectory()}.
108    *
109    * @param location       A <code>Location</code> instance (typically a <code>CVSLocationImpl</code> instance), containing
110    *                       the location and connection details of the CVS repository.
111    * @throws CVSException  <code>AUTHENTICATION_ERROR</code> is thrown when <code>location</code> cannot be authenticated.
112    * @throws nl.toolforge.karma.core.vc.AuthenticationException If the location cannot be authenticated.
113    */
114   public CVSRunner(Location location) throws CVSException, nl.toolforge.karma.core.vc.AuthenticationException {
115 
116     if (location == null) {
117       throw new CVSException(VersionControlException.MISSING_LOCATION);
118     }
119 
120     CVSRepository cvsLocation = null;
121     try {
122       cvsLocation = ((CVSRepository) location);
123       setLocation(cvsLocation);
124     } catch (ClassCastException e) {
125       logger.error("Wrong type for location. Should be CVSRepository.", e);
126       throw new KarmaRuntimeException("Wrong type for location. Should be CVSRepository.", e);
127     }
128 
129     //If we don't have a
130     //
131     Authenticator a = Authenticators.getAuthenticator(cvsLocation.getAuthenticatorKey());
132     cvsLocation.setUsername(a.getUsername());
133 
134     if (cvsLocation.getProtocol().equals(CVSRepository.EXT)) {
135 
136       isExt = true;
137 
138       this.listener = new LogParser();
139 
140     } else {
141 
142       this.listener = new CVSResponseAdapter();
143 
144       connection = ConnectionFactory.getConnection(cvsLocation.getCVSRoot());
145       if (connection instanceof PServerConnection) {
146         ((PServerConnection) connection).setEncodedPassword(a.getPassword());
147       }
148 
149       logger.debug("CVSRunner using CVSROOT : " + cvsLocation.getCVSRoot());
150       globalOptions.setCVSRoot(cvsLocation.getCVSRoot());
151     }
152 
153     // The default ...
154     //
155   }
156 
157   private void setLocation(CVSRepository location) {
158     this.location = location;
159   }
160 
161   private CVSRepository getLocation() {
162     return location;
163   }
164 
165   private Connection getConnection() {
166     return connection;
167   }
168 
169   /***
170    * Assigns a CommandResponse instance to the runner to optionally promote interactivity.
171    *
172    * @param response A - possibly <code>null</code> response instance.
173    */
174   public void setCommandResponse(CommandResponse response) {
175     listener = new CVSResponseAdapter(response);
176   }
177 
178   /***
179    * Checks if the module is located in CVS within a subdirectory (or subdirs, any amount is possible). If so, the
180    * module-name is prefixed with that offset.
181    *
182    * @param module
183    * @return        The module-name, or - when applicable - the module-name prefixed with the value of
184    *                {@link VersionControlSystem#getModuleOffset()}.
185    */
186   private String getModuleOffset(Module module) {
187 
188     // todo when modules from different locations have the same name, they should be given a namespace in front
189     // of the module.
190 
191     if (((VersionControlSystem) module.getLocation()).getModuleOffset() == null) {
192       return module.getName();
193     } else {
194       return ((VersionControlSystem) module.getLocation()).getModuleOffset() + "/" + module.getName();
195     }
196   }
197 
198   public void commit(File file) throws VersionControlException {
199 
200     // todo why not use the add()-method ????
201 
202     StandardAdminHandler adminHandler = new StandardAdminHandler();
203 
204     boolean isEntry = false;
205 
206     try {
207       isEntry = (adminHandler.getEntry(file) != null);
208     } catch (IOException e) {
209       isEntry = false;
210     }
211 
212     if (isEntry) {
213 
214       CommitCommand commitCommand = new CommitCommand();
215       commitCommand.setFiles(new File[]{file});
216       commitCommand.setMessage("<undocumented> File committed by Karma");
217 
218       executeOnCVS(commitCommand, file.getParentFile(), null);
219 
220     } else {
221       AddCommand addCommand = new AddCommand();
222       addCommand.setFiles(new File[]{file});
223 
224       executeOnCVS(addCommand, file.getParentFile(), null);
225 
226       CommitCommand commitCommand = new CommitCommand();
227       commitCommand.setFiles(new File[]{file});
228       commitCommand.setMessage("<undocumented> File committed by Karma");
229 
230       executeOnCVS(commitCommand, file.getParentFile(), null);
231     }
232 
233   }
234 
235   public void addModule(Module module, String comment) throws CVSException {
236 
237     // Step 1 : check if the module doesn't yet exist
238     //
239     if (existsInRepository(module)) {
240       throw new CVSException(CVSException.MODULE_EXISTS_IN_REPOSITORY, new Object[]{module.getName(), module.getLocation().getId()});
241     }
242 
243     // Step 2 : import the module, including its full structure.
244     //
245     ImportCommand importCommand = new ImportCommand();
246     importCommand.setModule(getModuleOffset(module));
247     importCommand.setLogMessage("Module " + module.getName() + " created automatically by Karma on " + new Date().toString());
248     importCommand.setVendorTag("Karma");
249     importCommand.setReleaseTag("MAINLINE_0-0");
250 
251     executeOnCVS(importCommand, module.getBaseDir(), null);
252   }
253 
254   /***
255    * Performs the <code>cvs checkout [-r &lt;symbolic-name&gt;] &lt;module&gt;</code>command for a module.
256    * <code>version</code> is used when not <code>null</code> to checkout a module with a symbolic name.
257    *
258    * @param module        The module to check out.
259    * @param version       The version number for the module to check out.
260    * @throws CVSException With errorcodes <code>NO_SUCH_MODULE_IN_REPOSITORY</code> when the module does not exist
261    *                      in the repository and <code>INVALID_SYMBOLIC_NAME</code>, when the version does not exists for the module.
262    */
263   public void checkout(Module module, Version version) throws CVSException {
264 
265     checkout(module, null, version);
266   }
267 
268 
269   public void checkout(Module module) throws CVSException {
270     checkout(module, null, null);
271   }
272 
273   /***
274    * See {@link #checkout(Module, Version)}. This method defaults to the HEAD of the development branch at hand.
275    *
276    * @param module          The module to check out.
277    * @param developmentLine The development line or <code>null</code> when the TRUNK is the context line.
278    * @throws CVSException   With errorcodes <code>NO_SUCH_MODULE_IN_REPOSITORY</code> when the module does not exist
279    *                        in the repository and <code>INVALID_SYMBOLIC_NAME</code>, when the version does not exists
280    *                        for the module.
281    */
282   public void checkout(Module module, DevelopmentLine developmentLine, Version version) throws CVSException {
283 
284     //todo: proper exception handling
285     try {
286       MyFileUtils.makeWriteable(module.getBaseDir(), true);
287     } catch (Exception e) {
288       logger.error("Exception when making module writeable just before checking it out.", e);
289     }
290 
291     boolean readonly = false;
292 
293     Map arguments = new Hashtable();
294     arguments.put("MODULE", module.getName());
295     arguments.put("REPOSITORY", module.getLocation().getId());
296 
297     CheckoutCommand checkoutCommand = new CheckoutCommand();
298     checkoutCommand.setModule(getModuleOffset(module));
299     checkoutCommand.setCheckoutDirectory(module.getName()); // Flatten to module name as the checkoutdir.
300     checkoutCommand.setPruneDirectories(true); //-P
301 
302     if ((version != null) || (developmentLine != null)) {
303       checkoutCommand.setCheckoutByRevision(Utils.createSymbolicName(module, developmentLine, version).getSymbolicName());
304       if (version != null) {
305         arguments.put("VERSION", version.toString());
306         readonly = true;
307       }
308     } else {
309       checkoutCommand.setResetStickyOnes(true);
310     }
311 
312     // The checkout directory for a module has to be relative to
313 
314     executeOnCVS(checkoutCommand, module.getBaseDir().getParentFile(), arguments);
315 
316     updateModuleHistoryXml(module);
317 
318     if (readonly) {
319       MyFileUtils.makeReadOnly(module.getBaseDir());
320     }
321   }
322 
323   private void updateModuleHistoryXml(Module module) throws CVSException {
324     logger.debug("Updating history.xml to HEAD.");
325     UpdateCommand command = new UpdateCommand();
326     try {
327       ModuleHistoryFactory factory = ModuleHistoryFactory.getInstance(module.getBaseDir());
328       File historyLocation = factory.getModuleHistory(module).getHistoryLocation();
329       command.setFiles(new File[]{historyLocation});
330       command.setResetStickyOnes(true);  //-A
331       executeOnCVS(command, module.getBaseDir(), null);
332       logger.debug("Done updating history.xml to HEAD.");
333     } catch (ModuleHistoryException mhe) {
334       logger.error("ModuleHistoryException when updating module history to HEAD", mhe);
335     }
336   }
337 
338 
339   /***
340    * @see #update(Module, DevelopmentLine, Version)
341    */
342   public void update(Module module) throws CVSException {
343     update(module, null);
344   }
345 
346   /***
347    * @see #update(Module, DevelopmentLine, Version)
348    */
349   public void update(Module module, Version version) throws CVSException {
350     update(module, null, version);
351   }
352 
353   /***
354    * For a module, the <code>cvs -q update -d -r &lt;symbolic-name&gt;</code> command is executed. Note that empty
355    * directories are not pruned.
356    *
357    * @param module          The module to update.
358    * @param developmentLine The development line or <code>null</code> when the TRUNK is the context line.
359    * @param version         The version of the module or <code>null</code> when no specific version applies.
360    */
361   public void update(Module module, DevelopmentLine developmentLine, Version version) throws CVSException {
362 
363     //todo: proper exception handling
364     try {
365       MyFileUtils.makeWriteable(module.getBaseDir(), true);
366     } catch (Exception e) {
367       logger.error("Exception when making module writeable just before updating it.", e);
368     }
369 
370     boolean readonly = false;
371 
372     Map arguments = new Hashtable();
373     arguments.put("MODULE", module.getName());
374     arguments.put("REPOSITORY", module.getLocation().getId());
375 
376     UpdateCommand updateCommand = new UpdateCommand();
377     updateCommand.setRecursive(true);
378     updateCommand.setBuildDirectories(true); //-d
379     updateCommand.setPruneDirectories(false); //-P
380 
381     if (version != null || module.hasPatchLine()) {
382       updateCommand.setUpdateByRevision(Utils.createSymbolicName(module, developmentLine, version).getSymbolicName());
383       if (version != null) {
384         arguments.put("VERSION", version.toString());
385         readonly = true;
386       }
387     } else {
388       updateCommand.setResetStickyOnes(true);
389     }
390 
391     // todo add data to the exception. this sort of business logic should be here, not in CVSResponseAdapter.
392     //
393     executeOnCVS(updateCommand, module.getBaseDir(), arguments);
394 
395     updateModuleHistoryXml(module);
396 
397     if (readonly) {
398       MyFileUtils.makeReadOnly(module.getBaseDir());
399     }
400   }
401 
402   public void add(Module module, File[] files, File[] dirs) throws CVSException {
403 
404     String[] f = (files == null ? new String[0] : new String[files.length]);
405     String[] d = (dirs == null ? new String[0] : new String[dirs.length]);
406 
407     for (int i = 0; i < f.length; i++) {
408       f[i] = files[i].getPath();
409     }
410     for (int i = 0; i < d.length; i++) {
411       d[i] = dirs[i].getPath();
412     }
413 
414     add(module, f, d);
415   }
416 
417   public void add(Module module, String[] files, String[] dirs) throws CVSException {
418 
419     files = (files == null ? new String[] {} : files);
420     dirs = (dirs == null ? new String[] {} : dirs);
421 
422     Map arguments = new Hashtable();
423     arguments.put("MODULE", module.getName());
424 
425     // Step 1 : Add the file to the CVS repository
426     //
427     AddCommand addCommand = new AddCommand();
428     addCommand.setMessage("Initial checkin in repository by Karma.");
429 
430     File modulePath = module.getBaseDir();
431 
432     // Create temp files
433     //
434     Collection cvsFilesCollection = new ArrayList();
435     int i = 0;
436     for (i=0; i < files.length; i++) {
437       File fileToAdd = new File(modulePath, files[i]);
438 
439       if (!fileToAdd.exists()) {
440         try {
441           File dir = new File(modulePath, files[i]).getParentFile();
442 
443           if (dir.mkdirs()) {
444             cvsFilesCollection.add(dir);
445           }
446 
447           fileToAdd.createNewFile();
448           logger.debug("Created file " + files[i] + " for module " + module.getName() + ".");
449         } catch (IOException e) {
450           throw new KarmaRuntimeException("Error while creating module layout for module " + module.getName());
451         }
452       }
453 
454       cvsFilesCollection.add(fileToAdd);
455     }
456 
457     // Create temp directories
458     //
459     for (i=0; i < dirs.length; i++) {
460       File dirToAdd = new File(modulePath, dirs[i]);
461 
462       //try to create the dirs
463       if (!dirToAdd.mkdirs() && !dirToAdd.exists()) {
464         //failed to create the dirs and they do not exist yet.
465         throw new KarmaRuntimeException("Error while creating module layout for module " + module.getName());
466       }
467       logger.debug("Created directory " + dirs[i] + " for module " + module.getName() + ".");
468 
469       // Ensure that all directories are added correctly
470       //
471       StringTokenizer tokenizer = new StringTokenizer(dirs[i], "/");
472       String base = "";
473       while (tokenizer.hasMoreTokens()) {
474         String subDir = tokenizer.nextToken();
475         base += subDir;
476         cvsFilesCollection.add(new File(modulePath, base));
477         base += "/";
478       }
479     }
480 
481     File[] cvsFiles = (File[]) cvsFilesCollection.toArray(new File[cvsFilesCollection.size()]);
482     addCommand.setFiles(cvsFiles);
483 
484     // A file is added against a module, thus the contextDirectory is constructed based on the basePoint and the
485     // modules' name.
486     //
487     executeOnCVS(addCommand, module.getBaseDir(), arguments);
488 
489     // Step 2 : Commit the file to the CVS repository
490     //
491     CommitCommand commitCommand = new CommitCommand();
492     commitCommand.setFiles(cvsFiles);
493     commitCommand.setMessage("File added automatically by Karma.");
494 
495     executeOnCVS(commitCommand, module.getBaseDir(), arguments);
496   }
497 
498   private void commit(DevelopmentLine developmentLine, Module module, File file, String message) throws CVSException {
499 
500     CommitCommand commitCommand = new CommitCommand();
501 
502     commitCommand.setFiles(new File[]{file});
503     if (developmentLine != null) {
504       commitCommand.setToRevisionOrBranch(developmentLine.getName());
505     }
506     commitCommand.setMessage(message);
507 
508     executeOnCVS(commitCommand, module.getBaseDir(), null);
509   }
510 
511   public void promote(Module module, String comment, Version version) throws CVSException {
512 
513     try {
514       //Add an event to the module history.
515       String author = ((CVSRepository) module.getLocation()).getUsername();
516       addModuleHistoryEvent(module, ModuleHistoryEvent.PROMOTE_MODULE_EVENT, version, new Date(), author, comment);
517 
518       tag(module, version);
519     } catch (ModuleHistoryException mhe) {
520       logger.error("Writing the history.xml failed", mhe);
521       throw new CVSException(CVSException.MODULE_HISTORY_ERROR, new Object[]{mhe.getMessage()});
522     }
523   }
524 
525   private void tag(Module module, Version version) throws CVSException {
526     if (hasVersion(module, version)) {
527       throw new CVSException(VersionControlException.DUPLICATE_VERSION, new Object[]{module.getName(), version.getVersionNumber()});
528     }
529     tag(module, Utils.createSymbolicName(module, version), false);
530   }
531 
532 //
533 // Private for the time being
534 //
535   private void tag(Module module, SymbolicName symbolicName, boolean branch) throws CVSException {
536 
537     TagCommand tagCommand = new TagCommand();
538     tagCommand.setRecursive(true);
539     tagCommand.setTag(symbolicName.getSymbolicName());
540     tagCommand.setMakeBranchTag(branch);
541 
542     executeOnCVS(tagCommand, module.getBaseDir(), null);
543   }
544 
545   /***
546    * Provide log information on a module. This method checks if the
547    * {@link #setCommandResponse(nl.toolforge.karma.core.cmd.CommandResponse)} method has been called, which results in
548    * this runner initializing the CVS API with the correct objects. This is a requirement for the Netbeans CVS API.
549    */
550   public LogInformation log(Module module) throws CVSException {
551 
552     Map arguments = new Hashtable();
553     arguments.put("MODULE", module.getName());
554     arguments.put("REPOSITORY", module.getLocation().getId());
555 
556     RlogCommand logCommand = new RlogCommand();
557     logCommand.setModule(getModuleOffset(module) + "/" + Module.MODULE_DESCRIPTOR);
558     logCommand.setRecursive(false);
559 
560     executeOnCVS(logCommand, MyFileUtils.getSystemTempDirectory(), arguments);
561 
562     // Another hook into ext support.
563     //
564     if (isExt) {
565       return ((LogParser) this.listener).getLogInformation();
566     } else {
567       return ((CVSResponseAdapter) this.listener).getLogInformation();
568     }
569   }
570 
571   /*
572 
573   DISABLED, DO NOT DELETE, EXPERIMENTAL
574 
575   public LogInformation log(Manifest manifest) throws CVSException {
576 
577 
578   RlogCommand logCommand = new RlogCommand();
579 
580   String[] modules = new String[manifest.getAllModules().size()];
581   int j = 0;
582   for (Iterator i = manifest.getAllModules().values().iterator(); i.hasNext();) {
583   modules[j] = getModuleOffset((Module) i.next()) + "/" + Module.MODULE_DESCRIPTOR;
584   j++;
585   }
586 
587   logCommand.setModules(modules);
588 
589   executeOnCVS(logCommand, new File("."), null);
590 
591   // Another hook into ext support.
592   //
593   return ((CVSResponseAdapter) this.listener).getLogInformation();
594   }
595 
596   */
597 
598 
599   /***
600    * Checks if the module has a CVS branch tag <code>module.getPatchLine().getName()</code> attached.
601    *
602    * @param module The module for which the patch line should be checked.
603    * @return <code>true</code> of the module has a patch line attached in the CVS repository, <code>false</code>
604    *         otherwise.
605    */
606   public boolean hasPatchLine(Module module) {
607     try {
608       return hasSymbolicName(module, new CVSTag(new PatchLine(module.getVersion()).getName()));
609     } catch (CVSException c) {
610       return false;
611     }
612   }
613 
614   /***
615    * Creates a patchline for the module, given the modules' current version.
616    *
617    * @param module The module.
618    * @throws CVSException When an error occurred during the creation process.
619    */
620   public void createPatchLine(Module module) throws CVSException {
621     try {
622       // Add an event to the module history.
623       //
624       String author = ((CVSRepository) module.getLocation()).getUsername();
625       tag(module, new CVSTag(module.getPatchLine().getName()), true);
626 
627       addModuleHistoryEvent(module, ModuleHistoryEvent.CREATE_PATCH_LINE_EVENT, module.getVersion(), new Date(), author, "Patch line created by Karma");
628 
629     } catch (ModuleHistoryException mhe) {
630       logger.error("Writing the history.xml failed", mhe);
631       throw new CVSException(CVSException.MODULE_HISTORY_ERROR, new Object[]{mhe.getMessage()});
632     }
633   }
634 
635   /***
636    * A check if a module exists is done by trying to checkout the modules' <code>module.info</code> file in a temporary
637    * location. If that succeeds, apparently the module exists in that location and we have a <code>true</code> to
638    * return.
639    */
640   public boolean existsInRepository(Module module) {
641 
642     if (module == null) {
643       return false;
644     }
645 
646     try {
647       RlogCommand logCommand = new RlogCommand();
648       logCommand.setModule(getModuleOffset(module));
649 
650       executeOnCVS(logCommand, MyFileUtils.getSystemTempDirectory(), null);
651     } catch (CVSException e) {
652       return false;
653     }
654     return true;
655   }
656 
657   private boolean hasVersion(Module module, Version version) throws CVSException {
658     return hasSymbolicName(module, Utils.createSymbolicName(module, version));
659   }
660 
661   private boolean hasSymbolicName(Module module, SymbolicName symbolicName) throws CVSException {
662 
663     if (symbolicName == null) {
664       return false;
665     }
666 
667     LogInformation logInfo = log(module);
668     List symbolicNames = new ArrayList();
669 
670     for (Iterator i = logInfo.getAllSymbolicNames().iterator(); i.hasNext();) {
671       symbolicNames.add(((LogInformation.SymName) i.next()).getName());
672     }
673 
674     return symbolicNames.contains(symbolicName.getSymbolicName());
675   }
676 
677   /***
678    * Runs a CVS command on the repository (through the Netbeans API). When a co
679    *
680    * @param command          A command object, representing the CVS command.
681    * @param contextDirectory The directory from where the command should be run.
682    * @param args             Arguments that can be passed to the CVSRuntimeException thrown by the CVSListener.
683    *
684    * @throws CVSException    When CVS has reported an error through its listener.
685    */
686   private void executeOnCVS(org.netbeans.lib.cvsclient.command.Command command,
687                             File contextDirectory, Map args) throws CVSException {
688     // Switch ...
689     //
690     if (isExt) {
691 
692       ExtClient client = new ExtClient();
693       client.addCVSListener((LogParser) listener);
694       client.runCommand(command, contextDirectory, getLocation());
695 
696     } else {
697 
698       if (contextDirectory == null) {
699         throw new NullPointerException("Context directory cannot be null.");
700       }
701 
702       CVSException exception = null;
703 
704       int retries = 0;
705 
706       while (retries < 3) {
707 
708         Client client = new Client(getConnection(), new StandardAdminHandler());
709         client.setLocalPath(contextDirectory.getPath());
710 
711         if (retries == 0) {
712           logger.debug("Running CVS command : '" + command.getCVSCommand() + "' in " + client.getLocalPath());
713         }
714 
715         try {
716           // A CVSResponseAdapter is registered as a listener for the response from CVS. This one adapts to Karma
717           // specific stuff.
718           //
719 
720           long start = System.currentTimeMillis();
721 
722           ((CVSResponseAdapter) listener).setArguments(args);
723           client.getEventManager().addCVSListener(listener);
724           client.executeCommand(command, globalOptions);
725 
726           logger.debug("CVS command finished in " + (System.currentTimeMillis() - start) + " ms.");
727 
728           // Apparently, were done succesfully.
729           //
730           break;
731 
732         } catch (CommandException e) {
733           logger.error(e.getMessage(), e);
734           if (e.getUnderlyingException() instanceof CVSRuntimeException) {
735             ErrorCode code = ((CVSRuntimeException) e.getUnderlyingException()).getErrorCode();
736             Object[] messageArgs = ((CVSRuntimeException) e.getUnderlyingException()).getMessageArguments();
737             throw new CVSException(code, messageArgs);
738           } else {
739             throw new CVSException(CVSException.INTERNAL_ERROR, new Object[]{globalOptions.getCVSRoot()});
740           }
741         } catch (AuthenticationException e) {
742           // Over here, we have implemented a retry-mechanism.
743           //
744           if (retries == 3) {
745             if (e.getUnderlyingThrowable() instanceof IOException) {
746               logger.error("Cannot retry any longer.", e);
747             }
748             exception = new CVSException(CVSException.INTERNAL_ERROR, new Object[]{client.getGlobalOptions().getCVSRoot()});
749           } else {
750             logger.info("Retrying CVS command : '" + command.getCVSCommand() + "' in " + client.getLocalPath());
751 //            logger.error(e.getMessage(), e);
752           }
753           retries ++;
754         } finally {
755           // See the static block in this class and corresponding documentation.
756           //
757           try {
758             client.getConnection().close();
759           } catch (IOException e) {
760             throw new CVSException(CVSException.INTERNAL_ERROR, new Object[]{globalOptions.getCVSRoot()});
761           }
762         }
763       }
764 
765       if (exception != null) {
766         throw exception;
767       }
768     }
769   }
770 
771   /***
772    * Helper method that add a new event to a module's history. When the history does not
773    * exist yet (in case of a new module) it is newly created. When the history does exist
774    * the event is added to the history.
775    *
776    * @param module                  The module involved.
777    * @param eventType               The type of {@link ModuleHistoryEvent}.
778    * @param version                 The version that the module is promoted to.
779    * @param datetime                The timestamp.
780    * @param author                  The authentication of the person who has triggered the event.
781    * @param comment                 The (optional) comment the author has given.
782    * @throws CVSException           Thrown in case something goes wrong with CVS
783    */
784   private void addModuleHistoryEvent(
785       Module module,
786       String eventType,
787       Version version,
788       Date datetime,
789       String author,
790       String comment) throws CVSException, ModuleHistoryException
791   {
792     ModuleHistoryFactory factory = ModuleHistoryFactory.getInstance(module.getBaseDir());
793     ModuleHistory history = factory.getModuleHistory(module);
794     if (history != null) {
795       ModuleHistoryEvent event = new ModuleHistoryEvent();
796       event.setType(eventType);
797       event.setVersion(version);
798       event.setDatetime(datetime);
799       event.setAuthor(author);
800       event.setComment(comment);
801       history.addEvent(event);
802 
803       try {
804         MyFileUtils.makeWriteable(new File(module.getBaseDir(), "CVS"), false);
805         if (history.getHistoryLocation().exists()) {
806           //history already exists. commit changes.
807           MyFileUtils.makeWriteable(history.getHistoryLocation(), false);
808           history.save();
809 
810           //development line is null, since the history.xml is always committed in the HEAD.
811           commit(null, module, new File(module.getBaseDir(), ModuleHistory.MODULE_HISTORY_FILE_NAME), "History updated by Karma");
812         } else {
813           //history did not exist yet. add to CVS and commit it.
814           history.save();
815           add(module, new String[]{history.getHistoryLocation().getName()}, null);
816         }
817         MyFileUtils.makeReadOnly(module.getBaseDir());
818       } catch (IOException ioe) {
819         logger.error("Error when making history.xml readonly/writeable", ioe);
820       } catch (InterruptedException ie) {
821         logger.error("Error when making history.xml readonly/writeable", ie);
822       }
823     } else {
824       //history wel null. EN NU?! todo
825       throw new OutOfTheBlueException("If this happens something rrreally went wrong with the history");
826     }
827   }
828 
829 }
830