123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- <?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($tmp, $output);
- return $output;
- }
- }
|