mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 02:26:14 +03:00
initial ssh tests and async git2 backend implementation
allow certain git backends to temporarily disable IO tests
This commit is contained in:
parent
f2c3a571a7
commit
b90c9235a3
315
Cargo.lock
generated
315
Cargo.lock
generated
@ -17,6 +17,16 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.3"
|
||||
@ -28,6 +38,20 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
@ -368,6 +392,17 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt-pbkdf"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
|
||||
dependencies = [
|
||||
"blowfish",
|
||||
"pbkdf2 0.12.2",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@ -377,6 +412,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
@ -410,6 +451,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.4.1"
|
||||
@ -426,6 +476,16 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.3.4"
|
||||
@ -548,6 +608,15 @@ dependencies = [
|
||||
"toml 0.7.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.83"
|
||||
@ -600,6 +669,17 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.33"
|
||||
@ -862,6 +942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
@ -902,6 +983,15 @@ dependencies = [
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.0"
|
||||
@ -916,6 +1006,7 @@ dependencies = [
|
||||
"platforms",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -977,6 +1068,12 @@ dependencies = [
|
||||
"parking_lot_core 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
|
||||
[[package]]
|
||||
name = "debugid"
|
||||
version = "0.8.0"
|
||||
@ -994,6 +1091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@ -1069,7 +1167,16 @@ version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1093,6 +1200,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
@ -1151,6 +1270,7 @@ version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
@ -1162,7 +1282,10 @@ checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"sha2",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1183,6 +1306,7 @@ dependencies = [
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"pem-rfc7468",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sec1",
|
||||
@ -1713,6 +1837,16 @@ dependencies = [
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.12.0"
|
||||
@ -1876,10 +2010,14 @@ dependencies = [
|
||||
name = "gitbutler-git"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dirs 5.0.1",
|
||||
"futures",
|
||||
"git2",
|
||||
"nix 0.27.1",
|
||||
"rand 0.8.5",
|
||||
"russh",
|
||||
"russh-keys",
|
||||
"serde",
|
||||
"sysinfo",
|
||||
"thiserror",
|
||||
@ -2138,6 +2276,12 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-literal"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@ -2409,6 +2553,7 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
@ -3050,6 +3195,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
@ -3194,6 +3351,12 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "3.2.0"
|
||||
@ -3258,6 +3421,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@ -3437,6 +3606,15 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@ -3705,6 +3883,29 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@ -4169,7 +4370,7 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "321e5e41b3b192dab6f1e75b9deacb6688b4b8c5e68906a78e8f43e7c2887bb5"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"dirs 4.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4243,6 +4444,92 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "russh"
|
||||
version = "0.41.0-beta.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f5a2a1836739e0dbbdb6efe481b37a540aea25ffa0af466ebb7790fc2f8f9a3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"async-trait",
|
||||
"bitflags 2.4.0",
|
||||
"byteorder",
|
||||
"chacha20",
|
||||
"ctr",
|
||||
"curve25519-dalek",
|
||||
"digest",
|
||||
"flate2",
|
||||
"futures",
|
||||
"generic-array",
|
||||
"hex-literal",
|
||||
"hmac",
|
||||
"log",
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"poly1305",
|
||||
"rand 0.8.5",
|
||||
"russh-cryptovec",
|
||||
"russh-keys",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "russh-cryptovec"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b077b6dd8d8c085dac62f7fcc5a83df60c7f7a22d49bfba994f2f4dbf60bc74"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "russh-keys"
|
||||
version = "0.41.0-beta.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d04bf4f4bea01661f1d7574607ffa510bbb11d19ffa91cda44c24feaa6e0960"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-trait",
|
||||
"bcrypt-pbkdf",
|
||||
"bit-vec",
|
||||
"block-padding",
|
||||
"byteorder",
|
||||
"cbc",
|
||||
"ctr",
|
||||
"data-encoding",
|
||||
"dirs 5.0.1",
|
||||
"ed25519-dalek",
|
||||
"futures",
|
||||
"hmac",
|
||||
"inout",
|
||||
"log",
|
||||
"md5",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"openssl",
|
||||
"p256",
|
||||
"p521",
|
||||
"pbkdf2 0.11.0",
|
||||
"rand 0.7.3",
|
||||
"rand_core 0.6.4",
|
||||
"russh-cryptovec",
|
||||
"serde",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
@ -4698,7 +4985,7 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"dirs 5.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5851,6 +6138,16 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.7.1"
|
||||
@ -6648,6 +6945,16 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
"num-bigint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "3.14.1"
|
||||
@ -6734,7 +7041,7 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
"hmac",
|
||||
"pbkdf2",
|
||||
"pbkdf2 0.11.0",
|
||||
"sha1",
|
||||
"time",
|
||||
"zstd",
|
||||
|
@ -18,23 +18,27 @@ required-features = ["cli"]
|
||||
|
||||
[features]
|
||||
default = ["git2", "cli", "serde", "tokio"]
|
||||
cli = ["std", "dep:nix", "dep:rand", "dep:futures", "dep:sysinfo"]
|
||||
git2 = ["dep:git2", "std"]
|
||||
cli = ["dep:nix", "dep:rand", "dep:futures", "dep:sysinfo"]
|
||||
git2 = ["dep:git2", "dep:dirs"]
|
||||
serde = ["dep:serde"]
|
||||
std = ["dep:thiserror"]
|
||||
tokio = ["dep:tokio"]
|
||||
|
||||
[dependencies]
|
||||
thiserror.workspace = true
|
||||
git2 = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, optional = true, features = ["process", "rt", "process", "time", "io-util", "net", "fs"]}
|
||||
tokio = { workspace = true, optional = true, features = ["process", "rt", "process", "time", "io-util", "net", "fs", "sync"]}
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
futures = { version = "0.3.30", optional = true }
|
||||
sysinfo = { version = "0.30.5", optional = true }
|
||||
dirs = { version = "5.0.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"]}
|
||||
git2.workspace = true # Used for tests
|
||||
async-trait = "0.1.77"
|
||||
russh = { version = "0.41.0-beta.4", features = ["openssl"] }
|
||||
russh-keys = "0.41.0-beta.3"
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
|
||||
[target."cfg(unix)".dependencies]
|
||||
nix = { version = "0.27.1", optional = true, features = ["process", "socket", "user"] }
|
||||
|
@ -1,4 +1,6 @@
|
||||
#[cfg(feature = "cli")]
|
||||
pub mod cli;
|
||||
#[cfg(feature = "git2")]
|
||||
|
||||
// We use the libgit2 backend for tests as well.
|
||||
#[cfg(any(test, feature = "git2"))]
|
||||
pub mod git2;
|
||||
|
@ -27,10 +27,11 @@ mod tests {
|
||||
.join(test_name);
|
||||
let _ = std::fs::remove_dir_all(&repo_path);
|
||||
std::fs::create_dir_all(&repo_path).unwrap();
|
||||
|
||||
Repository::open_or_init(executor::tokio::TokioExecutor, repo_path.to_str().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
crate::gitbutler_git_integration_tests!(make_repo);
|
||||
crate::gitbutler_git_integration_tests!(make_repo, enable_io);
|
||||
}
|
||||
|
@ -1,36 +1,33 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::io::{BufRead, BufReader, BufWriter, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
pub fn main(sock_path: &str, secret: &str, prompt: &str) {
|
||||
let mut stream = UnixStream::connect(sock_path).expect("connect():");
|
||||
let raw_stream = UnixStream::connect(sock_path).expect("connect():");
|
||||
|
||||
// Set a timer for 10s.
|
||||
stream
|
||||
raw_stream
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(10)))
|
||||
.expect("set_read_timeout():");
|
||||
|
||||
let mut reader = BufReader::new(raw_stream.try_clone().unwrap());
|
||||
let mut writer = BufWriter::new(raw_stream);
|
||||
|
||||
// Write the secret.
|
||||
stream
|
||||
.write_all(secret.as_bytes())
|
||||
.expect("write_all(secret):");
|
||||
writeln!(writer, "{secret}").expect("write(secret):");
|
||||
|
||||
// Write the prompt that Git gave us.
|
||||
stream
|
||||
.write_all(prompt.as_bytes())
|
||||
.expect("write_all(prompt):");
|
||||
writeln!(writer, "{prompt}").expect("write(prompt):");
|
||||
|
||||
writer.flush().expect("flush():");
|
||||
|
||||
// Wait for the response.
|
||||
let mut buf = [0; 2048];
|
||||
let n = stream.read(&mut buf).expect("read():");
|
||||
|
||||
// TODO(qix-): Figure out a way to do a single timeout
|
||||
// TODO(qix-): but allow any response size.
|
||||
if n == buf.len() {
|
||||
panic!("response too long");
|
||||
let mut password = String::new();
|
||||
let nread = reader.read_line(&mut password).expect("read_line():");
|
||||
if nread == 0 {
|
||||
panic!("read_line() returned 0");
|
||||
}
|
||||
|
||||
// Write the response back to Git.
|
||||
std::io::stdout()
|
||||
.write_all(&buf[..n])
|
||||
.expect("write_all(stdout):");
|
||||
// `password` already has a newline at the end.
|
||||
write!(std::io::stdout(), "{password}").expect("write(password):");
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ pub fn main() {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn main() {
|
||||
let has_pipe_var = std::env::var("GITBUTLER_ASKPASS_PIPE")
|
||||
.map(|v| v != "")
|
||||
.map(|v| !v.is_empty())
|
||||
.unwrap_or(false);
|
||||
if !has_pipe_var {
|
||||
panic!("This binary is only meant to be run by GitButler; please do not use it yourself as it's entirely unstable.");
|
||||
|
@ -1,5 +1,4 @@
|
||||
use crate::prelude::*;
|
||||
use core::time::Duration;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
pub mod tokio;
|
||||
@ -42,7 +41,7 @@ pub unsafe trait GitExecutor {
|
||||
///
|
||||
/// Otherwise, `Ok` is returned in call cases, even when
|
||||
/// the exit code is non-zero.
|
||||
type Error: core::error::Error + core::fmt::Debug + Send + Sync + 'static;
|
||||
type Error: std::error::Error + core::fmt::Debug + Send + Sync + 'static;
|
||||
|
||||
/// The type of the handle returned by [`GitExecutor::create_askpass_server`].
|
||||
type ServerHandle: AskpassServer + Send + Sync + 'static;
|
||||
@ -60,7 +59,7 @@ pub unsafe trait GitExecutor {
|
||||
async fn execute_raw(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
envs: Option<HashMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error>;
|
||||
|
||||
/// Executes the given Git command with sane defaults.
|
||||
@ -71,7 +70,7 @@ pub unsafe trait GitExecutor {
|
||||
async fn execute(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
envs: Option<HashMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error> {
|
||||
let mut args = args.as_ref().to_vec();
|
||||
|
||||
@ -174,7 +173,7 @@ pub struct FileStat {
|
||||
/// Upon dropping the handle, the server should be closed.
|
||||
pub trait AskpassServer: core::fmt::Display {
|
||||
/// The type of error that is returned by [`AskpassServer::accept`].
|
||||
type Error: core::error::Error + core::fmt::Debug + Send + Sync + 'static;
|
||||
type Error: std::error::Error + core::fmt::Debug + Send + Sync + 'static;
|
||||
|
||||
/// The type of the socket yielded by the incoming iterator.
|
||||
type SocketHandle: Socket + Send + Sync + 'static;
|
||||
@ -202,7 +201,7 @@ pub type Uid = u32;
|
||||
/// is established.
|
||||
pub trait Socket {
|
||||
/// The error type returned by I/O operations on this socket.
|
||||
type Error: core::error::Error + core::fmt::Debug + Send + Sync + 'static;
|
||||
type Error: std::error::Error + core::fmt::Debug + Send + Sync + 'static;
|
||||
|
||||
/// The process ID of the connecting client.
|
||||
fn pid(&self) -> Result<Pid, Self::Error>;
|
||||
|
@ -1,10 +1,8 @@
|
||||
//! A [Tokio](https://tokio.rs)-based [`super::GitExecutor`] implementation.
|
||||
|
||||
use crate::prelude::*;
|
||||
use core::time::Duration;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
|
||||
use std::{collections::HashMap, fs::Permissions, os::unix::fs::PermissionsExt, time::Duration};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// A [`super::GitExecutor`] implementation using the `git` command-line tool
|
||||
@ -19,10 +17,24 @@ unsafe impl super::GitExecutor for TokioExecutor {
|
||||
async fn execute_raw(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
envs: Option<HashMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error> {
|
||||
let mut cmd = Command::new("git");
|
||||
|
||||
// Output the command being executed to stderr, for debugging purposes
|
||||
// (only on test configs).
|
||||
#[cfg(test)]
|
||||
{
|
||||
let mut envs_str = String::new();
|
||||
if let Some(envs) = &envs {
|
||||
for (key, value) in envs.iter() {
|
||||
envs_str.push_str(&format!("{}={} ", key, value));
|
||||
}
|
||||
}
|
||||
let args_str = args.join(" ");
|
||||
eprintln!("env {envs_str} git {args_str}");
|
||||
}
|
||||
|
||||
cmd.kill_on_drop(true);
|
||||
cmd.args(args);
|
||||
|
||||
@ -88,12 +100,13 @@ impl super::Socket for tokio::io::BufStream<tokio::net::UnixStream> {
|
||||
async fn read_line(&mut self) -> Result<String, Self::Error> {
|
||||
let mut buf = String::new();
|
||||
<Self as tokio::io::AsyncBufReadExt>::read_line(self, &mut buf).await?;
|
||||
Ok(buf)
|
||||
Ok(buf.trim_end_matches(|c| c == '\r' || c == '\n').into())
|
||||
}
|
||||
|
||||
async fn write_line(&mut self, line: &str) -> Result<(), Self::Error> {
|
||||
<Self as tokio::io::AsyncWriteExt>::write_all(self, line.as_bytes()).await?;
|
||||
<Self as tokio::io::AsyncWriteExt>::write_all(self, b"\n").await?;
|
||||
<Self as tokio::io::AsyncWriteExt>::flush(self).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -119,7 +132,7 @@ impl super::AskpassServer for TokioAskpassServer {
|
||||
self.server.as_ref().unwrap().accept().await
|
||||
};
|
||||
|
||||
Ok(res.map(|(s, _)| tokio::io::BufStream::new(s))?)
|
||||
res.map(|(s, _)| tokio::io::BufStream::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
use super::executor::{AskpassServer, GitExecutor, Pid, Socket};
|
||||
use crate::{prelude::*, Authorization, ConfigScope};
|
||||
use core::time::Duration;
|
||||
use crate::{Authorization, ConfigScope, RefSpec};
|
||||
use futures::{select, FutureExt};
|
||||
use rand::Rng;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
/// The number of characters in the secret used for checking
|
||||
/// askpass invocations by ssh/git when connecting to our process.
|
||||
@ -13,9 +13,9 @@ const ASKPASS_SECRET_LENGTH: usize = 24;
|
||||
/// You probably don't want to use this type. Use [`Error`] instead.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RepositoryError<
|
||||
Eexec: core::error::Error + core::fmt::Debug + Send + Sync + 'static,
|
||||
Easkpass: core::error::Error + core::fmt::Debug + Send + Sync + 'static,
|
||||
Esocket: core::error::Error + core::fmt::Debug + Send + Sync + 'static,
|
||||
Eexec: std::error::Error + core::fmt::Debug + Send + Sync + 'static,
|
||||
Easkpass: std::error::Error + core::fmt::Debug + Send + Sync + 'static,
|
||||
Esocket: std::error::Error + core::fmt::Debug + Send + Sync + 'static,
|
||||
> {
|
||||
#[error("failed to execute git command: {0}")]
|
||||
Exec(Eexec),
|
||||
@ -24,9 +24,14 @@ pub enum RepositoryError<
|
||||
#[error("i/o error communicating with askpass utility: {0}")]
|
||||
AskpassIo(Esocket),
|
||||
#[error(
|
||||
"git command exited with non-zero exit code {0}: {1:?}\n\nSTDOUT:\n{2}\n\nSTDERR:\n{3}"
|
||||
"git command exited with non-zero exit code {status}: {args:?}\n\nSTDOUT:\n{stdout}\n\nSTDERR:\n{stderr}"
|
||||
)]
|
||||
Failed(usize, Vec<String>, String, String),
|
||||
Failed {
|
||||
status: usize,
|
||||
args: Vec<String>,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
},
|
||||
#[error("failed to determine path to this executable: {0}")]
|
||||
NoSelfExe(std::io::Error),
|
||||
#[error("askpass secret mismatch")]
|
||||
@ -75,6 +80,7 @@ impl<E: GitExecutor> Repository<E> {
|
||||
|
||||
/// (Re-)initializes a repository at the given path
|
||||
/// using the given [`GitExecutor`].
|
||||
#[cold]
|
||||
pub async fn open_or_init<P: AsRef<str>>(exec: E, path: P) -> Result<Self, Error<E>> {
|
||||
let path = path.as_ref().to_owned();
|
||||
let args = vec!["init", "--quiet", &path];
|
||||
@ -85,28 +91,62 @@ impl<E: GitExecutor> Repository<E> {
|
||||
if exit_code == 0 {
|
||||
Ok(Self { exec, path })
|
||||
} else {
|
||||
Err(Error::<E>::Failed(
|
||||
exit_code,
|
||||
args.into_iter().map(Into::into).collect(),
|
||||
Err(Error::<E>::Failed {
|
||||
status: exit_code,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// (Re-)initializes a bare repository at the given path
|
||||
/// using the given [`GitExecutor`].
|
||||
#[cold]
|
||||
pub async fn open_or_init_bare<P: AsRef<str>>(exec: E, path: P) -> Result<Self, Error<E>> {
|
||||
let path = path.as_ref().to_owned();
|
||||
let args = vec!["init", "--bare", "--quiet", &path];
|
||||
|
||||
let (exit_code, stdout, stderr) =
|
||||
exec.execute(&args, None).await.map_err(Error::<E>::Exec)?;
|
||||
|
||||
if exit_code == 0 {
|
||||
Ok(Self { exec, path })
|
||||
} else {
|
||||
Err(Error::<E>::Failed {
|
||||
status: exit_code,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
async fn execute_with_auth_harness(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
envs: Option<HashMap<String, String>>,
|
||||
authorization: &Authorization,
|
||||
) -> Result<(usize, String, String), Error<E>> {
|
||||
let path = std::env::current_exe().map_err(|e| Error::<E>::NoSelfExe(e))?;
|
||||
let our_pid = std::process::id();
|
||||
let path = std::env::current_exe().map_err(Error::<E>::NoSelfExe)?;
|
||||
|
||||
// TODO(qix-): Get parent PID of connecting processes to make sure they're us.
|
||||
//let our_pid = std::process::id();
|
||||
|
||||
// TODO(qix-): This is a bit of a hack. Under a test environment,
|
||||
// TODO(qix-): Cargo is running a test runner with a quasi-random
|
||||
// TODO(qix-): suffix. The actual executables live in the parent directory.
|
||||
// TODO(qix-): Thus, we have to do this under test. It's not ideal, but
|
||||
// TODO(qix-): it works for now.
|
||||
#[cfg(test)]
|
||||
let path = path.parent().unwrap();
|
||||
|
||||
let askpath_path = path
|
||||
.with_file_name("gitbutler-git-askpass")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let setsid_path = path
|
||||
.with_file_name("gitbutler-git-setsid")
|
||||
@ -118,6 +158,7 @@ impl<E: GitExecutor> Repository<E> {
|
||||
.stat(&askpath_path)
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)?;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let setsid_stat = self
|
||||
.exec
|
||||
@ -125,7 +166,10 @@ impl<E: GitExecutor> Repository<E> {
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)?;
|
||||
|
||||
let sock_path = std::env::temp_dir().join(format!("gitbutler-git-{our_pid}.sock"));
|
||||
#[allow(unsafe_code)]
|
||||
let sock_server = unsafe { self.exec.create_askpass_server() }
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)?;
|
||||
|
||||
// FIXME(qix-): This is probably not cryptographically secure, did this in a bit
|
||||
// FIXME(qix-): of a hurry. We should probably use a proper CSPRNG here, but this
|
||||
@ -138,50 +182,77 @@ impl<E: GitExecutor> Repository<E> {
|
||||
.collect::<String>();
|
||||
|
||||
let mut envs = envs.unwrap_or_default();
|
||||
envs.insert(
|
||||
"GITBUTLER_ASKPASS_PIPE".into(),
|
||||
sock_path.to_string_lossy().into_owned(),
|
||||
);
|
||||
envs.insert("GITBUTLER_ASKPASS_PIPE".into(), sock_server.to_string());
|
||||
envs.insert("GITBUTLER_ASKPASS_SECRET".into(), secret.clone());
|
||||
envs.insert("SSH_ASKPASS".into(), askpath_path);
|
||||
|
||||
// DISPLAY is required by SSH to check SSH_ASKPASS.
|
||||
// Please don't ask us why, it's unclear.
|
||||
if !std::env::var("DISPLAY").map(|v| v != "").unwrap_or(false) {
|
||||
if !std::env::var("DISPLAY")
|
||||
.map(|v| !v.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
envs.insert("DISPLAY".into(), ":".into());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
envs.insert(
|
||||
"GIT_SSH_COMMAND".into(),
|
||||
format!(
|
||||
"{} {}",
|
||||
setsid_path,
|
||||
envs.get("GIT_SSH_COMMAND").unwrap_or(&"ssh".into())
|
||||
"{}{}{} -o StrictHostKeyChecking=accept-new -o KbdInteractiveAuthentication=no{}",
|
||||
{
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
format!("{setsid_path} ")
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
""
|
||||
}
|
||||
},
|
||||
envs.get("GIT_SSH_COMMAND").unwrap_or(&"ssh".into()),
|
||||
match authorization {
|
||||
Authorization::Ssh { .. } => " -o PreferredAuthentications=publickey",
|
||||
Authorization::Basic { .. } => " -o PreferredAuthentications=password",
|
||||
_ => "",
|
||||
},
|
||||
{
|
||||
// In test environments, we don't want to pollute the user's known hosts file.
|
||||
// So, we just use /dev/null instead.
|
||||
#[cfg(test)]
|
||||
{
|
||||
" -o UserKnownHostsFile=/dev/null"
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
""
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
if let Authorization::Ssh { private_key, .. } = authorization {
|
||||
if let Some(private_key) = private_key {
|
||||
envs.insert("GIT_SSH_VARIANT".into(), "ssh".into());
|
||||
envs.insert("GIT_SSH_KEY".into(), private_key.clone());
|
||||
}
|
||||
if let Authorization::Ssh {
|
||||
private_key: Some(private_key),
|
||||
..
|
||||
} = authorization
|
||||
{
|
||||
envs.insert("GIT_SSH_VARIANT".into(), "ssh".into());
|
||||
envs.insert("GIT_SSH_KEY".into(), private_key.clone());
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
let sock_server = unsafe { self.exec.create_askpass_server() }
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)?;
|
||||
|
||||
let mut child_process = core::pin::pin! {async {
|
||||
self.exec
|
||||
.execute(args, Some(envs))
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)
|
||||
}.fuse()};
|
||||
let mut child_process = core::pin::pin! {
|
||||
async {
|
||||
self.exec
|
||||
.execute(args, Some(envs))
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)
|
||||
}.fuse()
|
||||
};
|
||||
|
||||
loop {
|
||||
select! {
|
||||
res = child_process => {
|
||||
return res;
|
||||
},
|
||||
res = sock_server.accept(Some(Duration::from_secs(60))).fuse() => {
|
||||
let mut sock = res.map_err(Error::<E>::AskpassServer)?;
|
||||
|
||||
@ -202,14 +273,14 @@ impl<E: GitExecutor> Repository<E> {
|
||||
|
||||
if peer_stat.ino == askpath_stat.ino {
|
||||
if peer_stat.dev != askpath_stat.dev {
|
||||
return Err(Error::<E>::AskpassDeviceMismatch);
|
||||
return Err(Error::<E>::AskpassDeviceMismatch)?;
|
||||
}
|
||||
} else if peer_stat.ino == setsid_stat.ino {
|
||||
if peer_stat.dev != setsid_stat.dev {
|
||||
return Err(Error::<E>::AskpassDeviceMismatch);
|
||||
return Err(Error::<E>::AskpassDeviceMismatch)?;
|
||||
}
|
||||
} else {
|
||||
return Err(Error::<E>::AskpassExecutableMismatch);
|
||||
return Err(Error::<E>::AskpassExecutableMismatch)?;
|
||||
}
|
||||
|
||||
// await for peer to send secret
|
||||
@ -217,39 +288,47 @@ impl<E: GitExecutor> Repository<E> {
|
||||
|
||||
// check the secret
|
||||
if peer_secret.trim() != secret {
|
||||
return Err(Error::<E>::AskpassSecretMismatch);
|
||||
return Err(Error::<E>::AskpassSecretMismatch)?;
|
||||
}
|
||||
|
||||
// get the prompt
|
||||
let prompt = sock.read_line().await.map_err(Error::<E>::AskpassIo)?;
|
||||
|
||||
// TODO(qix-): The prompt matching logic here is fragile as the remote
|
||||
// TODO(qix-): can customize prompts. I need to investigate if there's
|
||||
// TODO(qix-): a better way to do this.
|
||||
match authorization {
|
||||
Authorization::Auto => {
|
||||
return Err(Error::<E>::NeedsAuthorization(prompt));
|
||||
return Err(Error::<E>::NeedsAuthorization(prompt))?;
|
||||
}
|
||||
Authorization::Basic{username, password} => {
|
||||
if prompt.contains("Username for") {
|
||||
sock.write_line(username).await.map_err(Error::<E>::AskpassIo)?;
|
||||
} else if prompt.contains("Password for") {
|
||||
sock.write_line(password).await.map_err(Error::<E>::AskpassIo)?;
|
||||
if prompt.to_lowercase().contains("username:") || prompt.to_lowercase().contains("username for") {
|
||||
if let Some(username) = username {
|
||||
sock.write_line(username).await.map_err(Error::<E>::AskpassIo)?;
|
||||
} else {
|
||||
return Err(Error::<E>::NeedsAuthorization(prompt))?;
|
||||
}
|
||||
} else if prompt.to_lowercase().contains("password:") || prompt.to_lowercase().contains("password for") {
|
||||
if let Some(password) = password {
|
||||
sock.write_line(password).await.map_err(Error::<E>::AskpassIo)?;
|
||||
} else {
|
||||
return Err(Error::<E>::NeedsAuthorization(prompt))?;
|
||||
}
|
||||
} else {
|
||||
return Err(Error::<E>::NeedsAuthorization(prompt));
|
||||
return Err(Error::<E>::NeedsAuthorization(prompt))?;
|
||||
}
|
||||
},
|
||||
Authorization::Ssh { passphrase, .. } => {
|
||||
if let Some(passphrase) = passphrase {
|
||||
if prompt.contains("passphrase for key") {
|
||||
sock.write_line(passphrase).await.map_err(Error::<E>::AskpassIo)?;
|
||||
continue;
|
||||
}
|
||||
if prompt.contains("passphrase for key") {
|
||||
sock.write_line(passphrase).await.map_err(Error::<E>::AskpassIo)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return Err(Error::<E>::NeedsAuthorization(prompt));
|
||||
return Err(Error::<E>::NeedsAuthorization(prompt))?;
|
||||
}
|
||||
}
|
||||
},
|
||||
res = child_process => {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -263,7 +342,7 @@ impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
&self,
|
||||
key: &str,
|
||||
scope: ConfigScope,
|
||||
) -> Result<Option<String>, Self::Error> {
|
||||
) -> Result<Option<String>, crate::Error<Self::Error>> {
|
||||
let mut args = vec!["-C", &self.path, "config", "--get"];
|
||||
|
||||
// NOTE(qix-): See source comments for ConfigScope to explain
|
||||
@ -291,12 +370,12 @@ impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
} else if exit_code == 1 && stderr.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(Error::<E>::Failed(
|
||||
exit_code,
|
||||
args.into_iter().map(Into::into).collect(),
|
||||
Err(Error::<E>::Failed {
|
||||
status: exit_code,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
))
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,7 +384,7 @@ impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
key: &str,
|
||||
value: &str,
|
||||
scope: ConfigScope,
|
||||
) -> Result<(), Self::Error> {
|
||||
) -> Result<(), crate::Error<Self::Error>> {
|
||||
let mut args = vec!["-C", &self.path, "config", "--replace-all"];
|
||||
|
||||
// NOTE(qix-): See source comments for ConfigScope to explain
|
||||
@ -332,12 +411,159 @@ impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
if exit_code == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::<E>::Failed(
|
||||
exit_code,
|
||||
args.into_iter().map(Into::into).collect(),
|
||||
Err(Error::<E>::Failed {
|
||||
status: exit_code,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
))
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch(
|
||||
&self,
|
||||
remote: &str,
|
||||
refspec: RefSpec,
|
||||
authorization: &Authorization,
|
||||
) -> Result<(), crate::Error<Self::Error>> {
|
||||
let mut args = vec![
|
||||
"-C",
|
||||
&self.path,
|
||||
"fetch",
|
||||
"--quiet",
|
||||
"--no-write-fetch-head",
|
||||
];
|
||||
|
||||
let refspec = refspec.to_string();
|
||||
|
||||
args.push(remote);
|
||||
args.push(&refspec);
|
||||
|
||||
let (status, stdout, stderr) = self
|
||||
.execute_with_auth_harness(&args, None, authorization)
|
||||
.await?;
|
||||
|
||||
if status == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
// Was the ref not found?
|
||||
if let Some(refname) = stderr
|
||||
.lines()
|
||||
.find(|line| line.to_lowercase().contains("couldn't find remote ref"))
|
||||
.map(|line| line.split_whitespace().last().unwrap_or_default())
|
||||
{
|
||||
Err(crate::Error::RefNotFound(refname.to_owned()))?
|
||||
} else if stderr.to_lowercase().contains("permission denied") {
|
||||
Err(crate::Error::AuthorizationFailed(Error::<E>::Failed {
|
||||
status,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
}))?
|
||||
} else {
|
||||
Err(Error::<E>::Failed {
|
||||
status,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
})?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_remote(
|
||||
&self,
|
||||
remote: &str,
|
||||
uri: &str,
|
||||
) -> Result<(), crate::Error<Self::Error>> {
|
||||
let args = vec!["-C", &self.path, "remote", "add", remote, uri];
|
||||
|
||||
let (status, stdout, stderr) = self
|
||||
.exec
|
||||
.execute(&args, None)
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)?;
|
||||
|
||||
if status != 0 {
|
||||
Err(Error::<E>::Failed {
|
||||
status,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
})?
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_or_update_remote(
|
||||
&self,
|
||||
remote: &str,
|
||||
uri: &str,
|
||||
) -> Result<(), crate::Error<Self::Error>> {
|
||||
let created = self
|
||||
.create_remote(remote, uri)
|
||||
.await
|
||||
.map(|_| true)
|
||||
.or_else(|e| match e {
|
||||
crate::Error::RemoteExists(..) => Ok(false),
|
||||
e => Err(e),
|
||||
})?;
|
||||
|
||||
if created {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let args = vec!["-C", &self.path, "remote", "set-url", remote, uri];
|
||||
|
||||
let (status, stdout, stderr) = self
|
||||
.exec
|
||||
.execute(&args, None)
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)?;
|
||||
|
||||
if status == 0 {
|
||||
Ok(())
|
||||
} else if status != 0 && stderr.to_lowercase().contains("error: no such remote") {
|
||||
self.create_remote(remote, uri).await
|
||||
} else {
|
||||
Err(Error::<E>::Failed {
|
||||
status,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
async fn remote(&self, remote: &str) -> Result<String, crate::Error<Self::Error>> {
|
||||
let args = vec!["-C", &self.path, "remote", "get-url", remote];
|
||||
|
||||
let (status, stdout, stderr) = self
|
||||
.exec
|
||||
.execute(&args, None)
|
||||
.await
|
||||
.map_err(Error::<E>::Exec)?;
|
||||
|
||||
if status == 0 {
|
||||
Ok(stdout)
|
||||
} else if status != 0 && stderr.to_lowercase().contains("error: no such remote") {
|
||||
Err(crate::Error::NoSuchRemote(
|
||||
remote.to_owned(),
|
||||
Error::<E>::Failed {
|
||||
status,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
},
|
||||
))?
|
||||
} else {
|
||||
Err(Error::<E>::Failed {
|
||||
status,
|
||||
args: args.into_iter().map(Into::into).collect(),
|
||||
stdout,
|
||||
stderr,
|
||||
})?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,15 @@
|
||||
//! The entry point for this module is the [`Repository`] struct.
|
||||
|
||||
mod repository;
|
||||
mod thread_resource;
|
||||
|
||||
pub use self::repository::Repository;
|
||||
#[cfg(feature = "tokio")]
|
||||
pub use self::thread_resource::tokio;
|
||||
|
||||
pub use self::{
|
||||
repository::Repository,
|
||||
thread_resource::{ThreadedResource, ThreadedResourceHandle},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@ -19,8 +26,11 @@ mod tests {
|
||||
.join(test_name);
|
||||
let _ = std::fs::remove_dir_all(&repo_path);
|
||||
std::fs::create_dir_all(&repo_path).unwrap();
|
||||
Repository::open_or_init(&repo_path).unwrap()
|
||||
|
||||
Repository::<tokio::TokioThreadedResource>::open_or_init(&repo_path)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
crate::gitbutler_git_integration_tests!(make_repo);
|
||||
crate::gitbutler_git_integration_tests!(make_repo, disable_io);
|
||||
}
|
||||
|
@ -1,34 +1,42 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::ConfigScope;
|
||||
use super::{ThreadedResource, ThreadedResourceHandle};
|
||||
use crate::{Authorization, ConfigScope, RefSpec};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A [`crate::Repository`] implementation using the `git2` crate.
|
||||
pub struct Repository {
|
||||
repo: git2::Repository,
|
||||
pub struct Repository<R: ThreadedResource> {
|
||||
repo: R::Handle<git2::Repository>,
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
impl<R: ThreadedResource> Repository<R> {
|
||||
/// Initializes a repository at the given path.
|
||||
///
|
||||
/// Errors if the repository is already initialized.
|
||||
#[inline]
|
||||
pub fn init<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
pub async fn init<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
Ok(Self {
|
||||
repo: git2::Repository::init_opts(
|
||||
path,
|
||||
git2::RepositoryInitOptions::new().no_reinit(true),
|
||||
)?,
|
||||
repo: R::new(|| {
|
||||
git2::Repository::init_opts(
|
||||
path,
|
||||
git2::RepositoryInitOptions::new().no_reinit(true),
|
||||
)
|
||||
})
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Opens a repository at the given path, or initializes it if it doesn't exist.
|
||||
#[inline]
|
||||
pub fn open_or_init<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
pub async fn open_or_init<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
Ok(Self {
|
||||
repo: git2::Repository::init_opts(
|
||||
path,
|
||||
git2::RepositoryInitOptions::new().no_reinit(false),
|
||||
)?,
|
||||
repo: R::new(|| {
|
||||
git2::Repository::init_opts(
|
||||
path,
|
||||
git2::RepositoryInitOptions::new().no_reinit(false),
|
||||
)
|
||||
})
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
@ -36,77 +44,94 @@ impl Repository {
|
||||
///
|
||||
/// Errors if the repository is already initialized.
|
||||
#[inline]
|
||||
pub fn init_bare<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
pub async fn init_bare<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
Ok(Self {
|
||||
repo: git2::Repository::init_opts(
|
||||
path,
|
||||
git2::RepositoryInitOptions::new()
|
||||
.no_reinit(true)
|
||||
.bare(true),
|
||||
)?,
|
||||
repo: R::new(|| {
|
||||
git2::Repository::init_opts(
|
||||
path,
|
||||
git2::RepositoryInitOptions::new()
|
||||
.no_reinit(true)
|
||||
.bare(true),
|
||||
)
|
||||
})
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Opens a repository at the given path, or initializes a new bare repository
|
||||
/// if it doesn't exist.
|
||||
#[inline]
|
||||
pub fn open_or_init_bare<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
pub async fn open_or_init_bare<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
Ok(Self {
|
||||
repo: git2::Repository::init_opts(
|
||||
path,
|
||||
git2::RepositoryInitOptions::new()
|
||||
.no_reinit(false)
|
||||
.bare(true),
|
||||
)?,
|
||||
repo: R::new(|| {
|
||||
git2::Repository::init_opts(
|
||||
path,
|
||||
git2::RepositoryInitOptions::new()
|
||||
.no_reinit(false)
|
||||
.bare(true),
|
||||
)
|
||||
})
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Opens a repository at the given path.
|
||||
/// Will error if there's no existing repository at the given path.
|
||||
#[inline]
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
pub async fn open<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
Ok(Self {
|
||||
repo: git2::Repository::open(path)?,
|
||||
repo: R::new(|| git2::Repository::open(path)).await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Repository for Repository {
|
||||
impl<R: ThreadedResource> crate::Repository for Repository<R> {
|
||||
type Error = git2::Error;
|
||||
|
||||
async fn config_get(
|
||||
&self,
|
||||
key: &str,
|
||||
#[cfg_attr(test, allow(unused_variables))] scope: ConfigScope,
|
||||
) -> Result<Option<String>, Self::Error> {
|
||||
let config = self.repo.config()?;
|
||||
) -> Result<Option<String>, crate::Error<Self::Error>> {
|
||||
let key = key.to_owned();
|
||||
self.repo
|
||||
.with(move |repo| {
|
||||
let config = repo.config()?;
|
||||
|
||||
#[cfg(test)]
|
||||
let scope = ConfigScope::Local;
|
||||
#[cfg(test)]
|
||||
let scope = ConfigScope::Local;
|
||||
|
||||
// NOTE(qix-): See source comments for ConfigScope to explain
|
||||
// NOTE(qix-): the `#[cfg(not(test))]` attributes.
|
||||
let res = match scope {
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::Auto => config.get_string(key),
|
||||
ConfigScope::Local => config.open_level(git2::ConfigLevel::Local)?.get_string(key),
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::System => config
|
||||
.open_level(git2::ConfigLevel::System)?
|
||||
.get_string(key),
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::Global => config
|
||||
.open_level(git2::ConfigLevel::Global)?
|
||||
.get_string(key),
|
||||
};
|
||||
// NOTE(qix-): See source comments for ConfigScope to explain
|
||||
// NOTE(qix-): the `#[cfg(not(test))]` attributes.
|
||||
let res = match scope {
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::Auto => config.get_string(&key),
|
||||
ConfigScope::Local => config
|
||||
.open_level(git2::ConfigLevel::Local)?
|
||||
.get_string(&key),
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::System => config
|
||||
.open_level(git2::ConfigLevel::System)?
|
||||
.get_string(&key),
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::Global => config
|
||||
.open_level(git2::ConfigLevel::Global)?
|
||||
.get_string(&key),
|
||||
};
|
||||
|
||||
res.map(Some).or_else(|e| {
|
||||
if e.code() == git2::ErrorCode::NotFound {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
Ok(res.map(Some).or_else(|e| {
|
||||
if e.code() == git2::ErrorCode::NotFound {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})?)
|
||||
})
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
async fn config_set(
|
||||
@ -114,29 +139,190 @@ impl crate::Repository for Repository {
|
||||
key: &str,
|
||||
value: &str,
|
||||
#[cfg_attr(test, allow(unused_variables))] scope: ConfigScope,
|
||||
) -> Result<(), Self::Error> {
|
||||
#[cfg_attr(test, allow(unused_mut))]
|
||||
let mut config = self.repo.config()?;
|
||||
) -> Result<(), crate::Error<Self::Error>> {
|
||||
let key = key.to_owned();
|
||||
let value = value.to_owned();
|
||||
|
||||
#[cfg(test)]
|
||||
let scope = ConfigScope::Local;
|
||||
self.repo
|
||||
.with(move |repo| {
|
||||
#[cfg_attr(test, allow(unused_mut))]
|
||||
let mut config = repo.config()?;
|
||||
|
||||
// NOTE(qix-): See source comments for ConfigScope to explain
|
||||
// NOTE(qix-): the `#[cfg(not(test))]` attributes.
|
||||
match scope {
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::Auto => config.set_str(key, value),
|
||||
ConfigScope::Local => config
|
||||
.open_level(git2::ConfigLevel::Local)?
|
||||
.set_str(key, value),
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::System => config
|
||||
.open_level(git2::ConfigLevel::System)?
|
||||
.set_str(key, value),
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::Global => config
|
||||
.open_level(git2::ConfigLevel::Global)?
|
||||
.set_str(key, value),
|
||||
}
|
||||
#[cfg(test)]
|
||||
let scope = ConfigScope::Local;
|
||||
|
||||
// NOTE(qix-): See source comments for ConfigScope to explain
|
||||
// NOTE(qix-): the `#[cfg(not(test))]` attributes.
|
||||
match scope {
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::Auto => Ok(config.set_str(&key, &value)?),
|
||||
ConfigScope::Local => Ok(config
|
||||
.open_level(git2::ConfigLevel::Local)?
|
||||
.set_str(&key, &value)?),
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::System => Ok(config
|
||||
.open_level(git2::ConfigLevel::System)?
|
||||
.set_str(&key, &value)?),
|
||||
#[cfg(not(test))]
|
||||
ConfigScope::Global => Ok(config
|
||||
.open_level(git2::ConfigLevel::Global)?
|
||||
.set_str(&key, &value)?),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch(
|
||||
&self,
|
||||
remote: &str,
|
||||
refspec: RefSpec,
|
||||
authorization: &Authorization,
|
||||
) -> Result<(), crate::Error<Self::Error>> {
|
||||
let remote = remote.to_owned();
|
||||
let authorization = authorization.clone();
|
||||
|
||||
self.repo
|
||||
.with(move |repo| {
|
||||
let mut remote = repo.find_remote(&remote)?;
|
||||
|
||||
let mut callbacks = git2::RemoteCallbacks::new();
|
||||
|
||||
callbacks.credentials(|_url, username, _allowed| {
|
||||
let auth = match &authorization {
|
||||
Authorization::Auto => {
|
||||
let cred = git2::Cred::default()?;
|
||||
Ok(cred)
|
||||
}
|
||||
Authorization::Basic { username, password } => {
|
||||
let username = username.as_deref().unwrap_or_default();
|
||||
let password = password.as_deref().unwrap_or_default();
|
||||
|
||||
git2::Cred::userpass_plaintext(username, password)
|
||||
}
|
||||
Authorization::Ssh {
|
||||
passphrase,
|
||||
private_key,
|
||||
} => {
|
||||
let private_key =
|
||||
private_key.as_ref().map(PathBuf::from).unwrap_or_else(|| {
|
||||
let mut path = dirs::home_dir().unwrap();
|
||||
path.push(".ssh");
|
||||
path.push("id_rsa");
|
||||
path
|
||||
});
|
||||
|
||||
let username = username
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| std::env::var("USER").unwrap_or_default());
|
||||
|
||||
git2::Cred::ssh_key(
|
||||
&username,
|
||||
None,
|
||||
&private_key,
|
||||
passphrase.clone().as_deref(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
auth
|
||||
});
|
||||
|
||||
let mut fetch_options = git2::FetchOptions::new();
|
||||
fetch_options.remote_callbacks(callbacks);
|
||||
|
||||
let refspec = refspec.to_string();
|
||||
|
||||
let r = remote.fetch(&[&refspec], Some(&mut fetch_options), None);
|
||||
|
||||
r.map_err(|e| {
|
||||
if e.code() == git2::ErrorCode::NotFound {
|
||||
crate::Error::RefNotFound(refspec)
|
||||
} else {
|
||||
e.into()
|
||||
}
|
||||
})
|
||||
})
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_remote(
|
||||
&self,
|
||||
remote: &str,
|
||||
uri: &str,
|
||||
) -> Result<(), crate::Error<Self::Error>> {
|
||||
let remote = remote.to_owned();
|
||||
let uri = uri.to_owned();
|
||||
|
||||
self.repo
|
||||
.with(move |repo| {
|
||||
repo.remote(&remote, &uri).map_err(|e| {
|
||||
if e.code() == git2::ErrorCode::Exists {
|
||||
crate::Error::RemoteExists(remote.to_owned(), e)
|
||||
} else {
|
||||
e.into()
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_or_update_remote(
|
||||
&self,
|
||||
remote: &str,
|
||||
uri: &str,
|
||||
) -> Result<(), crate::Error<Self::Error>> {
|
||||
let remote = remote.to_owned();
|
||||
let uri = uri.to_owned();
|
||||
|
||||
self.repo
|
||||
.with(move |repo| {
|
||||
let r = repo
|
||||
.find_remote(&remote)
|
||||
.and_then(|_| repo.remote_set_url(&remote, &uri));
|
||||
|
||||
if let Err(e) = r {
|
||||
if e.code() == git2::ErrorCode::NotFound {
|
||||
repo.remote(&remote, &uri)?;
|
||||
} else {
|
||||
Err(e)?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remote(&self, remote: &str) -> Result<String, crate::Error<Self::Error>> {
|
||||
let remote = remote.to_owned();
|
||||
|
||||
self.repo
|
||||
.with(move |repo| {
|
||||
let r = repo.find_remote(&remote);
|
||||
|
||||
let r = match r {
|
||||
Err(e) if e.code() == git2::ErrorCode::NotFound => {
|
||||
return Err(crate::Error::NoSuchRemote(remote, e))?;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e)?;
|
||||
}
|
||||
Ok(r) => r,
|
||||
};
|
||||
|
||||
let url = r.url().ok_or_else(|| {
|
||||
crate::Error::NoSuchRemote(remote, git2::Error::from_str("remote has no URL"))
|
||||
})?;
|
||||
|
||||
Ok(url.to_string())
|
||||
})
|
||||
.await
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
56
gitbutler-git/src/backend/git2/thread_resource.rs
Normal file
56
gitbutler-git/src/backend/git2/thread_resource.rs
Normal file
@ -0,0 +1,56 @@
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
pub mod tokio;
|
||||
|
||||
/// A resource that is held on an owning thread, and that can be
|
||||
/// asynchronously locked and interacted with via lambda functions.
|
||||
///
|
||||
/// This is used to interact with `git2` resources in a thread-safe
|
||||
/// manner, since `git2` is not thread-safe nor asynchronous.
|
||||
pub trait ThreadedResource {
|
||||
/// The type of handle returned by [`Self::new`].
|
||||
type Handle<T: Unpin + Sized + 'static>: ThreadedResourceHandle<T>;
|
||||
|
||||
/// Creates a new resource; the function passed in will be
|
||||
/// executed on the owning thread, the result of which becomes
|
||||
/// the owned value that is later interacted with.
|
||||
async fn new<T, F, E>(f: F) -> Result<Self::Handle<T>, E>
|
||||
where
|
||||
F: FnOnce() -> Result<T, E> + Send + 'static,
|
||||
T: Unpin + Sized + 'static,
|
||||
E: Send + 'static;
|
||||
}
|
||||
|
||||
/// A handle to a resource that is held on an owning thread.
|
||||
/// This handle can be used to asynchronously lock the resource
|
||||
/// and interact with it via lambda functions.
|
||||
///
|
||||
/// Returned by [`ThreadedResource::new`].
|
||||
pub trait ThreadedResourceHandle<T: Unpin + Sized + 'static> {
|
||||
/// The type of future returned by [`Self::with`].
|
||||
type WithFuture<'a, R>: std::future::Future<Output = R> + Send
|
||||
where
|
||||
Self: 'a,
|
||||
R: Send + Unpin + 'static;
|
||||
|
||||
/// Locks the resource, and passes the locked value to the given
|
||||
/// function, which can then interact with it. The function is
|
||||
/// executed on the owning thread, and the result is returned
|
||||
/// to the calling thread asynchronously.
|
||||
///
|
||||
/// Note that this is an async-async function - meaning, it
|
||||
/// must be awaited in order to receive the future that actually
|
||||
/// executes the code, which itself must also be awaited.
|
||||
//
|
||||
// FIXME(qix-): I think I'm too stupid to understand pinning and phantom
|
||||
// FIXME(qix-): data, regardless of how many times I deep-dive into it.
|
||||
// FIXME(qix-): I'm now ~48 hours (nearly straight) into this problem,
|
||||
// FIXME(qix-): and I've lost a great deal of sanity trying to figure out
|
||||
// FIXME(qix-): how to make this work. For now, the async-async function
|
||||
// FIXME(qix-): will have to do, but I'm not happy with it. If you know
|
||||
// FIXME(qix-): how to make this work, please PLEASE please send a PR.
|
||||
// FIXME(qix-): I'm losing sleep and hair over this.
|
||||
async fn with<F, R>(&self, f: F) -> Self::WithFuture<'_, R>
|
||||
where
|
||||
F: FnOnce(&mut T) -> R + Send + Unpin + 'static,
|
||||
R: Send + Unpin + 'static;
|
||||
}
|
177
gitbutler-git/src/backend/git2/thread_resource/tokio.rs
Normal file
177
gitbutler-git/src/backend/git2/thread_resource/tokio.rs
Normal file
@ -0,0 +1,177 @@
|
||||
//! A [Tokio](https://tokio.rs)-based implementation for [libgit2](https://libgit2.org/)
|
||||
//! repository backends, allowing normally blocking libgit2 operations to be run on a
|
||||
//! threadpool, asynchronously.
|
||||
|
||||
use futures::Future;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{atomic::AtomicBool, Arc, Barrier, Mutex as SyncMutex},
|
||||
task::{Context, Poll, Waker},
|
||||
thread::{JoinHandle, Thread},
|
||||
};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
/// A [`super::ThreadedResource`] implementation using Tokio.
|
||||
pub struct TokioThreadedResource;
|
||||
|
||||
/// A [`super::ThreadedResourceHandle`] implementation using Tokio.
|
||||
pub struct TokioThreadedResourceHandle<T: Unpin + Sized + 'static> {
|
||||
terminate: Arc<AtomicBool>,
|
||||
thread: JoinHandle<()>,
|
||||
access_control_mutex: Arc<AsyncMutex<()>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
slot: Arc<SyncMutex<Option<(Waker, Box<dyn FnOnce(&mut T) + Send>)>>>,
|
||||
}
|
||||
|
||||
impl super::ThreadedResource for TokioThreadedResource {
|
||||
type Handle<T: Unpin + Sized + 'static> = TokioThreadedResourceHandle<T>;
|
||||
|
||||
async fn new<T, F, E>(f: F) -> Result<Self::Handle<T>, E>
|
||||
where
|
||||
F: FnOnce() -> Result<T, E> + Send + 'static,
|
||||
T: Unpin + Sized + 'static,
|
||||
E: Send + 'static,
|
||||
{
|
||||
#[allow(clippy::type_complexity)]
|
||||
let slot: Arc<SyncMutex<Option<(Waker, Box<dyn FnOnce(&mut T) + Send>)>>> =
|
||||
Arc::new(SyncMutex::new(None));
|
||||
|
||||
let maybe_error = Arc::new(SyncMutex::new(None));
|
||||
let barrier = Arc::new(Barrier::new(2));
|
||||
|
||||
let terminate_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let thread = std::thread::spawn({
|
||||
let slot = Arc::clone(&slot);
|
||||
let barrier = Arc::clone(&barrier);
|
||||
let maybe_error = Arc::clone(&maybe_error);
|
||||
let terminate_signal = Arc::clone(&terminate_signal);
|
||||
move || {
|
||||
let mut v = match f() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
*maybe_error.lock().unwrap() = Some(e);
|
||||
barrier.wait();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
barrier.wait();
|
||||
|
||||
loop {
|
||||
if terminate_signal.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
std::thread::park();
|
||||
if terminate_signal.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some((waker, fun)) = slot.lock().unwrap().take() {
|
||||
fun(&mut v);
|
||||
waker.wake();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
barrier.wait();
|
||||
|
||||
if let Some(e) = maybe_error.lock().unwrap().take() {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(TokioThreadedResourceHandle {
|
||||
thread,
|
||||
slot,
|
||||
access_control_mutex: Arc::new(AsyncMutex::new(())),
|
||||
terminate: terminate_signal,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for TokioThreadedResourceHandle<T>
|
||||
where
|
||||
T: Unpin + Sized + 'static,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
self.terminate
|
||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
self.thread.thread().unpark();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> super::ThreadedResourceHandle<T> for TokioThreadedResourceHandle<T>
|
||||
where
|
||||
T: Unpin + Sized + 'static,
|
||||
{
|
||||
type WithFuture<'a, R> = impl Future<Output = R> + Send
|
||||
where
|
||||
Self: 'a,
|
||||
R: Send + Unpin + 'static;
|
||||
|
||||
async fn with<F, R>(&self, f: F) -> Self::WithFuture<'_, R>
|
||||
where
|
||||
F: FnOnce(&mut T) -> R + Send + Unpin + 'static,
|
||||
R: Send + Unpin + 'static,
|
||||
{
|
||||
let guard = self.access_control_mutex.lock().await;
|
||||
|
||||
let result_slot = Arc::new(SyncMutex::new(Option::<R>::None));
|
||||
let result_slot_clone = Arc::clone(&result_slot);
|
||||
let slot = Arc::clone(&self.slot);
|
||||
|
||||
let boxed_f = Box::new(move |v: &mut T| {
|
||||
*result_slot.lock().unwrap() = Some(f(v));
|
||||
});
|
||||
|
||||
TokioThreadedResourceHandleFuture {
|
||||
set_fun: Some(Box::new(move |waker| {
|
||||
slot.lock().unwrap().replace((waker, boxed_f));
|
||||
})),
|
||||
result_slot: result_slot_clone,
|
||||
handle: self.thread.thread(),
|
||||
_access_guard: guard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The future returned by [`TokioThreadedResourceHandle`]::with.
|
||||
pub struct TokioThreadedResourceHandleFuture<'thread, R, Guard>
|
||||
where
|
||||
R: Send + Unpin + 'static,
|
||||
Guard: Unpin,
|
||||
{
|
||||
set_fun: Option<Box<dyn FnOnce(Waker) + Send + Unpin + 'static>>,
|
||||
result_slot: Arc<SyncMutex<Option<R>>>,
|
||||
_access_guard: Guard,
|
||||
handle: &'thread Thread,
|
||||
}
|
||||
|
||||
impl<'thread, R, Guard> Future for TokioThreadedResourceHandleFuture<'thread, R, Guard>
|
||||
where
|
||||
R: Send + Unpin + 'static,
|
||||
Guard: Unpin,
|
||||
{
|
||||
type Output = R;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<R> {
|
||||
let this = self.as_mut().get_mut();
|
||||
|
||||
if let Some(set_fun) = this.set_fun.take() {
|
||||
set_fun(cx.waker().clone());
|
||||
this.handle.unpark();
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
if let Ok(mut result_slot) = this.result_slot.try_lock() {
|
||||
if let Some(result) = result_slot.take() {
|
||||
return Poll::Ready(result);
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
pub(crate) mod private;
|
||||
|
||||
/// To use in a backend, create a function that initializes
|
||||
/// an empty repository, whatever that looks like, and returns
|
||||
/// something that implements the `Repository` trait.
|
||||
@ -20,11 +22,11 @@
|
||||
/// ```
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! gitbutler_git_integration_tests {
|
||||
($create_repo:expr) => {
|
||||
$crate::gitbutler_git_integration_tests! {
|
||||
$create_repo,
|
||||
($create_repo:expr, $io_tests:tt) => {
|
||||
$crate::private::test_impl! {
|
||||
$create_repo, enable_io,
|
||||
|
||||
async fn create_repo_selftest(_repo) {
|
||||
async fn create_repo_selftest(repo) {
|
||||
// Do-nothing, just a selftest.
|
||||
}
|
||||
|
||||
@ -35,30 +37,94 @@ macro_rules! gitbutler_git_integration_tests {
|
||||
crate::ops::set_utmost_discretion(&repo, false).await.unwrap();
|
||||
assert_eq!(crate::ops::has_utmost_discretion(&repo).await.unwrap(), false);
|
||||
}
|
||||
|
||||
async fn non_existent_remote(repo) {
|
||||
use crate::*;
|
||||
match repo.remote("non-existent").await.unwrap_err() {
|
||||
Error::NoSuchRemote(remote, _) => assert_eq!(remote, "non-existent"),
|
||||
err => panic!("expected NoSuchRemote, got {:?}", err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_remote(repo) {
|
||||
use crate::*;
|
||||
|
||||
match repo.remote("origin").await {
|
||||
Err($crate::Error::NoSuchRemote(remote, _)) if remote == "origin" => {},
|
||||
result => panic!("expected remote 'origin' query to fail with NoSuchRemote, but got {result:?}")
|
||||
}
|
||||
|
||||
repo.create_remote("origin", "https://example.com/test.git").await.unwrap();
|
||||
|
||||
assert_eq!(repo.remote("origin").await.unwrap(), "https://example.com/test.git".to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
$crate::private::test_impl! {
|
||||
$create_repo, $io_tests,
|
||||
|
||||
async fn fetch_with_ssh_basic_bad_password(repo, server, server_repo) {
|
||||
use crate::*;
|
||||
|
||||
server.allow_authorization(Authorization::Basic {
|
||||
username: Some("my_username".to_owned()),
|
||||
password: Some("my_password".to_owned())
|
||||
});
|
||||
|
||||
server.run_with_server(async move |port| {
|
||||
repo.create_remote("origin", &format!("[my_username@localhost:{port}]:test.git")).await.unwrap();
|
||||
|
||||
let err = repo.fetch(
|
||||
"origin",
|
||||
RefSpec{
|
||||
source: Some("refs/heads/master".to_owned()),
|
||||
destination: Some("refs/heads/master".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
&Authorization::Basic {
|
||||
username: Some("my_username".to_owned()),
|
||||
password: Some("wrong_password".to_owned()),
|
||||
}
|
||||
).await.unwrap_err();
|
||||
|
||||
match err {
|
||||
Error::AuthorizationFailed(_) => {},
|
||||
_ => panic!("expected AuthorizationFailed, got {:?}", err),
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
async fn fetch_with_ssh_basic_no_master(repo, server, server_repo) {
|
||||
use crate::*;
|
||||
|
||||
let auth = Authorization::Basic {
|
||||
username: Some("my_username".to_owned()),
|
||||
password: Some("my_password".to_owned()),
|
||||
};
|
||||
server.allow_authorization(auth.clone());
|
||||
|
||||
server.run_with_server(async move |port| {
|
||||
repo.create_remote("origin", &format!("[my_username@localhost:{port}]:test.git")).await.unwrap();
|
||||
|
||||
let err = repo.fetch(
|
||||
"origin",
|
||||
RefSpec{
|
||||
source: Some("refs/heads/master".to_owned()),
|
||||
destination: Some("refs/heads/master".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
&auth
|
||||
).await.unwrap_err();
|
||||
|
||||
if let Error::RefNotFound(refname) = err {
|
||||
assert_eq!(refname, "refs/heads/master");
|
||||
} else {
|
||||
panic!("expected RefNotFound, got {:?}", err);
|
||||
}
|
||||
}).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Don't use this one from your backend. This is an internal macro.
|
||||
($create_repo:expr, $(async fn $name:ident($repo:ident) { $($body:tt)* })*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
::tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let $repo = $create_repo({
|
||||
let mod_name = ::std::module_path!();
|
||||
let test_name = ::std::stringify!($name);
|
||||
format!("{mod_name}::{test_name}")
|
||||
}).await;
|
||||
|
||||
$($body)*
|
||||
})
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
|
355
gitbutler-git/src/integration_tests/private.rs
Normal file
355
gitbutler-git/src/integration_tests/private.rs
Normal file
@ -0,0 +1,355 @@
|
||||
use futures::FutureExt;
|
||||
use russh::{server, Channel, ChannelId, MethodSet, Pty};
|
||||
use std::{collections::HashMap, process::Stdio, sync::Arc};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TestSshServer {
|
||||
repo_path: String,
|
||||
allowed_auths: Vec<crate::Authorization>,
|
||||
}
|
||||
|
||||
impl TestSshServer {
|
||||
pub fn new(repo_path: String) -> Self {
|
||||
Self {
|
||||
repo_path,
|
||||
allowed_auths: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_with_server<F, FN>(self, cb: FN)
|
||||
where
|
||||
FN: FnOnce(u16) -> F,
|
||||
F: std::future::Future<Output = ()> + 'static,
|
||||
{
|
||||
// We manually set up a TcpListener here so that we can
|
||||
// bind to a random port and retrieve it.
|
||||
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let port = addr.port();
|
||||
|
||||
let config = Arc::new(russh::server::Config {
|
||||
inactivity_timeout: Some(std::time::Duration::from_secs(10)),
|
||||
auth_rejection_time: std::time::Duration::from_secs(3),
|
||||
auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)),
|
||||
keys: vec![russh_keys::key::KeyPair::generate_ed25519().unwrap()],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let socket_future = russh::server::run_on_socket(config, &listener, self);
|
||||
|
||||
futures::select! {
|
||||
_ = cb(port).fuse() => {},
|
||||
_ = socket_future.fuse() => {
|
||||
panic!("server exited prematurely");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn allow_authorization(&mut self, auth: crate::Authorization) {
|
||||
self.allowed_auths.push(auth);
|
||||
}
|
||||
}
|
||||
|
||||
impl server::Server for TestSshServer {
|
||||
type Handler = TestSshClient;
|
||||
fn new_client(&mut self, _: Option<std::net::SocketAddr>) -> Self::Handler {
|
||||
TestSshClient {
|
||||
repo_path: self.repo_path.clone(),
|
||||
channels: HashMap::new(),
|
||||
allowed_auths: self.allowed_auths.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TestSshClient {
|
||||
repo_path: String,
|
||||
channels: HashMap<ChannelId, TestSshChannel>,
|
||||
allowed_auths: Vec<crate::Authorization>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestSshChannel {
|
||||
envs: HashMap<String, String>,
|
||||
channel: Channel<server::Msg>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl server::Handler for TestSshClient {
|
||||
type Error = russh::Error;
|
||||
|
||||
async fn auth_password(
|
||||
self,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
) -> Result<(Self, server::Auth), Self::Error> {
|
||||
for auth in &self.allowed_auths {
|
||||
if let crate::Authorization::Basic { username, password } = auth {
|
||||
if username.as_deref() == Some(user) && password.as_deref() == Some(pass) {
|
||||
return Ok((self, server::Auth::Accept));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
self,
|
||||
server::Auth::Reject {
|
||||
proceed_with_methods: Some(MethodSet::PUBLICKEY),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
async fn env_request(
|
||||
mut self,
|
||||
channel: ChannelId,
|
||||
name: &str,
|
||||
value: &str,
|
||||
session: server::Session,
|
||||
) -> Result<(Self, server::Session), Self::Error> {
|
||||
match name {
|
||||
"GIT_PROTOCOL" | "LANG" | "LC_ALL" => {
|
||||
self.channels
|
||||
.get_mut(&channel)
|
||||
.expect("env_request on unknown channel")
|
||||
.envs
|
||||
.insert(name.to_owned(), value.to_owned());
|
||||
}
|
||||
disallowed => {
|
||||
panic!(
|
||||
"client attempted to set disallowed environment variable {:?} to {:?}",
|
||||
disallowed, value
|
||||
)
|
||||
}
|
||||
}
|
||||
Ok((self, session))
|
||||
}
|
||||
|
||||
async fn pty_request(
|
||||
self,
|
||||
_channel: ChannelId,
|
||||
_term: &str,
|
||||
_col_width: u32,
|
||||
_row_height: u32,
|
||||
_pix_width: u32,
|
||||
_pix_height: u32,
|
||||
_modes: &[(Pty, u32)],
|
||||
_session: server::Session,
|
||||
) -> Result<(Self, server::Session), Self::Error> {
|
||||
panic!("client requested a pty but we don't support that");
|
||||
}
|
||||
|
||||
async fn shell_request(
|
||||
self,
|
||||
_channel: ChannelId,
|
||||
_session: server::Session,
|
||||
) -> Result<(Self, server::Session), Self::Error> {
|
||||
panic!("client requested a shell but we don't support that");
|
||||
}
|
||||
|
||||
async fn exec_request(
|
||||
mut self,
|
||||
channel_id: ChannelId,
|
||||
command: &[u8],
|
||||
session: server::Session,
|
||||
) -> Result<(Self, server::Session), Self::Error> {
|
||||
let req = String::from_utf8_lossy(command);
|
||||
|
||||
if req.starts_with("git-upload-pack") {
|
||||
let channel = Box::leak(Box::new(self.channels.remove(&channel_id).unwrap()));
|
||||
let repo_path = self.repo_path.clone();
|
||||
let handle = session.handle();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let channel_id = channel.channel.id();
|
||||
let mut writer = channel.channel.make_writer_ext(None);
|
||||
let mut reader = channel.channel.make_reader_ext(None);
|
||||
|
||||
let mut cmd = tokio::process::Command::new("git-upload-pack")
|
||||
.kill_on_drop(true)
|
||||
.envs(channel.envs.iter())
|
||||
.arg(&repo_path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
let mut stdin = cmd.stdin.take().unwrap();
|
||||
let mut stdout = cmd.stdout.take().unwrap();
|
||||
|
||||
let copy_in = tokio::spawn(async move {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
//let file = tokio::fs::File::create("/tmp/gitbutler-upload-pack-in.log")
|
||||
// .await
|
||||
// .unwrap();
|
||||
//let mut file_writer = tokio::io::BufWriter::new(file);
|
||||
|
||||
let mut buffer = [0; 1024];
|
||||
while let Ok(n) = reader.read(&mut buffer).await {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
stdin.write_all(&buffer[..n]).await.unwrap();
|
||||
//file_writer.write_all(&buffer[..n]).await.unwrap();
|
||||
stdin.flush().await.unwrap();
|
||||
//file_writer.flush().await.unwrap();
|
||||
}
|
||||
|
||||
stdin.shutdown().await.ok(); // may have already been closed
|
||||
//file_writer.shutdown().await.unwrap();
|
||||
});
|
||||
|
||||
let copy_out = tokio::spawn(async move {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
//let file = tokio::fs::File::create("/tmp/gitbutler-upload-pack-out.log")
|
||||
// .await
|
||||
// .unwrap();
|
||||
//let mut file_writer = tokio::io::BufWriter::new(file);
|
||||
|
||||
let mut buffer = [0; 1024];
|
||||
while let Ok(n) = stdout.read(&mut buffer).await {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
writer.write_all(&buffer[..n]).await.unwrap();
|
||||
//file_writer.write_all(&buffer[..n]).await.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
//file_writer.flush().await.unwrap();
|
||||
}
|
||||
writer.shutdown().await.ok(); // may have already been closed.
|
||||
//file_writer.shutdown().await.unwrap();
|
||||
});
|
||||
|
||||
let cmd_future = tokio::spawn(async move { cmd.wait().await.unwrap() });
|
||||
|
||||
let (status, _, _) = futures::try_join!(cmd_future, copy_in, copy_out).unwrap();
|
||||
|
||||
let exit_code = status.code().unwrap_or(1) as u32;
|
||||
|
||||
handle
|
||||
.exit_status_request(channel_id, exit_code)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
handle.close(channel_id).await.unwrap();
|
||||
});
|
||||
} else {
|
||||
panic!("client requested a command we don't support: {:?}", req);
|
||||
}
|
||||
|
||||
Ok((self, session))
|
||||
}
|
||||
|
||||
async fn channel_open_session(
|
||||
mut self,
|
||||
channel: Channel<server::Msg>,
|
||||
session: server::Session,
|
||||
) -> Result<(Self, bool, server::Session), Self::Error> {
|
||||
self.channels.insert(
|
||||
channel.id(),
|
||||
TestSshChannel {
|
||||
channel,
|
||||
envs: HashMap::new(),
|
||||
},
|
||||
);
|
||||
Ok((self, true, session))
|
||||
}
|
||||
|
||||
async fn channel_close(
|
||||
mut self,
|
||||
channel: ChannelId,
|
||||
session: server::Session,
|
||||
) -> Result<(Self, server::Session), Self::Error> {
|
||||
// Best effort; may already be consumed.
|
||||
self.channels.remove(&channel);
|
||||
Ok((self, session))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! test_impl {
|
||||
($create_repo:expr, enable_io, $(async fn $name:ident($repo:ident $(, $server:ident , $server_repo:ident)?) { $($body:tt)* })*) => {
|
||||
$($crate::private::test_impl!($create_repo, $name, $repo $(, $server, $server_repo)?, { $($body)* });)*
|
||||
};
|
||||
($create_repo:expr, disable_io, $(async fn $name:ident($repo:ident $(, $server:ident , $server_repo:ident)?) { $($body:tt)* })*) => {};
|
||||
($create_repo:expr, $name:ident, $repo:ident, { $($body:tt)* }) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
::tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
#[allow(unused_variables)]
|
||||
let $repo = $create_repo({
|
||||
let mod_name = ::std::module_path!();
|
||||
let test_name = ::std::stringify!($name);
|
||||
format!("{mod_name}::{test_name}")
|
||||
}).await;
|
||||
|
||||
let test_future = async { $($body)* };
|
||||
|
||||
use futures::FutureExt;
|
||||
let timeout_future = ::tokio::time::sleep(::std::time::Duration::from_secs(10));
|
||||
|
||||
futures::select! {
|
||||
_ = test_future.fuse() => {},
|
||||
_ = timeout_future.fuse() => {
|
||||
panic!("test timed out");
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
($create_repo:expr, $name:ident, $repo:ident, $server:ident, $server_repo:ident, { $($body:tt)* }) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
::tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
#[allow(unused_variables)]
|
||||
let $repo = $create_repo({
|
||||
let mod_name = ::std::module_path!();
|
||||
let test_name = ::std::stringify!($name);
|
||||
format!("{mod_name}::{test_name}")
|
||||
}).await;
|
||||
|
||||
#[allow(unused_variables, unused_mut)]
|
||||
let (mut $server, $server_repo) = async {
|
||||
let mod_name = ::std::module_path!();
|
||||
let test_name = ::std::stringify!($name);
|
||||
let repo_path = ::std::env::temp_dir()
|
||||
.join("gitbutler-tests")
|
||||
.join("git")
|
||||
.join("remote")
|
||||
.join(test_name)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let repo = $crate::backend::git2::Repository::<$crate::backend::git2::tokio::TokioThreadedResource>::open_or_init_bare(repo_path.clone());
|
||||
let server = $crate::private::TestSshServer::new(repo_path);
|
||||
(server, repo)
|
||||
}.await;
|
||||
|
||||
let test_future = async { $($body)* };
|
||||
|
||||
use futures::FutureExt;
|
||||
let timeout_future = ::tokio::time::sleep(::std::time::Duration::from_secs(10));
|
||||
|
||||
futures::select! {
|
||||
_ = test_future.fuse() => {},
|
||||
_ = timeout_future.fuse() => {
|
||||
panic!("test timed out");
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use test_impl;
|
@ -23,13 +23,10 @@
|
||||
//!
|
||||
//! This hampers certain use cases, such as implementing
|
||||
//! [`cli::GitExecutor`] for e.g. remote connections.
|
||||
#![cfg_attr(not(feature = "std"), no_std)] // must be first
|
||||
#![feature(error_in_core)]
|
||||
#![deny(missing_docs, unsafe_code)]
|
||||
#![allow(async_fn_in_trait)]
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
extern crate alloc;
|
||||
#![cfg_attr(test, feature(async_closure))]
|
||||
#![feature(impl_trait_in_assoc_type)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
@ -42,8 +39,6 @@ pub mod ops;
|
||||
mod refspec;
|
||||
mod repository;
|
||||
|
||||
pub(crate) mod prelude;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub use backend::cli;
|
||||
#[cfg(feature = "git2")]
|
||||
@ -51,5 +46,5 @@ pub use backend::git2;
|
||||
|
||||
pub use self::{
|
||||
refspec::{Error as RefSpecError, RefSpec},
|
||||
repository::{Authorization, ConfigScope, Repository},
|
||||
repository::{Authorization, ConfigScope, Error, Repository},
|
||||
};
|
||||
|
@ -5,14 +5,13 @@
|
||||
//! into more complex operations, without caring about the
|
||||
//! underlying implementation.
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::{ConfigScope, Repository};
|
||||
|
||||
/// Returns whether or not the repository has GitButler's
|
||||
/// utmost discretion enabled.
|
||||
pub async fn has_utmost_discretion<R: Repository>(repo: &R) -> Result<bool, R::Error> {
|
||||
pub async fn has_utmost_discretion<R: Repository>(
|
||||
repo: &R,
|
||||
) -> Result<bool, crate::Error<R::Error>> {
|
||||
let config = repo
|
||||
.config_get("gitbutler.utmostDiscretion", ConfigScope::default())
|
||||
.await?;
|
||||
@ -21,7 +20,10 @@ pub async fn has_utmost_discretion<R: Repository>(repo: &R) -> Result<bool, R::E
|
||||
}
|
||||
|
||||
/// Sets whether or not the repository has GitButler's utmost discretion.
|
||||
pub async fn set_utmost_discretion<R: Repository>(repo: &R, value: bool) -> Result<(), R::Error> {
|
||||
pub async fn set_utmost_discretion<R: Repository>(
|
||||
repo: &R,
|
||||
value: bool,
|
||||
) -> Result<(), crate::Error<R::Error>> {
|
||||
repo.config_set(
|
||||
"gitbutler.utmostDiscretion",
|
||||
if value { "1" } else { "0" },
|
||||
|
@ -1,11 +0,0 @@
|
||||
#[cfg(not(feature = "std"))]
|
||||
#[allow(unused_imports)]
|
||||
pub use alloc::{
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[allow(unused_imports)]
|
||||
pub use std::collections::BTreeMap;
|
@ -1,8 +1,7 @@
|
||||
use core::fmt;
|
||||
|
||||
/// An error that can occur while parsing a refspec from a string.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Encountered an unexpected character when parsing a [`RefSpec`] from a string.
|
||||
#[error("unexpected character {0:?} (offset {1})")]
|
||||
@ -49,10 +48,10 @@ impl RefSpec {
|
||||
|
||||
let mut offset = 0;
|
||||
|
||||
let s = if s.starts_with('+') {
|
||||
let s = if let Some(stripped) = s.strip_prefix('+') {
|
||||
refspec.update_non_fastforward = true;
|
||||
offset += 1;
|
||||
&s[1..]
|
||||
stripped
|
||||
} else {
|
||||
s
|
||||
};
|
||||
|
@ -1,5 +1,35 @@
|
||||
#[allow(unused_imports)]
|
||||
use crate::prelude::*;
|
||||
use crate::RefSpec;
|
||||
|
||||
/// A backend-agnostic operation error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error<BE: std::error::Error + core::fmt::Debug + Send + Sync + 'static> {
|
||||
/// An otherwise backend-specific error that occurred and was not
|
||||
/// directly related to the inputs or repository state related to
|
||||
/// the operation, and instead occurred as a result of the backend
|
||||
/// executing the operation itself.
|
||||
#[error("backend error: {0}")]
|
||||
Backend(#[from] BE),
|
||||
/// The given refspec was not found.
|
||||
/// Usually returned by a push or fetch operation.
|
||||
#[error("a ref-spec was not found: {0}")]
|
||||
RefNotFound(String),
|
||||
/// An authorized operation was attempted, but the authorization
|
||||
/// credentials were rejected by the remote (or further credentials
|
||||
/// were required).
|
||||
///
|
||||
/// The inner error is the backend-specific error that may provide
|
||||
/// more context.
|
||||
#[error("authorization failed: {0}")]
|
||||
AuthorizationFailed(BE),
|
||||
/// An operation interacting with a remote by name failed to find
|
||||
/// the remote.
|
||||
#[error("no such remote: {0}")]
|
||||
NoSuchRemote(String, #[source] BE),
|
||||
/// An operation that expected a remote not to exist found that
|
||||
/// the remote already existed.
|
||||
#[error("remote already exists: {0}")]
|
||||
RemoteExists(String, #[source] BE),
|
||||
}
|
||||
|
||||
/// The scope from/to which a configuration value is read/written.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@ -38,7 +68,7 @@ pub enum ConfigScope {
|
||||
/// A handle to an open Git repository.
|
||||
pub trait Repository {
|
||||
/// The type of error returned by this repository.
|
||||
type Error: core::error::Error + core::fmt::Debug + Send + Sync + 'static;
|
||||
type Error: std::error::Error + core::fmt::Debug + Send + Sync + 'static;
|
||||
|
||||
/// Reads a configuration value.
|
||||
///
|
||||
@ -47,7 +77,7 @@ pub trait Repository {
|
||||
&self,
|
||||
key: &str,
|
||||
scope: ConfigScope,
|
||||
) -> Result<Option<String>, Self::Error>;
|
||||
) -> Result<Option<String>, Error<Self::Error>>;
|
||||
|
||||
/// Writes a configuration value.
|
||||
///
|
||||
@ -57,7 +87,34 @@ pub trait Repository {
|
||||
key: &str,
|
||||
value: &str,
|
||||
scope: ConfigScope,
|
||||
) -> Result<(), Self::Error>;
|
||||
) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Fetchs the given refspec from the given remote.
|
||||
///
|
||||
/// This is an authorized operation; the given authorization
|
||||
/// credentials will be used to authenticate with the remote.
|
||||
async fn fetch(
|
||||
&self,
|
||||
remote: &str,
|
||||
refspec: RefSpec,
|
||||
authorization: &Authorization,
|
||||
) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Sets the URI for a remote.
|
||||
/// If the remote does not exist, it will be created.
|
||||
/// If the remote already exists, [`Error::RemoteExists`] will be returned.
|
||||
async fn create_remote(&self, remote: &str, uri: &str) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Creates a remote with the given URI, or updates the URI
|
||||
/// if the remote already exists.
|
||||
async fn create_or_update_remote(
|
||||
&self,
|
||||
remote: &str,
|
||||
uri: &str,
|
||||
) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Gets the URI for a remote.
|
||||
async fn remote(&self, remote: &str) -> Result<String, Error<Self::Error>>;
|
||||
}
|
||||
|
||||
/// Provides authentication credentials when performing
|
||||
@ -68,17 +125,25 @@ pub enum Authorization {
|
||||
/// default authorization mechanism, if any.
|
||||
#[default]
|
||||
Auto,
|
||||
/// Performs HTTP(S) Basic authentication with a username
|
||||
/// and password.
|
||||
/// Performs HTTP(S) Basic authentication with a username and password.
|
||||
///
|
||||
/// Note that certain remotes may use this mechanism
|
||||
/// for passing tokens as well; consult the respective
|
||||
/// remote's documentation for what information to supply.
|
||||
/// In the case of an SSH remote, the username is ignored. The username is
|
||||
/// only used for HTTP(S) remotes, and in such cases, if username is `None`
|
||||
/// and the remote requests for it, the operation will fail.
|
||||
///
|
||||
/// In order for HTTP(S) remotes to work with a `None` username or password,
|
||||
/// the remote URI must include the basic auth credentials in the URI itself
|
||||
/// (e.g. `https://[user]:[pass]@host/path`). Otherwise, the operation will
|
||||
/// fail.
|
||||
///
|
||||
/// Note that certain remotes may use this mechanism for passing tokens as
|
||||
/// well; consult the respective remote's documentation for what information
|
||||
/// to supply.
|
||||
Basic {
|
||||
/// The username to use for authentication.
|
||||
username: String,
|
||||
username: Option<String>,
|
||||
/// The password to use for authentication.
|
||||
password: String,
|
||||
password: Option<String>,
|
||||
},
|
||||
/// Specifies a set of credentials for logging in with SSH.
|
||||
Ssh {
|
||||
|
Loading…
Reference in New Issue
Block a user