| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Test; |
| 39 | |
| 40 | require_once __DIR__.'/../../../autoload.php'; |
| 41 | |
| 42 | use Std\Lib\Tester; |
| 43 | use Std\Lib\Git\Blame; |
| 44 | use Std\Lib\Git\Commit; |
| 45 | use Std\Lib\Git\Config; |
| 46 | use Std\Lib\Git\Delta; |
| 47 | use Std\Lib\Git\Diff; |
| 48 | use Std\Lib\Git\GitException; |
| 49 | use Std\Lib\Git\Maintenance; |
| 50 | use Std\Lib\Git\Merge; |
| 51 | use Std\Lib\Git\PackIndexer; |
| 52 | use Std\Lib\Git\PackWriter; |
| 53 | use Std\Lib\Git\Person; |
| 54 | use Std\Lib\Git\PktLine; |
| 55 | use Std\Lib\Git\ReceivePack; |
| 56 | use Std\Lib\Git\Refs; |
| 57 | use Std\Lib\Git\Repository; |
| 58 | use Std\Lib\Git\Ssh; |
| 59 | use Std\Lib\Git\Tag; |
| 60 | use Std\Lib\Git\Tree; |
| 61 | use Std\Lib\Git\UploadPack; |
| 62 | use Std\Lib\Git\Walk; |
| 63 | |
| 64 | $test = new Tester([ |
| 65 | 'colorOutput' => true, |
| 66 | 'verboseOutput' => true |
| 67 | ]); |
| 68 | |
| 69 | $tmp = sys_get_temp_dir().'/lbgit_'.bin2hex(random_bytes(6)); |
| 70 | mkdir($tmp, 0700); |
| 71 | $rmTree = function(string $dir) use (&$rmTree): void { |
| 72 | foreach (@scandir($dir) ?: [] as $f) { |
| 73 | if ($f === '.' || $f === '..') continue; |
| 74 | $p = "{$dir}/{$f}"; |
| 75 | if (is_dir($p) && !is_link($p)) $rmTree($p); |
| 76 | else { |
| 77 | @chmod($p, 0666); |
| 78 | @unlink($p); |
| 79 | } |
| 80 | } |
| 81 | @rmdir($dir); |
| 82 | }; |
| 83 | register_shutdown_function(fn() => $rmTree($tmp)); |
| 84 | |
| 85 | // 本物のgitで作ったリポジトリ(SHA-1とSHA-256)と、gitが出した結果 |
| 86 | $fixtures = json_decode((string)file_get_contents(__DIR__.'/Helper/git.json'), true); |
| 87 | $extract = function(string $fmt, string $name) use ($fixtures, $tmp): string { |
| 88 | $dir = "{$tmp}/{$name}"; |
| 89 | foreach ($fixtures[$fmt]['files'] as $rel => $b64) { |
| 90 | @mkdir(dirname("{$dir}/{$rel}"), 0777, true); |
| 91 | file_put_contents("{$dir}/{$rel}", base64_decode($b64)); |
| 92 | } |
| 93 | foreach (['refs/heads', 'refs/tags'] as $d) @mkdir("{$dir}/{$d}", 0777, true); |
| 94 | return $dir; |
| 95 | }; |
| 96 | $catch = function(callable $fn): ?\Throwable { |
| 97 | try { |
| 98 | $fn(); |
| 99 | } catch (\Throwable $e) { |
| 100 | return $e; |
| 101 | } |
| 102 | return null; |
| 103 | }; |
| 104 | $me = new Person('諏訪子', 'suwako@example.com', 1700000000, '+0900'); |
| 105 | // 本物のgit(任意) |
| 106 | $git = null; |
| 107 | if (function_exists('proc_open') && function_exists('shell_exec')) { |
| 108 | $v = @shell_exec('git --version 2>&1'); |
| 109 | if (is_string($v) && str_starts_with($v, 'git version')) $git = 'git'; |
| 110 | } |
| 111 | $run = function(array $cmd, ?string $cwd = null, array $env = []) use ($tmp): array { |
| 112 | $p = proc_open($cmd, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $cwd, |
| 113 | $env + ['GIT_CONFIG_GLOBAL' => $tmp.'/gitconfig', 'GIT_CONFIG_NOSYSTEM' => '1', 'HOME' => $tmp, 'PATH' => (string)getenv('PATH'), |
| 114 | 'GIT_TERMINAL_PROMPT' => '0', 'SystemRoot' => (string)getenv('SystemRoot')]); |
| 115 | fclose($pipes[0]); |
| 116 | $out = stream_get_contents($pipes[1]); |
| 117 | $err = stream_get_contents($pipes[2]); |
| 118 | fclose($pipes[1]); |
| 119 | fclose($pipes[2]); |
| 120 | return [proc_close($p), (string)$out, (string)$err]; |
| 121 | }; |
| 122 | file_put_contents($tmp.'/gitconfig', "[user]\n\tname = テスト\n\temail = test@example.com\n[init]\n\tdefaultBranch = main\n[commit]\n\tgpgsign = false\n"); |
| 123 | |
| 124 | // プロトコルのテスト用 |
| 125 | $pkts = function(array $lines): string { |
| 126 | $s = ''; |
| 127 | foreach ($lines as $l) $s .= $l === 0 ? '0000' : ($l === 1 ? '0001' : PktLine::encode($l)); |
| 128 | return $s; |
| 129 | }; |
| 130 | $stream = function(string $s) { |
| 131 | $f = fopen('php://temp', 'w+b'); |
| 132 | fwrite($f, $s); |
| 133 | rewind($f); |
| 134 | return $f; |
| 135 | }; |
| 136 | /** 応答を行に分ける(サイドバンドの1はパック、2は進捗) */ |
| 137 | $parse = function($out, bool $sideband = true): array { |
| 138 | rewind($out); |
| 139 | $pkt = new PktLine($out); |
| 140 | $lines = []; |
| 141 | $pack = ''; |
| 142 | $progress = ''; |
| 143 | while (($l = $pkt->read(true)) !== null) { |
| 144 | if (is_int($l)) { |
| 145 | $lines[] = $l; |
| 146 | continue; |
| 147 | } |
| 148 | if (str_starts_with($l, 'PACK')) { |
| 149 | $pack = $l.stream_get_contents($out); |
| 150 | break; |
| 151 | } |
| 152 | if ($sideband && $pack === '' && strlen($l) > 0 && in_array(ord($l[0]), [1, 2, 3], true) && !preg_match('/^[\x20-\x7e]/', $l)) { |
| 153 | if ($l[0] === "\1") $pack .= substr($l, 1); |
| 154 | else if ($l[0] === "\2") $progress .= substr($l, 1); |
| 155 | else $lines[] = 'ERROR '.substr($l, 1); |
| 156 | continue; |
| 157 | } |
| 158 | if ($sideband && $l !== '' && $l[0] === "\1") { |
| 159 | $pack .= substr($l, 1); |
| 160 | continue; |
| 161 | } |
| 162 | $lines[] = rtrim($l, "\n"); |
| 163 | } |
| 164 | return ['lines' => $lines, 'pack' => $pack, 'progress' => $progress]; |
| 165 | }; |
| 166 | /** 新しいリポジトリにパックを入れる */ |
| 167 | $unpack = function(Repository $repo, string $pack): array { |
| 168 | $f = fopen('php://temp', 'w+b'); |
| 169 | fwrite($f, $pack); |
| 170 | rewind($f); |
| 171 | return (new PackIndexer($repo))->receive($f, 0); |
| 172 | }; |
| 173 | |
| 174 | |
| 175 | $test->describe('Git(設定とオブジェクト)', function($test) use ($me): void { |
| 176 | $test->it('設定ファイルを読み書き出来るはず', function($test): void { |
| 177 | $c = Config::fromString("# コメント\n[core]\n\tbare = true\n\tFileMode=false ; コメント\n[remote \"origin\"]\n\turl = \"https://x/a b\" # c\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n" |
| 178 | ."[Section \"Sub \\\"q\\\"\"]\n\tkey = a \\\n b\n\tflag\n[pack]\n\twindowMemory = 2k\n"); |
| 179 | $test->assertTrue($c->getBool('core.bare')); |
| 180 | $test->assertFalse($c->getBool('core.filemode', true)); |
| 181 | $test->assertSame('https://x/a b', $c->get('remote.origin.url')); |
| 182 | $test->assertSame('+refs/heads/*:refs/remotes/origin/*', $c->get('Remote.origin.Fetch')); |
| 183 | $test->assertNull($c->get('remote.Origin.url'), 'サブセクションは大文字・小文字を区別する'); |
| 184 | $test->assertSame('a b', $c->get('section.Sub "q".key')); |
| 185 | $test->assertTrue($c->getBool('section.Sub "q".flag')); |
| 186 | $test->assertSame(2048, $c->getInt('pack.windowmemory')); |
| 187 | $c->set('user.name', ' 前後に空白 '); |
| 188 | $c->set('core.bare', null); |
| 189 | $back = Config::fromString($c->toString()); |
| 190 | $test->assertSame(' 前後に空白 ', $back->get('user.name')); |
| 191 | $test->assertNull($back->get('core.bare')); |
| 192 | $test->assertSame('https://x/a b', $back->get('remote.origin.url')); |
| 193 | }); |
| 194 | |
| 195 | $test->it('コミット・タグ・作者を解析して元に戻せるはず', function($test): void { |
| 196 | $raw = "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\nparent 1111111111111111111111111111111111111111\nparent 2222222222222222222222222222222222222222\n" |
| 197 | ."author 諏訪子 <suwako@example.com> 1700000000 +0900\ncommitter Kanako <k@example.com> 1700000100 -0130\n" |
| 198 | ."gpgsig -----BEGIN SSH SIGNATURE-----\n U1NIU0lH\n \n -----END SSH SIGNATURE-----\n\n件名\n\n本文\n"; |
| 199 | $c = Commit::parse($raw); |
| 200 | $test->assertSame(['1111111111111111111111111111111111111111', '2222222222222222222222222222222222222222'], $c->parents); |
| 201 | $test->assertSame('諏訪子', $c->author->name); |
| 202 | $test->assertSame(1700000100, $c->committer->time); |
| 203 | $test->assertSame('-0130', $c->committer->tz); |
| 204 | $test->assertSame('2023-11-14 20:45:00', $c->committer->date()->format('Y-m-d H:i:s')); |
| 205 | $test->assertSame('件名', $c->subject()); |
| 206 | $test->assertSame("-----BEGIN SSH SIGNATURE-----\nU1NIU0lH\n\n-----END SSH SIGNATURE-----", $c->header('gpgsig')); |
| 207 | $test->assertSame($raw, $c->toString(), '同じバイト列に戻る'); |
| 208 | $t = Tag::parse("object 1111111111111111111111111111111111111111\ntype commit\ntag v1.0\ntagger 諏訪子 <s@x> 1 +0000\n\nリリース\n"); |
| 209 | $test->assertSame(['v1.0', 'commit', 'リリース'], [$t->tag, $t->type, rtrim($t->message)]); |
| 210 | $p = Person::parse('名前だけ <> 0 +0000'); |
| 211 | $test->assertSame(['名前だけ', ''], [$p->name, $p->email]); |
| 212 | }); |
| 213 | |
| 214 | $test->it('ツリーはGitの順番に並べ、不正な名前を拒否するはず', function($test) use ($me): void { |
| 215 | $id = str_repeat('a', 40); |
| 216 | $t = Tree::create([[Tree::FILE, 'a.b', $id], [Tree::DIR, 'a', $id], [Tree::FILE, 'a-', $id], [Tree::FILE, 'B', $id]]); |
| 217 | // ディレクトリ「a」は「a/」として並ぶ(「a-」「a.b」の後) |
| 218 | $test->assertSame(['B', 'a-', 'a.b', 'a'], array_column($t->entries, 1)); |
| 219 | $test->assertSame($t->entries, Tree::parse($t->toString(), 20)->entries); |
| 220 | foreach (['', '.', '..', 'a/b', "a\0b"] as $bad) { |
| 221 | $test->assertThrows(fn() => Tree::create([[Tree::FILE, $bad, $id]]), GitException::class); |
| 222 | } |
| 223 | $test->assertThrows(fn() => Tree::create([[Tree::FILE, 'x', $id], [Tree::EXEC, 'x', $id]]), GitException::class); |
| 224 | }); |
| 225 | |
| 226 | $test->it('差分を当てられるはず', function($test): void { |
| 227 | $base = str_repeat('0123456789', 100); |
| 228 | // 元の大きさ、結果の大きさ、コピー(オフセット10、長さ20)、挿入(3バイト) |
| 229 | $delta = chr(232).chr(7).chr(23).chr(0x80 | 0x01 | 0x10).chr(10).chr(20).chr(3).'XYZ'; |
| 230 | $test->assertSame('01234567890123456789XYZ', Delta::apply($base, $delta)); |
| 231 | $test->assertSame(23, Delta::targetSize($delta)); |
| 232 | $test->assertThrows(fn() => Delta::apply('short', $delta), GitException::class); |
| 233 | }); |
| 234 | |
| 235 | $test->it('参照の名前を確かめるはず', function($test): void { |
| 236 | foreach (['refs/heads/main', 'refs/tags/v1.0', 'refs/heads/feature/日本語', 'HEAD'] as $ok) $test->assertTrue(Refs::validName($ok), $ok); |
| 237 | foreach (['main', 'refs/heads/a..b', 'refs/heads/a.lock', 'refs/heads/.x', 'refs/heads/a b', 'refs/heads/a~1', 'refs/heads/a:b', 'refs/heads/', 'refs/heads//a', 'refs/heads/a@{1}', "refs/heads/\x7f"] as $bad) { |
| 238 | $test->assertFalse(Refs::validName($bad), $bad); |
| 239 | } |
| 240 | }); |
| 241 | }); |
| 242 | |
| 243 | foreach (['sha1', 'sha256'] as $fmt) { |
| 244 | $test->describe("Git(本物のリポジトリを読む、{$fmt})", function($test) use ($fmt, $fixtures, $extract): void { |
| 245 | $dir = $extract($fmt, "read-{$fmt}"); |
| 246 | $e = $fixtures[$fmt]['expect']; |
| 247 | $repo = Repository::open($dir); |
| 248 | |
| 249 | $test->it('参照と履歴を読めるはず', function($test) use ($repo, $e, $fmt): void { |
| 250 | $test->assertSame($fmt, $repo->hashAlgo); |
| 251 | $test->assertSame('refs/heads/main', $repo->headRef()); |
| 252 | $test->assertSame($e['refs'], $repo->refs->all()); |
| 253 | $test->assertSame(array_column($e['log'], 0), array_map(fn($c) => $c->id, iterator_to_array($repo->log('main'), false))); |
| 254 | $test->assertSame(array_column($e['log'], 1), array_map(fn($c) => $c->subject(), iterator_to_array($repo->log('main'), false))); |
| 255 | $test->assertSame($e['logPath'], array_map(fn($c) => $c->id, iterator_to_array($repo->log('main', ['path' => 'a.txt']), false))); |
| 256 | $test->assertSame(array_slice(array_column($e['log'], 0), 1, 2), array_map(fn($c) => $c->id, iterator_to_array($repo->log('main', ['skip' => 1, 'limit' => 2]), false))); |
| 257 | $test->assertSame([$e['log'][0][0]], array_map(fn($c) => $c->id, iterator_to_array($repo->log('main', ['not' => 'main~1']), false))); |
| 258 | }); |
| 259 | |
| 260 | $test->it('リビジョンを解決出来るはず', function($test) use ($repo, $e): void { |
| 261 | foreach ($e['revs'] as $rev => $id) $test->assertSame($id, $repo->resolve($rev), $rev); |
| 262 | $test->assertSame($e['revs']['HEAD'], $repo->resolve(substr($e['revs']['HEAD'], 0, 7)), '短いID'); |
| 263 | $test->assertNull($repo->resolve('nothing')); |
| 264 | $test->assertNull($repo->resolve('HEAD~100')); |
| 265 | $test->assertNull($repo->resolve('HEAD:no/such/file')); |
| 266 | }); |
| 267 | |
| 268 | $test->it('ツリーとオブジェクトを読めるはず', function($test) use ($repo, $e): void { |
| 269 | $list = []; |
| 270 | $walk = function(string $tree, string $prefix) use (&$walk, &$list, $repo): void { |
| 271 | foreach ($repo->tree($tree)->entries as [$mode, $name, $id]) { |
| 272 | $type = $mode === Tree::DIR ? 'tree' : ($mode === Tree::SUBMODULE ? 'commit' : 'blob'); |
| 273 | $list[] = [str_pad($mode, 6, '0', STR_PAD_LEFT), $type, $id, $prefix.$name]; |
| 274 | if ($mode === Tree::DIR) $walk($id, $prefix.$name.'/'); |
| 275 | } |
| 276 | }; |
| 277 | $walk($repo->treeOf('main'), ''); |
| 278 | usort($list, fn($a, $b) => strcmp($a[3], $b[3])); |
| 279 | $expect = $e['tree']; |
| 280 | usort($expect, fn($a, $b) => strcmp($a[3], $b[3])); |
| 281 | $test->assertSame($expect, $list); |
| 282 | $test->assertSame(Tree::LINK, $repo->entry('main', 'link')[0]); |
| 283 | $test->assertSame('a.txt', $repo->blob($repo->entry('main', 'link')[2])); |
| 284 | $test->assertSame("ファイル\n", $repo->blob($repo->resolve('main:dir/サブ/日本語.txt'))); |
| 285 | $tag = $repo->tag($e['revs']['v1.0']); |
| 286 | $test->assertSame($e['tagObject'], $tag->toString()); |
| 287 | $test->assertSame('v1.0', $tag->tag); |
| 288 | // 全部のオブジェクトのIDが正しい(パックの差分も) |
| 289 | $n = 0; |
| 290 | foreach ($repo->objects->ids() as $id) { |
| 291 | [$type, $data] = $repo->objects->read($id); |
| 292 | $test->assertSame($id, $repo->hashObject($type, $data)); |
| 293 | $test->assertSame([$type, strlen($data)], $repo->objects->info($id)); |
| 294 | $n++; |
| 295 | } |
| 296 | $test->assertTrue($n > 20); |
| 297 | }); |
| 298 | |
| 299 | $test->it('gitと同じ差分を出すはず', function($test) use ($repo, $e): void { |
| 300 | foreach ($e['diffs'] as $rev => $expect) { |
| 301 | $c = $repo->commit($repo->resolve($rev)); |
| 302 | $test->assertSame($expect, Diff::unified($repo, $c->parents[0] ?? null, $c->id), $rev); |
| 303 | } |
| 304 | }); |
| 305 | |
| 306 | $test->it('gitと同じblameを出すはず', function($test) use ($repo, $e): void { |
| 307 | $b = Blame::file($repo, 'main', 'a.txt'); |
| 308 | $test->assertSame($e['blame'], array_map(fn($l) => [$l['commit'], $l['line']], $b)); |
| 309 | $test->assertSame('top', $b[0]['text']); |
| 310 | }); |
| 311 | |
| 312 | $test->it('祖先と共通の祖先が分かるはず', function($test) use ($repo, $e): void { |
| 313 | $w = new Walk($repo); |
| 314 | $test->assertTrue($w->isAncestor($e['revs']['main~1^2'], $e['revs']['HEAD'])); |
| 315 | $test->assertFalse($w->isAncestor($e['revs']['HEAD'], $e['revs']['feature'])); |
| 316 | $test->assertSame($e['revs']['refs/heads/feature~1'], $w->mergeBase($e['revs']['feature'], $e['revs']['main^^'])); |
| 317 | }); |
| 318 | }); |
| 319 | } |
| 320 | |
| 321 | $test->describe('Git(書く)', function($test) use ($tmp, $me, $catch, $git, $run): void { |
| 322 | foreach (['sha1', 'sha256'] as $fmt) { |
| 323 | $test->it("リポジトリを作り、コミット・タグ・参照を書けるはず({$fmt})", function($test) use ($tmp, $me, $fmt, $git, $run, $catch): void { |
| 324 | $dir = "{$tmp}/write-{$fmt}.git"; |
| 325 | $repo = Repository::init($dir, $fmt, 'main', 'group', 'テスト'); |
| 326 | $test->assertTrue(Repository::isRepository($dir)); |
| 327 | $test->assertTrue($repo->isEmpty()); |
| 328 | $test->assertThrows(fn() => Repository::init($dir), GitException::class); |
| 329 | $t1 = $repo->updateTree(null, ['README.md' => [Tree::FILE, "# テスト\n"], 'src/a.php' => [Tree::FILE, "<?php\n"], 'bin/run' => [Tree::EXEC, "#!/bin/sh\n"]]); |
| 330 | $c1 = $repo->writeCommit(Commit::create($t1, [], $me, $me, "最初\n")); |
| 331 | $repo->refs->update('refs/heads/main', $c1, $repo->zeroId()); |
| 332 | $t2 = $repo->updateTree($t1, ['src/a.php' => [Tree::FILE, "<?php\necho 1;\n"], 'bin/run' => null, 'docs/x/y.txt' => [Tree::FILE, "y\n"]]); |
| 333 | $test->assertSame(['README.md', 'docs', 'src'], array_column($repo->tree($t2)->entries, 1), '空になったbinは消える'); |
| 334 | $c2 = $repo->writeCommit(Commit::create($t2, [$c1], $me, $me, "二番目\n")); |
| 335 | $repo->refs->update('refs/heads/main', $c2, $c1); |
| 336 | $test->assertNotNull($catch(fn() => $repo->refs->update('refs/heads/main', $c1, $c1)), '古い値が違えば失敗'); |
| 337 | $test->assertNotNull($catch(fn() => $repo->refs->update('refs/heads/new', $c1, $c2)), '無い参照'); |
| 338 | $test->assertNotNull($catch(fn() => $repo->refs->update('refs/heads/main/x', $c1)), '参照と重なる'); |
| 339 | $test->assertNotNull($catch(fn() => $repo->refs->update('refs/heads/bad..name', $c1))); |
| 340 | $tag = $repo->writeTag(Tag::create($c1, 'commit', 'v0.1', $me, "リリース\n")); |
| 341 | $repo->refs->transaction([['refs/tags/v0.1', $tag, $repo->zeroId()], ['refs/heads/dev', $c1, null]]); |
| 342 | $test->assertSame($c1, $repo->resolve('v0.1^{commit}')); |
| 343 | $repo->refs->update('refs/heads/dev', null, $c1); |
| 344 | $test->assertNull($repo->refs->resolve('refs/heads/dev')); |
| 345 | $test->assertSame($c2, $repo->resolve('HEAD')); |
| 346 | $test->assertSame(["二番目", "最初"], array_map(fn($c) => $c->subject(), iterator_to_array($repo->log('HEAD'), false))); |
| 347 | // HEADの更新はブランチを更新する |
| 348 | $repo->refs->update('HEAD', $c1, $c2); |
| 349 | $test->assertSame($c1, $repo->refs->resolve('refs/heads/main')); |
| 350 | $repo->refs->update('HEAD', $c2); |
| 351 | if (DIRECTORY_SEPARATOR === '/') $test->assertSame(02775, fileperms($dir.'/objects') & 07777, 'グループで共有'); |
| 352 | // 整理しても全部読める |
| 353 | $r = Maintenance::gc($repo, 0); |
| 354 | $test->assertTrue($r['objects'] >= 9); |
| 355 | $repo2 = Repository::open($dir); |
| 356 | $test->assertCount(1, $repo2->objects->packs(true)); |
| 357 | $test->assertSame("<?php\necho 1;\n", $repo2->blob($repo2->resolve('main:src/a.php'))); |
| 358 | $test->assertSame($c1, $repo2->resolve('v0.1^{}')); |
| 359 | if ($git) { |
| 360 | [$code, , $err] = $run(['git', '-C', $dir, 'fsck', '--strict']); |
| 361 | $test->assertSame(0, $code, $err); |
| 362 | [, $out] = $run(['git', '-C', $dir, 'log', '--format=%s']); |
| 363 | $test->assertSame("二番目\n最初\n", $out); |
| 364 | } |
| 365 | }); |
| 366 | } |
| 367 | }); |
| 368 | |
| 369 | foreach (['packOfs' => 'OFS_DELTA', 'packRef' => 'REF_DELTA'] as $key => $label) { |
| 370 | $test->describe("Git(パック、{$label})", function($test) use ($key, $fixtures, $tmp, $extract, $stream, $catch, $unpack): void { |
| 371 | foreach (['sha1', 'sha256'] as $fmt) { |
| 372 | $test->it("gitのパックを受け取り、全部のオブジェクトが同じはず({$fmt})", function($test) use ($key, $fmt, $fixtures, $tmp, $extract, $stream): void { |
| 373 | $src = Repository::open($extract($fmt, "pk-{$key}-{$fmt}")); |
| 374 | foreach ([0, 1000] as $limit) { |
| 375 | $dst = Repository::init("{$tmp}/pk-{$key}-{$fmt}-{$limit}.git", $fmt); |
| 376 | $r = (new PackIndexer($dst))->receive($stream(base64_decode($fixtures[$fmt][$key])), $limit); |
| 377 | $test->assertSame($limit === 0, $r['pack'] !== null); |
| 378 | $ids = iterator_to_array($src->objects->ids(), false); |
| 379 | $test->assertCount(count(array_unique($ids)), $r['objects']); |
| 380 | foreach (array_unique($ids) as $id) $test->assertSame($src->objects->read($id), $dst->objects->read($id)); |
| 381 | $test->assertSame([], array_values(array_filter(array_keys($r['links']), fn($l) => !$dst->objects->exists($l)))); |
| 382 | } |
| 383 | }); |
| 384 | } |
| 385 | |
| 386 | $test->it('壊れたパックは拒否するはず', function($test) use ($key, $fixtures, $tmp, $stream, $catch): void { |
| 387 | $pack = base64_decode($fixtures['sha1'][$key]); |
| 388 | $dst = Repository::init("{$tmp}/bad-{$key}.git"); |
| 389 | foreach (['短い' => substr($pack, 0, -30), 'チェックサム' => substr($pack, 0, -1).chr(ord($pack[-1]) ^ 1), '中身' => substr($pack, 0, 40).chr(ord($pack[40]) ^ 0xff).substr($pack, 41)] as $what => $bad) { |
| 390 | $e = $catch(fn() => (new PackIndexer($dst))->receive($stream($bad), 0)); |
| 391 | $test->assertTrue($e instanceof GitException, $what); |
| 392 | } |
| 393 | $test->assertSame([], glob("{$tmp}/bad-{$key}.git/objects/pack/*") ?: [], '一時ファイルも残さない'); |
| 394 | }); |
| 395 | }); |
| 396 | } |
| 397 | |
| 398 | $test->describe('Git(パックを作る)', function($test) use ($tmp, $extract, $unpack): void { |
| 399 | $test->it('パックを作って受け取ると同じオブジェクトになるはず(差分の再利用)', function($test) use ($tmp, $extract, $unpack): void { |
| 400 | foreach (['sha1', 'sha256'] as $fmt) { |
| 401 | $src = Repository::open($extract($fmt, "pw-{$fmt}")); |
| 402 | $objects = (new Walk($src))->objects(array_values($src->refs->all()), []); |
| 403 | $pack = ''; |
| 404 | $sum = (new PackWriter($src))->write($objects, function(string $b) use (&$pack): void { $pack .= $b; }); |
| 405 | $test->assertSame($sum, bin2hex(substr($pack, -$src->hashLen))); |
| 406 | $dst = Repository::init("{$tmp}/pw-{$fmt}-dst.git", $fmt); |
| 407 | $r = $unpack($dst, $pack); |
| 408 | $test->assertSame(count($objects), count($r['objects'])); |
| 409 | foreach ($objects as $id => $type) $test->assertSame($src->objects->read($id), $dst->objects->read($id)); |
| 410 | // 一部(相手が持っている物を除く) |
| 411 | $head = $src->resolve('main'); |
| 412 | $part = (new Walk($src))->objects([$head], [$src->resolve('main~1')]); |
| 413 | $test->assertSame('commit', $part[$head]); |
| 414 | $test->assertTrue(count($part) < count($objects)); |
| 415 | $test->assertFalse(isset($part[$src->resolve('main~1')])); |
| 416 | } |
| 417 | }); |
| 418 | }); |
| 419 | |
| 420 | $test->describe('Git(プロトコル)', function($test) use ($tmp, $me, $extract, $fixtures, $pkts, $stream, $parse, $unpack): void { |
| 421 | $src = Repository::open($extract('sha1', 'proto')); |
| 422 | $e = $fixtures['sha1']['expect']; |
| 423 | |
| 424 | $test->it('v0の広告はHEAD・能力・剥いたタグを含むはず', function($test) use ($src, $e, $parse): void { |
| 425 | $out = fopen('php://temp', 'w+b'); |
| 426 | (new UploadPack($src))->advertise($out, true); |
| 427 | $r = $parse($out, false); |
| 428 | $test->assertSame('# service=git-upload-pack', $r['lines'][0]); |
| 429 | $test->assertSame(0, $r['lines'][1]); |
| 430 | [$first, $caps] = explode("\0", $r['lines'][2]); |
| 431 | $test->assertSame($e['revs']['HEAD'].' HEAD', $first); |
| 432 | $test->assertStringContains('symref=HEAD:refs/heads/main', $caps); |
| 433 | $test->assertStringContains('side-band-64k', $caps); |
| 434 | $test->assertStringContains('object-format=sha1', $caps); |
| 435 | $test->assertTrue(in_array($e['revs']['v1.0^{}'].' refs/tags/v1.0^{}', $r['lines'], true)); |
| 436 | $test->assertSame(0, end($r['lines'])); |
| 437 | }); |
| 438 | |
| 439 | $test->it('v2のls-refsはシンボリック参照と剥いたタグを返すはず', function($test) use ($src, $e, $pkts, $stream, $parse): void { |
| 440 | $out = fopen('php://temp', 'w+b'); |
| 441 | (new UploadPack($src))->serve($stream($pkts(["command=ls-refs\n", "agent=git/2\n", 1, "symrefs\n", "peel\n", "ref-prefix HEAD\n", "ref-prefix refs/tags/\n", 0])), $out, true, 2); |
| 442 | $r = $parse($out, false); |
| 443 | $test->assertSame($e['revs']['HEAD'].' HEAD symref-target:refs/heads/main', $r['lines'][0]); |
| 444 | $test->assertTrue(in_array($e['refs']['refs/tags/v1.0'].' refs/tags/v1.0 peeled:'.$e['revs']['v1.0^{}'], $r['lines'], true)); |
| 445 | $test->assertFalse((bool)array_filter($r['lines'], fn($l) => is_string($l) && str_contains($l, ' refs/heads/'))); |
| 446 | }); |
| 447 | |
| 448 | $test->it('v0でクローンとフェッチが出来るはず', function($test) use ($src, $e, $tmp, $pkts, $stream, $parse, $unpack): void { |
| 449 | $head = $e['revs']['HEAD']; |
| 450 | // 全部 |
| 451 | $out = fopen('php://temp', 'w+b'); |
| 452 | (new UploadPack($src))->serve($stream($pkts(["want {$head} multi_ack_detailed side-band-64k ofs-delta include-tag\n", 0, "done\n"])), $out, true, 0); |
| 453 | $r = $parse($out); |
| 454 | $test->assertSame('NAK', $r['lines'][0]); |
| 455 | $dst = Repository::init("{$tmp}/proto-v0.git"); |
| 456 | $got = $unpack($dst, $r['pack']); |
| 457 | $test->assertTrue(isset($got['objects'][$e['refs']['refs/tags/v1.0']]), 'include-tag'); |
| 458 | $test->assertSame(count(iterator_to_array($src->log('HEAD'), false)), count(iterator_to_array((new Walk($dst))->log([$head]), false))); |
| 459 | // 持っている物を伝える(ネゴシエーションとdone) |
| 460 | $out = fopen('php://temp', 'w+b'); |
| 461 | (new UploadPack($src))->serve($stream($pkts(["want {$head} multi_ack_detailed side-band-64k\n", 0, "have {$e['revs']['main~1']}\n", "have ".str_repeat('1', 40)."\n", 0])), $out, true, 0); |
| 462 | $test->assertSame(["ACK {$e['revs']['main~1']} common", 'NAK'], $parse($out)['lines']); |
| 463 | $out = fopen('php://temp', 'w+b'); |
| 464 | (new UploadPack($src))->serve($stream($pkts(["want {$head} multi_ack_detailed side-band-64k no-progress\n", 0, "have {$e['revs']['main~1']}\n", "done\n"])), $out, true, 0); |
| 465 | $r = $parse($out); |
| 466 | $test->assertSame(["ACK {$e['revs']['main~1']} common", "ACK {$e['revs']['main~1']}"], array_values(array_filter($r['lines'], 'is_string'))); |
| 467 | $test->assertSame('', $r['progress']); |
| 468 | $n = unpack('N', $r['pack'], 8)[1]; |
| 469 | $test->assertTrue($n >= 2 && $n <= 4, '新しいコミットとツリーとブロブだけ'); |
| 470 | }); |
| 471 | |
| 472 | $test->it('v2でフェッチと深さの制限が出来るはず', function($test) use ($src, $e, $tmp, $pkts, $stream, $parse, $unpack): void { |
| 473 | $head = $e['revs']['HEAD']; |
| 474 | $out = fopen('php://temp', 'w+b'); |
| 475 | (new UploadPack($src))->serve($stream($pkts(["command=fetch\n", "object-format=sha1\n", 1, "want {$head}\n", "have {$e['revs']['main~1']}\n", 0])), $out, true, 2); |
| 476 | $test->assertSame(['acknowledgments', "ACK {$e['revs']['main~1']}", 0], $parse($out)['lines']); |
| 477 | $out = fopen('php://temp', 'w+b'); |
| 478 | (new UploadPack($src))->serve($stream($pkts(["command=fetch\n", 1, "want {$head}\n", "deepen 1\n", "ofs-delta\n", "done\n", 0])), $out, true, 2); |
| 479 | $r = $parse($out); |
| 480 | $test->assertSame(['shallow-info', "shallow {$head}", 1, 'packfile'], array_slice($r['lines'], 0, 4)); |
| 481 | $dst = Repository::init("{$tmp}/proto-v2.git"); |
| 482 | $got = $unpack($dst, $r['pack']); |
| 483 | $test->assertSame(1, count(array_filter($got['objects'], fn($t) => $t === 'commit')), '1つのコミットだけ'); |
| 484 | // 不明なオブジェクトはエラー |
| 485 | $out = fopen('php://temp', 'w+b'); |
| 486 | (new UploadPack($src))->serve($stream($pkts(["command=fetch\n", 1, 'want '.str_repeat('f', 40)."\n", "done\n", 0])), $out, true, 2); |
| 487 | $test->assertStringContains('ERR', implode("\n", array_filter($parse($out, false)['lines'], 'is_string'))); |
| 488 | }); |
| 489 | |
| 490 | $test->it('プッシュを受け取り、参照を更新するはず', function($test) use ($tmp, $me, $pkts, $stream, $parse): void { |
| 491 | $repo = Repository::init("{$tmp}/push.git"); |
| 492 | $zero = $repo->zeroId(); |
| 493 | // 送る側のリポジトリでコミットを作る |
| 494 | $client = Repository::init("{$tmp}/push-client.git"); |
| 495 | $t = $client->updateTree(null, ['a.txt' => [Tree::FILE, "a\n"]]); |
| 496 | $c1 = $client->writeCommit(Commit::create($t, [], $me, $me, "1\n")); |
| 497 | $c2 = $client->writeCommit(Commit::create($client->updateTree($t, ['b.txt' => [Tree::FILE, "b\n"]]), [$c1], $me, $me, "2\n")); |
| 498 | $c3 = $client->writeCommit(Commit::create($client->updateTree($t, ['c.txt' => [Tree::FILE, "c\n"]]), [$c1], $me, $me, "別の2\n")); |
| 499 | $pack = function(array $want, array $have = []) use ($client): string { |
| 500 | $s = ''; |
| 501 | (new PackWriter($client))->write((new Walk($client))->objects($want, $have), function($b) use (&$s): void { $s .= $b; }); |
| 502 | return $s; |
| 503 | }; |
| 504 | $after = []; |
| 505 | $push = function(array $commands, string $packData, array $opt = []) use ($repo, $pkts, $stream, $parse, &$after): array { |
| 506 | $lines = []; |
| 507 | foreach ($commands as $i => $c) $lines[] = $c.($i === 0 ? "\0report-status side-band-64k quiet".($opt['atomic'] ?? false ? ' atomic' : '') : '')."\n"; |
| 508 | $lines[] = 0; |
| 509 | $out = fopen('php://temp', 'w+b'); |
| 510 | (new ReceivePack($repo, $opt + ['after' => function(array $done) use (&$after): void { $after = $done; }]))->serve($stream($pkts($lines).$packData), $out, true); |
| 511 | // サイドバンドの中のpkt-line |
| 512 | $inner = $parse($out)['pack']; |
| 513 | return $parse($stream($inner), false)['lines']; |
| 514 | }; |
| 515 | $test->assertSame(['unpack ok', 'ok refs/heads/main', 0], $push(["{$zero} {$c2} refs/heads/main"], $pack([$c2]))); |
| 516 | $test->assertSame($c2, $repo->resolve('main')); |
| 517 | $test->assertSame([['refs/heads/main', $zero, $c2]], $after); |
| 518 | // 早送りでない更新(denyNonFastForwards) |
| 519 | $r = $push(["{$c2} {$c3} refs/heads/main"], $pack([$c3], [$c2]), ['denyNonFastForwards' => true]); |
| 520 | $test->assertSame('ng refs/heads/main non-fast-forward', $r[1]); |
| 521 | $test->assertSame($c2, $repo->resolve('main')); |
| 522 | // 古い値が違う |
| 523 | $r = $push(["{$c1} {$c3} refs/heads/main"], $pack([$c3], [$c2])); |
| 524 | $test->assertSame('ng refs/heads/main fetch first', $r[1]); |
| 525 | // フック(update)とatomic |
| 526 | $r = $push(["{$zero} {$c3} refs/heads/ok", "{$zero} {$c3} refs/heads/protected"], $pack([$c3], [$c2]), |
| 527 | ['atomic' => true, 'update' => fn(string $ref) => $ref === 'refs/heads/protected' ? '保護されています' : null]); |
| 528 | $test->assertSame(['unpack ok', 'ng refs/heads/ok atomic push failed', 'ng refs/heads/protected 保護されています', 0], $r); |
| 529 | $test->assertNull($repo->refs->resolve('refs/heads/ok')); |
| 530 | $r = $push(["{$zero} {$c3} refs/heads/ok", "{$zero} {$c3} refs/heads/protected"], $pack([$c3], [$c2]), |
| 531 | ['update' => fn(string $ref) => $ref === 'refs/heads/protected' ? '保護されています' : null]); |
| 532 | $test->assertSame(['unpack ok', 'ok refs/heads/ok', 'ng refs/heads/protected 保護されています', 0], $r); |
| 533 | // 削除(パック無し) |
| 534 | $test->assertSame(['unpack ok', 'ok refs/heads/ok', 0], $push(["{$c3} {$zero} refs/heads/ok"], '')); |
| 535 | $test->assertNull($repo->refs->resolve('refs/heads/ok')); |
| 536 | // 足りないオブジェクト(親が無い) |
| 537 | $other = Repository::init("{$tmp}/push-other.git"); |
| 538 | $c9 = $other->writeCommit(Commit::create($other->updateTree(null, ['z' => [Tree::FILE, "z\n"]]), [str_repeat('9', 40)], $me, $me, "x\n")); |
| 539 | $s = ''; |
| 540 | (new PackWriter($other))->write([$c9 => 'commit'], function($b) use (&$s): void { $s .= $b; }); |
| 541 | $r = $push(["{$zero} {$c9} refs/heads/broken"], $s); |
| 542 | $test->assertStringContains('unpack ', $r[0]); |
| 543 | $test->assertTrue($r[0] !== 'unpack ok'); |
| 544 | $test->assertSame('ng refs/heads/broken unpacker error', $r[1]); |
| 545 | $test->assertSame(['unpack ok', 'ng HEAD funny refname', 0], $push(["{$zero} {$c1} HEAD"], $pack([$c1]))); |
| 546 | // intercept(書かない)とprocess(結果の前に呼ぶ) |
| 547 | $processed = null; |
| 548 | $r = $push(["{$zero} {$c3} refs/for/main/topic"], $pack([$c3], [$c2]), ['intercept' => fn(string $ref) => str_starts_with($ref, 'refs/for/'), |
| 549 | 'process' => function(array $done) use (&$processed): array { $processed = $done; return ['作りました']; }]); |
| 550 | $test->assertSame(['unpack ok', 'ok refs/for/main/topic', 0], $r); |
| 551 | $test->assertNull($repo->refs->resolve('refs/for/main/topic'), '書かない'); |
| 552 | $test->assertTrue($repo->objects->exists($c3), 'オブジェクトは受け取る'); |
| 553 | $test->assertSame([['refs/for/main/topic', $zero, $c3]], $processed); |
| 554 | }); |
| 555 | |
| 556 | $test->it('SSHのコマンドを解析するはず', function($test): void { |
| 557 | $test->assertSame(['git-upload-pack', 'user/repo.git'], Ssh::parse("git-upload-pack 'user/repo.git'")); |
| 558 | $test->assertSame(['git-receive-pack', 'user/repo.git'], Ssh::parse("git-receive-pack '/user/repo.git'")); |
| 559 | $test->assertSame(['git-upload-pack', "it's.git"], Ssh::parse("git upload-pack 'it'\\''s.git'")); |
| 560 | $test->assertSame(['git-upload-pack', 'a/b.git'], Ssh::parse('git-upload-pack ~/a/b.git')); |
| 561 | foreach (['ls -la', "git-upload-pack '../etc'", "git-upload-pack 'a/../../b'", "git-upload-pack 'a", 'git-upload-pack'] as $bad) $test->assertNull(Ssh::parse($bad), $bad); |
| 562 | }); |
| 563 | }); |
| 564 | |
| 565 | $test->describe('Git(マージ)', function($test) use ($tmp, $me, $git, $run): void { |
| 566 | $l = fn(string $s) => Merge::split($s); |
| 567 | $test->it('行の3方向のマージ(離れた変更、同じ変更、コンフリクト)', function($test) use ($l): void { |
| 568 | $base = $l("1\n2\n3\n4\n5\n6\n7\n"); |
| 569 | $r = Merge::lines($base, $l("1\nA\n3\n4\n5\n6\n7\n"), $l("1\n2\n3\n4\n5\nB\n7\n")); |
| 570 | $test->assertSame(0, $r['conflicts']); |
| 571 | $test->assertSame("1\nA\n3\n4\n5\nB\n7\n", implode('', $r['lines'])); |
| 572 | $r = Merge::lines($base, $l("1\nX\n3\n4\n5\n6\n7\n"), $l("1\nX\n3\n4\n5\n6\n7\n8\n")); |
| 573 | $test->assertSame("1\nX\n3\n4\n5\n6\n7\n8\n", implode('', $r['lines']), '同じ変更'); |
| 574 | $r = Merge::lines($base, $l("1\nA\n3\n4\n5\n6\n7\n"), $l("1\nB\n3\n4\n5\n6\n7\n"), 'main', 'topic'); |
| 575 | $test->assertSame(1, $r['conflicts']); |
| 576 | $test->assertSame("1\n<<<<<<< main\nA\n=======\nB\n>>>>>>> topic\n3\n4\n5\n6\n7\n", implode('', $r['lines'])); |
| 577 | $test->assertSame(1, Merge::lines($base, $l("1\nA\n3\n4\n5\n6\n7\n"), $l("1\n2\nB\n4\n5\n6\n7\n"))['conflicts'], '隣り合う変更'); |
| 578 | $test->assertSame("x\ny", Merge::text("x\n", "x\ny", "x\n"), '最後の改行'); |
| 579 | $test->assertNull(Merge::text("a\n", "b\n", "c\n")); |
| 580 | $test->assertNull(Merge::text("a\0", "b\0", "c\0"), 'バイナリ'); |
| 581 | }); |
| 582 | |
| 583 | $test->it('ツリーの3方向のマージとコミットの適用(cherry-pick)', function($test) use ($tmp, $me): void { |
| 584 | $repo = Repository::init("{$tmp}/merge.git"); |
| 585 | $base = $repo->updateTree(null, ['a.txt' => [Tree::FILE, "1\n2\n3\n4\n5\n"], 'dir/b.txt' => [Tree::FILE, "b\n"], 'gone.txt' => [Tree::FILE, "g\n"]]); |
| 586 | $ours = $repo->updateTree($base, ['a.txt' => [Tree::FILE, "1\nO\n3\n4\n5\n"], 'dir/new.txt' => [Tree::FILE, "n\n"], 'gone.txt' => null]); |
| 587 | $theirs = $repo->updateTree($base, ['a.txt' => [Tree::FILE, "1\n2\n3\n4\nT\n"], 'dir/b.txt' => [Tree::EXEC, "b\n"], 'c.txt' => [Tree::FILE, "c\n"]]); |
| 588 | $r = Merge::trees($repo, $base, $ours, $theirs); |
| 589 | $test->assertSame([], $r['conflicts']); |
| 590 | $test->assertSame("1\nO\n3\n4\nT\n", $repo->blob($repo->entry($r['tree'], 'a.txt')[2])); |
| 591 | $test->assertSame(Tree::EXEC, $repo->entry($r['tree'], 'dir/b.txt')[0]); |
| 592 | $test->assertNotNull($repo->entry($r['tree'], 'dir/new.txt')); |
| 593 | $test->assertNotNull($repo->entry($r['tree'], 'c.txt')); |
| 594 | $test->assertNull($repo->entry($r['tree'], 'gone.txt'), '削除'); |
| 595 | // コンフリクト:同じ行、削除と変更 |
| 596 | $t2 = $repo->updateTree($base, ['a.txt' => [Tree::FILE, "1\nT\n3\n4\n5\n"], 'gone.txt' => [Tree::FILE, "changed\n"]]); |
| 597 | $r = Merge::trees($repo, $base, $ours, $t2); |
| 598 | $test->assertNull($r['tree']); |
| 599 | $test->assertSame(['a.txt', 'gone.txt'], $r['conflicts']); |
| 600 | // cherry-pick |
| 601 | $c0 = $repo->writeCommit(Commit::create($base, [], $me, $me, "0\n")); |
| 602 | $c1 = $repo->writeCommit(Commit::create($theirs, [$c0], $me, $me, "1\n")); |
| 603 | $c2 = $repo->writeCommit(Commit::create($ours, [$c0], $me, $me, "2\n")); |
| 604 | $test->assertSame($r = Merge::trees($repo, $base, $ours, $theirs)['tree'], Merge::pick($repo, $c1, $c2)); |
| 605 | }); |
| 606 | }); |
| 607 | |
| 608 | $test->describe('Git(本物のgitと)', function($test) use ($tmp, $git, $run, $extract): void { |
| 609 | if ($git === null) { |
| 610 | $test->skipAll('gitのコマンドが無い(又はproc_openが無効)'); |
| 611 | return; |
| 612 | } |
| 613 | $root = "{$tmp}/srv"; |
| 614 | mkdir($root); |
| 615 | $probe = @stream_socket_server('tcp://127.0.0.1:0'); |
| 616 | $port = (int)substr(strrchr(stream_socket_get_name($probe, false), ':'), 1); |
| 617 | fclose($probe); |
| 618 | $env = ['LB_GIT_ROOT' => $root]; |
| 619 | $proc = proc_open([PHP_BINARY, '-S', "127.0.0.1:{$port}", __DIR__.'/Helper/git-http.php'], [0 => ['pipe', 'r'], 1 => ['file', $tmp.'/srv.log', 'a'], 2 => ['file', $tmp.'/srv.log', 'a']], |
| 620 | $pipes, null, $env + ['PATH' => (string)getenv('PATH'), 'SystemRoot' => (string)getenv('SystemRoot')]); |
| 621 | for ($i = 0; $i < 50; $i++) { |
| 622 | $c = @stream_socket_client("tcp://127.0.0.1:{$port}", $errno, $errstr, 0.1); |
| 623 | if ($c) { |
| 624 | fclose($c); |
| 625 | break; |
| 626 | } |
| 627 | usleep(100000); |
| 628 | } |
| 629 | $url = "http://127.0.0.1:{$port}"; |
| 630 | $ssh = escapeshellarg(PHP_BINARY).' '.escapeshellarg(__DIR__.'/Helper/git-ssh.php'); |
| 631 | $gitEnv = $env + ['GIT_SSH_COMMAND' => $ssh, 'GIT_PROTOCOL_FROM_USER' => '0']; |
| 632 | $g = fn(array $args, ?string $cwd = null) => $run(array_merge(['git'], $args), $cwd, $gitEnv); |
| 633 | |
| 634 | foreach (['sha1', 'sha256'] as $fmt) { |
| 635 | $test->it("HTTPとSSHでクローン・プッシュ出来るはず({$fmt})", function($test) use ($fmt, $tmp, $root, $url, $g, $extract): void { |
| 636 | rename($extract($fmt, "srv/{$fmt}.git"), "{$root}/{$fmt}.git"); |
| 637 | $src = Repository::open("{$root}/{$fmt}.git"); |
| 638 | $count = count(iterator_to_array($src->log('main'), false)); |
| 639 | foreach (['http-v0' => [0, "{$url}/{$fmt}.git"], 'http-v2' => [2, "{$url}/{$fmt}.git"], 'ssh-v0' => [0, "git@example:{$fmt}.git"], 'ssh-v2' => [2, "git@example:{$fmt}.git"]] as $name => [$v, $remote]) { |
| 640 | $dir = "{$tmp}/c-{$fmt}-{$name}"; |
| 641 | [$code, , $err] = $g(['-c', "protocol.version={$v}", 'clone', '-q', $remote, $dir]); |
| 642 | $test->assertSame(0, $code, "{$name}: {$err}"); |
| 643 | [, $out] = $g(['-C', $dir, 'rev-list', '--count', 'HEAD']); |
| 644 | $test->assertSame((string)$count, trim($out), $name); |
| 645 | [$code, , $err] = $g(['-C', $dir, 'fsck']); |
| 646 | $test->assertSame(0, $code, $err); |
| 647 | // プッシュ |
| 648 | file_put_contents("{$dir}/{$name}.txt", "{$name}\n"); |
| 649 | $g(['-C', $dir, 'add', '-A']); |
| 650 | $g(['-C', $dir, 'commit', '-q', '-m', "push {$name}"]); |
| 651 | [$code, , $err] = $g(['-C', $dir, '-c', "protocol.version={$v}", 'push', '-q', 'origin', 'main']); |
| 652 | $test->assertSame(0, $code, "{$name}: {$err}"); |
| 653 | $test->assertSame("push {$name}", Repository::open("{$root}/{$fmt}.git")->commit(Repository::open("{$root}/{$fmt}.git")->resolve('main'))->subject()); |
| 654 | $count++; |
| 655 | } |
| 656 | // 浅いクローンと深くする |
| 657 | $dir = "{$tmp}/c-{$fmt}-depth"; |
| 658 | [$code, , $err] = $g(['clone', '-q', '--depth', '1', "{$url}/{$fmt}.git", $dir]); |
| 659 | $test->assertSame(0, $code, $err); |
| 660 | [, $out] = $g(['-C', $dir, 'rev-list', '--count', 'HEAD']); |
| 661 | $test->assertSame('1', trim($out)); |
| 662 | [$code, , $err] = $g(['-C', $dir, 'fetch', '-q', '--deepen', '2']); |
| 663 | $test->assertSame(0, $code, $err); |
| 664 | [, $out] = $g(['-C', $dir, 'rev-list', '--count', 'HEAD']); |
| 665 | $test->assertSame('3', trim($out)); |
| 666 | [$code, , $err] = $g(['-C', $dir, 'fetch', '-q', '--unshallow']); |
| 667 | $test->assertSame(0, $code, $err); |
| 668 | [, $out] = $g(['-C', $dir, 'rev-list', '--count', 'HEAD']); |
| 669 | $test->assertSame((string)$count, trim($out)); |
| 670 | // ブランチとタグの追加・削除、強制プッシュの拒否は無い(既定) |
| 671 | $dir = "{$tmp}/c-{$fmt}-http-v2"; |
| 672 | $g(['-C', $dir, 'pull', '-q']); |
| 673 | [$code, , $err] = $g(['-C', $dir, 'push', '-q', 'origin', 'main:refs/heads/topic', 'refs/tags/v1.0']); |
| 674 | $test->assertSame(0, $code, $err); |
| 675 | [$code, , $err] = $g(['-C', $dir, 'push', '-q', 'origin', '--delete', 'topic']); |
| 676 | $test->assertSame(0, $code, $err); |
| 677 | $repo = Repository::open("{$root}/{$fmt}.git"); |
| 678 | $test->assertNull($repo->refs->resolve('refs/heads/topic')); |
| 679 | // 新しいリポジトリへのプッシュ(空) |
| 680 | [$code, , $err] = $g(['-C', $dir, 'push', '-q', "{$url}/new-{$fmt}.git", 'main']); |
| 681 | if ($fmt === 'sha1') { |
| 682 | $test->assertSame(0, $code, $err); |
| 683 | $test->assertSame($repo->resolve('main'), Repository::open("{$root}/new-sha1.git")->resolve('main')); |
| 684 | } |
| 685 | }); |
| 686 | } |
| 687 | |
| 688 | $test->it('存在しないリポジトリはエラーのはず', function($test) use ($tmp, $url, $g): void { |
| 689 | [$code] = $g(['clone', '-q', "{$url}/none.git", "{$tmp}/none1"]); |
| 690 | $test->assertTrue($code !== 0); |
| 691 | [$code, , $err] = $g(['clone', '-q', 'git@example:none.git', "{$tmp}/none2"]); |
| 692 | $test->assertTrue($code !== 0); |
| 693 | $test->assertStringContains('none.git', $err); |
| 694 | }); |
| 695 | |
| 696 | proc_terminate($proc); |
| 697 | proc_close($proc); |
| 698 | }); |
| 699 | |
| 700 | $test->printSummary(); |