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