get_github_changes.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. <?php
  2. /**
  3. * Script to get changes between feature branch and the mainline
  4. *
  5. * @category dev
  6. * @package build
  7. * Copyright © Magento, Inc. All rights reserved.
  8. * See COPYING.txt for license details.
  9. */
  10. define(
  11. 'USAGE',
  12. <<<USAGE
  13. php -f get_github_changes.php --
  14. --output-file="<output_file>"
  15. --base-path="<base_path>"
  16. --repo="<main_repo>"
  17. --branch="<branch>"
  18. [--file-extensions="<comma_separated_list_of_formats>"]
  19. USAGE
  20. );
  21. $options = getopt('', ['output-file:', 'base-path:', 'repo:', 'file-extensions:', 'branch:']);
  22. $requiredOptions = ['output-file', 'base-path', 'repo', 'branch'];
  23. if (!validateInput($options, $requiredOptions)) {
  24. echo USAGE;
  25. exit(1);
  26. }
  27. $fileExtensions = explode(',', isset($options['file-extensions']) ? $options['file-extensions'] : 'php');
  28. include_once __DIR__ . '/framework/autoload.php';
  29. $mainline = 'mainline_' . (string)rand(0, 9999);
  30. $repo = getRepo($options, $mainline);
  31. $branches = $repo->getBranches('--remotes');
  32. generateBranchesList($options['output-file'], $branches, $options['branch']);
  33. $changes = retrieveChangesAcrossForks($mainline, $repo, $options['branch']);
  34. $changedFiles = getChangedFiles($changes, $fileExtensions);
  35. generateChangedFilesList($options['output-file'], $changedFiles);
  36. saveChangedFileContent($repo);
  37. $additions = retrieveNewFilesAcrossForks($mainline, $repo, $options['branch']);
  38. $addedFiles = getChangedFiles($additions, $fileExtensions);
  39. $additionsFile = pathinfo($options['output-file']);
  40. $additionsFile = $additionsFile['dirname']
  41. . DIRECTORY_SEPARATOR
  42. . $additionsFile['filename']
  43. . '.added.'
  44. . $additionsFile['extension'];
  45. generateChangedFilesList($additionsFile, $addedFiles);
  46. cleanup($repo, $mainline);
  47. /**
  48. * Save changed file content.
  49. *
  50. * @param GitRepo $repo
  51. * @return void
  52. */
  53. function saveChangedFileContent(GitRepo $repo)
  54. {
  55. $changedFilesContentFileName = BP . Magento\TestFramework\Utility\ChangedFiles::CHANGED_FILES_CONTENT_FILE;
  56. foreach ($repo->getChangedContentFiles() as $key => $changedContentFile) {
  57. $filePath = sprintf($changedFilesContentFileName, $key);
  58. $oldContent = file_exists($filePath) ? file_get_contents($filePath) : '{}';
  59. $oldData = json_decode($oldContent, true);
  60. $data = array_merge($oldData, $changedContentFile);
  61. file_put_contents($filePath, json_encode($data));
  62. }
  63. }
  64. /**
  65. * Generates a file containing changed files
  66. *
  67. * @param string $outputFile
  68. * @param array $changedFiles
  69. * @return void
  70. */
  71. function generateChangedFilesList($outputFile, $changedFiles)
  72. {
  73. $changedFilesList = fopen($outputFile, 'w');
  74. foreach ($changedFiles as $file) {
  75. fwrite($changedFilesList, $file . PHP_EOL);
  76. }
  77. fclose($changedFilesList);
  78. }
  79. /**
  80. * Generates a file containing origin branches
  81. *
  82. * @param string $outputFile
  83. * @param array $branches
  84. * @param string $branchName
  85. * @return void
  86. */
  87. function generateBranchesList($outputFile, $branches, $branchName)
  88. {
  89. $branchOutputFile = str_replace('changed_files', 'branches', $outputFile);
  90. $branchesList = fopen($branchOutputFile, 'w');
  91. fwrite($branchesList, $branchName . PHP_EOL);
  92. foreach ($branches as $branch) {
  93. fwrite($branchesList, substr(strrchr($branch, '/'), 1) . PHP_EOL);
  94. }
  95. fclose($branchesList);
  96. }
  97. /**
  98. * Gets list of changed files
  99. *
  100. * @param array $changes
  101. * @param array $fileExtensions
  102. * @return array
  103. */
  104. function getChangedFiles(array $changes, array $fileExtensions)
  105. {
  106. $files = [];
  107. foreach ($changes as $fileName) {
  108. foreach ($fileExtensions as $extensions) {
  109. $isFileExension = strpos($fileName, '.' . $extensions);
  110. if ($isFileExension) {
  111. $files[] = $fileName;
  112. }
  113. }
  114. }
  115. return $files;
  116. }
  117. /**
  118. * Retrieves changes across forks
  119. *
  120. * @param array $options
  121. * @param string $mainline
  122. * @return GitRepo
  123. * @throws Exception
  124. */
  125. function getRepo($options, $mainline)
  126. {
  127. $repo = new GitRepo($options['base-path']);
  128. $repo->addRemote($mainline, $options['repo']);
  129. $repo->fetch($mainline);
  130. return $repo;
  131. }
  132. /**
  133. * Combine list of changed files based on comparison between forks.
  134. *
  135. * @param string $mainline
  136. * @param GitRepo $repo
  137. * @param string $branchName
  138. * @return array
  139. */
  140. function retrieveChangesAcrossForks($mainline, GitRepo $repo, $branchName)
  141. {
  142. return $repo->compareChanges($mainline, $branchName, GitRepo::CHANGE_TYPE_ALL);
  143. }
  144. /**
  145. * Combine list of new files based on comparison between forks.
  146. *
  147. * @param string $mainline
  148. * @param GitRepo $repo
  149. * @param string $branchName
  150. * @return array
  151. */
  152. function retrieveNewFilesAcrossForks($mainline, GitRepo $repo, $branchName)
  153. {
  154. return $repo->compareChanges($mainline, $branchName, GitRepo::CHANGE_TYPE_ADDED);
  155. }
  156. /**
  157. * Deletes temporary "base" repo
  158. *
  159. * @param GitRepo $repo
  160. * @param string $mainline
  161. */
  162. function cleanup($repo, $mainline)
  163. {
  164. $repo->removeRemote($mainline);
  165. }
  166. /**
  167. * Validates input options based on required options
  168. *
  169. * @param array $options
  170. * @param array $requiredOptions
  171. * @return bool
  172. */
  173. function validateInput(array $options, array $requiredOptions)
  174. {
  175. foreach ($requiredOptions as $requiredOption) {
  176. if (!isset($options[$requiredOption]) || empty($options[$requiredOption])) {
  177. return false;
  178. }
  179. }
  180. return true;
  181. }
  182. //@codingStandardsIgnoreStart
  183. class GitRepo
  184. // @codingStandardsIgnoreEnd
  185. {
  186. const CHANGE_TYPE_ADDED = 1;
  187. const CHANGE_TYPE_MODIFIED = 2;
  188. const CHANGE_TYPE_ALL = 3;
  189. /**
  190. * Absolute path to git project
  191. *
  192. * @var string
  193. */
  194. private $workTree;
  195. /**
  196. * @var array
  197. */
  198. private $remoteList = [];
  199. /**
  200. * Array of changed content files.
  201. *
  202. * Example:
  203. * 'extension' =>
  204. * 'path_to_file/filename' => 'Content that was edited',
  205. * 'path_to_file/filename2' => 'Content that was edited',
  206. *
  207. * @var array
  208. */
  209. private $changedContentFiles = [];
  210. /**
  211. * @param string $workTree absolute path to git project
  212. */
  213. public function __construct($workTree)
  214. {
  215. if (empty($workTree) || !is_dir($workTree)) {
  216. throw new UnexpectedValueException('Working tree should be a valid path to directory');
  217. }
  218. $this->workTree = $workTree;
  219. }
  220. /**
  221. * Adds remote
  222. *
  223. * @param string $alias
  224. * @param string $url
  225. */
  226. public function addRemote($alias, $url)
  227. {
  228. if (isset($this->remoteList[$alias])) {
  229. return;
  230. }
  231. $this->remoteList[$alias] = $url;
  232. $this->call(sprintf('remote add %s %s', $alias, $url));
  233. }
  234. /**
  235. * Remove remote
  236. *
  237. * @param string $alias
  238. */
  239. public function removeRemote($alias)
  240. {
  241. if (isset($this->remoteList[$alias])) {
  242. $this->call(sprintf('remote rm %s', $alias));
  243. unset($this->remoteList[$alias]);
  244. }
  245. }
  246. /**
  247. * Fetches remote
  248. *
  249. * @param string $remoteAlias
  250. */
  251. public function fetch($remoteAlias)
  252. {
  253. if (!isset($this->remoteList[$remoteAlias])) {
  254. throw new LogicException('Alias "' . $remoteAlias . '" is not defined');
  255. }
  256. $this->call(sprintf('fetch %s', $remoteAlias));
  257. }
  258. /**
  259. * Returns branches
  260. *
  261. * @param string $source
  262. * @return array|mixed
  263. */
  264. public function getBranches($source = '--all')
  265. {
  266. $result = $this->call(sprintf('branch ' . $source));
  267. return is_array($result) ? $result : [];
  268. }
  269. /**
  270. * Returns files changes between branch and HEAD
  271. *
  272. * @param string $remoteAlias
  273. * @param string $remoteBranch
  274. * @param int $changesType
  275. * @return array
  276. */
  277. public function compareChanges($remoteAlias, $remoteBranch, $changesType = self::CHANGE_TYPE_ALL)
  278. {
  279. if (!isset($this->remoteList[$remoteAlias])) {
  280. throw new LogicException('Alias "' . $remoteAlias . '" is not defined');
  281. }
  282. $result = $this->call(sprintf('log %s/%s..HEAD --name-status --oneline', $remoteAlias, $remoteBranch));
  283. return is_array($result)
  284. ? $this->filterChangedFiles(
  285. $result,
  286. $remoteAlias,
  287. $remoteBranch,
  288. $changesType
  289. )
  290. : [];
  291. }
  292. /**
  293. * Makes a diff of file for specified remote/branch and filters only those have real changes
  294. *
  295. * @param array $changes
  296. * @param string $remoteAlias
  297. * @param string $remoteBranch
  298. * @param int $changesType
  299. * @return array
  300. */
  301. protected function filterChangedFiles(
  302. array $changes,
  303. $remoteAlias,
  304. $remoteBranch,
  305. $changesType = self::CHANGE_TYPE_ALL
  306. ) {
  307. $countScannedFiles = 0;
  308. $changedFilesMasks = $this->buildChangedFilesMask($changesType);
  309. $filteredChanges = [];
  310. foreach ($changes as $fileName) {
  311. $countScannedFiles++;
  312. if (($countScannedFiles % 5000) == 0) {
  313. echo $countScannedFiles . " files scanned so far\n";
  314. }
  315. $changeTypeMask = $this->detectChangeTypeMask($fileName, $changedFilesMasks);
  316. if (null === $changeTypeMask) {
  317. continue;
  318. }
  319. $fileName = trim(substr($fileName, strlen($changeTypeMask)));
  320. if (in_array($fileName, $filteredChanges)) {
  321. continue;
  322. }
  323. $fileChanges = $this->getFileChangeDetails($fileName, $remoteAlias, $remoteBranch);
  324. if (empty($fileChanges)) {
  325. continue;
  326. }
  327. if (!(isset($this->changedContentFiles[$fileName]))) {
  328. $this->setChangedContentFile($fileChanges, $fileName);
  329. }
  330. $filteredChanges[] = $fileName;
  331. }
  332. echo $countScannedFiles . " files scanned\n";
  333. return $filteredChanges;
  334. }
  335. /**
  336. * Build mask of git diff report
  337. *
  338. * @param int $changesType
  339. * @return array
  340. */
  341. private function buildChangedFilesMask(int $changesType): array
  342. {
  343. $changedFilesMasks = [];
  344. foreach ([
  345. self::CHANGE_TYPE_ADDED => "A\t",
  346. self::CHANGE_TYPE_MODIFIED => "M\t",
  347. ] as $changeType => $changedFilesMask) {
  348. if ($changeType & $changesType) {
  349. $changedFilesMasks[] = $changedFilesMask;
  350. }
  351. }
  352. return $changedFilesMasks;
  353. }
  354. /**
  355. * Find one of the allowed modification mask returned by git diff.
  356. *
  357. * Example of change record: "A path/to/added_file"
  358. *
  359. * @param string $changeRecord
  360. * @param array $allowedMasks
  361. * @return string|null
  362. */
  363. private function detectChangeTypeMask(string $changeRecord, array $allowedMasks)
  364. {
  365. foreach ($allowedMasks as $mask) {
  366. if (strpos($changeRecord, $mask) === 0) {
  367. return $mask;
  368. }
  369. }
  370. return null;
  371. }
  372. /**
  373. * Read detailed information about changes in a file
  374. *
  375. * @param string $fileName
  376. * @param string $remoteAlias
  377. * @param string $remoteBranch
  378. * @return array
  379. */
  380. private function getFileChangeDetails(string $fileName, string $remoteAlias, string $remoteBranch): array
  381. {
  382. if (!is_file($this->workTree . '/' . $fileName)) {
  383. return [];
  384. }
  385. $result = $this->call(
  386. sprintf(
  387. 'diff HEAD %s/%s -- %s',
  388. $remoteAlias,
  389. $remoteBranch,
  390. $fileName
  391. )
  392. );
  393. return $result;
  394. }
  395. /**
  396. * Set changed content for file.
  397. *
  398. * @param array $content
  399. * @param string $fileName
  400. * @return void
  401. */
  402. private function setChangedContentFile(array $content, $fileName)
  403. {
  404. $changedContent = '';
  405. $extension = Magento\TestFramework\Utility\ChangedFiles::getFileExtension($fileName);
  406. foreach ($content as $item) {
  407. if (strpos($item, '---') !== 0 && strpos($item, '-') === 0 && $line = ltrim($item, '-')) {
  408. $changedContent .= $line . "\n";
  409. }
  410. }
  411. if ($changedContent !== '') {
  412. $this->changedContentFiles[$extension][$fileName] = $changedContent;
  413. }
  414. }
  415. /**
  416. * Get changed content files collection.
  417. *
  418. * @return array
  419. */
  420. public function getChangedContentFiles()
  421. {
  422. return $this->changedContentFiles;
  423. }
  424. /**
  425. * Makes call ro git cli
  426. *
  427. * @param string $command
  428. * @return mixed
  429. */
  430. private function call($command)
  431. {
  432. $gitCmd = sprintf(
  433. 'git --git-dir %s --work-tree %s',
  434. escapeshellarg("{$this->workTree}/.git"),
  435. escapeshellarg($this->workTree)
  436. );
  437. $tmp = sprintf('%s %s', $gitCmd, $command);
  438. exec($tmp, $output);
  439. return $output;
  440. }
  441. }