PHP API
Pitmaster\Pitmaster
The Pitmaster\Pitmaster class is the static entry point. It provides factory methods for opening, creating, and cloning repositories, plus detection helpers.
use Pitmaster\Pitmaster;
open
public static function open(string $path, array $options = []): Repository
Open an existing repository at $path. The path should be the working tree root (containing .git/). Also supports linked worktrees (where .git is a file containing gitdir: <path>), and bare repositories (where $path is the .git directory itself).
$options accepts two booleans, both true by default:
hooks: run.git/hooksscripts. Passfalseto skip them for this handle.processes: allow anything that starts a process or opens a network transport. Passfalseto disable hooks, the fsmonitor hook, clone, fetch, push, and SSH process transport. Local reads and writes keep working.
Throws InvalidArgumentException if the path is not a git repository.
$repo = Pitmaster::open('/home/user/project');
$repo = Pitmaster::open('/home/user/project', ['hooks' => false]);
$repo = Pitmaster::open('/home/user/project', Pitmaster::processFreeOptions());
init
public static function init(string $path, string $objectFormat = 'sha1', array $options = []): Repository
Initialize a new repository at $path. Creates the .git directory structure with objects/, refs/heads/, refs/tags/, hooks/, info/, HEAD (pointing to refs/heads/main), and a default config.
$objectFormat is 'sha1' or 'sha256'. A SHA-256 repository gets repositoryformatversion = 1 and extensions.objectformat = sha256 in its config. Any other value throws InvalidArgumentException. $options takes the same keys as open().
Throws RuntimeException if a repository already exists at the path.
$repo = Pitmaster::init('/home/user/new-project');
$repo = Pitmaster::init('/home/user/sha256-project', 'sha256');
clone
public static function clone(string $url, string $path, ?int $depth = null, array $options = []): Repository
Clone a remote repository. Initializes a new repo, discovers remote refs, fetches the pack file, sets up remote tracking branches, and materializes the working tree. SSH URLs (ssh://... or user@host:path) use the SSH transport; other URLs use smart HTTP and fall back to dumb HTTP when the smart exchange fails. Pass $depth for a shallow clone.
Throws RuntimeException when $options['processes'] is false. If the clone fails and $path did not exist before, the partially created directory is removed.
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/tmp/repo');
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/tmp/shallow', 1);
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/tmp/repo', null, ['hooks' => false]);
processFreeOptions
public static function processFreeOptions(): array
Returns ['processes' => false, 'hooks' => false], for runtimes that must not start host processes. Pass it to open() or init().
isRepository
public static function isRepository(string $path): bool
Check if a path is a git repository. Returns true for regular repos (.git/ directory), linked worktrees (.git file), and bare repos (HEAD file present).
if (Pitmaster::isRepository('/path/to/check')) {
// It's a git repo
}
isWorktree
public static function isWorktree(string $path): bool
Check if a path is a linked worktree (not the main repo). Returns true only if .git is a file containing a gitdir: reference.
if (Pitmaster::isWorktree('/path/to/worktree')) {
// It's a linked worktree
}
commonGitDir
public static function commonGitDir(string $path): ?string
Resolve the common git directory from any checkout path. For regular repos, this is the .git directory. For linked worktrees, this is the shared git directory that contains objects, config, and packed-refs. Returns null if the path is not a repository.
$common = Pitmaster::commonGitDir('/path/to/worktree');
// '/path/to/main-repo/.git'
Pitmaster\Repository
The Pitmaster\Repository class is the main handle for all git operations. Obtain one via Pitmaster::open(), Pitmaster::init(), or Pitmaster::clone().
use Pitmaster\Repository;
Path accessors
gitDir
public function gitDir(): string
Returns the per-worktree git directory. For regular repos this is .git/. For linked worktrees, this is .git/worktrees/<name>/.
commonGitDir
public function commonGitDir(): string
Returns the shared git directory. Same as gitDir() for regular repos. For linked worktrees, this is the main repo's .git/ directory (where objects, config, and packed-refs live).
workDir
public function workDir(): string
Returns the working tree root path.
isLinkedWorktree
public function isLinkedWorktree(): bool
Returns true if this repository handle is a linked worktree.
isBare, hooksEnabled, processesEnabled
public function isBare(): bool
public function hooksEnabled(): bool
public function processesEnabled(): bool
Report whether the repository has no working tree, and which of the hooks / processes options are in effect for this handle. hooksEnabled() is false whenever processes are disabled.
Objects
readObject
public function readObject(string $hash): GitObject
Read any object by its full hex hash (40 characters, or 64 in a SHA-256 repository). Abbreviated hashes are not accepted. Returns a Blob, Tree, Commit, or Tag instance. Searches loose objects first, then pack files.
Throws ObjectNotFoundException if the hash is not found.
$object = $repo->readObject('a1b2c3d4e5f6...');
writeObject
public function writeObject(GitObject $object): ObjectId
Write an object to the loose object store. Returns the computed ObjectId.
use Pitmaster\Object\Blob;
$blob = Blob::fromContent('Hello, world!');
$id = $repo->writeObject($blob);
catFile
public function catFile(string $hash): string
Return the raw content of an object, equivalent to git cat-file -p.
echo $repo->catFile('a1b2c3d4...');
objectExists
public function objectExists(string $hash): bool
Check if an object exists in the repository (loose or packed).
listObjects
public function listObjects(): array
List all object hashes in the repository. Returns an array of hex strings.
Refs
head
public function head(): Commit
Returns the current HEAD commit. Follows symbolic refs (HEAD -> refs/heads/main -> commit hash).
Throws RuntimeException if HEAD does not point to a valid commit (e.g., empty repository).
branch
public function branch(?string $name = null): ?string
Without arguments, returns the current branch name (e.g., 'main'), or null if HEAD is detached.
With a branch name, resolves it to its commit hash (hex string) or returns null if the branch does not exist.
$current = $repo->branch(); // 'main'
$hash = $repo->branch('feature'); // 'abc123...' or null
branches
public function branches(): array
Returns a sorted array of all branch names (without the refs/heads/ prefix).
$branches = $repo->branches();
// ['bugfix/typo', 'feature/login', 'main']
tags
public function tags(): array
Returns a sorted array of all tag names (without the refs/tags/ prefix).
resolve
public function resolve(string $revision): ObjectId
Resolve a revision expression to an ObjectId. Supports:
- Full hex hashes (40 characters, or 64 for SHA-256). Abbreviated hashes such as
abc1234are not resolved. HEAD- Branch names (
main,feature/login) - Tag names (
v1.0.0) - Full ref paths (
refs/heads/main) - Revision expressions (
HEAD~3,main^2)
Throws RuntimeException if the revision cannot be resolved.
$id = $repo->resolve('HEAD~3');
$id = $repo->resolve('v1.0.0');
$id = $repo->resolve('feature/login');
allRefs
public function allRefs(): array
Returns all refs as an associative array of ref name to hex hash.
$refs = $repo->allRefs();
// ['refs/heads/main' => 'abc123...', 'refs/tags/v1.0.0' => 'def456...']
updateRef
public function updateRef(string $name, ObjectId $target): void
Update a ref to point to a new target.
$repo->updateRef('refs/heads/main', $commitId);
createBranch
public function createBranch(string $name, ?ObjectId $from = null): void
Create a new branch. If $from is null, branches from the current HEAD.
$repo->createBranch('feature/new-thing');
$repo->createBranch('hotfix', $repo->resolve('v1.0.0'));
deleteBranch
public function deleteBranch(string $name): void
Delete a branch by name. Throws RuntimeException if the branch is checked out or does not exist. Unlike git branch -d, it does not check whether the branch is merged.
$repo->deleteBranch('feature/old');
createLightweightTag
public function createLightweightTag(string $name, ?ObjectId $target = null): void
Create a lightweight tag. If $target is null, tags the current HEAD directly.
$repo->createLightweightTag('v2.0.0');
$repo->createLightweightTag('hotfix-start', $repo->resolve('main~2'));
createTag
public function createTag(string $name, string $message, ?ObjectId $target = null, ?string $tagger = null): ObjectId
Create an annotated tag. If $target is null, tags the current HEAD. Returns the tag object's ObjectId.
$tagId = $repo->createTag('v2.0.0', 'Release version 2.0.0');
deleteTag
public function deleteTag(string $name): void
Delete a tag by name.
$repo->deleteTag('v1.0.0');
packRefs
public function packRefs(): void
Rewrite the shared packed-refs file from the current loose ref set and prune the corresponding shared loose refs. For linked worktrees, this writes to the common git directory.
$repo->packRefs();
defaultBranch
public function defaultBranch(): string
Resolve the repository's default branch. Checks the remote HEAD symref first, then the local HEAD, then falls back to main or master.
isBranchMerged
public function isBranchMerged(string $branch, ?string $target = null): bool
Check if a branch is fully merged into another branch (defaults to the default branch).
if ($repo->isBranchMerged('feature/done')) {
$repo->deleteBranch('feature/done');
}
checkout
public function checkout(string $target): void
Switch branches or detach HEAD. Updates HEAD, the index, and the working tree. $target is a branch name, a tag, a full hash, or a revision expression; anything that is not a branch detaches HEAD.
Throws RuntimeException when a local change (staged or not) or an untracked file sits in a path that differs between the current and the target tree.
Uncommitted changes in other tracked files are not carried over: the index and working tree are rebuilt from the target tree, so edits to files that are identical on both sides are lost, where git would keep them (#2). Commit or stash them first. merge(), cherryPick(), revert(), rebase() and the bisect methods rebuild the working tree the same way.
$repo->checkout('feature/login');
$repo->checkout($repo->resolve('v1.0.0')->hex); // Detached HEAD
Index
index
public function index(): Index
Read and return the current index (staging area) from .git/index.
add
public function add(string ...$paths): void
Stage files. Reads each file from the working tree, creates a blob object, and updates the index. Each argument must be a file (or symlink) path relative to the working tree root; directories and globs are not expanded, and a missing file throws RuntimeException.
$repo->add('src/Feature.php', 'tests/FeatureTest.php');
remove
public function remove(string ...$paths): void
public function removeCached(string ...$paths): void
Remove tracked paths from both the index and the working tree. Supports --cached and -r / --recursive in the argument list for Git-shaped behavior.
$repo->remove('old-file.txt');
$repo->remove('-r', 'src');
$repo->remove('--cached', 'old-file.txt');
$repo->removeCached('old-file.txt');
mv
public function mv(string $source, string $destination): void
Move/rename a file in both the working tree and the index.
$repo->mv('old-name.php', 'new-name.php');
Commits
commit
public function commit(?string $message = null, ?string $author = null): ObjectId
Create a commit from the current index. Builds a tree hierarchy from the index entries, creates the commit object with the HEAD as parent (if any), and updates HEAD. Returns the new commit's ObjectId.
$message may be null only while a merge, cherry-pick, revert, or rebase is stopped on a conflict; the prepared message is used then. Otherwise a null message throws RuntimeException('Commit message required'). A commit also throws when the index has unmerged paths, when the index is empty in a repository without commits, and when the tree is unchanged from HEAD.
The $author parameter is a full git author string (Name <email> timestamp timezone). If null, it is resolved from environment variables, constants, and .git/config, in the order described in Getting Started.
$id = $repo->commit('Fix null pointer bug');
show
public function show(string $revision): array
Show a commit-ish and its diff against the first parent. When the revision resolves to an annotated tag, the return value also includes ['tag' => Tag] and commit is the peeled target commit.
$result = $repo->show('HEAD');
echo $result['commit']->message;
foreach ($result['diff'] as $diff) {
echo $diff->format();
}
reset
public function reset(string $revision, string $mode = 'mixed'): void
Reset HEAD to a commit. Modes:
'soft': move HEAD only'mixed': move HEAD + reset index (default)'hard': move HEAD + reset index + reset working tree
$repo->reset('HEAD~1', 'soft');
$repo->reset('main', 'hard');
restore
public function restore(string $path, ?string $source = null, bool $staged = false, bool $worktree = false): void
Restore a path from the index or a specific source revision. With neither flag set, Pitmaster restores the worktree from the index (or from $source when given). staged: true restores the index from HEAD (or $source) and leaves the worktree alone; pass staged: true, worktree: true to restore both.
$repo->restore('src/File.php'); // Worktree from index
$repo->restore('src/File.php', staged: true); // Index from HEAD
$repo->restore('src/File.php', 'HEAD~1'); // Worktree from another commit
$repo->restore('src/File.php', 'HEAD~1', staged: true, worktree: true); // Index + worktree from another commit
cherryPick
public function cherryPick(string $revision): ObjectId
public function cherryPickContinue(): ObjectId
public function cherryPickAbort(): void
Apply a commit as a new commit on the current branch, preserving the original author. If conflicts stop the operation, resolve them, stage the paths, then call cherryPickContinue() or cherryPickAbort().
$newId = $repo->cherryPick('feature~2');
// After resolving conflicts:
$repo->add('src/File.php');
$repo->cherryPickContinue();
revert
public function revert(string $revision): ObjectId
public function revertContinue(): ObjectId
public function revertAbort(): void
Create a commit that undoes another commit. If conflicts stop the revert, resolve them, stage the paths, then call revertContinue() or revertAbort().
$revertId = $repo->revert('HEAD~1');
// After resolving conflicts:
$repo->add('src/File.php');
$repo->revertContinue();
rebase
public function rebase(string $onto): array
public function rebaseContinue(): array
public function rebaseAbort(): void
public function rebaseSkip(): array
Rebase the current branch onto another revision. Pitmaster currently covers linear rebases with Git-shaped stop state for conflicts, plus continue, abort, and skip. HEAD must point to a branch.
rebase(), rebaseContinue(), and rebaseSkip() return ['success' => bool, 'commits' => int, 'conflicts' => list<string>]: whether the rebase finished, how many commits were applied, and the conflicting paths when it stopped.
$result = $repo->rebase('main');
if (!$result['success']) {
// Resolve conflicts, then continue or skip/abort.
$repo->add('src/File.php');
$repo->rebaseContinue();
}
Status
status
public function status(): array
Compute working tree status. Returns an array of StatusEntry objects, each containing:
path: the file pathindex:FileStatusenum for HEAD-to-index changesworktree:FileStatusenum for index-to-worktree changesorigPath,renameScore: set for renamed entries, otherwisenull
Untracked files have FileStatus::Untracked in both columns. Ignored files are not reported.
foreach ($repo->status() as $entry) {
echo $entry->shortFormat() . "\n"; // "M src/File.php", "?? notes.txt"
}
statusPorcelainV2
public function statusPorcelainV2(): string
Machine-readable status output in git's porcelain v2 format.
echo $repo->statusPorcelainV2();
// 1 M. N... 000000 000000 000000 0000...0000 0000...0000 modified-file.txt
// ? untracked-file.txt
Diff
diff
public function diff(?string $pathspec = null, string $algorithm = 'myers'): array
Diff working tree against the index (unstaged changes). Returns an array of DiffResult objects. $pathspec filters by one exact file path. $algorithm is 'myers' (default), 'patience', 'histogram', or 'minimal'; any other value throws InvalidArgumentException.
$diffs = $repo->diff();
foreach ($diffs as $diff) {
echo $diff->format(); // Unified diff output
}
diffStaged
public function diffStaged(?string $pathspec = null, string $algorithm = 'myers'): array
Diff index against HEAD (staged changes). Returns DiffResult[], with rename detection between deleted and added files.
The $pathspec argument is currently ignored, so every staged file is returned, and the method returns an empty array before the first commit (#5).
$staged = $repo->diffStaged();
diffTree
public function diffTree(ObjectId $a, ObjectId $b, string $algorithm = 'myers'): array
Diff two trees by their ObjectId. Returns DiffResult[]. Pass tree IDs, not commit IDs: a commit ID is not read as a tree and produces an empty result.
$old = $repo->readObject($repo->resolve('HEAD~1')->hex);
$new = $repo->head();
$diffs = $repo->diffTree($old->tree, $new->tree);
Log
log
public function log(int $limit = 50, ?ObjectId $from = null): array
Walk commit history, newest first by committer timestamp. Returns Commit[]. If $from is null, starts from HEAD. Returns an empty array when HEAD has no commit yet.
$commits = $repo->log(10);
logAll
public function logAll(int $limit = 50): array
Walk commits reachable from every ref and HEAD, newest first. Returns Commit[].
logOneline
public function logOneline(int $limit = 50, bool $all = false, ?string $path = null): array
Return log lines in git log --oneline form (abc1234 Subject), from HEAD, from all refs ($all), or filtered by path.
logPath
public function logPath(string $path, int $limit = 50): array
Log filtered by a file path. Returns only commits whose diff against their first parent adds, modifies, deletes, or renames that exact file. Directory paths do not match.
Only the newest 5 * $limit commits are examined, so older commits that touch the file can be missing from the result (#7).
$commits = $repo->logPath('src/Repository.php', 20);
Merge
merge
public function merge(string $branch, string $strategy = 'recursive'): MergeResult
public function mergeOctopus(array $branches): MergeResult
public function mergeContinue(): ObjectId
public function mergeAbort(): void
Merge a branch (or any revision) into HEAD. Detects fast-forward merges automatically. For non-fast-forward merges, performs tree-level conflict detection and creates a merge commit if clean. Conflict stops write Git-shaped merge state to the index and worktree; resolve the paths, stage them, then call mergeContinue() or mergeAbort().
$strategy 'ours' records a merge commit that keeps HEAD's tree; any other value uses the default recursive merge. mergeOctopus() merges two or more branches into one commit. If any branch conflicts, it returns an unclean result without writing merge state or creating a commit.
Merging a branch that is already contained in HEAD creates another merge commit instead of reporting "Already up to date" (#6). Uncommitted changes in tracked files are overwritten as described under checkout().
Returns a MergeResult with:
clean: whether the merge succeeded without conflictscommitId: the merge commit ID (if clean)conflictPaths: array of conflicting file paths (if not clean)mergedContents: conflicting path to file content with conflict markers (if not clean)
$result = $repo->merge('feature/login');
if ($result->clean) {
echo "Merged: {$result->commitId->hex}\n";
} else {
echo "Conflicts in: " . implode(', ', $result->conflictPaths) . "\n";
$repo->add('src/File.php');
$repo->mergeContinue();
}
mergeBase
public function mergeBase(ObjectId $a, ObjectId $b): ?ObjectId
Find the merge base (lowest common ancestor) of two commits. Returns null if no common ancestor exists.
$base = $repo->mergeBase(
$repo->resolve('main'),
$repo->resolve('feature/login'),
);
Bisect
public function bisectStart(string $bad, string $good): ObjectId
public function bisectGood(?string $revision = null): ?ObjectId
public function bisectBad(?string $revision = null): ?ObjectId
public function bisectReset(): void
Binary search for the commit that introduced a change. bisectStart() records the bad and good revisions, checks out the first candidate, and returns it. bisectGood() and bisectBad() mark the current HEAD (or $revision) and check out and return the next candidate. When a single suspect is left, that commit is returned and stays the candidate: it is the first bad commit. bisectReset() ends the session and returns to the branch or commit where it started.
$candidate = $repo->bisectStart('HEAD', 'v1.0.0');
// test $candidate ...
$candidate = $repo->bisectBad();
$candidate = $repo->bisectGood();
$repo->bisectReset();
Network
fetch
public function fetch(string $remote = 'origin', ?int $depth = null): void
Fetch from a remote. Downloads new objects and updates remote tracking refs. Pass $depth to deepen or limit a shallow history.
fetch() and the push*() methods throw RuntimeException on a handle opened with ['processes' => false].
$repo->fetch();
$repo->fetch('upstream');
push
public function push(string $remote = 'origin', ?string $branch = null): void
public function pushForceWithLease(string $remote = 'origin', ?string $branch = null, ?ObjectId $expected = null): void
public function pushAtomic(string $remote = 'origin', array $branches = []): void
public function pushMirror(string $remote = 'origin'): void
Push the current branch (or a specified branch) to a remote. Force-with-lease, atomic, and mirror variants are available for stricter or broader ref updates.
$repo->push();
$repo->push('origin', 'feature/login');
$repo->pushForceWithLease('origin', 'main', $repo->resolve('refs/remotes/origin/main'));
$repo->pushAtomic('origin', ['main', 'release']);
$repo->pushMirror();
Worktrees
addWorktree
public function addWorktree(string $path, string $branch, ?ObjectId $from = null, ?string $name = null): Worktree
Add a linked worktree. Creates the branch if it does not exist, sets up the worktree metadata, and materializes the working tree files. Pass $name when you need a deterministic metadata slug that differs from the checkout directory basename.
$wt = $repo->addWorktree('/tmp/review', 'feature/review');
$wt = $repo->addWorktree('/tmp/checkouts/review', 'feature/review', name: 'review-wt');
removeWorktree
public function removeWorktree(string $pathOrName, bool $force = false): void
Remove a linked worktree. Use $force = true to remove even if locked.
worktrees
public function worktrees(): array
List all worktrees (main + linked). Returns Worktree[].
foreach ($repo->worktrees() as $wt) {
echo "{$wt->path} [{$wt->branch}]\n";
}
Config
config
public function config(): GitConfig
Access the repository's git config. Read and write config values.
$config = $repo->config();
$name = $config->get('user.name');
$config->set('user.email', 'new@example.com');
Internal access
These methods expose internal components for advanced use cases.
objectDatabase
public function objectDatabase(): ObjectDatabase
Access the raw object database (loose + pack stores).
refDatabase
public function refDatabase(): RefDatabase
Access the raw ref database (loose + packed refs).
Lower-level classes
These classes are not wrapped by Repository and are constructed from its internal components.
Pitmaster\Graph\Blame
public function blame(ObjectId $headId, string $path): array
Returns one entry per line of $path at $headId: ['line' => int, 'hash' => string, 'author' => string, 'content' => string]. author is the full identity string (Name <email> timestamp timezone).
use Pitmaster\Graph\Blame;
$blame = new Blame($repo->objectDatabase());
foreach ($blame->blame($repo->resolve('HEAD'), 'src/File.php') as $line) {
echo substr($line['hash'], 0, 8) . " {$line['content']}\n";
}
Pitmaster\Graph\Grep
public function grep(ObjectId $treeId, string $pattern, string $prefix = '', array $options = []): array
Searches the blobs of a tree. Returns ['path' => string, 'line' => int, 'content' => string] per matching line. Options: regex (treat the pattern as a PCRE pattern) and ignore_case. An invalid regex throws InvalidArgumentException.
use Pitmaster\Graph\Grep;
$grep = new Grep($repo->objectDatabase());
$matches = $grep->grep($repo->head()->tree, 'TODO', '', ['ignore_case' => true]);
Pitmaster\Ref\Notes
public function get(ObjectId $commitId, string $notesRef = 'refs/notes/commits'): ?string
public function set(ObjectId $commitId, string $noteContent, string $notesRef = 'refs/notes/commits'): void
public function remove(ObjectId $commitId, string $notesRef = 'refs/notes/commits'): void
public function listAll(string $notesRef = 'refs/notes/commits'): array
public function merge(string $sourceRef, string $targetRef = 'refs/notes/commits'): void
Reads and writes git notes stored under a notes ref. listAll() returns commit hash to note content.
use Pitmaster\Ref\Notes;
$notes = new Notes($repo->objectDatabase(), $repo->refDatabase());
$notes->set($repo->resolve('HEAD'), "Reviewed\n");
echo $notes->get($repo->resolve('HEAD'));