<!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 ) +
+ ' ' +
+ htmlElement( 'del', {}, '-' + data.changes.deletions ) +
+ '</div>' +
+ '<div>' +
+ htmlElement( 'a', { href: data.changes.patch_url, class: 'button' }, 'View patch' ) +
+ ' ' +
+ 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>