/*============================================================================ CMake - Cross Platform Makefile Generator Copyright 2000-2009 Kitware, Inc. Distributed under the OSI-approved BSD License (the "License"); see accompanying file Copyright.txt for details. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the License for more information. ============================================================================*/ #include "cmCTestSVN.h" #include "cmCTest.h" #include "cmSystemTools.h" #include "cmXMLParser.h" #include "cmXMLSafe.h" #include //---------------------------------------------------------------------------- cmCTestSVN::cmCTestSVN(cmCTest* ct, std::ostream& log): cmCTestGlobalVC(ct, log) { this->PriorRev = this->Unknown; } //---------------------------------------------------------------------------- cmCTestSVN::~cmCTestSVN() { } //---------------------------------------------------------------------------- void cmCTestSVN::CleanupImpl() { const char* svn = this->CommandLineTool.c_str(); const char* svn_cleanup[] = {svn, "cleanup", 0}; OutputLogger out(this->Log, "cleanup-out> "); OutputLogger err(this->Log, "cleanup-err> "); this->RunChild(svn_cleanup, &out, &err); } //---------------------------------------------------------------------------- class cmCTestSVN::InfoParser: public cmCTestVC::LineParser { public: InfoParser(cmCTestSVN* svn, const char* prefix, std::string& rev): SVN(svn), Rev(rev) { this->SetLog(&svn->Log, prefix); this->RegexRev.compile("^Revision: ([0-9]+)"); this->RegexURL.compile("^URL: +([^ ]+) *$"); this->RegexRoot.compile("^Repository Root: +([^ ]+) *$"); } private: cmCTestSVN* SVN; std::string& Rev; cmsys::RegularExpression RegexRev; cmsys::RegularExpression RegexURL; cmsys::RegularExpression RegexRoot; virtual bool ProcessLine() { if(this->RegexRev.find(this->Line)) { this->Rev = this->RegexRev.match(1); } else if(this->RegexURL.find(this->Line)) { this->SVN->URL = this->RegexURL.match(1); } else if(this->RegexRoot.find(this->Line)) { this->SVN->Root = this->RegexRoot.match(1); } return true; } }; //---------------------------------------------------------------------------- static bool cmCTestSVNPathStarts(std::string const& p1, std::string const& p2) { // Does path p1 start with path p2? if(p1.size() == p2.size()) { return p1 == p2; } else if(p1.size() > p2.size() && p1[p2.size()] == '/') { return strncmp(p1.c_str(), p2.c_str(), p2.size()) == 0; } else { return false; } } //---------------------------------------------------------------------------- std::string cmCTestSVN::LoadInfo() { // Run "svn info" to get the repository info from the work tree. const char* svn = this->CommandLineTool.c_str(); const char* svn_info[] = {svn, "info", 0}; std::string rev; InfoParser out(this, "info-out> ", rev); OutputLogger err(this->Log, "info-err> "); this->RunChild(svn_info, &out, &err); return rev; } //---------------------------------------------------------------------------- void cmCTestSVN::NoteOldRevision() { this->OldRevision = this->LoadInfo(); 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; } //---------------------------------------------------------------------------- void cmCTestSVN::NoteNewRevision() { this->NewRevision = this->LoadInfo(); this->Log << "Revision after update: " << this->NewRevision << "\n"; 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"; // Compute the base path the working tree has checked out under // the repository root. if(!this->Root.empty() && cmCTestSVNPathStarts(this->URL, this->Root)) { this->Base = cmCTest::DecodeURL(this->URL.substr(this->Root.size())); this->Base += "/"; } this->Log << "Base = " << this->Base << "\n"; } //---------------------------------------------------------------------------- void cmCTestSVN::GuessBase(std::vector 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::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 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 svn_update; svn_update.push_back(this->CommandLineTool.c_str()); svn_update.push_back("update"); svn_update.push_back("--non-interactive"); for(std::vector::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 cmCTestVC::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 Changes; Change CurChange; std::vector 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() { // 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); } } //---------------------------------------------------------------------------- void cmCTestSVN::DoRevision(Revision const& revision, std::vector const& changes) { // Guess the base checkout path from the changes if necessary. if(this->Base.empty() && !changes.empty()) { this->GuessBase(changes); } this->cmCTestGlobalVC::DoRevision(revision, changes); } //---------------------------------------------------------------------------- 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': this->SVN->DoModification(PathModified, path); break; case 'C': case '~': this->SVN->DoModification(PathConflicting, path); break; case 'X': case 'I': case '?': case ' ': default: break; } } }; //---------------------------------------------------------------------------- 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); }