Browse Source

ENH: Rewrite CTest Update implementation

This adds a new VCS update implementation to the cmCTestVC hierarchy and
removes it from cmCTestUpdateHandler.  The new implementation has the
following advantages:

  - Factorized implementation instead of monolithic function
  - Logs vcs tool output as it is parsed (less memory, inline messages)
  - Uses one global svn log instead of one log per file
  - Reports changes on cvs branches (instead of latest trunk change)
  - Generates simpler Update.xml (only one Directory element per dir)

Shared components of the new implementation appear in cmCTestVC and may
be re-used by subclasses for other VCS tools in the future.
Brad King 16 years ago
parent
commit
80282b749f

+ 296 - 0
Source/CTest/cmCTestCVS.cxx

@@ -16,6 +16,12 @@
 =========================================================================*/
 #include "cmCTestCVS.h"
 
+#include "cmCTest.h"
+#include "cmSystemTools.h"
+#include "cmXMLSafe.h"
+
+#include <cmsys/RegularExpression.hxx>
+
 //----------------------------------------------------------------------------
 cmCTestCVS::cmCTestCVS(cmCTest* ct, std::ostream& log): cmCTestVC(ct, log)
 {
@@ -25,3 +31,293 @@ cmCTestCVS::cmCTestCVS(cmCTest* ct, std::ostream& log): cmCTestVC(ct, log)
 cmCTestCVS::~cmCTestCVS()
 {
 }
+
+//----------------------------------------------------------------------------
+class cmCTestCVS::UpdateParser: public cmCTestVC::LineParser
+{
+public:
+  UpdateParser(cmCTestCVS* cvs, const char* prefix): CVS(cvs)
+    {
+    this->SetLog(&cvs->Log, prefix);
+    // See "man cvs", section "update output".
+    this->RegexFileUpdated.compile("^([UP])  *(.*)");
+    this->RegexFileModified.compile("^([MRA])  *(.*)");
+    this->RegexFileConflicting.compile("^([C])  *(.*)");
+    this->RegexFileRemoved1.compile(
+      "cvs update: `?([^']*)'? is no longer in the repository");
+    this->RegexFileRemoved2.compile(
+      "cvs update: warning: `?([^']*)'? is not \\(any longer\\) pertinent");
+    }
+private:
+  cmCTestCVS* CVS;
+  cmsys::RegularExpression RegexFileUpdated;
+  cmsys::RegularExpression RegexFileModified;
+  cmsys::RegularExpression RegexFileConflicting;
+  cmsys::RegularExpression RegexFileRemoved1;
+  cmsys::RegularExpression RegexFileRemoved2;
+
+  virtual bool ProcessLine()
+    {
+    if(this->RegexFileUpdated.find(this->Line))
+      {
+      this->DoFile(PathUpdated, this->RegexFileUpdated.match(2));
+      }
+    else if(this->RegexFileModified.find(this->Line))
+      {
+      this->DoFile(PathModified, this->RegexFileModified.match(2));
+      }
+    else if(this->RegexFileConflicting.find(this->Line))
+      {
+      this->DoFile(PathConflicting, this->RegexFileConflicting.match(2));
+      }
+    else if(this->RegexFileRemoved1.find(this->Line))
+      {
+      this->DoFile(PathUpdated, this->RegexFileRemoved1.match(1));
+      }
+    else if(this->RegexFileRemoved2.find(this->Line))
+      {
+      this->DoFile(PathUpdated, this->RegexFileRemoved2.match(1));
+      }
+    return true;
+    }
+
+  void DoFile(PathStatus status, std::string const& file)
+    {
+    std::string dir = cmSystemTools::GetFilenamePath(file);
+    std::string name = cmSystemTools::GetFilenameName(file);
+    this->CVS->Dirs[dir][name] = status;
+    }
+};
+
+//----------------------------------------------------------------------------
+bool cmCTestCVS::UpdateImpl()
+{
+  // Get user-specified update options.
+  std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
+  if(opts.empty())
+    {
+    opts = this->CTest->GetCTestConfiguration("CVSUpdateOptions");
+    if(opts.empty())
+      {
+      opts = "-dP";
+      }
+    }
+  std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str());
+
+  // Specify the start time for nightly testing.
+  if(this->CTest->GetTestModel() == cmCTest::NIGHTLY)
+    {
+    args.push_back("-D" + this->GetNightlyTime() + " UTC");
+    }
+
+  // Run "cvs update" to update the work tree.
+  std::vector<char const*> cvs_update;
+  cvs_update.push_back(this->CommandLineTool.c_str());
+  cvs_update.push_back("-z3");
+  cvs_update.push_back("update");
+  for(std::vector<cmStdString>::const_iterator ai = args.begin();
+      ai != args.end(); ++ai)
+    {
+    cvs_update.push_back(ai->c_str());
+    }
+  cvs_update.push_back(0);
+
+  UpdateParser out(this, "up-out> ");
+  UpdateParser err(this, "up-err> ");
+  return this->RunUpdateCommand(&cvs_update[0], &out, &err);
+}
+
+//----------------------------------------------------------------------------
+class cmCTestCVS::LogParser: public cmCTestVC::LineParser
+{
+public:
+  typedef cmCTestCVS::Revision Revision;
+  LogParser(cmCTestCVS* cvs, const char* prefix, std::vector<Revision>& revs):
+    CVS(cvs), Revisions(revs), Section(SectionHeader)
+    {
+    this->SetLog(&cvs->Log, prefix),
+    this->RegexRevision.compile("^revision +([^ ]*) *$");
+    this->RegexBranches.compile("^branches: .*$");
+    this->RegexPerson.compile("^date: +([^;]+); +author: +([^;]+);");
+    }
+private:
+  cmCTestCVS* CVS;
+  std::vector<Revision>& Revisions;
+  cmsys::RegularExpression RegexRevision;
+  cmsys::RegularExpression RegexBranches;
+  cmsys::RegularExpression RegexPerson;
+  enum SectionType { SectionHeader, SectionRevisions, SectionEnd };
+  SectionType Section;
+  Revision Rev;
+
+  virtual bool ProcessLine()
+    {
+    if(this->Line == ("======================================="
+                      "======================================"))
+      {
+      // This line ends the revision list.
+      if(this->Section == SectionRevisions)
+        {
+        this->FinishRevision();
+        }
+      this->Section = SectionEnd;
+      }
+    else if(this->Line == "----------------------------")
+      {
+      // This line divides revisions from the header and each other.
+      if(this->Section == SectionHeader)
+        {
+        this->Section = SectionRevisions;
+        }
+      else if(this->Section == SectionRevisions)
+        {
+        this->FinishRevision();
+        }
+      }
+    else if(this->Section == SectionRevisions)
+      {
+      if(!this->Rev.Log.empty())
+        {
+        // Continue the existing log.
+        this->Rev.Log += this->Line;
+        this->Rev.Log += "\n";
+        }
+      else if(this->Rev.Rev.empty() && this->RegexRevision.find(this->Line))
+        {
+        this->Rev.Rev = this->RegexRevision.match(1);
+        }
+      else if(this->Rev.Date.empty() && this->RegexPerson.find(this->Line))
+        {
+        this->Rev.Date = this->RegexPerson.match(1);
+        this->Rev.Author = this->RegexPerson.match(2);
+        }
+      else if(!this->RegexBranches.find(this->Line))
+        {
+        // Start the log.
+        this->Rev.Log += this->Line;
+        this->Rev.Log += "\n";
+        }
+      }
+    return this->Section != SectionEnd;
+    }
+
+  void FinishRevision()
+    {
+    if(!this->Rev.Rev.empty())
+      {
+      // Record this revision.
+      this->CVS->Log << "Found revision " << this->Rev.Rev << "\n"
+                     << "  author = " << this->Rev.Author << "\n"
+                     << "  date = " << this->Rev.Date << "\n";
+      this->Revisions.push_back(this->Rev);
+
+      // We only need two revisions.
+      if(this->Revisions.size() >= 2)
+        {
+        this->Section = SectionEnd;
+        }
+      }
+    this->Rev = Revision();
+    }
+};
+
+//----------------------------------------------------------------------------
+std::string cmCTestCVS::ComputeBranchFlag(std::string const& dir)
+{
+  // Compute the tag file location for this directory.
+  std::string tagFile = this->SourceDirectory;
+  if(!dir.empty())
+    {
+    tagFile += "/";
+    tagFile += dir;
+    }
+  tagFile += "/CVS/Tag";
+
+  // Lookup the branch in the tag file, if any.
+  std::string tagLine;
+  std::ifstream tagStream(tagFile.c_str());
+  if(cmSystemTools::GetLineFromStream(tagStream, tagLine) &&
+     tagLine.size() > 1 && tagLine[0] == 'T')
+    {
+    // Use the branch specified in the tag file.
+    std::string flag = "-r";
+    flag += tagLine.substr(1);
+    return flag;
+    }
+  else
+    {
+    // Use the default branch.
+    return "-b";
+    }
+}
+
+//----------------------------------------------------------------------------
+void cmCTestCVS::LoadRevisions(std::string const& file,
+                               const char* branchFlag,
+                               std::vector<Revision>& revisions)
+{
+  cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush);
+
+  // Run "cvs log" to get revisions of this file on this branch.
+  const char* cvs = this->CommandLineTool.c_str();
+  const char* cvs_log[] =
+    {cvs, "log", "-N", "-d<now", branchFlag, file.c_str(), 0};
+
+  LogParser out(this, "log-out> ", revisions);
+  OutputLogger err(this->Log, "log-err> ");
+  this->RunChild(cvs_log, &out, &err);
+}
+
+//----------------------------------------------------------------------------
+void cmCTestCVS::WriteXMLDirectory(std::ostream& xml,
+                                   std::string const& path,
+                                   Directory const& dir)
+{
+  const char* slash = path.empty()? "":"/";
+  xml << "\t<Directory>\n"
+      << "\t\t<Name>" << cmXMLSafe(path) << "</Name>\n";
+
+  // Lookup the branch checked out in the working tree.
+  std::string branchFlag = this->ComputeBranchFlag(path);
+
+  // Load revisions and write an entry for each file in this directory.
+  std::vector<Revision> revisions;
+  for(Directory::const_iterator fi = dir.begin(); fi != dir.end(); ++fi)
+    {
+    std::string full = path + slash + fi->first;
+
+    // Load two real or unknown revisions.
+    revisions.clear();
+    if(fi->second != PathUpdated)
+      {
+      // For local modifications the current rev is unknown and the
+      // prior rev is the latest from cvs.
+      revisions.push_back(this->Unknown);
+      }
+    this->LoadRevisions(full, branchFlag.c_str(), revisions);
+    revisions.resize(2, this->Unknown);
+
+    // Write the entry for this file with these revisions.
+    File f(fi->second, &revisions[0], &revisions[1]);
+    this->WriteXMLEntry(xml, path, fi->first, full, f);
+    }
+  xml << "\t</Directory>\n";
+}
+
+//----------------------------------------------------------------------------
+bool cmCTestCVS::WriteXMLUpdates(std::ostream& xml)
+{
+  cmCTestLog(this->CTest, HANDLER_OUTPUT,
+             "   Gathering version information (one . per updated file):\n"
+             "    " << std::flush);
+
+  for(std::map<cmStdString, Directory>::const_iterator
+        di = this->Dirs.begin(); di != this->Dirs.end(); ++di)
+    {
+    this->WriteXMLDirectory(xml, di->first, di->second);
+    }
+
+  cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl);
+
+  return true;
+}

+ 21 - 0
Source/CTest/cmCTestCVS.h

@@ -30,6 +30,27 @@ public:
   cmCTestCVS(cmCTest* ctest, std::ostream& log);
 
   virtual ~cmCTestCVS();
+
+private:
+  // Implement cmCTestVC internal API.
+  virtual bool UpdateImpl();
+  virtual bool WriteXMLUpdates(std::ostream& xml);
+
+  // Update status for files in each directory.
+  class Directory: public std::map<cmStdString, PathStatus> {};
+  std::map<cmStdString, Directory> Dirs;
+
+  std::string ComputeBranchFlag(std::string const& dir);
+  void LoadRevisions(std::string const& file, const char* branchFlag,
+                     std::vector<Revision>& revisions);
+  void WriteXMLDirectory(std::ostream& xml, std::string const& path,
+                         Directory const& dir);
+
+  // Parsing helper classes.
+  class UpdateParser;
+  class LogParser;
+  friend class UpdateParser;
+  friend class LogParser;
 };
 
 #endif

+ 390 - 0
Source/CTest/cmCTestSVN.cxx

@@ -17,12 +17,16 @@
 #include "cmCTestSVN.h"
 
 #include "cmCTest.h"
+#include "cmSystemTools.h"
+#include "cmXMLParser.h"
+#include "cmXMLSafe.h"
 
 #include <cmsys/RegularExpression.hxx>
 
 //----------------------------------------------------------------------------
 cmCTestSVN::cmCTestSVN(cmCTest* ct, std::ostream& log): cmCTestVC(ct, log)
 {
+  this->PriorRev = this->Unknown;
 }
 
 //----------------------------------------------------------------------------
@@ -114,6 +118,7 @@ void cmCTestSVN::NoteOldRevision()
   this->Log << "Revision before update: " << this->OldRevision << "\n";
   cmCTestLog(this->CTest, HANDLER_OUTPUT, "   Old revision of repository is: "
              << this->OldRevision << "\n");
+  this->PriorRev.Rev = this->OldRevision;
 }
 
 //----------------------------------------------------------------------------
@@ -124,6 +129,7 @@ void cmCTestSVN::NoteNewRevision()
   cmCTestLog(this->CTest, HANDLER_OUTPUT, "   New revision of repository is: "
              << this->NewRevision << "\n");
 
+  // this->Root = ""; // uncomment to test GuessBase
   this->Log << "URL = " << this->URL << "\n";
   this->Log << "Root = " << this->Root << "\n";
 
@@ -136,3 +142,387 @@ void cmCTestSVN::NoteNewRevision()
     }
   this->Log << "Base = " << this->Base << "\n";
 }
+
+//----------------------------------------------------------------------------
+void cmCTestSVN::GuessBase(std::vector<Change> const& changes)
+{
+  // Subversion did not give us a good repository root so we need to
+  // guess the base path from the URL and the paths in a revision with
+  // changes under it.
+
+  // Consider each possible URL suffix from longest to shortest.
+  for(std::string::size_type slash = this->URL.find('/');
+      this->Base.empty() && slash != std::string::npos;
+      slash = this->URL.find('/', slash+1))
+    {
+    // If the URL suffix is a prefix of at least one path then it is the base.
+    std::string base = cmCTest::DecodeURL(this->URL.substr(slash));
+    for(std::vector<Change>::const_iterator ci = changes.begin();
+        this->Base.empty() && ci != changes.end(); ++ci)
+      {
+      if(cmCTestSVNPathStarts(ci->Path, base))
+        {
+        this->Base = base;
+        }
+      }
+    }
+
+  // We always append a slash so that we know paths beginning in the
+  // base lie under its path.  If no base was found then the working
+  // tree must be a checkout of the entire repo and this will match
+  // the leading slash in all paths.
+  this->Base += "/";
+
+  this->Log << "Guessed Base = " << this->Base << "\n";
+}
+
+//----------------------------------------------------------------------------
+const char* cmCTestSVN::LocalPath(std::string const& path)
+{
+  if(path.size() > this->Base.size() &&
+     strncmp(path.c_str(), this->Base.c_str(), this->Base.size()) == 0)
+    {
+    // This path lies under the base, so return a relative path.
+    return path.c_str() + this->Base.size();
+    }
+  else
+    {
+    // This path does not lie under the base, so ignore it.
+    return 0;
+    }
+}
+
+//----------------------------------------------------------------------------
+class cmCTestSVN::UpdateParser: public cmCTestVC::LineParser
+{
+public:
+  UpdateParser(cmCTestSVN* svn, const char* prefix): SVN(svn)
+    {
+    this->SetLog(&svn->Log, prefix);
+    this->RegexUpdate.compile("^([ADUCGE ])([ADUCGE ])[B ] +(.+)$");
+    }
+private:
+  cmCTestSVN* SVN;
+  cmsys::RegularExpression RegexUpdate;
+
+  bool ProcessLine()
+    {
+    if(this->RegexUpdate.find(this->Line))
+      {
+      this->DoPath(this->RegexUpdate.match(1)[0],
+                   this->RegexUpdate.match(2)[0],
+                   this->RegexUpdate.match(3));
+      }
+    return true;
+    }
+
+  void DoPath(char path_status, char prop_status, std::string const& path)
+    {
+    char status = (path_status != ' ')? path_status : prop_status;
+    std::string dir = cmSystemTools::GetFilenamePath(path);
+    std::string name = cmSystemTools::GetFilenameName(path);
+    // See "svn help update".
+    switch(status)
+      {
+      case 'G':
+        this->SVN->Dirs[dir][name].Status = PathModified;
+        break;
+      case 'C':
+        this->SVN->Dirs[dir][name].Status = PathConflicting;
+        break;
+      case 'A': case 'D': case 'U':
+        this->SVN->Dirs[dir][name].Status = PathUpdated;
+        break;
+      case 'E': // TODO?
+      case '?': case ' ': default:
+        break;
+      }
+    }
+};
+
+//----------------------------------------------------------------------------
+bool cmCTestSVN::UpdateImpl()
+{
+  // Get user-specified update options.
+  std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
+  if(opts.empty())
+    {
+    opts = this->CTest->GetCTestConfiguration("SVNUpdateOptions");
+    }
+  std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str());
+
+  // Specify the start time for nightly testing.
+  if(this->CTest->GetTestModel() == cmCTest::NIGHTLY)
+    {
+    args.push_back("-r{" + this->GetNightlyTime() + " +0000}");
+    }
+
+  std::vector<char const*> svn_update;
+  svn_update.push_back(this->CommandLineTool.c_str());
+  svn_update.push_back("update");
+  svn_update.push_back("--non-interactive");
+  for(std::vector<cmStdString>::const_iterator ai = args.begin();
+      ai != args.end(); ++ai)
+    {
+    svn_update.push_back(ai->c_str());
+    }
+  svn_update.push_back(0);
+
+  UpdateParser out(this, "up-out> ");
+  OutputLogger err(this->Log, "up-err> ");
+  return this->RunUpdateCommand(&svn_update[0], &out, &err);
+}
+
+//----------------------------------------------------------------------------
+class cmCTestSVN::LogParser: public OutputLogger, private cmXMLParser
+{
+public:
+  LogParser(cmCTestSVN* svn, const char* prefix):
+    OutputLogger(svn->Log, prefix), SVN(svn) { this->InitializeParser(); }
+  ~LogParser() { this->CleanupParser(); }
+private:
+  cmCTestSVN* SVN;
+
+  typedef cmCTestSVN::Revision Revision;
+  typedef cmCTestSVN::Change Change;
+  Revision Rev;
+  std::vector<Change> Changes;
+  Change CurChange;
+  std::vector<char> CData;
+
+  virtual bool ProcessChunk(const char* data, int length)
+    {
+    this->OutputLogger::ProcessChunk(data, length);
+    this->ParseChunk(data, length);
+    return true;
+    }
+
+  virtual void StartElement(const char* name, const char** atts)
+    {
+    this->CData.clear();
+    if(strcmp(name, "logentry") == 0)
+      {
+      this->Rev = Revision();
+      if(const char* rev = this->FindAttribute(atts, "revision"))
+        {
+        this->Rev.Rev = rev;
+        }
+      this->Changes.clear();
+      }
+    else if(strcmp(name, "path") == 0)
+      {
+      this->CurChange = Change();
+      if(const char* action = this->FindAttribute(atts, "action"))
+        {
+        this->CurChange.Action = action[0];
+        }
+      }
+    }
+
+  virtual void CharacterDataHandler(const char* data, int length)
+    {
+    this->CData.insert(this->CData.end(), data, data+length);
+    }
+
+  virtual void EndElement(const char* name)
+    {
+    if(strcmp(name, "logentry") == 0)
+      {
+      this->SVN->DoRevision(this->Rev, this->Changes);
+      }
+    else if(strcmp(name, "path") == 0 && !this->CData.empty())
+      {
+      this->CurChange.Path.assign(&this->CData[0], this->CData.size());
+      this->Changes.push_back(this->CurChange);
+      }
+    else if(strcmp(name, "author") == 0 && !this->CData.empty())
+      {
+      this->Rev.Author.assign(&this->CData[0], this->CData.size());
+      }
+    else if(strcmp(name, "date") == 0 && !this->CData.empty())
+      {
+      this->Rev.Date.assign(&this->CData[0], this->CData.size());
+      }
+    else if(strcmp(name, "msg") == 0 && !this->CData.empty())
+      {
+      this->Rev.Log.assign(&this->CData[0], this->CData.size());
+      }
+    this->CData.clear();
+    }
+
+  virtual void ReportError(int, int, const char* msg)
+    {
+    this->SVN->Log << "Error parsing svn log xml: " << msg << "\n";
+    }
+};
+
+//----------------------------------------------------------------------------
+void cmCTestSVN::LoadRevisions()
+{
+  cmCTestLog(this->CTest, HANDLER_OUTPUT,
+             "   Gathering version information (one . per revision):\n"
+             "    " << std::flush);
+
+  // We are interested in every revision included in the update.
+  std::string revs;
+  if(atoi(this->OldRevision.c_str()) < atoi(this->NewRevision.c_str()))
+    {
+    revs = "-r" + this->OldRevision + ":" + this->NewRevision;
+    }
+  else
+    {
+    revs = "-r" + this->NewRevision;
+    }
+
+  // Run "svn log" to get all global revisions of interest.
+  const char* svn = this->CommandLineTool.c_str();
+  const char* svn_log[] = {svn, "log", "--xml", "-v", revs.c_str(), 0};
+  {
+  LogParser out(this, "log-out> ");
+  OutputLogger err(this->Log, "log-err> ");
+  this->RunChild(svn_log, &out, &err);
+  }
+  cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl);
+}
+
+//----------------------------------------------------------------------------
+void cmCTestSVN::DoRevision(Revision const& revision,
+                            std::vector<Change> const& changes)
+{
+  // Guess the base checkout path from the changes if necessary.
+  if(this->Base.empty() && !changes.empty())
+    {
+    this->GuessBase(changes);
+    }
+
+  // Indicate we found a revision.
+  cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush);
+
+  // Ignore changes in the old revision.
+  if(revision.Rev == this->OldRevision)
+    {
+    this->PriorRev = revision;
+    return;
+    }
+
+  // Store the revision.
+  this->Revisions.push_back(revision);
+
+  // Report this revision.
+  Revision const& rev = this->Revisions.back();
+  this->Log << "Found revision " << rev.Rev << "\n"
+            << "  author = " << rev.Author << "\n"
+            << "  date = " << rev.Date << "\n";
+
+  // Update information about revisions of the changed files.
+  for(std::vector<Change>::const_iterator ci = changes.begin();
+      ci != changes.end(); ++ci)
+    {
+    if(const char* local = this->LocalPath(ci->Path))
+      {
+      std::string dir = cmSystemTools::GetFilenamePath(local);
+      std::string name = cmSystemTools::GetFilenameName(local);
+      File& file = this->Dirs[dir][name];
+      file.PriorRev = file.Rev? file.Rev : &this->PriorRev;
+      file.Rev = &rev;
+      this->Log << "  " << ci->Action << " " << local << " " << "\n";
+      }
+    }
+}
+
+//----------------------------------------------------------------------------
+class cmCTestSVN::StatusParser: public cmCTestVC::LineParser
+{
+public:
+  StatusParser(cmCTestSVN* svn, const char* prefix): SVN(svn)
+    {
+    this->SetLog(&svn->Log, prefix);
+    this->RegexStatus.compile("^([ACDIMRX?!~ ])([CM ])[ L]... +(.+)$");
+    }
+private:
+  cmCTestSVN* SVN;
+  cmsys::RegularExpression RegexStatus;
+  bool ProcessLine()
+    {
+    if(this->RegexStatus.find(this->Line))
+      {
+      this->DoPath(this->RegexStatus.match(1)[0],
+                   this->RegexStatus.match(2)[0],
+                   this->RegexStatus.match(3));
+      }
+    return true;
+    }
+
+  void DoPath(char path_status, char prop_status, std::string const& path)
+    {
+    char status = (path_status != ' ')? path_status : prop_status;
+    // See "svn help status".
+    switch(status)
+      {
+      case 'M': case '!': case 'A': case 'D': case 'R': case 'X':
+        this->DoPath(PathModified, path);
+        break;
+      case 'C': case '~':
+        this->DoPath(PathConflicting, path);
+        break;
+      case 'I': case '?': case ' ': default:
+        break;
+      }
+    }
+
+  void DoPath(PathStatus status, std::string const& path)
+    {
+    std::string dir = cmSystemTools::GetFilenamePath(path);
+    std::string name = cmSystemTools::GetFilenameName(path);
+    File& file = this->SVN->Dirs[dir][name];
+    file.Status = status;
+    // For local modifications the current rev is unknown and the
+    // prior rev is the latest from svn.
+    if(!file.Rev && !file.PriorRev)
+      {
+      file.PriorRev = &this->SVN->PriorRev;
+      }
+    }
+};
+
+//----------------------------------------------------------------------------
+void cmCTestSVN::LoadModifications()
+{
+  // Run "svn status" which reports local modifications.
+  const char* svn = this->CommandLineTool.c_str();
+  const char* svn_status[] = {svn, "status", "--non-interactive", 0};
+  StatusParser out(this, "status-out> ");
+  OutputLogger err(this->Log, "status-err> ");
+  this->RunChild(svn_status, &out, &err);
+}
+
+//----------------------------------------------------------------------------
+void cmCTestSVN::WriteXMLDirectory(std::ostream& xml,
+                                   std::string const& path,
+                                   Directory const& dir)
+{
+  const char* slash = path.empty()? "":"/";
+  xml << "\t<Directory>\n"
+      << "\t\t<Name>" << cmXMLSafe(path) << "</Name>\n";
+  for(Directory::const_iterator fi = dir.begin(); fi != dir.end(); ++fi)
+    {
+    std::string full = path + slash + fi->first;
+    this->WriteXMLEntry(xml, path, fi->first, full, fi->second);
+    }
+  xml << "\t</Directory>\n";
+}
+
+//----------------------------------------------------------------------------
+bool cmCTestSVN::WriteXMLUpdates(std::ostream& xml)
+{
+  this->LoadRevisions();
+  this->LoadModifications();
+
+  for(std::map<cmStdString, Directory>::const_iterator
+        di = this->Dirs.begin(); di != this->Dirs.end(); ++di)
+    {
+    this->WriteXMLDirectory(xml, di->first, di->second);
+    }
+
+  return true;
+}

+ 36 - 2
Source/CTest/cmCTestSVN.h

@@ -31,13 +31,25 @@ public:
 
   virtual ~cmCTestSVN();
 
-  int GetOldRevision() { return atoi(this->OldRevision.c_str()); }
-  int GetNewRevision() { return atoi(this->NewRevision.c_str()); }
 private:
   // Implement cmCTestVC internal API.
   virtual void CleanupImpl();
   virtual void NoteOldRevision();
   virtual void NoteNewRevision();
+  virtual bool UpdateImpl();
+  virtual bool WriteXMLUpdates(std::ostream& xml);
+
+  /** Represent a subversion-reported action for one path in a revision.  */
+  struct Change
+  {
+    char Action;
+    std::string Path;
+    Change(): Action('?') {}
+  };
+
+  // Update status for files in each directory.
+  class Directory: public std::map<cmStdString, File> {};
+  std::map<cmStdString, Directory> Dirs;
 
   // Old and new repository revisions.
   std::string OldRevision;
@@ -52,11 +64,33 @@ private:
   // Directory under repository root checked out in working tree.
   std::string Base;
 
+  // Information known about old revision.
+  Revision PriorRev;
+
+  // Information about revisions from a svn log.
+  std::list<Revision> Revisions;
+
   std::string LoadInfo();
+  void LoadModifications();
+  void LoadRevisions();
+
+  void GuessBase(std::vector<Change> const& changes);
+  const char* LocalPath(std::string const& path);
+
+  void DoRevision(Revision const& revision,
+                  std::vector<Change> const& changes);
+  void WriteXMLDirectory(std::ostream& xml, std::string const& path,
+                         Directory const& dir);
 
   // Parsing helper classes.
   class InfoParser;
+  class LogParser;
+  class StatusParser;
+  class UpdateParser;
   friend class InfoParser;
+  friend class LogParser;
+  friend class StatusParser;
+  friend class UpdateParser;
 };
 
 #endif

+ 20 - 637
Source/CTest/cmCTestUpdateHandler.cxx

@@ -63,130 +63,6 @@ static const char* cmCTestUpdateHandlerUpdateToString(int type)
   return cmCTestUpdateHandlerUpdateStrings[type];
 }
 
-//----------------------------------------------------------------------
-//**********************************************************************
-class cmCTestUpdateHandlerSVNXMLParser : public cmXMLParser
-{
-public:
-  struct t_CommitLog
-    {
-    int Revision;
-    std::string Author;
-    std::string Date;
-    std::string Message;
-    };
-  cmCTestUpdateHandlerSVNXMLParser(cmCTestUpdateHandler* up)
-    : cmXMLParser(), UpdateHandler(up), MinRevision(-1), MaxRevision(-1)
-    {
-    }
-
-  int Parse(const char* str)
-    {
-    this->MinRevision = -1;
-    this->MaxRevision = -1;
-    int res = this->cmXMLParser::Parse(str);
-    if ( this->MinRevision == -1 || this->MaxRevision == -1 )
-      {
-      return 0;
-      }
-    return res;
-    }
-
-  typedef std::vector<t_CommitLog> t_VectorOfCommits;
-
-  t_VectorOfCommits* GetCommits() { return &this->Commits; }
-  int GetMinRevision() { return this->MinRevision; }
-  int GetMaxRevision() { return this->MaxRevision; }
-
-protected:
-  void StartElement(const char* name, const char** atts)
-    {
-    if ( strcmp(name, "logentry") == 0 )
-      {
-      this->CommitLog = t_CommitLog();
-      const char* rev = this->FindAttribute(atts, "revision");
-      if ( rev)
-        {
-        this->CommitLog.Revision = atoi(rev);
-        if ( this->MinRevision < 0 ||
-          this->MinRevision > this->CommitLog.Revision )
-          {
-          this->MinRevision = this->CommitLog.Revision;
-          }
-        if ( this->MaxRevision < 0 ||
-          this->MaxRevision < this->CommitLog.Revision )
-          {
-          this->MaxRevision = this->CommitLog.Revision;
-          }
-        }
-      }
-    this->CharacterData.erase(
-      this->CharacterData.begin(), this->CharacterData.end());
-    }
-  void EndElement(const char* name)
-    {
-    if ( strcmp(name, "logentry") == 0 )
-      {
-      cmCTestLog(this->UpdateHandler->GetCTestInstance(),
-        HANDLER_VERBOSE_OUTPUT,
-        "\tRevision: " << this->CommitLog.Revision<< std::endl
-        << "\tAuthor:   " << this->CommitLog.Author.c_str() << std::endl
-        << "\tDate:     " << this->CommitLog.Date.c_str() << std::endl
-        << "\tMessage:  " << this->CommitLog.Message.c_str() << std::endl);
-      this->Commits.push_back(this->CommitLog);
-      }
-    else if ( strcmp(name, "author") == 0 )
-      {
-      this->CommitLog.Author.assign(&(*(this->CharacterData.begin())),
-        this->CharacterData.size());
-      }
-    else if ( strcmp(name, "date") == 0 )
-      {
-      this->CommitLog.Date.assign(&(*(this->CharacterData.begin())),
-        this->CharacterData.size());
-      }
-    else if ( strcmp(name, "msg") == 0 )
-      {
-      this->CommitLog.Message.assign(&(*(this->CharacterData.begin())),
-        this->CharacterData.size());
-      }
-    this->CharacterData.erase(this->CharacterData.begin(),
-      this->CharacterData.end());
-    }
-  void CharacterDataHandler(const char* data, int length)
-    {
-    this->CharacterData.insert(this->CharacterData.end(), data, data+length);
-    }
-  const char* FindAttribute( const char** atts, const char* attribute )
-    {
-    if ( !atts || !attribute )
-      {
-      return 0;
-      }
-    const char **atr = atts;
-    while ( *atr && **atr && **(atr+1) )
-      {
-      if ( strcmp(*atr, attribute) == 0 )
-        {
-        return *(atr+1);
-        }
-      atr+=2;
-      }
-    return 0;
-    }
-
-private:
-  std::vector<char> CharacterData;
-  cmCTestUpdateHandler* UpdateHandler;
-  t_CommitLog CommitLog;
-
-  t_VectorOfCommits Commits;
-  int MinRevision;
-  int MaxRevision;
-};
-//**********************************************************************
-//----------------------------------------------------------------------
-
 class cmCTestUpdateHandlerLocale
 {
 public:
@@ -280,17 +156,10 @@ int cmCTestUpdateHandler::DetermineType(const char* cmd, const char* type)
 //functions and commented...
 int cmCTestUpdateHandler::ProcessHandler()
 {
-  int count = 0;
-  std::string::size_type cc, kk;
-  std::string goutput;
-  std::string errors;
-
   // Make sure VCS tool messages are in English so we can parse them.
   cmCTestUpdateHandlerLocale fixLocale;
   static_cast<void>(fixLocale);
 
-  int retVal = 0;
-
   // Get source dir
   const char* sourceDirectory = this->GetOption("SourceDirectory");
   if ( !sourceDirectory )
@@ -340,64 +209,9 @@ int cmCTestUpdateHandler::ProcessHandler()
   vc->SetCommandLineTool(this->UpdateCommand);
   vc->SetSourceDirectory(sourceDirectory);
 
-  // And update options
-  std::string updateOptions
-    = this->CTest->GetCTestConfiguration("UpdateOptions");
-  if ( updateOptions.empty() )
-    {
-    switch (this->UpdateType)
-      {
-    case cmCTestUpdateHandler::e_CVS:
-      updateOptions = this->CTest->GetCTestConfiguration("CVSUpdateOptions");
-      if ( updateOptions.empty() )
-        {
-        updateOptions = "-dP";
-        }
-      break;
-    case cmCTestUpdateHandler::e_SVN:
-      updateOptions = this->CTest->GetCTestConfiguration("SVNUpdateOptions");
-      break;
-      }
-    }
-
-  // Get update time
-  std::string extra_update_opts;
-  if ( this->CTest->GetTestModel() == cmCTest::NIGHTLY )
-    {
-    std::string today_update_date = vc->GetNightlyTime();
-
-    // TODO: SVN
-    switch ( this->UpdateType )
-      {
-    case cmCTestUpdateHandler::e_CVS:
-      extra_update_opts += "-D \"" + today_update_date +" UTC\"";
-      break;
-    case cmCTestUpdateHandler::e_SVN:
-      extra_update_opts += "-r \"{" + today_update_date +" +0000}\"";
-      break;
-      }
-    }
-
   // Cleanup the working tree.
   vc->Cleanup();
 
-  bool res = true;
-
-  // CVS variables
-  // SVN variables
-  int svn_current_revision = 0;
-  int svn_latest_revision = 0;
-  int svn_use_status = 0;
-
-  // Get initial repository information if that is possible.
-  vc->MarkOldRevision();
-  if(this->UpdateType == e_SVN)
-    {
-    svn_current_revision =
-      static_cast<cmCTestSVN*>(vc.get())->GetOldRevision();
-    }
-
-
   //
   // Now update repository and remember what files were updated
   //
@@ -413,56 +227,7 @@ int cmCTestUpdateHandler::ProcessHandler()
     static_cast<unsigned int>(cmSystemTools::GetTime());
   double elapsed_time_start = cmSystemTools::GetTime();
 
-  std::string command;
-  cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "* Update repository: "
-    << command.c_str() << std::endl);
-  if ( !this->CTest->GetShowOnly() )
-    {
-    command = "";
-    switch( this->UpdateType )
-      {
-    case cmCTestUpdateHandler::e_CVS:
-      command = "\""+this->UpdateCommand+"\" -z3 update " + updateOptions +
-        " " + extra_update_opts;
-      ofs << "* Update repository: " << std::endl;
-      ofs << "  Command: " << command.c_str() << std::endl;
-      res = this->CTest->RunCommand(command.c_str(), &goutput, &errors,
-        &retVal, sourceDirectory, 0 /*this->TimeOut*/);
-      ofs << "  Output: " << goutput.c_str() << std::endl;
-      ofs << "  Errors: " << errors.c_str() << std::endl;
-      break;
-    case cmCTestUpdateHandler::e_SVN:
-        {
-        std::string partialOutput;
-        command = "\"" + this->UpdateCommand + "\" update " + updateOptions +
-          " " + extra_update_opts;
-        ofs << "* Update repository: " << std::endl;
-        ofs << "  Command: " << command.c_str() << std::endl;
-        bool res1 = this->CTest->RunCommand(command.c_str(), &partialOutput,
-          &errors,
-          &retVal, sourceDirectory, 0 /*this->TimeOut*/);
-        ofs << "  Output: " << partialOutput.c_str() << std::endl;
-        ofs << "  Errors: " << errors.c_str() << std::endl;
-        goutput = partialOutput;
-        command = "\"" + this->UpdateCommand + "\" status";
-        ofs << "* Status repository: " << std::endl;
-        ofs << "  Command: " << command.c_str() << std::endl;
-        res = this->CTest->RunCommand(command.c_str(), &partialOutput,
-          &errors, &retVal, sourceDirectory, 0 /*this->TimeOut*/);
-        ofs << "  Output: " << partialOutput.c_str() << std::endl;
-        ofs << "  Errors: " << errors.c_str() << std::endl;
-        goutput += partialOutput;
-        res = res && res1;
-        ofs << "  Total output of update: " << goutput.c_str() << std::endl;
-        }
-      }
-    if ( ofs )
-      {
-      ofs << "--- Update repository ---" << std::endl;
-      ofs << goutput << std::endl;
-      }
-    }
-  bool updateProducedError = !res || retVal;
+  bool updated = vc->Update();
 
   os << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
     << "<Update mode=\"Client\" Generator=\"ctest-"
@@ -474,404 +239,31 @@ int cmCTestUpdateHandler::ProcessHandler()
     << this->CTest->GetTestModelString() << "</BuildStamp>" << std::endl;
   os << "\t<StartDateTime>" << start_time << "</StartDateTime>\n"
     << "\t<StartTime>" << start_time_time << "</StartTime>\n"
-    << "\t<UpdateCommand>" << cmXMLSafe(command)
+    << "\t<UpdateCommand>" << cmXMLSafe(vc->GetUpdateCommandLine())
     << "</UpdateCommand>\n"
     << "\t<UpdateType>" << cmXMLSafe(
       cmCTestUpdateHandlerUpdateToString(this->UpdateType))
     << "</UpdateType>\n";
 
-  // Even though it failed, we may have some useful information. Try to
-  // continue...
-  std::vector<cmStdString> lines;
-  cmSystemTools::Split(goutput.c_str(), lines);
-  std::vector<cmStdString> errLines;
-  cmSystemTools::Split(errors.c_str(), errLines);
-  lines.insert(lines.end(), errLines.begin(), errLines.end());
-
-  // CVS style regular expressions
-  cmsys::RegularExpression cvs_date_author_regex(
-    "^date: +([^;]+); +author: +([^;]+); +state: +[^;]+;");
-  cmsys::RegularExpression cvs_revision_regex("^revision +([^ ]*) *$");
-  cmsys::RegularExpression cvs_end_of_file_regex(
-    "^=========================================="
-    "===================================$");
-  cmsys::RegularExpression cvs_end_of_comment_regex(
-    "^----------------------------$");
-
-  // Subversion style regular expressions
-  cmsys::RegularExpression svn_status_line_regex(
-    "^ *([0-9]+)  *([0-9]+)  *([^ ]+)  *([^ ][^\t\r\n]*)[ \t\r\n]*$");
-  cmsys::RegularExpression svn_latest_revision_regex(
-    "(Updated to|At) revision ([0-9]+)\\.");
-
-  cmsys::RegularExpression file_removed_line(
-    "cvs update: `?([^']*)'? is no longer in the repository");
-  cmsys::RegularExpression file_removed_line2(
-    "cvs update: warning: `?([^']*)'? is not \\(any longer\\) pertinent");
-  cmsys::RegularExpression file_update_line("([A-Z])  *(.*)");
-  std::string current_path = "<no-path>";
-  bool first_file = true;
-
-  std::set<cmStdString> author_set;
-  int numUpdated = 0;
-  int numModified = 0;
-  int numConflicting = 0;
-
-  // Get final repository information if that is possible.
-  vc->MarkNewRevision();
-  if ( this->UpdateType == cmCTestUpdateHandler::e_SVN )
-    {
-    svn_latest_revision =
-      static_cast<cmCTestSVN*>(vc.get())->GetNewRevision();
-    }
-
-  cmCTestLog(this->CTest, HANDLER_OUTPUT,
-    "   Gathering version information (each . represents one updated file):"
-    << std::endl);
-  int file_count = 0;
-  std::string removed_line;
-  for ( cc= 0; cc < lines.size(); cc ++ )
-    {
-    const char* line = lines[cc].c_str();
-    if ( file_removed_line.find(line) )
-      {
-      removed_line = "D " + file_removed_line.match(1);
-      line = removed_line.c_str();
-      }
-    else if ( file_removed_line2.find(line) )
-      {
-      removed_line = "D " + file_removed_line2.match(1);
-      line = removed_line.c_str();
-      }
-    if ( file_update_line.find(line) )
-      {
-      if ( file_count == 0 )
-        {
-        cmCTestLog(this->CTest, HANDLER_OUTPUT, "    " << std::flush);
-        }
-      cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush);
-      std::string upChar = file_update_line.match(1);
-      std::string upFile = file_update_line.match(2);
-      char mod = upChar[0];
-      bool notLocallyModified = false;
-      if ( mod == 'X' || mod == 'L')
-        {
-        continue;
-        }
-      if ( mod != 'M' && mod != 'C' && mod != 'G' )
-        {
-        count ++;
-        notLocallyModified = true;
-        }
-      const char* file = upFile.c_str();
-      cmCTestLog(this->CTest, DEBUG, "Line" << cc << ": " << mod << " - "
-        << file << std::endl);
+  vc->WriteXML(os);
 
-      std::string output;
-      if ( notLocallyModified )
-        {
-        std::string logcommand;
-        switch ( this->UpdateType )
-          {
-        case cmCTestUpdateHandler::e_CVS:
-          logcommand = "\"" + this->UpdateCommand + "\" -z3 log -N \""
-            + file + "\"";
-          break;
-        case cmCTestUpdateHandler::e_SVN:
-          if ( svn_latest_revision > 0 &&
-            svn_latest_revision > svn_current_revision )
-            {
-            cmOStringStream logCommandStream;
-            logCommandStream << "\"" << this->UpdateCommand << "\" log -r "
-              << svn_current_revision << ":" << svn_latest_revision
-              << " --xml \"" << file << "\"";
-            logcommand = logCommandStream.str();
-            }
-          else
-            {
-            logcommand = "\"" + this->UpdateCommand +
-              "\" status  --verbose \"" + file + "\"";
-            svn_use_status = 1;
-            }
-          break;
-          }
-        cmCTestLog(this->CTest, DEBUG, "Do log: " << logcommand << std::endl);
-        cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
-          "* Get file update information: " << logcommand.c_str()
-          << std::endl);
-        ofs << "* Get log information for file: " << file << std::endl;
-        ofs << "  Command: " << logcommand.c_str() << std::endl;
-        res = this->CTest->RunCommand(logcommand.c_str(), &output, &errors,
-          &retVal, sourceDirectory, 0 /*this->TimeOut*/);
-        ofs << "  Output: " << output.c_str() << std::endl;
-        ofs << "  Errors: " << errors.c_str() << std::endl;
-        if ( ofs )
-          {
-          ofs << output << std::endl;
-          }
-        }
-      else
-        {
-        res = false;
-        }
-      if ( res )
-        {
-        cmCTestLog(this->CTest, DEBUG, output << std::endl);
-        std::string::size_type sline = 0;
-        std::string srevision1 = "Unknown";
-        std::string sdate1     = "Unknown";
-        std::string sauthor1   = "Unknown";
-        std::string semail1    = "Unknown";
-        std::string comment1   = "";
-        std::string srevision2 = "Unknown";
-        if ( this->UpdateType == cmCTestUpdateHandler::e_CVS )
-          {
-          bool have_first = false;
-          bool have_second = false;
-          std::vector<cmStdString> ulines;
-          cmSystemTools::Split(output.c_str(), ulines);
-          for ( kk = 0; kk < ulines.size(); kk ++ )
-            {
-            const char* clp = ulines[kk].c_str();
-            if ( !have_second && !sline && cvs_revision_regex.find(clp) )
-              {
-              if ( !have_first )
-                {
-                srevision1 = cvs_revision_regex.match(1);
-                }
-              else
-                {
-                srevision2 = cvs_revision_regex.match(1);
-                }
-              }
-            else if ( !have_second && !sline &&
-              cvs_date_author_regex.find(clp) )
-              {
-              sline = kk + 1;
-              if ( !have_first )
-                {
-                sdate1 = cvs_date_author_regex.match(1);
-                sauthor1 = cvs_date_author_regex.match(2);
-                }
-              }
-            else if ( sline && cvs_end_of_comment_regex.find(clp) ||
-              cvs_end_of_file_regex.find(clp))
-              {
-              if ( !have_first )
-                {
-                have_first = true;
-                }
-              else if ( !have_second )
-                {
-                have_second = true;
-                }
-              sline = 0;
-              }
-            else if ( sline )
-              {
-              if ( !have_first )
-                {
-                comment1 += clp;
-                comment1 += "\n";
-                }
-              }
-            }
-          }
-        else if ( this->UpdateType == cmCTestUpdateHandler::e_SVN )
-          {
-          if ( svn_use_status )
-            {
-            cmOStringStream str;
-            str << svn_current_revision;
-            srevision1 = str.str();
-            if (!svn_status_line_regex.find(output))
-              {
-              cmCTestLog(this->CTest, ERROR_MESSAGE,
-                "Bad output from SVN status command: " << output
-                << std::endl);
-              }
-            else if ( svn_status_line_regex.match(4) != file )
-              {
-              cmCTestLog(this->CTest, ERROR_MESSAGE,
-                "Bad output from SVN status command. "
-                "The file name returned: \""
-                << svn_status_line_regex.match(4)
-                << "\" was different than the file specified: \"" << file
-                << "\"" << std::endl);
-              }
-            else
-              {
-              srevision1 = svn_status_line_regex.match(2);
-              int latest_revision = atoi(
-                svn_status_line_regex.match(2).c_str());
-              if ( svn_current_revision < latest_revision )
-                {
-                srevision2 = str.str();
-                }
-              sauthor1 = svn_status_line_regex.match(3);
-              }
-            }
-          else
-            {
-            cmCTestUpdateHandlerSVNXMLParser parser(this);
-            if ( parser.Parse(output.c_str()) )
-              {
-              int minrev = parser.GetMinRevision();
-              int maxrev = parser.GetMaxRevision();
-              cmCTestUpdateHandlerSVNXMLParser::
-                t_VectorOfCommits::iterator it;
-              for ( it = parser.GetCommits()->begin();
-                it != parser.GetCommits()->end();
-                ++ it )
-                {
-                if ( it->Revision == maxrev )
-                  {
-                  cmOStringStream mRevStream;
-                  mRevStream << maxrev;
-                  srevision1 = mRevStream.str();
-                  sauthor1 = it->Author;
-                  comment1 = it->Message;
-                  sdate1 = it->Date;
-                  }
-                else if ( it->Revision == minrev )
-                  {
-                  cmOStringStream mRevStream;
-                  mRevStream << minrev;
-                  srevision2 = mRevStream.str();
-                  }
-                }
-              }
-            }
-          }
-        if ( mod == 'M' )
-          {
-          comment1 = "Locally modified file\n";
-          sauthor1 = "Local User";
-          }
-        if ( mod == 'D' )
-          {
-          comment1 += " - Removed file\n";
-          }
-        if ( mod == 'C' )
-          {
-          comment1 = "Conflict while updating\n";
-          sauthor1 = "Local User";
-          }
-        std::string path = cmSystemTools::GetFilenamePath(file);
-        std::string fname = cmSystemTools::GetFilenameName(file);
-        if ( path != current_path )
-          {
-          if ( !first_file )
-            {
-            os << "\t</Directory>" << std::endl;
-            }
-          else
-            {
-            first_file = false;
-            }
-          os << "\t<Directory>\n"
-            << "\t\t<Name>" << path << "</Name>" << std::endl;
-          }
-        if ( mod == 'C' )
-          {
-          numConflicting ++;
-          os << "\t<Conflicting>" << std::endl;
-          }
-        else if ( mod == 'G' )
-          {
-          numConflicting ++;
-          os << "\t<Conflicting>" << std::endl;
-          }
-        else if ( mod == 'M' )
-          {
-          numModified ++;
-          os << "\t<Modified>" << std::endl;
-          }
-        else
-          {
-          numUpdated ++;
-          os << "\t<Updated>" << std::endl;
-          }
-        if ( srevision2 == "Unknown" )
-          {
-          srevision2 = srevision1;
-          }
-        cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "File: "
-          << path.c_str() << " / " << fname.c_str() << " was updated by "
-          << sauthor1.c_str() << " to revision: " << srevision1.c_str()
-          << " from revision: " << srevision2.c_str() << std::endl);
-        os << "\t\t<File>"
-          << cmXMLSafe(fname)
-          << "</File>\n"
-          << "\t\t<Directory>" << cmXMLSafe(path)
-          << "</Directory>\n"
-          << "\t\t<FullName>" << cmXMLSafe(file) << "</FullName>\n"
-          << "\t\t<CheckinDate>" << cmXMLSafe(sdate1)
-          << "</CheckinDate>\n"
-          << "\t\t<Author>" << cmXMLSafe(sauthor1) << "</Author>\n"
-          << "\t\t<Email>" << cmXMLSafe(semail1) << "</Email>\n"
-          << "\t\t<Log>" << cmXMLSafe(comment1) << "</Log>\n"
-          << "\t\t<Revision>" << srevision1 << "</Revision>\n"
-          << "\t\t<PriorRevision>" << srevision2 << "</PriorRevision>"
-          << std::endl;
-        if ( mod == 'C' )
-          {
-          os << "\t</Conflicting>" << std::endl;
-          }
-        else if ( mod == 'G' )
-          {
-          os << "\t</Conflicting>" << std::endl;
-          }
-        else if ( mod == 'M' )
-          {
-          os << "\t</Modified>" << std::endl;
-          }
-        else
-          {
-          os << "\t</Updated>" << std::endl;
-          }
-        author_set.insert(sauthor1);
-        current_path = path;
-        }
-      file_count ++;
-      }
-    }
-  if ( file_count )
-    {
-    cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl);
-    }
-  if ( numUpdated )
-    {
-    cmCTestLog(this->CTest, HANDLER_OUTPUT, "   Found " << numUpdated
-      << " updated files" << std::endl);
-    }
-  if ( numModified )
+  int localModifications = 0;
+  if(int numUpdated = vc->GetPathCount(cmCTestVC::PathUpdated))
     {
-    cmCTestLog(this->CTest, HANDLER_OUTPUT, "   Found " << numModified
-      << " locally modified files"
-      << std::endl);
-    }
-  if ( numConflicting )
-    {
-    cmCTestLog(this->CTest, HANDLER_OUTPUT, "   Found " << numConflicting
-      << " conflicting files"
-      << std::endl);
-    }
-  if ( numModified == 0 && numConflicting == 0 && numUpdated == 0 )
-    {
-    cmCTestLog(this->CTest, HANDLER_OUTPUT, "   Project is up-to-date"
-      << std::endl);
+    cmCTestLog(this->CTest, HANDLER_OUTPUT,
+               "   Found " << numUpdated << " updated files\n");
     }
-  if ( !first_file )
+  if(int numModified = vc->GetPathCount(cmCTestVC::PathModified))
     {
-    os << "\t</Directory>" << std::endl;
+    cmCTestLog(this->CTest, HANDLER_OUTPUT,
+               "   Found " << numModified << " locally modified files\n");
+    localModifications += numModified;
     }
-
-  // TODO: Skip the author list when submitting to CDash.
-  for(std::set<cmStdString>::const_iterator ai = author_set.begin();
-      ai != author_set.end(); ++ai)
+  if(int numConflicting = vc->GetPathCount(cmCTestVC::PathConflicting))
     {
-    os << "\t<Author><Name>" << cmXMLSafe(*ai) << "</Name></Author>\n";
+    cmCTestLog(this->CTest, HANDLER_OUTPUT,
+               "   Found " << numConflicting << " conflicting files\n");
+    localModifications += numConflicting;
     }
 
   cmCTestLog(this->CTest, DEBUG, "End" << std::endl);
@@ -883,7 +275,7 @@ int cmCTestUpdateHandler::ProcessHandler()
     static_cast<int>((cmSystemTools::GetTime() - elapsed_time_start)/6)/10.0
     << "</ElapsedMinutes>\n"
     << "\t<UpdateReturnStatus>";
-  if ( numModified > 0 || numConflicting > 0 )
+  if(localModifications)
     {
     os << "Update error: There are modified or conflicting files in the "
       "repository";
@@ -891,23 +283,14 @@ int cmCTestUpdateHandler::ProcessHandler()
       "   There are modified or conflicting files in the repository"
       << std::endl);
     }
-  if ( updateProducedError )
+  if(!updated)
     {
-    os << "Update error: ";
-    cmCTestLog(this->CTest, ERROR_MESSAGE, "   Update with command: "
-      << command << " failed" << std::endl);
+    cmCTestLog(this->CTest, ERROR_MESSAGE, "   Update command failed: "
+               << vc->GetUpdateCommandLine() << "\n");
     }
   os << "</UpdateReturnStatus>" << std::endl;
   os << "</Update>" << std::endl;
-  if (! res  )
-    {
-    cmCTestLog(this->CTest, ERROR_MESSAGE,
-      "Error(s) when updating the project" << std::endl);
-    cmCTestLog(this->CTest, ERROR_MESSAGE, "Output: "
-      << goutput << std::endl);
-    return -1;
-    }
-  return count;
+  return localModifications;
 }
 
 //----------------------------------------------------------------------

+ 82 - 0
Source/CTest/cmCTestVC.cxx

@@ -17,12 +17,19 @@
 #include "cmCTestVC.h"
 
 #include "cmCTest.h"
+#include "cmXMLSafe.h"
 
 #include <cmsys/Process.h>
 
 //----------------------------------------------------------------------------
 cmCTestVC::cmCTestVC(cmCTest* ct, std::ostream& log): CTest(ct), Log(log)
 {
+  this->PathCount[PathUpdated] = 0;
+  this->PathCount[PathModified] = 0;
+  this->PathCount[PathConflicting] = 0;
+  this->Unknown.Date = "Unknown";
+  this->Unknown.Author = "Unknown";
+  this->Unknown.Rev = "Unknown";
 }
 
 //----------------------------------------------------------------------------
@@ -71,6 +78,22 @@ std::string cmCTestVC::ComputeCommandLine(char const* const* cmd)
   return line.str();
 }
 
+//----------------------------------------------------------------------------
+bool cmCTestVC::RunUpdateCommand(char const* const* cmd,
+                                 OutputParser* out, OutputParser* err)
+{
+  // Report the command line.
+  this->UpdateCommandLine = this->ComputeCommandLine(cmd);
+  if(this->CTest->GetShowOnly())
+    {
+    this->Log << this->UpdateCommandLine << "\n";
+    return true;
+    }
+
+  // Run the command.
+  return this->RunChild(cmd, out, err);
+}
+
 //----------------------------------------------------------------------------
 std::string cmCTestVC::GetNightlyTime()
 {
@@ -103,6 +126,17 @@ void cmCTestVC::CleanupImpl()
   // We do no cleanup by default.
 }
 
+//----------------------------------------------------------------------------
+bool cmCTestVC::Update()
+{
+  this->NoteOldRevision();
+  this->Log << "--- Begin Update ---\n";
+  bool result = this->UpdateImpl();
+  this->Log << "--- End Update ---\n";
+  this->NoteNewRevision();
+  return result;
+}
+
 //----------------------------------------------------------------------------
 void cmCTestVC::NoteOldRevision()
 {
@@ -114,3 +148,51 @@ void cmCTestVC::NoteNewRevision()
 {
   // We do nothing by default.
 }
+
+//----------------------------------------------------------------------------
+bool cmCTestVC::UpdateImpl()
+{
+  cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
+             "* Unknown VCS tool, not updating!" << std::endl);
+  return true;
+}
+
+//----------------------------------------------------------------------------
+bool cmCTestVC::WriteXML(std::ostream& xml)
+{
+  this->Log << "--- Begin Revisions ---\n";
+  bool result = this->WriteXMLUpdates(xml);
+  this->Log << "--- End Revisions ---\n";
+  return result;
+}
+
+//----------------------------------------------------------------------------
+bool cmCTestVC::WriteXMLUpdates(std::ostream&)
+{
+  cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
+             "* CTest cannot extract updates for this VCS tool.\n");
+  return true;
+}
+
+//----------------------------------------------------------------------------
+void cmCTestVC::WriteXMLEntry(std::ostream& xml,
+                              std::string const& path,
+                              std::string const& name,
+                              std::string const& full,
+                              File const& f)
+{
+  static const char* desc[3] = { "Updated", "Modified", "Conflicting"};
+  Revision const& rev = f.Rev? *f.Rev : this->Unknown;
+  std::string prior = f.PriorRev? f.PriorRev->Rev : std::string("Unknown");
+  xml << "\t\t<" << desc[f.Status] << ">\n"
+      << "\t\t\t<File>" << cmXMLSafe(name) << "</File>\n"
+      << "\t\t\t<Directory>" << cmXMLSafe(path) << "</Directory>\n"
+      << "\t\t\t<FullName>" << cmXMLSafe(full) << "</FullName>\n"
+      << "\t\t\t<CheckinDate>" << cmXMLSafe(rev.Date) << "</CheckinDate>\n"
+      << "\t\t\t<Author>" << cmXMLSafe(rev.Author) << "</Author>\n"
+      << "\t\t\t<Log>" << cmXMLSafe(rev.Log) << "</Log>\n"
+      << "\t\t\t<Revision>" << cmXMLSafe(rev.Rev) << "</Revision>\n"
+      << "\t\t\t<PriorRevision>" << cmXMLSafe(prior) << "</PriorRevision>\n"
+      << "\t\t</" << desc[f.Status] << ">\n";
+  ++this->PathCount[f.Status];
+}

+ 56 - 2
Source/CTest/cmCTestVC.h

@@ -45,13 +45,49 @@ public:
   /** Perform cleanup operations on the work tree.  */
   void Cleanup();
 
-  void MarkOldRevision() { this->NoteOldRevision(); }
-  void MarkNewRevision() { this->NoteNewRevision(); }
+  /** Update the working tree to the new revision.  */
+  bool Update();
+
+  /** Get the command line used by the Update method.  */
+  std::string const& GetUpdateCommandLine() const
+    { return this->UpdateCommandLine; }
+
+  /** Write Update.xml entries for the updates found.  */
+  bool WriteXML(std::ostream& xml);
+
+  /** Enumerate non-trivial working tree states during update.  */
+  enum PathStatus { PathUpdated, PathModified, PathConflicting };
+
+  /** Get the number of working tree paths in each state after update.  */
+  int GetPathCount(PathStatus s) const { return this->PathCount[s]; }
+
 protected:
   // Internal API to be implemented by subclasses.
   virtual void CleanupImpl();
   virtual void NoteOldRevision();
+  virtual bool UpdateImpl();
   virtual void NoteNewRevision();
+  virtual bool WriteXMLUpdates(std::ostream& xml);
+
+  /** Basic information about one revision of a tree or file.  */
+  struct Revision
+  {
+    std::string Rev;
+    std::string Date;
+    std::string Author;
+    std::string Log;
+  };
+
+  /** Represent change to one file.  */
+  struct File
+  {
+    PathStatus Status;
+    Revision const* Rev;
+    Revision const* PriorRev;
+    File(): Status(PathUpdated), Rev(0), PriorRev(0) {}
+    File(PathStatus status, Revision const* rev, Revision const* priorRev):
+      Status(status), Rev(rev), PriorRev(priorRev) {}
+  };
 
   /** Convert a list of arguments to a human-readable command line.  */
   static std::string ComputeCommandLine(char const* const* cmd);
@@ -60,6 +96,15 @@ protected:
   bool RunChild(char const* const* cmd, OutputParser* out,
                 OutputParser* err, const char* workDir = 0);
 
+  /** Run VC update command line and send output to given parsers.  */
+  bool RunUpdateCommand(char const* const* cmd,
+                        OutputParser* out, OutputParser* err = 0);
+
+  /** Write xml element for one file.  */
+  void WriteXMLEntry(std::ostream& xml, std::string const& path,
+                     std::string const& name, std::string const& full,
+                     File const& f);
+
   // Instance of cmCTest running the script.
   cmCTest* CTest;
 
@@ -69,6 +114,15 @@ protected:
   // Basic information about the working tree.
   std::string CommandLineTool;
   std::string SourceDirectory;
+
+  // Record update command info.
+  std::string UpdateCommandLine;
+
+  // Placeholder for unknown revisions.
+  Revision Unknown;
+
+  // Count paths reported with each PathStatus value.
+  int PathCount[3];
 };
 
 #endif