Luke Madhanga Luke Madhanga - 6 months ago 18
PHP Question

Channel `git on the server` calls through PHP

Is it possible to channel

git pull
and
git push
commands through
PHP
?

By this I mean I would set
https://example.com/projectname?credentials=xxx
as the
git origin
, and then when I perform a
pull
or
push
, the PHP script analyses the credentials, if they're okay, send the original command sent by git to the git repo folder on my server, and then a response is sent back to the user?

Could
git-http-backend
be used to achieve this, or is there any other way?

Answer

I finally got it to work

static $basegitdir = '/path/to/where/bare/git/repos/live';

/**
 * A PHP wrapper for git-http-backend
 */
static function httpBackend() {
    $packagename = 'gitrepo';
    $gitdir = self::$basegitdir . "{$packagename}.git";
    $gitcoredir = '/usr/lib/git-core'; // True for Ubuntu systems
    try {
        // In my implementation, func_get_args will return e.g. ['info', 'refs'], basically, all of the stuff git appends to the remote url
        $arguments = func_get_args();
        // Remove the first argument
        array_unshift($arguments, "{$packagename}.git");
        // Will resolve to something like '/repo.git/info/refs'
        $path = '/' . implode('/', $arguments);
        $service = filter_input(INPUT_GET, 'service');
        if ($service) {
            $service = "?service={$service}";
        }
        $res = self::proc_open("{$gitcoredir}/git-http-backend", [], $gitdir, true, [
            'PATH' => filter_input(INPUT_SERVER, 'PATH'),
            'GIT_PROJECT_ROOT' => self::$basegitdir,
            'GIT_HTTP_EXPORT_ALL' => 1,
            // PATH_INFO <MUST> evaluate to something along the lines of '/repo.git/info/refs'. Note, basegitdir has been 
            //  dropped from the beginning of the path. This is because Git joins GIT_PROJECT_ROOT and PATH_INFO. Also note the lack of a trailing slash after refs
            'PATH_INFO' => $path,
            'REMOTE_ADDR' => filter_input(INPUT_SERVER, 'REMOTE_ADDR'),
            // QUERY_STRING MUST evaluate to something along the lines of '/repo.git/info/refs/?service=service', note the trailing slash after refs
            'QUERY_STRING' => "{$path}/{$service}",
            'REQUEST_METHOD' => filter_input(INPUT_SERVER, 'REQUEST_METHOD'),
        ]);
    } catch (\Exception $ex) {
        // Log "Exception: {$ex->getMessage()}";
        exit;
    }
    $resbits = explode("\n", $res);
    foreach ($resbits as $index => $header) {
        if ($header && strpos($header, ':') !== false) {
            // Headers
            header($header);
            unset($resbits[$index]);
        } else {
            // First blank line is the space between the headers and the start of the response from Git
            break;
        }
    }
    echo trim(implode("\n", $resbits));
    exit;
}

/**
 * A wrapper for the proc_open functions
 * @param string $command The command to execute
 * @param array $arguments Responses to bash
 * @param string $cwd The current working directory
 * @param boolean $blocking False to make requests asynchronous
 * @param array $env Environment variables
 * @return string The output
 * @throws \Exception
 */
static function proc_open($command, array $arguments = [], $cwd = rootdir, $blocking = true, array $env = null) {
    $pipes = [];
    $descriptorspec = array(
       array('pipe', 'r'),  // STDIN
       array('pipe', 'w'),  // STDOUT
       array('pipe', 'w'),  // STDERR
    );
    $process = proc_open(trim($command), $descriptorspec, $pipes, $cwd, $env);
    stream_set_blocking($pipes[1], (int) $blocking);
    stream_set_blocking($pipes[2], (int) 1);
    foreach ($arguments as $arg) {
        // Write each of the supplied arguments to STDIN and ensure that it finishes with one trailing 
        fwrite($pipes[0], (preg_match("/\n(:?\s+)?$/", $arg) ? $arg : "{$arg}\n"));
    }
    $response = stream_get_contents($pipes[1]);
    $error = stream_get_contents($pipes[2]);
    if ($error) {
        throw new \Exception($error);
    }
    // Make sure that each pipe is closed to prevent a lockout
    foreach ($pipes as $pipe) {
        fclose($pipe);
    }
    proc_close($process);
    return $response;
}
Comments