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