wenpai.org/wp-content/mu-plugins/tools/class-svn.php
2024-06-21 23:57:39 +08:00

349 lines
10 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Platform\Tools;
/**
* 封装常用的 SVN 操作方法
*
* 该类中不包括任何需要 SVN 管理员账号才能调用的功能。
*
* @package Platform\Tools
*/
class SVN {
/**
* 从 URL 获取 SVN 仓库信息
*
* @static
*
* @param string $url SVN 仓库 URL。
* @param array $options 可选。要传递给 SVN 的选项列表。默认值:空数组。
*
* @return array {
* @type bool|array $result 失败为假。 否则为关联数组。
* @type bool|array $errors 是否遇到任何错误或警告。
* }
*/
public static function info( string $url, array $options = array() ): array {
$esc_url = escapeshellarg( $url );
$options[] = 'non-interactive';
$esc_options = self::parse_esc_parameters( $options );
$output = self::shell_exec( "svn info $esc_options $esc_url 2>&1" );
if ( preg_match( '!URL: ' . untrailingslashit( $url ) . '\n!i', $output ) ) {
$lines = explode( "\n", $output );
$result = array_filter( array_reduce(
$lines,
function ( $carry, $item ) {
$pair = explode( ':', $item, 2 );
if ( isset( $pair[1] ) ) {
$key = trim( $pair[0] );
$carry[ $key ] = trim( $pair[1] );
} else {
$carry[] = trim( $pair[0] );
}
return $carry;
},
array()
) );
$errors = false;
} else {
$result = false;
$errors = self::parse_svn_errors( $output );
}
return compact( 'result', 'errors' );
}
/**
* 从 URL 导出 SVN 仓库。
*
* @static
*
* @param string $url SVN 仓库 URL。
* @param string $destination 要导出到的本地文件夹。
* @param array $options 可选。要传递给 SVN 的选项列表。默认值:空数组。
*
* @return array {
* @type bool $result 操作的结果。
* @type int $revision 已导出的修订。
* @type false|array $errors 是否遇到任何错误或警告。
* }
*/
public static function export( string $url, string $destination, array $options = array() ): array {
$options[] = 'non-interactive';
$esc_options = self::parse_esc_parameters( $options );
$esc_url = escapeshellarg( $url );
$esc_destination = escapeshellarg( $destination );
$output = self::shell_exec( "svn export $esc_options $esc_url $esc_destination 2>&1" );
if ( preg_match( '/Exported revision (?P<revision>\d+)[.]/i', $output, $m ) ) {
$revision = (int) $m['revision'];
$result = true;
$errors = false;
} else {
$result = false;
$revision = false;
$errors = self::parse_svn_errors( $output );
}
return compact( 'result', 'revision', 'errors' );
}
/**
* 从 URL 签出 SVN 仓库到本地
*
* @static
*
* @param string $url SVN 仓库 URL。
* @param string $destination 要签出到的本地文件夹。
* @param array $options 可选。要传递给 SVN 的选项列表。默认值:空数组。
*
* @return array {
* @type bool $result 操作的结果。
* @type int $revision 已导出的修订。
* @type false|array $errors 是否遇到任何错误或警告。
* }
*/
public static function checkout( string $url, string $destination, array $options = array() ): array {
$options[] = 'non-interactive';
$esc_options = self::parse_esc_parameters( $options );
$esc_url = escapeshellarg( $url );
$esc_destination = escapeshellarg( $destination );
$output = self::shell_exec( "svn checkout $esc_options $esc_url $esc_destination 2>&1" );
if ( preg_match( '/Checked out revision (?P<revision>\d+)[.]/i', $output, $m ) ) {
$revision = (int) $m['revision'];
$result = true;
$errors = false;
} else {
$result = false;
$revision = false;
$errors = self::parse_svn_errors( $output );
}
return compact( 'result', 'revision', 'errors' );
}
/**
* 更新 SVN 签出。
*
* @static
*
* @param string $checkout 要更新的 SVN 签出的路径。
* @param array $options 可选。要传递给 SVN 的选项列表。默认值:空数组。
*
* @return array {
* @type bool $result 操作的结果。
* @type int $revision 已导出的修订。
* @type false|array $errors 是否遇到任何错误或警告。
* }
*/
public static function up( string $checkout, array $options = array() ): array {
$options[] = 'non-interactive';
$esc_options = self::parse_esc_parameters( $options );
$esc_checkout = escapeshellarg( $checkout );
$output = self::shell_exec( "svn up $esc_options $esc_checkout 2>&1" );
if ( preg_match( '/Updated to revision (?P<revision>\d+)[.]/i', $output, $m ) ) {
$revision = (int) $m['revision'];
$result = true;
$errors = false;
} else {
$result = false;
$revision = false;
$errors = self::parse_svn_errors( $output );
}
return compact( 'result', 'revision', 'errors' );
}
/**
* 列出远程 SVN 中的文件。
*
* @static
*
* @param string $url SVN 仓库 URL。
* @param bool $verbose 可选。是否使用额外元数据详细列出文件。默认值false。
*
* @return array|bool 如果不详细,则为文件列表;如果详细,则为包含文件名、日期、文件大小、作者和版本的项目数组。如果出错返回 false
*/
public static function ls( string $url, bool $verbose = false ): bool|array {
$options = array(
'non-interactive',
'xml',
);
$esc_options = self::parse_esc_parameters( $options );
$esc_url = escapeshellarg( $url );
$output = self::shell_exec( "svn ls $esc_options $esc_url 2>&1" );
$errors = self::parse_svn_errors( $output );
if ( $errors ) {
return false;
}
$errors = libxml_use_internal_errors( true );
$xml = simplexml_load_string( $output );
libxml_use_internal_errors( $errors );
$files = [];
foreach ( $xml->list->children() as $entry ) {
$files[] = [
'revision' => (int) $entry->commit['revision'],
'author' => (string) $entry->commit->author,
'filesize' => (int) $entry->size,
'date' => gmdate( 'Y-m-d H:i:s', strtotime( (string) $entry->commit->date ) ),
'filename' => (string) $entry->name,
'kind' => (string) $entry['kind'],
];
}
if ( ! $verbose ) {
return wp_list_pluck( $files, 'filename' );
}
return $files;
}
/**
* 获取给定修订或修订范围的 SVN 修订。
*
* @static
*
* @param string $url SVN 仓库 URL。
* @param array|string $revision 可选。要获取有关信息的修订。默认值HEAD。
* @param array $options 可选。传递给 SVN 的选项列表。默认值:空数组。
*
* @return array {
* @type array|false $errors 是否遇到任何错误或警告。
* @type array $log SVN 日志数据结构。
* }
*/
public static function log( string $url, array|string $revision = 'HEAD', array $options = array() ): array {
$options[] = 'non-interactive';
$options[] = 'verbose';
$options[] = 'xml';
$options['revision'] = is_array( $revision ) ? sprintf( '{%s}:{%s}', $revision[0], $revision[1] ) : $revision;
$esc_options = self::parse_esc_parameters( $options );
$esc_url = escapeshellarg( $url );
$output = self::shell_exec( "svn log $esc_options $esc_url 2>&1" );
$errors = self::parse_svn_errors( $output );
$log = array();
/*
* 我们在这里使用时髦的字符串修饰来提取 XML因为它可能已被 SVN 错误截断,或以 SVN 警告为后缀。
*/
$xml = substr( $output, $start = stripos( $output, '<?xml' ), $end = ( strripos( $output, '</log>' ) - $start + 6 ) );
if ( $xml && false !== $start && false !== $end ) {
$user_errors = libxml_use_internal_errors( true );
$simple_xml = simplexml_load_string( $xml );
libxml_use_internal_errors( $user_errors );
if ( ! $simple_xml ) {
$errors[] = "SimpleXML failed to parse input";
} else {
foreach ( $simple_xml->logentry as $entry ) {
$revision = (int) $entry->attributes()['revision'];
$paths = array();
foreach ( $entry->paths->children() as $child_path ) {
$paths[] = (string) $child_path;
}
$log[ $revision ] = array(
'revision' => $revision,
'author' => (string) $entry->author,
'date' => strtotime( (string) $entry->date ),
'paths' => $paths,
'message' => (string) $entry->msg,
);
}
}
}
return compact( 'log', 'errors' );
}
/**
* 解析和转义提供的 SVN 参数以在 CLI 上使用。
*
* 参数可以作为 [ param ] 或 [ param = value ] 传递,如果参数没有以 - 为前缀,它将根据需要以 1 或 2 个 - 作为前缀。
*
* @static
* @access protected
*
* @param array $params 提供的参数数组。
*
* @return string 为 CLI 使用而格式化和转义的参数。
*/
protected static function parse_esc_parameters( array $params ): string {
$result = array();
foreach ( $params as $key => $value ) {
$no_parameters = is_numeric( $key );
if ( $no_parameters ) {
$key = $value;
}
// 如果选项长度超过2个字符需要在其前面加“-”或“--”。
if ( ! str_starts_with( $key, '-' ) ) {
$key = '-' . ( strlen( $key ) > 2 ? '-' : '' ) . $key;
}
$result[] = escapeshellarg( $key ) . ( $no_parameters ? '' : ' ' . escapeshellarg( $value ) );
}
return implode( ' ', $result );
}
/**
* 分析SVN输出以检测 SVN 错误并引发异常。
*
* @static
* @access protected
*
* @param string $output SVN 的输出。
*
* @return false|array 如果在输出中未检测到错误/警告则为False如果检测到则为包含警告/错误代码/错误消息的数组。
*/
protected static function parse_svn_errors( string $output ): bool|array {
if ( preg_match_all( '!^svn: (?P<warning>warning:)?\s*(?<error_code>[EW]\d+):\s*(?P<error_message>.+)$!im', $output, $messages, PREG_SET_ORDER ) ) {
$messages = array_map( function ( $item ) {
return array_filter( $item, 'is_string', ARRAY_FILTER_USE_KEY );
}, $messages );
return $messages;
}
return false;
}
/**
* 执行具有“正确”语言环境/语言设置的命令以便正确处理utf8字符串。
*
* WordPress.org 使用 en_US.UTF-8 环境。
*
* @static
* @access protected
*
* @param string $command 要执行的命令。
*
* @return false|string|null 已执行命令的输出,如果发生错误或命令不产生输出,则为 NULL。
*/
protected static function shell_exec( string $command ): bool|string|null {
return shell_exec( 'export LC_CTYPE="en_US.UTF-8" LANG="en_US.UTF-8"; ' . $command );
}
}