Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
> before assuming something isn't built. `@api` = stable to build on; `@internal` = may change.
> Grouped by **capability** (across layers), not by directory.

**240 classes** across **33 capabilities** · **26 modules**. Full prose: [FEATURES.md](FEATURES.md) (what) · [ARCHITECTURE.md](ARCHITECTURE.md) (why). Not-yet-built: [BACKLOG.md](BACKLOG.md).
**244 classes** across **33 capabilities** · **26 modules**. Full prose: [FEATURES.md](FEATURES.md) (what) · [ARCHITECTURE.md](ARCHITECTURE.md) (why). Not-yet-built: [BACKLOG.md](BACKLOG.md).

## Capabilities (`library/Tiger`)

Expand Down Expand Up @@ -328,7 +328,11 @@
- **Tiger_Git_Credential** `@api` — the write-only credential manager (TIGER-334 / TIGER-365). · `library/Tiger/Git/Credential.php`
- **Tiger_Git_Environment** `@api` — detect which transport this instance can use (TIGER-334). · `library/Tiger/Git/Environment.php`
- **Tiger_Git_Exception** `@api` — a structured, agent-readable, SECRET-FREE git failure (TIGER-334). · `library/Tiger/Git/Exception.php`
- **Tiger_Git_Provider_GitHub** `@api` — the GitHub adapter: WHICH host, composing the two transports (TIGER-334). · `library/Tiger/Git/Provider/GitHub.php`
- **Tiger_Git_Provider_Interface** `@api` — WHICH host a connection talks to (TIGER-334). · `library/Tiger/Git/Provider/Interface.php`
- **Tiger_Git_Transport_Api** `@api` — git operations through the GitHub API, with NO `.git` directory (TIGER-364). · `library/Tiger/Git/Transport/Api.php`
- **Tiger_Git_Transport_Interface** `@api` — HOW a git operation is carried out (TIGER-334). · `library/Tiger/Git/Transport/Interface.php`
- **Tiger_Git_Transport_Shell** `@api` — git operations driven by the `git` binary (TIGER-364). · `library/Tiger/Git/Transport/Shell.php`
- **Tiger_Git_WorkingTree** `@api` — one module's git working tree (TIGER-334). · `library/Tiger/Git/WorkingTree.php`
- **Tiger_Message** `@api` — the platform's way of telling people things (TIGER-114). · `library/Tiger/Message.php`
- **Tiger_Secret** `@api` — provider-neutral resolution of secrets and sensitive config values. · `library/Tiger/Secret.php`
Expand Down Expand Up @@ -359,7 +363,7 @@
- **CMS** (`cms`, app) · services: Menu, Page, Paths, Settings · `modules/cms`
- **Code** (`code`, developer) · services: Code · `modules/code`
- **Comments** (`comment`, app) · services: Comment, Render, Settings, Subjects · `modules/comment`
- **Git** (`git`, app) · `modules/git`
- **TigerGit** (`git`, app) · `modules/git`
- **Identity** (`identity`, plugin) · services: Identity · `modules/identity`
- **Matomo** (`matomo`, app) · services: Matomo · `modules/matomo`
- **Mcp** (`mcp`, module) · services: Settings · `modules/mcp`
Expand Down
2 changes: 2 additions & 0 deletions library/Tiger/Git/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class Tiger_Git_Exception extends RuntimeException
const INVALID_REF = 'INVALID_REF';
const API_ERROR = 'API_ERROR';
const SHELL_ERROR = 'SHELL_ERROR';
const NOT_IMPLEMENTED = 'NOT_IMPLEMENTED'; // a transport method not yet built (phased delivery)
const NOTHING_TO_COMMIT = 'NOTHING_TO_COMMIT';

/** @var string the stable error code */
protected $gitCode;
Expand Down
148 changes: 148 additions & 0 deletions library/Tiger/Git/Provider/GitHub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/**
* Tiger_Git_Provider_GitHub — the GitHub adapter: WHICH host, composing the two transports (TIGER-334).
*
* The transports answer HOW a git operation runs (the `git` binary, or the GitHub API with no `.git`);
* this provider answers WHICH host and composes them into one object a service can use without caring:
*
* - It picks the WORKING transport for a connection — the git binary where the host allows it, the API
* otherwise — and routes the git operations (status, diff, commit, branch, switch, push, pull, tag)
* to it. A connection stores the transport it chose at connect time (Tiger_Git_Environment detects
* it, re-checkable from the UI); the provider honours that, falling back to live detection only when
* a tree names none, and degrading a stale "shell" choice to the API when the host can no longer run
* a subprocess.
* - It ALWAYS routes the provider-API operations — listing releases and fetching a release archive —
* to the API transport, because a GitHub *release* is not a git concept: there is no git command for
* it, so even a shell-transport connection reaches releases over the API. This is the one place both
* transports are used together, and the reason the provider exists.
*
* v1 speaks only GitHub. The split (provider = host, transport = mechanism) is what lets a GitLab or
* Bitbucket adapter land later as another Tiger_Git_Provider_Interface without touching the transports
* or the callers.
*
* @api
* @since 1.38.0
*/
class Tiger_Git_Provider_GitHub implements Tiger_Git_Provider_Interface
{
const PROVIDER = 'github';

/** @var Tiger_Git_Transport_Api the API transport — always available (release ops + API working) */
private Tiger_Git_Transport_Api $api;

/** @var Tiger_Git_Transport_Shell the git-binary transport — used when a connection chose it */
private Tiger_Git_Transport_Shell $shell;

/** @var ?string memoized host transport recommendation (for name()) */
private ?string $hostTransport = null;

/**
* @param Tiger_Git_Credential $credentials the write-only credential manager both transports resolve through
* @param callable|null $http HTTP seam for the API transport (tests); null = real cURL
* @param callable|null $runner command-runner seam for the shell transport (tests); null = real proc_open
*/
public function __construct(Tiger_Git_Credential $credentials, ?callable $http = null, ?callable $runner = null)
{
$this->api = new Tiger_Git_Transport_Api($credentials, $http);
$this->shell = new Tiger_Git_Transport_Shell($credentials, $runner);
}

/** The provider/host key. */
public function provider(): string
{
return self::PROVIDER;
}

/**
* The working transport recommended for THIS host (detected once, memoized) — not a specific
* connection. A connection stores its own chosen transport (set at connect, re-checkable); this is
* the host-capability answer, used for display and as the fallback when a tree names no transport.
*/
public function name(): string
{
if ($this->hostTransport === null) {
$env = Tiger_Git_Environment::detect();
$this->hostTransport = $env['transport'] !== '' ? $env['transport'] : Tiger_Git::TRANSPORT_API;
}
return $this->hostTransport;
}

// --- git operations → the connection's working transport -------------------------------------

public function status(Tiger_Git_WorkingTree $tree): array
{
return $this->working($tree)->status($tree);
}

public function diff(Tiger_Git_WorkingTree $tree, array $opts = []): array
{
return $this->working($tree)->diff($tree, $opts);
}

public function commit(Tiger_Git_WorkingTree $tree, string $message, array $paths = []): string
{
return $this->working($tree)->commit($tree, $message, $paths);
}

public function branches(Tiger_Git_WorkingTree $tree): array
{
return $this->working($tree)->branches($tree);
}

public function createBranch(Tiger_Git_WorkingTree $tree, string $name): void
{
$this->working($tree)->createBranch($tree, $name);
}

public function switchBranch(Tiger_Git_WorkingTree $tree, string $name): void
{
$this->working($tree)->switchBranch($tree, $name);
}

public function push(Tiger_Git_WorkingTree $tree, string $branch): array
{
return $this->working($tree)->push($tree, $branch);
}

public function pull(Tiger_Git_WorkingTree $tree, string $branch): array
{
return $this->working($tree)->pull($tree, $branch);
}

public function tag(Tiger_Git_WorkingTree $tree, string $tag, ?string $ref = null): array
{
return $this->working($tree)->tag($tree, $tag, $ref);
}

// --- provider-API operations → ALWAYS the API transport --------------------------------------

public function listReleases(Tiger_Git_WorkingTree $tree): array
{
return $this->api->listReleases($tree);
}

public function fetchArchive(Tiger_Git_WorkingTree $tree, string $tag, string $destFile): string
{
return $this->api->fetchArchive($tree, $tag, $destFile);
}

// --- internals -------------------------------------------------------------------------------

/**
* The working transport for a connection: the git binary when the connection chose shell AND the
* host can still run a subprocess, the API otherwise. A tree that names no transport falls back to
* host detection; a stale "shell" choice on a host that lost `exec` degrades to the API rather than
* failing every operation.
*/
private function working(Tiger_Git_WorkingTree $tree): Tiger_Git_Transport_Interface
{
$want = $tree->transport();
if ($want === '') { $want = $this->name(); }
if ($want === Tiger_Git::TRANSPORT_SHELL && Tiger_Git_Environment::execUsable()) {
return $this->shell;
}
return $this->api;
}
}
25 changes: 25 additions & 0 deletions library/Tiger/Git/Provider/Interface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/**
* Tiger_Git_Provider_Interface — WHICH host a connection talks to (TIGER-334).
*
* A provider is the layer above the transports. Where Tiger_Git_Transport_Interface answers HOW an
* operation runs (the `git` binary, or the GitHub API with no `.git`), a provider answers WHICH host —
* and composes the transports so a caller never has to: it picks the working transport for a connection
* and routes the host's provider-API operations (releases, archives) to the API regardless of which
* transport is doing the git work. A provider therefore IS a transport (the composite one services use),
* plus a stable `provider()` key.
*
* v1 ships exactly one — Tiger_Git_Provider_GitHub. The split (provider = host, transport = mechanism)
* is the cheap convention that lets a GitLab/Bitbucket adapter land later as another implementation of
* this interface without touching the transports or the callers.
*
* @api
* @since 1.38.0
*/
interface Tiger_Git_Provider_Interface extends Tiger_Git_Transport_Interface
{
/** The provider/host key, e.g. 'github'. Stable — used for storage, routing, and display. */
public function provider(): string;
}
Loading
Loading