diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index fed59d8..444ab54 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -47,6 +47,9 @@ jobs: - name: Show PHP version run: php -v + - name: Install Composer dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + - name: Generate HTML files run: php generate.php diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index ec68bab..fa6e588 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -21,7 +21,7 @@ jobs: php-version: 8.2 - name: Install Composer dependencies - run: composer update --no-interaction --prefer-dist + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - name: Run tests via PHPStan run: composer phpstan diff --git a/.gitignore b/.gitignore index 035edb9..1049f24 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,12 @@ # Composer /vendor/ +# PHPUnit +.phpunit.cache + # Generated files index.html lang + +# Mac OS +.DS_Store diff --git a/generate.php b/generate.php index cc4affe..13c4b4d 100644 --- a/generate.php +++ b/generate.php @@ -1,110 +1,11 @@ run() +require_once('./vendor/autoload.php'); +use GoodFirstIssue\Generator; +use GoodFirstIssue\GitHubAPIClient; -if (!is_dir('lang')) { - mkdir('lang'); -} - - -// Read the JSON file -$json = file_get_contents('repositories.json'); - -// Decode the JSON file -$json_data = json_decode($json, true); - -// Display data -print_r($json_data); - -// TODO Рандомизируем массив с репозиториями - -$indexContent = "

Hello!

"; - -$repositoriesByLanguage = []; - -// Проходимся по всем репозиториям -foreach ($json_data as $line) { - echo "Line : https://api.github.com/repos/" . $line . "\n"; - // Записываем информацию о репозитории в файл data/repositories.json - // Это необходимо для главной страницы - - $opts = [ - 'http' => [ - 'method' => 'GET', - 'header' => [ - 'User-Agent: PHP' - ] - ] - ]; - - $context = stream_context_create($opts); - $repositoryJson = file_get_contents('https://api.github.com/repos/' . $line, false, $context); - $repository = json_decode($repositoryJson, true); - - $repositoryData = [ - 'html_url' => $repository['html_url'], // Ex: "https://github.com/octocat/Hello-World" - 'full_name' => $repository['full_name'], // Ex: "octocat/Hello-World" - 'description' => $repository['description'], // Ex: "This your first repo!" - 'language' => $repository['language'], // Ex: null, - 'stargazers_count' => $repository['stargazers_count'], // Ex: 80, - 'open_issues_count' => $repository['open_issues_count'], // Ex: 0, - 'open_issues' => $repository['open_issues'], // Ex: 0, - 'updated_at' => $repository['updated_at'], // Ex: "2011-01-26T19:14:43Z", - ]; - - print_r($repositoryData); - - // Конетент для главной страницы - $indexContent .= '

' . $repositoryData['full_name'] . '

'; - $indexContent .= '

' . $repositoryData['description'] . '

'; - - $repositoriesByLanguage[$repositoryData['language']][] = $repositoryData['full_name']; - - // Записываем ищуйки в общий файл -} - - -file_put_contents('index.html', $indexContent); - - -foreach ($repositoriesByLanguage as $lang => $repositories) { - if (strlen($lang) < 1) { - $lang = 'other'; - } - - print_r('Language: ' . $lang); - - $langFile = 'lang/' . $lang . '.html'; - if (file_exists($langFile)) { - $status = unlink($langFile) ? 'The file ' . $langFile . ' has been deleted' . "\n" : 'Error deleting ' . $langFile . "\n"; - echo $status; - } - - - // TODO Пишем шапку файла - file_put_contents($langFile, '

Lang: ' . $lang . '

' . "\n"); - - foreach ($repositories as $repository) { - print_r('Repository: ' . $repository."\n"); - - $issuesJson = file_get_contents('https://api.github.com/repos/' . $repository . '/issues?state=open&sort=updated&labels=good%20first%20issue', false, $context); - $issues = json_decode($issuesJson, true); - - foreach ($issues as $issue) { - print_r('Issue #' . $issue['number'] . ' ' . $issue['title'] . "\n"); - - $str = '

' . $issue['title'] . '

'; - $str .= '

' . $issue['html_url'] . '

'; - - file_put_contents($langFile, $str, FILE_APPEND); - } - - } - - -} - +$generator = new Generator(__DIR__, new GitHubAPIClient); +$generator->run(); diff --git a/src/DTO/Issue.php b/src/DTO/Issue.php new file mode 100644 index 0000000..befba3c --- /dev/null +++ b/src/DTO/Issue.php @@ -0,0 +1,15 @@ + + */ + private array $issues = []; + + public function __construct( + public readonly string $html_url, // Ex: "https://github.com/octocat/Hello-World" + public readonly string $full_name,// Ex: "octocat/Hello-World" + public readonly string $description, // Ex: "This your first repo!" + public readonly string $language, // Ex: null, + public readonly int $stargazers_count, // Ex: 80, + public readonly int $open_issues_count, // Ex: 0, + public readonly int $open_issues, // Ex: 0, + public readonly string $updated_at // Ex: "2011-01-26T19:14:43Z", + ) { + } + + /** + * @return array + */ + public function getIssues(): array + { + return $this->issues; + } + + /** + * @param array $issues + * + * @return void + */ + public function setIssues(array $issues): void + { + $this->issues = $issues; + } +} diff --git a/src/Generator.php b/src/Generator.php index d88ba22..c3e62ca 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -4,6 +4,55 @@ namespace GoodFirstIssue; -class Generator +use GoodFirstIssue\DTO\Repository; +use LogicException; + +readonly class Generator { + public function __construct( + private string $root_path, + private GitHubAPIClient $github_api_client + ) { + } + + public function run(): void + { + if (! is_dir($this->root_path . '/lang')) { + mkdir($this->root_path . '/lang'); + } + + // Get repository names from `repositories.json` file + $json = file_get_contents('repositories.json'); + if (! is_string($json)) { + throw new LogicException('Cannot read file: repositories.json'); + } + + $repository_names = json_decode($json, true); + if (! is_array($repository_names)) { + throw new LogicException('Cannot decode repository names'); + } + + print_r($repository_names); + + $repositories = $this->github_api_client->requestRepositoriesData($repository_names); + + // TODO сортируем репозитории по updated at + + foreach ($repositories as $repository) { + print_r('Get issues for repository: ' . $repository->full_name . "\n"); + + $issues = $this->github_api_client->requestIssues($repository->full_name); + $repository->setIssues($issues); + } + + $renderer = new Renderer($this->root_path); + $renderer->renderIndexPage($repositories); + + $repositories_by_language = []; + foreach ($repositories as $repository) { + $repositories_by_language[$repository->language][] = $repository; + } + + // TODO $renderer->renderPerLanguagePage($repositories); + } } diff --git a/src/GitHubAPIClient.php b/src/GitHubAPIClient.php new file mode 100644 index 0000000..faee22b --- /dev/null +++ b/src/GitHubAPIClient.php @@ -0,0 +1,101 @@ + [ + 'method' => 'GET', + 'header' => ['User-Agent: PHP'], + ], + ]; + + /** + * Get information about all repositories. + * + * @param array $repository_names + * + * @return array + */ + public function requestRepositoriesData(array $repository_names): array + { + foreach ($repository_names as $repository_name) { + $repository = $this->requestRepositoryData($repository_name); + $repositories[] = $repository; + } + + return $repositories ?? []; + } + + /** + * @param string $repository_name + * + * @return Repository + */ + public function requestRepositoryData(string $repository_name): Repository + { + $context = stream_context_create(self::OPTS); + + $repository_json = file_get_contents($api_route = 'https://api.github.com/repos/' . $repository_name, false, $context); + if (! is_string($repository_json)) { + throw new LogicException('Cannot read GitHub API response from ' . $api_route); + } + + $repository = json_decode($repository_json, true); + if (! is_array($repository)) { + throw new LogicException('Cannot decode repository data'); + } + + $lang = (strlen((string) $repository['language']) < 1) ? 'other' : $repository['language']; + + return new Repository( + $repository['html_url'], + $repository['full_name'], + $repository['description'], + $lang, + $repository['stargazers_count'], + $repository['open_issues_count'], + $repository['open_issues'], + $repository['updated_at'], + ); + } + + /** + * @param string $repository_name + * + * @return array + */ + public function requestIssues(string $repository_name): array + { + $context = stream_context_create(self::OPTS); + + $issues_json = file_get_contents($api_route = 'https://api.github.com/repos/' . $repository_name . '/issues?state=open&sort=updated&labels=good%20first%20issue', false, $context); + if (! is_string($issues_json)) { + throw new LogicException('Cannot read GitHub API response from ' . $api_route); + } + + $issues_data = json_decode($issues_json, true); + if (! is_array($issues_data)) { + throw new LogicException('Cannot decode issues data'); + } + + foreach ($issues_data as $data) { + print_r('Issue #' . $data['number'] . ' ' . $data['title'] . "\n"); + + $issues[] = new Issue( + $data['html_url'], + $data['title'], + $data['number'] + ); + } + + return $issues ?? []; + } +} diff --git a/src/Renderer.php b/src/Renderer.php new file mode 100644 index 0000000..318c525 --- /dev/null +++ b/src/Renderer.php @@ -0,0 +1,183 @@ + $repositories + * + * @return void + */ + public function renderIndexPage(array $repositories): void + { + $main_html = file_get_contents($template_path = $this->root_path . '/src/Templates/main.html'); + + if (! is_string($main_html)) { + throw new LogicException('Cannot read file: ' . $template_path); + } + + $replace_pairs = [ + '%CARDS%' => $this->renderCardsListHTML($repositories), + ]; + + $html = strtr($main_html, $replace_pairs); + + file_put_contents('index.html', $html); + } + + /** + * @param array $repositories + * + * @return string + */ + private function renderCardsListHTML(array $repositories): string + { + $cards_html = ''; + foreach ($repositories as $repository) { + $list_items_html = ''; + foreach ($repository->getIssues() as $issue) { + $list_items_html .= $this->renderCardListItemHTML($issue); + } + + $cards_html .= $this->renderCardHTML($repository, $list_items_html); + } + + return $cards_html; + } + + private function renderCardHTML(Repository $repository, string $list_items_html): string + { + $main_card_template = file_get_contents($template_path = $this->root_path . '/src/Templates/main_card.html'); + + if (! is_string($main_card_template)) { + throw new LogicException('Cannot read file: ' . $template_path); + } + + $replace_pairs = [ + '%REPO_URL%' => $repository->html_url, + '%REPO_NAME%' => $repository->full_name, + '%REPO_DESCRIPTION%' => $repository->description, + '%ISSUES_LIST_HTML%' => $list_items_html, + ]; + + return strtr($main_card_template, $replace_pairs); + } + + private function renderCardListItemHTML(Issue $issue): string + { + $list_item_template = file_get_contents($template_path = $this->root_path . '/src/Templates/main_card_li.html'); + + if (! is_string($list_item_template)) { + throw new LogicException('Cannot read file: ' . $template_path); + } + + $replace_pairs = [ + '_ISSUE_HREF_' => $issue->html_url, + '_ISSUE_TITLE_' => $issue->title, + '_ISSUE_UPDATED_AT_' => 'TODO', + ]; + + return strtr($list_item_template, $replace_pairs); + } + + // public function buildLangs(array $repositories): void + // { + // + // $repositoriesByLanguage = []; + // + // // Проходимся по всем репозиториям + // foreach ($json_data as $line) { + // echo 'Line : https://api.github.com/repos/' . $line . "\n"; + // // Записываем информацию о репозитории в файл data/repositories.json + // // Это необходимо для главной страницы + // + // $opts = [ + // 'http' => [ + // 'method' => 'GET', + // 'header' => [ + // 'User-Agent: PHP', + // ], + // ], + // ]; + // + // $context = stream_context_create($opts); + // $repositoryJson = file_get_contents('https://api.github.com/repos/' . $line, false, $context); + // $repository = json_decode($repositoryJson, true); + // + // $repositoryData = [ + // 'html_url' => $repository['html_url'], // Ex: "https://github.com/octocat/Hello-World" + // 'full_name' => $repository['full_name'], // Ex: "octocat/Hello-World" + // 'description' => $repository['description'], // Ex: "This your first repo!" + // 'language' => $repository['language'], // Ex: null, + // 'stargazers_count' => $repository['stargazers_count'], // Ex: 80, + // 'open_issues_count' => $repository['open_issues_count'], // Ex: 0, + // 'open_issues' => $repository['open_issues'], // Ex: 0, + // 'updated_at' => $repository['updated_at'], // Ex: "2011-01-26T19:14:43Z", + // ]; + // + // print_r($repositoryData); + // + // // Конетент для главной страницы + // $indexContent .= '

' . $repositoryData['full_name'] . '

'; + // $indexContent .= '

' . $repositoryData['description'] . '

'; + // + // $repositoriesByLanguage[$repositoryData['language']][] = $repositoryData['full_name']; + // + // // Записываем ищуйки в общий файл + // } + // + // + // file_put_contents($langFile, $str, FILE_APPEND); + // + // file_put_contents('index.html', $indexContent); + // + // + // foreach ($repositoriesByLanguage as $lang => $repositories) { + // if (strlen($lang) < 1) { + // $lang = 'other'; + // } + // + // print_r('Language: ' . $lang); + // + // $langFile = 'lang/' . $lang . '.html'; + // if (file_exists($langFile)) { + // $status = unlink($langFile) ? 'The file ' . $langFile . ' has been deleted' . "\n" : 'Error deleting ' . $langFile . "\n"; + // echo $status; + // } + // + // + // // TODO Пишем шапку файла + // file_put_contents($langFile, '

Lang: ' . $lang . '

' . "\n"); + // + // foreach ($repositories as $repository) { + // print_r('Repository: ' . $repository . "\n"); + // + // $issuesJson = file_get_contents('https://api.github.com/repos/' . $repository . '/issues?state=open&sort=updated&labels=good%20first%20issue', false, $context); + // $issues = json_decode($issuesJson, true); + // + // foreach ($issues as $issue) { + // print_r('Issue #' . $issue['number'] . ' ' . $issue['title'] . "\n"); + // + // $str = '

' . $issue['title'] . '

'; + // $str .= '

' . $issue['html_url'] . '

'; + // + // file_put_contents($langFile, $str, FILE_APPEND); + // } + // } + // } + // } +} diff --git a/src/Templates/Examples/issues.html b/src/Templates/Examples/issues.html new file mode 100644 index 0000000..fe5359e --- /dev/null +++ b/src/Templates/Examples/issues.html @@ -0,0 +1,211 @@ + + + + + + good first issue + + + + + + +
+
+
+ +
+
+ + Package tgalopin/html-sanitizer is abandoned, you should avoid using it. Use + symfony/html-sanitizer instead. + +
+ + lang: PHP + +   + repo: gomzyakov/good-first-issue +   + + upd: 10 months ago + +
+
+
+ +
+
+ + Set `level: 2` in phpstan.neon.dist + +
+ + lang: PHP + +   + repo: gomzyakov/good-first-issue +   + + upd: 10 months ago + +
+
+
+ +
+
+ + Fix column styles on md and sm-resolutions + +
+ + lang: PHP + +   + repo: gomzyakov/good-first-issue +   + + upd: 3 feb 2023 + +
+
+
+ + +
+
+ + gomzyakov/good-first-issue + +

+ + This is a wider card with supporting text below as a natural lead-in to + additional content. This content is a little bit longer. + +

+
+ + lang: C# + +   + stars: 4.17K +   + + last activity: 10 months ago + +
+
+ +
+ +
+
+ + gomzyakov/good-first-issue + +

+ + This is a wider card with supporting text below as a natural lead-in to + additional content. This content is a little bit longer. + +

+
+ + lang: C# + +   + stars: 4.17K +   + + last activity: 10 months ago + +
+
+ +
+ +
+
+
About
+ +

Good First Issue curates easy pickings from popular open-source projects, and helps you make your first + contribution to open-source.

+ + + +

+ Made with ♥ by + + Alexander Gomzyakov + +

+
+
+
+ + + + \ No newline at end of file diff --git a/src/Templates/Examples/main.html b/src/Templates/Examples/main.html new file mode 100644 index 0000000..b6a10fe --- /dev/null +++ b/src/Templates/Examples/main.html @@ -0,0 +1,198 @@ + + + + + + good first issue + + + + + + +
+
+
+ +
+
+ + gomzyakov/good-first-issue + +

+ + This is a wider card with supporting text below as a natural lead-in to + additional content. This content is a little bit longer. + +

+
+ + lang: C# + +   + stars: 4.17K +   + + last activity: 10 months ago + +
+
+ +
+ +
+
+ + gomzyakov/good-first-issue + +

+ + This is a wider card with supporting text below as a natural lead-in to + additional content. This content is a little bit longer. + +

+
+ + lang: C# + +   + stars: 4.17K +   + + last activity: 10 months ago + +
+
+ +
+ +
+
+ + gomzyakov/good-first-issue + +

+ + This is a wider card with supporting text below as a natural lead-in to + additional content. This content is a little bit longer. + +

+
+ + lang: C# + +   + stars: 4.17K +   + + last activity: 10 months ago + +
+
+ +
+ +
+
+
About
+ +

Good First Issue curates easy pickings from popular open-source projects, and helps you make your first + contribution to open-source.

+ + + +

+ Made with ♥ by + + Alexander Gomzyakov + +

+
+
+
+ + + + diff --git a/src/Templates/main.html b/src/Templates/main.html new file mode 100644 index 0000000..8a63260 --- /dev/null +++ b/src/Templates/main.html @@ -0,0 +1,53 @@ + + + + + + good first issue + + + + + + +
+
+
+ + %CARDS% + +
+
+
About
+ +

Good First Issue curates easy pickings from popular open-source projects, and helps you make your first + contribution to open-source.

+ + + +

+ Made with ♥ by + + Alexander Gomzyakov + +

+
+
+
+ + + + \ No newline at end of file diff --git a/src/Templates/main_card.html b/src/Templates/main_card.html new file mode 100644 index 0000000..c4dc175 --- /dev/null +++ b/src/Templates/main_card.html @@ -0,0 +1,31 @@ +
+
+ + %REPO_NAME% + +

+ + %REPO_DESCRIPTION% + +

+
+ + lang: C# + +   + stars: 4.17K +   + + last activity: 10 months ago + +
+
+ +
diff --git a/src/Templates/main_card_li.html b/src/Templates/main_card_li.html new file mode 100644 index 0000000..b4bc7c2 --- /dev/null +++ b/src/Templates/main_card_li.html @@ -0,0 +1,6 @@ +
  • + + _ISSUE_TITLE_ + + upd: _ISSUE_UPDATED_AT_ +
  • diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index 6de746f..6e4f701 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -2,17 +2,20 @@ declare(strict_types = 1); +namespace Tests; + use GoodFirstIssue\Generator; +use GoodFirstIssue\GitHubAPIClient; use PHPUnit\Framework\TestCase; /** - * @coversNothing + * @covers \GoodFirstIssue\Generator */ final class GeneratorTest extends TestCase { public function test_instance_of_generator(): void { - $generator = new Generator(); + $generator = new Generator(__DIR__, new GitHubAPIClient()); $this->assertInstanceOf(Generator::class, $generator); }