| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| file LICENSE.rst or https://cmake.org/licensing for details. */ | |
| #include "cmCTestUpdateCommand.h" | |
| #include <chrono> | |
| #include <iosfwd> | |
| #include <string> | |
| #include <cm/memory> | |
| #include <cmext/string_view> | |
| #include "cmArgumentParser.h" | |
| #include "cmCLocaleEnvironmentScope.h" | |
| #include "cmCTest.h" | |
| #include "cmCTestBZR.h" | |
| #include "cmCTestCVS.h" | |
| #include "cmCTestGIT.h" | |
| #include "cmCTestHG.h" | |
| #include "cmCTestP4.h" | |
| #include "cmCTestSVN.h" | |
| #include "cmCTestVC.h" | |
| #include "cmExecutionStatus.h" | |
| #include "cmGeneratedFileStream.h" | |
| #include "cmMakefile.h" | |
| #include "cmStringAlgorithms.h" | |
| #include "cmSystemTools.h" | |
| #include "cmVersion.h" | |
| #include "cmXMLWriter.h" | |
| namespace { | |
| enum | |
| { | |
| e_UNKNOWN = 0, | |
| e_CVS, | |
| e_SVN, | |
| e_BZR, | |
| e_GIT, | |
| e_HG, | |
| e_P4, | |
| e_LAST | |
| }; | |
| char const* TypeToString(int type) | |
| { | |
| // clang-format off | |
| switch (type) { | |
| case e_CVS: return "CVS"; | |
| case e_SVN: return "SVN"; | |
| case e_BZR: return "BZR"; | |
| case e_GIT: return "GIT"; | |
| case e_HG: return "HG"; | |
| case e_P4: return "P4"; | |
| default: return "Unknown"; | |
| } | |
| // clang-format on | |
| } | |
| char const* TypeToCommandKey(int type) | |
| { | |
| // clang-format off | |
| switch (type) { | |
| case e_CVS: return "CTEST_CVS_COMMAND"; | |
| case e_SVN: return "CTEST_SVN_COMMAND"; | |
| case e_BZR: return "CTEST_BZR_COMMAND"; | |
| case e_GIT: return "CTEST_GIT_COMMAND"; | |
| case e_HG: return "CTEST_HG_COMMAND"; | |
| case e_P4: return "CTEST_P4_COMMAND"; | |
| default: return nullptr; | |
| } | |
| // clang-format on | |
| } | |
| int DetermineType(std::string const& cmd) | |
| { | |
| std::string stype = cmSystemTools::LowerCase(cmd); | |
| if (stype.find("cvs") != std::string::npos) { | |
| return e_CVS; | |
| } | |
| if (stype.find("svn") != std::string::npos) { | |
| return e_SVN; | |
| } | |
| if (stype.find("bzr") != std::string::npos) { | |
| return e_BZR; | |
| } | |
| if (stype.find("git") != std::string::npos) { | |
| return e_GIT; | |
| } | |
| if (stype.find("hg") != std::string::npos) { | |
| return e_HG; | |
| } | |
| if (stype.find("p4") != std::string::npos) { | |
| return e_P4; | |
| } | |
| return e_UNKNOWN; | |
| } | |
| int DetectVCS(std::string const& dir) | |
| { | |
| if (cmSystemTools::FileExists(cmStrCat(dir, "/CVS"))) { | |
| return e_CVS; | |
| } | |
| if (cmSystemTools::FileExists(cmStrCat(dir, "/.svn"))) { | |
| return e_SVN; | |
| } | |
| if (cmSystemTools::FileExists(cmStrCat(dir, "/.bzr"))) { | |
| return e_BZR; | |
| } | |
| if (cmSystemTools::FileExists(cmStrCat(dir, "/.git"))) { | |
| return e_GIT; | |
| } | |
| if (cmSystemTools::FileExists(cmStrCat(dir, "/.hg"))) { | |
| return e_HG; | |
| } | |
| if (cmSystemTools::FileExists(cmStrCat(dir, "/.p4"))) { | |
| return e_P4; | |
| } | |
| if (cmSystemTools::FileExists(cmStrCat(dir, "/.p4config"))) { | |
| return e_P4; | |
| } | |
| return e_UNKNOWN; | |
| } | |
| std::unique_ptr<cmCTestVC> MakeVC(int type, cmCTest* ctest, cmMakefile* mf, | |
| std::ostream& os) | |
| { | |
| // clang-format off | |
| switch (type) { | |
| case e_CVS: return cm::make_unique<cmCTestCVS>(ctest, mf, os); | |
| case e_SVN: return cm::make_unique<cmCTestSVN>(ctest, mf, os); | |
| case e_BZR: return cm::make_unique<cmCTestBZR>(ctest, mf, os); | |
| case e_GIT: return cm::make_unique<cmCTestGIT>(ctest, mf, os); | |
| case e_HG: return cm::make_unique<cmCTestHG> (ctest, mf, os); | |
| case e_P4: return cm::make_unique<cmCTestP4> (ctest, mf, os); | |
| default: return cm::make_unique<cmCTestVC> (ctest, mf, os); | |
| } | |
| // clang-format on | |
| } | |
| } // namespace | |
| bool cmCTestUpdateCommand::ExecuteUpdate(UpdateArguments& args, | |
| cmExecutionStatus& status) const | |
| { | |
| cmMakefile& mf = status.GetMakefile(); | |
| std::string const& source_dir = !args.Source.empty() | |
| ? args.Source | |
| : mf.GetSafeDefinition("CTEST_SOURCE_DIRECTORY"); | |
| if (source_dir.empty()) { | |
| status.SetError("called with no source directory specified. " | |
| "Use SOURCE argument or set CTEST_SOURCE_DIRECTORY."); | |
| return false; | |
| } | |
| std::string const currentTag = this->CTest->GetCurrentTag(); | |
| if (currentTag.empty()) { | |
| status.SetError("called with no current tag."); | |
| return false; | |
| } | |
| // Detect the VCS managing the source tree. | |
| int updateType = DetectVCS(source_dir); | |
| // Get update command | |
| std::string updateCommand = mf.GetSafeDefinition("CTEST_UPDATE_COMMAND"); | |
| if (updateType == e_UNKNOWN && !updateCommand.empty()) { | |
| updateType = DetermineType(updateCommand); | |
| } | |
| if (updateType == e_UNKNOWN) { | |
| updateType = DetermineType(mf.GetSafeDefinition("CTEST_UPDATE_TYPE")); | |
| } | |
| std::string const& versionOverride = !args.VersionOverride.empty() | |
| ? args.VersionOverride | |
| : mf.GetSafeDefinition("CTEST_UPDATE_VERSION_OVERRIDE"); | |
| bool const versionOnly = | |
| args.VersionOnly || mf.IsOn("CTEST_UPDATE_VERSION_ONLY"); | |
| // If no update command was specified, lookup one for this VCS tool. | |
| if (updateCommand.empty()) { | |
| char const* key = TypeToCommandKey(updateType); | |
| if (key) { | |
| updateCommand = mf.GetSafeDefinition(key); | |
| } | |
| if (updateCommand.empty()) { | |
| status.SetError(cmStrCat("called with no update command specified. " | |
| "Please set CTEST_UPDATE_COMMAND", | |
| key ? " or "_s : ""_s, key ? key : "", '.')); | |
| return false; | |
| } | |
| } | |
| cmGeneratedFileStream ofs; | |
| if (!this->CTest->GetShowOnly()) { | |
| std::string logFile = cmStrCat("LastUpdate_", currentTag, ".log"); | |
| if (!this->CTest->OpenOutputFile("Temporary", logFile, ofs)) { | |
| status.SetError(cmStrCat("cannot create log file: ", logFile)); | |
| return false; | |
| } | |
| } | |
| cmGeneratedFileStream os; | |
| if (!this->CTest->OpenOutputFile(currentTag, "Update.xml", os, true)) { | |
| status.SetError("cannot create resulting XML file: Update.xml"); | |
| return false; | |
| } | |
| this->CTest->AddSubmitFile(cmCTest::PartUpdate, "Update.xml"); | |
| cmCTestOptionalLog(this->CTest, HANDLER_OUTPUT, args.Quiet, " Updating ", | |
| TypeToString(updateType), " repository: ", source_dir, | |
| '\n'); | |
| // Make sure VCS tool messages are in English so we can parse them. | |
| cmCLocaleEnvironmentScope fixLocale; | |
| static_cast<void>(fixLocale); | |
| // Create an object to interact with the VCS tool. | |
| std::unique_ptr<cmCTestVC> vc = MakeVC(updateType, this->CTest, &mf, ofs); | |
| vc->SetCommandLineTool(updateCommand); | |
| vc->SetSourceDirectory(source_dir); | |
| // Cleanup the working tree. | |
| vc->Cleanup(); | |
| std::string start_time = this->CTest->CurrentTime(); | |
| auto start_time_time = std::chrono::system_clock::now(); | |
| auto elapsed_time_start = std::chrono::steady_clock::now(); | |
| bool const updated = [&]() -> bool { | |
| if (!versionOverride.empty()) { | |
| return vc->UpdateVersionOverride(versionOverride); | |
| } | |
| if (versionOnly) { | |
| return vc->UpdateVersionOnly(); | |
| } | |
| return vc->Update(); | |
| }(); | |
| std::string buildname = | |
| cmCTest::SafeBuildIdField(mf.GetSafeDefinition("CTEST_BUILD_NAME")); | |
| cmXMLWriter xml(os); | |
| xml.StartDocument(); | |
| xml.StartElement("Update"); | |
| xml.Attribute("mode", "Client"); | |
| xml.Attribute("Generator", | |
| std::string("ctest-") + cmVersion::GetCMakeVersion()); | |
| xml.Element("Site", mf.GetSafeDefinition("CTEST_SITE")); | |
| xml.Element("BuildName", buildname); | |
| xml.Element("BuildStamp", | |
| cmStrCat(this->CTest->GetCurrentTag(), '-', | |
| this->CTest->GetTestGroupString())); | |
| xml.Element("StartDateTime", start_time); | |
| xml.Element("StartTime", start_time_time); | |
| xml.Element("UpdateCommand", vc->GetUpdateCommandLine()); | |
| xml.Element("UpdateType", TypeToString(updateType)); | |
| std::string changeId = mf.GetSafeDefinition("CTEST_CHANGE_ID"); | |
| if (!changeId.empty()) { | |
| xml.Element("ChangeId", changeId); | |
| } | |
| bool loadedMods = vc->WriteXML(xml); | |
| int localModifications = 0; | |
| int numUpdated = vc->GetPathCount(cmCTestVC::PathUpdated); | |
| if (numUpdated) { | |
| cmCTestOptionalLog(this->CTest, HANDLER_OUTPUT, args.Quiet, " Found ", | |
| numUpdated, " updated files\n"); | |
| } | |
| if (int numModified = vc->GetPathCount(cmCTestVC::PathModified)) { | |
| cmCTestOptionalLog(this->CTest, HANDLER_OUTPUT, args.Quiet, " Found ", | |
| numModified, " locally modified files\n"); | |
| localModifications += numModified; | |
| } | |
| if (int numConflicting = vc->GetPathCount(cmCTestVC::PathConflicting)) { | |
| cmCTestOptionalLog(this->CTest, HANDLER_OUTPUT, args.Quiet, " Found ", | |
| numConflicting, " conflicting files\n"); | |
| localModifications += numConflicting; | |
| } | |
| cmCTestOptionalLog(this->CTest, DEBUG, args.Quiet, "End\n"); | |
| std::string end_time = this->CTest->CurrentTime(); | |
| xml.Element("EndDateTime", end_time); | |
| xml.Element("EndTime", std::chrono::system_clock::now()); | |
| xml.Element("ElapsedMinutes", | |
| std::chrono::duration_cast<std::chrono::minutes>( | |
| std::chrono::steady_clock::now() - elapsed_time_start) | |
| .count()); | |
| xml.StartElement("UpdateReturnStatus"); | |
| if (localModifications) { | |
| xml.Content("Update error: " | |
| "There are modified or conflicting files in the repository"); | |
| cmCTestLog( | |
| this->CTest, WARNING, | |
| " There are modified or conflicting files in the repository\n"); | |
| } | |
| if (!updated) { | |
| xml.Content("Update command failed:\n"); | |
| xml.Content(vc->GetUpdateCommandLine()); | |
| cmCTestLog(this->CTest, HANDLER_OUTPUT, | |
| " Update command failed: ", vc->GetUpdateCommandLine(), '\n'); | |
| } | |
| xml.EndElement(); // UpdateReturnStatus | |
| xml.EndElement(); // Update | |
| xml.EndDocument(); | |
| if (!args.ReturnValue.empty()) { | |
| mf.AddDefinition(args.ReturnValue, | |
| std::to_string(updated && loadedMods ? numUpdated : -1)); | |
| } | |
| return true; | |
| } | |
| bool cmCTestUpdateCommand::InitialPass(std::vector<std::string> const& args, | |
| cmExecutionStatus& status) const | |
| { | |
| static auto const parser = | |
| cmArgumentParser<UpdateArguments>{ MakeBasicParser<UpdateArguments>() } | |
| .Bind("SOURCE"_s, &UpdateArguments::Source) | |
| .Bind("RETURN_VALUE"_s, &UpdateArguments::ReturnValue) | |
| .Bind("VERSION_ONLY"_s, &UpdateArguments::VersionOnly) | |
| .Bind("VERSION_OVERRIDE"_s, &UpdateArguments::VersionOverride) | |
| .Bind("QUIET"_s, &UpdateArguments::Quiet); | |
| return this->Invoke(parser, args, status, [&](UpdateArguments& a) { | |
| return this->ExecuteUpdate(a, status); | |
| }); | |
| } |