PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/shared.php

https://github.com/mrsinguyen/drupalorg-git
PHP | 536 lines | 392 code | 59 blank | 85 comment | 56 complexity | 115c81228a4bfb0a2764fada121a2fab MD5 | raw file
  1. <?php
  2. if (!defined('LOGLEVEL')) {
  3. // Let an environment variable set the log level
  4. $level = getenv('LOGLEVEL');
  5. if (is_string($level)) {
  6. define('LOGLEVEL', (int) $level);
  7. }
  8. else {
  9. // Or default to 'normal'
  10. define('LOGLEVEL', 3);
  11. }
  12. }
  13. if (!defined('CVS2GIT')) {
  14. $c2g = getenv('CVS2GIT');
  15. if (is_string($c2g)) {
  16. define('CVS2GIT', $c2g);
  17. }
  18. else {
  19. // Or default to 'cvs2git', so whatever's on the PATH
  20. define('CVS2GIT', 'cvs2git');
  21. }
  22. }
  23. global $rename_patterns;
  24. $rename_patterns = array(
  25. 'core' => array(
  26. 'branches' => array(
  27. // Strip DRUPAL- prefix.
  28. '/^DRUPAL-/' => '',
  29. // One version for 4-7 and prior...
  30. '/^(\d)-(\d)$/' => '\1.\2.x',
  31. // And another for D5 and later
  32. '/^(\d)$/' => '\1.x',
  33. // Plus one for the weird, weird security syntax
  34. '/^(\d)-(\d+)-SECURITY$/' => '\1.x-\2-security',
  35. ),
  36. 'tags' => array(
  37. // Strip DRUPAL- prefix.
  38. '/^DRUPAL-/' => '',
  39. // 4-7 and earlier base transform
  40. '/^(\d)-(\d)-(\d+)/' => '\1.\2.\3',
  41. // 5 and later base transform
  42. '/^(\d)-(\d+)/' => '\1.\2',
  43. // And now lowercase all possible extra strings
  44. '/UNSTABLE/' => 'unstable',
  45. '/ALPHA/' => 'alpha',
  46. '/BETA/' => 'beta',
  47. '/RC/' => 'rc',
  48. ),
  49. 'tagmatch' => '/^DRUPAL-\d(-\d)?-\d+(-(\w+)(-)?(\d+)?)?$/',
  50. ),
  51. 'contrib' => array(
  52. 'branches' => array(
  53. // Strip DRUPAL- prefix.
  54. '/^DRUPAL-/' => '',
  55. // Ensure that any "pseudo" branch names are made to follow the official pattern
  56. '/^(\d(-\d)?)$/' => '\1--1',
  57. // With pseudonames converted, do full transform. One version for 4-7 and prior...
  58. '/^(\d)-(\d)--(\d+)$/' => '\1.\2.x-\3.x',
  59. // And another for D5 and later
  60. '/^(\d)--(\d+)$/' => '\1.x-\2.x',
  61. // Plus one for the weird, weird security syntax
  62. '/^(\d)--(\d+)-(\d+)-SECURITY$/' => '\1.x-\2.\3-security',
  63. ),
  64. 'tags' => array(
  65. // Strip DRUPAL- prefix.
  66. '/^DRUPAL-/' => '',
  67. // 4-7 and earlier base transform
  68. '/^(\d)-(\d)--(\d+)-(\d+)$/' => '\1.\2.x-\3.\4',
  69. // Aggressively normalize post-version strings for 4-7 and earlier
  70. '/^(\d)-(\d)--(\d+)-(\d+)(-)?([A-Za-z]+)(-)?(\d+)?$/' => '\1.\2.x-\3.\4-\6\8',
  71. // 5 and later base transform
  72. '/^(\d)--(\d+)-(\d+)$/' => '\1.x-\2.\3',
  73. // Aggressively normalize post-version strings for 5 and later
  74. '/^(\d)--(\d+)-(\d+)(-)?([A-Za-z]+)(-)?(\d+)?$/' => '\1.x-\2.\3-\5\7',
  75. // And now lowercase all possible extra strings
  76. '/UNSTABLE/' => 'unstable',
  77. '/ALPHA/' => 'alpha',
  78. '/BETA/' => 'beta',
  79. '/RC/' => 'rc',
  80. ),
  81. 'tagmatch' => '/^DRUPAL-\d(-\d)?--\d+-\d+((-)?(\w+)(-)?(\d+)?)?$/',
  82. ),
  83. );
  84. function git_invoke($command, $fail_safe = FALSE, $repository_path = NULL, $cwd = NULL, $env = NULL) {
  85. if (!isset($env)) {
  86. $env = $_ENV;
  87. }
  88. if ($repository_path) {
  89. $env['GIT_DIR'] = $repository_path;
  90. }
  91. $descriptor_spec = array(
  92. 1 => array('pipe', 'w'),
  93. 2 => array('pipe', 'w'),
  94. );
  95. git_log('Invoking ' . $command, 'DEBUG');
  96. $process = proc_open($command, $descriptor_spec, $pipes, $cwd, $env);
  97. if (is_resource($process)) {
  98. $stdout = stream_get_contents($pipes[1]);
  99. fclose($pipes[1]);
  100. $stderr = stream_get_contents($pipes[2]);
  101. fclose($pipes[2]);
  102. $return_code = proc_close($process);
  103. if ($return_code != 0 && !$fail_safe) {
  104. throw new Exception("Invocation of '" . $command . "' failed with return code " . $return_code .": \n" . $stdout . $stderr);
  105. }
  106. return $stdout;
  107. }
  108. }
  109. function is_empty_dir($dir){
  110. $files = @scandir($dir);
  111. return (!$files || count($files) <= 2);
  112. }
  113. /**
  114. * Check if directory has any CVS information.
  115. *
  116. * This is actually sort of a recursive problem. If any subdirectory has
  117. * CVS information it can be imported.
  118. */
  119. function is_cvs_dir($dir) {
  120. $files = @scandir($dir);
  121. // If there are no files, fail early.
  122. if (!$files) {
  123. return FALSE;
  124. }
  125. foreach ($files as $file) {
  126. $absolute = $dir . '/' . $file;
  127. // Skip POSIX aliases
  128. if ($file == '.' || $file == '..') continue;
  129. if (is_dir($absolute) && $file == 'Attic') {
  130. return TRUE;
  131. }
  132. elseif (strpos($file, ',v') !== FALSE) {
  133. return TRUE;
  134. }
  135. elseif (is_dir($absolute) && is_cvs_dir($absolute)) {
  136. return TRUE;
  137. }
  138. }
  139. return FALSE;
  140. }
  141. /**
  142. * Recursively delete a directory on a local filesystem.
  143. *
  144. * @param string $path
  145. * The path to the directory.
  146. */
  147. function rmdirr($path) {
  148. foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::CHILD_FIRST) as $item) {
  149. $item->isFile() ? unlink($item) : rmdir($item);
  150. }
  151. rmdir($path);
  152. }
  153. function git_log($message, $level = 'NORMAL', $project = NULL) {
  154. $loglevels = array(
  155. 'WARN' => 1,
  156. 'QUIET' => 2,
  157. 'NORMAL' => 3,
  158. 'INFO' => 4,
  159. 'DEBUG' => 5,
  160. );
  161. if (LOGLEVEL !== 0 && LOGLEVEL >= $loglevels[$level]) {
  162. if (isset($project)) {
  163. echo "[" . date('Y-m-d H:i:s') . "] [$level] [$project] $message\n";
  164. }
  165. else {
  166. echo "[" . date('Y-m-d H:i:s') . "] [$level] $message\n";
  167. }
  168. }
  169. }
  170. /**
  171. * Helper function to import a directory to a git repository.
  172. */
  173. function import_directory($config, $root, $source, $destination, $wipe = FALSE) {
  174. global $rename_patterns, $wd;
  175. // Ensure no trailing slashes for cleanliness
  176. $source = rtrim($source, '/');
  177. $absolute_source_dir = $root . '/' . $source;
  178. $elements = explode('/', $source);
  179. $project = array_pop($elements);
  180. // If the source is an empty directory, skip it; cvs2git barfs on these.
  181. if (is_empty_dir($absolute_source_dir)) {
  182. git_log("Skipping empty source directory '$absolute_source_dir'.", 'QUIET', $project);
  183. return FALSE;
  184. }
  185. if (!is_cvs_dir($absolute_source_dir)) {
  186. git_log("Skipping non CVS source directory '$absolute_source_dir'.", 'QUIET', $project);
  187. return FALSE;
  188. }
  189. // If the target destination dir exists already, remove it.
  190. if ($wipe && file_exists($destination) && is_dir($destination)) {
  191. passthru('rm -Rf ' . escapeshellarg($destination));
  192. }
  193. // Create the destination directory.
  194. $ret = 0;
  195. passthru('mkdir -p ' . escapeshellarg($destination), $ret);
  196. if (!empty($ret)) {
  197. git_log("Failed to create output directory at $destination, project import will not procede.", 'WARN', $project);
  198. return FALSE;
  199. }
  200. // Create a temporary directory, and register a clean up.
  201. $cmd = 'mktemp -dt cvs2git-import-' . escapeshellarg($project) . '.XXXXXXXXXX';
  202. $temp_dir = realpath(trim(`$cmd`));
  203. register_shutdown_function('_clean_up_import', $temp_dir);
  204. // Move to the temporary directory.
  205. chdir($temp_dir);
  206. // Prepare and write the option file.
  207. $options = array(
  208. '#DIR#' => $absolute_source_dir,
  209. '#CSV#' => dirname($config) . '/cvs.csv',
  210. );
  211. file_put_contents('./cvs2git.options', strtr(file_get_contents($config), $options));
  212. // Start the import process.
  213. git_log("Generating the fast-import dump files.", 'DEBUG', $project);
  214. try {
  215. git_invoke(escapeshellarg(CVS2GIT) . ' --options=./cvs2git.options');
  216. }
  217. catch (Exception $e) {
  218. git_log("cvs2git failed with error '$e'. Terminating import.", 'WARN', $project);
  219. return FALSE;
  220. }
  221. // Load the data into git.
  222. git_log("Importing project data into Git.", 'DEBUG', $project);
  223. git_invoke('git init', FALSE, $destination);
  224. try {
  225. git_invoke('cat tmp-cvs2git/git-blob.dat tmp-cvs2git/git-dump.dat | git fast-import --quiet', FALSE, $destination);
  226. }
  227. catch (Exception $e) {
  228. git_log("Fast-import failed with error '$e'. Terminating import.", 'WARN', $project);
  229. return FALSE;
  230. }
  231. // Do branch/tag renaming
  232. git_log("Performing branch/tag renaming.", 'DEBUG', $project);
  233. // For core
  234. if ($project == 'drupal' && array_search('contributions', $elements) === FALSE) { // for core
  235. $trans_map = $rename_patterns['core']['branches'];
  236. convert_project_branches($project, $destination, $trans_map);
  237. // Now tags.
  238. $trans_map = $rename_patterns['core']['tags'];
  239. convert_project_tags($project, $destination, $rename_patterns['core']['tagmatch'], $trans_map);
  240. }
  241. // For contrib, minus sandboxes
  242. else if ($elements[0] == 'contributions' && isset($elements[1]) && $elements[1] != 'sandbox') {
  243. // Branches first.
  244. $trans_map = $rename_patterns['contrib']['branches'];
  245. convert_project_branches($project, $destination, $trans_map);
  246. // Now tags.
  247. $trans_map = $rename_patterns['contrib']['tags'];
  248. convert_project_tags($project, $destination, $rename_patterns['contrib']['tagmatch'], $trans_map);
  249. }
  250. // We succeeded despite all odds!
  251. return TRUE;
  252. }
  253. /*
  254. * Branch/tag renaming functions ------------------------
  255. */
  256. /**
  257. * Convert all of a contrib project's branches to the new naming convention.
  258. */
  259. function convert_project_branches($project, $destination_dir, $trans_map) {
  260. $all_branches = $branches = array();
  261. try {
  262. $all_branches = git_invoke("ls " . escapeshellarg("$destination_dir/refs/heads/"));
  263. $all_branches = array_filter(explode("\n", $all_branches)); // array-ify & remove empties
  264. }
  265. catch (Exception $e) {
  266. git_log("Branch list retrieval failed with error '$e'.", 'WARN', $project);
  267. }
  268. if (empty($all_branches)) {
  269. // No branches at all, bail out.
  270. git_log("Project has no branches whatsoever.", 'WARN', $project);
  271. return;
  272. }
  273. // Kill the 'unlabeled' branches generated by cvs2git
  274. $unlabeleds = preg_grep('/^unlabeled/', $all_branches);
  275. foreach ($unlabeleds as $branch) {
  276. git_invoke('git branch -D ' . escapeshellarg($branch), FALSE, $destination_dir);
  277. }
  278. // Remove cvs2git junk branches from the list.
  279. $all_branches = array_diff($all_branches, $unlabeleds);
  280. // Generate a list of all valid branch names, ignoring master
  281. $branches = preg_grep('/^DRUPAL-/', $all_branches); // @todo be stricter?
  282. // Remove existing branches that have already been converted
  283. if (empty($branches)) {
  284. // No branches to work with, bail out.
  285. if (array_search('master', $all_branches) !== FALSE) {
  286. // Project has only a master branch
  287. git_log("Project has no conforming branches apart from master.", 'INFO', $project);
  288. }
  289. else {
  290. // No non-labelled branches at all. This shouldn't happen; dump the whole list if it does.
  291. git_log("Project has no conforming branches and no master. Full branch list: " . implode(', ', $all_branches), 'WARN', $project);
  292. }
  293. return;
  294. }
  295. // Everything needs the initial DRUPAL- stripped out.
  296. git_log("FULL list of the project's branches: \n" . print_r($all_branches, TRUE), 'DEBUG', $project);
  297. git_log("Branches in \$branches pre-transform: \n" . print_r($branches, TRUE), 'DEBUG', $project);
  298. $branchestmp = preg_replace(array_keys($trans_map), array_values($trans_map), $branches);
  299. git_log("Branches after first transform: \n" . print_r($branchestmp, TRUE), 'DEBUG', $project);
  300. $branches = array_diff($branches, $branchestmp);
  301. $new_branches = preg_replace(array_keys($trans_map), array_values($trans_map), $branches);
  302. git_log("Branches after second transform: \n" . print_r($new_branches, TRUE), 'DEBUG', $project);
  303. foreach(array_combine($branches, $new_branches) as $old_name => $new_name) {
  304. try {
  305. // Now do the rename itself. -M forces overwriting of branches.
  306. git_invoke("git branch -M $old_name $new_name", FALSE, $destination_dir);
  307. }
  308. catch (Exception $e) {
  309. // These are failing sometimes, not sure why
  310. git_log("Branch rename failed on branch '$old_name' with error '$e'", 'WARN', $project);
  311. }
  312. }
  313. verify_project_branches($project, $destination_dir, $new_branches);
  314. }
  315. /**
  316. * Verify that the project contains exactly and only the set of branches we
  317. * expect it to.
  318. */
  319. function verify_project_branches($project, $destination_dir, $branches) {
  320. $all_branches = git_invoke("ls " . escapeshellarg("$destination_dir/refs/heads/"));
  321. $all_branches = array_filter(explode("\n", $all_branches)); // array-ify & remove empties
  322. if ($missing = array_diff($branches, $all_branches)) {
  323. git_log("Project should have the following branches after import, but does not: " . implode(', ', $missing), 'WARN', $project);
  324. }
  325. if ($nonconforming_branches = array_diff($all_branches, $branches, array('master'))) { // Ignore master
  326. git_log("Project has the following nonconforming branches: " . implode(', ', $nonconforming_branches), 'QUIET', $project);
  327. }
  328. }
  329. function convert_project_tags($project, $destination_dir, $match, $trans_map) {
  330. $all_tags = $tags = $new_tags = $nonconforming_tags = array();
  331. try {
  332. $all_tags = git_invoke('git tag -l', FALSE, $destination_dir);
  333. $all_tags = array_filter(explode("\n", $all_tags)); // array-ify & remove empties
  334. }
  335. catch (Exception $e) {
  336. git_log("Tag list retrieval failed with error '$e'", 'WARN', $project);
  337. return;
  338. }
  339. // Convert only tags that match naming conventions
  340. $tags = preg_grep($match, $all_tags);
  341. if (empty($tags)) {
  342. // No conforming tags to work with, bail out.
  343. if (empty($all_tags)) {
  344. git_log("Project has no tags at all.", 'NORMAL', $project);
  345. }
  346. else {
  347. git_log("Project has no conforming tags, and the following nonconforming tags: " . implode(', ', $all_tags), 'WARN', $project);
  348. }
  349. return;
  350. }
  351. // Everything needs the initial DRUPAL- stripped out.
  352. git_log("FULL list of the project's tags: \n" . print_r($all_tags, TRUE), 'DEBUG', $project);
  353. // Have to transform twice to discover tags already converted in previous runs
  354. git_log("Tags in \$tags pre-transform: \n" . print_r($tags, TRUE), 'DEBUG', $project);
  355. $tagstmp = preg_replace(array_keys($trans_map), array_values($trans_map), $tags);
  356. git_log("Tags after first transform: \n" . print_r($tagstmp, TRUE), 'DEBUG', $project);
  357. $tags = array_diff($tags, $tagstmp);
  358. $new_tags = preg_replace(array_keys($trans_map), array_values($trans_map), $tags);
  359. git_log("Tags after second transform: \n" . print_r($new_tags, TRUE), 'DEBUG', $project);
  360. $tag_list = array_combine($tags, $new_tags);
  361. foreach ($tag_list as $old_tag => $new_tag) {
  362. // Add the new tag.
  363. try {
  364. git_invoke("git tag -f $new_tag $old_tag", FALSE, $destination_dir);
  365. git_log("Created new tag '$new_tag' from old tag '$old_tag'", 'INFO', $project);
  366. //if ($key = array_search($new_tag, $all_tags)) {
  367. // existing tag - skip the rest, otherwise it'll delete the new one.
  368. //continue;
  369. //}
  370. }
  371. catch (Exception $e) {
  372. git_log("Creation of new tag '$new_tag' from old tag '$old_tag' failed with message $e", 'WARN', $project);
  373. }
  374. // Delete the old tag.
  375. try {
  376. git_invoke("git tag -d $old_tag", FALSE, $destination_dir);
  377. git_log("Deleted old tag '$old_tag'", 'INFO', $project);
  378. }
  379. catch (Exception $e) {
  380. git_log("Deletion of old tag '$old_tag' in project '$project' failed with message $e", 'WARN', $project);
  381. }
  382. }
  383. git_log("Final tag list: \n" . print_r($tag_list, TRUE), 'DEBUG', $project);
  384. verify_project_tags($project, $destination_dir, $tag_list);
  385. }
  386. /**
  387. * Verify that the project contains exactly and only the set of tags we
  388. * expect it to.
  389. */
  390. function verify_project_tags($project, $destination_dir, $tags) {
  391. $all_tags = git_invoke('git tag -l', FALSE, $destination_dir);
  392. $all_tags = array_filter(explode("\n", $all_tags)); // array-ify & remove empties
  393. if ($missing = array_diff($tags, $all_tags)) {
  394. git_log("Project should have the following tags after import, but does not: " . implode(', ', $missing), 'WARN', $project);
  395. }
  396. if ($nonconforming_tags = array_diff($all_tags, $tags)) {
  397. git_log("Project has the following nonconforming tags: " . implode(', ', $nonconforming_tags), 'QUIET', $project);
  398. }
  399. }
  400. function cleanup_migrated_repo($project, $destination_dir, $keywords, $translations) {
  401. $_ENV['GIT_AUTHOR_EMAIL'] = 'tggm@no-reply.drupal.org';
  402. $_ENV['GIT_AUTHOR_NAME'] = 'The Great Git Migration';
  403. // Create a temporary directory, and register a clean up.
  404. $cmd = 'mktemp -dt cvs2git-import-' . escapeshellarg($project) . '.XXXXXXXXXX';
  405. $temp_dir = realpath(trim(`$cmd`));
  406. register_shutdown_function('_clean_up_import', $temp_dir);
  407. git_invoke("git clone $destination_dir $temp_dir");
  408. try {
  409. $all_branches = git_invoke("ls " . escapeshellarg("$destination_dir/refs/heads/"));
  410. $all_branches = array_filter(explode("\n", $all_branches)); // array-ify & remove empties
  411. }
  412. catch (Exception $e) {
  413. git_log("Branch list retrieval failed with error '$e'.", 'WARN', $project);
  414. }
  415. foreach($all_branches as $name) {
  416. if ($name != 'master') {
  417. git_invoke("git checkout -t origin/$name", FALSE, "$temp_dir/.git", $temp_dir);
  418. }
  419. else {
  420. git_invoke("git checkout $name", FALSE, "$temp_dir/.git", $temp_dir);
  421. }
  422. // If needed, cleanup keywords.
  423. if ($keywords) {
  424. try {
  425. strip_cvs_keywords($project, $temp_dir);
  426. }
  427. catch (exception $e) {
  428. git_log("CVS tag removal for branch $name failed with error '$e'", 'WARN', $project);
  429. }
  430. }
  431. // If needed, cleanup translations.
  432. if ($translations) {
  433. try {
  434. kill_translations($project, $temp_dir);
  435. }
  436. catch (exception $e) {
  437. git_log("Translation removal for branch $name failed with error '$e'", 'WARN', $project);
  438. }
  439. }
  440. }
  441. git_invoke('git push', FALSE, "$temp_dir/.git");
  442. return TRUE;
  443. }
  444. function strip_cvs_keywords($project, $directory) {
  445. passthru('./strip-cvs-keywords.py ' . escapeshellarg($directory));
  446. $commit_message = escapeshellarg("Stripping CVS keywords");
  447. if (git_invoke('git status --untracked-files=no -sz --', TRUE, "$directory/.git", $directory)) {
  448. git_invoke("git commit -a -m $commit_message", FALSE, "$directory/.git", $directory);
  449. }
  450. }
  451. function kill_translations($project, $directory) {
  452. $directories = git_invoke('find ' . escapeshellarg($directory) . ' -name translations -type d');
  453. $translations = array_filter(explode("\n", $directories)); // array-ify & remove empties
  454. $directories = git_invoke('find ' . escapeshellarg($directory) . ' -name po -type d');
  455. $po = array_filter(explode("\n", $directories)); // array-ify & remove empties
  456. $directories = array_merge($translations, $po);
  457. if (!empty($directories)) {
  458. $commit_message = escapeshellarg("Removing translation directories");
  459. foreach ($directories as $dir) {
  460. git_invoke("git rm -r $dir", FALSE, "$directory/.git", $directory);
  461. }
  462. git_invoke("git commit -a -m $commit_message", FALSE, "$directory/.git", $directory);
  463. }
  464. }
  465. // ------- Utility functions -----------------------------------------------
  466. function _clean_up_import($dir) {
  467. git_log("Cleaning up import temp directory $dir.", 'DEBUG');
  468. passthru('rm -Rf ' . escapeshellarg($dir));
  469. }