641 lines
17 KiB
PHP
641 lines
17 KiB
PHP
<?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;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $inheritEnvironmentVariables;
|
|
|
|
/**
|
|
* 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 = [])
|
|
{
|
|
$options = array_merge([
|
|
'working_dir' => null,
|
|
'debug' => true,
|
|
'logger' => null,
|
|
'command' => 'git',
|
|
'environment_variables' => [],
|
|
'inherit_environment_variables' => false,
|
|
'process_timeout' => 3600,
|
|
], $options);
|
|
|
|
if (null !== $options['logger'] && !$options['logger'] instanceof LoggerInterface) {
|
|
throw new InvalidArgumentException(sprintf('Argument "logger" passed to Repository should be a Psr\Log\LoggerInterface. A %s was provided', is_object($options['logger']) ? get_class($options['logger']) : gettype($options['logger'])));
|
|
}
|
|
|
|
$this->logger = $options['logger'];
|
|
$this->initDir($dir, $options['working_dir']);
|
|
|
|
$this->objects = [];
|
|
$this->command = $options['command'];
|
|
$this->debug = (bool) $options['debug'];
|
|
$this->processTimeout = $options['process_timeout'];
|
|
|
|
if (defined('PHP_WINDOWS_VERSION_BUILD') && isset($_SERVER['PATH']) && !isset($options['environment_variables']['PATH'])) {
|
|
$options['environment_variables']['PATH'] = $_SERVER['PATH'];
|
|
}
|
|
|
|
$this->environmentVariables = $options['environment_variables'];
|
|
$this->inheritEnvironmentVariables = $options['inherit_environment_variables'];
|
|
|
|
if (true === $this->debug && null !== $this->logger) {
|
|
$this->logger->debug(sprintf('Repository created (git dir: "%s", working dir: "%s")', $this->gitDir, $this->workingDir ?: 'none'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
} elseif (!is_dir($realGitDir)) {
|
|
throw new InvalidArgumentException(sprintf('Directory "%s" does not exist or is not a directory', $realGitDir));
|
|
} elseif (null === $workingDir && is_dir($realGitDir.'/.git')) {
|
|
$workingDir = $realGitDir;
|
|
$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.
|
|
*
|
|
* @param bool $reload Reload references from the filesystem
|
|
*
|
|
* @return ReferenceBag
|
|
*/
|
|
public function getReferences($reload = false)
|
|
{
|
|
if (null === $this->referenceBag || $reload) {
|
|
$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];
|
|
}
|
|
|
|
/**
|
|
* @return Blame
|
|
*/
|
|
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(['-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
|
|
*/
|
|
public function getSize()
|
|
{
|
|
$totalBytes = 0;
|
|
$path = realpath($this->gitDir);
|
|
if ($path && file_exists($path)) {
|
|
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)) as $object) {
|
|
$totalBytes += $object->getSize();
|
|
}
|
|
}
|
|
|
|
return (int) ($totalBytes / 1000 + 0.5);
|
|
}
|
|
|
|
/**
|
|
* Executes a shell command on the repository, using PHP pipes.
|
|
*
|
|
* @param string $command The command to execute
|
|
*/
|
|
public function shell($command, array $env = [])
|
|
{
|
|
$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), [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
|
|
*
|
|
* @throws RuntimeException Error while executing git command (debug-mode only)
|
|
*
|
|
* @return string Output of a successful process or null if execution failed and debug-mode is disabled.
|
|
*/
|
|
public function run($command, $args = [])
|
|
{
|
|
$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 = [])
|
|
{
|
|
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 = [])
|
|
{
|
|
$base = [$this->command, '--git-dir', $this->gitDir];
|
|
|
|
if ($this->workingDir) {
|
|
$base = array_merge($base, ['--work-tree', $this->workingDir]);
|
|
}
|
|
|
|
$base[] = $command;
|
|
|
|
$process = new Process(array_merge($base, $args));
|
|
|
|
if ($this->inheritEnvironmentVariables) {
|
|
$process->setEnv(array_replace($_SERVER, $this->environmentVariables));
|
|
} else {
|
|
$process->setEnv($this->environmentVariables);
|
|
}
|
|
|
|
$process->setTimeout($this->processTimeout);
|
|
$process->setIdleTimeout($this->processTimeout);
|
|
|
|
return $process;
|
|
}
|
|
}
|