diff options
author | Brad King <brad.king@kitware.com> | 2009-02-25 19:42:45 (GMT) |
---|---|---|
committer | Brad King <brad.king@kitware.com> | 2009-02-25 19:42:45 (GMT) |
commit | 80282b749fb138ea8bd188dd5b7623c7545ea927 (patch) | |
tree | a654e1b28bf2b2bec97de9ee7dad695d322598f1 /Source/CTest/cmCTestSVN.cxx | |
parent | cb788e8f6dfeeb5a934679f671adc87116837834 (diff) | |
download | CMake-80282b749fb138ea8bd188dd5b7623c7545ea927.zip CMake-80282b749fb138ea8bd188dd5b7623c7545ea927.tar.gz CMake-80282b749fb138ea8bd188dd5b7623c7545ea927.tar.bz2 |
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.cxx | 390 |
1 files changed, 390 insertions, 0 deletions
diff --git a/Source/CTest/cmCTestSVN.cxx b/Source/CTest/cmCTestSVN.cxx index d6b8306..bb0c3ea 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; +} |