494 lines
13 KiB
PHP
Executable File
494 lines
13 KiB
PHP
Executable File
<?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;
|
|
}
|
|
}
|