1#################################################################################################################################### 2# DOC RELEASE MODULE 3#################################################################################################################################### 4package pgBackRestDoc::Custom::DocCustomRelease; 5 6use strict; 7use warnings FATAL => qw(all); 8use Carp qw(confess); 9 10use Cwd qw(abs_path); 11use Exporter qw(import); 12 our @EXPORT = qw(); 13use File::Basename qw(dirname); 14 15use pgBackRestBuild::Config::Data; 16 17use pgBackRestDoc::Common::DocRender; 18use pgBackRestDoc::Common::Log; 19use pgBackRestDoc::Common::String; 20use pgBackRestDoc::ProjectInfo; 21 22#################################################################################################################################### 23# XML node constants 24#################################################################################################################################### 25use constant XML_PARAM_ID => 'id'; 26 27use constant XML_CONTRIBUTOR_LIST => 'contributor-list'; 28use constant XML_CONTRIBUTOR => 'contributor'; 29use constant XML_CONTRIBUTOR_NAME_DISPLAY => 'contributor-name-display'; 30 31use constant XML_RELEASE_CORE_LIST => 'release-core-list'; 32use constant XML_RELEASE_DOC_LIST => 'release-doc-list'; 33use constant XML_RELEASE_TEST_LIST => 'release-test-list'; 34 35use constant XML_RELEASE_BUG_LIST => 'release-bug-list'; 36use constant XML_RELEASE_DEVELOPMENT_LIST => 'release-development-list'; 37use constant XML_RELEASE_FEATURE_LIST => 'release-feature-list'; 38use constant XML_RELEASE_IMPROVEMENT_LIST => 'release-improvement-list'; 39 40use constant XML_RELEASE_ITEM_CONTRIBUTOR_LIST => 'release-item-contributor-list'; 41 42use constant XML_RELEASE_ITEM_CONTRIBUTOR => 'release-item-contributor'; 43use constant XML_RELEASE_ITEM_IDEATOR => 'release-item-ideator'; 44use constant XML_RELEASE_ITEM_REVIEWER => 'release-item-reviewer'; 45 46#################################################################################################################################### 47# Contributor text constants 48#################################################################################################################################### 49use constant TEXT_CONTRIBUTED => 'Contributed'; 50use constant TEXT_FIXED => 'Fixed'; 51use constant TEXT_FOUND => 'Reported'; 52use constant TEXT_REVIEWED => 'Reviewed'; 53use constant TEXT_SUGGESTED => 'Suggested'; 54 55#################################################################################################################################### 56# CONSTRUCTOR 57#################################################################################################################################### 58sub new 59{ 60 my $class = shift; # Class name 61 62 # Create the class hash 63 my $self = {}; 64 bless $self, $class; 65 66 # Assign function parameters, defaults, and log debug info 67 ( 68 my $strOperation, 69 $self->{oDoc}, 70 $self->{bDev}, 71 ) = 72 logDebugParam 73 ( 74 __PACKAGE__ . '->new', \@_, 75 {name => 'oDoc'}, 76 {name => 'bDev', required => false, default => false}, 77 ); 78 79 # Get contributor list 80 foreach my $oContributor ($self->{oDoc}->nodeGet(XML_CONTRIBUTOR_LIST)->nodeList(XML_CONTRIBUTOR)) 81 { 82 my $strContributorId = $oContributor->paramGet(XML_PARAM_ID); 83 84 if (!defined($self->{hContributor})) 85 { 86 $self->{hContributor} = {}; 87 $self->{strContributorDefault} = $strContributorId; 88 } 89 90 ${$self->{hContributor}}{$strContributorId}{name} = $oContributor->fieldGet(XML_CONTRIBUTOR_NAME_DISPLAY); 91 } 92 93 # Return from function and log return values if any 94 return logDebugReturn 95 ( 96 $strOperation, 97 {name => 'self', value => $self} 98 ); 99} 100 101#################################################################################################################################### 102# currentStableVersion 103# 104# Return the current stable version. 105#################################################################################################################################### 106sub currentStableVersion 107{ 108 my $self = shift; 109 110 my $oDoc = $self->{oDoc}; 111 112 foreach my $oRelease ($oDoc->nodeGet('release-list')->nodeList('release')) 113 { 114 my $strVersion = $oRelease->paramGet('version'); 115 116 if ($strVersion !~ /dev$/) 117 { 118 return $strVersion; 119 } 120 } 121 122 confess &log(ERROR, "unable to find non-development version"); 123} 124 125 126#################################################################################################################################### 127# releaseLast 128# 129# Get the last release. 130#################################################################################################################################### 131sub releaseLast 132{ 133 my $self = shift; 134 135 my $oDoc = $self->{oDoc}; 136 137 foreach my $oRelease ($oDoc->nodeGet('release-list')->nodeList('release')) 138 { 139 return $oRelease; 140 } 141} 142 143#################################################################################################################################### 144# contributorTextGet 145# 146# Get a list of contributors for an item in text format. 147#################################################################################################################################### 148sub contributorTextGet 149{ 150 my $self = shift; 151 my $oReleaseItem = shift; 152 my $strItemType = shift; 153 154 my $strContributorText; 155 my $hItemContributorType = {}; 156 157 # Create a the list of contributors 158 foreach my $strContributorType (XML_RELEASE_ITEM_IDEATOR, XML_RELEASE_ITEM_CONTRIBUTOR, XML_RELEASE_ITEM_REVIEWER) 159 { 160 my $stryItemContributor = []; 161 162 if ($oReleaseItem->nodeTest(XML_RELEASE_ITEM_CONTRIBUTOR_LIST)) 163 { 164 foreach my $oContributor ($oReleaseItem->nodeGet(XML_RELEASE_ITEM_CONTRIBUTOR_LIST)-> 165 nodeList($strContributorType, false)) 166 { 167 push @{$stryItemContributor}, $oContributor->paramGet(XML_PARAM_ID); 168 } 169 } 170 171 if (@$stryItemContributor == 0 && $strContributorType eq XML_RELEASE_ITEM_CONTRIBUTOR) 172 { 173 push @{$stryItemContributor}, $self->{strContributorDefault} 174 } 175 176 # Add the default user as a reviewer if there are no reviewers listed and default user is not already a contributor 177 if (@$stryItemContributor == 0 && $strContributorType eq XML_RELEASE_ITEM_REVIEWER) 178 { 179 my $bFound = false; 180 181 foreach my $strContributor (@{$$hItemContributorType{&XML_RELEASE_ITEM_CONTRIBUTOR}}) 182 { 183 if ($strContributor eq $self->{strContributorDefault}) 184 { 185 $bFound = true; 186 last; 187 } 188 } 189 190 if (!$bFound) 191 { 192 push @{$stryItemContributor}, $self->{strContributorDefault} 193 } 194 } 195 196 $$hItemContributorType{$strContributorType} = $stryItemContributor; 197 } 198 199 # Error if a reviewer is also a contributor 200 foreach my $strReviewer (@{$$hItemContributorType{&XML_RELEASE_ITEM_REVIEWER}}) 201 { 202 foreach my $strContributor (@{$$hItemContributorType{&XML_RELEASE_ITEM_CONTRIBUTOR}}) 203 { 204 if ($strReviewer eq $strContributor) 205 { 206 confess &log(ERROR, "${strReviewer} cannot be both a contributor and a reviewer"); 207 } 208 } 209 } 210 211 # Error if the ideator list is the same as the contributor list 212 if (join(',', @{$$hItemContributorType{&XML_RELEASE_ITEM_IDEATOR}}) eq 213 join(',', @{$$hItemContributorType{&XML_RELEASE_ITEM_CONTRIBUTOR}})) 214 { 215 confess &log(ERROR, 'cannot have same contributor and ideator list: ' . 216 join(', ', @{$$hItemContributorType{&XML_RELEASE_ITEM_CONTRIBUTOR}})); 217 } 218 219 # Remove the default user if they are the only one in a group (to prevent the entire page from being splattered with one name) 220 foreach my $strContributorType (XML_RELEASE_ITEM_IDEATOR, XML_RELEASE_ITEM_CONTRIBUTOR) 221 { 222 if (@{$$hItemContributorType{$strContributorType}} == 1 && 223 @{$$hItemContributorType{$strContributorType}}[0] eq $self->{strContributorDefault}) 224 { 225 $$hItemContributorType{$strContributorType} = []; 226 } 227 } 228 229 # Render the string 230 foreach my $strContributorType (XML_RELEASE_ITEM_CONTRIBUTOR, XML_RELEASE_ITEM_REVIEWER, XML_RELEASE_ITEM_IDEATOR) 231 { 232 my $stryItemContributor = $$hItemContributorType{$strContributorType}; 233 my $strContributorTypeText; 234 235 foreach my $strContributor (@{$stryItemContributor}) 236 { 237 my $hContributor = ${$self->{hContributor}}{$strContributor}; 238 239 if (!defined($hContributor)) 240 { 241 confess &log(ERROR, "contributor ${strContributor} does not exist"); 242 } 243 244 $strContributorTypeText .= (defined($strContributorTypeText) ? ', ' : '') . $$hContributor{name}; 245 } 246 247 if (defined($strContributorTypeText)) 248 { 249 $strContributorTypeText = ' by ' . $strContributorTypeText . '.'; 250 251 if ($strContributorType eq XML_RELEASE_ITEM_CONTRIBUTOR) 252 { 253 $strContributorTypeText = ($strItemType eq 'bug' ? TEXT_FIXED : TEXT_CONTRIBUTED) . $strContributorTypeText; 254 } 255 elsif ($strContributorType eq XML_RELEASE_ITEM_IDEATOR) 256 { 257 $strContributorTypeText = ($strItemType eq 'bug' ? TEXT_FOUND : TEXT_SUGGESTED) . $strContributorTypeText; 258 } 259 elsif ($strContributorType eq XML_RELEASE_ITEM_REVIEWER) 260 { 261 $strContributorTypeText = TEXT_REVIEWED . $strContributorTypeText; 262 } 263 264 $strContributorText .= (defined($strContributorText) ? ' ' : '') . $strContributorTypeText; 265 } 266 } 267 268 return $strContributorText; 269} 270 271#################################################################################################################################### 272# Find a commit by subject prefix. Error if the prefix appears more than once. 273#################################################################################################################################### 274sub commitFindSubject 275{ 276 my $self = shift; 277 my $rhyCommit = shift; 278 my $strSubjectPrefix = shift; 279 my $bRegExp = shift; 280 281 $bRegExp = defined($bRegExp) ? $bRegExp : true; 282 my $rhResult = undef; 283 284 foreach my $rhCommit (@{$rhyCommit}) 285 { 286 if (($bRegExp && $rhCommit->{subject} =~ /^$strSubjectPrefix/) || 287 (!$bRegExp && length($rhCommit->{subject}) >= length($strSubjectPrefix) && 288 substr($rhCommit->{subject}, 0, length($strSubjectPrefix)) eq $strSubjectPrefix)) 289 { 290 if (defined($rhResult)) 291 { 292 confess &log(ERROR, "subject prefix '${strSubjectPrefix}' already found in commit " . $rhCommit->{commit}); 293 } 294 295 $rhResult = $rhCommit; 296 } 297 } 298 299 return $rhResult; 300} 301 302#################################################################################################################################### 303# Throw an error that includes a list of release commits 304#################################################################################################################################### 305sub commitError 306{ 307 my $self = shift; 308 my $strMessage = shift; 309 my $rstryCommitRemaining = shift; 310 my $rhyCommit = shift; 311 312 my $strList; 313 314 foreach my $strCommit (@{$rstryCommitRemaining}) 315 { 316 $strList .= 317 (defined($strList) ? "\n" : '') . 318 substr($rhyCommit->{$strCommit}{date}, 0, length($rhyCommit->{$strCommit}{date}) - 15) . " $strCommit: " . 319 $rhyCommit->{$strCommit}{subject}; 320 } 321 322 confess &log(ERROR, "${strMessage}:\n${strList}"); 323} 324 325#################################################################################################################################### 326# docGet 327# 328# Get the xml for release. 329#################################################################################################################################### 330sub docGet 331{ 332 my $self = shift; 333 334 # Assign function parameters, defaults, and log debug info 335 my $strOperation = logDebugParam(__PACKAGE__ . '->docGet'); 336 337 # Load the git history 338 my $oStorageDoc = new pgBackRestTest::Common::Storage( 339 dirname(abs_path($0)), new pgBackRestTest::Common::StoragePosix({bFileSync => false, bPathSync => false})); 340 my @hyGitLog = @{(JSON::PP->new()->allow_nonref())->decode(${$oStorageDoc->get("resource/git-history.cache")})}; 341 342 # Get renderer 343 my $oRender = new pgBackRestDoc::Common::DocRender('text'); 344 $oRender->tagSet('backrest', PROJECT_NAME); 345 346 # Create the doc 347 my $oDoc = new pgBackRestDoc::Common::Doc(); 348 $oDoc->paramSet('title', $self->{oDoc}->paramGet('title')); 349 $oDoc->paramSet('toc-number', $self->{oDoc}->paramGet('toc-number')); 350 351 # Set the description for use as a meta tag 352 $oDoc->fieldSet('description', $self->{oDoc}->fieldGet('description')); 353 354 # Add the introduction 355 my $oIntroSectionDoc = $oDoc->nodeAdd('section', undef, {id => 'introduction'}); 356 $oIntroSectionDoc->nodeAdd('title')->textSet('Introduction'); 357 $oIntroSectionDoc->textSet($self->{oDoc}->nodeGet('intro')->textGet()); 358 359 # Add each release section 360 my $oSection; 361 my $iDevReleaseTotal = 0; 362 my $iCurrentReleaseTotal = 0; 363 my $iStableReleaseTotal = 0; 364 my $iUnsupportedReleaseTotal = 0; 365 366 my @oyRelease = $self->{oDoc}->nodeGet('release-list')->nodeList('release'); 367 368 for (my $iReleaseIdx = 0; $iReleaseIdx < @oyRelease; $iReleaseIdx++) 369 { 370 my $oRelease = $oyRelease[$iReleaseIdx]; 371 372 # Get the release version and dev flag 373 my $strVersion = $oRelease->paramGet('version'); 374 my $bReleaseDev = $strVersion =~ /dev$/ ? true : false; 375 376 # Get a list of commits that apply to this release 377 my @rhyReleaseCommit; 378 my $rhReleaseCommitRemaining; 379 my @stryReleaseCommitRemaining; 380 my $bReleaseCheckCommit = false; 381 382 if ($strVersion ge '2.01') 383 { 384 # Should commits in the release be checked? 385 $bReleaseCheckCommit = !$bReleaseDev ? true : false; 386 387 # Get the begin commit 388 my $rhReleaseCommitBegin = $self->commitFindSubject(\@hyGitLog, "Begin v${strVersion} development\\."); 389 my $strReleaseCommitBegin = defined($rhReleaseCommitBegin) ? $rhReleaseCommitBegin->{commit} : undef; 390 391 # Get the end commit of the last release 392 my $strReleaseLastVersion = $oyRelease[$iReleaseIdx + 1]->paramGet('version'); 393 my $rhReleaseLastCommitEnd = $self->commitFindSubject(\@hyGitLog, "v${strReleaseLastVersion}\\: .+"); 394 395 if (!defined($rhReleaseLastCommitEnd)) 396 { 397 confess &log(ERROR, "release ${strReleaseLastVersion} must have an end commit"); 398 } 399 400 my $strReleaseLastCommitEnd = $rhReleaseLastCommitEnd->{commit}; 401 402 # Get the end commit 403 my $rhReleaseCommitEnd = $self->commitFindSubject(\@hyGitLog, "v${strVersion}\\: .+"); 404 my $strReleaseCommitEnd = defined($rhReleaseCommitEnd) ? $rhReleaseCommitEnd->{commit} : undef; 405 406 if ($bReleaseCheckCommit && !defined($rhReleaseCommitEnd) && $iReleaseIdx != 0) 407 { 408 confess &log(ERROR, "release ${strVersion} must have an end commit"); 409 } 410 411 # Make a list of commits for this release 412 while ($hyGitLog[0]->{commit} ne $strReleaseLastCommitEnd) 413 { 414 # Don't add begin/end commits to the list since they are already accounted for 415 if ((defined($strReleaseCommitEnd) && $hyGitLog[0]->{commit} eq $strReleaseCommitEnd) || 416 (defined($strReleaseCommitBegin) && $hyGitLog[0]->{commit} eq $strReleaseCommitBegin)) 417 { 418 shift(@hyGitLog); 419 } 420 # Else add the commit to this releases' list 421 else 422 { 423 push(@stryReleaseCommitRemaining, $hyGitLog[0]->{commit}); 424 push(@rhyReleaseCommit, $hyGitLog[0]); 425 426 $rhReleaseCommitRemaining->{$hyGitLog[0]->{commit}}{date} = $hyGitLog[0]->{date}; 427 $rhReleaseCommitRemaining->{$hyGitLog[0]->{commit}}{subject} = $hyGitLog[0]->{subject}; 428 429 shift(@hyGitLog); 430 } 431 } 432 433 # At least one commit is required for non-dev releases 434 if ($bReleaseCheckCommit && @stryReleaseCommitRemaining == 0) 435 { 436 confess &log(ERROR, "no commits found for release ${strVersion}"); 437 } 438 } 439 440 # Display versions in TOC? 441 my $bTOC = true; 442 443 # Create a release section 444 if ($bReleaseDev) 445 { 446 if ($iDevReleaseTotal > 1) 447 { 448 confess &log(ERROR, 'only one development release is allowed'); 449 } 450 451 $oSection = $oDoc->nodeAdd('section', undef, {id => 'development', if => "'{[dev]}' eq 'y'"}); 452 $oSection->nodeAdd('title')->textSet("Development Notes"); 453 454 $iDevReleaseTotal++; 455 } 456 elsif ($iCurrentReleaseTotal == 0) 457 { 458 $oSection = $oDoc->nodeAdd('section', undef, {id => 'current'}); 459 $oSection->nodeAdd('title')->textSet("Current Stable Release"); 460 $iCurrentReleaseTotal++; 461 } 462 elsif ($strVersion ge '1.00') 463 { 464 if ($iStableReleaseTotal == 0) 465 { 466 $oSection = $oDoc->nodeAdd('section', undef, {id => 'supported'}); 467 $oSection->nodeAdd('title')->textSet("Stable Releases"); 468 } 469 470 $iStableReleaseTotal++; 471 $bTOC = false; 472 } 473 else 474 { 475 if ($iUnsupportedReleaseTotal == 0) 476 { 477 $oSection = $oDoc->nodeAdd('section', undef, {id => 'unsupported'}); 478 $oSection->nodeAdd('title')->textSet("Pre-Stable Releases"); 479 } 480 481 $iUnsupportedReleaseTotal++; 482 $bTOC = false; 483 } 484 485 # Format the date 486 my $strDate = $oRelease->paramGet('date'); 487 my $strDateOut = ""; 488 489 my @stryMonth = ('January', 'February', 'March', 'April', 'May', 'June', 490 'July', 'August', 'September', 'October', 'November', 'December'); 491 492 if ($strDate =~ /^X/) 493 { 494 $strDateOut .= 'No Release Date Set'; 495 } 496 else 497 { 498 if ($strDate !~ /^(XXXX-XX-XX)|([0-9]{4}-[0-9]{2}-[0-9]{2})$/) 499 { 500 confess &log(ASSERT, "invalid date ${strDate} for release {$strVersion}"); 501 } 502 503 $strDateOut .= 'Released ' . $stryMonth[(substr($strDate, 5, 2) - 1)] . ' ' . 504 (substr($strDate, 8, 2) + 0) . ', ' . substr($strDate, 0, 4); 505 } 506 507 # Add section and titles 508 my $oReleaseSection = $oSection->nodeAdd('section', undef, {id => $strVersion, toc => !$bTOC ? 'n' : undef}); 509 $oReleaseSection->paramSet(XML_SECTION_PARAM_ANCHOR, XML_SECTION_PARAM_ANCHOR_VALUE_NOINHERIT); 510 511 $oReleaseSection->nodeAdd('title')->textSet( 512 "v${strVersion} " . ($bReleaseDev ? '' : 'Release ') . 'Notes'); 513 514 $oReleaseSection->nodeAdd('subtitle')->textSet($oRelease->paramGet('title')); 515 $oReleaseSection->nodeAdd('subsubtitle')->textSet($strDateOut); 516 517 # Add release sections 518 my $bAdditionalNotes = false; 519 my $bReleaseNote = false; 520 521 my $hSectionType = 522 { 523 &XML_RELEASE_CORE_LIST => {title => 'Core', type => 'core'}, 524 &XML_RELEASE_DOC_LIST => {title => 'Documentation', type => 'doc'}, 525 &XML_RELEASE_TEST_LIST => {title => 'Test Suite', type => 'test'}, 526 }; 527 528 foreach my $strSectionType (XML_RELEASE_CORE_LIST, XML_RELEASE_DOC_LIST, XML_RELEASE_TEST_LIST) 529 { 530 if ($oRelease->nodeTest($strSectionType)) 531 { 532 # Add release item types 533 my $hItemType = 534 { 535 &XML_RELEASE_BUG_LIST => {title => 'Bug Fixes', type => 'bug'}, 536 &XML_RELEASE_FEATURE_LIST => {title => 'Features', type => 'feature'}, 537 &XML_RELEASE_IMPROVEMENT_LIST => {title => 'Improvements', type => 'improvement'}, 538 &XML_RELEASE_DEVELOPMENT_LIST => {title => 'Development', type => 'development'}, 539 }; 540 541 foreach my $strItemType ( 542 XML_RELEASE_BUG_LIST, XML_RELEASE_FEATURE_LIST, XML_RELEASE_IMPROVEMENT_LIST, XML_RELEASE_DEVELOPMENT_LIST) 543 { 544 next if (!$self->{bDev} && $strItemType eq XML_RELEASE_DEVELOPMENT_LIST); 545 546 if ($oRelease->nodeGet($strSectionType)->nodeTest($strItemType)) 547 { 548 if ($strSectionType ne XML_RELEASE_CORE_LIST && !$bAdditionalNotes) 549 { 550 $oReleaseSection->nodeAdd('subtitle')->textSet('Additional Notes'); 551 $bAdditionalNotes = true; 552 } 553 554 # Add release note if present 555 if (!$bReleaseNote && $oRelease->nodeGet($strSectionType)->nodeTest('p')) 556 { 557 $oReleaseSection->nodeAdd('p')->textSet($oRelease->nodeGet($strSectionType)->nodeGet('p')->textGet()); 558 $bReleaseNote = true; 559 } 560 561 my $strTypeText = 562 ($strSectionType eq XML_RELEASE_CORE_LIST ? '' : $$hSectionType{$strSectionType}{title}) . ' ' . 563 $$hItemType{$strItemType}{title} . ':'; 564 565 $oReleaseSection-> 566 nodeAdd('p')->textSet( 567 {name => 'text', children=> [{name => 'b', value => $strTypeText}]}); 568 569 my $oList = $oReleaseSection->nodeAdd('list'); 570 571 # Add release items 572 foreach my $oReleaseFeature ($oRelease->nodeGet($strSectionType)-> 573 nodeGet($strItemType)->nodeList('release-item')) 574 { 575 my @rhyReleaseItemP = $oReleaseFeature->nodeList('p'); 576 my $oReleaseItemText = $rhyReleaseItemP[0]->textGet(); 577 578 # Check release item commits 579 if ($bReleaseCheckCommit && $strItemType ne XML_RELEASE_DEVELOPMENT_LIST) 580 { 581 my @oyCommit = $oReleaseFeature->nodeList('commit', false); 582 583 # If no commits found then try to use the description as the commit subject 584 if (@oyCommit == 0) 585 { 586 my $strSubject = $oRender->processText($oReleaseItemText); 587 my $rhCommit = $self->commitFindSubject(\@rhyReleaseCommit, $strSubject, false); 588 589 if (!defined($rhCommit)) 590 { 591 $self->commitError( 592 "unable to find commit or no subject match for release ${strVersion} item" . 593 " '${strSubject}'", 594 \@stryReleaseCommitRemaining, $rhReleaseCommitRemaining); 595 596 my $strCommit = $rhCommit->{commit}; 597 @stryReleaseCommitRemaining = grep(!/$strCommit/, @stryReleaseCommitRemaining); 598 } 599 } 600 601 # Check the rest of the commits to ensure they exist 602 foreach my $oCommit (@oyCommit) 603 { 604 my $strSubject = $oCommit->paramGet('subject'); 605 my $rhCommit = $self->commitFindSubject(\@rhyReleaseCommit, $strSubject, false); 606 607 if (defined($rhCommit)) 608 { 609 my $strCommit = $rhCommit->{commit}; 610 @stryReleaseCommitRemaining = grep(!/$strCommit/, @stryReleaseCommitRemaining); 611 } 612 else 613 { 614 $self->commitError( 615 "unable to find release ${strVersion} commit subject '${strSubject}' in list", 616 \@stryReleaseCommitRemaining, $rhReleaseCommitRemaining); 617 } 618 } 619 } 620 621 # Append the rest of the text 622 if (@rhyReleaseItemP > 1) 623 { 624 shift(@rhyReleaseItemP); 625 626 push(@{$oReleaseItemText->{oDoc}{children}}, ' '); 627 628 foreach my $rhReleaseItemP (@rhyReleaseItemP) 629 { 630 push(@{$oReleaseItemText->{oDoc}{children}}, @{$rhReleaseItemP->textGet()->{oDoc}{children}}); 631 } 632 } 633 634 # Append contributor info 635 my $strContributorText = $self->contributorTextGet($oReleaseFeature, $$hItemType{$strItemType}{type}); 636 637 if (defined($strContributorText)) 638 { 639 push(@{$oReleaseItemText->{oDoc}{children}}, ' ('); 640 push(@{$oReleaseItemText->{oDoc}{children}}, 641 {name => 'i', value => $strContributorText}); 642 push(@{$oReleaseItemText->{oDoc}{children}}, ')'); 643 } 644 645 # Add the list item 646 $oList->nodeAdd('list-item')->textSet($oReleaseItemText); 647 } 648 } 649 } 650 } 651 } 652 653 # Error if there are commits left over 654 # if ($bReleaseCheckCommit && @stryReleaseCommitRemaining != 0) 655 # { 656 # $self->commitError( 657 # "unassigned commits for release ${strVersion}", \@stryReleaseCommitRemaining, $rhReleaseCommitRemaining); 658 # } 659 } 660 661 # Return from function and log return values if any 662 return logDebugReturn 663 ( 664 $strOperation, 665 {name => 'oDoc', value => $oDoc} 666 ); 667} 668 6691; 670