Last active
November 18, 2015 22:38
-
-
Save westonruter/5c453a44bd36f6847c36 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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