" --base-path="" --repo="" --branch="" [--file-extensions=""] 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'); $mainline = 'mainline_' . (string)rand(0, 9999); $repo = getRepo($options, $mainline); $changes = retrieveChangesAcrossForks($mainline, $repo, $options['branch']); $changedFiles = getChangedFiles($changes, $fileExtensions); generateChangedFilesList($options['output-file'], $changedFiles); cleanup($repo, $mainline); /** * 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); } /** * 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 array * @throws Exception */ function getRepo($options, $mainline) { $repo = new GitRepo($options['base-path']); $repo->addRemote($mainline, $options['repo']); $repo->fetch($mainline); return $repo; } /** * @param string $mainline * @param GitRepo $repo * @param string $branchName * @return array */ function retrieveChangesAcrossForks($mainline, GitRepo $repo, $branchName) { return $repo->compareChanges($mainline, $branchName); } /** * Deletes temporary "base" repo * * @param GitRepo $repo * @param string $repo */ 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; } class GitRepo { /** * Absolute path to git project * * @var string */ private $workTree; /** * @var array */ private $remoteList = []; /** * @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 files changes between branch and HEAD * * @param string $remoteAlias * @param string $remoteBranch * @return array */ public function compareChanges($remoteAlias, $remoteBranch) { 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 ) : []; } /** * 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 * @return array */ protected function filterChangedFiles(array $changes, $remoteAlias, $remoteBranch) { $changedFilesMasks = [ 'M' => "M\t", 'A' => "A\t" ]; $filteredChanges = []; foreach ($changes as $fileName) { foreach ($changedFilesMasks as $mask) { if (strpos($fileName, $mask) === 0) { $fileName = str_replace($mask, '', $fileName); $fileName = trim($fileName); if (!in_array($fileName, $filteredChanges) && is_file($this->workTree . '/' . $fileName)) { $result = $this->call(sprintf( 'diff HEAD %s/%s -- %s', $remoteAlias, $remoteBranch, $this->workTree . '/' . $fileName) ); if ($result) { $filteredChanges[] = $fileName; } } break; } } } return $filteredChanges; } /** * 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; } }