RSS Git Download  Clone
Raw Blame History
<?php

declare(strict_types=1);

namespace GitList\SCM\Diff;

class Parse
{
    private const TOKENS = [
        '/^diff --c\s/' => 'start',
        '/^diff --cc\s/' => 'start',
        '/^diff --git\s/' => 'start',
        '/^diff -r\s/' => 'start',
        '/^new file mode \d+$/' => 'newFile',
        '/^deleted file mode \d+$/' => 'deletedFile',
        '/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/' => 'index',
        '/^index\s([\da-zA-Z]+,)([\da-zA-Z]+\.\.[\da-zA-Z]+)(\s(\d+))?$/' => 'mergeIndex',
        '/^---\s/' => 'fromFile',
        '/^\+\+\+\s/' => 'toFile',
        '/^@@\s+(\-(\d+),?(\d+)?\s+)+(\+(\d+),?(\d+)?\s)+@@/' => 'hunk',
        '/^@@@\s+(\-(\d+),?(\d+)?\s+)+(\+(\d+),?(\d+)?\s)+@@@/' => 'hunk',
        '/^-/' => 'deletedLine',
        '/^\+/' => 'addedLine',
    ];

    protected array $files = [];
    protected ?File $currentFile = null;
    protected ?Hunk $currentHunk = null;
    protected int $oldCounter = 0;
    protected int $newCounter = 0;
    protected int $deletedLines = 0;
    protected int $addedLines = 0;

    public function fromRawBlock(string $rawBlock)
    {
        $rawLines = explode(PHP_EOL, $rawBlock);
        $this->files = [];

        foreach ($rawLines as $rawLine) {
            $matched = false;

            foreach (self::TOKENS as $pattern => $action) {
                $matches = [];

                if (preg_match($pattern, $rawLine, $matches)) {
                    $this->$action($rawLine, $matches);
                    $matched = true;

                    break;
                }
            }

            if (!$matched) {
                $this->line($rawLine);
            }
        }

        $this->clearAccumulator();

        return $this->files;
    }

    protected function start(string $line, array $context): void
    {
        $this->clearAccumulator();

        $line = str_replace($context[0], '', $line);
        $files = explode(' ', $line);
        $filename = str_replace('a/', '', $files[0]);

        $this->currentFile = new File($filename);
    }

    protected function newFile(string $line, array $context): void
    {
        $this->currentFile->setType(File::TYPE_NEW);
    }

    protected function deletedFile(string $line, array $context): void
    {
        $this->currentFile->setType(File::TYPE_DELETED);
    }

    protected function index(string $line, array $context): void
    {
        $headerParts = explode(' ', $line);

        $this->currentFile->setIndex($headerParts[1]);
    }

    protected function mergeIndex(string $line, array $context): void
    {
        $this->currentFile->setIndex($context[2]);
    }

    protected function fromFile(string $line, array $context): void
    {
        $this->currentFile->setFrom(trim($line, '- '));
    }

    protected function toFile(string $line, array $context): void
    {
        $this->currentFile->setTo(trim($line, '+ '));
    }

    protected function hunk(string $line, array $context): void
    {
        if ($this->currentHunk) {
            $this->currentFile->addHunk($this->currentHunk);
        }

        $oldStart = (int) ($context[2] ?? 0);
        $oldCount = (int) ($context[3] ?? 0);
        $newStart = (int) ($context[5] ?? 0);
        $newCount = (int) ($context[6] ?? 0);

        $this->oldCounter = $oldStart;
        $this->newCounter = $newStart;
        $this->deletedLines = 0;
        $this->addedLines = 0;
        $this->currentHunk = new Hunk($line, $oldStart, $oldCount, $newStart, $newCount);
    }

    protected function deletedLine(string $line, array $context): void
    {
        $oldNumber = $this->oldCounter + $this->deletedLines;
        $newNumber = $this->newCounter;

        $this->currentHunk->addLine(new Line($line, Line::TYPE_DELETE, $oldNumber, $newNumber));
        $this->currentFile->increaseDeletions();
        $this->deletedLines++;
    }

    protected function addedLine(string $line, array $context): void
    {
        $oldNumber = $this->oldCounter;
        $newNumber = $this->newCounter + $this->addedLines;

        $this->currentHunk->addLine(new Line($line, Line::TYPE_ADD, $oldNumber, $newNumber));
        $this->currentFile->increaseAdditions();
        $this->addedLines++;
    }

    protected function line(string $line): void
    {
        if (!$this->currentHunk || !$this->currentFile) {
            return;
        }

        $oldNumber = $this->oldCounter + $this->deletedLines;
        $newNumber = $this->newCounter + $this->addedLines;

        $this->currentHunk->addLine(new Line($line, Line::TYPE_NO_CHANGE, $oldNumber, $newNumber));
        $this->oldCounter++;
        $this->newCounter++;
    }

    protected function clearAccumulator(): void
    {
        if ($this->currentFile) {
            if ($this->currentHunk) {
                $this->currentFile->addHunk($this->currentHunk);
            }

            $this->files[] = $this->currentFile;
        }

        $this->currentFile = $this->currentHunk = null;
    }
}