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 "cmCTestCVS.h"
4 
5 #include <utility>
6 
7 #include <cm/string_view>
8 
9 #include "cmsys/FStream.hxx"
10 #include "cmsys/RegularExpression.hxx"
11 
12 #include "cmCTest.h"
13 #include "cmProcessTools.h"
14 #include "cmStringAlgorithms.h"
15 #include "cmSystemTools.h"
16 #include "cmXMLWriter.h"
17 
cmCTestCVS(cmCTest * ct,std::ostream & log)18 cmCTestCVS::cmCTestCVS(cmCTest* ct, std::ostream& log)
19   : cmCTestVC(ct, log)
20 {
21 }
22 
23 cmCTestCVS::~cmCTestCVS() = default;
24 
25 class cmCTestCVS::UpdateParser : public cmCTestVC::LineParser
26 {
27 public:
UpdateParser(cmCTestCVS * cvs,const char * prefix)28   UpdateParser(cmCTestCVS* cvs, const char* prefix)
29     : CVS(cvs)
30   {
31     this->SetLog(&cvs->Log, prefix);
32     // See "man cvs", section "update output".
33     this->RegexFileUpdated.compile("^([UP])  *(.*)");
34     this->RegexFileModified.compile("^([MRA])  *(.*)");
35     this->RegexFileConflicting.compile("^([C])  *(.*)");
36     this->RegexFileRemoved1.compile(
37       "cvs[^ ]* update: `?([^']*)'? is no longer in the repository");
38     this->RegexFileRemoved2.compile(
39       "cvs[^ ]* update: "
40       "warning: `?([^']*)'? is not \\(any longer\\) pertinent");
41   }
42 
43 private:
44   cmCTestCVS* CVS;
45   cmsys::RegularExpression RegexFileUpdated;
46   cmsys::RegularExpression RegexFileModified;
47   cmsys::RegularExpression RegexFileConflicting;
48   cmsys::RegularExpression RegexFileRemoved1;
49   cmsys::RegularExpression RegexFileRemoved2;
50 
ProcessLine()51   bool ProcessLine() override
52   {
53     if (this->RegexFileUpdated.find(this->Line)) {
54       this->DoFile(PathUpdated, this->RegexFileUpdated.match(2));
55     } else if (this->RegexFileModified.find(this->Line)) {
56       this->DoFile(PathModified, this->RegexFileModified.match(2));
57     } else if (this->RegexFileConflicting.find(this->Line)) {
58       this->DoFile(PathConflicting, this->RegexFileConflicting.match(2));
59     } else if (this->RegexFileRemoved1.find(this->Line)) {
60       this->DoFile(PathUpdated, this->RegexFileRemoved1.match(1));
61     } else if (this->RegexFileRemoved2.find(this->Line)) {
62       this->DoFile(PathUpdated, this->RegexFileRemoved2.match(1));
63     }
64     return true;
65   }
66 
DoFile(PathStatus status,std::string const & file)67   void DoFile(PathStatus status, std::string const& file)
68   {
69     std::string dir = cmSystemTools::GetFilenamePath(file);
70     std::string name = cmSystemTools::GetFilenameName(file);
71     this->CVS->Dirs[dir][name] = status;
72   }
73 };
74 
UpdateImpl()75 bool cmCTestCVS::UpdateImpl()
76 {
77   // Get user-specified update options.
78   std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
79   if (opts.empty()) {
80     opts = this->CTest->GetCTestConfiguration("CVSUpdateOptions");
81     if (opts.empty()) {
82       opts = "-dP";
83     }
84   }
85   std::vector<std::string> args = cmSystemTools::ParseArguments(opts);
86 
87   // Specify the start time for nightly testing.
88   if (this->CTest->GetTestModel() == cmCTest::NIGHTLY) {
89     args.push_back("-D" + this->GetNightlyTime() + " UTC");
90   }
91 
92   // Run "cvs update" to update the work tree.
93   std::vector<char const*> cvs_update;
94   cvs_update.push_back(this->CommandLineTool.c_str());
95   cvs_update.push_back("-z3");
96   cvs_update.push_back("update");
97   for (std::string const& arg : args) {
98     cvs_update.push_back(arg.c_str());
99   }
100   cvs_update.push_back(nullptr);
101 
102   UpdateParser out(this, "up-out> ");
103   UpdateParser err(this, "up-err> ");
104   return this->RunUpdateCommand(&cvs_update[0], &out, &err);
105 }
106 
107 class cmCTestCVS::LogParser : public cmCTestVC::LineParser
108 {
109 public:
110   using Revision = cmCTestCVS::Revision;
LogParser(cmCTestCVS * cvs,const char * prefix,std::vector<Revision> & revs)111   LogParser(cmCTestCVS* cvs, const char* prefix, std::vector<Revision>& revs)
112     : CVS(cvs)
113     , Revisions(revs)
114     , Section(SectionHeader)
115   {
116     this->SetLog(&cvs->Log, prefix);
117     this->RegexRevision.compile("^revision +([^ ]*) *$");
118     this->RegexBranches.compile("^branches: .*$");
119     this->RegexPerson.compile("^date: +([^;]+); +author: +([^;]+);");
120   }
121 
122 private:
123   cmCTestCVS* CVS;
124   std::vector<Revision>& Revisions;
125   cmsys::RegularExpression RegexRevision;
126   cmsys::RegularExpression RegexBranches;
127   cmsys::RegularExpression RegexPerson;
128   enum SectionType
129   {
130     SectionHeader,
131     SectionRevisions,
132     SectionEnd
133   };
134   SectionType Section;
135   Revision Rev;
136 
ProcessLine()137   bool ProcessLine() override
138   {
139     if (this->Line ==
140         ("======================================="
141          "======================================")) {
142       // This line ends the revision list.
143       if (this->Section == SectionRevisions) {
144         this->FinishRevision();
145       }
146       this->Section = SectionEnd;
147     } else if (this->Line == "----------------------------") {
148       // This line divides revisions from the header and each other.
149       if (this->Section == SectionHeader) {
150         this->Section = SectionRevisions;
151       } else if (this->Section == SectionRevisions) {
152         this->FinishRevision();
153       }
154     } else if (this->Section == SectionRevisions) {
155       // XXX(clang-tidy): https://bugs.llvm.org/show_bug.cgi?id=44165
156       // NOLINTNEXTLINE(bugprone-branch-clone)
157       if (!this->Rev.Log.empty()) {
158         // Continue the existing log.
159         this->Rev.Log += this->Line;
160         this->Rev.Log += '\n';
161       } else if (this->Rev.Rev.empty() &&
162                  this->RegexRevision.find(this->Line)) {
163         this->Rev.Rev = this->RegexRevision.match(1);
164       } else if (this->Rev.Date.empty() &&
165                  this->RegexPerson.find(this->Line)) {
166         this->Rev.Date = this->RegexPerson.match(1);
167         this->Rev.Author = this->RegexPerson.match(2);
168       } else if (!this->RegexBranches.find(this->Line)) {
169         // Start the log.
170         this->Rev.Log += this->Line;
171         this->Rev.Log += '\n';
172       }
173     }
174     return this->Section != SectionEnd;
175   }
176 
FinishRevision()177   void FinishRevision()
178   {
179     if (!this->Rev.Rev.empty()) {
180       // Record this revision.
181       /* clang-format off */
182       this->CVS->Log << "Found revision " << this->Rev.Rev << "\n"
183                      << "  author = " << this->Rev.Author << "\n"
184                      << "  date = " << this->Rev.Date << "\n";
185       /* clang-format on */
186       this->Revisions.push_back(this->Rev);
187 
188       // We only need two revisions.
189       if (this->Revisions.size() >= 2) {
190         this->Section = SectionEnd;
191       }
192     }
193     this->Rev = Revision();
194   }
195 };
196 
ComputeBranchFlag(std::string const & dir)197 std::string cmCTestCVS::ComputeBranchFlag(std::string const& dir)
198 {
199   // Compute the tag file location for this directory.
200   std::string tagFile = this->SourceDirectory;
201   if (!dir.empty()) {
202     tagFile += "/";
203     tagFile += dir;
204   }
205   tagFile += "/CVS/Tag";
206 
207   // Lookup the branch in the tag file, if any.
208   std::string tagLine;
209   cmsys::ifstream tagStream(tagFile.c_str());
210   if (tagStream && cmSystemTools::GetLineFromStream(tagStream, tagLine) &&
211       tagLine.size() > 1 && tagLine[0] == 'T') {
212     // Use the branch specified in the tag file.
213     std::string flag = cmStrCat("-r", cm::string_view(tagLine).substr(1));
214     return flag;
215   }
216   // Use the default branch.
217   return "-b";
218 }
219 
LoadRevisions(std::string const & file,const char * branchFlag,std::vector<Revision> & revisions)220 void cmCTestCVS::LoadRevisions(std::string const& file, const char* branchFlag,
221                                std::vector<Revision>& revisions)
222 {
223   cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush);
224 
225   // Run "cvs log" to get revisions of this file on this branch.
226   const char* cvs = this->CommandLineTool.c_str();
227   const char* cvs_log[] = {
228     cvs, "log", "-N", branchFlag, file.c_str(), nullptr
229   };
230 
231   LogParser out(this, "log-out> ", revisions);
232   OutputLogger err(this->Log, "log-err> ");
233   this->RunChild(cvs_log, &out, &err);
234 }
235 
WriteXMLDirectory(cmXMLWriter & xml,std::string const & path,Directory const & dir)236 void cmCTestCVS::WriteXMLDirectory(cmXMLWriter& xml, std::string const& path,
237                                    Directory const& dir)
238 {
239   const char* slash = path.empty() ? "" : "/";
240   xml.StartElement("Directory");
241   xml.Element("Name", path);
242 
243   // Lookup the branch checked out in the working tree.
244   std::string branchFlag = this->ComputeBranchFlag(path);
245 
246   // Load revisions and write an entry for each file in this directory.
247   std::vector<Revision> revisions;
248   for (auto const& fi : dir) {
249     std::string full = path + slash + fi.first;
250 
251     // Load two real or unknown revisions.
252     revisions.clear();
253     if (fi.second != PathUpdated) {
254       // For local modifications the current rev is unknown and the
255       // prior rev is the latest from cvs.
256       revisions.push_back(this->Unknown);
257     }
258     this->LoadRevisions(full, branchFlag.c_str(), revisions);
259     revisions.resize(2, this->Unknown);
260 
261     // Write the entry for this file with these revisions.
262     File f(fi.second, &revisions[0], &revisions[1]);
263     this->WriteXMLEntry(xml, path, fi.first, full, f);
264   }
265   xml.EndElement(); // Directory
266 }
267 
WriteXMLUpdates(cmXMLWriter & xml)268 bool cmCTestCVS::WriteXMLUpdates(cmXMLWriter& xml)
269 {
270   cmCTestLog(this->CTest, HANDLER_OUTPUT,
271              "   Gathering version information (one . per updated file):\n"
272              "    "
273                << std::flush);
274 
275   for (auto const& d : this->Dirs) {
276     this->WriteXMLDirectory(xml, d.first, d.second);
277   }
278 
279   cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl);
280 
281   return true;
282 }
283