Git Sync
3. Synchronization & Pull Requests

Creating Branches, Commits & Pull Requests

Creating Branches, Commits & Pull Requests

At the core of tokomono/git-sync is the Tokomono\GitSync\Contracts\GitProviderInterface contract. You can inject this interface into any controller, job, listener, or Artisan command to create a branch, commit one or more files, and open a Pull Request in three calls.

GitProviderInterface Contract

namespace Tokomono\GitSync\Contracts;

interface GitProviderInterface
{
    /**
     * Create a new branch from a source branch.
     */
    public function createBranch(string $sourceBranch, string $newBranch): void;

    /**
     * Commit a file to the specified branch.
     */
    public function commitFile(string $branch, string $filePath, string $content, string $commitMessage): void;

    /**
     * Create a pull request.
     *
     * @return string The URL of the created Pull Request.
     */
    public function createPullRequest(string $sourceBranch, string $targetBranch, string $title, string $description): string;
}

Complete End-to-End Usage Example

Below is a complete example showing how to validate the target branch against config('git-sync.target_branches'), create a sync branch, commit a modified PHP class file, open a Pull Request, and record the Pull Request in the git_sync_pull_requests table:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tokomono\GitSync\Contracts\GitProviderInterface;

class SyncWorkflowToGitAction
{
    public function __construct(
        protected GitProviderInterface $gitProvider,
    ) {}

    public function execute(string $targetBranch, string $relativeFilePath, string $phpContent, string $workflowName): string
    {
        $allowedBranches = config('git-sync.target_branches', ['main', 'master', 'develop', 'staging']);

        if (! in_array($targetBranch, $allowedBranches, true)) {
            throw new \InvalidArgumentException("Target branch [{$targetBranch}] is not allowed.");
        }

        $sourceBranch = 'ai-sync/'.Str::slug($workflowName).'-'.now()->format('Ymd-His');
        $title = "chore(ai-suite): update {$workflowName} workflow definition";
        $description = "Automated Pull Request generated by `tokomono/git-sync`.\n\n- **Updated file:** `{$relativeFilePath}`\n- **Target branch:** `{$targetBranch}`";

        // 1. Create a new branch from the target branch
        $this->gitProvider->createBranch($targetBranch, $sourceBranch);

        // 2. Commit the updated file content to the new branch
        $this->gitProvider->commitFile(
            branch: $sourceBranch,
            filePath: $relativeFilePath,
            content: $phpContent,
            commitMessage: $title,
        );

        // 3. Open the Pull Request and receive the PR URL
        $prUrl = $this->gitProvider->createPullRequest(
            sourceBranch: $sourceBranch,
            targetBranch: $targetBranch,
            title: $title,
            description: $description,
        );

        // 4. Record the Pull Request in git_sync_pull_requests
        DB::table('git_sync_pull_requests')->insert([
            'target_branch' => $targetBranch,
            'source_branch' => $sourceBranch,
            'title' => $title,
            'provider' => config('git-sync.default_provider', 'github'),
            'pr_url' => $prUrl,
            'status' => 'open',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return $prUrl;
    }
}

Testing Git Sync with Http::fake()

Because both GitHubProvider and BitbucketProvider use Laravel's Http facade (Illuminate\Support\Facades\Http), you can test your entire Git Sync flow in PHPUnit without making real network calls:

use Illuminate\Support\Facades\Http;
use Tokomono\GitSync\Contracts\GitProviderInterface;

public function test_it_creates_branch_commits_file_and_opens_github_pr(): void
{
    config()->set('git-sync.default_provider', 'github');
    config()->set('git-sync.providers.github', [
        'token' => 'ghp_test_token',
        'repository' => 'tokomono/demo-app',
    ]);

    Http::fake([
        'https://api.github.com/repos/tokomono/demo-app/git/ref/heads/main' => Http::response([
            'object' => ['sha' => 'abc123456789'],
        ], 200),
        'https://api.github.com/repos/tokomono/demo-app/git/refs' => Http::response([
            'ref' => 'refs/heads/ai-sync/update-workflow',
        ], 201),
        'https://api.github.com/repos/tokomono/demo-app/contents/*' => Http::sequence()
            ->push(['sha' => 'existing_file_sha'], 200)
            ->push(['content' => ['path' => 'app/Ai/Workflows/OrderWorkflow.php']], 200),
        'https://api.github.com/repos/tokomono/demo-app/pulls' => Http::response([
            'html_url' => 'https://github.com/tokomono/demo-app/pull/15',
        ], 201),
    ]);

    $provider = app(GitProviderInterface::class);

    $provider->createBranch('main', 'ai-sync/update-workflow');
    $provider->commitFile('ai-sync/update-workflow', 'app/Ai/Workflows/OrderWorkflow.php', '<?php // updated', 'Update workflow');
    $prUrl = $provider->createPullRequest('ai-sync/update-workflow', 'main', 'Update OrderWorkflow', 'Synced from staging');

    $this->assertSame('https://github.com/tokomono/demo-app/pull/15', $prUrl);
}