1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-14 10:13:05 +02:00
pgbackrest/doc/lib/pgBackRestDoc/Custom/DocCustomRelease.pm
David Steele 0dd6629a2d Automatically add default user as reviewer.
Contributions looked like they had no reviewer when reviewed by the default user.
2020-04-24 08:40:54 -04:00

670 lines
27 KiB
Perl

####################################################################################################################################
# DOC RELEASE MODULE
####################################################################################################################################
package pgBackRestDoc::Custom::DocCustomRelease;
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use Cwd qw(abs_path);
use Exporter qw(import);
our @EXPORT = qw();
use File::Basename qw(dirname);
use pgBackRestBuild::Config::Data;
use pgBackRestDoc::Common::DocRender;
use pgBackRestDoc::Common::Log;
use pgBackRestDoc::Common::String;
use pgBackRestDoc::ProjectInfo;
####################################################################################################################################
# XML node constants
####################################################################################################################################
use constant XML_PARAM_ID => 'id';
use constant XML_CONTRIBUTOR_LIST => 'contributor-list';
use constant XML_CONTRIBUTOR => 'contributor';
use constant XML_CONTRIBUTOR_NAME_DISPLAY => 'contributor-name-display';
use constant XML_RELEASE_CORE_LIST => 'release-core-list';
use constant XML_RELEASE_DOC_LIST => 'release-doc-list';
use constant XML_RELEASE_TEST_LIST => 'release-test-list';
use constant XML_RELEASE_BUG_LIST => 'release-bug-list';
use constant XML_RELEASE_DEVELOPMENT_LIST => 'release-development-list';
use constant XML_RELEASE_FEATURE_LIST => 'release-feature-list';
use constant XML_RELEASE_IMPROVEMENT_LIST => 'release-improvement-list';
use constant XML_RELEASE_ITEM_CONTRIBUTOR_LIST => 'release-item-contributor-list';
use constant XML_RELEASE_ITEM_CONTRIBUTOR => 'release-item-contributor';
use constant XML_RELEASE_ITEM_IDEATOR => 'release-item-ideator';
use constant XML_RELEASE_ITEM_REVIEWER => 'release-item-reviewer';
####################################################################################################################################
# Contributor text constants
####################################################################################################################################
use constant TEXT_CONTRIBUTED => 'Contributed';
use constant TEXT_FIXED => 'Fixed';
use constant TEXT_FOUND => 'Reported';
use constant TEXT_REVIEWED => 'Reviewed';
use constant TEXT_SUGGESTED => 'Suggested';
####################################################################################################################################
# CONSTRUCTOR
####################################################################################################################################
sub new
{
my $class = shift; # Class name
# Create the class hash
my $self = {};
bless $self, $class;
# Assign function parameters, defaults, and log debug info
(
my $strOperation,
$self->{oDoc},
$self->{bDev},
) =
logDebugParam
(
__PACKAGE__ . '->new', \@_,
{name => 'oDoc'},
{name => 'bDev', required => false, default => false},
);
# Get contributor list
foreach my $oContributor ($self->{oDoc}->nodeGet(XML_CONTRIBUTOR_LIST)->nodeList(XML_CONTRIBUTOR))
{
my $strContributorId = $oContributor->paramGet(XML_PARAM_ID);
if (!defined($self->{hContributor}))
{
$self->{hContributor} = {};
$self->{strContributorDefault} = $strContributorId;
}
${$self->{hContributor}}{$strContributorId}{name} = $oContributor->fieldGet(XML_CONTRIBUTOR_NAME_DISPLAY);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'self', value => $self}
);
}
####################################################################################################################################
# currentStableVersion
#
# Return the current stable version.
####################################################################################################################################
sub currentStableVersion
{
my $self = shift;
my $oDoc = $self->{oDoc};
foreach my $oRelease ($oDoc->nodeGet('release-list')->nodeList('release'))
{
my $strVersion = $oRelease->paramGet('version');
if ($strVersion !~ /dev$/)
{
return $strVersion;
}
}
confess &log(ERROR, "unable to find non-development version");
}
####################################################################################################################################
# releaseLast
#
# Get the last release.
####################################################################################################################################
sub releaseLast
{
my $self = shift;
my $oDoc = $self->{oDoc};
foreach my $oRelease ($oDoc->nodeGet('release-list')->nodeList('release'))
{
return $oRelease;
}
}
####################################################################################################################################
# contributorTextGet
#
# Get a list of contributors for an item in text format.
####################################################################################################################################
sub contributorTextGet
{
my $self = shift;
my $oReleaseItem = shift;
my $strItemType = shift;
my $strContributorText;
my $hItemContributorType = {};
# Create a the list of contributors
foreach my $strContributorType (XML_RELEASE_ITEM_IDEATOR, XML_RELEASE_ITEM_CONTRIBUTOR, XML_RELEASE_ITEM_REVIEWER)
{
my $stryItemContributor = [];
if ($oReleaseItem->nodeTest(XML_RELEASE_ITEM_CONTRIBUTOR_LIST))
{
foreach my $oContributor ($oReleaseItem->nodeGet(XML_RELEASE_ITEM_CONTRIBUTOR_LIST)->
nodeList($strContributorType, false))
{
push @{$stryItemContributor}, $oContributor->paramGet(XML_PARAM_ID);
}
}
if (@$stryItemContributor == 0 && $strContributorType eq XML_RELEASE_ITEM_CONTRIBUTOR)
{
push @{$stryItemContributor}, $self->{strContributorDefault}
}
# Add the default user as a reviewer if there are no reviewers listed and default user is not already a contributor
if (@$stryItemContributor == 0 && $strContributorType eq XML_RELEASE_ITEM_REVIEWER)
{
my $bFound = false;
foreach my $strContributor (@{$$hItemContributorType{&XML_RELEASE_ITEM_CONTRIBUTOR}})
{
if ($strContributor eq $self->{strContributorDefault})
{
$bFound = true;
last;
}
}
if (!$bFound)
{
push @{$stryItemContributor}, $self->{strContributorDefault}
}
}
$$hItemContributorType{$strContributorType} = $stryItemContributor;
}
# Error if a reviewer is also a contributor
foreach my $strReviewer (@{$$hItemContributorType{&XML_RELEASE_ITEM_REVIEWER}})
{
foreach my $strContributor (@{$$hItemContributorType{&XML_RELEASE_ITEM_CONTRIBUTOR}})
{
if ($strReviewer eq $strContributor)
{
confess &log(ERROR, "${strReviewer} cannot be both a contributor and a reviewer");
}
}
}
# Error if the ideator list is the same as the contributor list
if (join(',', @{$$hItemContributorType{&XML_RELEASE_ITEM_IDEATOR}}) eq
join(',', @{$$hItemContributorType{&XML_RELEASE_ITEM_CONTRIBUTOR}}))
{
confess &log(ERROR, 'cannot have same contributor and ideator list: ' .
join(', ', @{$$hItemContributorType{&XML_RELEASE_ITEM_CONTRIBUTOR}}));
}
# Remove the default user if they are the only one in a group (to prevent the entire page from being splattered with one name)
foreach my $strContributorType (XML_RELEASE_ITEM_IDEATOR, XML_RELEASE_ITEM_CONTRIBUTOR)
{
if (@{$$hItemContributorType{$strContributorType}} == 1 &&
@{$$hItemContributorType{$strContributorType}}[0] eq $self->{strContributorDefault})
{
$$hItemContributorType{$strContributorType} = [];
}
}
# Render the string
foreach my $strContributorType (XML_RELEASE_ITEM_CONTRIBUTOR, XML_RELEASE_ITEM_REVIEWER, XML_RELEASE_ITEM_IDEATOR)
{
my $stryItemContributor = $$hItemContributorType{$strContributorType};
my $strContributorTypeText;
foreach my $strContributor (@{$stryItemContributor})
{
my $hContributor = ${$self->{hContributor}}{$strContributor};
if (!defined($hContributor))
{
confess &log(ERROR, "contributor ${strContributor} does not exist");
}
$strContributorTypeText .= (defined($strContributorTypeText) ? ', ' : '') . $$hContributor{name};
}
if (defined($strContributorTypeText))
{
$strContributorTypeText = ' by ' . $strContributorTypeText . '.';
if ($strContributorType eq XML_RELEASE_ITEM_CONTRIBUTOR)
{
$strContributorTypeText = ($strItemType eq 'bug' ? TEXT_FIXED : TEXT_CONTRIBUTED) . $strContributorTypeText;
}
elsif ($strContributorType eq XML_RELEASE_ITEM_IDEATOR)
{
$strContributorTypeText = ($strItemType eq 'bug' ? TEXT_FOUND : TEXT_SUGGESTED) . $strContributorTypeText;
}
elsif ($strContributorType eq XML_RELEASE_ITEM_REVIEWER)
{
$strContributorTypeText = TEXT_REVIEWED . $strContributorTypeText;
}
$strContributorText .= (defined($strContributorText) ? ' ' : '') . $strContributorTypeText;
}
}
return $strContributorText;
}
####################################################################################################################################
# Find a commit by subject prefix. Error if the prefix appears more than once.
####################################################################################################################################
sub commitFindSubject
{
my $self = shift;
my $rhyCommit = shift;
my $strSubjectPrefix = shift;
my $bRegExp = shift;
$bRegExp = defined($bRegExp) ? $bRegExp : true;
my $rhResult = undef;
foreach my $rhCommit (@{$rhyCommit})
{
if (($bRegExp && $rhCommit->{subject} =~ /^$strSubjectPrefix/) ||
(!$bRegExp && length($rhCommit->{subject}) >= length($strSubjectPrefix) &&
substr($rhCommit->{subject}, 0, length($strSubjectPrefix)) eq $strSubjectPrefix))
{
if (defined($rhResult))
{
confess &log(ERROR, "subject prefix '${strSubjectPrefix}' already found in commit " . $rhCommit->{commit});
}
$rhResult = $rhCommit;
}
}
return $rhResult;
}
####################################################################################################################################
# Throw an error that includes a list of release commits
####################################################################################################################################
sub commitError
{
my $self = shift;
my $strMessage = shift;
my $rstryCommitRemaining = shift;
my $rhyCommit = shift;
my $strList;
foreach my $strCommit (@{$rstryCommitRemaining})
{
$strList .=
(defined($strList) ? "\n" : '') .
substr($rhyCommit->{$strCommit}{date}, 0, length($rhyCommit->{$strCommit}{date}) - 15) . " $strCommit: " .
$rhyCommit->{$strCommit}{subject};
}
confess &log(ERROR, "${strMessage}:\n${strList}");
}
####################################################################################################################################
# docGet
#
# Get the xml for release.
####################################################################################################################################
sub docGet
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my $strOperation = logDebugParam(__PACKAGE__ . '->docGet');
# Load the git history
my $oStorageDoc = new pgBackRestTest::Common::Storage(
dirname(abs_path($0)), new pgBackRestTest::Common::StoragePosix({bFileSync => false, bPathSync => false}));
my @hyGitLog = @{(JSON::PP->new()->allow_nonref())->decode(${$oStorageDoc->get("resource/git-history.cache")})};
# Get renderer
my $oRender = new pgBackRestDoc::Common::DocRender('text');
$oRender->tagSet('backrest', PROJECT_NAME);
# Create the doc
my $oDoc = new pgBackRestDoc::Common::Doc();
$oDoc->paramSet('title', $self->{oDoc}->paramGet('title'));
$oDoc->paramSet('toc-number', $self->{oDoc}->paramGet('toc-number'));
# Set the description for use as a meta tag
$oDoc->fieldSet('description', $self->{oDoc}->fieldGet('description'));
# Add the introduction
my $oIntroSectionDoc = $oDoc->nodeAdd('section', undef, {id => 'introduction'});
$oIntroSectionDoc->nodeAdd('title')->textSet('Introduction');
$oIntroSectionDoc->textSet($self->{oDoc}->nodeGet('intro')->textGet());
# Add each release section
my $oSection;
my $iDevReleaseTotal = 0;
my $iCurrentReleaseTotal = 0;
my $iStableReleaseTotal = 0;
my $iUnsupportedReleaseTotal = 0;
my @oyRelease = $self->{oDoc}->nodeGet('release-list')->nodeList('release');
for (my $iReleaseIdx = 0; $iReleaseIdx < @oyRelease; $iReleaseIdx++)
{
my $oRelease = $oyRelease[$iReleaseIdx];
# Get the release version and dev flag
my $strVersion = $oRelease->paramGet('version');
my $bReleaseDev = $strVersion =~ /dev$/ ? true : false;
# Get a list of commits that apply to this release
my @rhyReleaseCommit;
my $rhReleaseCommitRemaining;
my @stryReleaseCommitRemaining;
my $bReleaseCheckCommit = false;
if ($strVersion ge '2.01')
{
# Should commits in the release be checked?
$bReleaseCheckCommit = !$bReleaseDev ? true : false;
# Get the begin commit
my $rhReleaseCommitBegin = $self->commitFindSubject(\@hyGitLog, "Begin v${strVersion} development\\.");
my $strReleaseCommitBegin = defined($rhReleaseCommitBegin) ? $rhReleaseCommitBegin->{commit} : undef;
# Get the end commit of the last release
my $strReleaseLastVersion = $oyRelease[$iReleaseIdx + 1]->paramGet('version');
my $rhReleaseLastCommitEnd = $self->commitFindSubject(\@hyGitLog, "v${strReleaseLastVersion}\\: .+");
if (!defined($rhReleaseLastCommitEnd))
{
confess &log(ERROR, "release ${strReleaseLastVersion} must have an end commit");
}
my $strReleaseLastCommitEnd = $rhReleaseLastCommitEnd->{commit};
# Get the end commit
my $rhReleaseCommitEnd = $self->commitFindSubject(\@hyGitLog, "v${strVersion}\\: .+");
my $strReleaseCommitEnd = defined($rhReleaseCommitEnd) ? $rhReleaseCommitEnd->{commit} : undef;
if ($bReleaseCheckCommit && !defined($rhReleaseCommitEnd) && $iReleaseIdx != 0)
{
confess &log(ERROR, "release ${strVersion} must have an end commit");
}
# Make a list of commits for this release
while ($hyGitLog[0]->{commit} ne $strReleaseLastCommitEnd)
{
# Don't add begin/end commits to the list since they are already accounted for
if ((defined($strReleaseCommitEnd) && $hyGitLog[0]->{commit} eq $strReleaseCommitEnd) ||
(defined($strReleaseCommitBegin) && $hyGitLog[0]->{commit} eq $strReleaseCommitBegin))
{
shift(@hyGitLog);
}
# Else add the commit to this releases' list
else
{
push(@stryReleaseCommitRemaining, $hyGitLog[0]->{commit});
push(@rhyReleaseCommit, $hyGitLog[0]);
$rhReleaseCommitRemaining->{$hyGitLog[0]->{commit}}{date} = $hyGitLog[0]->{date};
$rhReleaseCommitRemaining->{$hyGitLog[0]->{commit}}{subject} = $hyGitLog[0]->{subject};
shift(@hyGitLog);
}
}
# At least one commit is required for non-dev releases
if ($bReleaseCheckCommit && @stryReleaseCommitRemaining == 0)
{
confess &log(ERROR, "no commits found for release ${strVersion}");
}
}
# Display versions in TOC?
my $bTOC = true;
# Create a release section
if ($bReleaseDev)
{
if ($iDevReleaseTotal > 1)
{
confess &log(ERROR, 'only one development release is allowed');
}
$oSection = $oDoc->nodeAdd('section', undef, {id => 'development', if => "'{[dev]}' eq 'y'"});
$oSection->nodeAdd('title')->textSet("Development Notes");
$iDevReleaseTotal++;
}
elsif ($iCurrentReleaseTotal == 0)
{
$oSection = $oDoc->nodeAdd('section', undef, {id => 'current'});
$oSection->nodeAdd('title')->textSet("Current Stable Release");
$iCurrentReleaseTotal++;
}
elsif ($strVersion ge '1.00')
{
if ($iStableReleaseTotal == 0)
{
$oSection = $oDoc->nodeAdd('section', undef, {id => 'supported'});
$oSection->nodeAdd('title')->textSet("Stable Releases");
}
$iStableReleaseTotal++;
$bTOC = false;
}
else
{
if ($iUnsupportedReleaseTotal == 0)
{
$oSection = $oDoc->nodeAdd('section', undef, {id => 'unsupported'});
$oSection->nodeAdd('title')->textSet("Pre-Stable Releases");
}
$iUnsupportedReleaseTotal++;
$bTOC = false;
}
# Format the date
my $strDate = $oRelease->paramGet('date');
my $strDateOut = "";
my @stryMonth = ('January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December');
if ($strDate =~ /^X/)
{
$strDateOut .= 'No Release Date Set';
}
else
{
if ($strDate !~ /^(XXXX-XX-XX)|([0-9]{4}-[0-9]{2}-[0-9]{2})$/)
{
confess &log(ASSERT, "invalid date ${strDate} for release {$strVersion}");
}
$strDateOut .= 'Released ' . $stryMonth[(substr($strDate, 5, 2) - 1)] . ' ' .
(substr($strDate, 8, 2) + 0) . ', ' . substr($strDate, 0, 4);
}
# Add section and titles
my $oReleaseSection = $oSection->nodeAdd('section', undef, {id => $strVersion, toc => !$bTOC ? 'n' : undef});
$oReleaseSection->paramSet(XML_SECTION_PARAM_ANCHOR, XML_SECTION_PARAM_ANCHOR_VALUE_NOINHERIT);
$oReleaseSection->nodeAdd('title')->textSet(
"v${strVersion} " . ($bReleaseDev ? '' : 'Release ') . 'Notes');
$oReleaseSection->nodeAdd('subtitle')->textSet($oRelease->paramGet('title'));
$oReleaseSection->nodeAdd('subsubtitle')->textSet($strDateOut);
# Add release sections
my $bAdditionalNotes = false;
my $bReleaseNote = false;
my $hSectionType =
{
&XML_RELEASE_CORE_LIST => {title => 'Core', type => 'core'},
&XML_RELEASE_DOC_LIST => {title => 'Documentation', type => 'doc'},
&XML_RELEASE_TEST_LIST => {title => 'Test Suite', type => 'test'},
};
foreach my $strSectionType (XML_RELEASE_CORE_LIST, XML_RELEASE_DOC_LIST, XML_RELEASE_TEST_LIST)
{
if ($oRelease->nodeTest($strSectionType))
{
# Add release item types
my $hItemType =
{
&XML_RELEASE_BUG_LIST => {title => 'Bug Fixes', type => 'bug'},
&XML_RELEASE_FEATURE_LIST => {title => 'Features', type => 'feature'},
&XML_RELEASE_IMPROVEMENT_LIST => {title => 'Improvements', type => 'improvement'},
&XML_RELEASE_DEVELOPMENT_LIST => {title => 'Development', type => 'development'},
};
foreach my $strItemType (
XML_RELEASE_BUG_LIST, XML_RELEASE_FEATURE_LIST, XML_RELEASE_IMPROVEMENT_LIST, XML_RELEASE_DEVELOPMENT_LIST)
{
next if (!$self->{bDev} && $strItemType eq XML_RELEASE_DEVELOPMENT_LIST);
if ($oRelease->nodeGet($strSectionType)->nodeTest($strItemType))
{
if ($strSectionType ne XML_RELEASE_CORE_LIST && !$bAdditionalNotes)
{
$oReleaseSection->nodeAdd('subtitle')->textSet('Additional Notes');
$bAdditionalNotes = true;
}
# Add release note if present
if (!$bReleaseNote && $oRelease->nodeGet($strSectionType)->nodeTest('p'))
{
$oReleaseSection->nodeAdd('p')->textSet($oRelease->nodeGet($strSectionType)->nodeGet('p')->textGet());
$bReleaseNote = true;
}
my $strTypeText =
($strSectionType eq XML_RELEASE_CORE_LIST ? '' : $$hSectionType{$strSectionType}{title}) . ' ' .
$$hItemType{$strItemType}{title} . ':';
$oReleaseSection->
nodeAdd('p')->textSet(
{name => 'text', children=> [{name => 'b', value => $strTypeText}]});
my $oList = $oReleaseSection->nodeAdd('list');
# Add release items
foreach my $oReleaseFeature ($oRelease->nodeGet($strSectionType)->
nodeGet($strItemType)->nodeList('release-item'))
{
my @rhyReleaseItemP = $oReleaseFeature->nodeList('p');
my $oReleaseItemText = $rhyReleaseItemP[0]->textGet();
# Check release item commits
if ($bReleaseCheckCommit && $strItemType ne XML_RELEASE_DEVELOPMENT_LIST)
{
my @oyCommit = $oReleaseFeature->nodeList('commit', false);
# If no commits found then try to use the description as the commit subject
if (@oyCommit == 0)
{
my $strSubject = $oRender->processText($oReleaseItemText);
my $rhCommit = $self->commitFindSubject(\@rhyReleaseCommit, $strSubject, false);
if (!defined($rhCommit))
{
$self->commitError(
"unable to find commit or no subject match for release ${strVersion} item" .
" '${strSubject}'",
\@stryReleaseCommitRemaining, $rhReleaseCommitRemaining);
my $strCommit = $rhCommit->{commit};
@stryReleaseCommitRemaining = grep(!/$strCommit/, @stryReleaseCommitRemaining);
}
}
# Check the rest of the commits to ensure they exist
foreach my $oCommit (@oyCommit)
{
my $strSubject = $oCommit->paramGet('subject');
my $rhCommit = $self->commitFindSubject(\@rhyReleaseCommit, $strSubject, false);
if (defined($rhCommit))
{
my $strCommit = $rhCommit->{commit};
@stryReleaseCommitRemaining = grep(!/$strCommit/, @stryReleaseCommitRemaining);
}
else
{
$self->commitError(
"unable to find release ${strVersion} commit subject '${strSubject}' in list",
\@stryReleaseCommitRemaining, $rhReleaseCommitRemaining);
}
}
}
# Append the rest of the text
if (@rhyReleaseItemP > 1)
{
shift(@rhyReleaseItemP);
push(@{$oReleaseItemText->{oDoc}{children}}, ' ');
foreach my $rhReleaseItemP (@rhyReleaseItemP)
{
push(@{$oReleaseItemText->{oDoc}{children}}, @{$rhReleaseItemP->textGet()->{oDoc}{children}});
}
}
# Append contributor info
my $strContributorText = $self->contributorTextGet($oReleaseFeature, $$hItemType{$strItemType}{type});
if (defined($strContributorText))
{
push(@{$oReleaseItemText->{oDoc}{children}}, ' (');
push(@{$oReleaseItemText->{oDoc}{children}},
{name => 'i', value => $strContributorText});
push(@{$oReleaseItemText->{oDoc}{children}}, ')');
}
# Add the list item
$oList->nodeAdd('list-item')->textSet($oReleaseItemText);
}
}
}
}
}
# Error if there are commits left over
# if ($bReleaseCheckCommit && @stryReleaseCommitRemaining != 0)
# {
# $self->commitError(
# "unassigned commits for release ${strVersion}", \@stryReleaseCommitRemaining, $rhReleaseCommitRemaining);
# }
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'oDoc', value => $oDoc}
);
}
1;