From: etienne s. <eti...@ca...> - 2004-03-29 10:08:00
|
Yes. I did implement property and propertyOnDelete for the SVN task. An update of configxml.html to include SVN would be cool! Etienne -----Original Message----- From: Jeffrey Fredrick [mailto:jt...@ag...] Sent: Montag, 29. Marz 2004 11:01 To: 'etienne studer'; 'Cruisecontrol-Devel' Subject: RE: [Cruisecontrol-devel] SVN change request: use Date instead of Revision That's right -- thanks for refreshing my memory. I've committed the changes. BTW, it seems that you implemented property and propertyOnDelete yes? Does this mean that an update to configxml.html is in order? Jtf -----Original Message----- From: cru...@li... [mailto:cru...@li...] On Behalf Of etienne studer Sent: Saturday, March 27, 2004 1:53 AM To: Jeffrey Fredrick; 'Cruisecontrol-Devel' Subject: RE: [Cruisecontrol-devel] SVN change request: use Date instead of Revision Hi Jeffrey Yes, it is. During the discussion Garrick proposed that in the future the CC framework should be extended in a way that modification ranges can also be revision-based (instead of date-based only) to solve date-difference problems between repository server and CC server. Until we have this CC extension, nobody objected to change to the date-based solution analogous to the CVS task. Best regards, Etienne -----Original Message----- From: Jeffrey Fredrick [mailto:jt...@ag...] Sent: Freitag, 26. Marz 2004 22:23 To: 'etienne studer'; 'Cruisecontrol-Devel' Subject: RE: [Cruisecontrol-devel] SVN change request: use Date instead of Revision We discussed this a while ago yes? I don't recall the outcome but is this consistent w/that discussion? In any case I'm hoping to get some time this weekend to process submissions and I'll take a closer look then. Jtf -----Original Message----- From: cru...@li... [mailto:cru...@li...] On Behalf Of etienne studer Sent: Wednesday, February 25, 2004 1:06 PM To: Cruisecontrol-Devel Subject: [Cruisecontrol-devel] SVN change request: use Date instead of Revision Hi Jeffrey Recently, the "svn.revision" feature was added to the SVN task. I find this feature not ideal due to the following reasons: 1. The SVN task uses the repository head revision in order to define the end of the modification time range. This is unlike the CVS task, where the current time provided by the ModificationSet class is used. --> I propose to use the timestamp provided by CC instead of the head revision, in order to stick with the general and well-known CC "mechanism". It makes the code simpler, too. 2. To retrieve the repository head revision, an extra server call is necessary. --> I propose to use the timestamp provided by CC instead of the head revision, in order to avoid an extra server call. 3. The way of the SVN task to find the current repository head revision works well when a local copy of the project is available. But it does not work properly when querying a remote repository for changes because the "COMMITTED" revision is only available on local repositories. The implemented fallback scenario of using the HEAD revision does not solve the problems the "svn.revision" issue tries to solve. --> I propose to use the timestamp provided by CC instead of the head revision, in order to be able to query remote repositories for modifications properly, without needing a local copy. Attached you can find an implementation of SVN.java including test case that does not need the repository head revision but works with the date provided by CC and which integrates well. The implementation is very close to the CVS task. Etienne package net.sourceforge.cruisecontrol.sourcecontrols; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Modification; import net.sourceforge.cruisecontrol.SourceControl; import net.sourceforge.cruisecontrol.util.Commandline; import net.sourceforge.cruisecontrol.util.StreamPumper; import org.apache.log4j.Logger; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.TimeZone; /** * This class implements the SourceControl methods for a Subversion repository. * The call to Subversion is assumed to work without any setup. This implies * that either authentication data must be available or the login parameters are * specified in the cc configuration file. * * Note: You can also observe for changes a Subversion repository that you have * not checked out locally. * * @see <a href="http://subversion.tigris.org/">subversion.tigris.org</a> * @author <a href="eti...@ca...">Etienne Studer</a> */ public class SVN implements SourceControl { private static final Logger LOG = Logger.getLogger(SVN.class); /** Date format expected by Subversion */ static final SimpleDateFormat SVN_DATE_FORMAT_IN = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); /** Date format returned by Subversion in XML output */ static final SimpleDateFormat SVN_DATE_FORMAT_OUT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); /** Set the time zone of the formatters to GMT. */ static { SVN_DATE_FORMAT_IN.setTimeZone(TimeZone.getTimeZone("GMT")); SVN_DATE_FORMAT_OUT.setTimeZone(TimeZone.getTimeZone("GMT")); } private Hashtable properties = new Hashtable(); private String property; private String propertyOnDelete; /** Configuration parameters */ private String repositoryLocation; private String localWorkingCopy; private String userName; private String password; public Hashtable getProperties() { return properties; } public void setProperty(String property) { this.property = property; } public void setPropertyOnDelete(String propertyOnDelete) { this.propertyOnDelete = propertyOnDelete; } /** * Sets the repository location to use when making calls to Subversion. * * @param repositoryLocation String indicating the url to the Subversion * repository on which to find the log history. */ public void setRepositoryLocation(String repositoryLocation) { this.repositoryLocation = repositoryLocation; } /** * Sets the local working copy to use when making calls to Subversion. * * @param localWorkingCopy String indicating the relative or absolute path * to the local working copy of the Subversion * repository of which to find the log history. */ public void setLocalWorkingCopy(String localWorkingCopy) { this.localWorkingCopy = localWorkingCopy; } /** * Sets the username for authentication. */ public void setUsername(String userName) { this.userName = userName; } /** * Sets the password for authentication. */ public void setPassword(String password) { this.password = password; } /** * This method validates that at least the repository location or the local * working copy location has been specified. * * @throws CruiseControlException Thrown when the repository location and * the local working copy location are both * null */ public void validate() throws CruiseControlException { if (repositoryLocation == null && localWorkingCopy == null) { throw new CruiseControlException( "At least 'repositoryLocation'or 'localWorkingCopy' " + "is a required attribute on the Subversion task "); } if (localWorkingCopy != null) { File workingDir = new File(localWorkingCopy); if (!workingDir.exists() || !workingDir.isDirectory()) { throw new CruiseControlException( "'localWorkingCopy' must be an existing " + "directory."); } } } /** * Returns a list of modifications detailing all the changes between * the last build and the latest revision in the repository. */ public List getModifications(Date lastBuild, Date now) { List modifications = new ArrayList(); try { Commandline command = buildHistoryCommand(lastBuild, now); modifications = execHistoryCommand(command, lastBuild); fillPropertiesIfNeeded(modifications); } catch (Exception e) { LOG.error("Error executing svn log command", e); } return modifications; } /** * Generates the command line for the svn log command. * * For example: * * 'svn log --non-interactive --xml -v -r {lastbuildTime}:headRevision repositoryLocation' */ Commandline buildHistoryCommand(Date lastBuild, Date checkTime) throws CruiseControlException { Commandline command = new Commandline(); command.setExecutable("svn"); if (localWorkingCopy != null) { command.setWorkingDirectory(localWorkingCopy); } command.createArgument().setValue("log"); command.createArgument().setValue("--non-interactive"); command.createArgument().setValue("--xml"); command.createArgument().setValue("-v"); command.createArgument().setValue("-r"); command.createArgument().setValue("{" + formatSVNDate(lastBuild) + "}" + ":" + "{" + formatSVNDate(checkTime) + "}"); if (userName != null) { command.createArgument().setValue("--username"); command.createArgument().setValue(userName); } if (password != null) { command.createArgument().setValue("--password"); command.createArgument().setValue(password); } if (repositoryLocation != null) { command.createArgument().setValue(repositoryLocation); } LOG.debug("Executing command: " + command); return command; } static String formatSVNDate(Date lastBuild) { return SVN_DATE_FORMAT_IN.format(lastBuild); } private List execHistoryCommand(Commandline command, Date lastBuild) throws InterruptedException, IOException, ParseException, JDOMException { Process p = command.execute(); logErrorStream(p); InputStream svnStream = p.getInputStream(); List modifications = parseStream(svnStream, lastBuild); p.waitFor(); p.getInputStream().close(); p.getOutputStream().close(); p.getErrorStream().close(); return modifications; } private void logErrorStream(Process p) { StreamPumper errorPumper = new StreamPumper(p.getErrorStream(), new PrintWriter(System.err, true)); new Thread(errorPumper).start(); } private List parseStream(InputStream svnStream, Date lastBuild) throws JDOMException, IOException, ParseException, UnsupportedEncodingException { InputStreamReader reader = new InputStreamReader(svnStream, "UTF-8"); return SVNLogXMLParser.parseAndFilter(reader, lastBuild); } void fillPropertiesIfNeeded(List modifications) { if (property != null) { if (modifications.size() > 0) { properties.put(property, "true"); } } if (propertyOnDelete != null) { for (int i = 0; i < modifications.size(); i++) { Modification modification = (Modification) modifications.get(i); if (modification.type.equals("deleted")) { properties.put(propertyOnDelete, "true"); break; } } } } static final class SVNLogXMLParser { private SVNLogXMLParser() { } static List parseAndFilter(Reader reader, Date lastBuild) throws JDOMException, ParseException { Modification[] modifications = parse(reader); return filterModifications(modifications, lastBuild); } static Modification[] parse(Reader reader) throws JDOMException, ParseException { SAXBuilder builder = new SAXBuilder(false); Document document = builder.build(reader); return parseDOMTree(document); } static Modification[] parseDOMTree(Document document) throws ParseException { List modifications = new ArrayList(); Element rootElement = document.getRootElement(); List logEntries = rootElement.getChildren("logentry"); for (Iterator iterator = logEntries.iterator(); iterator.hasNext();) { Element logEntry = (Element) iterator.next(); Modification[] modificationsOfRevision = parseLogEntry(logEntry); modifications.addAll(Arrays.asList(modificationsOfRevision)); } return (Modification[]) modifications.toArray(new Modification[modifications.size()]); } static Modification[] parseLogEntry(Element logEntry) throws ParseException { List modifications = new ArrayList(); Element logEntryPaths = logEntry.getChild("paths"); List paths = logEntryPaths.getChildren("path"); for (Iterator iterator = paths.iterator(); iterator.hasNext();) { Element path = (Element) iterator.next(); Modification modification = new Modification(); modification.modifiedTime = convertDate(logEntry.getChildText("date")); modification.userName = logEntry.getChildText("author"); modification.comment = logEntry.getChildText("msg"); modification.revision = logEntry.getAttributeValue("revision"); modification.folderName = ""; modification.fileName = path.getText(); modification.type = convertAction(path.getAttributeValue("action")); modifications.add(modification); } return (Modification[]) modifications.toArray(new Modification[modifications.size()]); } static Date convertDate(String date) throws ParseException { String withoutMicroSeconds = date.substring(0, date.indexOf('Z') - 3); return SVN_DATE_FORMAT_OUT.parse(withoutMicroSeconds); } static String convertAction(String action) { if (action.equals("A")) { return "added"; } if (action.equals("M")) { return "modified"; } if (action.equals("D")) { return "deleted"; } return "unknown"; } /** * Unlike CVS, Subversion maps dates to revisions which leads to a * different behavior when using the svn log command in conjunction with * dates, e.g., a date maps to a revision but the revision may have been * created earlier than the specified date. Therefore, if we are only * interested in changes that took place after the last build date, we * have to filter the modifications returned from the log command and * omit modifications that are older than the last build date. * * @see <a href="http://subversion.tigris.org/">subversion.tigris.org</a> */ static List filterModifications(Modification[] modifications, Date lastBuild) { List filtered = new ArrayList(); for (int i = 0; i < modifications.length; i++) { Modification modification = modifications[i]; if (modification.modifiedTime.getTime() > lastBuild.getTime()) { filtered.add(modification); } } return filtered; } } } package net.sourceforge.cruisecontrol.sourcecontrols; import junit.framework.TestCase; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Modification; import org.jdom.JDOMException; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.text.ParseException; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.TimeZone; import java.util.ArrayList; /** * @see <a href="http://subversion.tigris.org/">subversion.tigris.org</a> * @author <a href="eti...@ca...">Etienne Studer</a> */ public class SVNTest extends TestCase { private SVN svn; protected void setUp() throws Exception { svn = new SVN(); } protected void tearDown() throws Exception { svn = null; } public void testValidate() throws CruiseControlException, IOException { try { svn.validate(); fail("should throw an exception when no attributes are set"); } catch (CruiseControlException e) { // expected } svn.setRepositoryLocation("http://svn.collab.net/repos/svn"); try { svn.validate(); } catch (CruiseControlException e) { fail( "should not throw an exception when at least the 'repositoryLocation' attribute " + "is set"); } svn = new SVN(); svn.setLocalWorkingCopy("invalid directory"); try { svn.validate(); fail("should throw an exception when an invalid 'localWorkingCopy' attribute is set"); } catch (CruiseControlException e) { // expected } File tempFile = File.createTempFile("temp", "txt"); svn = new SVN(); svn.setLocalWorkingCopy(tempFile.getParent()); try { svn.validate(); } catch (CruiseControlException e) { fail( "should not throw an exception when at least a valid 'localWorkingCopy' " + "attribute is set"); } svn = new SVN(); svn.setLocalWorkingCopy(tempFile.getAbsolutePath()); try { svn.validate(); fail("should throw an exception when 'localWorkingCopy' is file instead of directory."); } catch (CruiseControlException e) { // expected } } public void testBuildHistoryCommand() throws IOException, CruiseControlException { svn.setLocalWorkingCopy("."); Date checkTime = new Date(); long tenMinutes = 10 * 60 * 1000; Date lastBuild = new Date(checkTime.getTime() - tenMinutes); String[] expectedCmd = new String[] { "svn", "log", "--non-interactive", "--xml", "-v", "-r", "{" + SVN.formatSVNDate(lastBuild) + "}:{" + SVN.formatSVNDate(checkTime) + "}"}; String[] actualCmd = svn.buildHistoryCommand(lastBuild, checkTime).getCommandline(); assertArraysEquals(expectedCmd, actualCmd); svn.setRepositoryLocation("http://svn.collab.net/repos/svn"); expectedCmd = new String[] { "svn", "log", "--non-interactive", "--xml", "-v", "-r", "{" + SVN.formatSVNDate(lastBuild) + "}:{" + SVN.formatSVNDate(checkTime) + "}", "http://svn.collab.net/repos/svn" }; actualCmd = svn.buildHistoryCommand(lastBuild, checkTime).getCommandline(); assertArraysEquals(expectedCmd, actualCmd); svn.setUsername("lee"); svn.setPassword("secret"); expectedCmd = new String[] { "svn", "log", "--non-interactive", "--xml", "-v", "-r", "{" + SVN.formatSVNDate(lastBuild) + "}:{" + SVN.formatSVNDate(checkTime) + "}", "--username", "lee", "--password", "secret", "http://svn.collab.net/repos/svn" }; actualCmd = svn.buildHistoryCommand(lastBuild, checkTime).getCommandline(); assertArraysEquals(expectedCmd, actualCmd); } public void testParseModifications() throws JDOMException, ParseException, IOException { String svnLog = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" + "<log>\n" + " <logentry revision=\"663\">\n" + " <author>lee</author>\n" + " <date>2003-04-30T10:01:42.349105Z</date>\n" + " <paths>\n" + " <path action=\"A\">/trunk/playground/aaa/ccc</path>\n" + " <path action=\"M\">/trunk/playground/aaa/ccc/d.txt</path>\n" + " <path action=\"A\">/trunk/playground/bbb</path>\n" + " </paths>\n" + " <msg>bli</msg>\n" + " </logentry>\n" + " <logentry revision=\"664\">\n" + " <author>etienne</author>\n" + " <date>2003-04-30T10:03:14.100900Z</date>\n" + " <paths>\n" + " <path action=\"A\">/trunk/playground/aaa/f.txt</path>\n" + " </paths>\n" + " <msg>bla</msg>\n" + " </logentry>\n" + " <logentry revision=\"665\">\n" + " <author>martin</author>\n" + " <date>2003-04-30T10:04:48.050619Z</date>\n" + " <paths>\n" + " <path action=\"D\">/trunk/playground/bbb</path>\n" + " </paths>\n" + " <msg>blo</msg>\n" + " </logentry>\n" + "</log>"; Modification[] modifications = SVN.SVNLogXMLParser.parse(new StringReader(svnLog)); assertEquals(5, modifications.length); Modification modification = createModification( SVN.SVN_DATE_FORMAT_OUT.parse("2003-04-30T10:01:42.349"), "lee", "bli", "663", "", "/trunk/playground/aaa/ccc", "added"); assertEquals(modification, modifications[0]); modification = createModification( SVN.SVN_DATE_FORMAT_OUT.parse("2003-04-30T10:01:42.349"), "lee", "bli", "663", "", "/trunk/playground/aaa/ccc/d.txt", "modified"); assertEquals(modification, modifications[1]); modification = createModification( SVN.SVN_DATE_FORMAT_OUT.parse("2003-04-30T10:01:42.349"), "lee", "bli", "663", "", "/trunk/playground/bbb", "added"); assertEquals(modification, modifications[2]); modification = createModification( SVN.SVN_DATE_FORMAT_OUT.parse("2003-04-30T10:03:14.100"), "etienne", "bla", "664", "", "/trunk/playground/aaa/f.txt", "added"); assertEquals(modification, modifications[3]); modification = createModification( SVN.SVN_DATE_FORMAT_OUT.parse("2003-04-30T10:04:48.050"), "martin", "blo", "665", "", "/trunk/playground/bbb", "deleted"); assertEquals(modification, modifications[4]); } public void testParseEmptyModifications() throws JDOMException, ParseException, IOException { String svnLog = "<?xml version=\"1.0\" encoding = \"ISO-8859-1\"?>\n " + "<log>\n" + "</log>"; Modification[] modifications = SVN.SVNLogXMLParser.parse(new StringReader(svnLog)); assertEquals(0, modifications.length); } public void testParseAndFilter() throws ParseException, JDOMException, IOException { String svnLog = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" + "<log>\n" + " <logentry revision=\"663\">\n" + " <author>lee</author>\n" + " <date>2003-08-02T10:01:13.349105Z</date>\n" + " <paths>\n" + " <path action=\"A\">/trunk/playground/bbb</path>\n" + " </paths>\n" + " <msg>bli</msg>\n" + " </logentry>\n" + " <logentry revision=\"664\">\n" + " <author>etienne</author>\n" + " <date>2003-07-29T17:45:12.100900Z</date>\n" + " <paths>\n" + " <path action=\"A\">/trunk/playground/aaa/f.txt</path>\n" + " </paths>\n" + " <msg>bla</msg>\n" + " </logentry>\n" + " <logentry revision=\"665\">\n" + " <author>martin</author>\n" + " <date>2003-07-29T18:15:11.100900Z</date>\n" + " <paths>\n" + " <path action=\"D\">/trunk/playground/ccc</path>\n" + " </paths>\n" + " <msg>blo</msg>\n" + " </logentry>\n" + "</log>"; TimeZone.setDefault(TimeZone.getTimeZone("GMT+0:00")); Date julyTwentynineSixPM2003 = new GregorianCalendar(2003, Calendar.JULY, 29, 18, 0, 0).getTime(); List modifications = SVN.SVNLogXMLParser.parseAndFilter(new StringReader(svnLog), julyTwentynineSixPM2003); assertEquals(2, modifications.size()); Modification modification = createModification( SVN.SVN_DATE_FORMAT_OUT.parse("2003-08-02T10:01:13.349"), "lee", "bli", "663", "", "/trunk/playground/bbb", "added"); assertEquals(modification, modifications.get(0)); modification = createModification( SVN.SVN_DATE_FORMAT_OUT.parse("2003-07-29T18:15:11.100"), "martin", "blo", "665", "", "/trunk/playground/ccc", "deleted"); assertEquals(modification, modifications.get(1)); Date julyTwentyeightZeroPM2003 = new GregorianCalendar(2003, Calendar.JULY, 28, 0, 0, 0).getTime(); modifications = SVN.SVNLogXMLParser.parseAndFilter(new StringReader(svnLog), julyTwentyeightZeroPM2003); assertEquals(3, modifications.size()); } public void testFormatDatesForSvnLog() { TimeZone.setDefault(TimeZone.getTimeZone("GMT+10:00")); Date maySeventeenSixPM2001 = new GregorianCalendar(2001, Calendar.MAY, 17, 18, 0, 0).getTime(); assertEquals( "2001-05-17T08:00:00Z", SVN.SVN_DATE_FORMAT_IN.format(maySeventeenSixPM2001)); Date maySeventeenEightAM2001 = new GregorianCalendar(2001, Calendar.MAY, 17, 8, 0, 0).getTime(); assertEquals( "2001-05-16T22:00:00Z", SVN.SVN_DATE_FORMAT_IN.format(maySeventeenEightAM2001)); TimeZone.setDefault(TimeZone.getTimeZone("GMT-10:00")); Date marchTwelfFourPM2003 = new GregorianCalendar(2003, Calendar.MARCH, 12, 16, 0, 0).getTime(); assertEquals( "2003-03-13T02:00:00Z", SVN.SVN_DATE_FORMAT_IN.format(marchTwelfFourPM2003)); Date marchTwelfTenAM2003 = new GregorianCalendar(2003, Calendar.MARCH, 12, 10, 0, 0).getTime(); assertEquals("2003-03-12T20:00:00Z", SVN.SVN_DATE_FORMAT_IN.format(marchTwelfTenAM2003)); } public void testSetProperty() throws ParseException { svn.setProperty("hasChanges?"); List noModifications = new ArrayList(); svn.fillPropertiesIfNeeded(noModifications); assertEquals(null, svn.getProperties().get("hasChanges?")); List hasModifications = new ArrayList(); hasModifications.add(createModification(SVN.SVN_DATE_FORMAT_OUT.parse("2003- 08-02T10:01:13.349"), "lee", "bli", "663", "", "/trunk/playground/bbb", "added")); svn.fillPropertiesIfNeeded(hasModifications); assertEquals("true", svn.getProperties().get("hasChanges?")); } public void testSetPropertyOnDelete() throws ParseException { svn.setPropertyOnDelete("hasDeletions?"); List noModifications = new ArrayList(); svn.fillPropertiesIfNeeded(noModifications); assertEquals(null, svn.getProperties().get("hasDeletions?")); List noDeletions = new ArrayList(); noDeletions.add(createModification(SVN.SVN_DATE_FORMAT_OUT.parse("2003-08-02 T10:01:13.349"), "lee", "bli", "663", "", "/trunk/playground/bbb", "added")); svn.fillPropertiesIfNeeded(noDeletions); assertEquals(null, svn.getProperties().get("hasDeletions?")); List hasDeletions = new ArrayList(); hasDeletions.add(createModification(SVN.SVN_DATE_FORMAT_OUT.parse("2003-08-0 2T10:01:13.349"), "lee", "bli", "663", "", "/trunk/playground/aaa", "added")); hasDeletions.add(createModification(SVN.SVN_DATE_FORMAT_OUT.parse("2003-08-0 2T10:01:13.349"), "lee", "bli", "663", "", "/trunk/playground/bbb", "deleted")); svn.fillPropertiesIfNeeded(hasDeletions); assertEquals("true", svn.getProperties().get("hasDeletions?")); } private static Modification createModification( Date date, String user, String comment, String revision, String folder, String file, String type) { Modification modification = new Modification(); modification.modifiedTime = date; modification.userName = user; modification.comment = comment; modification.revision = revision; modification.folderName = folder; modification.fileName = file; modification.type = type; return modification; } private static void assertArraysEquals(Object[] expected, Object[] actual) { assertEquals("array lengths mismatch!", expected.length, actual.length); for (int i = 0; i < expected.length; i++) { assertEquals(expected[i], actual[i]); } } } ------------------------------------------------------- This SF.Net email is sponsored by: IBM Linux Tutorials Free Linux tutorial presented by Daniel Robbins, President and CEO of GenToo technologies. Learn everything from fundamentals to system administration.http://ads.osdn.com/?ad_id=1470&alloc_id=3638&op=click _______________________________________________ Cruisecontrol-devel mailing list Cru...@li... https://lists.sourceforge.net/lists/listinfo/cruisecontrol-devel |