From 7f0a79aff2f0dad62ccd1c72854152710f5fee85 Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Wed, 15 Jan 2025 14:56:26 +0000 Subject: [PATCH 01/16] Add the ability to checkout git repositories * `clone_git_repository` to clone arbitrary repo * `Job::clone_git_repository` to clone the jobs repo --- Cargo.lock | 1468 +++++++++++++++++++++++-- gitlab-runner/Cargo.toml | 1 + gitlab-runner/examples/demo-runner.rs | 104 +- gitlab-runner/src/client.rs | 63 ++ gitlab-runner/src/job.rs | 48 +- gitlab-runner/src/lib.rs | 154 ++- 6 files changed, 1748 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cad791b..dbd4e2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "1.0.0" @@ -84,6 +90,21 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -154,18 +175,41 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + [[package]] name = "bzip2" version = "0.6.1" @@ -266,6 +310,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "cmake" version = "0.1.58" @@ -350,6 +403,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -395,6 +463,20 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deadpool" version = "0.12.3" @@ -518,12 +600,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -553,6 +656,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -640,120 +749,1055 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] -name = "futures-util" -version = "0.3.32" +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "gitlab-runner" +version = "0.3.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "clap", + "doc-comment", + "flate2", + "futures", + "gitlab-runner-mock", + "gix", + "glob", + "hmac", + "parking_lot", + "pin-project", + "rand 0.10.0", + "reqwest", + "serde", + "serde_json", + "sha2", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-retry2", + "tokio-util", + "tracing", + "tracing-subscriber", + "url", + "vergen-gitcl", + "zip", +] + +[[package]] +name = "gitlab-runner-mock" +version = "0.3.0" +dependencies = [ + "futures", + "http", + "multer", + "serde", + "serde_json", + "thiserror 2.0.18", + "url", + "wiremock", +] + +[[package]] +name = "gix" +version = "0.83.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce52001b946a6249d5d0d3011df0a042ac3f8a4d013460db6476577b0b9c567" +dependencies = [ + "gix-actor", + "gix-archive", + "gix-attributes", + "gix-blame", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-mailmap", + "gix-merge", + "gix-negotiate", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-prompt", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-transport", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-state", + "gix-worktree-stream", + "nonempty", + "parking_lot", + "regex", + "signal-hook", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-actor" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272916673b83714734b15d4ef3c8b5f1ccddb15fea8ff548430b97c1ab7b7ed8" +dependencies = [ + "bstr", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-archive" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a20ec244b733338d4cb60e5e05eac700dab7fcc689647b1d1daa9396b119342" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "gix-object", + "gix-worktree-stream", +] + +[[package]] +name = "gix-attributes" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe17c5a1c0b6f2ef1476aa1d3222ea50cdff67608016613a58bfc3e078046000" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecbfc77ec6852294e341ecc305a490b59f2813e6ca42d79efda5099dcab1894" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-blame" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dab9a942ab54a9661ded7397c3bf927274e7afa94494db0d75cfcbde02ca0a" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-diff", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "gix-traverse", + "gix-worktree", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-chunk" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf288be9b60fe7231de03771faa292be1493d84786f68727e33ad1f91764320" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86335306511abe43d75c866d4b1f3d90932fe202edcd43e1314036333e7384d8" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3b5aa0f24e19028c261d229aeeedafcaaa52ebd71021cc15184620fc9d32eb" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c01848aebd21c67f6ba41f1de8efd46ae96df21f001954a3c9e1517e514d410" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-config-value" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b39ed39ee4c10a3b157f9fb94bac8098d9f8e56201f0cf7dee6c187416c4b2" +dependencies = [ + "bitflags", + "bstr", + "gix-path", + "libc", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-credentials" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ca11598b70811d7b16ff90945a6e57dfe521e85b744e51636965fe39cc8f60" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-date", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-trace", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-date" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94cdae4eb4b0f4136e3d9b3aa2d2cd03cfb5bb9b636b31263aea2df86d41543" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", + "smallvec", +] + +[[package]] +name = "gix-diff" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc08e0fa1a91ff5f24affeab052f198056645e1de004910bde7b82b50ea5982a" +dependencies = [ + "bstr", + "gix-attributes", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-dir" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a0fc06e9e1e430cbf0a313666976d90f822f461a6525320427aa9b8af5236c" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-discover" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17852e6a501e688a1702b24ebe5b3761d4719455bc869fd29f38b0b859bcad34" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-error" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e207b971746ab724fccdfced2e4e19e854744611904a0195d3aa8fda8a110613" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af375693ad5333d0a2c66b4c5b2cbe9ccc38e34f8e8bf24e4ae42c12307fdc4f" +dependencies = [ + "bytes", + "bytesize", + "crc32fast", + "crossbeam-channel", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot", + "prodash", + "thiserror 2.0.18", + "walkdir", + "zlib-rs", +] + +[[package]] +name = "gix-filter" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac917dbe9653c9b615d248db91907a365bd779750c9e1b457a9d9fdeece3a08" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-fs" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e1967daac9848757c47c2aef0c57bcadc1a897347f559778249bf286a536c86" +dependencies = [ + "bstr", + "fastrand", + "gix-features", + "gix-path", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-glob" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bf29249a069bf2507f5964f80997f37b134d320ea348d66527726b9be2c38c" +dependencies = [ + "bitflags", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf70d1e252337eed16360f8b8ebb71865ece58eab7954b39ce38b420de703d2" +dependencies = [ + "faster-hex", + "gix-features", + "sha1-checked", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-hashtable" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d33b455e07b3c16d3b2eeebc7b38d2dafcbf8a653de1138ef55d4c2a1fd0b08b" +dependencies = [ + "gix-hash", + "hashbrown 0.16.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb13fbbeeafee943e52b61fcc88dfddf6a452fcaf0c4d0cdc8f218fa25bbec5" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-imara-diff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39eb0623e15e4cb83c02ce6a959e48fadd1ae3b715b36b5acc01816e01388c82" +dependencies = [ + "bstr", + "hashbrown 0.16.1", +] + +[[package]] +name = "gix-index" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c3ef97ad08121e4327a6226bd63fed6b9e3c6b976d48bddd4356d9d41191db" +dependencies = [ + "bitflags", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.16.1", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-lock" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3bc074e5723027b482dcd9ab99d95804a53742f6de812d0172fbba4a186c1" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-mailmap" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023d3a6561cbebe45b89e0764d48928ad970667076f16fa5889e6f86d8432086" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-merge" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74bbcdcc52b70a32f0a151b024dff9d0fcf56ee48f00d9503e735af9d99ea881" +dependencies = [ + "bstr", + "gix-command", + "gix-diff", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-quote", + "gix-revision", + "gix-revwalk", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-negotiate" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103d42bfade1b8a96ca5005933127bdad461ce588d92422b2c2daa3ff20d780c" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", +] + +[[package]] +name = "gix-object" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38075a95d7cc5df8afd38e72c617026c1456952207a4120a7f55a3fbf93b4d7" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-odb" +version = "0.80.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeeda12a9663120418735ecdc1250d06eeab0be75700e47b3402a981331716ba" +dependencies = [ + "arc-swap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "memmap2", + "parking_lot", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pack" +version = "0.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf02e6f5c8f07a069c9ea5245f40d9b14856ada4086091dc99941b49002b4fa" +dependencies = [ + "clru", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-tempfile", + "memmap2", + "parking_lot", + "smallvec", + "thiserror 2.0.18", + "uluru", +] + +[[package]] +name = "gix-packetline" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362246df440ee691699f0664cbf7006a6ece477db6734222be95e4198e5656e6" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a6059e8a4c1b7f406e24716499cefa3926e060876fb1959ef225efeee346e" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pathspec" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a84a4f083dd70fb49f4377e13afa6d90df2daaa1c705c49d6ff1331fc7e8855" +dependencies = [ + "bitflags", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-prompt" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e041a626c64cb69e4117fcdf80da8d0e454fba3b1f420412792d191f52251aee" +dependencies = [ + "gix-command", + "gix-config-value", + "parking_lot", + "rustix", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-protocol" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4bee82db63ec635996b96efae71cf467c155fa3f34a556184373224a26c4fd" +dependencies = [ + "bstr", + "gix-credentials", + "gix-date", + "gix-features", + "gix-hash", + "gix-lock", + "gix-negotiate", + "gix-object", + "gix-ref", + "gix-refspec", + "gix-revwalk", + "gix-shallow", + "gix-trace", + "gix-transport", + "gix-utils", + "maybe-async", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-quote" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e97b73791a64bc0fa7dd2c5b3e551136115f97750b876ed1c952c7a7dbaf8be" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ba9cc15f558b274c99349b83130f5ec83459660828fde9718bbbb43a726167" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-refspec" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61755b27d57edc8940a1b1593c8c61548ca8e4c02da1ed8d5bfeda9eb2a6b761" +dependencies = [ + "bstr", + "gix-error", + "gix-glob", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-revision" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb5288fac706d3ea3e4e2ba9ec38b78743b8c02f422e18cb342299cfd6ab7e8" +dependencies = [ + "bitflags", + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "gix-trace", + "nonempty", +] + +[[package]] +name = "gix-revwalk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313813706b073a12ff7f9b2896bf3e6504cdac7cfbc97b1920114724705069f0" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-sec" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3a2d3e504a238136751e646a6c028252286a0ea64ea9974bf0498633407c6" +dependencies = [ + "bitflags", + "gix-path", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-shallow" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29187305521bfacf4aefd284ab28dbfa9fb74abd39a5e63dd313b1baa5808c27" +dependencies = [ + "bstr", + "gix-hash", + "gix-lock", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-status" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c6d2a8c521ffa205fe7e268c82e6d1378ba37cd826ca10ab6129fdc29a4b65" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-submodule" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd5fc8692890bd71a596e540fd4c364f8460eaa82c4eaaedebde6e1e3eb4d91" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-tempfile" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691ea1e31435c7e7d4d04705ec9d1c0d9482c46b2acf512bc723939d8f0af7fb" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "parking_lot", + "signal-hook", + "signal-hook-registry", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f23569e55f2ffaf958617353b9734a7d52a7c19c439eeaa5e3efc217fd2270e" + +[[package]] +name = "gix-transport" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6a5c676b92d4ead5f5a2b2935024415dec69edc997b6090ca9cac010a3018" +dependencies = [ + "base64", + "bstr", + "gix-command", + "gix-credentials", + "gix-features", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "reqwest", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-traverse" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a14b7052c0786676c03e71fcfde7d7f0f8e8316e642b5cec6bb3998719b2ce5c" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.18", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "gix-url" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "35842d099e813f6f6bba529e88d4670572149c3df79b7a412952259887721ece" dependencies = [ - "typenum", - "version_check", + "bstr", + "gix-path", + "percent-encoding", + "thiserror 2.0.18", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "gix-utils" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "4e477b4f07a6e8da4ba791c53c858102959703c60d70f199932010d5b94adb2c" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", + "bstr", + "fastrand", + "unicode-normalization", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "gix-validate" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "e26ac2602b43eadfdca0560b81d3341944162a3c9f64ccdeef8fc501ad80dad5" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", + "bstr", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "gix-worktree" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "d69955eb5e2910832f88d041964b809eee01dadd579237e0b55efec58fd406fd" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.0", - "wasip2", - "wasip3", - "wasm-bindgen", + "bstr", + "gix-attributes", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", ] [[package]] -name = "gitlab-runner" -version = "0.3.0" +name = "gix-worktree-state" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a96dccbcf9e8fe0291c55f06e08da93ebb2e691c1311276f541eefcc6d70800" dependencies = [ - "anyhow", - "async-trait", - "bytes", - "clap", - "doc-comment", - "flate2", - "futures", - "gitlab-runner-mock", - "glob", - "hmac", - "parking_lot", - "pin-project", - "rand 0.10.0", - "reqwest", - "serde", - "serde_json", - "sha2", - "tempfile", + "bstr", + "gix-features", + "gix-filter", + "gix-fs", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", "thiserror 2.0.18", - "tokio", - "tokio-retry2", - "tokio-util", - "tracing", - "tracing-subscriber", - "url", - "vergen-gitcl", - "zip", ] [[package]] -name = "gitlab-runner-mock" -version = "0.3.0" +name = "gix-worktree-stream" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8444b8ed4662e1a0c97f3eceda29630001a1bbb2632201e50312623e594213" dependencies = [ - "futures", - "http", - "multer", - "serde", - "serde_json", - "thiserror 2.0.18", - "url", - "wiremock", + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", ] [[package]] @@ -781,13 +1825,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -795,6 +1854,21 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] [[package]] name = "heck" @@ -862,6 +1936,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "human_format" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" + [[package]] name = "hyper" version = "1.8.1" @@ -1061,6 +2141,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-close" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1089,6 +2179,47 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -1155,6 +2286,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1179,6 +2319,18 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1230,12 +2382,32 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -1290,6 +2462,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1360,7 +2538,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -1419,6 +2597,27 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1468,6 +2667,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prodash" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" +dependencies = [ + "bytesize", + "human_format", + "parking_lot", +] + [[package]] name = "quinn" version = "0.11.9" @@ -1600,6 +2810,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -1638,6 +2857,7 @@ dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -1894,6 +3114,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest", + "sha1", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1914,12 +3144,28 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -1970,6 +3216,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2354,18 +3606,42 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uluru" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" +dependencies = [ + "arrayvec", +] + [[package]] name = "unicase" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2625,6 +3901,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2634,6 +3926,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/gitlab-runner/Cargo.toml b/gitlab-runner/Cargo.toml index d054ad5..af15516 100644 --- a/gitlab-runner/Cargo.toml +++ b/gitlab-runner/Cargo.toml @@ -36,6 +36,7 @@ hmac = "0.12.1" rand = "0.10.0" tokio-util = { version = "0.7.18", features = [ "io" ] } tokio-retry2 = { version = "0.9.1", features = ["jitter"] } +gix = { version = "0.83.0", features = [ "blocking-http-transport-reqwest", "worktree-mutation" ] } [dev-dependencies] tokio = { version = "1.50.0", features = [ "full", "test-util" ] } diff --git a/gitlab-runner/examples/demo-runner.rs b/gitlab-runner/examples/demo-runner.rs index d4050b0..1216b89 100644 --- a/gitlab-runner/examples/demo-runner.rs +++ b/gitlab-runner/examples/demo-runner.rs @@ -7,7 +7,8 @@ use futures::AsyncRead; use futures::io::Cursor; use gitlab_runner::job::Job; use gitlab_runner::{ - GitlabLayer, JobHandler, JobResult, Phase, RunnerBuilder, UploadableFile, outputln, + GitlabLayer, JobHandler, JobResult, Phase, RunnerBuilder, UploadableFile, clone_git_repository, + outputln, }; use serde::Deserialize; use tokio::signal::unix::{SignalKind, signal}; @@ -123,6 +124,107 @@ impl Run { } Ok(()) } + "clone" => { + let url = match p.next() { + Some(url) => url, + _ => { + outputln!("Missing repo url as first argument"); + return Err(()); + } + }; + let path = match p.next() { + Some(path) => self.job.build_dir().join(path), + _ => { + outputln!("Missing repo output path as second argument"); + return Err(()); + } + }; + let reference = p.next(); + + let repo_path = + clone_git_repository(self.job.build_dir(), url, reference, p, Some(1)) + .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))?; + + std::fs::rename(repo_path, path) + .map_err(|e| outputln!("Failed to move repo path: {}", e.to_string()))?; + + Ok(()) + } + "checkout" => { + let path = match p.next() { + Some(path) => self.job.build_dir().join(path), + _ => self.job.build_dir().to_path_buf(), + }; + let temp_repo_path = self + .job + .clone_git_repository() + .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))?; + + outputln!("Checked out to {:?}", temp_repo_path); + + if !path.exists() { + std::fs::create_dir(&path).map_err(|e| { + outputln!("Failed to create dir for repo: {}", e.to_string()) + })?; + } + + let repo_read_dir = temp_repo_path.read_dir().map_err(|e| { + outputln!("Failed to enumerate repo dir: {}", e.to_string()) + })?; + for entry in repo_read_dir { + let entry = entry.map_err(|e| { + outputln!("Failed to read repo file: {}", e.to_string()) + })?; + std::fs::rename(entry.path(), path.join(entry.file_name())).map_err( + |e| outputln!("Failed to move repo file: {}", e.to_string()), + )?; + } + + std::fs::remove_dir_all(temp_repo_path).map_err(|e| { + outputln!("Failed to remove temp directory: {}", e.to_string()) + })?; + + outputln!("Moved repo to {:?}", path); + + Ok(()) + } + "list-dir" => { + let path = match p.next() { + Some(path) => self.job.build_dir().join(path), + _ => self.job.build_dir().to_path_buf(), + }; + if !path.exists() { + outputln!("Path {:?} doesn't exist", path); + } else { + for entry in path.read_dir().unwrap() { + match entry { + Ok(entry) => outputln!("{}", entry.file_name().to_str().unwrap()), + Err(e) => outputln!("Error: {:?}: {}", e, e.to_string()), + } + } + } + Ok(()) + } + "cat" => { + let path = match p.next() { + Some(path) => self.job.build_dir().join(path), + _ => { + outputln!("No path provided"); + return Err(()); + } + }; + if !path.exists() { + outputln!("Path {:?} doesn't exist", path); + Err(()) + } else { + let data = match std::fs::read_to_string(path) { + Ok(s) => s, + _ => "Failed to read file to text".to_string(), + }; + outputln!("{}", data); + Ok(()) + } + } _ => { outputln!("Unknown command\n"); Err(()) diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index 6de062b..2165480 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -224,6 +224,16 @@ pub(crate) struct JobDependency { pub artifacts_file: Option, } +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub(crate) struct JobGitInfo { + pub repo_url: String, + pub refspecs: Vec, + pub sha: String, + pub depth: u32, + #[serde(flatten)] + unparsed: JsonValue, +} + #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub(crate) struct JobResponse { pub id: u64, @@ -236,6 +246,7 @@ pub(crate) struct JobResponse { pub dependencies: Vec, #[serde(deserialize_with = "deserialize_null_default")] pub artifacts: Vec, + pub git_info: JobGitInfo, #[serde(flatten)] unparsed: JsonValue, } @@ -246,6 +257,58 @@ impl JobResponse { } } +#[derive(Error, Debug)] +pub enum GitCheckoutError { + #[error(transparent)] + GitClone(#[from] gix::clone::Error), + #[error(transparent)] + GitFetch(#[from] gix::clone::fetch::Error), + #[error(transparent)] + GitCheckout(#[from] gix::clone::checkout::main_worktree::Error), + #[error(transparent)] + GitIndexFileWrite(#[from] gix::index::file::write::Error), + #[error(transparent)] + GitRefValidate(#[from] gix::index::validate::reference::name::Error), + #[error(transparent)] + GitRemoteFind(#[from] gix::remote::find::existing::Error), + #[error(transparent)] + GitRemoteConnect(#[from] gix::remote::connect::Error), + #[error(transparent)] + GitRemoteFetchPrepare(#[from] gix::remote::fetch::prepare::Error), + #[error(transparent)] + GitRemoteFetch(#[from] gix::remote::fetch::Error), + #[error(transparent)] + GitObjectFind(#[from] gix::object::find::Error), + #[error(transparent)] + GitObjectPeel(#[from] gix::object::peel::to_kind::Error), + #[error(transparent)] + GitObjsFindExisting(#[from] gix::objs::find::existing::Error), + #[error(transparent)] + GitRefIterInit(#[from] gix::reference::iter::init::Error), + #[error(transparent)] + GitRefFindExisting(#[from] gix::reference::find::existing::Error), + #[error(transparent)] + GitRefFind(#[from] gix::reference::find::Error), + #[error(transparent)] + GitRefPeel(#[from] gix::reference::peel::Error), + #[error(transparent)] + GitPackedBufferOpen(#[from] gix::refs::packed::buffer::open::Error), + #[error(transparent)] + GitRefspecParse(#[from] gix::refspec::parse::Error), + #[error(transparent)] + GitHashDecode(#[from] gix::hash::decode::Error), + #[error(transparent)] + GitWorktreeCheckout(#[from] gix::worktree::state::checkout::Error), + #[error(transparent)] + GitHeadPeel(#[from] gix::head::peel::Error), + #[error("Job does not allow fetch")] + FetchNotAllowed, + #[error("Failed to find commit")] + MissingCommit, + #[error(transparent)] + Write(#[from] std::io::Error), +} + #[derive(Error, Debug)] pub enum Error { #[error("Unexpected reply code {0}")] diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index 67114f4..05c8819 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -1,6 +1,8 @@ //! This module describes a single gitlab job use crate::artifact::Artifact; -use crate::client::{Client, JobArtifactFile, JobDependency, JobResponse, JobVariable}; +use crate::client::{ + Client, GitCheckoutError, JobArtifactFile, JobDependency, JobResponse, JobVariable, +}; use crate::outputln; use bytes::{Bytes, BytesMut}; use std::collections::HashMap; @@ -12,6 +14,8 @@ use tracing::info; use crate::client::Error as ClientError; +use crate::clone_git_repository; + /// Gitlab job environment variable /// /// To get the underlying value [`Variable::value`] should be used, however this should not be @@ -337,4 +341,46 @@ impl Job { pub fn build_dir(&self) -> &Path { &self.build_dir } + + /* + * Notes on gitlab-runner: + * https://gitlab.com/gitlab-org/gitlab-runner/-/blob/main/shells/abstract.go#L556 + * + * gitlab runner: + * * Clones to BuildDir + * * Checks out git_info.sha and fetches all in git_info.refspecs + * * Hardcoded username "gitlab-ci-token" + * * credHelperCommand? + * * if ".git/shallow" exists use git "--unshallow" argument + * + * Steps: + * * git config + * * cleanup .git directory? {index.lock, modbules, etc.} + * * git init + * * cd repo + * * git remote add origin + * * git fetch origin + * --no-recurse-submodules $refspecs + * --depth $depth $fetch_flags + * --unshallow if depth <= 0 && exists(.git/shallow) + * * git checkout $sha + */ + + /// Fetch and checkout worktree for job + /// + /// This creates a new path for the repo as gitoxide deletes it on failure. + #[allow(clippy::result_large_err)] + pub fn clone_git_repository(&self) -> Result { + if !self.response.allow_git_fetch { + return Err(GitCheckoutError::FetchNotAllowed); + } + + clone_git_repository( + self.build_dir(), + &self.response.git_info.repo_url, + Some(&self.response.git_info.sha), + &self.response.git_info.refspecs, + Some(self.response.git_info.depth), + ) + } } diff --git a/gitlab-runner/src/lib.rs b/gitlab-runner/src/lib.rs index d82211a..8869bda 100644 --- a/gitlab-runner/src/lib.rs +++ b/gitlab-runner/src/lib.rs @@ -7,7 +7,8 @@ use crate::client::Client; mod run; use crate::run::Run; pub mod job; -use client::ClientMetadata; +use client::{ClientMetadata, GitCheckoutError}; +use gix::{DynNestedProgress, clone, create, index, open, progress, refspec, remote}; use hmac::Hmac; use hmac::Mac; use job::{Job, JobLog}; @@ -19,8 +20,8 @@ use runlist::JobRunList; use tokio::fs::File; use tokio::io::AsyncReadExt; use tokio_util::sync::CancellationToken; -use tracing::debug; use tracing::instrument::WithSubscriber; +use tracing::{debug, trace, warn}; mod runlist; use crate::runlist::RunList; @@ -29,9 +30,11 @@ use futures::AsyncRead; use futures::prelude::*; use std::borrow::Cow; use std::fmt::Write; +use std::num::NonZeroU32; +use std::path::Path; use std::path::PathBuf; +use std::sync::atomic::AtomicBool; use tokio::time::{Duration, sleep}; -use tracing::warn; use url::Url; #[doc(hidden)] @@ -456,3 +459,148 @@ impl Runner { } } } + +// TODO: Should clone_git_repository be async +// gitoxide doesn't currently have async implemented for http backend +// TODO: Is "clippy::result_large_err" the best solution to GitCheckoutError + +/// Fetch and checkout a given worktree +/// +/// This creates a new path for the repo as gitoxide deletes it on failure. +#[allow(clippy::result_large_err)] +pub fn clone_git_repository( + parent_path: &Path, + repo_url: &str, + head_ref: Option<&str>, + refspecs: impl IntoIterator>, + depth: Option, +) -> Result { + let repo_dir = tempfile::Builder::new() + .prefix("repo_") + .tempdir_in(parent_path)?; + + // TODO: Should we expose the ability to interrupt / report progress + let should_interrupt = AtomicBool::new(false); + let mut progress = progress::Discard; + + // TODO: Is Options::isolated correct here? + // The url provided from gitlab has the credentials + let mut fetch = clone::PrepareFetch::new( + repo_url, + repo_dir.path(), + create::Kind::WithWorktree, + Default::default(), + open::Options::isolated(), + )?; + + // Specify which refspecs are fetched from remote + let mut refspecs = refspecs + .into_iter() + .map(|s| { + refspec::parse(s.as_ref().into(), refspec::parse::Operation::Fetch) + .map(|r| r.to_owned()) + }) + .collect::, _>>()?; + + let mut sha = None; + + if let Some(head_ref) = head_ref { + if let Ok(object_id) = gix::ObjectId::from_hex(head_ref.as_bytes()) { + // TODO: I need to specify that the sha should be fetched from remote, right? + // Is there a format for a sha as a refspec? + if let Ok(refspec) = refspec::parse( + format!("+{head_ref}").as_str().into(), + refspec::parse::Operation::Fetch, + ) { + refspecs.push(refspec.to_owned()); + } + sha = Some(object_id); + } else { + // TODO: For some reason this doesn't create refs/heads/{reference} + if let Ok(refspec) = refspec::parse( + format!("+refs/heads/{head_ref}:refs/remotes/origin/{head_ref}") + .as_str() + .into(), + refspec::parse::Operation::Fetch, + ) { + refspecs.push(refspec.to_owned()); + } + } + } + + if !refspecs.is_empty() { + let mut fetch_opts = remote::ref_map::Options::default(); + fetch_opts.extra_refspecs.extend(refspecs); + fetch = fetch.with_fetch_options(fetch_opts); + } + + if let Some(depth) = depth.and_then(NonZeroU32::new) { + fetch = fetch.with_shallow(remote::fetch::Shallow::DepthAtRemote(depth)); + } + + let checkout_progress = progress.add_child("checkout".to_string()); + let (checkout, outcome) = fetch.fetch_then_checkout(checkout_progress, should_interrupt)?; + + trace!("git clone fetch outcome for {repo_url}: {outcome:?}"); + + let repo = checkout.persist(); + + let root_tree_id = if let Some(sha) = sha { + // Checkout worktree at specific SHA + repo.try_find_object(sha)? + .ok_or(GitCheckoutError::MissingCommit)? + } else if let Some(reference) = head_ref { + repo + // TODO: This should be refs/heads/{reference} but it doesn't exist + .try_find_reference(format!("refs/remotes/origin/{reference}").as_str())? + .ok_or(GitCheckoutError::MissingCommit)? + .peel_to_id()? + .object()? + } else { + // Checkout head + repo.head()? + .try_peel_to_id()? + .ok_or(GitCheckoutError::MissingCommit)? + .object()? + }; + let root_tree = root_tree_id.peel_to_tree()?.id; + + let index = + index::State::from_tree(&root_tree, &repo.objects, Default::default()).map_err(|err| { + clone::checkout::main_worktree::Error::IndexFromTree { + id: root_tree, + source: err, + } + })?; + + let workdir = + repo.workdir() + .ok_or_else(|| clone::checkout::main_worktree::Error::BareRepository { + git_dir: repo.git_dir().to_owned(), + })?; + + let files_progress = progress.add_child_with_id( + "files".to_string(), + clone::checkout::main_worktree::ProgressId::CheckoutFiles.into(), + ); + let bytes_progress = progress.add_child_with_id( + "bytes".to_string(), + clone::checkout::main_worktree::ProgressId::BytesWritten.into(), + ); + let mut index = index::File::from_state(index, repo.index_path()); + let outcome = gix::worktree::state::checkout( + &mut index, + workdir, + repo.objects.clone().into_arc()?, + &files_progress, + &bytes_progress, + &should_interrupt, + Default::default(), + )?; + + trace!("git clone checkout outcome for {repo_url}: {outcome:?}"); + + index.write(Default::default())?; + + Ok(repo_dir.keep()) +} From 1a5b99f418023efabaf9f87a1584b88f168ab771 Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Mon, 28 Jul 2025 18:48:17 +0100 Subject: [PATCH 02/16] Offload gitoxide to a tokio blocking thread --- gitlab-runner/examples/demo-runner.rs | 19 +++++++--- gitlab-runner/src/client.rs | 4 +++ gitlab-runner/src/job.rs | 8 ++++- gitlab-runner/src/lib.rs | 50 ++++++++++++++++++++++++--- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/gitlab-runner/examples/demo-runner.rs b/gitlab-runner/examples/demo-runner.rs index 1216b89..bfedb05 100644 --- a/gitlab-runner/examples/demo-runner.rs +++ b/gitlab-runner/examples/demo-runner.rs @@ -12,6 +12,7 @@ use gitlab_runner::{ }; use serde::Deserialize; use tokio::signal::unix::{SignalKind, signal}; +use tokio_util::sync::CancellationToken; use tracing::{debug, info}; use tracing_subscriber::prelude::*; use url::Url; @@ -141,9 +142,17 @@ impl Run { }; let reference = p.next(); - let repo_path = - clone_git_repository(self.job.build_dir(), url, reference, p, Some(1)) - .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))?; + let cancel_token = CancellationToken::new(); + let repo_path = clone_git_repository( + self.job.build_dir(), + url, + reference, + p, + Some(1), + cancel_token, + ) + .await + .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))?; std::fs::rename(repo_path, path) .map_err(|e| outputln!("Failed to move repo path: {}", e.to_string()))?; @@ -155,9 +164,11 @@ impl Run { Some(path) => self.job.build_dir().join(path), _ => self.job.build_dir().to_path_buf(), }; + let cancel_token = CancellationToken::new(); let temp_repo_path = self .job - .clone_git_repository() + .clone_git_repository(cancel_token) + .await .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))?; outputln!("Checked out to {:?}", temp_repo_path); diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index 2165480..d11c52d 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -301,6 +301,10 @@ pub enum GitCheckoutError { GitWorktreeCheckout(#[from] gix::worktree::state::checkout::Error), #[error(transparent)] GitHeadPeel(#[from] gix::head::peel::Error), + #[error(transparent)] + ThreadJoinError(#[from] tokio::task::JoinError), + #[error("Checkout cancelled")] + Cancelled, #[error("Job does not allow fetch")] FetchNotAllowed, #[error("Failed to find commit")] diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index 05c8819..31e420f 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use tokio::io::AsyncWrite; use tokio_retry2::strategy::{FibonacciBackoff, jitter}; +use tokio_util::sync::CancellationToken; use tracing::info; use crate::client::Error as ClientError; @@ -370,7 +371,10 @@ impl Job { /// /// This creates a new path for the repo as gitoxide deletes it on failure. #[allow(clippy::result_large_err)] - pub fn clone_git_repository(&self) -> Result { + pub async fn clone_git_repository( + &self, + cancel_token: CancellationToken, + ) -> Result { if !self.response.allow_git_fetch { return Err(GitCheckoutError::FetchNotAllowed); } @@ -381,6 +385,8 @@ impl Job { Some(&self.response.git_info.sha), &self.response.git_info.refspecs, Some(self.response.git_info.depth), + cancel_token, ) + .await } } diff --git a/gitlab-runner/src/lib.rs b/gitlab-runner/src/lib.rs index 8869bda..ec5bbef 100644 --- a/gitlab-runner/src/lib.rs +++ b/gitlab-runner/src/lib.rs @@ -33,7 +33,9 @@ use std::fmt::Write; use std::num::NonZeroU32; use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; use tokio::time::{Duration, sleep}; use url::Url; @@ -460,27 +462,65 @@ impl Runner { } } -// TODO: Should clone_git_repository be async -// gitoxide doesn't currently have async implemented for http backend +// TODO: Should we re-export gix::progress to provide a means to monitor progress // TODO: Is "clippy::result_large_err" the best solution to GitCheckoutError +/// Fetch and checkout a given worktree on a thread +/// +/// See: [clone_git_repository_sync] +pub async fn clone_git_repository( + parent_path: &Path, + repo_url: &str, + head_ref: Option<&str>, + refspecs: impl IntoIterator>, + depth: Option, + cancel_token: CancellationToken, +) -> Result { + let parent_path = parent_path.to_owned(); + let repo_url = repo_url.to_owned(); + let head_ref = head_ref.map(|a| a.to_owned()); + let should_interrupt: Arc = Default::default(); + let should_interrupt_cancel = should_interrupt.clone(); + let refspecs: Vec<_> = refspecs + .into_iter() + .map(|s| s.as_ref().to_owned()) + .collect(); + // offload the clone operation to one of the tokio runtimes blocking threads + tokio::select! { + result = tokio::task::spawn_blocking(move || { + clone_git_repository_sync( + &parent_path, + &repo_url, + head_ref.as_deref(), + refspecs, + depth, + &should_interrupt, + ) + }) => result?, + _ = cancel_token.cancelled() => { + should_interrupt_cancel.store(true, Ordering::SeqCst); + Err(GitCheckoutError::Cancelled) + } + } +} + /// Fetch and checkout a given worktree /// /// This creates a new path for the repo as gitoxide deletes it on failure. #[allow(clippy::result_large_err)] -pub fn clone_git_repository( +pub fn clone_git_repository_sync( parent_path: &Path, repo_url: &str, head_ref: Option<&str>, refspecs: impl IntoIterator>, depth: Option, + should_interrupt: &AtomicBool, ) -> Result { let repo_dir = tempfile::Builder::new() .prefix("repo_") .tempdir_in(parent_path)?; // TODO: Should we expose the ability to interrupt / report progress - let should_interrupt = AtomicBool::new(false); let mut progress = progress::Discard; // TODO: Is Options::isolated correct here? @@ -594,7 +634,7 @@ pub fn clone_git_repository( repo.objects.clone().into_arc()?, &files_progress, &bytes_progress, - &should_interrupt, + should_interrupt, Default::default(), )?; From 97661b411528d37ae5950c1345289f169522bac1 Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Mon, 28 Jul 2025 18:29:45 +0100 Subject: [PATCH 03/16] Support git clone by commit id SHA Also throw an error if the provided HEAD ref cannot be parsed. --- gitlab-runner/src/client.rs | 2 ++ gitlab-runner/src/lib.rs | 45 ++++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index d11c52d..8b8ae85 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -284,6 +284,8 @@ pub enum GitCheckoutError { #[error(transparent)] GitObjsFindExisting(#[from] gix::objs::find::existing::Error), #[error(transparent)] + GitRefEdit(#[from] gix::reference::edit::Error), + #[error(transparent)] GitRefIterInit(#[from] gix::reference::iter::init::Error), #[error(transparent)] GitRefFindExisting(#[from] gix::reference::find::existing::Error), diff --git a/gitlab-runner/src/lib.rs b/gitlab-runner/src/lib.rs index ec5bbef..2004c1a 100644 --- a/gitlab-runner/src/lib.rs +++ b/gitlab-runner/src/lib.rs @@ -8,7 +8,7 @@ mod run; use crate::run::Run; pub mod job; use client::{ClientMetadata, GitCheckoutError}; -use gix::{DynNestedProgress, clone, create, index, open, progress, refspec, remote}; +use gix::{DynNestedProgress, clone, create, index, open, progress, refs, refspec, remote}; use hmac::Hmac; use hmac::Mac; use job::{Job, JobLog}; @@ -544,27 +544,26 @@ pub fn clone_git_repository_sync( let mut sha = None; + // Add object SHA to the fetch as `with_ref_name` does not support SHAs if let Some(head_ref) = head_ref { if let Ok(object_id) = gix::ObjectId::from_hex(head_ref.as_bytes()) { - // TODO: I need to specify that the sha should be fetched from remote, right? - // Is there a format for a sha as a refspec? - if let Ok(refspec) = refspec::parse( + let refspec = refspec::parse( format!("+{head_ref}").as_str().into(), refspec::parse::Operation::Fetch, - ) { - refspecs.push(refspec.to_owned()); - } + )? + .to_owned(); + refspecs.push(refspec); sha = Some(object_id); } else { - // TODO: For some reason this doesn't create refs/heads/{reference} - if let Ok(refspec) = refspec::parse( + let refspec = refspec::parse( format!("+refs/heads/{head_ref}:refs/remotes/origin/{head_ref}") .as_str() .into(), refspec::parse::Operation::Fetch, - ) { - refspecs.push(refspec.to_owned()); - } + )? + .to_owned(); + refspecs.push(refspec); + fetch = fetch.with_ref_name(Some(head_ref))? } } @@ -585,14 +584,30 @@ pub fn clone_git_repository_sync( let repo = checkout.persist(); + // Make HEAD point to the given SHA + // See: gix::util::update_head + if let Some(sha) = sha { + repo.edit_reference(refs::transaction::RefEdit { + change: refs::transaction::Change::Update { + log: refs::transaction::LogChange { + mode: refs::transaction::RefLog::AndReference, + force_create_reflog: false, + message: format!("clone repo at {}", sha).into(), + }, + expected: refs::transaction::PreviousValue::Any, + new: refs::Target::Object(sha.to_owned()), + }, + name: "HEAD".try_into()?, + deref: false, + })?; + } + let root_tree_id = if let Some(sha) = sha { // Checkout worktree at specific SHA repo.try_find_object(sha)? .ok_or(GitCheckoutError::MissingCommit)? } else if let Some(reference) = head_ref { - repo - // TODO: This should be refs/heads/{reference} but it doesn't exist - .try_find_reference(format!("refs/remotes/origin/{reference}").as_str())? + repo.try_find_reference(format!("refs/heads/{reference}").as_str())? .ok_or(GitCheckoutError::MissingCommit)? .peel_to_id()? .object()? From d2f42e693f240358c305ca8d472a7eb30e60dca6 Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Mon, 28 Jul 2025 18:21:28 +0100 Subject: [PATCH 04/16] typo: "Failed to create build dir" --- gitlab-runner/src/run.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab-runner/src/run.rs b/gitlab-runner/src/run.rs index 7c60298..cf83232 100644 --- a/gitlab-runner/src/run.rs +++ b/gitlab-runner/src/run.rs @@ -30,7 +30,7 @@ where Ret: Future>, { if let Err(e) = tokio::fs::create_dir(&build_dir).await { - job.trace(format!("Failed to remove build dir: {e}")); + job.trace(format!("Failed to create build dir: {e}")); return Err(()); } let mut handler = process(job).await?; From 15d04eb581e5540b53715ee7c01ce88665d258b7 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Mon, 4 May 2026 10:44:02 -0500 Subject: [PATCH 05/16] Make GitCheckoutError's inner errors boxed Otherwise, clippy gets quite upset because the error type is so large (>200 bytes). --- gitlab-runner/src/client.rs | 15 ++++++++++++++- gitlab-runner/src/job.rs | 6 +++--- gitlab-runner/src/lib.rs | 11 +++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index 8b8ae85..4815a92 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -258,7 +258,7 @@ impl JobResponse { } #[derive(Error, Debug)] -pub enum GitCheckoutError { +pub enum GitCheckoutErrorInner { #[error(transparent)] GitClone(#[from] gix::clone::Error), #[error(transparent)] @@ -315,6 +315,19 @@ pub enum GitCheckoutError { Write(#[from] std::io::Error), } +#[derive(Error, Debug)] +#[error(transparent)] +pub struct GitCheckoutError(pub Box); + +impl From for GitCheckoutError +where + GitCheckoutErrorInner: From, +{ + fn from(value: T) -> Self { + Self(Box::new(value.into())) + } +} + #[derive(Error, Debug)] pub enum Error { #[error("Unexpected reply code {0}")] diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index 31e420f..8f59e57 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -1,7 +1,8 @@ //! This module describes a single gitlab job use crate::artifact::Artifact; use crate::client::{ - Client, GitCheckoutError, JobArtifactFile, JobDependency, JobResponse, JobVariable, + Client, GitCheckoutError, GitCheckoutErrorInner, JobArtifactFile, JobDependency, JobResponse, + JobVariable, }; use crate::outputln; use bytes::{Bytes, BytesMut}; @@ -370,13 +371,12 @@ impl Job { /// Fetch and checkout worktree for job /// /// This creates a new path for the repo as gitoxide deletes it on failure. - #[allow(clippy::result_large_err)] pub async fn clone_git_repository( &self, cancel_token: CancellationToken, ) -> Result { if !self.response.allow_git_fetch { - return Err(GitCheckoutError::FetchNotAllowed); + return Err(GitCheckoutErrorInner::FetchNotAllowed.into()); } clone_git_repository( diff --git a/gitlab-runner/src/lib.rs b/gitlab-runner/src/lib.rs index 2004c1a..8942f34 100644 --- a/gitlab-runner/src/lib.rs +++ b/gitlab-runner/src/lib.rs @@ -5,6 +5,7 @@ mod client; mod logging; use crate::client::Client; mod run; +use crate::client::GitCheckoutErrorInner; use crate::run::Run; pub mod job; use client::{ClientMetadata, GitCheckoutError}; @@ -463,7 +464,6 @@ impl Runner { } // TODO: Should we re-export gix::progress to provide a means to monitor progress -// TODO: Is "clippy::result_large_err" the best solution to GitCheckoutError /// Fetch and checkout a given worktree on a thread /// @@ -499,7 +499,7 @@ pub async fn clone_git_repository( }) => result?, _ = cancel_token.cancelled() => { should_interrupt_cancel.store(true, Ordering::SeqCst); - Err(GitCheckoutError::Cancelled) + Err(GitCheckoutErrorInner::Cancelled.into()) } } } @@ -507,7 +507,6 @@ pub async fn clone_git_repository( /// Fetch and checkout a given worktree /// /// This creates a new path for the repo as gitoxide deletes it on failure. -#[allow(clippy::result_large_err)] pub fn clone_git_repository_sync( parent_path: &Path, repo_url: &str, @@ -605,17 +604,17 @@ pub fn clone_git_repository_sync( let root_tree_id = if let Some(sha) = sha { // Checkout worktree at specific SHA repo.try_find_object(sha)? - .ok_or(GitCheckoutError::MissingCommit)? + .ok_or(GitCheckoutErrorInner::MissingCommit)? } else if let Some(reference) = head_ref { repo.try_find_reference(format!("refs/heads/{reference}").as_str())? - .ok_or(GitCheckoutError::MissingCommit)? + .ok_or(GitCheckoutErrorInner::MissingCommit)? .peel_to_id()? .object()? } else { // Checkout head repo.head()? .try_peel_to_id()? - .ok_or(GitCheckoutError::MissingCommit)? + .ok_or(GitCheckoutErrorInner::MissingCommit)? .object()? }; let root_tree = root_tree_id.peel_to_tree()?.id; From 8db3b8d9b16a3bc4be373f7b757f71ce67421271 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Mon, 4 May 2026 14:50:48 -0500 Subject: [PATCH 06/16] Clarify comment on git credentials helper --- gitlab-runner/src/job.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index 8f59e57..c240a4d 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -352,7 +352,7 @@ impl Job { * * Clones to BuildDir * * Checks out git_info.sha and fetches all in git_info.refspecs * * Hardcoded username "gitlab-ci-token" - * * credHelperCommand? + * * 'credHelperCommand' is a shell command that's used to get the password from the env * * if ".git/shallow" exists use git "--unshallow" argument * * Steps: From 06c67bf43f927dc593e391440b59cb71b948a41d Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Mon, 4 May 2026 16:02:41 -0500 Subject: [PATCH 07/16] Fix incorrect interpretation of allow_git_fetch This is set if the runner isn't allowed to reuse an existing repository by re-running a fetch there, which is unrelated to the ability to clone itself. --- gitlab-runner/src/client.rs | 2 -- gitlab-runner/src/job.rs | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index 4815a92..73fe639 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -307,8 +307,6 @@ pub enum GitCheckoutErrorInner { ThreadJoinError(#[from] tokio::task::JoinError), #[error("Checkout cancelled")] Cancelled, - #[error("Job does not allow fetch")] - FetchNotAllowed, #[error("Failed to find commit")] MissingCommit, #[error(transparent)] diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index c240a4d..f03af60 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -1,8 +1,7 @@ //! This module describes a single gitlab job use crate::artifact::Artifact; use crate::client::{ - Client, GitCheckoutError, GitCheckoutErrorInner, JobArtifactFile, JobDependency, JobResponse, - JobVariable, + Client, GitCheckoutError, JobArtifactFile, JobDependency, JobResponse, JobVariable, }; use crate::outputln; use bytes::{Bytes, BytesMut}; @@ -375,10 +374,6 @@ impl Job { &self, cancel_token: CancellationToken, ) -> Result { - if !self.response.allow_git_fetch { - return Err(GitCheckoutErrorInner::FetchNotAllowed.into()); - } - clone_git_repository( self.build_dir(), &self.response.git_info.repo_url, From 94def56acafa3c35da37348da065c1ecaa8960f1 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Mon, 4 May 2026 16:02:41 -0500 Subject: [PATCH 08/16] Add comment clarifying meaning of the depth field It's a bit confusing that this automatically considers the corresponding CI variable, but some other settings (such as the git strategy allowing repo reuse) do not. --- gitlab-runner/src/client.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index 73fe639..5656fc0 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -229,6 +229,8 @@ pub(crate) struct JobGitInfo { pub repo_url: String, pub refspecs: Vec, pub sha: String, + // Note that this is already filled with the value of GIT_DEPTH: + // https://gitlab.com/gitlab-org/gitlab/-/blob/dd9009f7d9f57fdfd7496f14086c0f059f890688/app/presenters/ci/build_runner_presenter.rb#L164 pub depth: u32, #[serde(flatten)] unparsed: JsonValue, From c04d5694eff208da9eae3615e8e355230968583e Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Tue, 5 May 2026 10:06:51 -0500 Subject: [PATCH 09/16] Make git checkout APIs private No real reason for them to be public, and it frees us from handling a lot of edge cases and allows for some significant (future) code cleanup. --- gitlab-runner/examples/demo-runner.rs | 37 +-------------------------- gitlab-runner/src/lib.rs | 4 +-- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/gitlab-runner/examples/demo-runner.rs b/gitlab-runner/examples/demo-runner.rs index bfedb05..3031b9b 100644 --- a/gitlab-runner/examples/demo-runner.rs +++ b/gitlab-runner/examples/demo-runner.rs @@ -7,8 +7,7 @@ use futures::AsyncRead; use futures::io::Cursor; use gitlab_runner::job::Job; use gitlab_runner::{ - GitlabLayer, JobHandler, JobResult, Phase, RunnerBuilder, UploadableFile, clone_git_repository, - outputln, + GitlabLayer, JobHandler, JobResult, Phase, RunnerBuilder, UploadableFile, outputln, }; use serde::Deserialize; use tokio::signal::unix::{SignalKind, signal}; @@ -125,40 +124,6 @@ impl Run { } Ok(()) } - "clone" => { - let url = match p.next() { - Some(url) => url, - _ => { - outputln!("Missing repo url as first argument"); - return Err(()); - } - }; - let path = match p.next() { - Some(path) => self.job.build_dir().join(path), - _ => { - outputln!("Missing repo output path as second argument"); - return Err(()); - } - }; - let reference = p.next(); - - let cancel_token = CancellationToken::new(); - let repo_path = clone_git_repository( - self.job.build_dir(), - url, - reference, - p, - Some(1), - cancel_token, - ) - .await - .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))?; - - std::fs::rename(repo_path, path) - .map_err(|e| outputln!("Failed to move repo path: {}", e.to_string()))?; - - Ok(()) - } "checkout" => { let path = match p.next() { Some(path) => self.job.build_dir().join(path), diff --git a/gitlab-runner/src/lib.rs b/gitlab-runner/src/lib.rs index 8942f34..d29ede9 100644 --- a/gitlab-runner/src/lib.rs +++ b/gitlab-runner/src/lib.rs @@ -468,7 +468,7 @@ impl Runner { /// Fetch and checkout a given worktree on a thread /// /// See: [clone_git_repository_sync] -pub async fn clone_git_repository( +async fn clone_git_repository( parent_path: &Path, repo_url: &str, head_ref: Option<&str>, @@ -507,7 +507,7 @@ pub async fn clone_git_repository( /// Fetch and checkout a given worktree /// /// This creates a new path for the repo as gitoxide deletes it on failure. -pub fn clone_git_repository_sync( +fn clone_git_repository_sync( parent_path: &Path, repo_url: &str, head_ref: Option<&str>, From 10a90e2f545f8c41e7020573bfc862a0d868e30a Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Thu, 7 May 2026 15:43:19 -0500 Subject: [PATCH 10/16] Relocate git clone code to job.rs If this is private, and only used by Job, it makes sense for it to be in the same place. --- gitlab-runner/src/job.rs | 196 +++++++++++++++++++++++++++++++++--- gitlab-runner/src/lib.rs | 207 +-------------------------------------- 2 files changed, 185 insertions(+), 218 deletions(-) diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index f03af60..42677b6 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -1,22 +1,24 @@ //! This module describes a single gitlab job use crate::artifact::Artifact; use crate::client::{ - Client, GitCheckoutError, JobArtifactFile, JobDependency, JobResponse, JobVariable, + Client, GitCheckoutError, GitCheckoutErrorInner, JobArtifactFile, JobDependency, JobResponse, + JobVariable, }; use crate::outputln; use bytes::{Bytes, BytesMut}; +use gix::NestedProgress; use std::collections::HashMap; +use std::num::NonZeroU32; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use tokio::io::AsyncWrite; use tokio_retry2::strategy::{FibonacciBackoff, jitter}; use tokio_util::sync::CancellationToken; -use tracing::info; +use tracing::{info, trace}; use crate::client::Error as ClientError; -use crate::clone_git_repository; - /// Gitlab job environment variable /// /// To get the underlying value [`Variable::value`] should be used, however this should not be @@ -278,6 +280,158 @@ impl JobLog { } } +fn clone_git_repository_sync( + parent_path: &Path, + repo_url: &str, + head_ref: Option<&str>, + refspecs: impl IntoIterator>, + depth: Option, + should_interrupt: &AtomicBool, +) -> Result { + let repo_dir = tempfile::Builder::new() + .prefix("repo_") + .tempdir_in(parent_path)?; + + // TODO: Should we expose the ability to interrupt / report progress + let mut progress = gix::progress::Discard; + + // TODO: Is Options::isolated correct here? + // The url provided from gitlab has the credentials + let mut fetch = gix::clone::PrepareFetch::new( + repo_url, + repo_dir.path(), + gix::create::Kind::WithWorktree, + Default::default(), + gix::open::Options::isolated(), + )?; + + // Specify which refspecs are fetched from remote + let mut refspecs = refspecs + .into_iter() + .map(|s| { + gix::refspec::parse(s.as_ref().into(), gix::refspec::parse::Operation::Fetch) + .map(|r| r.to_owned()) + }) + .collect::, _>>()?; + + let mut sha = None; + + // Add object SHA to the fetch as `with_ref_name` does not support SHAs + if let Some(head_ref) = head_ref { + if let Ok(object_id) = gix::ObjectId::from_hex(head_ref.as_bytes()) { + let refspec = gix::refspec::parse( + format!("+{head_ref}").as_str().into(), + gix::refspec::parse::Operation::Fetch, + )? + .to_owned(); + refspecs.push(refspec); + sha = Some(object_id); + } else { + let refspec = gix::refspec::parse( + format!("+refs/heads/{head_ref}:refs/remotes/origin/{head_ref}") + .as_str() + .into(), + gix::refspec::parse::Operation::Fetch, + )? + .to_owned(); + refspecs.push(refspec); + fetch = fetch.with_ref_name(Some(head_ref))? + } + } + + if !refspecs.is_empty() { + let mut fetch_opts = gix::remote::ref_map::Options::default(); + fetch_opts.extra_refspecs.extend(refspecs); + fetch = fetch.with_fetch_options(fetch_opts); + } + + if let Some(depth) = depth.and_then(NonZeroU32::new) { + fetch = fetch.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(depth)); + } + + let checkout_progress = progress.add_child("checkout".to_string()); + let (checkout, outcome) = fetch.fetch_then_checkout(checkout_progress, should_interrupt)?; + + trace!("git clone fetch outcome for {repo_url}: {outcome:?}"); + + let repo = checkout.persist(); + + // Make HEAD point to the given SHA + // See: gix::util::update_head + if let Some(sha) = sha { + repo.edit_reference(gix::refs::transaction::RefEdit { + change: gix::refs::transaction::Change::Update { + log: gix::refs::transaction::LogChange { + mode: gix::refs::transaction::RefLog::AndReference, + force_create_reflog: false, + message: format!("clone repo at {}", sha).into(), + }, + expected: gix::refs::transaction::PreviousValue::Any, + new: gix::refs::Target::Object(sha.to_owned()), + }, + name: "HEAD".try_into()?, + deref: false, + })?; + } + + let root_tree_id = if let Some(sha) = sha { + // Checkout worktree at specific SHA + repo.try_find_object(sha)? + .ok_or(GitCheckoutErrorInner::MissingCommit)? + } else if let Some(reference) = head_ref { + repo.try_find_reference(format!("refs/heads/{reference}").as_str())? + .ok_or(GitCheckoutErrorInner::MissingCommit)? + .peel_to_id()? + .object()? + } else { + // Checkout head + repo.head()? + .try_peel_to_id()? + .ok_or(GitCheckoutErrorInner::MissingCommit)? + .object()? + }; + let root_tree = root_tree_id.peel_to_tree()?.id; + + let index = gix::index::State::from_tree(&root_tree, &repo.objects, Default::default()) + .map_err( + |err| gix::clone::checkout::main_worktree::Error::IndexFromTree { + id: root_tree, + source: err, + }, + )?; + + let workdir = repo.workdir().ok_or_else(|| { + gix::clone::checkout::main_worktree::Error::BareRepository { + git_dir: repo.git_dir().to_owned(), + } + })?; + + let files_progress = progress.add_child_with_id( + "files".to_string(), + gix::clone::checkout::main_worktree::ProgressId::CheckoutFiles.into(), + ); + let bytes_progress = progress.add_child_with_id( + "bytes".to_string(), + gix::clone::checkout::main_worktree::ProgressId::BytesWritten.into(), + ); + let mut index = gix::index::File::from_state(index, repo.index_path()); + let outcome = gix::worktree::state::checkout( + &mut index, + workdir, + repo.objects.clone().into_arc()?, + &files_progress, + &bytes_progress, + should_interrupt, + Default::default(), + )?; + + trace!("git clone checkout outcome for {repo_url}: {outcome:?}"); + + index.write(Default::default())?; + + Ok(repo_dir.keep()) +} + /// A running Gitlab Job #[derive(Debug)] pub struct Job { @@ -374,14 +528,30 @@ impl Job { &self, cancel_token: CancellationToken, ) -> Result { - clone_git_repository( - self.build_dir(), - &self.response.git_info.repo_url, - Some(&self.response.git_info.sha), - &self.response.git_info.refspecs, - Some(self.response.git_info.depth), - cancel_token, - ) - .await + let parent_path = self.build_dir.to_owned(); + let repo_url = self.response.git_info.repo_url.to_owned(); + let head_ref = Some(self.response.git_info.sha.to_owned()); + let should_interrupt: Arc = Default::default(); + let should_interrupt_cancel = should_interrupt.clone(); + let refspecs: Vec<_> = self.response.git_info.refspecs.clone(); + let depth = Some(self.response.git_info.depth); + + // offload the clone operation to one of the tokio runtimes blocking threads + tokio::select! { + result = tokio::task::spawn_blocking(move || { + clone_git_repository_sync( + &parent_path, + &repo_url, + head_ref.as_deref(), + refspecs, + depth, + &should_interrupt, + ) + }) => result?, + _ = cancel_token.cancelled() => { + should_interrupt_cancel.store(true, Ordering::SeqCst); + Err(GitCheckoutErrorInner::Cancelled.into()) + } + } } } diff --git a/gitlab-runner/src/lib.rs b/gitlab-runner/src/lib.rs index d29ede9..342da27 100644 --- a/gitlab-runner/src/lib.rs +++ b/gitlab-runner/src/lib.rs @@ -5,11 +5,9 @@ mod client; mod logging; use crate::client::Client; mod run; -use crate::client::GitCheckoutErrorInner; use crate::run::Run; pub mod job; -use client::{ClientMetadata, GitCheckoutError}; -use gix::{DynNestedProgress, clone, create, index, open, progress, refs, refspec, remote}; +use client::ClientMetadata; use hmac::Hmac; use hmac::Mac; use job::{Job, JobLog}; @@ -22,7 +20,7 @@ use tokio::fs::File; use tokio::io::AsyncReadExt; use tokio_util::sync::CancellationToken; use tracing::instrument::WithSubscriber; -use tracing::{debug, trace, warn}; +use tracing::{debug, warn}; mod runlist; use crate::runlist::RunList; @@ -31,12 +29,7 @@ use futures::AsyncRead; use futures::prelude::*; use std::borrow::Cow; use std::fmt::Write; -use std::num::NonZeroU32; -use std::path::Path; use std::path::PathBuf; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; use tokio::time::{Duration, sleep}; use url::Url; @@ -462,199 +455,3 @@ impl Runner { } } } - -// TODO: Should we re-export gix::progress to provide a means to monitor progress - -/// Fetch and checkout a given worktree on a thread -/// -/// See: [clone_git_repository_sync] -async fn clone_git_repository( - parent_path: &Path, - repo_url: &str, - head_ref: Option<&str>, - refspecs: impl IntoIterator>, - depth: Option, - cancel_token: CancellationToken, -) -> Result { - let parent_path = parent_path.to_owned(); - let repo_url = repo_url.to_owned(); - let head_ref = head_ref.map(|a| a.to_owned()); - let should_interrupt: Arc = Default::default(); - let should_interrupt_cancel = should_interrupt.clone(); - let refspecs: Vec<_> = refspecs - .into_iter() - .map(|s| s.as_ref().to_owned()) - .collect(); - // offload the clone operation to one of the tokio runtimes blocking threads - tokio::select! { - result = tokio::task::spawn_blocking(move || { - clone_git_repository_sync( - &parent_path, - &repo_url, - head_ref.as_deref(), - refspecs, - depth, - &should_interrupt, - ) - }) => result?, - _ = cancel_token.cancelled() => { - should_interrupt_cancel.store(true, Ordering::SeqCst); - Err(GitCheckoutErrorInner::Cancelled.into()) - } - } -} - -/// Fetch and checkout a given worktree -/// -/// This creates a new path for the repo as gitoxide deletes it on failure. -fn clone_git_repository_sync( - parent_path: &Path, - repo_url: &str, - head_ref: Option<&str>, - refspecs: impl IntoIterator>, - depth: Option, - should_interrupt: &AtomicBool, -) -> Result { - let repo_dir = tempfile::Builder::new() - .prefix("repo_") - .tempdir_in(parent_path)?; - - // TODO: Should we expose the ability to interrupt / report progress - let mut progress = progress::Discard; - - // TODO: Is Options::isolated correct here? - // The url provided from gitlab has the credentials - let mut fetch = clone::PrepareFetch::new( - repo_url, - repo_dir.path(), - create::Kind::WithWorktree, - Default::default(), - open::Options::isolated(), - )?; - - // Specify which refspecs are fetched from remote - let mut refspecs = refspecs - .into_iter() - .map(|s| { - refspec::parse(s.as_ref().into(), refspec::parse::Operation::Fetch) - .map(|r| r.to_owned()) - }) - .collect::, _>>()?; - - let mut sha = None; - - // Add object SHA to the fetch as `with_ref_name` does not support SHAs - if let Some(head_ref) = head_ref { - if let Ok(object_id) = gix::ObjectId::from_hex(head_ref.as_bytes()) { - let refspec = refspec::parse( - format!("+{head_ref}").as_str().into(), - refspec::parse::Operation::Fetch, - )? - .to_owned(); - refspecs.push(refspec); - sha = Some(object_id); - } else { - let refspec = refspec::parse( - format!("+refs/heads/{head_ref}:refs/remotes/origin/{head_ref}") - .as_str() - .into(), - refspec::parse::Operation::Fetch, - )? - .to_owned(); - refspecs.push(refspec); - fetch = fetch.with_ref_name(Some(head_ref))? - } - } - - if !refspecs.is_empty() { - let mut fetch_opts = remote::ref_map::Options::default(); - fetch_opts.extra_refspecs.extend(refspecs); - fetch = fetch.with_fetch_options(fetch_opts); - } - - if let Some(depth) = depth.and_then(NonZeroU32::new) { - fetch = fetch.with_shallow(remote::fetch::Shallow::DepthAtRemote(depth)); - } - - let checkout_progress = progress.add_child("checkout".to_string()); - let (checkout, outcome) = fetch.fetch_then_checkout(checkout_progress, should_interrupt)?; - - trace!("git clone fetch outcome for {repo_url}: {outcome:?}"); - - let repo = checkout.persist(); - - // Make HEAD point to the given SHA - // See: gix::util::update_head - if let Some(sha) = sha { - repo.edit_reference(refs::transaction::RefEdit { - change: refs::transaction::Change::Update { - log: refs::transaction::LogChange { - mode: refs::transaction::RefLog::AndReference, - force_create_reflog: false, - message: format!("clone repo at {}", sha).into(), - }, - expected: refs::transaction::PreviousValue::Any, - new: refs::Target::Object(sha.to_owned()), - }, - name: "HEAD".try_into()?, - deref: false, - })?; - } - - let root_tree_id = if let Some(sha) = sha { - // Checkout worktree at specific SHA - repo.try_find_object(sha)? - .ok_or(GitCheckoutErrorInner::MissingCommit)? - } else if let Some(reference) = head_ref { - repo.try_find_reference(format!("refs/heads/{reference}").as_str())? - .ok_or(GitCheckoutErrorInner::MissingCommit)? - .peel_to_id()? - .object()? - } else { - // Checkout head - repo.head()? - .try_peel_to_id()? - .ok_or(GitCheckoutErrorInner::MissingCommit)? - .object()? - }; - let root_tree = root_tree_id.peel_to_tree()?.id; - - let index = - index::State::from_tree(&root_tree, &repo.objects, Default::default()).map_err(|err| { - clone::checkout::main_worktree::Error::IndexFromTree { - id: root_tree, - source: err, - } - })?; - - let workdir = - repo.workdir() - .ok_or_else(|| clone::checkout::main_worktree::Error::BareRepository { - git_dir: repo.git_dir().to_owned(), - })?; - - let files_progress = progress.add_child_with_id( - "files".to_string(), - clone::checkout::main_worktree::ProgressId::CheckoutFiles.into(), - ); - let bytes_progress = progress.add_child_with_id( - "bytes".to_string(), - clone::checkout::main_worktree::ProgressId::BytesWritten.into(), - ); - let mut index = index::File::from_state(index, repo.index_path()); - let outcome = gix::worktree::state::checkout( - &mut index, - workdir, - repo.objects.clone().into_arc()?, - &files_progress, - &bytes_progress, - should_interrupt, - Default::default(), - )?; - - trace!("git clone checkout outcome for {repo_url}: {outcome:?}"); - - index.write(Default::default())?; - - Ok(repo_dir.keep()) -} From 42c0d1aad81b4308703e2ed7c03ceffb93b55d16 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Thu, 7 May 2026 16:20:49 -0500 Subject: [PATCH 11/16] Make clone_git_repository natively cancel-safe Given that the example didn't even use the cancellation token properly, it's better for cancellation to be handled internally. --- gitlab-runner/examples/demo-runner.rs | 4 +-- gitlab-runner/src/client.rs | 2 -- gitlab-runner/src/job.rs | 49 ++++++++++++++------------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/gitlab-runner/examples/demo-runner.rs b/gitlab-runner/examples/demo-runner.rs index 3031b9b..09c6da5 100644 --- a/gitlab-runner/examples/demo-runner.rs +++ b/gitlab-runner/examples/demo-runner.rs @@ -11,7 +11,6 @@ use gitlab_runner::{ }; use serde::Deserialize; use tokio::signal::unix::{SignalKind, signal}; -use tokio_util::sync::CancellationToken; use tracing::{debug, info}; use tracing_subscriber::prelude::*; use url::Url; @@ -129,10 +128,9 @@ impl Run { Some(path) => self.job.build_dir().join(path), _ => self.job.build_dir().to_path_buf(), }; - let cancel_token = CancellationToken::new(); let temp_repo_path = self .job - .clone_git_repository(cancel_token) + .clone_git_repository() .await .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))?; diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index 5656fc0..ef31c14 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -307,8 +307,6 @@ pub enum GitCheckoutErrorInner { GitHeadPeel(#[from] gix::head::peel::Error), #[error(transparent)] ThreadJoinError(#[from] tokio::task::JoinError), - #[error("Checkout cancelled")] - Cancelled, #[error("Failed to find commit")] MissingCommit, #[error(transparent)] diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index 42677b6..954c7fa 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -14,7 +14,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use tokio::io::AsyncWrite; use tokio_retry2::strategy::{FibonacciBackoff, jitter}; -use tokio_util::sync::CancellationToken; use tracing::{info, trace}; use crate::client::Error as ClientError; @@ -524,34 +523,38 @@ impl Job { /// Fetch and checkout worktree for job /// /// This creates a new path for the repo as gitoxide deletes it on failure. - pub async fn clone_git_repository( - &self, - cancel_token: CancellationToken, - ) -> Result { + pub async fn clone_git_repository(&self) -> Result { + struct InterruptOnDrop { + should_interrupt: Arc, + } + + impl Drop for InterruptOnDrop { + fn drop(&mut self) { + self.should_interrupt.store(true, Ordering::Relaxed); + } + } + let parent_path = self.build_dir.to_owned(); let repo_url = self.response.git_info.repo_url.to_owned(); let head_ref = Some(self.response.git_info.sha.to_owned()); - let should_interrupt: Arc = Default::default(); - let should_interrupt_cancel = should_interrupt.clone(); let refspecs: Vec<_> = self.response.git_info.refspecs.clone(); let depth = Some(self.response.git_info.depth); + let should_interrupt: Arc = Default::default(); + let _interrupt = InterruptOnDrop { + should_interrupt: should_interrupt.clone(), + }; // offload the clone operation to one of the tokio runtimes blocking threads - tokio::select! { - result = tokio::task::spawn_blocking(move || { - clone_git_repository_sync( - &parent_path, - &repo_url, - head_ref.as_deref(), - refspecs, - depth, - &should_interrupt, - ) - }) => result?, - _ = cancel_token.cancelled() => { - should_interrupt_cancel.store(true, Ordering::SeqCst); - Err(GitCheckoutErrorInner::Cancelled.into()) - } - } + tokio::task::spawn_blocking(move || { + clone_git_repository_sync( + &parent_path, + &repo_url, + head_ref.as_deref(), + refspecs, + depth, + &should_interrupt, + ) + }) + .await? } } From b93e102f83c712250895becf1eb0c352fca9adac Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Thu, 7 May 2026 16:37:35 -0500 Subject: [PATCH 12/16] Reduce scope of git cloning code to only handle GitLab's inputs The `sha` field is never going to contain a non-SHA, among other things, so this code can be wittled down quite a bit now. --- gitlab-runner/src/job.rs | 138 +++++++++++++-------------------------- 1 file changed, 47 insertions(+), 91 deletions(-) diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index 954c7fa..2d98a54 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -280,73 +280,34 @@ impl JobLog { } fn clone_git_repository_sync( - parent_path: &Path, + build_dir: PathBuf, repo_url: &str, - head_ref: Option<&str>, - refspecs: impl IntoIterator>, - depth: Option, + sha: gix::ObjectId, + refspecs: Vec, + depth: u32, should_interrupt: &AtomicBool, ) -> Result { - let repo_dir = tempfile::Builder::new() - .prefix("repo_") - .tempdir_in(parent_path)?; + let repo_dir = tempfile::TempDir::with_prefix_in("repo_", &build_dir)?; // TODO: Should we expose the ability to interrupt / report progress let mut progress = gix::progress::Discard; - // TODO: Is Options::isolated correct here? - // The url provided from gitlab has the credentials + let mut fetch_opts = gix::remote::ref_map::Options::default(); + fetch_opts.extra_refspecs.extend(refspecs); + let mut fetch = gix::clone::PrepareFetch::new( repo_url, repo_dir.path(), gix::create::Kind::WithWorktree, Default::default(), gix::open::Options::isolated(), - )?; - - // Specify which refspecs are fetched from remote - let mut refspecs = refspecs - .into_iter() - .map(|s| { - gix::refspec::parse(s.as_ref().into(), gix::refspec::parse::Operation::Fetch) - .map(|r| r.to_owned()) - }) - .collect::, _>>()?; - - let mut sha = None; - - // Add object SHA to the fetch as `with_ref_name` does not support SHAs - if let Some(head_ref) = head_ref { - if let Ok(object_id) = gix::ObjectId::from_hex(head_ref.as_bytes()) { - let refspec = gix::refspec::parse( - format!("+{head_ref}").as_str().into(), - gix::refspec::parse::Operation::Fetch, - )? - .to_owned(); - refspecs.push(refspec); - sha = Some(object_id); - } else { - let refspec = gix::refspec::parse( - format!("+refs/heads/{head_ref}:refs/remotes/origin/{head_ref}") - .as_str() - .into(), - gix::refspec::parse::Operation::Fetch, - )? - .to_owned(); - refspecs.push(refspec); - fetch = fetch.with_ref_name(Some(head_ref))? - } - } - - if !refspecs.is_empty() { - let mut fetch_opts = gix::remote::ref_map::Options::default(); - fetch_opts.extra_refspecs.extend(refspecs); - fetch = fetch.with_fetch_options(fetch_opts); - } - - if let Some(depth) = depth.and_then(NonZeroU32::new) { - fetch = fetch.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(depth)); - } + )? + .with_fetch_options(fetch_opts) + .with_shallow(if let Some(depth) = NonZeroU32::new(depth) { + gix::remote::fetch::Shallow::DepthAtRemote(depth) + } else { + gix::remote::fetch::Shallow::NoChange + }); let checkout_progress = progress.add_child("checkout".to_string()); let (checkout, outcome) = fetch.fetch_then_checkout(checkout_progress, should_interrupt)?; @@ -357,38 +318,23 @@ fn clone_git_repository_sync( // Make HEAD point to the given SHA // See: gix::util::update_head - if let Some(sha) = sha { - repo.edit_reference(gix::refs::transaction::RefEdit { - change: gix::refs::transaction::Change::Update { - log: gix::refs::transaction::LogChange { - mode: gix::refs::transaction::RefLog::AndReference, - force_create_reflog: false, - message: format!("clone repo at {}", sha).into(), - }, - expected: gix::refs::transaction::PreviousValue::Any, - new: gix::refs::Target::Object(sha.to_owned()), + repo.edit_reference(gix::refs::transaction::RefEdit { + change: gix::refs::transaction::Change::Update { + log: gix::refs::transaction::LogChange { + mode: gix::refs::transaction::RefLog::AndReference, + force_create_reflog: false, + message: format!("clone repo at {}", sha).into(), }, - name: "HEAD".try_into()?, - deref: false, - })?; - } - - let root_tree_id = if let Some(sha) = sha { - // Checkout worktree at specific SHA - repo.try_find_object(sha)? - .ok_or(GitCheckoutErrorInner::MissingCommit)? - } else if let Some(reference) = head_ref { - repo.try_find_reference(format!("refs/heads/{reference}").as_str())? - .ok_or(GitCheckoutErrorInner::MissingCommit)? - .peel_to_id()? - .object()? - } else { - // Checkout head - repo.head()? - .try_peel_to_id()? - .ok_or(GitCheckoutErrorInner::MissingCommit)? - .object()? - }; + expected: gix::refs::transaction::PreviousValue::Any, + new: gix::refs::Target::Object(sha.to_owned()), + }, + name: "HEAD".try_into()?, + deref: false, + })?; + + let root_tree_id = repo + .try_find_object(sha)? + .ok_or(GitCheckoutErrorInner::MissingCommit)?; let root_tree = root_tree_id.peel_to_tree()?.id; let index = gix::index::State::from_tree(&root_tree, &repo.objects, Default::default()) @@ -534,11 +480,21 @@ impl Job { } } - let parent_path = self.build_dir.to_owned(); + let build_dir = self.build_dir.to_owned(); let repo_url = self.response.git_info.repo_url.to_owned(); - let head_ref = Some(self.response.git_info.sha.to_owned()); - let refspecs: Vec<_> = self.response.git_info.refspecs.clone(); - let depth = Some(self.response.git_info.depth); + let sha = gix::ObjectId::from_hex(self.response.git_info.sha.as_bytes())?; + let refspecs = self + .response + .git_info + .refspecs + .iter() + .map(|s| { + gix::refspec::parse(s.as_str().into(), gix::refspec::parse::Operation::Fetch) + .map(|r| r.to_owned()) + }) + .collect::, _>>()?; + let depth = self.response.git_info.depth; + let should_interrupt: Arc = Default::default(); let _interrupt = InterruptOnDrop { should_interrupt: should_interrupt.clone(), @@ -547,9 +503,9 @@ impl Job { // offload the clone operation to one of the tokio runtimes blocking threads tokio::task::spawn_blocking(move || { clone_git_repository_sync( - &parent_path, + build_dir, &repo_url, - head_ref.as_deref(), + sha, refspecs, depth, &should_interrupt, From af309c5e72009ac6959a8e7b1cc72245cccd6317 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Thu, 7 May 2026 17:09:55 -0500 Subject: [PATCH 13/16] Use more gitoxide helpers for the cloning process Much of this can be replaced with helpers from the crate itself. --- gitlab-runner/src/client.rs | 34 ----------------- gitlab-runner/src/job.rs | 75 ++++++------------------------------- 2 files changed, 12 insertions(+), 97 deletions(-) diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index ef31c14..4b1200a 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -268,47 +268,13 @@ pub enum GitCheckoutErrorInner { #[error(transparent)] GitCheckout(#[from] gix::clone::checkout::main_worktree::Error), #[error(transparent)] - GitIndexFileWrite(#[from] gix::index::file::write::Error), - #[error(transparent)] - GitRefValidate(#[from] gix::index::validate::reference::name::Error), - #[error(transparent)] - GitRemoteFind(#[from] gix::remote::find::existing::Error), - #[error(transparent)] - GitRemoteConnect(#[from] gix::remote::connect::Error), - #[error(transparent)] - GitRemoteFetchPrepare(#[from] gix::remote::fetch::prepare::Error), - #[error(transparent)] - GitRemoteFetch(#[from] gix::remote::fetch::Error), - #[error(transparent)] - GitObjectFind(#[from] gix::object::find::Error), - #[error(transparent)] - GitObjectPeel(#[from] gix::object::peel::to_kind::Error), - #[error(transparent)] - GitObjsFindExisting(#[from] gix::objs::find::existing::Error), - #[error(transparent)] GitRefEdit(#[from] gix::reference::edit::Error), #[error(transparent)] - GitRefIterInit(#[from] gix::reference::iter::init::Error), - #[error(transparent)] - GitRefFindExisting(#[from] gix::reference::find::existing::Error), - #[error(transparent)] - GitRefFind(#[from] gix::reference::find::Error), - #[error(transparent)] - GitRefPeel(#[from] gix::reference::peel::Error), - #[error(transparent)] - GitPackedBufferOpen(#[from] gix::refs::packed::buffer::open::Error), - #[error(transparent)] GitRefspecParse(#[from] gix::refspec::parse::Error), #[error(transparent)] GitHashDecode(#[from] gix::hash::decode::Error), #[error(transparent)] - GitWorktreeCheckout(#[from] gix::worktree::state::checkout::Error), - #[error(transparent)] - GitHeadPeel(#[from] gix::head::peel::Error), - #[error(transparent)] ThreadJoinError(#[from] tokio::task::JoinError), - #[error("Failed to find commit")] - MissingCommit, #[error(transparent)] Write(#[from] std::io::Error), } diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index 2d98a54..6c9c306 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -1,8 +1,7 @@ //! This module describes a single gitlab job use crate::artifact::Artifact; use crate::client::{ - Client, GitCheckoutError, GitCheckoutErrorInner, JobArtifactFile, JobDependency, JobResponse, - JobVariable, + Client, GitCheckoutError, JobArtifactFile, JobDependency, JobResponse, JobVariable, }; use crate::outputln; use bytes::{Bytes, BytesMut}; @@ -11,7 +10,7 @@ use std::collections::HashMap; use std::num::NonZeroU32; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, LazyLock, Mutex}; use tokio::io::AsyncWrite; use tokio_retry2::strategy::{FibonacciBackoff, jitter}; use tracing::{info, trace}; @@ -279,6 +278,8 @@ impl JobLog { } } +static HEAD: LazyLock = LazyLock::new(|| "HEAD".try_into().unwrap()); + fn clone_git_repository_sync( build_dir: PathBuf, repo_url: &str, @@ -309,71 +310,19 @@ fn clone_git_repository_sync( gix::remote::fetch::Shallow::NoChange }); - let checkout_progress = progress.add_child("checkout".to_string()); - let (checkout, outcome) = fetch.fetch_then_checkout(checkout_progress, should_interrupt)?; - + let fetch_progress = progress.add_child("fetch".to_string()); + let (mut checkout, outcome) = fetch.fetch_then_checkout(fetch_progress, should_interrupt)?; trace!("git clone fetch outcome for {repo_url}: {outcome:?}"); - let repo = checkout.persist(); - - // Make HEAD point to the given SHA - // See: gix::util::update_head - repo.edit_reference(gix::refs::transaction::RefEdit { - change: gix::refs::transaction::Change::Update { - log: gix::refs::transaction::LogChange { - mode: gix::refs::transaction::RefLog::AndReference, - force_create_reflog: false, - message: format!("clone repo at {}", sha).into(), - }, - expected: gix::refs::transaction::PreviousValue::Any, - new: gix::refs::Target::Object(sha.to_owned()), - }, - name: "HEAD".try_into()?, - deref: false, - })?; - - let root_tree_id = repo - .try_find_object(sha)? - .ok_or(GitCheckoutErrorInner::MissingCommit)?; - let root_tree = root_tree_id.peel_to_tree()?.id; - - let index = gix::index::State::from_tree(&root_tree, &repo.objects, Default::default()) - .map_err( - |err| gix::clone::checkout::main_worktree::Error::IndexFromTree { - id: root_tree, - source: err, - }, - )?; - - let workdir = repo.workdir().ok_or_else(|| { - gix::clone::checkout::main_worktree::Error::BareRepository { - git_dir: repo.git_dir().to_owned(), - } - })?; - - let files_progress = progress.add_child_with_id( - "files".to_string(), - gix::clone::checkout::main_worktree::ProgressId::CheckoutFiles.into(), - ); - let bytes_progress = progress.add_child_with_id( - "bytes".to_string(), - gix::clone::checkout::main_worktree::ProgressId::BytesWritten.into(), - ); - let mut index = gix::index::File::from_state(index, repo.index_path()); - let outcome = gix::worktree::state::checkout( - &mut index, - workdir, - repo.objects.clone().into_arc()?, - &files_progress, - &bytes_progress, - should_interrupt, - Default::default(), + checkout.repo().reference( + HEAD.clone(), + sha, + gix::refs::transaction::PreviousValue::Any, + format!("clone repo at {sha}"), )?; + let (_repo, outcome) = checkout.main_worktree(progress, should_interrupt)?; trace!("git clone checkout outcome for {repo_url}: {outcome:?}"); - - index.write(Default::default())?; - Ok(repo_dir.keep()) } From cc08849c5de8a2a7054a6adc163dbabcb7e98b20 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Fri, 8 May 2026 11:04:38 -0500 Subject: [PATCH 14/16] Add support for avoiding clones when GIT_STRATEGY=none/empty In these cases, we just return None to signal back to the caller that the clone couldn't be performed for a non-error reason. --- Cargo.lock | 22 ++++++++++++++++++++++ gitlab-runner/Cargo.toml | 1 + gitlab-runner/examples/demo-runner.rs | 8 ++++++-- gitlab-runner/src/client.rs | 21 +++++++++++++++++++++ gitlab-runner/src/job.rs | 20 ++++++++++++++++---- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dbd4e2e..c2915d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,6 +840,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "strum", "tempfile", "thiserror 2.0.18", "tokio", @@ -3228,6 +3229,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/gitlab-runner/Cargo.toml b/gitlab-runner/Cargo.toml index af15516..490916d 100644 --- a/gitlab-runner/Cargo.toml +++ b/gitlab-runner/Cargo.toml @@ -37,6 +37,7 @@ rand = "0.10.0" tokio-util = { version = "0.7.18", features = [ "io" ] } tokio-retry2 = { version = "0.9.1", features = ["jitter"] } gix = { version = "0.83.0", features = [ "blocking-http-transport-reqwest", "worktree-mutation" ] } +strum = { version = "0.28.0", features = ["derive"] } [dev-dependencies] tokio = { version = "1.50.0", features = [ "full", "test-util" ] } diff --git a/gitlab-runner/examples/demo-runner.rs b/gitlab-runner/examples/demo-runner.rs index 09c6da5..f8602b2 100644 --- a/gitlab-runner/examples/demo-runner.rs +++ b/gitlab-runner/examples/demo-runner.rs @@ -128,11 +128,15 @@ impl Run { Some(path) => self.job.build_dir().join(path), _ => self.job.build_dir().to_path_buf(), }; - let temp_repo_path = self + let Some(temp_repo_path) = self .job .clone_git_repository() .await - .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))?; + .map_err(|e| outputln!("Failed to checkout repo: {}", e.to_string()))? + else { + outputln!("Git cloning is disabled, nothing to do"); + return Ok(()); + }; outputln!("Checked out to {:?}", temp_repo_path); diff --git a/gitlab-runner/src/client.rs b/gitlab-runner/src/client.rs index 4b1200a..5a166a2 100644 --- a/gitlab-runner/src/client.rs +++ b/gitlab-runner/src/client.rs @@ -259,6 +259,25 @@ impl JobResponse { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString)] +#[strum( + serialize_all = "lowercase", + parse_err_ty = GitCheckoutError, + parse_err_fn = GitStrategy::make_error, +)] +pub enum GitStrategy { + Clone, + Fetch, + None, + Empty, +} + +impl GitStrategy { + fn make_error(s: &str) -> GitCheckoutError { + GitCheckoutErrorInner::BadGitStrategy(s.to_owned()).into() + } +} + #[derive(Error, Debug)] pub enum GitCheckoutErrorInner { #[error(transparent)] @@ -277,6 +296,8 @@ pub enum GitCheckoutErrorInner { ThreadJoinError(#[from] tokio::task::JoinError), #[error(transparent)] Write(#[from] std::io::Error), + #[error("invalid GIT_STRATEGY: {0}")] + BadGitStrategy(String), } #[derive(Error, Debug)] diff --git a/gitlab-runner/src/job.rs b/gitlab-runner/src/job.rs index 6c9c306..b75ec61 100644 --- a/gitlab-runner/src/job.rs +++ b/gitlab-runner/src/job.rs @@ -1,7 +1,7 @@ //! This module describes a single gitlab job use crate::artifact::Artifact; use crate::client::{ - Client, GitCheckoutError, JobArtifactFile, JobDependency, JobResponse, JobVariable, + Client, GitCheckoutError, GitStrategy, JobArtifactFile, JobDependency, JobResponse, JobVariable, }; use crate::outputln; use bytes::{Bytes, BytesMut}; @@ -9,6 +9,7 @@ use gix::NestedProgress; use std::collections::HashMap; use std::num::NonZeroU32; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Mutex}; use tokio::io::AsyncWrite; @@ -415,10 +416,20 @@ impl Job { * * git checkout $sha */ - /// Fetch and checkout worktree for job + /// Fetch and checkout worktree for job, returning the checkout directory /// - /// This creates a new path for the repo as gitoxide deletes it on failure. - pub async fn clone_git_repository(&self) -> Result { + /// If GIT_STRATEGY is set to disable fetching (i.e. `none` or `empty`), + /// returns None. + pub async fn clone_git_repository(&self) -> Result, GitCheckoutError> { + if let Some(strategy) = self.variable("GIT_STRATEGY") + && matches!( + GitStrategy::from_str(strategy.value())?, + GitStrategy::None | GitStrategy::Empty + ) + { + return Ok(None); + } + struct InterruptOnDrop { should_interrupt: Arc, } @@ -461,5 +472,6 @@ impl Job { ) }) .await? + .map(Some) } } From b297f5aaaae2cd7aaca28a4e85e92bdcc1f2bc5b Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Tue, 5 May 2026 10:06:51 -0500 Subject: [PATCH 15/16] mock: Add the ability to customize more GitInfo fields --- gitlab-runner-mock/src/api/request.rs | 11 ++++----- gitlab-runner-mock/src/job.rs | 34 +++++++++++++++++++++++++++ gitlab-runner-mock/src/lib.rs | 3 ++- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/gitlab-runner-mock/src/api/request.rs b/gitlab-runner-mock/src/api/request.rs index 3afd191..50a2569 100644 --- a/gitlab-runner-mock/src/api/request.rs +++ b/gitlab-runner-mock/src/api/request.rs @@ -156,16 +156,13 @@ impl Respond for JobRequestResponder { "project_name": "gitlab-test" }, "git_info": { - "repo_url": "https://bla/dummy.git", + "repo_url": job.git_info().repo_url, "ref": "custom", - "sha": "265c14cf140a66cfc61e40e4ab45c95ba8df5ed1", + "sha": job.git_info().sha, "before_sha": "fc40ad32cdd36b814f07a540605110edc209a38c", "ref_type": "branch", - "refspecs": [ - "+265c14cf140a66cfc61e40e4ab45c95ba8df5ed1:refs/pipelines/120", - "+refs/heads/custom:refs/remotes/origin/custom" - ], - "depth": 50 + "refspecs": job.git_info().refspecs, + "depth": job.git_info().depth }, "runner_info": { "timeout": 3600, diff --git a/gitlab-runner-mock/src/job.rs b/gitlab-runner-mock/src/job.rs index 72b411d..9e2aa72 100644 --- a/gitlab-runner-mock/src/job.rs +++ b/gitlab-runner-mock/src/job.rs @@ -117,6 +117,28 @@ pub struct MockJobArtifact { pub expire_in: Option, } +#[derive(Clone, Debug)] +pub struct MockJobGitInfo { + pub repo_url: String, + pub sha: String, + pub refspecs: Vec, + pub depth: u32, +} + +impl Default for MockJobGitInfo { + fn default() -> Self { + Self { + repo_url: "https://bla/dummy.git".to_string(), + sha: "265c14cf140a66cfc61e40e4ab45c95ba8df5ed1".to_string(), + refspecs: vec![ + "+265c14cf140a66cfc61e40e4ab45c95ba8df5ed1:refs/pipelines/120".to_string(), + "+refs/heads/custom:refs/remotes/origin/custom".to_string(), + ], + depth: 50, + } + } +} + #[derive(Clone, Debug)] pub struct MockJob { name: String, @@ -126,6 +148,7 @@ pub struct MockJob { steps: Vec, dependencies: Vec, artifacts: Vec, + git_info: MockJobGitInfo, inner: Arc>, } @@ -163,6 +186,7 @@ impl MockJob { steps: Vec::new(), dependencies: Vec::new(), artifacts: Vec::new(), + git_info: MockJobGitInfo::default(), inner: Arc::new(Mutex::new(MockJobInner { state: MockJobState::Success, state_updates: 2, @@ -193,6 +217,10 @@ impl MockJob { &self.artifacts } + pub fn git_info(&self) -> &MockJobGitInfo { + &self.git_info + } + pub fn variables(&self) -> &[MockJobVariable] { &self.variables } @@ -296,6 +324,7 @@ pub struct MockJobBuilder { steps: Vec, dependencies: Vec, artifacts: Vec, + git_info: MockJobGitInfo, } impl MockJobBuilder { @@ -384,6 +413,10 @@ impl MockJobBuilder { self.dependencies.push(dependency); } + pub fn git_info(&mut self, git_info: MockJobGitInfo) { + self.git_info = git_info; + } + pub fn build(self) -> MockJob { assert!(!self.steps.is_empty(), "Should have at least one step"); let inner = MockJobInner { @@ -403,6 +436,7 @@ impl MockJobBuilder { variables: self.variables.into_values().collect(), dependencies: self.dependencies, artifacts: self.artifacts, + git_info: self.git_info, inner, } } diff --git a/gitlab-runner-mock/src/lib.rs b/gitlab-runner-mock/src/lib.rs index 1aff6f6..36a6c61 100644 --- a/gitlab-runner-mock/src/lib.rs +++ b/gitlab-runner-mock/src/lib.rs @@ -10,7 +10,8 @@ use wiremock::{Mock, MockServer}; mod job; mod variables; pub use job::{ - MockJob, MockJobArtifactWhen, MockJobBuilder, MockJobState, MockJobStepName, MockJobStepWhen, + MockJob, MockJobArtifactWhen, MockJobBuilder, MockJobGitInfo, MockJobState, MockJobStepName, + MockJobStepWhen, }; mod api; From bd1249888258a838c3b82bed1cf3318499ec1dcb Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Tue, 5 May 2026 10:06:51 -0500 Subject: [PATCH 16/16] Add tests for clone_git_repository --- Cargo.lock | 163 ++++++++++++++++++++++ gitlab-runner/Cargo.toml | 2 + gitlab-runner/tests/integration.rs | 209 ++++++++++++++++++++++++++++- 3 files changed, 373 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index c2915d6..8e431fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "assert_cmd" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -541,6 +556,12 @@ dependencies = [ "syn", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -748,6 +769,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.32" @@ -823,6 +850,7 @@ name = "gitlab-runner" version = "0.3.0" dependencies = [ "anyhow", + "assert_cmd", "async-trait", "bytes", "clap", @@ -837,6 +865,7 @@ dependencies = [ "pin-project", "rand 0.10.0", "reqwest", + "rstest", "serde", "serde_json", "sha2", @@ -2649,6 +2678,33 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2659,6 +2715,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2849,6 +2914,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.13.2" @@ -2908,12 +2979,50 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rustc-hash" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3321,6 +3430,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -3491,6 +3606,36 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.3" @@ -3749,6 +3894,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4211,6 +4365,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + [[package]] name = "wiremock" version = "0.6.5" diff --git a/gitlab-runner/Cargo.toml b/gitlab-runner/Cargo.toml index 490916d..3db4a2a 100644 --- a/gitlab-runner/Cargo.toml +++ b/gitlab-runner/Cargo.toml @@ -46,6 +46,8 @@ gitlab-runner-mock = { path = "../gitlab-runner-mock" } futures = "0.3.32" anyhow = "1.0.102" clap = { version = "4.6.0", features = ["derive"] } +assert_cmd = "2.2.1" +rstest = "0.26.1" [build-dependencies] vergen-gitcl = "9.1.0" diff --git a/gitlab-runner/tests/integration.rs b/gitlab-runner/tests/integration.rs index d160de1..42308e1 100644 --- a/gitlab-runner/tests/integration.rs +++ b/gitlab-runner/tests/integration.rs @@ -1,8 +1,10 @@ +use assert_cmd::Command; +use assert_cmd::assert::OutputAssertExt; use futures::future; use gitlab_runner::job::Job; use gitlab_runner::{GitlabLayer, JobHandler, JobResult, Phase, Runner, RunnerBuilder, outputln}; use gitlab_runner_mock::{ - GitlabRunnerMock, MockJob, MockJobState, MockJobStepName, MockJobStepWhen, + GitlabRunnerMock, MockJob, MockJobGitInfo, MockJobState, MockJobStepName, MockJobStepWhen, }; use std::fmt::Debug; use std::future::Future; @@ -13,6 +15,7 @@ use tracing::instrument::WithSubscriber; use tracing_subscriber::Registry; use tracing_subscriber::layer::SubscriberExt; +use rstest::rstest; use std::time::Duration; use tokio::sync::oneshot::{self, Receiver, Sender}; use tokio::time::sleep; @@ -749,3 +752,207 @@ async fn job_drain() { .with_subscriber(subscriber) .await; } + +const GIT_BRANCH: &str = "main"; +const GIT_COMMITS: usize = 3; +const GIT_FILE: &str = "file"; +const GIT_FILE_CONTENT: &str = "content"; + +struct TestRepo { + dir: TempDir, + sha: String, +} + +impl TestRepo { + fn file_url(&self) -> String { + format!("file://{}", self.dir.path().as_os_str().to_str().unwrap()) + } + + fn refspec(&self) -> String { + format!("+{}:refs/heads/{GIT_BRANCH}", self.sha) + } +} + +#[rstest::fixture] +fn test_repo() -> TestRepo { + let dir = tempfile::tempdir().unwrap(); + Command::new("git") + .args(["init", "-b", GIT_BRANCH]) + .current_dir(dir.path()) + .assert() + .success(); + + Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(dir.path()) + .assert() + .success(); + Command::new("git") + .args(["config", "user.name", "test"]) + .current_dir(dir.path()) + .assert() + .success(); + + let contents_by_commit: [&str; GIT_COMMITS] = ["hello", "world", GIT_FILE_CONTENT]; + for (i, content) in contents_by_commit.iter().enumerate() { + std::fs::write(dir.path().join(GIT_FILE), content).unwrap(); + Command::new("git") + .args(["add", GIT_FILE]) + .current_dir(dir.path()) + .assert() + .success(); + Command::new("git") + .args(["commit", "-m", &format!("Commit #{}", i + 1)]) + .current_dir(dir.path()) + .assert() + .success(); + } + + let mut sha = + std::fs::read_to_string(dir.path().join(format!(".git/refs/heads/{GIT_BRANCH}"))).unwrap(); + sha.truncate(sha.trim_end().len()); + + // Ensure that HEAD switching actually works in the clones by changing the + // upstream repo HEAD to a different branch. + Command::new("git") + .args(["switch", "--orphan", "empty"]) + .current_dir(dir.path()) + .assert() + .success(); + + TestRepo { dir, sha } +} + +#[rstest] +#[case(0)] +#[case(1)] +#[case(2)] +#[tokio::test] +async fn job_clone(test_repo: TestRepo, #[case] depth: u32) { + let mock = GitlabRunnerMock::start().await; + let mut builder = mock.job_builder("clone test".to_string()); + builder.add_step( + MockJobStepName::Script, + vec!["dummy".to_string()], + 3600, + MockJobStepWhen::OnSuccess, + false, + ); + builder.git_info(MockJobGitInfo { + repo_url: test_repo.file_url(), + sha: test_repo.sha.clone(), + refspecs: vec![test_repo.refspec()], + depth, + }); + + let job = builder.build(); + mock.enqueue_job(job.clone()); + + let (mut runner, subscriber, _dir) = setup_runner(&mock).await; + async move { + let got_job = runner + .request_job(async move |job| { + let cloned = job.clone_git_repository().await.unwrap().unwrap(); + + let head = std::fs::read_to_string(cloned.join(".git/HEAD")).unwrap(); + assert_eq!(head.trim(), test_repo.sha); + + let content = std::fs::read_to_string(cloned.join(GIT_FILE)).unwrap(); + assert_eq!(content, GIT_FILE_CONTENT); + + let count_cmd = Command::new("git") + .args(["rev-list", "--count", "HEAD"]) + .current_dir(&cloned) + .output() + .unwrap() + .assert() + .success(); + let count: u32 = String::from_utf8(count_cmd.get_output().stdout.clone()) + .unwrap() + .trim() + .parse() + .unwrap(); + + assert_eq!( + count, + if depth == 0 { + GIT_COMMITS as u32 + } else { + depth + } + ); + + SimpleRun::dummy(Ok(())).await + }) + .await + .unwrap(); + assert!(got_job); + runner.wait_for_space(1).await; + assert_eq!(MockJobState::Success, job.state()); + } + .with_subscriber(subscriber) + .await; +} + +#[derive(Debug, Clone, PartialEq, Eq, strum::Display)] +#[strum(serialize_all = "lowercase")] +enum GitStrategyTest { + None, + Invalid, +} + +#[rstest] +#[case(GitStrategyTest::None)] +#[case(GitStrategyTest::Invalid)] +#[tokio::test] +async fn job_clone_strategy_none(test_repo: TestRepo, #[case] strategy: GitStrategyTest) { + let mock = GitlabRunnerMock::start().await; + let mut builder = mock.job_builder("clone test".to_string()); + builder.add_step( + MockJobStepName::Script, + vec!["dummy".to_string()], + 3600, + MockJobStepWhen::OnSuccess, + false, + ); + builder.add_variable( + "GIT_STRATEGY".to_string(), + format!("{strategy}"), + false, + false, + ); + builder.git_info(MockJobGitInfo { + repo_url: test_repo.file_url(), + sha: test_repo.sha.clone(), + refspecs: vec![test_repo.refspec()], + depth: 0, + }); + + let job = builder.build(); + mock.enqueue_job(job.clone()); + + let (mut runner, subscriber, _dir) = setup_runner(&mock).await; + async move { + let got_job = runner + .request_job(async move |job| { + let ret = job.clone_git_repository().await; + match strategy { + GitStrategyTest::None => assert!(ret.unwrap().is_none()), + GitStrategyTest::Invalid => { + assert_eq!( + ret.unwrap_err().0.to_string(), + format!("invalid GIT_STRATEGY: {strategy}") + ); + } + } + SimpleRun::dummy(Ok(())).await + }) + .await + .unwrap(); + assert!(got_job); + runner.wait_for_space(1).await; + assert_eq!(MockJobState::Success, job.state()); + } + .with_subscriber(subscriber) + .await; +}