
# 克隆、获取、推送

Pitmaster 支持 git 的三种基本网络操作：克隆（首次下载）、获取（增量下载）和推送（上传）。SSH URL（`ssh://...`、`user@host:path`）使用 SSH 传输方式；其他 URL 使用 smart HTTP。如果 smart 交换失败，克隆和获取会回退到 dumb HTTP（参见[协议](https://pitmaster.inline0.dev/zh/docs/network/protocols)）。

这三种操作都会启动网络传输，因此在使用 `['processes' => false]` 或 `Pitmaster::processFreeOptions()` 打开的句柄上会抛出 `RuntimeException`。

## 克隆

```php
use Pitmaster\Pitmaster;

// public static function clone(string $url, string $path, ?int $depth = null, array $options = []): Repository
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/path/to/destination');
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/path/to/shallow', 1);
```

新仓库始终使用 SHA-1。如果克隆失败，且目标目录原先不存在，则会删除已部分创建的目录。

### 克隆流程

1. **初始化**：在目标路径创建新的空仓库
2. **配置远程仓库**：将 URL 保存为 `remote.origin.url`，并设置获取 refspec
3. **发现 refs**：使用 `GET /info/refs?service=git-upload-pack` 查询服务器上的内容
4. **协商**：为所有远程 refs 发送 `want` 行（按哈希去重）
5. **接收 pack**：下载包含所有请求对象的 pack 文件
6. **写入 pack**：将 pack 文件保存到 `.git/objects/pack/`
7. **建立 pack 索引**：生成 `.idx` 文件，以便快速查找
8. **设置 refs**：创建远程跟踪分支（`refs/remotes/origin/*`）和标签
9. **设置 HEAD**：将 HEAD 指向默认分支（取自服务器的 `symref=HEAD:...` 能力；若服务器未发送该能力，则使用 `refs/heads/main`），并设置 `branch.<name>.remote` 和 `branch.<name>.merge`
10. **检出**：检出该分支，将工作树和索引写入磁盘

最终检出期间发生的错误会被捕获并忽略，因此克隆完成时，工作树可能为空或不完整（[#19](https://github.com/inline0/pitmaster/issues/19)）。

```php
$repo = Pitmaster::clone($url, $path);
$head = $repo->head();
echo $head->message;

$branches = $repo->branches();
$files = $repo->status();
```

### 工作树物化

收到 pack 后，Pitmaster 会为默认分支调用 `$repo->checkout()`。此操作会将 HEAD 提交树中的所有文件写入工作目录，并根据树条目构建索引，因此 `status()` 会报告工作树干净。

## 获取

```php
// public function fetch(string $remote = 'origin', ?int $depth = null): void
$repo->fetch();              // Fetch from 'origin'
$repo->fetch('upstream');    // Fetch from a named remote
$repo->fetch('origin', 1);   // Shallow fetch
```

### 获取流程

1. **读取远程 URL**：从 `.git/config` 读取（`remote.<name>.url`）
2. **发现 refs**：获取所有远程 refs 的当前状态
3. **协商 wants/haves**：
   - Want：由 `remote.<name>.fetch` refspec 匹配的远程 refs（默认为 `+refs/heads/*:refs/remotes/<name>/*`），且其对象在本地缺失（指定深度时则包含所有对象）
   - Have：每个本地 ref 的顶端提交
4. **接收 pack**：仅下载新对象
5. **写入 pack**：保存到 `.git/objects/pack/` 并建立索引
6. **更新跟踪 refs**：为每个匹配的远程分支更新 `refs/remotes/<remote>/<branch>`，并写入所有公布的标签 ref。标签对象不会单独请求，因此新建的附注标签可能会被写成指向未下载对象的 ref（[#24](https://github.com/inline0/pitmaster/issues/24)）

```php
// Fetch is incremental: only downloads what's new
$repo->fetch();

// After fetching, merge or rebase to incorporate changes (use the full ref name)
$result = $repo->merge('refs/remotes/origin/main');
```

`resolve()` 会按 `refs/heads/<name>`、`refs/tags/<name>` 和名称本身的顺序查找名称，因此无法找到简写形式 `origin/main`。

Pitmaster 会请求尚无法为其建立索引的 thin pack（[#16](https://github.com/inline0/pitmaster/issues/16)）。如果服务器在增量获取时返回 thin pack，`fetch()` 就会抛出 `PackParseException`。

### 已是最新

如果远程 refs 指向的所有对象都已存在于本地，则不会请求 pack；发现 refs 后，`fetch()` 就会返回。在这种情况下，即使远程分支已移动到你本地已有的提交，跟踪 refs 也不会更新（[#23](https://github.com/inline0/pitmaster/issues/23)）。

## 推送

```php
$repo->push();                        // Push current branch to 'origin'
$repo->push('origin', 'main');        // Push a specific branch
$repo->push('upstream', 'feature');   // Push to a named remote
```

### 推送流程

1. **读取远程 URL**：从 `.git/config` 读取
2. **确定推送内容**：将本地分支解析为其提交哈希
3. **发现远程 refs**：获取远程分支的当前状态（以计算旧哈希）
4. **检查快进更新**：如果远程分支不是本地分支的祖先，则在发送任何内容前抛出 `RuntimeException("Push rejected: non-fast-forward update to <branch>")`
5. **发送 ref 更新**：`<old-hash> <new-hash> refs/heads/<branch>`
6. **发送 pack 数据**：打包远程仓库所需的对象
7. **检查结果**：服务器会针对每个 ref 返回成功或失败

除非禁用钩子，否则发送更新前会运行 `pre-push` 钩子。

```php
// Push the current branch
$repo->push();

// Push a specific branch to a specific remote
$repo->push('origin', 'feature/login');
```

### 推送要求

- 当前分支必须存在于本地
- 必须为远程仓库配置 URL
- 远程仓库必须接受推送（身份验证、权限、非快进策略）

### 强制推送

Pitmaster 会发送远程实时 ref 公告中的旧哈希，而 `push()` 会在本地拒绝非快进更新。若要覆盖远程分支，请使用 force-with-lease：

```php
// Lease defaults to refs/remotes/<remote>/<branch>; pass an ObjectId to set it explicitly
$repo->pushForceWithLease('origin', 'main');
$repo->pushForceWithLease('origin', 'main', $repo->resolve('refs/remotes/origin/main'));

// Update several branches in one atomic request
$repo->pushAtomic('origin', ['main', 'release']);

// Make the remote mirror local refs (also deletes remote refs that no longer exist locally)
$repo->pushMirror('origin');
```

## UploadPackClient（获取内部实现）

```php
use Pitmaster\Protocol\UploadPackClient;
use Pitmaster\Protocol\SmartHttpClient;

$http = new SmartHttpClient();
$uploadPack = new UploadPackClient($http);

// Fetch with explicit wants and haves
$packData = $uploadPack->fetch($url, $wants, $haves);
```

wants/haves 协商会构建一个 pkt-line 请求：

```
want <hash1> <capabilities>\n
want <hash2>\n
deepen <depth>\n        (only when a depth is given)
0000
have <hash3>\n
have <hash4>\n
done\n
```

服务器先返回确认信息，然后返回 pack 数据（可能会使用 side-band 封装）。

## ReceivePackClient（推送内部实现）

```php
use Pitmaster\Protocol\ReceivePackClient;
use Pitmaster\Protocol\SmartHttpClient;

$http = new SmartHttpClient();
$receivePack = new ReceivePackClient($http);

$receivePack->push($url, [
    [
        'old' => $oldId,      // Current remote hash (or zero hash for new branch)
        'new' => $localId,    // Local commit hash to push
        'ref' => 'refs/heads/main',
    ],
], $packData);
```

推送请求格式：

```
<old-hash> <new-hash> refs/heads/main\0<capabilities>\n
0000
PACK<pack data>
```

## 错误处理

```php
use Pitmaster\Exceptions\ProtocolException;

try {
    $repo->fetch();
} catch (ProtocolException $e) {
    // Network unreachable, auth failed, server error, etc.
    echo $e->getMessage();
}

try {
    $repo->push();
} catch (\RuntimeException $e) {
    // Remote not configured, not on a branch, branch not found, non-fast-forward
    echo $e->getMessage();
}
```

`fetch()` 也可能抛出普通的 `RuntimeException`（远程仓库未知或句柄未启动进程）或 `PackParseException`（无法建立索引的 pack）。`ProtocolException` 和 `PackParseException` 均继承自 `\RuntimeException`，因此捕获 `\RuntimeException` 即可处理所有这些异常。

## 远程仓库配置

远程仓库存储在 `.git/config` 中：

```ini
[remote "origin"]
    url = https://github.com/user/repo.git
    fetch = +refs/heads/*:refs/remotes/origin/*
```

通过配置 API 访问：

```php
$config = $repo->config();
$url = $config->get('remote.origin.url');
$config->set('remote.upstream.url', 'https://github.com/other/repo.git');
$config->set('remote.upstream.fetch', '+refs/heads/*:refs/remotes/upstream/*');

// set() changes the in-memory config only; write it to persist the change
$config->writeToFile($repo->commonGitDir() . '/config');
```

## 推送的 pack 数据

目前，Pitmaster 会收集待推送提交可达、但远程仓库当前 refs 不可达的对象，并将它们作为基础对象（不进行 delta 压缩）写入 pack。此实现功能正确，但生成的 pack 比 `git push` 更大。计划在之后优化 pack 写入时的 delta 压缩。
