Location: PHPKode > scripts > igitigit > kayahr-igitigit-1bfe208/lib/classes/GitRepository.php
<?php
/*
 * igitigit - Web frontend for Git repositories
 * Copyright (C) 2011  Klaus Reimer <hide@address.com>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

namespace igitigit;

/**
 * A GIT repository.
 *
 * @author Klaus Reimer <hide@address.com>
 */
class GitRepository implements Repository, Directory, SystemObject
{  
    /** The parent directory. */
    private $parent;
    
    /** The repository name. */
    private $name;
    
    /** The path. */
    private $path;
    
    /** The absolute repository path. */
    private $repoPath;
    
    /** The executed GIT command. */
    private $gitCmd;
    
    /** The GIT error file. */
    private $gitErrorFile;
    
    /** The GIT pipes. */
    private $gitPipes;
    
    /** The GIT process. */
    private $gitProc;
    
    /** The selected branch. */
    private $branch;
    
    /**
     * Constructs a new repository.
     *
     * @param string $name
     *             The directory name.
     * @param Directory $parent
     *             The parent directory.
     */    
    public function __construct($name, Directory $parent = NULL)
    {
        $this->name = $name;
        $this->parent = $parent;

        // Determine the path.
        if ($parent)
        {
            $parentPath = $this->parent->getPath();
            $this->path = $parentPath;
            if ($parentPath && $name) $this->path .= "/";
            $this->path .= $name;
        }
        else
            $this->path = "";
        
        // Determine repository path.
        $this->repoPath = $this->getAbsPath();        
        if (is_dir($this->repoPath . "/.git/refs/heads"))
            $this->repoPath .= "/.git";

        // Determine selected branch.
        $key = $this->getSelectedBranchKey();
        if (isset($_SESSION[$key]))
        {
            // Read selected branch from session.
            $this->branch = $_SESSION[$key];
                
            // Correct selection if branch no longer exists.
            if (!$this->hasBranch($this->branch))
                $this->setBranch($this->branch);
        }
        else $this->branch = $this->getCurrentBranch();
    }
    
    /**
     * Checks if the specified absolute path is a repository.
     *
     * @param string $path
     *            The absolute path to the potential repository.
     * @return boolean
     *            True if path is a repository, false if not.
     */
    public static function isGitRepository($path)
    {
        return is_dir("$path/refs/heads") || is_dir("$path/.git/refs/heads");
    }
    
    /**
     * @see Object#getName()
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }
    
    /**
     * @see Object#getPath()
     *
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }
    
    /**
     * @see Object#getMode()
     *
     * @return int
     */
    public function getMode()
    {
        $stats = stat($this->getAbsPath());
        return $stats["mode"];
    }

    /**
     * @see Object#getLastModified()
     * 
     * @return int
     */
    public function getLastModified()
    {
        return filemtime($this->getAbsPath());
    }
    
    /**
     * @see Object#getUrl()
     *
     * @return string
     */    
    public function getUrl()
    {
        $path = $this->getPath();
        return CONTROLLER . ($path ? "/$path" : "");
    }
    
    /**
     * @see Object#getBreadcrumbs()
     * 
     * @return Breadcrumb[]
     */
    public function getBreadcrumbs()
    {
        $breadcrumbs = array();
        $current = $this;
        while ($current)
        {
            array_unshift($breadcrumbs, new Breadcrumb($current->getName(),
                $current->getUrl(), $current == $this));
            $current = $current->getParent();
        }
        return $breadcrumbs;
    }
    
    /**
     * @see SystemObject#getAbsPath()
     *
     * @return string
     */    
    public function getAbsPath()
    {
        $path = $this->getPath();
        return REPOS . ($path ? ("/" . $this->getPath()) : "");
    }
    
    /**
     * @see Directory#getParent()
     *
     * @return Directory
     */
    public function getParent()
    {
        return $this->parent;
    }
    
    /**
     * @see Directory#isRoot()
     *
     * @return boolean
     */
    public function isRoot()
    {
        return $this->parent != NULL;
    }
    
    /**
     * @see Directory#getObject()
     *
     * @param string $revision
     * @param string $path
     * @return GitObject
     */
    public function getObject($revision = NULL, $path = "")
    {
        if (!$revision) $revision = $this->branch;
        
        // If no revision is specified then we can't access anything.
        if (!$revision)
            throw new HttpException("No such file or directory", 404);
            
        // GIT has no properties for the root directory so we have to use
        // default values here.
        if (!$path)
            return new GitDirectory($this, $revision, "$path", 0100755);
    
        $data = $this->gitString("ls-tree", "-l", $revision, $path);
        if (!$data)
            throw new HttpException("No such file or directory", 404);
        $columns = preg_split('/\s+/', trim($data), 5);
        $mode = octdec($columns[0]);
        $type = $columns[1];
        $object = $columns[2];
        $size = $columns[3];
        $size = $size == "-" ? 0 : intval($size);
        if ($type == "blob")
            return new GitFile($this, $revision, "$path", $mode, $size);
        else
            return new GitDirectory($this, $revision, "$path", $mode);
    }
    
    /**
     * @see Directory#getObjects()
     *
     * @param string $revision
     * @param string $path
     * @return GitObject[]
     */
    public function getObjects($revision = NULL, $path = "")
    {
        if (!$revision) $revision = $this->branch;
        
        // If no revision is specified then there are no objects
        if (!$revision) return array();
        
        $objects = array();
        $repo = $this;
        if ($path) $path .= "/";
        $this->gitForEachLine(function($line) use ($revision, $path,
            &$objects, $repo)
        {
            $columns = preg_split('/\s+/', trim($line), 5);
            $mode = octdec($columns[0]);
            $type = $columns[1];
            $object = $columns[2];
            $size = $columns[3];
            $size = $size == "-" ? 0 : intval($size);
            $file = basename($columns[4]);
            if ($type == "blob")
                $object = new GitFile($repo, $revision, "$path$file", $mode,
                    $size);
            else
                $object = new GitDirectory($repo, $revision, "$path$file",
                    $mode);
            $objects[] = $object;
        }, "ls-tree", "-l", $revision, $path);
        
        usort($objects, array($this, "compareObjects"));
        return $objects;
    }

    /**
     * Compare method to sort tree items by type and name.
     *
     * @param GitObject $a
     *            First item to compare.
     * @param GitObject $b
     *            Second item to compare.
     * @return int
     *            The comparison result.     
     */
    private function compareObjects($a, $b)
    {
        $r = strcmp($a->getType(), $b->getType());
        if (!$r) $r = strcmp($a->getName(), $b->getName());
        return $r;
    }

    /**
     * @see Object#getType()
     *
     * @return string
     */
    public function getType()
    {
        return Object::TYPE_REPO;
    }
    
    /**
     * Opens GIT.
     *
     * @param mixed $args___
     *            Variable number of arguments to pass to GIT.
     */
    private function openGit($args___)
    {
        $this->gitErrorFile = tempnam(sys_get_temp_dir(), "igitigit");
        $descriptors = array(
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("file", $this->gitErrorFile, "w")
        );
        $args = array(
            GIT,
            "--git-dir",
            escapeshellarg($this->repoPath)
        );
        foreach (func_get_args() as $arg)
            $args[] = escapeshellarg($arg);
        $this->gitCmd = implode(" ", $args);
        $this->gitProc = proc_open($this->gitCmd, $descriptors,
            $this->gitPipes);
        if (!is_resource($this->gitProc))
            throw new GitException($this->gitCmd, -1, $this->gitErrorFile);
    }

    /**
     * Closes GIT.
     */    
    private function closeGit()
    {
        $result = proc_close($this->gitProc);
        if ($result)
            throw new GitException($this->gitCmd, $result,
                $this->gitErrorFile);
        unlink($this->gitErrorFile);            
    }
    
    /**
     * Executes a GIT command and returns the result as rows.
     *
     * @param string $rowDelimiter
     *            The row delimiter.
     * @param mixed $args___
     *            Variable number of GIT arguments.
     */
    private function gitRows($rowDelimiter, $args___)
    {
        $args = func_get_args();
        array_shift($args);
        call_user_func_array(array($this, "openGit"), $args);
        $data = stream_get_contents($this->gitPipes[1]);
        $this->closeGit();
        $rows = explode($rowDelimiter, $data);
        return $rows;
    }
    
    /**
     * Executes a GIT command and returns the result as a string.
     *
     * @param mixed $args___
     *            Variable number of GIT arguments.
     */
    private function gitString($args___)
    {
        $args = func_get_args();
        call_user_func_array(array($this, "openGit"), $args);
        $data = stream_get_contents($this->gitPipes[1]);
        $this->closeGit();
        return $data;
    }
    
    /**
     * Executes a GIT command and passes the returned lines one
     * by one to the specified callback function.
     *
     * @param callback $callback
     *            The callback function.
     * @param mixed $args___
     *            Variable number of GIT arguments.
     */
    public function gitForEachLine($callback, $args___)
    {
        $args = func_get_args();
        array_shift($args);
        call_user_func_array(array($this, "openGit"), $args);
        while ($line = fgets($this->gitPipes[1]))
        {
            call_user_func($callback, $line);
        }
        $this->closeGit();
    }
    
    /**
     * @see Repository#getBranches()
     *
     * @return string[]
     */
    public function getBranches()
    {
        $rows = $this->gitRows("\n", "branch");
        $branches = array();
        foreach ($rows as $row)
        {
            if (!$row) continue;
            $branch = substr($row, 2);
            if ($branch == "(no branch)") continue;
            $branches[] = $branch;
        }
        return $branches;
    }

    /**
     * @see Repository#getTags()
     *
     * @return string[]
     */
    public function getTags()
    {
        $rows = $this->gitRows("\n", "for-each-ref", "--sort=taggerdate",
            "--format=%(refname:short)", "refs/tags");
        $tags = array();
        foreach ($rows as $row)
        {
            if (!$row) continue;
            array_unshift($tags, $row);
        }
        return $tags;
    }

    /**
     * @see Repository#hasBranch()
     *
     * @param string $branch
     * @return boolean
     */    
    public function hasBranch($branch)
    {
        $branches = $this->getBranches();
        return in_array($branch, $branches);
    }

    /**
     * @see Repository#getCurrentBranch()
     *
     * @return string
     */    
    public function getCurrentBranch()
    {
        $rows = $this->gitRows("\n", "branch");
        $branches = array();
        foreach ($rows as $row)
        {
            if (!$row) continue;
            if ($row[0] == "*" && $row != "* (no branch)")
                return substr($row, 2);
        }
        $branches = $this->getBranches();
        
        if (!count($branches)) return NULL;
        
        return $branches[0];
    }
    
    /**
     * @see Repository#getBranch()
     *
     * @return string
     */
    public function getBranch()
    {
        return $this->branch;
    }

    /**
     * @see Repository#setBranch()
     *
     * @param string $branch    
     */
    public function setBranch($branch)
    {
        $this->branch = $branch;
        $key = $this->getSelectedBranchKey();
        $_SESSION[$key] = $branch;
    }
    
    /**
     * @see RepoObject#getRevision()
     *
     * @return string
     */
    public function getRevision()
    {
        return $this->branch;
    }
    
    /**
     * @see RepoObject#getRep()
     *
     * @return GitRepository
     */
    public function getRepo()
    {
        return $this;
    }

    /**
     * @see RepoObject#getLastCommit()
     *
     * @return Commit
     */    
    public function getLastCommit()
    {
        $commits = $this->getCommits($this->branch, "",  1);
        
        // Check if there is a commit at all.
        if (!count($commits)) return NULL;
        
        return $commits[0];
    }
    
    /**
     * @see RepoObject#getCommit()
     *
     * @return Commit
     */
    public function getCommit($hash)
    {
        $commits = $this->getCommits($hash, "",  1);
        if (!$commits)
            throw new HttpException("Commit not found", 404);
        return $commits[0];
    }
    
    /**
     * Returns the session key for the selected branch of this repository.
     *
     * @return string
     *            The session key.
     */
    private function getSelectedBranchKey()
    {
        return "selectedBranch_" . md5($this->repoPath);
    }
    
    /**
     * Checks if the specified path is a GIT repository or a file.
     * 
     * @return boolean
     *            True if GIT repository, false if just a file.
     */
    private function isGitDirectory($revision = NULL, $path = "")
    {
        // If path is empty then it must be a tree.
        if (!$path) return true;
        
        if (!$revision) $revision = $this->branch;
        
        // If no revision is specified then this can't be a Git directory.
        if (!$revision) return false;
        
        $data = $this->gitString("ls-tree", "-l", $revision, $path);
        $columns = preg_split('/\s+/', trim($data), 5);
        return $columns[1] == "tree";
    }

    /**
     * Returns the URL to view the full history of the currently selected
     * branch.
     *
     * @return string
     *            The history URL.
     */
    public function getHistoryUrl()
    {
        return $this->getUrl() . "/commits/" . $this->branch;
    }

    /**
     * Returns the commits for the specified revision and path.
     *
     * @param string $revision
     *            Optional revision. Defaults to currently selected branch.
     * @param string $path
     *            Optional path. Defaults to root directory.
     * @param int number
     *            Optional number of commits to return. Defaults to 35.
     * @return Commit[]
     *            The commits.
     */
    public function getCommits($revision = NULL, $path = "", $number = 35)
    {
        if (!$revision) $revision = $this->branch;

        // If no revision is specified then there are no commits.
        if (!$revision) return array();
            
        $commits = array();
        $repo = $this;
        $this->gitForEachLine(function($row) use (&$commits, $repo)
        {
            $cols = explode("\0", $row);
            $commitHash = $cols[0];
            $treeHash = $cols[1];
            $parentHash = $cols[2];
            $authorDate = $cols[3];
            $author = new Contact($cols[4], $cols[5]);
            $authorEMail = $cols[5];
            $committerDate = $cols[6];
            $committer = new Contact($cols[7], $cols[8]);
            $subject = $cols[9];
            $commits[] = new Commit($repo, $commitHash, $treeHash,
                $parentHash, $authorDate, $author, $committerDate,
                $committer, $subject);
        }, "log", "-n", $number,
            "--format=format:%H%x00%T%x00%P%x00%at%x00%an%x00%ae%x00%ct%x00%cn%x00%ce%x00%s",
            $revision, "--", $path);
        return $commits;
    }

    /**
     * Dumps the content of a blob to the HTML output. Special 
     * characters are encoded but line-breaks are not touched so the caller
     * must wrap the output with a pre tag.
     *
     * @param string $revision
     *            The revision. If NULL then currently selected branch is used.
     * @param string $path
     *            The path.
     * @return int
     *            The number of dumped lines. NULL if no line numbers are
     *            available.
     */
    public function dumpBlob($revision = NULL, $path = "")
    {
        if (!$revision) $revision = $this->branch;
        $lines = 0;
        $this->gitForEachLine(function($row) use (&$lines)
        {
            $lines++;
            printf("<span id=\"L%d\" class=\"line\">%s</span>", $lines,
                htmlspecialchars($row));
        }, "show", "$revision:$path");
        return $lines;
    }

    /**
     * Dumps the raw content of a blob to the HTML output.
     *
     * @param string $revision
     *            The revision.
     * @param string $path
     *            The path.
     */
    public function dumpRawBlob($revision = NULL, $path = "")
    {
        if (!$revision) $revision = $this->branch;
        $this->gitForEachLine(function($row) use (&$lines)
        {
            echo $row;
        }, "show", "$revision:$path");
    }


    /**
     * Dumps the blame lines of a blob to the HTML output. The lines are
     * grouped by commit in div statements. The blame information is
     * returned.
     *
     * @param string $revision
     *            The revision. If NULL then currently selected branch is used.
     * @param string $path
     *            The path.
     * @return Blame
     *            The blame information.
     */
    public function dumpBlame($revision = NULL, $path = "")
    {
        if (!$revision) $revision = $this->branch;
        $blame = new Blame($this, $path);
        $mode = 0;
        $first = true;
        $lineNo = 1;
        $this->gitForEachLine(function($line) use (&$blame, &$mode, &$first,
            &$lineNo)
        {
            switch ($mode)
            {
                case 0:
                    $parts = explode(" ", $line);
                    $hash = $parts[0];
                    $lines = count($parts) > 3 ? intval($parts[3]) : NULL;
                    if ($lines && $blame->setCommit($hash, $lines))
                    {
                        if ($first)
                            $first = false;
                        else
                            echo "</span>";
                        echo "<span class=\"commit\">";
                    }
                    $mode = 1;
                    break;
                    
                case 1:
                    if ($line[0] == "\t")
                    {
                        printf("<span id=\"L%d\" class=\"line\">%s</span>",
                            $lineNo, htmlspecialchars(substr($line, 1)));
                        $lineNo++;
                        $mode = 0;
                    }
                    else
                    {
                        $parts = explode(" ", trim($line), 2);
                        if (count($parts) >1 )
                        {
                            $key = $parts[0];
                            $value = $parts[1];
                            if ($key == "author")
                                $blame->setAuthorName($value);
                            else if ($key == "author-time")
                                $blame->setAuthorDate($value);
                            else if ($key == "author-mail")
                                $blame->setAuthorEMail(substr($value, 1, -1));
                            else if ($key == "summary")
                                $blame->setSummary($value);
                        }
                    }
            }
        }, "blame", "-p", $revision, "--", $path);
        if (!$first) echo "</span>";
        return $blame;
    }
}
Return current item: igitigit