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
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/HEADcontainingref: refs/heads/main.git/hooks/,.git/info/exclude, and.git/description.git/configwith 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
$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:
- Reads the file content from the working tree
- Creates a
Blobobject and computes its hash - Writes the blob to
.git/objects/(zlib-compressed) - Updates the index entry for that path with the new hash and file metadata
remove
$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
$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
$commitId = $repo->commit('Fix null pointer bug');
echo $commitId->hex;
The commit process:
- 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.
- Determine parent: the current HEAD commit becomes the parent. Root commits (first commit in a repo) have no parents.
- Determine author: taken from the
PITMASTER_AUTHOR_*andGIT_AUTHOR_*environment variables, then thePITMASTER_AUTHOR_NAME/PITMASTER_AUTHOR_EMAILconstants, then.git/config(user.name/user.email), thenPitmaster <pitmaster@localhost>. The committer is resolved the same way. - Write commit object: the commit content (
tree,parent,author,committer, message) is serialized, hashed, and written. - 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
$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:
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
// 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
$repo->deleteBranch('feature/old');
Switch branches
$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
if ($repo->isBranchMerged('feature/done')) {
$repo->deleteBranch('feature/done');
}
Managing tags
Lightweight tag
$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
$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
$tagId = $repo->createTag(
'v2.0.0',
'Release version 2.0.0',
$repo->resolve('HEAD'),
'Jane Smith <jane@example.com> 1700000000 +0000',
);
Updating refs directly
$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
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
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
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
// 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
// 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
// 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.