diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..590c9593 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# CONTRIBUTOR COVENANT CODE OF CONDUCT + +## Our Pledge + +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 a positive environment for our +community include: + +* 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 include: + +* 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 email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +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. + +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 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 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. + +**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 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +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 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 new file mode 100644 index 00000000..d33db7e8 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# CONTRIBUTION GUIDELINES + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via pull requests on GitHub. Please review these guidelines before continuing. + +## Guidelines + +* 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 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 [Semantic Versioning](https://semver.org/). + +## Running Tests + +First, install the dependencies using [Composer](https://getcomposer.org/): + +```bash +$ composer install +``` + +Then run [PHPUnit](https://phpunit.de/): + +```bash +$ 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 [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/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..856fb026 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: "packagist/gitonomy/gitlib" diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..0c3e8a24 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,14 @@ +# SECURITY POLICY + +## Supported Versions + +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 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 abb9c75a..376a2cd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/vendor +/.phpunit.result.cache /composer.lock /phpunit.xml +/vendor diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 39c3779f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: php - -sudo: false - -php: - - 5.3 - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - hhvm - -install: - - composer install --prefer-source - -script: phpunit diff --git a/README.md b/README.md index fb03092d..e0dcade7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,73 @@ -Git lib for Gitonomy -==================== +Gitlib for Gitonomy +=================== -[![Build Status](https://secure.travis-ci.org/gitonomy/gitlib.png)](https://travis-ci.org/gitonomy/gitlib) +[![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. +This library provides methods to access Git repository from PHP 5.6+. It makes shell calls, which makes it less performant than any solution. Anyway, it's convenient and don't need to build anything to use it. That's how we love it. -*Documentation*: http://gitonomy.com/doc/gitlib/master/ +Quick Start +----------- + +You can install gitlib using [Composer](https://getcomposer.org/). Simply require the version you need: + +```bash +$ composer require gitonomy/gitlib +``` + +or edit your `composer.json` file by hand: + +```json +{ + "require": { + "gitonomy/gitlib": "^1.3" + } +} +``` + +Example Usage +------------- + +```php +getReferences()->getBranches() as $branch) { + echo '- '.$branch->getName().PHP_EOL; +} + +$repository->run('fetch', ['--all']); +``` + +API Documentation +----------------- + ++ [Admin](doc/admin.md) ++ [Blame](doc/blame.md) ++ [Blob](doc/blob.md) ++ [Branch](doc/branch.md) ++ [Commit](doc/commit.md) ++ [Diff](doc/diff.md) ++ [Hooks](doc/hooks.md) ++ [Log](doc/log.md) ++ [References](doc/references.md) ++ [Repository](doc/repository.md) ++ [Revision](doc/revision.md) ++ [Tree](doc/tree.md) ++ [Working Copy](doc/workingcopy.md) + +For Enterprise +-------------- + +Available as part of the Tidelift Subscription + +The maintainers of `gitonomy/gitlib` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-gitonomy-gitlib?utm_source=packagist-gitonomy-gitlib&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) diff --git a/composer.json b/composer.json index 2d13c8eb..a3de0536 100644 --- a/composer.json +++ b/composer.json @@ -4,17 +4,26 @@ "license": "MIT", "authors": [ { - "name": "Alexandre Salomé", - "email": "alexandre.salome@gmail.com", - "homepage": "http://alexandre-salome.fr/" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" }, { - "name": "Julien DIDIER", + "name": "Julien Didier", "email": "genzo.wm@gmail.com", - "homepage": "http://www.jdidier.net/" + "homepage": "https://github.com/juliendidier" + }, + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info", + "homepage": "https://github.com/lyrixx" + }, + { + "name": "Alexandre Salomé", + "email": "alexandre.salome@gmail.com", + "homepage": "https://github.com/alexandresalome" } ], - "homepage": "http://gitonomy.com/", "autoload": { "psr-4": { "Gitonomy\\Git\\": "src/Gitonomy/Git/" @@ -26,17 +35,21 @@ } }, "require": { - "symfony/process": "^2.3|^3.0" + "php": "^8.0", + "ext-pcre": "*", + "symfony/polyfill-mbstring": "^1.7", + "symfony/process": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { + "ext-fileinfo": "*", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.20 || ^9.5.9", "psr/log": "^1.0" }, - "suggest": { - "psr/log": "Add some log" + "config": { + "preferred-install": "dist", + "sort-packages": true }, - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - } + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/doc/admin.md b/doc/admin.md new file mode 100644 index 00000000..d4776804 --- /dev/null +++ b/doc/admin.md @@ -0,0 +1,77 @@ +Create and access git repositories +================================== + +gitlib provides methods to initialize new repositories. + +Create a repository +------------------- + +To initialize a new repository, use method `Admin::init`. + +```php +// Initialize a bare repository +$repository = Gitonomy\Git\Admin::init('/path/to/repository'); + +// Initialize a non-bare repository +$repository = Gitonomy\Git\Admin::init('/path/to/repository', false); +``` + +Default behavior is to create a bare repository. If you want to +initialize a repository with a working copy,pass `false` as third +argument of Repository constructor. + +Cloning repositories +-------------------- + +You can clone a repository from an URL by doing: + +```php +// Clone to a bare repository +$repository = Gitonomy\Git\Admin::cloneTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git'); + +// Clone to a non-bare repository +$repository = Gitonomy\Git\Admin::cloneTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', false); +``` + +Default behavior is to clone in a bare repository. + +You can also clone a repository and point it to a specific branch. In a +non-bare repository, this branch will be checked out: + +```php +// Clone to a bare repository +$repository = Gitonomy\Git\Admin::cloneBranchTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', 'a-branch'); + +// Clone to a non-bare repository +$repository = Gitonomy\Git\Admin::cloneBranchTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', 'a-branch', false); +``` + +Clone a Repository object +------------------------- + +If you already have a Repository instance and want to clone it, you can +use this shortcut: + +```php +$new = $repository->cloneTo('/tmp/clone'); +``` + +Mirror a repository +------------------- + +If you want to mirror fully a repository and all references, use the +`mirrorTo` method. This method takes only two arguments, where to mirror +and what to mirror: + +```php +// Mirror to a bare repository +$mirror = Gitonomy\Git\Admin::mirrorTo('/tmp/mirror', 'https://github.com/gitonomy/gitlib.git'); + +// Mirror to a non-bare repository +$mirror = Gitonomy\Git\Admin::mirrorTo('/tmp/mirror', 'https://github.com/gitonomy/gitlib.git', false); +``` + +### References + +- +- diff --git a/doc/api/admin.rst b/doc/api/admin.rst deleted file mode 100644 index f1ec8b66..00000000 --- a/doc/api/admin.rst +++ /dev/null @@ -1,76 +0,0 @@ -Create and access git repositories -================================== - -gitlib provides methods to initialize new repositories. - -Create a repository -------------------- - -To initialize a new repository, use method ``Admin::init``. - -.. code-block:: php - - // Initialize a bare repository - $repository = Gitonomy\Git\Admin::init('/path/to/repository'); - - // Initialize a non-bare repository - $repository = Gitonomy\Git\Admin::init('/path/to/repository', false); - -Default behavior is to create a bare repository. If you want to initialize a -repository with a working copy,pass ``false`` as third argument of Repository -constructor. - -Cloning repositories --------------------- - -You can clone a repository from an URL by doing: - -.. code-block:: php - - // Clone to a bare repository - $repository = Gitonomy\Git\Admin::cloneTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git'); - - // Clone to a non-bare repository - $repository = Gitonomy\Git\Admin::cloneTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', false); - -Default behavior is to clone in a bare repository. - -You can also clone a repository and point it to a specific branch. In a non-bare repository, this branch will be checked out: - -.. code-block:: php - - // Clone to a bare repository - $repository = Gitonomy\Git\Admin::cloneBranchTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', 'a-branch'); - - // Clone to a non-bare repository - $repository = Gitonomy\Git\Admin::cloneBranchTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', 'a-branch' false); - -Clone a Repository object -------------------------- - -If you already have a Repository instance and want to clone it, you can use this shortcut: - -.. code-block:: php - - $new = $repository->cloneTo('/tmp/clone'); - -Mirror a repository -------------------- - -If you want to mirror fully a repository and all references, use the ``mirrorTo`` method. This method -takes only two arguments, where to mirror and what to mirror: - -.. code-block:: php - - // Mirror to a bare repository - $mirror = Gitonomy\Git\Admin::mirrorTo('/tmp/mirror', 'https://github.com/gitonomy/gitlib.git'); - - // Mirror to a non-bare repository - $mirror = Gitonomy\Git\Admin::mirrorTo('/tmp/mirror', 'https://github.com/gitonomy/gitlib.git', false); - - -References -:::::::::: - -* http://linux.die.net/man/1/git-init -* http://linux.die.net/man/1/git-clone diff --git a/doc/api/blame.rst b/doc/api/blame.rst deleted file mode 100644 index 7fcbdcdc..00000000 --- a/doc/api/blame.rst +++ /dev/null @@ -1,54 +0,0 @@ -Blaming files -============= - -Line-per-line iteration ------------------------ - -To iterate on lines of a blame: - -.. code-block:: php - - $blame = $repository->getBlame('master', 'README.md'); - - foreach ($blame->getLines() as $lineNumber => $line) { - $commit = $line->getCommit(); - echo $lineNumber.': '.$line->getContent()." - ".$commit->getAuthorName()."\n"; - } - -The *getLines* method returns an array indexed starting from 1. - -As you can see, you can access the commit object related to the line you are iterating on. - -If you want to access directly a line: - -.. code-block:: php - - $line = $blame->getLine(32); - -The Line object ---------------- - -LineObject represents an item of the blame file. It is composed of those informations: - -.. code-block:: php - - $line->getCommit(); // returns a Commit - $line->getContent(); // returns text - - // you can access author from commmit: - $author = $line->getCommit()->getAuthorName(); - -Group reading by commit ------------------------ - -If you plan to display it, you'll probably need a version where lines from same commit are grouped. - -To do so, use the *getGroupedLines* method that will return an array like this: - -.. code-block:: php - - $blame = array( - array(Commit, array(1 => Line, 2 => Line, 3 => Line)), - array(Commit, array(4 => Line)), - array(Commit, array(5 => Line, 6 => Line)) - ) diff --git a/doc/api/branch.rst b/doc/api/branch.rst deleted file mode 100644 index c7c0c408..00000000 --- a/doc/api/branch.rst +++ /dev/null @@ -1,16 +0,0 @@ -Branch -====== - -To access a *Branch*, starting from a repository object: - -.. code-block:: php - - $repository = new Gitonomy\Git\Repository('/path/to/repository'); - $branch = $repository->getReferences()->getBranch('master'); - -You can check is the branch is a local or remote one: - -.. code-block:: php - - $branch->isLocal(); - $branch->isRemote(); diff --git a/doc/api/commit.rst b/doc/api/commit.rst deleted file mode 100644 index d11ea810..00000000 --- a/doc/api/commit.rst +++ /dev/null @@ -1,168 +0,0 @@ -Commit -====== - -To access a *Commit*, starting from a repository object: - -.. code-block:: php - - $repository = new Gitonomy\Git\Repository('/path/to/repository'); - $commit = $repository->getCommit('a7c8d2b4'); - -Browsing parents ----------------- - -A *Commit* can have a natural number of parents: - -* **no parent**: it's an initial commit, the root of a tree -* **one parent**: it means it's not a merge, just a regular commit -* **many parents**: it's a merge-commit - -You have 2 methods available for accessing parents: - -.. code-block:: php - - // Access parent hashes - $hashes = $commit->getParentHashes(); - - // Access parent commit objects - $commits = $commit->getParents(); - -For example, if you want to display all parents, starting from a commit: - -.. code-block:: php - - function displayLog(Gitonomy\Git\Commit $commit) { - echo '- '.$commit->getShortMessage()."\n"; - foreach ($commit->getParents() as $parent) { - displayLog($parent); - } - } - -Notice that this function will first display all commits from first merged -branch and then display all commits from next branch, and so on. - -Accessing tree --------------- - -The tree object contains the reference to the files associated to a given -commit. Every commit has one and only one tree, referencing all files and -folders of a given state for a project. For more informations about the tree, -see the chapter dedicated to it. - -To access a tree starting from a commit: - -.. code-block:: php - - // Returns the tree hash - $tree = $commit->getTreeHash(); - - // Returns the tree object - $tree = $commit->getTree(); - -Author & Committer informations -------------------------------- - -Each commit has two authoring informations: an author and a committer. The -author is the creator of the modification, authoring a modification in the -repository. The committer is responsible of introducing this modification to -the repository. - -You can access informations from author and committer using those methods: - -.. code-block:: php - - // Author - $commit->getAuthorName(); - $commit->getAuthorEmail(); - $commit->getAuthorDate(); // returns a DateTime object - - // Committer - $commit->getCommitterName(); - $commit->getCommitterEmail(); - $commit->getCommitterDate(); // returns a DateTime object - -Commit message and short message --------------------------------- - -Each commit also has a message, associated to the modification. This message -can be multilined. - -To access the message, you can use the *getMessage* method: - -.. code-block:: php - - $commit->getMessage(); - -For your convenience, this library provides a shortcut method to keep only the -first line or first 50 characters if the first line is too long: - -.. code-block:: php - - $commit->getShortMessage(); - -You can customize it like this: - -.. code-block:: php - - $commit->getShortMessage(45, true, '.'); - -* The first parameter is the max length of the message. -* The second parameter determine if the last word should be cut or preserved -* The third parameter is the separator - -There are also two other methods for your convenience: - -.. code-block:: php - - // The first line - $commit->getSubjectMessage(); - - // The body (rest of the message) - $commit->getBodyMessage(); - -Diff of a commit ----------------- - -You can check the modifications introduced by a commit using the *getDiff* -method. When you request a diff for a commit, depending of the number of -parents, the strategy will be different: - -* If you have *no parent*, the diff will be the content of the tree -* If you only have *one parent*, the diff will be between the commit and his - parent -* If you have *multiple parents*, the diff will be the difference between the - commit and the first common ancestor of all parents - -For more informations about the diff API, read the related chapter. - -To access the *Diff* object of a commit, use the method *getDiff*: - -.. code-block:: php - - $diff = $commit->getDiff(); - -Last modification of a file ---------------------------- - -To know the last modification of a file, you can use the *getLastModification* -method on a commit. - -Here is a very straightforward example: - -.. code-block:: php - - $last = $commit->getLastModification('README'); - - echo "Last README modification:\n"; - echo" Author: ".$last->getAuthorName()."\n"; - echo" Date: ".$last->getAuthorDate()->format('d/m/Y')."\n"; - echo" Message: ".$last->getMessage(); - -Find every branches containing a commit ---------------------------------------- - -.. code-block:: php - - $branches = $commit->getIncludingBranches($includeLocalBranches, $includeRemoteBranches); - $localBranches = $commit->getIncludingBranches(true, false); - $remoteBranches = $commit->getIncludingBranches(false, true); diff --git a/doc/api/diff.rst b/doc/api/diff.rst deleted file mode 100644 index badd0776..00000000 --- a/doc/api/diff.rst +++ /dev/null @@ -1,102 +0,0 @@ -Computing diff -============== - -Even if git is a diff-less storage engine, it's possible to compute them. - -To compute a diff in git, you need to specify a *revision*. This revision can -be a commit (*2bc7a8*) or a range (*2bc7a8..ff4c21b*). - -For more informations about git revisions: *man gitrevisions*. - -When you have decided the revision you want and have your *Repository* object, -you can call the *getDiff* method on the repository: - -.. code-block:: php - - $diff = $repository->getDiff('master@{2 days ago}..master'); - -You can also access it from a *Log* object: - -.. code-block:: php - - $log = $repository->getLog('master@{2 days ago}..master'); - $diff = $log->getDiff(); - -Iterating a diff ----------------- - -When you have a *Diff* object, you can iterate over files using method -*getFiles()*. This method returns a list of *File* objects, who represents the -modifications for a single file. - -.. code-block:: php - - $files = $diff->getFiles(); - echo sprintf("%s files modified", count($files)); - - foreach ($files as $fileDiff) { - echo sprintf("Old name: (%s) %s\n", $fileDiff->getOldMode(), $fileDiff->getOldName()); - echo sprintf("New name: (%s) %s\n", $fileDiff->getNewMode(), $fileDiff->getNewName()); - } - -The File object ---------------- - -Here is an exhaustive list of the *File* class methods: - -.. code-block:: php - - $file->getOldName(); - $file->getNewName(); - $file->getOldDiff(); - $file->getNewDiff(); - - $file->isCreation(); - $file->isDeletion(); - $file->isModification(); - - $file->isRename(); - $file->isChangeMode(); - - $file->getAdditions(); // Number of added lines - $file->getDeletions(); // Number of deleted lines - - $file->isBinary(); // Binary files have no "lines" - - $file->getChanges(); // See next chapter - -The FileChange object ---------------------- - -.. note:: - - This part of API is not very clean, very consistent. If you have any idea - or suggestion on how to enhance this, your comment would be appreciated. - -A *File* object is composed of many changes. For each of those changes, -a *FileChange* object is associated. - -To access changes from a file, use the *getChanges* method: - -.. code-block:: php - - $changes = $file->getChanges(); - foreach ($changes as $change) { - foreach ($lines as $data) { - list ($type, $line) = $data; - if ($type === FileChange::LINE_CONTEXT) { - echo ' '.$line."\n"; - } elseif ($type === FileChange::LINE_ADD) { - echo '+'.$line."\n"; - } else { - echo '-'.$line."\n"; - } - } - } - -To get line numbers, use the range methods: - -.. code-block:: php - - echo sprintf("Previously from line %s to %s\n", $change->getOldRangeStart(), $change->getOldRangeEnd()); - echo sprintf("Now from line %s to %s\n", $change->getNewRangeStart(), $change->getNewRangeEnd()); diff --git a/doc/api/hooks.rst b/doc/api/hooks.rst deleted file mode 100644 index 20c4abec..00000000 --- a/doc/api/hooks.rst +++ /dev/null @@ -1,76 +0,0 @@ -Hooks -===== - -It's possible to define custom hooks on any repository with git. Those hooks -are located in the *.git/hooks* folder. - -Those files need to be executable. For convenience, gitlib will set them to -*777*. - -With *gitlib*, you can manage hooks over a repository using the *Hooks* object. - -To access it from a repository, use the *getHooks* method on a *Repository* -object: - -.. code-block:: php - - $hooks = $repository->getHooks(); - -Reading hooks -------------- - -To read the content of a hook, use the *get* method like this: - -.. code-block:: php - - $content = $hooks->get('pre-receive'); // returns a string - -If the hook does not exist, an exception will be thrown (*InvalidArgumentException*). - -You can test if a hook is present using the method *has*: - -.. code-block:: php - - $hooks->has('pre-receive'); // a boolean indicating presence - -Inserting hooks ---------------- - -You can modify a hook in two different ways: creating a new file or using a symlink. - -To create the hook using a symlink: - -.. code-block:: php - - $hooks->setSymlink('pre-receive', '/path/to/file-to-link'); - -If the hook already exist, a *LogicException* will be thrown. If an error occured -during symlink creation, a *RuntimeException* will be thrown. - -If you want to directly create a new file in hooks directory, use the -method *set*. This method will create a new file, put content in it and make it -executable: - -.. code-block:: php - - $content = <<set('pre-receive', $content); - -If the hook already exists, a *LogicException* will be thrown. - -Removing hooks --------------- - -To remove a hook from a repository, use the function *remove*: - -.. code-block:: php - - $hooks->remove('pre-receive'); diff --git a/doc/api/log.rst b/doc/api/log.rst deleted file mode 100644 index 5a795b9e..00000000 --- a/doc/api/log.rst +++ /dev/null @@ -1,54 +0,0 @@ -Getting log history -=================== - -Crawling manually commits and parents to browse history is surely a good -solution. But when it comes to ordering them or aggregate them from multiple -branches, we tend to use ``git log``. - -To get a *Log* object from a repository: - -.. code-block:: php - - $log = $repository->getLog(); - -You can pass four arguments to *getLog* method: - -.. code-block:: php - - // Global log for repository - $log = $repository->getLog(); - - // Log for master branch - $log = $repository->getLog('master'); - - // Returns last 10 commits on README file - $log = $repository->getLog('master', 'README', 0, 10); - - // Returns last 10 commits on README or UPGRADE files - $log = $repository->getLog('master', array('README', 'UPGRADE'), 0, 10); - -Counting --------- - -If you want to count overall commits, without offset or limit, use the *countCommits* method: - -.. code-block:: php - - echo sprintf("This log contains %s commits\n", $log->countCommits()); - - // Countable interface - echo sprintf("This log contains %s commits\n", count($log)); - -Offset and limit ----------------- - -Use those methods: - -.. code-block:: php - - $log->setOffset(32); - $log->setLimit(40); - - // or read it: - $log->getOffset(); - $log->getLimit(); diff --git a/doc/api/references.rst b/doc/api/references.rst deleted file mode 100644 index 5b7fa975..00000000 --- a/doc/api/references.rst +++ /dev/null @@ -1,91 +0,0 @@ -Tags and branches -================= - -Accessing tags and branches ---------------------------- - -With *gitlib*, you can access them via the *ReferenceBag* object. To get this -object from a *Repository*, use the *getReferences* method: - -.. code-block:: php - - $references = $repository->getReferences(); - -First, you can test existence of tags and branches like this: - -.. code-block:: php - - if ($references->hasBranch('master') && $references->hasTag('0.1')) { - echo "Good start!"; - } - -If you want to access all branches or all tags: - -.. code-block:: php - - $branches = $references->getBranches(); - $localBranches = $references->getLocalBranches(); - $remoteBranches = $references->getRemoteBranches(); - $tags = $references->getTags(); - $all = $references->getAll(); - -To get a given branch or tag, call *getBranch* or *getTag* on the -*ReferenceBag*. Those methods return *Branch* and *Tag* objects: - -.. code-block:: php - - $master = $references->getBranch('master'); - $feat123 = $references->getLocalBranch('feat123'); - $feat456 = $references->getRemoteBranch('origin/feat456'); - $v0_1 = $references->getTag('0.1'); - -If the reference cannot be resolved, a *ReferenceNotFoundException* will be -thrown. - -On each of those objects, you can access those informations: - -.. code-block:: php - - // Get the associated commit - $commit = $master->getCommit(); - - // Get the commit hash - $hash = $master->getCommitHash(); - - // Get the last modification - $lastModification = $master->getLastModification(); - -Create and delete reference ---------------------------- - -You can create new tags and branches on repository, using helper methods -on ReferenceBag object: - -.. code-block:: php - - // create a branch - $references = $repository->getReferences(); - $branch = $references->createBranch('foobar', 'a8b7e4...'); // commit to reference - - // create a tag - $references = $repository->getReferences(); - $tag = $references->createTag('0.3', 'a8b7e4...'); // commit to reference - - // delete a branch or a tag - $branch->delete(); - -Resolution from a commit ------------------------- - -To resolve a branch or a commit from a commit, you can use the *resolveTags* -and *resolveBranches* methods on it: - -.. code-block:: php - - $branches = $references->resolveBranches($commit); - $tags = $references->resolveTags($commit); - - // Resolve branches and tags - $all = $references->resolve($commit); - -You can pass a *Commit* object or a hash to the method, gitlib will handle it. diff --git a/doc/api/repository.rst b/doc/api/repository.rst deleted file mode 100644 index 53fa07a3..00000000 --- a/doc/api/repository.rst +++ /dev/null @@ -1,135 +0,0 @@ -Repository methods -================== - -Creating a *Repository* object is possible, providing a *path* argument to the -constructor: - -.. code-block:: php - - $repository = new Repository('/path/to/repo'); - -Repository options ------------------- - -The constructor of Repository takes an additional parameter: ``$options``. -This parameter can be used used to tune behavior of library. - -Available options are: - -* **debug** (default: true): Enables exception when edge cases are met -* **environment_variables**: (default: none) An array of environment variables to be set in sub-process -* **logger**: (default: none) Logger to use for reporting of execution (a ``Psr\Log\LoggerInterface``) -* **command**: (default: ``git``) Specify command to execute to run git -* **working_dir**: If you are using multiple working directories, this option is for you - -An example: - -.. code-block:: php - - $repository = new Repository('/path/to/repo', array( - 'debug' => true, - 'logger' => new Monolog\Logger() - )); - -Test if a repository is bare ----------------------------- - -On a *Repository* object, you can call method *isBare* to test if your repository is bare or not: - -.. code-block:: php - - $repository->isBare(); - -Compute size of a repository ----------------------------- - -To know how much size a repository is using on your drive, you can use ``getSize`` method on a *Repository* object. - -.. warning:: This command was only tested with linux. - -The returned size is in kilobytes: - -.. code-block:: php - - $size = $repository->getSize(); - - echo "Your repository size is ".$size."KB"; - -Access HEAD ------------ - -``HEAD`` represents in git the version you are working on (in working tree). -Your ``HEAD`` can be attached (using a reference) or detached (using a commit). - -.. code-block:: php - - $head = $repository->getHead(); // Commit or Reference - $head = $repository->getHeadCommit(); // Commit - - if ($repository->isHeadDetached()) { - echo "Sorry man\n"; - } - -Options for repository ----------------------- - -Logger -...... - -If you are developing, you may appreciate to have a logger inside repository, telling you every executed command. - -You call method ``setLogger`` as an option on repository creation: - -.. code-block:: php - - $repository->setLogger(new Monolog\Logger('repository')); - - $repository->run('fetch', array('--all')); - -You can also specify as an option on repository creation: - - $logger = new Monolog\Logger('repository'); - $repository = new Repository('/path/foo', array('logger' => $logger)); - - $repository->run('fetch', array('--all')); - -This will output: - -.. code-block:: text - - info run command: fetch "--all" - debug last command (fetch) duration: 23.24ms - debug last command (fetch) return code: 0 - debug last command (fetch) output: Fetching origin - -Disable debug-mode -.................. - -Gitlib throws an exception when something seems wrong. If a ``git` command returns a non-zero result, it will stop execution and throw an ``RuntimeException``. - -If you want to prevent this, set ``debug`` option to ``false``. This will make Repository log errors and return empty data instead of throwing exceptions. - -.. code-block:: php - - $repository = new Repository('/tmp/foo', array('debug' => false, 'logger' => $logger)); - -.. note:: if you plan to disable debug, you should rely on logger to keep a trace of edge failing cases. - -Specify git command to use -.......................... - -You can pass option ``command`` to specify which command to use to run git calls. If you have a git binary -located somewhere else, use this option to specify to gitlib path to your git binary: - -.. code-block:: php - - $repository = new Gitonomy\Git\Repository('/tmp/foo', array('command' => '/home/alice/bin/git')); - -Environment variables -..................... - -Now you want to set environment variables to use to run ``git`` commands. It might be useful. - -.. code-block:: php - - $repository = new Gitonomy\Git\Repository('/tmp/foo', array('environment_variables' => array('GIT_'))) diff --git a/doc/api/revision.rst b/doc/api/revision.rst deleted file mode 100644 index 8ffc3289..00000000 --- a/doc/api/revision.rst +++ /dev/null @@ -1,28 +0,0 @@ -Revision -======== - -To get a revision from a *Repository* object: - -.. code-block:: php - - $revision = $repository->getRevision('master@{2 days ago}'); - -Getting the log ---------------- - -You can access a *Log* object starting from a revision using the *getLog* -method. This method takes two parameters: *offset* and *limit*: - -.. code-block:: php - - // Returns 100 lasts commits - $log = $revision->getLog(null, 100); - -Resolve a revision ------------------- - -To resolve a revision to a commit: - -.. code-block:: php - - $commit = $revision->getCommit(); diff --git a/doc/api/tree.rst b/doc/api/tree.rst deleted file mode 100644 index 4641a9f3..00000000 --- a/doc/api/tree.rst +++ /dev/null @@ -1,54 +0,0 @@ -Tree and files -============== - -To organize folders, git uses trees. In gitlib, those trees are represented -via *Tree* object. - -To get the root tree associated to a commit, use the *getTree* method on the -commit object: - -.. code-block:: php - - $tree = $commit->getTree(); - -This tree is the entry point of all of your files. - -The main method for a tree is the *getEntries* method. This method will -return an array, indexed by name. Each of those elements will be the entry mode -and the entry object. - -Let's understand how it works with a concrete example: - -.. code-block:: php - - function displayTree(Tree $tree, $indent = 0) - { - $indent = str_repeat(' ', $indent); - foreach ($tree->getEntries() as $name => $data) { - list($mode, $entry) = $data; - if ($entry instanceof Tree) { - echo $indent.$name."/\n"; - displayTree($tree, $indent + 1); - } else { - echo $indent.$name."\n"; - } - } - } - - displayTree($commit->getTree()); - -This method will recursively display all entries of a tree. - -Resolve a path --------------- - -To access directly a sub-file, the easier is probably to use the *resolvePath* -method. - -An example: - -.. code-block:: php - - $source = $tree->resolvePath('src/Gitonomy/Git'); - - $source instanceof Tree; diff --git a/doc/api/workingcopy.rst b/doc/api/workingcopy.rst deleted file mode 100644 index 4b652230..00000000 --- a/doc/api/workingcopy.rst +++ /dev/null @@ -1,45 +0,0 @@ -Working copy -============ - -Working copy is the folder associated to a git repository. In *gitlib*, you -can access this object using the *getWorkingCopy* on a *Repository* object: - -.. code-block:: php - - $repo = new Repository('/path/to/working-dir'); - $wc = $repo->getWorkingCopy(); - -Checkout a revision -------------------- - -You can checkout any revision using *checkout* method. You can also pass a -second argument, which will be passed as argument with ``-b``: - -.. code-block:: php - - // git checkout master - $wc->checkout('master'); - - // git checkout origin/master -b master - $wc->checkout('origin/master', 'master'); - -You can also pass a *Reference* or a *Commit*. - -Staged modifications --------------------- - -You can get a diff of modifications pending in staging area. To get the ``Diff`` object, -call method ``getDiffStaged()``: - -.. code-block:: php - - $diff = $wc->getDiffStaged(); - -Pending modifications ---------------------- - -You can get pending modifications on tracked files by calling method ``getDiffPending()``: - -.. code-block:: php - - $diff = $wc->getDiffPending(); diff --git a/doc/blame.md b/doc/blame.md new file mode 100644 index 00000000..80816c9b --- /dev/null +++ b/doc/blame.md @@ -0,0 +1,58 @@ +Blaming files +============= + +Line-per-line iteration +----------------------- + +To iterate on lines of a blame: + +```php +$blame = $repository->getBlame('master', 'README.md'); + +foreach ($blame->getLines() as $lineNumber => $line) { + $commit = $line->getCommit(); + echo $lineNumber.': '.$line->getContent().' - '.$commit->getAuthorName().PHP_EOL; +} +``` + +The *getLines* method returns an array indexed starting from 1. + +As you can see, you can access the commit object related to the line you +are iterating on. + +If you want to access directly a line: + +```php +$line = $blame->getLine(32); +``` + +The Line object +--------------- + +LineObject represents an item of the blame file. It is composed of those +informations: + +```php +$line->getCommit(); // returns a Commit +$line->getContent(); // returns text + +// you can access author from commmit: +$author = $line->getCommit()->getAuthorName(); +``` + +Group reading by commit +----------------------- + +If you plan to display it, you'll probably need a version where lines +from same commit are grouped. + +To do so, use the *getGroupedLines* method that will return an array +like this: + +```php +$blame = array( + array(Commit, array(1 => Line, 2 => Line, 3 => Line)), + array(Commit, array(4 => Line)), + array(Commit, array(5 => Line, 6 => Line)) +) +``` diff --git a/doc/api/blob.rst b/doc/blob.md similarity index 51% rename from doc/api/blob.rst rename to doc/blob.md index 78d36c25..2ed3bd41 100644 --- a/doc/api/blob.rst +++ b/doc/blob.md @@ -2,43 +2,43 @@ Blob ==== In git, a blob represents a file content. You can't access the file name -directly from the *Blob* object; the filename information is stored within -the tree, not in the blob. +directly from the *Blob* object; the filename information is stored +within the tree, not in the blob. -It means that for git, two files with different names but same content will -have the same hash. +It means that for git, two files with different names but same content +will have the same hash. To access a repository *Blob*, you need the hash identifier: -.. code-block:: php - - $repository = new Gitonomy\Git\Repository('/path/to/repository'); - $blob = $repository->getBlob('a7c8d2b4'); +```php +$repository = new Gitonomy\Git\Repository('/path/to/repository'); +$blob = $repository->getBlob('a7c8d2b4'); +``` Get content ----------- To get content from a *Blob* object: -.. code-block:: php - - echo $blob->getContent(); +```php +echo $blob->getContent(); +``` File informations ----------------- To get mimetype of a *Blob* object using finfo extension: -.. code-block:: php - - echo $blob->getMimetype(); +```php +echo $blob->getMimetype(); +``` You can also test if *Blob* is a text of a binary file: -.. code-block:: php - - if ($blob->isText()) { - echo $blob->getContent(), "\n"; - } elseif ($blob->isBinary()) { - echo "File is binary\n"; - } +```php +if ($blob->isText()) { + echo $blob->getContent(), PHP_EOL; +} elseif ($blob->isBinary()) { + echo 'File is binary', PHP_EOL; +} +``` diff --git a/doc/branch.md b/doc/branch.md new file mode 100644 index 00000000..d6e0872c --- /dev/null +++ b/doc/branch.md @@ -0,0 +1,16 @@ +Branch +====== + +To access a *Branch*, starting from a repository object: + +```php +$repository = new Gitonomy\Git\Repository('/path/to/repository'); +$branch = $repository->getReferences()->getBranch('master'); +``` + +You can check is the branch is a local or remote one: + +```php +$branch->isLocal(); +$branch->isRemote(); +``` diff --git a/doc/commit.md b/doc/commit.md new file mode 100644 index 00000000..08e9b1e3 --- /dev/null +++ b/doc/commit.md @@ -0,0 +1,171 @@ +Commit +====== + +To access a *Commit*, starting from a repository object: + +```php +$repository = new Gitonomy\Git\Repository('/path/to/repository'); +$commit = $repository->getCommit('a7c8d2b4'); +``` + +Browsing parents +---------------- + +A *Commit* can have a natural number of parents: + +- **no parent**: it's an initial commit, the root of a tree +- **one parent**: it means it's not a merge, just a regular commit +- **many parents**: it's a merge-commit + +You have 2 methods available for accessing parents: + +```php +// Access parent hashes +$hashes = $commit->getParentHashes(); + +// Access parent commit objects +$commits = $commit->getParents(); +``` + +For example, if you want to display all parents, starting from a commit: + +```php +function displayLog(Gitonomy\Git\Commit $commit) { + echo '- '.$commit->getShortMessage().PHP_EOL; + foreach ($commit->getParents() as $parent) { + displayLog($parent); + } +} +``` + +Notice that this function will first display all commits from first +merged branch and then display all commits from next branch, and so on. + +Accessing tree +-------------- + +The tree object contains the reference to the files associated to a +given commit. Every commit has one and only one tree, referencing all +files and folders of a given state for a project. For more informations +about the tree, see the chapter dedicated to it. + +To access a tree starting from a commit: + +```php +// Returns the tree hash +$tree = $commit->getTreeHash(); + +// Returns the tree object +$tree = $commit->getTree(); +``` + +Author & Committer informations +------------------------------- + +Each commit has two authoring informations: an author and a committer. +The author is the creator of the modification, authoring a modification +in the repository. The committer is responsible of introducing this +modification to the repository. + +You can access informations from author and committer using those +methods: + +```php +// Author +$commit->getAuthorName(); +$commit->getAuthorEmail(); +$commit->getAuthorDate(); // returns a DateTime object + +// Committer +$commit->getCommitterName(); +$commit->getCommitterEmail(); +$commit->getCommitterDate(); // returns a DateTime object +``` + +Commit message and short message +-------------------------------- + +Each commit also has a message, associated to the modification. This +message can be multilined. + +To access the message, you can use the *getMessage* method: + +```php +$commit->getMessage(); +``` + +For your convenience, this library provides a shortcut method to keep +only the first line or first 50 characters if the first line is too +long: + +```php +$commit->getShortMessage(); +``` + +You can customize it like this: + +```php +$commit->getShortMessage(45, true, '.'); +``` + +- The first parameter is the max length of the message. +- The second parameter determine if the last word should be cut or + preserved +- The third parameter is the separator + +There are also two other methods for your convenience: + +```php +// The first line +$commit->getSubjectMessage(); + +// The body (rest of the message) +$commit->getBodyMessage(); +``` + +Diff of a commit +---------------- + +You can check the modifications introduced by a commit using the +*getDiff* method. When you request a diff for a commit, depending of the +number of parents, the strategy will be different: + +- If you have *no parent*, the diff will be the content of the tree +- If you only have *one parent*, the diff will be between the commit + and his parent +- If you have *multiple parents*, the diff will be the difference + between the commit and the first common ancestor of all parents + +For more informations about the diff API, read the related chapter. + +To access the *Diff* object of a commit, use the method *getDiff*: + +```php +$diff = $commit->getDiff(); +``` + +Last modification of a file +--------------------------- + +To know the last modification of a file, you can use the +*getLastModification* method on a commit. + +Here is a very straightforward example: + +```php +$last = $commit->getLastModification('README'); + +echo 'Last README modification'.PHP_EOL; +echo ' Author: '.$last->getAuthorName().PHP_EOL; +echo ' Date: '.$last->getAuthorDate()->format('d/m/Y').PHP_EOL; +echo ' Message: '.$last->getMessage(); +``` + +Find every branches containing a commit +--------------------------------------- + +```php +$branches = $commit->getIncludingBranches($includeLocalBranches, $includeRemoteBranches); +$localBranches = $commit->getIncludingBranches(true, false); +$remoteBranches = $commit->getIncludingBranches(false, true); +``` diff --git a/doc/debug.rst b/doc/debug.rst deleted file mode 100644 index 704fd353..00000000 --- a/doc/debug.rst +++ /dev/null @@ -1,22 +0,0 @@ -Debug-mode -========== - -gitlib offers a debug mode, to make you see edge-cases of your usage. This is called -debug-mode. - -Debug-mode is enabled by default. If you disable it, gitlib will behave differently: - -* when an error is met during execution, gitlib will try to minimize it, to not block - execution flow. Errors will still be reporter in logger. -* logs will be more verbose. They will contain every output, every return code, every - possible information to ease debugging. - -If you want to disable exceptions and try to minimize as much as possible errors, pass -``false`` when construction a repository: - -.. code-block:: php - - $repository = new Gitonomy\Git\Repository($path'/tmp/repo', $debug = false) - -``$debug`` argument should be available in every method you can use to create a -repository. diff --git a/doc/development.rst b/doc/development.rst deleted file mode 100644 index 986b0868..00000000 --- a/doc/development.rst +++ /dev/null @@ -1,24 +0,0 @@ -Developing gitlib -================= - -If you plan to contribute to gitlib, here are few things you should know: - -Documentation generation -:::::::::::::::::::::::: - -Documentation is generated using Sphinx (restructured text). Configuration file -is located in https://github.com/gitonomy/website/blob/master/bin/conf.py - -You will need to fetch vendor modules for PHP blocks especially. If you really -want to generate it, install the website project locally and hack into it. - -Test against different git versions -::::::::::::::::::::::::::::::::::: - -A script ``test-git-version.sh`` is available in repository to test gitlib against -many git versions. - -This script is not usable on Travis-CI, they would hate me for this. It creates -a local cache to avoid fetching from Github and compiling if already compiled. - -Use it at your own risk, it's still under experiment. diff --git a/doc/diff.md b/doc/diff.md new file mode 100644 index 00000000..2ce7870a --- /dev/null +++ b/doc/diff.md @@ -0,0 +1,104 @@ +Computing diff +============== + +Even if git is a diff-less storage engine, it's possible to compute +them. + +To compute a diff in git, you need to specify a *revision*. This +revision can be a commit (*2bc7a8*) or a range (*2bc7a8..ff4c21b*). + +For more informations about git revisions: *man gitrevisions*. + +When you have decided the revision you want and have your *Repository* +object, you can call the *getDiff* method on the repository: + +```php +$diff = $repository->getDiff('master@{2 days ago}..master'); +``` + +You can also access it from a *Log* object: + +```php +$log = $repository->getLog('master@{2 days ago}..master'); +$diff = $log->getDiff(); +``` + +Iterating a diff +---------------- + +When you have a *Diff* object, you can iterate over files using method +*getFiles()*. This method returns a list of *File* objects, who +represents the modifications for a single file. + +```php +$files = $diff->getFiles(); +echo sprintf('%s files modified%s', count($files), PHP_EOL); + +foreach ($files as $fileDiff) { + echo sprintf('Old name: (%s) %s%s', $fileDiff->getOldMode(), $fileDiff->getOldName(), PHP_EOL); + echo sprintf('New name: (%s) %s%s', $fileDiff->getNewMode(), $fileDiff->getNewName(), PHP_EOL); +} +``` + +The File object +--------------- + +Here is an exhaustive list of the *File* class methods: + +```php +$file->getOldName(); +$file->getNewName(); +$file->getOldDiff(); +$file->getNewDiff(); + +$file->isCreation(); +$file->isDeletion(); +$file->isModification(); + +$file->isRename(); +$file->isChangeMode(); + +$file->getAdditions(); // Number of added lines +$file->getDeletions(); // Number of deleted lines + +$file->isBinary(); // Binary files have no "lines" + +$file->getChanges(); // See next chapter +``` + +The FileChange object +--------------------- + +> **note** +> +> This part of API is not very clean, very consistent. If you have any +> idea or suggestion on how to enhance this, your comment would be +> appreciated. + +A *File* object is composed of many changes. For each of those changes, +a *FileChange* object is associated. + +To access changes from a file, use the *getChanges* method: + +```php +$changes = $file->getChanges(); +foreach ($changes as $change) { + foreach ($lines as $data) { + list ($type, $line) = $data; + if ($type === FileChange::LINE_CONTEXT) { + echo ' '.$line.PHP_EOL; + } elseif ($type === FileChange::LINE_ADD) { + echo '+'.$line.PHP_EOL; + } else { + echo '-'.$line.PHP_EOL; + } + } +} +``` + +To get line numbers, use the range methods: + +```php +echo sprintf('Previously from line %s to %s%s', $change->getOldRangeStart(), $change->getOldRangeEnd(), PHP_EOL); +echo sprintf('Now from line %s to %s%s', $change->getNewRangeStart(), $change->getNewRangeEnd(), PHP_EOL); +``` diff --git a/doc/hooks.md b/doc/hooks.md new file mode 100644 index 00000000..eb5c8ab4 --- /dev/null +++ b/doc/hooks.md @@ -0,0 +1,80 @@ +Hooks +===== + +It's possible to define custom hooks on any repository with git. Those +hooks are located in the *.git/hooks* folder. + +Those files need to be executable. For convenience, gitlib will set them +to *777*. + +With *gitlib*, you can manage hooks over a repository using the *Hooks* +object. + +To access it from a repository, use the *getHooks* method on a +*Repository* object: + +```php +$hooks = $repository->getHooks(); +``` + +Reading hooks +------------- + +To read the content of a hook, use the *get* method like this: + +```php +$content = $hooks->get('pre-receive'); // returns a string +``` + +If the hook does not exist, an exception will be thrown +(*InvalidArgumentException*). + +You can test if a hook is present using the method *has*: + +```php +$hooks->has('pre-receive'); // a boolean indicating presence +``` + +Inserting hooks +--------------- + +You can modify a hook in two different ways: creating a new file or +using a symlink. + +To create the hook using a symlink: + +```php +$hooks->setSymlink('pre-receive', '/path/to/file-to-link'); +``` + +If the hook already exist, a *LogicException* will be thrown. If an +error occured during symlink creation, a *RuntimeException* will be +thrown. + +If you want to directly create a new file in hooks directory, use the +method *set*. This method will create a new file, put content in it and +make it executable: + +```php +$content = <<set('pre-receive', $content); +``` + +If the hook already exists, a *LogicException* will be thrown. + +Removing hooks +-------------- + +To remove a hook from a repository, use the function *remove*: + +```php +$hooks->remove('pre-receive'); +``` diff --git a/doc/index.rst b/doc/index.rst deleted file mode 100644 index 919e91c5..00000000 --- a/doc/index.rst +++ /dev/null @@ -1,59 +0,0 @@ -gitlib - library to manipulate git -================================== - -gitlib requires PHP 5.3 and class autoloading (PSR-0) to work properly. Internally, it relies on ``git`` method calls -to fetch informations from repository. - -.. code-block:: php - - use Gitonomy\Git\Repository; - - $repository = new Repository('/path/to/repository'); - - foreach ($repository->getReferences()->getBranches() as $branch) { - echo "- ".$branch->getName(); - } - - $repository->run('fetch', array('--all')); - - -Reference ---------- - -.. toctree:: - :maxdepth: 1 - - api/admin - api/repository - api/hooks - api/workingcopy - api/commit - api/blame - api/blob - api/branch - api/tree - api/log - api/diff - api/references - api/revision - - -Documentation -------------- - -.. toctree:: - :maxdepth: 2 - - installation - debug - development - -Missing features ----------------- - -Some major features are still missing from gitlib: - -* Remotes -* Submodules - -If you want to run git commands on repository, call method ``Repository::run`` with method and arguments. diff --git a/doc/installation.rst b/doc/installation.rst deleted file mode 100644 index 7b237895..00000000 --- a/doc/installation.rst +++ /dev/null @@ -1,20 +0,0 @@ -Installation of gitlib -====================== - -Autoloading -::::::::::: - -gitlib relies on class autoloading. It does not require any additional setup. - -Using composer -:::::::::::::: - -Edit your ``composer.json`` file and add ``gitonomy/gitlib`` in section ``require``: - -.. code-block:: json - - { - "require": { - "gitonomy/gitlib": "dev-master" - } - } diff --git a/doc/log.md b/doc/log.md new file mode 100644 index 00000000..4dc5a465 --- /dev/null +++ b/doc/log.md @@ -0,0 +1,55 @@ +Getting log history +=================== + +Crawling manually commits and parents to browse history is surely a good +solution. But when it comes to ordering them or aggregate them from +multiple branches, we tend to use `git log`. + +To get a *Log* object from a repository: + +```php +$log = $repository->getLog(); +``` + +You can pass four arguments to *getLog* method: + +```php +// Global log for repository +$log = $repository->getLog(); + +// Log for master branch +$log = $repository->getLog('master'); + +// Returns last 10 commits on README file +$log = $repository->getLog('master', 'README', 0, 10); + +// Returns last 10 commits on README or UPGRADE files +$log = $repository->getLog('master', ['README', 'UPGRADE'], 0, 10); +``` + +Counting +-------- + +If you want to count overall commits, without offset or limit, use the +*countCommits* method: + +```php +echo sprintf('This log contains %s commits%s', $log->countCommits(), PHP_EOL); + +// Countable interface +echo sprintf('This log contains %s commits%s', count($log), PHP_EOL); +``` + +Offset and limit +---------------- + +Use those methods: + +```php +$log->setOffset(32); +$log->setLimit(40); + +// or read it: +$log->getOffset(); +$log->getLimit(); +``` diff --git a/doc/references.md b/doc/references.md new file mode 100644 index 00000000..64430929 --- /dev/null +++ b/doc/references.md @@ -0,0 +1,92 @@ +Tags and branches +================= + +Accessing tags and branches +--------------------------- + +With *gitlib*, you can access them via the *ReferenceBag* object. To get +this object from a *Repository*, use the *getReferences* method: + +```php +$references = $repository->getReferences(); +``` + +First, you can test existence of tags and branches like this: + +```php +if ($references->hasBranch('master') && $references->hasTag('0.1')) { + echo 'Good start!'.PHP_EOL; +} +``` + +If you want to access all branches or all tags: + +```php +$branches = $references->getBranches(); +$localBranches = $references->getLocalBranches(); +$remoteBranches = $references->getRemoteBranches(); +$tags = $references->getTags(); +$all = $references->getAll(); +``` + +To get a given branch or tag, call *getBranch* or *getTag* on the +*ReferenceBag*. Those methods return *Branch* and *Tag* objects: + +```php +$master = $references->getBranch('master'); +$feat123 = $references->getLocalBranch('feat123'); +$feat456 = $references->getRemoteBranch('origin/feat456'); +$v0_1 = $references->getTag('0.1'); +``` + +If the reference cannot be resolved, a *ReferenceNotFoundException* will +be thrown. + +On each of those objects, you can access those informations: + +```php +// Get the associated commit +$commit = $master->getCommit(); + +// Get the commit hash +$hash = $master->getCommitHash(); + +// Get the last modification +$lastModification = $master->getLastModification(); +``` + +Create and delete reference +--------------------------- + +You can create new tags and branches on repository, using helper methods +on ReferenceBag object: + +```php +// create a branch +$references = $repository->getReferences(); +$branch = $references->createBranch('foobar', 'a8b7e4...'); // commit to reference + +// create a tag +$references = $repository->getReferences(); +$tag = $references->createTag('0.3', 'a8b7e4...'); // commit to reference + +// delete a branch or a tag +$branch->delete(); +``` + +Resolution from a commit +------------------------ + +To resolve a branch or a commit from a commit, you can use the +*resolveTags* and *resolveBranches* methods on it: + +```php +$branches = $references->resolveBranches($commit); +$tags = $references->resolveTags($commit); + +// Resolve branches and tags +$all = $references->resolve($commit); +``` + +You can pass a *Commit* object or a hash to the method, gitlib will +handle it. diff --git a/doc/repository.md b/doc/repository.md new file mode 100644 index 00000000..05b4a4e7 --- /dev/null +++ b/doc/repository.md @@ -0,0 +1,147 @@ +Repository methods +================== + +Creating a *Repository* object is possible, providing a *path* argument +to the constructor: + +```php +$repository = new Repository('/path/to/repo'); +``` + +Repository options +------------------ + +The constructor of Repository takes an additional parameter: `$options`. +This parameter can be used used to tune behavior of library. + +Available options are: + +- **debug** (default: true): Enables exception when edge cases are met +- **environment\_variables**: (default: none) An array of environment + variables to be set in sub-process +- **logger**: (default: none) Logger to use for reporting of execution + (a `Psr\Log\LoggerInterface`) +- **command**: (default: `git`) Specify command to execute to run git +- **working\_dir**: If you are using multiple working directories, + this option is for you + +An example: + +```php +$repository = new Repository('/path/to/repo', [ + 'debug' => true, + 'logger' => new Monolog\Logger(), +]); +``` + +Test if a repository is bare +---------------------------- + +On a *Repository* object, you can call method *isBare* to test if your +repository is bare or not: + +```php +$repository->isBare(); +``` + +Compute size of a repository +---------------------------- + +To know how much size a repository is using on your drive, you can use +`getSize` method on a *Repository* object. + +> **warning** +> +> This command was only tested with linux. + +The returned size is in kilobytes: + +```php +$size = $repository->getSize(); + +echo 'Your repository size is '.$size.'KB'; +``` + +Access HEAD +----------- + +`HEAD` represents in git the version you are working on (in working +tree). Your `HEAD` can be attached (using a reference) or detached +(using a commit). + +```php +$head = $repository->getHead(); // Commit or Reference +$head = $repository->getHeadCommit(); // Commit + +if ($repository->isHeadDetached()) { + echo 'Sorry man'.PHP_EOL; +} +``` + +Options for repository +---------------------- + +### Logger + +If you are developing, you may appreciate to have a logger inside +repository, telling you every executed command. + +You call method `setLogger` as an option on repository creation: + +```php +$repository->setLogger(new Monolog\Logger('repository')); + +$repository->run('fetch', ['--all']); +``` + +You can also specify as an option on repository creation: + +```php +$logger = new MonologLogger('repository'); +$repository = new Repository('/path/foo', ['logger' => $logger]); +$repository->run('fetch', ['--all']); +``` + +This will output: + +``` +info run command: fetch "--all" +debug last command (fetch) duration: 23.24ms +debug last command (fetch) return code: 0 +debug last command (fetch) output: Fetching origin +``` + +### Disable debug-mode + +Gitlib throws an exception when something seems wrong. If a `git` command exits +with a non-zero code, then execution will be stopped, and a `RuntimeException` +will be thrown. If you want to prevent this, set the `debug` option to` false`. +This will make `Repository` log errors and return empty data instead of +throwing exceptions. + +```php +$repository = new Repository('/tmp/foo', ['debug' => false, 'logger' => $logger]); +``` + +> **note** +> +> If you plan to disable debug, you should rely on the logger to keep a trace +> of the failing cases. + +### Specify git command to use + +You can pass the option `command` to specify which command to use to run git +calls. If you have a git binary located somewhere else, use this option to +specify to gitlib path to your git binary: + +```php +$repository = new Gitonomy\Git\Repository('/tmp/foo', ['command' => '/home/alice/bin/git']); +``` + +### Environment variables + +It is possible to send environment variables to the `git` commands. + +```php +$repository = new Gitonomy\Git\Repository('/tmp/foo', ['environment_variables' => ['GIT_']]) +``` diff --git a/doc/revision.md b/doc/revision.md new file mode 100644 index 00000000..81370c80 --- /dev/null +++ b/doc/revision.md @@ -0,0 +1,28 @@ +Revision +======== + +To get a revision from a *Repository* object: + +```php +$revision = $repository->getRevision('master@{2 days ago}'); +``` + +Getting the log +--------------- + +You can access a *Log* object starting from a revision using the +*getLog* method. This method takes two parameters: *offset* and *limit*: + +```php +// Returns 100 lasts commits +$log = $revision->getLog(null, 100); +``` + +Resolve a revision +------------------ + +To resolve a revision to a commit: + +```php +$commit = $revision->getCommit(); +``` diff --git a/doc/tree.md b/doc/tree.md new file mode 100644 index 00000000..0c5c8ef7 --- /dev/null +++ b/doc/tree.md @@ -0,0 +1,54 @@ +Tree and files +============== + +To organize folders, git uses trees. In gitlib, those trees are +represented via *Tree* object. + +To get the root tree associated to a commit, use the *getTree* method on +the commit object: + +```php +$tree = $commit->getTree(); +``` + +This tree is the entry point of all of your files. + +The main method for a tree is the *getEntries* method. This method will +return an array, indexed by name. Each of those elements will be the +entry mode and the entry object. + +Let's understand how it works with a concrete example: + +```php +function displayTree(Tree $tree, $indent = 0) +{ + $indent = str_repeat(' ', $indent); + foreach ($tree->getEntries() as $name => $data) { + list($mode, $entry) = $data; + if ($entry instanceof Tree) { + echo $indent.$name.'/'.PHP_EOL; + displayTree($tree, $indent + 1); + } else { + echo $indent.$name.PHP_EOL; + } + } +} + +displayTree($commit->getTree()); +``` + +This method will recursively display all entries of a tree. + +Resolve a path +-------------- + +To access directly a sub-file, the easier is probably to use the +*resolvePath* method. + +An example: + +```php +$source = $tree->resolvePath('src/Gitonomy/Git'); + +$source instanceof Tree; +``` diff --git a/doc/workingcopy.md b/doc/workingcopy.md new file mode 100644 index 00000000..4ecea6c1 --- /dev/null +++ b/doc/workingcopy.md @@ -0,0 +1,47 @@ +Working copy +============ + +Working copy is the folder associated to a git repository. In *gitlib*, +you can access this object using the *getWorkingCopy* on a *Repository* +object: + +```php +$repo = new Repository('/path/to/working-dir'); +$wc = $repo->getWorkingCopy(); +``` + +Checkout a revision +------------------- + +You can checkout any revision using *checkout* method. You can also pass +a second argument, which will be passed as argument with `-b`: + +```php +// git checkout master +$wc->checkout('master'); + +// git checkout origin/master -b master +$wc->checkout('origin/master', 'master'); +``` + +You can also pass a *Reference* or a *Commit*. + +Staged modifications +-------------------- + +You can get a diff of modifications pending in staging area. To get the +`Diff` object, call method `getDiffStaged()`: + +```php +$diff = $wc->getDiffStaged(); +``` + +Pending modifications +--------------------- + +You can get pending modifications on tracked files by calling method +`getDiffPending()`: + +```php +$diff = $wc->getDiffPending(); +``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 36964385..77fe6238 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,21 +1,29 @@ - - - + - tests/Gitonomy/Git/Tests + ./tests/Gitonomy/Git/Tests - + + + ./src/Gitonomy/Git + + diff --git a/src/Gitonomy/Git/Admin.php b/src/Gitonomy/Git/Admin.php index 5576a184..7b32794c 100644 --- a/src/Gitonomy/Git/Admin.php +++ b/src/Gitonomy/Git/Admin.php @@ -9,10 +9,11 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Exception\RuntimeException; -use Symfony\Component\Process\ProcessBuilder; +use Symfony\Component\Process\Process; /** * Administration class for Git repositories. @@ -28,13 +29,13 @@ class Admin * @param bool $bare indicate to create a bare repository * @param array $options options for Repository creation * - * @return Repository - * * @throws RuntimeException Directory exists or not writable (only if debug=true) + * + * @return Repository */ - public static function init($path, $bare = true, array $options = array()) + public static function init($path, $bare = true, array $options = []) { - $process = static::getProcess('init', array_merge(array('-q'), $bare ? array('--bare') : array(), array($path)), $options); + $process = static::getProcess('init', array_merge(['-q'], $bare ? ['--bare'] : [], [$path]), $options); $process->run(); @@ -57,15 +58,39 @@ public static function init($path, $bare = true, array $options = array()) * * @return bool true if url is valid */ - public static function isValidRepository($url, array $options = array()) + public static function isValidRepository($url, array $options = []) { - $process = static::getProcess('ls-remote', array($url), $options); + $process = static::getProcess('ls-remote', [$url], $options); $process->run(); 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. * @@ -76,9 +101,9 @@ public static function isValidRepository($url, array $options = array()) * * @return Repository */ - public static function cloneTo($path, $url, $bare = true, array $options = array()) + public static function cloneTo($path, $url, $bare = true, array $options = []) { - $args = $bare ? array('--bare') : array(); + $args = $bare ? ['--bare'] : []; return static::cloneRepository($path, $url, $args, $options); } @@ -94,9 +119,9 @@ public static function cloneTo($path, $url, $bare = true, array $options = array * * @return Repository */ - public static function cloneBranchTo($path, $url, $branch, $bare = true, $options = array()) + public static function cloneBranchTo($path, $url, $branch, $bare = true, $options = []) { - $args = array('--branch', $branch); + $args = ['--branch', $branch]; if ($bare) { $args[] = '--bare'; } @@ -113,9 +138,9 @@ public static function cloneBranchTo($path, $url, $branch, $bare = true, $option * * @return Repository */ - public static function mirrorTo($path, $url, array $options = array()) + public static function mirrorTo($path, $url, array $options = []) { - return static::cloneRepository($path, $url, array('--mirror'), $options); + return static::cloneRepository($path, $url, ['--mirror'], $options); } /** @@ -128,9 +153,9 @@ public static function mirrorTo($path, $url, array $options = array()) * * @return Repository */ - public static function cloneRepository($path, $url, array $args = array(), array $options = array()) + public static function cloneRepository($path, $url, array $args = [], array $options = []) { - $process = static::getProcess('clone', array_merge(array('-q'), $args, array($url, $path)), $options); + $process = static::getProcess('clone', array_merge(['-q'], $args, [$url, $path]), $options); $process->run(); @@ -144,19 +169,16 @@ public static function cloneRepository($path, $url, array $args = array(), array /** * This internal method is used to create a process object. */ - private static function getProcess($command, array $args = array(), array $options = array()) + private static function getProcess($command, array $args = [], array $options = []) { $is_windows = defined('PHP_WINDOWS_VERSION_BUILD'); - $options = array_merge(array( - 'environment_variables' => $is_windows ? array('PATH' => getenv('PATH')) : array(), - 'command' => 'git', - 'process_timeout' => 3600, - ), $options); - - $builder = ProcessBuilder::create(array_merge(array($options['command'], $command), $args)); - $builder->inheritEnvironmentVariables(false); + $options = array_merge([ + 'environment_variables' => $is_windows ? ['PATH' => getenv('PATH')] : [], + 'command' => 'git', + 'process_timeout' => 3600, + ], $options); - $process = $builder->getProcess(); + $process = new Process(array_merge([$options['command'], $command], $args)); $process->setEnv($options['environment_variables']); $process->setTimeout($options['process_timeout']); $process->setIdleTimeout($options['process_timeout']); diff --git a/src/Gitonomy/Git/Blame.php b/src/Gitonomy/Git/Blame.php index 7624bbbc..dec0773b 100644 --- a/src/Gitonomy/Git/Blame.php +++ b/src/Gitonomy/Git/Blame.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Blame\Line; @@ -83,33 +84,31 @@ public function getLine($number) */ public function getGroupedLines() { - $result = array(); + $result = []; $commit = null; - $current = array(); + $current = []; foreach ($this->getLines() as $lineNumber => $line) { if ($commit !== $line->getCommit()) { if (count($current)) { - $result[] = array($commit, $current); + $result[] = [$commit, $current]; } $commit = $line->getCommit(); - $current = array(); + $current = []; } $current[$lineNumber] = $line; } if (count($current)) { - $result[] = array($commit, $current); + $result[] = [$commit, $current]; } return $result; } /** - * Returns all lines of the blame. - * - * @return array + * @return Line[] All lines of the blame. */ public function getLines() { @@ -117,7 +116,7 @@ public function getLines() return $this->lines; } - $args = array('-p'); + $args = ['-p']; if (null !== $this->lineRange) { $args[] = '-L'; @@ -138,6 +137,7 @@ public function getLines() /** * @return int */ + #[\ReturnTypeWillChange] public function count() { return count($this->getLines()); diff --git a/src/Gitonomy/Git/Blame/Line.php b/src/Gitonomy/Git/Blame/Line.php index afeb0093..83a7a912 100644 --- a/src/Gitonomy/Git/Blame/Line.php +++ b/src/Gitonomy/Git/Blame/Line.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Blame; use Gitonomy\Git\Commit; diff --git a/src/Gitonomy/Git/Blob.php b/src/Gitonomy/Git/Blob.php index 53db15b8..dfe885fe 100644 --- a/src/Gitonomy/Git/Blob.php +++ b/src/Gitonomy/Git/Blob.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; /** @@ -18,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 */ @@ -38,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 @@ -57,14 +68,14 @@ 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() { if (null === $this->content) { - $this->content = $this->repository->run('cat-file', array('-p', $this->hash)); + $this->content = $this->repository->run('cat-file', ['-p', $this->hash]); } return $this->content; @@ -88,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() @@ -98,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 8715e515..573d3b52 100644 --- a/src/Gitonomy/Git/Commit.php +++ b/src/Gitonomy/Git/Commit.php @@ -9,12 +9,14 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Diff\Diff; 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; /** @@ -29,15 +31,15 @@ class Commit extends Revision * * @var array */ - private $data = array(); + private $data = []; /** * 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 = array()) + public function __construct(Repository $repository, $hash, array $data = []) { if (!preg_match('/^[a-f0-9]{40}$/', $hash)) { throw new ReferenceNotFoundException($hash); @@ -60,7 +62,7 @@ public function setData(array $data) */ public function getDiff() { - $args = array('-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); @@ -90,6 +92,8 @@ public function getShortHash() /** * Returns a fixed-with short hash. + * + * @return string Short hash */ public function getFixedShortHash($length = 6) { @@ -99,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() { @@ -109,11 +113,11 @@ public function getParentHashes() /** * Returns the parent commits. * - * @return array An array of Commit objects + * @return Commit[] An array of Commit objects */ public function getParents() { - $result = array(); + $result = []; foreach ($this->getData('parentHashes') as $parentHash) { $result[] = $this->repository->getCommit($parentHash); } @@ -131,6 +135,9 @@ public function getTreeHash() return $this->getData('treeHash'); } + /** + * @return Tree + */ public function getTree() { return $this->getData('tree'); @@ -149,7 +156,7 @@ public function getLastModification($path = null) $path = $getWorkingDir.'/'.$path; } - $result = $this->repository->run('log', array('--format=%H', '-n', 1, $this->revision, '--', $path)); + $result = $this->repository->run('log', ['--format=%H', '-n', 1, $this->revision, '--', $path]); return $this->repository->getCommit(trim($result)); } @@ -183,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() { @@ -196,11 +203,11 @@ 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) { - $arguments = array('--contains', $this->revision); + $arguments = ['--contains', $this->revision]; if ($local && $remote) { $arguments[] = '-a'; @@ -213,20 +220,22 @@ public function getIncludingBranches($local = true, $remote = true) try { $result = $this->repository->run('branch', $arguments); } catch (ProcessException $e) { - return array(); + return []; } if (!$result) { - return array(); + return []; } $branchesName = explode("\n", trim(str_replace('*', '', $result))); - $branchesName = array_filter($branchesName, function ($v) { return false === StringHelper::strpos($v, '->');}); + $branchesName = array_filter($branchesName, function ($v) { + return false === StringHelper::strpos($v, '->'); + }); $branchesName = array_map('trim', $branchesName); $references = $this->repository->getReferences(); - $branches = array(); + $branches = []; foreach ($branchesName as $branchName) { if (false === $local) { $branches[] = $references->getRemoteBranch($branchName); @@ -263,7 +272,7 @@ public function getAuthorEmail() /** * Returns the authoring date. * - * @return DateTime A time object + * @return \DateTime A time object */ public function getAuthorDate() { @@ -293,7 +302,7 @@ public function getCommitterEmail() /** * Returns the authoring date. * - * @return DateTime A time object + * @return \DateTime A time object */ public function getCommitterDate() { @@ -331,7 +340,7 @@ public function getBodyMessage() } /** - * @inheritdoc + * {@inheritdoc} */ public function getCommit() { @@ -345,7 +354,7 @@ private function getData($name) } if ($name === 'shortHash') { - $this->data['shortHash'] = trim($this->repository->run('log', array('--abbrev-commit', '--format=%h', '-n', 1, $this->revision))); + $this->data['shortHash'] = trim($this->repository->run('log', ['--abbrev-commit', '--format=%h', '-n', 1, $this->revision])); return $this->data['shortHash']; } @@ -371,14 +380,15 @@ 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(); + try { - $result = $this->repository->run('cat-file', array('commit', $this->revision)); + $result = $this->repository->run('cat-file', ['commit', $this->revision]); } catch (ProcessException $e) { throw new ReferenceNotFoundException(sprintf('Can not find reference "%s"', $this->revision)); } diff --git a/src/Gitonomy/Git/CommitReference.php b/src/Gitonomy/Git/CommitReference.php index 1a247947..7be3d89c 100644 --- a/src/Gitonomy/Git/CommitReference.php +++ b/src/Gitonomy/Git/CommitReference.php @@ -9,10 +9,14 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; 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 180ca25b..7737ee8f 100644 --- a/src/Gitonomy/Git/Diff/Diff.php +++ b/src/Gitonomy/Git/Diff/Diff.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Diff; use Gitonomy\Git\Parser\DiffParser; @@ -22,7 +23,7 @@ class Diff { /** - * @var array + * @var File[] */ protected $files; @@ -61,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() { @@ -96,14 +89,15 @@ public function getRawDiff() */ public function toArray() { - return array( + return [ 'rawDiff' => $this->rawDiff, - 'files' => array_map( + 'files' => array_map( function (File $file) { return $file->toArray(); - }, $this->files + }, + $this->files ), - ); + ]; } /** @@ -119,7 +113,8 @@ public static function fromArray(array $array) array_map( function ($array) { return File::fromArray($array); - }, $array['files'] + }, + $array['files'] ), $array['rawDiff'] ); diff --git a/src/Gitonomy/Git/Diff/File.php b/src/Gitonomy/Git/Diff/File.php index 9fa36348..bfca155c 100644 --- a/src/Gitonomy/Git/Diff/File.php +++ b/src/Gitonomy/Git/Diff/File.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Diff; use Gitonomy\Git\Repository; @@ -54,7 +55,7 @@ class File protected $isBinary; /** - * @var array An array of FileChange objects + * @var FileChange[] An array of FileChange objects */ protected $changes; @@ -76,7 +77,7 @@ public function __construct($oldName, $newName, $oldMode, $newMode, $oldIndex, $ $this->newIndex = $newIndex; $this->isBinary = $isBinary; - $this->changes = array(); + $this->changes = []; } public function addChange(FileChange $change) @@ -214,6 +215,9 @@ public function isBinary() return $this->isBinary; } + /** + * @return FileChange[] + */ public function getChanges() { return $this->changes; @@ -221,20 +225,23 @@ public function getChanges() public function toArray() { - return array( - 'old_name' => $this->oldName, - 'new_name' => $this->newName, - 'old_mode' => $this->oldMode, - 'new_mode' => $this->newMode, + return [ + 'old_name' => $this->oldName, + 'new_name' => $this->newName, + 'old_mode' => $this->oldMode, + 'new_mode' => $this->newMode, 'old_index' => $this->oldIndex, 'new_index' => $this->newIndex, 'is_binary' => $this->isBinary, - 'changes' => array_map(function (FileChange $change) { + 'changes' => array_map(function (FileChange $change) { return $change->toArray(); }, $this->changes), - ); + ]; } + /** + * @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']); @@ -271,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); } @@ -284,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 e065be68..cca18dc6 100644 --- a/src/Gitonomy/Git/Diff/FileChange.php +++ b/src/Gitonomy/Git/Diff/FileChange.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Diff; class FileChange @@ -23,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; @@ -32,54 +42,80 @@ public function __construct($rangeOldStart, $rangeOldCount, $rangeNewStart, $ran $this->lines = $lines; } + /** + * @return int + */ public function getCount($type) { $result = 0; foreach ($this->lines as $line) { if ($line[0] === $type) { - ++$result; + $result++; } } 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 array( + return [ 'range_old_start' => $this->rangeOldStart, 'range_old_count' => $this->rangeOldCount, 'range_new_start' => $this->rangeNewStart, 'range_new_count' => $this->rangeNewCount, - 'lines' => $this->lines, - ); + 'lines' => $this->lines, + ]; } + /** + * @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/Exception/GitExceptionInterface.php b/src/Gitonomy/Git/Exception/GitExceptionInterface.php index 1c51e1c9..6e7e3898 100644 --- a/src/Gitonomy/Git/Exception/GitExceptionInterface.php +++ b/src/Gitonomy/Git/Exception/GitExceptionInterface.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Exception; interface GitExceptionInterface diff --git a/src/Gitonomy/Git/Exception/ProcessException.php b/src/Gitonomy/Git/Exception/ProcessException.php index 604cecb0..6d6b6912 100644 --- a/src/Gitonomy/Git/Exception/ProcessException.php +++ b/src/Gitonomy/Git/Exception/ProcessException.php @@ -10,7 +10,8 @@ class ProcessException extends RuntimeException implements GitExceptionInterface public function __construct(Process $process) { - parent::__construct("Error while running git command:\n". + parent::__construct( + "Error while running git command:\n". $process->getCommandLine()."\n". "\n". $process->getErrorOutput()."\n". diff --git a/src/Gitonomy/Git/Exception/ReferenceNotFoundException.php b/src/Gitonomy/Git/Exception/ReferenceNotFoundException.php index f213acfd..dd9ae98d 100644 --- a/src/Gitonomy/Git/Exception/ReferenceNotFoundException.php +++ b/src/Gitonomy/Git/Exception/ReferenceNotFoundException.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Exception; class ReferenceNotFoundException extends \InvalidArgumentException implements GitExceptionInterface diff --git a/src/Gitonomy/Git/Hooks.php b/src/Gitonomy/Git/Hooks.php index 8cd01e75..dd0adbe8 100644 --- a/src/Gitonomy/Git/Hooks.php +++ b/src/Gitonomy/Git/Hooks.php @@ -9,10 +9,12 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Exception\InvalidArgumentException; use Gitonomy\Git\Exception\LogicException; +use Gitonomy\Git\Exception\RuntimeException; /** * Hooks collection, aggregated by repository. @@ -22,7 +24,7 @@ class Hooks { /** - * @var Gitonomy\Git\Repository + * @var \Gitonomy\Git\Repository */ protected $repository; @@ -51,9 +53,9 @@ public function has($name) * * @param string $name Name of the hook * - * @return string Content of the hook - * * @throws InvalidArgumentException Hook does not exist + * + * @return string Content of the hook */ public function get($name) { @@ -81,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)); } } @@ -120,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 67506e9a..47665359 100644 --- a/src/Gitonomy/Git/Log.php +++ b/src/Gitonomy/Git/Log.php @@ -9,8 +9,10 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; +use Gitonomy\Git\Diff\Diff; use Gitonomy\Git\Exception\ProcessException; use Gitonomy\Git\Exception\ReferenceNotFoundException; use Gitonomy\Git\Util\StringHelper; @@ -26,7 +28,7 @@ class Log implements \Countable, \IteratorAggregate protected $repository; /** - * @var array + * @var null|RevisionList */ protected $revisions; @@ -48,11 +50,11 @@ class Log implements \Countable, \IteratorAggregate /** * Instanciates a git log object. * - * @param Repository $repository the repository where log occurs - * @param RevisionList $revisions a list of revisions or null if you want all history - * @param array $paths paths to filter on - * @param int|null $offset start list from a given position - * @param int|null $limit limit number of fetched elements + * @param Repository $repository the repository where log occurs + * @param RevisionList|Revision|array|null $revisions a list of revisions or null if you want all history + * @param array $paths paths to filter on + * @param int|null $offset start list from a given position + * @param int|null $limit limit number of fetched elements */ public function __construct(Repository $repository, $revisions = null, $paths = null, $offset = null, $limit = null) { @@ -61,9 +63,9 @@ public function __construct(Repository $repository, $revisions = null, $paths = } if (null === $paths) { - $paths = array(); + $paths = []; } elseif (is_string($paths)) { - $paths = array($paths); + $paths = [$paths]; } elseif (!is_array($paths)) { throw new \InvalidArgumentException(sprintf('Expected a string or an array, got a "%s".', is_object($paths) ? get_class($paths) : gettype($paths))); } @@ -135,6 +137,9 @@ public function setLimit($limit) return $this; } + /** + * @return Commit + */ public function getSingleCommit() { $limit = $this->limit; @@ -150,11 +155,11 @@ public function getSingleCommit() } /** - * @return array + * @return Commit[] */ public function getCommits() { - $args = array('--encoding='.StringHelper::getEncoding(), '--format=raw'); + $args = ['--encoding='.StringHelper::getEncoding(), '--format=raw']; if (null !== $this->offset) { $args[] = '--skip='.((int) $this->offset); @@ -184,7 +189,7 @@ public function getCommits() $parser = new Parser\LogParser(); $parser->parse($output); - $result = array(); + $result = []; foreach ($parser->log as $commitData) { $hash = $commitData['id']; unset($commitData['id']); @@ -201,6 +206,7 @@ public function getCommits() /** * @see Countable */ + #[\ReturnTypeWillChange] public function count() { return $this->countCommits(); @@ -209,6 +215,7 @@ public function count() /** * @see IteratorAggregate */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->getCommits()); @@ -221,10 +228,10 @@ public function getIterator() */ public function countCommits() { - if (count($this->revisions)) { - $output = $this->repository->run('rev-list', array_merge(array('--count'), $this->revisions->getAsTextArray(), array('--'), $this->paths)); + if (null !== $this->revisions && count($this->revisions)) { + $output = $this->repository->run('rev-list', array_merge(['--count'], $this->revisions->getAsTextArray(), ['--'], $this->paths)); } else { - $output = $this->repository->run('rev-list', array_merge(array('--count', '--all', '--'), $this->paths)); + $output = $this->repository->run('rev-list', array_merge(['--count', '--all', '--'], $this->paths)); } return (int) $output; diff --git a/src/Gitonomy/Git/Parser/BlameParser.php b/src/Gitonomy/Git/Parser/BlameParser.php index dcbe3a7b..8dd6c570 100644 --- a/src/Gitonomy/Git/Parser/BlameParser.php +++ b/src/Gitonomy/Git/Parser/BlameParser.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Parser; use Gitonomy\Git\Blame\Line; @@ -27,9 +28,9 @@ public function __construct(Repository $repository) protected function doParse() { - $this->lines = array(); + $this->lines = []; - $memory = array(); + $memory = []; $line = 1; while (!$this->isFinished()) { @@ -43,9 +44,9 @@ protected function doParse() $this->consumeNewLine(); if (!isset($memory[$hash])) { - foreach (array('author', 'author-mail', 'author-time', 'author-tz', + foreach (['author', 'author-mail', 'author-time', 'author-tz', 'committer', 'committer-mail', 'committer-time', 'committer-tz', - 'summary', ) as $key) { + 'summary', ] as $key) { $this->consume($key); $this->consumeTo("\n"); $this->consumeNewLine(); @@ -68,7 +69,7 @@ protected function doParse() $this->consumeNewLine(); $this->lines[$line] = new Line($memory[$hash], $sourceLine, $targetLine, $blockLine, $content); - ++$line; + $line++; } } } diff --git a/src/Gitonomy/Git/Parser/CommitParser.php b/src/Gitonomy/Git/Parser/CommitParser.php index eba71e48..6fff1ea4 100644 --- a/src/Gitonomy/Git/Parser/CommitParser.php +++ b/src/Gitonomy/Git/Parser/CommitParser.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Parser; use Gitonomy\Git\Exception\RuntimeException; @@ -31,7 +32,7 @@ protected function doParse() $this->tree = $this->consumeHash(); $this->consumeNewLine(); - $this->parents = array(); + $this->parents = []; while ($this->expects('parent ')) { $this->parents[] = $this->consumeHash(); $this->consumeNewLine(); @@ -43,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(); @@ -63,7 +66,7 @@ protected function consumeNameEmailDate() $this->cursor += strlen($vars[1]); - return array($vars[2], $vars[3], $vars[4]); + return [$vars[2], $vars[3], $vars[4]]; } protected function parseDate($text) diff --git a/src/Gitonomy/Git/Parser/DiffParser.php b/src/Gitonomy/Git/Parser/DiffParser.php index c9f011ba..15e6c037 100644 --- a/src/Gitonomy/Git/Parser/DiffParser.php +++ b/src/Gitonomy/Git/Parser/DiffParser.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Parser; use Gitonomy\Git\Diff\File; @@ -20,15 +21,32 @@ class DiffParser extends ParserBase protected function doParse() { - $this->files = array(); + $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; @@ -37,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"); @@ -48,6 +67,7 @@ protected function doParse() if ($this->expects('deleted file mode ')) { $oldMode = $this->consumeTo("\n"); $newMode = null; + $newName = '/dev/null'; $this->consumeNewLine(); } @@ -64,23 +84,24 @@ protected function doParse() // 4. File informations $isBinary = false; if ($this->expects('index ')) { - $oldIndex = $this->consumeHash(); + $oldIndex = $this->consumeShortHash(); $this->consume('..'); - $newIndex = $this->consumeHash(); + $newIndex = $this->consumeShortHash(); if ($this->expects(' ')) { $vars = $this->consumeRegexp('/\d{6}/'); $newMode = $oldMode = $vars[0]; } $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]; @@ -89,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); @@ -96,23 +120,23 @@ 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(); // 6. Lines - $lines = array(); + $lines = []; while (true) { if ($this->expects(' ')) { - $lines[] = array(FileChange::LINE_CONTEXT, $this->consumeTo("\n")); + $lines[] = [FileChange::LINE_CONTEXT, $this->consumeTo("\n")]; } elseif ($this->expects('+')) { - $lines[] = array(FileChange::LINE_ADD, $this->consumeTo("\n")); + $lines[] = [FileChange::LINE_ADD, $this->consumeTo("\n")]; } elseif ($this->expects('-')) { - $lines[] = array(FileChange::LINE_REMOVE, $this->consumeTo("\n")); + $lines[] = [FileChange::LINE_REMOVE, $this->consumeTo("\n")]; } elseif ($this->expects("\ No newline at end of file")) { // Ignore this case... } else { diff --git a/src/Gitonomy/Git/Parser/LogParser.php b/src/Gitonomy/Git/Parser/LogParser.php index a675f2a2..67b86687 100644 --- a/src/Gitonomy/Git/Parser/LogParser.php +++ b/src/Gitonomy/Git/Parser/LogParser.php @@ -9,18 +9,19 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Parser; class LogParser extends CommitParser { - public $log = array(); + public $log = []; protected function doParse() { - $this->log = array(); + $this->log = []; while (!$this->isFinished()) { - $commit = array(); + $commit = []; $this->consume('commit '); $commit['id'] = $this->consumeHash(); $this->consumeNewLine(); @@ -29,31 +30,42 @@ protected function doParse() $commit['treeHash'] = $this->consumeHash(); $this->consumeNewLine(); - $commit['parentHashes'] = array(); + $commit['parentHashes'] = []; while ($this->expects('parent ')) { $commit['parentHashes'][] = $this->consumeHash(); $this->consumeNewLine(); } $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 = ''; - while ($this->expects(' ')) { - $message .= $this->consumeTo("\n")."\n"; - $this->consumeNewLine(); + if ($this->expects(' ')) { + $this->cursor -= strlen(' '); + + while ($this->expects(' ')) { + $message .= $this->consumeTo("\n")."\n"; + $this->consumeNewLine(); + } + } else { + $this->cursor--; } if (!$this->isFinished()) { @@ -65,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 aebd4e68..45a80020 100644 --- a/src/Gitonomy/Git/Parser/ParserBase.php +++ b/src/Gitonomy/Git/Parser/ParserBase.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Parser; use Gitonomy\Git\Exception\RuntimeException; @@ -24,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(); @@ -58,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)); } @@ -69,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)); } @@ -80,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]); @@ -123,15 +124,30 @@ protected function consumeNewLine() /** * @return string */ - protected function consumeGPGSignature() { + protected function consumeGPGSignature() + { $expected = "\ngpgsig "; $length = strlen($expected); $actual = substr($this->content, $this->cursor, $length); - if($actual != $expected) { + if ($actual != $expected) { return ''; } $this->cursor += $length; 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/ReferenceParser.php b/src/Gitonomy/Git/Parser/ReferenceParser.php index d7a317cd..82e3a8b5 100644 --- a/src/Gitonomy/Git/Parser/ReferenceParser.php +++ b/src/Gitonomy/Git/Parser/ReferenceParser.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Parser; class ReferenceParser extends ParserBase @@ -17,14 +18,14 @@ class ReferenceParser extends ParserBase protected function doParse() { - $this->references = array(); + $this->references = []; while (!$this->isFinished()) { $hash = $this->consumeHash(); $this->consume(' '); $name = $this->consumeTo("\n"); $this->consumeNewLine(); - $this->references[] = array($hash, $name); + $this->references[] = [$hash, $name]; } } } diff --git a/src/Gitonomy/Git/Parser/TagParser.php b/src/Gitonomy/Git/Parser/TagParser.php new file mode 100644 index 00000000..1a1c084c --- /dev/null +++ b/src/Gitonomy/Git/Parser/TagParser.php @@ -0,0 +1,91 @@ + + * (c) Julien DIDIER + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Gitonomy\Git\Parser; + +use Gitonomy\Git\Exception\RuntimeException; + +class TagParser extends ParserBase +{ + public $object; + public $type; + public $tag; + public $taggerName; + public $taggerEmail; + public $taggerDate; + public $gpgSignature; + public $message; + + protected function doParse() + { + $this->consume('object '); + $this->object = $this->consumeHash(); + $this->consumeNewLine(); + + $this->consume('type '); + $this->type = $this->consumeTo("\n"); + $this->consumeNewLine(); + + $this->consume('tag '); + $this->tag = $this->consumeTo("\n"); + $this->consumeNewLine(); + + $this->consume('tagger '); + list($this->taggerName, $this->taggerEmail, $taggerDate) = $this->consumeNameEmailDate(); + $this->taggerDate = $this->parseDate($taggerDate); + + $this->consumeNewLine(); + $this->consumeNewLine(); + + try { + $this->message = $this->consumeTo('-----BEGIN PGP SIGNATURE-----'); + $this->gpgSignature = $this->consumeGPGSignature(); + } catch (RuntimeException $e) { + $this->message = $this->consumeAll(); + } + } + + protected function consumeGPGSignature() + { + $expected = '-----BEGIN PGP SIGNATURE-----'; + $length = strlen($expected); + $actual = substr($this->content, $this->cursor, $length); + if ($actual != $expected) { + return ''; + } + $this->cursor += $length; + + return $this->consumeTo('-----END PGP SIGNATURE-----'); + } + + protected function consumeNameEmailDate() + { + if (!preg_match('/(([^\n]*) <([^\n]*)> (\d+ [+-]\d{4}))/A', $this->content, $vars, 0, $this->cursor)) { + throw new RuntimeException('Unable to parse name, email and date'); + } + + $this->cursor += strlen($vars[1]); + + return [$vars[2], $vars[3], $vars[4]]; + } + + protected function parseDate($text) + { + $date = \DateTime::createFromFormat('U e O', $text.' UTC'); + + if (!$date instanceof \DateTime) { + throw new RuntimeException(sprintf('Unable to convert "%s" to datetime', $text)); + } + + return $date; + } +} diff --git a/src/Gitonomy/Git/Parser/TreeParser.php b/src/Gitonomy/Git/Parser/TreeParser.php index aa0dd295..d01f4b28 100644 --- a/src/Gitonomy/Git/Parser/TreeParser.php +++ b/src/Gitonomy/Git/Parser/TreeParser.php @@ -9,11 +9,12 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Parser; class TreeParser extends ParserBase { - public $entries = array(); + public $entries = []; protected function doParse() { @@ -32,7 +33,7 @@ protected function doParse() $name = $this->consumeTo("\n"); $this->consumeNewLine(); - $this->entries[] = array($mode, $type, $hash, $name); + $this->entries[] = [$mode, $type, $hash, $name]; } } } diff --git a/src/Gitonomy/Git/PushReference.php b/src/Gitonomy/Git/PushReference.php index ce7b859f..dea10823 100644 --- a/src/Gitonomy/Git/PushReference.php +++ b/src/Gitonomy/Git/PushReference.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Exception\LogicException; @@ -23,6 +24,10 @@ class PushReference { const ZERO = '0000000000000000000000000000000000000000'; + /** + * @var Repository + */ + protected $repository; /** * @var string */ @@ -85,18 +90,21 @@ public function getAfter() } /** - * @return array + * @return Log */ - public function getLog($excludes = array()) + public function getLog($excludes = []) { return $this->repository->getLog(array_merge( - array($this->getRevision()), + [$this->getRevision()], array_map(function ($e) { return '^'.$e; }, $excludes) )); } + /** + * @return string + */ public function getRevision() { if ($this->isDelete()) { @@ -159,10 +167,10 @@ protected function getForce() return false; } - $result = $this->repository->run('merge-base', array( + $result = $this->repository->run('merge-base', [ $this->before, $this->after, - )); + ]); return $this->before !== trim($result); } diff --git a/src/Gitonomy/Git/Reference.php b/src/Gitonomy/Git/Reference.php index 85fc6891..96b8fa12 100644 --- a/src/Gitonomy/Git/Reference.php +++ b/src/Gitonomy/Git/Reference.php @@ -9,8 +9,12 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; +use Gitonomy\Git\Exception\ProcessException; +use Gitonomy\Git\Exception\ReferenceNotFoundException; + /** * Reference in a Git repository. * @@ -28,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) { @@ -45,7 +58,7 @@ public function getCommitHash() } try { - $result = $this->repository->run('rev-parse', array('--verify', $this->revision)); + $result = $this->repository->run('rev-parse', ['--verify', $this->revision]); } catch (ProcessException $e) { throw new ReferenceNotFoundException(sprintf('Can not find revision "%s"', $this->revision)); } @@ -54,15 +67,16 @@ public function getCommitHash() } /** - * Returns the commit associated to the reference. - * - * @return Gitonomy\Git\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 df837e73..0d27fb88 100644 --- a/src/Gitonomy/Git/Reference/Branch.php +++ b/src/Gitonomy/Git/Reference/Branch.php @@ -9,10 +9,13 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + 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. @@ -52,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/Stash.php b/src/Gitonomy/Git/Reference/Stash.php index fb2214e6..9f26802f 100644 --- a/src/Gitonomy/Git/Reference/Stash.php +++ b/src/Gitonomy/Git/Reference/Stash.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Reference; use Gitonomy\Git\Reference; diff --git a/src/Gitonomy/Git/Reference/Tag.php b/src/Gitonomy/Git/Reference/Tag.php index 8c529f9b..78c43b25 100644 --- a/src/Gitonomy/Git/Reference/Tag.php +++ b/src/Gitonomy/Git/Reference/Tag.php @@ -9,18 +9,26 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Reference; +use Gitonomy\Git\Commit; +use Gitonomy\Git\Exception\ProcessException; use Gitonomy\Git\Exception\RuntimeException; +use Gitonomy\Git\Parser\ReferenceParser; +use Gitonomy\Git\Parser\TagParser; use Gitonomy\Git\Reference; /** * Representation of a tag reference. * * @author Alexandre Salomé + * @author Bruce Wells */ class Tag extends Reference { + protected $data; + public function getName() { if (!preg_match('#^refs/tags/(.*)$#', $this->revision, $vars)) { @@ -29,4 +37,180 @@ public function getName() return $vars[1]; } + + /** + * Check if tag is annotated. + * + * @return bool + */ + public function isAnnotated() + { + try { + $this->repository->run('cat-file', ['tag', $this->revision]); + } catch (ProcessException $e) { + return false; // Is not an annotated tag + } + + return true; + } + + /** + * Returns the actual commit associated with the tag, and not the hash of the tag if annotated. + * + * @return Commit + */ + public function getCommit() + { + if ($this->isAnnotated()) { + try { + $output = $this->repository->run('show-ref', ['-d', '--tag', $this->revision]); + $parser = new ReferenceParser(); + $parser->parse($output); + + foreach ($parser->references as list($row)) { + $commitHash = $row; + } + + return $this->repository->getCommit($commitHash); + } catch (ProcessException $e) { + // ignore the exception + } + } + + return parent::getCommit(); + } + + /** + * Returns the tagger name. + * + * @return string A name + */ + public function getTaggerName() + { + return $this->getData('taggerName'); + } + + /** + * Returns the comitter email. + * + * @return string An email + */ + public function getTaggerEmail() + { + return $this->getData('taggerEmail'); + } + + /** + * Returns the authoring date. + * + * @return \DateTime A time object + */ + public function getTaggerDate() + { + return $this->getData('taggerDate'); + } + + /** + * Returns the message of the commit. + * + * @return string A tag message + */ + public function getMessage() + { + return $this->getData('message'); + } + + /** + * Returns the subject message (the first line). + * + * @return string The subject message + */ + public function getSubjectMessage() + { + return $this->getData('subjectMessage'); + } + + /** + * Return the body message. + * + * @return string The body message + */ + public function getBodyMessage() + { + return $this->getData('bodyMessage'); + } + + /** + * Return the GPG signature. + * + * @return string The GPG signature + */ + public function getGPGSignature() + { + return $this->getData('gpgSignature'); + } + + /** + * Check whether tag is signed. + * + * @return bool + */ + public function isSigned() + { + try { + $this->getGPGSignature(); + + return true; + } catch (\InvalidArgumentException $e) { + return false; + } + } + + private function getData($name) + { + if (!$this->isAnnotated()) { + return false; + } + + if (isset($this->data[$name])) { + return $this->data[$name]; + } + + if ($name === 'subjectMessage') { + $lines = explode("\n", $this->getData('message')); + $this->data['subjectMessage'] = reset($lines); + + return $this->data['subjectMessage']; + } + + if ($name === 'bodyMessage') { + $message = $this->getData('message'); + + $lines = explode("\n", $message); + + array_shift($lines); + array_pop($lines); + + $this->data['bodyMessage'] = implode("\n", $lines); + + return $this->data['bodyMessage']; + } + + $parser = new TagParser(); + $result = $this->repository->run('cat-file', ['tag', $this->revision]); + + $parser->parse($result); + + $this->data['taggerName'] = $parser->taggerName; + $this->data['taggerEmail'] = $parser->taggerEmail; + $this->data['taggerDate'] = $parser->taggerDate; + $this->data['message'] = $parser->message; + $this->data['gpgSignature'] = $parser->gpgSignature; + + if (!isset($this->data[$name])) { + throw new \InvalidArgumentException(sprintf('No data named "%s" in Tag.', $name)); + } + + return $this->data[$name]; + } } diff --git a/src/Gitonomy/Git/ReferenceBag.php b/src/Gitonomy/Git/ReferenceBag.php index f7ca9a7b..71ef9ae4 100644 --- a/src/Gitonomy/Git/ReferenceBag.php +++ b/src/Gitonomy/Git/ReferenceBag.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Exception\ReferenceNotFoundException; @@ -28,7 +29,7 @@ class ReferenceBag implements \Countable, \IteratorAggregate /** * Repository object. * - * @var Gitonomy\Git\Repository + * @var Repository */ protected $repository; @@ -42,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; @@ -63,14 +64,14 @@ class ReferenceBag implements \Countable, \IteratorAggregate /** * Constructor. * - * @param Gitonomy\Git\Repository $repository The repository + * @param Repository $repository The repository */ public function __construct($repository) { $this->repository = $repository; - $this->references = array(); - $this->tags = array(); - $this->branches = array(); + $this->references = []; + $this->tags = []; + $this->branches = []; } /** @@ -78,7 +79,7 @@ public function __construct($repository) * * @param string $fullname Fullname of the reference (refs/heads/master, for example). * - * @return Gitonomy\Git\Reference A reference object. + * @return Reference A reference object. */ public function get($fullname) { @@ -91,6 +92,9 @@ public function get($fullname) return $this->references[$fullname]; } + /** + * @return bool + */ public function has($fullname) { $this->initialize(); @@ -98,18 +102,24 @@ public function has($fullname) return isset($this->references[$fullname]); } + /** + * @return Reference + */ public function update(Reference $reference) { $fullname = $reference->getFullname(); $this->initialize(); - $this->repository->run('update-ref', array($fullname, $reference->getCommitHash())); + $this->repository->run('update-ref', [$fullname, $reference->getCommitHash()]); $this->references[$fullname] = $reference; return $reference; } + /** + * @return Reference + */ public function createBranch($name, $commitHash) { $branch = new Branch($this->repository, 'refs/heads/'.$name, $commitHash); @@ -117,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); @@ -124,13 +137,19 @@ public function createTag($name, $commitHash) return $this->update($tag); } + /** + * @return void + */ public function delete($fullname) { - $this->repository->run('update-ref', array('-d', $fullname)); + $this->repository->run('update-ref', ['-d', $fullname]); unset($this->references[$fullname]); } + /** + * @return bool + */ public function hasBranches() { $this->initialize(); @@ -153,6 +172,9 @@ public function hasTag($name) return $this->has('refs/tags/'.$name); } + /** + * @return Branch + */ public function getFirstBranch() { $this->initialize(); @@ -162,7 +184,7 @@ public function getFirstBranch() } /** - * @return array An array of Tag objects + * @return Tag[] An array of Tag objects */ public function resolveTags($hash) { @@ -172,7 +194,7 @@ public function resolveTags($hash) $hash = $hash->getHash(); } - $tags = array(); + $tags = []; foreach ($this->references as $reference) { if ($reference instanceof Reference\Tag && $reference->getCommitHash() === $hash) { $tags[] = $reference; @@ -183,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) { @@ -193,7 +215,7 @@ public function resolveBranches($hash) $hash = $hash->getHash(); } - $branches = array(); + $branches = []; foreach ($this->references as $reference) { if ($reference instanceof Reference\Branch && $reference->getCommitHash() === $hash) { $branches[] = $reference; @@ -204,7 +226,7 @@ public function resolveBranches($hash) } /** - * @return array An array of references + * @return Reference[] An array of references */ public function resolve($hash) { @@ -214,7 +236,7 @@ public function resolve($hash) $hash = $hash->getHash(); } - $result = array(); + $result = []; foreach ($this->references as $k => $reference) { if ($reference->getCommitHash() === $hash) { $result[] = $reference; @@ -225,9 +247,7 @@ public function resolve($hash) } /** - * Returns all tags. - * - * @return array + * @return Tag[] All tags. */ public function getTags() { @@ -237,15 +257,13 @@ public function getTags() } /** - * Returns all branches. - * - * @return array + * @return Branch[] All branches. */ public function getBranches() { $this->initialize(); - $result = array(); + $result = []; foreach ($this->references as $reference) { if ($reference instanceof Reference\Branch) { $result[] = $reference; @@ -256,13 +274,11 @@ public function getBranches() } /** - * Returns all locales branches. - * - * @return array + * @return Branch[] All local branches. */ public function getLocalBranches() { - $result = array(); + $result = []; foreach ($this->getBranches() as $branch) { if ($branch->isLocal()) { $result[] = $branch; @@ -273,13 +289,11 @@ public function getLocalBranches() } /** - * Returns all remote branches. - * - * @return array + * @return Branch[] All remote branches. */ public function getRemoteBranches() { - $result = array(); + $result = []; foreach ($this->getBranches() as $branch) { if ($branch->isRemote()) { $result[] = $branch; @@ -340,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); @@ -365,10 +375,6 @@ protected function initialize() } elseif ($fullname === 'refs/stash') { $reference = new Stash($this->repository, $fullname, $commitHash); $this->references[$fullname] = $reference; - } elseif (preg_match('#^refs/pull/(.*)$#', $fullname)) { - // Do nothing here - } else { - throw new RuntimeException(sprintf('Unable to parse "%s"', $fullname)); } } } @@ -378,6 +384,7 @@ protected function initialize() * * @see Countable */ + #[\ReturnTypeWillChange] public function count() { $this->initialize(); @@ -388,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 47305ea5..296496ae 100644 --- a/src/Gitonomy/Git/Repository.php +++ b/src/Gitonomy/Git/Repository.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Diff\Diff; @@ -17,7 +18,6 @@ use Gitonomy\Git\Exception\RuntimeException; use Psr\Log\LoggerInterface; use Symfony\Component\Process\Process; -use Symfony\Component\Process\ProcessBuilder; /** * Git repository object. @@ -86,6 +86,11 @@ class Repository */ protected $environmentVariables; + /** + * @var bool + */ + protected $inheritEnvironmentVariables; + /** * Timeout that should be set for every running process. * @@ -111,17 +116,17 @@ class Repository * * @throws InvalidArgumentException The folder does not exists */ - public function __construct($dir, $options = array()) + public function __construct($dir, $options = []) { - $is_windows = defined('PHP_WINDOWS_VERSION_BUILD'); - $options = array_merge(array( - 'working_dir' => null, - 'debug' => true, - 'logger' => null, - 'environment_variables' => $is_windows ? array('PATH' => getenv('path')) : array(), - 'command' => 'git', - 'process_timeout' => 3600, - ), $options); + $options = array_merge([ + 'working_dir' => null, + 'debug' => true, + 'logger' => null, + 'command' => 'git', + 'environment_variables' => [], + 'inherit_environment_variables' => false, + 'process_timeout' => 3600, + ], $options); if (null !== $options['logger'] && !$options['logger'] instanceof LoggerInterface) { throw new InvalidArgumentException(sprintf('Argument "logger" passed to Repository should be a Psr\Log\LoggerInterface. A %s was provided', is_object($options['logger']) ? get_class($options['logger']) : gettype($options['logger']))); @@ -130,11 +135,17 @@ public function __construct($dir, $options = array()) $this->logger = $options['logger']; $this->initDir($dir, $options['working_dir']); - $this->objects = array(); + $this->objects = []; + $this->command = $options['command']; $this->debug = (bool) $options['debug']; - $this->environmentVariables = $options['environment_variables']; $this->processTimeout = $options['process_timeout']; - $this->command = $options['command']; + + if (defined('PHP_WINDOWS_VERSION_BUILD') && isset($_SERVER['PATH']) && !isset($options['environment_variables']['PATH'])) { + $options['environment_variables']['PATH'] = $_SERVER['PATH']; + } + + $this->environmentVariables = $options['environment_variables']; + $this->inheritEnvironmentVariables = $options['inherit_environment_variables']; if (true === $this->debug && null !== $this->logger) { $this->logger->debug(sprintf('Repository created (git dir: "%s", working dir: "%s")', $this->gitDir, $this->workingDir ?: 'none')); @@ -153,7 +164,7 @@ private function initDir($gitDir, $workingDir = null) if (false === $realGitDir) { throw new InvalidArgumentException(sprintf('Directory "%s" does not exist or is not a directory', $gitDir)); - } else if (!is_dir($realGitDir)) { + } elseif (!is_dir($realGitDir)) { throw new InvalidArgumentException(sprintf('Directory "%s" does not exist or is not a directory', $realGitDir)); } elseif (null === $workingDir && is_dir($realGitDir.'/.git')) { $workingDir = $realGitDir; @@ -304,11 +315,13 @@ public function getWorkingCopy() /** * Returns the reference list associated to the repository. * + * @param bool $reload Reload references from the filesystem + * * @return ReferenceBag */ - public function getReferences() + public function getReferences($reload = false) { - if (null === $this->referenceBag) { + if (null === $this->referenceBag || $reload) { $this->referenceBag = new ReferenceBag($this); } @@ -363,6 +376,9 @@ public function getBlob($hash) return $this->objects[$hash]; } + /** + * @return Blame + */ public function getBlame($revision, $file, $lineRange = null) { if (is_string($revision)) { @@ -400,7 +416,7 @@ public function getDiff($revisions) $revisions = new RevisionList($this, $revisions); } - $args = array_merge(array('-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); @@ -412,30 +428,18 @@ public function getDiff($revisions) * Returns the size of repository, in kilobytes. * * @return int A sum, in kilobytes - * - * @throws RuntimeException An error occurred while computing size */ public function getSize() { - $process = ProcessBuilder::create(array('du', '-skc', $this->gitDir))->getProcess(); - - $process->run(); - - if (!preg_match('/(\d+)\s+total$/', trim($process->getOutput()), $vars)) { - $message = sprintf("Unable to parse process output\ncommand: %s\noutput: %s", $process->getCommandLine(), $process->getOutput()); - - if (null !== $this->logger) { - $this->logger->error($message); + $totalBytes = 0; + $path = realpath($this->gitDir); + if ($path && file_exists($path)) { + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)) as $object) { + $totalBytes += $object->getSize(); } - - if (true === $this->debug) { - throw new RuntimeException('unable to parse repository size output'); - } - - return; } - return $vars[1]; + return (int) ($totalBytes / 1000 + 0.5); } /** @@ -443,7 +447,7 @@ public function getSize() * * @param string $command The command to execute */ - public function shell($command, array $env = array()) + public function shell($command, array $env = []) { $argument = sprintf('%s \'%s\'', $command, $this->gitDir); @@ -452,7 +456,7 @@ public function shell($command, array $env = array()) $prefix .= sprintf('export %s=%s;', escapeshellarg($name), escapeshellarg($value)); } - proc_open($prefix.'git shell -c '.escapeshellarg($argument), array(STDIN, STDOUT, STDERR), $pipes); + proc_open($prefix.'git shell -c '.escapeshellarg($argument), [STDIN, STDOUT, STDERR], $pipes); } /** @@ -524,11 +528,11 @@ public function setDescription($description) * @param string $command Git command to run (checkout, branch, tag) * @param array $args Arguments of git command * - * @return string Output of a successful process or null if execution failed and debug-mode is disabled. - * * @throws RuntimeException Error while executing git command (debug-mode only) + * + * @return string Output of a successful process or null if execution failed and debug-mode is disabled. */ - public function run($command, $args = array()) + public function run($command, $args = []) { $process = $this->getProcess($command, $args); @@ -597,7 +601,7 @@ public function getLogger() * * @return Repository the newly created repository */ - public function cloneTo($path, $bare = true, array $options = array()) + public function cloneTo($path, $bare = true, array $options = []) { return Admin::cloneTo($path, $this->gitDir, $bare, $options); } @@ -610,20 +614,24 @@ public function cloneTo($path, $bare = true, array $options = array()) * * @see self::run */ - private function getProcess($command, $args = array()) + private function getProcess($command, $args = []) { - $base = array($this->command, '--git-dir', $this->gitDir); + $base = [$this->command, '--git-dir', $this->gitDir]; if ($this->workingDir) { - $base = array_merge($base, array('--work-tree', $this->workingDir)); + $base = array_merge($base, ['--work-tree', $this->workingDir]); } $base[] = $command; - $builder = new ProcessBuilder(array_merge($base, $args)); - $builder->inheritEnvironmentVariables(false); - $process = $builder->getProcess(); - $process->setEnv($this->environmentVariables); + $process = new Process(array_merge($base, $args)); + + if ($this->inheritEnvironmentVariables) { + $process->setEnv(array_replace($_SERVER, $this->environmentVariables)); + } else { + $process->setEnv($this->environmentVariables); + } + $process->setTimeout($this->processTimeout); $process->setIdleTimeout($this->processTimeout); diff --git a/src/Gitonomy/Git/Revision.php b/src/Gitonomy/Git/Revision.php index 13e085bf..9e1921fb 100644 --- a/src/Gitonomy/Git/Revision.php +++ b/src/Gitonomy/Git/Revision.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; /** diff --git a/src/Gitonomy/Git/RevisionList.php b/src/Gitonomy/Git/RevisionList.php index 52bede40..1c1a1dcd 100644 --- a/src/Gitonomy/Git/RevisionList.php +++ b/src/Gitonomy/Git/RevisionList.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; /** @@ -26,9 +27,9 @@ class RevisionList implements \IteratorAggregate, \Countable public function __construct(Repository $repository, $revisions) { if (is_string($revisions)) { - $revisions = array($repository->getRevision($revisions)); + $revisions = [$repository->getRevision($revisions)]; } elseif ($revisions instanceof Revision) { - $revisions = array($revisions); + $revisions = [$revisions]; } elseif (!is_array($revisions)) { throw new \InvalidArgumentException(sprintf('Expected a string, a Revision or an array, got a "%s".', is_object($revisions) ? get_class($revisions) : gettype($revisions))); } @@ -48,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 4406b069..7830cfa4 100644 --- a/src/Gitonomy/Git/Tree.php +++ b/src/Gitonomy/Git/Tree.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Exception\InvalidArgumentException; @@ -23,6 +24,7 @@ class Tree protected $hash; protected $isInitialized = false; protected $entries; + protected $entriesByType; public function __construct(Repository $repository, $hash) { @@ -41,36 +43,73 @@ protected function initialize() return; } - $output = $this->repository->run('cat-file', array('-p', $this->hash)); + $output = $this->repository->run('cat-file', ['-p', $this->hash]); $parser = new Parser\TreeParser(); $parser->parse($output); - $this->entries = array(); + $this->entries = []; + $this->entriesByType = [ + 'blob' => [], + 'tree' => [], + 'commit' => [], + ]; foreach ($parser->entries as $entry) { list($mode, $type, $hash, $name) = $entry; if ($type == 'blob') { - $this->entries[$name] = array($mode, $this->repository->getBlob($hash)); + $treeEntry = [$mode, $this->repository->getBlob($hash)]; } elseif ($type == 'tree') { - $this->entries[$name] = array($mode, $this->repository->getTree($hash)); + $treeEntry = [$mode, $this->repository->getTree($hash)]; } else { - $this->entries[$name] = array($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(); @@ -95,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/Util/StringHelper.php b/src/Gitonomy/Git/Util/StringHelper.php index 36e533c9..d784b66b 100644 --- a/src/Gitonomy/Git/Util/StringHelper.php +++ b/src/Gitonomy/Git/Util/StringHelper.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Util; /** diff --git a/src/Gitonomy/Git/WorkingCopy.php b/src/Gitonomy/Git/WorkingCopy.php index 7cc06490..a94fbbb9 100644 --- a/src/Gitonomy/Git/WorkingCopy.php +++ b/src/Gitonomy/Git/WorkingCopy.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git; use Gitonomy\Git\Diff\Diff; @@ -34,23 +35,22 @@ public function __construct(Repository $repository) } } - public function getStatus() - { - return WorkingStatus::parseOutput(); - } - public function getUntrackedFiles() { - $lines = explode("\0", $this->run('status', array('--porcelain', '--untracked-files=all', '-z'))); - $lines = array_filter($lines, function ($l) { return substr($l, 0, 3) === '?? '; }); - $lines = array_map(function ($l) { return substr($l, 3); }, $lines); + $lines = explode("\0", $this->run('status', ['--porcelain', '--untracked-files=all', '-z'])); + $lines = array_filter($lines, function ($l) { + return substr($l, 0, 3) === '?? '; + }); + $lines = array_map(function ($l) { + return substr($l, 3); + }, $lines); return $lines; } public function getDiffPending() { - $diff = Diff::parse($this->run('diff', array('-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; @@ -58,7 +58,7 @@ public function getDiffPending() public function getDiffStaged() { - $diff = Diff::parse($this->run('diff', array('-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; @@ -69,7 +69,7 @@ public function getDiffStaged() */ public function checkout($revision, $branch = null) { - $args = array(); + $args = []; if ($revision instanceof Commit) { $args[] = $revision->getHash(); } elseif ($revision instanceof Reference) { @@ -81,7 +81,7 @@ public function checkout($revision, $branch = null) } if (null !== $branch) { - $args = array_merge($args, array('-b', $branch)); + $args = array_merge($args, ['-b', $branch]); } $this->run('checkout', $args); @@ -89,7 +89,7 @@ public function checkout($revision, $branch = null) return $this; } - protected function run($command, array $args = array()) + protected function run($command, array $args = []) { return $this->repository->run($command, $args); } diff --git a/tests/Gitonomy/Git/Tests/AbstractTest.php b/tests/Gitonomy/Git/Tests/AbstractTest.php index 22c8cb4c..0b9a51b6 100644 --- a/tests/Gitonomy/Git/Tests/AbstractTest.php +++ b/tests/Gitonomy/Git/Tests/AbstractTest.php @@ -9,15 +9,18 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\Admin; use Gitonomy\Git\Repository; +use PHPUnit\Framework\TestCase; -abstract class AbstractTest extends \PHPUnit_Framework_TestCase +abstract class AbstractTest extends TestCase { - const REPOSITORY_URL = 'http://github.com/gitonomy/foobar.git'; + 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'; @@ -50,10 +53,10 @@ public static function createEmptyRepository($bare = true) */ public static function provideFoobar() { - return array( - array(self::createFoobarRepository()), - array(self::createFoobarRepository(false)), - ); + return [ + [self::createFoobarRepository()], + [self::createFoobarRepository(false)], + ]; } /** @@ -61,10 +64,10 @@ public static function provideFoobar() */ public static function provideEmpty() { - return array( - array(self::createEmptyRepository()), - array(self::createEmptyRepository(false)), - ); + return [ + [self::createEmptyRepository()], + [self::createEmptyRepository(false)], + ]; } /** @@ -92,7 +95,7 @@ public static function registerDeletion(Repository $repository) } else { $dir = $repository->getGitDir(); } - AbstractTest::deleteDir($dir); + self::deleteDir($dir); }); } @@ -115,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); @@ -137,12 +140,12 @@ public static function deleteDir($dir) protected static function getOptions() { $command = isset($_SERVER['GIT_COMMAND']) ? $_SERVER['GIT_COMMAND'] : 'git'; - $envs = isset($_SERVER['GIT_ENVS']) ? (array) $_SERVER['GIT_ENVS'] : array(); + $envs = isset($_SERVER['GIT_ENVS']) ? (array) $_SERVER['GIT_ENVS'] : []; - return array( - 'command' => $command, + return [ + 'command' => $command, 'environment_variables' => $envs, - 'process_timeout' => 60, - ); + 'process_timeout' => 60, + ]; } } diff --git a/tests/Gitonomy/Git/Tests/AdminTest.php b/tests/Gitonomy/Git/Tests/AdminTest.php index 9f2a1876..0f7460fa 100644 --- a/tests/Gitonomy/Git/Tests/AdminTest.php +++ b/tests/Gitonomy/Git/Tests/AdminTest.php @@ -9,9 +9,11 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\Admin; +use Gitonomy\Git\Exception\RuntimeException; use Gitonomy\Git\Reference\Branch; use Gitonomy\Git\Repository; @@ -19,14 +21,20 @@ class AdminTest extends AbstractTest { private $tmpDir; - public function setUp() + /** + * @before + */ + public function setUpTmpDir() { $this->tmpDir = self::createTempDir(); } - public function tearDown() + /** + * @after + */ + public function tearDownTmpDir() { - $this->deleteDir(self::createTempDir()); + self::deleteDir(self::createTempDir()); } public function testBare() @@ -36,8 +44,8 @@ public function testBare() $objectDir = $this->tmpDir.'/objects'; $this->assertTrue($repository->isBare(), 'Repository is bare'); - $this->assertTrue(is_dir($objectDir), 'objects/ folder is present'); - $this->assertTrue($repository instanceof Repository, 'Admin::init returns a repository'); + $this->assertDirectoryExists($objectDir, 'objects/ folder is present'); + $this->assertInstanceOf(Repository::class, $repository, 'Admin::init returns a repository'); $this->assertEquals($this->tmpDir, $repository->getGitDir(), 'The folder passed as argument is git dir'); $this->assertNull($repository->getWorkingDir(), 'No working dir in bare repository'); } @@ -49,8 +57,8 @@ public function testNotBare() $objectDir = $this->tmpDir.'/.git/objects'; $this->assertFalse($repository->isBare(), 'Repository is not bare'); - $this->assertTrue(is_dir($objectDir), 'objects/ folder is present'); - $this->assertTrue($repository instanceof Repository, 'Admin::init returns a repository'); + $this->assertDirectoryExists($objectDir, 'objects/ folder is present'); + $this->assertInstanceOf(Repository::class, $repository, 'Admin::init returns a repository'); $this->assertEquals($this->tmpDir.'/.git', $repository->getGitDir(), 'git dir as subfolder of argument'); $this->assertEquals($this->tmpDir, $repository->getWorkingDir(), 'working dir present in bare repository'); } @@ -66,12 +74,12 @@ public function testClone($repository) $newRefs = array_keys($new->getReferences()->getAll()); - $this->assertTrue(in_array('refs/heads/master', $newRefs)); - $this->assertTrue(in_array('refs/tags/0.1', $newRefs)); + $this->assertContains('refs/heads/master', $newRefs); + $this->assertContains('refs/tags/0.1', $newRefs); if ($repository->isBare()) { $this->assertEquals($newDir, $new->getGitDir()); - $this->assertTrue(in_array('refs/heads/new-feature', $newRefs)); + $this->assertContains('refs/heads/new-feature', $newRefs); } else { $this->assertEquals($newDir.'/.git', $new->getGitDir()); $this->assertEquals($newDir, $new->getWorkingDir()); @@ -89,7 +97,7 @@ public function testCloneBranchBare() self::registerDeletion($new); $head = $new->getHead(); - $this->assertTrue($head instanceof Branch, 'HEAD is a branch'); + $this->assertInstanceOf(Branch::class, $head, 'HEAD is a branch'); $this->assertEquals('new-feature', $head->getName(), 'HEAD is branch new-feature'); } @@ -104,7 +112,7 @@ public function testCloneBranchNotBare() self::registerDeletion($new); $head = $new->getHead(); - $this->assertTrue($head instanceof Branch, 'HEAD is a branch'); + $this->assertInstanceOf(Branch::class, $head, 'HEAD is a branch'); $this->assertEquals('new-feature', $head->getName(), 'HEAD is branch new-feature'); } @@ -119,14 +127,14 @@ public function testMirror($repository) $newRefs = array_keys($new->getReferences()->getAll()); - $this->assertTrue(in_array('refs/heads/master', $newRefs)); - $this->assertTrue(in_array('refs/tags/0.1', $newRefs)); + $this->assertContains('refs/heads/master', $newRefs); + $this->assertContains('refs/tags/0.1', $newRefs); $this->assertEquals($newDir, $new->getGitDir()); if ($repository->isBare()) { - $this->assertTrue(in_array('refs/heads/new-feature', $newRefs)); + $this->assertContains('refs/heads/new-feature', $newRefs); } else { - $this->assertTrue(in_array('refs/remotes/origin/new-feature', $newRefs)); + $this->assertContains('refs/remotes/origin/new-feature', $newRefs); } } @@ -148,10 +156,27 @@ public function testCheckInvalidRepository() } /** - * @expectedException RuntimeException + * @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); + $file = $this->tmpDir.'/test'; touch($file); @@ -161,15 +186,15 @@ public function testExistingFile() public function testCloneRepository() { $newDir = self::createTempDir(); - $args = array(); + $args = []; $new = Admin::cloneRepository($newDir, self::REPOSITORY_URL, $args, self::getOptions()); self::registerDeletion($new); $newRefs = array_keys($new->getReferences()->getAll()); - $this->assertTrue(in_array('refs/heads/master', $newRefs)); - $this->assertTrue(in_array('refs/tags/0.1', $newRefs)); + $this->assertContains('refs/heads/master', $newRefs); + $this->assertContains('refs/tags/0.1', $newRefs); $this->assertEquals($newDir.'/.git', $new->getGitDir()); $this->assertEquals($newDir, $new->getWorkingDir()); diff --git a/tests/Gitonomy/Git/Tests/BlameTest.php b/tests/Gitonomy/Git/Tests/BlameTest.php index d599d8c1..6d92547d 100644 --- a/tests/Gitonomy/Git/Tests/BlameTest.php +++ b/tests/Gitonomy/Git/Tests/BlameTest.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; class BlameTest extends AbstractTest diff --git a/tests/Gitonomy/Git/Tests/BlobTest.php b/tests/Gitonomy/Git/Tests/BlobTest.php index d233e7cd..431a0a46 100644 --- a/tests/Gitonomy/Git/Tests/BlobTest.php +++ b/tests/Gitonomy/Git/Tests/BlobTest.php @@ -9,8 +9,11 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; +use Gitonomy\Git\Exception\RuntimeException; + class BlobTest extends AbstractTest { const README_FRAGMENT = 'Foo Bar project'; @@ -20,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 */ @@ -27,15 +35,20 @@ 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()); + } } /** * @dataProvider provideFoobar - * @expectedException RuntimeException */ public function testNotExisting($repository) { + $this->expectException(RuntimeException::class); + $blob = $repository->getBlob('foobar'); $blob->getContent(); } @@ -46,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()); + } } /** @@ -54,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()); } /** @@ -63,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 8a7c2c65..7d094648 100644 --- a/tests/Gitonomy/Git/Tests/CommitTest.php +++ b/tests/Gitonomy/Git/Tests/CommitTest.php @@ -9,10 +9,15 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\Commit; 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 { @@ -25,7 +30,7 @@ public function testGetDiff($repository) $diff = $commit->getDiff(); - $this->assertTrue($diff instanceof Diff, 'getDiff() returns a Diff object'); + $this->assertInstanceOf(Diff::class, $diff, 'getDiff() returns a Diff object'); } /** @@ -40,13 +45,13 @@ public function testGetHash($repository) /** * @dataProvider provideFoobar - * - * @expectedException Gitonomy\Git\Exception\ReferenceNotFoundException - * @expectedExceptionMessage Reference not found: "that-hash-doest-not-exists" */ public function testInvalideHashThrowException($repository) { - $commit = new Commit($repository, 'that-hash-doest-not-exists'); + $this->expectException(ReferenceNotFoundException::class); + $this->expectExceptionMessage('Reference not found: "that-hash-doest-not-exists"'); + + new Commit($repository, 'that-hash-doest-not-exists'); } /** @@ -66,7 +71,7 @@ public function testGetParentHashes_WithNoParent($repository) { $commit = $repository->getCommit(self::INITIAL_COMMIT); - $this->assertEquals(0, count($commit->getParentHashes()), 'No parent on initial commit'); + $this->assertCount(0, $commit->getParentHashes(), 'No parent on initial commit'); } /** @@ -77,7 +82,7 @@ public function testGetParentHashes_WithOneParent($repository) $commit = $repository->getCommit(self::LONGFILE_COMMIT); $parents = $commit->getParentHashes(); - $this->assertEquals(1, count($parents), 'One parent found'); + $this->assertCount(1, $parents, 'One parent found'); $this->assertEquals(self::BEFORE_LONGFILE_COMMIT, $parents[0], 'Parent hash is correct'); } @@ -89,8 +94,8 @@ public function testGetParents_WithOneParent($repository) $commit = $repository->getCommit(self::LONGFILE_COMMIT); $parents = $commit->getParents(); - $this->assertEquals(1, count($parents), 'One parent found'); - $this->assertTrue($parents[0] instanceof Commit, 'First parent is a Commit object'); + $this->assertCount(1, $parents, 'One parent found'); + $this->assertInstanceOf(Commit::class, $parents[0], 'First parent is a Commit object'); $this->assertEquals(self::BEFORE_LONGFILE_COMMIT, $parents[0]->getHash(), "First parents's hash is correct"); } @@ -111,7 +116,7 @@ public function testGetTree($repository) { $commit = $repository->getCommit(self::LONGFILE_COMMIT); - $this->assertInstanceOf('Gitonomy\Git\Tree', $commit->getTree(), 'Tree is a tree'); + $this->assertInstanceOf(Tree::class, $commit->getTree(), 'Tree is a tree'); $this->assertEquals('b06890c7b10904979d2f69613c2ccda30aafe262', $commit->getTree()->getHash(), 'Tree hash is correct'); } @@ -185,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. @@ -223,14 +253,8 @@ public function testGetShortMessage($repository) public function testGetBodyMessage($repository) { $commit = $repository->getCommit(self::LONGMESSAGE_COMMIT); - $message = <<assertEquals($message, $commit->getBodyMessage()); $commit = $repository->getCommit(self::INITIAL_COMMIT); @@ -239,11 +263,12 @@ public function testGetBodyMessage($repository) } /** - * @expectedException InvalidArgumentException * @dataProvider provideFoobar */ public function testGetIncludingBranchesException($repository) { + $this->expectException(InvalidArgumentException::class); + $commit = $repository->getCommit(self::INITIAL_COMMIT); $commit->getIncludingBranches(false, false); diff --git a/tests/Gitonomy/Git/Tests/DiffTest.php b/tests/Gitonomy/Git/Tests/DiffTest.php index d9ad4e02..e51fc7fc 100644 --- a/tests/Gitonomy/Git/Tests/DiffTest.php +++ b/tests/Gitonomy/Git/Tests/DiffTest.php @@ -9,9 +9,12 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\Diff\Diff; +use Gitonomy\Git\Diff\File; +use Gitonomy\Git\Repository; class DiffTest extends AbstractTest { @@ -19,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 @@ -44,17 +48,17 @@ protected function verifyCreateCommitDiff(Diff $diff) { $files = $diff->getFiles(); - $this->assertEquals(2, count($files), '1 file in diff'); + $this->assertCount(2, $files, '1 file in diff'); $this->assertTrue($files[0]->isCreation(), 'script_A.php created'); - $this->assertEquals(null, $files[0]->getOldName(), 'First file name is a new file'); + $this->assertEquals(null, $files[0]->getOldName(), 'First file name is a new file'); $this->assertEquals('script_A.php', $files[0]->getNewName(), 'First file name is script_A.php'); - $this->assertEquals(null, $files[0]->getOldMode(), 'First file mode is a new file'); - $this->assertEquals('100644', $files[0]->getNewMode(), 'First file mode is correct'); + $this->assertEquals(null, $files[0]->getOldMode(), 'First file mode is a new file'); + $this->assertEquals('100644', $files[0]->getNewMode(), 'First file mode is correct'); $this->assertEquals(1, $files[0]->getAdditions(), '1 line added'); - $this->assertEquals(0, $files[0]->getDeletions(), '0 lines deleted'); + $this->assertEquals(0, $files[0]->getDeletions(), '0 lines deleted'); } /** @@ -64,14 +68,14 @@ public function testGetFiles_Modification($repository) { $files = $repository->getCommit(self::BEFORE_LONGFILE_COMMIT)->getDiff()->getFiles(); - $this->assertEquals(1, count($files), '1 files in diff'); + $this->assertCount(1, $files, '1 files in diff'); $this->assertTrue($files[0]->isModification(), 'image.jpg modified'); $this->assertEquals('image.jpg', $files[0]->getOldName(), 'Second file name is image.jpg'); $this->assertEquals('image.jpg', $files[0]->getNewName(), 'Second file name is image.jpg'); - $this->assertEquals('100644', $files[0]->getOldMode(), 'Second file mode is a new file'); - $this->assertEquals('100644', $files[0]->getNewMode(), 'Second file mode is correct'); + $this->assertEquals('100644', $files[0]->getOldMode(), 'Second file mode is a new file'); + $this->assertEquals('100644', $files[0]->getNewMode(), 'Second file mode is correct'); $this->assertTrue($files[0]->isBinary(), 'binary file'); $this->assertEquals(0, $files[0]->getAdditions(), '0 lines added'); @@ -85,7 +89,7 @@ public function testGetFiles_Deletion($repository) { $files = $repository->getCommit(self::DELETE_COMMIT)->getDiff()->getFiles(); - $this->assertEquals(1, count($files), '1 files modified'); + $this->assertCount(1, $files, '1 files modified'); $this->assertTrue($files[0]->isDeletion(), 'File deletion'); $this->assertEquals('script_B.php', $files[0]->getOldName(), 'verify old filename'); @@ -99,7 +103,7 @@ public function testGetFiles_Rename($repository) { $files = $repository->getCommit(self::RENAME_COMMIT)->getDiff()->getFiles(); - $this->assertEquals(1, count($files), '1 files modified'); + $this->assertCount(1, $files, '1 files modified'); $this->assertTrue($files[0]->isModification()); $this->assertTrue($files[0]->isRename()); @@ -115,7 +119,7 @@ public function testGetFiles_Changemode($repository) { $files = $repository->getCommit(self::CHANGEMODE_COMMIT)->getDiff()->getFiles(); - $this->assertEquals(1, count($files), '1 files modified'); + $this->assertCount(1, $files, '1 files modified'); $this->assertTrue($files[0]->isModification()); $this->assertTrue($files[0]->isChangeMode()); @@ -133,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->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->assertEquals(1, $changes[0]->getRangeNewStart()); - $this->assertEquals(0, $changes[0]->getRangeNewCount()); + $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 d9e40787..1b09fe55 100644 --- a/tests/Gitonomy/Git/Tests/HooksTest.php +++ b/tests/Gitonomy/Git/Tests/HooksTest.php @@ -9,13 +9,20 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; +use Gitonomy\Git\Exception\InvalidArgumentException; +use Gitonomy\Git\Exception\LogicException; + 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; @@ -48,7 +55,8 @@ public function assertHasHook($repository, $hook) $file = $this->hookPath($repository, $hook); $this->assertTrue($repository->getHooks()->has($hook), "hook $hook in repository"); - $this->assertTrue(file_exists($file), "Hook $hook is present"); + + $this->assertFileExists($file, "Hook $hook is present"); } public function assertNoHook($repository, $hook) @@ -56,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->assertFalse(file_exists($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"); + } } /** @@ -71,10 +84,11 @@ public function testHas($repository) /** * @dataProvider provideFoobar - * @expectedException InvalidArgumentException */ public function testGet_InvalidName_ThrowsException($repository) { + $this->expectException(InvalidArgumentException::class); + $repository->getHooks()->get('foo'); } @@ -99,15 +113,21 @@ public function testSymlink($repository) $repository->getHooks()->setSymlink('foo', $file); $this->assertTrue(is_link($this->hookPath($repository, 'foo')), 'foo hook is a symlink'); - $this->assertEquals($file, 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' + ); } /** * @dataProvider provideFoobar - * @expectedException LogicException */ public function testSymlink_WithExisting_ThrowsLogicException($repository) { + $this->expectException(LogicException::class); + $this->markAsSkippedIfSymlinkIsMissing(); $file = $this->hookPath($repository, 'target-symlink'); @@ -140,7 +160,8 @@ public function testSet_Existing_ThrowsLogicException($repository) { $repository->getHooks()->set('foo', 'bar'); - $this->setExpectedException('LogicException'); + $this->expectException(LogicException::class); + $repository->getHooks()->set('foo', 'bar'); } @@ -153,15 +174,21 @@ public function testRemove($repository) touch($file); $repository->getHooks()->remove('foo'); - $this->assertFalse(file_exists($file)); + + if (method_exists($this, 'assertFileDoesNotExist')) { + $this->assertFileDoesNotExist($file); + } else { + $this->assertFileNotExists($file); + } } /** * @dataProvider provideFoobar - * @expectedException LogicException */ public function testRemove_NotExisting_ThrowsLogicException($repository) { + $this->expectException(LogicException::class); + $repository->getHooks()->remove('foo'); } diff --git a/tests/Gitonomy/Git/Tests/LogTest.php b/tests/Gitonomy/Git/Tests/LogTest.php index e1fa8c82..a02184dd 100644 --- a/tests/Gitonomy/Git/Tests/LogTest.php +++ b/tests/Gitonomy/Git/Tests/LogTest.php @@ -9,9 +9,10 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; -use Gitonomy\Git\Log; +use Gitonomy\Git\Parser\LogParser; class LogTest extends AbstractTest { @@ -23,8 +24,8 @@ public function testRevisionAndPath($repository) $logReadme = $repository->getLog(self::LONGFILE_COMMIT, 'README'); $logImage = $repository->getLog(self::LONGFILE_COMMIT, 'image.jpg'); - $this->assertEquals(3, count($logReadme)); - $this->assertEquals(2, count($logImage)); + $this->assertCount(3, $logReadme); + $this->assertCount(2, $logImage); } /** @@ -36,7 +37,7 @@ public function testGetCommits($repository) $commits = $log->getCommits(); - $this->assertEquals(3, count($commits), '3 commits in log'); + $this->assertCount(3, $commits, '3 commits in log'); $this->assertEquals(self::LONGFILE_COMMIT, $commits[0]->getHash(), 'First is requested one'); $this->assertEquals(self::BEFORE_LONGFILE_COMMIT, $commits[1]->getHash(), "Second is longfile parent\'s"); } @@ -68,7 +69,7 @@ public function testIterable($repository) { $log = $repository->getLog(self::LONGFILE_COMMIT); - $expectedHashes = array(self::LONGFILE_COMMIT, self::BEFORE_LONGFILE_COMMIT); + $expectedHashes = [self::LONGFILE_COMMIT, self::BEFORE_LONGFILE_COMMIT]; foreach ($log as $entry) { $hash = array_shift($expectedHashes); $this->assertEquals($hash, $entry->getHash()); @@ -77,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/PushReferenceTest.php b/tests/Gitonomy/Git/Tests/PushReferenceTest.php index 9795b25e..4490a3f9 100644 --- a/tests/Gitonomy/Git/Tests/PushReferenceTest.php +++ b/tests/Gitonomy/Git/Tests/PushReferenceTest.php @@ -9,6 +9,7 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\PushReference; @@ -23,12 +24,12 @@ class PushReferenceTest extends AbstractTest public function provideIsers() { // mask: force fastforward create delete - return array( - array('foo', PushReference::ZERO, self::LONGFILE_COMMIT, self::CREATE), - array('foo', self::LONGFILE_COMMIT, PushReference::ZERO, self::DELETE), - array('foo', self::LONGFILE_COMMIT, self::BEFORE_LONGFILE_COMMIT, self::FORCE), - array('foo', self::BEFORE_LONGFILE_COMMIT, self::LONGFILE_COMMIT, self::FAST_FORWARD), - ); + return [ + ['foo', PushReference::ZERO, self::LONGFILE_COMMIT, self::CREATE], + ['foo', self::LONGFILE_COMMIT, PushReference::ZERO, self::DELETE], + ['foo', self::LONGFILE_COMMIT, self::BEFORE_LONGFILE_COMMIT, self::FORCE], + ['foo', self::BEFORE_LONGFILE_COMMIT, self::LONGFILE_COMMIT, self::FAST_FORWARD], + ]; } /** @@ -37,10 +38,10 @@ public function provideIsers() public function testIsers($reference, $before, $after, $mask) { $reference = new PushReference(self::createFoobarRepository(), $reference, $before, $after); - $this->assertEquals($mask & self::CREATE, $reference->isCreate(), 'Create value is correct.'); - $this->assertEquals($mask & self::DELETE, $reference->isDelete(), 'Delete value is correct.'); - $this->assertEquals($mask & self::FORCE, $reference->isForce(), 'Force value is correct.'); - $this->assertEquals($mask & self::FAST_FORWARD, $reference->isFastForward(), 'FastForward value is correct.'); + $this->assertEquals($mask & self::CREATE, $reference->isCreate(), 'Create value is correct.'); + $this->assertEquals($mask & self::DELETE, $reference->isDelete(), 'Delete value is correct.'); + $this->assertEquals($mask & self::FORCE, $reference->isForce(), 'Force value is correct.'); + $this->assertEquals($mask & self::FAST_FORWARD, $reference->isFastForward(), 'FastForward value is correct.'); } /** @@ -51,7 +52,7 @@ public function testLog($repository) $ref = new PushReference($repository, 'foo', self::INITIAL_COMMIT, self::LONGFILE_COMMIT); $log = $ref->getLog()->getCommits(); - $this->assertEquals(7, count($log), '7 commits in log'); + $this->assertCount(7, $log, '7 commits in log'); $this->assertEquals('add a long file', $log[0]->getShortMessage(), 'First commit is correct'); } @@ -64,7 +65,7 @@ public function testSignedLog($repository) { $ref = new PushReference($repository, 'foo', self::INITIAL_COMMIT, self::SIGNED_COMMIT); $log = $ref->getLog()->getCommits(); - $this->assertEquals(16, count($log), '16 commits in log'); + $this->assertCount(16, $log, '16 commits in log'); $this->assertEquals('signed commit', $log[0]->getShortMessage(), 'Last commit is correct'); } @@ -75,8 +76,8 @@ public function testLogWithExclude($repository) { $ref = new PushReference($repository, 'foo', PushReference::ZERO, self::LONGFILE_COMMIT); - $log = $ref->getLog(array(self::INITIAL_COMMIT))->getCommits(); - $this->assertEquals(7, count($log), '7 commits in log'); + $log = $ref->getLog([self::INITIAL_COMMIT])->getCommits(); + $this->assertCount(7, $log, '7 commits in log'); $this->assertEquals('add a long file', $log[0]->getShortMessage(), 'First commit is correct'); } } diff --git a/tests/Gitonomy/Git/Tests/ReferenceBagTest.php b/tests/Gitonomy/Git/Tests/ReferenceBagTest.php new file mode 100644 index 00000000..f9b9c82c --- /dev/null +++ b/tests/Gitonomy/Git/Tests/ReferenceBagTest.php @@ -0,0 +1,56 @@ + + * (c) Julien DIDIER + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Gitonomy\Git\Tests; + +use Gitonomy\Git\Repository; + +class ReferenceBagTest extends AbstractTest +{ + /** + * @dataProvider provideFoobar + */ + public function testUnknownReference(Repository $repository) + { + $hash = $repository->getLog()->getSingleCommit()->getHash(); + + $repository->run('update-ref', ['refs/pipelines/1', $hash]); + $repository->run('update-ref', ['refs/merge-request/1/head', $hash]); + $repository->run('update-ref', ['refs/pull/1/head', $hash]); + $repository->run('update-ref', ['refs/notes/gtm-data', $hash]); + + $refs = $repository->getReferences()->getAll(); + if (method_exists($this, 'assertIsArray')) { + $this->assertIsArray($refs); + } else { + $this->assertInternalType('array', $refs); + } + + // Check that at least it has the master ref + $this->assertArrayHasKey('refs/heads/master', $refs); + + // Check that our custom refs have been ignored + $this->assertArrayNotHasKey('refs/pipelines/1', $refs); + $this->assertArrayNotHasKey('refs/merge-request/1/head', $refs); + $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 87dfbbba..0f975577 100644 --- a/tests/Gitonomy/Git/Tests/ReferenceTest.php +++ b/tests/Gitonomy/Git/Tests/ReferenceTest.php @@ -9,22 +9,22 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; +use Gitonomy\Git\Exception\ReferenceNotFoundException; use Gitonomy\Git\Reference\Branch; use Gitonomy\Git\Reference\Tag; class ReferenceTest extends AbstractTest { - private $references; - /** * @dataProvider provideEmpty */ public function testEmptyRepository($repository) { $this->assertCount(0, $repository->getReferences()); - $this->assertEquals(array(), $repository->getReferences()->getAll()); + $this->assertEquals([], $repository->getReferences()->getAll()); } /** @@ -34,7 +34,7 @@ public function testGetBranch($repository) { $branch = $repository->getReferences()->getBranch('master'); - $this->assertTrue($branch instanceof Branch, 'Branch object is correct type'); + $this->assertInstanceOf(Branch::class, $branch, 'Branch object is correct type'); $this->assertEquals($branch->getCommitHash(), $branch->getCommit()->getHash(), 'Hash is correctly resolved'); } @@ -58,11 +58,12 @@ public function testHasTag($repository) /** * @dataProvider provideFoobar - * @expectedException Gitonomy\Git\Exception\ReferenceNotFoundException */ public function testGetBranch_NotExisting_Error($repository) { - $branch = $repository->getReferences()->getBranch('notexisting'); + $this->expectException(ReferenceNotFoundException::class); + + $repository->getReferences()->getBranch('notexisting'); } /** @@ -72,7 +73,8 @@ public function testGetTag($repository) { $tag = $repository->getReferences()->getTag('0.1'); - $this->assertTrue($tag instanceof Tag, 'Tag object is correct type'); + $this->assertInstanceOf(Tag::class, $tag, 'Tag object is correct type'); + $this->assertFalse($tag->isAnnotated(), 'Tag is not annotated'); $this->assertEquals(self::LONGFILE_COMMIT, $tag->getCommitHash(), 'Commit hash is correct'); $this->assertEquals(self::LONGFILE_COMMIT, $tag->getCommit()->getHash(), 'Commit hash is correct'); @@ -80,11 +82,38 @@ public function testGetTag($repository) /** * @dataProvider provideFoobar - * @expectedException Gitonomy\Git\Exception\ReferenceNotFoundException + */ + public function testAnnotatedTag($repository) + { + $tag = $repository->getReferences()->getTag('annotated'); + + $this->assertInstanceOf(Tag::class, $tag, 'Tag object is correct type'); + $this->assertTrue($tag->isAnnotated(), 'Tag is annotated'); + $this->assertFalse($tag->isSigned(), 'Tag is not signed'); + + $this->assertEquals('Graham Campbell', $tag->getTaggerName(), 'Tagger name is correct'); + $this->assertEquals('graham@alt-three.com', $tag->getTaggerEmail(), 'Tagger email is correct'); + $this->assertEquals(1471428000, $tag->getTaggerDate()->getTimestamp(), 'Tag date is correct'); + + $this->assertEquals('heading', $tag->getSubjectMessage(), 'Message heading is correct'); + $this->assertEquals("body\nbody", $tag->getBodyMessage(), 'Message body is correct'); + + $closure = function () { + return parent::getCommit(); + }; + $parentCommit = $closure->bindTo($tag, Tag::class); + $this->assertNotEquals($parentCommit()->getHash(), $tag->getCommit()->getHash(), 'Tag commit is not the same as main commit'); + $this->assertEquals('fbde681b329a39e08b63dc54b341a3274c0380c0', $tag->getCommit()->getHash(), 'Tag commit is correct'); + } + + /** + * @dataProvider provideFoobar */ public function testGetTag_NotExisting_Error($repository) { - $branch = $repository->getReferences()->getTag('notexisting'); + $this->expectException(ReferenceNotFoundException::class); + + $repository->getReferences()->getTag('notexisting'); } /** @@ -95,8 +124,8 @@ public function testResolve($repository) $commit = $repository->getReferences()->getTag('0.1')->getCommit(); $resolved = $repository->getReferences()->resolve($commit->getHash()); - $this->assertEquals(1, count($resolved), '1 revision resolved'); - $this->assertTrue(reset($resolved) instanceof Tag, 'Resolved object is a tag'); + $this->assertCount(1, $resolved, '1 revision resolved'); + $this->assertInstanceOf(Tag::class, reset($resolved), 'Resolved object is a tag'); } /** @@ -107,8 +136,8 @@ public function testResolveTags($repository) $commit = $repository->getReferences()->getTag('0.1')->getCommit(); $resolved = $repository->getReferences()->resolveTags($commit->getHash()); - $this->assertEquals(1, count($resolved), '1 revision resolved'); - $this->assertTrue(reset($resolved) instanceof Tag, 'Resolved object is a tag'); + $this->assertCount(1, $resolved, '1 revision resolved'); + $this->assertInstanceOf(Tag::class, reset($resolved), 'Resolved object is a tag'); } /** @@ -121,12 +150,12 @@ public function testResolveBranches($repository) $resolved = $repository->getReferences()->resolveBranches($master->getCommitHash()); if ($repository->isBare()) { - $this->assertEquals(1, count($resolved), '1 revision resolved'); + $this->assertCount(1, $resolved, '1 revision resolved'); } else { - $this->assertEquals(2, count($resolved), '2 revision resolved'); + $this->assertCount(2, $resolved, '2 revision resolved'); } - $this->assertTrue(reset($resolved) instanceof Branch, 'Resolved object is a branch'); + $this->assertInstanceOf(Branch::class, reset($resolved), 'Resolved object is a branch'); } /** @@ -144,7 +173,7 @@ public function testIterable($repository) { $i = 0; foreach ($repository->getReferences() as $ref) { - ++$i; + $i++; } $this->assertGreaterThanOrEqual(2, $i, 'At least two references in repository'); } @@ -180,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 40934192..fc5c7deb 100644 --- a/tests/Gitonomy/Git/Tests/RepositoryTest.php +++ b/tests/Gitonomy/Git/Tests/RepositoryTest.php @@ -9,22 +9,32 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\Blob; -use Gitonomy\Git\Repository; +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->assertTrue($blob instanceof Blob, 'getBlob() returns a Blob object'); - $this->assertContains('Foo Bar project', $blob->getContent(), 'file is correct'); + $this->assertInstanceOf(Blob::class, $blob, 'getBlob() returns a Blob object'); + + 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'); + } } /** @@ -33,7 +43,8 @@ public function testGetBlob_WithExisting_Works($repository) public function testGetSize($repository) { $size = $repository->getSize(); - $this->assertGreaterThan(70, $size, 'Repository is greater than 70KB'); + $this->assertGreaterThanOrEqual(57, $size, 'Repository is at least 57KB'); + $this->assertLessThan(84, $size, 'Repository is less than 84KB'); } public function testIsBare() @@ -62,24 +73,21 @@ public function testLoggerOk($repository) $this->markTestSkipped(); } - $logger = $this->getMock('Psr\Log\LoggerInterface'); - $logger - ->expects($this->once()) - ->method('info') - ; - $logger - ->expects($this->exactly(3)) // duration, return code and output - ->method('debug') - ; + $loggerProphecy = $this->prophesize('Psr\Log\LoggerInterface'); + $loggerProphecy + ->info('run command: remote "" ') + ->shouldBeCalledTimes(1); + $loggerProphecy + ->debug(Argument::type('string')) // duration, return code and output + ->shouldBeCalledTimes(3); - $repository->setLogger($logger); + $repository->setLogger($loggerProphecy->reveal()); $repository->run('remote'); } /** * @dataProvider provideFoobar - * @expectedException RuntimeException */ public function testLoggerNOk($repository) { @@ -87,21 +95,20 @@ public function testLoggerNOk($repository) $this->markTestSkipped(); } - $logger = $this->getMock('Psr\Log\LoggerInterface'); - $logger - ->expects($this->once()) - ->method('info') - ; - $logger - ->expects($this->exactly(3)) // duration, return code and output - ->method('debug') - ; - $logger - ->expects($this->once()) - ->method('error') - ; - - $repository->setLogger($logger); + $this->expectException(RuntimeException::class); + + $loggerProphecy = $this->prophesize('Psr\Log\LoggerInterface'); + $loggerProphecy + ->info(Argument::type('string')) + ->shouldBeCalledTimes(1); + $loggerProphecy + ->debug(Argument::type('string')) // duration, return code and output + ->shouldBeCalledTimes(3); + $loggerProphecy + ->error(Argument::type('string')) + ->shouldBeCalledTimes(1); + + $repository->setLogger($loggerProphecy->reveal()); $repository->run('not-work'); } diff --git a/tests/Gitonomy/Git/Tests/RevisionTest.php b/tests/Gitonomy/Git/Tests/RevisionTest.php index 253237f5..84711843 100644 --- a/tests/Gitonomy/Git/Tests/RevisionTest.php +++ b/tests/Gitonomy/Git/Tests/RevisionTest.php @@ -9,9 +9,11 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\Commit; +use Gitonomy\Git\Exception\ReferenceNotFoundException; use Gitonomy\Git\Log; use Gitonomy\Git\Revision; @@ -24,23 +26,24 @@ public function testGetCommit($repository) { $revision = $repository->getRevision(self::LONGFILE_COMMIT.'^'); - $this->assertTrue($revision instanceof Revision, 'Revision object type'); + $this->assertInstanceOf(Revision::class, $revision, 'Revision object type'); $commit = $revision->getCommit(); - $this->assertTrue($commit instanceof Commit, 'getCommit returns a Commit'); + $this->assertInstanceOf(Commit::class, $commit, 'getCommit returns a Commit'); $this->assertEquals(self::BEFORE_LONGFILE_COMMIT, $commit->getHash(), 'Resolution is correct'); } /** * @dataProvider provideFoobar - * @expectedException Gitonomy\Git\Exception\ReferenceNotFoundException - * @expectedExceptionMessage Can not find revision "non-existent-commit" */ public function testGetFailingReference($repository) { - $revision = $repository->getRevision('non-existent-commit')->getCommit(); + $this->expectException(ReferenceNotFoundException::class); + $this->expectExceptionMessage('Can not find revision "non-existent-commit"'); + + $repository->getRevision('non-existent-commit')->getCommit(); } /** @@ -52,9 +55,9 @@ public function testGetLog($repository) $log = $revision->getLog(null, 2, 3); - $this->assertTrue($log instanceof Log, 'Log type object'); + $this->assertInstanceOf(Log::class, $log, 'Log type object'); $this->assertEquals(2, $log->getOffset(), 'Log offset is passed'); $this->assertEquals(3, $log->getLimit(), 'Log limit is passed'); - $this->assertEquals(array($revision), $log->getRevisions()->getAll(), 'Revision is passed'); + $this->assertEquals([$revision], $log->getRevisions()->getAll(), 'Revision is passed'); } } diff --git a/tests/Gitonomy/Git/Tests/TreeTest.php b/tests/Gitonomy/Git/Tests/TreeTest.php index 1587033a..b7ba7d32 100644 --- a/tests/Gitonomy/Git/Tests/TreeTest.php +++ b/tests/Gitonomy/Git/Tests/TreeTest.php @@ -9,29 +9,70 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\Blob; +use Gitonomy\Git\CommitReference; class TreeTest extends AbstractTest { const PATH_RESOLVING_COMMIT = 'cc06ac171d884282202dff88c1ded499a1f89420'; + /** * @dataProvider provideFooBar */ - public function testCase($repository) + public function testGetEntries($repository) { $tree = $repository->getCommit(self::LONGFILE_COMMIT)->getTree(); $entries = $tree->getEntries(); - $this->assertTrue(isset($entries['long.php']), 'long.php is present'); + $this->assertNotEmpty($entries['long.php'], 'long.php is present'); $this->assertTrue($entries['long.php'][1] instanceof Blob, 'long.php is a Blob'); - $this->assertTrue(isset($entries['README.md']), 'README.md is present'); + $this->assertNotEmpty($entries['README.md'], 'README.md is present'); $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 */ @@ -43,6 +84,6 @@ public function testResolvePath($repository) $resolved = $tree->resolvePath($path); $entries = $resolved->getEntries(); - $this->assertTrue(isset($entries['d']), 'Successfully resolved source folder'); + $this->assertNotEmpty($entries['d'], 'Successfully resolved source folder'); } } diff --git a/tests/Gitonomy/Git/Tests/WorkingCopyTest.php b/tests/Gitonomy/Git/Tests/WorkingCopyTest.php index 7d4d109f..dfa048f9 100644 --- a/tests/Gitonomy/Git/Tests/WorkingCopyTest.php +++ b/tests/Gitonomy/Git/Tests/WorkingCopyTest.php @@ -9,18 +9,20 @@ * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ + namespace Gitonomy\Git\Tests; use Gitonomy\Git\Admin; +use Gitonomy\Git\Exception\LogicException; +use Gitonomy\Git\Exception\RuntimeException; use Gitonomy\Git\Reference\Branch; class WorkingCopyTest extends AbstractTest { - /** - * @expectedException LogicException - */ public function testNoWorkingCopyInBare() { + $this->expectException(LogicException::class); + $path = self::createTempDir(); $repo = Admin::init($path, true, self::getOptions()); @@ -34,7 +36,7 @@ public function testCheckout() $wc->checkout('origin/new-feature', 'new-feature'); $head = $repository->getHead(); - $this->assertTrue($head instanceof Branch, 'HEAD is a branch'); + $this->assertInstanceOf(Branch::class, $head, 'HEAD is a branch'); $this->assertEquals('new-feature', $head->getName(), 'HEAD is branch new-feature'); } @@ -48,7 +50,7 @@ public function testDiffStaged() $file = $repository->getWorkingDir().'/foobar-test'; file_put_contents($file, 'test'); - $repository->run('add', array($file)); + $repository->run('add', [$file]); $diffStaged = $wc->getDiffStaged(); $this->assertCount(1, $diffStaged->getFiles()); @@ -69,11 +71,10 @@ public function testDiffPending() $this->assertCount(1, $diffPending->getFiles()); } - /** - * @expectedException RuntimeException - */ public function testCheckoutUnexisting() { + $this->expectException(RuntimeException::class); + self::createFoobarRepository(false)->getWorkingCopy()->checkout('foobar'); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f3735d58..a988f4ab 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,8 +4,8 @@ if (defined('PHP_WINDOWS_VERSION_BUILD')) { $server = array_change_key_case($_SERVER, true); - $_SERVER['GIT_ENVS'] = array(); - foreach (array('PATH', 'SYSTEMROOT') as $key) { + $_SERVER['GIT_ENVS'] = []; + foreach (['PATH', 'SYSTEMROOT'] as $key) { if (isset($server[$key])) { $_SERVER['GIT_ENVS'][$key] = $server[$key]; }