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