Skip to main content

max / makenotwork

Port git object and ref reads from libgit2 to gitoxide objects.rs and refs.rs now read through gix, which is MIT/Apache-2.0, rather than libgit2. Both libraries are linked while the rest of the browse surface moves over: with_repo hands its closure a handle from each, and to_git2_oid bridges ids into the log, diff and blame paths that are still on git2. The raw download path came along, since it consumed resolve_ref and read its blob through git2 by hand. It now takes the blob size from the object header, so an over-cap file is rejected before it is decoded rather than after. Repositories open with gix isolated options, so no ambient git configuration can change how a visitor's repository renders.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-01 20:56 UTC
Signed with PGP, not checked
Commit: 54c8f289280d0f8cfe14db607e5bd8e0bab04fc8
Parent: f6ba086
9 files changed, +664 insertions, -161 deletions
M server/Cargo.lock +445 -4
@@ -134,7 +134,7 @@
134 134 source = "registry+https://github.com/rust-lang/crates.io-index"
135 135 checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
136 136 dependencies = [
137 - "windows-sys 0.61.2",
137 + "windows-sys 0.60.2",
138 138 ]
139 139
140 140 [[package]]
@@ -145,7 +145,7 @@
145 145 dependencies = [
146 146 "anstyle",
147 147 "once_cell_polyfill",
148 - "windows-sys 0.61.2",
148 + "windows-sys 0.60.2",
149 149 ]
150 150
151 151 [[package]]
@@ -1363,6 +1363,21 @@
1363 1363 "virtue",
1364 1364 ]
1365 1365
1366 + [[package]]
1367 + name = "bisync"
1368 + version = "0.3.0"
1369 + source = "registry+https://github.com/rust-lang/crates.io-index"
1370 + checksum = "5020822f6d6f23196ccaf55e228db36f9de1cf788052b37992e17cbc96ec41a7"
1371 + dependencies = [
1372 + "bisync_macros",
1373 + ]
1374 +
1375 + [[package]]
1376 + name = "bisync_macros"
1377 + version = "0.2.3"
1378 + source = "registry+https://github.com/rust-lang/crates.io-index"
1379 + checksum = "d21f40d350a700f6aa107e45fb26448cf489d34794b2ba4522181dc9f1173af6"
1380 +
1366 1381 [[package]]
1367 1382 name = "bit-set"
1368 1383 version = "0.8.0"
@@ -1695,6 +1710,15 @@
1695 1710 source = "registry+https://github.com/rust-lang/crates.io-index"
1696 1711 checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
1697 1712
1713 + [[package]]
1714 + name = "clru"
1715 + version = "0.6.3"
1716 + source = "registry+https://github.com/rust-lang/crates.io-index"
1717 + checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5"
1718 + dependencies = [
1719 + "hashbrown 0.16.1",
1720 + ]
1721 +
1698 1722 [[package]]
1699 1723 name = "cmake"
1700 1724 version = "0.1.58"
@@ -2113,6 +2137,15 @@
2113 2137 "cfg-if",
2114 2138 ]
2115 2139
2140 + [[package]]
2141 + name = "crossbeam-channel"
2142 + version = "0.5.16"
2143 + source = "registry+https://github.com/rust-lang/crates.io-index"
2144 + checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
2145 + dependencies = [
2146 + "crossbeam-utils",
2147 + ]
2148 +
2116 2149 [[package]]
2117 2150 name = "crossbeam-deque"
2118 2151 version = "0.8.6"
@@ -2781,7 +2814,7 @@
2781 2814 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
2782 2815 dependencies = [
2783 2816 "libc",
2784 - "windows-sys 0.61.2",
2817 + "windows-sys 0.52.0",
2785 2818 ]
2786 2819
2787 2820 [[package]]
@@ -2834,6 +2867,16 @@
2834 2867 "regex-syntax",
2835 2868 ]
2836 2869
2870 + [[package]]
2871 + name = "faster-hex"
2872 + version = "0.10.0"
2873 + source = "registry+https://github.com/rust-lang/crates.io-index"
2874 + checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73"
2875 + dependencies = [
2876 + "heapless",
2877 + "serde",
2878 + ]
2879 +
2837 2880 [[package]]
2838 2881 name = "fastrand"
2839 2882 version = "2.4.1"
@@ -2971,7 +3014,7 @@
2971 3014 source = "registry+https://github.com/rust-lang/crates.io-index"
2972 3015 checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
2973 3016 dependencies = [
2974 - "nonempty",
3017 + "nonempty 0.7.0",
2975 3018 "thiserror 1.0.69",
2976 3019 ]
2977 3020
@@ -3213,6 +3256,766 @@
3213 3256 "log",
3214 3257 ]
3215 3258
3259 + [[package]]
3260 + name = "gix"
3261 + version = "0.86.0"
3262 + source = "registry+https://github.com/rust-lang/crates.io-index"
3263 + checksum = "bb3790fd8981cba7949f1ba924ef865d902df731627bc5998d14164063892fce"
3264 + dependencies = [
3265 + "gix-actor",
3266 + "gix-attributes",
3267 + "gix-blame",
3268 + "gix-command",
3269 + "gix-commitgraph",
3270 + "gix-config",
3271 + "gix-date",
3272 + "gix-diff",
3273 + "gix-discover",
3274 + "gix-error",
3275 + "gix-features",
3276 + "gix-filter",
3277 + "gix-fs",
3278 + "gix-glob",
3279 + "gix-hash",
3280 + "gix-hashtable",
3281 + "gix-ignore",
3282 + "gix-index",
3283 + "gix-lock",
3284 + "gix-object",
3285 + "gix-odb",
3286 + "gix-pack",
3287 + "gix-path",
3288 + "gix-pathspec",
3289 + "gix-protocol",
3290 + "gix-ref",
3291 + "gix-refspec",
3292 + "gix-revision",
3293 + "gix-revwalk",
3294 + "gix-sec",
3295 + "gix-shallow",
3296 + "gix-submodule",
3297 + "gix-tempfile",
3298 + "gix-trace",
3299 + "gix-traverse",
3300 + "gix-url",
3301 + "gix-utils",
3302 + "gix-validate",
3303 + "gix-worktree",
3304 + "gix-worktree-stream",
3305 + "gix-zlib",
3306 + "nonempty 0.12.0",
3307 + "smallvec",
3308 + "thiserror 2.0.18",
3309 + ]
3310 +
3311 + [[package]]
3312 + name = "gix-actor"
3313 + version = "0.41.2"
3314 + source = "registry+https://github.com/rust-lang/crates.io-index"
3315 + checksum = "33f9308ad6fd35b2a865cbe4117ac61b2be59e4a9ef1621c7a9794f7c8e52c5b"
3316 + dependencies = [
3317 + "bstr",
3318 + "gix-date",
3319 + "gix-error",
3320 + ]
3321 +
3322 + [[package]]
3323 + name = "gix-attributes"
3324 + version = "0.34.0"
3325 + source = "registry+https://github.com/rust-lang/crates.io-index"
3326 + checksum = "c31c593692ebdc1e38858d9a2b56f6a594c501e24a38971fe6685571f5a07be0"
3327 + dependencies = [
3328 + "bstr",
3329 + "gix-features",
3330 + "gix-glob",
3331 + "gix-path",
3332 + "gix-quote",
3333 + "gix-trace",
3334 + "smallvec",
3335 + "thiserror 2.0.18",
3336 + "unicode-bom",
3337 + ]
3338 +
3339 + [[package]]
3340 + name = "gix-bitmap"
3341 + version = "0.3.3"
3342 + source = "registry+https://github.com/rust-lang/crates.io-index"
3343 + checksum = "7cd1d118d0f5d88b96e6f6e13b566475fef4797ead4a02c26fed36c1375066f7"
3344 + dependencies = [
3345 + "gix-error",
3346 + ]
3347 +
3348 + [[package]]
3349 + name = "gix-blame"
3350 + version = "0.16.0"
3351 + source = "registry+https://github.com/rust-lang/crates.io-index"
3352 + checksum = "b06d20ff0e88ada2dd852b3852727b664ec9baefdf653d1d4e722e858c52cd17"
3353 + dependencies = [
3354 + "gix-commitgraph",
3355 + "gix-date",
3356 + "gix-diff",
3357 + "gix-error",
3358 + "gix-hash",
3359 + "gix-object",
3360 + "gix-revwalk",
3361 + "gix-trace",
3362 + "gix-traverse",
3363 + "gix-worktree",
3364 + "smallvec",
3365 + "thiserror 2.0.18",
3366 + ]
3367 +
3368 + [[package]]
3369 + name = "gix-chunk"
3370 + version = "0.7.3"
3371 + source = "registry+https://github.com/rust-lang/crates.io-index"
3372 + checksum = "b2a871e5cab12ba568845714473505deefffb3c04eb47f4708ce344cd459c1cc"
3373 + dependencies = [
3374 + "gix-error",
3375 + ]
3376 +
3377 + [[package]]
3378 + name = "gix-command"
3379 + version = "0.9.1"
3380 + source = "registry+https://github.com/rust-lang/crates.io-index"
3381 + checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6"
3382 + dependencies = [
3383 + "bstr",
3384 + "gix-path",
3385 + "gix-quote",
3386 + "gix-trace",
3387 + "shell-words",
3388 + ]
3389 +
3390 + [[package]]
3391 + name = "gix-commitgraph"
3392 + version = "0.38.0"
3393 + source = "registry+https://github.com/rust-lang/crates.io-index"
3394 + checksum = "a2cd7f054ae2727223fe46dd39c012f066b12f532962d336d29ee193261787da"
3395 + dependencies = [
3396 + "bstr",
3397 + "gix-chunk",
3398 + "gix-error",
3399 + "gix-hash",
3400 + "memmap2",
3401 + "nonempty 0.12.0",
3402 + ]
3403 +
3404 + [[package]]
3405 + name = "gix-config"
3406 + version = "0.59.0"
3407 + source = "registry+https://github.com/rust-lang/crates.io-index"
3408 + checksum = "103d11bef95c467577ecfa8b7b86a22e65af3507b2c9bfa3809a4afbae7df301"
3409 + dependencies = [
3410 + "bstr",
3411 + "gix-config-value",
3412 + "gix-features",
3413 + "gix-glob",
3414 + "gix-path",
3415 + "gix-ref",
3416 + "gix-sec",
3417 + "gix-utils",
3418 + "smallvec",
3419 + "thiserror 2.0.18",
3420 + "unicode-bom",
3421 + ]
3422 +
3423 + [[package]]
3424 + name = "gix-config-value"
3425 + version = "0.19.0"
3426 + source = "registry+https://github.com/rust-lang/crates.io-index"
3427 + checksum = "9f813e312a3f7f327187823cd4c754a3e4d948c98907d11e8f37554a2b6b8059"
3428 + dependencies = [
3429 + "bitflags 2.13.0",
3430 + "bstr",
3431 + "gix-path",
3432 + "libc",
3433 + "thiserror 2.0.18",
3434 + ]
3435 +
3436 + [[package]]
3437 + name = "gix-date"
3438 + version = "0.15.6"
3439 + source = "registry+https://github.com/rust-lang/crates.io-index"
3440 + checksum = "7e47b9e8cdc688296609b706428de570f88b1e0eed7156dde7b4a89d26fa4567"
3441 + dependencies = [
3442 + "bstr",
3443 + "gix-error",
3444 + "itoa",
3445 + "jiff",
3446 + ]
3447 +
3448 + [[package]]
3449 + name = "gix-diff"
3450 + version = "0.66.0"
3451 + source = "registry+https://github.com/rust-lang/crates.io-index"
3452 + checksum = "fee7d89a3c507491cdfc57a1d1e0e300214720b4f7709ebc253e422f99822bfc"
3453 + dependencies = [
3454 + "bstr",
3455 + "gix-command",
3456 + "gix-filter",
3457 + "gix-fs",
3458 + "gix-hash",
3459 + "gix-imara-diff",
3460 + "gix-object",
3461 + "gix-path",
3462 + "gix-tempfile",
3463 + "gix-trace",
3464 + "gix-traverse",
3465 + "gix-worktree",
3466 + "thiserror 2.0.18",
3467 + ]
3468 +
3469 + [[package]]
3470 + name = "gix-discover"
3471 + version = "0.54.0"
3472 + source = "registry+https://github.com/rust-lang/crates.io-index"
3473 + checksum = "b9f517766fa1101dfe2606c1a19a8ffa699099030995a9194445446dfe261bdf"
3474 + dependencies = [
3475 + "bstr",
3476 + "dunce",
3477 + "gix-fs",
3478 + "gix-path",
3479 + "gix-ref",
3480 + "gix-sec",
3481 + "thiserror 2.0.18",
3482 + ]
3483 +
3484 + [[package]]
3485 + name = "gix-error"
3486 + version = "0.2.5"
3487 + source = "registry+https://github.com/rust-lang/crates.io-index"
3488 + checksum = "4a9292309fd944e71b2a3c96d3c03a6feb8852db646febdde7cbb9f79cb5f329"
3489 + dependencies = [
3490 + "bstr",
3491 + ]
3492 +
3493 + [[package]]
3494 + name = "gix-features"
3495 + version = "0.49.0"
3496 + source = "registry+https://github.com/rust-lang/crates.io-index"
3497 + checksum = "20aa09e83a48dc02c5f5f08578aa79d3ab1bab4618b8c362f88684645a02bdcc"
3498 + dependencies = [
3499 + "bytes",
3500 + "crc32fast",
3501 + "crossbeam-channel",
3502 + "gix-path",
3503 + "gix-trace",
3504 + "gix-utils",
3505 + "libc",
3506 + "once_cell",
3507 + "parking_lot",
3508 + "prodash",
3509 + "walkdir",
3510 + ]
3511 +
3512 + [[package]]
3513 + name = "gix-filter"
3514 + version = "0.33.0"
3515 + source = "registry+https://github.com/rust-lang/crates.io-index"
3516 + checksum = "5e7b5dbf524d97e839f642930c76d7f011c0791e7d11d8148989ac5af7c76aa8"
3517 + dependencies = [
3518 + "bstr",
3519 + "encoding_rs",
3520 + "gix-attributes",
3521 + "gix-command",
3522 + "gix-hash",
3523 + "gix-object",
3524 + "gix-packetline",
3525 + "gix-path",
3526 + "gix-quote",
3527 + "gix-trace",
3528 + "gix-utils",
3529 + "smallvec",
3530 + "thiserror 2.0.18",
3531 + ]
3532 +
3533 + [[package]]
3534 + name = "gix-fs"
3535 + version = "0.22.0"
3536 + source = "registry+https://github.com/rust-lang/crates.io-index"
3537 + checksum = "865cf13fcaf5455220546cb9607c416bd1be9a6caafd143655a362fdeab64e80"
3538 + dependencies = [
3539 + "bstr",
3540 + "gix-features",
3541 + "gix-path",
3542 + "gix-utils",
3543 + "thiserror 2.0.18",
3544 + ]
3545 +
3546 + [[package]]
3547 + name = "gix-glob"
3548 + version = "0.27.0"
3549 + source = "registry+https://github.com/rust-lang/crates.io-index"
3550 + checksum = "421e92a711554fa5827d1b0599d3389acdd0f6729e97a8c5a57d79af1e50bf36"
3551 + dependencies = [
3552 + "bitflags 2.13.0",
3553 + "bstr",
3554 + "gix-features",
3555 + "gix-path",
3556 + ]
3557 +
3558 + [[package]]
3559 + name = "gix-hash"
3560 + version = "0.26.0"
3561 + source = "registry+https://github.com/rust-lang/crates.io-index"
3562 + checksum = "13adaa73415fd6c902310923f68d0b98e8cecf14b33ea58c02cc387cee56f54e"
3563 + dependencies = [
3564 + "faster-hex",
3565 + "gix-features",
3566 + "sha1-checked",
3567 + "thiserror 2.0.18",
3568 + ]
3569 +
3570 + [[package]]
3571 + name = "gix-hashtable"
3572 + version = "0.16.0"
3573 + source = "registry+https://github.com/rust-lang/crates.io-index"
3574 + checksum = "78fccd6fea3bcf0b39c076bae60ae49b08daaf538b950202101a981f9d3c01d3"
3575 + dependencies = [
3576 + "gix-hash",
3577 + "hashbrown 0.17.1",
3578 + "parking_lot",
3579 + ]
3580 +
3581 + [[package]]
3582 + name = "gix-ignore"
3583 + version = "0.22.0"
3584 + source = "registry+https://github.com/rust-lang/crates.io-index"
3585 + checksum = "12cff8e8aa125e39377456073e63df3334d9e5741372ddcc226198015076dda2"
3586 + dependencies = [
3587 + "bstr",
3588 + "gix-glob",
3589 + "gix-path",
3590 + "gix-trace",
3591 + "unicode-bom",
3592 + ]
3593 +
3594 + [[package]]
3595 + name = "gix-imara-diff"
3596 + version = "0.2.4"
3597 + source = "registry+https://github.com/rust-lang/crates.io-index"
3598 + checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1"
3599 + dependencies = [
3600 + "bstr",
3601 + "hashbrown 0.15.5",
3602 + ]
3603 +
3604 + [[package]]
3605 + name = "gix-index"
3606 + version = "0.54.0"
3607 + source = "registry+https://github.com/rust-lang/crates.io-index"
3608 + checksum = "5009c4e7e9f9b4cfaaab1153e49133eb04d79c015b5702d6c3d2ab94271a89c6"
3609 + dependencies = [
3610 + "bitflags 2.13.0",
3611 + "bstr",
3612 + "filetime",
3613 + "fnv",
3614 + "gix-bitmap",
3615 + "gix-features",
3616 + "gix-fs",
3617 + "gix-hash",
3618 + "gix-lock",
3619 + "gix-object",
3620 + "gix-traverse",
3621 + "gix-utils",
3622 + "gix-validate",
3623 + "hashbrown 0.17.1",
3624 + "itoa",
3625 + "libc",
3626 + "memmap2",
3627 + "rustix 1.1.4",
3628 + "smallvec",
3629 + "thiserror 2.0.18",
3630 + ]
3631 +
3632 + [[package]]
3633 + name = "gix-lock"
3634 + version = "24.0.0"
3635 + source = "registry+https://github.com/rust-lang/crates.io-index"
3636 + checksum = "d4c69157820343bf1c6e4b88b9808e920900de02e18aaf5862b30ada43814848"
3637 + dependencies = [
3638 + "gix-tempfile",
3639 + "gix-utils",
3640 + "thiserror 2.0.18",
3641 + ]
3642 +
3643 + [[package]]
3644 + name = "gix-object"
3645 + version = "0.63.0"
3646 + source = "registry+https://github.com/rust-lang/crates.io-index"
3647 + checksum = "0e48c235e7f886eb819fc878af75be889333dd3c38bee02ed7af48ae2cf596c4"
3648 + dependencies = [
3649 + "bstr",
3650 + "gix-actor",
3651 + "gix-date",
3652 + "gix-features",
3653 + "gix-hash",
3654 + "gix-hashtable",
3655 + "gix-utils",
3656 + "gix-validate",
Lines truncated
@@ -127,8 +127,11 @@
127 127 # Shared theme palette + the bundled theme set (Tier 0 creator theming).
128 128 makeover = "2.4.1"
129 129
130 - # Git source browser
130 + # Git source browser. Migrating off git2 (libgit2, GPL-2.0 with linking
131 + # exception) onto gix, which is MIT/Apache-2.0. Both are linked while the port
132 + # is in flight; git2 goes away when the last read path moves over.
131 133 git2 = { version = "0.21", features = ["vendored-libgit2"] }
134 + gix = { version = "0.86", default-features = false, features = ["blame", "revision", "sha1", "max-performance-safe"] }
132 135 syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "html", "regex-fancy"] }
133 136 regex = "1"
134 137 semver = "1"
@@ -330,7 +330,13 @@
330 330 // line, so blaming a large file is expensive; `read_file` returns early for
331 331 // binary and over-cap blobs without loading them, so checking it first avoids
332 332 // the walk entirely on a file we won't render (ultra-fuzz Run 6 R6-Sec-L3).
333 - let file = super::read_file(repo, commit_oid, path)?;
333 + //
334 + // The gate reads through gitoxide because `read_file` has already been
335 + // ported; the blame walk below is still libgit2. Both handles open the same
336 + // path, and the second open goes away when this function ports.
337 + let gix_repo = super::open_gix_repo_at(repo.path())?;
338 + let gix_oid = gix::ObjectId::from_bytes_or_panic(commit_oid.as_bytes());
339 + let file = super::read_file(&gix_repo, gix_oid, path)?;
334 340 if file.is_binary || file.size > crate::constants::GIT_MAX_FILE_SIZE_BYTES as u64 {
335 341 return Err(GitError::PathNotFound);
336 342 }
@@ -272,6 +272,28 @@
272 272 Repository::open_bare(repo_path).map_err(|_| GitError::RepoNotFound)
273 273 }
274 274
275 + /// Open a bare repository with gitoxide at an already-resolved, validated path.
276 + ///
277 + /// Sibling of [`open_repo_at`] while the libgit2 port is in flight. Unlike
278 + /// `git2::Repository`, `gix::Repository` is `Send`, so once every read path has
279 + /// moved over, the reopen-per-call dance in `routes::git::ResolvedRepo` can go
280 + /// away with it.
281 + /// Opened with `isolated()` options, so no ambient git configuration (the
282 + /// service account's `~/.gitconfig`, `/etc/gitconfig`, `GIT_*` environment
283 + /// variables) can change how a visitor's repository renders.
284 + pub(crate) fn open_gix_repo_at(repo_path: &Path) -> Result<gix::Repository, GitError> {
285 + gix::open_opts(repo_path, gix::open::Options::isolated()).map_err(|_| GitError::RepoNotFound)
286 + }
287 +
288 + /// Convert a gitoxide object id to a libgit2 one.
289 + ///
290 + /// Transitional: the ported modules hand back `gix::ObjectId` while
291 + /// `git/history.rs` still takes `git2::Oid`. Delete this along with the last
292 + /// git2 read path.
293 + pub(crate) fn to_git2_oid(id: gix::ObjectId) -> git2::Oid {
294 + git2::Oid::from_bytes(id.as_bytes()).expect("gix and libgit2 agree on hash length")
295 + }
296 +
275 297 /// Get basic repository info.
276 298 pub fn repo_info(repo: &Repository, name: &str) -> RepoInfo {
277 299 let description = std::fs::read_to_string(repo.path().join("description"))
@@ -502,6 +524,13 @@
502 524 (tmp, bare_path)
503 525 }
504 526
527 + /// Open the same test repo through gitoxide. The fixtures are still built
528 + /// with git2 (write paths are not part of this migration), while the ported
529 + /// read paths take a `gix::Repository`.
530 + fn open_gix(bare_path: &std::path::Path) -> gix::Repository {
531 + open_gix_repo_at(bare_path).unwrap()
532 + }
533 +
505 534 #[test]
506 535 fn open_repo_valid() {
507 536 let (tmp, _) = make_test_repo();
@@ -554,8 +583,8 @@
554 583
555 584 #[test]
556 585 fn resolve_ref_and_list_tree() {
557 - let (tmp, _) = make_test_repo();
558 - let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
586 + let (_tmp, bare_path) = make_test_repo();
587 + let repo = open_gix(&bare_path);
559 588 let oid = resolve_ref(&repo, "main").unwrap();
560 589
561 590 let items = list_tree(&repo, oid, "").unwrap();
@@ -568,8 +597,8 @@
568 597
569 598 #[test]
570 599 fn list_tree_subdir() {
571 - let (tmp, _) = make_test_repo();
572 - let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
600 + let (_tmp, bare_path) = make_test_repo();
601 + let repo = open_gix(&bare_path);
573 602 let oid = resolve_ref(&repo, "main").unwrap();
574 603
575 604 let items = list_tree(&repo, oid, "src").unwrap();
@@ -580,8 +609,8 @@
580 609
581 610 #[test]
582 611 fn read_file_content() {
583 - let (tmp, _) = make_test_repo();
584 - let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
612 + let (_tmp, bare_path) = make_test_repo();
613 + let repo = open_gix(&bare_path);
585 614 let oid = resolve_ref(&repo, "main").unwrap();
586 615
587 616 let file = read_file(&repo, oid, "src/main.rs").unwrap();
@@ -591,19 +620,19 @@
591 620
592 621 #[test]
593 622 fn read_file_nonexistent() {
594 - let (tmp, _) = make_test_repo();
595 - let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
623 + let (_tmp, bare_path) = make_test_repo();
624 + let repo = open_gix(&bare_path);
596 625 let oid = resolve_ref(&repo, "main").unwrap();
597 626 assert!(read_file(&repo, oid, "nope.txt").is_err());
598 627 }
599 628
600 629 #[test]
601 630 fn commit_log_returns_commits() {
602 - let (tmp, _) = make_test_repo();
631 + let (tmp, bare_path) = make_test_repo();
603 632 let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
604 - let oid = resolve_ref(&repo, "main").unwrap();
633 + let oid = resolve_ref(&open_gix(&bare_path), "main").unwrap();
605 634
606 - let commits = commit_log(&repo, oid, 10, 0).unwrap();
635 + let commits = commit_log(&repo, to_git2_oid(oid), 10, 0).unwrap();
607 636 assert_eq!(commits.len(), 1);
608 637 assert_eq!(commits[0].summary, "Initial commit");
609 638 assert_eq!(commits[0].author_name, "Test");
@@ -611,8 +640,8 @@
611 640
612 641 #[test]
613 642 fn find_readme_returns_rendered_markdown() {
614 - let (tmp, _) = make_test_repo();
615 - let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
643 + let (_tmp, bare_path) = make_test_repo();
644 + let repo = open_gix(&bare_path);
616 645 let oid = resolve_ref(&repo, "main").unwrap();
617 646
618 647 let readme = find_readme(&repo, oid);
@@ -624,8 +653,8 @@
624 653
625 654 #[test]
626 655 fn list_refs_includes_branch() {
627 - let (tmp, _) = make_test_repo();
628 - let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
656 + let (_tmp, bare_path) = make_test_repo();
657 + let repo = open_gix(&bare_path);
629 658
630 659 let refs = list_refs(&repo);
631 660 assert!(refs.iter().any(|r| r.name == "main" && r.is_branch));
@@ -718,9 +747,9 @@
718 747
719 748 #[test]
720 749 fn file_commit_log_filters_by_path() {
721 - let (tmp, _) = make_two_commit_repo();
750 + let (tmp, bare_path) = make_two_commit_repo();
722 751 let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
723 - let oid = resolve_ref(&repo, "main").unwrap();
752 + let oid = to_git2_oid(resolve_ref(&open_gix(&bare_path), "main").unwrap());
724 753
725 754 // src/main.rs was changed in both commits
726 755 let main_rs_log = file_commit_log(&repo, oid, "src/main.rs", 10, 0, 1000).unwrap();
@@ -751,9 +780,9 @@
751 780
752 781 #[test]
753 782 fn file_commit_log_pagination() {
754 - let (tmp, _) = make_two_commit_repo();
783 + let (tmp, bare_path) = make_two_commit_repo();
755 784 let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap();
756 - let oid = resolve_ref(&repo, "main").unwrap();
785 + let oid = to_git2_oid(resolve_ref(&open_gix(&bare_path), "main").unwrap());
757 786
758 787 // Get first commit only (limit=1)
759 788 let page1 = file_commit_log(&repo, oid, "src/main.rs", 1, 0, 1000).unwrap();
@@ -1,8 +1,6 @@
1 1 //! Object reading, tree listing, file content, and README detection.
2 2
3 - use std::path::Path;
4 -
5 - use git2::{ObjectType, Oid, Repository};
3 + use gix::{ObjectId, Repository, objs::tree::EntryKind};
6 4
7 5 use super::{FileContent, GitError, TreeItem, TreeItemKind};
8 6 use crate::constants;
@@ -10,38 +8,38 @@
10 8 /// List entries in a tree at the given path (empty string for root).
11 9 pub fn list_tree(
12 10 repo: &Repository,
13 - commit_oid: Oid,
11 + commit_oid: ObjectId,
14 12 path: &str,
15 13 ) -> Result<Vec<TreeItem>, GitError> {
16 14 let commit = repo
17 15 .find_commit(commit_oid)
18 16 .map_err(|_| GitError::RefNotFound)?;
19 - let tree = commit.tree().map_err(|_| GitError::TreeNotFound)?;
17 + let mut tree = commit.tree().map_err(|_| GitError::TreeNotFound)?;
20 18
21 19 let target_tree = if path.is_empty() {
22 20 tree
23 21 } else {
24 - let entry = tree
25 - .get_path(Path::new(path))
26 - .map_err(|_| GitError::PathNotFound)?;
27 - let obj = entry.to_object(repo).map_err(|_| GitError::PathNotFound)?;
28 - obj.peel_to_tree().map_err(|_| GitError::PathNotFound)?
22 + tree.peel_to_entry_by_path(path)
23 + .map_err(|_| GitError::PathNotFound)?
24 + .ok_or(GitError::PathNotFound)?
25 + .object()
26 + .map_err(|_| GitError::PathNotFound)?
27 + .peel_to_tree()
28 + .map_err(|_| GitError::PathNotFound)?
29 29 };
30 30
31 31 let mut items: Vec<TreeItem> = target_tree
32 32 .iter()
33 33 .filter_map(|entry| {
34 - let name = entry.name().ok()?.to_string();
35 - let kind = match entry.kind()? {
36 - ObjectType::Tree => TreeItemKind::Dir,
37 - ObjectType::Blob => TreeItemKind::File,
34 + let entry = entry.ok()?;
35 + let name = entry.filename().to_string();
36 + let kind = match entry.mode().kind() {
37 + EntryKind::Tree => TreeItemKind::Dir,
38 + EntryKind::Blob | EntryKind::BlobExecutable => TreeItemKind::File,
38 39 _ => return None,
39 40 };
40 41 let size = if kind == TreeItemKind::File {
41 - entry
42 - .to_object(repo)
43 - .ok()
44 - .and_then(|o| o.as_blob().map(|b| b.size() as u64))
42 + repo.find_header(entry.oid()).ok().map(|h| h.size())
45 43 } else {
46 44 None
47 45 };
@@ -60,20 +58,14 @@
60 58 }
61 59
62 60 /// Read a file blob at the given path.
63 - pub fn read_file(repo: &Repository, commit_oid: Oid, path: &str) -> Result<FileContent, GitError> {
64 - let commit = repo
65 - .find_commit(commit_oid)
66 - .map_err(|_| GitError::RefNotFound)?;
67 - let tree = commit.tree().map_err(|_| GitError::TreeNotFound)?;
61 + pub fn read_file(
62 + repo: &Repository,
63 + commit_oid: ObjectId,
64 + path: &str,
65 + ) -> Result<FileContent, GitError> {
66 + let content = read_blob_at(repo, commit_oid, path)?;
68 67
69 - let entry = tree
70 - .get_path(Path::new(path))
71 - .map_err(|_| GitError::PathNotFound)?;
72 - let obj = entry.to_object(repo).map_err(|_| GitError::PathNotFound)?;
73 - let blob = obj.as_blob().ok_or(GitError::PathNotFound)?;
74 -
75 - let size = blob.size() as u64;
76 - let content = blob.content();
68 + let size = content.len() as u64;
77 69
78 70 // Binary detection: check first 8KB for null bytes
79 71 let check_len = content.len().min(8192);
@@ -95,7 +87,7 @@
95 87 });
96 88 }
97 89
98 - let text = String::from_utf8_lossy(content).into_owned();
90 + let text = String::from_utf8_lossy(&content).into_owned();
99 91 Ok(FileContent {
100 92 content: text,
101 93 size,
@@ -103,11 +95,29 @@
103 95 })
104 96 }
105 97
106 - /// Look for a README file at the tree root.
107 - pub fn find_readme(repo: &Repository, commit_oid: Oid) -> Option<String> {
108 - let commit = repo.find_commit(commit_oid).ok()?;
109 - let tree = commit.tree().ok()?;
98 + /// Read the raw bytes of a blob at `path` in the tree of `commit_oid`.
99 + fn read_blob_at(repo: &Repository, commit_oid: ObjectId, path: &str) -> Result<Vec<u8>, GitError> {
100 + let commit = repo
101 + .find_commit(commit_oid)
102 + .map_err(|_| GitError::RefNotFound)?;
103 + let mut tree = commit.tree().map_err(|_| GitError::TreeNotFound)?;
110 104
105 + let object = tree
106 + .peel_to_entry_by_path(path)
107 + .map_err(|_| GitError::PathNotFound)?
108 + .ok_or(GitError::PathNotFound)?
109 + .object()
110 + .map_err(|_| GitError::PathNotFound)?;
111 +
112 + if object.kind != gix::object::Kind::Blob {
113 + return Err(GitError::PathNotFound);
114 + }
115 +
116 + Ok(object.detach().data)
117 + }
118 +
119 + /// Look for a README file at the tree root.
120 + pub fn find_readme(repo: &Repository, commit_oid: ObjectId) -> Option<String> {
111 121 let readme_names = [
112 122 "README.md",
113 123 "readme.md",
@@ -117,29 +127,27 @@
117 127 ];
118 128
119 129 for name in &readme_names {
120 - if let Ok(entry) = tree.get_path(Path::new(name))
121 - && let Ok(obj) = entry.to_object(repo)
122 - && let Some(blob) = obj.as_blob()
123 - {
124 - // Skip oversized READMEs to avoid unbounded memory allocation
125 - if blob.size() > constants::GIT_MAX_FILE_SIZE_BYTES {
126 - return Some("<p><em>README too large to display.</em></p>".to_string());
127 - }
128 - let content = blob.content();
129 - if !content[..content.len().min(8192)].contains(&0) {
130 - let text = String::from_utf8_lossy(content);
131 - if std::path::Path::new(name)
132 - .extension()
133 - .is_some_and(|e| e == "md")
134 - {
135 - return Some(docengine::render_permissive(&text));
136 - }
137 - let escaped = text
138 - .replace('&', "&amp;")
139 - .replace('<', "&lt;")
140 - .replace('>', "&gt;");
141 - return Some(format!("<pre>{escaped}</pre>"));
130 + let Ok(content) = read_blob_at(repo, commit_oid, name) else {
131 + continue;
132 + };
133 +
134 + // Skip oversized READMEs to avoid unbounded memory allocation
135 + if content.len() > constants::GIT_MAX_FILE_SIZE_BYTES {
136 + return Some("<p><em>README too large to display.</em></p>".to_string());
137 + }
138 + if !content[..content.len().min(8192)].contains(&0) {
139 + let text = String::from_utf8_lossy(&content);
140 + if std::path::Path::new(name)
141 + .extension()
142 + .is_some_and(|e| e == "md")
143 + {
144 + return Some(docengine::render_permissive(&text));
142 145 }
146 + let escaped = text
147 + .replace('&', "&amp;")
148 + .replace('<', "&lt;")
149 + .replace('>', "&gt;");
150 + return Some(format!("<pre>{escaped}</pre>"));
143 151 }
144 152 }
145 153
@@ -1,31 +1,30 @@
1 1 //! Reference management, branch/tag listing and ref resolution.
2 2
3 + use gix::{ObjectId, Repository, bstr::ByteSlice};
4 +
3 5 use super::{GitError, RefInfo};
4 - use git2::{ObjectType, Oid, Repository};
5 6
6 7 /// Resolve a ref name to a commit OID. Tries branch, tag, raw OID, then revparse.
7 - pub fn resolve_ref(repo: &Repository, refname: &str) -> Result<Oid, GitError> {
8 + pub fn resolve_ref(repo: &Repository, refname: &str) -> Result<ObjectId, GitError> {
8 9 // Try as branch
9 - if let Ok(branch) = repo.find_branch(refname, git2::BranchType::Local)
10 - && let Some(target) = branch.get().target()
10 + if let Ok(mut reference) = repo.find_reference(&format!("refs/heads/{refname}"))
11 + && let Ok(id) = reference.peel_to_id()
11 12 {
12 - return Ok(target);
13 + return Ok(id.detach());
13 14 }
14 15
15 - // Try as tag
16 - if let Ok(reference) = repo.find_reference(&format!("refs/tags/{refname}")) {
17 - let obj = reference
18 - .peel(ObjectType::Commit)
19 - .map_err(|_| GitError::RefNotFound)?;
20 - return Ok(obj.id());
16 + // Try as tag. Peeling follows an annotated tag through to the commit it
17 + // points at, which is what `peel(ObjectType::Commit)` did under libgit2.
18 + if let Ok(mut reference) = repo.find_reference(&format!("refs/tags/{refname}")) {
19 + let id = reference.peel_to_id().map_err(|_| GitError::RefNotFound)?;
20 + return Ok(id.detach());
21 21 }
22 22
23 23 // Try as raw OID
24 - if let Ok(oid) = Oid::from_str(refname) {
25 - // Verify it resolves to a commit
26 - if repo.find_commit(oid).is_ok() {
27 - return Ok(oid);
28 - }
24 + if let Ok(oid) = ObjectId::from_hex(refname.as_bytes())
25 + && repo.find_commit(oid).is_ok()
26 + {
27 + return Ok(oid);
29 28 }
30 29
31 30 // Reject arbitrary revparse expressions (e.g. HEAD~99999, @{upstream}).
@@ -33,10 +32,11 @@
33 32 if refname
34 33 .chars()
35 34 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '/'))
36 - && let Ok(obj) = repo.revparse_single(refname)
37 - && let Ok(commit) = obj.peel(ObjectType::Commit)
35 + && let Ok(id) = repo.rev_parse_single(refname)
36 + && let Ok(object) = id.object()
37 + && let Ok(commit) = object.peel_to_commit()
38 38 {
39 - return Ok(commit.id());
39 + return Ok(commit.id);
40 40 }
41 41
42 42 Err(GitError::RefNotFound)
@@ -46,21 +46,23 @@
46 46 pub fn list_refs(repo: &Repository) -> Vec<RefInfo> {
47 47 let mut refs = Vec::new();
48 48
49 - if let Ok(branches) = repo.branches(Some(git2::BranchType::Local)) {
49 + let Ok(platform) = repo.references() else {
50 + return refs;
51 + };
52 +
53 + if let Ok(branches) = platform.prefixed("refs/heads/") {
50 54 for branch in branches.flatten() {
51 - if let Some(name) = branch.0.name().ok().flatten() {
52 - refs.push(RefInfo {
53 - name: name.to_string(),
54 - is_branch: true,
55 - });
56 - }
55 + refs.push(RefInfo {
56 + name: branch.name().shorten().to_str_lossy().into_owned(),
57 + is_branch: true,
58 + });
57 59 }
58 60 }
59 61
60 - if let Ok(tags) = repo.tag_names(None) {
61 - for tag in tags.iter().flatten().flatten() {
62 + if let Ok(tags) = platform.prefixed("refs/tags/") {
63 + for tag in tags.flatten() {
62 64 refs.push(RefInfo {
63 - name: tag.to_string(),
65 + name: tag.name().shorten().to_str_lossy().into_owned(),
64 66 is_branch: false,
65 67 });
66 68 }
@@ -48,12 +48,12 @@
48 48 .await?;
49 49 let repo_name_c = repo_name.clone();
50 50 let (info, refs, tree_items, readme_html) = resolved
51 - .with_repo(move |repo| {
51 + .with_repo(move |repo, gix_repo| {
52 52 let info = git::repo_info(repo, &repo_name_c);
53 - let refs = git::list_refs(repo);
54 - let commit_oid = git::resolve_ref(repo, &info.default_branch)?;
55 - let tree_items = git::list_tree(repo, commit_oid, "")?;
56 - let readme_html = git::find_readme(repo, commit_oid);
53 + let refs = git::list_refs(gix_repo);
54 + let commit_oid = git::resolve_ref(gix_repo, &info.default_branch)?;
55 + let tree_items = git::list_tree(gix_repo, commit_oid, "")?;
56 + let readme_html = git::find_readme(gix_repo, commit_oid);
57 57 Ok((info, refs, tree_items, readme_html))
58 58 })
59 59 .await?;
@@ -111,11 +111,11 @@
111 111 .await?;
112 112 let git_ref_c = git_ref.clone();
113 113 let (refs, tree_items, readme_html) = resolved
114 - .with_repo(move |repo| {
115 - let refs = git::list_refs(repo);
116 - let commit_oid = git::resolve_ref(repo, &git_ref_c)?;
117 - let tree_items = git::list_tree(repo, commit_oid, "")?;
118 - let readme_html = git::find_readme(repo, commit_oid);
114 + .with_repo(move |_repo, gix_repo| {
115 + let refs = git::list_refs(gix_repo);
116 + let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?;
117 + let tree_items = git::list_tree(gix_repo, commit_oid, "")?;
118 + let readme_html = git::find_readme(gix_repo, commit_oid);
119 119 Ok((refs, tree_items, readme_html))
120 120 })
121 121 .await?;
@@ -186,13 +186,13 @@
186 186 let git_ref_c = git_ref.clone();
187 187 let path_c = path.clone();
188 188 let dir_or_file = resolved
189 - .with_repo(move |repo| {
190 - let commit_oid = git::resolve_ref(repo, &git_ref_c)?;
191 - match git::list_tree(repo, commit_oid, &path_c) {
192 - Ok(tree_items) => Ok(DirOrFile::Dir(tree_items, git::list_refs(repo))),
189 + .with_repo(move |_repo, gix_repo| {
190 + let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?;
191 + match git::list_tree(gix_repo, commit_oid, &path_c) {
192 + Ok(tree_items) => Ok(DirOrFile::Dir(tree_items, git::list_refs(gix_repo))),
193 193 Err(_) => {
194 - let file_content = git::read_file(repo, commit_oid, &path_c)?;
195 - Ok(DirOrFile::File(file_content, git::list_refs(repo)))
194 + let file_content = git::read_file(gix_repo, commit_oid, &path_c)?;
195 + Ok(DirOrFile::File(file_content, git::list_refs(gix_repo)))
196 196 }
197 197 }
198 198 })
@@ -300,10 +300,10 @@
300 300
301 301 let git_ref_c = git_ref.clone();
302 302 let (refs, commits) = resolved
303 - .with_repo(move |repo| {
304 - let refs = git::list_refs(repo);
305 - let commit_oid = git::resolve_ref(repo, &git_ref_c)?;
306 - let commits = git::commit_log(repo, commit_oid, limit + 1, offset)?;
303 + .with_repo(move |repo, gix_repo| {
304 + let refs = git::list_refs(gix_repo);
305 + let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?;
306 + let commits = git::commit_log(repo, git::to_git2_oid(commit_oid), limit + 1, offset)?;
307 307 Ok((refs, commits))
308 308 })
309 309 .await?;
@@ -354,7 +354,7 @@
354 354 let oid = git2::Oid::from_str(&oid_str).map_err(|_| AppError::NotFound)?;
355 355 let repo_name_c = repo_name.clone();
356 356 let (detail, diff_files, refs, info) = resolved
357 - .with_repo(move |repo| {
357 + .with_repo(move |repo, gix_repo| {
358 358 repo.find_commit(oid).map_err(|_| AppError::NotFound)?;
359 359 let detail = git::commit_detail(repo, oid)?;
360 360 let diff_files = git::commit_diff(
@@ -363,7 +363,7 @@
363 363 constants::GIT_DIFF_MAX_FILES,
364 364 constants::GIT_DIFF_MAX_LINES,
365 365 )?;
366 - let refs = git::list_refs(repo);
366 + let refs = git::list_refs(gix_repo);
367 367 let info = git::repo_info(repo, &repo_name_c);
368 368 Ok((detail, diff_files, refs, info))
369 369 })
@@ -417,10 +417,10 @@
417 417 let git_ref_c = git_ref.clone();
418 418 let path_c = path.clone();
419 419 let (blame_lines, refs) = resolved
420 - .with_repo(move |repo| {
421 - let commit_oid = git::resolve_ref(repo, &git_ref_c)?;
422 - let blame_lines = git::blame_file(repo, commit_oid, &path_c)?;
423 - let refs = git::list_refs(repo);
420 + .with_repo(move |repo, gix_repo| {
421 + let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?;
422 + let blame_lines = git::blame_file(repo, git::to_git2_oid(commit_oid), &path_c)?;
423 + let refs = git::list_refs(gix_repo);
424 424 Ok((blame_lines, refs))
425 425 })
426 426 .await?;
@@ -544,12 +544,12 @@
544 544 let git_ref_c = git_ref.clone();
545 545 let path_c = path.clone();
546 546 let (refs, commits) = resolved
547 - .with_repo(move |repo| {
548 - let refs = git::list_refs(repo);
549 - let commit_oid = git::resolve_ref(repo, &git_ref_c)?;
547 + .with_repo(move |repo, gix_repo| {
548 + let refs = git::list_refs(gix_repo);
549 + let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?;
550 550 let commits = git::file_commit_log(
551 551 repo,
552 - commit_oid,
552 + git::to_git2_oid(commit_oid),
553 553 &path_c,
554 554 limit + 1,
555 555 offset,
@@ -140,8 +140,8 @@
140 140 ///
141 141 /// We store the *path* rather than an open `git2::Repository` because the repo
142 142 /// handle is `!Send` and would pin the synchronous libgit2 work to the async
143 - /// runtime. Handlers run their libgit2 reads through [`ResolvedRepo::with_repo`],
144 - /// which reopens the repo on the blocking pool.
143 + /// runtime. Handlers run their reads through [`ResolvedRepo::with_repo`], which
144 + /// reopens the repo on the blocking pool.
145 145 pub(crate) struct ResolvedRepo {
146 146 pub(crate) db_repo: DbGitRepo,
147 147 pub(crate) repo_path: PathBuf,
@@ -149,19 +149,25 @@
149 149 }
150 150
151 151 impl ResolvedRepo {
152 - /// Run a synchronous libgit2 closure on the blocking pool against a freshly
153 - /// opened handle. `git2::Repository` is `!Send`, so the handle is reopened
154 - /// inside the closure rather than moved across the await. Keeps libgit2's
155 - /// disk + zlib work off the Tokio worker threads (Run #14 Perf LOW).
152 + /// Run a synchronous git closure on the blocking pool against freshly opened
153 + /// handles. `git2::Repository` is `!Send`, so the handle is reopened inside
154 + /// the closure rather than moved across the await. Keeps the disk + zlib
155 + /// work off the Tokio worker threads (Run #14 Perf LOW).
156 + ///
157 + /// Both handles are passed while the port from libgit2 to gitoxide is in
158 + /// flight: ported reads take the `gix` handle, the rest still take the
159 + /// `git2` one. The `git2` parameter goes away with the last git2 read path,
160 + /// and the reopen itself can go with it, since `gix::Repository` is `Send`.
156 161 pub(crate) async fn with_repo<T, F>(&self, f: F) -> Result<T>
157 162 where
158 163 T: Send + 'static,
159 - F: FnOnce(&git2::Repository) -> Result<T> + Send + 'static,
164 + F: FnOnce(&git2::Repository, &gix::Repository) -> Result<T> + Send + 'static,
160 165 {
161 166 let path = self.repo_path.clone();
162 167 tokio::task::spawn_blocking(move || {
163 168 let repo = git::open_repo_at(&path)?;
164 - f(&repo)
169 + let gix_repo = git::open_gix_repo_at(&path)?;
170 + f(&repo, &gix_repo)
165 171 })
166 172 .await
167 173 .map_err(|e| AppError::Internal(anyhow::anyhow!("git worker task failed: {e}")))?
@@ -42,20 +42,27 @@
42 42 let git_ref_c = git_ref.clone();
43 43 let path_c = path.clone();
44 44 let content: Vec<u8> = resolved
45 - .with_repo(move |repo| {
46 - let commit_oid = git::resolve_ref(repo, &git_ref_c)?;
47 - let commit = repo
45 + .with_repo(move |_repo, gix_repo| {
46 + let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?;
47 + let commit = gix_repo
48 48 .find_commit(commit_oid)
49 49 .map_err(|_| AppError::NotFound)?;
50 - let tree = commit.tree().map_err(|_| AppError::NotFound)?;
50 + let mut tree = commit.tree().map_err(|_| AppError::NotFound)?;
51 51 let entry = tree
52 - .get_path(std::path::Path::new(&path_c))
53 - .map_err(|_| AppError::NotFound)?;
54 - let obj = entry.to_object(repo).map_err(|_| AppError::NotFound)?;
55 - let blob = obj.as_blob().ok_or(AppError::NotFound)?;
52 + .peel_to_entry_by_path(path_c.as_str())
53 + .map_err(|_| AppError::NotFound)?
54 + .ok_or(AppError::NotFound)?;
55 + if !entry.mode().is_blob() {
56 + return Err(AppError::NotFound);
57 + }
56 58
57 - // Prevent unbounded memory allocation from large blobs.
58 - let size = blob.size();
59 + // Prevent unbounded memory allocation from large blobs. The header
60 + // carries the size without decoding the object, so an over-cap blob
61 + // is rejected before it is read.
62 + let size = gix_repo
63 + .find_header(entry.oid())
64 + .map_err(|_| AppError::NotFound)?
65 + .size() as usize;
59 66 if size > constants::GIT_RAW_MAX_BYTES {
60 67 return Err(AppError::BadRequest(format!(
61 68 "File too large for raw download ({} bytes, max {})",
@@ -63,7 +70,8 @@
63 70 constants::GIT_RAW_MAX_BYTES,
64 71 )));
65 72 }
66 - Ok(blob.content().to_vec())
73 + let object = entry.object().map_err(|_| AppError::NotFound)?;
74 + Ok(object.detach().data)
67 75 })
68 76 .await?;
69 77