| /* 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 <cstdlib> |
| #include <cstring> |
| #include <map> |
| #include <ostream> |
| |
| #include <cmext/algorithm> |
| |
| #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<std::string> 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<std::string> 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<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 = 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<std::string> 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<std::string> 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<std::string> const& parameters, |
| OutputParser* out, OutputParser* err) |
| { |
| if (parameters.empty()) { |
| return false; |
| } |
| |
| std::vector<std::string> args; |
| args.push_back(this->CommandLineTool); |
| cm::append(args, parameters); |
| args.emplace_back("--non-interactive"); |
| |
| std::string userOptions = this->CTest->GetCTestConfiguration("SVNOptions"); |
| |
| std::vector<std::string> 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<Change> Changes; |
| Change CurChange; |
| std::vector<char> 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<std::string> 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<Change> 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<std::string> 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<std::string> 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; |
| } |