Browse Source

ENH: Teach CTest to handle git repositories

This creates cmCTestGIT to drive CTest Update handling on git-based work
trees.  Currently we always update to the head of the remote tracking
branch (git pull), so the nightly start time is ignored for Nightly
builds.  A later change will address this.  See issue #6994.
Brad King 16 years ago
parent
commit
9c17cbeb44

+ 2 - 0
Source/CMakeLists.txt

@@ -361,6 +361,8 @@ SET(CTEST_SRCS cmCTest.cxx
   CTest/cmCTestCVS.h
   CTest/cmCTestSVN.cxx
   CTest/cmCTestSVN.h
+  CTest/cmCTestGIT.cxx
+  CTest/cmCTestGIT.h
   )
 
 # Build CTestLib

+ 416 - 0
Source/CTest/cmCTestGIT.cxx

@@ -0,0 +1,416 @@
+/*=========================================================================
+
+  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 "cmCTestGIT.h"
+
+#include "cmCTest.h"
+#include "cmSystemTools.h"
+#include "cmXMLSafe.h"
+
+#include <cmsys/RegularExpression.hxx>
+#include <cmsys/ios/sstream>
+#include <cmsys/Process.h>
+
+#include <ctype.h>
+
+//----------------------------------------------------------------------------
+cmCTestGIT::cmCTestGIT(cmCTest* ct, std::ostream& log):
+  cmCTestGlobalVC(ct, log)
+{
+  this->PriorRev = this->Unknown;
+}
+
+//----------------------------------------------------------------------------
+cmCTestGIT::~cmCTestGIT()
+{
+}
+
+//----------------------------------------------------------------------------
+class cmCTestGIT::OneLineParser: public cmCTestVC::LineParser
+{
+public:
+  OneLineParser(cmCTestGIT* git, const char* prefix,
+                std::string& l): Line1(l)
+    {
+    this->SetLog(&git->Log, prefix);
+    }
+private:
+  std::string& Line1;
+  virtual bool ProcessLine()
+    {
+    // Only the first line is of interest.
+    this->Line1 = this->Line;
+    return false;
+    }
+};
+
+//----------------------------------------------------------------------------
+std::string cmCTestGIT::GetWorkingRevision()
+{
+  // Run plumbing "git rev-list" to get work tree revision.
+  const char* git = this->CommandLineTool.c_str();
+  const char* git_rev_list[] = {git, "rev-list", "-n", "1", "HEAD", 0};
+  std::string rev;
+  OneLineParser out(this, "rl-out> ", rev);
+  OutputLogger err(this->Log, "rl-err> ");
+  this->RunChild(git_rev_list, &out, &err);
+  return rev;
+}
+
+//----------------------------------------------------------------------------
+void cmCTestGIT::NoteOldRevision()
+{
+  this->OldRevision = this->GetWorkingRevision();
+  cmCTestLog(this->CTest, HANDLER_OUTPUT, "   Old revision of repository is: "
+             << this->OldRevision << "\n");
+  this->PriorRev.Rev = this->OldRevision;
+}
+
+//----------------------------------------------------------------------------
+void cmCTestGIT::NoteNewRevision()
+{
+  this->NewRevision = this->GetWorkingRevision();
+  cmCTestLog(this->CTest, HANDLER_OUTPUT, "   New revision of repository is: "
+             << this->NewRevision << "\n");
+}
+
+//----------------------------------------------------------------------------
+bool cmCTestGIT::UpdateImpl()
+{
+  // Use "git pull" to update the working tree.
+  std::vector<char const*> git_pull;
+  git_pull.push_back(this->CommandLineTool.c_str());
+  git_pull.push_back("pull");
+
+  // TODO: if(this->CTest->GetTestModel() == cmCTest::NIGHTLY)
+
+  // Add user-specified update options.
+  std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
+  if(opts.empty())
+    {
+    opts = this->CTest->GetCTestConfiguration("GITUpdateOptions");
+    }
+  std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str());
+  for(std::vector<cmStdString>::const_iterator ai = args.begin();
+      ai != args.end(); ++ai)
+    {
+    git_pull.push_back(ai->c_str());
+    }
+
+  // Sentinel argument.
+  git_pull.push_back(0);
+
+  OutputLogger out(this->Log, "pull-out> ");
+  OutputLogger err(this->Log, "pull-err> ");
+  return this->RunUpdateCommand(&git_pull[0], &out, &err);
+}
+
+//----------------------------------------------------------------------------
+/* Diff format:
+
+   :src-mode dst-mode src-sha1 dst-sha1 status\0
+   src-path\0
+   [dst-path\0]
+
+   The format is repeated for every file changed.  The [dst-path\0]
+   line appears only for lines with status 'C' or 'R'.  See 'git help
+   diff-tree' for details.
+*/
+class cmCTestGIT::DiffParser: public cmCTestVC::LineParser
+{
+public:
+  DiffParser(cmCTestGIT* git, const char* prefix):
+    LineParser('\0', false), GIT(git), DiffField(DiffFieldNone)
+    {
+    this->SetLog(&git->Log, prefix);
+    }
+
+  typedef cmCTestGIT::Change Change;
+  std::vector<Change> Changes;
+protected:
+  cmCTestGIT* GIT;
+  enum DiffFieldType { DiffFieldNone, DiffFieldChange,
+                       DiffFieldSrc, DiffFieldDst };
+  DiffFieldType DiffField;
+  Change CurChange;
+
+  void DiffReset()
+    {
+    this->DiffField = DiffFieldNone;
+    this->Changes.clear();
+    }
+
+  virtual bool ProcessLine()
+    {
+    if(this->Line[0] == ':')
+      {
+      this->DiffField = DiffFieldChange;
+      this->CurChange = Change();
+      }
+    if(this->DiffField == DiffFieldChange)
+      {
+      // :src-mode dst-mode src-sha1 dst-sha1 status
+      if(this->Line[0] != ':')
+        {
+        this->DiffField = DiffFieldNone;
+        return true;
+        }
+      const char* src_mode_first = this->Line.c_str()+1;
+      const char* src_mode_last  = this->ConsumeField(src_mode_first);
+      const char* dst_mode_first = this->ConsumeSpace(src_mode_last);
+      const char* dst_mode_last  = this->ConsumeField(dst_mode_first);
+      const char* src_sha1_first = this->ConsumeSpace(dst_mode_last);
+      const char* src_sha1_last  = this->ConsumeField(src_sha1_first);
+      const char* dst_sha1_first = this->ConsumeSpace(src_sha1_last);
+      const char* dst_sha1_last  = this->ConsumeField(dst_sha1_first);
+      const char* status_first   = this->ConsumeSpace(dst_sha1_last);
+      const char* status_last    = this->ConsumeField(status_first);
+      if(status_first != status_last)
+        {
+        this->CurChange.Action = *status_first;
+        this->DiffField = DiffFieldSrc;
+        }
+      else
+        {
+        this->DiffField = DiffFieldNone;
+        }
+      }
+    else if(this->DiffField == DiffFieldSrc)
+      {
+      // src-path
+      if(this->CurChange.Action == 'C')
+        {
+        // Convert copy to addition of destination.
+        this->CurChange.Action = 'A';
+        this->DiffField = DiffFieldDst;
+        }
+      else if(this->CurChange.Action == 'R')
+        {
+        // Convert rename to deletion of source and addition of destination.
+        this->CurChange.Action = 'D';
+        this->CurChange.Path = this->Line;
+        this->Changes.push_back(this->CurChange);
+
+        this->CurChange = Change('A');
+        this->DiffField = DiffFieldDst;
+        }
+      else
+        {
+        this->CurChange.Path = this->Line;
+        this->Changes.push_back(this->CurChange);
+        this->DiffField = this->DiffFieldNone;
+        }
+      }
+    else if(this->DiffField == DiffFieldDst)
+      {
+      // dst-path
+      this->CurChange.Path = this->Line;
+      this->Changes.push_back(this->CurChange);
+      this->DiffField = this->DiffFieldNone;
+      }
+    return true;
+    }
+
+  const char* ConsumeSpace(const char* c)
+    {
+    while(*c && isspace(*c)) { ++c; }
+    return c;
+    }
+  const char* ConsumeField(const char* c)
+    {
+    while(*c && !isspace(*c)) { ++c; }
+    return c;
+    }
+};
+
+//----------------------------------------------------------------------------
+/* Commit format:
+
+   commit ...\n
+   tree ...\n
+   parent ...\n
+   author ...\n
+   committer ...\n
+   \n
+       Log message indented by (4) spaces\n
+       (even blank lines have the spaces)\n
+   \n
+   [Diff format]
+
+   The header may have more fields.  See 'git help diff-tree'.
+*/
+class cmCTestGIT::CommitParser: public DiffParser
+{
+public:
+  CommitParser(cmCTestGIT* git, const char* prefix):
+    DiffParser(git, prefix), Section(SectionHeader)
+    {
+    this->Separator = SectionSep[this->Section];
+    }
+
+private:
+  typedef cmCTestGIT::Revision Revision;
+  enum SectionType { SectionHeader, SectionBody, SectionDiff, SectionCount };
+  static char const SectionSep[SectionCount];
+  SectionType Section;
+  Revision Rev;
+
+  struct Person
+  {
+    std::string Name;
+    std::string EMail;
+    unsigned long Time;
+    long TimeZone;
+    Person(): Name(), EMail(), Time(0), TimeZone(0) {}
+  };
+
+  void ParsePerson(const char* str, Person& person)
+    {
+    // Person Name <[email protected]> 1234567890 +0000
+    const char* c = str;
+    while(*c && isspace(*c)) { ++c; }
+
+    const char* name_first = c;
+    while(*c && *c != '<') { ++c; }
+    const char* name_last = c;
+    while(name_last != name_first && isspace(*(name_last-1))) { --name_last; }
+    person.Name.assign(name_first, name_last-name_first);
+
+    const char* email_first = *c? ++c : c;
+    while(*c && *c != '>') { ++c; }
+    const char* email_last = *c? c++ : c;
+    person.EMail.assign(email_first, email_last-email_first);
+
+    person.Time = strtoul(c, (char**)&c, 10);
+    person.TimeZone = strtol(c, (char**)&c, 10);
+    }
+
+  virtual bool ProcessLine()
+    {
+    if(this->Line.empty())
+      {
+      this->NextSection();
+      }
+    else
+      {
+      switch(this->Section)
+        {
+        case SectionHeader: this->DoHeaderLine(); break;
+        case SectionBody:   this->DoBodyLine(); break;
+        case SectionDiff:   this->DiffParser::ProcessLine(); break;
+        case SectionCount:  break; // never happens
+        }
+      }
+    return true;
+    }
+
+  void NextSection()
+    {
+    this->Section = SectionType((this->Section+1) % SectionCount);
+    this->Separator = SectionSep[this->Section];
+    if(this->Section == SectionHeader)
+      {
+      this->GIT->DoRevision(this->Rev, this->Changes);
+      this->Rev = Revision();
+      this->DiffReset();
+      }
+    }
+
+  void DoHeaderLine()
+    {
+    // Look for header fields that we need.
+    if(strncmp(this->Line.c_str(), "commit ", 7) == 0)
+      {
+      this->Rev.Rev = this->Line.c_str()+7;
+      }
+    else if(strncmp(this->Line.c_str(), "author ", 7) == 0)
+      {
+      Person author;
+      this->ParsePerson(this->Line.c_str()+7, author);
+      this->Rev.Author = author.Name;
+      char buf[1024];
+      if(author.TimeZone >= 0)
+        {
+        sprintf(buf, "%lu +%04ld", author.Time, author.TimeZone);
+        }
+      else
+        {
+        sprintf(buf, "%lu -%04ld", author.Time, -author.TimeZone);
+        }
+      this->Rev.Date = buf;
+      }
+    }
+
+  void DoBodyLine()
+    {
+    // Commit log lines are indented by 4 spaces.
+    if(this->Line.size() >= 4)
+      {
+      this->Rev.Log += this->Line.substr(4);
+      }
+    this->Rev.Log += "\n";
+    }
+};
+
+char const cmCTestGIT::CommitParser::SectionSep[SectionCount] =
+{'\n', '\n', '\0'};
+
+//----------------------------------------------------------------------------
+void cmCTestGIT::LoadRevisions()
+{
+  // Use 'git rev-list ... | git diff-tree ...' to get revisions.
+  std::string range = this->OldRevision + ".." + this->NewRevision;
+  const char* git = this->CommandLineTool.c_str();
+  const char* git_rev_list[] =
+    {git, "rev-list", "--reverse", range.c_str(), "--", 0};
+  const char* git_diff_tree[] =
+    {git, "diff-tree", "--stdin", "--always", "-z", "-r", "--pretty=raw",
+     "--encoding=utf-8", 0};
+  this->Log << this->ComputeCommandLine(git_rev_list) << " | "
+            << this->ComputeCommandLine(git_diff_tree) << "\n";
+
+  cmsysProcess* cp = cmsysProcess_New();
+  cmsysProcess_AddCommand(cp, git_rev_list);
+  cmsysProcess_AddCommand(cp, git_diff_tree);
+  cmsysProcess_SetWorkingDirectory(cp, this->SourceDirectory.c_str());
+
+  CommitParser out(this, "dt-out> ");
+  OutputLogger err(this->Log, "dt-err> ");
+  this->RunProcess(cp, &out, &err);
+
+  // Send one extra zero-byte to terminate the last record.
+  out.Process("", 1);
+
+  cmsysProcess_Delete(cp);
+}
+
+//----------------------------------------------------------------------------
+void cmCTestGIT::LoadModifications()
+{
+  // Use 'git diff-index' to get modified files.
+  const char* git = this->CommandLineTool.c_str();
+  const char* git_diff_index[] = {git, "diff-index", "-z", "HEAD", 0};
+
+  DiffParser out(this, "di-out> ");
+  OutputLogger err(this->Log, "di-err> ");
+  this->RunChild(git_diff_index, &out, &err);
+
+  for(std::vector<Change>::const_iterator ci = out.Changes.begin();
+      ci != out.Changes.end(); ++ci)
+    {
+    this->DoModification(PathModified, ci->Path);
+    }
+}

+ 52 - 0
Source/CTest/cmCTestGIT.h

@@ -0,0 +1,52 @@
+/*=========================================================================
+
+  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.
+
+=========================================================================*/
+#ifndef cmCTestGIT_h
+#define cmCTestGIT_h
+
+#include "cmCTestGlobalVC.h"
+
+/** \class cmCTestGIT
+ * \brief Interaction with git command-line tool
+ *
+ */
+class cmCTestGIT: public cmCTestGlobalVC
+{
+public:
+  /** Construct with a CTest instance and update log stream.  */
+  cmCTestGIT(cmCTest* ctest, std::ostream& log);
+
+  virtual ~cmCTestGIT();
+
+private:
+  std::string GetWorkingRevision();
+  virtual void NoteOldRevision();
+  virtual void NoteNewRevision();
+  virtual bool UpdateImpl();
+
+  void LoadRevisions();
+  void LoadModifications();
+
+  // Parsing helper classes.
+  class OneLineParser;
+  class DiffParser;
+  class CommitParser;
+  friend class OneLineParser;
+  friend class DiffParser;
+  friend class CommitParser;
+};
+
+#endif

+ 4 - 0
Source/CTest/cmCTestUpdateCommand.cxx

@@ -48,6 +48,10 @@ cmCTestGenericHandler* cmCTestUpdateCommand::InitializeHandler()
     "SVNCommand", "CTEST_SVN_COMMAND");
   this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile,
     "SVNUpdateOptions", "CTEST_SVN_UPDATE_OPTIONS");
+  this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile,
+    "GITCommand", "CTEST_GIT_COMMAND");
+  this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile,
+    "GITUpdateOptions", "CTEST_GIT_UPDATE_OPTIONS");
 
   const char* initialCheckoutCommand
     = this->Makefile->GetDefinition("CTEST_CHECKOUT_COMMAND");

+ 19 - 1
Source/CTest/cmCTestUpdateHandler.cxx

@@ -30,6 +30,7 @@
 #include "cmCTestVC.h"
 #include "cmCTestCVS.h"
 #include "cmCTestSVN.h"
+#include "cmCTestGIT.h"
 
 #include <cmsys/auto_ptr.hxx>
 
@@ -50,7 +51,8 @@ static const char* cmCTestUpdateHandlerUpdateStrings[] =
 {
   "Unknown",
   "CVS",
-  "SVN"
+  "SVN",
+  "GIT"
 };
 
 static const char* cmCTestUpdateHandlerUpdateToString(int type)
@@ -133,6 +135,10 @@ int cmCTestUpdateHandler::DetermineType(const char* cmd, const char* type)
       {
       return cmCTestUpdateHandler::e_SVN;
       }
+    if ( stype.find("git") != std::string::npos )
+      {
+      return cmCTestUpdateHandler::e_GIT;
+      }
     }
   else
     {
@@ -147,6 +153,10 @@ int cmCTestUpdateHandler::DetermineType(const char* cmd, const char* type)
       {
       return cmCTestUpdateHandler::e_SVN;
       }
+    if ( stype.find("git") != std::string::npos )
+      {
+      return cmCTestUpdateHandler::e_GIT;
+      }
     }
   return cmCTestUpdateHandler::e_UNKNOWN;
 }
@@ -204,6 +214,7 @@ int cmCTestUpdateHandler::ProcessHandler()
     {
     case e_CVS: vc.reset(new cmCTestCVS(this->CTest, ofs)); break;
     case e_SVN: vc.reset(new cmCTestSVN(this->CTest, ofs)); break;
+    case e_GIT: vc.reset(new cmCTestGIT(this->CTest, ofs)); break;
     default:    vc.reset(new cmCTestVC(this->CTest, ofs));  break;
     }
   vc->SetCommandLineTool(this->UpdateCommand);
@@ -337,6 +348,12 @@ int cmCTestUpdateHandler::DetectVCS(const char* dir)
     {
     return cmCTestUpdateHandler::e_CVS;
     }
+  sourceDirectory = dir;
+  sourceDirectory += "/.git";
+  if ( cmSystemTools::FileExists(sourceDirectory.c_str()) )
+    {
+    return cmCTestUpdateHandler::e_GIT;
+    }
   return cmCTestUpdateHandler::e_UNKNOWN;
 }
 
@@ -364,6 +381,7 @@ bool cmCTestUpdateHandler::SelectVCS()
       {
       case e_CVS: key = "CVSCommand"; break;
       case e_SVN: key = "SVNCommand"; break;
+      case e_GIT: key = "GITCommand"; break;
       default: break;
       }
     if (key)

+ 1 - 0
Source/CTest/cmCTestUpdateHandler.h

@@ -46,6 +46,7 @@ public:
     e_UNKNOWN = 0,
     e_CVS,
     e_SVN,
+    e_GIT,
     e_LAST
   };
 

+ 13 - 0
Tests/CMakeLists.txt

@@ -925,6 +925,19 @@ ${CMake_BINARY_DIR}/bin/cmake -DVERSION=CVS -P ${CMake_SOURCE_DIR}/Utilities/Rel
       LIST(APPEND TEST_BUILD_DIRS "${CMake_BINARY_DIR}/Tests/${CTestUpdateCVS_DIR}")
     ENDIF(CTEST_TEST_UPDATE_CVS AND CVS_FOUND)
 
+    # Test CTest Update with GIT
+    FIND_PROGRAM(GIT_EXECUTABLE NAMES git)
+    MARK_AS_ADVANCED(GIT_EXECUTABLE)
+    IF(GIT_EXECUTABLE)
+      SET(CTestUpdateGIT_DIR "CTest UpdateGIT")
+      CONFIGURE_FILE("${CMake_SOURCE_DIR}/Tests/CTestUpdateGIT.cmake.in"
+        "${CMake_BINARY_DIR}/Tests/CTestUpdateGIT.cmake" @ONLY)
+      ADD_TEST(CTest.UpdateGIT ${CMAKE_CMAKE_COMMAND}
+        -P "${CMake_BINARY_DIR}/Tests/CTestUpdateGIT.cmake"
+        )
+      LIST(APPEND TEST_BUILD_DIRS "${CMake_BINARY_DIR}/Tests/${CTestUpdateGIT_DIR}")
+    ENDIF(GIT_EXECUTABLE)
+
   ENDIF(CTEST_TEST_UPDATE)
 
   IF (CTEST_TEST_CTEST AND CMAKE_RUN_LONG_TESTS)

+ 174 - 0
Tests/CTestUpdateGIT.cmake.in

@@ -0,0 +1,174 @@
+# This script drives creation of a git repository and checks
+# that CTest can update from it.
+
+#-----------------------------------------------------------------------------
+# Test in a directory next to this script.
+get_filename_component(TOP "${CMAKE_CURRENT_LIST_FILE}" PATH)
+set(TOP "${TOP}/@CTestUpdateGIT_DIR@")
+
+# Include code common to all update tests.
+include("@CMAKE_CURRENT_SOURCE_DIR@/CTestUpdateCommon.cmake")
+
+#-----------------------------------------------------------------------------
+# Report git tools in use.
+message("Using GIT tools:")
+set(GIT "@GIT_EXECUTABLE@")
+message(" git = ${GIT}")
+
+set(AUTHOR_CONFIG "[user]
+\tname = Test Author
+\temail = [email protected]
+")
+
+#-----------------------------------------------------------------------------
+# Initialize the testing directory.
+message("Creating test directory...")
+init_testing()
+
+#-----------------------------------------------------------------------------
+# Create the repository.
+message("Creating repository...")
+file(MAKE_DIRECTORY ${TOP}/repo.git)
+run_child(
+  WORKING_DIRECTORY ${TOP}/repo.git
+  COMMAND ${GIT} init --bare
+  )
+file(REMOVE_RECURSE ${TOP}/repo.git/hooks)
+set(REPO file://${TOP}/repo.git)
+
+#-----------------------------------------------------------------------------
+# Import initial content into the repository.
+message("Importing content...")
+create_content(import)
+
+# Import the content into the repository.
+run_child(WORKING_DIRECTORY ${TOP}/import
+  COMMAND ${GIT} init
+  )
+file(REMOVE_RECURSE ${TOP}/import/.git/hooks)
+file(APPEND ${TOP}/import/.git/config "
+[remote \"origin\"]
+\turl = ${REPO}
+\tfetch = +refs/heads/*:refs/remotes/origin/*
+${AUTHOR_CONFIG}")
+run_child(WORKING_DIRECTORY ${TOP}/import
+  COMMAND ${GIT} add .
+  )
+run_child(WORKING_DIRECTORY ${TOP}/import
+  COMMAND ${GIT} commit -m "Initial content"
+  )
+run_child(WORKING_DIRECTORY ${TOP}/import
+  COMMAND ${GIT} push origin master:refs/heads/master
+  )
+
+#-----------------------------------------------------------------------------
+# Create a working tree.
+message("Checking out revision 1...")
+run_child(
+  WORKING_DIRECTORY ${TOP}
+  COMMAND ${GIT} clone ${REPO} user-source
+  )
+file(REMOVE_RECURSE ${TOP}/user-source/.git/hooks)
+file(APPEND ${TOP}/user-source/.git/config "${AUTHOR_CONFIG}")
+
+#-----------------------------------------------------------------------------
+# Make changes in the working tree.
+message("Changing content...")
+update_content(user-source files_added files_removed dirs_added)
+if(dirs_added)
+  run_child(
+    WORKING_DIRECTORY ${TOP}/user-source
+    COMMAND ${GIT} add ${dirs_added}
+    )
+endif(dirs_added)
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} add ${files_added}
+  )
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} rm ${files_removed}
+  )
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} add -u
+  )
+
+#-----------------------------------------------------------------------------
+# Commit the changes to the repository.
+message("Committing revision 2...")
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} commit -m "Changed content"
+  )
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} push origin
+  )
+
+#-----------------------------------------------------------------------------
+# Make changes in the working tree.
+message("Changing content again...")
+change_content(user-source)
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} add -u
+  )
+
+#-----------------------------------------------------------------------------
+# Commit the changes to the repository.
+message("Committing revision 3...")
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} commit -m "Changed content again"
+  )
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} push origin
+  )
+
+#-----------------------------------------------------------------------------
+# Go back to before the changes so we can test updating.
+message("Backing up to revision 1...")
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${GIT} reset --hard master~2
+  )
+
+# Create a modified file.
+modify_content(user-source)
+
+#-----------------------------------------------------------------------------
+# Test updating the user work directory with the command-line interface.
+message("Running CTest Dashboard Command Line...")
+
+# Create the user build tree.
+create_build_tree(user-source user-binary)
+file(APPEND ${TOP}/user-binary/CTestConfiguration.ini
+  "# GIT command configuration
+UpdateCommand: ${GIT}
+")
+
+# Run the dashboard command line interface.
+run_dashboard_command_line(user-binary)
+
+#-----------------------------------------------------------------------------
+# Test initial checkout and update with a dashboard script.
+message("Running CTest Dashboard Script...")
+
+create_dashboard_script(dashboard.cmake
+  "# git command configuration
+set(CTEST_GIT_COMMAND \"${GIT}\")
+set(CTEST_GIT_UPDATE_OPTIONS)
+execute_process(
+  WORKING_DIRECTORY \"${TOP}\"
+  COMMAND \"${GIT}\" clone \"${REPO}\" dash-source
+  )
+execute_process(
+  WORKING_DIRECTORY \"${TOP}/dash-source\"
+  COMMAND \"${GIT}\" reset --hard master~2
+  )
+")
+
+# Run the dashboard script with CTest.
+run_dashboard_script(dashboard.cmake)