Browse Source

Merge remote-tracking branch 'xrstf/bitbucket-support'

Jordi Boggiano 12 years ago
parent
commit
5a6e063e65

+ 2 - 3
app/Resources/FOSUserBundle/views/Profile/show.html.twig

@@ -19,9 +19,8 @@
             <p>You can use your API token to interact with the Packagist API.</p>
             <h1>GitHub Service Hook</h1>
             <p>Enabling the Packagist service hook ensures that your package will always be updated instantly when you push to GitHub. To do so you can go to your GitHub repository, click the "Settings" button, then "Service Hooks". Pick "Packagist" in the list, and add your API token (see above), plus your Packagist username if it is not the same as on GitHub. Check the "Active" box and submit the form.</p>
-            <h1>Bitbucket Service</h1>
-            <p><em>Note:</em> The service has currently not been enabled on Bitbucket's side, so please give it some time to become available.</p>
-            <p>To enable the Bitbucket service hook, go to your BitBucket repository, open the "Admin" tab and select "Services" in the menu. Pick "Packagist" in the list and add it to your repository. Afterwards, configure it like the GitHub service hook.</p>
+            <h1>Bitbucket POST Service</h1>
+            <p>To enable the Bitbucket service hook, go to your BitBucket repository, open the "Admin" tab and select "Services" in the menu. Pick "POST" in the list and add it to your repository. Afterwards, you have to enter the Packagist endpoint, containing both your username and API token (see above). Enter <code>https://packagist.org/api/bitbucket?username={{ app.user.username }}&amp;apiToken=&hellip;</code> for the service's URL. Save your changes and you're done.</p>
         {% endif %}
 
         <h1>Your packages</h1>

+ 114 - 45
src/Packagist/WebBundle/Controller/ApiController.php

@@ -20,6 +20,7 @@ use Composer\Package\Loader\ValidatingArrayLoader;
 use Composer\Package\Loader\ArrayLoader;
 use Packagist\WebBundle\Package\Updater;
 use Packagist\WebBundle\Entity\Package;
+use Packagist\WebBundle\Entity\User;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -84,7 +85,17 @@ class ApiController extends Controller
      */
     public function githubPostReceive(Request $request)
     {
-        return $this->receivePost($request, '{^(?:https?://|git://|git@)?(?P<domain>github\.com)[:/](?P<repo>[\w.-]+/[\w.-]+?)(?:\.git)?$}');
+        // parse the GitHub payload
+        $payload = json_decode($request->request->get('payload'), true);
+
+        if (!$payload || !isset($payload['repository']['url'])) {
+            return new Response(json_encode(array('status' => 'error', 'message' => 'Missing or invalid payload',)), 406);
+        }
+
+        $urlRegex = '{^(?:https?://|git://|git@)?(?P<host>github\.com)[:/](?P<path>[\w.-]+/[\w.-]+?)(?:\.git)?$}';
+        $repoUrl = $payload['repository']['url'];
+
+        return $this->receivePost($request, $repoUrl, $urlRegex);
     }
 
     /**
@@ -93,7 +104,17 @@ class ApiController extends Controller
      */
     public function bitbucketPostReceive(Request $request)
     {
-        return $this->receivePost($request, '{^(?:https?://)?(?P<domain>bitbucket\.org)/(?P<repo>[\w.-]+/[\w.-]+?)/?$}');
+        // decode Bitbucket's POST payload
+        $payload = json_decode($request->request->get('payload'), true);
+
+        if (!$payload || !isset($payload['canon_url']) || !isset($payload['repository']['absolute_url'])) {
+            return new Response(json_encode(array('status' => 'error', 'message' => 'Missing or invalid payload',)), 406);
+        }
+
+        $urlRegex = '{^(?:https?://)?(?P<host>bitbucket\.org)/(?P<path>[\w.-]+/[\w.-]+?)/?$}';
+        $repoUrl = $payload['canon_url'].$payload['repository']['absolute_url'];
+
+        return $this->receivePost($request, $repoUrl, $urlRegex);
     }
 
     /**
@@ -182,18 +203,83 @@ class ApiController extends Controller
         }
     }
 
-    protected function receivePost(Request $request, $urlRegex)
+    /**
+     * Perform the package update
+     *
+     * @param Request $request the current request
+     * @param string $url the repository's URL (deducted from the request)
+     * @param string $urlRegex the regex used to split the user packages into domain and path
+     * @return Response
+     */
+    protected function receivePost(Request $request, $url, $urlRegex)
     {
-        $payload = json_decode($request->request->get('payload'), true);
-        if (!$payload || !isset($payload['repository']['url'])) {
-            return new Response(json_encode(array('status' => 'error', 'message' => 'Missing or invalid payload',)), 406);
+        // try to parse the URL first to avoid the DB lookup on malformed requests
+        if (!preg_match($urlRegex, $url)) {
+            return new Response(json_encode(array('status' => 'error', 'message' => 'Could not parse payload repository URL')), 406);
         }
 
-        // try to parse the URL first to avoid the DB lookup on malformed requests
-        if (!preg_match($urlRegex, $payload['repository']['url'], $requestedRepo)) {
-            return new Response(json_encode(array('status' => 'error', 'message' => 'Could not parse payload repository URL',)), 406);
+        // find the user
+        $user = $this->findUser($request);
+
+        if (!$user) {
+            return new Response(json_encode(array('status' => 'error', 'message' => 'Invalid credentials')), 403);
+        }
+
+        // try to find the user package
+        $package = $this->findPackageByUrl($user, $url, $urlRegex);
+
+        if (!$package) {
+            return new Response(json_encode(array('status' => 'error', 'message' => 'Could not find a package that matches this request (does user maintain the package?)')), 404);
+        }
+
+        // don't die if this takes a while
+        set_time_limit(3600);
+
+        // put both updating the database and scanning the repository in a transaction
+        $em = $this->get('doctrine.orm.entity_manager');
+        $updater = $this->get('packagist.package_updater');
+        $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE);
+
+        try {
+            $em->transactional(function($em) use ($package, $updater, $io) {
+                // prepare dependencies
+                $config = Factory::createConfig();
+                $loader = new ValidatingArrayLoader(new ArrayLoader());
+
+                // prepare repository
+                $repository = new VcsRepository(array('url' => $package->getRepository()), $io, $config);
+                $repository->setLoader($loader);
+
+                // perform the actual update (fetch and re-scan the repository's source)
+                $updater->update($package, $repository);
+
+                // update the package entity
+                $package->setAutoUpdated(true);
+                $em->flush();
+            });
+        } catch (\Exception $e) {
+            if ($e instanceof InvalidRepositoryException) {
+                $this->get('packagist.package_manager')->notifyUpdateFailure($package, $e, $io->getOutput());
+            }
+
+            return new Response(json_encode(array(
+                'status' => 'error',
+                'message' => '['.get_class($e).'] '.$e->getMessage(),
+                'details' => '<pre>'.$io->getOutput().'</pre>'
+            )), 400);
         }
 
+        return new JsonResponse(array('status' => 'success'), 202);
+    }
+
+    /**
+     * Find a user by his username and API token
+     *
+     * @param Request $request
+     * @return User|null the found user or null otherwise
+     */
+    protected function findUser(Request $request)
+    {
         $username = $request->request->has('username') ?
             $request->request->get('username') :
             $request->query->get('username');
@@ -205,49 +291,32 @@ class ApiController extends Controller
         $user = $this->get('packagist.user_repository')
             ->findOneBy(array('username' => $username, 'apiToken' => $apiToken));
 
-        if (!$user) {
-            return new Response(json_encode(array('status' => 'error', 'message' => 'Invalid credentials',)), 403);
-        }
+        return $user;
+    }
 
-        $updated = false;
-        $config = Factory::createConfig();
-        $loader = new ValidatingArrayLoader(new ArrayLoader());
-        $updater = $this->get('packagist.package_updater');
-        $em = $this->get('doctrine.orm.entity_manager');
+    /**
+     * Find a user package given by its full URL
+     *
+     * @param User $user
+     * @param string $url
+     * @param string $urlRegex
+     * @return Package|null the found package or null otherwise
+     */
+    protected function findPackageByUrl(User $user, $url, $urlRegex)
+    {
+        if (!preg_match($urlRegex, $url, $matched)) {
+            return null;
+        }
 
         foreach ($user->getPackages() as $package) {
             if (preg_match($urlRegex, $package->getRepository(), $candidate)
-                && $candidate['domain'] === $requestedRepo['domain']
-                && $candidate['repo'] === $requestedRepo['repo']
+                && $candidate['host'] === $matched['host']
+                && $candidate['path'] === $matched['path']
             ) {
-                set_time_limit(3600);
-                $updated = true;
-
-                $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE);
-                $repository = new VcsRepository(array('url' => $package->getRepository()), $io, $config);
-                $repository->setLoader($loader);
-                $package->setAutoUpdated(true);
-                $em->flush();
-                try {
-                    $updater->update($package, $repository);
-                } catch (\Exception $e) {
-                    if ($e instanceof InvalidRepositoryException) {
-                        $this->get('packagist.package_manager')->notifyUpdateFailure($package, $e, $io->getOutput());
-                    }
-
-                    return new Response(json_encode(array(
-                        'status' => 'error',
-                        'message' => '['.get_class($e).'] '.$e->getMessage(),
-                        'details' => '<pre>'.$io->getOutput().'</pre>'
-                    )), 400);
-                }
+                return $package;
             }
         }
 
-        if ($updated) {
-            return new Response('{"status": "success"}', 202);
-        }
-
-        return new Response(json_encode(array('status' => 'error', 'message' => 'Could not find a package that matches this request (does user maintain the package?)',)), 404);
+        return null;
     }
 }