Subversion Repositories DevTools

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
267 dpurdie 1
########################################################################
2
# Copyright (C) 2008 ERG Limited, All rights reserved
3
#
4
# Module name   : jats.sh
5
# Module type   : Makefile system
6
# Compiler(s)   : n/a
7
# Environment(s): jats
8
#
9
# Description   : JATS Subversion Interface Functions
10
#
11
#                 Requires a subversion client to be present on the machine
12
#                 Does require at least SubVersion 1.5
13
#                 Uses features not available in 1.4
14
#
15
#                 The package currently implements a set of functions
16
#                 There are some intentional limitations:
17
#                   1) Non recursive
18
#                   2) Errors terminate operation
19
#
20
#                 This package contains experimental argument passing
21
#                 processes. Sometimes use a hash of arguments
22
#
23
#......................................................................#
24
 
25
require 5.008_002;
26
use strict;
27
use warnings;
28
our $GBE_SVN_URL;
29
use JatsEnv;
30
 
31
package JatsSvn;
32
 
33
use JatsError;
34
use JatsSystem;
35
use JatsSvnCore qw(:All);
36
 
37
use File::Path;             # Instead of FileUtils
38
use File::Basename;
39
use Cwd;
40
 
41
 
42
# automatically export what we need into namespace of caller.
43
use Exporter();
44
our (@ISA, @EXPORT, %EXPORT_TAGS, @EXPORT_OK);
45
@ISA         = qw(Exporter JatsSvnCore);
46
 
47
@EXPORT      = qw(
48
                    NewSession
49
                    NewSessionByWS
50
                    NewSessionByUrl
51
 
52
                    SvnRmView
53
                    SvnIsaSimpleLabel
54
                    SvnComment
55
 
56
                    SvnUserCmd
57
                );
58
@EXPORT_OK =  qw(
59
                );
60
 
61
%EXPORT_TAGS = (All => [@EXPORT, @EXPORT_OK]);
62
 
63
#
64
#   Global Variables
65
#
66
 
67
#-------------------------------------------------------------------------------
68
# Function        : BEGIN
69
#
70
# Description     : Module Initialization
71
#                   Invoked by Perl as soon as possible
72
#                       Setup environment variables
73
#                       Calculate globals
74
#
75
# Inputs          : None
76
#
77
# Returns         : Nothing
78
#
79
sub BEGIN
80
{
81
    #
82
    #   Need to have a configured Repository
83
    #
84
    ::EnvImport ('GBE_SVN_URL' );
85
 
86
    #
87
    #   Remove trailing /s
88
    #
89
    $::GBE_SVN_URL =~ s~/*$~~;
90
 
91
    #
92
    #   Ensure that it is in valid format
93
    #   Three forms are supported
94
    #
95
    if ( $::GBE_SVN_URL =~ m{^svn://[^/]+$} ) {
96
        #
97
        #   Type is SVN server
98
        #   Protocol + server name
99
        #
100
    } elsif ( $::GBE_SVN_URL =~ m{^http://.+} ) {
101
        #
102
        #   Type is HTTP server
103
        #   Protocol + server name + path on server
104
        #
105
    } elsif ( $::GBE_SVN_URL =~ m{^file:///+[A-Z]:/} ) {
106
        #
107
        #   Type is local Repo (file)
108
        #   Windows absolute pathname
109
        #   file:///I:/path/...
110
        #
111
    } elsif ( $::GBE_SVN_URL =~ m{^file:///+[^/]} ) {
112
        #
113
        #   Type is local Repo (file)
114
        #   Unix absolute pathname
115
        #   file:///path/...
116
        #
117
    } else {
118
        Error ("GBE_SVN_URL format not understood","GBE_SVN_URL: $::GBE_SVN_URL");
119
    }
120
 
121
    #
122
    #   Add a trailing '/'
123
    #   This will make it easier to use
124
    #
125
    $::GBE_SVN_URL .= '/';
126
 
127
}
128
 
129
#-------------------------------------------------------------------------------
130
# Function        : SvnCo
131
#
132
# Description     : Create a workspace
133
#                   Can be used to extract files, without creating the
134
#                   subversion control files.
135
#
136
# Inputs          : $self               - Instance data
137
#                   $RepoPath           - Within the repository
138
#                   $Path               - Local path
139
#                   Options             - Options
140
#                           --Export    - Export Only
141
#
142
# Returns         : Nothing
143
#
144
sub SvnCo
145
{
146
    my ($self, $RepoPath, $path, @opts) = @_;
147
    my $export = grep (/^--Export/, @opts );
148
    Debug ("SvnCo");
149
 
150
    #
151
    #   Ensure that the output path does not exist
152
    #   Do not allow the user to create a local work space
153
    #   where one already exists
154
    #
155
    Error ("SvnCo: No PATH specified" ) unless ( $path );
156
    Error ("SvnCo: Target path already exists", "Path: " . $path ) if ( -e $path );
157
 
158
    #
159
    #   Build up the command line
160
    #
161
    my @args = $export ? 'export' : 'checkout';
162
    push @args, qw( --ignore-externals );
163
    push @args, $RepoPath, $path;
164
 
165
    my @co_list;
166
    if ( $self->SvnCmd ( @args,
167
                            {
168
                                'process' => \&ProcessCo,
169
                                'data' => \@co_list,
170
                                'credentials' => 1,
171
                                'nosavedata' => 1,
172
                            }
173
                       ) || @co_list )
174
    {
175
        #
176
        #   We have a checkout limitation
177
        #   Delete the workspace and then report the error
178
        #
179
        Verbose2 ("Remove WorkSpace: $path");
180
        rmtree( $path, IsVerbose(3) );
181
        Error ("Checking out Workspace", @{$self->{ERROR_LIST}}, @co_list );
182
    }
183
    return;
184
 
185
    #
186
    #   Internal routine to scan each the checkout
187
    #
188
    #   Due to the structure of a SubVersion repository it would be
189
    #   possible for a user to extract the entire repository. This is
190
    #   not good as the repo could be very very large
191
    #
192
    #   Assume that the structure of the repo is such that our
193
    #   user is not allowed to extract a directory tree that contains
194
    #   key paths - such as /tags/ as this would indicate that they are
195
    #   attempting to extract something that is not a package
196
    #
197
    #
198
    sub ProcessCo
199
    {
200
        my $self = shift;
201
        if (  m~((/)(tags|branches|trunk)(/|$))~ )
202
        {
203
            my $bad_dir = $1;
204
            push @{$self->{ERROR_LIST}}, "Checkout does not describe the root of a package. Contains: $bad_dir";
205
            return 1;
206
        }
207
 
208
        ##
209
        ##   Limit the size of the WorkSpace
210
        ##   This limit is a bit artificial, but does attempt to
211
        ##   limit views that encompass too much.
212
        ##
213
        #if ( $#{$self->{RESULT_LIST}} > 100 )
214
        #{
215
        #    Warning ("View is too large - DEBUG USE ONLY. WILL BE REMOVED" );
216
        #    push @{$self->{ERROR_LIST}}, "View too large";
217
        #    return 1;
218
        #}
219
    }
220
}
221
 
222
#-------------------------------------------------------------------------------
223
# Function        : SvnCi
224
#
225
# Description     : Check in the specified WorkSpace
226
#
227
# Inputs          : $self           - Instance data
228
#                   A hash of named arguments
229
#                       comment     - Commit comment
230
#
231
# Returns         : Tag of the checkin
232
#
233
sub SvnCi
234
{
235
    my $self = shift;
236
    my %opt = @_;
237
    my $status_url;
238
 
239
    Debug ("SvnCi");
240
    Error ("SvnCi: Odd number of args") unless ((@_ % 2) == 0);
241
 
242
    #
243
    #   Validate the source path
244
    #   Note: populates @{$self->{RESULT_LIST}} with 'info' commands
245
    #
246
    my $path = SvnValidateWs ($self, 'SvnCi');
247
 
248
    #
249
    #   Scan the @{$self->{RESULT_LIST}}, which has the results of an 'info'
250
    #   command the locate the URL.
251
    #
252
    #   This contains the target view space
253
    #   Sanity test. Don't allow Checkin to a /tags/ area
254
    #
255
    foreach ( @{$self->{RESULT_LIST}})
256
    {
257
        if ( m~^URL:\s+(.+)~ )
258
        {
259
            $status_url = $1;
260
            last;
261
        }
262
    }
263
 
264
    Error ("SvnCi: Cannot determine Repositoty URL")
265
        unless ( $status_url );
266
 
267
    Error ("SvnCi: Not allowed to commit to a 'tags' area", "URL: $status_url")
268
        if ( $status_url =~ m~/tags(/|$)~ );
269
 
270
    #
271
    #   Commit
272
    #
273
    $self->SvnCmd ( 'commit', $path
274
                    , '-m', SvnComment( $opt{'comment'}, 'Created by SvnCi' ),
275
                    , { 'credentials' => 1,
276
                        'process' => \&ProcessRevNo,
277
                        'error' => "SvnCi: Copy Error" }
278
                        );
279
    Error ("SvnCi: Cannot determine Revision Number", @{$self->{RESULT_LIST}})
280
        unless ( $self->{REVNO} );
281
 
282
    #
283
    #   Update the view
284
    #   Doing this so that the local view contains an up to date
285
    #   revision number. If not done, and a 'copy' is done based
286
    #   on this view then the branch information will indicate that
287
    #   the copy is based on an incorrect version.
288
    #   This can be confusing!
289
    #
290
    $self->SvnCmd ( 'update'   , $path
291
                        , '--ignore-externals'
292
                        , { 'credentials' => 1,
293
                            'error' => "SvnCi: Updating WorkSpace" }
294
                        );
295
    #
296
    #   Pass the updated revision number back to the user
297
    #
298
    $self->CalcRmReference($status_url);
299
    Message ("Tag is: " . $self->{RMREF} );
300
    return $self->{RMREF} ;
301
}
302
 
303
#-------------------------------------------------------------------------------
304
# Function        : SvnCreatePackage
305
#
306
# Description     : Create a package and any associated files
307
#
308
# Inputs          : $self        - Instance data
309
#                   A hash of named arguments
310
#                       package  - Name of the package
311
#                                  May include subdirs
312
#                       new      - True: Must not already exist
313
#                       replace  - True: Replace targets
314
#                       import   - DirTree to import
315
#                       label    - Tag for imported DirTree
316
#                       type     - Import TTB target
317
#
318
# Returns         : Revision of the copy
319
#
320
sub SvnCreatePackage
321
{
322
    my $self = shift;
323
    my %opt = @_;
324
    my $target;
325
 
326
    Debug ("SvnCreatePackage", @_);
327
    Error ("Odd number of args to SvnCreatePackage") unless ((@_ % 2) == 0);
328
    my %dirs = ( 'trunk/'       => 0,
329
                 'tags/'        => 0,
330
                 'branches/'    => 0 );
331
 
332
    #
333
    #   Sanity Tests
334
    #
335
    my $package = $self->Full || Error ("SvnCreatePackage: No package name provided");
336
    Error ("SvnCreatePackage: Invalid import path") if ( $opt{'import'} && ! -d $opt{'import'} );
337
    Error ("SvnCreatePackage: Tag without Import") if ( $opt{'label'} && ! $opt{'import'} );
338
    $opt{'label'} = SvnIsaSimpleLabel( $opt{'label'} ) if (  $opt{'label'} );
339
 
340
    #
341
    #   Package path cannot contain any of the keyword paths tags,trunk,branches
342
    #   as this would place a package with a package
343
    #
344
    Error ("Package path contains a reserved word ($1)", "Path: $package")
345
        if (  $package =~ m~/(tags|branches|trunk)(/|$)~ );
346
 
347
    #
348
    #   Package path cannot be pegged, or look like one
349
    #
350
    Error ("Package name contains a Peg ($1)", "Path: $package")
351
        if ( $package =~ m~.*(@\d+)$~ );
352
 
353
    #
354
    #   Determine TTB target
355
    #
356
    $opt{'type'} = 'trunk' unless ( $opt{'type'} );
357
    Error ("Invalid TTB type") unless ( $opt{'type'} =~ m{^(tags|branches|trunk)$} );
358
    Error ("Import without label") if ( $opt{'type'} ne  'trunk'  && ! $opt{'label'} );
359
 
360
    #
361
    #   Before we import data we must ensure that the targets do not exist
362
    #   Determine the import target(s)
363
    #
364
    my $import_target;
365
    my $copy_target;
366
 
367
    if ( $opt{'import'} )
368
    {
369
        #
370
        #   Primary target
371
        #   trunk, branck or tag
372
        #
373
        $import_target = $package . '/' . $opt{'type'};
374
        $import_target .= '/' .$opt{'label'} if ( $opt{'type'} ne 'trunk');
375
 
376
        $self->SvnValidateTarget( 'target'    => $import_target,
377
                                  'delete'    => $opt{'replace'},
378
                                  'available' => 1 );
379
 
380
        #
381
        #   Secondary target
382
        #   If primary is a trunk and a label is provided
383
        #
384
        if ( $opt{'type'} eq 'trunk' && $opt{'label'} )
385
        {
386
            $copy_target = $package . '/tags/' . $opt{'label'};
387
            $self->SvnValidateTarget( 'target'    => $copy_target,
388
                                      'delete'    => $opt{'replace'},
389
                                      'available' => 1 );
390
        }
391
    }
392
 
393
    #
394
    #   Probe to see if the package exists
395
    #
396
    my ( $ref_files, $ref_dirs, $ref_svn, $found ) = $self->SvnScanPath ( 'SvnCreatePackage', $package );
397
    if ( @$ref_dirs )
398
    {
399
        Error ("SvnCreatePackage: Package directory exists",
400
               "Cannot create a package here. Unexpected subdirectories:", @$ref_dirs);
401
    }
402
 
403
    if ( @$ref_files )
404
    {
405
        Warning ("SvnCreatePackage: Unexpected files found",
406
               "Path: $package",
407
               "Unexpected files found: @$ref_files");
408
    }
409
 
410
    if ( @$ref_svn )
411
    {
412
        #
413
        #   Do we need a new one
414
        #
415
        Error ("SvnCreatePackage: Package exists: $package") if $opt{'new'};
416
 
417
        #
418
        #   Some subversion files have been found here
419
        #   Create the rest
420
        #   Assume that the directory tree is good
421
        #
422
        #
423
        #   Some, or all, of the required package subdirectories exist
424
        #   Determine the new ones to created so that it can be done
425
        #   in an atomic step
426
        #
427
        delete $dirs{$_} foreach  ( @$ref_svn );
428
        if ( keys %dirs )
429
        {
430
            Warning ("SvnCreatePackage: Not all package subdirs present",
431
                     "Remaining dirs will be created",
432
                     "Found: @$ref_svn") if @$ref_svn;
433
        }
434
        else
435
        {
436
            Warning ("SvnCreatePackage: Package already present");
437
        }
438
    }
439
 
440
    #
441
    #   Create package directories that have not been discovered
442
    #       trunk
443
    #       branches
444
    #       tags
445
    #
446
    my @dirs;
447
    push @dirs, $package . '/' . $_ foreach ( keys %dirs );
448
    $target = $package . '/trunk';
449
 
450
    #
451
    #   Create missing directories - if any
452
    #
453
    if ( @dirs )
454
    {
455
        $self->SvnCmd ('mkdir', @dirs
456
                       , '-m', 'Created by SvnCreatePackage'
457
                       , '--parents'
458
                       , { 'credentials' => 1,
459
                           'error' => "SvnCreatePackage" } );
460
    }
461
 
462
    #
463
    #   Import data into the package if required
464
    #   Import data. Possible cases:
465
    #       - Import to trunk - and then tag it
466
    #       - Import to branches
467
    #       - Import to tags
468
    #
469
    if ( $import_target )
470
    {
471
        Verbose ("Importing directory into new package: $opt{'import'}");
472
 
473
        $target = $import_target;
474
        $self->{PRINTDATA} = 1;
475
        $self->SvnCmd ('import', $opt{'import'}
476
                        , $target
477
                        , '-m', 'Import by SvnCreatePackage'
478
                        , '--force'
479
                        , { 'credentials' => 1
480
                           ,'error' => "Import Incomplete"
481
                           ,'process' => \&ProcessRevNo
482
                          })
483
    }
484
 
485
    #
486
    #   If imported to the trunk AND a label is provided
487
    #   then tag the import as well.
488
    #   A simple URL copy
489
    #
490
    if ( $copy_target )
491
    {
492
        Verbose ("Labeling imported trunk: $opt{'label'} ");
493
        $target = $copy_target;
494
        $self->SvnCmd ('copy'  , $import_target
495
                        , $target
496
                        , '-m', 'Import tagged by SvnCreatePackage'
497
                        , { 'credentials' => 1
498
                          , 'process' => \&ProcessRevNo
499
                          , 'error' => "Import Incomplete" } );
500
    }
501
 
502
    #
503
    #   Pass the updated revision number back to the user
504
    #
505
    $self->CalcRmReference($target);
506
    Message ("Tag is: " . $self->{RMREF} );
507
    return $self->{RMREF} ;
508
}
509
 
510
#-------------------------------------------------------------------------------
511
# Function        : SvnRmView
512
#
513
# Description     : Remove a Subversion view
514
#                   Will run sanity checks and only remove the view if
515
#                   all is well
516
#
517
# Inputs          : A hash of named arguments
518
#                       path     - Path to local workspace
519
#                       modified - Array of files that are allowed to be modified
520
#                       force    - True: Force deletion
521
#
522
# Returns         :
523
#
524
sub SvnRmView
525
{
526
    my %opt = @_;
527
    Debug ("SvnRmView");
528
    Error ("Odd number of args to SvnRmView") unless ((@_ % 2) == 0);
529
 
530
    #
531
    #   Sanity test
532
    #
533
    my $path = $opt{'path'} || '';
534
    my $path_length = length ($path);
535
    Verbose2 ("Delete WorkSpace: $path");
536
 
537
    #
538
    #   If the path does not exist then assume that its already deleted
539
    #
540
    unless ( $path && -e $path )
541
    {
542
        Verbose2 ("SvnRmView: Path does not exist");
543
        return;
544
    }
545
 
546
    #
547
    #   Create information about the workspace
548
    #   This will also validate the path
549
    #
550
    my $session = NewSessionByWS ( $path );
551
 
552
    #
553
    #   Validate the path
554
    #
555
    $session->SvnValidateWs ($path, 'SvnRmView');
556
 
557
    #
558
    #   Ask subversion if there are any files to be updated
559
    #   Prevent deletion of a view that has modified files
560
    #
561
    unless ( $opt{'force'} )
562
    {
563
        $session->SvnWsModified ( 'cmd' => 'SvnRmView', %opt );
564
    }
565
 
566
    #
567
    #   Now we can delete it
568
    #
569
    Verbose2 ("Remove WorkSpace: $path");
570
    rmtree( $path, IsVerbose(3) );
571
}
572
 
573
 
574
#-------------------------------------------------------------------------------
575
# Function        : SvnCopyWs
576
#
577
# Description     : Copy a workspace to a new place in the repository
578
#                   Effectively 'label' the workspace
579
#
580
#                   It would appear that the 'copy' command is very clever
581
#                   If a version-controlled file has been changed
582
#                   in the source workspace, then it will automatically be
583
#                   copied. This is a trap.
584
#
585
#                   Only allow a 'copy' if there are no modified
586
#                   files in the work space (unless overridden)
587
#
588
#
589
# Inputs          : $self        - Instance data
590
#                   A hash of named arguments
591
#                       path     - Path to local workspace
592
#                       target   - Location within the repository to copy to
593
#                       comment  - Commit comment
594
#                       modified - Array of files that are allowed to
595
#                                  be modified in the workspace.
596
#                       noswitch  - True: Don't switch to the new URL
597
#                       replace   - True: Delete existing tag if present
598
#
599
# Returns         : Revision of the copy
600
#
601
sub SvnCopyWs
602
{
603
    my $self = shift;
604
    my %opt = @_;
605
    Debug ("SvnCopyWs");
606
    Error ("Odd number of args to SvnCopyWs") unless ((@_ % 2) == 0);
607
    Error ("SvnCopyWs: No Workspace" ) unless ( $self->{WS} );
608
 
609
    #
610
    #   Insert defaults
611
    #
612
    my $target = $opt{target} || Error ("SvnCopyWs: Target not specified" );
613
 
614
    #
615
    #   Validate the source path
616
    #
617
    my $path = SvnValidateWs ($self, 'SvnCopyWs');
618
 
619
    #
620
    #   Validate the target
621
    #   Cannot have a 'peg'
622
    #
623
    Error ("SvnCopyWs: Target contains a Peg: ($1)", $target)
624
        if ( $target =~ m~(@\d+)\s*$~ );
625
 
626
    #
627
    #   Examine the workspace and ensure that there are no modified
628
    #   files - unless they are expected
629
    #
630
    $self->SvnWsModified ( 'cmd' => 'SvnCopyWs', %opt );
631
 
632
    #
633
    #   Validate the repository
634
    #   Ensure that the target does not exist
635
    #   The target may be deleted if it exists and allowed by the user
636
    #
637
    $self->SvnValidateTarget ( 'cmd'    => 'SvnCopyWs',
638
                        'target' => $target,
639
                        'delete' => $opt{replace},
640
                        'comment' => 'Deleted by SvnCopyWs'
641
                        );
642
    #
643
    #   Copy source to destination
644
    #   It would appear that even though the source is a WorkSpace, the copy
645
    #   does not transfer data from the WorkSpace. It appears as though its all
646
    #   done on the server. This is good - and fast.
647
    #
648
    #   More over files that are modified in the local workspace will
649
    #   be copied and checked into the target.
650
    #
651
    if ( $self->SvnCmd ( 'cp'  , $path
652
                        , $target
653
                        , '--parents'
654
                        , '-m', SvnComment( $opt{'comment'}, 'Created by SvnCopyWs' ),
655
                        , { 'process' => \&ProcessRevNo,
656
                            'credentials' => 1, }
657
                        ) )
658
    {
659
        #
660
        #   Error in copy
661
        #   Attempt to delete the target. Don't worry if we can't do that
662
        #
663
        my @err1 = @{$self->{ERROR_LIST}};
664
        $self->SvnCmd ( 'delete'
665
                    , $target
666
                    , '-m', 'Deleted by SvnCopyWs after creation failure'
667
                    , { 'credentials' => 1, }
668
               );
669
        Error ("SvnCopyWs: Copy Error", @err1);
670
    }
671
 
672
    Error ("SvnCopyWs: Cannot determine Revision Number", @{$self->{RESULT_LIST}})
673
        unless ( $self->{REVNO} );
674
 
675
    Verbose2 ("Copy committed as revision: " . $self->{REVNO} );
676
 
677
    unless ( $opt{'noswitch'} )
678
    {
679
        #
680
        #   Switch to the new URL
681
        #   This will link the Workspace with the copy that we have just made
682
        #
683
        $self->SvnCmd ( 'switch', $target
684
                         , $path
685
                         , { 'credentials' => 1,
686
                             'error' => "SvnCopyWs: Cannot switch to new URL" }
687
               );
688
    }
689
 
690
    #
691
    #   Pass the updated revision number back to the user
692
    #
693
    $self->CalcRmReference($target );
694
    Message ("Tag is: " . $self->{RMREF} );
695
    return $self->{RMREF} ;
696
}
697
 
698
#-------------------------------------------------------------------------------
699
# Function        : SvnWsModified
700
#
701
# Description     : Test a Workspace for modified files
702
#                   Allow some files to be modified
703
#
704
# Inputs          : $self           - Instance data
705
#                   A hash of named arguments
706
#                       path        - Path to local workspace
707
#                       modified    - Files that are allowed to be modified
708
#                                     Relative to the 'path'
709
#                                     May be a single file or an array of files
710
#                       cmd         - Command name for error reporting
711
#
712
# Returns         :
713
#
714
sub SvnWsModified
715
{
716
    my $self = shift;
717
    my %opt = @_;
718
    Debug ("SvnWsModified");
719
    Error ("Odd number of args to SvnWsModified") unless ((@_ % 2) == 0);
720
 
721
    my $cmd = $opt{'cmd'} || 'SvnWsModified';
722
 
723
    #
724
    #   Validate the path
725
    #
726
    SvnValidateWs ($self, $cmd);
727
    my $path = $self->{WS};
728
    my $path_length = length ($path);
729
    Verbose2 ("Test Workspace for Modifications: $path");
730
 
731
    #
732
    #   Ask subversion if there are any files to be updated
733
    #
734
    $self->SvnCmd ('status', $path, {'error' => "Svn status command error"} );
735
 
736
    #
737
    #   Examine the list of modified files
738
    #
739
    if ( @{$self->{RESULT_LIST}} )
740
    {
741
        #
742
        #   Create a hash of file that are allowed to change
743
        #   These are files relative to the base of the view
744
        #
745
        #   The svn command has the 'path' prepended, so this
746
        #   will be removed as we process the commands
747
        #
748
        my %allowed;
749
        my @unexpected;
750
 
751
        if ( exists $opt{'modified'}  )
752
        {
753
            $allowed{'/' . $_} = 1 foreach ( ref ($opt{'modified'}) ? @{$opt{'modified'}} : $opt{'modified'}  );
754
        }
755
 
756
        #
757
        #   Process the list of modified files
758
        #   Do this even if we aren't allowed modified files as we
759
        #   still need to examine the status and kill off junk entries
760
        #   ie: ?, I, ! and ~
761
        #
762
        #    First column: Says if item was added, deleted, or otherwise changed
763
        #      ' ' no modifications
764
        #      'A' Added
765
        #      'C' Conflicted
766
        #      'D' Deleted
767
        #      'I' Ignored
768
        #      'M' Modified
769
        #      'R' Replaced
770
        #      'X' item is unversioned, but is used by an externals definition
771
        #      '?' item is not under version control
772
        #      '!' item is missing (removed by non-svn command) or incomplete
773
        #      '~' versioned item obstructed by some item of a different kind
774
        #
775
        foreach my $entry ( @{$self->{RESULT_LIST}} )
776
        {
777
            #
778
            #   Extract filename from line
779
            #       First 7 chars are status
780
            #       Remove WS path too
781
            #
782
            my $file = substr ( $entry, 7 + $path_length );
783
            next if ( $allowed{$file} );
784
 
785
            #
786
            #   Examine the first char and rule out funny things
787
            #
788
            my $f1 =  substr ($entry, 0,1 );
789
            next if ( $f1 =~ m{[?I!~]} );
790
            push @unexpected, $entry;
791
        }
792
        Error ("Workspace contains unexpected modified files", @unexpected)
793
            if ( @unexpected );
794
    }
795
}
796
 
797
#-------------------------------------------------------------------------------
798
# Function        : SvnListPackages
799
#
800
# Description     : Determine a list of packages within the repo
801
#                   This turns out to be a very slow process
802
#                   so don't use it unless you really really need to
803
#
804
# Inputs          : $repo       - Name of the repository
805
#
806
# Returns         : 
807
#
808
sub SvnListPackages
809
{
810
    my ($repo) = @_;
811
 
812
    my @path_list = $repo;
813
    my @list;
814
    my $scanned = 0;
815
    Debug ("SvnListPackages");
816
    while ( @path_list )
817
    {
818
        my $path = shift @path_list;
819
        $scanned++;
820
print "Reading: $path\n";
821
        my ( $ref_files, $ref_dirs, $ref_svn, $found ) = SvnScanPath ( 'Listing Packages', $path );
822
 
823
        #
824
        #   If there are Subversion dirs (ttb) in this directory
825
        #   then this is a package. Add to list
826
        #
827
        push @list, $path if ( @$ref_svn );
828
 
829
        #
830
        #   Add subdirs to the list of paths to explore
831
        #
832
        foreach  ( @$ref_dirs )
833
        {
834
            chop;                                 # Remove trailing '/'
835
            push @path_list, $path . '/' . $_;    # Extend the path
836
        }
837
    }
838
 
839
    Message ("Found:", @list );
840
    Message ("Dirs Scanned: $scanned");
841
    Message ("Packages Found: $#list");
842
}
843
 
844
#-------------------------------------------------------------------------------
845
# Function        : ListLabels
846
#
847
# Description     : List labels within a given package
848
#
849
# Inputs          : $self               - Instance data
850
#                   $path               - path to label source
851
#
852
# Returns         : Ref to an array
853
#
854
sub ListLabels
855
{
856
    my ($self, $path) = @_;
857
    Debug ("ListLabels");
858
 
859
    my ( $ref_files, $ref_dirs, $ref_svn, $found ) = $self->SvnScanPath ( 'Listing Versions', $path );
860
 
861
    Error ("List: Path not found: $path") unless ( $found );
862
 
863
    #
864
    #   Dont report files - just directories
865
    #
866
    return $ref_dirs;
867
}
868
 
869
 
870
#-------------------------------------------------------------------------------
871
# Function        : SvnLocateWsRoot
872
#
873
# Description     : Given a WorkSpace, determine the root of the work space
874
#                   This is not as simple as you might think
875
#
876
#                   Algorithm
877
#                       svn ls ..
878
#                       Am I in the parent directory
879
#                       Repeat
880
#
881
# Inputs          : $self               - Instance data
882
#                   $test               - True: Don't die on error
883
#
884
# Returns         : Root of workspace as an absolute address
885
#                   Will not return if there is an error
886
#
887
sub SvnLocateWsRoot
888
{
889
    my ($self, $test) = @_;
890
    my @path;
891
    my $path = $self->{WS};
892
 
893
    Debug ("SvnLocateWsRoot");
894
    Error ("SvnLocateWsRoot: No Workspace") unless ( $path  );
895
    Verbose2 ("SvnLocateWsRoot: Start in $path");
896
 
897
    #
898
    #   Validate the source path
899
    #
900
    if ( SvnValidateWs ($self, 'SvnLocateWsRoot', $test) )
901
    {
902
        return undef;
903
    }
904
 
905
    #
906
    #   Need to sanitize the users path to ensure that the following
907
    #   algorithm works. Need:
908
    #       1) Absolute Path
909
    #       2) Not ending in '/'
910
    #
911
 
912
    #
913
    #   If we have a relative path then prepend the current directory
914
    #   An absolute path is:
915
    #           /aaa/aa/aa
916
    #       or  c:/aaa/aa/aa
917
    #
918
    $path = getcwd() . '/' . $path
919
        unless ( $path =~ m~^/|\w:/~  );
920
 
921
    #
922
    #   Walk the bits and remove ".." directories
923
    #       Done by pushing non-.. elements and poping last entry for .. elements.
924
    #   Have a leading "/" which is good.
925
    #
926
    #   Create a array of directories in the path
927
    #   Split on one or more \ or / separators
928
    #
929
    foreach ( split /[\\\/]+/ , $path )
930
    {
931
        next if ( $_ eq '.' );
932
        unless ( $_ eq '..' )
933
        {
934
            push @path, $_;
935
        }
936
        else
937
        {
938
            Error ("SvnLocateWsRoot: Bad Pathname: $path")
939
                if ( $#path <= 0 );
940
            pop @path;
941
        }
942
    }
943
 
944
    Verbose2 ("Clean absolute path elements: @path");
945
    PATH_LOOP:
946
    while ( @path )
947
    {
948
        #
949
        #   This directory element. Append / to assist in compare
950
        #   Determine parent path
951
        #
952
        my $name = pop (@path) . '/';
953
        my $parent = join ('/', @path );
954
 
955
        #
956
        #   Examine the parent directory
957
        #   Get a list of all elements in the parent
958
        #   Need to ensure that this directory is one of them
959
        #
960
        #   Ignore any errors - assume that they are because the
961
        #   parent is not a part of the work space. This will terminate the
962
        #   search.
963
        #
964
        $self->SvnCmd ('list', $parent, '--depth', 'immediates' );
965
        foreach my $entry ( @{$self->{RESULT_LIST}} )
966
        {
967
            next PATH_LOOP
968
                if ( $entry eq $name );
969
        }
970
 
971
        #
972
        #   Didn't find 'dir' in directory svn listing of parent
973
        #   This parent is not a part of the same WorkSpace as 'dir'
974
        #   We have a winner.
975
        #
976
        chop $name;                         #   Chop the '/' previously added
977
        $self->{WS} = $parent . '/' . $name;
978
        return $self->{WS};
979
    }
980
 
981
    #
982
    #   Shouldn't get this far
983
    #
984
    Error ("SvnLocateWsRoot: Root not found");
985
}
986
 
987
#-------------------------------------------------------------------------------
988
# Function        : SvnValidateWs
989
#
990
# Description     : Validate the path to a working store
991
#
992
# Inputs          : $self           - Instance data
993
#                   $user           - Optional prefix for error messages
994
#                   $test           - True: Just test, Else Error
995
#
996
# Returns         : Will not return if not a workspace
997
#                   Returns the users path
998
#                   @{$self->{RESULT_LIST}} will be populated with info about the
999
#                   item requested as per an 'info' call
1000
#
1001
sub SvnValidateWs
1002
{
1003
    my ($self, $user, $test) = @_;
1004
    Debug ("SvnValidateWs");
1005
 
1006
    $user = "Invalid Subversion Workspace" unless ( $user );
1007
    my $path = $self->{WS} ;
1008
 
1009
    #
1010
    #   Only validate it one
1011
    #
1012
    return $path if ( $self->{WS_VALIDATED} );
1013
 
1014
    #
1015
    #   Validate the source path
1016
    #   Must exist and must be a directory
1017
    #
1018
    if ( ! $path ) {
1019
        @{$self->{ERROR_LIST}} = "$user: No path specified";
1020
 
1021
    } elsif ( ! -e $path ) {
1022
        @{$self->{ERROR_LIST}} = "$user: Path does not exist: $path";
1023
 
1024
    } elsif ( ! -d $path ) {
1025
        @{$self->{ERROR_LIST}} = "$user: Path is not a directory";
1026
    } else {
1027
        #
1028
        #   Determine the source path is an fact a view
1029
        #   The info command can do this. Use depth empty to limit the work done
1030
        #
1031
        $self->SvnCmd ('info', $path, '--depth', 'empty' );
1032
 
1033
        #
1034
        #   Error. Prepend nice message
1035
        #
1036
        unshift @{$self->{ERROR_LIST}}, "$user: Path is not a WorkSpace: $path"
1037
            if ( @{$self->{ERROR_LIST}} );
1038
    }
1039
 
1040
    #
1041
    #   Figure out what to do
1042
    #
1043
    if ( $test )
1044
    {
1045
        return @{$self->{ERROR_LIST}};
1046
    }
1047
    else
1048
    {
1049
        Error @{$self->{ERROR_LIST}} if @{$self->{ERROR_LIST}};
1050
        $self->{WS_VALIDATED} = 1;
1051
        return $path;
1052
    }
1053
}
1054
 
1055
#-------------------------------------------------------------------------------
1056
# Function        : SvnValidatePackageRoot
1057
#
1058
# Description     : Validate a package root
1059
#
1060
# Inputs          : $self           - Instance data
1061
#
1062
# Returns         : Will only return if valid
1063
#                   Returns a cleaned package root
1064
#
1065
sub SvnValidatePackageRoot
1066
{
1067
    my ($self) = @_;
1068
    Debug ("SvnValidatePackageRoot");
1069
    my $url = $self->Full || Error ("SvnValidatePackageRoot: No URL");
1070
 
1071
    Error ("Package path contains a reserved word ($self->{TAGTYPE})", "Path: $url")
1072
        if (  $self->{TAGTYPE} );
1073
 
1074
    Error ("Package name contains a Peg ($self->{PEG})", "Path: $url")
1075
        if ( $self->{PEG} );
1076
 
1077
    #
1078
    #   Ensure that the target path does exist
1079
    #   Moreover it needs to be a directory and it should have a
1080
    #   a ttb structure
1081
    #
1082
    my ( $ref_files, $ref_dirs, $ref_svn, $found ) = $self->SvnScanPath ( 'Package Base Test', $url );
1083
 
1084
    #
1085
    #   Examine the results to see if we have a valid package base
1086
    #
1087
    Error ("Package Base Test: Not a valid package") unless ( $found );
1088
 
1089
    #
1090
    #   Extra bits found
1091
    #   Its not the root of a package
1092
    #
1093
    if ( @$ref_files )
1094
    {
1095
        Warning ("Package Base Test: Files exists",
1096
               "Unexpected files found:", @$ref_files );
1097
    }
1098
 
1099
    #
1100
    #   Need a truck directory
1101
    #   If we don't have a truck we don't have a package
1102
    #
1103
    my $trunk_found = grep ( /trunk\//, @$ref_svn );
1104
    Error ("Invalid Package Base. Does not contain a 'trunk' directory")
1105
        unless ( $trunk_found );
1106
 
1107
    return $url;
1108
}
1109
 
1110
 
1111
#-------------------------------------------------------------------------------
1112
# Function        : SvnIsaSimpleLabel
1113
#
1114
# Description     : Check a label
1115
#                       Must not contain a PEG
1116
#                       Must not contain invalid characters (@ or /)
1117
#                       Must not contain a :: sequence (will confuse other tools)
1118
#                       Handle special label of TIMESTAMP
1119
#
1120
# Inputs          : $label          - to test
1121
#
1122
# Returns         : Will not return on error
1123
#                   Returns label on success
1124
#
1125
sub SvnIsaSimpleLabel
1126
{
1127
    my ($label) = @_;
1128
    Debug ("SvnIsaSimpleLabel, $label");
1129
 
1130
    Error ("No label provided") unless ( $label );
1131
    Error ("Invalid label. Peg (\@nnn) is not allowed: \"$label\"" ) if ( $label =~ m~@\d+$~ );
1132
    Error ("Invalid label. Package Path is not allowed: \"$label\"" ) if ( $label =~ m~/~ );
1133
    Error ("Invalid label. Invalid Characters: \"$label\"" ) unless ( $label =~ m~^[-.:0-9a-zA-Z_]+$~ );
1134
    Error ("Invalid label. Double :: not allowed: \"$label\"" ) if ( $label =~m~::~ );
1135
 
1136
    #
1137
    #   Allow for a label of TIMESTAMP and have it expand
1138
    #
1139
    if ( $label eq 'TIMESTAMP' )
1140
    {
1141
        $label = localtime();
1142
        $label =~ s~\s+~_~g;
1143
    }
1144
    return $label;
1145
}
1146
 
1147
#-------------------------------------------------------------------------------
1148
# Function        : NewSession
1149
#
1150
# Description     : Create a new empty SvnSession Class
1151
#
1152
# Inputs          : None
1153
#
1154
# Returns         : Class
1155
#
1156
sub NewSession
1157
{
1158
    Debug ("NewSession");
1159
    my $self  = SvnSession();
1160
 
1161
    #
1162
    #   Document Class Variables
1163
    #
1164
    $self->{URL} = '';                  # Repo URL prefix
1165
    $self->{WS}  = '';                  # Users WorkSpace
1166
    $self->{NAMEDSERVER} = '';          # User specified server
1167
    $self->{PROTOCOL} = '';             # Named Access Protocol
1168
    $self->{PKGROOT} = '';              # Package root
1169
 
1170
    #
1171
    #   Create a class
1172
    #   Bless my self
1173
    #
1174
    bless ($self, __PACKAGE__);
1175
    return $self;
1176
}
1177
 
1178
#-------------------------------------------------------------------------------
1179
# Function        : NewSessionByWS
1180
#
1181
# Description     : Establish a new SVN Session based on a Workspace
1182
#                   Given a workspace path determine the SvnServer and other
1183
#                   relevent information.
1184
#
1185
#                   Requires some rules
1186
#                       * The package is rooted within a 'ttb'
1187
#
1188
# Inputs          : $path                   - Path to WorkSpace
1189
#                   $test                   - No Error on no WS
1190
#
1191
# Returns         : Ref to Session Information
1192
#
1193
sub NewSessionByWS
1194
{
1195
    my ($path, $test) = @_;
1196
    Debug ("NewSessionByWS", @_);
1197
 
1198
    #
1199
    #   Create a basic Session
1200
    #   Populate it with information that is known
1201
    #
1202
    my $self = NewSession();
1203
    $self->{WS} = $path;
1204
 
1205
    #
1206
    #   Validate the path provided
1207
    #   In the process populate the @{$self->{RESULT_LIST}} with information
1208
    #   about the workspace.
1209
    #
1210
    if ($self->SvnValidateWs ( undef, 1) )
1211
    {
1212
        return $self if ( $test );
1213
        Error ( @{$self->{ERROR_LIST}} );
1214
    }
1215
 
1216
    #
1217
    #   Extract useful info
1218
    #       URL: svn://auperaws996vm21/test/MixedView/trunk
1219
    #       Repository Root: svn://auperaws996vm21/test
1220
    #
1221
    my $url;
1222
    my $reporoot;
1223
 
1224
    foreach ( @{$self->{RESULT_LIST}} )
1225
    {
1226
        $url = $1 if ( m~^URL:\s+(.+)~ );
1227
        $reporoot = $1 if ( m~^Repository Root:\s+(.+)~ );
1228
        last if ( $url && $reporoot );
1229
    }
1230
    Error ("JatsSvn Internal error. Can't parse info")
1231
        unless ( $url && $reporoot );
1232
 
1233
    #
1234
    #   Need the length of the path to the repository
1235
    #   but not the name of the repostory itself.
1236
    #
1237
    #   Remove that from the head of the URL to give a
1238
    #   path within the repository, that includes the repos name
1239
    #
1240
    $reporoot = (fileparse( $reporoot ))[1];
1241
    $url = substr ($url, length ($reporoot));
1242
    $self->{WSURL} = $url;
1243
    chop $reporoot;
1244
 
1245
    Verbose2 ("SvnLocatePackageRoot: $reporoot, $url" );
1246
 
1247
    #
1248
    #   Remove anything after a ttb ( truck, tags, branch ) element
1249
    #   This will be the root of the package within the repo
1250
    #
1251
    if (  $url =~ m~(.+)/((tags|branches|trunk)(/|$).*)~ )
1252
    {
1253
        $url = $1;
1254
        $self->{WSTYPE} = $3;
1255
    }
1256
    else
1257
    {
1258
        Error ("SvnLocatePackageRoot. Non standard repository format",
1259
               "Url: $url");
1260
    }
1261
 
1262
    #
1263
    #   Insert known information
1264
    #
1265
    $self->{URL} = $reporoot . '/';
1266
    $self->{PKGROOT} = $url;
1267
 
1268
    #
1269
    #   Create useful information
1270
    #
1271
    SplitPackageUrl($self);
1272
    return $self;
1273
}
1274
 
1275
#-------------------------------------------------------------------------------
1276
# Function        : NewSessionByUrl
1277
#
1278
# Description     : Establish a new SVN Session based on a user URL
1279
#
1280
# Inputs          : $uurl                   - Users URL
1281
#                   $session                - Optional: Existing session
1282
#
1283
# Returns         : Ref to Session Information
1284
#
1285
sub NewSessionByUrl
1286
{
1287
    my ($uurl, $self ) = @_;
1288
    Debug ("NewSessionByUrl", @_);
1289
    Error ("No Repostory Path specified") unless ( $uurl );
1290
 
1291
    #
1292
    #   Create a basic Session
1293
    #   Populate it with information that is known
1294
    #
1295
    $self = NewSession() unless ( $self );
1296
 
1297
    #
1298
    #   Examine the URL and determine if we have a FULL Url or
1299
    #   a path within the 'default' server
1300
    #
1301
    if ( $uurl =~ m~^((file|http|svn):///?([^/]+)/)(.+)~ )
1302
    {
1303
        #       http://server/
1304
        #       svn://server/
1305
        #       file://This/Isa/Bad/Guess
1306
        #
1307
        $self->{NAMEDSERVER} = 1;
1308
        $self->{PROTOCOL} = $2;
1309
 
1310
        $self->{URL} = $1;
1311
        $self->{PKGROOT} = $4;
1312
    }
1313
    else
1314
    {
1315
        #
1316
        #   User default (site configured) Repo Root
1317
        #
1318
        Error ("No site repository configured",
1319
               "Configure GBE_SVN_URL" ) unless ( $::GBE_SVN_URL );
1320
 
1321
        $self->{URL} = $::GBE_SVN_URL;
1322
        $self->{PKGROOT} = $uurl;
1323
    }
1324
 
1325
    #
1326
    #   Create useful information
1327
    #
1328
    SplitPackageUrl($self);
1329
    return $self;
1330
}
1331
 
1332
#-------------------------------------------------------------------------------
1333
# Function        : SplitPackageUrl
1334
#
1335
# Description     : Slip the package URL into a few useful bits
1336
#
1337
# Inputs          : $self           - Instance data
1338
#
1339
# Returns         : Nothing
1340
#
1341
sub SplitPackageUrl
1342
{
1343
    my ($self) = @_;
1344
    Debug ("SplitPackageUrl", @_);
1345
 
1346
    #
1347
    #   Remove any protocol that may be present
1348
    #       http://server/
1349
    #       svn://server/
1350
    #       file://This/Isa/Bad/Guess
1351
    #
1352
    if ( $self->{URL} =~ m~^(file|http|svn)://([^/]+)~ )
1353
    {
1354
        $self->{PROTOCOL} = $1;
1355
        $self->{SERVER} = $2;
1356
    }
1357
 
1358
    if ( $self->{PROTOCOL} eq 'svn' && $self->{PKGROOT} =~ m~([^/]+)/~ )
1359
    {
1360
        $self->{REPO} = $1;
1361
    }
1362
 
1363
 
1364
    if ( $self->{PKGROOT} =~ m~(.*)(@\d+)$~ )
1365
    {
1366
        $self->{PEG} = $2;
1367
    }
1368
 
1369
    #
1370
    #   Determine TTB type
1371
    #   Need to handle
1372
    #       .../trunk
1373
    #       .../trunk@nnnnn
1374
    #       .../tags/version@nnnnn
1375
    #       .../branches/version@nnnnn
1376
    #
1377
    #
1378
    if (  $self->{PKGROOT} =~ m~/?(.*)/(tags|branches|trunk)(/|$|@)(.*)$~ )
1379
    {
1380
        $self->{PATH}         = $1;
1381
        $self->{TAGTYPE}      = $2;
1382
        $self->{VERSION}      = $4;
1383
 
1384
        my $package = $self->{PATH};
1385
        $package =~ s~.*/~~;
1386
        $self->{PACKAGE}      = $package
1387
    }
1388
    else
1389
    {
1390
        $self->{PATH} = $self->{PKGROOT};
1391
    }
1392
 
1393
    DebugDumpData ('SplitPackageUrl', $self ) if ( IsDebug(2) );
1394
}
1395
 
1396
#-------------------------------------------------------------------------------
1397
# Function        : Full
1398
#                   FullWs
1399
#                   Repo
1400
#                   Peg
1401
#                   Type
1402
#                   WsType
1403
#                   Package
1404
#                   Path
1405
#                   Version
1406
#                   RmRef
1407
#                   Url
1408
#
1409
# Description     : Accessor functions
1410
#
1411
# Inputs          : $self       - Instance data
1412
#                                 self (is $_[0])
1413
#
1414
# Returns         : Data Item
1415
#
1416
sub Url     { return $_[0]->{URL} . ($_[1] || '') ; }
1417
sub Full    { return $_[0]->{URL} . $_[0]->{PKGROOT} ; }
1418
sub FullWs  { return $_[0]->{URL} . $_[0]->{WSURL} ; }
1419
sub Peg     { return $_[0]->{PEG} ; }
1420
sub Type    { return $_[0]->{TAGTYPE} || '' ; }
1421
sub WsType  { return $_[0]->{WSTYPE}  || '' ; }
1422
sub Package { return $_[0]->{PACKAGE} ; }
1423
sub Path    { return $_[0]->{PATH} ; }
1424
sub Version { return $_[0]->{VERSION} ; }
1425
sub RmRef   { return $_[0]->{RMREF} ; }
1426
 
1427
#-------------------------------------------------------------------------------
1428
# Function        : Print
1429
#
1430
# Description     : Debug display the URL
1431
#
1432
# Inputs          : $self           - Instance data
1433
#                   $header
1434
#                   $indent
1435
#
1436
# Returns         : Nothing
1437
#
1438
sub Print
1439
{
1440
    my ($self, $header, $indent) = @_;
1441
    print "$header\n" if $header;
1442
    $indent = 4 unless ( defined $indent );
1443
    $indent = ' ' x $indent;
1444
 
1445
 
1446
    print $indent . "PROTOCOL:" . $self->{PROTOCOL} . "\n";
1447
    print $indent . "SERVER  :" . $self->{SERVER} . "\n";
1448
    print $indent . "URL     :" . $self->{URL} . "\n";
1449
    print $indent . "PKGROOT :" . $self->{PKGROOT} . "\n";
1450
    print $indent . "PATH    :" . $self->{PATH} . "\n";
1451
    print $indent . "PACKAGE :" . $self->{PACKAGE} . "\n";
1452
    print $indent . "TAGTYPE :" . $self->{TAGTYPE} . "\n";
1453
    print $indent . "VERSION :" . $self->{VERSION} . "\n";
1454
    print $indent . "PEG     :" . $self->{PEG} . "\n";
1455
    print $indent . "FULL    :" . $self->Full . "\n";
1456
}
1457
 
1458
#-------------------------------------------------------------------------------
1459
# Function        : BranchName
1460
#
1461
# Description     : Create a full URL to a branch or tag based on the
1462
#                   current entry
1463
#
1464
#                   URL must have a TTB format
1465
#
1466
# Inputs          : $self           - Instance data
1467
#                   $branch         - Name of the branch
1468
#                   $type           - Optional branch type
1469
#
1470
# Returns         : Full URL name to the new branch
1471
#
1472
sub BranchName
1473
{
1474
    my ($self, $branch, $type ) = @_;
1475
    Debug ( "BranchName", $branch );
1476
 
1477
    $type = 'branches' unless ( $type );
1478
    my $root = $self->{PKGROOT};
1479
 
1480
    $root =~ s~/(tags|branches|trunk)(/|$|@).*~~;
1481
 
1482
    return $self->{URL} . $root . '/' . $type . '/' . $branch;
1483
}
1484
 
1485
#------------------------------------------------------------------------------
1486
1;