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 Wrapper Laravel Package

graham-campbell/git-wrapper

A PHP wrapper around the Git command-line client that provides a clean, object-oriented API for running Git commands via Symfony Process. It simplifies repository operations like clone, checkout, commit, and log, with configurable binary paths and support for custom environments.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Resolution spike first (this is a gate, not a step). Before writing any code, in a throwaway app on your exact PHP/Laravel pair:

composer require graham-campbell/git-wrapper:^1.1

If it fights illuminate/support constraints, stop and take the fallback: depend on the underlying cpliakas/php-git line (or the wrapper's own core classes) directly and register your own binding. The value this package adds over symfony/process is small; a version fight is not worth winning.

2. Read four files. The whole surface is GitWrapper, GitWorkingCopy, GitCommand, and GitException. An hour of reading tells you everything: how the command string is assembled (check escaping in GitCommand::__toString()), how output is captured, and what a non-zero exit throws.

3. Bind one configured singleton. There is no service provider, no facade, no publishable config on this line — that glue is yours, and it should exist exactly once:

// app/Providers/GitServiceProvider.php
$this->app->singleton(GitWrapper::class, fn () => tap(
    new GitWrapper(config('git.binary', '/usr/bin/git')), // confirm ctor signature on your pin
    fn (GitWrapper $w) => $w->setPrivateKey(config('git.private_key'))
));

$this->app->bind(RepositoryInspector::class, GitRepositoryInspector::class);

Add config/git.php with binary, private_key, timeout, work_root. Never new GitWrapper() at a call site.

4. First use case: a read-only, queued job. Do not debut this on deployment. Start with changelog assembly or repo metadata for an internal dashboard — a job that runs git log --pretty=format:... between two tags and writes markdown to storage. It exercises the real risks (binary present, permissions, output capture, error handling) with zero blast radius.

5. Define your interface before the second call site. RepositoryInspector (lastCommit(), tags(), status()), later ReleaseTagger. The wrapper stays an implementation detail you can swap in an afternoon.

Implementation Patterns

One working copy per operation, resolved explicitly. The wrapper's core ergonomic is workingCopy():

$git = $wrapper->workingCopy($repoPath);
$git->status();
$git->add('content/pages/home.md');
$git->commit('Update homepage copy');
$git->push('origin', 'main');

Treat $repoPath as a parameter threaded from the domain (tenant id, content set id), never as ambient global state. That keeps you free to move to job-per-repository affinity later.

Use argument arrays and --porcelain/--format output. Magic methods per subcommand ($git->checkout('main')) are convenient for humans and brittle for parsing. For anything you consume programmatically, drop to the generic runner and request machine-readable output:

$status = $git->run(['status', '--porcelain=v1']);
$log    = $git->run(['log', '--max-count=50', '--pretty=format:%H%x00%an%x00%at%x00%s']);

Split on %x00 rather than whitespace; commit messages contain spaces, newlines, and pipes.

Clone into a scratch directory, work, delete. For anything involving a remote (changelog from a repo URL, release tagging), clone into storage/app/git/{uuid}, act, and finally-delete. A cloned copy is disposable and immune to index.lock contention; a long-lived working copy is a shared mutable resource you now have to schedule around.

$tmp = storage_path('app/git/'.Str::uuid());
try {
    $wrapper->clone($remoteUrl, $tmp);
    $git = $wrapper->workingCopy($tmp);
    $git->tag($version);
    $git->push('origin', $version);
} finally {
    File::deleteDirectory($tmp);
}

Queue everything, and serialize per repository. Git calls fork a process, block on disk, and can run for minutes on a large clone. Jobs only, with $timeout, $tries, and — critically — WithoutOverlapping keyed by repository path or ShouldBeUnique. Two workers in the same working copy is the dominant production failure, and adding workers makes it worse, not better.

Commit with explicit identity. Do not rely on the machine's global git config:

$git->run(['-c', 'user.name=App', '-c', '[email protected]', 'commit', '-m', $message]);

Or set it once per working copy during setup. CI containers and slim base images frequently have no user.email, and commit fails with an opaque message that looks like a permissions problem.

Content-versioning loop. The high-leverage pattern: materialise the editable artifact as a file in the repo, commit one change per user action (author = the acting user), and expose history/diff/restore as reads over log and show. Restore-to-version is git checkout <sha> -- path plus a commit — which means "rollback" becomes a feature, not an audit-table migration.

Fixture-based tests against real repos. Mocking the wrapper tests nothing. Build a GitFixture trait that creates a temp dir, git init, seeds commits, and returns the path — including failure states: dirty tree, detached HEAD, empty repo, missing remote. Assert on parsed structures, not raw stdout.

Log the command, not the credential. The stringable command object is excellent for observability — write it to a log channel at debug level, but strip anything you injected via setPrivateKey or remote URLs before it lands there.

Gotchas and Tips

Exit code 1 is not always failure. git commit with nothing staged exits non-zero with "nothing to commit"; git diff --quiet uses exit status as its return value. Catch GitException narrowly and inspect the message/output rather than treating every throw as an incident.

index.lock contention is the failure mode that will page you. It surfaces as Unable to create '.git/index.lock': File exists and is not fixed by concurrency. Fix it with per-repository job locking and short-lived clones — not with retries, which just shuffle the collision.

Fail fast on missing credentials. Set GIT_TERMINAL_PROMPT=0 in the process environment unconditionally, and use batch-mode SSH (GIT_SSH_COMMAND="ssh -o BatchMode=yes"). Without both, a missing key turns a worker into a hung process until your job timeout fires.

The git binary is a first-class runtime dependency. Pin the version in your base image, assert a minimum at boot (a git --version check in a health command), and assign ownership of git CVE tracking to a person — composer audit will never surface CVE-2022-24765 (safe.directory), CVE-2022-39253, or CVE-2024-32002 (submodule RCE on clone). A base-image rebuild silently upgrading or dropping git is a real outage path.

Permissions bite in two directions. The PHP user must own or be trusted by the working copy — git's safe.directory check rejects repos owned by a different UID with an error that reads like corruption. Set safe.directory deliberately or, better, make ownership consistent.

Cap your output. log, diff, and show on a large repo can put hundreds of MB in memory through the captured stdout. Always pass --max-count, --stat, or a path filter. There is no built-in streaming backpressure you should rely on.

Validate anything user-derived. The library escapes arguments, but a branch name or path that reaches it is still an argument-injection candidate, and a remote URL is a fetch target. Whitelist refs with /^[A-Za-z0-9._\/-]+$/, reject leading -, resolve paths under a fixed root, and reject ...

Environment blockers are absolute. Behind disable_functions (proc_open/exec disabled), open_basedir, or a read-only filesystem, this package cannot work at all. Confirm before design, not during integration.

Empty repos and fresh clones. git log and git describe fail with no commits and no tags respectively — both common on a first run or a newly created tenant repo. Handle them as expected states, not exceptions.

Trim and normalise output. Captured stdout carries a trailing newline; commits and paths may carry CRLF on mixed-tooling setups. trim() and normalise separators before hashing or comparing.

Ephemeral storage kills the model. If working copies live on pod-local disk, horizontal scaling silently loses data. Either commit to disposable clones (best), or put working copies on a shared volume with job affinity.

Extension points are thin by design. What you can configure is essentially the binary path, the private key, the working directory, and the environment you pass to the process. Everything else — retries, locking, logging, domain interfaces, config — is your code. Budget accordingly: the integration is days, the operational surface is the real line item.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package