NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Git for Laravel

Write operations

All writes take typed input objects and require an authenticated provider — an unauthenticated one throws InvalidCredentialsException before any request is sent:

use RoundlyConsulting\Git\Dto\Input\{NewRepository, NewBranch, NewFile, NewPullRequest, NewComment, NewRelease, NewTag};

$created = Git::github()->createRepository(new NewRepository(name: 'acme', private: true, autoInit: true));

$repo = Git::github()->repo('acme/acme');
$repo->createBranch(new NewBranch('feature/ci', 'main'));                              // the created branch ref
$commit = $repo->createFile(new NewFile('ci.yml', '...', 'Add CI', 'feature/ci'));     // Commit
$pr = $repo->createPullRequest(new NewPullRequest('Add CI', 'feature/ci', 'main', body: 'Adds the CI workflow.'));
$repo->pullRequest($pr->number)->comment('LGTM');   // or $repo->comment(new NewComment($pr->number, 'LGTM'))
$repo->createRelease(new NewRelease('v1.0', 'First release'));
$repo->createTag(new NewTag('v1.0.1', 'main'));                                         // a branch, tag or commit sha

Updating a file

updateFile() replaces an existing file on a branch. Read it first to get the blob sha the update needs:

use RoundlyConsulting\Git\Dto\Input\UpdatedFile;

$repo = Git::github()->repo('acme/acme');
$current = $repo->contents('ci.yml', ref: 'feature/ci');

$commit = $repo->updateFile(new UpdatedFile(
    path: 'ci.yml',
    content: $newYaml,
    message: 'Tune CI',
    branch: 'feature/ci',
    sha: $current->sha,     // the blob being replaced
));

Input objects

Every input validates itself in its constructor and throws InvalidArgumentException, so a malformed write fails at the call site instead of as a forge 422:

InputConstructorRejected locally when
NewRepositoryname, private = false, description, owner, template, autoInit = false, defaultBranchName is empty, template isn’t owner/repo, or defaultBranch is set without autoInit or a template.
NewBranchname, fromRefEither is empty.
NewFilepath, content, message, branchPath, message or branch is empty.
UpdatedFilepath, content, message, branch, shaPath, message, branch or sha is empty.
NewPullRequesttitle, head, base, body = nullTitle, head or base is empty.
NewCommentnumber, body, target = nullBody is empty.
NewReleasetagName, name, body, draft = false, prerelease = falseTag name is empty.
NewTagname, refEither is empty.
NewWebhookurl, events = ['push'], secret, active = trueURL is empty or no event is given.
NewReviewevent, body, comments = []A non-approval review has no body.
NewReviewCommentpath, line, body, side = Right, startLinePath or body is empty, a line is below 1, or startLine isn’t before line.

Worth knowing

  • Writes are never retried — a 5xx on a write may already have landed, and a second POST would open a second pull request.
  • createBranch() branches from fromRef and returns the new branch — its full ref on GitHub, its name on GitLab.
  • NewTag’s ref may be a branch, a tag or a sha. GitHub’s tag endpoint takes only a commit sha, so a branch or tag is first resolved to the commit it points at (a full sha is used as is).
  • File writes — createFile() and updateFile() — return the commit they made, with its real sha, author and date, on GitHub and GitLab alike.
  • For webhooks on your own endpoint, prefer the idempotent repo($path)->webhooks() helper — see Webhooks.

Comment targets

GitHub comments on issues and pull requests through one endpoint, but GitLab numbers issues and merge requests separately, so a GitLab comment must say which with a CommentTarget. An untargeted GitLab comment throws InvalidArgumentException rather than guessing; ->pullRequest($n)->comment() sets the target for you:

use RoundlyConsulting\Git\Dto\Input\NewComment;
use RoundlyConsulting\Git\Enums\CommentTarget;

$repo = Git::gitlab()->repo('acme/api');

$repo->comment(new NewComment(3, 'Thanks', target: CommentTarget::Issue)); // issue #3
$repo->comment(new NewComment(7, 'LGTM', target: CommentTarget::PullRequest)); // merge request !7
$repo->pullRequest(7)->comment('LGTM');                                     // sets the target for you

Bitbucket comments on pull requests only and refuses CommentTarget::Issue with FeatureNotSupportedException.

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

By donating, you agree to our donation terms.

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.