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