composer update

This commit is contained in:
Manish Verma
2018-12-05 10:50:52 +05:30
parent 9eabcacfa7
commit 4addd1e9c6
3328 changed files with 156676 additions and 138988 deletions

1
vendor/gitonomy/gitlib/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto

3
vendor/gitonomy/gitlib/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/vendor
/composer.lock
/phpunit.xml

26
vendor/gitonomy/gitlib/.travis.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
language: php
sudo: false
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- hhvm
matrix:
include:
- php: 5.3
dist: precise
before_install:
# turn off XDebug, if present
- phpenv config-rm xdebug.ini || return 0
install:
- composer install --no-interaction --no-progress
script: vendor/bin/phpunit

20
vendor/gitonomy/gitlib/LICENSE vendored Normal file
View File

@@ -0,0 +1,20 @@
Copyright (c) 2012 Alexandre Salomé
Copyright (c) 2012 Julien DIDIER
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

12
vendor/gitonomy/gitlib/README.md vendored Normal file
View File

@@ -0,0 +1,12 @@
Git lib for Gitonomy
====================
[![Build Status](https://secure.travis-ci.org/gitonomy/gitlib.png)](https://travis-ci.org/gitonomy/gitlib)
This library provides methods to access Git repository from PHP.
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/

44
vendor/gitonomy/gitlib/composer.json vendored Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "gitonomy/gitlib",
"description": "Library for accessing git",
"license": "MIT",
"authors": [
{
"name": "Alexandre Salomé",
"email": "alexandre.salome@gmail.com",
"homepage": "http://alexandre-salome.fr"
},
{
"name": "Julien DIDIER",
"email": "genzo.wm@gmail.com",
"homepage": "http://www.jdidier.net"
}
],
"homepage": "http://gitonomy.com",
"autoload": {
"psr-4": {
"Gitonomy\\Git\\": "src/Gitonomy/Git/"
}
},
"autoload-dev": {
"psr-4": {
"Gitonomy\\Git\\Tests\\": "tests/Gitonomy/Git/Tests/"
}
},
"require": {
"php": "^5.3 || ^7.0",
"symfony/process": "^2.3|^3.0|^4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|^5.7",
"psr/log": "^1.0"
},
"suggest": {
"psr/log": "Add some log"
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}

View File

@@ -0,0 +1,76 @@
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

View File

@@ -0,0 +1,54 @@
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))
)

44
vendor/gitonomy/gitlib/doc/api/blob.rst vendored Normal file
View File

@@ -0,0 +1,44 @@
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.
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');
Get content
-----------
To get content from a *Blob* object:
.. code-block:: php
echo $blob->getContent();
File informations
-----------------
To get mimetype of a *Blob* object using finfo extension:
.. code-block:: 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";
}

View File

@@ -0,0 +1,16 @@
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();

View File

@@ -0,0 +1,168 @@
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);

102
vendor/gitonomy/gitlib/doc/api/diff.rst vendored Normal file
View File

@@ -0,0 +1,102 @@
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());

View File

@@ -0,0 +1,76 @@
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 = <<<HOOK
#!/bin/bash
echo "Push is disabled"
exit 1
HOOK;
// this hook will reject every push
$hooks->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');

54
vendor/gitonomy/gitlib/doc/api/log.rst vendored Normal file
View File

@@ -0,0 +1,54 @@
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();

View File

@@ -0,0 +1,91 @@
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.

View File

@@ -0,0 +1,135 @@
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_')))

View File

@@ -0,0 +1,28 @@
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();

54
vendor/gitonomy/gitlib/doc/api/tree.rst vendored Normal file
View File

@@ -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:
.. 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;

View File

@@ -0,0 +1,45 @@
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();

22
vendor/gitonomy/gitlib/doc/debug.rst vendored Normal file
View File

@@ -0,0 +1,22 @@
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.

View File

@@ -0,0 +1,24 @@
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.

59
vendor/gitonomy/gitlib/doc/index.rst vendored Normal file
View File

@@ -0,0 +1,59 @@
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.

View File

@@ -0,0 +1,20 @@
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": "^1.0"
}
}

21
vendor/gitonomy/gitlib/phpunit.xml.dist vendored Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
backupGlobals = "false"
backupStaticAttributes = "false"
colors = "true"
convertErrorsToExceptions = "true"
convertNoticesToExceptions = "true"
convertWarningsToExceptions = "true"
processIsolation = "false"
stopOnFailure = "false"
syntaxCheck = "false"
bootstrap = "tests/bootstrap.php" >
<testsuites>
<testsuite name="Test Suite">
<directory>tests/Gitonomy/Git/Tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,173 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\Process;
/**
* Administration class for Git repositories.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Admin
{
/**
* Initializes a repository and returns the instance.
*
* @param string $path path to the repository
* @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)
*/
public static function init($path, $bare = true, array $options = array())
{
$process = static::getProcess('init', array_merge(array('-q'), $bare ? array('--bare') : array(), array($path)), $options);
$process->run();
if (!$process->isSuccessFul()) {
throw new RuntimeException(sprintf("Error on repository initialization, command wasn't successful (%s). Error output:\n%s", $process->getCommandLine(), $process->getErrorOutput()));
}
return new Repository($path, $options);
}
/**
* Checks the validity of a git repository url without cloning it.
*
* 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 array $options options for Repository creation
*
* @return bool true if url is valid
*/
public static function isValidRepository($url, array $options = array())
{
$process = static::getProcess('ls-remote', array($url), $options);
$process->run();
return $process->isSuccessFul();
}
/**
* Clone a repository to a local path.
*
* @param string $path indicates where to clone repository
* @param string $url url of repository to clone
* @param bool $bare indicates if repository should be bare or have a working copy
* @param array $options options for Repository creation
*
* @return Repository
*/
public static function cloneTo($path, $url, $bare = true, array $options = array())
{
$args = $bare ? array('--bare') : array();
return static::cloneRepository($path, $url, $args, $options);
}
/**
* Clone a repository branch to a local path.
*
* @param string $path indicates where to clone repository
* @param string $url url of repository to clone
* @param string $branch branch to clone
* @param bool $bare indicates if repository should be bare or have a working copy
* @param array $options options for Repository creation
*
* @return Repository
*/
public static function cloneBranchTo($path, $url, $branch, $bare = true, $options = array())
{
$args = array('--branch', $branch);
if ($bare) {
$args[] = '--bare';
}
return static::cloneRepository($path, $url, $args, $options);
}
/**
* Mirrors a repository (fetch all revisions, not only branches).
*
* @param string $path indicates where to clone repository
* @param string $url url of repository to clone
* @param array $options options for Repository creation
*
* @return Repository
*/
public static function mirrorTo($path, $url, array $options = array())
{
return static::cloneRepository($path, $url, array('--mirror'), $options);
}
/**
* Internal method to launch effective ``git clone`` command.
*
* @param string $path indicates where to clone repository
* @param string $url url of repository to clone
* @param array $args arguments to be added to the command-line
* @param array $options options for Repository creation
*
* @return Repository
*/
public static function cloneRepository($path, $url, array $args = array(), array $options = array())
{
$process = static::getProcess('clone', array_merge(array('-q'), $args, array($url, $path)), $options);
$process->run();
if (!$process->isSuccessFul()) {
throw new RuntimeException(sprintf('Error while initializing repository: %s', $process->getErrorOutput()));
}
return new Repository($path, $options);
}
/**
* This internal method is used to create a process object.
*/
private static function getProcess($command, array $args = array(), array $options = array())
{
$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);
$commandline = array_merge(array($options['command'], $command), $args);
// Backward compatible layer for Symfony Process < 4.0.
if (class_exists('Symfony\Component\Process\ProcessBuilder')) {
$commandline = implode(' ', array_map(
'Symfony\Component\Process\ProcessUtils::escapeArgument',
$commandline
));
}
$process = new Process($commandline);
$process->setEnv($options['environment_variables']);
$process->setTimeout($options['process_timeout']);
$process->setIdleTimeout($options['process_timeout']);
return $process;
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
use Gitonomy\Git\Exception\InvalidArgumentException;
use Gitonomy\Git\Parser\BlameParser;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Blame implements \Countable
{
/**
* @var Repository
*/
protected $repository;
/**
* @var Revision
*/
protected $revision;
/**
* @var string
*/
protected $file;
/**
* @var string|null
*/
protected $lineRange;
/**
* @var array|null
*/
protected $lines;
/**
* @param string $lineRange Argument to pass to git blame (-L).
* Can be a line range (40,60 or 40,+21)
* or a regexp ('/^function$/')
*/
public function __construct(Repository $repository, Revision $revision, $file, $lineRange = null)
{
$this->repository = $repository;
$this->revision = $revision;
$this->lineRange = $lineRange;
$this->file = $file;
}
/**
* @return Line
*/
public function getLine($number)
{
if ($number < 1) {
throw new InvalidArgumentException('Line number should be at least 1');
}
$lines = $this->getLines();
if (!isset($lines[$number])) {
throw new InvalidArgumentException('Line does not exist');
}
return $lines[$number];
}
/**
* Returns lines grouped by commit.
*
* @return array a list of two-elements array (commit, lines)
*/
public function getGroupedLines()
{
$result = array();
$commit = null;
$current = array();
foreach ($this->getLines() as $lineNumber => $line) {
if ($commit !== $line->getCommit()) {
if (count($current)) {
$result[] = array($commit, $current);
}
$commit = $line->getCommit();
$current = array();
}
$current[$lineNumber] = $line;
}
if (count($current)) {
$result[] = array($commit, $current);
}
return $result;
}
/**
* Returns all lines of the blame.
*
* @return array
*/
public function getLines()
{
if (null !== $this->lines) {
return $this->lines;
}
$args = array('-p');
if (null !== $this->lineRange) {
$args[] = '-L';
$args[] = $this->lineRange;
}
$args[] = $this->revision->getRevision();
$args[] = '--';
$args[] = $this->file;
$parser = new BlameParser($this->repository);
$parser->parse($this->repository->run('blame', $args));
$this->lines = $parser->lines;
return $this->lines;
}
/**
* @return int
*/
public function count()
{
return count($this->getLines());
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Line
{
/**
* @var Commit
*/
protected $commit;
protected $sourceLine;
protected $targetLine;
protected $blockLine;
protected $content;
/**
* Instanciates a new Line object.
*/
public function __construct(Commit $commit, $sourceLine, $targetLine, $blockLine, $content)
{
$this->commit = $commit;
$this->sourceLine = $sourceLine;
$this->targetLine = $targetLine;
$this->blockLine = $blockLine;
$this->content = $content;
}
public function getContent()
{
return $this->content;
}
public function getLine()
{
return $this->sourceLine;
}
public function getCommit()
{
return $this->commit;
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Gitonomy\Git;
/**
* Representation of a Blob commit.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Blob
{
/**
* @var Repository
*/
protected $repository;
/**
* @var string
*/
protected $hash;
/**
* @var string
*/
protected $content;
/**
* @var string
*/
protected $mimetype;
/**
* @param Repository $repository Repository where the blob is located
* @param string $hash Hash of the blob
*/
public function __construct(Repository $repository, $hash)
{
$this->repository = $repository;
$this->hash = $hash;
}
/**
* @return string
*/
public function getHash()
{
return $this->hash;
}
/**
* Returns content of the blob.
*
* @throws ProcessException Error occurred while getting content of blob
*/
public function getContent()
{
if (null === $this->content) {
$this->content = $this->repository->run('cat-file', array('-p', $this->hash));
}
return $this->content;
}
/**
* Determine the mimetype of the blob.
*
* @return string A mimetype
*/
public function getMimetype()
{
if (null === $this->mimetype) {
$finfo = new \finfo(FILEINFO_MIME);
$this->mimetype = $finfo->buffer($this->getContent());
}
return $this->mimetype;
}
/**
* Determines if file is binary.
*
* @return bool
*/
public function isBinary()
{
return !$this->isText();
}
/**
* Determines if file is text.
*
* @return bool
*/
public function isText()
{
return (bool) preg_match('#^text/|^application/xml#', $this->getMimetype());
}
}

View File

@@ -0,0 +1,404 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\Util\StringHelper;
/**
* Representation of a Git commit.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Commit extends Revision
{
/**
* Associative array of commit data.
*
* @var array
*/
private $data = array();
/**
* Constructor.
*
* @param Gitonomy\Git\Repository $repository Repository of the commit
* @param string $hash Hash of the commit
*/
public function __construct(Repository $repository, $hash, array $data = array())
{
if (!preg_match('/^[a-f0-9]{40}$/', $hash)) {
throw new ReferenceNotFoundException($hash);
}
parent::__construct($repository, $hash);
$this->setData($data);
}
public function setData(array $data)
{
foreach ($data as $name => $value) {
$this->data[$name] = $value;
}
}
/**
* @return Diff
*/
public function getDiff()
{
$args = array('-r', '-p', '-m', '-M', '--no-commit-id', '--full-index', $this->revision);
$diff = Diff::parse($this->repository->run('diff-tree', $args));
$diff->setRepository($this->repository);
return $diff;
}
/**
* Returns the commit hash.
*
* @return string A SHA1 hash
*/
public function getHash()
{
return $this->revision;
}
/**
* Returns the short commit hash.
*
* @return string A SHA1 hash
*/
public function getShortHash()
{
return $this->getData('shortHash');
}
/**
* Returns a fixed-with short hash.
*/
public function getFixedShortHash($length = 6)
{
return StringHelper::substr($this->revision, 0, $length);
}
/**
* Returns parent hashes.
*
* @return array An array of SHA1 hashes
*/
public function getParentHashes()
{
return $this->getData('parentHashes');
}
/**
* Returns the parent commits.
*
* @return array An array of Commit objects
*/
public function getParents()
{
$result = array();
foreach ($this->getData('parentHashes') as $parentHash) {
$result[] = $this->repository->getCommit($parentHash);
}
return $result;
}
/**
* Returns the tree hash.
*
* @return string A SHA1 hash
*/
public function getTreeHash()
{
return $this->getData('treeHash');
}
public function getTree()
{
return $this->getData('tree');
}
/**
* @return Commit
*/
public function getLastModification($path = null)
{
if (0 === strpos($path, '/')) {
$path = StringHelper::substr($path, 1);
}
if ($getWorkingDir = $this->repository->getWorkingDir()) {
$path = $getWorkingDir.'/'.$path;
}
$result = $this->repository->run('log', array('--format=%H', '-n', 1, $this->revision, '--', $path));
return $this->repository->getCommit(trim($result));
}
/**
* Returns the first line of the commit, and the first 50 characters.
*
* Ported from https://github.com/fabpot/Twig-extensions/blob/d67bc7e69788795d7905b52d31188bbc1d390e01/lib/Twig/Extensions/Extension/Text.php#L52-L109
*
* @param int $length
* @param bool $preserve
* @param string $separator
*
* @return string
*/
public function getShortMessage($length = 50, $preserve = false, $separator = '...')
{
$message = $this->getData('subjectMessage');
if (StringHelper::strlen($message) > $length) {
if ($preserve && false !== ($breakpoint = StringHelper::strpos($message, ' ', $length))) {
$length = $breakpoint;
}
return rtrim(StringHelper::substr($message, 0, $length)).$separator;
}
return $message;
}
/**
* Resolves all references associated to this commit.
*
* @return array An array of references (Branch, Tag, Squash)
*/
public function resolveReferences()
{
return $this->repository->getReferences()->resolve($this);
}
/**
* Find branch containing the commit.
*
* @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
*/
public function getIncludingBranches($local = true, $remote = true)
{
$arguments = array('--contains', $this->revision);
if ($local && $remote) {
$arguments[] = '-a';
} elseif (!$local && $remote) {
$arguments[] = '-r';
} elseif (!$local && !$remote) {
throw new InvalidArgumentException('You should a least set one argument to true');
}
try {
$result = $this->repository->run('branch', $arguments);
} catch (ProcessException $e) {
return array();
}
if (!$result) {
return array();
}
$branchesName = explode("\n", trim(str_replace('*', '', $result)));
$branchesName = array_filter($branchesName, function ($v) { return false === StringHelper::strpos($v, '->');});
$branchesName = array_map('trim', $branchesName);
$references = $this->repository->getReferences();
$branches = array();
foreach ($branchesName as $branchName) {
if (false === $local) {
$branches[] = $references->getRemoteBranch($branchName);
} elseif (0 === StringHelper::strrpos($branchName, 'remotes/')) {
$branches[] = $references->getRemoteBranch(str_replace('remotes/', '', $branchName));
} else {
$branches[] = $references->getBranch($branchName);
}
}
return $branches;
}
/**
* Returns the author name.
*
* @return string A name
*/
public function getAuthorName()
{
return $this->getData('authorName');
}
/**
* Returns the author email.
*
* @return string An email
*/
public function getAuthorEmail()
{
return $this->getData('authorEmail');
}
/**
* Returns the authoring date.
*
* @return DateTime A time object
*/
public function getAuthorDate()
{
return $this->getData('authorDate');
}
/**
* Returns the committer name.
*
* @return string A name
*/
public function getCommitterName()
{
return $this->getData('committerName');
}
/**
* Returns the comitter email.
*
* @return string An email
*/
public function getCommitterEmail()
{
return $this->getData('committerEmail');
}
/**
* Returns the authoring date.
*
* @return DateTime A time object
*/
public function getCommitterDate()
{
return $this->getData('committerDate');
}
/**
* Returns the message of the commit.
*
* @return string A commit 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');
}
/**
* @inheritdoc
*/
public function getCommit()
{
return $this;
}
private function getData($name)
{
if (isset($this->data[$name])) {
return $this->data[$name];
}
if ($name === 'shortHash') {
$this->data['shortHash'] = trim($this->repository->run('log', array('--abbrev-commit', '--format=%h', '-n', 1, $this->revision)));
return $this->data['shortHash'];
}
if ($name === 'tree') {
$this->data['tree'] = $this->repository->getTree($this->getData('treeHash'));
return $this->data['tree'];
}
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_shift($lines);
$data['bodyMessage'] = implode("\n", $lines);
return $data['bodyMessage'];
}
$parser = new Parser\CommitParser();
try {
$result = $this->repository->run('cat-file', array('commit', $this->revision));
} catch (ProcessException $e) {
throw new ReferenceNotFoundException(sprintf('Can not find reference "%s"', $this->revision));
}
$parser->parse($result);
$this->data['treeHash'] = $parser->tree;
$this->data['parentHashes'] = $parser->parents;
$this->data['authorName'] = $parser->authorName;
$this->data['authorEmail'] = $parser->authorEmail;
$this->data['authorDate'] = $parser->authorDate;
$this->data['committerName'] = $parser->committerName;
$this->data['committerEmail'] = $parser->committerEmail;
$this->data['committerDate'] = $parser->committerDate;
$this->data['message'] = $parser->message;
if (!isset($this->data[$name])) {
throw new \InvalidArgumentException(sprintf('No data named "%s" in Commit.', $name));
}
return $this->data[$name];
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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
{
private $hash;
public function __construct($hash)
{
$this->hash = $hash;
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
use Gitonomy\Git\Repository;
/**
* Representation of a diff.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Diff
{
/**
* @var array
*/
protected $files;
/**
* @var string
*/
protected $rawDiff;
/**
* Constructs a new diff for a given revision.
*
* @param array $files The files
* @param string $rawDiff The raw diff
*/
public function __construct(array $files, $rawDiff)
{
$this->files = $files;
$this->rawDiff = $rawDiff;
}
/**
* @return Diff
*/
public static function parse($rawDiff)
{
$parser = new DiffParser();
$parser->parse($rawDiff);
return new self($parser->files, $rawDiff);
}
public function setRepository(Repository $repository)
{
foreach ($this->files as $file) {
$file->setRepository($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
*/
public function getFiles()
{
return $this->files;
}
/**
* Returns the raw diff.
*
* @return string The raw diff
*/
public function getRawDiff()
{
return $this->rawDiff;
}
/**
* Export a diff as array.
*
* @return array The array
*/
public function toArray()
{
return array(
'rawDiff' => $this->rawDiff,
'files' => array_map(
function (File $file) {
return $file->toArray();
}, $this->files
),
);
}
/**
* Create a new instance of Diff from an array.
*
* @param array $array The array
*
* @return Diff The new instance
*/
public static function fromArray(array $array)
{
return new static(
array_map(
function ($array) {
return File::fromArray($array);
}, $array['files']
),
$array['rawDiff']
);
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class File
{
/**
* @var string
*/
protected $oldName;
/**
* @var string
*/
protected $newName;
/**
* @var string
*/
protected $oldMode;
/**
* @var string
*/
protected $newMode;
/**
* @var string
*/
protected $oldIndex;
/**
* @var string
*/
protected $newIndex;
/**
* @var bool
*/
protected $isBinary;
/**
* @var array An array of FileChange objects
*/
protected $changes;
/**
* @var Repository
*/
protected $repository;
/**
* Instanciates a new File object.
*/
public function __construct($oldName, $newName, $oldMode, $newMode, $oldIndex, $newIndex, $isBinary)
{
$this->oldName = $oldName;
$this->newName = $newName;
$this->oldMode = $oldMode;
$this->newMode = $newMode;
$this->oldIndex = $oldIndex;
$this->newIndex = $newIndex;
$this->isBinary = $isBinary;
$this->changes = array();
}
public function addChange(FileChange $change)
{
$this->changes[] = $change;
}
/**
* Indicates if this diff file is a creation.
*
* @return bool
*/
public function isCreation()
{
return null === $this->oldName;
}
/**
* Indicates if this diff file is a modification.
*
* @return bool
*/
public function isModification()
{
return null !== $this->oldName && null !== $this->newName;
}
/**
* Indicates if it's a rename.
*
* A rename can only occurs if it's a modification (not a creation or a deletion).
*
* @return bool
*/
public function isRename()
{
return $this->isModification() && $this->oldName !== $this->newName;
}
/**
* Indicates if the file mode has changed.
*/
public function isChangeMode()
{
return $this->isModification() && $this->oldMode !== $this->newMode;
}
/**
* Indicates if this diff file is a deletion.
*
* @return bool
*/
public function isDeletion()
{
return null === $this->newName;
}
/**
* Indicates if this diff file is a deletion.
*
* @return bool
*/
public function isDelete()
{
return null === $this->newName;
}
/**
* @return int Number of added lines
*/
public function getAdditions()
{
$result = 0;
foreach ($this->changes as $change) {
$result += $change->getCount(FileChange::LINE_ADD);
}
return $result;
}
/**
* @return int Number of deleted lines
*/
public function getDeletions()
{
$result = 0;
foreach ($this->changes as $change) {
$result += $change->getCount(FileChange::LINE_REMOVE);
}
return $result;
}
public function getOldName()
{
return $this->oldName;
}
public function getNewName()
{
return $this->newName;
}
public function getName()
{
if (null === $this->newName) {
return $this->oldName;
}
return $this->newName;
}
public function getOldMode()
{
return $this->oldMode;
}
public function getNewMode()
{
return $this->newMode;
}
public function getOldIndex()
{
return $this->oldIndex;
}
public function getNewIndex()
{
return $this->newIndex;
}
public function isBinary()
{
return $this->isBinary;
}
public function getChanges()
{
return $this->changes;
}
public function toArray()
{
return array(
'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) {
return $change->toArray();
}, $this->changes),
);
}
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']);
foreach ($array['changes'] as $change) {
$file->addChange(FileChange::fromArray($change));
}
return $file;
}
public function getAnchor()
{
return substr($this->newIndex, 0, 12);
}
public function getRepository()
{
return $this->repository;
}
public function setRepository(Repository $repository)
{
$this->repository = $repository;
}
public function getOldBlob()
{
if (null === $this->repository) {
throw new \RuntimeException('Repository is missing to return Blob object.');
}
if ($this->isCreation()) {
throw new \LogicException('Can\'t return old Blob on a creation');
}
return $this->repository->getBlob($this->oldIndex);
}
public function getNewBlob()
{
if (null === $this->repository) {
throw new \RuntimeException('Repository is missing to return Blob object.');
}
if ($this->isDeletion()) {
throw new \LogicException('Can\'t return new Blob on a deletion');
}
return $this->repository->getBlob($this->newIndex);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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
{
const LINE_CONTEXT = 0;
const LINE_REMOVE = -1;
const LINE_ADD = 1;
protected $rangeOldStart;
protected $rangeOldCount;
protected $rangeNewStart;
protected $rangeNewCount;
protected $lines;
public function __construct($rangeOldStart, $rangeOldCount, $rangeNewStart, $rangeNewCount, $lines)
{
$this->rangeOldStart = $rangeOldStart;
$this->rangeOldCount = $rangeOldCount;
$this->rangeNewStart = $rangeNewStart;
$this->rangeNewCount = $rangeNewCount;
$this->lines = $lines;
}
public function getCount($type)
{
$result = 0;
foreach ($this->lines as $line) {
if ($line[0] === $type) {
++$result;
}
}
return $result;
}
public function getRangeOldStart()
{
return $this->rangeOldStart;
}
public function getRangeOldCount()
{
return $this->rangeOldCount;
}
public function getRangeNewStart()
{
return $this->rangeNewStart;
}
public function getRangeNewCount()
{
return $this->rangeNewCount;
}
public function getLines()
{
return $this->lines;
}
public function toArray()
{
return array(
'range_old_start' => $this->rangeOldStart,
'range_old_count' => $this->rangeOldCount,
'range_new_start' => $this->rangeNewStart,
'range_new_count' => $this->rangeNewCount,
'lines' => $this->lines,
);
}
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']);
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Gitonomy\Git\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements GitExceptionInterface
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Gitonomy\Git\Exception;
class LogicException extends \LogicException implements GitExceptionInterface
{
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Gitonomy\Git\Exception;
use Symfony\Component\Process\Process;
class ProcessException extends RuntimeException implements GitExceptionInterface
{
protected $process;
public function __construct(Process $process)
{
parent::__construct("Error while running git command:\n".
$process->getCommandLine()."\n".
"\n".
$process->getErrorOutput()."\n".
"\n".
$process->getOutput()
);
$this->process = $process;
}
public function getErrorOutput()
{
return $this->process->getErrorOutput();
}
public function getOutput()
{
return $this->process->getOutput();
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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
{
public function __construct($reference)
{
parent::__construct(sprintf('Reference not found: "%s"', $reference));
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Gitonomy\Git\Exception;
class RuntimeException extends \RuntimeException implements GitExceptionInterface
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Gitonomy\Git\Exception;
class UnexpectedValueException extends \UnexpectedValueException implements GitExceptionInterface
{
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
/**
* Hooks collection, aggregated by repository.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Hooks
{
/**
* @var Gitonomy\Git\Repository
*/
protected $repository;
/**
* @var Repository
*/
public function __construct(Repository $repository)
{
$this->repository = $repository;
}
/**
* Tests if repository has a given hook.
*
* @param string $name Name of the hook
*
* @return bool
*/
public function has($name)
{
return file_exists($this->getPath($name));
}
/**
* Fetches content of a hook.
*
* @param string $name Name of the hook
*
* @return string Content of the hook
*
* @throws InvalidArgumentException Hook does not exist
*/
public function get($name)
{
if (!$this->has($name)) {
throw new InvalidArgumentException(sprintf('Hook named "%s" is not present', $name));
}
return file_get_contents($this->getPath($name));
}
/**
* Insert a hook in repository using a symlink.
*
* @param string $name Name of the hook to insert
* @param string $file Target of symlink
*
* @throws LogicException Hook is already present
* @throws RuntimeException Error on symlink creation
*/
public function setSymlink($name, $file)
{
if ($this->has($name)) {
throw new LogicException(sprintf('A hook "%s" is already defined', $name));
}
$path = $this->getPath($name);
if (false === symlink($file, $path)) {
throw new RuntimeException(sprintf('Unable to create hook "%s"', $name, $path));
}
}
/**
* Set a hook in repository.
*
* @param string $name Name of the hook
* @param string $content Content of the hook
*
* @throws LogicException The hook is already defined
*/
public function set($name, $content)
{
if ($this->has($name)) {
throw new LogicException(sprintf('A hook "%s" is already defined', $name));
}
$path = $this->getPath($name);
file_put_contents($path, $content);
chmod($path, 0777);
}
/**
* Removes a hook from repository.
*
* @param string $name Name of the hook
*
* @throws LogicException The hook is not present
*/
public function remove($name)
{
if (!$this->has($name)) {
throw new LogicException(sprintf('The hook "%s" was not found', $name));
}
unlink($this->getPath($name));
}
protected function getPath($name)
{
return $this->repository->getGitDir().'/hooks/'.$name;
}
}

View File

@@ -0,0 +1,232 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
use Gitonomy\Git\Util\StringHelper;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Log implements \Countable, \IteratorAggregate
{
/**
* @var Repository
*/
protected $repository;
/**
* @var null|RevisionList
*/
protected $revisions;
/**
* @var array
*/
protected $paths;
/**
* @var int
*/
protected $offset;
/**
* @var int
*/
protected $limit;
/**
* Instanciates a git log object.
*
* @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)
{
if (null !== $revisions && !$revisions instanceof RevisionList) {
$revisions = new RevisionList($repository, $revisions);
}
if (null === $paths) {
$paths = array();
} elseif (is_string($paths)) {
$paths = array($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)));
}
$this->repository = $repository;
$this->revisions = $revisions;
$this->paths = $paths;
$this->offset = $offset;
$this->limit = $limit;
}
/**
* @return Diff
*/
public function getDiff()
{
return $this->repository->getDiff($this->revisions);
}
/**
* @return RevisionList
*/
public function getRevisions()
{
return $this->revisions;
}
/**
* @return array
*/
public function getPaths()
{
return $this->paths;
}
/**
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* @param int $offset
*/
public function setOffset($offset)
{
$this->offset = $offset;
return $this;
}
/**
* @return int
*/
public function getLimit()
{
return $this->limit;
}
/**
* @param int $limit
*/
public function setLimit($limit)
{
$this->limit = $limit;
return $this;
}
public function getSingleCommit()
{
$limit = $this->limit;
$this->limit = 1;
$commits = $this->getCommits();
$this->setLimit($limit);
if (count($commits) === 0) {
throw new ReferenceNotFoundException('The log is empty');
}
return array_pop($commits);
}
/**
* @return array
*/
public function getCommits()
{
$args = array('--encoding='.StringHelper::getEncoding(), '--format=raw');
if (null !== $this->offset) {
$args[] = '--skip='.((int) $this->offset);
}
if (null !== $this->limit) {
$args[] = '-n';
$args[] = (int) $this->limit;
}
if (null !== $this->revisions) {
$args = array_merge($args, $this->revisions->getAsTextArray());
} else {
$args[] = '--all';
}
$args[] = '--';
$args = array_merge($args, $this->paths);
try {
$output = $this->repository->run('log', $args);
} catch (ProcessException $e) {
throw new ReferenceNotFoundException(sprintf('Can not find revision "%s"', implode(' ', $this->revisions->getAsTextArray())), null, $e);
}
$parser = new Parser\LogParser();
$parser->parse($output);
$result = array();
foreach ($parser->log as $commitData) {
$hash = $commitData['id'];
unset($commitData['id']);
$commit = $this->repository->getCommit($hash);
$commit->setData($commitData);
$result[] = $commit;
}
return $result;
}
/**
* @see Countable
*/
public function count()
{
return $this->countCommits();
}
/**
* @see IteratorAggregate
*/
public function getIterator()
{
return new \ArrayIterator($this->getCommits());
}
/**
* Count commits, without offset or limit.
*
* @return int
*/
public function countCommits()
{
if (null !== $this->revisions && count($this->revisions)) {
$output = $this->repository->run('rev-list', array_merge(array('--count'), $this->revisions->getAsTextArray(), array('--'), $this->paths));
} else {
$output = $this->repository->run('rev-list', array_merge(array('--count', '--all', '--'), $this->paths));
}
return (int) $output;
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
use Gitonomy\Git\Repository;
class BlameParser extends ParserBase
{
public $lines;
protected $repository;
public function __construct(Repository $repository)
{
$this->repository = $repository;
}
protected function doParse()
{
$this->lines = array();
$memory = array();
$line = 1;
while (!$this->isFinished()) {
$hash = $this->consumeHash();
$this->consume(' ');
$vars = $this->consumeRegexp('/(\d+) (\d+)( (\d+))?/A');
$sourceLine = $vars[1];
$targetLine = $vars[2];
$blockLine = isset($vars[4]) ? $vars[4] : null;
$this->consumeTo("\n");
$this->consumeNewLine();
if (!isset($memory[$hash])) {
foreach (array('author', 'author-mail', 'author-time', 'author-tz',
'committer', 'committer-mail', 'committer-time', 'committer-tz',
'summary', ) as $key) {
$this->consume($key);
$this->consumeTo("\n");
$this->consumeNewLine();
}
if ($this->expects('previous ')) {
$this->consumeTo("\n");
$this->consumeNewLine();
}
if ($this->expects('boundary')) {
$this->consumeNewLine();
}
$this->consume('filename');
$this->consumeTo("\n"); // filename
$this->consumeNewLine();
$memory[$hash] = $this->repository->getCommit($hash);
}
$content = $this->consumeTo("\n"); // content of line
$this->consumeNewLine();
$this->lines[$line] = new Line($memory[$hash], $sourceLine, $targetLine, $blockLine, $content);
++$line;
}
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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 CommitParser extends ParserBase
{
public $tree;
public $parents;
public $authorName;
public $authorEmail;
public $authorDate;
public $committerName;
public $committerEmail;
public $committerDate;
public $message;
protected function doParse()
{
$this->consume('tree ');
$this->tree = $this->consumeHash();
$this->consumeNewLine();
$this->parents = array();
while ($this->expects('parent ')) {
$this->parents[] = $this->consumeHash();
$this->consumeNewLine();
}
$this->consume('author ');
list($this->authorName, $this->authorEmail, $this->authorDate) = $this->consumeNameEmailDate();
$this->authorDate = $this->parseDate($this->authorDate);
$this->consumeNewLine();
$this->consume('committer ');
list($this->committerName, $this->committerEmail, $this->committerDate) = $this->consumeNameEmailDate();
$this->committerDate = $this->parseDate($this->committerDate);
// will consume an GPG signed commit if there is one
$this->consumeGPGSignature();
$this->consumeNewLine();
$this->consumeNewLine();
$this->message = $this->consumeAll();
}
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 array($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;
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
use Gitonomy\Git\Diff\FileChange;
class DiffParser extends ParserBase
{
public $files;
protected function doParse()
{
$this->files = array();
while (!$this->isFinished()) {
// 1. title
$vars = $this->consumeRegexp('/diff --git (a\/.*) (b\/.*)\n/');
$oldName = $vars[1];
$newName = $vars[2];
$oldIndex = null;
$newIndex = null;
$oldMode = null;
$newMode = null;
// 2. mode
if ($this->expects('new file mode ')) {
$newMode = $this->consumeTo("\n");
$this->consumeNewLine();
$oldMode = null;
}
if ($this->expects('old mode ')) {
$oldMode = $this->consumeTo("\n");
$this->consumeNewLine();
$this->consume('new mode ');
$newMode = $this->consumeTo("\n");
$this->consumeNewLine();
}
if ($this->expects('deleted file mode ')) {
$oldMode = $this->consumeTo("\n");
$newMode = null;
$this->consumeNewLine();
}
if ($this->expects('similarity index ')) {
$this->consumeRegexp('/\d{1,3}%\n/');
$this->consume('rename from ');
$this->consumeTo("\n");
$this->consumeNewLine();
$this->consume('rename to ');
$this->consumeTo("\n");
$this->consumeNewLine();
}
// 4. File informations
$isBinary = false;
if ($this->expects('index ')) {
$oldIndex = $this->consumeShortHash();
$this->consume('..');
$newIndex = $this->consumeShortHash();
if ($this->expects(' ')) {
$vars = $this->consumeRegexp('/\d{6}/');
$newMode = $oldMode = $vars[0];
}
$this->consumeNewLine();
if ($this->expects('--- ')) {
$oldName = $this->consumeTo("\n");
$this->consumeNewLine();
$this->consume('+++ ');
$newName = $this->consumeTo("\n");
$this->consumeNewLine();
} elseif ($this->expects('Binary files ')) {
$vars = $this->consumeRegexp('/(.*) and (.*) differ\n/');
$isBinary = true;
$oldName = $vars[1];
$newName = $vars[2];
}
}
$oldName = $oldName === '/dev/null' ? null : substr($oldName, 2);
$newName = $newName === '/dev/null' ? null : substr($newName, 2);
$oldIndex = preg_match('/^0+$/', $oldIndex) ? null : $oldIndex;
$newIndex = preg_match('/^0+$/', $newIndex) ? null : $newIndex;
$file = new File($oldName, $newName, $oldMode, $newMode, $oldIndex, $newIndex, $isBinary);
// 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
$this->consume(' @@');
$this->consumeTo("\n");
$this->consumeNewLine();
// 6. Lines
$lines = array();
while (true) {
if ($this->expects(' ')) {
$lines[] = array(FileChange::LINE_CONTEXT, $this->consumeTo("\n"));
} elseif ($this->expects('+')) {
$lines[] = array(FileChange::LINE_ADD, $this->consumeTo("\n"));
} elseif ($this->expects('-')) {
$lines[] = array(FileChange::LINE_REMOVE, $this->consumeTo("\n"));
} elseif ($this->expects("\ No newline at end of file")) {
// Ignore this case...
} else {
break;
}
$this->consumeNewLine();
}
$change = new FileChange($rangeOldStart, $rangeOldCount, $rangeNewStart, $rangeNewCount, $lines);
$file->addChange($change);
}
$this->files[] = $file;
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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();
protected function doParse()
{
$this->log = array();
while (!$this->isFinished()) {
$commit = array();
$this->consume('commit ');
$commit['id'] = $this->consumeHash();
$this->consumeNewLine();
$this->consume('tree ');
$commit['treeHash'] = $this->consumeHash();
$this->consumeNewLine();
$commit['parentHashes'] = array();
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']);
$this->consumeNewLine();
$this->consume('committer ');
list($commit['committerName'], $commit['committerEmail'], $commit['committerDate']) = $this->consumeNameEmailDate();
$commit['committerDate'] = $this->parseDate($commit['committerDate']);
// will consume an GPG signed commit if there is one
$this->consumeGPGSignature();
$this->consumeNewLine();
$this->consumeNewLine();
$message = '';
if ($this->expects(' ')) {
$this->cursor -= strlen(' ');
while ($this->expects(' ')) {
$message .= $this->consumeTo("\n")."\n";
$this->consumeNewLine();
}
}
else {
$this->cursor--;
}
if (!$this->isFinished()) {
$this->consumeNewLine();
}
$commit['message'] = $message;
$this->log[] = $commit;
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
abstract class ParserBase
{
protected $cursor;
protected $content;
protected $length;
abstract protected function doParse();
public function parse($content)
{
$this->cursor = 0;
$this->content = $content;
$this->length = strlen($this->content);
$this->doParse();
}
protected function isFinished()
{
return $this->cursor === $this->length;
}
protected function consumeAll()
{
$rest = substr($this->content, $this->cursor);
$this->cursor += strlen($rest);
return $rest;
}
protected function expects($expected)
{
$length = strlen($expected);
$actual = substr($this->content, $this->cursor, $length);
if ($actual !== $expected) {
return false;
}
$this->cursor += $length;
return true;
}
protected function consumeShortHash()
{
if (!preg_match('/([A-Za-z0-9]{7,40})/A', $this->content, $vars, null, $this->cursor)) {
throw new RuntimeException('No short hash found: '.substr($this->content, $this->cursor, 7));
}
$this->cursor += strlen($vars[1]);
return $vars[1];
}
protected function consumeHash()
{
if (!preg_match('/([A-Za-z0-9]{40})/A', $this->content, $vars, null, $this->cursor)) {
throw new RuntimeException('No hash found: '.substr($this->content, $this->cursor, 40));
}
$this->cursor += 40;
return $vars[1];
}
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));
}
$this->cursor += strlen($vars[0]);
return $vars;
}
protected function consumeTo($text)
{
$pos = strpos($this->content, $text, $this->cursor);
if (false === $pos) {
throw new RuntimeException(sprintf('Unable to find "%s"', $text));
}
$result = substr($this->content, $this->cursor, $pos - $this->cursor);
$this->cursor = $pos;
return $result;
}
protected function consume($expected)
{
$length = strlen($expected);
$actual = substr($this->content, $this->cursor, $length);
if ($actual !== $expected) {
throw new RuntimeException(sprintf('Expected "%s", but got "%s" (%s)', $expected, $actual, substr($this->content, $this->cursor, 10)));
}
$this->cursor += $length;
return $expected;
}
protected function consumeNewLine()
{
return $this->consume("\n");
}
/**
* @return string
*/
protected function consumeGPGSignature() {
$expected = "\ngpgsig ";
$length = strlen($expected);
$actual = substr($this->content, $this->cursor, $length);
if($actual != $expected) {
return '';
}
$this->cursor += $length;
return $this->consumeTo("\n\n");
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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
{
public $references;
protected function doParse()
{
$this->references = array();
while (!$this->isFinished()) {
$hash = $this->consumeHash();
$this->consume(' ');
$name = $this->consumeTo("\n");
$this->consumeNewLine();
$this->references[] = array($hash, $name);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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();
protected function doParse()
{
while (!$this->isFinished()) {
$vars = $this->consumeRegexp('/\d{6}/A');
$mode = $vars[0];
$this->consume(' ');
$vars = $this->consumeRegexp('/(blob|tree|commit)/A');
$type = $vars[0];
$this->consume(' ');
$hash = $this->consumeHash();
$this->consume("\t");
$name = $this->consumeTo("\n");
$this->consumeNewLine();
$this->entries[] = array($mode, $type, $hash, $name);
}
}
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
/**
* Push reference contains a commit interval. This object aggregates methods
* for this interval.
*
* @author Julien DIDIER <genzo.wm@gmail.com>
*/
class PushReference
{
const ZERO = '0000000000000000000000000000000000000000';
/**
* @var string
*/
protected $reference;
/**
* @var string
*/
protected $before;
/**
* @var string
*/
protected $after;
/**
* @var bool
*/
protected $isForce;
public function __construct(Repository $repository, $reference, $before, $after)
{
$this->repository = $repository;
$this->reference = $reference;
$this->before = $before;
$this->after = $after;
$this->isForce = $this->getForce();
}
/**
* @return Repository
*/
public function getRepository()
{
return $this->repository;
}
/**
* @return string
*/
public function getReference()
{
return $this->reference;
}
/**
* @return string
*/
public function getBefore()
{
return $this->before;
}
/**
* @return string
*/
public function getAfter()
{
return $this->after;
}
/**
* @return array
*/
public function getLog($excludes = array())
{
return $this->repository->getLog(array_merge(
array($this->getRevision()),
array_map(function ($e) {
return '^'.$e;
}, $excludes)
));
}
public function getRevision()
{
if ($this->isDelete()) {
throw new LogicException('No revision for deletion');
}
if ($this->isCreate()) {
return $this->getAfter();
}
return $this->getBefore().'..'.$this->getAfter();
}
/**
* @return bool
*/
public function isCreate()
{
return $this->isZero($this->before);
}
/**
* @return bool
*/
public function isDelete()
{
return $this->isZero($this->after);
}
/**
* @return bool
*/
public function isForce()
{
return $this->isForce;
}
/**
* @return bool
*/
public function isFastForward()
{
return !$this->isDelete() && !$this->isCreate() && !$this->isForce();
}
/**
* @return bool
*/
protected function isZero($reference)
{
return self::ZERO === $reference;
}
/**
* @return bool
*/
protected function getForce()
{
if ($this->isDelete() || $this->isCreate()) {
return false;
}
$result = $this->repository->run('merge-base', array(
$this->before,
$this->after,
));
return $this->before !== trim($result);
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Gitonomy\Git;
/**
* Reference in a Git repository.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
* @author Julien DIDIER <genzo.wm@gmail.com>
*/
abstract class Reference extends Revision
{
protected $commitHash;
public function __construct(Repository $repository, $revision, $commitHash = null)
{
$this->repository = $repository;
$this->revision = $revision;
$this->commitHash = $commitHash;
}
public function getFullname()
{
return $this->revision;
}
public function delete()
{
$this->repository->getReferences()->delete($this->getFullname());
}
public function getCommitHash()
{
if (null !== $this->commitHash) {
return $this->commitHash;
}
try {
$result = $this->repository->run('rev-parse', array('--verify', $this->revision));
} catch (ProcessException $e) {
throw new ReferenceNotFoundException(sprintf('Can not find revision "%s"', $this->revision));
}
return $this->commitHash = trim($result);
}
/**
* Returns the commit associated to the reference.
*
* @return Commit
*/
public function getCommit()
{
return $this->repository->getCommit($this->getCommitHash());
}
public function getLastModification($path = null)
{
return $this->getCommit()->getLastModification($path);
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\RuntimeException;
use Gitonomy\Git\Reference;
/**
* Representation of a branch reference.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Branch extends Reference
{
private $local = null;
public function getName()
{
$fullname = $this->getFullname();
if (preg_match('#^refs/heads/(?<name>.*)$#', $fullname, $vars)) {
return $vars['name'];
}
if (preg_match('#^refs/remotes/(?<remote>[^/]*)/(?<name>.*)$#', $fullname, $vars)) {
return $vars['remote'].'/'.$vars['name'];
}
throw new RuntimeException(sprintf('Cannot extract branch name from "%s"', $fullname));
}
public function isRemote()
{
$this->detectBranchType();
return !$this->local;
}
public function isLocal()
{
$this->detectBranchType();
return $this->local;
}
private function detectBranchType()
{
if (null === $this->local) {
$this->local = !preg_match('#^refs/remotes/(?<remote>[^/]*)/(?<name>.*)$#', $this->getFullname());
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Stash extends Reference
{
public function getName()
{
return 'stash';
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\RuntimeException;
use Gitonomy\Git\Reference;
/**
* Representation of a tag reference.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Tag extends Reference
{
public function getName()
{
if (!preg_match('#^refs/tags/(.*)$#', $this->revision, $vars)) {
throw new RuntimeException(sprintf('Cannot extract tag name from "%s"', $this->revision));
}
return $vars[1];
}
}

View File

@@ -0,0 +1,399 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
use Gitonomy\Git\Exception\RuntimeException;
use Gitonomy\Git\Reference\Branch;
use Gitonomy\Git\Reference\Stash;
use Gitonomy\Git\Reference\Tag;
/**
* Reference set associated to a repository.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
* @author Julien DIDIER <genzo.wm@gmail.com>
*/
class ReferenceBag implements \Countable, \IteratorAggregate
{
/**
* Repository object.
*
* @var Gitonomy\Git\Repository
*/
protected $repository;
/**
* Associative array of fullname references.
*
* @var array
*/
protected $references;
/**
* List with all tags.
*
* @var array
*/
protected $tags;
/**
* List with all branches.
*
* @var array
*/
protected $branches;
/**
* A boolean indicating if the bag is already initialized.
*
* @var bool
*/
protected $initialized = false;
/**
* Constructor.
*
* @param Gitonomy\Git\Repository $repository The repository
*/
public function __construct($repository)
{
$this->repository = $repository;
$this->references = array();
$this->tags = array();
$this->branches = array();
}
/**
* Returns a reference, by name.
*
* @param string $fullname Fullname of the reference (refs/heads/master, for example).
*
* @return Reference A reference object.
*/
public function get($fullname)
{
$this->initialize();
if (!isset($this->references[$fullname])) {
throw new ReferenceNotFoundException($fullname);
}
return $this->references[$fullname];
}
public function has($fullname)
{
$this->initialize();
return isset($this->references[$fullname]);
}
public function update(Reference $reference)
{
$fullname = $reference->getFullname();
$this->initialize();
$this->repository->run('update-ref', array($fullname, $reference->getCommitHash()));
$this->references[$fullname] = $reference;
return $reference;
}
public function createBranch($name, $commitHash)
{
$branch = new Branch($this->repository, 'refs/heads/'.$name, $commitHash);
return $this->update($branch);
}
public function createTag($name, $commitHash)
{
$tag = new Tag($this->repository, 'refs/tags/'.$name, $commitHash);
return $this->update($tag);
}
public function delete($fullname)
{
$this->repository->run('update-ref', array('-d', $fullname));
unset($this->references[$fullname]);
}
public function hasBranches()
{
$this->initialize();
return count($this->branches) > 0;
}
public function hasBranch($name)
{
return $this->has('refs/heads/'.$name);
}
public function hasRemoteBranch($name)
{
return $this->has('refs/remotes/'.$name);
}
public function hasTag($name)
{
return $this->has('refs/tags/'.$name);
}
public function getFirstBranch()
{
$this->initialize();
reset($this->branches);
return current($this->references);
}
/**
* @return array An array of Tag objects
*/
public function resolveTags($hash)
{
$this->initialize();
if ($hash instanceof Commit) {
$hash = $hash->getHash();
}
$tags = array();
foreach ($this->references as $reference) {
if ($reference instanceof Reference\Tag && $reference->getCommitHash() === $hash) {
$tags[] = $reference;
}
}
return $tags;
}
/**
* @return array An array of Branch objects
*/
public function resolveBranches($hash)
{
$this->initialize();
if ($hash instanceof Commit) {
$hash = $hash->getHash();
}
$branches = array();
foreach ($this->references as $reference) {
if ($reference instanceof Reference\Branch && $reference->getCommitHash() === $hash) {
$branches[] = $reference;
}
}
return $branches;
}
/**
* @return array An array of references
*/
public function resolve($hash)
{
$this->initialize();
if ($hash instanceof Commit) {
$hash = $hash->getHash();
}
$result = array();
foreach ($this->references as $k => $reference) {
if ($reference->getCommitHash() === $hash) {
$result[] = $reference;
}
}
return $result;
}
/**
* Returns all tags.
*
* @return array
*/
public function getTags()
{
$this->initialize();
return $this->tags;
}
/**
* Returns all branches.
*
* @return array
*/
public function getBranches()
{
$this->initialize();
$result = array();
foreach ($this->references as $reference) {
if ($reference instanceof Reference\Branch) {
$result[] = $reference;
}
}
return $result;
}
/**
* Returns all locales branches.
*
* @return array
*/
public function getLocalBranches()
{
$result = array();
foreach ($this->getBranches() as $branch) {
if ($branch->isLocal()) {
$result[] = $branch;
}
}
return $result;
}
/**
* Returns all remote branches.
*
* @return array
*/
public function getRemoteBranches()
{
$result = array();
foreach ($this->getBranches() as $branch) {
if ($branch->isRemote()) {
$result[] = $branch;
}
}
return $result;
}
/**
* @return array An associative array with fullname as key (refs/heads/master, refs/tags/0.1)
*/
public function getAll()
{
$this->initialize();
return $this->references;
}
/**
* @return Tag
*/
public function getTag($name)
{
$this->initialize();
return $this->get('refs/tags/'.$name);
}
/**
* @return Branch
*/
public function getBranch($name)
{
$this->initialize();
return $this->get('refs/heads/'.$name);
}
/**
* @return Branch
*/
public function getRemoteBranch($name)
{
$this->initialize();
return $this->get('refs/remotes/'.$name);
}
protected function initialize()
{
if (true === $this->initialized) {
return;
}
$this->initialized = true;
try {
$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);
}
}
$parser->parse($output);
foreach ($parser->references as $row) {
list($commitHash, $fullname) = $row;
if (preg_match('#^refs/(heads|remotes)/(.*)$#', $fullname)) {
if (preg_match('#.*HEAD$#', $fullname)) {
continue;
}
$reference = new Branch($this->repository, $fullname, $commitHash);
$this->references[$fullname] = $reference;
$this->branches[] = $reference;
} elseif (preg_match('#^refs/tags/(.*)$#', $fullname)) {
$reference = new Tag($this->repository, $fullname, $commitHash);
$this->references[$fullname] = $reference;
$this->tags[] = $reference;
} elseif ($fullname === 'refs/stash') {
$reference = new Stash($this->repository, $fullname, $commitHash);
$this->references[$fullname] = $reference;
} elseif (preg_match('#^refs/pull/(.*)$#', $fullname)) {
// Do nothing here
} elseif ($fullname === 'refs/notes/gtm-data') {
// Do nothing here
} else {
throw new RuntimeException(sprintf('Unable to parse "%s"', $fullname));
}
}
}
/**
* @return int
*
* @see Countable
*/
public function count()
{
$this->initialize();
return count($this->references);
}
/**
* @see IteratorAggregate
*/
public function getIterator()
{
$this->initialize();
return new \ArrayIterator($this->references);
}
}

View File

@@ -0,0 +1,667 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\RuntimeException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\ProcessUtils;
/**
* Git repository object.
*
* Main entry point for browsing a Git repository.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Repository
{
const DEFAULT_DESCRIPTION = "Unnamed repository; edit this file 'description' to name the repository.\n";
/**
* Directory containing git files.
*
* @var string
*/
protected $gitDir;
/**
* Working directory.
*
* @var string
*/
protected $workingDir;
/**
* Cache containing all objects of the repository.
*
* Associative array, indexed by object hash
*
* @var array
*/
protected $objects;
/**
* Reference bag associated to this repository.
*
* @var ReferenceBag
*/
protected $referenceBag;
/**
* Logger (can be null).
*
* @var LoggerInterface
*/
protected $logger;
/**
* Path to git command.
*/
protected $command;
/**
* Debug flag, indicating if errors should be thrown.
*
* @var bool
*/
protected $debug;
/**
* Environment variables that should be set for every running process.
*
* @var array
*/
protected $environmentVariables;
/**
* Timeout that should be set for every running process.
*
* @var int
*/
protected $processTimeout;
/**
* Constructs a new repository.
*
* Available options are:
*
* * working_dir : specify where working copy is located (option --work-tree)
*
* * debug : (default: true) enable/disable minimize errors and reduce log level
*
* * logger : a logger to use for logging actions (Psr\Log\LoggerInterface)
*
* * environment_variables : define environment variables for every ran process
*
* @param string $dir path to git repository
* @param array $options array of options values
*
* @throws InvalidArgumentException The folder does not exists
*/
public function __construct($dir, $options = array())
{
$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);
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'])));
}
$this->logger = $options['logger'];
$this->initDir($dir, $options['working_dir']);
$this->objects = array();
$this->debug = (bool) $options['debug'];
$this->environmentVariables = $options['environment_variables'];
$this->processTimeout = $options['process_timeout'];
$this->command = $options['command'];
if (true === $this->debug && null !== $this->logger) {
$this->logger->debug(sprintf('Repository created (git dir: "%s", working dir: "%s")', $this->gitDir, $this->workingDir ?: 'none'));
}
}
/**
* Initializes directory attributes on repository:.
*
* @param string $gitDir directory of a working copy with files checked out
* @param string $workingDir directory containing git files (objects, config...)
*/
private function initDir($gitDir, $workingDir = null)
{
$realGitDir = realpath($gitDir);
if (false === $realGitDir) {
throw new InvalidArgumentException(sprintf('Directory "%s" does not exist or is not a directory', $gitDir));
} else if (!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;
$realGitDir = $realGitDir.'/.git';
}
$this->gitDir = $realGitDir;
$this->workingDir = $workingDir;
}
/**
* Tests if repository is a bare repository.
*
* @return bool
*/
public function isBare()
{
return null === $this->workingDir;
}
/**
* Returns the HEAD resolved as a commit.
*
* @return Commit|null returns a Commit or ``null`` if repository is empty
*/
public function getHeadCommit()
{
$head = $this->getHead();
if ($head instanceof Reference) {
return $head->getCommit();
}
return $head;
}
/**
* @throws RuntimeException Unable to find file HEAD (debug-mode only)
*
* @return Reference|Commit|null current HEAD object or null if error occurs
*/
public function getHead()
{
$file = $this->gitDir.'/HEAD';
if (!file_exists($file)) {
$message = sprintf('Unable to find HEAD file ("%s")', $file);
if (null !== $this->logger) {
$this->logger->error($message);
}
if (true === $this->debug) {
throw new RuntimeException($message);
}
}
$content = trim(file_get_contents($file));
if (null !== $this->logger) {
$this->logger->debug('HEAD file read: '.$content);
}
if (preg_match('/^ref: (.+)$/', $content, $vars)) {
return $this->getReferences()->get($vars[1]);
} elseif (preg_match('/^[0-9a-f]{40}$/', $content)) {
return $this->getCommit($content);
}
$message = sprintf('Unexpected HEAD file content (file: %s). Content of file: %s', $file, $content);
if (null !== $this->logger) {
$this->logger->error($message);
}
if (true === $this->debug) {
throw new RuntimeException($message);
}
}
/**
* @return bool
*/
public function isHeadDetached()
{
return !$this->isHeadAttached();
}
/**
* @return bool
*/
public function isHeadAttached()
{
return $this->getHead() instanceof Reference;
}
/**
* Returns the path to the git repository.
*
* @return string A directory path
*/
public function getPath()
{
return $this->workingDir === null ? $this->gitDir : $this->workingDir;
}
/**
* Returns the directory containing git files (git-dir).
*
* @return string
*/
public function getGitDir()
{
return $this->gitDir;
}
/**
* Returns the work-tree directory. This may be null if repository is
* bare.
*
* @return string path to repository or null if repository is bare
*/
public function getWorkingDir()
{
return $this->workingDir;
}
/**
* Instanciates a revision.
*
* @param string $name Name of the revision
*
* @return Revision
*/
public function getRevision($name)
{
return new Revision($this, $name);
}
/**
* @return WorkingCopy
*/
public function getWorkingCopy()
{
return new WorkingCopy($this);
}
/**
* Returns the reference list associated to the repository.
*
* @return ReferenceBag
*/
public function getReferences()
{
if (null === $this->referenceBag) {
$this->referenceBag = new ReferenceBag($this);
}
return $this->referenceBag;
}
/**
* Instanciates a commit object or fetches one from the cache.
*
* @param string $hash A commit hash, with a length of 40
*
* @return Commit
*/
public function getCommit($hash)
{
if (!isset($this->objects[$hash])) {
$this->objects[$hash] = new Commit($this, $hash);
}
return $this->objects[$hash];
}
/**
* Instanciates a tree object or fetches one from the cache.
*
* @param string $hash A tree hash, with a length of 40
*
* @return Tree
*/
public function getTree($hash)
{
if (!isset($this->objects[$hash])) {
$this->objects[$hash] = new Tree($this, $hash);
}
return $this->objects[$hash];
}
/**
* Instanciates a blob object or fetches one from the cache.
*
* @param string $hash A blob hash, with a length of 40
*
* @return Blob
*/
public function getBlob($hash)
{
if (!isset($this->objects[$hash])) {
$this->objects[$hash] = new Blob($this, $hash);
}
return $this->objects[$hash];
}
public function getBlame($revision, $file, $lineRange = null)
{
if (is_string($revision)) {
$revision = $this->getRevision($revision);
}
return new Blame($this, $revision, $file, $lineRange);
}
/**
* Returns log for a given set of revisions and paths.
*
* All those values can be null, meaning everything.
*
* @param array $revisions An array of revisions to show logs from. Can be
* any text value type
* @param array $paths Restrict log to modifications occurring on given
* paths.
* @param int $offset Start from a given offset in results.
* @param int $limit Limit number of total results.
*
* @return Log
*/
public function getLog($revisions = null, $paths = null, $offset = null, $limit = null)
{
return new Log($this, $revisions, $paths, $offset, $limit);
}
/**
* @return Diff
*/
public function getDiff($revisions)
{
if (null !== $revisions && !$revisions instanceof RevisionList) {
$revisions = new RevisionList($this, $revisions);
}
$args = array_merge(array('-r', '-p', '-m', '-M', '--no-commit-id', '--full-index'), $revisions->getAsTextArray());
$diff = Diff::parse($this->run('diff', $args));
$diff->setRepository($this);
return $diff;
}
/**
* Returns the size of repository, in kilobytes.
*
* @return int A sum, in kilobytes
*
* @throws RuntimeException An error occurred while computing size
*/
public function getSize()
{
$commandlineArguments = array('du', '-skc', $this->gitDir);
$commandline = $this->normalizeCommandlineArguments($commandlineArguments);
$process = new Process($commandline);
$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);
}
if (true === $this->debug) {
throw new RuntimeException('unable to parse repository size output');
}
return;
}
return $vars[1];
}
/**
* Executes a shell command on the repository, using PHP pipes.
*
* @param string $command The command to execute
*/
public function shell($command, array $env = array())
{
$argument = sprintf('%s \'%s\'', $command, $this->gitDir);
$prefix = '';
foreach ($env as $name => $value) {
$prefix .= sprintf('export %s=%s;', escapeshellarg($name), escapeshellarg($value));
}
proc_open($prefix.'git shell -c '.escapeshellarg($argument), array(STDIN, STDOUT, STDERR), $pipes);
}
/**
* Returns the hooks object.
*
* @return Hooks
*/
public function getHooks()
{
return new Hooks($this);
}
/**
* Returns description of repository from description file in git directory.
*
* @return string The description
*/
public function getDescription()
{
$file = $this->gitDir.'/description';
$exists = is_file($file);
if (null !== $this->logger && true === $this->debug) {
if (false === $exists) {
$this->logger->debug(sprintf('no description file in repository ("%s")', $file));
} else {
$this->logger->debug(sprintf('reading description file in repository ("%s")', $file));
}
}
if (false === $exists) {
return static::DEFAULT_DESCRIPTION;
}
return file_get_contents($this->gitDir.'/description');
}
/**
* Tests if repository has a custom set description.
*
* @return bool
*/
public function hasDescription()
{
return static::DEFAULT_DESCRIPTION !== $this->getDescription();
}
/**
* Changes the repository description (file description in git-directory).
*
* @return Repository the current repository
*/
public function setDescription($description)
{
$file = $this->gitDir.'/description';
if (null !== $this->logger && true === $this->debug) {
$this->logger->debug(sprintf('change description file content to "%s" (file: %s)', $description, $file));
}
file_put_contents($file, $description);
return $this;
}
/**
* This command is a facility command. You can run any command
* directly on git repository.
*
* @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)
*/
public function run($command, $args = array())
{
$process = $this->getProcess($command, $args);
if ($this->logger) {
$this->logger->info(sprintf('run command: %s "%s" ', $command, implode(' ', $args)));
$before = microtime(true);
}
$process->run();
$output = $process->getOutput();
if ($this->logger && $this->debug) {
$duration = microtime(true) - $before;
$this->logger->debug(sprintf('last command (%s) duration: %sms', $command, sprintf('%.2f', $duration * 1000)));
$this->logger->debug(sprintf('last command (%s) return code: %s', $command, $process->getExitCode()));
$this->logger->debug(sprintf('last command (%s) output: %s', $command, $output));
}
if (!$process->isSuccessful()) {
$error = sprintf("error while running %s\n output: \"%s\"", $command, $process->getErrorOutput());
if ($this->logger) {
$this->logger->error($error);
}
if ($this->debug) {
throw new ProcessException($process);
}
return;
}
return $output;
}
/**
* Set repository logger.
*
* @param LoggerInterface $logger A logger
*
* @return Repository The current repository
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
return $this;
}
/**
* Returns repository logger.
*
* @return LoggerInterface the logger or null
*/
public function getLogger()
{
return $this->logger;
}
/**
* Clones the current repository to a new directory and return instance of new repository.
*
* @param string $path path to the new repository in which current repository will be cloned
* @param bool $bare flag indicating if repository is bare or has a working-copy
*
* @return Repository the newly created repository
*/
public function cloneTo($path, $bare = true, array $options = array())
{
return Admin::cloneTo($path, $this->gitDir, $bare, $options);
}
/**
* This internal method is used to create a process object.
*
* Made private to be sure that process creation is handled through the run method.
* run method ensures logging and debug.
*
* @see self::run
*/
private function getProcess($command, $args = array())
{
$base = array($this->command, '--git-dir', $this->gitDir);
if ($this->workingDir) {
$base = array_merge($base, array('--work-tree', $this->workingDir));
}
$base[] = $command;
$commandlineArguments = array_merge($base, $args);
$commandline = $this->normalizeCommandlineArguments($commandlineArguments);
$process = new Process($commandline);
$process->setEnv($this->environmentVariables);
$process->setTimeout($this->processTimeout);
$process->setIdleTimeout($this->processTimeout);
return $process;
}
/**
* This internal helper method is used to convert an array of commandline
* arguments to an escaped commandline string for older versions of the
* Symfony Process component.
*
* It acts as a backward compatible layer for Symfony Process < 3.3.
*
* @param array $arguments a list of command line arguments
*
* @return string|array a single escaped string (< 4.0) or a raw array of
* the arguments passed in (4.0+)
*
* @see Process::escapeArgument()
* @see ProcessUtils::escapeArgument()
*/
private function normalizeCommandlineArguments(array $arguments)
{
// From version 4.0 and onwards, the Process accepts an array of
// arguments, and escaping is taken care of automatically.
if (!class_exists('Symfony\Component\Process\ProcessBuilder')) {
return $arguments;
}
// For version < 3.3, the Process only accepts a simple string
// as commandline, and escaping has to happen manually.
$commandline = implode(' ', array_map(
'Symfony\Component\Process\ProcessUtils::escapeArgument',
$arguments
));
return $commandline;
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Gitonomy\Git;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Revision
{
/**
* @var Repository
*/
protected $repository;
/**
* @var string
*/
protected $revision;
public function __construct(Repository $repository, $revision)
{
$this->repository = $repository;
$this->revision = $revision;
}
/**
* @return Log
*/
public function getLog($paths = null, $offset = null, $limit = null)
{
return $this->repository->getLog($this, $paths, $offset, $limit);
}
/**
* Returns the last modification date of the reference.
*
* @return Commit
*/
public function getCommit()
{
return $this->getLog()->getSingleCommit();
}
/**
* @return string
*/
public function getRevision()
{
return $this->revision;
}
/**
* @return Repository
*/
public function getRepository()
{
return $this->repository;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Gitonomy\Git;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class RevisionList implements \IteratorAggregate, \Countable
{
protected $revisions;
/**
* Constructs a revision list from a variety of types.
*
* @param mixed $revisions can be a string, an array of strings or an array of Revision, Branch, Tag, Commit
*/
public function __construct(Repository $repository, $revisions)
{
if (is_string($revisions)) {
$revisions = array($repository->getRevision($revisions));
} elseif ($revisions instanceof Revision) {
$revisions = array($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)));
}
if (count($revisions) == 0) {
throw new \InvalidArgumentException(sprintf('Empty revision list not allowed'));
}
foreach ($revisions as $i => $revision) {
if (is_string($revision)) {
$revisions[$i] = new Revision($repository, $revision);
} elseif (!$revision instanceof Revision) {
throw new \InvalidArgumentException(sprintf('Expected a "Revision", got a "%s".', is_object($revision) ? get_class($revision) : gettype($revision)));
}
}
$this->revisions = $revisions;
}
public function getAll()
{
return $this->revisions;
}
public function getIterator()
{
return new \ArrayIterator($this->revisions);
}
public function count()
{
return count($this->revisions);
}
public function getAsTextArray()
{
return array_map(function ($revision) {
return $revision->getRevision();
}, $this->revisions);
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\UnexpectedValueException;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class Tree
{
protected $repository;
protected $hash;
protected $isInitialized = false;
protected $entries;
public function __construct(Repository $repository, $hash)
{
$this->repository = $repository;
$this->hash = $hash;
}
public function getHash()
{
return $this->hash;
}
protected function initialize()
{
if (true === $this->isInitialized) {
return;
}
$output = $this->repository->run('cat-file', array('-p', $this->hash));
$parser = new Parser\TreeParser();
$parser->parse($output);
$this->entries = array();
foreach ($parser->entries as $entry) {
list($mode, $type, $hash, $name) = $entry;
if ($type == 'blob') {
$this->entries[$name] = array($mode, $this->repository->getBlob($hash));
} elseif ($type == 'tree') {
$this->entries[$name] = array($mode, $this->repository->getTree($hash));
} else {
$this->entries[$name] = array($mode, new CommitReference($hash));
}
}
$this->isInitialized = true;
}
/**
* @return array An associative array name => $object
*/
public function getEntries()
{
$this->initialize();
return $this->entries;
}
public function getEntry($name)
{
$this->initialize();
if (!isset($this->entries[$name])) {
throw new InvalidArgumentException('No entry '.$name);
}
return $this->entries[$name][1];
}
public function resolvePath($path)
{
if ($path == '') {
return $this;
}
$path = preg_replace('#^/#', '', $path);
$segments = explode('/', $path);
$element = $this;
foreach ($segments as $segment) {
if ($element instanceof self) {
$element = $element->getEntry($segment);
} elseif ($entry instanceof Blob) {
throw new InvalidArgumentException('Unresolvable path');
} else {
throw new UnexpectedValueException('Unknow type of element');
}
}
return $element;
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Gitonomy\Git\Util;
/**
* Helper class to support language particularity.
*
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class StringHelper
{
private static $encoding = 'utf-8';
public static function getEncoding()
{
return self::$encoding;
}
public static function setEncoding($encoding)
{
self::$encoding = $encoding;
}
public static function strlen($string)
{
return function_exists('mb_strlen') ? mb_strlen($string, self::$encoding) : strlen($string);
}
public static function substr($string, $start, $length = false)
{
if (false === $length) {
$length = self::strlen($string);
}
return function_exists('mb_substr') ? mb_substr($string, $start, $length, self::$encoding) : substr($string, $start, $length);
}
public static function strpos($haystack, $needle, $offset = 0)
{
return function_exists('mb_strpos') ? mb_strpos($haystack, $needle, $offset, self::$encoding) : strpos($haystack, $needle, $offset);
}
public static function strrpos($haystack, $needle, $offset = 0)
{
return function_exists('mb_strrpos') ? mb_strrpos($haystack, $needle, $offset, self::$encoding) : strrpos($haystack, $needle, $offset);
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\LogicException;
/**
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*/
class WorkingCopy
{
/**
* @var Repository
*/
protected $repository;
public function __construct(Repository $repository)
{
$this->repository = $repository;
if ($this->repository->isBare()) {
throw new LogicException('Can\'t create a working copy on a bare 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);
return $lines;
}
public function getDiffPending()
{
$diff = Diff::parse($this->run('diff', array('-r', '-p', '-m', '-M', '--full-index')));
$diff->setRepository($this->repository);
return $diff;
}
public function getDiffStaged()
{
$diff = Diff::parse($this->run('diff', array('-r', '-p', '-m', '-M', '--full-index', '--staged')));
$diff->setRepository($this->repository);
return $diff;
}
/**
* @return WorkingCopy
*/
public function checkout($revision, $branch = null)
{
$args = array();
if ($revision instanceof Commit) {
$args[] = $revision->getHash();
} elseif ($revision instanceof Reference) {
$args[] = $revision->getFullname();
} elseif (is_string($revision)) {
$args[] = $revision;
} else {
throw new InvalidArgumentException(sprintf('Unknown type "%s"', gettype($revision)));
}
if (null !== $branch) {
$args = array_merge($args, array('-b', $branch));
}
$this->run('checkout', $args);
return $this;
}
protected function run($command, array $args = array())
{
return $this->repository->run($command, $args);
}
}

74
vendor/gitonomy/gitlib/test-git-versions.sh vendored Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
set -e
URL="git@github.com:git/git.git"
VERSIONS="v1.6.3 v1.6.6 v1.7.1 v1.7.5 v1.7.9 v1.8.1"
DIRNAME="git-builds"
if [ ! -d "$DIRNAME" ]; then
mkdir - "$DIRNAME"
fi
cd "$DIRNAME"
# remote repository
if [ -d cache ]; then
echo "- update from github..."
cd cache
git fetch -q
cd ..
else
echo "- clone from github..."
git clone "$URL" cache -q
fi
# build
for VERSION in $VERSIONS; do
echo "- build $VERSION"
LOGFILE="build.$VERSION.log"
LOCKFILE="build.$VERSION.lock"
if [ -f "$LOCKFILE" ]; then
echo " - lock present, destroy build..."
rm -rf "$VERSION" "build.$VERSION.log"
fi
if [ -d "$VERSION" ]; then
echo " - already built, skipping..."
continue
fi
touch "$LOCKFILE"
echo " - log: $LOGFILE"
touch "$LOGFILE"
mkdir "$VERSION"
echo " - clone repository"
git clone -s cache "$VERSION/source" -q
echo " - checkout version"
cd "$VERSION/source"
git checkout "$VERSION" -q
mkdir ../build
PREFIX="`readlink -f ../build`"
echo " - build..."
autoconf >> "../../$LOGFILE" 2>&1
./configure --prefix="$PREFIX" >> "../../$LOGFILE" 2>&1
make all >> "../../$LOGFILE" 2>&1
make install >> "../../$LOGFILE" 2>&1
echo " - build ok"
cd ../..
rm "$LOCKFILE"
done
cd ..
# test
echo ""
for VERSION in $VERSIONS; do
echo "- test $VERSION"
GIT_COMMAND="`readlink -f $DIRNAME/$VERSION/build/bin/git`"
echo " - command: $GIT_COMMAND"
GIT_COMMAND="$GIT_COMMAND" phpunit || true
done

View File

@@ -0,0 +1,149 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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 TestCase
{
const REPOSITORY_URL = 'http://github.com/gitonomy/foobar.git';
const LONGFILE_COMMIT = '4f17752acc9b7c54ba679291bf24cb7d354f0f4f';
const BEFORE_LONGFILE_COMMIT = 'e0ec50e2af75fa35485513f60b2e658e245227e9';
const LONGMESSAGE_COMMIT = '3febd664b6886344a9b32d70657687ea4b1b4fab';
const INITIAL_COMMIT = '74acd054c8ec873ae6be044041d3a85a4f890ba5';
const MERGE_COMMIT = '2f5b9d0a4e6e7173d7816e417805709c708674f8';
const ENCODING_COMMIT = '779420b9b936f18a0b6579e1499a85b14270802e';
const SIGNED_COMMIT = 'e1a83f16ed61ae3807e5652c7ef894692c813513';
/**
* Local clone of remote URL. Avoids network call on each test.
*/
private static $localRepository;
/**
* Creates an empty git repository and returns instance.
*
* @return Repository
*/
public static function createEmptyRepository($bare = true)
{
$dir = self::createTempDir();
$repository = Admin::init($dir, $bare, self::getOptions());
self::registerDeletion($repository);
return $repository;
}
/**
* Can be used as data provider to get bare/not-bare repositories.
*/
public static function provideFoobar()
{
return array(
array(self::createFoobarRepository()),
array(self::createFoobarRepository(false)),
);
}
/**
* Can be used as data provider to get bare/not-bare repositories.
*/
public static function provideEmpty()
{
return array(
array(self::createEmptyRepository()),
array(self::createEmptyRepository(false)),
);
}
/**
* Creates a fixture test repository.
*
* @return Repository
*/
public static function createFoobarRepository($bare = true)
{
if (null === self::$localRepository) {
self::$localRepository = Admin::cloneTo(self::createTempDir(), self::REPOSITORY_URL, $bare, self::getOptions());
}
$repository = self::$localRepository->cloneTo(self::createTempDir(), $bare, self::getOptions());
self::registerDeletion($repository);
return $repository;
}
public static function registerDeletion(Repository $repository)
{
register_shutdown_function(function () use ($repository) {
if ($repository->getWorkingDir()) {
$dir = $repository->getWorkingDir();
} else {
$dir = $repository->getGitDir();
}
AbstractTest::deleteDir($dir);
});
}
/**
* Created an empty directory and return path to it.
*
* @return string a fullpath
*/
public static function createTempDir()
{
$tmpDir = tempnam(sys_get_temp_dir(), 'gitlib_');
unlink($tmpDir);
mkdir($tmpDir);
return $tmpDir;
}
/**
* Deletes a directory recursively.
*
* @param string $dir directory to delete
*/
public static function deleteDir($dir)
{
$iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $file) {
if (!is_link($file)) {
chmod($file, 0777);
}
if (is_dir($file)) {
rmdir($file);
} else {
unlink($file);
}
}
chmod($dir, 0777);
rmdir($dir);
}
protected static function getOptions()
{
$command = isset($_SERVER['GIT_COMMAND']) ? $_SERVER['GIT_COMMAND'] : 'git';
$envs = isset($_SERVER['GIT_ENVS']) ? (array) $_SERVER['GIT_ENVS'] : array();
return array(
'command' => $command,
'environment_variables' => $envs,
'process_timeout' => 60,
);
}
}

View File

@@ -0,0 +1,177 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\Reference\Branch;
use Gitonomy\Git\Repository;
class AdminTest extends AbstractTest
{
private $tmpDir;
public function setUp()
{
$this->tmpDir = self::createTempDir();
}
public function tearDown()
{
$this->deleteDir(self::createTempDir());
}
public function testBare()
{
$repository = Admin::init($this->tmpDir, true, self::getOptions());
$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->assertEquals($this->tmpDir, $repository->getGitDir(), 'The folder passed as argument is git dir');
$this->assertNull($repository->getWorkingDir(), 'No working dir in bare repository');
}
public function testNotBare()
{
$repository = Admin::init($this->tmpDir, false, self::getOptions());
$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->assertEquals($this->tmpDir.'/.git', $repository->getGitDir(), 'git dir as subfolder of argument');
$this->assertEquals($this->tmpDir, $repository->getWorkingDir(), 'working dir present in bare repository');
}
/**
* @dataProvider provideFoobar
*/
public function testClone($repository)
{
$newDir = self::createTempDir();
$new = $repository->cloneTo($newDir, $repository->isBare(), 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));
if ($repository->isBare()) {
$this->assertEquals($newDir, $new->getGitDir());
$this->assertTrue(in_array('refs/heads/new-feature', $newRefs));
} else {
$this->assertEquals($newDir.'/.git', $new->getGitDir());
$this->assertEquals($newDir, $new->getWorkingDir());
}
}
public function testCloneBranchBare()
{
//we can't use AbstractText::createFoobarRepository()
//because it does not clone other branches than "master"
//so we test it directly against the remote repository
$newDir = self::createTempDir();
$new = Admin::cloneBranchTo($newDir, self::REPOSITORY_URL, 'new-feature');
self::registerDeletion($new);
$head = $new->getHead();
$this->assertTrue($head instanceof Branch, 'HEAD is a branch');
$this->assertEquals('new-feature', $head->getName(), 'HEAD is branch new-feature');
}
public function testCloneBranchNotBare()
{
//we can't use AbstractText::createFoobarRepository()
//because it does not clone other branches than "master"
//so we test it directly against remote repository
$newDir = self::createTempDir();
$new = Admin::cloneBranchTo($newDir, self::REPOSITORY_URL, 'new-feature', false);
self::registerDeletion($new);
$head = $new->getHead();
$this->assertTrue($head instanceof Branch, 'HEAD is a branch');
$this->assertEquals('new-feature', $head->getName(), 'HEAD is branch new-feature');
}
/**
* @dataProvider provideFoobar
*/
public function testMirror($repository)
{
$newDir = self::createTempDir();
$new = Admin::mirrorTo($newDir, $repository->getGitDir(), 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->assertEquals($newDir, $new->getGitDir());
if ($repository->isBare()) {
$this->assertTrue(in_array('refs/heads/new-feature', $newRefs));
} else {
$this->assertTrue(in_array('refs/remotes/origin/new-feature', $newRefs));
}
}
/**
* @dataProvider provideFoobar
*/
public function testCheckValidRepository($repository)
{
$url = $repository->getGitDir();
$this->assertTrue(Admin::isValidRepository($url));
}
public function testCheckInvalidRepository()
{
$url = $this->tmpDir.'/invalid.git';
mkdir($url);
$this->assertFalse(Admin::isValidRepository($url));
}
/**
* @expectedException RuntimeException
*/
public function testExistingFile()
{
$file = $this->tmpDir.'/test';
touch($file);
Admin::init($file, true, self::getOptions());
}
public function testCloneRepository()
{
$newDir = self::createTempDir();
$args = array();
$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->assertEquals($newDir.'/.git', $new->getGitDir());
$this->assertEquals($newDir, $new->getWorkingDir());
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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
{
/**
* @dataProvider provideFoobar
*/
public function testBlame($repository)
{
$blame = $repository->getBlame(self::LONGFILE_COMMIT, 'README.md');
$this->assertCount(7, $blame);
$this->assertEquals('alice', $blame->getLine(1)->getCommit()->getAuthorName());
$this->assertEquals(self::INITIAL_COMMIT, $blame->getLine(1)->getCommit()->getHash());
$this->assertEquals('alice', $blame->getLine(5)->getCommit()->getAuthorName());
$this->assertNotEquals(self::INITIAL_COMMIT, $blame->getLine(5)->getCommit()->getHash());
}
/**
* @dataProvider provideFoobar
*/
public function testGroupedBlame($repository)
{
$blame = $repository->getBlame(self::LONGFILE_COMMIT, 'README.md')->getGroupedLines();
$this->assertCount(3, $blame);
$this->assertEquals(self::INITIAL_COMMIT, $blame[0][0]->getHash());
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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 BlobTest extends AbstractTest
{
const README_FRAGMENT = 'Foo Bar project';
public function getReadmeBlob($repository)
{
return $repository->getCommit(self::LONGFILE_COMMIT)->getTree()->resolvePath('README.md');
}
/**
* @dataProvider provideFoobar
*/
public function testGetContent($repository)
{
$blob = $this->getReadmeBlob($repository);
$this->assertContains(self::README_FRAGMENT, $blob->getContent());
}
/**
* @dataProvider provideFoobar
* @expectedException RuntimeException
*/
public function testNotExisting($repository)
{
$blob = $repository->getBlob('foobar');
$blob->getContent();
}
/**
* @dataProvider provideFoobar
*/
public function testGetMimetype($repository)
{
$blob = $this->getReadmeBlob($repository);
$this->assertRegexp('#text/plain#', $blob->getMimetype());
}
/**
* @dataProvider provideFoobar
*/
public function testIsText($repository)
{
$blob = $this->getReadmeBlob($repository);
$this->assertTrue($blob->isText());
}
/**
* @dataProvider provideFoobar
*/
public function testIsBinary($repository)
{
$blob = $this->getReadmeBlob($repository);
$this->assertFalse($blob->isBinary());
}
}

View File

@@ -0,0 +1,301 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
class CommitTest extends AbstractTest
{
/**
* @dataProvider provideFoobar
*/
public function testGetDiff($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$diff = $commit->getDiff();
$this->assertTrue($diff instanceof Diff, 'getDiff() returns a Diff object');
}
/**
* @dataProvider provideFoobar
*/
public function testGetHash($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals(self::LONGFILE_COMMIT, $commit->getHash());
}
/**
* @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');
}
/**
* @dataProvider provideFoobar
*/
public function testGetShortHash($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('4f17752', $commit->getShortHash(), 'Short hash');
}
/**
* @dataProvider provideFoobar
*/
public function testGetParentHashes_WithNoParent($repository)
{
$commit = $repository->getCommit(self::INITIAL_COMMIT);
$this->assertEquals(0, count($commit->getParentHashes()), 'No parent on initial commit');
}
/**
* @dataProvider provideFoobar
*/
public function testGetParentHashes_WithOneParent($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$parents = $commit->getParentHashes();
$this->assertEquals(1, count($parents), 'One parent found');
$this->assertEquals(self::BEFORE_LONGFILE_COMMIT, $parents[0], 'Parent hash is correct');
}
/**
* @dataProvider provideFoobar
*/
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->assertEquals(self::BEFORE_LONGFILE_COMMIT, $parents[0]->getHash(), "First parents's hash is correct");
}
/**
* @dataProvider provideFoobar
*/
public function testGetTreeHash($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('b06890c7b10904979d2f69613c2ccda30aafe262', $commit->getTreeHash(), 'Tree hash is correct');
}
/**
* @dataProvider provideFoobar
*/
public function testGetTree($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertInstanceOf('Gitonomy\Git\Tree', $commit->getTree(), 'Tree is a tree');
$this->assertEquals('b06890c7b10904979d2f69613c2ccda30aafe262', $commit->getTree()->getHash(), 'Tree hash is correct');
}
/**
* @dataProvider provideFoobar
*/
public function testGetAuthorName($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('alice', $commit->getAuthorName(), 'Author name');
}
/**
* @dataProvider provideFoobar
*/
public function testGetAuthorEmail($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('alice@example.org', $commit->getAuthorEmail(), 'Author email');
}
/**
* @dataProvider provideFoobar
*/
public function testGetAuthorDate($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('2012-12-31 14:21:03', $commit->getAuthorDate()->format('Y-m-d H:i:s'), 'Author date');
}
/**
* @dataProvider provideFoobar
*/
public function testGetCommitterName($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('alice', $commit->getCommitterName(), 'Committer name');
}
/**
* @dataProvider provideFoobar
*/
public function testGetCommitterEmail($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('alice@example.org', $commit->getCommitterEmail(), 'Committer email');
}
/**
* @dataProvider provideFoobar
*/
public function testGetCommitterDate($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('2012-12-31 14:21:03', $commit->getCommitterDate()->format('Y-m-d H:i:s'), 'Committer date');
}
/**
* @dataProvider provideFoobar
*/
public function testGetMessage($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$this->assertEquals('add a long file'."\n", $commit->getMessage());
}
/**
* This test ensures that GPG signed commits does not break the reading of a commit
* message.
*
* @dataProvider provideFoobar
*/
public function testGetSignedMessage($repository)
{
$commit = $repository->getCommit(self::SIGNED_COMMIT);
$this->assertEquals('signed commit'."\n", $commit->getMessage());
}
/**
* @dataProvider provideFoobar
*/
public function testGetShortMessage($repository)
{
// tests with a multi-line message
$commit = $repository->getCommit(self::LONGMESSAGE_COMMIT);
$this->assertEquals('Fixed perm...', $commit->getShortMessage(10));
$this->assertEquals('Fixed perm!!!', $commit->getShortMessage(10, false, '!!!'));
$this->assertEquals('Fixed permissions!!!', $commit->getShortMessage(10, true, '!!!'));
// tests with a single-line message
$commit = $repository->getCommit(self::INITIAL_COMMIT);
$this->assertEquals('Add README', $commit->getShortMessage(20));
$this->assertEquals('A', $commit->getShortMessage(1, false, ''));
$this->assertEquals('Add!!!', $commit->getShortMessage(1, true, '!!!'));
}
/**
* @dataProvider provideFoobar
*/
public function testGetBodyMessage($repository)
{
$commit = $repository->getCommit(self::LONGMESSAGE_COMMIT);
$message = <<<EOL
If you want to know everything,
I ran something like `chmox +x test.sh`
Hello and good bye.
EOL;
$this->assertEquals($message, $commit->getBodyMessage());
$commit = $repository->getCommit(self::INITIAL_COMMIT);
$this->assertEquals('', $commit->getBodyMessage());
}
/**
* @expectedException InvalidArgumentException
* @dataProvider provideFoobar
*/
public function testGetIncludingBranchesException($repository)
{
$commit = $repository->getCommit(self::INITIAL_COMMIT);
$commit->getIncludingBranches(false, false);
}
/**
* @dataProvider provideFoobar
*/
public function testGetIncludingBranches($repository)
{
$commit = $repository->getCommit(self::INITIAL_COMMIT);
$branches = $commit->getIncludingBranches(true, false);
$this->assertCount(count($repository->getReferences()->getLocalBranches()), $branches);
$branches = $commit->getIncludingBranches(true, true);
$this->assertCount(count($repository->getReferences()->getBranches()), $branches);
$branches = $commit->getIncludingBranches(false, true);
$this->assertCount(count($repository->getReferences()->getRemoteBranches()), $branches);
}
/**
* @dataProvider provideFoobar
*/
public function testGetLastModification($repository)
{
$commit = $repository->getCommit(self::LONGFILE_COMMIT);
$lastModification = $commit->getLastModification('image.jpg');
$this->assertTrue($lastModification instanceof Commit, 'Last modification is a Commit object');
$this->assertEquals(self::BEFORE_LONGFILE_COMMIT, $lastModification->getHash(), 'Last modification is current commit');
}
/**
* @dataProvider provideFoobar
*/
public function testMergeCommit($repository)
{
$commit = $repository->getCommit(self::MERGE_COMMIT);
$this->assertEquals("Merge branch 'authors'", $commit->getSubjectMessage());
}
/**
* @dataProvider provideFoobar
*/
public function testEncoding($repository)
{
$commit = $repository->getCommit(self::ENCODING_COMMIT);
$this->assertEquals('contribute to AUTHORS file', $commit->getSubjectMessage());
}
}

View File

@@ -0,0 +1,142 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
class DiffTest extends AbstractTest
{
const DELETE_COMMIT = '519d5693c72c925cd59205d9f11e9fa1d550028b';
const CREATE_COMMIT = 'e6fa3c792facc06faa049a6938c84c411954deb5';
const RENAME_COMMIT = '6640e0ef31518054847a1876328e26ee64083e0a';
const CHANGEMODE_COMMIT = '93da965f58170f13017477b9a608657e87e23230';
/**
* @dataProvider provideFoobar
*/
public function testSerialization($repository)
{
$data = $repository->getCommit(self::CREATE_COMMIT)->getDiff()->toArray();
$diff = Diff::fromArray($data);
$this->verifyCreateCommitDiff($diff);
}
/**
* @dataProvider provideFoobar
*/
public function testGetFiles_Addition($repository)
{
$diff = $repository->getCommit(self::CREATE_COMMIT)->getDiff();
$this->verifyCreateCommitDiff($diff);
}
protected function verifyCreateCommitDiff(Diff $diff)
{
$files = $diff->getFiles();
$this->assertEquals(2, count($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('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(1, $files[0]->getAdditions(), '1 line added');
$this->assertEquals(0, $files[0]->getDeletions(), '0 lines deleted');
}
/**
* @dataProvider provideFoobar
*/
public function testGetFiles_Modification($repository)
{
$files = $repository->getCommit(self::BEFORE_LONGFILE_COMMIT)->getDiff()->getFiles();
$this->assertEquals(1, count($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->assertTrue($files[0]->isBinary(), 'binary file');
$this->assertEquals(0, $files[0]->getAdditions(), '0 lines added');
$this->assertEquals(0, $files[0]->getDeletions(), '0 lines deleted');
}
/**
* @dataProvider provideFoobar
*/
public function testGetFiles_Deletion($repository)
{
$files = $repository->getCommit(self::DELETE_COMMIT)->getDiff()->getFiles();
$this->assertEquals(1, count($files), '1 files modified');
$this->assertTrue($files[0]->isDeletion(), 'File deletion');
$this->assertEquals('script_B.php', $files[0]->getOldName(), 'verify old filename');
$this->assertEquals(1, $files[0]->getDeletions(), '1 line deleted');
}
/**
* @dataProvider provideFoobar
*/
public function testGetFiles_Rename($repository)
{
$files = $repository->getCommit(self::RENAME_COMMIT)->getDiff()->getFiles();
$this->assertEquals(1, count($files), '1 files modified');
$this->assertTrue($files[0]->isModification());
$this->assertTrue($files[0]->isRename());
$this->assertFalse($files[0]->isDeletion());
$this->assertFalse($files[0]->isCreation());
$this->assertFalse($files[0]->isChangeMode());
}
/**
* @dataProvider provideFoobar
*/
public function testGetFiles_Changemode($repository)
{
$files = $repository->getCommit(self::CHANGEMODE_COMMIT)->getDiff()->getFiles();
$this->assertEquals(1, count($files), '1 files modified');
$this->assertTrue($files[0]->isModification());
$this->assertTrue($files[0]->isChangeMode());
$this->assertFalse($files[0]->isDeletion());
$this->assertFalse($files[0]->isCreation());
$this->assertFalse($files[0]->isRename());
}
/**
* @dataProvider provideFoobar
*/
public function testDiffRangeParse($repository)
{
$files = $repository->getCommit(self::CREATE_COMMIT)->getDiff()->getFiles();
$changes = $files[0]->getChanges();
$this->assertEquals(0, $changes[0]->getRangeOldStart());
$this->assertEquals(0, $changes[0]->getRangeOldCount());
$this->assertEquals(1, $changes[0]->getRangeNewStart());
$this->assertEquals(0, $changes[0]->getRangeNewCount());
}
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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 HooksTest extends AbstractTest
{
private static $symlinkOnWindows = null;
public static function setUpBeforeClass()
{
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
self::$symlinkOnWindows = true;
$originDir = tempnam(sys_get_temp_dir(), 'sl');
$targetDir = tempnam(sys_get_temp_dir(), 'sl');
if (true !== @symlink($originDir, $targetDir)) {
$report = error_get_last();
if (is_array($report) && false !== strpos($report['message'], 'error code(1314)')) {
self::$symlinkOnWindows = false;
}
}
}
}
public function hookPath($repository, $hook)
{
return $repository->getGitDir().'/hooks/'.$hook;
}
public function touchHook($repository, $hook, $content = '')
{
$path = $this->hookPath($repository, $hook);
file_put_contents($path, $content);
return $path;
}
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");
}
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");
}
/**
* @dataProvider provideFoobar
*/
public function testHas($repository)
{
$this->assertNoHook($repository, 'foo');
$this->touchHook($repository, 'foo');
$this->assertHasHook($repository, 'foo');
}
/**
* @dataProvider provideFoobar
* @expectedException InvalidArgumentException
*/
public function testGet_InvalidName_ThrowsException($repository)
{
$repository->getHooks()->get('foo');
}
/**
* @dataProvider provideFoobar
*/
public function testGet($repository)
{
$this->touchHook($repository, 'foo', 'foobar');
$this->assertEquals('foobar', $repository->getHooks()->get('foo'));
}
/**
* @dataProvider provideFoobar
*/
public function testSymlink($repository)
{
$this->markAsSkippedIfSymlinkIsMissing();
$file = $this->touchHook($repository, 'bar', 'barbarbar');
$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');
}
/**
* @dataProvider provideFoobar
* @expectedException LogicException
*/
public function testSymlink_WithExisting_ThrowsLogicException($repository)
{
$this->markAsSkippedIfSymlinkIsMissing();
$file = $this->hookPath($repository, 'target-symlink');
$fooFile = $this->hookPath($repository, 'foo');
file_put_contents($file, 'foobar');
touch($fooFile);
$repository->getHooks()->setSymlink('foo', $file);
}
/**
* @dataProvider provideFoobar
*/
public function testSet($repository)
{
$file = $this->hookPath($repository, 'foo');
$repository->getHooks()->set('foo', 'bar');
$this->assertEquals('bar', file_get_contents($file), 'Hook content is correct');
$perms = fileperms($file);
$this->assertEquals(defined('PHP_WINDOWS_VERSION_BUILD') ? 0666 : 0777, $perms & 0777, 'Hook permissions are correct');
}
/**
* @dataProvider provideFoobar
*/
public function testSet_Existing_ThrowsLogicException($repository)
{
$repository->getHooks()->set('foo', 'bar');
$this->setExpectedException('LogicException');
$repository->getHooks()->set('foo', 'bar');
}
/**
* @dataProvider provideFoobar
*/
public function testRemove($repository)
{
$file = $this->hookPath($repository, 'foo');
touch($file);
$repository->getHooks()->remove('foo');
$this->assertFalse(file_exists($file));
}
/**
* @dataProvider provideFoobar
* @expectedException LogicException
*/
public function testRemove_NotExisting_ThrowsLogicException($repository)
{
$repository->getHooks()->remove('foo');
}
private function markAsSkippedIfSymlinkIsMissing()
{
if (!function_exists('symlink')) {
$this->markTestSkipped('symlink is not supported');
}
if (defined('PHP_WINDOWS_VERSION_MAJOR') && false === self::$symlinkOnWindows) {
$this->markTestSkipped('symlink requires "Create symbolic links" privilege on windows');
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
class LogTest extends AbstractTest
{
/**
* @dataProvider provideFoobar
*/
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));
}
/**
* @dataProvider provideFoobar
*/
public function testGetCommits($repository)
{
$log = $repository->getLog(self::LONGFILE_COMMIT, null, null, 3);
$commits = $log->getCommits();
$this->assertEquals(3, count($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");
}
/**
* @dataProvider provideFoobar
*/
public function testCountCommits($repository)
{
$log = $repository->getLog(self::LONGFILE_COMMIT, null, 2, 3);
$this->assertEquals(8, $log->countCommits(), '8 commits found in history');
}
/**
* @dataProvider provideFoobar
*/
public function testCountAllCommits($repository)
{
$log = $log = $repository->getLog();
$this->assertGreaterThan(100, $log->countCommits(), 'Returns all commits from all branches');
}
/**
* @dataProvider provideFoobar
*/
public function testIterable($repository)
{
$log = $repository->getLog(self::LONGFILE_COMMIT);
$expectedHashes = array(self::LONGFILE_COMMIT, self::BEFORE_LONGFILE_COMMIT);
foreach ($log as $entry) {
$hash = array_shift($expectedHashes);
$this->assertEquals($hash, $entry->getHash());
if (count($expectedHashes) == 0) {
break;
}
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
class PushReferenceTest extends AbstractTest
{
const CREATE = 1;
const DELETE = 2;
const FORCE = 4;
const FAST_FORWARD = 8;
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),
);
}
/**
* @dataProvider 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.');
}
/**
* @dataProvider provideFoobar
*/
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->assertEquals('add a long file', $log[0]->getShortMessage(), 'First commit is correct');
}
/**
* This test ensures that GPG signed requests does not break the reading of commit logs.
*
* @dataProvider provideFoobar
*/
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->assertEquals('signed commit', $log[0]->getShortMessage(), 'Last commit is correct');
}
/**
* @dataProvider provideFoobar
*/
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');
$this->assertEquals('add a long file', $log[0]->getShortMessage(), 'First commit is correct');
}
}

View File

@@ -0,0 +1,183 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\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());
}
/**
* @dataProvider provideFoobar
*/
public function testGetBranch($repository)
{
$branch = $repository->getReferences()->getBranch('master');
$this->assertTrue($branch instanceof Branch, 'Branch object is correct type');
$this->assertEquals($branch->getCommitHash(), $branch->getCommit()->getHash(), 'Hash is correctly resolved');
}
/**
* @dataProvider provideFoobar
*/
public function testHasBranch($repository)
{
$this->assertTrue($repository->getReferences()->hasBranch('master'), 'Branch master exists');
$this->assertFalse($repository->getReferences()->hasBranch('foobar'), 'Branch foobar does not exists');
}
/**
* @dataProvider provideFoobar
*/
public function testHasTag($repository)
{
$this->assertTrue($repository->getReferences()->hasTag('0.1'), 'Tag 0.1 exists');
$this->assertFalse($repository->getReferences()->hasTag('foobar'), 'Tag foobar does not exists');
}
/**
* @dataProvider provideFoobar
* @expectedException Gitonomy\Git\Exception\ReferenceNotFoundException
*/
public function testGetBranch_NotExisting_Error($repository)
{
$branch = $repository->getReferences()->getBranch('notexisting');
}
/**
* @dataProvider provideFoobar
*/
public function testGetTag($repository)
{
$tag = $repository->getReferences()->getTag('0.1');
$this->assertTrue($tag instanceof Tag, 'Tag object is correct type');
$this->assertEquals(self::LONGFILE_COMMIT, $tag->getCommitHash(), 'Commit hash is correct');
$this->assertEquals(self::LONGFILE_COMMIT, $tag->getCommit()->getHash(), 'Commit hash is correct');
}
/**
* @dataProvider provideFoobar
* @expectedException Gitonomy\Git\Exception\ReferenceNotFoundException
*/
public function testGetTag_NotExisting_Error($repository)
{
$branch = $repository->getReferences()->getTag('notexisting');
}
/**
* @dataProvider provideFoobar
*/
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');
}
/**
* @dataProvider provideFoobar
*/
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');
}
/**
* @dataProvider provideFoobar
*/
public function testResolveBranches($repository)
{
$master = $repository->getReferences()->getBranch('master');
$resolved = $repository->getReferences()->resolveBranches($master->getCommitHash());
if ($repository->isBare()) {
$this->assertEquals(1, count($resolved), '1 revision resolved');
} else {
$this->assertEquals(2, count($resolved), '2 revision resolved');
}
$this->assertTrue(reset($resolved) instanceof Branch, 'Resolved object is a branch');
}
/**
* @dataProvider provideFoobar
*/
public function testCountable($repository)
{
$this->assertGreaterThanOrEqual(2, count($repository->getReferences()), 'At least two references in repository');
}
/**
* @dataProvider provideFoobar
*/
public function testIterable($repository)
{
$i = 0;
foreach ($repository->getReferences() as $ref) {
++$i;
}
$this->assertGreaterThanOrEqual(2, $i, 'At least two references in repository');
}
/**
* @dataProvider provideFoobar
*/
public function testCreateAndDeleteTag($repository)
{
$references = $repository->getReferences();
$tag = $references->createTag('0.0', self::INITIAL_COMMIT);
$this->assertTrue($references->hasTag('0.0'), 'Tag 0.0 created');
$this->assertEquals(self::INITIAL_COMMIT, $tag->getCommit()->getHash());
$this->assertSame($tag, $references->getTag('0.0'));
$tag->delete();
$this->assertFalse($references->hasTag('0.0'), 'Tag 0.0 removed');
}
/**
* @dataProvider provideFoobar
*/
public function testCreateAndDeleteBranch($repository)
{
$references = $repository->getReferences();
$branch = $references->createBranch('foobar', self::INITIAL_COMMIT);
$this->assertTrue($references->hasBranch('foobar'), 'Branch foobar created');
$this->assertEquals(self::INITIAL_COMMIT, $branch->getCommit()->getHash());
$this->assertSame($branch, $references->getBranch('foobar'));
$branch->delete();
$this->assertFalse($references->hasBranch('foobar'), 'Branch foobar removed');
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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 Prophecy\Argument;
class RepositoryTest extends AbstractTest
{
/**
* @dataProvider provideFoobar
*/
public function testGetBlob_WithExisting_Works($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');
}
/**
* @dataProvider provideFoobar
*/
public function testGetSize($repository)
{
$size = $repository->getSize();
$this->assertGreaterThan(70, $size, 'Repository is greater than 70KB');
}
public function testIsBare()
{
$bare = self::createFoobarRepository(true);
$this->assertTrue($bare->isBare(), 'Lib repository is bare');
$notBare = self::createFoobarRepository(false);
$this->assertFalse($notBare->isBare(), 'Working copy is not bare');
}
/**
* @dataProvider provideFoobar
*/
public function testGetDescription($repository)
{
$this->assertSame("Unnamed repository; edit this file 'description' to name the repository.\n", $repository->getDescription());
}
/**
* @dataProvider provideFoobar
*/
public function testLoggerOk($repository)
{
if (!interface_exists('Psr\Log\LoggerInterface')) {
$this->markTestSkipped();
}
$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($loggerProphecy->reveal());
$repository->run('remote');
}
/**
* @dataProvider provideFoobar
* @expectedException RuntimeException
*/
public function testLoggerNOk($repository)
{
if (!interface_exists('Psr\Log\LoggerInterface')) {
$this->markTestSkipped();
}
$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');
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\Log;
use Gitonomy\Git\Revision;
class RevisionTest extends AbstractTest
{
/**
* @dataProvider provideFoobar
*/
public function testGetCommit($repository)
{
$revision = $repository->getRevision(self::LONGFILE_COMMIT.'^');
$this->assertTrue($revision instanceof Revision, 'Revision object type');
$commit = $revision->getCommit();
$this->assertTrue($commit instanceof 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();
}
/**
* @dataProvider provideFoobar
*/
public function testGetLog($repository)
{
$revision = $repository->getRevision(self::LONGFILE_COMMIT);
$log = $revision->getLog(null, 2, 3);
$this->assertTrue($log instanceof 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');
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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;
class TreeTest extends AbstractTest
{
const PATH_RESOLVING_COMMIT = 'cc06ac171d884282202dff88c1ded499a1f89420';
/**
* @dataProvider provideFooBar
*/
public function testCase($repository)
{
$tree = $repository->getCommit(self::LONGFILE_COMMIT)->getTree();
$entries = $tree->getEntries();
$this->assertTrue(isset($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->assertTrue($entries['README.md'][1] instanceof Blob, 'README.md is a Blob');
}
/**
* @dataProvider provideFooBar
*/
public function testResolvePath($repository)
{
$tree = $repository->getCommit(self::PATH_RESOLVING_COMMIT)->getTree();
$path = 'test/a/b/c';
$resolved = $tree->resolvePath($path);
$entries = $resolved->getEntries();
$this->assertTrue(isset($entries['d']), 'Successfully resolved source folder');
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* This file is part of Gitonomy.
*
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
* (c) Julien DIDIER <genzo.wm@gmail.com>
*
* 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\Reference\Branch;
class WorkingCopyTest extends AbstractTest
{
/**
* @expectedException LogicException
*/
public function testNoWorkingCopyInBare()
{
$path = self::createTempDir();
$repo = Admin::init($path, true, self::getOptions());
$repo->getWorkingCopy();
}
public function testCheckout()
{
$repository = self::createFoobarRepository(false);
$wc = $repository->getWorkingCopy();
$wc->checkout('origin/new-feature', 'new-feature');
$head = $repository->getHead();
$this->assertTrue($head instanceof Branch, 'HEAD is a branch');
$this->assertEquals('new-feature', $head->getName(), 'HEAD is branch new-feature');
}
public function testDiffStaged()
{
$repository = self::createFoobarRepository(false);
$wc = $repository->getWorkingCopy();
$diffStaged = $wc->getDiffStaged();
$this->assertCount(0, $diffStaged->getFiles());
$file = $repository->getWorkingDir().'/foobar-test';
file_put_contents($file, 'test');
$repository->run('add', array($file));
$diffStaged = $wc->getDiffStaged();
$this->assertCount(1, $diffStaged->getFiles());
}
public function testDiffPending()
{
$repository = self::createFoobarRepository(false);
$wc = $repository->getWorkingCopy();
$diffPending = $wc->getDiffPending();
$this->assertCount(0, $diffPending->getFiles());
$file = $repository->getWorkingDir().'/test.sh';
file_put_contents($file, 'test');
$diffPending = $wc->getDiffPending();
$this->assertCount(1, $diffPending->getFiles());
}
/**
* @expectedException RuntimeException
*/
public function testCheckoutUnexisting()
{
self::createFoobarRepository(false)->getWorkingCopy()->checkout('foobar');
}
public function testAttachedHead()
{
$repository = self::createFoobarRepository(false);
$wc = $repository->getWorkingCopy();
$wc->checkout('master');
$head = $repository->getHead();
$this->assertTrue($repository->isHeadAttached(), 'HEAD is attached');
$this->assertFalse($repository->isHeadDetached(), 'HEAD is not detached');
}
public function testDetachedHead()
{
$repository = self::createFoobarRepository(false);
$wc = $repository->getWorkingCopy();
$wc->checkout('0.1');
$head = $repository->getHead();
$this->assertFalse($repository->isHeadAttached(), 'HEAD is not attached');
$this->assertTrue($repository->isHeadDetached(), 'HEAD is detached');
}
public function testGetUntracked()
{
$repository = self::createFoobarRepository(false);
$wc = $repository->getWorkingCopy();
$file = $repository->getWorkingDir().'/untracked.txt';
file_put_contents($file, 'foo');
$this->assertContains('untracked.txt', $wc->getUntrackedFiles());
}
}

View File

@@ -0,0 +1,14 @@
<?php
require __DIR__.'/../vendor/autoload.php';
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
$server = array_change_key_case($_SERVER, true);
$_SERVER['GIT_ENVS'] = array();
foreach (array('PATH', 'SYSTEMROOT') as $key) {
if (isset($server[$key])) {
$_SERVER['GIT_ENVS'][$key] = $server[$key];
}
}
unset($server);
}