#!/usr/bin/perl
# ------------------------------------------------------------------------------
# NAME
#   Fcm::CmUrl
#
# DESCRIPTION
#   This class contains methods for manipulating a Subversion URL in a standard
#   FCM project.
#
# COPYRIGHT
#   (C) Crown copyright Met Office. All rights reserved.
#   For further details please refer to the file COPYRIGHT.txt
#   which you should have received as part of this distribution.
# ------------------------------------------------------------------------------

package Fcm::CmUrl;

# Standard pragma
use warnings;
use strict;

# Standard modules
use Carp;
use File::Spec;
use HTTP::Date;
use XML::DOM;

# FCM component modules
use Fcm::Util qw/run_command svn_date get_browser_url/;

# Revision pattern
my $rev_pattern = '\d+|HEAD|BASE|COMMITTED|PREV|\{.+\}';

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $cm_url = Fcm::CmUrl->new (
#     CONFIG  => $config,
#     URL     => $url,
#   );
#
# DESCRIPTION
#   This method constructs a new instance of the Fcm::CmUrl class.
#
# ARGUMENTS
#   CONFIG - reference to a Fcm::Config instance
#   URL    - URL of a branch
# ------------------------------------------------------------------------------

sub new {
  my $this  = shift;
  my %args  = @_;
  my $class = ref $this || $this;

  my $self = {
    CONFIG      => (exists $args{CONFIG} ? $args{CONFIG} : &main::cfg),
    URL         => (exists $args{URL}    ? $args{URL}    : ''),
    ANALYSED    => undef,
    PROJECT     => undef,
    BRANCH      => undef,
    SUBDIR      => undef,
    PEGREV      => undef,
    LIST        => undef,
    RLIST       => undef,
    LOG         => undef,
    LOG_RANGE   => undef,
    INFO        => undef,
    BRANCH_LIST => undef, # list of branches in current project
  };

  bless $self, $class;
  return $self;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $config = $cm_url->config;
#
# DESCRIPTION
#   This method returns a reference to the Fcm::Config instance.
# ------------------------------------------------------------------------------

sub config {
  my $self = shift;

  return $self->{CONFIG};
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $url = $cm_url->url_peg;
#   $cm_url->url_peg ($url);
#
# DESCRIPTION
#   This method returns/sets the current URL@PEG.
# ------------------------------------------------------------------------------

sub url_peg {
  my $self = shift;

  if (@_) {
    if (! $self->{URL} or $_[0] ne $self->{URL}) {
      # Re-set URL
      $self->{URL} = shift;

      # Re-set essential variables
      $self->{$_}  = undef for (qw/ANALYSED RLIST LIST INFO LOG LOG_RANGE/);
    }
  }

  return $self->{URL};
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $flag = $cm_url->is_url ();
#
# DESCRIPTION
#   Returns true if current url is a valid Subversion URL.
# ------------------------------------------------------------------------------

sub is_url {
  my $self = shift;

  # This should handle URL beginning with svn://, http:// and svn+ssh://
  return ($self->url_peg =~ m#^[\+\w]+://#);
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $flag = $cm_url->url_exists ([$rev]);
#
# DESCRIPTION
#   Returns true if current url exists (at operative revision $rev) in a
#   Subversion repository.
# ------------------------------------------------------------------------------

sub url_exists {
  my ($self, $rev) = @_;

  my $exists = $self->svnlist (REV => $rev);

  return defined ($exists);
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $string = $cm_url->svninfo ([FLAG => $flag], [REV => $rev]);
#
# DESCRIPTION
#   Returns the value of $flag, where $flag is a field returned by "svn info".
#   (If $flag is not set, default to "URL".) Otherwise returns an empty string.
#   If REV is specified, it will be used as the operative revision.
# ------------------------------------------------------------------------------

sub svninfo {
  my $self = shift;
  my %args = @_;

  my $flag = exists $args{FLAG} ? $args{FLAG} : 'URL';
  my $rev  = exists $args{REV}  ? $args{REV}  : undef;

  $rev = ($self->pegrev ? $self->pegrev : 'HEAD') if not $rev;

  return if not $self->is_url;

  # Get "info" for the specified revision if necessary
  if (not exists $self->{INFO}{$rev}) {
    # Invoke "svn info" command
    my @info = &run_command (
      [qw/svn info -r/, $rev, $self->url_peg],
      PRINT   => $self->config->verbose > 2,
      METHOD  => 'qx',
      DEVNULL => 1,
      ERROR   => 'ignore',
    );

    # Store selected information
    for (@info) {
      chomp;

      if (/^(.+?):\s*(.+)$/) {
        $self->{INFO}{$rev}{$1} = $2;
      }
    }
  }

  my $return = exists $self->{INFO}{$rev}{$flag}
               ? $self->{INFO}{$rev}{$flag} : undef;

  return $return;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   %logs = $cm_url->svnlog (
#     [REV          => $rev,]
#     [REV          => \@revs,] # reference to a 2-element array
#     [STOP_ON_COPY => 1,]
#   );
#
# DESCRIPTION
#   Returns the logs for the current URL. If REV is a range of revisions or not
#   specified, return a hash where the keys are revision numbers and the values
#   are the entries (which are hash references). If a single REV is specified,
#   return the entry (a hash reference) at the specified REV. Each entry in the
#   returned list is a hash reference, with the following structure:
#
#   $entry = {
#     author => $author,              # the commit author
#     date   => $date,                # the commit date (in seconds since epoch)
#     msg    => $msg,                 # the log message
#     paths  => {                     # list of changed paths
#       $path1  => {                  # a changed path
#         copyfrom-path => $frompath, # copy-from-path
#         copyfrom-rev  => $fromrev,  # copy-from-revision
#         action        => $action,   # action status code
#       },
#       ...     => { ... },           # ... more changed paths ...
#     },
#   }
# ------------------------------------------------------------------------------

sub svnlog {
  my $self = shift;
  my %args = @_;

  my $stop_on_copy  = exists $args{STOP_ON_COPY} ? $args{STOP_ON_COPY} : 0;
  my $rev_arg       = exists $args{REV}          ? $args{REV}          : 0;

  my @revs;

  # Get revision options
  # ----------------------------------------------------------------------------
  if ($rev_arg) {
    if (ref ($rev_arg)) {
      # Revsion option is an array, a range of revisions specified?
      ($revs [0], $revs [1]) = @$rev_arg;

    } else {
      # A single revision specified
      $revs [0] = $rev_arg;
    }

    # Expand 'HEAD' revision
    for my $rev (@revs) {
      next unless uc ($rev) eq 'HEAD';
      $rev = $self->svninfo (FLAG => 'Revision', REV => 'HEAD');
    }

  } else {
    # No revision option specified, get log for all revisions
    $revs [0] = $self->svninfo (FLAG => 'Revision');
    $revs [1] = 1;
  }

  $revs [1] = $revs [0] if not $revs [1];
  @revs     = sort {$b <=> $a} @revs;

  # Check whether a "svn log" run is necessary
  # ----------------------------------------------------------------------------
  my $need_update = ! ($revs [0] == $revs [1] and exists $self->{LOG}{$revs [0]});
  my @ranges      = @revs;
  if ($need_update and $self->{LOG_RANGE}) {
    my %log_range = %{ $self->{LOG_RANGE} };

    if ($stop_on_copy) {
      $ranges [1] = $log_range{UPPER} if $ranges [1] >= $log_range{LOWER_SOC};

    } else {
      $ranges [1] = $log_range{UPPER} if $ranges [1] >= $log_range{LOWER};
    }
  }

  $need_update = 0 if $ranges [0] < $ranges [1];

  if ($need_update) {
    # Invoke "svn log" command for all revisions of the current branch
    # --------------------------------------------------------------------------
    my @command = (
      qw/svn log --xml -v/, ($stop_on_copy ? '--stop-on-copy' : ()),
      '-r' . join (':', @ranges),
      $self->url,
    );

    my $rc;
    my @xml = &run_command (
      \@command,
      PRINT   => $self->config->verbose > 2,
      METHOD  => 'qx',
      DEVNULL => 1,
      ERROR   => 'ignore',
      RC      => \$rc,
    );

    # Parse the XML
    # --------------------------------------------------------------------------
    if (not $rc) {
      my $parser = XML::DOM::Parser->new;
      my $doc    = $parser->parse (join ('', @xml));

      my $entry_list = $doc->getElementsByTagName ('logentry');

      # Record the author, date, message and path change for each revision
      for my $i (0 .. $entry_list->getLength - 1) {
        # Select current entry from node list
        my $entry = $entry_list->item ($i);
        my %this = ();

        # Revision is an attribute of the entry node
        my $rev   = $entry->getAttributeNode ('revision')->getValue;

        # Author, date and log message are children elements of the entry node
        for my $key (qw/author date msg/) {
          # Get data of each node, also convert date to seconds since epoch
          my $node    = $entry->getElementsByTagName ($key)->item (0);
          my $data    = ($node and $node->getFirstChild)
                        ? $node->getFirstChild->getData : '';
          $this{$key} = ($key eq 'date' ? str2time ($data) : $data);
        }

        # Path nodes are grand children elements of the entry node
        my $paths = $entry->getElementsByTagName ('path');

        for my $p (0 .. $paths->getLength - 1) {
          # Select current path node from node list
          my $node = $paths->item ($p);

          # Get data from the path node
          my $path = $node->getFirstChild->getData;
          $this{paths}{$path} = {};

          # Action, copyfrom-path and copyfrom-rev are attributes of path nodes
          for my $key (qw/action copyfrom-path copyfrom-rev/) {
            next unless $node->getAttributeNode ($key); # ensure attribute exists

            $this{paths}{$path}{$key} = $node->getAttributeNode ($key)->getValue;
          }
        }

        $self->{LOG}{$rev} = \%this;
      }
    }

    # Update the range cache
    # --------------------------------------------------------------------------
    # Upper end of the range
    $self->{LOG_RANGE}{UPPER} = $ranges [0]
      if ! $self->{LOG_RANGE}{UPPER} or $ranges [0] > $self->{LOG_RANGE}{UPPER};

    # Lower end of the range, need to take into account the stop-on-copy option
    if ($stop_on_copy) {
      # Lower end of the range with stop-on-copy option
      $self->{LOG_RANGE}{LOWER_SOC} = $ranges [1]
        if ! $self->{LOG_RANGE}{LOWER_SOC} or
           $ranges [1] < $self->{LOG_RANGE}{LOWER_SOC};

      my $low = (sort {$a <=> $b} keys %{ $self->{LOG} }) [0];
      $self->{LOG_RANGE}{LOWER} = $low
        if ! $self->{LOG_RANGE}{LOWER} or $low < $self->{LOG_RANGE}{LOWER};

    } else {
      # Lower end of the range without the stop-on-copy option
      $self->{LOG_RANGE}{LOWER} = $ranges [1]
        if ! $self->{LOG_RANGE}{LOWER} or
           $ranges [1] < $self->{LOG_RANGE}{LOWER};

      $self->{LOG_RANGE}{LOWER_SOC} = $ranges [1]
        if ! $self->{LOG_RANGE}{LOWER_SOC} or
           $ranges [1] < $self->{LOG_RANGE}{LOWER_SOC};
    }
  }

  my %return = ();

  if (! $rev_arg or ref ($rev_arg)) {
    # REV is an array, return log entries if they are within range
    for my $rev (sort {$b <=> $a} keys %{ $self->{LOG} }) {
      next if $rev > $revs [0] or $revs [1] > $rev;

      $return{$rev} = $self->{LOG}{$rev};

      if ($stop_on_copy) {
        last if exists $self->{LOG}{$rev}{paths}{$self->branch_path} and
           $self->{LOG}{$rev}{paths}{$self->branch_path}{action} eq 'A';
      }
    }

  } else {
    # REV is a scalar, return log of the specified revision if it exists
    %return = %{ $self->{LOG}{$revs [0]} } if exists $self->{LOG}{$revs [0]};
  }

  return %return;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $string = $cm_branch->display_svnlog ($rev, [$wiki]);
#
# DESCRIPTION
#   This method returns a string for displaying the log of the current branch
#   at a $rev. If $wiki is set, returns a string for displaying in a Trac wiki
#   table.  The value of $wiki should be the Subversion URL of a FCM project
#   associated with the intended Trac system.
# ------------------------------------------------------------------------------

sub display_svnlog {
  my ($self, $rev, $wiki) = @_;
  my $return = '';

  my %log  = $self->svnlog (REV => $rev);

  if ($wiki) {
    # Output in Trac wiki format
    $return .= '|| ' . &svn_date ($log{date}) . ' || ' . $log{author} . ' || ';

    my $trac_url = &get_browser_url (URL => $self->url);

    if ($trac_url =~ m#^$wiki(?:/*|$)#) {
      $return .= '[' . $rev . '] ||';

    } else {
      my $rev_url = $trac_url;
      $rev_url    =~ s#/browser/.*$##;
      $rev_url   .= '/changeset/' . $rev;
      $return    .= '[' . $rev_url . '] ||';
    }

  } else {
    # Output in plain text format
    my @msg  = split /\n/, $log{msg};
    my $line = (@msg > 1 ? ' lines' : ' line');

    $return .= join (
      ' | ',
      ('r' . $rev, $log{author}, &svn_date ($log{date}), scalar (@msg) . $line),
    );
    $return .= "\n\n";
    $return .= $log{msg};
  }

  return $return;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   @list = $cm_url->svnlist ([REV => $rev], [RECURSIVE => 1]);
#
# DESCRIPTION
#   The method returns a list of paths as returned by "svn list". If RECURSIVE
#   is set, "svn list" is invoked with the "-R" option.
# ------------------------------------------------------------------------------

sub svnlist {
  my $self = shift;
  my %args = @_;

  my $recursive = exists $args{RECURSIVE} ? $args{RECURSIVE} : 0;
  my $rev       = exists $args{REV}       ? $args{REV}       : undef;
  my $key       = $recursive ? 'RLIST' : 'LIST';

  # Find out last changed revision of the current URL
  $rev = $self->svninfo (FLAG => 'Last Changed Rev', REV => $rev);
  return () if not $rev;

  # Get directory listing for the current URL at the last changed revision
  if (not exists $self->{$key}{$rev}) {
    my $rc;

    my @list = map {chomp; $_} &run_command (
      [qw/svn list -r/, $rev, ($recursive ? '-R' : ()), $self->url_peg],
      METHOD => 'qx', ERROR => 'ignore', DEVNULL => 1, RC => \$rc,
    );

    $self->{$key}{$rev} = $rc ? undef : \@list;
  }

  return (defined ($self->{$key}{$rev}) ? @{ $self->{$key}{$rev} } : undef);
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   @list = $cm_url->branch_list ($rev);
#
# DESCRIPTION
#   The method returns a list of branches in the current project, assuming the
#   FCM naming convention. If $rev if specified, it returns the list of
#   branches at $rev.
# ------------------------------------------------------------------------------

sub branch_list {
  my ($self, $rev) = @_;

  # Current URL must be a valid FCM project
  return if not $self->project;

  # Find out last changed revision of the current URL
  $rev = $self->svninfo (FLAG => 'Revision', REV => $rev);
  return () if not $rev;

  if (not exists $self->{BRANCH_LIST}{$rev}) {
    # Get URL of the project "branches/" sub-directory
    my $url = Fcm::CmUrl->new (URL => $self->project_url . '/branches');

    # List three levels underneath "branches/"
    # First level, i.e. dev, test, pkg, etc
    my @list1 = map {$url->url . '/' . $_} $url->svnlist (REV => $rev);
    @list1    = grep m#/$#, @list1;

    # Second level, i.e. user name, Shared, Rel or Config
    my @list2;
    for (@list1) {
      my $u    = Fcm::CmUrl->new (URL => $_);
      my @list = $u->svnlist (REV => $rev);

      push @list2, map {$u->url . $_} @list;
    }

    # Third level, branch name
    for (@list2) {
      my $u    = Fcm::CmUrl->new (URL => $_);
      my @list = map {s#/*$##; $_} $u->svnlist (REV => $rev);

      push @{ $self->{BRANCH_LIST}{$rev} }, map {$u->url . $_} @list;
    }
  }

  return @{ $self->{BRANCH_LIST}{$rev} };
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $self->_analyse_url ();
#
# DESCRIPTION
#   The method analyses the current URL, breaking it up into the project
#   (substring of URL up to the slash before "trunk", "branches" or "tags"),
#   branch name ("trunk", "branches/<type>/<id>/<name>" or "tags/<name>") and
#   the sub-directory below the top of the project sub-tree. It re-sets the
#   corresponding interal variables.
# ------------------------------------------------------------------------------

sub _analyse_url {
  my $self = shift;
  my ($url, $project, $branch, $subdir, $pegrev);

  # Check that URL is set
  $url    = $self->url_peg;
  return if not $url;
  return if not $self->is_url;

  # Extract from URL the peg revision
  $pegrev = $1 if $url =~ s/@($rev_pattern)$//i;

  if ($url =~ m#^(.*?)/+(trunk|branches|tags)(?:/+(.*))?/*$#) {
    # URL is under the "trunk", a branch or a tag
    $project                 = $1;
    my ($branch_id, $remain) = ($2, $3);

    $remain = '' if not defined $remain;

    if ($branch_id eq 'trunk') {
      # URL under the "trunk"
      $branch = 'trunk';

    } else {
      # URL under a branch or a tag
      $branch = $branch_id;

      # Assume "3 sub-directories", FCM branch naming convention
      for (1 .. 3) {
        if ($remain =~ s#^([^/]+)(?:/+|$)##) {
          $branch .=  '/' . $1;

        } else {
          $branch = undef;
          last;
        }
      }
    }

    $subdir = $remain ? $remain : '' if $branch;

  } else {
    # URL is at some level above the "trunk", a branch or a tag
    # Use "svn ls" to determine whether it is a project URL
    my @list = $self->svnlist (REV => ($pegrev ? $pegrev : 'HEAD'));
    my %lines = map {chomp $_; ($_, 1)} @list;

    # A project URL should have the "trunk", "branches" and "tags" directories
    ($project = $url) =~ s#/*$##
      if $lines{'trunk/'} and $lines{'branches/'} and $lines{'tags/'};
  }

  $self->{PROJECT}  = $project;
  $self->{BRANCH}   = $branch;
  $self->{SUBDIR}   = $subdir;
  $self->{PEGREV}   = $pegrev;
  $self->{ANALYSED} = 1;

  return;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $url = $cm_url->root ();
#
# DESCRIPTION
#   The method returns the repository root of the current URL.
# ------------------------------------------------------------------------------

sub root {
  my $self = shift;

  return $self->svninfo (FLAG => 'Repository Root');
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $url = $cm_url->project_url_peg ();
#   $cm_url->project_url_peg ($url);
#
# DESCRIPTION
#   The method returns the URL@PEG of the "project" part of the current URL. If
#   an argument is specified, the URL of the "project" part and the peg
#   revision of the current URL are re-set.
# ------------------------------------------------------------------------------

sub project_url_peg {
  my $self = shift;

  if (@_) {
    my $url = shift;

    # Re-construct URL is necessary
    if (! $self->project_url_peg or $url ne $self->project_url_peg) {
      my $pegrev = ($url =~ s/@($rev_pattern)$//i) ? $1 : '';

      $url .= '/' . $self->branch if $self->branch;
      $url .= '/' . $self->subdir if $self->subdir;
      $url .= '@' . $pegrev       if $pegrev;

      $self->url_peg ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return $self->{PROJECT} . ($self->pegrev ? '@' . $self->pegrev : '');
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $url = $cm_url->project_url ();
#   $cm_url->project_url ($url);
#
# DESCRIPTION
#   The method returns the URL of the "project" part of the current URL. If an
#   argument is specified, the URL of the "project" part of the current URL is
#   re-set.
# ------------------------------------------------------------------------------

sub project_url {
  my $self = shift;

  if (@_) {
    my $url = shift;
    $url =~ s/@($rev_pattern)$//i;

    # Re-construct URL is necessary
    if (! $self->project_url or $url ne $self->project_url) {
      $url .= '/' . $self->branch if $self->branch;
      $url .= '/' . $self->subdir if $self->subdir;

      $self->url ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return $self->{PROJECT};
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $path = $cm_url->project_path ();
#   $cm_url->project_path ($path);
#
# DESCRIPTION
#   The method returns the path of the "project" part of the current URL. If an
#   argument is specified, the path of the "project" part of the current URL is
#   re-set.
# ------------------------------------------------------------------------------

sub project_path {
  my $self = shift;

  # Repository root
  my $root = $self->root;
  $root    = substr (
    $self->project_url,
    0,
    length ($self->project_url) - length ($self->project) - 1
  ) if not $root;

  if (@_) {
    my $path = shift;

    # Re-construct URL is necessary
    if (! $self->project_path or $path ne $self->project_path) {
      $path .= '/' . $self->branch if $self->branch;
      $path .= '/' . $self->subdir if $self->subdir;

      $self->path ($path);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return substr ($self->{PROJECT}, length ($root));
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $name = $cm_url->project ();
#   $cm_url->project ($name);
#
# DESCRIPTION
#   The method returns the basename of the "project" part of the current URL.
#   If an argument is specified, the basename of the "project" part of the
#   current URL is re-set.
# ------------------------------------------------------------------------------

sub project {
  my $self = shift;

  if (@_) {
    my $name = shift;

    # Re-construct URL is necessary
    if (! $self->project or $name ne $self->project) {
      my $url = '';
      if ($self->project) {
        $url =  $self->project;
        $url =~ s#/[^/]+$##;

      } else {
        $url =  $self->root;
      }

      $url .=  '/' . $name;
      $url .=  '/' . $self->branch if $self->branch;
      $url .=  '/' . $self->subdir if $self->subdir;
      $url .=  '@' . $self->pegrev if $self->pegrev;

      $self->url_peg ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  my $name = $self->{PROJECT};
  $name =~ s#^.*/([^/]+)$#$1# if $name;

  return $name;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $url = $cm_url->branch_url_peg ();
#   $cm_url->branch_url_peg ($url);
#
# DESCRIPTION
#   The method returns the URL@PEG of the "branch" part of the current URL. If
#   an argument is specified, the URL@PEG of the "branch" part of the current
#   URL is re-set.
# ------------------------------------------------------------------------------

sub branch_url_peg {
  my $self = shift;

  if (@_) {
    my $url = shift;

    # Re-construct URL is necessary
    if (! $self->branch_url_peg or $url ne $self->branch_url_peg) {
      my $pegrev = ($url =~ s/@($rev_pattern)$//i) ? $1 : '';

      $url .= '/' . $self->subdir if $self->subdir;
      $url .= '@' . $pegrev       if $pegrev;

      $self->url_peg ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return $self->project_url . '/' . $self->branch .
         ($self->pegrev ? '@' . $self->pegrev : '');
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $url = $cm_url->branch_url ();
#   $cm_url->branch_url ($url);
#
# DESCRIPTION
#   The method returns the URL of the "branch" part of the current URL. If an
#   argument is specified, the URL of the "branch" part of the current URL is
#   re-set.
# ------------------------------------------------------------------------------

sub branch_url {
  my $self = shift;

  if (@_) {
    my $url = shift;
    $url =~ s/@($rev_pattern)$//i;

    # Re-construct URL is necessary
    if (! $self->branch_url or $url ne $self->branch_url) {
      $url .= '/' . $self->subdir if $self->subdir;

      $self->url ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return $self->project_url . '/' . $self->branch;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $path = $cm_url->branch_path ();
#   $cm_url->branch_path ($path);
#
# DESCRIPTION
#   The method returns the path of the "branch" part of the current URL. If an
#   argument is specified, the path of the "branch" part of the current URL is
#   re-set.
# ------------------------------------------------------------------------------

sub branch_path {
  my $self = shift;

  if (@_) {
    my $path = shift;

    # Re-construct URL is necessary
    if (! $self->branch_path or $path ne $self->branch_path) {
      $path .= '/' . $self->subdir if $self->subdir;

      $self->path ($path);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return $self->project_path . '/' . $self->branch;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $branch = $cm_url->branch ();
#   $cm_url->branch ($branch);
#
# DESCRIPTION
#   The method returns the "branch" part of the current URL. If an argument is
#   specified, the "branch" part of the current URL is re-set.
# ------------------------------------------------------------------------------

sub branch {
  my $self = shift;

  if (@_) {
    my $branch = shift;

    # Re-construct URL is necessary
    if (! $self->branch or $branch ne $self->branch) {
      my $url = $self->project_url;
      $url   .= '/' . $branch;
      $url   .= '/' . $self->subdir if $self->subdir;

      $self->url ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return $self->{BRANCH};
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $flag = $cm_url->is_trunk ();
#
# DESCRIPTION
#   The method returns true if the the current URL is (a sub-tree of) the trunk.
# ------------------------------------------------------------------------------

sub is_trunk {
  my $self = shift;

  $self->_analyse_url () if not $self->{ANALYSED};

  return ($self->branch and $self->branch eq 'trunk');
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $flag = $cm_url->is_branch ();
#
# DESCRIPTION
#   The method returns true if the the current URL is (a sub-tree of) a branch.
# ------------------------------------------------------------------------------

sub is_branch {
  my $self = shift;

  $self->_analyse_url () if not $self->{ANALYSED};

  return ($self->branch and $self->branch =~ m#^branches/#);
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $flag = $cm_url->is_tag ();
#
# DESCRIPTION
#   The method returns true if the the current URL is (a sub-tree of) a tag.
# ------------------------------------------------------------------------------

sub is_tag {
  my $self = shift;

  $self->_analyse_url () if not $self->{ANALYSED};

  return ($self->branch and $self->branch =~ m#^tags/#);
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $subdir = $cm_url->subdir ();
#   $cm_url->subdir ($subdir);
#
# DESCRIPTION
#   The method returns the "subdir" part of the current URL. If an argument is
#   specified, the "subdir" part of the current URL is re-set.
# ------------------------------------------------------------------------------

sub subdir {
  my $self = shift;

  if (@_) {
    my $subdir = shift;

    # Re-construct URL is necessary
    if (! $self->subdir or $subdir ne $self->subdir) {
      my $url = $self->project_url;
      $url   .= '/' . $self->branch if $self->branch;
      $url   .= '/' . $subdir if $subdir;

      $self->url ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return $self->{SUBDIR};
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $url = $cm_url->url ();
#   $cm_url->url ($url);
#
# DESCRIPTION
#   The method returns the URL without the "peg revision" part. If an argument
#   is specified, the URL is re-set without modifying the "peg revision" part.
# ------------------------------------------------------------------------------

sub url {
  my $self = shift;

  if (@_) {
    my $url = shift;
    $url    =~ s/@($rev_pattern)$//i;

    # Re-construct URL if necessary
    if (! $self->url or $url ne $self->url) {
      $self->url_peg ($url . ($self->pegrev ? '@' . $self->pegrev : ''));
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  (my $url = $self->url_peg) =~ s/@($rev_pattern)$//i;

  return $url;
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $path = $cm_url->path ();
#   $cm_url->path ($path);
#
# DESCRIPTION
#   The method returns the "path" part of the URL (i.e. URL without the
#   "root" part). If an argument is specified, the "path" part of the URL is
#   re-set.
# ------------------------------------------------------------------------------

sub path {
  my $self = shift;

  # Repository root
  my $root = $self->root;
  $root    = substr (
    $self->project_url,
    0,
    length ($self->project_url) - length ($self->project) - 1
  ) if not $root;

  if (@_) {
    my $path = shift;
    $path    =~ s/@($rev_pattern)$//i;

    # Re-construct URL is necessary
    if (! $self->path or $path ne $self->path) {
      my $url = ($root . (substr ($path, 0, 1) eq '/' ? '' : '/') . $path);
      $self->url ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return substr ($self->url, length ($root));
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $path = $cm_url->path_peg ();
#   $cm_url->path_peg ($path);
#
# DESCRIPTION
#   The method returns the PATH@PEG part of the URL (i.e. URL without the
#   "root" part). If an argument is specified, the PATH@PEG part of the URL is
#   re-set.
# ------------------------------------------------------------------------------

sub path_peg {
  my $self = shift;

  # Repository root
  my $root = $self->root;
  $root    = substr (
    $self->project_url,
    0,
    length ($self->project_url) - length ($self->project) - 1
  ) if not $root;

  if (@_) {
    my $path = shift;

    # Re-construct URL is necessary
    if (! $self->path_peg or $path ne $self->path_peg) {
      my $url = ($root . (substr ($path, 0, 1) eq '/' ? '' : '/') . $path);
      $self->url_peg ($url);
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return substr ($self->url_peg, length ($root));
}

# ------------------------------------------------------------------------------
# SYNOPSIS
#   $rev = $cm_url->pegrev ();
#   $cm_url->pegrev ($rev);
#
# DESCRIPTION
#   The method returns the "peg revision" part of the current URL. If an
#   argument is specified, the "peg revision" part of the current URL is
#   re-set.
# ------------------------------------------------------------------------------

sub pegrev {
  my $self = shift;

  if (@_) {
    my $pegrev = shift;

    # Re-construct URL is necessary
    if (! $self->pegrev or $pegrev ne $self->pegrev) {
      $self->url_peg ($self->url . ($pegrev ? '@' . $pegrev : ''));
    }
  }

  $self->_analyse_url () if not $self->{ANALYSED};

  return $self->{PEGREV};
}

# ------------------------------------------------------------------------------

1;

__END__
