diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..3253e2c3 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,16 @@ +name: "CodeQL" + +on: [pull_request] +jobs: + lint: + name: CodeQL + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Run CodeQL + run: | + docker run --rm -v $PWD:/app composer sh -c \ + "composer install --profile --ignore-platform-reqs && composer check" \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 00000000..28f4c6a0 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,16 @@ +name: "Linter" + +on: [pull_request] +jobs: + lint: + name: Linter + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Run Linter + run: | + docker run --rm -v $PWD:/app composer sh -c \ + "composer install --profile --ignore-platform-reqs && composer lint" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..f17f711f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: "Tests" + +on: [pull_request] +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Start Test Stack + env: + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }} + INSTALLATION_ID: ${{ secrets.INSTALLATION_ID }} + run: | + docker compose up -d + sleep 15 + + - name: Doctor + run: | + docker compose logs + docker ps + docker network ls + + - name: Run Tests + run: | + docker-compose exec -T tests vendor/bin/phpunit --configuration phpunit.xml tests \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a8e3d840 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +/.idea/ +.phpunit.result.cache +.env +.DS_Store \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..e3974013 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity, expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at team@appwrite.io. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c986fbab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,109 @@ +# Contributing + +We would ❀️ for you to contribute to Utopia-php and help make it better! We want contributing to Utopia-php to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, new docs as well as updates and tweaks, blog posts, workshops, and more. + +## How to Start? + +If you are worried or don’t know where to start, check out our next section explaining what kind of help we could use and where can you get involved. You can reach out with questions to [Eldad Fux (@eldadfux)](https://twitter.com/eldadfux) or anyone from the [Appwrite team on Discord](https://appwrite.io/discord). You can also submit an issue, and a maintainer can guide you! + +## Code of Conduct + +Help us keep Utopia-php open and inclusive. Please read and follow our [Code of Conduct](https://github.com/utopia-php/abuse/blob/master/CODE_OF_CONDUCT.md). + +## Submit a Pull Request πŸš€ + +Branch naming convention is as following + +``` +TYPE-ISSUE_ID-DESCRIPTION +``` + +example: + +``` +doc-548-submit-a-pull-request-section-to-contribution-guide +``` + +When `TYPE` can be: + +- **feat** - is a new feature +- **doc** - documentation only changes +- **cicd** - changes related to CI/CD system +- **fix** - a bug fix +- **refactor** - code change that neither fixes a bug nor adds a feature + +**All PRs must include a commit message with the changes description!** + +For the initial start, fork the project and use git clone command to download the repository to your computer. A standard procedure for working on an issue would be to: + +1. `git pull`, before creating a new branch, pull the changes from upstream. Your master needs to be up to date. + +``` +$ git pull +``` + +2. Create new branch from `master` like: `doc-548-submit-a-pull-request-section-to-contribution-guide` + +``` +$ git checkout -b [name_of_your_new_branch] +``` + +3. Work - commit - repeat ( be sure to be in your branch ) + +4. Push changes to GitHub + +``` +$ git push origin [name_of_your_new_branch] +``` + +6. Submit your changes for review If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button. + +7. Start a Pull Request Now submit the pull request and click on `Create pull request`. + +8. Get a code review approval/reject + +9. After approval, merge your PR + +10. GitHub will automatically delete the branch after the merge is done. (they can still be restored). + +## Introducing New Features + +We would πŸ’– you to contribute to Utopia-php, but we would also like to make sure Utopia-php is as great as possible and loyal to its vision and mission statement πŸ™. + +For us to find the right balance, please open an issue explaining your ideas before introducing a new pull request. + +This will allow the Utopia-php community to have sufficient discussion about the new feature value and how it fits in the product roadmap and vision. + +This is also important for the Utopia-php lead developers to be able to give technical input and different emphasis regarding the feature design and architecture. Some bigger features might need to go through our [RFC process](https://github.com/appwrite/rfc). + +## Adding A New Adapter + +You can follow our [Adding new VCS Adapter](docs/add-new-vcs-adapter.md) tutorial to add a new VCS adapter like GitLab, Bitbucket etc. and [Adding new Detector Adapter](docs/add-new-detector-adapter.md) tutorial to add new detector support in this library. + +## Other Ways to Help + +Pull requests are great, but there are many other areas where you can help Utopia-php. + +### Blogging & Speaking + +Blogging, speaking about, or creating tutorials about one of Utopia-php’s many features is great way to contribute and help our project grow. + +### Presenting at Meetups + +Presenting at meetups and conferences about your Utopia-php projects. Your unique challenges and successes in building things with Utopia-php can provide great speaking material. We’d love to review your talk abstract/CFP, so get in touch with us if you’d like some help! + +### Sending Feedbacks & Reporting Bugs + +Sending feedback is a great way for us to understand your different use cases of Utopia-php better. If you had any issues, bugs, or want to share about your experience, feel free to do so on our GitHub issues page or at our [Discord channel](https://discord.gg/GSeTUeA). + +### Submitting New Ideas + +If you think Utopia-php could use a new feature, please open an issue on our GitHub repository, stating as much information as you can think about your new idea and it's implications. We would also use this issue to gather more information, get more feedback from the community, and have a proper discussion about the new feature. + +### Improving Documentation + +Submitting documentation updates, enhancements, designs, or bug fixes. Spelling or grammar fixes will be very much appreciated. + +### Helping Someone + +Searching for Utopia-php, GitHub or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Utopia-php's repo! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..19d90e3a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM composer:2.0 as composer + +ARG TESTING=false +ENV TESTING=$TESTING + +WORKDIR /usr/local/src/ + +COPY composer.lock /usr/local/src/ +COPY composer.json /usr/local/src/ + +RUN composer install \ + --ignore-platform-reqs \ + --optimize-autoloader \ + --no-plugins \ + --no-scripts \ + --prefer-dist + +FROM php:8.0-cli-alpine + +WORKDIR /usr/local/src/ + +COPY --from=composer /usr/local/src/vendor /usr/local/src/vendor +COPY . /usr/local/src/ + +CMD [ "tail", "-f", "/dev/null" ] \ No newline at end of file diff --git a/README.md b/README.md index 5356415f..bda54586 100644 --- a/README.md +++ b/README.md @@ -1 +1,122 @@ -# vcs +# Utopia VCS + +[![Build Status](https://travis-ci.org/utopia-php/vcs.svg?branch=master)](https://travis-ci.com/utopia-php/vcs) +![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/vcs.svg) +[![Discord](https://img.shields.io/discord/564160730845151244?label=discord)](https://appwrite.io/discord) + +Utopia VCS is a simple and lite library for interacting with version control systems (VCS) in Utopia-PHP using adapters for different providers like GitHub, GitLab etc. This library is aiming to be as simple and easy to learn and use. This library is maintained by the [Appwrite team](https://appwrite.io). + +## Getting Started + +Install using composer: +```bash +composer require utopia-php/vcs +``` + +Init in your application: +```php +initializeVariables($installationId, $privateKey, $githubAppId); + +// Perform the actions that you want, ex: create repository +$owner = ''; +$name = ''; +$isPrivate = true; // Set to false if you want to create a public repository +$repository = $github->createRepository($owner, $name, $private); +``` + +### Environment Variables +To configure your GitHub App properly, you'll need to set up the following environment variables in your environment or configuration file. These values are crucial for authenticating and interacting with the GitHub API on behalf of your GitHub App. + +1. *PRIVATE_KEY*: You can generate this from your GitHub App settings. +```bash +PRIVATE_KEY = your-github-app-private-key +``` +2. *GITHUB_APP_ID*: You can find this in the GitHub App dashboard. +```bash +GITHUB_APP_ID = your-github-app-id +``` +3. *INSTALLATION_ID*: You can find this in the GitHub App installation settings after installation. +```bash +INSTALLATION_ID = your-github-app-installation-id +``` + +Remember to replace the placeholders (*your-github-app-private-key*, *your-github-app-id*, and *your-github-app-installation-id*) with the actual values from your GitHub App configuration. +By using these environment variables, you can ensure that sensitive information is kept separate from your codebase and can be easily managed across different environments without exposing sensitive data. + +### Supported Adapters + +VCS Adapters: + +| Adapter | Status | +|---------|---------| +| GitHub | βœ… | +| GitLab | | +| Bitbucket | | +| Azure DevOps | | + +Detector Adapters: + +| Adapter | Status | +|---------|---------| +| CPP | βœ… | +| Dart | βœ… | +| Deno | βœ… | +| Dotnet | βœ… | +| Java | βœ… | +| JavaScript | βœ… | +| PHP | βœ… | +| Python | βœ… | +| Ruby | βœ… | +| Swift | βœ… | + +`βœ… - supported, πŸ›  - work in progress` + +## System Requirements + +Utopia VCS requires PHP 8.0 or later. We recommend using the latest PHP version whenever possible. + + +## Contributing + +All code contributions - including those of people having commit access - must go through a pull request and approved by a core developer before being merged. This is to ensure proper review of all the code. + +Fork the project, create a feature branch, and send us a pull request. + +You can refer to the [Contributing Guide](CONTRIBUTING.md) for more info. + +## Tests + +To run tests, you first need to bring up the example Docker stack with the following command: + +```bash +docker compose up -d --build +``` + +To run all unit tests, use the following Docker command: + +```bash +docker compose exec tests ./vendor/bin/phpunit +``` + +## Copyright and license + +The MIT License (MIT) [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php) diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..60c9d274 --- /dev/null +++ b/composer.json @@ -0,0 +1,41 @@ +{ + "name": "utopia-php/vcs", + "description": "A simple library to integrate version control systems like GitHub, GitLab etc. to receive webhook events", + "type": "library", + "keywords": [ + "php", + "framework", + "utopia", + "vcs" + ], + "license": "MIT", + "minimum-stability": "stable", + "scripts": { + "lint": "./vendor/bin/pint --test --config pint.json", + "format": "./vendor/bin/pint --config pint.json", + "check": "./vendor/bin/phpstan analyse --level 8 -c phpstan.neon src tests", + "test": "./vendor/bin/phpunit --configuration phpunit.xml --debug" + }, + "autoload": { + "psr-4": { + "Utopia\\VCS\\": "src/VCS", + "Utopia\\Detector\\": "src/Detector" + } + }, + "autoload-dev": { + "psr-4": { + "Utopia\\Tests\\": "tests/VCS" + } + }, + "require": { + "php": ">=8.0", + "adhocore/jwt": "^1.1", + "utopia-php/framework": "0.30.*", + "utopia-php/cache": "^0.8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.4", + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.8.*" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..1a941264 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2030 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0514888858a68ac038aaa4f60dc65203", + "packages": [ + { + "name": "adhocore/jwt", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/adhocore/php-jwt.git", + "reference": "6c434af7170090bb7a8880d2bc220a2254ba7899" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/adhocore/php-jwt/zipball/6c434af7170090bb7a8880d2bc220a2254ba7899", + "reference": "6c434af7170090bb7a8880d2bc220a2254ba7899", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ahc\\Jwt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jitendra Adhikari", + "email": "jiten.adhikary@gmail.com" + } + ], + "description": "Ultra lightweight JSON web token (JWT) library for PHP5.5+.", + "keywords": [ + "auth", + "json-web-token", + "jwt", + "jwt-auth", + "jwt-php", + "token" + ], + "support": { + "issues": "https://github.com/adhocore/php-jwt/issues", + "source": "https://github.com/adhocore/php-jwt/tree/1.1.2" + }, + "funding": [ + { + "url": "https://paypal.me/ji10", + "type": "custom" + } + ], + "time": "2021-02-20T09:56:44+00:00" + }, + { + "name": "utopia-php/cache", + "version": "0.8.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/cache.git", + "reference": "212e66100a1f32e674fca5d9bc317cc998303089" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/212e66100a1f32e674fca5d9bc317cc998303089", + "reference": "212e66100a1f32e674fca5d9bc317cc998303089", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-memcached": "*", + "ext-redis": "*", + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Cache\\": "src/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple cache library to manage application cache storing, loading and purging", + "keywords": [ + "cache", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/cache/issues", + "source": "https://github.com/utopia-php/cache/tree/0.8.0" + }, + "time": "2022-10-16T16:48:09+00:00" + }, + { + "name": "utopia-php/framework", + "version": "0.30.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/framework.git", + "reference": "0969d429997996ceade53e477fce31221b415651" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/0969d429997996ceade53e477fce31221b415651", + "reference": "0969d429997996ceade53e477fce31221b415651", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple, light and advanced PHP framework", + "keywords": [ + "framework", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/framework/issues", + "source": "https://github.com/utopia-php/framework/tree/0.30.0" + }, + "time": "2023-08-07T07:55:48+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "e60e2112ee779ce60f253695b273d1646a17d6f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/e60e2112ee779ce60f253695b273d1646a17d6f1", + "reference": "e60e2112ee779ce60f253695b273d1646a17d6f1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.11.0", + "illuminate/view": "^9.32.0", + "laravel-zero/framework": "^9.2.0", + "mockery/mockery": "^1.5.1", + "nunomaduro/larastan": "^2.2.0", + "nunomaduro/termwind": "^1.14.0", + "pestphp/pest": "^1.22.1" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2022-11-29T16:25:20+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.16.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" + }, + "time": "2023-06-25T14:52:30+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.8.11", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "46e223dd68a620da18855c23046ddb00940b4014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014", + "reference": "46e223dd68a620da18855c23046ddb00940b4014", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/1.8.11" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2022-10-24T15:45:13+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.27", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-07-26T13:44:30+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6d351645c3fe5a30f5e86be6577d946af65a328", + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.10" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-07-10T04:04:23+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-07T05:35:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bde739e7565280bda77be70044ac1047bc007e34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-02T09:26:13+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.0" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..bf600426 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.9' + +services: + tests: + build: + context: . + volumes: + - ./src:/usr/local/src/src + - ./tests:/usr/local/src/tests + - ./phpunit.xml:/usr/local/src/phpunit.xml + environment: + - PRIVATE_KEY + - APP_IDENTIFIER + - INSTALLATION_ID \ No newline at end of file diff --git a/docs/add-new-detector-adapter.md b/docs/add-new-detector-adapter.md new file mode 100644 index 00000000..6ab818b3 --- /dev/null +++ b/docs/add-new-detector-adapter.md @@ -0,0 +1,69 @@ +# Add new Detector Adapter + +To get started with implementing a new detector adapter, start by reviewing the [README](/README.md) to understand the goals of this library. + +### Introduction +- A `detector` is a class that defines the files, extensions, and languages that are associated with a specific runtime environment, such as Node.js, PHP, Ruby, or Python. The presence of these files, extensions, or languages can help automatically detect that runtime environment. +- To add a new detector adapter, you need to extend the Adapter parent class and define the following methods: + + - `getFiles()`: This method returns an array of files that are known to be associated with the runtime environment. + - `getFileExtensions()`: This method returns an array of file extensions that are known to be associated with the runtime environment. + - `getLanguages()`: This method returns an array of languages that are known to be associated with the runtime environment. + - `getInstallCommand()`: This method returns the command that can be used to install the runtime environment. + - `getBuildCommand()`: This method returns the command that can be used to build the project for the runtime environment. + - `getEntryPoint()`: This method returns the entry point for the project in the runtime environment. + + +### File Structure + +Below are outlined the most useful files for adding a new detector adapter: + +```bash +. +β”œβ”€β”€ src # Source code +β”‚ └── Detector +β”‚ β”œβ”€β”€ Adapter/ # Where your new adapter goes! +β”‚ β”œβ”€β”€ Adapter.php # Parent class for individual adapters +β”‚ └── Detector.php # Detector class - calls individual adapter methods +└── tests + └── Detector + └── DetectorTest.php # Test class that holds all tests +``` + + +### Extend the Adapter + +Create your `NewDetector.php` file in `src/Detector/Adapter/` and extend the parent class: + +```php +addDetector(new NewDetector()); +``` + +Once the `NewDetector` class is registered, the Appwrite runtime detector will be able to detect the new runtime to detect the runtime environment for the given files. + +Only include dependencies strictly necessary for the detector, preferably official PHP libraries, if available. + +### Testing with Docker + +The existing test suite is helpful when developing a new detector adapter. Use official Docker images from trusted sources. Add new tests for your NewDetector in `tests/Detector/DetectorTest.php` test class. The specific `docker-compose` command for testing can be found in the [README](/README.md#tests). + +### Tips and Tricks + +- Keep it simple :) +- Prioritize code performance. diff --git a/docs/add-new-vcs-adapter.md b/docs/add-new-vcs-adapter.md new file mode 100644 index 00000000..e0f1fb93 --- /dev/null +++ b/docs/add-new-vcs-adapter.md @@ -0,0 +1,86 @@ +# Add new VCS Adapter + +To get started with implementing a new VCS adapter, start by reviewing the [README](/README.md) to understand the goals of this library. ❀ + +### Introduction πŸ“ +- A `VCS (version control system)` is a software tool that helps you track changes to your code over time. +- A `VCS adapter` is a class that provides an interface to a specific VCS like GitHub, Bitbucket etc. It provides methods for interacting with the VCS user account and repositories, such as listing repositories, adding a comment on a pull request, cloning the repository etc. +- To add a new VCS adapter, you need to extend the `Adapter` parent class and define the required methods. + +### File Structure πŸ“‚ + +Below are outlined the most useful files for adding a new VCS adapter: + +```bash +. +β”œβ”€β”€ src # Source code +β”‚Β Β  └── VCS +β”‚Β Β  β”œβ”€β”€ Adapter/ # Where your new adapter goes! +β”‚Β  β”‚Β  β”œβ”€β”€ Git/ # Where your new Git-based adapter goes! +β”‚Β  β”‚Β  └── Git.php # Parent class for Git-based adapters +β”‚Β Β  └── Adapter.php # Parent class for individual adapters +└── tests + └── VCS + β”œβ”€β”€ Adapter/ # Where tests of your new adapter go! + └── Base.php # Parent class that holds all tests +``` +### Extend the Adapter πŸ’» + +Create your Git-based adapter `NewGitAdapter.php` file in `src/VCS/Adapter/Git` and extend the parent class: + +```php +initializeVariables($installationId, $privateKey, $appId); +``` + +Only include dependencies strictly necessary for the adapter, preferably official PHP libraries, if available. + +### Testing with Docker πŸ› οΈ + +The existing test suite is helpful when developing a new VCS adapter. Use official Docker images from trusted sources. Add new tests for your new VCS adapter in `tests/VCS/Adapter/VCSTest.php` test class. The specific `docker-compose` command for testing can be found in the [README](/README.md#tests). + +### Tips and Tricks πŸ’‘ + +- Keep it simple :) +- Prioritize code performance. diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..8b576c6f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,3 @@ +parameters: + excludePaths: + - tests/resources \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..ec39a7ed --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,16 @@ + + + + ./tests/ + + + \ No newline at end of file diff --git a/pint.json b/pint.json new file mode 100644 index 00000000..70add709 --- /dev/null +++ b/pint.json @@ -0,0 +1,6 @@ +{ + "preset": "psr12", + "exclude": [ + "tests/resources" + ] +} \ No newline at end of file diff --git a/src/Detector/Adapter.php b/src/Detector/Adapter.php new file mode 100644 index 00000000..2c73f344 --- /dev/null +++ b/src/Detector/Adapter.php @@ -0,0 +1,29 @@ +files = $files; + $this->languages = $languages; + } + + public function addDetector(Adapter $detector): self + { + $this->detectors[] = $detector; + + return $this; + } + + public function detect(): ?string + { + // 1. Look for specific files + foreach ($this->detectors as $detector) { + $detectorFiles = $detector->getFiles(); + + $matches = \array_intersect($detectorFiles, $this->files); + if (\count($matches) > 0) { + return $detector->getRuntime(); + } + } + + // 2. Look for files with extension + foreach ($this->detectors as $detector) { + foreach ($this->files as $file) { + if (\in_array(pathinfo($file, PATHINFO_EXTENSION), $detector->getFileExtensions())) { + return $detector->getRuntime(); + } + } + } + + // 3. Look for match with language detected by Git + foreach ($this->detectors as $detector) { + foreach ($this->languages as $language) { + if (\in_array($language, $detector->getLanguages())) { + return $detector->getRuntime(); + } + } + } + + return null; + } +} diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php new file mode 100644 index 00000000..e46188c6 --- /dev/null +++ b/src/VCS/Adapter.php @@ -0,0 +1,392 @@ + + */ + protected $headers = []; + + /** + * Get Adapter Name + * + * @return string + */ + abstract public function getName(): string; + + /** + * Get Adapter Type + * + * @return string + */ + abstract public function getType(): string; + + /** + * Initialize Variables + * + * @param string $installationId + * @param string $privateKey + * @param string $githubAppId + * @return void + */ + abstract public function initializeVariables(string $installationId, string $privateKey, string $githubAppId): void; + + /** + * Generate Access Token + * + * @param string $privateKey + * @param string $githubAppId + * @return void + */ + abstract protected function generateAccessToken(string $privateKey, string $githubAppId): void; + + /** + * Get user + * + * @return array + * + */ + abstract public function getUser(string $username): array; + + /** + * Get owner name of the installation + * + * @return string + */ + abstract public function getOwnerName(string $installationId): string; + + /** + * List repositories for Git App + * @param int $page page number + * @param int $per_page number of results per page + * @return array + */ + abstract public function listRepositories($page, $per_page): array; + + /** + * Get repository + * + * @return array + */ + abstract public function getRepository(string $owner, string $repositoryName): array; + + /** + * Create new repository + * + * @return array Details of new repository + */ + abstract public function createRepository(string $owner, string $repositoryName, bool $private): array; + + /** + * Delete repository + */ + abstract public function deleteRepository(string $owner, string $repositoryName): void; + + /** + * Get latest opened pull request with specific base branch + * @return array + */ + abstract public function getPullRequestFromBranch(string $owner, string $repositoryName, string $branch): array; + + /** + * Get Pull Request + * + * @return array The retrieved pull request + */ + abstract public function getPullRequest(string $owner, string $repositoryName, int $pullRequestNumber): array; + + /** + * Add Comment to Pull Request + * + * @return string + */ + abstract public function createComment(string $owner, string $repositoryName, int $pullRequestNumber, string $comment): string; + + /** + * Get Comment of Pull Request + * + * @param string $owner The owner of the repository + * @param string $repositoryName The name of the repository + * @param string $commentId The ID of the comment to retrieve + * @return string The retrieved comment + */ + abstract public function getComment(string $owner, string $repositoryName, string $commentId): string; + + /** + * Update Pull Request Comment + * + * @param string $owner The owner of the repository + * @param string $repositoryName The name of the repository + * @param int $commentId The ID of the comment to update + * @param string $comment The updated comment content + * @return string The ID of the updated comment + */ + abstract public function updateComment(string $owner, string $repositoryName, int $commentId, string $comment): string; + + /** + * Generates a clone command using app access token + */ + abstract public function generateCloneCommand(string $owner, string $repositoryName, string $branchName, string $directory, string $rootDirectory): string; + + /** + * Parses webhook event payload + * + * @param string $payload Raw body of HTTP request + * @param string $signature Signature provided by Git provider in header + * @param string $signatureKey Webhook secret configured on Git provider + * @return bool + */ + abstract public function validateWebhookEvent(string $payload, string $signature, string $signatureKey): bool; + + /** + * Parses webhook event payload + * + * @param string $event Type of event: push, pull_request etc + * @param string $payload The webhook payload received from Git provider + * @return array Parsed payload as a json object + */ + abstract public function getEvent(string $event, string $payload): array; + + /** + * Fetches repository name using repository id + * + * @param string $repositoryId ID of the repository + * @return string name of the repository + */ + abstract public function getRepositoryName(string $repositoryId): string; + + /** + * Lists branches for a given repository + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the repository + * @return array List of branch names as array + */ + abstract public function listBranches(string $owner, string $repositoryName): array; + + /** + * Updates status check of each commit + * state can be one of: error, failure, pending, success + */ + abstract public function updateCommitStatus(string $repositoryName, string $SHA, string $owner, string $state, string $description = '', string $target_url = '', string $context = ''): void; + + /** + * Get repository languages + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the repository + * @return array List of repository languages + */ + abstract public function listRepositoryLanguages(string $owner, string $repositoryName): array; + + /** + * List contents of the specified root directory. + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the repository + * @param string $path Path to list contents from + * @return array List of contents at the specified path + */ + abstract public function listRepositoryContents(string $owner, string $repositoryName, string $path = ''): array; + + /** + * Get details of a commit using commit hash + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the GitHub repository + * @param string $commitHash SHA of the commit + * @return array Details of the commit + */ + abstract public function getCommit(string $owner, string $repositoryName, string $commitHash): array; + + /** + * Get latest commit of a branch + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the GitHub repository + * @param string $branch Name of the branch + * @return array Details of the commit + */ + abstract public function getLatestCommit(string $owner, string $repositoryName, string $branch): array; + + /** + * Call + * + * Make an API call + * + * @param string $method + * @param string $path + * @param array $params + * @param array $headers + * @param bool $decode + * @return array + * + * @throws Exception + */ + protected function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true) + { + $headers = array_merge($this->headers, $headers); + $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); + + if (!$ch) { + throw new Exception('Curl failed to initialize'); + } + + $responseHeaders = []; + $responseStatus = -1; + $responseType = ''; + $responseBody = ''; + + switch ($headers['content-type']) { + case 'application/json': + $query = json_encode($params); + break; + + case 'multipart/form-data': + $query = $this->flatten($params); + break; + + case 'application/graphql': + $query = $params[0]; + break; + + default: + $query = http_build_query($params); + break; + } + + foreach ($headers as $i => $header) { + $headers[] = $i . ':' . $header; + unset($headers[$i]); + } + + curl_setopt($ch, CURLOPT_PATH_AS_IS, 1); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $header = explode(':', $header, 2); + + if (count($header) < 2) { // ignore invalid headers + return $len; + } + + $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); + + return $len; + }); + + if ($method != self::METHOD_GET) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + } + + // Allow self signed certificates + if ($this->selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + + $responseBody = \curl_exec($ch) ?: ''; + + if ($responseBody === true) { + $responseBody = ''; + } + + $responseType = $responseHeaders['content-type'] ?? ''; + $responseStatus = \curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($decode) { + $length = strpos($responseType, ';') ?: 0; + switch (substr($responseType, 0, $length)) { + case 'application/json': + $json = \json_decode($responseBody, true); + + if ($json === null) { + throw new Exception('Failed to parse response: ' . $responseBody); + } + + $responseBody = $json; + $json = null; + break; + } + } + + if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { + throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus); + } + + curl_close($ch); + + $responseHeaders['status-code'] = $responseStatus; + + if ($responseStatus === 500) { + echo 'Server error(' . $method . ': ' . $path . '. Params: ' . json_encode($params) . '): ' . json_encode($responseBody) . "\n"; + } + + return [ + 'headers' => $responseHeaders, + 'body' => $responseBody, + ]; + } + + /** + * Flatten params array to PHP multiple format + * + * @param array $data + * @param string $prefix + * @return array + */ + protected function flatten(array $data, string $prefix = ''): array + { + $output = []; + + foreach ($data as $key => $value) { + $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; + + if (is_array($value)) { + $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed + } else { + $output[$finalKey] = $value; + } + } + + return $output; + } +} diff --git a/src/VCS/Adapter/Git.php b/src/VCS/Adapter/Git.php new file mode 100644 index 00000000..268f75b6 --- /dev/null +++ b/src/VCS/Adapter/Git.php @@ -0,0 +1,37 @@ + + */ + protected $headers = ['content-type' => 'application/json']; + + public function __construct(Cache $cache) + { + $this->cache = $cache; + } + + /** + * Get Adapter Type + * + * @return string + */ + public function getType(): string + { + return self::TYPE_GIT; + } +} diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php new file mode 100644 index 00000000..736792e8 --- /dev/null +++ b/src/VCS/Adapter/Git/GitHub.php @@ -0,0 +1,587 @@ + + */ + protected $headers = ['content-type' => 'application/json']; + + public function __construct(Cache $cache) + { + $this->cache = $cache; + } + + /** + * Get Adapter Name + * + * @return string + */ + public function getName(): string + { + return 'github'; + } + + /** + * GitHub Initialisation with access token generation. + */ + public function initializeVariables(string $installationId, string $privateKey, string $githubAppId): void + { + $this->installationId = $installationId; + + $response = $this->cache->load($installationId, 60 * 9); // 10 minutes, but 1 minute earlier to be safe + if ($response == false) { + $this->generateAccessToken($privateKey, $githubAppId); + + $tokens = \json_encode([ + 'jwtToken' => $this->jwtToken, + 'accessToken' => $this->accessToken, + ]) ?: '{}'; + + $this->cache->save($installationId, $tokens); + } else { + $parsed = \json_decode($response, true); + $this->jwtToken = $parsed['jwtToken']; + $this->accessToken = $parsed['accessToken']; + } + } + + /** + * Create new repository + * + * @return array Details of new repository + */ + public function createRepository(string $owner, string $repositoryName, bool $private): array + { + $url = "/orgs/{$owner}/repos"; + + $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], [ + 'name' => $repositoryName, + 'private' => $private, + ]); + + return $response['body'] ?? []; + } + + /** + * List repositories for GitHub App + * @param int $page page number + * @param int $per_page number of results per page + * @return array + * + * @throws Exception + */ + public function listRepositories($page, $per_page): array + { + $url = '/installation/repositories?page=' . $page . '&per_page=' . $per_page; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + if (!isset($response['body']['repositories'])) { + throw new Exception("Repositories list missing in the response."); + } + + return $response['body']['repositories']; + } + + /** + * Get GitHub repository + * + * @return array + * + * @throws Exception + */ + public function getRepository(string $owner, string $repositoryName): array + { + $url = "/repos/{$owner}/{$repositoryName}"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + return $response['body'] ?? []; + } + + /** + * Fetches repository name using repository id + * + * @param string $repositoryId ID of GitHub Repository + * @return string name of GitHub repository + */ + public function getRepositoryName(string $repositoryId): string + { + $url = "/repositories/$repositoryId"; + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + if (!isset($response['body']['name'])) { + throw new Exception("Repository name not found"); + } + + return $response['body']['name']; + } + + /** + * Get repository languages + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the GitHub repository + * @return array List of repository languages + */ + public function listRepositoryLanguages(string $owner, string $repositoryName): array + { + $url = "/repos/$owner/$repositoryName/languages"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + if (isset($response['body'])) { + return array_keys($response['body']); + } + + return []; + } + + /** + * List contents of the specified root directory. + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the GitHub repository + * @param string $path Path to list contents from + * @return array List of contents at the specified path + */ + public function listRepositoryContents(string $owner, string $repositoryName, string $path = ''): array + { + $url = "/repos/$owner/$repositoryName/contents"; + if (!empty($path)) { + $url .= "/$path"; + } + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + return array_map(static function ($item) { + return $item['name']; + }, $response['body']); + } + + public function deleteRepository(string $owner, string $repositoryName): void + { + $url = "/repos/{$owner}/{$repositoryName}"; + + $this->call(self::METHOD_DELETE, $url, ['Authorization' => "Bearer $this->accessToken"]); + } + + /** + * Add Comment to Pull Request + * + * @return string + * + * @throws Exception + */ + public function createComment(string $owner, string $repositoryName, int $pullRequestNumber, string $comment): string + { + $url = '/repos/' . $owner . '/' . $repositoryName . '/issues/' . $pullRequestNumber . '/comments'; + + $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], ['body' => $comment]); + + if (!isset($response['body']['id'])) { + throw new Exception("Comment creation response is missing comment ID."); + } + + $commentId = $response['body']['id']; + + return $commentId; + } + + /** + * Get Comment of Pull Request + * + * @param string $owner The owner of the repository + * @param string $repositoryName The name of the repository + * @param string $commentId The ID of the comment to retrieve + * @return string The retrieved comment + * + * @throws Exception + */ + public function getComment(string $owner, string $repositoryName, string $commentId): string + { + $url = '/repos/' . $owner . '/' . $repositoryName . '/issues/comments/' . $commentId; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + $comment = $response['body']['body'] ?? ''; + + return $comment; + } + + /** + * Update Pull Request Comment + * + * @param string $owner The owner of the repository + * @param string $repositoryName The name of the repository + * @param int $commentId The ID of the comment to update + * @param string $comment The updated comment content + * @return string The ID of the updated comment + * + * @throws Exception + */ + public function updateComment(string $owner, string $repositoryName, int $commentId, string $comment): string + { + $url = '/repos/' . $owner . '/' . $repositoryName . '/issues/comments/' . $commentId; + + $response = $this->call(self::METHOD_PATCH, $url, ['Authorization' => "Bearer $this->accessToken"], ['body' => $comment]); + + if (!isset($response['body']['id'])) { + throw new Exception("Comment update response is missing comment ID."); + } + + $commentId = $response['body']['id']; + + return $commentId; + } + + /** + * Generate Access Token + */ + protected function generateAccessToken(string $privateKey, string $githubAppId): void + { + /** + * @var resource $privateKeyObj + */ + $privateKeyObj = \openssl_pkey_get_private($privateKey); + + $appIdentifier = $githubAppId; + + $iat = time(); + $exp = $iat + 10 * 60; + $payload = [ + 'iat' => $iat, + 'exp' => $exp, + 'iss' => $appIdentifier, + ]; + + // generate access token + $jwt = new JWT($privateKeyObj, 'RS256'); + $token = $jwt->encode($payload); + $this->jwtToken = $token; + $res = $this->call(self::METHOD_POST, '/app/installations/' . $this->installationId . '/access_tokens', ['Authorization' => 'Bearer ' . $token]); + $this->accessToken = $res['body']['token']; + } + + /** + * Get user + * + * @return array + * + * @throws Exception + */ + public function getUser(string $username): array + { + $response = $this->call(self::METHOD_GET, '/users/' . $username); + + return $response; + } + + /** + * Get owner name of the GitHub installation + * + * @return string + */ + public function getOwnerName(string $installationId): string + { + $url = '/app/installations/' . $installationId; + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->jwtToken"]); + + if (!isset($response['body']['account']['login'])) { + throw new Exception("Owner name retrieval response is missing account login."); + } + + return $response['body']['account']['login']; + } + + /** + * Get Pull Request + * + * @return array The retrieved pull request + */ + public function getPullRequest(string $owner, string $repositoryName, int $pullRequestNumber): array + { + $url = "/repos/{$owner}/{$repositoryName}/pulls/{$pullRequestNumber}"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + return $response['body'] ?? []; + } + + /** + * Get latest opened pull request with specific base branch + * @return array + */ + public function getPullRequestFromBranch(string $owner, string $repositoryName, string $branch): array + { + $head = "{$owner}:{$branch}"; + $url = "/repos/{$owner}/{$repositoryName}/pulls?head={$head}&state=open&sort=updated&per_page=1"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + return $response['body'][0] ?? []; + } + + /** + * Lists branches for a given repository + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the GitHub repository + * @return array List of branch names as array + */ + public function listBranches(string $owner, string $repositoryName): array + { + $url = "/repos/$owner/$repositoryName/branches"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + $names = []; + foreach ($response['body'] as $subarray) { + $names[] = $subarray['name']; + } + + return $names; + } + + /** + * Get details of a commit using commit hash + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the GitHub repository + * @param string $commitHash SHA of the commit + * @return array Details of the commit + */ + public function getCommit(string $owner, string $repositoryName, string $commitHash): array + { + $url = "/repos/$owner/$repositoryName/commits/$commitHash"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + if (!isset($response['body']['commit']['author']['name']) || !isset($response['body']['commit']['message'])) { + throw new Exception("Commit author or message information missing."); + } + + return [ + 'commitAuthor' => $response['body']['commit']['author']['name'], + 'commitMessage' => $response['body']['commit']['message'], + ]; + } + + /** + * Get latest commit of a branch + * + * @param string $owner Owner name of the repository + * @param string $repositoryName Name of the GitHub repository + * @param string $branch Name of the branch + * @return array Details of the commit + */ + public function getLatestCommit(string $owner, string $repositoryName, string $branch): array + { + $url = "/repos/$owner/$repositoryName/commits/$branch?per_page=1"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + if ( + !isset($response['body']['commit']['author']['name']) || + !isset($response['body']['commit']['message']) || + !isset($response['body']['sha']) || + !isset($response['body']['html_url']) + ) { + throw new Exception("Latest commit response is missing required information."); + } + + return [ + 'commitAuthor' => $response['body']['commit']['author']['name'], + 'commitMessage' => $response['body']['commit']['message'], + 'commitHash' => $response['body']['sha'], + 'commitUrl' => $response['body']['html_url'] + ]; + } + + /** + * Updates status check of each commit + * state can be one of: error, failure, pending, success + */ + public function updateCommitStatus(string $repositoryName, string $commitHash, string $owner, string $state, string $description = '', string $target_url = '', string $context = ''): void + { + $url = "/repos/$owner/$repositoryName/statuses/$commitHash"; + + $body = [ + 'state' => $state, + 'target_url' => $target_url, + 'description' => $description, + 'context' => $context, + ]; + + $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); + } + + /** + * Generates a clone command using app access token + */ + public function generateCloneCommand(string $owner, string $repositoryName, string $branchName, string $directory, string $rootDirectory): string + { + if (empty($rootDirectory)) { + $rootDirectory = '*'; + } + + // URL encode the components for the clone URL + $owner = urlencode($owner); + $repositoryName = urlencode($repositoryName); + $accessToken = urlencode($this->accessToken); + $cloneUrl = "https://{$owner}:{$accessToken}@github.com/{$owner}/{$repositoryName}"; + + $directory = escapeshellarg($directory); + $rootDirectory = escapeshellarg($rootDirectory); + $branchName = escapeshellarg($branchName); + + $commands = [ + "mkdir -p {$directory}", + "cd {$directory}", + "git config --global init.defaultBranch main", + "git init", + "git remote add origin {$cloneUrl}", + "git config core.sparseCheckout true", + "echo {$rootDirectory} >> .git/info/sparse-checkout", + "if git ls-remote --exit-code --heads origin {$branchName}; then git pull origin {$branchName} && git checkout {$branchName}; else git checkout -b {$branchName}; fi" + ]; + + $fullCommand = implode(" && ", $commands); + + return $fullCommand; + } + + /** + * Parses webhook event payload + * + * @param string $event Type of event: push, pull_request etc + * @param string $payload The webhook payload received from GitHub + * @return array Parsed payload as a json object + */ + public function getEvent(string $event, string $payload): array + { + $payload = json_decode($payload, true); + + if ($payload === null || !is_array($payload)) { + throw new Exception("Invalid payload."); + } + + $installationId = strval($payload['installation']['id']); + + switch ($event) { + case 'push': + $branchCreated = isset($payload['created']) ? $payload['created'] : false; + $ref = $payload['ref'] ?? ''; + $repositoryId = strval($payload['repository']['id'] ?? ''); + $repositoryName = $payload['repository']['name'] ?? ''; + $branch = str_replace('refs/heads/', '', $ref); + $branchUrl = $payload['repository']['url'] . "/tree/" . $branch; + $repositoryUrl = $payload['repository']['url']; + $commitHash = $payload['after'] ?? ''; + $owner = $payload['repository']['owner']['name'] ?? ''; + $authorUrl = $payload['sender']['html_url']; + $headCommitAuthor = $payload['head_commit']['author']['name'] ?? ''; + $headCommitMessage = $payload['head_commit']['message'] ?? ''; + $headCommitUrl = $payload['head_commit']['url'] ?? ''; + + return [ + 'branchCreated' => $branchCreated, + 'branch' => $branch, + 'branchUrl' => $branchUrl, + 'repositoryId' => $repositoryId, + 'repositoryName' => $repositoryName, + 'repositoryUrl' => $repositoryUrl, + 'installationId' => $installationId, + 'commitHash' => $commitHash, + 'owner' => $owner, + 'authorUrl' => $authorUrl, + 'headCommitAuthor' => $headCommitAuthor, + 'headCommitMessage' => $headCommitMessage, + 'headCommitUrl' => $headCommitUrl, + 'external' => false, + 'pullRequestNumber' => '', + 'action' => '', + ]; + case 'pull_request': + $repositoryId = strval($payload['repository']['id'] ?? ''); + $branch = $payload['pull_request']['head']['ref'] ?? ''; + $repositoryName = $payload['repository']['name'] ?? ''; + $repositoryUrl = $payload['repository']['html_url'] ?? ''; + $branchUrl = "$repositoryUrl/tree/$branch"; + $pullRequestNumber = $payload['number'] ?? ''; + $action = $payload['action'] ?? ''; + $owner = $payload['repository']['owner']['login'] ?? ''; + $authorUrl = $payload['sender']['html_url']; + $commitHash = $payload['pull_request']['head']['sha'] ?? ''; + $headCommitUrl = $repositoryUrl . "/commits/" . $commitHash; + $external = $payload['pull_request']['head']['user']['login'] !== $payload['pull_request']['base']['user']['login']; + + return [ + 'branch' => $branch, + 'branchUrl' => $branchUrl, + 'repositoryId' => $repositoryId, + 'repositoryName' => $repositoryName, + 'repositoryUrl' => $repositoryUrl, + 'installationId' => $installationId, + 'commitHash' => $commitHash, + 'owner' => $owner, + 'authorUrl' => $authorUrl, + 'headCommitUrl' => $headCommitUrl, + 'external' => $external, + 'pullRequestNumber' => $pullRequestNumber, + 'action' => $action, + ]; + case 'installation': + case 'installation_repositories': + $action = $payload['action'] ?? ''; + $userName = $payload['installation']['account']['login'] ?? ''; + + return [ + 'action' => $action, + 'installationId' => $installationId, + 'userName' => $userName, + ]; + } + + return []; + } + + + /** + * Validate webhook event + * + * @param string $payload Raw body of HTTP request + * @param string $signature Signature provided by GitHub in header + * @param string $signatureKey Webhook secret configured on GitHub + * @return bool + */ + public function validateWebhookEvent(string $payload, string $signature, string $signatureKey): bool + { + return $signature === ('sha256=' . hash_hmac('sha256', $payload, $signatureKey)); + } +} diff --git a/tests/Detector/DetectorTest.php b/tests/Detector/DetectorTest.php new file mode 100644 index 00000000..fd229bcd --- /dev/null +++ b/tests/Detector/DetectorTest.php @@ -0,0 +1,82 @@ + $files + * @param array $languages + */ + public function detect($files, $languages): ?string + { + $detectorFactory = new Detector($files, $languages); + + $detectorFactory + ->addDetector(new JavaScript()) + ->addDetector(new PHP()) + ->addDetector(new Python()) + ->addDetector(new Dart()) + ->addDetector(new Swift()) + ->addDetector(new Ruby()) + ->addDetector(new Java()) + ->addDetector(new CPP()) + ->addDetector(new Deno()) + ->addDetector(new Dotnet()); + + $runtime = $detectorFactory->detect(); + + return $runtime; + } + + public function setUp(): void + { + $this->github = new GitHub(new Cache(new None())); + $privateKey = App::getEnv('PRIVATE_KEY') ?? ''; + $githubAppId = App::getEnv('APP_IDENTIFIER') ?? ''; + $installationId = App::getEnv('INSTALLATION_ID') ?? ''; + $this->github->initializeVariables($installationId, $privateKey, $githubAppId); + } + + public function testLanguageDetection(): void + { + $languageMap = [ + ['vermakhushboo', 'basic-js-crud', 'node'], + ['appwrite', 'appwrite', 'php'], + ['joblib', 'joblib', 'python'], + ['smartherd', 'DartTutorial', 'dart'], + ['realm', 'realm-swift', 'swift'], + ['aws', 'aws-sdk-ruby', 'ruby'], + ['functionaljava', 'functionaljava', 'java'], + ['Dobiasd', 'FunctionalPlus', 'cpp'], + ['anthonychu', 'azure-functions-deno-worker', 'deno'], + ['mono', 'mono-basic', 'dotnet'], + ]; + + foreach ($languageMap as [$owner, $repositoryName, $expectedRuntime]) { + $files = $this->github->listRepositoryContents($owner, $repositoryName); + $languages = $this->github->listRepositoryLanguages($owner, $repositoryName); + $runtime = $this->detect($files, $languages); + $this->assertEquals($expectedRuntime, $runtime); + } + } +} diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php new file mode 100644 index 00000000..56bc80ac --- /dev/null +++ b/tests/VCS/Adapter/GitHubTest.php @@ -0,0 +1,180 @@ +vcsAdapter = new GitHub(new Cache(new None())); + $privateKey = App::getEnv('PRIVATE_KEY') ?? ''; + $githubAppId = App::getEnv('APP_IDENTIFIER') ?? ''; + $installationId = App::getEnv('INSTALLATION_ID') ?? ''; + $this->vcsAdapter->initializeVariables($installationId, $privateKey, $githubAppId); + } + + public function testgetEvent(): void + { + $payload_push = '{ + "created": false, + "ref": "refs/heads/main", + "before": "1234", + "after": "4567", + "repository": { + "id": 603754812, + "node_id": "R_kgDOI_yRPA", + "name": "testing-fork", + "full_name": "vermakhushboo/testing-fork", + "private": true, + "url": "https://github.com/vermakhushboo/g4-node-function", + "owner": { + "name": "vermakhushboo" + } + }, + "installation": { + "id": 1234 + }, + "head_commit": { + "author": { + "name": "Khushboo Verma" + }, + "message": "Update index.js", + "url": "https://github.com/vermakhushboo/g4-node-function/commit/b787f03343171ff5a477627796140bfa1d02da09" + }, + "sender": { + "html_url": "https://github.com/vermakhushboo" + } + }'; + + $payload_pull_request = '{ + "action": "opened", + "number": 1, + "pull_request": { + "id": 1303283688, + "state": "open", + "html_url": "https://github.com/vermakhushboo/g4-node-function/pull/17", + "head": { + "ref": "test", + "sha": "a27dbe54b17032ee35a16c24bac151e5c2b33328", + "label": "vermakhushboo:test", + "user": { + "login": "vermakhushboo" + } + }, + "base": { + "label": "vermakhushboo:main", + "user": { + "login": "vermakhushboo" + } + } + }, + "repository": { + "id": 3498, + "name": "functions-example", + "owner": { + "login": "vermakhushboo" + }, + "html_url": "https://github.com/vermakhushboo/g4-node-function" + }, + "installation": { + "id": 9876 + }, + "sender": { + "html_url": "https://github.com/vermakhushboo" + } + }'; + + $payload_uninstall = '{ + "action": "deleted", + "installation": { + "id": 1234, + "account": { + "login": "vermakhushboo" + } + } + } + '; + + $pushResult = $this->vcsAdapter->getEvent('push', $payload_push); + $this->assertEquals('main', $pushResult['branch']); + $this->assertEquals('603754812', $pushResult['repositoryId']); + + $pullRequestResult = $this->vcsAdapter->getEvent('pull_request', $payload_pull_request); + $this->assertEquals('opened', $pullRequestResult['action']); + $this->assertEquals(1, $pullRequestResult['pullRequestNumber']); + + $uninstallResult = $this->vcsAdapter->getEvent('installation', $payload_uninstall); + $this->assertEquals('deleted', $uninstallResult['action']); + $this->assertEquals(1234, $uninstallResult['installationId']); + } + + public function testGetComment(): void + { + $owner = 'vermakhushboo'; + $repositoryName = 'basic-js-crud'; + $commentId = '1431560395'; + + $result = $this->vcsAdapter->getComment($owner, $repositoryName, $commentId); + + $this->assertIsString($result); + $this->assertNotEmpty($result); + } + + public function testGetRepositoryName(): void + { + $repositoryName = $this->vcsAdapter->getRepositoryName('432284323'); + $this->assertEquals('basic-js-crud', $repositoryName); + } + + public function testGetPullRequest(): void + { + $owner = 'vermakhushboo'; + $repositoryName = 'basic-js-crud'; + $pullRequestNumber = 1; + + $result = $this->vcsAdapter->getPullRequest($owner, $repositoryName, $pullRequestNumber); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + $this->assertEquals($pullRequestNumber, $result['number']); + $this->assertEquals($owner, $result['base']['user']['login']); + $this->assertEquals($repositoryName, $result['base']['repo']['name']); + } + + public function testGenerateCloneCommand(): void + { + $gitCloneCommand = $this->vcsAdapter->generateCloneCommand('test-kh', 'test2', 'main', 'test', '*'); + $this->assertNotEmpty($gitCloneCommand); + $this->assertStringContainsString('sparse-checkout', $gitCloneCommand); + } + + public function testUpdateComment(): void + { + $commentId = $this->vcsAdapter->updateComment('test-kh', 'test2', 1630320767, 'update'); + $this->assertNotEmpty($commentId); + } + + public function testGetCommit(): void + { + $commitDetails = $this->vcsAdapter->getCommit('test-kh', 'test1', '7ae65094d56edafc48596ffbb77950e741e56412'); + $this->assertIsArray($commitDetails); + } + + public function testGetLatestCommit(): void + { + $commitDetails = $this->vcsAdapter->getLatestCommit('test-kh', 'test1', 'test'); + $this->assertEquals('Khushboo Verma', $commitDetails['commitAuthor']); + } +} diff --git a/tests/VCS/Base.php b/tests/VCS/Base.php new file mode 100644 index 00000000..26c446a0 --- /dev/null +++ b/tests/VCS/Base.php @@ -0,0 +1,82 @@ +vcsAdapter = $this->createVCSAdapter(); + } + + abstract protected function createVCSAdapter(): Git; + + abstract public function testUpdateComment(): void; + + abstract public function testGenerateCloneCommand(): void; + + abstract public function testgetEvent(): void; + + abstract public function testGetRepositoryName(): void; + + abstract public function testGetComment(): void; + + abstract public function testGetPullRequest(): void; + + public function testGetPullRequestFromBranch(): void + { + $result = $this->vcsAdapter->getPullRequestFromBranch('vermakhushboo', 'basic-js-crud', 'test'); + $this->assertIsArray($result); + $this->assertNotEmpty($result); + } + + public function testGetOwnerName(): void + { + $installationId = App::getEnv('INSTALLATION_ID') ?? ''; + $owner = $this->vcsAdapter->getOwnerName($installationId); + $this->assertEquals('test-kh', $owner); + } + + public function testListRepositories(): void + { + $repos = $this->vcsAdapter->listRepositories(1, 2); + $this->assertCount(2, $repos); + } + + public function testCreateComment(): void + { + $commentId = $this->vcsAdapter->createComment('test-kh', 'test2', 1, 'hello'); + $this->assertNotEmpty($commentId); + } + + public function testListBranches(): void + { + $branches = $this->vcsAdapter->listBranches('vermakhushboo', 'basic-js-crud'); + $this->assertIsArray($branches); + $this->assertNotEmpty($branches); + } + + public function testListRepositoryLanguages(): void + { + $languages = $this->vcsAdapter->listRepositoryLanguages('vermakhushboo', 'basic-js-crud'); + + $this->assertIsArray($languages); + + $this->assertContains('JavaScript', $languages); + $this->assertContains('HTML', $languages); + $this->assertContains('CSS', $languages); + } + + public function testListRepositoryContents(): void + { + $contents = $this->vcsAdapter->listRepositoryContents('appwrite', 'appwrite', 'src/Appwrite'); + $this->assertIsArray($contents); + $this->assertNotEmpty($contents); + } +}