克隆、获取、推送
Pitmaster 支持 git 的三种基本网络操作:克隆(首次下载)、获取(增量下载)和推送(上传)。SSH URL(ssh://...、user@host:path)使用 SSH 传输方式;其他 URL 使用 smart HTTP。如果 smart 交换失败,克隆和获取会回退到 dumb HTTP(参见协议)。
这三种操作都会启动网络传输,因此在使用 ['processes' => false] 或 Pitmaster::processFreeOptions() 打开的句柄上会抛出 RuntimeException。
克隆
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。如果克隆失败,且目标目录原先不存在,则会删除已部分创建的目录。
克隆流程
- 初始化:在目标路径创建新的空仓库
- 配置远程仓库:将 URL 保存为
remote.origin.url,并设置获取 refspec - 发现 refs:使用
GET /info/refs?service=git-upload-pack查询服务器上的内容 - 协商:为所有远程 refs 发送
want行(按哈希去重) - 接收 pack:下载包含所有请求对象的 pack 文件
- 写入 pack:将 pack 文件保存到
.git/objects/pack/ - 建立 pack 索引:生成
.idx文件,以便快速查找 - 设置 refs:创建远程跟踪分支(
refs/remotes/origin/*)和标签 - 设置 HEAD:将 HEAD 指向默认分支(取自服务器的
symref=HEAD:...能力;若服务器未发送该能力,则使用refs/heads/main),并设置branch.<name>.remote和branch.<name>.merge - 检出:检出该分支,将工作树和索引写入磁盘
最终检出期间发生的错误会被捕获并忽略,因此克隆完成时,工作树可能为空或不完整(#19)。
$repo = Pitmaster::clone($url, $path);
$head = $repo->head();
echo $head->message;
$branches = $repo->branches();
$files = $repo->status();
工作树物化
收到 pack 后,Pitmaster 会为默认分支调用 $repo->checkout()。此操作会将 HEAD 提交树中的所有文件写入工作目录,并根据树条目构建索引,因此 status() 会报告工作树干净。
获取
// 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
获取流程
- 读取远程 URL:从
.git/config读取(remote.<name>.url) - 发现 refs:获取所有远程 refs 的当前状态
- 协商 wants/haves:
- Want:由
remote.<name>.fetchrefspec 匹配的远程 refs(默认为+refs/heads/*:refs/remotes/<name>/*),且其对象在本地缺失(指定深度时则包含所有对象) - Have:每个本地 ref 的顶端提交
- Want:由
- 接收 pack:仅下载新对象
- 写入 pack:保存到
.git/objects/pack/并建立索引 - 更新跟踪 refs:为每个匹配的远程分支更新
refs/remotes/<remote>/<branch>,并写入所有公布的标签 ref。标签对象不会单独请求,因此新建的附注标签可能会被写成指向未下载对象的 ref(#24)
// 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)。
推送
$repo->push(); // Push current branch to 'origin'
$repo->push('origin', 'main'); // Push a specific branch
$repo->push('upstream', 'feature'); // Push to a named remote
推送流程
- 读取远程 URL:从
.git/config读取 - 确定推送内容:将本地分支解析为其提交哈希
- 发现远程 refs:获取远程分支的当前状态(以计算旧哈希)
- 检查快进更新:如果远程分支不是本地分支的祖先,则在发送任何内容前抛出
RuntimeException("Push rejected: non-fast-forward update to <branch>") - 发送 ref 更新:
<old-hash> <new-hash> refs/heads/<branch> - 发送 pack 数据:打包远程仓库所需的对象
- 检查结果:服务器会针对每个 ref 返回成功或失败
除非禁用钩子,否则发送更新前会运行 pre-push 钩子。
// 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:
// 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(获取内部实现)
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(推送内部实现)
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>
错误处理
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 中:
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
通过配置 API 访问:
$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 压缩。