CmBranch.pm 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217
  1. # ------------------------------------------------------------------------------
  2. # NAME
  3. # Fcm::CmBranch
  4. #
  5. # DESCRIPTION
  6. # This class contains methods for manipulating a branch. It is a sub-class of
  7. # Fcm::CmUrl, and inherits all methods from that class.
  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::CmBranch;
  15. @ISA = qw(Fcm::CmUrl);
  16. # Standard pragma
  17. use warnings;
  18. use strict;
  19. # Standard modules
  20. use Carp;
  21. use File::Spec;
  22. # FCM component modules
  23. use Fcm::CmCommitMessage;
  24. use Fcm::CmUrl;
  25. use Fcm::Config;
  26. use Fcm::Interactive;
  27. use Fcm::Keyword;
  28. use Fcm::Util qw/run_command e_report w_report svn_date/;
  29. my @properties = (
  30. 'CREATE_REV', # revision at which the branch is created
  31. 'DELETE_REV', # revision at which the branch is deleted
  32. 'PARENT', # reference to parent branch Fcm::CmBranch
  33. 'ANCESTOR', # list of common ancestors with other branches
  34. # key = URL, value = ancestor Fcm::CmBranch
  35. 'LAST_MERGE', # list of last merges from branches
  36. # key = URL@REV, value = [TARGET, UPPER, LOWER]
  37. 'AVAIL_MERGE', # list of available revisions for merging
  38. # key = URL@REV, value = [REV ...]
  39. 'CHILDREN', # list of children of this branch
  40. 'SIBLINGS', # list of siblings of this branch
  41. );
  42. # ------------------------------------------------------------------------------
  43. # SYNOPSIS
  44. # $cm_branch = Fcm::CmBranch->new (URL => $url,);
  45. #
  46. # DESCRIPTION
  47. # This method constructs a new instance of the Fcm::CmBranch class.
  48. #
  49. # ARGUMENTS
  50. # URL - URL of a branch
  51. # ------------------------------------------------------------------------------
  52. sub new {
  53. my $this = shift;
  54. my %args = @_;
  55. my $class = ref $this || $this;
  56. my $self = Fcm::CmUrl->new (%args);
  57. $self->{$_} = undef for (@properties);
  58. bless $self, $class;
  59. return $self;
  60. }
  61. # ------------------------------------------------------------------------------
  62. # SYNOPSIS
  63. # $url = $cm_branch->url_peg;
  64. # $cm_branch->url_peg ($url);
  65. #
  66. # DESCRIPTION
  67. # This method returns/sets the current URL.
  68. # ------------------------------------------------------------------------------
  69. sub url_peg {
  70. my $self = shift;
  71. if (@_) {
  72. if (! $self->{URL} or $_[0] ne $self->{URL}) {
  73. # Re-set URL and other essential variables in the SUPER-class
  74. $self->SUPER::url_peg (@_);
  75. # Re-set essential variables
  76. $self->{$_} = undef for (@properties);
  77. }
  78. }
  79. return $self->{URL};
  80. }
  81. # ------------------------------------------------------------------------------
  82. # SYNOPSIS
  83. # $rev = $cm_branch->create_rev;
  84. #
  85. # DESCRIPTION
  86. # This method returns the revision at which the branch was created.
  87. # ------------------------------------------------------------------------------
  88. sub create_rev {
  89. my $self = shift;
  90. if (not $self->{CREATE_REV}) {
  91. return unless $self->url_exists ($self->pegrev);
  92. # Use "svn log" to find out the first revision of the branch
  93. my %log = $self->svnlog (STOP_ON_COPY => 1);
  94. # Look at log in ascending order
  95. my $rev = (sort {$a <=> $b} keys %log) [0];
  96. my $paths = $log{$rev}{paths};
  97. # Get revision when URL is first added to the repository
  98. if (exists $paths->{$self->branch_path}) {
  99. $self->{CREATE_REV} = $rev if $paths->{$self->branch_path}{action} eq 'A';
  100. }
  101. }
  102. return $self->{CREATE_REV};
  103. }
  104. # ------------------------------------------------------------------------------
  105. # SYNOPSIS
  106. # $parent = $cm_branch->parent;
  107. #
  108. # DESCRIPTION
  109. # This method returns the parent (a Fcm::CmBranch object) of the current
  110. # branch.
  111. # ------------------------------------------------------------------------------
  112. sub parent {
  113. my $self = shift;
  114. if (not $self->{PARENT}) {
  115. # Use the log to find out the parent revision
  116. my %log = $self->svnlog (REV => $self->create_rev);
  117. if (exists $log{paths}{$self->branch_path}) {
  118. my $path = $log{paths}{$self->branch_path};
  119. if ($path->{action} eq 'A') {
  120. if (exists $path->{'copyfrom-path'}) {
  121. # Current branch is copied from somewhere, set the source as the parent
  122. my $url = $self->root . $path->{'copyfrom-path'};
  123. my $rev = $path->{'copyfrom-rev'};
  124. $self->{PARENT} = Fcm::CmBranch->new (URL => $url . '@' . $rev);
  125. } else {
  126. # Current branch is not copied from somewhere
  127. $self->{PARENT} = $self;
  128. }
  129. }
  130. }
  131. }
  132. return $self->{PARENT};
  133. }
  134. # ------------------------------------------------------------------------------
  135. # SYNOPSIS
  136. # $rev = $cm_branch->delete_rev;
  137. #
  138. # DESCRIPTION
  139. # This method returns the revision at which the branch was deleted.
  140. # ------------------------------------------------------------------------------
  141. sub delete_rev {
  142. my $self = shift;
  143. if (not $self->{DELETE_REV}) {
  144. return if $self->url_exists ('HEAD');
  145. # Container of the current URL
  146. (my $dir_url = $self->branch_url) =~ s#/+[^/]+/*$##;
  147. # Use "svn log" on the container between a revision where the branch exists
  148. # and the HEAD
  149. my $dir = Fcm::CmUrl->new (URL => $dir_url);
  150. my %log = $dir->svnlog (
  151. REV => ['HEAD', ($self->pegrev ? $self->pegrev : $self->create_rev)],
  152. );
  153. # Go through the log to see when branch no longer exists
  154. for my $rev (sort {$a <=> $b} keys %log) {
  155. next unless exists $log{$rev}{paths}{$self->branch_path} and
  156. $log{$rev}{paths}{$self->branch_path}{action} eq 'D';
  157. $self->{DELETE_REV} = $rev;
  158. last;
  159. }
  160. }
  161. return $self->{DELETE_REV};
  162. }
  163. # ------------------------------------------------------------------------------
  164. # SYNOPSIS
  165. # $flag = $cm_branch->is_child_of ($branch);
  166. #
  167. # DESCRIPTION
  168. # This method returns true if the current branch is a child of $branch.
  169. # ------------------------------------------------------------------------------
  170. sub is_child_of {
  171. my ($self, $branch) = @_;
  172. # The trunk cannot be a child branch
  173. return if $self->is_trunk;
  174. # If $branch is a branch, use name of $self to see when it is created
  175. if ($branch->is_branch and $self->url =~ m#/r(\d+)_[^/]+/*$#) {
  176. my $rev = $1;
  177. # $self can only be a child if it is copied from a revision > the create
  178. # revision of $branch
  179. return if $rev < $branch->create_rev;
  180. }
  181. return if $self->parent->url ne $branch->url;
  182. # If $branch is a branch, ensure that it is created before $self
  183. return if $branch->is_branch and $self->create_rev <= $branch->create_rev;
  184. return 1;
  185. }
  186. # ------------------------------------------------------------------------------
  187. # SYNOPSIS
  188. # $flag = $cm_branch->is_sibling_of ($branch);
  189. #
  190. # DESCRIPTION
  191. # This method returns true if the current branch is a sibling of $branch.
  192. # ------------------------------------------------------------------------------
  193. sub is_sibling_of {
  194. my ($self, $branch) = @_;
  195. # The trunk cannot be a sibling branch
  196. return if $branch->is_trunk;
  197. return if $self->parent->url ne $branch->parent->url;
  198. # If the parent is a branch, ensure they are actually the same branch
  199. return if $branch->parent->is_branch and
  200. $self->parent->create_rev != $branch->parent->create_rev;
  201. return 1;
  202. }
  203. # ------------------------------------------------------------------------------
  204. # SYNOPSIS
  205. # $self->_get_relatives ($relation);
  206. #
  207. # DESCRIPTION
  208. # This method sets the $self->{$relation} variable by inspecting the list of
  209. # branches at the current revision of the current branch. $relation can be
  210. # either "CHILDREN" or "SIBLINGS".
  211. # ------------------------------------------------------------------------------
  212. sub _get_relatives {
  213. my ($self, $relation) = @_;
  214. my @branch_list = $self->branch_list;
  215. $self->{$relation} = [];
  216. # If we are searching for CHILDREN, get list of SIBLINGS, and vice versa
  217. my $other = ($relation eq 'CHILDREN' ? 'SIBLINGS' : 'CHILDREN');
  218. my %other_list;
  219. if ($self->{$other}) {
  220. %other_list = map {$_->url, 1} @{ $self->{$other} };
  221. }
  222. for my $u (@branch_list) {
  223. # Ignore URL of current branch and its parent
  224. next if $u eq $self->url;
  225. next if $self->is_branch and $u eq $self->parent->url;
  226. # Ignore if URL is a branch detected to be another type of relative
  227. next if exists $other_list{$u};
  228. # Construct new Fcm::CmBranch object from branch URL
  229. my $url = $u . ($self->pegrev ? '@' . $self->pegrev : '');
  230. my $branch = Fcm::CmBranch->new (URL => $url);
  231. # Test whether $branch is a relative we are looking for
  232. if ($relation eq 'CHILDREN') {
  233. push @{ $self->{$relation} }, $branch if $branch->is_child_of ($self);
  234. } else {
  235. push @{ $self->{$relation} }, $branch if $branch->is_sibling_of ($self);
  236. }
  237. }
  238. return;
  239. }
  240. # ------------------------------------------------------------------------------
  241. # SYNOPSIS
  242. # @children = $cm_branch->children;
  243. #
  244. # DESCRIPTION
  245. # This method returns a list of children (Fcm::CmBranch objects) of the
  246. # current branch that exists in the current revision.
  247. # ------------------------------------------------------------------------------
  248. sub children {
  249. my $self = shift;
  250. $self->_get_relatives ('CHILDREN') if not $self->{CHILDREN};
  251. return @{ $self->{CHILDREN} };
  252. }
  253. # ------------------------------------------------------------------------------
  254. # SYNOPSIS
  255. # @siblings = $cm_branch->siblings;
  256. #
  257. # DESCRIPTION
  258. # This method returns a list of siblings (Fcm::CmBranch objects) of the
  259. # current branch that exists in the current revision.
  260. # ------------------------------------------------------------------------------
  261. sub siblings {
  262. my $self = shift;
  263. $self->_get_relatives ('SIBLINGS') if not $self->{SIBLINGS};
  264. return @{ $self->{SIBLINGS} };
  265. }
  266. # ------------------------------------------------------------------------------
  267. # SYNOPSIS
  268. # $ancestor = $cm_branch->ancestor ($branch);
  269. #
  270. # DESCRIPTION
  271. # This method returns the common ancestor (a Fcm::CmBranch object) of a
  272. # specified $branch and the current branch. The argument $branch must be a
  273. # Fcm::CmBranch object. Both the current branch and $branch are assumed to be
  274. # in the same project.
  275. # ------------------------------------------------------------------------------
  276. sub ancestor {
  277. my ($self, $branch) = @_;
  278. if (not exists $self->{ANCESTOR}{$branch->url_peg}) {
  279. if ($self->url_peg eq $branch->url_peg) {
  280. $self->{ANCESTOR}{$branch->url_peg} = $self;
  281. } else {
  282. # Get family tree of current branch, from trunk to current branch
  283. my @this_family = ($self);
  284. while (not $this_family [0]->is_trunk) {
  285. unshift @this_family, $this_family [0]->parent;
  286. }
  287. # Get family tree of $branch, from trunk to $branch
  288. my @that_family = ($branch);
  289. while (not $that_family [0]->is_trunk) {
  290. unshift @that_family, $that_family [0]->parent;
  291. }
  292. # Find common ancestor from list of parents
  293. my $ancestor = undef;
  294. while (not $ancestor) {
  295. # $this and $that should both start as some revisions on the trunk.
  296. # Walk down a generation each time it loops around.
  297. my $this = shift @this_family;
  298. my $that = shift @that_family;
  299. if ($this->url eq $that->url) {
  300. if ($this->is_trunk or $this->create_rev eq $that->create_rev) {
  301. # $this and $that are the same branch
  302. if (@this_family and @that_family) {
  303. # More generations in both branches, try comparing the next
  304. # generations.
  305. next;
  306. } else {
  307. # End of lineage in one of the branches, ancestor is at the lower
  308. # revision of the current URL.
  309. if ($this->pegrev and $that->pegrev) {
  310. $ancestor = $this->pegrev < $that->pegrev ? $this : $that;
  311. } else {
  312. $ancestor = $this->pegrev ? $this : $that;
  313. }
  314. }
  315. } else {
  316. # Despite the same URL, $this and $that are different branches as
  317. # they are created at different revisions. The ancestor must be the
  318. # parent with the lower revision. (This should not occur at the
  319. # start.)
  320. $ancestor = $this->parent->pegrev < $that->parent->pegrev
  321. ? $this->parent : $that->parent;
  322. }
  323. } else {
  324. # Different URLs, ancestor must be the parent with the lower revision.
  325. # (This should not occur at the start.)
  326. $ancestor = $this->parent->pegrev < $that->parent->pegrev
  327. ? $this->parent : $that->parent;
  328. }
  329. }
  330. $self->{ANCESTOR}{$branch->url_peg} = $ancestor;
  331. }
  332. }
  333. return $self->{ANCESTOR}{$branch->url_peg};
  334. }
  335. # ------------------------------------------------------------------------------
  336. # SYNOPSIS
  337. # ($target, $upper, $lower) = $cm_branch->last_merge_from (
  338. # $branch, $stop_on_copy,
  339. # );
  340. #
  341. # DESCRIPTION
  342. # This method returns a 3-element list with information of the last merge
  343. # into the current branch from a specified $branch. The first element in the
  344. # list $target (a Fcm::CmBranch object) is the target at which the merge was
  345. # performed. (This can be the current branch or a parent branch up to the
  346. # common ancestor with the specified $branch.) The second and third elements,
  347. # $upper and $lower, (both Fcm::CmBranch objects), are the upper and lower
  348. # ends of the source delta. If there is no merge from $branch into the
  349. # current branch from their common ancestor to the current revision, this
  350. # method will return an empty list. If $stop_on_copy is specified, it ignores
  351. # merges from parents of $branch, and merges into parents of the current
  352. # branch.
  353. # ------------------------------------------------------------------------------
  354. sub last_merge_from {
  355. my ($self, $branch, $stop_on_copy) = @_;
  356. if (not exists $self->{LAST_MERGE}{$branch->url_peg}) {
  357. # Get "log" of current branch down to the common ancestor
  358. my %log = $self->svnlog (
  359. REV => [
  360. ($self->pegrev ? $self->pegrev : 'HEAD'),
  361. $self->ancestor ($branch)->pegrev,
  362. ],
  363. STOP_ON_COPY => $stop_on_copy,
  364. );
  365. my $cr = $self;
  366. # Go down the revision log, checking for merge template messages
  367. REV: for my $rev (sort {$b <=> $a} keys %log) {
  368. # Loop each line of the log message at each revision
  369. my @msg = split /\n/, $log{$rev}{msg};
  370. # Also consider merges into parents of current branch
  371. $cr = $cr->parent if ($cr->is_branch and $rev < $cr->create_rev);
  372. for (@msg) {
  373. # Ignore unless log message matches a merge template
  374. next unless /Merged into \S+: (\S+) cf\. (\S+)/;
  375. # Upper $1 and lower $2 ends of the source delta
  376. my $u_path = $1;
  377. my $l_path = $2;
  378. # Add the root directory to the paths if necessary
  379. $u_path = '/' . $u_path if substr ($u_path, 0, 1) ne '/';
  380. $l_path = '/' . $l_path if substr ($l_path, 0, 1) ne '/';
  381. # Only consider merges with specified branch (and its parent)
  382. (my $path = $u_path) =~ s/@(\d+)$//;
  383. my $u_rev = $1;
  384. my $br = $branch;
  385. $br = $br->parent while (
  386. $br->is_branch and $u_rev < $br->create_rev and not $stop_on_copy
  387. );
  388. next unless $br->branch_path eq $path;
  389. # If $br is a parent of branch, ignore those merges with the parent
  390. # above the branch point of the current branch
  391. next if $br->pegrev and $br->pegrev < $u_rev;
  392. # Set the return values
  393. $self->{LAST_MERGE}{$branch->url_peg} = [
  394. Fcm::CmBranch->new (URL => $cr->url . '@' . $rev), # target
  395. Fcm::CmBranch->new (URL => $self->root . $u_path), # delta upper
  396. Fcm::CmBranch->new (URL => $self->root . $l_path), # delta lower
  397. ];
  398. last REV;
  399. }
  400. }
  401. }
  402. return (exists $self->{LAST_MERGE}{$branch->url_peg}
  403. ? @{ $self->{LAST_MERGE}{$branch->url_peg} } : ());
  404. }
  405. # ------------------------------------------------------------------------------
  406. # SYNOPSIS
  407. # @revs = $cm_branch->avail_merge_from ($branch[, $stop_on_copy]);
  408. #
  409. # DESCRIPTION
  410. # This method returns a list of revisions of a specified $branch, which are
  411. # available for merging into the current branch. If $stop_on_copy is
  412. # specified, it will not list available merges from the parents of $branch.
  413. # ------------------------------------------------------------------------------
  414. sub avail_merge_from {
  415. my ($self, $branch, $stop_on_copy) = @_;
  416. if (not exists $self->{AVAIL_MERGE}{$branch->url_peg}) {
  417. # Find out the revision of the upper delta at the last merge from $branch
  418. # If no merge is found, use revision of common ancestor with $branch
  419. my @last_merge = $self->last_merge_from ($branch);
  420. my $rev = $self->ancestor ($branch)->pegrev;
  421. $rev = $last_merge [1]->pegrev
  422. if @last_merge and $last_merge [1]->pegrev > $rev;
  423. # Get the "log" of the $branch down to $rev
  424. my %log = $branch->svnlog (
  425. REV => [($branch->pegrev ? $branch->pegrev : 'HEAD'), $rev],
  426. STOP_ON_COPY => $stop_on_copy,
  427. );
  428. # No need to include $rev itself, as it has already been merged
  429. delete $log{$rev};
  430. # No need to include the branch create revision
  431. delete $log{$branch->create_rev}
  432. if $branch->is_branch and exists $log{$branch->create_rev};
  433. if (keys %log) {
  434. # Check whether there is a latest merge from $self into $branch, if so,
  435. # all revisions of $branch below that merge should become unavailable
  436. my @last_merge_into = $branch->last_merge_from ($self);
  437. if (@last_merge_into) {
  438. for my $rev (keys %log) {
  439. delete $log{$rev} if $rev < $last_merge_into [0]->pegrev;
  440. }
  441. }
  442. }
  443. # Available merges include all revisions above the branch creation revision
  444. # or the revision of the last merge
  445. $self->{AVAIL_MERGE}{$branch->url_peg} = [sort {$b <=> $a} keys %log];
  446. }
  447. return @{ $self->{AVAIL_MERGE}{$branch->url_peg} };
  448. }
  449. # ------------------------------------------------------------------------------
  450. # SYNOPSIS
  451. # $lower = $cm_branch->base_of_merge_from ($branch);
  452. #
  453. # DESCRIPTION
  454. # This method returns the lower delta (a Fcm::CmBranch object) for the next
  455. # merge from $branch.
  456. # ------------------------------------------------------------------------------
  457. sub base_of_merge_from {
  458. my ($self, $branch) = @_;
  459. # Base is the ancestor if there is no merge between $self and $branch
  460. my $return = $self->ancestor ($branch);
  461. # Get configuration for the last merge from $branch to $self
  462. my @merge_from = $self->last_merge_from ($branch);
  463. # Use the upper delta of the last merge from $branch, as all revisions below
  464. # that have already been merged into the $self
  465. $return = $merge_from [1]
  466. if @merge_from and $merge_from [1]->pegrev > $return->pegrev;
  467. # Get configuration for the last merge from $self to $branch
  468. my @merge_into = $branch->last_merge_from ($self);
  469. # Use the upper delta of the last merge from $self, as the current revision
  470. # of $branch already contains changes of $self up to the peg revision of the
  471. # upper delta
  472. $return = $merge_into [1]
  473. if @merge_into and $merge_into [0]->pegrev > $return->pegrev;
  474. return $return;
  475. }
  476. # ------------------------------------------------------------------------------
  477. # SYNOPSIS
  478. # $flag = $cm_branch->allow_subdir_merge_from ($branch, $subdir);
  479. #
  480. # DESCRIPTION
  481. # This method returns true if a merge from the sub-directory $subdir in
  482. # $branch is allowed - i.e. it does not result in losing changes made in
  483. # $branch outside of $subdir.
  484. # ------------------------------------------------------------------------------
  485. sub allow_subdir_merge_from {
  486. my ($self, $branch, $subdir) = @_;
  487. # Get revision at last merge from $branch or ancestor
  488. my @merge_from = $self->last_merge_from ($branch);
  489. my $last = @merge_from ? $merge_from [1] : $self->ancestor ($branch);
  490. my $rev = $last->pegrev;
  491. my $return = 1;
  492. if ($branch->pegrev > $rev) {
  493. # Use "svn diff --summarize" to work out what's changed between last
  494. # merge/ancestor and current revision
  495. my $range = $branch->pegrev . ':' . $rev;
  496. my @out = &run_command (
  497. [qw/svn diff --summarize -r/, $range, $branch->url_peg], METHOD => 'qx',
  498. );
  499. # Returns false if there are changes outside of $subdir
  500. my $url = join ('/', $branch->url, $subdir);
  501. for my $line (@out) {
  502. chomp $line;
  503. $line = substr ($line, 7); # file name begins at column 7
  504. if ($line !~ m#^$url(?:/|$)#) {
  505. $return = 0;
  506. last;
  507. }
  508. }
  509. }
  510. return $return;
  511. }
  512. # ------------------------------------------------------------------------------
  513. # SYNOPSIS
  514. # $cm_branch->create (
  515. # SRC => $src,
  516. # TYPE => $type,
  517. # NAME => $name,
  518. # [PASSWORD => $password,]
  519. # [REV_FLAG => $rev_flag,]
  520. # [TICKET => \@tickets,]
  521. # [REV => $rev,]
  522. # [NON_INTERACTIVE => 1,]
  523. # [SVN_NON_INTERACTIVE => 1,]
  524. # );
  525. #
  526. # DESCRIPTION
  527. # This method creates a branch in a Subversion repository.
  528. #
  529. # OPTIONS
  530. # SRC - reference to a Fcm::CmUrl object.
  531. # TYPE - Specify the branch type. See help in "fcm branch" for
  532. # further information.
  533. # NAME - specify the name of the branch.
  534. # NON_INTERACTIVE - Do no interactive prompting, set SVN_NON_INTERACTIVE
  535. # to true automatically.
  536. # PASSWORD - specify the password for commit access.
  537. # REV - specify the operative revision of the source.
  538. # REV_FLAG - A flag to specify the behaviour of the prefix to the
  539. # branch name. See help in "fcm branch" for further
  540. # information.
  541. # SVN_NON_INTERACTIVE - Do no interactive prompting when running svn commit,
  542. # etc. This option is implied by NON_INTERACTIVE.
  543. # TICKET - Specify one or more related tickets for the branch.
  544. # ------------------------------------------------------------------------------
  545. sub create {
  546. my $self = shift;
  547. my %args = @_;
  548. # Options
  549. # ----------------------------------------------------------------------------
  550. # Compulsory options
  551. my $src = $args{SRC};
  552. my $type = $args{TYPE};
  553. my $name = $args{NAME};
  554. # Other options
  555. my $rev_flag = $args{REV_FLAG} ? $args{REV_FLAG} : 'NORMAL';
  556. my @tickets = exists $args{TICKET} ? @{ $args{TICKET} } : ();
  557. my $password = exists $args{PASSWORD} ? $args{PASSWORD} : undef;
  558. my $orev = exists $args{REV} ? $args{REV} : 'HEAD';
  559. my $non_interactive = exists $args{NON_INTERACTIVE}
  560. ? $args{NON_INTERACTIVE} : 0;
  561. my $svn_non_interactive = exists $args{SVN_NON_INTERACTIVE}
  562. ? $args{SVN_NON_INTERACTIVE} : 0;
  563. $svn_non_interactive = $non_interactive ? 1 : $svn_non_interactive;
  564. # Analyse the source URL
  565. # ----------------------------------------------------------------------------
  566. # Create branch from the trunk by default
  567. $src->branch ('trunk') if not $src->branch;
  568. # Remove "sub-directory" part from source URL
  569. $src->subdir ('') if $src->subdir;
  570. # Remove "peg revision" part because it does not work with "svn copy"
  571. $src->pegrev ('') if $src->pegrev;
  572. # Find out the URL and the last changed revision of the specified URL at the
  573. # specified operative revision
  574. my $url = $src->svninfo (FLAG => 'URL', REV => $orev);
  575. e_report $src->url, ': cannot determine the operative URL at revision ',
  576. $orev, ', abort.' if not $url;
  577. $src->url ($url) if $url ne $src->url;
  578. my $rev = $src->svninfo (FLAG => 'Last Changed Rev', REV => $orev);
  579. e_report $src->url, ': cannot determine the last changed rev at revision',
  580. $orev, ', abort.' if not $rev;
  581. # Warn user if last changed revision is not the specified revision
  582. w_report 'Warning: branch will be created from revision ', $rev,
  583. ', i.e. the last changed rev.'
  584. unless $orev and $orev eq $rev;
  585. # Determine the sub-directory names of the branch
  586. # ----------------------------------------------------------------------------
  587. my @branch_dirs = ('branches');
  588. # Split branch type flags into a hash table
  589. my %type_flags = ();
  590. $type_flags{$_} = 1 for ((split /$Fcm::Config::DELIMITER/, $type));
  591. # Branch sub-directory 1, development, test or package
  592. for my $flag (qw/DEV TEST PKG/) {
  593. if (exists $type_flags{$flag}) {
  594. push @branch_dirs, lc ($flag);
  595. last;
  596. }
  597. }
  598. # Branch sub-directory 2, user, share, configuration or release
  599. if (exists $type_flags{USER}) {
  600. die 'Unable to determine your user ID, abort' unless $self->config->user_id;
  601. push @branch_dirs, $self->config->user_id;
  602. } else {
  603. for my $flag (keys %Fcm::CmUrl::owner_keywords) {
  604. if (exists $type_flags{uc ($flag)}) {
  605. push @branch_dirs, $flag;
  606. last;
  607. }
  608. }
  609. }
  610. # Branch sub-directory 3, branch name
  611. # Prefix branch name with revision number/keyword if necessary
  612. my $prefix = '';
  613. if ($rev_flag ne 'NONE') {
  614. $prefix = $rev;
  615. # Attempt to replace revision number with a revision keyword if necessary
  616. if ($rev_flag eq 'NORMAL') {
  617. $prefix = (Fcm::Keyword::unexpand($src->url_peg(), $rev))[1];
  618. }
  619. # $prefix is still a revision number, add "r" in front of it
  620. $prefix = 'r' . $prefix if $prefix eq $rev;
  621. # Add an underscore before the branch name
  622. $prefix.= '_';
  623. }
  624. # Branch name
  625. push @branch_dirs, $prefix . $name;
  626. # Check whether the branch already exists, fail if so
  627. # ----------------------------------------------------------------------------
  628. # Construct the URL of the branch
  629. $self->project_url ($src->project_url);
  630. $self->branch (join ('/', @branch_dirs));
  631. # Check that branch does not already exists
  632. e_report $self->url, ': branch already exists, abort.' if $self->url_exists;
  633. # Message for the commit log
  634. # ----------------------------------------------------------------------------
  635. my @message = ('Created ' . $self->branch_path . ' from ' .
  636. $src->branch_path . '@' . $rev . '.' . "\n");
  637. # Add related Trac ticket links to commit log if set
  638. if (@tickets) {
  639. my $ticket_mesg = 'Relates to ticket' . (@tickets > 1 ? 's' : '');
  640. while (my $ticket = shift @tickets) {
  641. $ticket_mesg .= ' #' . $ticket;
  642. $ticket_mesg .= (@tickets > 1 ? ',' : ' and') if @tickets >= 1;
  643. }
  644. push @message, $ticket_mesg . ".\n";
  645. }
  646. # Create a temporary file for the commit log message
  647. my $ci_mesg = Fcm::CmCommitMessage->new;
  648. $ci_mesg->auto_mesg (\@message);
  649. $ci_mesg->ignore_mesg (['A' . ' ' x 4 . $self->url . "\n"]);
  650. my $logfile = $ci_mesg->edit_file (TEMP => 1, BATCH => $non_interactive);
  651. # Check with the user to see if he/she wants to go ahead
  652. # ----------------------------------------------------------------------------
  653. if (not $non_interactive) {
  654. my $reply = Fcm::Interactive::get_input(
  655. title => 'fcm branch',
  656. message => 'Would you like to go ahead and create this branch?',
  657. type => 'yn',
  658. default => 'n',
  659. );
  660. return unless $reply eq 'y';
  661. }
  662. # Ensure existence of container sub-directories of the branch
  663. # ----------------------------------------------------------------------------
  664. for my $i (0 .. $#branch_dirs - 1) {
  665. my $subdir = join ('/', @branch_dirs[0 .. $i]);
  666. my $subdir_url = Fcm::CmUrl->new (URL => $src->project_url . '/' . $subdir);
  667. # Check whether each sub-directory of the branch already exists,
  668. # if sub-directory does not exist, create it
  669. next if $subdir_url->url_exists;
  670. print 'Creating sub-directory: ', $subdir, "\n";
  671. my @command = (
  672. qw/svn mkdir/,
  673. '-m', 'Created ' . $subdir . ' directory.',
  674. ($svn_non_interactive ? '--non-interactive' : ()),
  675. (defined $password ? ('--password', $password) : ()),
  676. $subdir_url->url,
  677. );
  678. &run_command (\@command);
  679. }
  680. # Create the branch
  681. # ----------------------------------------------------------------------------
  682. {
  683. print 'Creating branch ', $self->url, ' ...', "\n";
  684. my @command = (
  685. qw/svn copy/,
  686. '-r', $rev,
  687. '-F', $logfile,
  688. ($svn_non_interactive ? '--non-interactive' : ()),
  689. (defined $password ? ('--password', $password) : ()),
  690. $src->url, $self->url,
  691. );
  692. &run_command (\@command);
  693. }
  694. return;
  695. }
  696. # ------------------------------------------------------------------------------
  697. # SYNOPSIS
  698. # $cm_branch->delete (
  699. # [NON_INTERACTIVE => 1,]
  700. # [PASSWORD => $password,]
  701. # [SVN_NON_INTERACTIVE => 1,]
  702. # );
  703. #
  704. # DESCRIPTION
  705. # This method deletes the current branch from the Subversion repository.
  706. #
  707. # OPTIONS
  708. # NON_INTERACTIVE - Do no interactive prompting, set SVN_NON_INTERACTIVE
  709. # to true automatically.
  710. # PASSWORD - specify the password for commit access.
  711. # SVN_NON_INTERACTIVE - Do no interactive prompting when running svn commit,
  712. # etc. This option is implied by NON_INTERACTIVE.
  713. # ------------------------------------------------------------------------------
  714. sub del {
  715. my $self = shift;
  716. my %args = @_;
  717. # Options
  718. # ----------------------------------------------------------------------------
  719. my $password = exists $args{PASSWORD} ? $args{PASSWORD} : undef;
  720. my $non_interactive = exists $args{NON_INTERACTIVE}
  721. ? $args{NON_INTERACTIVE} : 0;
  722. my $svn_non_interactive = exists $args{SVN_NON_INTERACTIVE}
  723. ? $args{SVN_NON_INTERACTIVE} : 0;
  724. $svn_non_interactive = $non_interactive ? 1 : $svn_non_interactive;
  725. # Ensure URL is a branch
  726. # ----------------------------------------------------------------------------
  727. e_report $self->url_peg, ': not a branch, abort.' if not $self->is_branch;
  728. # Message for the commit log
  729. # ----------------------------------------------------------------------------
  730. my @message = ('Deleted ' . $self->branch_path . '.' . "\n");
  731. # Create a temporary file for the commit log message
  732. my $ci_mesg = Fcm::CmCommitMessage->new;
  733. $ci_mesg->auto_mesg (\@message);
  734. $ci_mesg->ignore_mesg (['D' . ' ' x 4 . $self->url . "\n"]);
  735. my $logfile = $ci_mesg->edit_file (TEMP => 1, BATCH => $non_interactive);
  736. # Check with the user to see if he/she wants to go ahead
  737. # ----------------------------------------------------------------------------
  738. if (not $non_interactive) {
  739. my $mesg = '';
  740. my $user = $self->config->user_id;
  741. if ($user and $self->branch_owner ne $user) {
  742. $mesg .= "\n";
  743. if (exists $Fcm::CmUrl::owner_keywords{$self->branch_owner}) {
  744. my $type = $Fcm::CmUrl::owner_keywords{$self->branch_owner};
  745. $mesg .= '*** WARNING: YOU ARE DELETING A ' . uc ($type) .
  746. ' BRANCH.';
  747. } else {
  748. $mesg .= '*** WARNING: YOU ARE DELETING A BRANCH NOT OWNED BY YOU.';
  749. }
  750. $mesg .= "\n" .
  751. '*** Please ensure that you have the owner\'s permission.' .
  752. "\n\n";
  753. }
  754. $mesg .= 'Would you like to go ahead and delete this branch?';
  755. my $reply = Fcm::Interactive::get_input (
  756. title => 'fcm branch',
  757. message => $mesg,
  758. type => 'yn',
  759. default => 'n',
  760. );
  761. return unless $reply eq 'y';
  762. }
  763. # Delete branch if answer is "y" for "yes"
  764. # ----------------------------------------------------------------------------
  765. print 'Deleting branch ', $self->url, ' ...', "\n";
  766. my @command = (
  767. qw/svn delete/,
  768. '-F', $logfile,
  769. (defined $password ? ('--password', $password) : ()),
  770. ($svn_non_interactive ? '--non-interactive' : ()),
  771. $self->url,
  772. );
  773. &run_command (\@command);
  774. return;
  775. }
  776. # ------------------------------------------------------------------------------
  777. # SYNOPSIS
  778. # $cm_branch->display_info (
  779. # [SHOW_CHILDREN => 1],
  780. # [SHOW_OTHER => 1]
  781. # [SHOW_SIBLINGS => 1]
  782. # );
  783. #
  784. # DESCRIPTION
  785. # This method displays information of the current branch. If SHOW_CHILDREN is
  786. # set, it shows information of all current children branches of the current
  787. # branch. If SHOW_SIBLINGS is set, it shows information of siblings that have
  788. # been merged recently with the current branch. If SHOW_OTHER is set, it shows
  789. # information of custom/reverse merges.
  790. # ------------------------------------------------------------------------------
  791. sub display_info {
  792. my $self = shift;
  793. my %args = @_;
  794. # Arguments
  795. # ----------------------------------------------------------------------------
  796. my $show_children = exists $args{SHOW_CHILDREN} ? $args{SHOW_CHILDREN} : 0;
  797. my $show_other = exists $args{SHOW_OTHER } ? $args{SHOW_OTHER} : 0;
  798. my $show_siblings = exists $args{SHOW_SIBLINGS} ? $args{SHOW_SIBLINGS} : 0;
  799. # Useful variables
  800. # ----------------------------------------------------------------------------
  801. my $separator = '-' x 80 . "\n";
  802. my $separator2 = ' ' . '-' x 78 . "\n";
  803. # Print "info" as returned by "svn info"
  804. # ----------------------------------------------------------------------------
  805. for my $key ('URL', 'Repository Root', 'Revision', 'Last Changed Author',
  806. 'Last Changed Rev', 'Last Changed Date') {
  807. print $key, ': ', $self->svninfo (FLAG => $key), "\n"
  808. if $self->svninfo (FLAG => $key);
  809. }
  810. if ($self->config->verbose) {
  811. # Verbose mode, print log message at last changed revision
  812. my %log = $self->svnlog (REV => $self->svninfo (FLAG => 'Last Changed Rev'));
  813. my @log = split /\n/, $log{msg};
  814. print 'Last Changed Log:', "\n\n", map ({' ' . $_ . "\n"} @log), "\n";
  815. }
  816. if ($self->is_branch) {
  817. # Print create information
  818. # --------------------------------------------------------------------------
  819. my %log = $self->svnlog (REV => $self->create_rev);
  820. print $separator;
  821. print 'Branch Create Author: ', $log{author}, "\n" if $log{author};
  822. print 'Branch Create Rev: ', $self->create_rev, "\n";
  823. print 'Branch Create Date: ', &svn_date ($log{date}), "\n";
  824. if ($self->config->verbose) {
  825. # Verbose mode, print log message at last create revision
  826. my @log = split /\n/, $log{msg};
  827. print 'Branch Create Log:', "\n\n", map ({' ' . $_ . "\n"} @log), "\n";
  828. }
  829. # Print delete information if branch no longer exists
  830. # --------------------------------------------------------------------------
  831. print 'Branch Delete Rev: ', $self->delete_rev, "\n" if $self->delete_rev;
  832. # Report merges into/from the parent
  833. # --------------------------------------------------------------------------
  834. # Print the URL@REV of the parent branch
  835. print $separator, 'Branch Parent: ', $self->parent->url_peg, "\n";
  836. # Set up a new object for the parent at the current revision
  837. # --------------------------------------------------------------------------
  838. my $p_url = $self->parent->url;
  839. $p_url .= '@' . $self->pegrev if $self->pegrev;
  840. my $parent = Fcm::CmBranch->new (URL => $p_url);
  841. if (not $parent->url_exists) {
  842. print 'Branch parent deleted.', "\n";
  843. return;
  844. }
  845. # Report merges into/from the parent
  846. # --------------------------------------------------------------------------
  847. print $self->_report_merges ($parent, 'Parent');
  848. }
  849. # Report merges with siblings
  850. # ----------------------------------------------------------------------------
  851. if ($show_siblings) {
  852. # Report number of sibling branches found
  853. print $separator, 'Searching for siblings ... ';
  854. my @siblings = $self->siblings;
  855. print scalar (@siblings), ' ', (@siblings> 1 ? 'siblings' : 'sibling'),
  856. ' found.', "\n";
  857. # Report branch name and merge information only if there are recent merges
  858. my $out = '';
  859. for my $sibling (@siblings) {
  860. my $string = $self->_report_merges ($sibling, 'Sibling');
  861. $out .= $separator2 . ' ' . $sibling->url . "\n" . $string if $string;
  862. }
  863. if (@siblings) {
  864. if ($out) {
  865. print 'Merges with existing siblings:', "\n", $out;
  866. } else {
  867. print 'No merges with existing siblings.', "\n";
  868. }
  869. }
  870. }
  871. # Report children
  872. # ----------------------------------------------------------------------------
  873. if ($show_children) {
  874. # Report number of child branches found
  875. print $separator, 'Searching for children ... ';
  876. my @children = $self->children;
  877. print scalar (@children), ' ', (@children > 1 ? 'children' : 'child'),
  878. ' found.', "\n";
  879. # Report children if they exist
  880. print 'Current children:', "\n" if @children;
  881. for my $child (@children) {
  882. print $separator2, ' ', $child->url, "\n";
  883. print ' Child Create Rev: ', $child->create_rev, "\n";
  884. print $self->_report_merges ($child, 'Child');
  885. }
  886. }
  887. # Report custom/reverse merges into the branch
  888. # ----------------------------------------------------------------------------
  889. if ($show_other) {
  890. my %log = $self->svnlog (STOP_ON_COPY => 1);
  891. my @out;
  892. # Go down the revision log, checking for merge template messages
  893. REV: for my $rev (sort {$b <=> $a} keys %log) {
  894. # Loop each line of the log message at each revision
  895. my @msg = split /\n/, $log{$rev}{msg};
  896. for (@msg) {
  897. # Ignore unless log message matches a merge template
  898. if (/^Reversed r\d+(:\d+)? of \S+$/ or
  899. s/^(Custom merge) into \S+(:.+)$/$1$2/) {
  900. push @out, ('r' . $rev . ': ' . $_) . "\n";
  901. }
  902. }
  903. }
  904. print $separator, 'Other merges:', "\n", @out if @out;
  905. }
  906. return;
  907. }
  908. # ------------------------------------------------------------------------------
  909. # SYNOPSIS
  910. # $string = $self->_report_merges ($branch, $relation);
  911. #
  912. # DESCRIPTION
  913. # This method returns a string for displaying merge information with a
  914. # branch, the $relation of which can be a Parent, a Sibling or a Child.
  915. # ------------------------------------------------------------------------------
  916. sub _report_merges {
  917. my ($self, $branch, $relation) = @_;
  918. my $indent = ($relation eq 'Parent') ? '' : ' ';
  919. my $separator = ($relation eq 'Parent') ? ('-' x 80) : (' ' . '-' x 78);
  920. $separator .= "\n";
  921. my $return = '';
  922. # Report last merges into/from the $branch
  923. # ----------------------------------------------------------------------------
  924. my %merge = (
  925. 'Last Merge From ' . $relation . ':'
  926. => [$self->last_merge_from ($branch, 1)],
  927. 'Last Merge Into ' . $relation . ':'
  928. => [$branch->last_merge_from ($self, 1)],
  929. );
  930. if ($self->config->verbose) {
  931. # Verbose mode, print the log of the merge
  932. for my $key (keys %merge) {
  933. next if not @{ $merge{$key} };
  934. # From: target (0) is self, upper delta (1) is $branch
  935. # Into: target (0) is $branch, upper delta (1) is self
  936. my $t = ($key =~ /From/) ? $self : $branch;
  937. $return .= $indent . $key . "\n";
  938. $return .= $separator . $t->display_svnlog ($merge{$key}[0]->pegrev);
  939. }
  940. } else {
  941. # Normal mode, print in simplified form (rREV Parent@REV)
  942. for my $key (keys %merge) {
  943. next if not @{ $merge{$key} };
  944. # From: target (0) is self, upper delta (1) is $branch
  945. # Into: target (0) is $branch, upper delta (1) is self
  946. $return .= $indent . $key . ' r' . $merge{$key}[0]->pegrev . ' ' .
  947. $merge{$key}[1]->path_peg . ' cf. ' .
  948. $merge{$key}[2]->path_peg . "\n";
  949. }
  950. }
  951. if ($relation eq 'Sibling') {
  952. # For sibling, do not report further if there is no recent merge
  953. my @values = values %merge;
  954. return $return unless (@{ $values[0] } or @{ $values[1] });
  955. }
  956. # Report available merges into/from the $branch
  957. # ----------------------------------------------------------------------------
  958. my %avail = (
  959. 'Merges Avail From ' . $relation . ':'
  960. => ($self->delete_rev ? [] : [$self->avail_merge_from ($branch, 1)]),
  961. 'Merges Avail Into ' . $relation . ':'
  962. => [$branch->avail_merge_from ($self, 1)],
  963. );
  964. if ($self->config->verbose) {
  965. # Verbose mode, print the log of each revision
  966. for my $key (keys %avail) {
  967. next unless @{ $avail{$key} };
  968. $return .= $indent . $key . "\n";
  969. my $s = ($key =~ /From/) ? $branch: $self;
  970. for my $rev (@{ $avail{$key} }) {
  971. $return .= $separator . $s->display_svnlog ($rev);
  972. }
  973. }
  974. } else {
  975. # Normal mode, print only the revisions
  976. for my $key (keys %avail) {
  977. next unless @{ $avail{$key} };
  978. $return .= $indent . $key . ' ' . join (' ', @{ $avail{$key} }) . "\n";
  979. }
  980. }
  981. return $return;
  982. }
  983. # ------------------------------------------------------------------------------
  984. 1;
  985. __END__