Skip to content

Commit

Permalink
Add basic implementation for reading index files
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianclay committed Oct 13, 2023
1 parent 4d449c2 commit 68ffa95
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 0 deletions.
147 changes: 147 additions & 0 deletions src/IndexFile.php
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;
}
}
104 changes: 104 additions & 0 deletions tests/IndexFileTest.php
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 added tests/fixtures/index-files/one-entry
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added tests/fixtures/index-files/zero-entries
Binary file not shown.

0 comments on commit 68ffa95

Please sign in to comment.