diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 72084f84..590c9593 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -2,75 +2,131 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community 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 +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or 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 +* Publishing others' private information, such as a physical or email + address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a - professional setting + professional setting -## Our Responsibilities +## Enforcement 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. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -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. +Community leaders 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, and will communicate reasons for moderation +decisions when appropriate. ## 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. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at graham@alt-three.com. 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. +reported to the community leaders responsible for enforcement at +hello@gjcampbell.co.uk. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. -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. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. ## 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 +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -[homepage]: https://www.contributor-covenant.org +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7c8267dc..d33db7e8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,16 +2,16 @@ Contributions are **welcome** and will be fully **credited**. -We accept contributions via pull requests on Github. Please review these guidelines before continuing. +We accept contributions via pull requests on GitHub. Please review these guidelines before continuing. ## Guidelines -* Please follow the [PSR-2 Coding Style Guide](https://www.php-fig.org/psr/psr-2/), enforced by [StyleCI](https://styleci.io/). +* Please follow the [PSR-12 Coding Style Guide](https://www.php-fig.org/psr/psr-12/), enforced by [StyleCI](https://styleci.io/). * Ensure that the current tests pass, and if you've added something new, add the tests where relevant. -* Send a coherent commit history, making sure each individual commit in your pull request is meaningful. +* Send a coherent commit history, making sure each commit in your pull request is meaningful. * You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts. -* If you are changing or adding to the behaviour or public api, you may need to update the docs. -* Please remember that we follow [SemVer](https://semver.org/). +* If you are changing or adding to the behaviour or public API, you may need to update the docs. +* Please remember that we follow [Semantic Versioning](https://semver.org/). ## Running Tests @@ -28,5 +28,5 @@ $ vendor/bin/phpunit ``` * A script `test-git-version.sh` is available in repository to test gitlib against many git versions. -* The tests will be automatically run by [Travis CI](https://travis-ci.org/) against pull requests. -* We also have [StyleCI](https://styleci.io/) setup to automatically fix any code style issues. +* The tests will be automatically run by [GitHub Actions](https://github.com/features/actions) against pull requests. +* We also have [StyleCI](https://styleci.io/) set up to automatically fix any code style issues. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 79a128e7..0c3e8a24 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -2,20 +2,13 @@ ## Supported Versions -After each new major (minor) release, the previous release will be supported -for no less than 12 (6) months, unless explictly otherwise. This may mean that +After each new major release, the previous release will be supported for no +less than 12 months, unless explictly stated otherwise. This may mean that there are multiple supported versions at any given time. ## Reporting a Vulnerability If you discover a security vulnerability within this package, please send an -email to one the security contacts. All security vulnerabilities will be -promptly addressed. Please do not disclose security-related issues publicly -until a fix has been announced. - -### Security Contacts - -* Graham Campbell (graham@alt-three.com) -* Julien Didier (genzo.wm@gmail.com) -* Grégoire Pineau (lyrixx@lyrixx.info) -* Alexandre Salomé (alexandre.salome@gmail.com) +email to security@tidelift.com. All security vulnerabilities will be promptly +addressed. Please do not disclose security-related issues publicly until a fix +has been announced. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..4e611ce8 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +name: Tests + +on: + push: + branches: [ main ] + pull_request: + +jobs: + tests: + name: Test PHP ${{ matrix.php }} ${{ matrix.name }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + composer-flags: [''] + name: [''] + include: + - php: '8.0' + composer-flags: '--prefer-lowest' + name: '(prefer lowest dependencies)' + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Setup Problem Matchers + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Install Composer dependencies + run: | + composer update --prefer-dist --no-interaction ${{ matrix.composer-flags }} + + - name: Execute PHPUnit + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index bc959c53..376a2cd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.phpunit.result.cache /composer.lock /phpunit.xml /vendor diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index db3bdc7a..00000000 --- a/.travis.yml +++ /dev/null @@ -1,49 +0,0 @@ -language: php - -matrix: - include: - - php: 5.6 - dist: xenial - env: SYMFONY_VERSION=^3.4 - - php: 7.0 - dist: xenial - env: SYMFONY_VERSION=^3.4 - - php: 7.1 - dist: bionic - env: SYMFONY_VERSION=^3.4 - - php: 7.1 - dist: bionic - env: SYMFONY_VERSION=^4.0 - - php: 7.2 - dist: bionic - env: SYMFONY_VERSION=^3.4 - - php: 7.2 - dist: bionic - env: SYMFONY_VERSION=^4.0 - - php: 7.2 - dist: bionic - env: SYMFONY_VERSION=^5.0 - - php: 7.3 - dist: bionic - env: SYMFONY_VERSION=^3.4 - - php: 7.3 - dist: bionic - env: SYMFONY_VERSION=^4.0 - - php: 7.3 - dist: bionic - env: SYMFONY_VERSION=^5.0 - - php: 7.4 - dist: bionic - env: SYMFONY_VERSION=^3.4 - - php: 7.4 - dist: bionic - env: SYMFONY_VERSION=^4.0 - - php: 7.4 - dist: bionic - env: SYMFONY_VERSION=^5.0 - -before_install: travis_retry composer require "symfony/process:${SYMFONY_VERSION}" --no-update - -install: travis_retry composer install --prefer-dist --no-progress -n -o - -script: vendor/bin/phpunit diff --git a/README.md b/README.md index 7aae7fca..e0dcade7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ Gitlib for Gitonomy =================== -[![Build Status](https://img.shields.io/travis/com/gitonomy/gitlib/master.svg?style=flat-square)](https://travis-ci.com/gitonomy/gitlib) -[![StyleCI](https://github.styleci.io/repos/5709354/shield?branch=master)](https://github.styleci.io/repos/5709354) +[![Build Status](https://img.shields.io/github/actions/workflow/status/gitonomy/gitlib/tests.yml?label=Tests&style=flat-square&branch=1.3)](https://github.com/gitonomy/gitlib/actions?query=workflow%3ATests+branch%3A1.3) +[![StyleCI](https://github.styleci.io/repos/5709354/shield?branch=1.3)](https://github.styleci.io/repos/5709354?branch=1.3) [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://opensource.org/licenses/MIT) +[![Downloads](https://img.shields.io/packagist/dt/gitonomy/gitlib?style=flat-square)](https://packagist.org/packages/gitonomy/gitlib) -This library provides methods to access Git repository from PHP 5.6 or 7. +This library provides methods to access Git repository from PHP 5.6+. It makes shell calls, which makes it less performant than any solution. @@ -25,7 +26,7 @@ or edit your `composer.json` file by hand: ```json { "require": { - "gitonomy/gitlib": "^1.2" + "gitonomy/gitlib": "^1.3" } } ``` diff --git a/composer.json b/composer.json index 6c57fc47..a3de0536 100644 --- a/composer.json +++ b/composer.json @@ -5,19 +5,23 @@ "authors": [ { "name": "Graham Campbell", - "email": "graham@alt-three.com" + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" }, { "name": "Julien Didier", - "email": "genzo.wm@gmail.com" + "email": "genzo.wm@gmail.com", + "homepage": "https://github.com/juliendidier" }, { "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" + "email": "lyrixx@lyrixx.info", + "homepage": "https://github.com/lyrixx" }, { "name": "Alexandre Salomé", - "email": "alexandre.salome@gmail.com" + "email": "alexandre.salome@gmail.com", + "homepage": "https://github.com/alexandresalome" } ], "autoload": { @@ -31,23 +35,21 @@ } }, "require": { - "php": "^5.6 || ^7.0", + "php": "^8.0", "ext-pcre": "*", "symfony/polyfill-mbstring": "^1.7", - "symfony/process": "^3.4|^4.0|^5.0" + "symfony/process": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { "ext-fileinfo": "*", - "phpunit/phpunit": "^5.7|^6.5|^7.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.20 || ^9.5.9", "psr/log": "^1.0" }, - "suggest": { - "ext-fileinfo": "Required to determine the mimetype of a blob", - "psr/log": "Required to use loggers for reporting of execution" + "config": { + "preferred-install": "dist", + "sort-packages": true }, - "extra": { - "branch-alias": { - "dev-master": "1.2-dev" - } - } + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 333343d5..77fe6238 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,11 +5,12 @@ beStrictAboutOutputDuringTests="true" bootstrap="tests/bootstrap.php" colors="true" + convertDeprecationsToExceptions="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" failOnRisky="true" - failOnWarning="true" + failOnWarning="false" processIsolation="false" stopOnError="false" stopOnFailure="false" diff --git a/src/Gitonomy/Git/Admin.php b/src/Gitonomy/Git/Admin.php index 7b64cd77..7b32794c 100644 --- a/src/Gitonomy/Git/Admin.php +++ b/src/Gitonomy/Git/Admin.php @@ -67,6 +67,30 @@ public static function isValidRepository($url, array $options = []) return $process->isSuccessFul(); } + /** + * Checks the validity of a git repository url without cloning it and + * check if a certain branch exists in that repository. + * + * This will use the `ls-remote` command of git against the given url. + * Usually, this command returns 0 when successful, and 128 when the + * repository is not found. + * + * @param string $url url of repository to check + * @param string $branchName name of branch to check + * @param array $options options for Repository creation + * + * @return bool true if url is valid and branch exists + */ + public static function isValidRepositoryAndBranch($url, $branchName, array $options = []) + { + $process = static::getProcess('ls-remote', ['--heads', $url, $branchName], $options); + + $process->run(); + $processOutput = $process->getOutput(); + + return $process->isSuccessFul() && strpos($processOutput, $branchName) !== false; + } + /** * Clone a repository to a local path. * diff --git a/src/Gitonomy/Git/Blame.php b/src/Gitonomy/Git/Blame.php index 17d8bfe7..dec0773b 100644 --- a/src/Gitonomy/Git/Blame.php +++ b/src/Gitonomy/Git/Blame.php @@ -108,9 +108,7 @@ public function getGroupedLines() } /** - * Returns all lines of the blame. - * - * @return array + * @return Line[] All lines of the blame. */ public function getLines() { @@ -139,6 +137,7 @@ public function getLines() /** * @return int */ + #[\ReturnTypeWillChange] public function count() { return count($this->getLines()); diff --git a/src/Gitonomy/Git/Blob.php b/src/Gitonomy/Git/Blob.php index 10150935..dfe885fe 100644 --- a/src/Gitonomy/Git/Blob.php +++ b/src/Gitonomy/Git/Blob.php @@ -19,6 +19,11 @@ */ class Blob { + /** + * @var int Size that git uses to look for NULL byte: https://git.kernel.org/pub/scm/git/git.git/tree/xdiff-interface.c?h=v2.44.0#n193 + */ + private const FIRST_FEW_BYTES = 8000; + /** * @var Repository */ @@ -39,6 +44,11 @@ class Blob */ protected $mimetype; + /** + * @var bool + */ + protected $text; + /** * @param Repository $repository Repository where the blob is located * @param string $hash Hash of the blob @@ -58,9 +68,9 @@ public function getHash() } /** - * Returns content of the blob. - * * @throws ProcessException Error occurred while getting content of blob + * + * @return string Content of the blob. */ public function getContent() { @@ -89,6 +99,9 @@ public function getMimetype() /** * Determines if file is binary. * + * Uses the same check that git uses to determine if a file is binary or not + * https://git.kernel.org/pub/scm/git/git.git/tree/xdiff-interface.c?h=v2.44.0#n193 + * * @return bool */ public function isBinary() @@ -99,10 +112,17 @@ public function isBinary() /** * Determines if file is text. * + * Uses the same check that git uses to determine if a file is binary or not + * https://git.kernel.org/pub/scm/git/git.git/tree/xdiff-interface.c?h=v2.44.0#n193 + * * @return bool */ public function isText() { - return (bool) preg_match('#^text/|^application/xml#', $this->getMimetype()); + if (null === $this->text) { + $this->text = !str_contains(substr($this->getContent(), 0, self::FIRST_FEW_BYTES), chr(0)); + } + + return $this->text; } } diff --git a/src/Gitonomy/Git/Commit.php b/src/Gitonomy/Git/Commit.php index 10141bec..573d3b52 100644 --- a/src/Gitonomy/Git/Commit.php +++ b/src/Gitonomy/Git/Commit.php @@ -16,6 +16,7 @@ use Gitonomy\Git\Exception\InvalidArgumentException; use Gitonomy\Git\Exception\ProcessException; use Gitonomy\Git\Exception\ReferenceNotFoundException; +use Gitonomy\Git\Reference\Branch; use Gitonomy\Git\Util\StringHelper; /** @@ -35,8 +36,8 @@ class Commit extends Revision /** * Constructor. * - * @param Gitonomy\Git\Repository $repository Repository of the commit - * @param string $hash Hash of the commit + * @param Repository $repository Repository of the commit + * @param string $hash Hash of the commit */ public function __construct(Repository $repository, $hash, array $data = []) { @@ -61,7 +62,7 @@ public function setData(array $data) */ public function getDiff() { - $args = ['-r', '-p', '-m', '-M', '--no-commit-id', '--full-index', $this->revision]; + $args = ['-r', '-p', '--raw', '-m', '-M', '--no-commit-id', '--full-index', $this->revision]; $diff = Diff::parse($this->repository->run('diff-tree', $args)); $diff->setRepository($this->repository); @@ -91,6 +92,8 @@ public function getShortHash() /** * Returns a fixed-with short hash. + * + * @return string Short hash */ public function getFixedShortHash($length = 6) { @@ -100,7 +103,7 @@ public function getFixedShortHash($length = 6) /** * Returns parent hashes. * - * @return array An array of SHA1 hashes + * @return string[] An array of SHA1 hashes */ public function getParentHashes() { @@ -110,7 +113,7 @@ public function getParentHashes() /** * Returns the parent commits. * - * @return array An array of Commit objects + * @return Commit[] An array of Commit objects */ public function getParents() { @@ -132,6 +135,9 @@ public function getTreeHash() return $this->getData('treeHash'); } + /** + * @return Tree + */ public function getTree() { return $this->getData('tree'); @@ -184,7 +190,7 @@ public function getShortMessage($length = 50, $preserve = false, $separator = '. /** * Resolves all references associated to this commit. * - * @return array An array of references (Branch, Tag, Squash) + * @return Reference[] An array of references (Branch, Tag, Squash) */ public function resolveReferences() { @@ -197,7 +203,7 @@ public function resolveReferences() * @param bool $local set true to try to locate a commit on local repository * @param bool $remote set true to try to locate a commit on remote repository * - * @return array An array of Reference\Branch + * @return Reference[]|Branch[] An array of Reference\Branch */ public function getIncludingBranches($local = true, $remote = true) { @@ -374,9 +380,9 @@ private function getData($name) array_shift($lines); array_shift($lines); - $data['bodyMessage'] = implode("\n", $lines); + $this->data['bodyMessage'] = implode("\n", $lines); - return $data['bodyMessage']; + return $this->data['bodyMessage']; } $parser = new Parser\CommitParser(); diff --git a/src/Gitonomy/Git/CommitReference.php b/src/Gitonomy/Git/CommitReference.php index bb20bc3c..7be3d89c 100644 --- a/src/Gitonomy/Git/CommitReference.php +++ b/src/Gitonomy/Git/CommitReference.php @@ -14,6 +14,9 @@ class CommitReference { + /** + * @var string + */ private $hash; public function __construct($hash) diff --git a/src/Gitonomy/Git/Diff/Diff.php b/src/Gitonomy/Git/Diff/Diff.php index faafbcba..7737ee8f 100644 --- a/src/Gitonomy/Git/Diff/Diff.php +++ b/src/Gitonomy/Git/Diff/Diff.php @@ -23,7 +23,7 @@ class Diff { /** - * @var array + * @var File[] */ protected $files; @@ -62,18 +62,10 @@ public function setRepository(Repository $repository) } } - /** - * @return array - */ - public function getRevisions() - { - return $this->revisions; - } - /** * Get list of files modified in the diff's revision. * - * @return array An array of Diff\File objects + * @return File[] An array of Diff\File objects */ public function getFiles() { diff --git a/src/Gitonomy/Git/Diff/File.php b/src/Gitonomy/Git/Diff/File.php index f7099769..bfca155c 100644 --- a/src/Gitonomy/Git/Diff/File.php +++ b/src/Gitonomy/Git/Diff/File.php @@ -55,7 +55,7 @@ class File protected $isBinary; /** - * @var array An array of FileChange objects + * @var FileChange[] An array of FileChange objects */ protected $changes; @@ -215,6 +215,9 @@ public function isBinary() return $this->isBinary; } + /** + * @return FileChange[] + */ public function getChanges() { return $this->changes; @@ -236,6 +239,9 @@ public function toArray() ]; } + /** + * @return File + */ public static function fromArray(array $array) { $file = new self($array['old_name'], $array['new_name'], $array['old_mode'], $array['new_mode'], $array['old_index'], $array['new_index'], $array['is_binary']); @@ -272,6 +278,10 @@ public function getOldBlob() throw new \LogicException('Can\'t return old Blob on a creation'); } + if ($this->oldIndex === '') { + throw new \RuntimeException('Index is missing to return Blob object.'); + } + return $this->repository->getBlob($this->oldIndex); } @@ -285,6 +295,10 @@ public function getNewBlob() throw new \LogicException('Can\'t return new Blob on a deletion'); } + if ($this->newIndex === '') { + throw new \RuntimeException('Index is missing to return Blob object.'); + } + return $this->repository->getBlob($this->newIndex); } } diff --git a/src/Gitonomy/Git/Diff/FileChange.php b/src/Gitonomy/Git/Diff/FileChange.php index 898bbb48..cca18dc6 100644 --- a/src/Gitonomy/Git/Diff/FileChange.php +++ b/src/Gitonomy/Git/Diff/FileChange.php @@ -24,6 +24,15 @@ class FileChange protected $rangeNewCount; protected $lines; + /** + * @param int $rangeOldStart + * @param int $rangeOldCount + * @param int $rangeNewStart + * @param int $rangeNewCount + * @param array $lines + * + * @return void + */ public function __construct($rangeOldStart, $rangeOldCount, $rangeNewStart, $rangeNewCount, $lines) { $this->rangeOldStart = $rangeOldStart; @@ -33,6 +42,9 @@ public function __construct($rangeOldStart, $rangeOldCount, $rangeNewStart, $ran $this->lines = $lines; } + /** + * @return int + */ public function getCount($type) { $result = 0; @@ -45,31 +57,49 @@ public function getCount($type) return $result; } + /** + * @return int + */ public function getRangeOldStart() { return $this->rangeOldStart; } + /** + * @return int + */ public function getRangeOldCount() { return $this->rangeOldCount; } + /** + * @return int + */ public function getRangeNewStart() { return $this->rangeNewStart; } + /** + * @return int + */ public function getRangeNewCount() { return $this->rangeNewCount; } + /** + * @return array + */ public function getLines() { return $this->lines; } + /** + * @return array + */ public function toArray() { return [ @@ -81,6 +111,11 @@ public function toArray() ]; } + /** + * @param array $array + * + * @return self + */ public static function fromArray(array $array) { return new self($array['range_old_start'], $array['range_old_count'], $array['range_new_start'], $array['range_new_count'], $array['lines']); diff --git a/src/Gitonomy/Git/Hooks.php b/src/Gitonomy/Git/Hooks.php index 3c19d2c4..dd0adbe8 100644 --- a/src/Gitonomy/Git/Hooks.php +++ b/src/Gitonomy/Git/Hooks.php @@ -14,6 +14,7 @@ use Gitonomy\Git\Exception\InvalidArgumentException; use Gitonomy\Git\Exception\LogicException; +use Gitonomy\Git\Exception\RuntimeException; /** * Hooks collection, aggregated by repository. @@ -23,7 +24,7 @@ class Hooks { /** - * @var Gitonomy\Git\Repository + * @var \Gitonomy\Git\Repository */ protected $repository; @@ -82,7 +83,7 @@ public function setSymlink($name, $file) $path = $this->getPath($name); if (false === symlink($file, $path)) { - throw new RuntimeException(sprintf('Unable to create hook "%s"', $name, $path)); + throw new RuntimeException(sprintf('Unable to create hook "%s" (%s)', $name, $path)); } } @@ -121,6 +122,9 @@ public function remove($name) unlink($this->getPath($name)); } + /** + * @return string + */ protected function getPath($name) { return $this->repository->getGitDir().'/hooks/'.$name; diff --git a/src/Gitonomy/Git/Log.php b/src/Gitonomy/Git/Log.php index 13f1fb0f..47665359 100644 --- a/src/Gitonomy/Git/Log.php +++ b/src/Gitonomy/Git/Log.php @@ -12,6 +12,7 @@ namespace Gitonomy\Git; +use Gitonomy\Git\Diff\Diff; use Gitonomy\Git\Exception\ProcessException; use Gitonomy\Git\Exception\ReferenceNotFoundException; use Gitonomy\Git\Util\StringHelper; @@ -136,6 +137,9 @@ public function setLimit($limit) return $this; } + /** + * @return Commit + */ public function getSingleCommit() { $limit = $this->limit; @@ -151,7 +155,7 @@ public function getSingleCommit() } /** - * @return array + * @return Commit[] */ public function getCommits() { @@ -202,6 +206,7 @@ public function getCommits() /** * @see Countable */ + #[\ReturnTypeWillChange] public function count() { return $this->countCommits(); @@ -210,6 +215,7 @@ public function count() /** * @see IteratorAggregate */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->getCommits()); diff --git a/src/Gitonomy/Git/Parser/CommitParser.php b/src/Gitonomy/Git/Parser/CommitParser.php index 98234f6d..6fff1ea4 100644 --- a/src/Gitonomy/Git/Parser/CommitParser.php +++ b/src/Gitonomy/Git/Parser/CommitParser.php @@ -44,8 +44,10 @@ protected function doParse() $this->consumeNewLine(); $this->consume('committer '); - list($this->committerName, $this->committerEmail, $this->committerDate) = $this->consumeNameEmailDate(); - $this->committerDate = $this->parseDate($this->committerDate); + list($this->committerName, $this->committerEmail, $committerDate) = $this->consumeNameEmailDate(); + $this->committerDate = $this->parseDate($committerDate); + + $this->consumeMergeTag(); // will consume an GPG signed commit if there is one $this->consumeGPGSignature(); diff --git a/src/Gitonomy/Git/Parser/DiffParser.php b/src/Gitonomy/Git/Parser/DiffParser.php index 2b84df8b..15e6c037 100644 --- a/src/Gitonomy/Git/Parser/DiffParser.php +++ b/src/Gitonomy/Git/Parser/DiffParser.php @@ -22,14 +22,31 @@ class DiffParser extends ParserBase protected function doParse() { $this->files = []; + $indexes = []; + // Diff contains raw information + if (str_starts_with($this->content, ':')) { + while ($this->expects(':')) { + $this->consumeRegexp('/\d{6} \d{6} /'); + $oldIndex = $this->consumeShortHash(); + $this->consume(' '); + $newIndex = $this->consumeShortHash(); + $this->consumeTo("\n"); + $this->consumeNewLine(); + $indexes[] = [$oldIndex, $newIndex]; + } + $this->consumeNewLine(); + } elseif (!$this->isFinished()) { + trigger_error('Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.', E_USER_DEPRECATED); + } + $fileIndex = 0; while (!$this->isFinished()) { // 1. title - $vars = $this->consumeRegexp('/diff --git (a\/.*) (b\/.*)\n/'); + $vars = $this->consumeRegexp("/diff --git \"?(a\/.*?)\"? \"?(b\/.*?)\"?\n/"); $oldName = $vars[1]; $newName = $vars[2]; - $oldIndex = null; - $newIndex = null; + $oldIndex = isset($indexes[$fileIndex]) ? $indexes[$fileIndex][0] : null; + $newIndex = isset($indexes[$fileIndex]) ? $indexes[$fileIndex][1] : null; $oldMode = null; $newMode = null; @@ -38,6 +55,7 @@ protected function doParse() $newMode = $this->consumeTo("\n"); $this->consumeNewLine(); $oldMode = null; + $oldName = '/dev/null'; } if ($this->expects('old mode ')) { $oldMode = $this->consumeTo("\n"); @@ -49,6 +67,7 @@ protected function doParse() if ($this->expects('deleted file mode ')) { $oldMode = $this->consumeTo("\n"); $newMode = null; + $newName = '/dev/null'; $this->consumeNewLine(); } @@ -74,14 +93,15 @@ protected function doParse() } $this->consumeNewLine(); + //verifying if the file was deleted or created if ($this->expects('--- ')) { - $oldName = $this->consumeTo("\n"); + $oldName = $this->consumeTo("\n") === '/dev/null' ? '/dev/null' : $oldName; $this->consumeNewLine(); $this->consume('+++ '); - $newName = $this->consumeTo("\n"); + $newName = $this->consumeTo("\n") === '/dev/null' ? '/dev/null' : $newName; $this->consumeNewLine(); } elseif ($this->expects('Binary files ')) { - $vars = $this->consumeRegexp('/(.*) and (.*) differ\n/'); + $vars = $this->consumeRegexp('/"?(.*?)"? and "?(.*?)"? differ\n/'); $isBinary = true; $oldName = $vars[1]; $newName = $vars[2]; @@ -90,6 +110,9 @@ protected function doParse() $oldName = $oldName === '/dev/null' ? null : substr($oldName, 2); $newName = $newName === '/dev/null' ? null : substr($newName, 2); + + $oldIndex = $oldIndex === null ? '' : $oldIndex; + $newIndex = $newIndex === null ? '' : $newIndex; $oldIndex = preg_match('/^0+$/', $oldIndex) ? null : $oldIndex; $newIndex = preg_match('/^0+$/', $newIndex) ? null : $newIndex; $file = new File($oldName, $newName, $oldMode, $newMode, $oldIndex, $newIndex, $isBinary); @@ -97,10 +120,10 @@ protected function doParse() // 5. Diff while ($this->expects('@@ ')) { $vars = $this->consumeRegexp('/-(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?/'); - $rangeOldStart = $vars[1]; - $rangeOldCount = $vars[2]; - $rangeNewStart = $vars[3]; - $rangeNewCount = isset($vars[4]) ? $vars[4] : $vars[2]; // @todo Ici, t'as pris un gros raccourci mon loulou + $rangeOldStart = (int) $vars[1]; + $rangeOldCount = (int) ($vars[2] ?? 1); + $rangeNewStart = (int) $vars[3]; + $rangeNewCount = (int) ($vars[4] ?? 1); $this->consume(' @@'); $this->consumeTo("\n"); $this->consumeNewLine(); diff --git a/src/Gitonomy/Git/Parser/LogParser.php b/src/Gitonomy/Git/Parser/LogParser.php index 2672186e..67b86687 100644 --- a/src/Gitonomy/Git/Parser/LogParser.php +++ b/src/Gitonomy/Git/Parser/LogParser.php @@ -37,19 +37,24 @@ protected function doParse() } $this->consume('author '); - list($commit['authorName'], $commit['authorEmail'], $commit['authorDate']) = $this->consumeNameEmailDate(); - $commit['authorDate'] = $this->parseDate($commit['authorDate']); + list($commit['authorName'], $commit['authorEmail'], $authorDate) = $this->consumeNameEmailDate(); + $commit['authorDate'] = $this->parseDate($authorDate); $this->consumeNewLine(); $this->consume('committer '); - list($commit['committerName'], $commit['committerEmail'], $commit['committerDate']) = $this->consumeNameEmailDate(); - $commit['committerDate'] = $this->parseDate($commit['committerDate']); + list($commit['committerName'], $commit['committerEmail'], $committerDate) = $this->consumeNameEmailDate(); + $commit['committerDate'] = $this->parseDate($committerDate); + + $this->consumeMergeTag(); // will consume an GPG signed commit if there is one $this->consumeGPGSignature(); $this->consumeNewLine(); - $this->consumeNewLine(); + $this->consumeUnsupportedLinesToNewLine(); + if ($this->cursor < strlen($this->content)) { + $this->consumeNewLine(); + } $message = ''; if ($this->expects(' ')) { @@ -72,4 +77,15 @@ protected function doParse() $this->log[] = $commit; } } + + protected function consumeUnsupportedLinesToNewLine() + { + // Consume any unsupported lines that may appear in the log output. For + // example, gitbutler headers or other custom metadata but this should + // work regardless of the content. + while (!$this->isFinished() && substr($this->content, $this->cursor, 1) !== "\n") { + $this->consumeTo("\n"); + $this->consumeNewLine(); + } + } } diff --git a/src/Gitonomy/Git/Parser/ParserBase.php b/src/Gitonomy/Git/Parser/ParserBase.php index 94155ce1..45a80020 100644 --- a/src/Gitonomy/Git/Parser/ParserBase.php +++ b/src/Gitonomy/Git/Parser/ParserBase.php @@ -25,7 +25,7 @@ abstract protected function doParse(); public function parse($content) { $this->cursor = 0; - $this->content = $content; + $this->content = $content ?? ''; $this->length = strlen($this->content); $this->doParse(); @@ -59,7 +59,7 @@ protected function expects($expected) protected function consumeShortHash() { - if (!preg_match('/([A-Za-z0-9]{7,40})/A', $this->content, $vars, null, $this->cursor)) { + if (!preg_match('/([A-Za-z0-9]{7,40})/A', $this->content, $vars, 0, $this->cursor)) { throw new RuntimeException('No short hash found: '.substr($this->content, $this->cursor, 7)); } @@ -70,7 +70,7 @@ protected function consumeShortHash() protected function consumeHash() { - if (!preg_match('/([A-Za-z0-9]{40})/A', $this->content, $vars, null, $this->cursor)) { + if (!preg_match('/([A-Za-z0-9]{40})/A', $this->content, $vars, 0, $this->cursor)) { throw new RuntimeException('No hash found: '.substr($this->content, $this->cursor, 40)); } @@ -81,8 +81,8 @@ protected function consumeHash() protected function consumeRegexp($regexp) { - if (!preg_match($regexp.'A', $this->content, $vars, null, $this->cursor)) { - throw new RuntimeException('No match for regexp '.$regexp.' Upcoming: '.substr($this->content, $this->cursor, 30)); + if (!preg_match($regexp.'A', $this->content, $vars, 0, $this->cursor)) { + throw new RuntimeException('No match for regexp '.$regexp.' Upcoming: '.substr($this->content, $this->cursor, 500)); } $this->cursor += strlen($vars[0]); @@ -136,4 +136,18 @@ protected function consumeGPGSignature() return $this->consumeTo("\n\n"); } + + protected function consumeMergeTag() + { + $expected = "\nmergetag "; + $length = strlen($expected); + $actual = substr($this->content, $this->cursor, $length); + if ($actual != $expected) { + return ''; + } + $this->cursor += $length; + + $this->consumeTo('-----END PGP SIGNATURE-----'); + $this->consume('-----END PGP SIGNATURE-----'); + } } diff --git a/src/Gitonomy/Git/Parser/TagParser.php b/src/Gitonomy/Git/Parser/TagParser.php index 659be18a..1a1c084c 100644 --- a/src/Gitonomy/Git/Parser/TagParser.php +++ b/src/Gitonomy/Git/Parser/TagParser.php @@ -40,8 +40,8 @@ protected function doParse() $this->consumeNewLine(); $this->consume('tagger '); - list($this->taggerName, $this->taggerEmail, $this->taggerDate) = $this->consumeNameEmailDate(); - $this->taggerDate = $this->parseDate($this->taggerDate); + list($this->taggerName, $this->taggerEmail, $taggerDate) = $this->consumeNameEmailDate(); + $this->taggerDate = $this->parseDate($taggerDate); $this->consumeNewLine(); $this->consumeNewLine(); diff --git a/src/Gitonomy/Git/PushReference.php b/src/Gitonomy/Git/PushReference.php index 3911bd2e..dea10823 100644 --- a/src/Gitonomy/Git/PushReference.php +++ b/src/Gitonomy/Git/PushReference.php @@ -24,6 +24,10 @@ class PushReference { const ZERO = '0000000000000000000000000000000000000000'; + /** + * @var Repository + */ + protected $repository; /** * @var string */ @@ -86,7 +90,7 @@ public function getAfter() } /** - * @return array + * @return Log */ public function getLog($excludes = []) { @@ -98,6 +102,9 @@ public function getLog($excludes = []) )); } + /** + * @return string + */ public function getRevision() { if ($this->isDelete()) { diff --git a/src/Gitonomy/Git/Reference.php b/src/Gitonomy/Git/Reference.php index 23f1cb75..96b8fa12 100644 --- a/src/Gitonomy/Git/Reference.php +++ b/src/Gitonomy/Git/Reference.php @@ -12,6 +12,9 @@ namespace Gitonomy\Git; +use Gitonomy\Git\Exception\ProcessException; +use Gitonomy\Git\Exception\ReferenceNotFoundException; + /** * Reference in a Git repository. * @@ -29,16 +32,25 @@ public function __construct(Repository $repository, $revision, $commitHash = nul $this->commitHash = $commitHash; } + /** + * @return string + */ public function getFullname() { return $this->revision; } + /** + * @return void + */ public function delete() { $this->repository->getReferences()->delete($this->getFullname()); } + /** + * @return string + */ public function getCommitHash() { if (null !== $this->commitHash) { @@ -55,15 +67,16 @@ public function getCommitHash() } /** - * Returns the commit associated to the reference. - * - * @return Commit + * @return Commit Commit associated to the reference. */ public function getCommit() { return $this->repository->getCommit($this->getCommitHash()); } + /** + * @return Commit + */ public function getLastModification($path = null) { return $this->getCommit()->getLastModification($path); diff --git a/src/Gitonomy/Git/Reference/Branch.php b/src/Gitonomy/Git/Reference/Branch.php index 0f263a76..0d27fb88 100644 --- a/src/Gitonomy/Git/Reference/Branch.php +++ b/src/Gitonomy/Git/Reference/Branch.php @@ -12,8 +12,10 @@ namespace Gitonomy\Git\Reference; +use Gitonomy\Git\Exception\ProcessException; use Gitonomy\Git\Exception\RuntimeException; use Gitonomy\Git\Reference; +use Gitonomy\Git\Util\StringHelper; /** * Representation of a branch reference. @@ -53,6 +55,49 @@ public function isLocal() return $this->local; } + /** + * Check if this branch is merged to a destination branch + * Optionally, check only with remote branches. + * + * @param string $destinationBranchName + * @param bool $compareOnlyWithRemote + * + * @return null|bool + */ + public function isMergedTo($destinationBranchName = 'master', $compareOnlyWithRemote = false) + { + $arguments = ['-a']; + + if ($compareOnlyWithRemote) { + $arguments = ['-r']; + } + + $arguments[] = '--merged'; + $arguments[] = $destinationBranchName; + + try { + $result = $this->repository->run('branch', $arguments); + } catch (ProcessException $e) { + throw new RuntimeException( + sprintf('Cannot determine if merged to the branch "%s"', $destinationBranchName), + $e->getCode(), + $e + ); + } + + if (!$result) { + return false; + } + + $output = explode("\n", trim(str_replace(['*', 'remotes/'], '', $result))); + $filtered_output = array_filter($output, static function ($v) { + return false === StringHelper::strpos($v, '->'); + }); + $trimmed_output = array_map('trim', $filtered_output); + + return in_array($this->getName(), $trimmed_output, true); + } + private function detectBranchType() { if (null === $this->local) { diff --git a/src/Gitonomy/Git/Reference/Tag.php b/src/Gitonomy/Git/Reference/Tag.php index f8461e1b..78c43b25 100644 --- a/src/Gitonomy/Git/Reference/Tag.php +++ b/src/Gitonomy/Git/Reference/Tag.php @@ -12,6 +12,7 @@ namespace Gitonomy\Git\Reference; +use Gitonomy\Git\Commit; use Gitonomy\Git\Exception\ProcessException; use Gitonomy\Git\Exception\RuntimeException; use Gitonomy\Git\Parser\ReferenceParser; @@ -26,6 +27,8 @@ */ class Tag extends Reference { + protected $data; + public function getName() { if (!preg_match('#^refs/tags/(.*)$#', $this->revision, $vars)) { @@ -64,8 +67,8 @@ public function getCommit() $parser = new ReferenceParser(); $parser->parse($output); - foreach ($parser->references as $row) { - list($commitHash, $fullname) = $row; + foreach ($parser->references as list($row)) { + $commitHash = $row; } return $this->repository->getCommit($commitHash); @@ -100,7 +103,7 @@ public function getTaggerEmail() /** * Returns the authoring date. * - * @return DateTime A time object + * @return \DateTime A time object */ public function getTaggerDate() { @@ -188,9 +191,9 @@ private function getData($name) array_shift($lines); array_pop($lines); - $data['bodyMessage'] = implode("\n", $lines); + $this->data['bodyMessage'] = implode("\n", $lines); - return $data['bodyMessage']; + return $this->data['bodyMessage']; } $parser = new TagParser(); diff --git a/src/Gitonomy/Git/ReferenceBag.php b/src/Gitonomy/Git/ReferenceBag.php index 57aec259..71ef9ae4 100644 --- a/src/Gitonomy/Git/ReferenceBag.php +++ b/src/Gitonomy/Git/ReferenceBag.php @@ -29,7 +29,7 @@ class ReferenceBag implements \Countable, \IteratorAggregate /** * Repository object. * - * @var Gitonomy\Git\Repository + * @var Repository */ protected $repository; @@ -43,14 +43,14 @@ class ReferenceBag implements \Countable, \IteratorAggregate /** * List with all tags. * - * @var array + * @var Tag[] */ protected $tags; /** * List with all branches. * - * @var array + * @var Branch[] */ protected $branches; @@ -64,7 +64,7 @@ class ReferenceBag implements \Countable, \IteratorAggregate /** * Constructor. * - * @param Gitonomy\Git\Repository $repository The repository + * @param Repository $repository The repository */ public function __construct($repository) { @@ -92,6 +92,9 @@ public function get($fullname) return $this->references[$fullname]; } + /** + * @return bool + */ public function has($fullname) { $this->initialize(); @@ -99,6 +102,9 @@ public function has($fullname) return isset($this->references[$fullname]); } + /** + * @return Reference + */ public function update(Reference $reference) { $fullname = $reference->getFullname(); @@ -111,6 +117,9 @@ public function update(Reference $reference) return $reference; } + /** + * @return Reference + */ public function createBranch($name, $commitHash) { $branch = new Branch($this->repository, 'refs/heads/'.$name, $commitHash); @@ -118,6 +127,9 @@ public function createBranch($name, $commitHash) return $this->update($branch); } + /** + * @return Reference + */ public function createTag($name, $commitHash) { $tag = new Tag($this->repository, 'refs/tags/'.$name, $commitHash); @@ -125,6 +137,9 @@ public function createTag($name, $commitHash) return $this->update($tag); } + /** + * @return void + */ public function delete($fullname) { $this->repository->run('update-ref', ['-d', $fullname]); @@ -132,6 +147,9 @@ public function delete($fullname) unset($this->references[$fullname]); } + /** + * @return bool + */ public function hasBranches() { $this->initialize(); @@ -154,6 +172,9 @@ public function hasTag($name) return $this->has('refs/tags/'.$name); } + /** + * @return Branch + */ public function getFirstBranch() { $this->initialize(); @@ -163,7 +184,7 @@ public function getFirstBranch() } /** - * @return array An array of Tag objects + * @return Tag[] An array of Tag objects */ public function resolveTags($hash) { @@ -184,7 +205,7 @@ public function resolveTags($hash) } /** - * @return array An array of Branch objects + * @return Branch[] An array of Branch objects */ public function resolveBranches($hash) { @@ -205,7 +226,7 @@ public function resolveBranches($hash) } /** - * @return array An array of references + * @return Reference[] An array of references */ public function resolve($hash) { @@ -226,9 +247,7 @@ public function resolve($hash) } /** - * Returns all tags. - * - * @return array + * @return Tag[] All tags. */ public function getTags() { @@ -238,9 +257,7 @@ public function getTags() } /** - * Returns all branches. - * - * @return array + * @return Branch[] All branches. */ public function getBranches() { @@ -257,9 +274,7 @@ public function getBranches() } /** - * Returns all locales branches. - * - * @return array + * @return Branch[] All local branches. */ public function getLocalBranches() { @@ -274,9 +289,7 @@ public function getLocalBranches() } /** - * Returns all remote branches. - * - * @return array + * @return Branch[] All remote branches. */ public function getRemoteBranches() { @@ -341,11 +354,7 @@ protected function initialize() $parser = new Parser\ReferenceParser(); $output = $this->repository->run('show-ref'); } catch (RuntimeException $e) { - $output = $e->getOutput(); - $error = $e->getErrorOutput(); - if ($error) { - throw new RuntimeException('Error while getting list of references: '.$error); - } + return; } $parser->parse($output); @@ -375,6 +384,7 @@ protected function initialize() * * @see Countable */ + #[\ReturnTypeWillChange] public function count() { $this->initialize(); @@ -385,6 +395,7 @@ public function count() /** * @see IteratorAggregate */ + #[\ReturnTypeWillChange] public function getIterator() { $this->initialize(); diff --git a/src/Gitonomy/Git/Repository.php b/src/Gitonomy/Git/Repository.php index 3920616b..296496ae 100644 --- a/src/Gitonomy/Git/Repository.php +++ b/src/Gitonomy/Git/Repository.php @@ -376,6 +376,9 @@ public function getBlob($hash) return $this->objects[$hash]; } + /** + * @return Blame + */ public function getBlame($revision, $file, $lineRange = null) { if (is_string($revision)) { @@ -413,7 +416,7 @@ public function getDiff($revisions) $revisions = new RevisionList($this, $revisions); } - $args = array_merge(['-r', '-p', '-m', '-M', '--no-commit-id', '--full-index'], $revisions->getAsTextArray()); + $args = array_merge(['-r', '-p', '--raw', '-m', '-M', '--no-commit-id', '--full-index'], $revisions->getAsTextArray()); $diff = Diff::parse($this->run('diff', $args)); $diff->setRepository($this); diff --git a/src/Gitonomy/Git/RevisionList.php b/src/Gitonomy/Git/RevisionList.php index 760abb67..1c1a1dcd 100644 --- a/src/Gitonomy/Git/RevisionList.php +++ b/src/Gitonomy/Git/RevisionList.php @@ -49,16 +49,21 @@ public function __construct(Repository $repository, $revisions) $this->revisions = $revisions; } + /** + * @return Revision[] + */ public function getAll() { return $this->revisions; } + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->revisions); } + #[\ReturnTypeWillChange] public function count() { return count($this->revisions); diff --git a/src/Gitonomy/Git/Tree.php b/src/Gitonomy/Git/Tree.php index d3179837..7830cfa4 100644 --- a/src/Gitonomy/Git/Tree.php +++ b/src/Gitonomy/Git/Tree.php @@ -24,6 +24,7 @@ class Tree protected $hash; protected $isInitialized = false; protected $entries; + protected $entriesByType; public function __construct(Repository $repository, $hash) { @@ -47,31 +48,68 @@ protected function initialize() $parser->parse($output); $this->entries = []; + $this->entriesByType = [ + 'blob' => [], + 'tree' => [], + 'commit' => [], + ]; foreach ($parser->entries as $entry) { list($mode, $type, $hash, $name) = $entry; if ($type == 'blob') { - $this->entries[$name] = [$mode, $this->repository->getBlob($hash)]; + $treeEntry = [$mode, $this->repository->getBlob($hash)]; } elseif ($type == 'tree') { - $this->entries[$name] = [$mode, $this->repository->getTree($hash)]; + $treeEntry = [$mode, $this->repository->getTree($hash)]; } else { - $this->entries[$name] = [$mode, new CommitReference($hash)]; + $treeEntry = [$mode, new CommitReference($hash)]; } + $this->entries[$name] = $treeEntry; + $this->entriesByType[$type][$name] = $treeEntry; } $this->isInitialized = true; } /** - * @return array An associative array name => $object + * @return array An associative array name => $object */ - public function getEntries() + public function getEntries(): array { $this->initialize(); return $this->entries; } + /** + * @return array An associative array of name => [mode, commit reference] + */ + public function getCommitReferenceEntries(): array + { + $this->initialize(); + + return $this->entriesByType['commit']; + } + + /** + * @return array An associative array of name => [mode, tree] + */ + public function getTreeEntries(): array + { + $this->initialize(); + + return $this->entriesByType['tree']; + } + + /** + * @return array An associative array of name => [mode, blob] + */ + public function getBlobEntries(): array + { + $this->initialize(); + + return $this->entriesByType['blob']; + } + public function getEntry($name) { $this->initialize(); @@ -96,7 +134,7 @@ public function resolvePath($path) foreach ($segments as $segment) { if ($element instanceof self) { $element = $element->getEntry($segment); - } elseif ($entry instanceof Blob) { + } elseif ($element instanceof Blob) { throw new InvalidArgumentException('Unresolvable path'); } else { throw new UnexpectedValueException('Unknow type of element'); diff --git a/src/Gitonomy/Git/WorkingCopy.php b/src/Gitonomy/Git/WorkingCopy.php index 057253b5..a94fbbb9 100644 --- a/src/Gitonomy/Git/WorkingCopy.php +++ b/src/Gitonomy/Git/WorkingCopy.php @@ -35,11 +35,6 @@ public function __construct(Repository $repository) } } - public function getStatus() - { - return WorkingStatus::parseOutput(); - } - public function getUntrackedFiles() { $lines = explode("\0", $this->run('status', ['--porcelain', '--untracked-files=all', '-z'])); @@ -55,7 +50,7 @@ public function getUntrackedFiles() public function getDiffPending() { - $diff = Diff::parse($this->run('diff', ['-r', '-p', '-m', '-M', '--full-index'])); + $diff = Diff::parse($this->run('diff', ['-r', '-p', '--raw', '-m', '-M', '--full-index'])); $diff->setRepository($this->repository); return $diff; @@ -63,7 +58,7 @@ public function getDiffPending() public function getDiffStaged() { - $diff = Diff::parse($this->run('diff', ['-r', '-p', '-m', '-M', '--full-index', '--staged'])); + $diff = Diff::parse($this->run('diff', ['-r', '-p', '--raw', '-m', '-M', '--full-index', '--staged'])); $diff->setRepository($this->repository); return $diff; diff --git a/tests/Gitonomy/Git/Tests/AbstractTest.php b/tests/Gitonomy/Git/Tests/AbstractTest.php index 13f40da9..0b9a51b6 100644 --- a/tests/Gitonomy/Git/Tests/AbstractTest.php +++ b/tests/Gitonomy/Git/Tests/AbstractTest.php @@ -20,6 +20,7 @@ abstract class AbstractTest extends TestCase { const REPOSITORY_URL = 'https://github.com/gitonomy/foobar.git'; + const NO_MESSAGE_COMMIT = '011cd0c1625190d2959ee9a8f9f822006d94b661'; const LONGFILE_COMMIT = '4f17752acc9b7c54ba679291bf24cb7d354f0f4f'; const BEFORE_LONGFILE_COMMIT = 'e0ec50e2af75fa35485513f60b2e658e245227e9'; const LONGMESSAGE_COMMIT = '3febd664b6886344a9b32d70657687ea4b1b4fab'; @@ -117,7 +118,7 @@ public static function createTempDir() * * @param string $dir directory to delete */ - public static function deleteDir($dir) + protected static function deleteDir($dir) { $iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS); $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); diff --git a/tests/Gitonomy/Git/Tests/AdminTest.php b/tests/Gitonomy/Git/Tests/AdminTest.php index 7234bdff..0f7460fa 100644 --- a/tests/Gitonomy/Git/Tests/AdminTest.php +++ b/tests/Gitonomy/Git/Tests/AdminTest.php @@ -21,14 +21,20 @@ class AdminTest extends AbstractTest { private $tmpDir; - protected function setUp() + /** + * @before + */ + public function setUpTmpDir() { $this->tmpDir = self::createTempDir(); } - protected function tearDown() + /** + * @after + */ + public function tearDownTmpDir() { - $this->deleteDir(self::createTempDir()); + self::deleteDir(self::createTempDir()); } public function testBare() @@ -149,6 +155,24 @@ public function testCheckInvalidRepository() $this->assertFalse(Admin::isValidRepository($url)); } + /** + * @dataProvider provideFoobar + */ + public function testCheckValidRepositoryAndBranch($repository) + { + $url = $repository->getGitDir(); + $this->assertTrue(Admin::isValidRepositoryAndBranch($url, 'master')); + } + + /** + * @dataProvider provideFoobar + */ + public function testCheckInvalidRepositoryAndBranch($repository) + { + $url = $repository->getGitDir(); + $this->assertFalse(Admin::isValidRepositoryAndBranch($url, 'invalid-branch-name')); + } + public function testExistingFile() { $this->expectException(RuntimeException::class); diff --git a/tests/Gitonomy/Git/Tests/BlobTest.php b/tests/Gitonomy/Git/Tests/BlobTest.php index 36b1d58d..431a0a46 100644 --- a/tests/Gitonomy/Git/Tests/BlobTest.php +++ b/tests/Gitonomy/Git/Tests/BlobTest.php @@ -23,6 +23,11 @@ public function getReadmeBlob($repository) return $repository->getCommit(self::LONGFILE_COMMIT)->getTree()->resolvePath('README.md'); } + public function getImageBlob($repository) + { + return $repository->getCommit(self::LONGFILE_COMMIT)->getTree()->resolvePath('image.jpg'); + } + /** * @dataProvider provideFoobar */ @@ -30,7 +35,11 @@ public function testGetContent($repository) { $blob = $this->getReadmeBlob($repository); - $this->assertContains(self::README_FRAGMENT, $blob->getContent()); + if (method_exists($this, 'assertStringContainsString')) { + $this->assertStringContainsString(self::README_FRAGMENT, $blob->getContent()); + } else { + $this->assertContains(self::README_FRAGMENT, $blob->getContent()); + } } /** @@ -50,7 +59,12 @@ public function testNotExisting($repository) public function testGetMimetype($repository) { $blob = $this->getReadmeBlob($repository); - $this->assertRegexp('#text/plain#', $blob->getMimetype()); + + if (method_exists($this, 'assertMatchesRegularExpression')) { + $this->assertMatchesRegularExpression('#text/plain#', $blob->getMimetype()); + } else { + $this->assertRegExp('#text/plain#', $blob->getMimetype()); + } } /** @@ -58,8 +72,10 @@ public function testGetMimetype($repository) */ public function testIsText($repository) { - $blob = $this->getReadmeBlob($repository); - $this->assertTrue($blob->isText()); + $readmeBlob = $this->getReadmeBlob($repository); + $this->assertTrue($readmeBlob->isText()); + $imageBlob = $this->getImageBlob($repository); + $this->assertFalse($imageBlob->isText()); } /** @@ -67,7 +83,9 @@ public function testIsText($repository) */ public function testIsBinary($repository) { - $blob = $this->getReadmeBlob($repository); - $this->assertFalse($blob->isBinary()); + $readmeBlob = $this->getReadmeBlob($repository); + $this->assertFalse($readmeBlob->isBinary()); + $imageBlob = $this->getImageBlob($repository); + $this->assertTrue($imageBlob->isBinary()); } } diff --git a/tests/Gitonomy/Git/Tests/CommitTest.php b/tests/Gitonomy/Git/Tests/CommitTest.php index 8dca9c40..7d094648 100644 --- a/tests/Gitonomy/Git/Tests/CommitTest.php +++ b/tests/Gitonomy/Git/Tests/CommitTest.php @@ -16,6 +16,7 @@ use Gitonomy\Git\Diff\Diff; use Gitonomy\Git\Exception\InvalidArgumentException; use Gitonomy\Git\Exception\ReferenceNotFoundException; +use Gitonomy\Git\Repository; use Gitonomy\Git\Tree; class CommitTest extends AbstractTest @@ -189,6 +190,31 @@ public function testGetMessage($repository) $this->assertEquals('add a long file'."\n", $commit->getMessage()); } + /** + * @dataProvider provideFoobar + * + * @param $repository Repository + */ + public function testGetEmptyMessage($repository) + { + $commit = $repository->getCommit(self::NO_MESSAGE_COMMIT); + + $this->assertEquals('', $commit->getMessage()); + } + + /** + * @dataProvider provideFoobar + * + * @param $repository Repository + */ + public function testGetEmptyMessageFromLog($repository) + { + $commit = $repository->getCommit(self::NO_MESSAGE_COMMIT); + $commitMessageFromLog = $commit->getLog()->getCommits()[0]->getMessage(); + + $this->assertEquals('', $commitMessageFromLog); + } + /** * This test ensures that GPG signed commits does not break the reading of a commit * message. diff --git a/tests/Gitonomy/Git/Tests/DiffTest.php b/tests/Gitonomy/Git/Tests/DiffTest.php index ec0eebe7..e51fc7fc 100644 --- a/tests/Gitonomy/Git/Tests/DiffTest.php +++ b/tests/Gitonomy/Git/Tests/DiffTest.php @@ -13,6 +13,8 @@ namespace Gitonomy\Git\Tests; use Gitonomy\Git\Diff\Diff; +use Gitonomy\Git\Diff\File; +use Gitonomy\Git\Repository; class DiffTest extends AbstractTest { @@ -20,6 +22,7 @@ class DiffTest extends AbstractTest const CREATE_COMMIT = 'e6fa3c792facc06faa049a6938c84c411954deb5'; const RENAME_COMMIT = '6640e0ef31518054847a1876328e26ee64083e0a'; const CHANGEMODE_COMMIT = '93da965f58170f13017477b9a608657e87e23230'; + const FILE_WITH_UMLAUTS_COMMIT = '8defb9217692dc1f4c18e05e343ca91cf5047702'; /** * @dataProvider provideFoobar @@ -134,10 +137,148 @@ public function testDiffRangeParse($repository) $changes = $files[0]->getChanges(); - $this->assertEquals(0, $changes[0]->getRangeOldStart()); - $this->assertEquals(0, $changes[0]->getRangeOldCount()); + $this->assertSame(0, $changes[0]->getRangeOldStart()); + $this->assertSame(0, $changes[0]->getRangeOldCount()); - $this->assertEquals(1, $changes[0]->getRangeNewStart()); - $this->assertEquals(0, $changes[0]->getRangeNewCount()); + $this->assertSame(1, $changes[0]->getRangeNewStart()); + $this->assertSame(1, $changes[0]->getRangeNewCount()); + } + + /** + * @dataProvider provideFoobar + */ + public function testWorksWithUmlauts($repository) + { + $files = $repository->getCommit(self::FILE_WITH_UMLAUTS_COMMIT)->getDiff()->getFiles(); + $this->assertSame('file_with_umlauts_\303\244\303\266\303\274', $files[0]->getNewName()); + } + + public function testDeleteFileWithoutRaw() + { + $deprecationCalled = false; + $self = $this; + set_error_handler(function (int $errno, string $errstr) use ($self, &$deprecationCalled): void { + $deprecationCalled = true; + $self->assertSame('Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.', $errstr); + }, E_USER_DEPRECATED); + + $diff = Diff::parse(<<<'DIFF' + diff --git a/test b/test + deleted file mode 100644 + index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 + + DIFF); + $firstFile = $diff->getFiles()[0]; + + restore_exception_handler(); + + $this->assertTrue($deprecationCalled); + $this->assertFalse($firstFile->isCreation()); + // TODO: Enable after #226 is merged + //$this->assertTrue($firstFile->isDeletion()); + //$this->assertFalse($firstFile->isChangeMode()); + $this->assertSame('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', $firstFile->getOldIndex()); + $this->assertNull($firstFile->getNewIndex()); + } + + public function testModeChangeFileWithoutRaw() + { + $deprecationCalled = false; + $self = $this; + set_error_handler(function (int $errno, string $errstr) use ($self, &$deprecationCalled): void { + $deprecationCalled = true; + $self->assertSame('Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.', $errstr); + }, E_USER_DEPRECATED); + + $diff = Diff::parse(<<<'DIFF' + diff --git a/a.out b/a.out + old mode 100755 + new mode 100644 + + DIFF); + $firstFile = $diff->getFiles()[0]; + + restore_exception_handler(); + + $this->assertTrue($deprecationCalled); + $this->assertFalse($firstFile->isCreation()); + $this->assertFalse($firstFile->isDeletion()); + $this->assertTrue($firstFile->isChangeMode()); + $this->assertSame('', $firstFile->getOldIndex()); + $this->assertSame('', $firstFile->getNewIndex()); + } + + public function testModeChangeFileWithRaw() + { + $deprecationCalled = false; + set_error_handler(function (int $errno, string $errstr) use (&$deprecationCalled): void { + $deprecationCalled = true; + }, E_USER_DEPRECATED); + + $diff = Diff::parse(<<<'DIFF' + :100755 100644 d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa81 d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa81 M a.out + + diff --git a/a.out b/a.out + old mode 100755 + new mode 100644 + + DIFF); + $firstFile = $diff->getFiles()[0]; + + restore_exception_handler(); + + $this->assertFalse($deprecationCalled); + $this->assertFalse($firstFile->isCreation()); + $this->assertFalse($firstFile->isDeletion()); + $this->assertTrue($firstFile->isChangeMode()); + $this->assertSame('d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa81', $firstFile->getOldIndex()); + $this->assertSame('d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa81', $firstFile->getNewIndex()); + } + + public function testThrowErrorOnBlobGetWithoutIndex() + { + $repository = $this->getMockBuilder(Repository::class)->disableOriginalConstructor()->getMock(); + $file = new File('oldName', 'newName', '100755', '100644', '', '', false); + $file->setRepository($repository); + + try { + $file->getOldBlob(); + } catch(\RuntimeException $exception) { + $this->assertSame('Index is missing to return Blob object.', $exception->getMessage()); + } + + try { + $file->getNewBlob(); + } catch(\RuntimeException $exception) { + $this->assertSame('Index is missing to return Blob object.', $exception->getMessage()); + } + + $this->assertFalse($file->isCreation()); + $this->assertFalse($file->isDeletion()); + $this->assertTrue($file->isChangeMode()); + $this->assertSame('', $file->getOldIndex()); + $this->assertSame('', $file->getNewIndex()); + } + + public function testEmptyNewFile() + { + $diff = Diff::parse("diff --git a/test b/test\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n"); + $firstFile = $diff->getFiles()[0]; + + $this->assertTrue($firstFile->isCreation()); + $this->assertFalse($firstFile->isDeletion()); + $this->assertSame('test', $firstFile->getNewName()); + $this->assertNull($firstFile->getOldName()); + } + + public function testEmptyOldFile() + { + $diff = Diff::parse("diff --git a/test b/test\ndeleted file mode 100644\nindex e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000\n"); + $firstFile = $diff->getFiles()[0]; + + $this->assertFalse($firstFile->isCreation()); + $this->assertTrue($firstFile->isDeletion()); + $this->assertNull($firstFile->getNewName()); + $this->assertSame('test', $firstFile->getOldName()); } } diff --git a/tests/Gitonomy/Git/Tests/HooksTest.php b/tests/Gitonomy/Git/Tests/HooksTest.php index 597a0b4b..1b09fe55 100644 --- a/tests/Gitonomy/Git/Tests/HooksTest.php +++ b/tests/Gitonomy/Git/Tests/HooksTest.php @@ -19,7 +19,10 @@ class HooksTest extends AbstractTest { private static $symlinkOnWindows = null; - public static function setUpBeforeClass() + /** + * @beforeClass + */ + public static function setUpWindows() { if (defined('PHP_WINDOWS_VERSION_MAJOR')) { self::$symlinkOnWindows = true; @@ -52,6 +55,7 @@ public function assertHasHook($repository, $hook) $file = $this->hookPath($repository, $hook); $this->assertTrue($repository->getHooks()->has($hook), "hook $hook in repository"); + $this->assertFileExists($file, "Hook $hook is present"); } @@ -60,7 +64,12 @@ public function assertNoHook($repository, $hook) $file = $this->hookPath($repository, $hook); $this->assertFalse($repository->getHooks()->has($hook), "No hook $hook in repository"); - $this->assertFileNotExists($file, "Hook $hook is not present"); + + if (method_exists($this, 'assertFileDoesNotExist')) { + $this->assertFileDoesNotExist($file, "Hook $hook is not present"); + } else { + $this->assertFileNotExists($file, "Hook $hook is not present"); + } } /** @@ -104,7 +113,12 @@ public function testSymlink($repository) $repository->getHooks()->setSymlink('foo', $file); $this->assertTrue(is_link($this->hookPath($repository, 'foo')), 'foo hook is a symlink'); - $this->assertEquals(str_replace('\\', '/', $file), str_replace('\\', '/', readlink($this->hookPath($repository, 'foo'))), 'target of symlink is correct'); + + $this->assertEquals( + str_replace('\\', '/', $file), + str_replace('\\', '/', readlink($this->hookPath($repository, 'foo'))), + 'target of symlink is correct' + ); } /** @@ -147,6 +161,7 @@ public function testSet_Existing_ThrowsLogicException($repository) $repository->getHooks()->set('foo', 'bar'); $this->expectException(LogicException::class); + $repository->getHooks()->set('foo', 'bar'); } @@ -159,7 +174,12 @@ public function testRemove($repository) touch($file); $repository->getHooks()->remove('foo'); - $this->assertFileNotExists($file); + + if (method_exists($this, 'assertFileDoesNotExist')) { + $this->assertFileDoesNotExist($file); + } else { + $this->assertFileNotExists($file); + } } /** diff --git a/tests/Gitonomy/Git/Tests/LogTest.php b/tests/Gitonomy/Git/Tests/LogTest.php index cdf15630..a02184dd 100644 --- a/tests/Gitonomy/Git/Tests/LogTest.php +++ b/tests/Gitonomy/Git/Tests/LogTest.php @@ -12,6 +12,8 @@ namespace Gitonomy\Git\Tests; +use Gitonomy\Git\Parser\LogParser; + class LogTest extends AbstractTest { /** @@ -76,4 +78,59 @@ public function testIterable($repository) } } } + + public function testFirstMessageEmpty() + { + $repository = $this->createEmptyRepository(false); + $repository->run('config', ['--local', 'user.name', '"Unit Test"']); + $repository->run('config', ['--local', 'user.email', '"unit_test@unit-test.com"']); + + // Edge case: first commit lacks a message. + file_put_contents($repository->getWorkingDir().'/file', 'foo'); + $repository->run('add', ['.']); + $repository->run('commit', ['--allow-empty-message', '--no-edit']); + + $commits = $repository->getLog()->getCommits(); + $this->assertCount(1, $commits); + } + + public function testParsesCommitsWithAndWithoutGitButlerHeaders(): void + { + $logContent = <<<'EOT' + commit 1111111111111111111111111111111111111111 + tree abcdefabcdefabcdefabcdefabcdefabcdefabcd + author John Doe 1620000000 +0000 + committer John Doe 1620000000 +0000 + + First commit message + + commit 2222222222222222222222222222222222222222 + tree abcdefabcdefabcdefabcdefabcdefabcdefabcd + parent 1111111111111111111111111111111111111111 + author Jane Smith 1620003600 +0000 + committer Jane Smith 1620003600 +0000 + gitbutler-headers-version: 2 + gitbutler-change-id: a7bd485c-bae6-45b2-910f-163c78aace81 + + Commit with GitButler headers + + commit 3333333333333333333333333333333333333333 + tree abcdefabcdefabcdefabcdefabcdefabcdefabcd + author John Doe 1620007200 +0000 + committer Jane Smith 1620007200 +0000 + + Another commit without GitButler headers + + EOT; + + $parser = new LogParser(); + $parser->parse($logContent); + + $log = $parser->log; + $this->assertCount(3, $log); + + $this->assertEquals("First commit message\n", $log[0]['message']); + $this->assertEquals("Commit with GitButler headers\n", $log[1]['message']); + $this->assertEquals("Another commit without GitButler headers\n", $log[2]['message']); + } } diff --git a/tests/Gitonomy/Git/Tests/ReferenceBagTest.php b/tests/Gitonomy/Git/Tests/ReferenceBagTest.php index 35756bf0..f9b9c82c 100644 --- a/tests/Gitonomy/Git/Tests/ReferenceBagTest.php +++ b/tests/Gitonomy/Git/Tests/ReferenceBagTest.php @@ -44,4 +44,13 @@ public function testUnknownReference(Repository $repository) $this->assertArrayNotHasKey('refs/pull/1/head', $refs); $this->assertArrayNotHasKey('refs/notes/gtm-data', $refs); } + + /** + * @dataProvider provideEmpty + */ + public function testEmptyRepo(Repository $repository) + { + $refs = $repository->getReferences()->getAll(); + $this->assertSame([], $refs); + } } diff --git a/tests/Gitonomy/Git/Tests/ReferenceTest.php b/tests/Gitonomy/Git/Tests/ReferenceTest.php index d24a5e64..0f975577 100644 --- a/tests/Gitonomy/Git/Tests/ReferenceTest.php +++ b/tests/Gitonomy/Git/Tests/ReferenceTest.php @@ -209,4 +209,32 @@ public function testCreateAndDeleteBranch($repository) $branch->delete(); $this->assertFalse($references->hasBranch('foobar'), 'Branch foobar removed'); } + + /** + * @dataProvider provideFoobar + */ + public function testIsBranchMergedToMaster() + { + $repository = self::createFoobarRepository(false); + + $repository->run('config', ['--local', 'user.name', '"Unit Test"']); + $repository->run('config', ['--local', 'user.email', '"unit_test@unit-test.com"']); + + $master = $repository->getReferences()->getBranch('master'); + $references = $repository->getReferences(); + + $branch = $references->createBranch('foobar-new', $master->getCommit()->getHash()); + + $this->assertTrue($branch->isMergedTo('master')); + + $wc = $repository->getWorkingCopy(); + $wc->checkout('foobar-new'); + + $file = $repository->getWorkingDir().'/foobar-test.txt'; + file_put_contents($file, 'test'); + $repository->run('add', [$file]); + $repository->run('commit', ['-m', 'foobar-test.txt updated']); + + $this->assertFalse($branch->isMergedTo('master')); + } } diff --git a/tests/Gitonomy/Git/Tests/RepositoryTest.php b/tests/Gitonomy/Git/Tests/RepositoryTest.php index 2cafdd22..fc5c7deb 100644 --- a/tests/Gitonomy/Git/Tests/RepositoryTest.php +++ b/tests/Gitonomy/Git/Tests/RepositoryTest.php @@ -15,18 +15,26 @@ use Gitonomy\Git\Blob; use Gitonomy\Git\Exception\RuntimeException; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; class RepositoryTest extends AbstractTest { + use ProphecyTrait; + /** * @dataProvider provideFoobar */ - public function testGetBlob_WithExisting_Works($repository) + public function testGetBlobWithExistingWorks($repository) { $blob = $repository->getCommit(self::LONGFILE_COMMIT)->getTree()->resolvePath('README.md'); $this->assertInstanceOf(Blob::class, $blob, 'getBlob() returns a Blob object'); - $this->assertContains('Foo Bar project', $blob->getContent(), 'file is correct'); + + if (method_exists($this, 'assertStringContainsString')) { + $this->assertStringContainsString('Foo Bar project', $blob->getContent(), 'file is correct'); + } else { + $this->assertContains('Foo Bar project', $blob->getContent(), 'file is correct'); + } } /** @@ -35,8 +43,8 @@ public function testGetBlob_WithExisting_Works($repository) public function testGetSize($repository) { $size = $repository->getSize(); - $this->assertGreaterThanOrEqual(69, $size, 'Repository is at least 69KB'); - $this->assertLessThan(80, $size, 'Repository is less than 80KB'); + $this->assertGreaterThanOrEqual(57, $size, 'Repository is at least 57KB'); + $this->assertLessThan(84, $size, 'Repository is less than 84KB'); } public function testIsBare() diff --git a/tests/Gitonomy/Git/Tests/TreeTest.php b/tests/Gitonomy/Git/Tests/TreeTest.php index 4c0476de..b7ba7d32 100644 --- a/tests/Gitonomy/Git/Tests/TreeTest.php +++ b/tests/Gitonomy/Git/Tests/TreeTest.php @@ -13,6 +13,7 @@ namespace Gitonomy\Git\Tests; use Gitonomy\Git\Blob; +use Gitonomy\Git\CommitReference; class TreeTest extends AbstractTest { @@ -21,7 +22,7 @@ class TreeTest extends AbstractTest /** * @dataProvider provideFooBar */ - public function testCase($repository) + public function testGetEntries($repository) { $tree = $repository->getCommit(self::LONGFILE_COMMIT)->getTree(); @@ -34,6 +35,44 @@ public function testCase($repository) $this->assertTrue($entries['README.md'][1] instanceof Blob, 'README.md is a Blob'); } + /** + * @dataProvider provideFooBar + */ + public function testGetCommitReferenceEntries($repository) + { + $tree = $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree(); + + $commits = $tree->getCommitReferenceEntries(); + + $this->assertNotEmpty($commits['barbaz'], 'barbaz is present'); + $this->assertTrue($commits['barbaz'][1] instanceof CommitReference, 'barbaz is a Commit'); + } + + /** + * @dataProvider provideFooBar + */ + public function testGetTreeEntries($repository) + { + $tree = $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree(); + + $trees = $tree->getTreeEntries(); + + $this->assertEmpty($trees); + } + + /** + * @dataProvider provideFooBar + */ + public function testGetBlobEntries($repository) + { + $tree = $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree(); + + $blobs = $tree->getBlobEntries(); + + $this->assertNotEmpty($blobs['README.md'], 'README.md is present'); + $this->assertTrue($blobs['README.md'][1] instanceof Blob, 'README.md is a blob'); + } + /** * @dataProvider provideFooBar */