Skip to content

Writing to Repositories

Pitmaster writes git repositories using the same binary formats as the canonical git binary. Objects are written to the loose object store (zlib-compressed), the index is written in v2 format (or v3 when entries need extended flags; an existing index keeps its version), and refs are written as loose files. Written data is verifiable with git fsck --strict.

Initializing a repository

PHP
use Pitmaster\Pitmaster;

$repo = Pitmaster::init('/path/to/new-project');

This creates:

  • .git/objects/ (empty object store)
  • .git/refs/heads/ and .git/refs/tags/ (empty ref directories)
  • .git/HEAD containing ref: refs/heads/main
  • .git/hooks/, .git/info/exclude, and .git/description
  • .git/config with default settings

Pass 'sha256' as the second argument to create a SHA-256 repository: Pitmaster::init($path, 'sha256').

The default config does not set core.logallrefupdates, so Pitmaster records no reflog entries in a repository it created until you set that key to true (#8). Repositories created by git init set it already.

Staging files

add

PHP
$repo->add('src/Feature.php');
$repo->add('src/Feature.php', 'tests/FeatureTest.php', 'README.md');

Each argument is a file path relative to the working tree root. Directories and globs are not expanded, and a missing file throws RuntimeException. For each file, Pitmaster:

  1. Reads the file content from the working tree
  2. Creates a Blob object and computes its hash
  3. Writes the blob to .git/objects/ (zlib-compressed)
  4. Updates the index entry for that path with the new hash and file metadata

remove

PHP
$repo->remove('old-file.txt');
$repo->remove('-r', 'src');
$repo->remove('--cached', 'old-file.txt');

Removes tracked paths from the index and working tree. Use --cached for index-only removal, or -r / --recursive for tracked directories.

mv

PHP
$repo->mv('old-name.php', 'new-name.php');

Renames the file in the working tree, removes the old index entry, and adds the new one.

Creating commits

PHP
$commitId = $repo->commit('Fix null pointer bug');
echo $commitId->hex;

The commit process:

  1. Build tree from index: the flat list of index entries is organized into a tree hierarchy (directories become tree objects, files are blob references). Each tree is written to the object store.
  2. Determine parent: the current HEAD commit becomes the parent. Root commits (first commit in a repo) have no parents.
  3. Determine author: taken from the PITMASTER_AUTHOR_* and GIT_AUTHOR_* environment variables, then the PITMASTER_AUTHOR_NAME/PITMASTER_AUTHOR_EMAIL constants, then .git/config (user.name/user.email), then Pitmaster <pitmaster@localhost>. The committer is resolved the same way.
  4. Write commit object: the commit content (tree, parent, author, committer, message) is serialized, hashed, and written.
  5. Update HEAD: the branch ref that HEAD points to is updated to the new commit hash.

Hooks run around these steps (pre-commit, prepare-commit-msg, commit-msg, post-commit) unless the handle was opened with ['hooks' => false]. commit() throws when the message is missing, when the tree is unchanged from HEAD, or when the index has unmerged paths.

Custom author

PHP
$author = 'Jane Smith <jane@example.com> 1700000000 +0000';
$commitId = $repo->commit('Add feature', $author);

Verifying with git fsck

Pitmaster's integration coverage checks written objects against git fsck --strict, and you can verify the same manually:

Shell
cd /path/to/repo
git fsck --strict

If Pitmaster writes an invalid object, git fsck will report the error immediately.

Managing branches

Create a branch

PHP
// Branch from HEAD
$repo->createBranch('feature/new-thing');

// Branch from a specific commit
$repo->createBranch('hotfix', $repo->resolve('v1.0.0'));

Creates a ref file at .git/refs/heads/<name> containing the target commit hash.

Delete a branch

PHP
$repo->deleteBranch('feature/old');

Switch branches

PHP
$repo->checkout('feature/login');

Updates HEAD to a symbolic ref pointing to the branch, resets the index to match the branch tip's tree, and updates the working tree.

Local changes in paths that differ between the two trees stop the checkout with a RuntimeException. Uncommitted changes in all other tracked files are replaced by the branch's content, where git would keep them (#2). Commit or stash them before switching.

Check if merged

PHP
if ($repo->isBranchMerged('feature/done')) {
    $repo->deleteBranch('feature/done');
}

Managing tags

Lightweight tag

PHP
$repo->createLightweightTag('v1.0.0');                          // At HEAD
$repo->createLightweightTag('v0.9.0', $repo->resolve('HEAD~5'));

A lightweight tag is just a ref pointing directly to a commit.

Annotated tag

PHP
$tagId = $repo->createTag('v2.0.0', 'Release version 2.0.0');

An annotated tag creates a tag object (with tagger, message, and target) and a ref pointing to it. The tagger is the current committer identity, resolved like the commit author.

Custom tagger

PHP
$tagId = $repo->createTag(
    'v2.0.0',
    'Release version 2.0.0',
    $repo->resolve('HEAD'),
    'Jane Smith <jane@example.com> 1700000000 +0000',
);

Updating refs directly

PHP
$repo->updateRef('refs/heads/main', $commitId);
$repo->updateRef('refs/tags/latest', $commitId);

Writing objects directly

For advanced use cases, you can write objects without going through the high-level API.

Write a blob

PHP
use Pitmaster\Object\Blob;

$blob = Blob::fromContent('Hello, world!');
$id = $repo->writeObject($blob);
echo $id->hex;  // SHA-1 of "blob 13\0Hello, world!"

In a SHA-256 repository pass the algorithm: Blob::fromContent('Hello, world!', 'sha256').

Write a tree

PHP
use Pitmaster\Object\Tree;
use Pitmaster\Object\TreeEntry;
use Pitmaster\Object\ObjectId;

$entries = [
    new TreeEntry('100644', 'README.md', $blobId),
    new TreeEntry('40000', 'src', $subtreeId),
];

$tree = Tree::fromEntries($entries);
$id = $repo->writeObject($tree);

Tree::fromEntries() writes the entries in the order given and the mode strings as given. Pass them in git's tree order (by name, with directories compared as if their name ended in /) and write directory modes as 40000: git stores tree modes without the leading zero, and git fsck --strict rejects 040000.

Write a commit

PHP
use Pitmaster\Object\Commit;
use Pitmaster\Object\ObjectId;
use Pitmaster\Object\ObjectType;

$content = Commit::buildContent(
    tree: $treeId,
    parents: [$parentId],
    author: 'Name <email> 1700000000 +0000',
    committer: 'Name <email> 1700000000 +0000',
    message: "Commit message\n",
);

$commitId = ObjectId::compute(ObjectType::Commit, $content);
$commit = Commit::parse($content, $commitId);
$repo->writeObject($commit);

Reset

PHP
// Soft: move HEAD only
$repo->reset('HEAD~1', 'soft');

// Mixed: move HEAD + reset index (default)
$repo->reset('HEAD~1');

// Hard: move HEAD + reset index + reset working tree
$repo->reset('HEAD~3', 'hard');

Restore

PHP
// Restore a file from the index into the worktree
$repo->restore('src/File.php');

// Restore the index version from HEAD (the worktree is left alone)
$repo->restore('src/File.php', staged: true);

// Restore from a specific commit
$repo->restore('src/File.php', 'HEAD~1');

// Restore both index and worktree from a specific commit
$repo->restore('src/File.php', 'HEAD~1', staged: true, worktree: true);

Cherry-pick and revert

PHP
// Apply a commit on the current branch
$newId = $repo->cherryPick('feature~1');

// Undo a commit
$revertId = $repo->revert('HEAD~2');

Both take any revision resolve() accepts. Full hashes work; abbreviated hashes do not.