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

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);
}
}