source: ProjectBuilder/devel/pb-modules/lib/ProjectBuilder/VCS.pm@ 1800

Last change on this file since 1800 was 1800, checked in by Bruno Cornec, 10 years ago
  • Start to support git-svn
File size: 18.1 KB
Line 
1#!/usr/bin/perl -w
2#
3# Project Builder VCS module
4# VCS subroutines brought by the the Project-Builder project
5# which can be easily used across projects needing to perform
6# VCS related operations
7#
8# $Id$
9#
10# Copyright B. Cornec 2007-2012
11# Eric Anderson's changes are (c) Copyright 2012 Hewlett Packard
12# Provided under the GPL v2
13
14package ProjectBuilder::VCS;
15
16use strict 'vars';
17use Carp 'confess';
18use Cwd 'abs_path';
19use Data::Dumper;
20use English;
21use File::Basename;
22use File::Copy;
23use POSIX qw(strftime);
24use lib qw (lib);
25use ProjectBuilder::Version;
26use ProjectBuilder::Base;
27use ProjectBuilder::Conf;
28
29# Inherit from the "Exporter" module which handles exporting functions.
30
31use vars qw($VERSION $REVISION @ISA @EXPORT);
32use Exporter;
33
34# Export, by default, all the functions into the namespace of
35# any code which uses this module.
36
37our @ISA = qw(Exporter);
38our @EXPORT = qw(pb_vcs_export pb_vcs_get_uri pb_vcs_copy pb_vcs_checkout pb_vcs_up pb_vcs_checkin pb_vcs_isdiff pb_vcs_add pb_vcs_add_if_not_in pb_vcs_cmd);
39($VERSION,$REVISION) = pb_version_init();
40
41=pod
42
43=head1 NAME
44
45ProjectBuilder::VCS, part of the project-builder.org
46
47=head1 DESCRIPTION
48
49This modules provides version control system functions.
50
51=head1 USAGE
52
53=over 4
54
55=item B<pb_vcs_export>
56
57This function exports a VCS content to a directory.
58The first parameter is the URL of the VCS content.
59The second parameter is the directory in which it is locally exposed (result of a checkout). If undef, then use the original VCS content.
60The third parameter is the directory where we want to deliver it (result of export).
61It returns the original tar file if we need to preserve it and undef if we use the produced one.
62
63=cut
64
65sub pb_vcs_export {
66
67my $uri = shift;
68my $source = shift;
69my $destdir = shift;
70my $tmp;
71my $tmp1;
72
73pb_log(1,"pb_vcs_export uri: $uri - destdir: $destdir\n");
74pb_log(1,"pb_vcs_export source: $source\n") if (defined $source);
75my @date = pb_get_date();
76# If it's not flat, then we have a real uri as source
77my ($scheme, $account, $host, $port, $path) = pb_get_uri($uri);
78my $vcscmd = pb_vcs_cmd($scheme);
79$uri = pb_vcs_mod_socks($uri);
80
81if ($scheme =~ /^svn/) {
82 if (defined $source) {
83 if (-d $source) {
84 $tmp = $destdir;
85 } else {
86 $tmp = "$destdir/".basename($source);
87 }
88 $source = pb_vcs_mod_htftp($source,"svn");
89 pb_system("$vcscmd export $source $tmp","Exporting $source from $scheme to $tmp ");
90 } else {
91 $uri = pb_vcs_mod_htftp($uri,"svn");
92 pb_system("$vcscmd export $uri $destdir","Exporting $uri from $scheme to $destdir ");
93 }
94} elsif ($scheme eq "svk") {
95 my $src = $source;
96 if (defined $source) {
97 if (-d $source) {
98 $tmp = $destdir;
99 } else {
100 $tmp = "$destdir/".basename($source);
101 $src = dirname($source);
102 }
103 $source = pb_vcs_mod_htftp($source,"svk");
104 # This doesn't exist !
105 # pb_system("$vcscmd export $path $tmp","Exporting $path from $scheme to $tmp ");
106 pb_log(4,"$uri,$source,$destdir,$scheme, $account, $host, $port, $path,$tmp");
107 if (-d $source) {
108 pb_system("mkdir -p $tmp ; cd $tmp; tar -cf - -C $source . | tar xf -","Exporting $source from $scheme to $tmp ");
109 } else {
110 # If source is file do not use -C with source
111 pb_system("mkdir -p ".dirname($tmp)." ; cd ".dirname($tmp)."; tar -cf - -C $src ".basename($source)." | tar xf -","Exporting $src/".basename($source)." from $scheme to $tmp ");
112 }
113 } else {
114 # Look at svk admin hotcopy
115 confess "Unable to export from svk without a source defined";
116 }
117} elsif ($scheme eq "dir") {
118 pb_system("cp -r $path $destdir","Copying $uri from DIR to $destdir ");
119} elsif (($scheme eq "http") || ($scheme eq "ftp")) {
120 my $f = basename($path);
121 unlink "$ENV{'PBTMP'}/$f";
122 pb_system("$vcscmd $ENV{'PBTMP'}/$f $uri","Downloading $uri with $vcscmd to $ENV{'PBTMP'}/$f\n");
123 # We want to preserve the original tar file
124 pb_vcs_export("file://$ENV{'PBTMP'}/$f",$source,$destdir);
125 return("$ENV{'PBTMP'}/$f");
126} elsif ($scheme =~ /^file/) {
127 eval
128 {
129 require File::MimeInfo;
130 File::MimeInfo->import();
131 };
132 if ($@) {
133 # File::MimeInfo not found
134 confess("ERROR: Install File::MimeInfo to handle scheme $scheme\n");
135 }
136
137 my $mm = mimetype($path);
138 pb_log(2,"mimetype: $mm\n");
139
140 # Check whether the file is well formed
141 # (containing already a directory with the project-version name)
142 #
143 # If it's not the case, we try to adapt, but distro needing
144 # to verify the checksum will have issues (Fedora)
145 # Then upstream should be notified that they need to change their rules
146 # This doesn't apply to patches or additional sources of course.
147 my ($pbwf) = pb_conf_get_if("pbwf");
148 if ((defined $pbwf) && (defined $pbwf->{$ENV{'PBPROJ'}}) && ($path !~ /\/pbpatch\//) && ($path !~ /\/pbsrc\//)) {
149 $destdir = dirname($destdir);
150 pb_log(2,"This is a well-formed file so destdir is now $destdir\n");
151 }
152 pb_mkdir_p($destdir);
153
154 if ($mm =~ /\/x-bzip-compressed-tar$/) {
155 # tar+bzip2
156 pb_system("cd $destdir ; tar xfj $path","Extracting $path in $destdir ");
157 } elsif ($mm =~ /\/x-lzma-compressed-tar$/) {
158 # tar+lzma
159 pb_system("cd $destdir ; tar xfY $path","Extracting $path in $destdir ");
160 } elsif ($mm =~ /\/x-compressed-tar$/) {
161 # tar+gzip
162 pb_system("cd $destdir ; tar xfz $path","Extracting $path in $destdir ");
163 } elsif ($mm =~ /\/x-tar$/) {
164 # tar
165 pb_system("cd $destdir ; tar xf $path","Extracting $path in $destdir ");
166 } elsif ($mm =~ /\/zip$/) {
167 # zip
168 pb_system("cd $destdir ; unzip $path","Extracting $path in $destdir ");
169 } else {
170 # simple file: copy it (patch e.g.)
171 copy($path,$destdir);
172 }
173} elsif ($scheme =~ /^hg/) {
174 if (defined $source) {
175 if (-d $source) {
176 $tmp = $destdir;
177 } else {
178 $tmp = "$destdir/".basename($source);
179 }
180 $source = pb_vcs_mod_htftp($source,"hg");
181 pb_system("cd $source ; $vcscmd archive $tmp","Exporting $source from Mercurial to $tmp ");
182 } else {
183 $uri = pb_vcs_mod_htftp($uri,"hg");
184 pb_system("$vcscmd clone $uri $destdir","Exporting $uri from Mercurial to $destdir ");
185 }
186} elsif ($scheme =~ /^git/) {
187 if ($scheme =~ /svn/) {
188 if (defined $source) {
189 if (-d $source) {
190 $tmp = $destdir;
191 } else {
192 $tmp = "$destdir/".basename($source);
193 }
194 $source = pb_vcs_mod_htftp($source,"git");
195 pb_system("$vcscmd clone $source $tmp","Exporting $source from $scheme to $tmp ");
196 } else {
197 $uri = pb_vcs_mod_htftp($uri,"git");
198 pb_system("$vcscmd clone $uri $destdir","Exporting $uri from $scheme to $destdir ");
199 }
200 } else {
201 if (defined $source) {
202 if (-d $source) {
203 $tmp = $destdir;
204 } else {
205 $tmp = "$destdir/".basename($source);
206 }
207 $source = pb_vcs_mod_htftp($source,"git");
208 pb_system("cd $source ; $vcscmd archive --format=tar HEAD | (mkdir $tmp && cd $tmp && tar xf -)","Exporting $source/HEAD from GIT to $tmp ");
209 } else {
210 $uri = pb_vcs_mod_htftp($uri,"git");
211 pb_system("$vcscmd clone $uri $destdir","Exporting $uri from GIT to $destdir ");
212 }
213 }
214} elsif ($scheme =~ /^cvs/) {
215 # CVS needs a relative path !
216 my $dir=dirname($destdir);
217 my $base=basename($destdir);
218 if (defined $source) {
219 # CVS also needs a modules name not a dir
220 $tmp1 = basename($source);
221 } else {
222 # Probably not right, should be checked, but that way I'll notice it :-)
223 pb_log(0,"You're in an untested part of project-builder.org, please report any result upstream\n");
224 $tmp1 = $uri;
225 }
226 # If we're working on the CVS itself
227 my $cvstag = basename($ENV{'PBROOTDIR'});
228 my $cvsopt = "";
229 if ($cvstag eq "cvs") {
230 my $pbdate = strftime("%Y-%m-%d %H:%M:%S", @date);
231 $cvsopt = "-D \"$pbdate\"";
232 } else {
233 # we're working on a tag which should be the last part of PBROOTDIR
234 $cvsopt = "-r $cvstag";
235 }
236 pb_system("cd $dir ; $vcscmd -d $account\@$host:$path export $cvsopt -d $base $tmp1","Exporting $tmp1 from $source under CVS to $destdir ");
237} else {
238 confess "cms $scheme unknown";
239}
240return(undef);
241}
242
243=item B<pb_vcs_get_uri>
244
245This function is only called with a real VCS system and gives the URL stored in the checked out directory.
246The first parameter is the schema of the VCS systems (svn, cvs, svn+ssh, ...)
247The second parameter is the directory in which it is locally exposed (result of a checkout).
248
249=cut
250
251sub pb_vcs_get_uri {
252
253my $scheme = shift;
254my $dir = shift;
255
256my $res = "";
257my $void = "";
258my $vcscmd = pb_vcs_cmd($scheme);
259
260if ($scheme =~ /^svn/) {
261 open(PIPE,"LANGUAGE=C $vcscmd info $dir 2> /dev/null |") || return("");
262 while (<PIPE>) {
263 ($void,$res) = split(/^URL:/) if (/^URL:/);
264 }
265 $res =~ s/^\s*//;
266 close(PIPE);
267 chomp($res);
268} elsif ($scheme =~ /^svk/) {
269 open(PIPE,"LANGUAGE=C $vcscmd info $dir 2> /dev/null |") || return("");
270 my $void2 = "";
271 while (<PIPE>) {
272 ($void,$void2,$res) = split(/ /) if (/^Depot/);
273 }
274 $res =~ s/^\s*//;
275 close(PIPE);
276 chomp($res);
277} elsif ($scheme =~ /^hg/) {
278 open(HGRC,".hg/hgrc/") || return("");
279 while (<HGRC>) {
280 ($void,$res) = split(/^default.*=/) if (/^default.*=/);
281 }
282 close(HGRC);
283 chomp($res);
284} elsif ($scheme =~ /^git/) {
285 if ($scheme =~ /svn/) {
286 my $cwd = abs_path();
287 chdir($dir) || return("");;
288 open(PIPE,"LANGUAGE=C $vcscmd info . 2> /dev/null |") || return("");
289 chdir($cwd) || return("");
290 while (<PIPE>) {
291 ($void,$res) = split(/^URL:/) if (/^URL:/);
292 }
293 $res =~ s/^\s*//;
294 close(PIPE);
295 chomp($res);
296 # We've got an SVN ref so add git in front of it for coherency
297 $res = "git+".$res;
298 } else {
299 # Pure git
300 open(GIT,"LANGUAGE=C $vcscmd --git-dir=$dir/.git remote -v 2> /dev/null |") || return("");
301 while (<GIT>) {
302 next unless (/^origin\s+(\S+) \(push\)$/);
303 return $1;
304 }
305 close(GIT);
306 warn "Unable to find remote origin for $dir";
307 return "";
308 }
309} elsif ($scheme =~ /^cvs/) {
310 # This path is always the root path of CVS, but we may be below
311 open(FILE,"$dir/CVS/Root") || confess "$dir isn't CVS controlled";
312 $res = <FILE>;
313 chomp($res);
314 close(FILE);
315 # Find where we are in the tree
316 my $rdir = $dir;
317 while ((! -d "$rdir/CVSROOT") && ($rdir ne "/")) {
318 $rdir = dirname($rdir);
319 }
320 confess "Unable to find a CVSROOT dir in the parents of $dir" if (! -d "$rdir/CVSROOT");
321 #compute our place under that root dir - should be a relative path
322 $dir =~ s|^$rdir||;
323 my $suffix = "";
324 $suffix = "$dir" if ($dir ne "");
325
326 my $prefix = "";
327 if ($scheme =~ /ssh/) {
328 $prefix = "cvs+ssh://";
329 } else {
330 $prefix = "cvs://";
331 }
332 $res = $prefix.$res.$suffix;
333} else {
334 confess "cms $scheme unknown";
335}
336pb_log(1,"pb_vcs_get_uri returns $res\n");
337return($res);
338}
339
340=item B<pb_vcs_copy>
341
342This function copies a VCS content to another.
343The first parameter is the schema of the VCS systems (svn, cvs, svn+ssh, ...)
344The second parameter is the URL of the original VCS content.
345The third parameter is the URL of the destination VCS content.
346
347Only coded for SVN now as used for pbconf itself not the project
348
349=cut
350
351sub pb_vcs_copy {
352my $scheme = shift;
353my $oldurl = shift;
354my $newurl = shift;
355my $vcscmd = pb_vcs_cmd($scheme);
356$oldurl = pb_vcs_mod_socks($oldurl);
357$newurl = pb_vcs_mod_socks($newurl);
358
359if ($scheme =~ /^svn/) {
360 $oldurl = pb_vcs_mod_htftp($oldurl,"svn");
361 $newurl = pb_vcs_mod_htftp($newurl,"svn");
362 pb_system("$vcscmd copy -m \"Creation of $newurl from $oldurl\" $oldurl $newurl","Copying $oldurl to $newurl ");
363} elsif ($scheme =~ /^(flat)|(ftp)|(http)|(file)\b/o) {
364 # Nothing to do.
365} else {
366 confess "cms $scheme unknown for project management";
367}
368}
369
370=item B<pb_vcs_checkout>
371
372This function checks a VCS content out to a directory.
373The first parameter is the schema of the VCS systems (svn, cvs, svn+ssh, ...)
374The second parameter is the URL of the VCS content.
375The third parameter is the directory where we want to deliver it (result of export).
376
377=cut
378
379sub pb_vcs_checkout {
380my $scheme = shift;
381my $url = shift;
382my $destination = shift;
383my $vcscmd = pb_vcs_cmd($scheme);
384$url = pb_vcs_mod_socks($url);
385
386if ($scheme =~ /^svn/) {
387 $url = pb_vcs_mod_htftp($url,"svn");
388 pb_system("$vcscmd co $url $destination","Checking out $url to $destination ");
389} elsif ($scheme =~ /^svk/) {
390 $url = pb_vcs_mod_htftp($url,"svk");
391 pb_system("$vcscmd co $url $destination","Checking out $url to $destination ");
392} elsif ($scheme =~ /^hg/) {
393 $url = pb_vcs_mod_htftp($url,"hg");
394 pb_system("$vcscmd clone $url $destination","Checking out $url to $destination ");
395} elsif ($scheme =~ /^git/) {
396 $url = pb_vcs_mod_htftp($url,"git");
397 pb_system("$vcscmd clone $url $destination","Checking out $url to $destination ");
398} elsif (($scheme eq "ftp") || ($scheme eq "http")) {
399 return;
400} elsif ($scheme =~ /^cvs/) {
401 my ($scheme, $account, $host, $port, $path) = pb_get_uri($url);
402
403 # If we're working on the CVS itself
404 my $cvstag = basename($ENV{'PBROOTDIR'});
405 my $cvsopt = "";
406 if ($cvstag eq "cvs") {
407 my @date = pb_get_date();
408 my $pbdate = strftime("%Y-%m-%d %H:%M:%S", @date);
409 $cvsopt = "-D \"$pbdate\"";
410 } else {
411 # we're working on a tag which should be the last part of PBROOTDIR
412 $cvsopt = "-r $cvstag";
413 }
414 pb_mkdir_p("$destination");
415 pb_system("cd $destination ; $vcscmd -d $account\@$host:$path co $cvsopt .","Checking out $url to $destination ");
416} elsif ($scheme =~ /^file/) {
417 pb_vcs_export($url,undef,$destination);
418} else {
419 confess "cms $scheme unknown";
420}
421}
422
423=item B<pb_vcs_up>
424
425This function updates a local directory with the VCS content.
426The first parameter is the schema of the VCS systems (svn, cvs, svn+ssh, ...)
427The second parameter is the list of directory to update.
428
429=cut
430
431sub pb_vcs_up {
432my $scheme = shift;
433my @dir = @_;
434my $vcscmd = pb_vcs_cmd($scheme);
435
436if ($scheme =~ /^((svn)|(cvs)|(svk))/o) {
437 pb_system("$vcscmd up ".join(' ',@dir),"Updating ".join(' ',@dir));
438} elsif ($scheme =~ /^((hg)|(git))/o) {
439 foreach my $d (@dir) {
440 pb_system("(cd $d && $vcscmd pull)", "Updating $d ");
441 }
442} elsif ($scheme =~ /^(flat)|(ftp)|(http)|(file)\b/o) {
443 # Nothing to do.
444} else {
445 confess "cms $scheme unknown";
446}
447}
448
449=item B<pb_vcs_checkin>
450
451This function updates a VCS content from a local directory.
452The first parameter is the schema of the VCS systems (svn, cvs, svn+ssh, ...)
453The second parameter is the directory to update from.
454The third parameter is the comment to pass during the commit
455
456=cut
457
458sub pb_vcs_checkin {
459my $scheme = shift;
460my $dir = shift;
461my $msg = shift;
462my $vcscmd = pb_vcs_cmd($scheme);
463
464if ($scheme =~ /^((svn)|(cvs)|(svk))/o) {
465 pb_system("cd $dir && $vcscmd ci -m \"$msg\" .","Checking in $dir ");
466} elsif ($scheme =~ /^git/) {
467 pb_system("cd $dir && $vcscmd commit -a -m \"$msg\"", "Checking in $dir ");
468} elsif ($scheme =~ /^(flat)|(ftp)|(http)|(file)\b/o) {
469 # Nothing to do.
470} else {
471 confess "cms $scheme unknown";
472}
473pb_vcs_up($scheme,$dir);
474}
475
476=item B<pb_vcs_add_if_not_in>
477
478This function adds to a VCS content from a local directory if the content wasn't already managed under th VCS.
479The first parameter is the schema of the VCS systems (svn, cvs, svn+ssh, ...)
480The second parameter is a list of directory/file to add.
481
482=cut
483
484sub pb_vcs_add_if_not_in {
485my $scheme = shift;
486my @f = @_;
487my $vcscmd = pb_vcs_cmd($scheme);
488
489if ($scheme =~ /^((hg)|(git)|(svn)|(svk)|(cvs))/o) {
490 for my $f (@f) {
491 my $uri = pb_vcs_get_uri($scheme,$f);
492 pb_vcs_add($scheme,$f) if ($uri !~ /^$scheme/);
493 }
494} elsif ($scheme =~ /^(flat)|(ftp)|(http)|(file)\b/o) {
495 # Nothing to do.
496} else {
497 confess "cms $scheme unknown";
498}
499}
500
501=item B<pb_vcs_add>
502
503This function adds to a VCS content from a local directory.
504The first parameter is the schema of the VCS systems (svn, cvs, svn+ssh, ...)
505The second parameter is a list of directory/file to add.
506
507=cut
508
509sub pb_vcs_add {
510my $scheme = shift;
511my @f = @_;
512my $vcscmd = pb_vcs_cmd($scheme);
513
514if ($scheme =~ /^((hg)|(git)|(svn)|(svk)|(cvs))/o) {
515 pb_system("$vcscmd add ".join(' ',@f),"Adding ".join(' ',@f)." to VCS ");
516} elsif ($scheme =~ /^(flat)|(ftp)|(http)|(file)\b/o) {
517 # Nothing to do.
518} else {
519 confess "cms $scheme unknown";
520}
521pb_vcs_up($scheme,@f);
522}
523
524=item B<pb_vcs_isdiff>
525
526This function returns a integer indicating the number of differences between the VCS content and the local directory where it's checked out.
527The first parameter is the schema of the VCS systems (svn, cvs, svn+ssh, ...)
528The second parameter is the directory to consider.
529
530=cut
531
532sub pb_vcs_isdiff {
533my $scheme = shift;
534my $dir =shift;
535my $vcscmd = pb_vcs_cmd($scheme);
536my $l = undef;
537
538if ($scheme =~ /^((svn)|(cvs)|(svk))/o) {
539 open(PIPE,"$vcscmd diff $dir |") || confess "Unable to get $vcscmd diff from $dir";
540 $l = 0;
541 while (<PIPE>) {
542 # Skipping normal messages in case of CVS
543 next if (/^cvs diff:/);
544 $l++;
545 }
546} elsif ($scheme =~ /^(flat)|(ftp)|(http)|(file)\b/o) {
547 $l = 0;
548} else {
549 confess "cms $scheme unknown";
550}
551pb_log(1,"pb_vcs_isdiff returns $l\n");
552return($l);
553}
554
555sub pb_vcs_mod_htftp {
556
557my $url = shift;
558my $proto = shift;
559
560$url =~ s/^$proto\+((ht|f)tp[s]*):/$1:/;
561pb_log(1,"pb_vcs_mod_htftp returns $url\n");
562return($url);
563}
564
565sub pb_vcs_mod_socks {
566
567my $url = shift;
568
569$url =~ s/^([A-z0-9]+)\+(socks):/$1:/;
570pb_log(1,"pb_vcs_mod_socks returns $url\n");
571return($url);
572}
573
574
575sub pb_vcs_cmd {
576
577my $scheme = shift;
578my $cmd = "";
579my $cmdopt = "";
580
581# If there is a socks proxy to use
582if ($scheme =~ /socks/) {
583 # Get the socks proxy command from the conf file
584 my ($pbsockscmd) = pb_conf_get("pbsockscmd");
585 $cmd = "$pbsockscmd->{$ENV{'PBPROJ'}} ";
586}
587
588if (defined $ENV{'PBVCSOPT'}) {
589 $cmdopt .= " $ENV{'PBVCSOPT'}";
590}
591
592if ($scheme =~ /hg/) {
593 $cmd .= "hg".$cmdopt;
594} elsif ($scheme =~ /git/) {
595 if ($scheme =~ /svn/) {
596 $cmd .= "git svn".$cmdopt;
597 } else {
598 $cmd .= "git".$cmdopt;
599 }
600} elsif ($scheme =~ /svn/) {
601 $cmd .= "svn".$cmdopt;
602} elsif ($scheme =~ /svk/) {
603 $cmd .= "svk".$cmdopt;
604} elsif ($scheme =~ /cvs/) {
605 $cmd .= "cvs".$cmdopt;
606} elsif (($scheme =~ /http/) || ($scheme =~ /ftp/)) {
607 my $command = pb_check_req("wget",1);
608 if (-x $command) {
609 $cmd .= "$command -nv -O ";
610 } else {
611 $command = pb_check_req("curl",1);
612 if (-x $command) {
613 $cmd .= "$command -o ";
614 } else {
615 confess "Unable to handle $scheme.\nNo wget/curl available, please install one of those";
616 }
617 }
618} else {
619 $cmd = "";
620}
621pb_log(3,"pb_vcs_cmd returns $cmd\n");
622return($cmd);
623}
624
625
626
627=back
628
629=head1 WEB SITES
630
631The main Web site of the project is available at L<http://www.project-builder.org/>. Bug reports should be filled using the trac instance of the project at L<http://trac.project-builder.org/>.
632
633=head1 USER MAILING LIST
634
635None exists for the moment.
636
637=head1 AUTHORS
638
639The Project-Builder.org team L<http://trac.project-builder.org/> lead by Bruno Cornec L<mailto:bruno@project-builder.org>.
640
641=head1 COPYRIGHT
642
643Project-Builder.org is distributed under the GPL v2.0 license
644described in the file C<COPYING> included with the distribution.
645
646=cut
647
6481;
Note: See TracBrowser for help on using the repository browser.