
# PHP API

## `Pitmaster\Pitmaster`

Die Klasse `Pitmaster\Pitmaster` ist der statische Einstiegspunkt. Sie bietet Factory-Methoden zum Öffnen, Erstellen und Klonen von Repositorys sowie Erkennungsfunktionen.

```php
use Pitmaster\Pitmaster;
```

### `open`

```php
public static function open(string $path, array $options = []): Repository
```

Öffne ein vorhandenes Repository unter `$path`. Der Pfad sollte auf die Wurzel des Arbeitsverzeichnisses zeigen (mit `.git/`). Unterstützt werden auch verknüpfte Worktrees (bei denen `.git` eine Datei mit `gitdir: <path>` enthält) und Bare-Repositorys (bei denen `$path` selbst das Verzeichnis `.git` ist).

`$options` akzeptiert zwei boolesche Werte, die standardmäßig beide `true` sind:

- `hooks`: `.git/hooks`-Skripte ausführen. Übergib `false`, um sie für dieses Handle zu überspringen.
- `processes`: alles zulassen, was einen Prozess startet oder einen Netzwerktransport öffnet. Übergib `false`, um Hooks, den fsmonitor-Hook, Klonen, Fetch, Push und den SSH-Prozesstransport zu deaktivieren. Lokales Lesen und Schreiben funktioniert weiterhin.

Löst `InvalidArgumentException` aus, wenn der Pfad kein git-Repository ist.

```php
$repo = Pitmaster::open('/home/user/project');
$repo = Pitmaster::open('/home/user/project', ['hooks' => false]);
$repo = Pitmaster::open('/home/user/project', Pitmaster::processFreeOptions());
```

### `init`

```php
public static function init(string $path, string $objectFormat = 'sha1', array $options = []): Repository
```

Ein neues Repository unter `$path` initialisieren. Erstellt die `.git`-Verzeichnisstruktur mit `objects/`, `refs/heads/`, `refs/tags/`, `hooks/`, `info/`, `HEAD` (zeigt auf `refs/heads/main`) und einer Standard-`config`.

`$objectFormat` ist `'sha1'` oder `'sha256'`. Ein SHA-256-Repository erhält `repositoryformatversion = 1` und `extensions.objectformat = sha256` in seiner Konfiguration. Jeder andere Wert löst `InvalidArgumentException` aus. `$options` verwendet dieselben Schlüssel wie `open()`.

Löst `RuntimeException` aus, wenn unter diesem Pfad bereits ein Repository vorhanden ist.

```php
$repo = Pitmaster::init('/home/user/new-project');
$repo = Pitmaster::init('/home/user/sha256-project', 'sha256');
```

### `clone`

```php
public static function clone(string $url, string $path, ?int $depth = null, array $options = []): Repository
```

Ein entferntes Repository klonen. Initialisiert ein neues Repo, ermittelt Remote-Refs, lädt die Pack-Datei herunter, richtet Remote-Tracking-Branches ein und erstellt den Arbeitsbaum. SSH-URLs (`ssh://...` oder `user@host:path`) verwenden den SSH-Transport; andere URLs nutzen smart HTTP und greifen auf dumb HTTP zurück, wenn der Smart-Austausch fehlschlägt. Für einen flachen Klon `$depth` übergeben.

Löst `RuntimeException` aus, wenn `$options['processes']` `false` ist. Wenn das Klonen fehlschlägt und `$path` zuvor nicht vorhanden war, wird das teilweise erstellte Verzeichnis entfernt.

```php
$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`

```php
public static function processFreeOptions(): array
```

Gibt `['processes' => false, 'hooks' => false]` für Laufzeitumgebungen zurück, die keine Host-Prozesse starten dürfen. An `open()` oder `init()` übergeben.

### `isRepository`

```php
public static function isRepository(string $path): bool
```

Prüft, ob ein Pfad ein git-Repository ist. Gibt `true` für gewöhnliche Repositories (Verzeichnis `.git/`), verknüpfte Arbeitsbäume (Datei `.git`) und Bare-Repositories (Datei `HEAD` vorhanden) zurück.

```php
if (Pitmaster::isRepository('/path/to/check')) {
    // It's a git repo
}
```

### `isWorktree`

```php
public static function isWorktree(string $path): bool
```

Prüfen, ob ein Pfad ein verknüpfter Arbeitsbaum ist (nicht das Haupt-Repo). Gibt `true` nur zurück, wenn `.git` eine Datei mit einer `gitdir:`-Referenz ist.

```php
if (Pitmaster::isWorktree('/path/to/worktree')) {
    // It's a linked worktree
}
```

### `commonGitDir`

```php
public static function commonGitDir(string $path): ?string
```

Löst das gemeinsame git-Verzeichnis anhand eines beliebigen Checkout-Pfads auf. Bei gewöhnlichen Repositories ist dies das Verzeichnis `.git`. Bei verknüpften Arbeitsbäumen ist es das gemeinsame Verzeichnis git, das Objekte, Konfiguration und Packed-Refs enthält. Gibt `null` zurück, wenn der Pfad kein Repository ist.

```php
$common = Pitmaster::commonGitDir('/path/to/worktree');
// '/path/to/main-repo/.git'
```

---

## `Pitmaster\Repository`

Die Klasse `Pitmaster\Repository` ist die zentrale Schnittstelle für alle git-Operationen. Erzeuge sie mit `Pitmaster::open()`, `Pitmaster::init()` oder `Pitmaster::clone()`.

```php
use Pitmaster\Repository;
```

### Pfadzugriffe

#### `gitDir`

```php
public function gitDir(): string
```

Gibt das git-Verzeichnis des jeweiligen Arbeitsbaums zurück. Bei gewöhnlichen Repositories ist dies `.git/`. Bei verknüpften Arbeitsbäumen ist es `.git/worktrees/<name>/`.

#### `commonGitDir`

```php
public function commonGitDir(): string
```

Gibt das gemeinsame Git-Verzeichnis zurück. Bei regulären Repos entspricht es `gitDir()`. Bei verknüpften Arbeitsbäumen ist es das Verzeichnis `.git/` des Haupt-Repos (dort befinden sich Objekte, Konfiguration und packed-refs).

#### `workDir`

```php
public function workDir(): string
```

Gibt den Stammpfad des Arbeitsbaums zurück.

#### `isLinkedWorktree`

```php
public function isLinkedWorktree(): bool
```

Gibt `true` zurück, wenn dieses Repository-Handle ein verknüpfter Arbeitsbaum ist.

#### `isBare`, `hooksEnabled`, `processesEnabled`

```php
public function isBare(): bool
public function hooksEnabled(): bool
public function processesEnabled(): bool
```

Gibt an, ob das Repository keinen Arbeitsbaum hat und welche der Optionen `hooks` / `processes` für dieses Handle gelten. `hooksEnabled()` ist immer `false`, wenn Prozesse deaktiviert sind.

---

### Objekte

#### `readObject`

```php
public function readObject(string $hash): GitObject
```

Ein beliebiges Objekt anhand seines vollständigen hexadezimalen Hashes lesen (40 Zeichen oder 64 in einem SHA-256-Repository). Abgekürzte Hashes werden nicht akzeptiert. Gibt eine Instanz von `Blob`, `Tree`, `Commit` oder `Tag` zurück. Durchsucht zuerst lose Objekte, dann Pack-Dateien.

Löst `ObjectNotFoundException` aus, wenn der Hash nicht gefunden wird.

```php
$object = $repo->readObject('a1b2c3d4e5f6...');
```

#### `writeObject`

```php
public function writeObject(GitObject $object): ObjectId
```

Ein Objekt in den Speicher für lose Objekte schreiben. Gibt den berechneten `ObjectId` zurück.

```php
use Pitmaster\Object\Blob;

$blob = Blob::fromContent('Hello, world!');
$id = $repo->writeObject($blob);
```

#### `catFile`

```php
public function catFile(string $hash): string
```

Den Rohinhalt eines Objekts zurückgeben, gleichbedeutend mit `git cat-file -p`.

```php
echo $repo->catFile('a1b2c3d4...');
```

#### `objectExists`

```php
public function objectExists(string $hash): bool
```

Prüfen, ob ein Objekt im Repository vorhanden ist (lose oder gepackt).

#### `listObjects`

```php
public function listObjects(): array
```

Alle Objekt-Hashes im Repository auflisten. Gibt ein Array hexadezimaler Zeichenfolgen zurück.

---

### Refs

#### `head`

```php
public function head(): Commit
```

Den aktuellen HEAD-Commit zurückgeben. Folgt symbolischen Refs (HEAD -> refs/heads/main -> Commit-Hash).

Löst `RuntimeException` aus, wenn HEAD nicht auf einen gültigen Commit zeigt (e.g, leeres Repository).

#### `branch`

```php
public function branch(?string $name = null): ?string
```

Ohne Argumente wird der aktuelle Branch-Name zurückgegeben (e.g, `'main'`) oder `null`, wenn HEAD losgelöst ist.

Mit einem Branch-Namen wird dieser zu seinem Commit-Hash (hexadezimale Zeichenfolge) aufgelöst oder `null` zurückgegeben, wenn der Branch nicht existiert.

```php
$current = $repo->branch();        // 'main'
$hash = $repo->branch('feature');   // 'abc123...' or null
```

#### `branches`

```php
public function branches(): array
```

Ein sortiertes Array aller Branch-Namen zurückgeben (ohne das Präfix `refs/heads/`).

```php
$branches = $repo->branches();
// ['bugfix/typo', 'feature/login', 'main']
```

#### `tags`

```php
public function tags(): array
```

Ein sortiertes Array aller Tag-Namen zurückgeben (ohne das Präfix `refs/tags/`).

#### `resolve`

```php
public function resolve(string $revision): ObjectId
```

Einen Revisionsausdruck zu einem `ObjectId` auflösen. Unterstützt werden:
- Vollständige Hex-Hashes (40 Zeichen oder 64 für SHA-256). Abgekürzte Hashes wie `abc1234` werden nicht aufgelöst.
- `HEAD`
- Branch-Namen (`main`, `feature/login`)
- Tag-Namen (`v1.0.0`)
- Vollständige Ref-Pfade (`refs/heads/main`)
- Revisionsausdrücke (`HEAD~3`, `main^2`)

Löst `RuntimeException` aus, wenn die Revision nicht aufgelöst werden kann.

```php
$id = $repo->resolve('HEAD~3');
$id = $repo->resolve('v1.0.0');
$id = $repo->resolve('feature/login');
```

#### `allRefs`

```php
public function allRefs(): array
```

Alle Refs als assoziatives Array mit Ref-Namen und Hex-Hashes zurückgeben.

```php
$refs = $repo->allRefs();
// ['refs/heads/main' => 'abc123...', 'refs/tags/v1.0.0' => 'def456...']
```

#### `updateRef`

```php
public function updateRef(string $name, ObjectId $target): void
```

Eine Ref aktualisieren, sodass sie auf ein neues Ziel zeigt.

```php
$repo->updateRef('refs/heads/main', $commitId);
```

#### `createBranch`

```php
public function createBranch(string $name, ?ObjectId $from = null): void
```

Einen neuen Branch erstellen. Wenn `$from` null ist, wird der aktuelle HEAD als Ausgangspunkt verwendet.

```php
$repo->createBranch('feature/new-thing');
$repo->createBranch('hotfix', $repo->resolve('v1.0.0'));
```

#### `deleteBranch`

```php
public function deleteBranch(string $name): void
```

Einen Branch anhand seines Namens löschen. Löst `RuntimeException` aus, wenn der Branch ausgecheckt ist oder nicht existiert. Anders als `git branch -d` wird nicht geprüft, ob der Branch gemergt wurde.

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

#### `createLightweightTag`

```php
public function createLightweightTag(string $name, ?ObjectId $target = null): void
```

Einen Lightweight-Tag erstellen. Wenn `$target` null ist, wird der aktuelle HEAD direkt getaggt.

```php
$repo->createLightweightTag('v2.0.0');
$repo->createLightweightTag('hotfix-start', $repo->resolve('main~2'));
```

#### `createTag`

```php
public function createTag(string $name, string $message, ?ObjectId $target = null, ?string $tagger = null): ObjectId
```

Einen annotierten Tag erstellen. Wenn `$target` null ist, wird der aktuelle HEAD getaggt. Gibt den `ObjectId` des Tag-Objekts zurück.

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

#### `deleteTag`

```php
public function deleteTag(string $name): void
```

Einen Tag anhand seines Namens löschen.

```php
$repo->deleteTag('v1.0.0');
```

#### `packRefs`

```php
public function packRefs(): void
```

Schreibt die gemeinsame `packed-refs`-Datei anhand der aktuellen Menge loser Refs neu und entfernt die entsprechenden gemeinsamen losen Refs. Bei verknüpften Arbeitsbäumen wird in das gemeinsame Verzeichnis git geschrieben.

```php
$repo->packRefs();
```

#### `defaultBranch`

```php
public function defaultBranch(): string
```

Den Standard-Branch des Repositorys ermitteln. Prüft zuerst den Remote-HEAD-Symref, dann den lokalen HEAD und greift schließlich auf `main` oder `master` zurück.

#### `isBranchMerged`

```php
public function isBranchMerged(string $branch, ?string $target = null): bool
```

Prüfen, ob ein Branch vollständig in einen anderen Branch gemergt wurde (standardmäßig in den Standard-Branch).

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

#### `checkout`

```php
public function checkout(string $target): void
```

Branches wechseln oder HEAD loslösen. Aktualisiert HEAD, den Index und den Arbeitsbaum. `$target` ist ein Branch-Name, ein Tag, ein vollständiger Hash oder ein Revisionsausdruck; alles, was kein Branch ist, löst HEAD los.

Löst `RuntimeException` aus, wenn eine lokale Änderung (gestaged oder nicht) oder eine nicht verfolgte Datei in einem Pfad liegt, der sich zwischen dem aktuellen und dem Ziel-Tree unterscheidet.

Nicht committete Änderungen in anderen verfolgten Dateien werden nicht übernommen: Index und Arbeitsbaum werden anhand des Ziel-Trees neu erstellt. Dadurch gehen Änderungen an Dateien verloren, die auf beiden Seiten identisch sind und die Git beibehalten würde ([#2](https://github.com/inline0/pitmaster/issues/2)). Committe oder stash sie zuerst. `merge()`, `cherryPick()`, `revert()`, `rebase()` und die Bisect-Methoden erstellen den Arbeitsbaum auf dieselbe Weise neu.

```php
$repo->checkout('feature/login');
$repo->checkout($repo->resolve('v1.0.0')->hex);  // Detached HEAD
```

---

### Index

#### `index`

```php
public function index(): Index
```

Den aktuellen Index (Staging-Bereich) aus `.git/index` lesen und zurückgeben.

#### `add`

```php
public function add(string ...$paths): void
```

Dateien stagen. Liest jede Datei aus dem Arbeitsbaum, erstellt ein Blob-Objekt und aktualisiert den Index. Jedes Argument muss ein Datei- oder Symlink-Pfad relativ zum Stammverzeichnis des Arbeitsbaums sein; Verzeichnisse und Globs werden nicht erweitert, und eine fehlende Datei löst `RuntimeException` aus.

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

#### `remove`

```php
public function remove(string ...$paths): void
public function removeCached(string ...$paths): void
```

Verfolgte Pfade sowohl aus dem Index als auch aus dem Arbeitsbaum entfernen. Für ein Verhalten wie bei Git werden `--cached` sowie `-r` / `--recursive` in der Argumentliste unterstützt.

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

#### `mv`

```php
public function mv(string $source, string $destination): void
```

Eine Datei sowohl im Arbeitsbaum als auch im Index verschieben oder umbenennen.

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

---

### Commits

#### `commit`

```php
public function commit(?string $message = null, ?string $author = null): ObjectId
```

Einen Commit aus dem aktuellen Index erstellen. Erstellt aus den Indexeinträgen eine Tree-Hierarchie, erzeugt das Commit-Objekt mit HEAD als übergeordnetem Commit (falls vorhanden) und aktualisiert HEAD. Gibt den `ObjectId` des neuen Commits zurück.

`$message` darf nur `null` sein, wenn ein Merge, Cherry-Pick, Revert oder Rebase wegen eines Konflikts angehalten wurde; dann wird die vorbereitete Nachricht verwendet. Andernfalls löst eine `null`-Nachricht `RuntimeException('Commit message required')` aus. Ein Commit löst außerdem einen Fehler aus, wenn der Index nicht gemergte Pfade enthält, der Index in einem Repository ohne Commits leer ist oder sich der Tree seit HEAD nicht geändert hat.

Der Parameter `$author` ist eine vollständige git-Zeichenfolge für den Autor (`Name <email> timestamp timezone`). Ist er null, wird der Wert anhand von Umgebungsvariablen, Konstanten und `.git/config` aufgelöst, und zwar in der unter [Erste Schritte](https://pitmaster.inline0.dev/de/docs/getting-started) beschriebenen Reihenfolge.

```php
$id = $repo->commit('Fix null pointer bug');
```

#### `show`

```php
public function show(string $revision): array
```

Einen Commit-ähnlichen Verweis und seinen Diff gegenüber dem ersten übergeordneten Commit anzeigen. Wird die Revision zu einem annotierten Tag aufgelöst, enthält der Rückgabewert außerdem `['tag' => Tag]`, und `commit` ist der aufgelöste Ziel-Commit.

```php
$result = $repo->show('HEAD');
echo $result['commit']->message;

foreach ($result['diff'] as $diff) {
    echo $diff->format();
}
```

#### `reset`

```php
public function reset(string $revision, string $mode = 'mixed'): void
```

HEAD auf einen Commit zurücksetzen. Modi:
- `'soft'`: nur HEAD verschieben
- `'mixed'`: HEAD verschieben + Index zurücksetzen (Standard)
- `'hard'`: HEAD verschieben + Index zurücksetzen + Arbeitsbaum zurücksetzen

```php
$repo->reset('HEAD~1', 'soft');
$repo->reset('main', 'hard');
```

#### `restore`

```php
public function restore(string $path, ?string $source = null, bool $staged = false, bool $worktree = false): void
```

Einen Pfad aus dem Index oder einer bestimmten Quellrevision wiederherstellen. Wenn kein Flag gesetzt ist, stellt Pitmaster den Arbeitsbaum aus dem Index wieder her (oder aus `$source`, falls angegeben). `staged: true` stellt den Index aus HEAD (oder `$source`) wieder her und lässt den Arbeitsbaum unverändert; mit `staged: true, worktree: true` werden beide wiederhergestellt.

```php
$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`

```php
public function cherryPick(string $revision): ObjectId
public function cherryPickContinue(): ObjectId
public function cherryPickAbort(): void
```

Einen Commit als neuen Commit auf dem aktuellen Branch anwenden und dabei den ursprünglichen Autor beibehalten. Wenn Konflikte den Vorgang anhalten, löse sie auf, stage die Pfade und rufe dann `cherryPickContinue()` oder `cherryPickAbort()` auf.

```php
$newId = $repo->cherryPick('feature~2');

// After resolving conflicts:
$repo->add('src/File.php');
$repo->cherryPickContinue();
```

#### `revert`

```php
public function revert(string $revision): ObjectId
public function revertContinue(): ObjectId
public function revertAbort(): void
```

Einen Commit erstellen, der einen anderen Commit rückgängig macht. Wenn Konflikte den Revert anhalten, löse sie auf, stage die Pfade und rufe dann `revertContinue()` oder `revertAbort()` auf.

```php
$revertId = $repo->revert('HEAD~1');

// After resolving conflicts:
$repo->add('src/File.php');
$repo->revertContinue();
```

#### `rebase`

```php
public function rebase(string $onto): array
public function rebaseContinue(): array
public function rebaseAbort(): void
public function rebaseSkip(): array
```

Den aktuellen Branch auf eine andere Revision rebasen. Pitmaster unterstützt derzeit lineare Rebases mit einem Git-ähnlichen Unterbrechungsstatus bei Konflikten sowie fortsetzen, abbrechen und überspringen. HEAD muss auf einen Branch zeigen.

`rebase()`, `rebaseContinue()` und `rebaseSkip()` geben `['success' => bool, 'commits' => int, 'conflicts' => list<string>]` zurück: ob der Rebase abgeschlossen wurde, wie viele Commits angewendet wurden und welche Pfade beim Anhalten Konflikte verursachten.

```php
$result = $repo->rebase('main');

if (!$result['success']) {
    // Resolve conflicts, then continue or skip/abort.
    $repo->add('src/File.php');
    $repo->rebaseContinue();
}
```

---

### Status

#### `status`

```php
public function status(): array
```

Status des Arbeitsbaums ermitteln. Gibt ein Array von `StatusEntry`-Objekten zurück, die jeweils Folgendes enthalten:
- `path`: den Dateipfad
- `index`: ein `FileStatus`-Enum für Änderungen von HEAD zum Index
- `worktree`: ein `FileStatus`-Enum für Änderungen vom Index zum Arbeitsbaum
- `origPath`, `renameScore`: bei umbenannten Einträgen gesetzt, andernfalls `null`

Nicht verfolgte Dateien haben in beiden Spalten den Wert `FileStatus::Untracked`. Ignorierte Dateien werden nicht aufgeführt.

```php
foreach ($repo->status() as $entry) {
    echo $entry->shortFormat() . "\n"; // "M  src/File.php", "?? notes.txt"
}
```

#### `statusPorcelainV2`

```php
public function statusPorcelainV2(): string
```

Maschinenlesbare Statusausgabe im Porzellanformat v2 von git.

```php
echo $repo->statusPorcelainV2();
// 1 M. N... 000000 000000 000000 0000...0000 0000...0000 modified-file.txt
// ? untracked-file.txt
```

---

### Diff

#### `diff`

```php
public function diff(?string $pathspec = null, string $algorithm = 'myers'): array
```

Diff des Arbeitsbaums gegenüber dem Index (nicht gestagte Änderungen). Gibt ein Array von `DiffResult`-Objekten zurück. `$pathspec` filtert nach einem exakten Dateipfad. `$algorithm` ist `'myers'` (Standard), `'patience'`, `'histogram'` oder `'minimal'`; jeder andere Wert löst `InvalidArgumentException` aus.

```php
$diffs = $repo->diff();

foreach ($diffs as $diff) {
    echo $diff->format();  // Unified diff output
}
```

#### `diffStaged`

```php
public function diffStaged(?string $pathspec = null, string $algorithm = 'myers'): array
```

Diff des Index gegenüber HEAD (gestagte Änderungen). Gibt `DiffResult[]` zurück und erkennt dabei Umbenennungen zwischen gelöschten und hinzugefügten Dateien.

Das Argument `$pathspec` wird derzeit ignoriert, sodass alle gestagten Dateien zurückgegeben werden. Vor dem ersten Commit gibt die Methode ein leeres Array zurück ([#5](https://github.com/inline0/pitmaster/issues/5)).

```php
$staged = $repo->diffStaged();
```

#### `diffTree`

```php
public function diffTree(ObjectId $a, ObjectId $b, string $algorithm = 'myers'): array
```

Zwei Trees anhand ihrer `ObjectId` vergleichen. Gibt `DiffResult[]` zurück. Tree-IDs übergeben, keine Commit-IDs: Eine Commit-ID wird nicht als Tree gelesen und ergibt ein leeres Ergebnis.

```php
$old = $repo->readObject($repo->resolve('HEAD~1')->hex);
$new = $repo->head();
$diffs = $repo->diffTree($old->tree, $new->tree);
```

---

### Verlauf

#### `log`

```php
public function log(int $limit = 50, ?ObjectId $from = null): array
```

Den Commit-Verlauf durchlaufen, neueste zuerst, sortiert nach Committer-Zeitstempel. Gibt `Commit[]` zurück. Wenn `$from` null ist, wird bei HEAD begonnen. Wenn HEAD noch keinen Commit hat, wird ein leeres Array zurückgegeben.

```php
$commits = $repo->log(10);
```

#### `logAll`

```php
public function logAll(int $limit = 50): array
```

Commits durchlaufen, die von jeder Ref und HEAD aus erreichbar sind, neueste zuerst. Gibt `Commit[]` zurück.

#### `logOneline`

```php
public function logOneline(int $limit = 50, bool $all = false, ?string $path = null): array
```

Protokollzeilen im Format `git log --oneline` (`abc1234 Subject`) ausgeben, ausgehend von HEAD, von allen Refs (`$all`) oder nach Pfad gefiltert.

#### `logPath`

```php
public function logPath(string $path, int $limit = 50): array
```

Verlauf nach einem Dateipfad filtern. Gibt nur Commits zurück, deren Diff gegenüber dem ersten übergeordneten Commit genau diese Datei hinzufügt, ändert, löscht oder umbenennt. Verzeichnispfade stimmen nicht überein.

Es werden nur die neuesten `5 * $limit` Commits geprüft. Ältere Commits, die die Datei betreffen, können daher im Ergebnis fehlen ([#7](https://github.com/inline0/pitmaster/issues/7)).

```php
$commits = $repo->logPath('src/Repository.php', 20);
```

---

### Merge

#### `merge`

```php
public function merge(string $branch, string $strategy = 'recursive'): MergeResult
public function mergeOctopus(array $branches): MergeResult
public function mergeContinue(): ObjectId
public function mergeAbort(): void
```

Einen Branch (oder eine beliebige Revision) in HEAD mergen. Erkennt Fast-Forward-Merges automatisch. Bei anderen Merges werden Konflikte auf Tree-Ebene erkannt und bei Erfolg wird ein Merge-Commit erstellt. Bei Konflikten wird der Vorgang angehalten und ein Git-ähnlicher Merge-Status in Index und Arbeitsbaum geschrieben. Löse die Konflikte auf, stage die Pfade und rufe dann `mergeContinue()` oder `mergeAbort()` auf.

`$strategy` `'ours'` erstellt einen Merge-Commit, der den Tree von HEAD beibehält; jeder andere Wert verwendet den standardmäßigen rekursiven Merge. `mergeOctopus()` führt zwei oder mehr Branches in einem Commit zusammen. Bei Konflikten in einem Branch wird ein nicht sauberes Ergebnis zurückgegeben, ohne einen Merge-Status zu schreiben oder einen Commit zu erstellen.

Wird ein Branch gemergt, der bereits in HEAD enthalten ist, entsteht ein weiterer Merge-Commit, statt „Already up to date“ zu melden ([#6](https://github.com/inline0/pitmaster/issues/6)). Nicht committete Änderungen in verfolgten Dateien werden wie unter `checkout()` beschrieben überschrieben.

Gibt ein `MergeResult` zurück mit:
- `clean`: ob der Merge ohne Konflikte erfolgreich war
- `commitId`: die ID des Merge-Commits (bei sauberem Ergebnis)
- `conflictPaths`: Array der Pfade zu Dateien mit Konflikten (bei nicht sauberem Ergebnis)
- `mergedContents`: Zuordnung von Konfliktpfaden zu Dateiinhalten mit Konfliktmarkierungen (bei nicht sauberem Ergebnis)

```php
$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`

```php
public function mergeBase(ObjectId $a, ObjectId $b): ?ObjectId
```

Die Merge-Basis (den niedrigsten gemeinsamen Vorfahren) zweier Commits finden. Gibt null zurück, wenn kein gemeinsamer Vorfahr existiert.

```php
$base = $repo->mergeBase(
    $repo->resolve('main'),
    $repo->resolve('feature/login'),
);
```

### Bisect

```php
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
```

Binär nach dem Commit suchen, der eine Änderung eingeführt hat. `bisectStart()` speichert die fehlerhaften und guten Revisionen, checkt den ersten Kandidaten aus und gibt ihn zurück. `bisectGood()` und `bisectBad()` markieren den aktuellen HEAD (oder `$revision`), checken den nächsten Kandidaten aus und geben ihn zurück. Bleibt nur ein Verdächtiger übrig, wird dieser Commit zurückgegeben und bleibt der Kandidat: Er ist der erste fehlerhafte Commit. `bisectReset()` beendet die Sitzung und kehrt zum Branch oder Commit zurück, an dem sie begonnen hat.

```php
$candidate = $repo->bisectStart('HEAD', 'v1.0.0');
// test $candidate ...
$candidate = $repo->bisectBad();
$candidate = $repo->bisectGood();
$repo->bisectReset();
```

---

### Netzwerk

#### `fetch`

```php
public function fetch(string $remote = 'origin', ?int $depth = null): void
```

Von einem Remote abrufen. Lädt neue Objekte herunter und aktualisiert Remote-Tracking-Refs. `$depth` übergeben, um einen flachen Verlauf zu vertiefen oder zu begrenzen.

`fetch()` und die Methoden `push*()` lösen bei einem Handle, das mit `['processes' => false]` geöffnet wurde, `RuntimeException` aus.

```php
$repo->fetch();
$repo->fetch('upstream');
```

#### `push`

```php
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
```

Den aktuellen oder einen angegebenen Branch zu einem Remote pushen. Varianten mit Force-with-lease, atomar und als Spiegelung sind für strengere oder umfassendere Ref-Aktualisierungen verfügbar.

```php
$repo->push();
$repo->push('origin', 'feature/login');
$repo->pushForceWithLease('origin', 'main', $repo->resolve('refs/remotes/origin/main'));
$repo->pushAtomic('origin', ['main', 'release']);
$repo->pushMirror();
```

---

### Arbeitsbäume

#### `addWorktree`

```php
public function addWorktree(string $path, string $branch, ?ObjectId $from = null, ?string $name = null): Worktree
```

Einen verknüpften Arbeitsbaum hinzufügen. Erstellt den Branch, falls er nicht existiert, richtet die Metadaten des Arbeitsbaums ein und erstellt die Arbeitsbaumdateien. `$name` übergeben, wenn du einen deterministischen Metadaten-Slug brauchst, der sich vom Basisnamen des Checkout-Verzeichnisses unterscheidet.

```php
$wt = $repo->addWorktree('/tmp/review', 'feature/review');
$wt = $repo->addWorktree('/tmp/checkouts/review', 'feature/review', name: 'review-wt');
```

#### `removeWorktree`

```php
public function removeWorktree(string $pathOrName, bool $force = false): void
```

Einen verknüpften Arbeitsbaum entfernen. Mit `$force = true` auch dann entfernen, wenn er gesperrt ist.

#### `worktrees`

```php
public function worktrees(): array
```

Alle Arbeitsbäume auflisten (Haupt- und verknüpfte). Gibt `Worktree[]` zurück.

```php
foreach ($repo->worktrees() as $wt) {
    echo "{$wt->path} [{$wt->branch}]\n";
}
```

---

### Konfiguration

#### `config`

```php
public function config(): GitConfig
```

Greift auf die Repository-Konfiguration von git zu. Liest und schreibt Konfigurationswerte.

```php
$config = $repo->config();
$name = $config->get('user.name');
$config->set('user.email', 'new@example.com');
```

---

### Interner Zugriff

Diese Methoden legen interne Komponenten für fortgeschrittene Anwendungsfälle offen.

#### `objectDatabase`

```php
public function objectDatabase(): ObjectDatabase
```

Auf die rohe Objektdatenbank zugreifen (lose und gepackte Speicher).

#### `refDatabase`

```php
public function refDatabase(): RefDatabase
```

Auf die rohe Ref-Datenbank zugreifen (lose und gepackte Refs).

---

## Klassen auf niedrigerer Ebene

Diese Klassen werden nicht von `Repository` umschlossen und aus dessen internen Komponenten erstellt.

### `Pitmaster\Graph\Blame`

```php
public function blame(ObjectId $headId, string $path): array
```

Gibt eine Zeile pro Eintrag aus `$path` unter `$headId` zurück: `['line' => int, 'hash' => string, 'author' => string, 'content' => string]`. `author` ist die vollständige Identitätszeichenfolge (`Name <email> timestamp timezone`).

```php
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`

```php
public function grep(ObjectId $treeId, string $pattern, string $prefix = '', array $options = []): array
```

Durchsucht die Blobs eines Trees. Gibt `['path' => string, 'line' => int, 'content' => string]` pro passender Zeile zurück. Optionen: `regex` (behandelt das Muster als PCRE-Muster) und `ignore_case`. Ein ungültiger regulärer Ausdruck löst `InvalidArgumentException` aus.

```php
use Pitmaster\Graph\Grep;

$grep = new Grep($repo->objectDatabase());
$matches = $grep->grep($repo->head()->tree, 'TODO', '', ['ignore_case' => true]);
```

### `Pitmaster\Ref\Notes`

```php
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
```

Liest und schreibt git-Notizen, die unter einer Notes-Ref gespeichert sind. `listAll()` liefert die Zuordnung von Commit-Hashes zu Notizinhalten zurück.

```php
use Pitmaster\Ref\Notes;

$notes = new Notes($repo->objectDatabase(), $repo->refDatabase());
$notes->set($repo->resolve('HEAD'), "Reviewed\n");
echo $notes->get($repo->resolve('HEAD'));
```
