Skip to content

Instantly share code, notes, and snippets.

@westonruter
Last active November 18, 2015 22:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save westonruter/5c453a44bd36f6847c36 to your computer and use it in GitHub Desktop.
Save westonruter/5c453a44bd36f6847c36 to your computer and use it in GitHub Desktop.
<?php
class WP_CLI_Trac_Patched_Git_Branch extends WP_CLI_Command {
/**
* Create Git branch from Core Trac patches, or amend an existing branch with the latest patches.
*
* ## OPTIONS
*
* <ticket>
* : The ticket that patches will be applied from. If you are on master, it will checkout trac-<ticket> branch.
*
* ## EXAMPLES
*
* wp git-apply-core-trac-patches 12345
*
* @when before_wp_load
* @synopsis <ticket>
* @param array $args
* @param array $assoc_args
*/
function __invoke( $args, $assoc_args ) {
$ticket = array_shift( $args );
if ( ! preg_match( '/^\d+$/', $ticket ) ) {
WP_CLI::error( 'Expected ticket arg to be integer.' );
}
if ( 'commit' !== trim( shell_exec( 'git cat-file -t 5190215645328199b11a076bd718461d941a6ec3' ) ) ) {
WP_CLI::error( 'This repo does not appear to be a clone of develop.git.wordpress.org' );
}
$git_root_level = trim( shell_exec( 'git rev-parse --show-toplevel' ) );
if ( empty( $git_root_level ) || ! is_dir( $git_root_level ) ) {
WP_CLI::error( 'Unable to find Git root directory.' );
}
chdir( $git_root_level );
system( 'git diff-index --quiet HEAD', $return_var );
if ( 0 !== $return_var ) {
WP_CLI::error( 'git repo state dirty. Stash changes?' );
}
$current_branch = trim( shell_exec( 'git rev-parse --abbrev-ref HEAD' ) );
if ( 'master' === $current_branch ) {
WP_CLI::error( sprintf( 'You cannot apply patches to master. First checkout a new branch, e.g. git checkout -b trac-%d', $ticket ) );
}
$trac_ticket_url = sprintf( 'https://core.trac.wordpress.org/ticket/%d', $ticket );
WP_CLI::line( "Fetching HTML from Trac ticket $ticket" );
$html = file_get_contents( $trac_ticket_url );
$doc = new DOMDocument();
// @codingStandardsIgnoreStart
@$doc->loadHTML( $html );
// @codingStandardsIgnoreEnd
$xpath = new DOMXPath( $doc );
$dl = $xpath->query( '//dl[ @class = "attachments" ]' )->item( 0 );
if ( empty( $dl ) ) {
WP_CLI::error( 'No attachments found.' );
}
$attachments = array();
/** @var DOMElement $dl */
foreach ( $dl->getElementsByTagName( 'dt' ) as $dt ) {
/** @var DOMElement $dt */
$nice_link = $dt->getElementsByTagName( 'a' )->item( 0 );
$patch_file_name = $nice_link->textContent;
if ( ! preg_match( '/\.(diff|patch)$/', $patch_file_name ) ) {
continue;
}
$patch_nice_url = 'https://core.trac.wordpress.org' . $nice_link->getAttribute( 'href' );
$patch_raw_url = 'https://core.trac.wordpress.org' . $dt->getElementsByTagName( 'a' )->item( 1 )->getAttribute( 'href' );
$author_user_name = $dt->getElementsByTagName( 'em' )->item( 0 )->textContent;
$author_full_name = $this->get_display_name( $author_user_name );
$datetime = new DateTime( $dt->getElementsByTagName( 'a' )->item( 2 )->textContent, new DateTimeZone( 'UTC' ) );
$dd = $xpath->query( 'following-sibling::*[1][self::dd]', $dt )->item( 0 );
$description = null;
if ( $dd ) {
$description = trim( $dd->textContent );
}
$author_email = sprintf( '%s@git.wordpress.org', $author_user_name );
$attachments[] = compact(
'patch_file_name',
'patch_nice_url',
'patch_raw_url',
'author_user_name',
'author_full_name',
'author_email',
'datetime',
'description'
);
}
if ( 0 === count( $attachments ) ) {
WP_CLI::warning( 'No patches on ticket' );
return;
}
$commit = trim( shell_exec( sprintf( 'git --no-pager log -1 --format="%%H %%P" --grep %s', escapeshellarg( $attachments[0]['patch_nice_url'] ) ) ) );
if ( ! empty( $commit ) ) {
$hashes = explode( ' ', $commit );
$base_treeish = $hashes[1]; // The parent of the first patch that was applied.
WP_CLI::log( 'Initial base commit for branch:' );
system( sprintf( 'git --no-pager log -1 %s', escapeshellarg( $base_treeish ) ) );
WP_CLI::line();
$last_merge_commit_hash = trim( shell_exec( sprintf( 'git --no-pager log -1 --reverse --merges --format="%%H" %s...HEAD', escapeshellarg( $base_treeish ) ) ) );
if ( $last_merge_commit_hash ) {
WP_CLI::log( 'Fast-forwarding base commit to last merge:' );
system( sprintf( 'git log -1 %s', escapeshellarg( $last_merge_commit_hash ) ) );
WP_CLI::line();
$base_treeish = $last_merge_commit_hash;
}
} else {
$base_treeish = trim( shell_exec( 'git show-ref --heads -s master' ) );
}
$patches_applied = array();
$patches_needed = array();
foreach ( $attachments as $attachment ) {
$commit_hash = trim( shell_exec( sprintf( 'git --no-pager log -1 --format="%%H" --fixed-strings --grep %s HEAD', escapeshellarg( $attachment['patch_nice_url'] ) ) ) );
if ( empty( $commit_hash ) ) {
$commit_hash = trim( shell_exec( sprintf( 'git --no-pager log -1 --format="%%H" --fixed-strings --grep %s HEAD', escapeshellarg( $attachment['patch_raw_url'] ) ) ) );
}
if ( ! empty( $commit_hash ) ) {
WP_CLI::log( "Skipping previously-applied patch $attachment[patch_file_name] by $attachment[author_user_name], see commit $commit_hash." );
$patches_applied[] = $attachment;
foreach ( $patches_needed as $dropped_attachment ) {
WP_CLI::log( "Dropping apparently already-applied patch $dropped_attachment[patch_file_name] by $dropped_attachment[author_user_name]." );
}
$patches_needed = array();
} else {
$patches_needed[] = $attachment;
}
}
foreach ( $patches_needed as $patch ) {
$cache_file = '/tmp/wp-trac-patch-cache.' . md5( $patch['patch_raw_url'] ) . '-' . $patch['patch_file_name'];
if ( file_exists( $cache_file ) ) {
$contents = file_get_contents( $cache_file );
WP_CLI::log( 'Cache file: ' . $cache_file );
} else {
WP_CLI::log( 'Fetching: ' . $patch['patch_raw_url'] );
$contents = file_get_contents( $patch['patch_raw_url'] );
file_put_contents( $cache_file, $contents );
}
if ( empty( $contents ) ) {
WP_CLI::error( 'HTTP error, empty patch: ' . $patch['patch_raw_url'] );
}
$changed_files = array();
if ( ! preg_match_all( '#^(?:---|\+\+\+) (\S+)#m', $contents, $matches ) ) {
WP_CLI::error( "Malformed patch: $cache_file" );
}
foreach ( array_unique( $matches[1] ) as $path ) {
if ( preg_match( '#^build/#', $path ) ) {
continue;
}
if ( ! $this->is_develop_repo_rooted( $path ) ) {
$path = 'src/' . $path;
}
$changed_files[] = $path;
}
file_put_contents( '.git/COMMIT_EDITMSG', $patch['patch_nice_url'] );
system( sprintf( 'git checkout %s -- .', escapeshellarg( $base_treeish ) ) );
system( sprintf( 'grunt patch:%s', escapeshellarg( $cache_file ), $return_var ) );
if ( 0 !== $return_var ) {
WP_CLI::error( 'Failed to apply patch. Resolve conflicts and `git commit -F .git/COMMIT_EDITMSG`, or `git merge master` and try again.' );
}
system( 'git add -A ' . join( ' ', array_map( 'escapeshellarg', $changed_files ) ) );
system( 'git diff-index --quiet HEAD', $return_var );
if ( 1 === $return_var ) {
system(
sprintf( 'git commit --author=%s -F .git/COMMIT_EDITMSG',
escapeshellarg( sprintf( '%s <%s>', $patch['author_full_name'], $patch['author_email'] ) ),
escapeshellarg( $patch['patch_nice_url'] )
),
$return_var
);
}
}
}
/**
* Prepare commit message, sorting props list by number of contributions made by each patch.
*/
function commit_message() {
// @todo
}
/**
* @param string $username
*/
protected function get_display_name( $username ) {
$cache_file = '/tmp/wp-trac-user-display-names.json';
if ( is_readable( $cache_file ) ) {
$display_names = json_decode( file_get_contents( $cache_file ), true );
} else {
$display_names = array();
}
if ( array_key_exists( $username, $display_names ) ) {
return $display_names[ $username ];
}
$html = file_get_contents( "https://profiles.wordpress.org/$username" );
WP_CLI::log( "Fetching display name for $username" );
$doc = new DOMDocument();
// @codingStandardsIgnoreStart
@$doc->loadHTML( $html );
// @codingStandardsIgnoreEnd
$xpath = new DOMXPath( $doc );
$h2 = $xpath->query( '//h2[ @class = "fn" ]' )->item( 0 );
if ( $h2 ) {
$display_names[ $username ] = $h2->textContent;
} else {
$display_names[ $username ] = $username;
}
file_put_contents( $cache_file, json_encode( $display_names ) );
return $display_names[ $username ];
}
protected function is_develop_repo_rooted( $path ) {
$node = preg_replace( '#/.*#', '', $path );
$root_nodes = array(
'.editorconfig',
'.gitignore',
'.jshintrc',
'.travis.yml',
'Gruntfile.js',
'package.json',
'phpunit.xml.dist',
'src',
'tests',
'tools',
'wp-cli.yml',
'wp-config-sample.php',
'wp-tests-config-sample.php',
);
return in_array( $node, $root_nodes );
}
}
<?php
/**
* Plugin Name: WP-CLI Git-Branch Core Trac
* Author: XWP, Weston Ruter
*/
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once __DIR__ . '/class-wp-cli-trac-patched-git-branch.php';
WP_CLI::add_command( 'git-apply-core-trac-patches', 'WP_CLI_Trac_Patched_Git_Branch' );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment