summaryrefslogtreecommitdiff
path: root/Source/CTest/cmCTestSVN.cxx
diff options
context:
space:
mode:
authorBrad King <brad.king@kitware.com>2009-02-25 14:42:45 -0500
committerBrad King <brad.king@kitware.com>2009-02-25 14:42:45 -0500
commit80282b749fb138ea8bd188dd5b7623c7545ea927 (patch)
treea654e1b28bf2b2bec97de9ee7dad695d322598f1 /Source/CTest/cmCTestSVN.cxx
parentcb788e8f6dfeeb5a934679f671adc87116837834 (diff)
downloadcmake-80282b749fb138ea8bd188dd5b7623c7545ea927.tar.gz
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.
Diffstat (limited to 'Source/CTest/cmCTestSVN.cxx')
-rw-r--r--Source/CTest/cmCTestSVN.cxx390
1 files changed, 390 insertions, 0 deletions
diff --git a/Source/CTest/cmCTestSVN.cxx b/Source/CTest/cmCTestSVN.cxx
index d6b830614e..bb0c3ea3ff 100644
--- a/Source/CTest/cmCTestSVN.cxx
+++ b/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;
+}