cmCTestSVN.cxx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
  2. file Copyright.txt or https://cmake.org/licensing for details. */
  3. #include "cmCTestSVN.h"
  4. #include <cstdlib>
  5. #include <cstring>
  6. #include <map>
  7. #include <ostream>
  8. #include <cmext/algorithm>
  9. #include "cmsys/RegularExpression.hxx"
  10. #include "cmCTest.h"
  11. #include "cmCTestVC.h"
  12. #include "cmMakefile.h"
  13. #include "cmStringAlgorithms.h"
  14. #include "cmSystemTools.h"
  15. #include "cmXMLParser.h"
  16. #include "cmXMLWriter.h"
  17. struct cmCTestSVN::Revision : public cmCTestVC::Revision
  18. {
  19. cmCTestSVN::SVNInfo* SVNInfo;
  20. };
  21. cmCTestSVN::cmCTestSVN(cmCTest* ct, cmMakefile* mf, std::ostream& log)
  22. : cmCTestGlobalVC(ct, mf, log)
  23. {
  24. this->PriorRev = this->Unknown;
  25. }
  26. cmCTestSVN::~cmCTestSVN() = default;
  27. void cmCTestSVN::CleanupImpl()
  28. {
  29. std::vector<std::string> svn_cleanup;
  30. svn_cleanup.emplace_back("cleanup");
  31. OutputLogger out(this->Log, "cleanup-out> ");
  32. OutputLogger err(this->Log, "cleanup-err> ");
  33. this->RunSVNCommand(svn_cleanup, &out, &err);
  34. }
  35. class cmCTestSVN::InfoParser : public cmCTestVC::LineParser
  36. {
  37. public:
  38. InfoParser(cmCTestSVN* svn, char const* prefix, std::string& rev,
  39. SVNInfo& svninfo)
  40. : Rev(rev)
  41. , SVNRepo(svninfo)
  42. {
  43. this->SetLog(&svn->Log, prefix);
  44. this->RegexRev.compile("^Revision: ([0-9]+)");
  45. this->RegexURL.compile("^URL: +([^ ]+) *$");
  46. this->RegexRoot.compile("^Repository Root: +([^ ]+) *$");
  47. }
  48. private:
  49. std::string& Rev;
  50. cmCTestSVN::SVNInfo& SVNRepo;
  51. cmsys::RegularExpression RegexRev;
  52. cmsys::RegularExpression RegexURL;
  53. cmsys::RegularExpression RegexRoot;
  54. bool ProcessLine() override
  55. {
  56. if (this->RegexRev.find(this->Line)) {
  57. this->Rev = this->RegexRev.match(1);
  58. } else if (this->RegexURL.find(this->Line)) {
  59. this->SVNRepo.URL = this->RegexURL.match(1);
  60. } else if (this->RegexRoot.find(this->Line)) {
  61. this->SVNRepo.Root = this->RegexRoot.match(1);
  62. }
  63. return true;
  64. }
  65. };
  66. static bool cmCTestSVNPathStarts(std::string const& p1, std::string const& p2)
  67. {
  68. // Does path p1 start with path p2?
  69. if (p1.size() == p2.size()) {
  70. return p1 == p2;
  71. }
  72. if (p1.size() > p2.size() && p1[p2.size()] == '/') {
  73. return strncmp(p1.c_str(), p2.c_str(), p2.size()) == 0;
  74. }
  75. return false;
  76. }
  77. std::string cmCTestSVN::LoadInfo(SVNInfo& svninfo)
  78. {
  79. // Run "svn info" to get the repository info from the work tree.
  80. std::vector<std::string> svn_info;
  81. svn_info.emplace_back("info");
  82. svn_info.push_back(svninfo.LocalPath);
  83. std::string rev;
  84. InfoParser out(this, "info-out> ", rev, svninfo);
  85. OutputLogger err(this->Log, "info-err> ");
  86. this->RunSVNCommand(svn_info, &out, &err);
  87. return rev;
  88. }
  89. bool cmCTestSVN::NoteOldRevision()
  90. {
  91. if (!this->LoadRepositories()) {
  92. return false;
  93. }
  94. for (SVNInfo& svninfo : this->Repositories) {
  95. svninfo.OldRevision = this->LoadInfo(svninfo);
  96. this->Log << "Revision for repository '" << svninfo.LocalPath
  97. << "' before update: " << svninfo.OldRevision << "\n";
  98. cmCTestLog(this->CTest, HANDLER_OUTPUT,
  99. " Old revision of external repository '"
  100. << svninfo.LocalPath << "' is: " << svninfo.OldRevision
  101. << "\n");
  102. }
  103. // Set the global old revision to the one of the root
  104. this->OldRevision = this->RootInfo->OldRevision;
  105. this->PriorRev.Rev = this->OldRevision;
  106. return true;
  107. }
  108. bool cmCTestSVN::NoteNewRevision()
  109. {
  110. if (!this->LoadRepositories()) {
  111. return false;
  112. }
  113. for (SVNInfo& svninfo : this->Repositories) {
  114. svninfo.NewRevision = this->LoadInfo(svninfo);
  115. this->Log << "Revision for repository '" << svninfo.LocalPath
  116. << "' after update: " << svninfo.NewRevision << "\n";
  117. cmCTestLog(this->CTest, HANDLER_OUTPUT,
  118. " New revision of external repository '"
  119. << svninfo.LocalPath << "' is: " << svninfo.NewRevision
  120. << "\n");
  121. // svninfo.Root = ""; // uncomment to test GuessBase
  122. this->Log << "Repository '" << svninfo.LocalPath
  123. << "' URL = " << svninfo.URL << "\n";
  124. this->Log << "Repository '" << svninfo.LocalPath
  125. << "' Root = " << svninfo.Root << "\n";
  126. // Compute the base path the working tree has checked out under
  127. // the repository root.
  128. if (!svninfo.Root.empty() &&
  129. cmCTestSVNPathStarts(svninfo.URL, svninfo.Root)) {
  130. svninfo.Base = cmStrCat(
  131. cmCTest::DecodeURL(svninfo.URL.substr(svninfo.Root.size())), '/');
  132. }
  133. this->Log << "Repository '" << svninfo.LocalPath
  134. << "' Base = " << svninfo.Base << "\n";
  135. }
  136. // Set the global new revision to the one of the root
  137. this->NewRevision = this->RootInfo->NewRevision;
  138. return true;
  139. }
  140. void cmCTestSVN::GuessBase(SVNInfo& svninfo,
  141. std::vector<Change> const& changes)
  142. {
  143. // Subversion did not give us a good repository root so we need to
  144. // guess the base path from the URL and the paths in a revision with
  145. // changes under it.
  146. // Consider each possible URL suffix from longest to shortest.
  147. for (std::string::size_type slash = svninfo.URL.find('/');
  148. svninfo.Base.empty() && slash != std::string::npos;
  149. slash = svninfo.URL.find('/', slash + 1)) {
  150. // If the URL suffix is a prefix of at least one path then it is the base.
  151. std::string base = cmCTest::DecodeURL(svninfo.URL.substr(slash));
  152. for (auto ci = changes.begin();
  153. svninfo.Base.empty() && ci != changes.end(); ++ci) {
  154. if (cmCTestSVNPathStarts(ci->Path, base)) {
  155. svninfo.Base = base;
  156. }
  157. }
  158. }
  159. // We always append a slash so that we know paths beginning in the
  160. // base lie under its path. If no base was found then the working
  161. // tree must be a checkout of the entire repo and this will match
  162. // the leading slash in all paths.
  163. svninfo.Base += "/";
  164. this->Log << "Guessed Base = " << svninfo.Base << "\n";
  165. }
  166. class cmCTestSVN::UpdateParser : public cmCTestVC::LineParser
  167. {
  168. public:
  169. UpdateParser(cmCTestSVN* svn, char const* prefix)
  170. : SVN(svn)
  171. {
  172. this->SetLog(&svn->Log, prefix);
  173. this->RegexUpdate.compile("^([ADUCGE ])([ADUCGE ])[B ] +(.+)$");
  174. }
  175. private:
  176. cmCTestSVN* SVN;
  177. cmsys::RegularExpression RegexUpdate;
  178. bool ProcessLine() override
  179. {
  180. if (this->RegexUpdate.find(this->Line)) {
  181. this->DoPath(this->RegexUpdate.match(1)[0],
  182. this->RegexUpdate.match(2)[0], this->RegexUpdate.match(3));
  183. }
  184. return true;
  185. }
  186. void DoPath(char path_status, char prop_status, std::string const& path)
  187. {
  188. char status = (path_status != ' ') ? path_status : prop_status;
  189. std::string dir = cmSystemTools::GetFilenamePath(path);
  190. std::string name = cmSystemTools::GetFilenameName(path);
  191. // See "svn help update".
  192. switch (status) {
  193. case 'G':
  194. this->SVN->Dirs[dir][name].Status = PathModified;
  195. break;
  196. case 'C':
  197. this->SVN->Dirs[dir][name].Status = PathConflicting;
  198. break;
  199. case 'A':
  200. case 'D':
  201. case 'U':
  202. this->SVN->Dirs[dir][name].Status = PathUpdated;
  203. break;
  204. case 'E': // TODO?
  205. case '?':
  206. case ' ':
  207. default:
  208. break;
  209. }
  210. }
  211. };
  212. bool cmCTestSVN::UpdateImpl()
  213. {
  214. // Get user-specified update options.
  215. std::string opts = this->Makefile->GetSafeDefinition("CTEST_UPDATE_OPTIONS");
  216. if (opts.empty()) {
  217. opts = this->Makefile->GetSafeDefinition("CTEST_SVN_UPDATE_OPTIONS");
  218. }
  219. std::vector<std::string> args = cmSystemTools::ParseArguments(opts);
  220. // Specify the start time for nightly testing.
  221. if (this->CTest->GetTestModel() == cmCTest::NIGHTLY) {
  222. args.push_back("-r{" + this->GetNightlyTime() + " +0000}");
  223. }
  224. std::vector<std::string> svn_update;
  225. svn_update.emplace_back("update");
  226. cm::append(svn_update, args);
  227. UpdateParser out(this, "up-out> ");
  228. OutputLogger err(this->Log, "up-err> ");
  229. return this->RunSVNCommand(svn_update, &out, &err);
  230. }
  231. bool cmCTestSVN::RunSVNCommand(std::vector<std::string> const& parameters,
  232. OutputParser* out, OutputParser* err)
  233. {
  234. if (parameters.empty()) {
  235. return false;
  236. }
  237. std::vector<std::string> args;
  238. args.push_back(this->CommandLineTool);
  239. cm::append(args, parameters);
  240. args.emplace_back("--non-interactive");
  241. std::string userOptions =
  242. this->Makefile->GetSafeDefinition("CTEST_SVN_OPTIONS");
  243. std::vector<std::string> parsedUserOptions =
  244. cmSystemTools::ParseArguments(userOptions);
  245. cm::append(args, parsedUserOptions);
  246. if (parameters[0] == "update") {
  247. return this->RunUpdateCommand(args, out, err);
  248. }
  249. return this->RunChild(args, out, err);
  250. }
  251. class cmCTestSVN::LogParser
  252. : public cmCTestVC::OutputLogger
  253. , private cmXMLParser
  254. {
  255. public:
  256. LogParser(cmCTestSVN* svn, char const* prefix, SVNInfo& svninfo)
  257. : OutputLogger(svn->Log, prefix)
  258. , SVN(svn)
  259. , SVNRepo(svninfo)
  260. {
  261. this->InitializeParser();
  262. }
  263. ~LogParser() override { this->CleanupParser(); }
  264. private:
  265. cmCTestSVN* SVN;
  266. cmCTestSVN::SVNInfo& SVNRepo;
  267. using Revision = cmCTestSVN::Revision;
  268. using Change = cmCTestSVN::Change;
  269. Revision Rev;
  270. std::vector<Change> Changes;
  271. Change CurChange;
  272. std::vector<char> CData;
  273. bool ProcessChunk(char const* data, int length) override
  274. {
  275. this->OutputLogger::ProcessChunk(data, length);
  276. this->ParseChunk(data, length);
  277. return true;
  278. }
  279. void StartElement(std::string const& name, char const** atts) override
  280. {
  281. this->CData.clear();
  282. if (name == "logentry") {
  283. this->Rev = Revision();
  284. this->Rev.SVNInfo = &this->SVNRepo;
  285. if (char const* rev =
  286. cmCTestSVN::LogParser::FindAttribute(atts, "revision")) {
  287. this->Rev.Rev = rev;
  288. }
  289. this->Changes.clear();
  290. } else if (name == "path") {
  291. this->CurChange = Change();
  292. if (char const* action =
  293. cmCTestSVN::LogParser::FindAttribute(atts, "action")) {
  294. this->CurChange.Action = action[0];
  295. }
  296. }
  297. }
  298. void CharacterDataHandler(char const* data, int length) override
  299. {
  300. cm::append(this->CData, data, data + length);
  301. }
  302. void EndElement(std::string const& name) override
  303. {
  304. if (name == "logentry") {
  305. this->SVN->DoRevisionSVN(this->Rev, this->Changes);
  306. } else if (!this->CData.empty() && name == "path") {
  307. std::string orig_path(this->CData.data(), this->CData.size());
  308. std::string new_path = this->SVNRepo.BuildLocalPath(orig_path);
  309. this->CurChange.Path.assign(new_path);
  310. this->Changes.push_back(this->CurChange);
  311. } else if (!this->CData.empty() && name == "author") {
  312. this->Rev.Author.assign(this->CData.data(), this->CData.size());
  313. } else if (!this->CData.empty() && name == "date") {
  314. this->Rev.Date.assign(this->CData.data(), this->CData.size());
  315. } else if (!this->CData.empty() && name == "msg") {
  316. this->Rev.Log.assign(this->CData.data(), this->CData.size());
  317. }
  318. this->CData.clear();
  319. }
  320. void ReportError(int /*line*/, int /*column*/, char const* msg) override
  321. {
  322. this->SVN->Log << "Error parsing svn log xml: " << msg << "\n";
  323. }
  324. };
  325. bool cmCTestSVN::LoadRevisions()
  326. {
  327. bool result = true;
  328. // Get revisions for all the external repositories
  329. for (SVNInfo& svninfo : this->Repositories) {
  330. result = this->LoadRevisions(svninfo) && result;
  331. }
  332. return result;
  333. }
  334. bool cmCTestSVN::LoadRevisions(SVNInfo& svninfo)
  335. {
  336. // We are interested in every revision included in the update.
  337. std::string revs;
  338. if (atoi(svninfo.OldRevision.c_str()) < atoi(svninfo.NewRevision.c_str())) {
  339. revs = "-r" + svninfo.OldRevision + ":" + svninfo.NewRevision;
  340. } else {
  341. revs = "-r" + svninfo.NewRevision;
  342. }
  343. // Run "svn log" to get all global revisions of interest.
  344. std::vector<std::string> svn_log;
  345. svn_log.emplace_back("log");
  346. svn_log.emplace_back("--xml");
  347. svn_log.emplace_back("-v");
  348. svn_log.emplace_back(revs);
  349. svn_log.emplace_back(svninfo.LocalPath);
  350. LogParser out(this, "log-out> ", svninfo);
  351. OutputLogger err(this->Log, "log-err> ");
  352. return this->RunSVNCommand(svn_log, &out, &err);
  353. }
  354. void cmCTestSVN::DoRevisionSVN(Revision const& revision,
  355. std::vector<Change> const& changes)
  356. {
  357. // Guess the base checkout path from the changes if necessary.
  358. if (this->RootInfo->Base.empty() && !changes.empty()) {
  359. this->GuessBase(*this->RootInfo, changes);
  360. }
  361. // Ignore changes in the old revision for external repositories
  362. if (revision.Rev == revision.SVNInfo->OldRevision &&
  363. !revision.SVNInfo->LocalPath.empty()) {
  364. return;
  365. }
  366. this->cmCTestGlobalVC::DoRevision(revision, changes);
  367. }
  368. class cmCTestSVN::StatusParser : public cmCTestVC::LineParser
  369. {
  370. public:
  371. StatusParser(cmCTestSVN* svn, char const* prefix)
  372. : SVN(svn)
  373. {
  374. this->SetLog(&svn->Log, prefix);
  375. this->RegexStatus.compile("^([ACDIMRX?!~ ])([CM ])[ L]... +(.+)$");
  376. }
  377. private:
  378. cmCTestSVN* SVN;
  379. cmsys::RegularExpression RegexStatus;
  380. bool ProcessLine() override
  381. {
  382. if (this->RegexStatus.find(this->Line)) {
  383. this->DoPath(this->RegexStatus.match(1)[0],
  384. this->RegexStatus.match(2)[0], this->RegexStatus.match(3));
  385. }
  386. return true;
  387. }
  388. void DoPath(char path_status, char prop_status, std::string const& path)
  389. {
  390. char status = (path_status != ' ') ? path_status : prop_status;
  391. // See "svn help status".
  392. switch (status) {
  393. case 'M':
  394. case '!':
  395. case 'A':
  396. case 'D':
  397. case 'R':
  398. this->SVN->DoModification(PathModified, path);
  399. break;
  400. case 'C':
  401. case '~':
  402. this->SVN->DoModification(PathConflicting, path);
  403. break;
  404. case 'X':
  405. case 'I':
  406. case '?':
  407. case ' ':
  408. default:
  409. break;
  410. }
  411. }
  412. };
  413. bool cmCTestSVN::LoadModifications()
  414. {
  415. // Run "svn status" which reports local modifications.
  416. std::vector<std::string> svn_status;
  417. svn_status.emplace_back("status");
  418. StatusParser out(this, "status-out> ");
  419. OutputLogger err(this->Log, "status-err> ");
  420. this->RunSVNCommand(svn_status, &out, &err);
  421. return true;
  422. }
  423. void cmCTestSVN::WriteXMLGlobal(cmXMLWriter& xml)
  424. {
  425. this->cmCTestGlobalVC::WriteXMLGlobal(xml);
  426. xml.Element("SVNPath", this->RootInfo->Base);
  427. }
  428. class cmCTestSVN::ExternalParser : public cmCTestVC::LineParser
  429. {
  430. public:
  431. ExternalParser(cmCTestSVN* svn, char const* prefix)
  432. : SVN(svn)
  433. {
  434. this->SetLog(&svn->Log, prefix);
  435. this->RegexExternal.compile("^X..... +(.+)$");
  436. }
  437. private:
  438. cmCTestSVN* SVN;
  439. cmsys::RegularExpression RegexExternal;
  440. bool ProcessLine() override
  441. {
  442. if (this->RegexExternal.find(this->Line)) {
  443. this->DoPath(this->RegexExternal.match(1));
  444. }
  445. return true;
  446. }
  447. void DoPath(std::string const& path)
  448. {
  449. // Get local path relative to the source directory
  450. std::string local_path;
  451. if (path.size() > this->SVN->SourceDirectory.size() &&
  452. strncmp(path.c_str(), this->SVN->SourceDirectory.c_str(),
  453. this->SVN->SourceDirectory.size()) == 0) {
  454. local_path = path.substr(this->SVN->SourceDirectory.size() + 1);
  455. } else {
  456. local_path = path;
  457. }
  458. this->SVN->Repositories.emplace_back(local_path);
  459. }
  460. };
  461. bool cmCTestSVN::LoadRepositories()
  462. {
  463. if (!this->Repositories.empty()) {
  464. return true;
  465. }
  466. // Info for root repository
  467. this->Repositories.emplace_back();
  468. this->RootInfo = &(this->Repositories.back());
  469. // Run "svn status" to get the list of external repositories
  470. std::vector<std::string> svn_status;
  471. svn_status.emplace_back("status");
  472. ExternalParser out(this, "external-out> ");
  473. OutputLogger err(this->Log, "external-err> ");
  474. return this->RunSVNCommand(svn_status, &out, &err);
  475. }
  476. std::string cmCTestSVN::SVNInfo::BuildLocalPath(std::string const& path) const
  477. {
  478. std::string local_path;
  479. // Add local path prefix if not empty
  480. if (!this->LocalPath.empty()) {
  481. local_path += this->LocalPath;
  482. local_path += "/";
  483. }
  484. // Add path with base prefix removed
  485. if (path.size() > this->Base.size() &&
  486. strncmp(path.c_str(), this->Base.c_str(), this->Base.size()) == 0) {
  487. local_path += path.substr(this->Base.size());
  488. } else {
  489. local_path += path;
  490. }
  491. return local_path;
  492. }