CmUrl.pm 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149
  1. # ------------------------------------------------------------------------------
  2. # NAME
  3. # Fcm::CmUrl
  4. #
  5. # DESCRIPTION
  6. # This class contains methods for manipulating a Subversion URL in a standard
  7. # FCM project.
  8. #
  9. # COPYRIGHT
  10. # (C) Crown copyright Met Office. All rights reserved.
  11. # For further details please refer to the file COPYRIGHT.txt
  12. # which you should have received as part of this distribution.
  13. # ------------------------------------------------------------------------------
  14. package Fcm::CmUrl;
  15. @ISA = qw(Fcm::Base);
  16. # Standard pragma
  17. use warnings;
  18. use strict;
  19. # Standard modules
  20. use HTTP::Date;
  21. use XML::DOM;
  22. # FCM component modules
  23. use Fcm::Base;
  24. use Fcm::Keyword;
  25. use Fcm::Util qw/run_command svn_date/;
  26. # Special branches
  27. our %owner_keywords = (Share => 'shared', Config => 'config', Rel => 'release');
  28. # Revision pattern
  29. my $rev_pattern = '\d+|HEAD|BASE|COMMITTED|PREV|\{.+\}';
  30. # ------------------------------------------------------------------------------
  31. # SYNOPSIS
  32. # $cm_url = Fcm::CmUrl->new ([URL => $url,]);
  33. #
  34. # DESCRIPTION
  35. # This method constructs a new instance of the Fcm::CmUrl class.
  36. #
  37. # ARGUMENTS
  38. # URL - URL of a branch
  39. # ------------------------------------------------------------------------------
  40. sub new {
  41. my $this = shift;
  42. my %args = @_;
  43. my $class = ref $this || $this;
  44. my $self = Fcm::Base->new (%args);
  45. $self->{URL} = (exists $args{URL} ? $args{URL} : '');
  46. for (qw/ANALYSED BRANCH BRANCH_LIST INFO LIST LOG LOG_RANGE PEGREV RLIST
  47. PROJECT SUBDIR/) {
  48. $self->{$_} = undef;
  49. }
  50. bless $self, $class;
  51. return $self;
  52. }
  53. # ------------------------------------------------------------------------------
  54. # SYNOPSIS
  55. # $url = $cm_url->url_peg;
  56. # $cm_url->url_peg ($url);
  57. #
  58. # DESCRIPTION
  59. # This method returns/sets the current URL@PEG.
  60. # ------------------------------------------------------------------------------
  61. sub url_peg {
  62. my $self = shift;
  63. if (@_) {
  64. if (! $self->{URL} or $_[0] ne $self->{URL}) {
  65. # Re-set URL
  66. $self->{URL} = shift;
  67. # Re-set essential variables
  68. $self->{$_} = undef for (qw/ANALYSED RLIST LIST INFO LOG LOG_RANGE/);
  69. }
  70. }
  71. return $self->{URL};
  72. }
  73. # ------------------------------------------------------------------------------
  74. # SYNOPSIS
  75. # $flag = $cm_url->is_url ();
  76. #
  77. # DESCRIPTION
  78. # Returns true if current url is a valid Subversion URL.
  79. # ------------------------------------------------------------------------------
  80. sub is_url {
  81. my $self = shift;
  82. # This should handle URL beginning with svn://, http:// and svn+ssh://
  83. return ($self->url_peg =~ m#^[\+\w]+://#);
  84. }
  85. # ------------------------------------------------------------------------------
  86. # SYNOPSIS
  87. # $flag = $cm_url->url_exists ([$rev]);
  88. #
  89. # DESCRIPTION
  90. # Returns true if current url exists (at operative revision $rev) in a
  91. # Subversion repository.
  92. # ------------------------------------------------------------------------------
  93. sub url_exists {
  94. my ($self, $rev) = @_;
  95. my $exists = $self->svnlist (REV => $rev);
  96. return defined ($exists);
  97. }
  98. # ------------------------------------------------------------------------------
  99. # SYNOPSIS
  100. # $string = $cm_url->svninfo ([FLAG => $flag], [REV => $rev]);
  101. #
  102. # DESCRIPTION
  103. # Returns the value of $flag, where $flag is a field returned by "svn info".
  104. # (If $flag is not set, default to "URL".) Otherwise returns an empty string.
  105. # If REV is specified, it will be used as the operative revision.
  106. # ------------------------------------------------------------------------------
  107. sub svninfo {
  108. my $self = shift;
  109. my %args = @_;
  110. my $flag = exists $args{FLAG} ? $args{FLAG} : 'URL';
  111. my $rev = exists $args{REV} ? $args{REV} : undef;
  112. $rev = ($self->pegrev ? $self->pegrev : 'HEAD') if not $rev;
  113. return if not $self->is_url;
  114. # Get "info" for the specified revision if necessary
  115. if (not exists $self->{INFO}{$rev}) {
  116. # Invoke "svn info" command
  117. my @info = &run_command (
  118. [qw/svn info -r/, $rev, $self->url_peg],
  119. PRINT => $self->config->verbose > 2,
  120. METHOD => 'qx',
  121. DEVNULL => 1,
  122. ERROR => 'ignore',
  123. );
  124. # Store selected information
  125. for (@info) {
  126. chomp;
  127. if (/^(.+?):\s*(.+)$/) {
  128. $self->{INFO}{$rev}{$1} = $2;
  129. }
  130. }
  131. }
  132. my $return = exists $self->{INFO}{$rev}{$flag}
  133. ? $self->{INFO}{$rev}{$flag} : undef;
  134. return $return;
  135. }
  136. # ------------------------------------------------------------------------------
  137. # SYNOPSIS
  138. # %logs = $cm_url->svnlog (
  139. # [REV => $rev,]
  140. # [REV => \@revs,] # reference to a 2-element array
  141. # [STOP_ON_COPY => 1,]
  142. # );
  143. #
  144. # DESCRIPTION
  145. # Returns the logs for the current URL. If REV is a range of revisions or not
  146. # specified, return a hash where the keys are revision numbers and the values
  147. # are the entries (which are hash references). If a single REV is specified,
  148. # return the entry (a hash reference) at the specified REV. Each entry in the
  149. # returned list is a hash reference, with the following structure:
  150. #
  151. # $entry = {
  152. # author => $author, # the commit author
  153. # date => $date, # the commit date (in seconds since epoch)
  154. # msg => $msg, # the log message
  155. # paths => { # list of changed paths
  156. # $path1 => { # a changed path
  157. # copyfrom-path => $frompath, # copy-from-path
  158. # copyfrom-rev => $fromrev, # copy-from-revision
  159. # action => $action, # action status code
  160. # },
  161. # ... => { ... }, # ... more changed paths ...
  162. # },
  163. # }
  164. # ------------------------------------------------------------------------------
  165. sub svnlog {
  166. my $self = shift;
  167. my %args = @_;
  168. my $stop_on_copy = exists $args{STOP_ON_COPY} ? $args{STOP_ON_COPY} : 0;
  169. my $rev_arg = exists $args{REV} ? $args{REV} : 0;
  170. my @revs;
  171. # Get revision options
  172. # ----------------------------------------------------------------------------
  173. if ($rev_arg) {
  174. if (ref ($rev_arg)) {
  175. # Revsion option is an array, a range of revisions specified?
  176. ($revs [0], $revs [1]) = @$rev_arg;
  177. } else {
  178. # A single revision specified
  179. $revs [0] = $rev_arg;
  180. }
  181. # Expand 'HEAD' revision
  182. for my $rev (@revs) {
  183. next unless uc ($rev) eq 'HEAD';
  184. $rev = $self->svninfo (FLAG => 'Revision', REV => 'HEAD');
  185. }
  186. } else {
  187. # No revision option specified, get log for all revisions
  188. $revs [0] = $self->svninfo (FLAG => 'Revision');
  189. $revs [1] = 1;
  190. }
  191. $revs [1] = $revs [0] if not $revs [1];
  192. @revs = sort {$b <=> $a} @revs;
  193. # Check whether a "svn log" run is necessary
  194. # ----------------------------------------------------------------------------
  195. my $need_update = ! ($revs [0] == $revs [1] and exists $self->{LOG}{$revs [0]});
  196. my @ranges = @revs;
  197. if ($need_update and $self->{LOG_RANGE}) {
  198. my %log_range = %{ $self->{LOG_RANGE} };
  199. if ($stop_on_copy) {
  200. $ranges [1] = $log_range{UPPER} if $ranges [1] >= $log_range{LOWER_SOC};
  201. } else {
  202. $ranges [1] = $log_range{UPPER} if $ranges [1] >= $log_range{LOWER};
  203. }
  204. }
  205. $need_update = 0 if $ranges [0] < $ranges [1];
  206. if ($need_update) {
  207. # Invoke "svn log" command for all revisions of the current branch
  208. # --------------------------------------------------------------------------
  209. my @command = (
  210. qw/svn log --xml -v/, ($stop_on_copy ? '--stop-on-copy' : ()),
  211. '-r' . join (':', @ranges),
  212. $self->url_peg,
  213. );
  214. my $rc;
  215. my @xml = &run_command (
  216. \@command,
  217. PRINT => $self->config->verbose > 2,
  218. METHOD => 'qx',
  219. DEVNULL => 1,
  220. ERROR => 'ignore',
  221. RC => \$rc,
  222. );
  223. # Parse the XML
  224. # --------------------------------------------------------------------------
  225. if (not $rc) {
  226. my $parser = XML::DOM::Parser->new;
  227. my $doc = $parser->parse (join ('', @xml));
  228. my $entry_list = $doc->getElementsByTagName ('logentry');
  229. # Record the author, date, message and path change for each revision
  230. for my $i (0 .. $entry_list->getLength - 1) {
  231. # Select current entry from node list
  232. my $entry = $entry_list->item ($i);
  233. my %this = ();
  234. # Revision is an attribute of the entry node
  235. my $rev = $entry->getAttributeNode ('revision')->getValue;
  236. # Author, date and log message are children elements of the entry node
  237. for my $key (qw/author date msg/) {
  238. # Get data of each node, also convert date to seconds since epoch
  239. my $node = $entry->getElementsByTagName ($key)->item (0);
  240. my $data = ($node and $node->getFirstChild)
  241. ? $node->getFirstChild->getData : '';
  242. $this{$key} = ($key eq 'date' ? str2time ($data) : $data);
  243. }
  244. # Path nodes are grand children elements of the entry node
  245. my $paths = $entry->getElementsByTagName ('path');
  246. for my $p (0 .. $paths->getLength - 1) {
  247. # Select current path node from node list
  248. my $node = $paths->item ($p);
  249. # Get data from the path node
  250. my $path = $node->getFirstChild->getData;
  251. $this{paths}{$path} = {};
  252. # Action, copyfrom-path and copyfrom-rev are attributes of path nodes
  253. for my $key (qw/action copyfrom-path copyfrom-rev/) {
  254. next unless $node->getAttributeNode ($key); # ensure attribute exists
  255. $this{paths}{$path}{$key} = $node->getAttributeNode ($key)->getValue;
  256. }
  257. }
  258. $self->{LOG}{$rev} = \%this;
  259. }
  260. }
  261. # Update the range cache
  262. # --------------------------------------------------------------------------
  263. # Upper end of the range
  264. $self->{LOG_RANGE}{UPPER} = $ranges [0]
  265. if ! $self->{LOG_RANGE}{UPPER} or $ranges [0] > $self->{LOG_RANGE}{UPPER};
  266. # Lower end of the range, need to take into account the stop-on-copy option
  267. if ($stop_on_copy) {
  268. # Lower end of the range with stop-on-copy option
  269. $self->{LOG_RANGE}{LOWER_SOC} = $ranges [1]
  270. if ! $self->{LOG_RANGE}{LOWER_SOC} or
  271. $ranges [1] < $self->{LOG_RANGE}{LOWER_SOC};
  272. my $low = (sort {$a <=> $b} keys %{ $self->{LOG} }) [0];
  273. $self->{LOG_RANGE}{LOWER} = $low
  274. if ! $self->{LOG_RANGE}{LOWER} or $low < $self->{LOG_RANGE}{LOWER};
  275. } else {
  276. # Lower end of the range without the stop-on-copy option
  277. $self->{LOG_RANGE}{LOWER} = $ranges [1]
  278. if ! $self->{LOG_RANGE}{LOWER} or
  279. $ranges [1] < $self->{LOG_RANGE}{LOWER};
  280. $self->{LOG_RANGE}{LOWER_SOC} = $ranges [1]
  281. if ! $self->{LOG_RANGE}{LOWER_SOC} or
  282. $ranges [1] < $self->{LOG_RANGE}{LOWER_SOC};
  283. }
  284. }
  285. my %return = ();
  286. if (! $rev_arg or ref ($rev_arg)) {
  287. # REV is an array, return log entries if they are within range
  288. for my $rev (sort {$b <=> $a} keys %{ $self->{LOG} }) {
  289. next if $rev > $revs [0] or $revs [1] > $rev;
  290. $return{$rev} = $self->{LOG}{$rev};
  291. if ($stop_on_copy) {
  292. last if exists $self->{LOG}{$rev}{paths}{$self->branch_path} and
  293. $self->{LOG}{$rev}{paths}{$self->branch_path}{action} eq 'A';
  294. }
  295. }
  296. } else {
  297. # REV is a scalar, return log of the specified revision if it exists
  298. %return = %{ $self->{LOG}{$revs [0]} } if exists $self->{LOG}{$revs [0]};
  299. }
  300. return %return;
  301. }
  302. # ------------------------------------------------------------------------------
  303. # SYNOPSIS
  304. # $string = $cm_branch->display_svnlog ($rev, [$wiki]);
  305. #
  306. # DESCRIPTION
  307. # This method returns a string for displaying the log of the current branch
  308. # at a $rev. If $wiki is set, returns a string for displaying in a Trac wiki
  309. # table. The value of $wiki should be the Subversion URL of a FCM project
  310. # associated with the intended Trac system.
  311. # ------------------------------------------------------------------------------
  312. sub display_svnlog {
  313. my ($self, $rev, $wiki) = @_;
  314. my $return = '';
  315. my %log = $self->svnlog (REV => $rev);
  316. if ($wiki) {
  317. # Output in Trac wiki format
  318. # --------------------------------------------------------------------------
  319. $return .= '|| ' . &svn_date ($log{date}) . ' || ' . $log{author} . ' || ';
  320. my $trac_url = Fcm::Keyword::get_browser_url($self->url);
  321. # Get list of tickets from log
  322. my @tickets;
  323. while ($log{msg} =~ /(?:(\w+):)?(?:#|ticket:)(\d+)/g) {
  324. push @tickets, [$1, $2];
  325. }
  326. @tickets = sort {
  327. if ($a->[0] and $b->[0]) {
  328. $a->[0] cmp $b->[0] or $a->[1] <=> $b->[1];
  329. } elsif ($a->[0]) {
  330. 1;
  331. } else {
  332. $a->[1] <=> $b->[1];
  333. }
  334. } @tickets;
  335. if ($trac_url =~ m#^$wiki(?:/*|$)#) {
  336. # URL is in the specified $wiki, use Trac link
  337. $return .= '[' . $rev . '] ||';
  338. for my $ticket (@tickets) {
  339. $return .= ' ';
  340. $return .= $ticket->[0] . ':' if $ticket->[0];
  341. $return .= '#' . $ticket->[1];
  342. }
  343. $return .= ' ||';
  344. } else {
  345. # URL is not in the specified $wiki, use full URL
  346. my $rev_url = $trac_url;
  347. $rev_url =~ s{/intertrac/source:.*\z}{/intertrac/changeset:$rev}xms;
  348. $return .= '[' . $rev_url . ' ' . $rev . '] ||';
  349. my $ticket_url = $trac_url;
  350. $ticket_url =~ s{/intertrac/source:.*\z}{/intertrac/}xms;
  351. for my $ticket (@tickets) {
  352. $return .= ' [' . $ticket_url;
  353. $return .= $ticket->[0] . ':' if $ticket->[0];
  354. $return .= 'ticket:' . $ticket->[1] . ' ' . $ticket->[1] . ']';
  355. }
  356. $return .= ' ||';
  357. }
  358. } else {
  359. # Output in plain text format
  360. # --------------------------------------------------------------------------
  361. my @msg = split /\n/, $log{msg};
  362. my $line = (@msg > 1 ? ' lines' : ' line');
  363. $return .= join (
  364. ' | ',
  365. ('r' . $rev, $log{author}, &svn_date ($log{date}), scalar (@msg) . $line),
  366. );
  367. $return .= "\n\n";
  368. $return .= $log{msg};
  369. }
  370. return $return;
  371. }
  372. # ------------------------------------------------------------------------------
  373. # SYNOPSIS
  374. # @list = $cm_url->svnlist ([REV => $rev], [RECURSIVE => 1]);
  375. #
  376. # DESCRIPTION
  377. # The method returns a list of paths as returned by "svn list". If RECURSIVE
  378. # is set, "svn list" is invoked with the "-R" option.
  379. # ------------------------------------------------------------------------------
  380. sub svnlist {
  381. my $self = shift;
  382. my %args = @_;
  383. my $recursive = exists $args{RECURSIVE} ? $args{RECURSIVE} : 0;
  384. my $rev = exists $args{REV} ? $args{REV} : undef;
  385. my $key = $recursive ? 'RLIST' : 'LIST';
  386. # Find out last changed revision of the current URL
  387. $rev = $self->svninfo (FLAG => 'Last Changed Rev', REV => $rev);
  388. return () if not $rev;
  389. # Get directory listing for the current URL at the last changed revision
  390. if (not exists $self->{$key}{$rev}) {
  391. my $rc;
  392. my @list = map {chomp; $_} &run_command (
  393. [qw/svn list -r/, $rev, ($recursive ? '-R' : ()), $self->url_peg],
  394. METHOD => 'qx', ERROR => 'ignore', DEVNULL => 1, RC => \$rc,
  395. );
  396. $self->{$key}{$rev} = $rc ? undef : \@list;
  397. }
  398. return (defined ($self->{$key}{$rev}) ? @{ $self->{$key}{$rev} } : undef);
  399. }
  400. # ------------------------------------------------------------------------------
  401. # SYNOPSIS
  402. # @list = $cm_url->branch_list ($rev);
  403. #
  404. # DESCRIPTION
  405. # The method returns a list of branches in the current project, assuming the
  406. # FCM naming convention. If $rev if specified, it returns the list of
  407. # branches at $rev.
  408. # ------------------------------------------------------------------------------
  409. sub branch_list {
  410. my ($self, $rev) = @_;
  411. # Current URL must be a valid FCM project
  412. return if not $self->project;
  413. # Find out last changed revision of the current URL
  414. $rev = $self->svninfo (FLAG => 'Revision', REV => $rev);
  415. return () if not $rev;
  416. if (not exists $self->{BRANCH_LIST}{$rev}) {
  417. $self->{BRANCH_LIST}{$rev} = [];
  418. # Get URL of the project "branches/" sub-directory
  419. my $url = Fcm::CmUrl->new (URL => $self->project_url . '/branches');
  420. # List three levels underneath "branches/"
  421. # First level, i.e. dev, test, pkg, etc
  422. my @list1 = map {$url->url . '/' . $_} $url->svnlist (REV => $rev);
  423. @list1 = grep m#/$#, @list1;
  424. # Second level, i.e. user name, Shared, Rel or Config
  425. my @list2;
  426. for (@list1) {
  427. my $u = Fcm::CmUrl->new (URL => $_);
  428. my @list = $u->svnlist (REV => $rev);
  429. push @list2, map {$u->url . $_} @list;
  430. }
  431. # Third level, branch name
  432. for (@list2) {
  433. my $u = Fcm::CmUrl->new (URL => $_);
  434. my @list = map {s#/*$##; $_} $u->svnlist (REV => $rev);
  435. push @{ $self->{BRANCH_LIST}{$rev} }, map {$u->url . $_} @list;
  436. }
  437. }
  438. return @{ $self->{BRANCH_LIST}{$rev} };
  439. }
  440. # ------------------------------------------------------------------------------
  441. # SYNOPSIS
  442. # $self->_analyse_url ();
  443. #
  444. # DESCRIPTION
  445. # The method analyses the current URL, breaking it up into the project
  446. # (substring of URL up to the slash before "trunk", "branches" or "tags"),
  447. # branch name ("trunk", "branches/<type>/<id>/<name>" or "tags/<name>") and
  448. # the sub-directory below the top of the project sub-tree. It re-sets the
  449. # corresponding interal variables.
  450. # ------------------------------------------------------------------------------
  451. sub _analyse_url {
  452. my $self = shift;
  453. my ($url, $project, $branch, $subdir, $pegrev);
  454. # Check that URL is set
  455. $url = $self->url_peg;
  456. return if not $url;
  457. return if not $self->is_url;
  458. # Extract from URL the peg revision
  459. $pegrev = $1 if $url =~ s/@($rev_pattern)$//i;
  460. if ($url =~ m#^(.*?)/+(trunk|branches|tags)(?:/+(.*))?/*$#) {
  461. # URL is under the "trunk", a branch or a tag
  462. $project = $1;
  463. my ($branch_id, $remain) = ($2, $3);
  464. $remain = '' if not defined $remain;
  465. if ($branch_id eq 'trunk') {
  466. # URL under the "trunk"
  467. $branch = 'trunk';
  468. } else {
  469. # URL under a branch or a tag
  470. $branch = $branch_id;
  471. # Assume "3 sub-directories", FCM branch naming convention
  472. for (1 .. 3) {
  473. if ($remain =~ s#^([^/]+)(?:/+|$)##) {
  474. $branch .= '/' . $1;
  475. } else {
  476. $branch = undef;
  477. last;
  478. }
  479. }
  480. }
  481. $subdir = $remain ? $remain : '' if $branch;
  482. } else {
  483. # URL is at some level above the "trunk", a branch or a tag
  484. # Use "svn ls" to determine whether it is a project URL
  485. my @list = $self->svnlist (REV => ($pegrev ? $pegrev : 'HEAD'));
  486. my %lines = map {chomp $_; ($_, 1)} @list;
  487. # A project URL should have the "trunk", "branches" and "tags" directories
  488. ($project = $url) =~ s#/*$##
  489. if $lines{'trunk/'} and $lines{'branches/'} and $lines{'tags/'};
  490. }
  491. $self->{PROJECT} = $project;
  492. $self->{BRANCH} = $branch;
  493. $self->{SUBDIR} = $subdir;
  494. $self->{PEGREV} = $pegrev;
  495. $self->{ANALYSED} = 1;
  496. return;
  497. }
  498. # ------------------------------------------------------------------------------
  499. # SYNOPSIS
  500. # $url = $cm_url->root ();
  501. #
  502. # DESCRIPTION
  503. # The method returns the repository root of the current URL.
  504. # ------------------------------------------------------------------------------
  505. sub root {
  506. my $self = shift;
  507. return $self->svninfo (FLAG => 'Repository Root');
  508. }
  509. # ------------------------------------------------------------------------------
  510. # SYNOPSIS
  511. # $url = $cm_url->project_url_peg ();
  512. # $cm_url->project_url_peg ($url);
  513. #
  514. # DESCRIPTION
  515. # The method returns the URL@PEG of the "project" part of the current URL. If
  516. # an argument is specified, the URL of the "project" part and the peg
  517. # revision of the current URL are re-set.
  518. # ------------------------------------------------------------------------------
  519. sub project_url_peg {
  520. my $self = shift;
  521. if (@_) {
  522. my $url = shift;
  523. # Re-construct URL is necessary
  524. if (! $self->project_url_peg or $url ne $self->project_url_peg) {
  525. my $pegrev = ($url =~ s/@($rev_pattern)$//i) ? $1 : '';
  526. $url .= '/' . $self->branch if $self->branch;
  527. $url .= '/' . $self->subdir if $self->subdir;
  528. $url .= '@' . $pegrev if $pegrev;
  529. $self->url_peg ($url);
  530. }
  531. }
  532. $self->_analyse_url () if not $self->{ANALYSED};
  533. return $self->{PROJECT} . ($self->pegrev ? '@' . $self->pegrev : '');
  534. }
  535. # ------------------------------------------------------------------------------
  536. # SYNOPSIS
  537. # $url = $cm_url->project_url ();
  538. # $cm_url->project_url ($url);
  539. #
  540. # DESCRIPTION
  541. # The method returns the URL of the "project" part of the current URL. If an
  542. # argument is specified, the URL of the "project" part of the current URL is
  543. # re-set.
  544. # ------------------------------------------------------------------------------
  545. sub project_url {
  546. my $self = shift;
  547. if (@_) {
  548. my $url = shift;
  549. $url =~ s/@($rev_pattern)$//i;
  550. # Re-construct URL is necessary
  551. if (! $self->project_url or $url ne $self->project_url) {
  552. $url .= '/' . $self->branch if $self->branch;
  553. $url .= '/' . $self->subdir if $self->subdir;
  554. $self->url ($url);
  555. }
  556. }
  557. $self->_analyse_url () if not $self->{ANALYSED};
  558. return $self->{PROJECT};
  559. }
  560. # ------------------------------------------------------------------------------
  561. # SYNOPSIS
  562. # $path = $cm_url->project_path ();
  563. # $cm_url->project_path ($path);
  564. #
  565. # DESCRIPTION
  566. # The method returns the path of the "project" part of the current URL. If an
  567. # argument is specified, the path of the "project" part of the current URL is
  568. # re-set.
  569. # ------------------------------------------------------------------------------
  570. sub project_path {
  571. my $self = shift;
  572. # Repository root
  573. my $root = $self->root;
  574. $root = substr (
  575. $self->project_url,
  576. 0,
  577. length ($self->project_url) - length ($self->project) - 1
  578. ) if not $root;
  579. if (@_) {
  580. my $path = shift;
  581. # Re-construct URL is necessary
  582. if (! $self->project_path or $path ne $self->project_path) {
  583. $path .= '/' . $self->branch if $self->branch;
  584. $path .= '/' . $self->subdir if $self->subdir;
  585. $self->path ($path);
  586. }
  587. }
  588. $self->_analyse_url () if not $self->{ANALYSED};
  589. return substr ($self->{PROJECT}, length ($root));
  590. }
  591. # ------------------------------------------------------------------------------
  592. # SYNOPSIS
  593. # $name = $cm_url->project ();
  594. # $cm_url->project ($name);
  595. #
  596. # DESCRIPTION
  597. # The method returns the basename of the "project" part of the current URL.
  598. # If an argument is specified, the basename of the "project" part of the
  599. # current URL is re-set.
  600. # ------------------------------------------------------------------------------
  601. sub project {
  602. my $self = shift;
  603. if (@_) {
  604. my $name = shift;
  605. # Re-construct URL is necessary
  606. if (! $self->project or $name ne $self->project) {
  607. my $url = '';
  608. if ($self->project) {
  609. $url = $self->project;
  610. $url =~ s#/[^/]+$##;
  611. } else {
  612. $url = $self->root;
  613. }
  614. $url .= '/' . $name;
  615. $url .= '/' . $self->branch if $self->branch;
  616. $url .= '/' . $self->subdir if $self->subdir;
  617. $url .= '@' . $self->pegrev if $self->pegrev;
  618. $self->url_peg ($url);
  619. }
  620. }
  621. $self->_analyse_url () if not $self->{ANALYSED};
  622. my $name = $self->{PROJECT};
  623. $name =~ s#^.*/([^/]+)$#$1# if $name;
  624. return $name;
  625. }
  626. # ------------------------------------------------------------------------------
  627. # SYNOPSIS
  628. # $url = $cm_url->branch_url_peg ();
  629. # $cm_url->branch_url_peg ($url);
  630. #
  631. # DESCRIPTION
  632. # The method returns the URL@PEG of the "branch" part of the current URL. If
  633. # an argument is specified, the URL@PEG of the "branch" part of the current
  634. # URL is re-set.
  635. # ------------------------------------------------------------------------------
  636. sub branch_url_peg {
  637. my $self = shift;
  638. if (@_) {
  639. my $url = shift;
  640. # Re-construct URL is necessary
  641. if (! $self->branch_url_peg or $url ne $self->branch_url_peg) {
  642. my $pegrev = ($url =~ s/@($rev_pattern)$//i) ? $1 : '';
  643. $url .= '/' . $self->subdir if $self->subdir;
  644. $url .= '@' . $pegrev if $pegrev;
  645. $self->url_peg ($url);
  646. }
  647. }
  648. $self->_analyse_url () if not $self->{ANALYSED};
  649. return $self->project_url . '/' . $self->branch .
  650. ($self->pegrev ? '@' . $self->pegrev : '');
  651. }
  652. # ------------------------------------------------------------------------------
  653. # SYNOPSIS
  654. # $url = $cm_url->branch_url ();
  655. # $cm_url->branch_url ($url);
  656. #
  657. # DESCRIPTION
  658. # The method returns the URL of the "branch" part of the current URL. If an
  659. # argument is specified, the URL of the "branch" part of the current URL is
  660. # re-set.
  661. # ------------------------------------------------------------------------------
  662. sub branch_url {
  663. my $self = shift;
  664. if (@_) {
  665. my $url = shift;
  666. $url =~ s/@($rev_pattern)$//i;
  667. # Re-construct URL is necessary
  668. if (! $self->branch_url or $url ne $self->branch_url) {
  669. $url .= '/' . $self->subdir if $self->subdir;
  670. $self->url ($url);
  671. }
  672. }
  673. $self->_analyse_url () if not $self->{ANALYSED};
  674. return $self->project_url . '/' . $self->branch;
  675. }
  676. # ------------------------------------------------------------------------------
  677. # SYNOPSIS
  678. # $path = $cm_url->branch_path ();
  679. # $cm_url->branch_path ($path);
  680. #
  681. # DESCRIPTION
  682. # The method returns the path of the "branch" part of the current URL. If an
  683. # argument is specified, the path of the "branch" part of the current URL is
  684. # re-set.
  685. # ------------------------------------------------------------------------------
  686. sub branch_path {
  687. my $self = shift;
  688. if (@_) {
  689. my $path = shift;
  690. # Re-construct URL is necessary
  691. if (! $self->branch_path or $path ne $self->branch_path) {
  692. $path .= '/' . $self->subdir if $self->subdir;
  693. $self->path ($path);
  694. }
  695. }
  696. $self->_analyse_url () if not $self->{ANALYSED};
  697. return ($self->branch ? $self->project_path . '/' . $self->branch : undef);
  698. }
  699. # ------------------------------------------------------------------------------
  700. # SYNOPSIS
  701. # $branch = $cm_url->branch ();
  702. # $cm_url->branch ($branch);
  703. #
  704. # DESCRIPTION
  705. # The method returns the "branch" part of the current URL. If an argument is
  706. # specified, the "branch" part of the current URL is re-set.
  707. # ------------------------------------------------------------------------------
  708. sub branch {
  709. my $self = shift;
  710. if (@_) {
  711. my $branch = shift;
  712. # Re-construct URL is necessary
  713. if (! $self->branch or $branch ne $self->branch) {
  714. my $url = $self->project_url;
  715. $url .= '/' . $branch;
  716. $url .= '/' . $self->subdir if $self->subdir;
  717. $self->url ($url);
  718. }
  719. }
  720. $self->_analyse_url () if not $self->{ANALYSED};
  721. return $self->{BRANCH};
  722. }
  723. # ------------------------------------------------------------------------------
  724. # SYNOPSIS
  725. # $string = $obj->branch_owner;
  726. #
  727. # DESCRIPTION
  728. # This method returns the owner of the branch.
  729. # ------------------------------------------------------------------------------
  730. sub branch_owner {
  731. my $self = shift;
  732. my $return;
  733. if ($self->is_branch and $self->branch_url =~ m#/([^/]+)/[^/]+/*$#) {
  734. my $user = $1;
  735. $return = $user;
  736. }
  737. return $return;
  738. }
  739. # ------------------------------------------------------------------------------
  740. # SYNOPSIS
  741. # $flag = $cm_url->is_trunk ();
  742. #
  743. # DESCRIPTION
  744. # The method returns true if the the current URL is (a sub-tree of) the trunk.
  745. # ------------------------------------------------------------------------------
  746. sub is_trunk {
  747. my $self = shift;
  748. $self->_analyse_url () if not $self->{ANALYSED};
  749. return ($self->branch and $self->branch eq 'trunk');
  750. }
  751. # ------------------------------------------------------------------------------
  752. # SYNOPSIS
  753. # $flag = $cm_url->is_branch ();
  754. #
  755. # DESCRIPTION
  756. # The method returns true if the the current URL is (a sub-tree of) a branch.
  757. # ------------------------------------------------------------------------------
  758. sub is_branch {
  759. my $self = shift;
  760. $self->_analyse_url () if not $self->{ANALYSED};
  761. return ($self->branch and $self->branch =~ m#^branches/#);
  762. }
  763. # ------------------------------------------------------------------------------
  764. # SYNOPSIS
  765. # $flag = $cm_url->is_tag ();
  766. #
  767. # DESCRIPTION
  768. # The method returns true if the the current URL is (a sub-tree of) a tag.
  769. # ------------------------------------------------------------------------------
  770. sub is_tag {
  771. my $self = shift;
  772. $self->_analyse_url () if not $self->{ANALYSED};
  773. return ($self->branch and $self->branch =~ m#^tags/#);
  774. }
  775. # ------------------------------------------------------------------------------
  776. # SYNOPSIS
  777. # $subdir = $cm_url->subdir ();
  778. # $cm_url->subdir ($subdir);
  779. #
  780. # DESCRIPTION
  781. # The method returns the "subdir" part of the current URL. If an argument is
  782. # specified, the "subdir" part of the current URL is re-set.
  783. # ------------------------------------------------------------------------------
  784. sub subdir {
  785. my $self = shift;
  786. if (@_) {
  787. my $subdir = shift;
  788. # Re-construct URL is necessary
  789. if (! $self->subdir or $subdir ne $self->subdir) {
  790. my $url = $self->project_url;
  791. $url .= '/' . $self->branch if $self->branch;
  792. $url .= '/' . $subdir if $subdir;
  793. $self->url ($url);
  794. }
  795. }
  796. $self->_analyse_url () if not $self->{ANALYSED};
  797. return $self->{SUBDIR};
  798. }
  799. # ------------------------------------------------------------------------------
  800. # SYNOPSIS
  801. # $url = $cm_url->url ();
  802. # $cm_url->url ($url);
  803. #
  804. # DESCRIPTION
  805. # The method returns the URL without the "peg revision" part. If an argument
  806. # is specified, the URL is re-set without modifying the "peg revision" part.
  807. # ------------------------------------------------------------------------------
  808. sub url {
  809. my $self = shift;
  810. if (@_) {
  811. my $url = shift;
  812. $url =~ s/@($rev_pattern)$//i;
  813. # Re-construct URL if necessary
  814. if (! $self->url or $url ne $self->url) {
  815. $self->url_peg ($url . ($self->pegrev ? '@' . $self->pegrev : ''));
  816. }
  817. }
  818. $self->_analyse_url () if not $self->{ANALYSED};
  819. (my $url = $self->url_peg) =~ s/@($rev_pattern)$//i;
  820. return $url;
  821. }
  822. # ------------------------------------------------------------------------------
  823. # SYNOPSIS
  824. # $path = $cm_url->path ();
  825. # $cm_url->path ($path);
  826. #
  827. # DESCRIPTION
  828. # The method returns the "path" part of the URL (i.e. URL without the
  829. # "root" part). If an argument is specified, the "path" part of the URL is
  830. # re-set.
  831. # ------------------------------------------------------------------------------
  832. sub path {
  833. my $self = shift;
  834. # Repository root
  835. my $root = $self->root;
  836. $root = substr (
  837. $self->project_url,
  838. 0,
  839. length ($self->project_url) - length ($self->project) - 1
  840. ) if not $root;
  841. if (@_) {
  842. my $path = shift;
  843. $path =~ s/@($rev_pattern)$//i;
  844. # Re-construct URL is necessary
  845. if (! $self->path or $path ne $self->path) {
  846. my $url = ($root . (substr ($path, 0, 1) eq '/' ? '' : '/') . $path);
  847. $self->url ($url);
  848. }
  849. }
  850. $self->_analyse_url () if not $self->{ANALYSED};
  851. return substr ($self->url, length ($root));
  852. }
  853. # ------------------------------------------------------------------------------
  854. # SYNOPSIS
  855. # $path = $cm_url->path_peg ();
  856. # $cm_url->path_peg ($path);
  857. #
  858. # DESCRIPTION
  859. # The method returns the PATH@PEG part of the URL (i.e. URL without the
  860. # "root" part). If an argument is specified, the PATH@PEG part of the URL is
  861. # re-set.
  862. # ------------------------------------------------------------------------------
  863. sub path_peg {
  864. my $self = shift;
  865. # Repository root
  866. my $root = $self->root;
  867. $root = substr (
  868. $self->project_url,
  869. 0,
  870. length ($self->project_url) - length ($self->project) - 1
  871. ) if not $root;
  872. if (@_) {
  873. my $path = shift;
  874. # Re-construct URL is necessary
  875. if (! $self->path_peg or $path ne $self->path_peg) {
  876. my $url = ($root . (substr ($path, 0, 1) eq '/' ? '' : '/') . $path);
  877. $self->url_peg ($url);
  878. }
  879. }
  880. $self->_analyse_url () if not $self->{ANALYSED};
  881. return substr ($self->url_peg, length ($root));
  882. }
  883. # ------------------------------------------------------------------------------
  884. # SYNOPSIS
  885. # $rev = $cm_url->pegrev ();
  886. # $cm_url->pegrev ($rev);
  887. #
  888. # DESCRIPTION
  889. # The method returns the "peg revision" part of the current URL. If an
  890. # argument is specified, the "peg revision" part of the current URL is
  891. # re-set.
  892. # ------------------------------------------------------------------------------
  893. sub pegrev {
  894. my $self = shift;
  895. if (@_) {
  896. my $pegrev = shift;
  897. # Re-construct URL is necessary
  898. if (! $self->pegrev or $pegrev ne $self->pegrev) {
  899. $self->url_peg ($self->url . ($pegrev ? '@' . $pegrev : ''));
  900. }
  901. }
  902. $self->_analyse_url () if not $self->{ANALYSED};
  903. return $self->{PEGREV};
  904. }
  905. # ------------------------------------------------------------------------------
  906. 1;
  907. __END__