1
/*============================================================================
2
CMake - Cross Platform Makefile Generator
3
Copyright 2000-2009 Kitware, Inc.
5
Distributed under the OSI-approved BSD License (the "License");
6
see accompanying file Copyright.txt for details.
8
This software is distributed WITHOUT ANY WARRANTY; without even the
9
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
See the License for more information.
11
============================================================================*/
12
#include "cmCTestSVN.h"
15
#include "cmSystemTools.h"
16
#include "cmXMLParser.h"
17
#include "cmXMLWriter.h"
19
#include <cmsys/RegularExpression.hxx>
21
struct cmCTestSVN::Revision: public cmCTestVC::Revision
23
cmCTestSVN::SVNInfo* SVNInfo;
26
//----------------------------------------------------------------------------
27
cmCTestSVN::cmCTestSVN(cmCTest* ct, std::ostream& log):
28
cmCTestGlobalVC(ct, log)
30
this->PriorRev = this->Unknown;
33
//----------------------------------------------------------------------------
34
cmCTestSVN::~cmCTestSVN()
38
//----------------------------------------------------------------------------
39
void cmCTestSVN::CleanupImpl()
41
std::vector<const char*> svn_cleanup;
42
svn_cleanup.push_back("cleanup");
43
OutputLogger out(this->Log, "cleanup-out> ");
44
OutputLogger err(this->Log, "cleanup-err> ");
45
this->RunSVNCommand(svn_cleanup, &out, &err);
48
//----------------------------------------------------------------------------
49
class cmCTestSVN::InfoParser: public cmCTestVC::LineParser
52
InfoParser(cmCTestSVN* svn,
56
Rev(rev), SVNRepo(svninfo)
58
this->SetLog(&svn->Log, prefix);
59
this->RegexRev.compile("^Revision: ([0-9]+)");
60
this->RegexURL.compile("^URL: +([^ ]+) *$");
61
this->RegexRoot.compile("^Repository Root: +([^ ]+) *$");
65
cmCTestSVN::SVNInfo& SVNRepo;
66
cmsys::RegularExpression RegexRev;
67
cmsys::RegularExpression RegexURL;
68
cmsys::RegularExpression RegexRoot;
69
virtual bool ProcessLine()
71
if(this->RegexRev.find(this->Line))
73
this->Rev = this->RegexRev.match(1);
75
else if(this->RegexURL.find(this->Line))
77
this->SVNRepo.URL = this->RegexURL.match(1);
79
else if(this->RegexRoot.find(this->Line))
81
this->SVNRepo.Root = this->RegexRoot.match(1);
87
//----------------------------------------------------------------------------
88
static bool cmCTestSVNPathStarts(std::string const& p1, std::string const& p2)
90
// Does path p1 start with path p2?
91
if(p1.size() == p2.size())
95
else if(p1.size() > p2.size() && p1[p2.size()] == '/')
97
return strncmp(p1.c_str(), p2.c_str(), p2.size()) == 0;
105
//----------------------------------------------------------------------------
106
std::string cmCTestSVN::LoadInfo(SVNInfo& svninfo)
108
// Run "svn info" to get the repository info from the work tree.
109
std::vector<const char*> svn_info;
110
svn_info.push_back("info");
111
svn_info.push_back(svninfo.LocalPath.c_str());
113
InfoParser out(this, "info-out> ", rev, svninfo);
114
OutputLogger err(this->Log, "info-err> ");
115
this->RunSVNCommand(svn_info, &out, &err);
119
//----------------------------------------------------------------------------
120
void cmCTestSVN::NoteOldRevision()
122
// Info for root repository
123
this->Repositories.push_back( SVNInfo("") );
124
this->RootInfo = &(this->Repositories.back());
125
// Info for the external repositories
126
this->LoadExternals();
128
// Get info for all the repositories
129
std::list<SVNInfo>::iterator itbeg = this->Repositories.begin();
130
std::list<SVNInfo>::iterator itend = this->Repositories.end();
131
for( ; itbeg != itend ; itbeg++)
133
SVNInfo& svninfo = *itbeg;
134
svninfo.OldRevision = this->LoadInfo(svninfo);
135
this->Log << "Revision for repository '" << svninfo.LocalPath
136
<< "' before update: " << svninfo.OldRevision << "\n";
137
cmCTestLog(this->CTest, HANDLER_OUTPUT,
138
" Old revision of external repository '"
139
<< svninfo.LocalPath << "' is: "
140
<< svninfo.OldRevision << "\n");
143
// Set the global old revision to the one of the root
144
this->OldRevision = this->RootInfo->OldRevision;
145
this->PriorRev.Rev = this->OldRevision;
148
//----------------------------------------------------------------------------
149
void cmCTestSVN::NoteNewRevision()
151
// Get info for the external repositories
152
std::list<SVNInfo>::iterator itbeg = this->Repositories.begin();
153
std::list<SVNInfo>::iterator itend = this->Repositories.end();
154
for( ; itbeg != itend ; itbeg++)
156
SVNInfo& svninfo = *itbeg;
157
svninfo.NewRevision = this->LoadInfo(svninfo);
158
this->Log << "Revision for repository '" << svninfo.LocalPath
159
<< "' after update: " << svninfo.NewRevision << "\n";
160
cmCTestLog(this->CTest, HANDLER_OUTPUT,
161
" New revision of external repository '"
162
<< svninfo.LocalPath << "' is: "
163
<< svninfo.NewRevision << "\n");
165
// svninfo.Root = ""; // uncomment to test GuessBase
166
this->Log << "Repository '" << svninfo.LocalPath
167
<< "' URL = " << svninfo.URL << "\n";
168
this->Log << "Repository '" << svninfo.LocalPath
169
<< "' Root = " << svninfo.Root << "\n";
171
// Compute the base path the working tree has checked out under
172
// the repository root.
173
if(!svninfo.Root.empty()
174
&& cmCTestSVNPathStarts(svninfo.URL, svninfo.Root))
176
svninfo.Base = cmCTest::DecodeURL(
177
svninfo.URL.substr(svninfo.Root.size()));
180
this->Log << "Repository '" << svninfo.LocalPath
181
<< "' Base = " << svninfo.Base << "\n";
185
// Set the global new revision to the one of the root
186
this->NewRevision = this->RootInfo->NewRevision;
189
//----------------------------------------------------------------------------
190
void cmCTestSVN::GuessBase(SVNInfo& svninfo,
191
std::vector<Change> const& changes)
193
// Subversion did not give us a good repository root so we need to
194
// guess the base path from the URL and the paths in a revision with
197
// Consider each possible URL suffix from longest to shortest.
198
for(std::string::size_type slash = svninfo.URL.find('/');
199
svninfo.Base.empty() && slash != std::string::npos;
200
slash = svninfo.URL.find('/', slash+1))
202
// If the URL suffix is a prefix of at least one path then it is the base.
203
std::string base = cmCTest::DecodeURL(svninfo.URL.substr(slash));
204
for(std::vector<Change>::const_iterator ci = changes.begin();
205
svninfo.Base.empty() && ci != changes.end(); ++ci)
207
if(cmCTestSVNPathStarts(ci->Path, base))
214
// We always append a slash so that we know paths beginning in the
215
// base lie under its path. If no base was found then the working
216
// tree must be a checkout of the entire repo and this will match
217
// the leading slash in all paths.
220
this->Log << "Guessed Base = " << svninfo.Base << "\n";
223
//----------------------------------------------------------------------------
224
class cmCTestSVN::UpdateParser: public cmCTestVC::LineParser
227
UpdateParser(cmCTestSVN* svn, const char* prefix): SVN(svn)
229
this->SetLog(&svn->Log, prefix);
230
this->RegexUpdate.compile("^([ADUCGE ])([ADUCGE ])[B ] +(.+)$");
234
cmsys::RegularExpression RegexUpdate;
238
if(this->RegexUpdate.find(this->Line))
240
this->DoPath(this->RegexUpdate.match(1)[0],
241
this->RegexUpdate.match(2)[0],
242
this->RegexUpdate.match(3));
247
void DoPath(char path_status, char prop_status, std::string const& path)
249
char status = (path_status != ' ')? path_status : prop_status;
250
std::string dir = cmSystemTools::GetFilenamePath(path);
251
std::string name = cmSystemTools::GetFilenameName(path);
252
// See "svn help update".
256
this->SVN->Dirs[dir][name].Status = PathModified;
259
this->SVN->Dirs[dir][name].Status = PathConflicting;
261
case 'A': case 'D': case 'U':
262
this->SVN->Dirs[dir][name].Status = PathUpdated;
265
case '?': case ' ': default:
271
//----------------------------------------------------------------------------
272
bool cmCTestSVN::UpdateImpl()
274
// Get user-specified update options.
275
std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
278
opts = this->CTest->GetCTestConfiguration("SVNUpdateOptions");
280
std::vector<std::string> args = cmSystemTools::ParseArguments(opts.c_str());
282
// Specify the start time for nightly testing.
283
if(this->CTest->GetTestModel() == cmCTest::NIGHTLY)
285
args.push_back("-r{" + this->GetNightlyTime() + " +0000}");
288
std::vector<char const*> svn_update;
289
svn_update.push_back("update");
290
for(std::vector<std::string>::const_iterator ai = args.begin();
291
ai != args.end(); ++ai)
293
svn_update.push_back(ai->c_str());
296
UpdateParser out(this, "up-out> ");
297
OutputLogger err(this->Log, "up-err> ");
298
return this->RunSVNCommand(svn_update, &out, &err);
301
//----------------------------------------------------------------------------
302
bool cmCTestSVN::RunSVNCommand(std::vector<char const*> const& parameters,
303
OutputParser* out, OutputParser* err)
305
if(parameters.empty()) return false;
307
std::vector<char const*> args;
308
args.push_back(this->CommandLineTool.c_str());
310
args.insert(args.end(), parameters.begin(), parameters.end());
312
args.push_back("--non-interactive");
314
std::string userOptions =
315
this->CTest->GetCTestConfiguration("SVNOptions");
317
std::vector<std::string> parsedUserOptions =
318
cmSystemTools::ParseArguments(userOptions.c_str());
319
for(std::vector<std::string>::iterator i = parsedUserOptions.begin();
320
i != parsedUserOptions.end(); ++i)
322
args.push_back(i->c_str());
327
if(strcmp(parameters[0], "update") == 0)
329
return RunUpdateCommand(&args[0], out, err);
333
return RunChild(&args[0], out, err);
337
//----------------------------------------------------------------------------
338
class cmCTestSVN::LogParser: public cmCTestVC::OutputLogger,
342
LogParser(cmCTestSVN* svn, const char* prefix, SVNInfo& svninfo):
343
OutputLogger(svn->Log, prefix), SVN(svn), SVNRepo(svninfo)
344
{ this->InitializeParser(); }
345
~LogParser() { this->CleanupParser(); }
348
cmCTestSVN::SVNInfo& SVNRepo;
350
typedef cmCTestSVN::Revision Revision;
351
typedef cmCTestSVN::Change Change;
353
std::vector<Change> Changes;
355
std::vector<char> CData;
357
virtual bool ProcessChunk(const char* data, int length)
359
this->OutputLogger::ProcessChunk(data, length);
360
this->ParseChunk(data, length);
364
virtual void StartElement(const std::string& name, const char** atts)
367
if(name == "logentry")
369
this->Rev = Revision();
370
this->Rev.SVNInfo = &SVNRepo;
371
if(const char* rev = this->FindAttribute(atts, "revision"))
375
this->Changes.clear();
377
else if(name == "path")
379
this->CurChange = Change();
380
if(const char* action = this->FindAttribute(atts, "action"))
382
this->CurChange.Action = action[0];
387
virtual void CharacterDataHandler(const char* data, int length)
389
this->CData.insert(this->CData.end(), data, data+length);
392
virtual void EndElement(const std::string& name)
394
if(name == "logentry")
396
this->SVN->DoRevisionSVN(this->Rev, this->Changes);
398
else if(!this->CData.empty() && name == "path")
400
std::string orig_path(&this->CData[0], this->CData.size());
401
std::string new_path = SVNRepo.BuildLocalPath( orig_path );
402
this->CurChange.Path.assign(new_path);
403
this->Changes.push_back(this->CurChange);
405
else if(!this->CData.empty() && name == "author")
407
this->Rev.Author.assign(&this->CData[0], this->CData.size());
409
else if(!this->CData.empty() && name == "date")
411
this->Rev.Date.assign(&this->CData[0], this->CData.size());
413
else if(!this->CData.empty() && name == "msg")
415
this->Rev.Log.assign(&this->CData[0], this->CData.size());
420
virtual void ReportError(int, int, const char* msg)
422
this->SVN->Log << "Error parsing svn log xml: " << msg << "\n";
426
//----------------------------------------------------------------------------
427
void cmCTestSVN::LoadRevisions()
429
// Get revisions for all the external repositories
430
std::list<SVNInfo>::iterator itbeg = this->Repositories.begin();
431
std::list<SVNInfo>::iterator itend = this->Repositories.end();
432
for( ; itbeg != itend ; itbeg++)
434
SVNInfo& svninfo = *itbeg;
435
LoadRevisions(svninfo);
439
//----------------------------------------------------------------------------
440
void cmCTestSVN::LoadRevisions(SVNInfo &svninfo)
442
// We are interested in every revision included in the update.
444
if(atoi(svninfo.OldRevision.c_str()) < atoi(svninfo.NewRevision.c_str()))
446
revs = "-r" + svninfo.OldRevision + ":" + svninfo.NewRevision;
450
revs = "-r" + svninfo.NewRevision;
453
// Run "svn log" to get all global revisions of interest.
454
std::vector<const char*> svn_log;
455
svn_log.push_back("log");
456
svn_log.push_back("--xml");
457
svn_log.push_back("-v");
458
svn_log.push_back(revs.c_str());
459
svn_log.push_back(svninfo.LocalPath.c_str());
460
LogParser out(this, "log-out> ", svninfo);
461
OutputLogger err(this->Log, "log-err> ");
462
this->RunSVNCommand(svn_log, &out, &err);
465
//----------------------------------------------------------------------------
466
void cmCTestSVN::DoRevisionSVN(Revision const& revision,
467
std::vector<Change> const& changes)
469
// Guess the base checkout path from the changes if necessary.
470
if(this->RootInfo->Base.empty() && !changes.empty())
472
this->GuessBase(*this->RootInfo, changes);
475
// Ignore changes in the old revision for external repositories
476
if(revision.Rev == revision.SVNInfo->OldRevision
477
&& revision.SVNInfo->LocalPath != "")
482
this->cmCTestGlobalVC::DoRevision(revision, changes);
485
//----------------------------------------------------------------------------
486
class cmCTestSVN::StatusParser: public cmCTestVC::LineParser
489
StatusParser(cmCTestSVN* svn, const char* prefix): SVN(svn)
491
this->SetLog(&svn->Log, prefix);
492
this->RegexStatus.compile("^([ACDIMRX?!~ ])([CM ])[ L]... +(.+)$");
496
cmsys::RegularExpression RegexStatus;
499
if(this->RegexStatus.find(this->Line))
501
this->DoPath(this->RegexStatus.match(1)[0],
502
this->RegexStatus.match(2)[0],
503
this->RegexStatus.match(3));
508
void DoPath(char path_status, char prop_status, std::string const& path)
510
char status = (path_status != ' ')? path_status : prop_status;
511
// See "svn help status".
514
case 'M': case '!': case 'A': case 'D': case 'R':
515
this->SVN->DoModification(PathModified, path);
518
this->SVN->DoModification(PathConflicting, path);
520
case 'X': case 'I': case '?': case ' ': default:
526
//----------------------------------------------------------------------------
527
void cmCTestSVN::LoadModifications()
529
// Run "svn status" which reports local modifications.
530
std::vector<const char*> svn_status;
531
svn_status.push_back("status");
532
StatusParser out(this, "status-out> ");
533
OutputLogger err(this->Log, "status-err> ");
534
this->RunSVNCommand(svn_status, &out, &err);
537
//----------------------------------------------------------------------------
538
void cmCTestSVN::WriteXMLGlobal(cmXMLWriter& xml)
540
this->cmCTestGlobalVC::WriteXMLGlobal(xml);
542
xml.Element("SVNPath", this->RootInfo->Base);
545
//----------------------------------------------------------------------------
546
class cmCTestSVN::ExternalParser: public cmCTestVC::LineParser
549
ExternalParser(cmCTestSVN* svn, const char* prefix): SVN(svn)
551
this->SetLog(&svn->Log, prefix);
552
this->RegexExternal.compile("^X..... +(.+)$");
556
cmsys::RegularExpression RegexExternal;
559
if(this->RegexExternal.find(this->Line))
561
this->DoPath(this->RegexExternal.match(1));
566
void DoPath(std::string const& path)
568
// Get local path relative to the source directory
569
std::string local_path;
570
if(path.size() > this->SVN->SourceDirectory.size() &&
571
strncmp(path.c_str(), this->SVN->SourceDirectory.c_str(),
572
this->SVN->SourceDirectory.size()) == 0)
574
local_path = path.c_str() + this->SVN->SourceDirectory.size() + 1;
580
this->SVN->Repositories.push_back( SVNInfo(local_path.c_str()) );
584
//----------------------------------------------------------------------------
585
void cmCTestSVN::LoadExternals()
587
// Run "svn status" to get the list of external repositories
588
std::vector<const char*> svn_status;
589
svn_status.push_back("status");
590
ExternalParser out(this, "external-out> ");
591
OutputLogger err(this->Log, "external-err> ");
592
this->RunSVNCommand(svn_status, &out, &err);
595
//----------------------------------------------------------------------------
596
std::string cmCTestSVN::SVNInfo::BuildLocalPath(std::string const& path) const
598
std::string local_path;
600
// Add local path prefix if not empty
601
if (!this->LocalPath.empty())
603
local_path += this->LocalPath;
607
// Add path with base prefix removed
608
if(path.size() > this->Base.size() &&
609
strncmp(path.c_str(), this->Base.c_str(), this->Base.size()) == 0)
611
local_path += (path.c_str() + this->Base.size());