-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic implementation for reading index files
- Loading branch information
1 parent
4d449c2
commit 68ffa95
Showing
6 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
<?php | ||
|
||
namespace adrianclay\git; | ||
|
||
/** | ||
* @see https://git-scm.com/docs/index-format | ||
* @todo Support more functionality + extensions | ||
*/ | ||
class IndexFile implements \Countable, \Iterator | ||
{ | ||
/** | ||
* @var false|resource | ||
*/ | ||
private $indexFile; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
private $iteratorIndex; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
private $count; | ||
|
||
/** | ||
* @var IndexEntry | ||
*/ | ||
private $current; | ||
|
||
public function __construct( string $string ) | ||
{ | ||
$this->indexFile = \fopen($string, 'r'); | ||
$this->rewind(); | ||
} | ||
|
||
private function _count(): void | ||
{ | ||
\fseek($this->indexFile, 0); | ||
$headerString = \fread($this->indexFile, 12); | ||
$header = \unpack(\implode('/', ['Nversion', 'NindexEntries']), $headerString, 4); | ||
$this->count = $header['indexEntries']; | ||
} | ||
|
||
public function count(): int | ||
{ | ||
return $this->count; | ||
} | ||
|
||
private function readIndexEntry(): IndexEntry | ||
{ | ||
// 32-bit ctime seconds, the last time a file's metadata changed | ||
// 32-bit ctime nanosecond fractions | ||
// 32-bit mtime seconds, the last time a file's data changed | ||
// 32-bit mtime nanosecond fractions | ||
// 32-bit dev | ||
// 32-bit ino | ||
// 32-bit mode, split into (high to low bits) | ||
// 4-bit object type | ||
// 3-bit unused | ||
// 9-bit unix permission. Only 0755 and 0644 are valid for regular files. | ||
// 32-bit uid | ||
// 32-bit gid | ||
// 32-bit file size | ||
// Object name for the represented object (20 bytes) | ||
// A 16-bit 'flags' field split into (high to low bits) | ||
// | ||
\fseek($this->indexFile, 40, SEEK_CUR); | ||
$objectName = fread($this->indexFile, GitObject::SHA_BIN_SIZE); | ||
\fseek($this->indexFile, 2, SEEK_CUR); | ||
$name = $this->read_null_terminated_name_string(); | ||
return new IndexEntry($name, bin2hex($objectName)); | ||
} | ||
|
||
private function read_null_terminated_name_string(): string | ||
{ | ||
$name = ''; | ||
while(($char = fgetc($this->indexFile)) !== false) { | ||
if(!ord($char)) { | ||
break; | ||
} | ||
$name .= $char; | ||
} | ||
$padding = 8 - (6 + strlen($name) + 1) % 8; | ||
if($padding == 8) { | ||
$padding = 0; | ||
} | ||
\fseek($this->indexFile, $padding, SEEK_CUR); | ||
return $name; | ||
} | ||
|
||
public function next(): void | ||
{ | ||
$this->iteratorIndex++; | ||
$this->current = $this->readIndexEntry(); | ||
} | ||
|
||
public function key(): int | ||
{ | ||
return $this->iteratorIndex; | ||
} | ||
|
||
public function valid(): bool | ||
{ | ||
return $this->iteratorIndex < $this->count(); | ||
} | ||
|
||
public function rewind(): void | ||
{ | ||
$this->iteratorIndex = 0; | ||
$this->_count(); | ||
$this->current = $this->readIndexEntry(); | ||
} | ||
|
||
public function current(): IndexEntry | ||
{ | ||
return $this->current; | ||
} | ||
} | ||
|
||
class IndexEntry implements SHAReference { | ||
/** | ||
* @var string | ||
*/ | ||
private $name; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $sha; | ||
|
||
public function __construct(string $name, string $sha) | ||
{ | ||
$this->name = $name; | ||
$this->sha = $sha; | ||
} | ||
|
||
public function name(): string | ||
{ | ||
return $this->name; | ||
} | ||
|
||
public function getSHA(): string | ||
{ | ||
return $this->sha; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
<?php | ||
namespace adrianclay\git; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
|
||
class IndexFileTest extends TestCase | ||
{ | ||
public function testCountForZeroEntries() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/zero-entries"); | ||
$this->assertCount(0, $indexFile ); | ||
} | ||
|
||
public function testCountForOneEntry() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/one-entry"); | ||
$this->assertCount(1, $indexFile ); | ||
} | ||
|
||
public function testCountCanBeCalledTwiceWithSameResult() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/one-entry"); | ||
$this->assertCount(1, $indexFile ); | ||
$this->assertCount(1, $indexFile ); | ||
} | ||
|
||
public function testNameForOneEntry() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/one-entry"); | ||
$this->assertEquals('a', $indexFile->current()->name()); | ||
} | ||
|
||
public function testValidIsFalseForZeroEntries() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/zero-entries"); | ||
$this->assertFalse($indexFile->valid()); | ||
} | ||
|
||
public function testValidBecomesFalseAfterOneCallToNextForOneEntry() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/one-entry"); | ||
$this->assertTrue($indexFile->valid()); | ||
$indexFile->next(); | ||
$this->assertFalse($indexFile->valid()); | ||
} | ||
|
||
public function testGetShaForOneEntry() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/one-entry"); | ||
$this->assertEquals('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', $indexFile->current()->getSHA()); | ||
} | ||
|
||
public function testShaForSecondEntry() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/two-entries-with-names-of-length-two"); | ||
$indexFile->current(); | ||
$indexFile->next(); | ||
$this->assertEquals('3b18e512dba79e4c8300dd08aeb37f8e728b8dad', $indexFile->current()->getSHA()); | ||
} | ||
|
||
public function testTwoNamesEachOfLengthTwo() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/two-entries-with-names-of-length-two"); | ||
$this->assertEquals('00', $indexFile->current()->name()); | ||
$indexFile->next(); | ||
$this->assertEquals('ab', $indexFile->current()->name()); | ||
$indexFile->next(); | ||
$this->assertFalse($indexFile->valid()); | ||
} | ||
|
||
public function testCallingNextAdvancesToSecondEntry() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/two-entries-with-names-of-length-two"); | ||
$indexFile->next(); | ||
$this->assertEquals('ab', $indexFile->current()->name()); | ||
} | ||
|
||
public function testFetchesNameContainingAZero() | ||
{ | ||
// Need to be careful to handle '0' and '\0' characters differently | ||
// within the name field. | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/two-entries-with-names-of-length-two"); | ||
$this->assertEquals('00', $indexFile->current()->name()); | ||
} | ||
|
||
public function testIterationIsDeterministic() | ||
{ | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/two-entries-with-names-of-length-two"); | ||
$firstIteration = iterator_to_array($indexFile, false); | ||
$secondIteration = iterator_to_array($indexFile, false); | ||
$this->assertEquals($firstIteration, $secondIteration); | ||
} | ||
|
||
public function testNameHandlesZeroLengthNamePadding() | ||
{ | ||
// Entries have padding added depending on their name length. | ||
// Checks that the padding calculation works correctly in the case that there is zero padding. | ||
$indexFile = new IndexFile(__DIR__ . "/fixtures/index-files/two-entries-with-zero-length-name-padding"); | ||
$indexFile->next(); | ||
// Padding calculation only observable when looking at the second value retrieved. | ||
$this->assertEquals("1", $indexFile->current()->name()); | ||
$this->assertEquals("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", $indexFile->current()->getSHA()); | ||
} | ||
} |
Binary file not shown.
Binary file not shown.
Binary file added
BIN
+160 Bytes
tests/fixtures/index-files/two-entries-with-zero-length-name-padding
Binary file not shown.
Binary file not shown.