Skip to content

Commit b8a745e

Browse files
committed
Open repositories the same way with both drivers, and fix what the docs revealed
Both drivers find the git directory and the working tree with the same code: a directory that isn't a repository throws an InvalidArgumentException right away, where CliDriver waited for the first operation, and a SHA-256 repository an UnsupportedOperationException, where LibGit2Driver threw a DriverException. Writing and running every example of the docs found: - Repository::getMergeBase(), the commit "git diff a...b" compares with, which the deprecated getDiff('a...b') used without exposing it; - an abbreviated hash given to getTree() or getBlob() resolved to an object of any type, and "^{tree}" peeled a commit to its tree; - ReferenceBag::resolve() only took a full hash, not a revision name; - Tree::resolvePath() had no message for a missing path; - LibGit2Driver counted one line too many in the blame of a file not ending with a line break; - Hooks wrote the hooks of a linked worktree in its own git directory, where git never runs them; - the timeout message rounded 0.5 seconds down to 0.
1 parent e507dbb commit b8a745e

17 files changed

Lines changed: 251 additions & 85 deletions

‎src/Gitonomy/Git/Commit.php‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ public function getShortHash(): string
8686

8787
/**
8888
* Returns a fixed-with short hash.
89-
*/
90-
/**
89+
*
9190
* @deprecated since gitonomy/gitlib 2.0, use substr($commit->getHash(), 0, $length) instead
9291
*/
9392
public function getFixedShortHash(int $length = 6): string
@@ -148,8 +147,7 @@ public function getLastModification(?string $path = null): self
148147
* Returns the first line of the commit, and the first 50 characters.
149148
*
150149
* Ported from https://github.com/fabpot/Twig-extensions/blob/d67bc7e69788795d7905b52d31188bbc1d390e01/lib/Twig/Extensions/Extension/Text.php#L52-L109
151-
*/
152-
/**
150+
*
153151
* @deprecated since gitonomy/gitlib 2.0, use symfony/string instead: u($commit->getSubjectMessage())->truncate($length, $separator, !$preserve)
154152
*/
155153
public function getShortMessage(int $length = 50, bool $preserve = false, string $separator = '...'): string

‎src/Gitonomy/Git/Diff/File.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ final class File
3333
* @param string|null $oldName Null for a creation
3434
* @param string|null $newName Null for a deletion
3535
* @param string|null $oldIndex The hash of the old blob, null when there is none (a creation...)
36-
* @param string|null $newIndex The hash of the new blob, null when there is none (a deletion, a file of the working tree...)
36+
* @param string|null $newIndex The hash of the new blob, null when there is none (a deletion, a modified submodule...); the hash of a file of the working tree isn't in the repository
3737
* @param FileChange[] $changes
3838
* @param Repository|null $repository The repository to read the blobs from
3939
*

‎src/Gitonomy/Git/Driver/Cli/CliDriver.php‎

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313
namespace Gitonomy\Git\Driver\Cli;
1414

1515
use Gitonomy\Git\Driver\DriverInterface;
16-
use Gitonomy\Git\Exception\DriverException;
17-
use Gitonomy\Git\Exception\InvalidArgumentException;
16+
use Gitonomy\Git\Driver\RepositoryPaths;
1817
use Gitonomy\Git\Exception\ProcessException;
19-
use Gitonomy\Git\Exception\UnsupportedOperationException;
2018
use Psr\Log\LoggerInterface;
2119

2220
/**
@@ -44,25 +42,9 @@ public function __construct(
4442

4543
public function open(string $dir, ?string $workingDir): CliRepositoryDriver
4644
{
47-
$realDir = realpath($dir);
48-
if (false === $realDir || !is_dir($realDir)) {
49-
throw new InvalidArgumentException(\sprintf('Directory "%s" does not exist or is not a directory.', $dir));
50-
}
51-
52-
[$gitDir, $workingDir] = match (true) {
53-
null !== $workingDir => [$realDir, $workingDir],
54-
is_dir($realDir.'/.git') => [$realDir.'/.git', $realDir],
55-
// In a linked worktree or a submodule, ".git" is a file pointing to the git directory.
56-
is_file($realDir.'/.git') => [self::readGitFile($realDir), $realDir],
57-
default => [$realDir, null],
58-
};
59-
60-
$config = is_file($gitDir.'/config') ? file_get_contents($gitDir.'/config') : false;
61-
if (false !== $config && preg_match('/^\s*objectformat\s*=\s*sha256\s*$/mi', $config)) {
62-
throw new UnsupportedOperationException(\sprintf('The repository "%s" uses SHA-256 hashes, which gitlib doesn\'t support yet.', $dir));
63-
}
45+
$paths = RepositoryPaths::resolve($dir, $workingDir);
6446

65-
return new CliRepositoryDriver($this->runner(), $gitDir, $workingDir);
47+
return new CliRepositoryDriver($this->runner(), $paths->gitDir, $paths->workingDir);
6648
}
6749

6850
public function init(string $path): void
@@ -147,19 +129,4 @@ private function mustRun(string $command, array $args): void
147129
throw new ProcessException($process);
148130
}
149131
}
150-
151-
/**
152-
* @return string The git directory the ".git" file of $dir points to
153-
*/
154-
private static function readGitFile(string $dir): string
155-
{
156-
$content = file_get_contents($dir.'/.git');
157-
if (false === $content || !preg_match('/^gitdir: (.+)$/m', $content, $match)) {
158-
throw new DriverException(\sprintf('The "%s/.git" file doesn\'t point to a git directory.', $dir));
159-
}
160-
161-
$gitDir = str_starts_with($match[1], '/') || preg_match('/^[a-zA-Z]:[\\\\\/]/', $match[1]) ? $match[1] : $dir.'/'.$match[1];
162-
163-
return realpath($gitDir) ?: throw new DriverException(\sprintf('The git directory "%s" of "%s" does not exist.', $match[1], $dir));
164-
}
165132
}

‎src/Gitonomy/Git/Driver/Cli/GitRunner.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public function run(string $command, array $args, array $globalOptions = [], ?st
6666
try {
6767
$process->run();
6868
} catch (SymfonyProcessTimedOutException $e) {
69-
throw new ProcessTimedOutException(\sprintf('The git command "%s" ran longer than %d seconds.', $command, $this->timeout), 0, $e);
69+
throw new ProcessTimedOutException(\sprintf('The git command "%s" ran longer than %s seconds.', $command, $this->timeout), 0, $e);
7070
} catch (SymfonyRuntimeException $e) {
7171
// A working directory removed since the repository was opened...
7272
throw new DriverException(\sprintf('Unable to run the git command "%s": %s', $command, $e->getMessage()), 0, $e);

‎src/Gitonomy/Git/Driver/DriverInterface.php‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Gitonomy\Git\Driver\Cli\CliDriver;
1616
use Gitonomy\Git\Exception\DriverException;
1717
use Gitonomy\Git\Exception\InvalidArgumentException;
18+
use Gitonomy\Git\Exception\UnsupportedOperationException;
1819

1920
/**
2021
* A configured git backend: opens repositories, and runs the operations that
@@ -33,8 +34,9 @@ interface DriverInterface
3334
*
3435
* @param string|null $workingDir The working tree, when $dir is a git directory kept apart from it
3536
*
36-
* @throws InvalidArgumentException $dir doesn't exist
37-
* @throws DriverException $dir isn't a repository the driver can open
37+
* @throws InvalidArgumentException $dir doesn't exist, or isn't a repository
38+
* @throws DriverException The driver can't open the repository
39+
* @throws UnsupportedOperationException The repository uses SHA-256 hashes
3840
*/
3941
public function open(string $dir, ?string $workingDir): RepositoryDriverInterface;
4042

‎src/Gitonomy/Git/Driver/Ffi/LibGit2Driver.php‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
use FFI\CData;
1616
use Gitonomy\Git\Driver\DriverInterface;
17+
use Gitonomy\Git\Driver\RepositoryPaths;
1718
use Gitonomy\Git\Exception\DriverException;
18-
use Gitonomy\Git\Exception\InvalidArgumentException;
1919
use Gitonomy\Git\Exception\UnsupportedOperationException;
2020

2121
/**
@@ -76,11 +76,9 @@ public static function isSupported(): bool
7676

7777
public function open(string $dir, ?string $workingDir): LibGit2RepositoryDriver
7878
{
79-
if (!is_dir($dir)) {
80-
throw new InvalidArgumentException(\sprintf('Directory "%s" does not exist or is not a directory.', $dir));
81-
}
79+
$paths = RepositoryPaths::resolve($dir, $workingDir);
8280

83-
return new LibGit2RepositoryDriver($this->ffi, $dir, $workingDir);
81+
return new LibGit2RepositoryDriver($this->ffi, $paths->gitDir, $paths->workingDir);
8482
}
8583

8684
public function init(string $path): void

‎src/Gitonomy/Git/Driver/Ffi/LibGit2RepositoryDriver.php‎

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,36 +64,27 @@ final class LibGit2RepositoryDriver implements RepositoryDriverInterface
6464

6565
private readonly CData $repo;
6666

67-
private readonly string $gitDir;
68-
69-
private readonly ?string $workingDir;
70-
7167
/**
72-
* @param string $dir A git directory, or a working tree with its ".git" directory or file
73-
* @param string|null $workingDir The working tree, when $dir is a git directory kept apart from it
68+
* @param string $gitDir The git directory
69+
* @param string|null $workingDir The working tree, null for a bare repository
7470
*/
7571
public function __construct(
7672
private readonly \FFI $ffi,
77-
string $dir,
78-
?string $workingDir,
73+
private readonly string $gitDir,
74+
private readonly ?string $workingDir,
7975
) {
8076
$out = $this->ffi->new('git_repository*');
81-
LibGit2::check($this->ffi, $this->ffi->git_repository_open(\FFI::addr($out), $dir), \sprintf('Unable to open the repository "%s"', $dir));
77+
LibGit2::check($this->ffi, $this->ffi->git_repository_open(\FFI::addr($out), $gitDir), \sprintf('Unable to open the repository "%s"', $gitDir));
8278

83-
try {
84-
if (null !== $workingDir) {
79+
if (null !== $workingDir) {
80+
try {
8581
LibGit2::check($this->ffi, $this->ffi->git_repository_set_workdir($out, $workingDir, 0), 'Unable to set working directory');
86-
}
87-
88-
// libgit2 ends the paths with a slash.
89-
$this->gitDir = realpath($this->ffi->git_repository_path($out)) ?: throw new DriverException(\sprintf('The git directory of "%s" does not exist.', $dir));
90-
$workdir = $this->ffi->git_repository_workdir($out);
91-
$this->workingDir = $workingDir ?? (null === $workdir ? null : rtrim($workdir, '/'));
92-
} catch (DriverException $e) {
93-
// __destruct() isn't called when the constructor throws.
94-
$this->ffi->git_repository_free($out);
82+
} catch (DriverException $e) {
83+
// __destruct() isn't called when the constructor throws.
84+
$this->ffi->git_repository_free($out);
9585

96-
throw $e;
86+
throw $e;
87+
}
9788
}
9889

9990
$this->repo = $out;
@@ -458,6 +449,10 @@ public function getBlameLines(string $hash, string $path, ?int $firstLine, ?int
458449

459450
try {
460451
$count = $this->ffi->git_blame_linecount($blame);
452+
// git_blame_linecount() counts one more line for a file not ending with a newline.
453+
if ($count > 0 && null === $this->ffi->git_blame_line_byindex($blame, $count)) {
454+
--$count;
455+
}
461456
// Like git, which stops a range at the end of the file, but refuses one starting after it.
462457
if ($range && $firstLine > $count) {
463458
throw new DriverException(\sprintf('The file "%s" has only %d lines.', $path, $count));

‎src/Gitonomy/Git/Driver/Ffi/libgit2.h‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,6 @@ int git_oid_is_zero(const git_oid *id);
102102
/* -- repository -- */
103103

104104
int git_repository_open(git_repository **out, const char *path);
105-
const char *git_repository_path(const git_repository *repo);
106-
const char *git_repository_workdir(const git_repository *repo);
107105
int git_repository_set_workdir(git_repository *repo, const char *workdir, int update_gitlink);
108106
void git_repository_free(git_repository *repo);
109107
int git_repository_init(git_repository **out, const char *path, unsigned is_bare);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Gitonomy.
5+
*
6+
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
7+
* (c) Julien DIDIER <genzo.wm@gmail.com>
8+
*
9+
* This source file is subject to the MIT license that is bundled
10+
* with this source code in the file LICENSE.
11+
*/
12+
13+
namespace Gitonomy\Git\Driver;
14+
15+
use Gitonomy\Git\Exception\DriverException;
16+
use Gitonomy\Git\Exception\InvalidArgumentException;
17+
use Gitonomy\Git\Exception\UnsupportedOperationException;
18+
19+
/**
20+
* The git directory and the working tree of a repository, found the same way by every driver.
21+
*
22+
* @internal
23+
*/
24+
final readonly class RepositoryPaths
25+
{
26+
private function __construct(
27+
public string $gitDir,
28+
public ?string $workingDir,
29+
) {
30+
}
31+
32+
/**
33+
* @param string $dir A bare repository, a working tree, or a git directory when $workingDir is given
34+
* @param string|null $workingDir The working tree of the git directory $dir
35+
*
36+
* @throws InvalidArgumentException $dir doesn't exist, or isn't a repository
37+
* @throws DriverException The ".git" file of $dir doesn't point to a git directory
38+
* @throws UnsupportedOperationException The repository uses SHA-256 hashes
39+
*/
40+
public static function resolve(string $dir, ?string $workingDir): self
41+
{
42+
$realDir = realpath($dir);
43+
if (false === $realDir || !is_dir($realDir)) {
44+
throw new InvalidArgumentException(\sprintf('Directory "%s" does not exist or is not a directory.', $dir));
45+
}
46+
47+
$paths = match (true) {
48+
null !== $workingDir => new self($realDir, $workingDir),
49+
is_dir($realDir.'/.git') => new self($realDir.'/.git', $realDir),
50+
// In a linked worktree or a submodule, ".git" is a file pointing to the git directory.
51+
is_file($realDir.'/.git') => new self(self::readGitFile($realDir), $realDir),
52+
default => new self($realDir, null),
53+
};
54+
55+
// Like git, which requires it in any git directory.
56+
if (!is_file($paths->gitDir.'/HEAD')) {
57+
throw new InvalidArgumentException(\sprintf('Directory "%s" is not a git repository.', $dir));
58+
}
59+
60+
$config = is_file($paths->gitDir.'/config') ? file_get_contents($paths->gitDir.'/config') : false;
61+
if (false !== $config && preg_match('/^\s*objectformat\s*=\s*sha256\s*$/mi', $config)) {
62+
throw new UnsupportedOperationException(\sprintf('The repository "%s" uses SHA-256 hashes, which gitlib doesn\'t support yet.', $dir));
63+
}
64+
65+
return $paths;
66+
}
67+
68+
/**
69+
* @return string The git directory the ".git" file of $dir points to
70+
*/
71+
private static function readGitFile(string $dir): string
72+
{
73+
$content = file_get_contents($dir.'/.git');
74+
if (false === $content || !preg_match('/^gitdir: (.+)$/m', $content, $match)) {
75+
throw new DriverException(\sprintf('The "%s/.git" file doesn\'t point to a git directory.', $dir));
76+
}
77+
78+
$gitDir = str_starts_with($match[1], '/') || preg_match('/^[a-zA-Z]:[\\\\\/]/', $match[1]) ? $match[1] : $dir.'/'.$match[1];
79+
80+
return realpath($gitDir) ?: throw new DriverException(\sprintf('The git directory "%s" of "%s" does not exist.', $match[1], $dir));
81+
}
82+
}

‎src/Gitonomy/Git/Hooks.php‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ private function createPath(string $name): string
123123
}
124124

125125
// A repository created with an empty template has none.
126-
$directory = $this->repository->getGitDir().'/hooks';
126+
$directory = $this->getDirectory();
127127
if (!is_dir($directory) && !@mkdir($directory) && !is_dir($directory)) {
128128
throw new RuntimeException(\sprintf('Unable to create the hooks directory "%s".', $directory));
129129
}
@@ -133,6 +133,27 @@ private function createPath(string $name): string
133133

134134
private function getPath(string $name): string
135135
{
136-
return $this->repository->getGitDir().'/hooks/'.$name;
136+
return $this->getDirectory().'/'.$name;
137+
}
138+
139+
/**
140+
* @throws RuntimeException The "commondir" file of a linked worktree can't be read
141+
*/
142+
private function getDirectory(): string
143+
{
144+
$gitDir = $this->repository->getGitDir();
145+
// Like git, a linked worktree runs the hooks of the repository it belongs to.
146+
if (!is_file($gitDir.'/commondir')) {
147+
return $gitDir.'/hooks';
148+
}
149+
150+
$commonDir = file_get_contents($gitDir.'/commondir');
151+
if (false === $commonDir) {
152+
throw new RuntimeException(\sprintf('Unable to read "%s/commondir".', $gitDir));
153+
}
154+
155+
$commonDir = rtrim($commonDir, "\r\n");
156+
157+
return (preg_match('#^(/|[a-zA-Z]:[\\\/])#', $commonDir) ? $commonDir : $gitDir.'/'.$commonDir).'/hooks';
137158
}
138159
}

0 commit comments

Comments
 (0)