跳转到正文

克隆、获取、推送

Pitmaster 支持 git 的三种基本网络操作:克隆(首次下载)、获取(增量下载)和推送(上传)。SSH URL(ssh://...、user@host:path)使用 SSH 传输方式;其他 URL 使用 smart HTTP。如果 smart 交换失败,克隆和获取会回退到 dumb HTTP(参见协议)。

这三种操作都会启动网络传输,因此在使用 ['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)。

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)
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)。如果服务器在增量获取时返回 thin pack,fetch() 就会抛出 PackParseException。

已是最新

如果远程 refs 指向的所有对象都已存在于本地,则不会请求 pack;发现 refs 后,fetch() 就会返回。在这种情况下,即使远程分支已移动到你本地已有的提交,跟踪 refs 也不会更新(#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 压缩。