init
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script to get changes between feature branch and the mainline
|
||||
*
|
||||
* @category dev
|
||||
* @package build
|
||||
* Copyright © Magento, Inc. All rights reserved.
|
||||
* See COPYING.txt for license details.
|
||||
*/
|
||||
|
||||
define(
|
||||
'USAGE',
|
||||
<<<USAGE
|
||||
php -f get_github_changes.php --
|
||||
--output-file="<output_file>"
|
||||
--base-path="<base_path>"
|
||||
--repo="<main_repo>"
|
||||
--branch="<branch>"
|
||||
[--file-extensions="<comma_separated_list_of_formats>"]
|
||||
|
||||
USAGE
|
||||
);
|
||||
|
||||
$options = getopt('', ['output-file:', 'base-path:', 'repo:', 'file-extensions:', 'branch:']);
|
||||
|
||||
$requiredOptions = ['output-file', 'base-path', 'repo', 'branch'];
|
||||
if (!validateInput($options, $requiredOptions)) {
|
||||
echo USAGE;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$fileExtensions = explode(',', isset($options['file-extensions']) ? $options['file-extensions'] : 'php');
|
||||
|
||||
include_once __DIR__ . '/framework/autoload.php';
|
||||
|
||||
$mainline = 'mainline_' . (string)rand(0, 9999);
|
||||
$repo = getRepo($options, $mainline);
|
||||
$branches = $repo->getBranches('--remotes');
|
||||
generateBranchesList($options['output-file'], $branches, $options['branch']);
|
||||
$changes = retrieveChangesAcrossForks($mainline, $repo, $options['branch']);
|
||||
$changedFiles = getChangedFiles($changes, $fileExtensions);
|
||||
generateChangedFilesList($options['output-file'], $changedFiles);
|
||||
saveChangedFileContent($repo);
|
||||
|
||||
$additions = retrieveNewFilesAcrossForks($mainline, $repo, $options['branch']);
|
||||
$addedFiles = getChangedFiles($additions, $fileExtensions);
|
||||
$additionsFile = pathinfo($options['output-file']);
|
||||
$additionsFile = $additionsFile['dirname']
|
||||
. DIRECTORY_SEPARATOR
|
||||
. $additionsFile['filename']
|
||||
. '.added.'
|
||||
. $additionsFile['extension'];
|
||||
generateChangedFilesList($additionsFile, $addedFiles);
|
||||
|
||||
cleanup($repo, $mainline);
|
||||
|
||||
/**
|
||||
* Save changed file content.
|
||||
*
|
||||
* @param GitRepo $repo
|
||||
* @return void
|
||||
*/
|
||||
function saveChangedFileContent(GitRepo $repo)
|
||||
{
|
||||
$changedFilesContentFileName = BP . Magento\TestFramework\Utility\ChangedFiles::CHANGED_FILES_CONTENT_FILE;
|
||||
foreach ($repo->getChangedContentFiles() as $key => $changedContentFile) {
|
||||
$filePath = sprintf($changedFilesContentFileName, $key);
|
||||
$oldContent = file_exists($filePath) ? file_get_contents($filePath) : '{}';
|
||||
$oldData = json_decode($oldContent, true);
|
||||
$data = array_merge($oldData, $changedContentFile);
|
||||
file_put_contents($filePath, json_encode($data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a file containing changed files
|
||||
*
|
||||
* @param string $outputFile
|
||||
* @param array $changedFiles
|
||||
* @return void
|
||||
*/
|
||||
function generateChangedFilesList($outputFile, $changedFiles)
|
||||
{
|
||||
$changedFilesList = fopen($outputFile, 'w');
|
||||
foreach ($changedFiles as $file) {
|
||||
fwrite($changedFilesList, $file . PHP_EOL);
|
||||
}
|
||||
fclose($changedFilesList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a file containing origin branches
|
||||
*
|
||||
* @param string $outputFile
|
||||
* @param array $branches
|
||||
* @param string $branchName
|
||||
* @return void
|
||||
*/
|
||||
function generateBranchesList($outputFile, $branches, $branchName)
|
||||
{
|
||||
$branchOutputFile = str_replace('changed_files', 'branches', $outputFile);
|
||||
$branchesList = fopen($branchOutputFile, 'w');
|
||||
fwrite($branchesList, $branchName . PHP_EOL);
|
||||
foreach ($branches as $branch) {
|
||||
fwrite($branchesList, substr(strrchr($branch, '/'), 1) . PHP_EOL);
|
||||
}
|
||||
fclose($branchesList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets list of changed files
|
||||
*
|
||||
* @param array $changes
|
||||
* @param array $fileExtensions
|
||||
* @return array
|
||||
*/
|
||||
function getChangedFiles(array $changes, array $fileExtensions)
|
||||
{
|
||||
$files = [];
|
||||
foreach ($changes as $fileName) {
|
||||
foreach ($fileExtensions as $extensions) {
|
||||
$isFileExension = strpos($fileName, '.' . $extensions);
|
||||
if ($isFileExension) {
|
||||
$files[] = $fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves changes across forks
|
||||
*
|
||||
* @param array $options
|
||||
* @param string $mainline
|
||||
* @return GitRepo
|
||||
* @throws Exception
|
||||
*/
|
||||
function getRepo($options, $mainline)
|
||||
{
|
||||
$repo = new GitRepo($options['base-path']);
|
||||
$repo->addRemote($mainline, $options['repo']);
|
||||
$repo->fetch($mainline);
|
||||
return $repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine list of changed files based on comparison between forks.
|
||||
*
|
||||
* @param string $mainline
|
||||
* @param GitRepo $repo
|
||||
* @param string $branchName
|
||||
* @return array
|
||||
*/
|
||||
function retrieveChangesAcrossForks($mainline, GitRepo $repo, $branchName)
|
||||
{
|
||||
return $repo->compareChanges($mainline, $branchName, GitRepo::CHANGE_TYPE_ALL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine list of new files based on comparison between forks.
|
||||
*
|
||||
* @param string $mainline
|
||||
* @param GitRepo $repo
|
||||
* @param string $branchName
|
||||
* @return array
|
||||
*/
|
||||
function retrieveNewFilesAcrossForks($mainline, GitRepo $repo, $branchName)
|
||||
{
|
||||
return $repo->compareChanges($mainline, $branchName, GitRepo::CHANGE_TYPE_ADDED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes temporary "base" repo
|
||||
*
|
||||
* @param GitRepo $repo
|
||||
* @param string $mainline
|
||||
*/
|
||||
function cleanup($repo, $mainline)
|
||||
{
|
||||
$repo->removeRemote($mainline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input options based on required options
|
||||
*
|
||||
* @param array $options
|
||||
* @param array $requiredOptions
|
||||
* @return bool
|
||||
*/
|
||||
function validateInput(array $options, array $requiredOptions)
|
||||
{
|
||||
foreach ($requiredOptions as $requiredOption) {
|
||||
if (!isset($options[$requiredOption]) || empty($options[$requiredOption])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//@codingStandardsIgnoreStart
|
||||
class GitRepo
|
||||
// @codingStandardsIgnoreEnd
|
||||
{
|
||||
const CHANGE_TYPE_ADDED = 1;
|
||||
const CHANGE_TYPE_MODIFIED = 2;
|
||||
const CHANGE_TYPE_ALL = 3;
|
||||
|
||||
/**
|
||||
* Absolute path to git project
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $workTree;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $remoteList = [];
|
||||
|
||||
/**
|
||||
* Array of changed content files.
|
||||
*
|
||||
* Example:
|
||||
* 'extension' =>
|
||||
* 'path_to_file/filename' => 'Content that was edited',
|
||||
* 'path_to_file/filename2' => 'Content that was edited',
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $changedContentFiles = [];
|
||||
|
||||
/**
|
||||
* @param string $workTree absolute path to git project
|
||||
*/
|
||||
public function __construct($workTree)
|
||||
{
|
||||
if (empty($workTree) || !is_dir($workTree)) {
|
||||
throw new UnexpectedValueException('Working tree should be a valid path to directory');
|
||||
}
|
||||
$this->workTree = $workTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds remote
|
||||
*
|
||||
* @param string $alias
|
||||
* @param string $url
|
||||
*/
|
||||
public function addRemote($alias, $url)
|
||||
{
|
||||
if (isset($this->remoteList[$alias])) {
|
||||
return;
|
||||
}
|
||||
$this->remoteList[$alias] = $url;
|
||||
|
||||
$this->call(sprintf('remote add %s %s', $alias, $url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove remote
|
||||
*
|
||||
* @param string $alias
|
||||
*/
|
||||
public function removeRemote($alias)
|
||||
{
|
||||
if (isset($this->remoteList[$alias])) {
|
||||
$this->call(sprintf('remote rm %s', $alias));
|
||||
unset($this->remoteList[$alias]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches remote
|
||||
*
|
||||
* @param string $remoteAlias
|
||||
*/
|
||||
public function fetch($remoteAlias)
|
||||
{
|
||||
if (!isset($this->remoteList[$remoteAlias])) {
|
||||
throw new LogicException('Alias "' . $remoteAlias . '" is not defined');
|
||||
}
|
||||
|
||||
$this->call(sprintf('fetch %s', $remoteAlias));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns branches
|
||||
*
|
||||
* @param string $source
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getBranches($source = '--all')
|
||||
{
|
||||
$result = $this->call(sprintf('branch ' . $source));
|
||||
|
||||
return is_array($result) ? $result : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns files changes between branch and HEAD
|
||||
*
|
||||
* @param string $remoteAlias
|
||||
* @param string $remoteBranch
|
||||
* @param int $changesType
|
||||
* @return array
|
||||
*/
|
||||
public function compareChanges($remoteAlias, $remoteBranch, $changesType = self::CHANGE_TYPE_ALL)
|
||||
{
|
||||
if (!isset($this->remoteList[$remoteAlias])) {
|
||||
throw new LogicException('Alias "' . $remoteAlias . '" is not defined');
|
||||
}
|
||||
|
||||
$result = $this->call(sprintf('log %s/%s..HEAD --name-status --oneline', $remoteAlias, $remoteBranch));
|
||||
|
||||
return is_array($result)
|
||||
? $this->filterChangedFiles(
|
||||
$result,
|
||||
$remoteAlias,
|
||||
$remoteBranch,
|
||||
$changesType
|
||||
)
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a diff of file for specified remote/branch and filters only those have real changes
|
||||
*
|
||||
* @param array $changes
|
||||
* @param string $remoteAlias
|
||||
* @param string $remoteBranch
|
||||
* @param int $changesType
|
||||
* @return array
|
||||
*/
|
||||
protected function filterChangedFiles(
|
||||
array $changes,
|
||||
$remoteAlias,
|
||||
$remoteBranch,
|
||||
$changesType = self::CHANGE_TYPE_ALL
|
||||
) {
|
||||
$countScannedFiles = 0;
|
||||
$changedFilesMasks = $this->buildChangedFilesMask($changesType);
|
||||
$filteredChanges = [];
|
||||
foreach ($changes as $fileName) {
|
||||
$countScannedFiles++;
|
||||
if (($countScannedFiles % 5000) == 0) {
|
||||
echo $countScannedFiles . " files scanned so far\n";
|
||||
}
|
||||
|
||||
$changeTypeMask = $this->detectChangeTypeMask($fileName, $changedFilesMasks);
|
||||
if (null === $changeTypeMask) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileName = trim(substr($fileName, strlen($changeTypeMask)));
|
||||
if (in_array($fileName, $filteredChanges)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileChanges = $this->getFileChangeDetails($fileName, $remoteAlias, $remoteBranch);
|
||||
if (empty($fileChanges)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(isset($this->changedContentFiles[$fileName]))) {
|
||||
$this->setChangedContentFile($fileChanges, $fileName);
|
||||
}
|
||||
$filteredChanges[] = $fileName;
|
||||
}
|
||||
echo $countScannedFiles . " files scanned\n";
|
||||
|
||||
return $filteredChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build mask of git diff report
|
||||
*
|
||||
* @param int $changesType
|
||||
* @return array
|
||||
*/
|
||||
private function buildChangedFilesMask(int $changesType): array
|
||||
{
|
||||
$changedFilesMasks = [];
|
||||
foreach ([
|
||||
self::CHANGE_TYPE_ADDED => "A\t",
|
||||
self::CHANGE_TYPE_MODIFIED => "M\t",
|
||||
] as $changeType => $changedFilesMask) {
|
||||
if ($changeType & $changesType) {
|
||||
$changedFilesMasks[] = $changedFilesMask;
|
||||
}
|
||||
}
|
||||
return $changedFilesMasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find one of the allowed modification mask returned by git diff.
|
||||
*
|
||||
* Example of change record: "A path/to/added_file"
|
||||
*
|
||||
* @param string $changeRecord
|
||||
* @param array $allowedMasks
|
||||
* @return string|null
|
||||
*/
|
||||
private function detectChangeTypeMask(string $changeRecord, array $allowedMasks)
|
||||
{
|
||||
foreach ($allowedMasks as $mask) {
|
||||
if (strpos($changeRecord, $mask) === 0) {
|
||||
return $mask;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read detailed information about changes in a file
|
||||
*
|
||||
* @param string $fileName
|
||||
* @param string $remoteAlias
|
||||
* @param string $remoteBranch
|
||||
* @return array
|
||||
*/
|
||||
private function getFileChangeDetails(string $fileName, string $remoteAlias, string $remoteBranch): array
|
||||
{
|
||||
if (!is_file($this->workTree . '/' . $fileName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = $this->call(
|
||||
sprintf(
|
||||
'diff HEAD %s/%s -- %s',
|
||||
$remoteAlias,
|
||||
$remoteBranch,
|
||||
$fileName
|
||||
)
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set changed content for file.
|
||||
*
|
||||
* @param array $content
|
||||
* @param string $fileName
|
||||
* @return void
|
||||
*/
|
||||
private function setChangedContentFile(array $content, $fileName)
|
||||
{
|
||||
$changedContent = '';
|
||||
$extension = Magento\TestFramework\Utility\ChangedFiles::getFileExtension($fileName);
|
||||
|
||||
foreach ($content as $item) {
|
||||
if (strpos($item, '---') !== 0 && strpos($item, '-') === 0 && $line = ltrim($item, '-')) {
|
||||
$changedContent .= $line . "\n";
|
||||
}
|
||||
}
|
||||
if ($changedContent !== '') {
|
||||
$this->changedContentFiles[$extension][$fileName] = $changedContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get changed content files collection.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChangedContentFiles()
|
||||
{
|
||||
return $this->changedContentFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes call ro git cli
|
||||
*
|
||||
* @param string $command
|
||||
* @return mixed
|
||||
*/
|
||||
private function call($command)
|
||||
{
|
||||
$gitCmd = sprintf(
|
||||
'git --git-dir %s --work-tree %s',
|
||||
escapeshellarg("{$this->workTree}/.git"),
|
||||
escapeshellarg($this->workTree)
|
||||
);
|
||||
$tmp = sprintf('%s %s', $gitCmd, $command);
|
||||
// exec() have to be here since this is test.
|
||||
// phpcs:ignore Magento2.Security.InsecureFunction
|
||||
exec($tmp, $output);
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user