/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmCTestSVN.h" #include #include #include #include #include #include "cmsys/RegularExpression.hxx" #include "cmCTest.h" #include "cmCTestVC.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmXMLParser.h" #include "cmXMLWriter.h" struct cmCTestSVN::Revision : public cmCTestVC::Revision { cmCTestSVN::SVNInfo* SVNInfo; }; cmCTestSVN::cmCTestSVN(cmCTest* ct, std::ostream& log) : cmCTestGlobalVC(ct, log) { this->PriorRev = this->Unknown; } cmCTestSVN::~cmCTestSVN() = default; void cmCTestSVN::CleanupImpl() { std::vector svn_cleanup; svn_cleanup.emplace_back("cleanup"); OutputLogger out(this->Log, "cleanup-out> "); OutputLogger err(this->Log, "cleanup-err> "); this->RunSVNCommand(svn_cleanup, &out, &err); } class cmCTestSVN::InfoParser : public cmCTestVC::LineParser { public: InfoParser(cmCTestSVN* svn, const char* prefix, std::string& rev, SVNInfo& svninfo) : Rev(rev) , SVNRepo(svninfo) { this->SetLog(&svn->Log, prefix); this->RegexRev.compile("^Revision: ([0-9]+)"); this->RegexURL.compile("^URL: +([^ ]+) *$"); this->RegexRoot.compile("^Repository Root: +([^ ]+) *$"); } private: std::string& Rev; cmCTestSVN::SVNInfo& SVNRepo; cmsys::RegularExpression RegexRev; cmsys::RegularExpression RegexURL; cmsys::RegularExpression RegexRoot; bool ProcessLine() override { if (this->RegexRev.find(this->Line)) { this->Rev = this->RegexRev.match(1); } else if (this->RegexURL.find(this->Line)) { this->SVNRepo.URL = this->RegexURL.match(1); } else if (this->RegexRoot.find(this->Line)) { this->SVNRepo.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; } if (p1.size() > p2.size() && p1[p2.size()] == '/') { return strncmp(p1.c_str(), p2.c_str(), p2.size()) == 0; } return false; } std::string cmCTestSVN::LoadInfo(SVNInfo& svninfo) { // Run "svn info" to get the repository info from the work tree. std::vector svn_info; svn_info.emplace_back("info"); svn_info.push_back(svninfo.LocalPath); std::string rev; InfoParser out(this, "info-out> ", rev, svninfo); OutputLogger err(this->Log, "info-err> "); this->RunSVNCommand(svn_info, &out, &err); return rev; } bool cmCTestSVN::NoteOldRevision() { if (!this->LoadRepositories()) { return false; } for (SVNInfo& svninfo : this->Repositories) { svninfo.OldRevision = this->LoadInfo(svninfo); this->Log << "Revision for repository '" << svninfo.LocalPath << "' before update: " << svninfo.OldRevision << "\n"; cmCTestLog(this->CTest, HANDLER_OUTPUT, " Old revision of external repository '" << svninfo.LocalPath << "' is: " << svninfo.OldRevision << "\n"); } // Set the global old revision to the one of the root this->OldRevision = this->RootInfo->OldRevision; this->PriorRev.Rev = this->OldRevision; return true; } bool cmCTestSVN::NoteNewRevision() { if (!this->LoadRepositories()) { return false; } for (SVNInfo& svninfo : this->Repositories) { svninfo.NewRevision = this->LoadInfo(svninfo); this->Log << "Revision for repository '" << svninfo.LocalPath << "' after update: " << svninfo.NewRevision << "\n"; cmCTestLog(this->CTest, HANDLER_OUTPUT, " New revision of external repository '" << svninfo.LocalPath << "' is: " << svninfo.NewRevision << "\n"); // svninfo.Root = ""; // uncomment to test GuessBase this->Log << "Repository '" << svninfo.LocalPath << "' URL = " << svninfo.URL << "\n"; this->Log << "Repository '" << svninfo.LocalPath << "' Root = " << svninfo.Root << "\n"; // Compute the base path the working tree has checked out under // the repository root. if (!svninfo.Root.empty() && cmCTestSVNPathStarts(svninfo.URL, svninfo.Root)) { svninfo.Base = cmStrCat( cmCTest::DecodeURL(svninfo.URL.substr(svninfo.Root.size())), '/'); } this->Log << "Repository '" << svninfo.LocalPath << "' Base = " << svninfo.Base << "\n"; } // Set the global new revision to the one of the root this->NewRevision = this->RootInfo->NewRevision; return true; } void cmCTestSVN::GuessBase(SVNInfo& svninfo, 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 = svninfo.URL.find('/'); svninfo.Base.empty() && slash != std::string::npos; slash = svninfo.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(svninfo.URL.substr(slash)); for (auto ci = changes.begin(); svninfo.Base.empty() && ci != changes.end(); ++ci) { if (cmCTestSVNPathStarts(ci->Path, base)) { svninfo.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. svninfo.Base += "/"; this->Log << "Guessed Base = " << svninfo.Base << "\n"; } 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() override { 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); // 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.emplace_back("update"); cm::append(svn_update, args); UpdateParser out(this, "up-out> "); OutputLogger err(this->Log, "up-err> "); return this->RunSVNCommand(svn_update, &out, &err); } bool cmCTestSVN::RunSVNCommand(std::vector const& parameters, OutputParser* out, OutputParser* err) { if (parameters.empty()) { return false; } std::vector args; args.push_back(this->CommandLineTool); cm::append(args, parameters); args.emplace_back("--non-interactive"); std::string userOptions = this->CTest->GetCTestConfiguration("SVNOptions"); std::vector parsedUserOptions = cmSystemTools::ParseArguments(userOptions); cm::append(args, parsedUserOptions); if (parameters[0] == "update") { return this->RunUpdateCommand(args, out, err); } return this->RunChild(args, out, err); } class cmCTestSVN::LogParser : public cmCTestVC::OutputLogger , private cmXMLParser { public: LogParser(cmCTestSVN* svn, const char* prefix, SVNInfo& svninfo) : OutputLogger(svn->Log, prefix) , SVN(svn) , SVNRepo(svninfo) { this->InitializeParser(); } ~LogParser() override { this->CleanupParser(); } private: cmCTestSVN* SVN; cmCTestSVN::SVNInfo& SVNRepo; using Revision = cmCTestSVN::Revision; using Change = cmCTestSVN::Change; Revision Rev; std::vector Changes; Change CurChange; std::vector CData; bool ProcessChunk(const char* data, int length) override { this->OutputLogger::ProcessChunk(data, length); this->ParseChunk(data, length); return true; } void StartElement(const std::string& name, const char** atts) override { this->CData.clear(); if (name == "logentry") { this->Rev = Revision(); this->Rev.SVNInfo = &this->SVNRepo; if (const char* rev = cmCTestSVN::LogParser::FindAttribute(atts, "revision")) { this->Rev.Rev = rev; } this->Changes.clear(); } else if (name == "path") { this->CurChange = Change(); if (const char* action = cmCTestSVN::LogParser::FindAttribute(atts, "action")) { this->CurChange.Action = action[0]; } } } void CharacterDataHandler(const char* data, int length) override { cm::append(this->CData, data, data + length); } void EndElement(const std::string& name) override { if (name == "logentry") { this->SVN->DoRevisionSVN(this->Rev, this->Changes); } else if (!this->CData.empty() && name == "path") { std::string orig_path(this->CData.data(), this->CData.size()); std::string new_path = this->SVNRepo.BuildLocalPath(orig_path); this->CurChange.Path.assign(new_path); this->Changes.push_back(this->CurChange); } else if (!this->CData.empty() && name == "author") { this->Rev.Author.assign(this->CData.data(), this->CData.size()); } else if (!this->CData.empty() && name == "date") { this->Rev.Date.assign(this->CData.data(), this->CData.size()); } else if (!this->CData.empty() && name == "msg") { this->Rev.Log.assign(this->CData.data(), this->CData.size()); } this->CData.clear(); } void ReportError(int /*line*/, int /*column*/, const char* msg) override { this->SVN->Log << "Error parsing svn log xml: " << msg << "\n"; } }; bool cmCTestSVN::LoadRevisions() { bool result = true; // Get revisions for all the external repositories for (SVNInfo& svninfo : this->Repositories) { result = this->LoadRevisions(svninfo) && result; } return result; } bool cmCTestSVN::LoadRevisions(SVNInfo& svninfo) { // We are interested in every revision included in the update. std::string revs; if (atoi(svninfo.OldRevision.c_str()) < atoi(svninfo.NewRevision.c_str())) { revs = "-r" + svninfo.OldRevision + ":" + svninfo.NewRevision; } else { revs = "-r" + svninfo.NewRevision; } // Run "svn log" to get all global revisions of interest. std::vector svn_log; svn_log.emplace_back("log"); svn_log.emplace_back("--xml"); svn_log.emplace_back("-v"); svn_log.emplace_back(revs); svn_log.emplace_back(svninfo.LocalPath); LogParser out(this, "log-out> ", svninfo); OutputLogger err(this->Log, "log-err> "); return this->RunSVNCommand(svn_log, &out, &err); } void cmCTestSVN::DoRevisionSVN(Revision const& revision, std::vector const& changes) { // Guess the base checkout path from the changes if necessary. if (this->RootInfo->Base.empty() && !changes.empty()) { this->GuessBase(*this->RootInfo, changes); } // Ignore changes in the old revision for external repositories if (revision.Rev == revision.SVNInfo->OldRevision && !revision.SVNInfo->LocalPath.empty()) { return; } 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() override { 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; } } }; bool cmCTestSVN::LoadModifications() { // Run "svn status" which reports local modifications. std::vector svn_status; svn_status.emplace_back("status"); StatusParser out(this, "status-out> "); OutputLogger err(this->Log, "status-err> "); this->RunSVNCommand(svn_status, &out, &err); return true; } void cmCTestSVN::WriteXMLGlobal(cmXMLWriter& xml) { this->cmCTestGlobalVC::WriteXMLGlobal(xml); xml.Element("SVNPath", this->RootInfo->Base); } class cmCTestSVN::ExternalParser : public cmCTestVC::LineParser { public: ExternalParser(cmCTestSVN* svn, const char* prefix) : SVN(svn) { this->SetLog(&svn->Log, prefix); this->RegexExternal.compile("^X..... +(.+)$"); } private: cmCTestSVN* SVN; cmsys::RegularExpression RegexExternal; bool ProcessLine() override { if (this->RegexExternal.find(this->Line)) { this->DoPath(this->RegexExternal.match(1)); } return true; } void DoPath(std::string const& path) { // Get local path relative to the source directory std::string local_path; if (path.size() > this->SVN->SourceDirectory.size() && strncmp(path.c_str(), this->SVN->SourceDirectory.c_str(), this->SVN->SourceDirectory.size()) == 0) { local_path = path.substr(this->SVN->SourceDirectory.size() + 1); } else { local_path = path; } this->SVN->Repositories.emplace_back(local_path); } }; bool cmCTestSVN::LoadRepositories() { if (!this->Repositories.empty()) { return true; } // Info for root repository this->Repositories.emplace_back(); this->RootInfo = &(this->Repositories.back()); // Run "svn status" to get the list of external repositories std::vector svn_status; svn_status.emplace_back("status"); ExternalParser out(this, "external-out> "); OutputLogger err(this->Log, "external-err> "); return this->RunSVNCommand(svn_status, &out, &err); } std::string cmCTestSVN::SVNInfo::BuildLocalPath(std::string const& path) const { std::string local_path; // Add local path prefix if not empty if (!this->LocalPath.empty()) { local_path += this->LocalPath; local_path += "/"; } // Add path with base prefix removed if (path.size() > this->Base.size() && strncmp(path.c_str(), this->Base.c_str(), this->Base.size()) == 0) { local_path += path.substr(this->Base.size()); } else { local_path += path; } return local_path; }