Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Git Laravel Package

kzykhys/git

kzykhys/git is a lightweight PHP wrapper around the Git command-line tool, offering an object-oriented API for common repository tasks such as cloning, committing, branching, status, log, and diff. It requires Git installed but no PHP extension, making Git automation simple in PHP projects.

View on GitHub
Deep Wiki
Context7

Getting Started

PHPGit is a thin, object-oriented wrapper around the git binary. It ships no Laravel service provider, no config file, and no facade — you wire it up yourself in about ten lines.

Install and verify

composer require kzykhys/git
git --version   # the binary must exist on the PATH of the PHP process

Before anything else, confirm the Composer resolution actually worked — the last release is from Feb 2014 and pins an old symfony/process line, which will collide with the Symfony version Laravel ships. If composer require fails, resolve it first (fork + bump the constraint, or replace/alias in composer.json) rather than fighting it later.

The only two methods you need to learn first

use PHPGit\Git;

$git = new Git();
$git->setRepository(base_path());   // ALWAYS do this explicitly
$status = $git->status();           // ['branch' => ..., 'changes' => [...]]

setRepository() is the whole configuration surface: it tells every command which working tree to operate on. Under PHP-FPM the process CWD is public/, and under a queue worker it's whatever the daemon started in — never rely on the default.

Bind it as a singleton

// app/Providers/GitServiceProvider.php
public function register(): void
{
    $this->app->singleton(Git::class, function () {
        $git = new Git();
        $git->setRepository(config('git.repository', base_path()));

        return $git;
    });
}

First use case: a deploy:pull artisan command or a GitHub/GitLab webhook controller that fetches and hard-resets a checkout. It is the shortest path from "I need to know what changed in this repo" to structured PHP data, and it beats shelling out by hand because you get escaping and exception handling for free.

Implementation Patterns

Magic command dispatch

$git->status() is __call; $git->branch->create('x') is __get returning a command object that is then invoked. The same verb is reachable both ways — pick one style per codebase and stick to it.

$git->checkout('main');
$git->checkout->create('feature/invoices');   // git checkout -b
$git->branch->delete('feature/old');
$branches = $git->branch();                   // array of branch names

Options are a flag array

Most methods take ($arg1, $arg2, array $options). Booleans render bare flags, strings render --key=value:

$git->commit('chore: bump deps', [
    'author' => 'Deploy Bot <[email protected]>',
    'all'    => true,
]);

$git->push('origin', 'main', ['u' => true]);
$git->merge('origin/main', ['no-ff' => true]);
$git->reset('origin/main', ['hard' => true]);

Argument order is not uniform across commands (push is remote-then-ref, diff is from-then-to, log takes a path or null first). Read the matching class in src/PHPGit/Command/ once rather than guessing — it's ~40 lines each.

Deployment / sync workflow

public function handle(Git $git): int
{
    $git->fetch('origin', ['prune' => true]);
    $git->reset('origin/main', ['hard' => true]);

    $this->info($git->log(null, ['limit' => 1])[0]['message']);

    return self::SUCCESS;
}

Run it from a queued job with WithoutOverlapping, not from a request — fetch/clone block with no configurable timeout, and a slow remote will hang a PHP worker.

Reading history as data

log, status, and blame return parsed arrays rather than raw stdout, which is the main reason to use this package over exec():

foreach ($git->log('app/Models', ['limit' => 20]) as $commit) {
    $commit['hash']; $commit['author']; $commit['date']; $commit['message'];
}

dd() the first result in tinker before writing code against key names — they come from a hardcoded --format string inside the command class, not from a stable API contract.

Wrap it behind your own interface

Because there's no Laravel fake, testing against a real repository is the only built-in option. Put a narrow interface in front of it so unit tests never touch the binary:

interface Repository
{
    public function head(): string;
    public function isDirty(): bool;
    public function commitAll(string $message): void;
}

Implement it with PHPGit in app/Support/GitRepository.php, and bind a fake in tests. Keep integration tests (real temp repo + real git) in a separate, slower suite.

Gotchas and Tips

  • The Symfony Process constraint is the real blocker. A 2014 release cannot depend on Symfony 6/7, which Laravel 9+ requires. Expect composer require to fail on any modern Laravel. Plan for a fork with a widened constraint; do not spend an afternoon on dependency gymnastics for a 176-star package.
  • clone is a reserved word. $git->clone(...) only parses on PHP 7+ (context-sensitive lexer). Fine today, but it means the package's own PHP 5.3 claim is misleading — and any static analysis or older tooling you run against it will choke.
  • The PHP user is not your user. www-data needs read/write on the checkout, a HOME with the right .gitconfig, and the deploy key / known_hosts in place. Set GIT_SSH_COMMAND or GIT_TERMINAL_PROMPT=0 in the process env so a missing credential fails fast instead of hanging.
  • safe.directory on git ≥ 2.35.2. If the repo directory is owned by deploy but the process runs as www-data, git refuses with "dubious ownership". Either chown, or run git config --global --add safe.directory /path as the PHP user.
  • Non-zero exit throws PHPGit\Exception\GitException. Catch it and log the message (stderr is folded in). Remember some commands legitimately exit 1 — diff --quiet, grep with no matches — so wrap those specifically or you'll treat "no changes" as an error.
  • index.lock under concurrency. Two jobs in the same working tree will collide. Guard with Cache::lock("git:{$path}", 60) or WithoutOverlapping; the wrapper does nothing to serialize access.
  • No output streaming and no timeouts. Every call buffers stdout and returns at the end. clone of a large repo or log of a huge history blocks. Keep long operations in jobs.
  • Escape and validate anything user-supplied. Arguments are shell-escaped, but a branch name or path from a request is still a git argument. Validate against /^[A-Za-z0-9._\/-]+$/ and reject anything starting with - so it can't be read as a flag.
  • Missing verbs. There is no stash or clean wrapper. For those, drop to Symfony\Component\Process\Process directly — Laravel already ships it — and keep the two styles clearly separated in your code.
  • Extension point: none, really. There is no registry or container hook. If you need custom behavior, subclass Git/AbstractCommand in app/Support/ or just write the one command you need with Process. Copying a 40-line command class is cheaper than patching the vendor directory.
  • For greenfield work consider czproject/git-php or gitonomy/gitlib — both are maintained, both support modern PHP/Symfony, and neither will block your next Laravel upgrade.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib