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