cmCTestSVN.cxx 17 KB

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