Browse Source

Change version storage, store branch/tag information for installs, add multi-license support

Jordi Boggiano 13 years ago
parent
commit
1306d1ca82

+ 30 - 39
src/Packagist/WebBundle/Command/UpdatePackagesCommand.php

@@ -23,12 +23,15 @@ use Packagist\WebBundle\Entity\Version;
 use Packagist\WebBundle\Entity\Tag;
 use Packagist\WebBundle\Entity\Tag;
 use Packagist\WebBundle\Entity\Author;
 use Packagist\WebBundle\Entity\Author;
 use Packagist\WebBundle\Repository\Repository\RepositoryInterface;
 use Packagist\WebBundle\Repository\Repository\RepositoryInterface;
+use Composer\Package\Version\VersionParser;
 
 
 /**
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
  */
 class UpdatePackagesCommand extends ContainerAwareCommand
 class UpdatePackagesCommand extends ContainerAwareCommand
 {
 {
+    protected $versionParser;
+
     protected $supportedLinkTypes = array(
     protected $supportedLinkTypes = array(
         'require'   => 'RequireLink',
         'require'   => 'RequireLink',
         'conflict'  => 'ConflictLink',
         'conflict'  => 'ConflictLink',
@@ -65,6 +68,8 @@ EOF
         $logger = $this->getContainer()->get('logger');
         $logger = $this->getContainer()->get('logger');
         $provider = $this->getContainer()->get('packagist.repository_provider');
         $provider = $this->getContainer()->get('packagist.repository_provider');
 
 
+        $this->versionParser = new VersionParser;
+
         $packages = $doctrine->getRepository('PackagistWebBundle:Package')->getStalePackages();
         $packages = $doctrine->getRepository('PackagistWebBundle:Package')->getStalePackages();
 
 
         foreach ($packages as $package) {
         foreach ($packages as $package) {
@@ -79,29 +84,32 @@ EOF
 
 
             try {
             try {
                 foreach ($repository->getTags() as $tag => $identifier) {
                 foreach ($repository->getTags() as $tag => $identifier) {
-                    if ($repository->hasComposerFile($identifier) && $this->parseVersion($tag)) {
+                    if ($repository->hasComposerFile($identifier) && $this->validateTag($tag)) {
                         $data = $repository->getComposerInformation($identifier);
                         $data = $repository->getComposerInformation($identifier);
+                        $data['version_normalized'] = $this->versionParser->normalize($data['version']);
                         // Strip -dev that could have been left over accidentally in a tag
                         // Strip -dev that could have been left over accidentally in a tag
-                        $data['version'] = preg_replace('{-?dev$}i', '', $data['version']);
+                        $data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']);
                         $this->updateInformation($output, $doctrine, $package, $repository, $identifier, $data);
                         $this->updateInformation($output, $doctrine, $package, $repository, $identifier, $data);
                         $doctrine->getEntityManager()->flush();
                         $doctrine->getEntityManager()->flush();
                     }
                     }
                 }
                 }
 
 
                 foreach ($repository->getBranches() as $branch => $identifier) {
                 foreach ($repository->getBranches() as $branch => $identifier) {
-                    if ($repository->hasComposerFile($identifier) && ($parsed = $this->parseBranch($branch))) {
+                    if ($repository->hasComposerFile($identifier) && $this->validateBranch($branch)) {
                         $data = $repository->getComposerInformation($identifier);
                         $data = $repository->getComposerInformation($identifier);
-                        $parsedVersion = $this->parseVersion($data['version']);
+                        $data['version_normalized'] = $this->versionParser->normalize($data['version']);
 
 
                         // Skip branches that contain a version that's been tagged already
                         // Skip branches that contain a version that's been tagged already
                         foreach ($package->getVersions() as $existingVersion) {
                         foreach ($package->getVersions() as $existingVersion) {
-                            if ($parsedVersion['version'] === $existingVersion->getVersion() && !$existingVersion->getDevelopment()) {
+                            if ($data['version_normalized'] === $existingVersion->getNormalizedVersion() && !$existingVersion->getDevelopment()) {
                                 continue;
                                 continue;
                             }
                             }
                         }
                         }
 
 
-                        // Force branches to use -dev type releases
-                        $data['version'] = $parsedVersion['version'].'-'.$parsedVersion['type'].'-dev';
+                        // Force branches to use -dev releases
+                        if (!preg_match('{[.-]?dev$}i', $data['version'])) {
+                            $data['version'] .= '-dev';
+                        }
 
 
                         $this->updateInformation($output, $doctrine, $package, $repository, $identifier, $data);
                         $this->updateInformation($output, $doctrine, $package, $repository, $identifier, $data);
                         $doctrine->getEntityManager()->flush();
                         $doctrine->getEntityManager()->flush();
@@ -118,51 +126,34 @@ EOF
         }
         }
     }
     }
 
 
-    private function parseBranch($branch)
+    private function validateBranch($branch)
     {
     {
         if (in_array($branch, array('master', 'trunk'))) {
         if (in_array($branch, array('master', 'trunk'))) {
-            return 'master';
+            return true;
         }
         }
 
 
-        if (!preg_match('#^v?(\d+)(\.(?:\d+|[x*]))?(\.[x*])?$#i', $branch, $matches)) {
-            return false;
-        }
-
-        return $matches[1]
-            .(!empty($matches[2]) ? strtr($matches[2], '*', 'x') : '.x')
-            .(!empty($matches[3]) ? strtr($matches[3], '*', 'x') : '.x');
+        return (Boolean) preg_match('#^v?(\d+)(\.(?:\d+|[x*]))?(\.(?:\d+|[x*]))?(\.[x*])?$#i', $branch, $matches);
     }
     }
 
 
-    private function parseVersion($version)
+    private function validateTag($version)
     {
     {
-        if (!preg_match('#^v?(\d+)(\.\d+)?(\.\d+)?-?((?:beta|RC|alpha)\d*)?-?(dev)?$#i', $version, $matches)) {
+        try {
+            $this->versionParser->normalize($version);
+            return true;
+        } catch (\Exception $e) {
             return false;
             return false;
         }
         }
-
-        return array(
-            'version' => $matches[1]
-                .(!empty($matches[2]) ? $matches[2] : '.0')
-                .(!empty($matches[3]) ? $matches[3] : '.0'),
-            'type' => !empty($matches[4]) ? strtolower($matches[4]) : '',
-            'dev' => !empty($matches[5]),
-        );
     }
     }
 
 
     private function updateInformation(OutputInterface $output, RegistryInterface $doctrine, $package, RepositoryInterface $repository, $identifier, array $data)
     private function updateInformation(OutputInterface $output, RegistryInterface $doctrine, $package, RepositoryInterface $repository, $identifier, array $data)
     {
     {
-        if (strtolower($data['name']) !== strtolower($package->getName())) {
-            $output->writeln('<error>Package name seems to have changed for '.$repository->getUrl().'@'.$identifier.', skipping.</error>');
-            return;
-        }
-
         $em = $doctrine->getEntityManager();
         $em = $doctrine->getEntityManager();
         $version = new Version();
         $version = new Version();
 
 
-        $parsedVersion = $this->parseVersion($data['version']);
-        $version->setName($data['name']);
-        $version->setVersion($parsedVersion['version']);
-        $version->setVersionType($parsedVersion['type']);
-        $version->setDevelopment($parsedVersion['dev']);
+        $version->setName($package->getName());
+        $version->setVersion($data['version']);
+        $version->setNormalizedVersion($data['version_normalized']);
+        $version->setDevelopment(substr($data['version'], -4) === '-dev');
 
 
         // check if we have that version yet
         // check if we have that version yet
         foreach ($package->getVersions() as $existingVersion) {
         foreach ($package->getVersions() as $existingVersion) {
@@ -179,12 +170,12 @@ EOF
 
 
         $version->setDescription($data['description']);
         $version->setDescription($data['description']);
         $version->setHomepage($data['homepage']);
         $version->setHomepage($data['homepage']);
-        $version->setLicense($data['license']);
+        $version->setLicense(is_array($data['license']) ? $data['license'] : array($data['license']));
 
 
         $version->setPackage($package);
         $version->setPackage($package);
         $version->setUpdatedAt(new \DateTime);
         $version->setUpdatedAt(new \DateTime);
         $version->setReleasedAt(new \DateTime($data['time']));
         $version->setReleasedAt(new \DateTime($data['time']));
-        $version->setSource(array('type' => $repository->getType(), 'url' => $repository->getUrl()));
+        $version->setSource($repository->getSource($identifier));
         $version->setDist($repository->getDist($identifier));
         $version->setDist($repository->getDist($identifier));
 
 
         if (isset($data['type'])) {
         if (isset($data['type'])) {
@@ -255,7 +246,7 @@ EOF
                     $link = new $class;
                     $link = new $class;
                     $link->setPackageName($linkPackageName);
                     $link->setPackageName($linkPackageName);
                     $link->setPackageVersion($linkPackageVersion);
                     $link->setPackageVersion($linkPackageVersion);
-                    $version->{'add'.$linkType}($link);
+                    $version->{'add'.$linkType.'Link'}($link);
                     $link->setVersion($version);
                     $link->setVersion($version);
                     $em->persist($link);
                     $em->persist($link);
                 }
                 }

+ 1 - 1
src/Packagist/WebBundle/Controller/WebController.php

@@ -129,7 +129,7 @@ class WebController extends Controller
 
 
     /**
     /**
      * @Template()
      * @Template()
-     * @Route("/view/{name}", name="view")
+     * @Route("/view/{name}", name="view", requirements={"name"="[A-Za-z0-9/_-]+"})
      */
      */
     public function viewAction($name)
     public function viewAction($name)
     {
     {

+ 1 - 1
src/Packagist/WebBundle/Entity/ConflictLink.php

@@ -25,5 +25,5 @@ class ConflictLink extends PackageLink
     /**
     /**
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="conflict")
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="conflict")
      */
      */
-    private $version;
+    protected $version;
 }
 }

+ 5 - 0
src/Packagist/WebBundle/Entity/Package.php

@@ -138,6 +138,11 @@ class Package
             $context->addViolation('The package name was not found, your composer.json file must be invalid or missing in your master branch/trunk. Maybe the URL you entered has a typo.', array(), null);
             $context->addViolation('The package name was not found, your composer.json file must be invalid or missing in your master branch/trunk. Maybe the URL you entered has a typo.', array(), null);
             return;
             return;
         }
         }
+
+        if (!preg_match('{^[a-z0-9_-]+/[a-z0-9_-]+$}i', $information['name'])) {
+            $context->addViolation('The package name '.$information['name'].' is invalid, it should have a vendor name, a forward slash, and a package name, matching <em>[a-z0-9_-]+/[a-z0-9_-]+</em>.', array(), null);
+            return;
+        }
     }
     }
 
 
     public function isPackageUnique(ExecutionContext $context)
     public function isPackageUnique(ExecutionContext $context)

+ 1 - 1
src/Packagist/WebBundle/Entity/ProvideLink.php

@@ -25,5 +25,5 @@ class ProvideLink extends PackageLink
     /**
     /**
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="provide")
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="provide")
      */
      */
-    private $version;
+    protected $version;
 }
 }

+ 1 - 1
src/Packagist/WebBundle/Entity/RecommendLink.php

@@ -25,5 +25,5 @@ class RecommendLink extends PackageLink
     /**
     /**
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="recommend")
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="recommend")
      */
      */
-    private $version;
+    protected $version;
 }
 }

+ 1 - 1
src/Packagist/WebBundle/Entity/ReplaceLink.php

@@ -25,5 +25,5 @@ class ReplaceLink extends PackageLink
     /**
     /**
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="replace")
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="replace")
      */
      */
-    private $version;
+    protected $version;
 }
 }

+ 1 - 1
src/Packagist/WebBundle/Entity/RequireLink.php

@@ -25,5 +25,5 @@ class RequireLink extends PackageLink
     /**
     /**
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="require")
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="require")
      */
      */
-    private $version;
+    protected $version;
 }
 }

+ 1 - 1
src/Packagist/WebBundle/Entity/SuggestLink.php

@@ -25,5 +25,5 @@ class SuggestLink extends PackageLink
     /**
     /**
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="suggest")
      * @ORM\ManyToOne(targetEntity="Packagist\WebBundle\Entity\Version", inversedBy="suggest")
      */
      */
-    private $version;
+    protected $version;
 }
 }

+ 61 - 16
src/Packagist/WebBundle/Entity/Version.php

@@ -80,6 +80,12 @@ class Version
      */
      */
     private $version;
     private $version;
 
 
+    /**
+     * @ORM\Column
+     * @Assert\NotBlank()
+     */
+    private $normalizedVersion;
+
     /**
     /**
      * @ORM\Column(type="boolean")
      * @ORM\Column(type="boolean")
      * @Assert\NotBlank()
      * @Assert\NotBlank()
@@ -87,7 +93,7 @@ class Version
     private $development;
     private $development;
 
 
     /**
     /**
-     * @ORM\Column(nullable="true")
+     * @ORM\Column(type="text", nullable="true")
      */
      */
     private $license;
     private $license;
 
 
@@ -159,7 +165,12 @@ class Version
     public function __construct()
     public function __construct()
     {
     {
         $this->tags = new \Doctrine\Common\Collections\ArrayCollection();
         $this->tags = new \Doctrine\Common\Collections\ArrayCollection();
-        $this->requirements = new \Doctrine\Common\Collections\ArrayCollection();
+        $this->require = new \Doctrine\Common\Collections\ArrayCollection();
+        $this->replace = new \Doctrine\Common\Collections\ArrayCollection();
+        $this->conflict = new \Doctrine\Common\Collections\ArrayCollection();
+        $this->provide = new \Doctrine\Common\Collections\ArrayCollection();
+        $this->recommend = new \Doctrine\Common\Collections\ArrayCollection();
+        $this->suggest = new \Doctrine\Common\Collections\ArrayCollection();
         $this->authors = new \Doctrine\Common\Collections\ArrayCollection();
         $this->authors = new \Doctrine\Common\Collections\ArrayCollection();
         $this->createdAt = new \DateTime;
         $this->createdAt = new \DateTime;
         $this->updatedAt = new \DateTime;
         $this->updatedAt = new \DateTime;
@@ -175,31 +186,45 @@ class Version
         foreach ($this->getAuthors() as $author) {
         foreach ($this->getAuthors() as $author) {
             $authors[] = $author->toArray();
             $authors[] = $author->toArray();
         }
         }
-        $requirements = array();
-        foreach ($this->getRequirements() as $requirement) {
-            $requirement = $requirement->toArray();
-            $requirements[key($requirement)] = current($requirement);
-        }
-        return array(
+
+        $data = array(
             'name' => $this->name,
             'name' => $this->name,
             'description' => $this->description,
             'description' => $this->description,
             'keywords' => $tags,
             'keywords' => $tags,
             'homepage' => $this->homepage,
             'homepage' => $this->homepage,
             'version' => $this->version,
             'version' => $this->version,
-            'license' => $this->license,
+            'license' => $this->getLicense(),
             'authors' => $authors,
             'authors' => $authors,
-            'require' => $requirements,
             'source' => $this->getSource(),
             'source' => $this->getSource(),
             'time' => $this->releasedAt ? $this->releasedAt->format('Y-m-d\TH:i:sP') : null,
             'time' => $this->releasedAt ? $this->releasedAt->format('Y-m-d\TH:i:sP') : null,
             'dist' => $this->getDist(),
             'dist' => $this->getDist(),
             'type' => $this->type,
             'type' => $this->type,
             'extra' => $this->extra,
             'extra' => $this->extra,
         );
         );
+
+        $supportedLinkTypes = array(
+            'require',
+            'conflict',
+            'provide',
+            'replace',
+            'recommend',
+            'suggest',
+        );
+
+        foreach ($supportedLinkTypes as $linkType) {
+            foreach ($this->{'get'.$linkType}() as $link) {
+                $link = $link->toArray();
+                $data[$linkType][key($link)] = current($link);
+            }
+        }
+
+        return $data;
     }
     }
 
 
     public function equals(Version $version)
     public function equals(Version $version)
     {
     {
-        return $version->getName() === $this->getName() && $version->getVersion() === $this->getVersion();
+        return strtolower($version->getName()) === strtolower($this->getName())
+            && $version->getNormalizedVersion() === $this->getNormalizedVersion();
     }
     }
 
 
     /**
     /**
@@ -279,7 +304,7 @@ class Version
      */
      */
     public function setVersion($version)
     public function setVersion($version)
     {
     {
-        $this->version = ltrim($version, 'vV.');
+        $this->version = $version;
     }
     }
 
 
     /**
     /**
@@ -292,24 +317,44 @@ class Version
         return $this->version;
         return $this->version;
     }
     }
 
 
+    /**
+     * Set normalizedVersion
+     *
+     * @param string $normalizedVersion
+     */
+    public function setNormalizedVersion($normalizedVersion)
+    {
+        $this->normalizedVersion = $normalizedVersion;
+    }
+
+    /**
+     * Get normalizedVersion
+     *
+     * @return string $normalizedVersion
+     */
+    public function getNormalizedVersion()
+    {
+        return $this->normalizedVersion;
+    }
+
     /**
     /**
      * Set license
      * Set license
      *
      *
      * @param string $license
      * @param string $license
      */
      */
-    public function setLicense($license)
+    public function setLicense(array $license)
     {
     {
-        $this->license = $license;
+        $this->license = json_encode($license);
     }
     }
 
 
     /**
     /**
      * Get license
      * Get license
      *
      *
-     * @return string $license
+     * @return array $license
      */
      */
     public function getLicense()
     public function getLicense()
     {
     {
-        return $this->license;
+        return json_decode($this->license, true);
     }
     }
 
 
     /**
     /**

+ 19 - 5
src/Packagist/WebBundle/Repository/Repository/GitHubRepository.php

@@ -42,21 +42,35 @@ class GitHubRepository implements RepositoryInterface
         return 'http://github.com/'.$this->owner.'/'.$this->repository.'.git';
         return 'http://github.com/'.$this->owner.'/'.$this->repository.'.git';
     }
     }
 
 
+    /**
+     * {@inheritDoc}
+     */
+    public function getSource($identifier)
+    {
+        $label = array_search($identifier, (array) $this->tags) ?: $identifier;
+        return array('type' => $this->getType(), 'url' => $this->getUrl(), 'reference' => $label, 'shasum' => '');
+    }
+
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
     public function getDist($identifier)
     public function getDist($identifier)
     {
     {
         $repoData = $this->getRepositoryData();
         $repoData = $this->getRepositoryData();
-        if ($repoData['repository']['has_downloads']) {
-            $label = array_search($identifier, (array) $this->tags) ?: $identifier;
+        $attempts = 3;
+
+        while ($attempts--) {
+            $label = array_search($identifier, (array) $this->tags) ?: array_search($identifier, (array) $this->branches) ?: $identifier;
             $url = 'https://github.com/'.$this->owner.'/'.$this->repository.'/zipball/'.$label;
             $url = 'https://github.com/'.$this->owner.'/'.$this->repository.'/zipball/'.$label;
-            $checksum = hash_file('sha1', $url);
-            return array('type' => 'zip', 'url' => $url, 'shasum' => $checksum ?: '');
+            if (!$checksum = @hash_file('sha1', $url)) {
+                continue;
+            }
+
+            return array('type' => 'zip', 'url' => $url, 'shasum' => $checksum, 'reference' => $label);
         }
         }
 
 
         // TODO clone the repo and build/host a zip ourselves. Not sure if this can happen, but it'll be needed for non-GitHub repos anyway
         // TODO clone the repo and build/host a zip ourselves. Not sure if this can happen, but it'll be needed for non-GitHub repos anyway
-        throw new \LogicException('Not implemented yet.');
+        throw new \LogicException('Could not retrieve dist file');
     }
     }
 
 
     /**
     /**

+ 7 - 3
src/Packagist/WebBundle/Repository/Repository/RepositoryInterface.php

@@ -34,13 +34,17 @@ interface RepositoryInterface
     function getTags();
     function getTags();
 
 
     /**
     /**
-     * Return the URL of the repository
-     *
      * @param string $identifier Any identifier to a specific branch/tag/commit
      * @param string $identifier Any identifier to a specific branch/tag/commit
-     * @return array With type, url and shasum properties.
+     * @return array With type, url, reference and shasum keys.
      */
      */
     function getDist($identifier);
     function getDist($identifier);
 
 
+    /**
+     * @param string $identifier Any identifier to a specific branch/tag/commit
+     * @return array With type, url, reference and shasum keys.
+     */
+    function getSource($identifier);
+
     /**
     /**
      * Return the URL of the repository
      * Return the URL of the repository
      *
      *

+ 1 - 1
src/Packagist/WebBundle/Resources/views/Web/index.html.twig

@@ -7,7 +7,7 @@
             <h2><a href="{{ url('view', { 'name' : package.name }) }}">{{ package.name }}</a></h2>
             <h2><a href="{{ url('view', { 'name' : package.name }) }}">{{ package.name }}</a></h2>
             {% if package.versions|length %}
             {% if package.versions|length %}
                 <p class="description">{{ package.versions[0].description }}</p>
                 <p class="description">{{ package.versions[0].description }}</p>
-                <p class="license">License: {{ package.versions[0].license|default("Unknown") }}</p>
+                <p class="license">License: {{ package.versions[0].license ? package.versions[0].license|join(', ') : "Unknown" }}</p>
                 <p class="links">
                 <p class="links">
                     {% if package.versions[0].homepage %}
                     {% if package.versions[0].homepage %}
                         Homepage: <a href="{{ package.versions[0].homepage }}">{{ package.versions[0].homepage|replace({'http://': ''}) }}</a><br />
                         Homepage: <a href="{{ package.versions[0].homepage }}">{{ package.versions[0].homepage|replace({'http://': ''}) }}</a><br />

+ 4 - 4
src/Packagist/WebBundle/Resources/views/Web/view.html.twig

@@ -31,9 +31,9 @@
 
 
     {% if package.versions|length %}
     {% if package.versions|length %}
         {% for version in package.versions %}
         {% for version in package.versions %}
-            <h2>Version {{ version.version }}{% if version.versionType %}-{{ version.versionType }}{% endif %}{% if version.development %}-dev{% endif %}</h2>
+            <h2>Version {{ version.version }}</h2>
             <p class="description">{{ version.description }}</p>
             <p class="description">{{ version.description }}</p>
-            <p class="license">License: {{ version.license|default("Unknown") }}</p>
+            <p class="license">License: {{ version.license ? version.license|join(', ') : "Unknown" }}</p>
             <p class="release-date">Date: {{ version.releasedAt|date("Y-m-d") }}</p>
             <p class="release-date">Date: {{ version.releasedAt|date("Y-m-d") }}</p>
             <p class="links">
             <p class="links">
                 {% if version.homepage %}
                 {% if version.homepage %}
@@ -51,8 +51,8 @@
                 {% if author.email %}&lt;<a href="mailto:{{ author.email }}">{{ author.email }}</a>&gt;{% endif %}
                 {% if author.email %}&lt;<a href="mailto:{{ author.email }}">{{ author.email }}</a>&gt;{% endif %}
                 <br />
                 <br />
             {% endfor %}</p>
             {% endfor %}</p>
-            <p class="requires">Requirement{{ version.requirements|length > 1 ? 's' : '' }}:
-            {% for req in version.requirements %}
+            <p class="requires">Requirement{{ version.require|length > 1 ? 's' : '' }}:
+            {% for req in version.require %}
                 {{ req.packageName }} ({{ req.packageVersion }})<br />
                 {{ req.packageName }} ({{ req.packageVersion }})<br />
             {% endfor %}</p>
             {% endfor %}</p>
         {% endfor %}
         {% endfor %}