Commit 8371fc79 authored by Leigh Stoller's avatar Leigh Stoller

Checkpoint my cvs interface to the workbench. This first cut uses the

"rtag" directive to initiate template modify operations. So, to get started
you do a checkout:

  cvs -d ops.emulab.net:/proj/$pid/templates/XXXXX/cvsrepo checkout XXXXX

where XXXXX is the part of the guid (10000/1) before the slash. Might try
and roll all templates into a single project wide repo at some point, to
avoid the extraneous path stuff, but didn't want to worry that just yet.

Okay, so have a checkout. You can work along the trunk, doing commits. To
create a new template (a modify of the existing template), you tag the tree
using rtag:

  cvs -d ops.emulab.net:/proj/$pid/templates/XXXXX/cvsrepo rtag mytag XXXXX

A template modify is started at the end, and you should probably wait for
email before continuing. Eventually I will need to add locking of some
kind, but I have to do the modify in the background, or else I get deadlock
cause cvs keeps the repo locked, and the modify also needs to access it.

Each time you tag along the trunk, you get a modified template, which in
the history diagram looks like:

  10000/1 --> 10000/2 --> 10000/3 ...

If you want to branch, say at 10000/2 you can create a branch tag using rtag:

  cvs -d [cut] rtag -r T10000/2 -b mytag2 XXXXX

You can also use your own tags for -r option, but I also create a TXXXXX/YY
tag at each template modify, which is easy to remember.

Then update your sandbox to the new branch, commit changes along that
branch, and then later use rtag again to initiate a template modify
operation:

  cvs update -r mytag2
  cvs commit ...
  cvs -d [cut] rtag -r mytag2 mytag3 XXXXX

And now the history diagram looks like:

  10000/1 --> 10000/2 --> 10000/3 ...
                |
                |
                -> 10000/4 ...

You should be able to mix interaction via the web with interaction via the
cvs interface. I've tested it, although not extensively.
parent 0cb4530f
......@@ -2480,7 +2480,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
tbsetup/ipassign/ipassign_wrapper tbsetup/assign_prepass \
tbsetup/panic tbsetup/tbrsync tbsetup/nfstrace \
tbsetup/checkup/GNUmakefile tbsetup/checkup/checkup_daemon \
tbsetup/libtblog.pm \
tbsetup/libtblog.pm tbsetup/template_cvsroot/GNUmakefile \
tip/GNUmakefile tip/console \
tmcd/GNUmakefile tmcd/tmcd.restart \
tmcd/common/GNUmakefile tmcd/common/config/GNUmakefile \
......
......@@ -861,7 +861,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
tbsetup/ipassign/ipassign_wrapper tbsetup/assign_prepass \
tbsetup/panic tbsetup/tbrsync tbsetup/nfstrace \
tbsetup/checkup/GNUmakefile tbsetup/checkup/checkup_daemon \
tbsetup/libtblog.pm \
tbsetup/libtblog.pm tbsetup/template_cvsroot/GNUmakefile \
tip/GNUmakefile tip/console \
tmcd/GNUmakefile tmcd/tmcd.restart \
tmcd/common/GNUmakefile tmcd/common/config/GNUmakefile \
......
......@@ -15,7 +15,7 @@ SYSTEM := $(shell uname -s)
include $(OBJDIR)/Makeconf
SUBDIRS = checkpass ns2ir nsverify nseparse checkup
SUBDIRS = checkpass ns2ir nsverify nseparse checkup template_cvsroot
BIN_STUFF = power snmpit tbend tbprerun tbreport \
os_load endexp batchexp swapexp \
......@@ -158,6 +158,7 @@ endif
subdir-install:
@$(MAKE) -C checkpass install
@$(MAKE) -C ns2ir install
@$(MAKE) -C template_cvsroot install
@$(MAKE) -C nsverify install
@$(MAKE) -C nseparse install
$(PLAB_INSTALL)
......
......@@ -1463,6 +1463,21 @@ sub InstanceList($$$)
return 0;
}
#
# Change description
#
sub SetDescription($$)
{
my ($self, $description) = @_;
# Must be a real reference.
return -1
if (! ref($self));
return $self->ModifyMetadata("description", $description,
User->ThisUser(), "template_description");
}
#
# Copy the datastore from parent template to child template.
#
......@@ -1723,7 +1738,7 @@ sub CreateDirectory($)
print "*** Could not create directory $versdir: $!\n";
return $rval;
}
foreach my $token ("tbdata", "archive", "datastore", "logs") {
foreach my $token ("tbdata", "datastore") {
my $dir = "$versdir/$token";
if (! mkdir($dir, 0770) ||
......@@ -1733,7 +1748,7 @@ sub CreateDirectory($)
return $rval;
}
}
if (0) {
my $workdir = TBExptWorkDir($pid, $eid);
#
......@@ -1748,6 +1763,7 @@ sub CreateDirectory($)
print "*** Could not create $workdir: $!\n";
return -1;
}
}
return 0;
}
......
......@@ -325,41 +325,27 @@ tblog_set_info($pid,$eid,$UID);
#
# Create a directory structure for the experiment.
# There is no need to do this for the template wrapper experiment; it was
# already done when the template was created.
#
if (!defined($template) || defined($instance)) {
if ($experiment->CreateDirectory() != 0) {
if (($? >> 8) == EDQUOT()) {
# Obey exit status protocol for web page; User should see this.
$errorstat = 1;
}
fatal({type => 'secondary', severity => SEV_SECONDARY,
error => ['create_experiment_directory_failed']},
"Failed to created experiment directory");
}
if (defined($instance)) {
# Need to cross-mark the instance right away so that it is flagged.
# Would be better to do this with a plain flag.
my %args = ();
$args{'exptidx'} = $experiment->idx();
$instance->Update(0, \%args) == 0
or fatal("Could not update experiment instance record!");
#
if ($experiment->CreateDirectory() != 0) {
if (($? >> 8) == EDQUOT()) {
# Obey exit status protocol for web page; User should see this.
$errorstat = 1;
}
fatal({type => 'secondary', severity => SEV_SECONDARY,
error => ['create_experiment_directory_failed']},
"Failed to created experiment directory");
}
else {
#
# But we do need to update the experiment record with the template path.
#
if (defined($instance)) {
# Need to cross-mark the instance right away so that it is flagged.
# Would be better to do this with a plain flag.
my %args = ();
$args{'path'} = $template->path();
$args{'exptidx'} = $experiment->idx();
$experiment->Update(\%args) == 0
or fatal("Could not update experiment record!");
# And the template with the new experiment index.
$instance->Update(0, \%args) == 0
or fatal("Could not update experiment instance record!");
}
elsif (defined($template)) {
# Tell the template with the new experiment index.
%args = ();
$args{'exptidx'} = $experiment->idx();
......
......@@ -18,7 +18,7 @@ use Exporter;
TBSCRIPTLOCK_OKAY TBSCRIPTLOCK_TIMEDOUT
TBSCRIPTLOCK_IGNORE TBSCRIPTLOCK_FAILED
PROJROOT GROUPROOT USERROOT SCRATCHROOT SHAREROOT
TBValidUserDir TBValidUserDirList TBMakeTempFile NewUUID);
TBValidUserDir TBValidUserDirList TBMakeTempFile NewUUID System);
# After package decl.
use English;
......@@ -29,6 +29,7 @@ use Fcntl ':flock';
use IO::Handle;
use File::Basename;
use Time::HiRes qw(gettimeofday);
use vars qw(@EXPORT_OK);
#my $MAILTAG = "@OURDOMAIN@";
my $MAILTAG = "@THISHOMEBASE@";
......@@ -37,6 +38,10 @@ my $TIMESTAMPS = "@TIMESTAMPS@";
my $TBOPSEMAIL = "@TBOPSEMAIL@";
my $SCRIPTNAME = "Unknown";
# Exported.
$SYSTEM_DEBUG = 0;
@EXPORT_OK = qw($SYSTEM_DEBUG);
#
# Real mount points (on the fileserver) for exported directories.
# At the moment, we have no reason to export these via functions.
......@@ -684,4 +689,18 @@ sub NewUUID()
return undef;
}
sub System($)
{
my ($command) = @_;
print STDERR "Running '$command'\n"
if ($system_debug);
TBDebugTimeStamp($command);
my $retval = system($command);
TBDebugTimeStamp("Done");
return $retval;
}
1;
......@@ -34,16 +34,18 @@ use Data::Dumper;
sub usage()
{
print(STDERR
"Usage: template_checkout [-q] -f <path> <guid/vers>\n".
"Usage: template_checkout [-q] [-u] -f <path> <guid/vers>\n".
"switches and arguments:\n".
"-q - be less chatty\n".
"-u - update existing checkout\n".
"-f - path to checkout directory on ops (via NFS).\n".
"<guid/vers> - GUID and version to swapin\n");
exit(-1);
}
my $optlist = "qdf:";
my $optlist = "qdf:u";
my %options = ();
my $quiet = 0;
my $update = 0;
my $debug = 0;
my $directory;
my $guid;
......@@ -124,7 +126,7 @@ ParseArgs();
# A little cookie file in that indicates what template it refers to.
my $cookie = "$directory/.template";
if (-e $cookie) {
if (-e $cookie && !$update) {
print STDERR "$directory already contains a checkout!\n";
print STDERR "There is no 'update' operation yet; delete or move it.\n";
exit(1);
......@@ -158,6 +160,14 @@ if (! -e "$directory/exp") {
tbdie("Could not create exp symlink in $directory!");
}
#
# No update in place (yet) since we use simple zip files. Move the current
# datastore directory out of the way.
#
if ($update && -d "$directory/datastore") {
}
#
# There is a plain experiment underlying the template, which has a lot
# of stuff we do not want to show the user in the checkout cause its
......@@ -236,6 +246,9 @@ sub ParseArgs()
if (defined($options{"d"})) {
$debug = 2;
}
if (defined($options{"u"})) {
$update = 1;
}
}
#
......
......@@ -29,20 +29,25 @@ sub usage()
{
print(STDERR
"Usage: template_commit [-q] -f <path>\n".
" template_commit [-q] [-e eid] <guid/vers>\n".
" template_commit [-q] [-e eid | -r tag] <guid/vers>\n".
" template_commit [-q] -p pid -e eid\n".
"switches and arguments:\n".
"-q - be less chatty\n".
"-e <eid> - Experiment instance to commit from\n".
"-E <str> - A pithy sentence describing the new template\n".
"-t <tid> - The template name (alphanumeric, no blanks)\n".
"-p <pid> - Project for -e option\n".
"<guid/vers> - GUID and version to swapin\n");
exit(-1);
}
my $optlist = "qe:dp:f:";
my $optlist = "qe:dp:f:t:E:r:";
my %options = ();
my $quiet = 0;
my $debug = 0;
my $frompath;
my $repotag;
my $tid;
my $description;
my $eid;
my $pid;
my $guid;
......@@ -74,6 +79,7 @@ sub ParseArgs();
sub fatal($$);
sub sighandler($);
sub cleanup();
sub CommitFromRepo();
sub CommitFromInstance();
sub CommitFromTemplate();
sub CommitFromCheckout();
......@@ -177,28 +183,75 @@ if (! $template->AccessCheck($this_user, TB_EXPT_MODIFY)) {
#
$SIG{TERM} = \&sighandler;
if (defined($frompath)) {
CommitFromCheckout();
}
elsif (defined($eid)) {
CommitFromInstance();
if (defined($repotag)) {
CommitFromRepo();
}
else {
CommitFromTemplate();
}
tbdie("Unsupported template commit operation!");
if (defined($frompath)) {
CommitFromCheckout();
}
elsif (defined($eid)) {
CommitFromInstance();
}
else {
CommitFromTemplate();
}
}
exit(0);
#
# Commit from a Repo.
#
sub CommitFromRepo()
{
my $pid = $template->pid();
my $gid = $template->gid();
my $optarg = ($quiet ? "-q" : "");
# Optional override from commandline
$tid = $template->tid()
if (!defined($tid));
#
# Template modify to give us a new version. Giving it $frompath causes
# the datastore to be imported from the directory instead of the parent
# template.
#
system("$modify -w -m $guid/$version $optarg -g $gid -r $repotag ".
"$pid $tid");
if ($?) {
fatal($? >> 8, "Failed to modify $template!");
}
# Pick up changes to child guid/vers.
$template->Refresh();
my $child_guid = $template->child_guid();
my $child_vers = $template->child_vers();
my $child = Template->Lookup($child_guid, $child_vers);
if (!defined($child)) {
fatal(-1, "Lookup of child template failed!");
}
$child->SetDescription($description)
if (defined($description));
return 0;
}
#
# Commit from a checkout.
#
sub CommitFromCheckout()
{
my $pid = $template->pid();
my $tid = $template->tid();
my $gid = $template->gid();
my $nsfile = "$frompath/tbdata/nsfile.ns";
my $optarg = ($quiet ? "-q" : "");
# Optional override from commandline
$tid = $template->tid()
if (!defined($tid));
#
# The NS file is taken from the checkout.
#
......@@ -219,6 +272,12 @@ sub CommitFromCheckout()
$template->Refresh();
my $child_guid = $template->child_guid();
my $child_vers = $template->child_vers();
my $child = Template->Lookup($child_guid, $child_vers);
if (!defined($child)) {
fatal(-1, "Lookup of child template failed!");
}
$child->SetDescription($description)
if (defined($description));
# Update the cookie so that the checkout refers to the new template
# not the original template.
......@@ -241,12 +300,15 @@ sub CommitFromCheckout()
sub CommitFromTemplate()
{
my $pid = $template->pid();
my $tid = $template->tid();
my $gid = $template->gid();
my $userdir = $template->path();
my $nsfile = "$userdir/tbdata/nsfile.ns";
my $optarg = ($quiet ? "-q" : "");
# Optional override from commandline
$tid = $template->tid()
if (!defined($tid));
#
# The NS file is taken from the template.
#
......@@ -260,6 +322,19 @@ sub CommitFromTemplate()
if ($?) {
fatal($? >> 8, "Failed to modify template!");
}
# Pick up changes to child guid/vers.
$template->Refresh();
my $child_guid = $template->child_guid();
my $child_vers = $template->child_vers();
my $child = Template->Lookup($child_guid, $child_vers);
if (!defined($child)) {
fatal(-1, "Lookup of child template failed!");
}
$child->SetDescription($description)
if (defined($description));
return 0;
}
#
......@@ -268,6 +343,7 @@ sub CommitFromTemplate()
sub CommitFromInstance()
{
my $pid = $template->pid();
my $gid = $template->gid();
my $experiment = Experiment->Lookup($pid, $eid);
if (!defined($experiment)) {
......@@ -289,12 +365,13 @@ sub CommitFromInstance()
fatal(1, "There is no NS file in $userdir/tbdata!")
if (! -e $nsfile);
# Optional override from commandline
$tid = $template->tid()
if (!defined($tid));
#
# Start with a plain template modify of the current template.
#
my $tid = $template->tid();
my $gid = $template->gid();
system("$modify -m $guid/$version -w -g $gid $pid $tid $nsfile");
if ($?) {
fatal($? >> 8, "Failed to commit instance to template!");
......@@ -396,6 +473,40 @@ sub ParseArgs()
tbdie("Improper experiment name (id)!");
}
}
elsif (defined($options{"r"})) {
$repotag = $options{"r"};
if ($repotag =~ /^([-\w:]+)$/) {
$repotag = $1;
}
else {
tbdie("Bad data in argument: $repotag");
}
}
if (defined($options{"t"})) {
$tid = $options{"t"};
if ($tid =~ /^([-\w]+)$/) {
$tid = $1;
}
else {
tbdie("Bad data in argument: $tid.");
}
if (! TBcheck_dbslot($tid, "experiments", "eid",
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
tbdie("Improper template name (id)!");
}
}
if (defined($options{"E"})) {
if (! TBcheck_dbslot($options{"E"},
"experiment_templates", "description",
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
tbdie("Improper template description!");
}
$description = $options{"E"};
}
if (defined($options{"q"})) {
$quiet = 1;
......
......@@ -39,10 +39,12 @@ sub usage()
"<input> - Input file for experiment.\n");
exit(-1);
}
my $optlist = "qwE:g:m:f:";
my $optlist = "qwE:g:m:f:r:";
my $quiet = 0;
my $waitmode = 0;
my $modify = 0;
my $repotag;
my $repobase;
my $frompath;
my $description;
my $pid;
......@@ -71,7 +73,9 @@ my $template;
my $guid;
my $vers;
my $eid;
my $archive_idx = 0;
my $cvsdir;
my $tmpdir;
my $archive;
# For the END block below.
my $cleaning = 0;
my $exptcreated = 0;
......@@ -82,6 +86,8 @@ my $checkquota = "$TB/sbin/checkquota";
my $batchexp = "$TB/bin/batchexp";
my $endexp = "$TB/bin/endexp";
my $makegraph = "$TB/bin/template_graph";
my $CVSBIN = "/usr/bin/cvs";
my $RLOG = "/usr/bin/rlog";
# Protos
sub ParseArgs();
......@@ -99,8 +105,11 @@ use Template;
use libaudit;
use User;
# Be careful not to exit on transient error
$libdb::DBQUERY_MAXTRIES = 120;
# In libdb
my $projroot = PROJROOT();
# Temporary
$libtestbed::SYSTEM_DEBUG = 1;
#
# Turn off line buffering on output
......@@ -225,9 +234,11 @@ if ($STAMPS) {
$justexit = 0;
# Grab stuff we need out of the template.
$guid = $template->guid();
$vers = $template->vers();
$eid = $template->eid();
$guid = $template->guid();
$vers = $template->vers();
$eid = $template->eid();
$cvsdir = "$projroot/$pid/templates/$guid/cvsrepo";
$tmpdir = "$projroot/$pid/templates/$guid/cvstmp.$$";
#
# Use the logonly option to audit so that we get a record mailed.
......@@ -310,6 +321,137 @@ $SIG{QUIT} = 'DEFAULT';
if (my $rval = $template->CreateDirectory()) {
fatal($rval, "Failed to create directories for template");
}
my $template_dir = $template->path();
#
# Set up CVS repo on ops to use as alternate interface.
#
if (!$modify) {
if (! mkdir("$cvsdir", 0777)) {
fatal(-1, "Could not mkdir $cvsdir: $!");
}
if (! chmod(0777, "$cvsdir")) {
fatal(-1, "Could not chmod $cvsdir: $!");
}
# Init the CVS control files.
System("$CVSBIN -d $cvsdir init") == 0
or fatal(-1, "Could not initialize $cvsdir");
open(GUID, ">$cvsdir/CVSROOT/GUID") or
fatal(-1, "Could not create cookie file in CVSROOT directory");
print GUID "$guid\n";
close(GUID);
System("cd $cvsdir/CVSROOT; co -l modules taginfo") == 0
or fatal(-1, "Could not lock $cvsdir/CVSROOT/modules");
System("cp -fp $TB/lib/cvsroot/* $cvsdir/CVSROOT") == 0
or fatal(-1, "Could not copy cvsroot files to $cvsdir/CVSROOT!");
open(MODULES, ">> $cvsdir/CVSROOT/modules")
or fatal(-1, "Could not open $cvsdir/CVSROOT/modules for writing");
print MODULES "\n";
print MODULES "$guid -t $TB/sbin/rtag_commit $guid\n";
close(MODULES);
}
#
# We stick in a little cookie file so we know what template this is,
# from within the CVS repo (well, sandbox).
#
open(COOKIE, "> $template_dir/.template") or
fatal(-1, "Could not create cookie file in stub import directory");
print COOKIE "# DO NOT REMOVE THIS FILE OR MODIFY THIS FILE, EVER!\n";
print COOKIE "GUID: $guid/$vers\n";
print COOKIE "TIME: " . time() . "\n";
close(COOKIE);
if ($repotag) {
#
# We need to get a checkout from the repo for both the nsfile and
# the datastore directory.
#
mkdir("$tmpdir", 0777) or
fatal(-1, "Could not mkdir $tmpdir: $!");
# Checkout ...
System("cd $tmpdir; $CVSBIN -d $cvsdir checkout -r $repotag $guid")
== 0 or fatal(-1, "Could not checkout tag '$repotag' from $cvsdir");
$inputfile = "$tmpdir/$guid/tbdata/nsfile.ns";
fatal(-1, "NS file missing from repo checkout!")
if (!-e $inputfile);
#
# Need the numeric revision corresponding to the tag. From that we can
# determine the branch tag.
#
my $revision;
open(TAGLOG, "$cvsdir/CVSROOT/tags") or
fatal(-1, "Could not open $cvsdir/CVSROOT/tags for reading");
while (<TAGLOG>) {
if ($_ =~ /^$repotag,\s*([\w]+),\s*([\d\.]+)$/) {
print "$repotag $1 at revision $2\n";
if ($1 eq "add" || $1 eq "mov") {
$revision = $2;
}
else {
$revision = undef;
}
}
}
close(TAGLOG);
if (!defined($revision)) {
fatal(-1, "Could not find base revision for $repotag");
}
#
# And now the branch tag that this tag is on. Split the revision
# up so we can find the second to last token. Basically, if
# 1.2.2.1 is the revision, then the branch tag has a revision
# number that looks like 1.2.0.2 (magic). The HEAD is special of
# course.
#
my @tokens = split(/\./, $revision);
if (scalar(@tokens) > 2) {
my $branchtag;
$tokens[scalar(@tokens)-1] = $tokens[scalar(@tokens)-2];
$tokens[scalar(@tokens)-2] = 0;
my $branchrev = join(".", @tokens);
open(RLOG, "$RLOG -h $cvsdir/$guid/.template,v |")
or fatal(-1, "Could not run rlog on $cvsdir/$guid/.template,v!");
my $intags = 0;
while (<RLOG>) {
if ($intags) {
# Look for end of tag section. Consume rest of input file.
if ($_ =~ /^[\w]+/) {
while (<RLOG>) {
;
}
last;
}
# Otherwise process tags ...
if ($_ =~ /^\s+([-\w]+):\s+([\d\.]+)$/) {
#print "tag $1 at revision $2\n";
if ($branchrev eq $2) {
$branchtag = $1;
}
}
next;
}
# Look for the start of the tag section.
if ($_ =~ /^symbolic names:/) {
$intags = 1;
}
}
close(RLOG);
if (!defined($branchtag)) {
fatal(-1, "Could not find branch tag for revision $revision!");
}
$repobase = $branchtag;
}
}
#
# The description is versioned metadata the user can modify.
......@@ -346,24 +488,12 @@ fatal(-1, "Could not add NS file to template store")
#
# Grab archive index for new templates.
#
if (!$modify) {
my $archive;
libArchive::TBExperimentArchive($pid, $eid, \$archive, undef)
>= 0 or fatal(-1, "Could not get archive for new template!");
$archive_idx = $archive->idx();
}
else {
#
# Grab the archive index for the parent; the archive is shared.
#
$archive_idx = $parent_template->archive_idx();
}
libArchive::TBExperimentArchive($pid, $eid, \$archive, undef)
>= 0 or fatal(-1, "Could not get archive for $template!");
# And update the record.
%args = ();
$args{'archive_idx'} = $archive_idx;
$args{'archive_idx'} = $archive->idx();
$template->Update(\%args) == 0
or fatal(-1, "Could not update template record!");
......@@ -381,7 +511,11 @@ if ($modify) {
libArchive::TBCommitExperimentArchive($pid, $eid, "template_modify")
>= 0 or fatal(-1, "Failed to commit experiment archive!");
if (defined($frompath)) {
if ($repotag) {
$template->ImportDataStore("$tmpdir/$guid/datastore") == 0
or fatal(-1, "Failed to import data store");
}
elsif (defined($frompath)) {
$template->ImportDataStore("$frompath/datastore") == 0
or fatal(-1, "Failed to import data store");
}
......@@ -394,11 +528,146 @@ if ($modify) {
libArchive::TBExperimentArchiveAddUserFiles($pid, $eid) == 0
or fatal(-1, "Failed to add datastore files to the archive!");
}
# Make a copy of the inputfile in the template_dir so that it finds its
# way into the user accessible CVS repo. This is also where the datastore
# gets copied to for import.
system("cp -p $inputfile $template_dir/tbdata/nsfile.ns") == 0
or fatal(-1, "Could not copy $inputfile to $template_dir/tbdata");
# Need this so the datastore directory looks populated in the CVS repo.
if (! -e "$template_dir/datastore/.ignore") {
System("cd $template_dir/datastore; touch .ignore")
== 0 or fatal(-1,
"Could not touch .ignore in $template_dir/datastore");
}
#
# When the template is first created, import an initial vendor branch
# into the CVS repo so that there is something there.
#
if (!$modify) {
System("cd $template_dir; ".