---
title: "The Git facade — Git for Laravel | Roundly"
description: "Everything starts at Git:: — a driver per forge, repo() and pullRequest() handles, installation lookups, webhook checks and a scope guard."
url: https://roundly-consulting.com/open-source/docs/git-for-laravel/facade
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Git for Laravel](https://roundly-consulting.com/open-source/docs/git-for-laravel.md)

# The Git facade

Everything starts at the Git facade. Git::github() (or gitlab(), bitbucket(), provider()) hands you an authenticated driver; ->repo('owner/name') scopes it to one repository, and ->pullRequest($number) to one pull request of it. With a token in config you don’t pass a credential at all; an explicit credential always overrides config:

```php
use RoundlyConsulting\Git\Enums\MergeMethod;
use RoundlyConsulting\Git\Facades\Git;

$github = Git::github();                       // a driver: account-wide reads and writes
$github->repositories();                       // Page<Repository>
$github->createRepository($newRepository);     // Repository

$repo = $github->repo('acme/app');             // a handle on one repository
$repo->pullRequests('open');                   // Page<PullRequest>
$repo->commits()->branch('main')->lazy();      // LazyCollection<int, Commit>
$repo->contents('README.md');                  // FileContent (decoded)
$repo->compare('main', 'feature/ci');          // Comparison
$repo->branch('main');                         // Branch — the head of that exact branch
$repo->actions()->run($runId);                 // WorkflowRun — GitHub Actions
$repo->webhooks()->register();                 // Webhook

$pr = $repo->pullRequest(12);                  // a handle on one pull request
$pr->approve('LGTM');
$pr->merge(MergeMethod::Squash, sha: $pr->get()->raw()['head']['sha'] ?? null);

Git::githubApp()->installations()->find($id);  // app-JWT lookups
Git::capabilities('bitbucket');                // no authentication needed
Git::credentials('github');                    // ?Credentials — what github() would use
Git::verifyWebhook('github', $request);        // bool
```

## The API at a glance

| Where | Methods |
| --- | --- |
| `Git::` | github(?Credentials), githubApp(?GithubApp), gitlab(?Credentials), bitbucket(?Credentials), provider(name, ?Credentials), capabilities(name), credentials(name), verifyWebhook(name, Request), fake(), macro() |
| `Git::github()` | repo(path\|Repository), installations(), user(), repositories(), allRepositories(), searchRepositories(), createRepository(), installationRepositories(), allInstallationRepositories(), batch(), rateLimit(), supports() / supportsAll() / supportsAny() / capabilities() / features() / featureMatrix() / featureInfo(), authenticate(), isAuthenticated() |
| `->repo('acme/app')` | get(), path(), branches(), branch($name), createBranch(), commit($sha), commits(), pullRequests(), pullRequest($n), createPullRequest(), issues(), issue($n), comment(), tags(), createTag(), releases(), release(), createRelease(), contents(), createFile(), updateFile(), compare(), contributors(), activity(), languages(), actions(), webhooks(), cloneUrl() |
| `->pullRequest(12)` | get(), number(), merge(), approve(), review(), reviews(), close(), comment() |
| `Git::githubApp()->installations()` | all(), find($id), forOrganization($org), forUser($login), installUrl(?$state) |
| `->repo(…)->actions()` | path(), dispatch(), runs(), run($id), jobs($runId), cancel($runId) |
| `->repo(…)->webhooks()` | register(), all(), registered(), delete(), deleteByUrl() |

Git::credentials() returns the credential a driver gets when you pass none — the configured GitHub App installation first, then the static token, else null — so you can hand the same credential to something outside the package, such as a clone URL. Git::verifyWebhook() runs the package’s own signature check for a route you own (see Webhooks).

## Handles refuse to leave their scope

Every path a handle takes ends up inside a forge URL, so the handles check it first and throw OutOfScopeException (an InvalidArgumentException) instead of addressing some other resource:

```php
use RoundlyConsulting\Git\Exceptions\OutOfScopeException;

Git::github()->repo('acme/app/../billing');          // OutOfScopeException
Git::github()->repo($gitlabRepository);              // OutOfScopeException: belongs to GitLab
Git::github()->repo('acme/app')->contents('../.env'); // OutOfScopeException
Git::github()->repo('acme/app')->contents('%2e%2e/%2e%2e/x');   // OutOfScopeException — decoded first
Git::github()->repo('acme/app')->webhooks()->delete('1/../..'); // OutOfScopeException
Git::github()->repo('acme/app')->pullRequest(0);     // OutOfScopeException
```

- repo() refuses an empty path, an empty, . or .. segment, and ?, #, \\, a NUL byte or whitespace — and a Repository object that belongs to another provider (a GitLab repository on Git::github()).
- contents(), createFile() and updateFile() refuse file paths with ., .. or empty segments, \\ or a NUL byte; commit(), release() and compare() refuse such refs — and ? or whitespace in them — as does any other ref that lands in a URL path (GitHub’s createBranch() base ref and createTag() ref). A file name may carry ? and #, a ref # (contents('docs/C#/intro.md'), compare('main', 'fix/#123')): they are sent as %3F and %23, while a segment that is . or .. up to its first ? or # (..#x, ..%3F) is still refused.
- Every value is checked as given and after each percent-decoding, because the HTTP stack decodes %2e and collapses dot segments before a request leaves: %2e%2e/victim, %252e%252e/victim and ..%2Fvictim are refused exactly like ../victim. What passes is percent-encoded segment by segment, so the forge receives the literal name you gave (docs/a b.md → docs/a%20b.md, 100%.md → 100%25.md).
- pullRequest() refuses a number below 1; installations()->find() a non-numeric id; forOrganization() / forUser() anything but a single segment.
- branch() and a workflow dispatch’s ref go through the ref checks above. actions() takes a workflow only as a numeric id or one file name ending .yml or .yaml (no /, even encoded, and no whitespace), and a run id only as digits.
- webhooks()->delete($id) — and git:webhook --delete= — takes only the id shape the forge issues: numeric on GitHub and GitLab, a braced {uuid} on Bitbucket, because the id lands in a DELETE URL.

The flat driver methods (Git::github()->contents('acme/app', …)) and batch() build their URLs through the same checks, so they refuse the same values.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/git-for-laravel.md)
