diff --git a/CHANGELOG.md b/CHANGELOG.md index 916eb6c..e6d67ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.4.2] - 2024-05-28 + +### Changed +- Users are now warned if they pull from an archived GitHub repository + ## [2.4.1] - 2024-03-11 ### Changed diff --git a/src/Git/Git.php b/src/Git/Git.php index 1fd744d..c748248 100644 --- a/src/Git/Git.php +++ b/src/Git/Git.php @@ -39,8 +39,69 @@ public function is_repo() return file_exists("{$this->repo_path}/.git"); } + private function is_github_repository($repository) + { + $pos = strpos($repository, 'github.com'); + return $pos !== false; + } + + /** Issue a warning to the user if a GitHub repository is archived. + * + * Note that we specifically ignore any non-GitHub repository for now, + * which is why we have not factored this code into its own class structure. + * + * See: https://docs.github.com/en/rest/repos/repos?get-a-repository + */ + public function check_is_archived_github_repository($repository) + { + if (!$this->is_github_repository($repository)) { + return; + } + $baseurl = 'https://api.github.com/repos'; # Must not have a trailing slash. + $substrings = explode('/', $repository); + $num_substrings = count($substrings); + # If the URL is http formatted: ['https', 'github.com', 'org', 'repo'] + # If the URL is ssh formatted: ['git@git.github.com:org', 'repo'] + if ($num_substrings < 2) { + return false; + } + $repo = $substrings[$num_substrings - 1]; + if (false !== strpos($repo, '.git')) { # repo.git + $repo = str_replace('.git', '', $repo); + } + + if (false !== strpos($repository, '@')) { + # ssh formatted... + $org = explode(':', $substrings[$num_substrings - 2])[1]; + } else { + # http formatted... + $org = $substrings[$num_substrings - 2]; + } + $api_url = join('/', [$baseurl, $org, $repo]); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $api_url); + curl_setopt($curl, CURLOPT_USERAGENT, 'Whippet'); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + $raw_json = curl_exec($curl); + $json = json_decode($raw_json); + curl_close($curl); + if (!is_null($json) && property_exists($json, 'archived') && $json->archived) { + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"; + echo "!! WARNING: GitHub repo is archived. This dependency !!\n"; + echo "!! should be replaced before the repo is removed. !!\n"; + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"; + } + } + public function checkout($revision) { + list($output, $return) = $this->run_command(['git', 'remote', 'get-url', 'origin']); + if ($return === 0) { + $this->check_is_archived_github_repository($output[0]); + } + list($output, $return) = $this->run_command(['git', 'fetch', '-a', '--force', '&&', 'git', 'checkout', $revision]); return $this->check_git_return('Checkout failed', $return, $output); @@ -62,6 +123,8 @@ public function mixed_reset($revision = 'HEAD') public function clone_repo($repository) { + $this->check_is_archived_github_repository($repository); + list($output, $return) = $this->run_command(['git', 'clone', $repository, $this->repo_path], false); if (!$this->check_git_return('Clone failed', $return, $output)) { @@ -73,6 +136,8 @@ public function clone_repo($repository) public function clone_no_checkout($repository) { + $this->check_is_archived_github_repository($repository); + $tmpdir = $this->get_tmpdir(); list($output, $return) = $this->run_command(['git', 'clone', '--no-checkout', $repository, $tmpdir], false); diff --git a/tests/Helpers.php b/tests/Helpers.php index 53290d4..6621250 100644 --- a/tests/Helpers.php +++ b/tests/Helpers.php @@ -31,8 +31,22 @@ private function getWhippetLock(/* string */ $hash, array $dependencyMap) return $whippetLock; } - private function getGit($isRepo, $cloneRepo, $checkout) + private function getArchivedWarning() { + $warning = <<<'EOT' +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!! WARNING: GitHub repo is archived. This dependency !! +!! should be replaced before the repo is removed. !! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +EOT; + return $warning; + } + + private function getGit($isRepo, $cloneRepo, $checkout, $isArchived = false) + { + $archived_warning = $this->getArchivedWarning(); + $git = $this->getMockBuilder('\\Dxw\\Whippet\\Git\\Git') ->disableOriginalConstructor() ->getMock(); @@ -42,6 +56,11 @@ private function getGit($isRepo, $cloneRepo, $checkout) if ($cloneRepo !== null) { $return = true; + $output = "git clone output\n"; + + if ($isArchived) { + $output = $archived_warning . $output; + } if (is_array($cloneRepo)) { $return = $cloneRepo['return']; @@ -51,8 +70,8 @@ private function getGit($isRepo, $cloneRepo, $checkout) $git->expects($this->exactly(1)) ->method('clone_repo') ->with($cloneRepo) - ->will($this->returnCallback(function () use ($return) { - echo "git clone output\n"; + ->will($this->returnCallback(function () use ($output, $return) { + echo $output; return $return; })); @@ -60,6 +79,11 @@ private function getGit($isRepo, $cloneRepo, $checkout) if ($checkout !== null) { $return = true; + $output = "git checkout output\n"; + + if ($isArchived) { + $output = $archived_warning . $output; + } if (is_array($checkout)) { $return = $checkout['return']; @@ -69,8 +93,8 @@ private function getGit($isRepo, $cloneRepo, $checkout) $git->expects($this->exactly(1)) ->method('checkout') ->with($checkout) - ->will($this->returnCallback(function () use ($return) { - echo "git checkout output\n"; + ->will($this->returnCallback(function () use ($output, $return) { + echo $output; return $return; })); diff --git a/tests/dependencies/installer_test.php b/tests/dependencies/installer_test.php index 5911a51..a712e2d 100644 --- a/tests/dependencies/installer_test.php +++ b/tests/dependencies/installer_test.php @@ -161,6 +161,65 @@ public function testInspectionsApiUnavailable() $this->assertEquals($expectedOutput, $output); } + public function testInstallArchiveRepo() + { + $dir = $this->getDir(); + file_put_contents($dir.'/whippet.json', 'foobar'); + file_put_contents($dir.'/whippet.lock', 'foobar'); + + $my_theme = [ + 'name' => 'my-theme', + 'src' => 'git@git.govpress.com:wordpress-themes/my-theme', + 'revision' => '27ba906', + ]; + + $whippetLock = $this->getWhippetLock(sha1('foobar'), [ + 'themes' => [ + $my_theme, + ], + 'plugins' => [], + ]); + $this->addFactoryCallStatic('\\Dxw\\Whippet\\Files\\WhippetLock', 'fromFile', $dir.'/whippet.lock', \Result\Result::ok($whippetLock)); + + $gitMyTheme = $this->getGit(false, 'git@git.govpress.com:wordpress-themes/my-theme', '27ba906', true); + $this->addFactoryNewInstance('\\Dxw\\Whippet\\Git\\Git', $dir.'/wp-content/themes/my-theme', $gitMyTheme); + + $inspection_check_results = function ($type, $dep) { + return [ + 'themes' => [ + 'my-theme' => \Result\Result::ok('') + ], + ][$type][$dep['name']]; + }; + + $dependencies = new \Dxw\Whippet\Dependencies\Installer( + $this->getFactory(), + $this->getProjectDirectory($dir), + $this->fakeInspectionCheckerWithResults($inspection_check_results) + ); + + ob_start(); + $result = $dependencies->installAll(); + $output = ob_get_clean(); + + $this->assertFalse($result->isErr()); + $expectedOutput = <<<'EOT' +[Adding themes/my-theme] +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!! WARNING: GitHub repo is archived. This dependency !! +!! should be replaced before the repo is removed. !! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +git clone output +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!! WARNING: GitHub repo is archived. This dependency !! +!! should be replaced before the repo is removed. !! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +git checkout output + + +EOT; + $this->assertEquals($expectedOutput, $output); + } public function testInstallAllThemeAlreadyCloned() { @@ -468,6 +527,67 @@ public function testInstallSingleAlreadyCloned() $this->assertFalse($result->isErr()); } + public function testInstallSingleAlreadyClonedAndArchived() + { + $dir = $this->getDir(); + file_put_contents($dir.'/whippet.json', 'foobar'); + file_put_contents($dir.'/whippet.lock', 'foobar'); + + $whippetLock = $this->getWhippetLock(sha1('foobar'), [ + 'themes' => [ + [ + 'name' => 'my-theme', + 'src' => 'git@git.govpress.com:wordpress-themes/my-theme', + 'revision' => '27ba906', + ], + ], + 'plugins' => [ + [ + 'name' => 'my-plugin', + 'src' => 'git@git.govpress.com:wordpress-plugins/my-plugin', + 'revision' => '123456', + ], + [ + 'name' => 'another-plugin', + 'src' => 'git@git.govpress.com:wordpress-plugins/another-plugin', + 'revision' => '789abc', + ], + ], + ]); + $this->addFactoryCallStatic('\\Dxw\\Whippet\\Files\\WhippetLock', 'fromFile', $dir.'/whippet.lock', \Result\Result::ok($whippetLock)); + + $gitMyPlugin = $this->getGit(false, 'git@git.govpress.com:wordpress-plugins/my-plugin', '123456', true); + $this->addFactoryNewInstance('\\Dxw\\Whippet\\Git\\Git', $dir.'/wp-content/plugins/my-plugin', $gitMyPlugin); + + $dependencies = new \Dxw\Whippet\Dependencies\Installer( + $this->getFactory(), + $this->getProjectDirectory($dir), + $this->fakeInspectionChecker() + ); + ob_start(); + $result = $dependencies->installSingle('plugins/my-plugin'); + $output = ob_get_clean(); + $expectedOutput = <<<'EOT' +[Adding plugins/my-plugin] +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!! WARNING: GitHub repo is archived. This dependency !! +!! should be replaced before the repo is removed. !! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +git clone output +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!! WARNING: GitHub repo is archived. This dependency !! +!! should be replaced before the repo is removed. !! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +git checkout output + + +EOT; + + + $this->assertEquals($expectedOutput, $output); + $this->assertFalse($result->isErr()); + } + public function testInstallSingleCloneFails() { $dir = $this->getDir();