<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[9340] sites/trunk: Trac: Add initial code to show Github PRs on Trac tickets.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/9340">9340</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/9340","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dd32</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2019-12-13 05:02:31 +0000 (Fri, 13 Dec 2019)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Trac: Add initial code to show Github PRs on Trac tickets.

Props dd32, andraganescu, desrosj, isabel_brison, noisysocks, pento, talldanwp.
See <a href="http://meta.trac.wordpress.org/ticket/4903">#4903</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlstyletracwptraccss">sites/trunk/wordpress.org/public_html/style/trac/wp-trac.css</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlstyletracwptracjs">sites/trunk/wordpress.org/public_html/style/trac/wp-trac.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/</li>
<li><a href="#sitestrunkapiwordpressorgpublic_htmldotorgtracprfunctionsphp">sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/functions.php</a></li>
<li><a href="#sitestrunkapiwordpressorgpublic_htmldotorgtracprindexphp">sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/index.php</a></li>
<li><a href="#sitestrunkapiwordpressorgpublic_htmldotorgtracprwebhookphp">sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/webhook.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkapiwordpressorgpublic_htmldotorgtracprfunctionsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/functions.php                            (rev 0)
+++ sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/functions.php      2019-12-13 05:02:31 UTC (rev 9340)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,130 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\API\Trac\GithubPRs;
+
+/**
+ * Fetches and reformats the Github PR API response to the details we need.
+ */
+function fetch_pr_data( $repo, $pr ) {
+       $url = '/repos/' . $repo . '/pulls/' . intval( $pr );
+       $data = api_request( $url );
+
+       // Error time..
+       if ( ! $data || ! $data->number ) {
+               return false;
+       }
+
+       return (object) [
+               'repo'            => $data->base->repo->full_name,
+               'number'          => $data->number,
+               'html_url'        => $data->html_url,
+               'state'           => $data->state,
+               'title'           => $data->title,
+               'created_at'      => $data->created_at,
+               'updated_at'      => $data->updated_at,
+               'closed_at'       => $data->closed_at,
+               'mergeable_state' => $data->mergeable_state,
+               'user'            => (object) [
+                       'url'  => $data->user->html_url,
+                       'name' => $data->user->login,
+               ],
+               'changes'         => (object) [
+                       'additions' => $data->additions,
+                       'deletions' => $data->deletions,
+                       'patch_url' => $data->patch_url,
+                       'html_url'  => $data->html_url,
+               ],
+               'trac_ticket'    => determine_trac_ticket( $data ),
+       ];
+}
+
+/**
+ * A simple wrapper to make a Github API request..
+ */
+function api_request( $url, $args = null, $headers = [], $method = null ) {
+       // Prepend GitHub URL for relative URLs, not all API URI's are on api.github.com, which is why we support full URI's.
+       if ( '/' === substr( $url, 0, 1 ) ) {
+               $url = 'https://api.github.com' . $url;
+       }
+
+       $context = stream_context_create( $c = [ 'http' => [
+               'method'        => $method ?: ( is_null( $args ) ? 'GET' : 'POST' ),
+               'user_agent'    => 'WordPress.org Trac; trac.WordPress.org',
+               'max_redirects' => 0,
+               'timeout'       => 5,
+               'ignore_errors' => true,
+               'headers'       => array_merge(
+                       [
+                               'Accept'        => 'application/json',
+                               'Authorization' => get_authorization_token(),
+                       ],
+                       $headers
+               ),
+               'body'          => $args ?: null,
+       ] ] );
+
+       return json_decode( file_get_contents(
+               $url,
+               false,
+               $context
+       ) );
+}
+
+/**
+ * Fetch an Authorization token for a Github API request.
+ */
+function get_authorization_token() {
+       global $wpdb;
+       
+       // TODO: This needs to be switched to a Github App token.
+       // This works temporarily to avoid the low unauthenticated limits.
+       return 'BEARER ' . $wpdb->get_var( "SELECT access_token FROM wporg_github_users WHERE github_user = 'dd32'");
+}
+
+/**
+ * Use some rough heuristics to find the Trac ticket for a given PR.
+ * 
+ * TODO: This should probably support multiple Trac Tickets, but once you start to use the final few regexes it can start to match Gutenberg references.
+ */
+function determine_trac_ticket( $pr ) {
+       $ticket = false;
+
+       // For now, we assume everything is destined for the Core Trac.
+       switch ( $pr->base->repo->full_name ) {
+               case 'WordPress/wordpress-develop':
+               default:
+                       $trac = 'core';
+                       break;
+       }
+
+       $regexes = [
+               '!' . $trac . '.trac.wordpress.org/ticket/(\d+)!i',
+               '!(?:^|\s)#WP(\d+)!', // #WP1234
+               '!(?:^|\s)#(\d{4,5})!', // #1234
+               '!Ticket[ /-](\d+)!i',
+               // diff filenames.
+               '!\b(\d+)(\.\d)?\.(?:diff|patch)!i',
+               // Formats of common branches
+               '!(?:' . $trac . '|WordPress|fix|trac)[-/](\d+)!i',
+               // Starts or ends with a ticketish number
+               // These match things it really shouldn't, and are a last-ditch effort.
+               '!\s(\d{4,5})$!i',
+               '!^(\d{4,5})[\s\W]!i',
+       ];
+
+       // Simple, the Trac ticket is mentioned in the title, or body.
+       foreach ( $regexes as $regex ) {
+               foreach ( [
+                       $pr->title,
+                       $pr->body,
+                       $pr->head->label,
+                       $pr->head->ref
+               ] as $field ) {
+                       if ( preg_match( $regex, $field, $m ) ) {
+                               return [ $trac, $m[1] ];
+                       }
+               }
+       }
+
+       return false;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/functions.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkapiwordpressorgpublic_htmldotorgtracprindexphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/index.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/index.php                                (rev 0)
+++ sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/index.php  2019-12-13 05:02:31 UTC (rev 9340)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,101 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\API\Trac\GithubPRs;
+
+require dirname( dirname( dirname( __DIR__ ) ) ) . '/init.php';
+require dirname( dirname( dirname( __DIR__ ) ) ) . '/includes/hyperdb/bb-10-hyper-db.php';
+require dirname( dirname( dirname( __DIR__ ) ) ) . '/includes/wp-json-encode.php';
+
+require __DIR__ . '/functions.php';
+
+$trac          = preg_replace( '![^a-z]!', '', $_GET['trac'] ?? '' );
+$ticket        = intval( $_GET['ticket'] ?? 0 );
+$authenticated = ! empty( $_GET['authenticated'] ); // Longer caches for logged out requests.
+
+if ( empty( $trac ) || empty( $ticket ) ) {
+       header( 'HTTP/1.0 400 Bad Request' );
+       header( 'Content-Type: application/json' );
+       die( '{"error":"Ticket number is invalid."}' );
+}
+
+// Fetch any linked PRs
+$prs = $wpdb->get_results( $wpdb->prepare(
+       "SELECT `repo`, `pr`, `data`, `last_checked`
+       FROM `trac_github_prs`
+       WHERE trac = %s AND ticket = %s",
+       $trac,
+       $ticket
+) );
+
+// Expand the JSON `data` field.
+array_walk( $prs, function( $data ) {
+       $data->data = json_decode( $data->data ) ?: false;
+       return $data;
+} );
+
+// Refresh any data that's needed
+// Rules:
+//  - 5 minutes for logged in requests, 60 mins for unauthenticated.
+//  - If PR created/updated in last half hour, every two minutes
+array_walk( $prs, function( $data ) use ( $authenticated ) {
+       global $wpdb;
+
+       if (
+               // If no data..
+               ! $data->data ||
+               // or it's out of date..
+               strtotime( $data->last_checked ) <= time() - ($authenticated ? 5*60 : 60*60) ||
+               // or the PR was created/updated within the last 30 minutes AND is more than 2 minutes out of date
+               (
+                       strtotime( $data->data->updated_at ) > time() - 30*60
+                       &&
+                       strtotime( $data->last_checked_at ) <= time() - 2*60
+               )
+       ) {
+               $pr_data = fetch_pr_data( $data->repo, $data->pr );
+
+               if ( $pr_data ) {
+                       $data->data = $pr_data;
+
+                       // TODO: catch the trac ticket changing and update the database.
+                       unset( $data->data->trac_ticket );
+
+                       $wpdb->update(
+                               'trac_github_prs',
+                               [
+                                       'data'         => json_encode( $pr_data ),
+                                       'last_checked' => gmdate( 'Y-m-d H:i:s' ),
+                               ],
+                               [
+                                       'repo' => $data->repo,
+                                       'pr'   => $data->pr
+                               ]
+                       );
+               }
+       }
+
+       return $data;
+} );
+
+// Expiry is an hour for everyone..
+// ..unless authenticated and a linked PR has changed within the last week, then 5 min.
+// ..unless the PR is created/updated within the 30 min, in which case 2min
+$expiry = 60*60;
+if ( $authenticated ) {
+       foreach ( $prs as $pr ) {
+               if ( strtotime( $pr->updated_at ) > time() - 30*60 ) {
+                       $expiry = min( $expiry, 2*60 );
+               } elseif ( strtotime( $pr->updated_at ) > time() - 7*24*60*60 ) {
+                       $expiry = min( $expiry, 5*60 );
+               }
+       }
+}
+
+header( 'Cache-Control: max-age=' . $expiry );
+header( 'Expires: ' . gmdate( 'D, d M Y H:i:s \G\M\T', time() + $expiry ) );
+header( 'Content-Type: application/json' );
+header( 'Access-Control-Allow-Origin: *' );
+
+// Only return the actual PR data needed
+$prs = array_column( $prs, 'data' );
+
+echo wp_json_encode( $prs );
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/index.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkapiwordpressorgpublic_htmldotorgtracprwebhookphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/webhook.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/webhook.php                              (rev 0)
+++ sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/webhook.php        2019-12-13 05:02:31 UTC (rev 9340)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,99 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\API\Trac\GithubPRs;
+
+require dirname( dirname( dirname( __DIR__ ) ) ) . '/init.php';
+require dirname( dirname( dirname( __DIR__ ) ) ) . '/includes/hyperdb/bb-10-hyper-db.php';
+
+require __DIR__ . '/functions.php';
+
+function verify_signature() {
+       // Validate that the request came from GitHub.
+       if ( ! defined( 'GH_PRBOT_WEBHOOK_SECRET' ) ) {
+               return;
+       }
+
+       $sent_signature     = $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '';
+       $expected_signature = 'sha1=' . hash_hmac( 'sha1', file_get_contents( 'php://input' ), GH_PRBOT_WEBHOOK_SECRET );
+
+       if ( ! hash_equals( $expected_signature, $sent_signature ) ) {
+               header( 'HTTP/1.0 403 Forbidden' );
+               die('-1');
+       }
+}
+
+verify_signature();
+
+$payload = json_decode( file_get_contents( 'php://input' ) );
+
+switch ( $_SERVER['HTTP_X_GITHUB_EVENT'] ) {
+       // Pull Request
+       case 'pull_request':
+
+               // A Pull Request has been created, updated, sync'd or reviewed.
+               // Ensure our DB is up-to-date with this news.
+
+               $pr_repo   = $payload->pull_request->base->repo->full_name;
+               $pr_number = $payload->number;
+
+               // API call to get the latest PR details, not all actions that trigger this include the full PR details.
+               $pr_data   = fetch_pr_data( $pr_repo, $pr_number );
+
+               // Step 1. Is this PR associated with any Trac tickets?
+               $existing_refs = $wpdb->get_results( $wpdb->prepare(
+                       "SELECT trac, ticket FROM trac_github_prs" .
+                       " WHERE repo = %s and pr = %d",
+                       $pr_repo, $pr_number
+               ) );
+       
+               // Step 2. Is that Trac Ticket still what we expect?
+               $matched_existing_ref = false;
+               foreach ( $existing_refs as $ref ) {
+                       if (
+                               $ref->trac === $pr_data->trac_ticket[0] &&
+                               $ref->ticket === $pr_data->trac_ticket[1]
+                       ) {
+                               $matched_existing_ref = true;
+                       }
+               }
+
+               $_pr_data_no_ticket = clone $pr_data;
+               unset( $_pr_data_no_ticket->trac_ticket );
+
+               // Step 3. If not in DB, or $pr_data->trac_ticket isn't yet in the DB, add a new row of it.
+               if ( $pr_data->trac_ticket && ( ! $existing_refs || ! $matched_existing_ref ) ) {
+                       $wpdb->insert(
+                               'trac_github_prs',
+                               [
+                                       'created'      => gmdate( 'Y-m-d H:i:s', strtotime( $pr_data->created_at ) ),
+                                       'last_checked' => gmdate( 'Y-m-d H:i:s' ),
+                                       'trac'         => $pr_data->trac_ticket[0],
+                                       'ticket'       => $pr_data->trac_ticket[1],
+                                       'repo'         => $pr_repo,
+                                       'pr'           => $pr_number,
+                                       'data'         => json_encode( $_pr_data_no_ticket ),
+                               ]
+                       );
+
+                       // TODO: Create a Trac ticket comment mentioning that the PR has been linked to the ticket.
+               }
+
+               // Step 4. Update all the instances of this PR with the new data, it may be linked to multiple tickets/tracs.
+               $wpdb->update(
+                       'trac_github_prs',
+                       [
+                               'last_checked' => gmdate( 'Y-m-d H:i:s' ),
+                               'data'         => json_encode( $_pr_data_no_ticket ),
+                       ],
+                       [
+                               'repo' => $pr_repo,
+                               'pr'   => $pr_number,
+                       ]
+               );
+
+               die( 'OK' );
+               break;
+
+       case 'pull_request_review':
+       case 'pull_request_review_comment':
+               die( 'N/A' );
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/api.wordpress.org/public_html/dotorg/trac/pr/webhook.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlstyletracwptraccss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/style/trac/wp-trac.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/style/trac/wp-trac.css      2019-12-12 23:41:03 UTC (rev 9339)
+++ sites/trunk/wordpress.org/public_html/style/trac/wp-trac.css        2019-12-13 05:02:31 UTC (rev 9340)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2292,3 +2292,40 @@
</span><span class="cx" style="display: block; padding: 0 10px">                width: 40%; /* More room so columns don't overlap */
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#github-prs .hidden {
+       display: none;
+}
+#github-prs ul.pull-requests {
+       border: 1px solid #ccc;
+       padding: 0 1em;
+       position: relative;
+}
+#github-prs ul.pull-requests li {
+       align-items: center;
+       display: flex;
+       justify-content: space-between;
+       padding: 0;
+}
+#github-prs ul.pull-requests li div {
+       margin: 0.5em 0;
+}
+#github-prs .button {
+       color: black;
+}
+#github-prs ins {
+       color: green;
+       text-decoration: none
+}
+#github-prs del {
+       color: red;
+       text-decoration: none;
+}
+@media screen and (max-width: 782px) {
+       #github-prs .button {
+               line-height: 20px;
+               margin-right: 0.3em;
+       }
+       #github-prs li {
+               display: block;
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlstyletracwptracjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/style/trac/wp-trac.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/style/trac/wp-trac.js       2019-12-12 23:41:03 UTC (rev 9339)
+++ sites/trunk/wordpress.org/public_html/style/trac/wp-trac.js 2019-12-13 05:02:31 UTC (rev 9340)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -84,6 +84,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        wpTrac.autocomplete.init();
</span><span class="cx" style="display: block; padding: 0 10px">                        wpTrac.linkMentions();
</span><span class="cx" style="display: block; padding: 0 10px">                        wpTrac.linkGutenbergIssues();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        wpTrac.githubPRs.init();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( ! $body.hasClass( 'plugins' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                wpTrac.workflow.init();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1541,6 +1542,157 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        };
</span><span class="cx" style="display: block; padding: 0 10px">                }()),
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                githubPRs: (function() {
+                       var apiEndpoint = 'https://api.wordpress.org/dotorg/trac/pr/',
+                               authenticated = !! ( wpTracCurrentUser && wpTracCurrentUser !== "anonymous" ),
+                               trac = false, ticket = 0,
+                               container;
+
+                       function init() {
+                               // TODO: If this is added to other Trac's, expand this..
+                               if ( $body.hasClass( 'core' ) ) {
+                                       trac = 'core';
+                               }
+
+                               // This seems to be the easiest place to find the current Ticket ID..
+                               var canonical = $( 'link[rel="canonical"]' ).prop( 'href' );
+                               if ( canonical ) {
+                                       ticket = canonical.match( /\/ticket\/(\d+)$/ )[1];
+                               }
+
+                               if ( ! trac || ! ticket ) {
+                                       return;
+                               }
+
+                               // Add the section immediately.
+                               renderAddSection();
+
+                               if ( authenticated ) {
+                                       // Fetch the PRs immediately for authenciated users.
+                                       fetchPRs();
+
+                                       // ..and expand the section by default.
+                                       container.toggleClass( 'collapsed', false );
+                               } else {
+                                       // Not authenticated? Fetch PRs upon expanding.
+                                       container.find( 'h3 a' ).one( 'click', function() {
+                                               fetchPRs();
+                                       });
+                               }
+                       }
+
+                       function fetchPRs() {
+                               $.ajax(
+                                       apiEndpoint
+                                               + '?trac=' + trac
+                                               + '&ticket=' + ticket
+                                               + ( authenticated ? '&authenticated=1' : '' )
+                               ).success( function( data ) {
+                                       // Update the number
+                                       container.find( 'h3 .trac-count' ).removeClass( 'hidden' ).find( 'span' ).text( data.length );
+
+                                       var prContainer = container.find( '.pull-requests' );
+                                       if ( data.length ) {
+                                               // Remove the placeholder.
+                                               prContainer.find( '.loading' ).remove();
+
+                                               // Render the PRs
+                                               for ( var i in data ) {
+                                                       renderPR( prContainer, data[i] );
+                                               }
+                                       } else {
+                                               // Change the loading placeholder
+                                               prContainer.find( '.loading' ).text( 'No linked PRs found.' );
+                                       }
+                               });
+                       }
+
+                       function renderAddSection() {
+                               // Add the Pull Requests section.
+                               $( '#attachments' ).append(
+                                       '<div id="github-prs" class="collapsed">' +
+                                               '<h3 class="foldable"><a id="section-pr" href="#section-pr">Pull requests <span class="trac-count hidden">(<span></span>)</span></a></h3>' +
+                                               '<ul class="pull-requests">' +
+                                                       '<li class="loading">Loading...</li>' +
+                                               '</ul>' +
+                                       '</div>'
+                               );
+                               // keep this for later.
+                               container = $( '#github-prs' );
+
+                               // Make the section collapse.
+                               container.find( '#section-pr' ).on( 'click', function() {
+                                       var $div = $( this.parentNode.parentNode ).toggleClass( 'collapsed' );
+                                       return ! $div.hasClass( 'collapsed' );
+                               } );
+                       }
+
+                       // Logic to determine what the PRs status is
+                       function prStatus( data ) {
+                               // Closed?
+                               if ( data.closed_at ) {
+                                       if ( data.mergeable_state == 'clean' ) {
+                                               return '✅ Closed';
+                                       } else {
+                                               return '❌ Closed'
+                                       }
+                               }
+
+                               // Merge State then
+                               switch ( data.mergeable_state ) {
+                                       case 'draft':
+                                               return 'Work in progress';
+                                       case 'clean':
+                                               return '✅ All checks pass';
+                                       case 'dirty':
+                                               return '❌ Merge conflicts';
+                                       case 'unstable':
+                                               return '❌ Failing tests';
+                               }
+                       }
+
+                       function renderPR( container, data ) {
+                               // Not the nicest, but it works and escapes things properly if given correct inputs.
+                               var htmlElement = function( element, attributes, text = '' ) {
+                                       return $( '<p>' ).append(
+                                               $( '<' + element + '/>', attributes ).text( text )
+                                       ).html();
+                               }
+
+                               container.append(
+                                       '<li>' +
+                                       '<div>' +
+                                               htmlElement(
+                                                       'a',
+                                                       { href: data.changes.html_url, title: data.title },
+                                                       '#' + data.number + ' ' +
+                                                               ( data.title.length > 23 ? data.title.substr( 0, 20 ) + '...' : data.title )
+                                               ) +
+                                               ' by ' +
+                                               htmlElement( 'a', { href: data.user.url }, '@' + data.user.name ) +
+                                       '</div>' +
+                                       '<div>' +
+                                               prStatus( data ) +
+                                       '</div>' +
+                                       '<div>' +
+                                               htmlElement( 'ins', {}, '+' + data.changes.additions ) +
+                                               '&nbsp;' +
+                                               htmlElement( 'del', {}, '-' + data.changes.deletions ) +
+                                       '</div>' +
+                                       '<div>' +
+                                               htmlElement( 'a', { href: data.changes.patch_url, class: 'button' }, 'View patch' ) +
+                                               '&nbsp;' +
+                                               htmlElement( 'a', { href: data.changes.html_url, class: 'button' }, 'View PR' ) +
+                                       '</div>' +
+                                       '</li>'
+                               );
+                       }
+
+                       return {
+                               init: init
+                       };
+               }()),
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 patchTracFor122Changes: function() {
</span><span class="cx" style="display: block; padding: 0 10px">                        console.log( "wp-trac: Applying compat patches for Trac 1.2.2" );
</span><span class="cx" style="display: block; padding: 0 10px">                        // From Trac 1.2.2 threaded_comments.js:
</span></span></pre>
</div>
</div>

</body>
</html>