530 lines
16 KiB
C++
530 lines
16 KiB
C++
/*=========================================================================
|
|
|
|
Program: CMake - Cross-Platform Makefile Generator
|
|
Module: $RCSfile$
|
|
Language: C++
|
|
Date: $Date$
|
|
Version: $Revision$
|
|
|
|
Copyright (c) 2002 Kitware, Inc. All rights reserved.
|
|
See Copyright.txt or http://www.cmake.org/HTML/Copyright.html for details.
|
|
|
|
This software is distributed WITHOUT ANY WARRANTY; without even
|
|
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
PURPOSE. See the above copyright notices for more information.
|
|
|
|
=========================================================================*/
|
|
#include "cmCTestSVN.h"
|
|
|
|
#include "cmCTest.h"
|
|
#include "cmSystemTools.h"
|
|
#include "cmXMLParser.h"
|
|
#include "cmXMLSafe.h"
|
|
|
|
#include <cmsys/RegularExpression.hxx>
|
|
|
|
//----------------------------------------------------------------------------
|
|
cmCTestSVN::cmCTestSVN(cmCTest* ct, std::ostream& log): cmCTestVC(ct, log)
|
|
{
|
|
this->PriorRev = this->Unknown;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
cmCTestSVN::~cmCTestSVN()
|
|
{
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void cmCTestSVN::CleanupImpl()
|
|
{
|
|
const char* svn = this->CommandLineTool.c_str();
|
|
const char* svn_cleanup[] = {svn, "cleanup", 0};
|
|
OutputLogger out(this->Log, "cleanup-out> ");
|
|
OutputLogger err(this->Log, "cleanup-err> ");
|
|
this->RunChild(svn_cleanup, &out, &err);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
class cmCTestSVN::InfoParser: public cmCTestVC::LineParser
|
|
{
|
|
public:
|
|
InfoParser(cmCTestSVN* svn, const char* prefix, std::string& rev):
|
|
SVN(svn), Rev(rev)
|
|
{
|
|
this->SetLog(&svn->Log, prefix);
|
|
this->RegexRev.compile("^Revision: ([0-9]+)");
|
|
this->RegexURL.compile("^URL: +([^ ]+) *$");
|
|
this->RegexRoot.compile("^Repository Root: +([^ ]+) *$");
|
|
}
|
|
private:
|
|
cmCTestSVN* SVN;
|
|
std::string& Rev;
|
|
cmsys::RegularExpression RegexRev;
|
|
cmsys::RegularExpression RegexURL;
|
|
cmsys::RegularExpression RegexRoot;
|
|
virtual bool ProcessLine()
|
|
{
|
|
if(this->RegexRev.find(this->Line))
|
|
{
|
|
this->Rev = this->RegexRev.match(1);
|
|
}
|
|
else if(this->RegexURL.find(this->Line))
|
|
{
|
|
this->SVN->URL = this->RegexURL.match(1);
|
|
}
|
|
else if(this->RegexRoot.find(this->Line))
|
|
{
|
|
this->SVN->Root = this->RegexRoot.match(1);
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------------
|
|
static bool cmCTestSVNPathStarts(std::string const& p1, std::string const& p2)
|
|
{
|
|
// Does path p1 start with path p2?
|
|
if(p1.size() == p2.size())
|
|
{
|
|
return p1 == p2;
|
|
}
|
|
else if(p1.size() > p2.size() && p1[p2.size()] == '/')
|
|
{
|
|
return strncmp(p1.c_str(), p2.c_str(), p2.size()) == 0;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
std::string cmCTestSVN::LoadInfo()
|
|
{
|
|
// Run "svn info" to get the repository info from the work tree.
|
|
const char* svn = this->CommandLineTool.c_str();
|
|
const char* svn_info[] = {svn, "info", 0};
|
|
std::string rev;
|
|
InfoParser out(this, "info-out> ", rev);
|
|
OutputLogger err(this->Log, "info-err> ");
|
|
this->RunChild(svn_info, &out, &err);
|
|
return rev;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void cmCTestSVN::NoteOldRevision()
|
|
{
|
|
this->OldRevision = this->LoadInfo();
|
|
this->Log << "Revision before update: " << this->OldRevision << "\n";
|
|
cmCTestLog(this->CTest, HANDLER_OUTPUT, " Old revision of repository is: "
|
|
<< this->OldRevision << "\n");
|
|
this->PriorRev.Rev = this->OldRevision;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void cmCTestSVN::NoteNewRevision()
|
|
{
|
|
this->NewRevision = this->LoadInfo();
|
|
this->Log << "Revision after update: " << this->NewRevision << "\n";
|
|
cmCTestLog(this->CTest, HANDLER_OUTPUT, " New revision of repository is: "
|
|
<< this->NewRevision << "\n");
|
|
|
|
// this->Root = ""; // uncomment to test GuessBase
|
|
this->Log << "URL = " << this->URL << "\n";
|
|
this->Log << "Root = " << this->Root << "\n";
|
|
|
|
// Compute the base path the working tree has checked out under
|
|
// the repository root.
|
|
if(!this->Root.empty() && cmCTestSVNPathStarts(this->URL, this->Root))
|
|
{
|
|
this->Base = cmCTest::DecodeURL(this->URL.substr(this->Root.size()));
|
|
this->Base += "/";
|
|
}
|
|
this->Log << "Base = " << this->Base << "\n";
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void cmCTestSVN::GuessBase(std::vector<Change> const& changes)
|
|
{
|
|
// Subversion did not give us a good repository root so we need to
|
|
// guess the base path from the URL and the paths in a revision with
|
|
// changes under it.
|
|
|
|
// Consider each possible URL suffix from longest to shortest.
|
|
for(std::string::size_type slash = this->URL.find('/');
|
|
this->Base.empty() && slash != std::string::npos;
|
|
slash = this->URL.find('/', slash+1))
|
|
{
|
|
// If the URL suffix is a prefix of at least one path then it is the base.
|
|
std::string base = cmCTest::DecodeURL(this->URL.substr(slash));
|
|
for(std::vector<Change>::const_iterator ci = changes.begin();
|
|
this->Base.empty() && ci != changes.end(); ++ci)
|
|
{
|
|
if(cmCTestSVNPathStarts(ci->Path, base))
|
|
{
|
|
this->Base = base;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We always append a slash so that we know paths beginning in the
|
|
// base lie under its path. If no base was found then the working
|
|
// tree must be a checkout of the entire repo and this will match
|
|
// the leading slash in all paths.
|
|
this->Base += "/";
|
|
|
|
this->Log << "Guessed Base = " << this->Base << "\n";
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
const char* cmCTestSVN::LocalPath(std::string const& path)
|
|
{
|
|
if(path.size() > this->Base.size() &&
|
|
strncmp(path.c_str(), this->Base.c_str(), this->Base.size()) == 0)
|
|
{
|
|
// This path lies under the base, so return a relative path.
|
|
return path.c_str() + this->Base.size();
|
|
}
|
|
else
|
|
{
|
|
// This path does not lie under the base, so ignore it.
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
class cmCTestSVN::UpdateParser: public cmCTestVC::LineParser
|
|
{
|
|
public:
|
|
UpdateParser(cmCTestSVN* svn, const char* prefix): SVN(svn)
|
|
{
|
|
this->SetLog(&svn->Log, prefix);
|
|
this->RegexUpdate.compile("^([ADUCGE ])([ADUCGE ])[B ] +(.+)$");
|
|
}
|
|
private:
|
|
cmCTestSVN* SVN;
|
|
cmsys::RegularExpression RegexUpdate;
|
|
|
|
bool ProcessLine()
|
|
{
|
|
if(this->RegexUpdate.find(this->Line))
|
|
{
|
|
this->DoPath(this->RegexUpdate.match(1)[0],
|
|
this->RegexUpdate.match(2)[0],
|
|
this->RegexUpdate.match(3));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void DoPath(char path_status, char prop_status, std::string const& path)
|
|
{
|
|
char status = (path_status != ' ')? path_status : prop_status;
|
|
std::string dir = cmSystemTools::GetFilenamePath(path);
|
|
std::string name = cmSystemTools::GetFilenameName(path);
|
|
// See "svn help update".
|
|
switch(status)
|
|
{
|
|
case 'G':
|
|
this->SVN->Dirs[dir][name].Status = PathModified;
|
|
break;
|
|
case 'C':
|
|
this->SVN->Dirs[dir][name].Status = PathConflicting;
|
|
break;
|
|
case 'A': case 'D': case 'U':
|
|
this->SVN->Dirs[dir][name].Status = PathUpdated;
|
|
break;
|
|
case 'E': // TODO?
|
|
case '?': case ' ': default:
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------------
|
|
bool cmCTestSVN::UpdateImpl()
|
|
{
|
|
// Get user-specified update options.
|
|
std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
|
|
if(opts.empty())
|
|
{
|
|
opts = this->CTest->GetCTestConfiguration("SVNUpdateOptions");
|
|
}
|
|
std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str());
|
|
|
|
// Specify the start time for nightly testing.
|
|
if(this->CTest->GetTestModel() == cmCTest::NIGHTLY)
|
|
{
|
|
args.push_back("-r{" + this->GetNightlyTime() + " +0000}");
|
|
}
|
|
|
|
std::vector<char const*> svn_update;
|
|
svn_update.push_back(this->CommandLineTool.c_str());
|
|
svn_update.push_back("update");
|
|
svn_update.push_back("--non-interactive");
|
|
for(std::vector<cmStdString>::const_iterator ai = args.begin();
|
|
ai != args.end(); ++ai)
|
|
{
|
|
svn_update.push_back(ai->c_str());
|
|
}
|
|
svn_update.push_back(0);
|
|
|
|
UpdateParser out(this, "up-out> ");
|
|
OutputLogger err(this->Log, "up-err> ");
|
|
return this->RunUpdateCommand(&svn_update[0], &out, &err);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
class cmCTestSVN::LogParser: public cmCTestVC::OutputLogger,
|
|
private cmXMLParser
|
|
{
|
|
public:
|
|
LogParser(cmCTestSVN* svn, const char* prefix):
|
|
OutputLogger(svn->Log, prefix), SVN(svn) { this->InitializeParser(); }
|
|
~LogParser() { this->CleanupParser(); }
|
|
private:
|
|
cmCTestSVN* SVN;
|
|
|
|
typedef cmCTestSVN::Revision Revision;
|
|
typedef cmCTestSVN::Change Change;
|
|
Revision Rev;
|
|
std::vector<Change> Changes;
|
|
Change CurChange;
|
|
std::vector<char> CData;
|
|
|
|
virtual bool ProcessChunk(const char* data, int length)
|
|
{
|
|
this->OutputLogger::ProcessChunk(data, length);
|
|
this->ParseChunk(data, length);
|
|
return true;
|
|
}
|
|
|
|
virtual void StartElement(const char* name, const char** atts)
|
|
{
|
|
this->CData.clear();
|
|
if(strcmp(name, "logentry") == 0)
|
|
{
|
|
this->Rev = Revision();
|
|
if(const char* rev = this->FindAttribute(atts, "revision"))
|
|
{
|
|
this->Rev.Rev = rev;
|
|
}
|
|
this->Changes.clear();
|
|
}
|
|
else if(strcmp(name, "path") == 0)
|
|
{
|
|
this->CurChange = Change();
|
|
if(const char* action = this->FindAttribute(atts, "action"))
|
|
{
|
|
this->CurChange.Action = action[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual void CharacterDataHandler(const char* data, int length)
|
|
{
|
|
this->CData.insert(this->CData.end(), data, data+length);
|
|
}
|
|
|
|
virtual void EndElement(const char* name)
|
|
{
|
|
if(strcmp(name, "logentry") == 0)
|
|
{
|
|
this->SVN->DoRevision(this->Rev, this->Changes);
|
|
}
|
|
else if(strcmp(name, "path") == 0 && !this->CData.empty())
|
|
{
|
|
this->CurChange.Path.assign(&this->CData[0], this->CData.size());
|
|
this->Changes.push_back(this->CurChange);
|
|
}
|
|
else if(strcmp(name, "author") == 0 && !this->CData.empty())
|
|
{
|
|
this->Rev.Author.assign(&this->CData[0], this->CData.size());
|
|
}
|
|
else if(strcmp(name, "date") == 0 && !this->CData.empty())
|
|
{
|
|
this->Rev.Date.assign(&this->CData[0], this->CData.size());
|
|
}
|
|
else if(strcmp(name, "msg") == 0 && !this->CData.empty())
|
|
{
|
|
this->Rev.Log.assign(&this->CData[0], this->CData.size());
|
|
}
|
|
this->CData.clear();
|
|
}
|
|
|
|
virtual void ReportError(int, int, const char* msg)
|
|
{
|
|
this->SVN->Log << "Error parsing svn log xml: " << msg << "\n";
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------------
|
|
void cmCTestSVN::LoadRevisions()
|
|
{
|
|
cmCTestLog(this->CTest, HANDLER_OUTPUT,
|
|
" Gathering version information (one . per revision):\n"
|
|
" " << std::flush);
|
|
|
|
// We are interested in every revision included in the update.
|
|
std::string revs;
|
|
if(atoi(this->OldRevision.c_str()) < atoi(this->NewRevision.c_str()))
|
|
{
|
|
revs = "-r" + this->OldRevision + ":" + this->NewRevision;
|
|
}
|
|
else
|
|
{
|
|
revs = "-r" + this->NewRevision;
|
|
}
|
|
|
|
// Run "svn log" to get all global revisions of interest.
|
|
const char* svn = this->CommandLineTool.c_str();
|
|
const char* svn_log[] = {svn, "log", "--xml", "-v", revs.c_str(), 0};
|
|
{
|
|
LogParser out(this, "log-out> ");
|
|
OutputLogger err(this->Log, "log-err> ");
|
|
this->RunChild(svn_log, &out, &err);
|
|
}
|
|
cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void cmCTestSVN::DoRevision(Revision const& revision,
|
|
std::vector<Change> const& changes)
|
|
{
|
|
// Guess the base checkout path from the changes if necessary.
|
|
if(this->Base.empty() && !changes.empty())
|
|
{
|
|
this->GuessBase(changes);
|
|
}
|
|
|
|
// Indicate we found a revision.
|
|
cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush);
|
|
|
|
// Ignore changes in the old revision.
|
|
if(revision.Rev == this->OldRevision)
|
|
{
|
|
this->PriorRev = revision;
|
|
return;
|
|
}
|
|
|
|
// Store the revision.
|
|
this->Revisions.push_back(revision);
|
|
|
|
// Report this revision.
|
|
Revision const& rev = this->Revisions.back();
|
|
this->Log << "Found revision " << rev.Rev << "\n"
|
|
<< " author = " << rev.Author << "\n"
|
|
<< " date = " << rev.Date << "\n";
|
|
|
|
// Update information about revisions of the changed files.
|
|
for(std::vector<Change>::const_iterator ci = changes.begin();
|
|
ci != changes.end(); ++ci)
|
|
{
|
|
if(const char* local = this->LocalPath(ci->Path))
|
|
{
|
|
std::string dir = cmSystemTools::GetFilenamePath(local);
|
|
std::string name = cmSystemTools::GetFilenameName(local);
|
|
File& file = this->Dirs[dir][name];
|
|
file.PriorRev = file.Rev? file.Rev : &this->PriorRev;
|
|
file.Rev = &rev;
|
|
this->Log << " " << ci->Action << " " << local << " " << "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
class cmCTestSVN::StatusParser: public cmCTestVC::LineParser
|
|
{
|
|
public:
|
|
StatusParser(cmCTestSVN* svn, const char* prefix): SVN(svn)
|
|
{
|
|
this->SetLog(&svn->Log, prefix);
|
|
this->RegexStatus.compile("^([ACDIMRX?!~ ])([CM ])[ L]... +(.+)$");
|
|
}
|
|
private:
|
|
cmCTestSVN* SVN;
|
|
cmsys::RegularExpression RegexStatus;
|
|
bool ProcessLine()
|
|
{
|
|
if(this->RegexStatus.find(this->Line))
|
|
{
|
|
this->DoPath(this->RegexStatus.match(1)[0],
|
|
this->RegexStatus.match(2)[0],
|
|
this->RegexStatus.match(3));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void DoPath(char path_status, char prop_status, std::string const& path)
|
|
{
|
|
char status = (path_status != ' ')? path_status : prop_status;
|
|
// See "svn help status".
|
|
switch(status)
|
|
{
|
|
case 'M': case '!': case 'A': case 'D': case 'R': case 'X':
|
|
this->DoPath(PathModified, path);
|
|
break;
|
|
case 'C': case '~':
|
|
this->DoPath(PathConflicting, path);
|
|
break;
|
|
case 'I': case '?': case ' ': default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void DoPath(PathStatus status, std::string const& path)
|
|
{
|
|
std::string dir = cmSystemTools::GetFilenamePath(path);
|
|
std::string name = cmSystemTools::GetFilenameName(path);
|
|
File& file = this->SVN->Dirs[dir][name];
|
|
file.Status = status;
|
|
// For local modifications the current rev is unknown and the
|
|
// prior rev is the latest from svn.
|
|
if(!file.Rev && !file.PriorRev)
|
|
{
|
|
file.PriorRev = &this->SVN->PriorRev;
|
|
}
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------------
|
|
void cmCTestSVN::LoadModifications()
|
|
{
|
|
// Run "svn status" which reports local modifications.
|
|
const char* svn = this->CommandLineTool.c_str();
|
|
const char* svn_status[] = {svn, "status", "--non-interactive", 0};
|
|
StatusParser out(this, "status-out> ");
|
|
OutputLogger err(this->Log, "status-err> ");
|
|
this->RunChild(svn_status, &out, &err);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void cmCTestSVN::WriteXMLDirectory(std::ostream& xml,
|
|
std::string const& path,
|
|
Directory const& dir)
|
|
{
|
|
const char* slash = path.empty()? "":"/";
|
|
xml << "\t<Directory>\n"
|
|
<< "\t\t<Name>" << cmXMLSafe(path) << "</Name>\n";
|
|
for(Directory::const_iterator fi = dir.begin(); fi != dir.end(); ++fi)
|
|
{
|
|
std::string full = path + slash + fi->first;
|
|
this->WriteXMLEntry(xml, path, fi->first, full, fi->second);
|
|
}
|
|
xml << "\t</Directory>\n";
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
bool cmCTestSVN::WriteXMLUpdates(std::ostream& xml)
|
|
{
|
|
this->LoadRevisions();
|
|
this->LoadModifications();
|
|
|
|
for(std::map<cmStdString, Directory>::const_iterator
|
|
di = this->Dirs.begin(); di != this->Dirs.end(); ++di)
|
|
{
|
|
this->WriteXMLDirectory(xml, di->first, di->second);
|
|
}
|
|
|
|
return true;
|
|
}
|