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 "cmCTestGIT.h"
4
5 #include <cctype>
6 #include <cstdio>
7 #include <cstdlib>
8 #include <ctime>
9 #include <utility>
10 #include <vector>
11
12 #include "cmsys/FStream.hxx"
13 #include "cmsys/Process.h"
14
15 #include "cmCTest.h"
16 #include "cmCTestVC.h"
17 #include "cmProcessOutput.h"
18 #include "cmProcessTools.h"
19 #include "cmStringAlgorithms.h"
20 #include "cmSystemTools.h"
21 #include "cmValue.h"
22
cmCTestGITVersion(unsigned int epic,unsigned int major,unsigned int minor,unsigned int fix)23 static unsigned int cmCTestGITVersion(unsigned int epic, unsigned int major,
24 unsigned int minor, unsigned int fix)
25 {
26 // 1.6.5.0 maps to 10605000
27 return fix + minor * 1000 + major * 100000 + epic * 10000000;
28 }
29
cmCTestGIT(cmCTest * ct,std::ostream & log)30 cmCTestGIT::cmCTestGIT(cmCTest* ct, std::ostream& log)
31 : cmCTestGlobalVC(ct, log)
32 {
33 this->PriorRev = this->Unknown;
34 this->CurrentGitVersion = 0;
35 }
36
37 cmCTestGIT::~cmCTestGIT() = default;
38
39 class cmCTestGIT::OneLineParser : public cmCTestVC::LineParser
40 {
41 public:
OneLineParser(cmCTestGIT * git,const char * prefix,std::string & l)42 OneLineParser(cmCTestGIT* git, const char* prefix, std::string& l)
43 : Line1(l)
44 {
45 this->SetLog(&git->Log, prefix);
46 }
47
48 private:
49 std::string& Line1;
ProcessLine()50 bool ProcessLine() override
51 {
52 // Only the first line is of interest.
53 this->Line1 = this->Line;
54 return false;
55 }
56 };
57
GetWorkingRevision()58 std::string cmCTestGIT::GetWorkingRevision()
59 {
60 // Run plumbing "git rev-list" to get work tree revision.
61 const char* git = this->CommandLineTool.c_str();
62 const char* git_rev_list[] = { git, "rev-list", "-n", "1",
63 "HEAD", "--", nullptr };
64 std::string rev;
65 OneLineParser out(this, "rl-out> ", rev);
66 OutputLogger err(this->Log, "rl-err> ");
67 this->RunChild(git_rev_list, &out, &err);
68 return rev;
69 }
70
NoteOldRevision()71 bool cmCTestGIT::NoteOldRevision()
72 {
73 this->OldRevision = this->GetWorkingRevision();
74 cmCTestLog(this->CTest, HANDLER_OUTPUT,
75 " Old revision of repository is: " << this->OldRevision
76 << "\n");
77 this->PriorRev.Rev = this->OldRevision;
78 return true;
79 }
80
NoteNewRevision()81 bool cmCTestGIT::NoteNewRevision()
82 {
83 this->NewRevision = this->GetWorkingRevision();
84 cmCTestLog(this->CTest, HANDLER_OUTPUT,
85 " New revision of repository is: " << this->NewRevision
86 << "\n");
87 return true;
88 }
89
FindGitDir()90 std::string cmCTestGIT::FindGitDir()
91 {
92 std::string git_dir;
93
94 // Run "git rev-parse --git-dir" to locate the real .git directory.
95 const char* git = this->CommandLineTool.c_str();
96 char const* git_rev_parse[] = { git, "rev-parse", "--git-dir", nullptr };
97 std::string git_dir_line;
98 OneLineParser rev_parse_out(this, "rev-parse-out> ", git_dir_line);
99 OutputLogger rev_parse_err(this->Log, "rev-parse-err> ");
100 if (this->RunChild(git_rev_parse, &rev_parse_out, &rev_parse_err, nullptr,
101 cmProcessOutput::UTF8)) {
102 git_dir = git_dir_line;
103 }
104 if (git_dir.empty()) {
105 git_dir = ".git";
106 }
107
108 // Git reports a relative path only when the .git directory is in
109 // the current directory.
110 if (git_dir[0] == '.') {
111 git_dir = this->SourceDirectory + "/" + git_dir;
112 }
113 #if defined(_WIN32) && !defined(__CYGWIN__)
114 else if (git_dir[0] == '/') {
115 // Cygwin Git reports a full path that Cygwin understands, but we
116 // are a Windows application. Run "cygpath" to get Windows path.
117 std::string cygpath_exe =
118 cmStrCat(cmSystemTools::GetFilenamePath(git), "/cygpath.exe");
119 if (cmSystemTools::FileExists(cygpath_exe)) {
120 char const* cygpath[] = { cygpath_exe.c_str(), "-w", git_dir.c_str(),
121 0 };
122 OneLineParser cygpath_out(this, "cygpath-out> ", git_dir_line);
123 OutputLogger cygpath_err(this->Log, "cygpath-err> ");
124 if (this->RunChild(cygpath, &cygpath_out, &cygpath_err, nullptr,
125 cmProcessOutput::UTF8)) {
126 git_dir = git_dir_line;
127 }
128 }
129 }
130 #endif
131 return git_dir;
132 }
133
FindTopDir()134 std::string cmCTestGIT::FindTopDir()
135 {
136 std::string top_dir = this->SourceDirectory;
137
138 // Run "git rev-parse --show-cdup" to locate the top of the tree.
139 const char* git = this->CommandLineTool.c_str();
140 char const* git_rev_parse[] = { git, "rev-parse", "--show-cdup", nullptr };
141 std::string cdup;
142 OneLineParser rev_parse_out(this, "rev-parse-out> ", cdup);
143 OutputLogger rev_parse_err(this->Log, "rev-parse-err> ");
144 if (this->RunChild(git_rev_parse, &rev_parse_out, &rev_parse_err, nullptr,
145 cmProcessOutput::UTF8) &&
146 !cdup.empty()) {
147 top_dir += "/";
148 top_dir += cdup;
149 top_dir = cmSystemTools::CollapseFullPath(top_dir);
150 }
151 return top_dir;
152 }
153
UpdateByFetchAndReset()154 bool cmCTestGIT::UpdateByFetchAndReset()
155 {
156 const char* git = this->CommandLineTool.c_str();
157
158 // Use "git fetch" to get remote commits.
159 std::vector<char const*> git_fetch;
160 git_fetch.push_back(git);
161 git_fetch.push_back("fetch");
162
163 // Add user-specified update options.
164 std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
165 if (opts.empty()) {
166 opts = this->CTest->GetCTestConfiguration("GITUpdateOptions");
167 }
168 std::vector<std::string> args = cmSystemTools::ParseArguments(opts);
169 for (std::string const& arg : args) {
170 git_fetch.push_back(arg.c_str());
171 }
172
173 // Sentinel argument.
174 git_fetch.push_back(nullptr);
175
176 // Fetch upstream refs.
177 OutputLogger fetch_out(this->Log, "fetch-out> ");
178 OutputLogger fetch_err(this->Log, "fetch-err> ");
179 if (!this->RunUpdateCommand(&git_fetch[0], &fetch_out, &fetch_err)) {
180 return false;
181 }
182
183 // Identify the merge head that would be used by "git pull".
184 std::string sha1;
185 {
186 std::string fetch_head = this->FindGitDir() + "/FETCH_HEAD";
187 cmsys::ifstream fin(fetch_head.c_str(), std::ios::in | std::ios::binary);
188 if (!fin) {
189 this->Log << "Unable to open " << fetch_head << "\n";
190 return false;
191 }
192 std::string line;
193 while (sha1.empty() && cmSystemTools::GetLineFromStream(fin, line)) {
194 this->Log << "FETCH_HEAD> " << line << "\n";
195 if (line.find("\tnot-for-merge\t") == std::string::npos) {
196 std::string::size_type pos = line.find('\t');
197 if (pos != std::string::npos) {
198 sha1 = std::move(line);
199 sha1.resize(pos);
200 }
201 }
202 }
203 if (sha1.empty()) {
204 this->Log << "FETCH_HEAD has no upstream branch candidate!\n";
205 return false;
206 }
207 }
208
209 // Reset the local branch to point at that tracked from upstream.
210 char const* git_reset[] = { git, "reset", "--hard", sha1.c_str(), nullptr };
211 OutputLogger reset_out(this->Log, "reset-out> ");
212 OutputLogger reset_err(this->Log, "reset-err> ");
213 return this->RunChild(&git_reset[0], &reset_out, &reset_err);
214 }
215
UpdateByCustom(std::string const & custom)216 bool cmCTestGIT::UpdateByCustom(std::string const& custom)
217 {
218 std::vector<std::string> git_custom_command = cmExpandedList(custom, true);
219 std::vector<char const*> git_custom;
220 git_custom.reserve(git_custom_command.size() + 1);
221 for (std::string const& i : git_custom_command) {
222 git_custom.push_back(i.c_str());
223 }
224 git_custom.push_back(nullptr);
225
226 OutputLogger custom_out(this->Log, "custom-out> ");
227 OutputLogger custom_err(this->Log, "custom-err> ");
228 return this->RunUpdateCommand(&git_custom[0], &custom_out, &custom_err);
229 }
230
UpdateInternal()231 bool cmCTestGIT::UpdateInternal()
232 {
233 std::string custom = this->CTest->GetCTestConfiguration("GITUpdateCustom");
234 if (!custom.empty()) {
235 return this->UpdateByCustom(custom);
236 }
237 return this->UpdateByFetchAndReset();
238 }
239
UpdateImpl()240 bool cmCTestGIT::UpdateImpl()
241 {
242 if (!this->UpdateInternal()) {
243 return false;
244 }
245
246 std::string top_dir = this->FindTopDir();
247 const char* git = this->CommandLineTool.c_str();
248 const char* recursive = "--recursive";
249 const char* sync_recursive = "--recursive";
250
251 // Git < 1.6.5 did not support submodule --recursive
252 if (this->GetGitVersion() < cmCTestGITVersion(1, 6, 5, 0)) {
253 recursive = nullptr;
254 // No need to require >= 1.6.5 if there are no submodules.
255 if (cmSystemTools::FileExists(top_dir + "/.gitmodules")) {
256 this->Log << "Git < 1.6.5 cannot update submodules recursively\n";
257 }
258 }
259
260 // Git < 1.8.1 did not support sync --recursive
261 if (this->GetGitVersion() < cmCTestGITVersion(1, 8, 1, 0)) {
262 sync_recursive = nullptr;
263 // No need to require >= 1.8.1 if there are no submodules.
264 if (cmSystemTools::FileExists(top_dir + "/.gitmodules")) {
265 this->Log << "Git < 1.8.1 cannot synchronize submodules recursively\n";
266 }
267 }
268
269 OutputLogger submodule_out(this->Log, "submodule-out> ");
270 OutputLogger submodule_err(this->Log, "submodule-err> ");
271
272 bool ret;
273
274 std::string init_submodules =
275 this->CTest->GetCTestConfiguration("GITInitSubmodules");
276 if (cmIsOn(init_submodules)) {
277 char const* git_submodule_init[] = { git, "submodule", "init", nullptr };
278 ret = this->RunChild(git_submodule_init, &submodule_out, &submodule_err,
279 top_dir.c_str());
280
281 if (!ret) {
282 return false;
283 }
284 }
285
286 char const* git_submodule_sync[] = { git, "submodule", "sync",
287 sync_recursive, nullptr };
288 ret = this->RunChild(git_submodule_sync, &submodule_out, &submodule_err,
289 top_dir.c_str());
290
291 if (!ret) {
292 return false;
293 }
294
295 char const* git_submodule[] = { git, "submodule", "update", recursive,
296 nullptr };
297 return this->RunChild(git_submodule, &submodule_out, &submodule_err,
298 top_dir.c_str());
299 }
300
GetGitVersion()301 unsigned int cmCTestGIT::GetGitVersion()
302 {
303 if (!this->CurrentGitVersion) {
304 const char* git = this->CommandLineTool.c_str();
305 char const* git_version[] = { git, "--version", nullptr };
306 std::string version;
307 OneLineParser version_out(this, "version-out> ", version);
308 OutputLogger version_err(this->Log, "version-err> ");
309 unsigned int v[4] = { 0, 0, 0, 0 };
310 if (this->RunChild(git_version, &version_out, &version_err) &&
311 sscanf(version.c_str(), "git version %u.%u.%u.%u", &v[0], &v[1], &v[2],
312 &v[3]) >= 3) {
313 this->CurrentGitVersion = cmCTestGITVersion(v[0], v[1], v[2], v[3]);
314 }
315 }
316 return this->CurrentGitVersion;
317 }
318
319 /* Diff format:
320
321 :src-mode dst-mode src-sha1 dst-sha1 status\0
322 src-path\0
323 [dst-path\0]
324
325 The format is repeated for every file changed. The [dst-path\0]
326 line appears only for lines with status 'C' or 'R'. See 'git help
327 diff-tree' for details.
328 */
329 class cmCTestGIT::DiffParser : public cmCTestVC::LineParser
330 {
331 public:
DiffParser(cmCTestGIT * git,const char * prefix)332 DiffParser(cmCTestGIT* git, const char* prefix)
333 : LineParser('\0', false)
334 , GIT(git)
335 , DiffField(DiffFieldNone)
336 {
337 this->SetLog(&git->Log, prefix);
338 }
339
340 using Change = cmCTestGIT::Change;
341 std::vector<Change> Changes;
342
343 protected:
344 cmCTestGIT* GIT;
345 enum DiffFieldType
346 {
347 DiffFieldNone,
348 DiffFieldChange,
349 DiffFieldSrc,
350 DiffFieldDst
351 };
352 DiffFieldType DiffField;
353 Change CurChange;
354
DiffReset()355 void DiffReset()
356 {
357 this->DiffField = DiffFieldNone;
358 this->Changes.clear();
359 }
360
ProcessLine()361 bool ProcessLine() override
362 {
363 if (this->Line[0] == ':') {
364 this->DiffField = DiffFieldChange;
365 this->CurChange = Change();
366 }
367 if (this->DiffField == DiffFieldChange) {
368 // :src-mode dst-mode src-sha1 dst-sha1 status
369 if (this->Line[0] != ':') {
370 this->DiffField = DiffFieldNone;
371 return true;
372 }
373 const char* src_mode_first = this->Line.c_str() + 1;
374 const char* src_mode_last = this->ConsumeField(src_mode_first);
375 const char* dst_mode_first = this->ConsumeSpace(src_mode_last);
376 const char* dst_mode_last = this->ConsumeField(dst_mode_first);
377 const char* src_sha1_first = this->ConsumeSpace(dst_mode_last);
378 const char* src_sha1_last = this->ConsumeField(src_sha1_first);
379 const char* dst_sha1_first = this->ConsumeSpace(src_sha1_last);
380 const char* dst_sha1_last = this->ConsumeField(dst_sha1_first);
381 const char* status_first = this->ConsumeSpace(dst_sha1_last);
382 const char* status_last = this->ConsumeField(status_first);
383 if (status_first != status_last) {
384 this->CurChange.Action = *status_first;
385 this->DiffField = DiffFieldSrc;
386 } else {
387 this->DiffField = DiffFieldNone;
388 }
389 } else if (this->DiffField == DiffFieldSrc) {
390 // src-path
391 if (this->CurChange.Action == 'C') {
392 // Convert copy to addition of destination.
393 this->CurChange.Action = 'A';
394 this->DiffField = DiffFieldDst;
395 } else if (this->CurChange.Action == 'R') {
396 // Convert rename to deletion of source and addition of destination.
397 this->CurChange.Action = 'D';
398 this->CurChange.Path = this->Line;
399 this->Changes.push_back(this->CurChange);
400
401 this->CurChange = Change('A');
402 this->DiffField = DiffFieldDst;
403 } else {
404 this->CurChange.Path = this->Line;
405 this->Changes.push_back(this->CurChange);
406 this->DiffField = this->DiffFieldNone;
407 }
408 } else if (this->DiffField == DiffFieldDst) {
409 // dst-path
410 this->CurChange.Path = this->Line;
411 this->Changes.push_back(this->CurChange);
412 this->DiffField = this->DiffFieldNone;
413 }
414 return true;
415 }
416
ConsumeSpace(const char * c)417 const char* ConsumeSpace(const char* c)
418 {
419 while (*c && isspace(*c)) {
420 ++c;
421 }
422 return c;
423 }
ConsumeField(const char * c)424 const char* ConsumeField(const char* c)
425 {
426 while (*c && !isspace(*c)) {
427 ++c;
428 }
429 return c;
430 }
431 };
432
433 /* Commit format:
434
435 commit ...\n
436 tree ...\n
437 parent ...\n
438 author ...\n
439 committer ...\n
440 \n
441 Log message indented by (4) spaces\n
442 (even blank lines have the spaces)\n
443 [[
444 \n
445 [Diff format]
446 OR
447 \0
448 ]]
449
450 The header may have more fields. See 'git help diff-tree'.
451 */
452 class cmCTestGIT::CommitParser : public cmCTestGIT::DiffParser
453 {
454 public:
CommitParser(cmCTestGIT * git,const char * prefix)455 CommitParser(cmCTestGIT* git, const char* prefix)
456 : DiffParser(git, prefix)
457 , Section(SectionHeader)
458 {
459 this->Separator = SectionSep[this->Section];
460 }
461
462 private:
463 using Revision = cmCTestGIT::Revision;
464 enum SectionType
465 {
466 SectionHeader,
467 SectionBody,
468 SectionDiff,
469 SectionCount
470 };
471 static char const SectionSep[SectionCount];
472 SectionType Section;
473 Revision Rev;
474
475 struct Person
476 {
477 std::string Name;
478 std::string EMail;
479 unsigned long Time = 0;
480 long TimeZone = 0;
481 };
482
ParsePerson(const char * str,Person & person)483 void ParsePerson(const char* str, Person& person)
484 {
485 // Person Name <person@domain.com> 1234567890 +0000
486 const char* c = str;
487 while (*c && isspace(*c)) {
488 ++c;
489 }
490
491 const char* name_first = c;
492 while (*c && *c != '<') {
493 ++c;
494 }
495 const char* name_last = c;
496 while (name_last != name_first && isspace(*(name_last - 1))) {
497 --name_last;
498 }
499 person.Name.assign(name_first, name_last - name_first);
500
501 const char* email_first = *c ? ++c : c;
502 while (*c && *c != '>') {
503 ++c;
504 }
505 const char* email_last = *c ? c++ : c;
506 person.EMail.assign(email_first, email_last - email_first);
507
508 person.Time = strtoul(c, const_cast<char**>(&c), 10);
509 person.TimeZone = strtol(c, const_cast<char**>(&c), 10);
510 }
511
ProcessLine()512 bool ProcessLine() override
513 {
514 if (this->Line.empty()) {
515 if (this->Section == SectionBody && this->LineEnd == '\0') {
516 // Skip SectionDiff
517 this->NextSection();
518 }
519 this->NextSection();
520 } else {
521 switch (this->Section) {
522 case SectionHeader:
523 this->DoHeaderLine();
524 break;
525 case SectionBody:
526 this->DoBodyLine();
527 break;
528 case SectionDiff:
529 this->DiffParser::ProcessLine();
530 break;
531 case SectionCount:
532 break; // never happens
533 }
534 }
535 return true;
536 }
537
NextSection()538 void NextSection()
539 {
540 this->Section = SectionType((this->Section + 1) % SectionCount);
541 this->Separator = SectionSep[this->Section];
542 if (this->Section == SectionHeader) {
543 this->GIT->DoRevision(this->Rev, this->Changes);
544 this->Rev = Revision();
545 this->DiffReset();
546 }
547 }
548
DoHeaderLine()549 void DoHeaderLine()
550 {
551 // Look for header fields that we need.
552 if (cmHasLiteralPrefix(this->Line, "commit ")) {
553 this->Rev.Rev = this->Line.substr(7);
554 } else if (cmHasLiteralPrefix(this->Line, "author ")) {
555 Person author;
556 this->ParsePerson(this->Line.c_str() + 7, author);
557 this->Rev.Author = author.Name;
558 this->Rev.EMail = author.EMail;
559 this->Rev.Date = this->FormatDateTime(author);
560 } else if (cmHasLiteralPrefix(this->Line, "committer ")) {
561 Person committer;
562 this->ParsePerson(this->Line.c_str() + 10, committer);
563 this->Rev.Committer = committer.Name;
564 this->Rev.CommitterEMail = committer.EMail;
565 this->Rev.CommitDate = this->FormatDateTime(committer);
566 }
567 }
568
DoBodyLine()569 void DoBodyLine()
570 {
571 // Commit log lines are indented by 4 spaces.
572 if (this->Line.size() >= 4) {
573 this->Rev.Log += this->Line.substr(4);
574 }
575 this->Rev.Log += "\n";
576 }
577
FormatDateTime(Person const & person)578 std::string FormatDateTime(Person const& person)
579 {
580 // Convert the time to a human-readable format that is also easy
581 // to machine-parse: "CCYY-MM-DD hh:mm:ss".
582 time_t seconds = static_cast<time_t>(person.Time);
583 struct tm* t = gmtime(&seconds);
584 char dt[1024];
585 sprintf(dt, "%04d-%02d-%02d %02d:%02d:%02d", t->tm_year + 1900,
586 t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
587 std::string out = dt;
588
589 // Add the time-zone field "+zone" or "-zone".
590 char tz[32];
591 if (person.TimeZone >= 0) {
592 sprintf(tz, " +%04ld", person.TimeZone);
593 } else {
594 sprintf(tz, " -%04ld", -person.TimeZone);
595 }
596 out += tz;
597 return out;
598 }
599 };
600
601 char const cmCTestGIT::CommitParser::SectionSep[SectionCount] = { '\n', '\n',
602 '\0' };
603
LoadRevisions()604 bool cmCTestGIT::LoadRevisions()
605 {
606 // Use 'git rev-list ... | git diff-tree ...' to get revisions.
607 std::string range = this->OldRevision + ".." + this->NewRevision;
608 const char* git = this->CommandLineTool.c_str();
609 const char* git_rev_list[] = { git, "rev-list", "--reverse",
610 range.c_str(), "--", nullptr };
611 const char* git_diff_tree[] = {
612 git, "diff-tree", "--stdin", "--always", "-z",
613 "-r", "--pretty=raw", "--encoding=utf-8", nullptr
614 };
615 this->Log << cmCTestGIT::ComputeCommandLine(git_rev_list) << " | "
616 << cmCTestGIT::ComputeCommandLine(git_diff_tree) << "\n";
617
618 cmsysProcess* cp = cmsysProcess_New();
619 cmsysProcess_AddCommand(cp, git_rev_list);
620 cmsysProcess_AddCommand(cp, git_diff_tree);
621 cmsysProcess_SetWorkingDirectory(cp, this->SourceDirectory.c_str());
622
623 CommitParser out(this, "dt-out> ");
624 OutputLogger err(this->Log, "dt-err> ");
625 cmCTestGIT::RunProcess(cp, &out, &err, cmProcessOutput::UTF8);
626
627 // Send one extra zero-byte to terminate the last record.
628 out.Process("", 1);
629
630 cmsysProcess_Delete(cp);
631 return true;
632 }
633
LoadModifications()634 bool cmCTestGIT::LoadModifications()
635 {
636 const char* git = this->CommandLineTool.c_str();
637
638 // Use 'git update-index' to refresh the index w.r.t. the work tree.
639 const char* git_update_index[] = { git, "update-index", "--refresh",
640 nullptr };
641 OutputLogger ui_out(this->Log, "ui-out> ");
642 OutputLogger ui_err(this->Log, "ui-err> ");
643 this->RunChild(git_update_index, &ui_out, &ui_err, nullptr,
644 cmProcessOutput::UTF8);
645
646 // Use 'git diff-index' to get modified files.
647 const char* git_diff_index[] = { git, "diff-index", "-z",
648 "HEAD", "--", nullptr };
649 DiffParser out(this, "di-out> ");
650 OutputLogger err(this->Log, "di-err> ");
651 this->RunChild(git_diff_index, &out, &err, nullptr, cmProcessOutput::UTF8);
652
653 for (Change const& c : out.Changes) {
654 this->DoModification(PathModified, c.Path);
655 }
656 return true;
657 }
658