mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-08-17 08:10:52 +03:00
Merge pull request #4165 from Byron/secrets-in-keyring
migrate plain-text secrets to keystore
This commit is contained in:
commit
7e6e854d5e
419
Cargo.lock
generated
419
Cargo.lock
generated
@ -546,6 +546,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.5.1"
|
||||
@ -733,6 +742,15 @@ dependencies = [
|
||||
"toml 0.7.8",
|
||||
]
|
||||
|
||||
[[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.98"
|
||||
@ -2120,6 +2138,7 @@ dependencies = [
|
||||
"glob",
|
||||
"hex",
|
||||
"itertools 0.13.0",
|
||||
"keyring",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"md5",
|
||||
@ -2131,6 +2150,7 @@ dependencies = [
|
||||
"resolve-path",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"sha2",
|
||||
"ssh-key",
|
||||
"ssh2",
|
||||
@ -2152,7 +2172,7 @@ name = "gitbutler-git"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nix 0.29.0",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
@ -2226,8 +2246,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"git2",
|
||||
"gitbutler-core",
|
||||
"keyring",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
@ -2251,14 +2273,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix"
|
||||
version = "0.63.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "984c5018adfa7a4536ade67990b3ebc6e11ab57b3d6cd9968de0947ca99b4b06"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"gix-actor",
|
||||
"gix-attributes",
|
||||
"gix-command",
|
||||
"gix-commitgraph",
|
||||
"gix-config",
|
||||
"gix-credentials",
|
||||
"gix-date",
|
||||
"gix-diff",
|
||||
"gix-dir",
|
||||
@ -2273,11 +2295,13 @@ dependencies = [
|
||||
"gix-index",
|
||||
"gix-lock",
|
||||
"gix-macros",
|
||||
"gix-negotiate",
|
||||
"gix-object",
|
||||
"gix-odb",
|
||||
"gix-pack",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-pathspec",
|
||||
"gix-prompt",
|
||||
"gix-ref",
|
||||
"gix-refspec",
|
||||
"gix-revision",
|
||||
@ -2285,14 +2309,13 @@ dependencies = [
|
||||
"gix-sec",
|
||||
"gix-submodule",
|
||||
"gix-tempfile",
|
||||
"gix-trace",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-traverse",
|
||||
"gix-url",
|
||||
"gix-utils",
|
||||
"gix-validate",
|
||||
"gix-worktree",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
]
|
||||
@ -2300,8 +2323,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-actor"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d69c59d392c7e6c94385b6fd6089d6df0fe945f32b4357687989f3aee253cd7f"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-date",
|
||||
@ -2314,14 +2336,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-attributes"
|
||||
version = "0.22.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eefb48f42eac136a4a0023f49a54ec31be1c7a9589ed762c45dcb9b953f7ecc8"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-glob",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-quote",
|
||||
"gix-trace",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"kstring",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
@ -2331,8 +2352,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-bitmap"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a371db66cbd4e13f0ed9dc4c0fea712d7276805fccc877f77e96374d317e87ae"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
@ -2340,8 +2360,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-chunk"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45c8751169961ba7640b513c3b24af61aa962c967aaf04116734975cd5af0c52"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
@ -2349,20 +2368,18 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-command"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c22e086314095c43ffe5cdc5c0922d5439da4fd726f3b0438c56147c34dc225"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-path",
|
||||
"gix-trace",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"shell-words",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-commitgraph"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7b102311085da4af18823413b5176d7c500fb2272eaf391cfa8635d8bcb12c4"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-chunk",
|
||||
@ -2375,14 +2392,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-config"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53fafe42957e11d98e354a66b6bd70aeea00faf2f62dd11164188224a507c840"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-config-value",
|
||||
"gix-features",
|
||||
"gix-glob",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-ref",
|
||||
"gix-sec",
|
||||
"memchr",
|
||||
@ -2396,21 +2412,35 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-config-value"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbd06203b1a9b33a78c88252a625031b094d9e1b647260070c25b09910c0a804"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bstr",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"libc",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-credentials"
|
||||
version = "0.24.2"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-command",
|
||||
"gix-config-value",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-prompt",
|
||||
"gix-sec",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-url",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-date"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "367ee9093b0c2b04fd04c5c7c8b6a1082713534eab537597ae343663a518fa99"
|
||||
version = "0.8.7"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"itoa 1.0.11",
|
||||
@ -2421,8 +2451,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-diff"
|
||||
version = "0.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b9bd8b2d07b6675a840b56a6c177d322d45fa082672b0dad8f063b25baf0a4"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-hash",
|
||||
@ -2433,8 +2462,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-dir"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60c99f8c545abd63abe541d20ab6cda347de406c0a3f1c80aadc12d9b0e94974"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-discover",
|
||||
@ -2442,9 +2470,9 @@ dependencies = [
|
||||
"gix-ignore",
|
||||
"gix-index",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-pathspec",
|
||||
"gix-trace",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-utils",
|
||||
"gix-worktree",
|
||||
"thiserror",
|
||||
@ -2453,14 +2481,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-discover"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc27c699b63da66b50d50c00668bc0b7e90c3a382ef302865e891559935f3dbf"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"dunce",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-ref",
|
||||
"gix-sec",
|
||||
"thiserror",
|
||||
@ -2469,16 +2496,17 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-features"
|
||||
version = "0.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"crossbeam-channel",
|
||||
"flate2",
|
||||
"gix-hash",
|
||||
"gix-trace",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-utils",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"prodash",
|
||||
"sha1_smol",
|
||||
"thiserror",
|
||||
@ -2488,8 +2516,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-filter"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00ce6ea5ac8fca7adbc63c48a1b9e0492c222c386aa15f513405f1003f2f4ab2"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"encoding_rs",
|
||||
@ -2498,9 +2525,9 @@ dependencies = [
|
||||
"gix-hash",
|
||||
"gix-object",
|
||||
"gix-packetline-blocking",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-quote",
|
||||
"gix-trace",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-utils",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
@ -2509,8 +2536,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-fs"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3338ff92a2164f5209f185ec0cd316f571a72676bb01d27e22f2867ba69f77a"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"fastrand 2.1.0",
|
||||
"gix-features",
|
||||
@ -2520,20 +2546,18 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-glob"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a29ad0990cf02c48a7aac76ed0dbddeb5a0d070034b83675cc3bbf937eace4"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bstr",
|
||||
"gix-features",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-hash"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"faster-hex",
|
||||
"thiserror",
|
||||
@ -2542,8 +2566,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-hashtable"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"gix-hash",
|
||||
"hashbrown 0.14.3",
|
||||
@ -2553,21 +2576,19 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-ignore"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "640dbeb4f5829f9fc14d31f654a34a0350e43a24e32d551ad130d99bf01f63f1"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-glob",
|
||||
"gix-path",
|
||||
"gix-trace",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"unicode-bom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-index"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d8c5a5f1c58edcbc5692b174cda2703aba82ed17d7176ff4c1752eb48b1b167"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bstr",
|
||||
@ -2594,8 +2615,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-lock"
|
||||
version = "14.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"gix-tempfile",
|
||||
"gix-utils",
|
||||
@ -2605,19 +2625,32 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-macros"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "999ce923619f88194171a67fb3e6d613653b8d4d6078b529b15a765da0edcc17"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-negotiate"
|
||||
version = "0.13.1"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"gix-commitgraph",
|
||||
"gix-date",
|
||||
"gix-hash",
|
||||
"gix-object",
|
||||
"gix-revwalk",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-object"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fe2dc4a41191c680c942e6ebd630c8107005983c4679214fdb1007dcf5ae1df"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-actor",
|
||||
@ -2635,8 +2668,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-odb"
|
||||
version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e92b9790e2c919166865d0825b26cc440a387c175bed1b43a2fa99c0e9d45e98"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"gix-date",
|
||||
@ -2645,7 +2677,7 @@ dependencies = [
|
||||
"gix-hash",
|
||||
"gix-object",
|
||||
"gix-pack",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-quote",
|
||||
"parking_lot 0.12.3",
|
||||
"tempfile",
|
||||
@ -2655,8 +2687,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-pack"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a8da51212dbff944713edb2141ed7e002eea326b8992070374ce13a6cb610b3"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"clru",
|
||||
"gix-chunk",
|
||||
@ -2664,10 +2695,8 @@ dependencies = [
|
||||
"gix-hash",
|
||||
"gix-hashtable",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-tempfile",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"memmap2",
|
||||
"parking_lot 0.12.3",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
]
|
||||
@ -2675,12 +2704,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-packetline-blocking"
|
||||
version = "0.17.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31d42378a3d284732e4d589979930d0d253360eccf7ec7a80332e5ccb77e14a"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"faster-hex",
|
||||
"gix-trace",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@ -2691,7 +2719,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca987128ffb056d732bd545db5db3d8b103d252fbf083c2567bb0796876619a4"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-trace",
|
||||
"gix-trace 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"home",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-path"
|
||||
version = "0.10.8"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-trace 0.1.9 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"home",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
@ -2700,23 +2740,33 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-pathspec"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a76cab098dc10ba2d89f634f66bf196dea4d7db4bf10b75c7a9c201c55a2ee19"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bstr",
|
||||
"gix-attributes",
|
||||
"gix-config-value",
|
||||
"gix-glob",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-prompt"
|
||||
version = "0.8.5"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"gix-command",
|
||||
"gix-config-value",
|
||||
"parking_lot 0.12.3",
|
||||
"rustix 0.38.32",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-quote"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbff4f9b9ea3fa7a25a70ee62f545143abef624ac6aa5884344e70c8b0a1d9ff"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-utils",
|
||||
@ -2726,17 +2776,15 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-ref"
|
||||
version = "0.44.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3394a2997e5bc6b22ebc1e1a87b41eeefbcfcff3dbfa7c4bd73cb0ac8f1f3e2e"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"gix-actor",
|
||||
"gix-date",
|
||||
"gix-features",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-lock",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-tempfile",
|
||||
"gix-utils",
|
||||
"gix-validate",
|
||||
@ -2748,8 +2796,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-refspec"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dde848865834a54fe4d9b4573f15d0e9a68eaf3d061b42d3ed52b4b8acf880b2"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-hash",
|
||||
@ -2762,24 +2809,20 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-revision"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e08f8107ed1f93a83bcfbb4c38084c7cb3f6cd849793f1d5eec235f9b13b2b"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-date",
|
||||
"gix-hash",
|
||||
"gix-hashtable",
|
||||
"gix-object",
|
||||
"gix-revwalk",
|
||||
"gix-trace",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-revwalk"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4181db9cfcd6d1d0fd258e91569dbb61f94cb788b441b5294dd7f1167a3e788f"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"gix-commitgraph",
|
||||
"gix-date",
|
||||
@ -2793,11 +2836,10 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-sec"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fddc27984a643b20dd03e97790555804f98cf07404e0e552c0ad8133266a79a1"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@ -2805,12 +2847,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-submodule"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "921cd49924ac14b6611b22e5fb7bbba74d8780dc7ad26153304b64d1272460ac"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-config",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-pathspec",
|
||||
"gix-refspec",
|
||||
"gix-url",
|
||||
@ -2820,8 +2861,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-tempfile"
|
||||
version = "14.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3b0e276cd08eb2a22e9f286a4f13a222a01be2defafa8621367515375644b99"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"gix-fs",
|
||||
"libc",
|
||||
@ -2836,11 +2876,15 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e"
|
||||
|
||||
[[package]]
|
||||
name = "gix-trace"
|
||||
version = "0.1.9"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
|
||||
[[package]]
|
||||
name = "gix-traverse"
|
||||
version = "0.39.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f20cb69b63eb3e4827939f42c05b7756e3488ef49c25c412a876691d568ee2a0"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"gix-commitgraph",
|
||||
@ -2856,12 +2900,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-url"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0db829ebdca6180fbe32be7aed393591df6db4a72dbbc0b8369162390954d1cf"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-features",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"home",
|
||||
"thiserror",
|
||||
"url",
|
||||
@ -2870,8 +2913,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-utils"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"fastrand 2.1.0",
|
||||
@ -2881,8 +2923,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-validate"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82c27dd34a49b1addf193c92070bcbf3beaf6e10f16a78544de6372e146a0acf"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"thiserror",
|
||||
@ -2891,8 +2932,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gix-worktree"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53f6b7de83839274022aff92157d7505f23debf739d257984a300a35972ca94e"
|
||||
source = "git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e#55cbc1b9d6f210298a86502a7f20f9736c7e963e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-attributes",
|
||||
@ -2903,7 +2943,7 @@ dependencies = [
|
||||
"gix-ignore",
|
||||
"gix-index",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-path 0.10.8 (git+https://github.com/Byron/gitoxide?rev=55cbc1b9d6f210298a86502a7f20f9736c7e963e)",
|
||||
"gix-validate",
|
||||
]
|
||||
|
||||
@ -3161,6 +3201,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@ -3518,6 +3567,7 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
@ -3694,6 +3744,20 @@ dependencies = [
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "363387f0019d714aa60cc30ab4fe501a747f4c08fc58f069dd14be971bd495a0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"linux-keyutils",
|
||||
"secret-service",
|
||||
"security-framework",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.0.8"
|
||||
@ -3820,6 +3884,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e"
|
||||
|
||||
[[package]]
|
||||
name = "linux-keyutils"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.3.8"
|
||||
@ -4132,6 +4206,30 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
@ -4149,6 +4247,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@ -4175,6 +4282,17 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.18"
|
||||
@ -5512,6 +5630,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc"
|
||||
dependencies = [
|
||||
"sdd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.23"
|
||||
@ -5533,6 +5660,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sdd"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d"
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
@ -5553,6 +5686,25 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secret-service"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5204d39df37f06d1944935232fd2dfe05008def7ca599bf28c0800366c8a8f9"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cbc",
|
||||
"futures-util",
|
||||
"generic-array",
|
||||
"hkdf",
|
||||
"num",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"sha2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.10.0"
|
||||
@ -5627,9 +5779,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.117"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"itoa 1.0.11",
|
||||
@ -5699,6 +5851,31 @@ dependencies = [
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"scc",
|
||||
"serial_test_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test_derive"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript"
|
||||
version = "0.1.1"
|
||||
@ -6987,9 +7164,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.0"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
|
@ -11,12 +11,14 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
gix = { version = "0.63.0", default-features = false, features = [] } # add performance features here as needed
|
||||
# Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes.
|
||||
gix = { git = "https://github.com/Byron/gitoxide", rev = "55cbc1b9d6f210298a86502a7f20f9736c7e963e", default-features = false, features = [] }
|
||||
git2 = { version = "0.18.3", features = ["vendored-openssl", "vendored-libgit2"] }
|
||||
uuid = { version = "1.8.0", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0.61"
|
||||
tokio = { version = "1.38.0", default-features = false }
|
||||
keyring = "2.3.3"
|
||||
|
||||
gitbutler-git = { path = "crates/gitbutler-git" }
|
||||
gitbutler-core = { path = "crates/gitbutler-core" }
|
||||
|
@ -2,7 +2,7 @@ import { AnthropicAIClient } from '$lib/ai/anthropicClient';
|
||||
import { ButlerAIClient } from '$lib/ai/butlerClient';
|
||||
import { OpenAIClient } from '$lib/ai/openAIClient';
|
||||
import { SHORT_DEFAULT_BRANCH_TEMPLATE, SHORT_DEFAULT_COMMIT_TEMPLATE } from '$lib/ai/prompts';
|
||||
import { AIService, GitAIConfigKey, KeyOption, buildDiff } from '$lib/ai/service';
|
||||
import { AISecretHandle, AIService, GitAIConfigKey, KeyOption, buildDiff } from '$lib/ai/service';
|
||||
import {
|
||||
AnthropicModelName,
|
||||
ModelKind,
|
||||
@ -16,17 +16,21 @@ import { Hunk } from '$lib/vbranches/types';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { expect, test, describe, vi } from 'vitest';
|
||||
import type { GbConfig, GitConfigService } from '$lib/backend/gitConfigService';
|
||||
import type { SecretsService } from '$lib/secrets/secretsService';
|
||||
|
||||
const defaultGitConfig = Object.freeze({
|
||||
[GitAIConfigKey.ModelProvider]: ModelKind.OpenAI,
|
||||
[GitAIConfigKey.OpenAIKeyOption]: KeyOption.ButlerAPI,
|
||||
[GitAIConfigKey.OpenAIKey]: undefined,
|
||||
[GitAIConfigKey.OpenAIModelName]: OpenAIModelName.GPT35Turbo,
|
||||
[GitAIConfigKey.AnthropicKeyOption]: KeyOption.ButlerAPI,
|
||||
[GitAIConfigKey.AnthropicKey]: undefined,
|
||||
[GitAIConfigKey.AnthropicModelName]: AnthropicModelName.Haiku
|
||||
});
|
||||
|
||||
const defaultSecretsConfig = Object.freeze({
|
||||
[AISecretHandle.AnthropicKey]: undefined,
|
||||
[AISecretHandle.OpenAIKey]: undefined
|
||||
});
|
||||
|
||||
class DummyGitConfigService implements GitConfigService {
|
||||
constructor(private config: { [index: string]: string | undefined }) {}
|
||||
async getGbConfig(_projectId: string): Promise<GbConfig> {
|
||||
@ -46,6 +50,24 @@ class DummyGitConfigService implements GitConfigService {
|
||||
async set<T extends string>(key: string, value: T): Promise<T | undefined> {
|
||||
return (this.config[key] = value);
|
||||
}
|
||||
async remove(key: string): Promise<undefined> {
|
||||
delete this.config[key];
|
||||
}
|
||||
}
|
||||
|
||||
class DummySecretsService implements SecretsService {
|
||||
private config: { [index: string]: string | undefined };
|
||||
constructor(config?: { [index: string]: string | undefined }) {
|
||||
this.config = config || {};
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
async set(handle: string, secret: string): Promise<void> {
|
||||
this.config[handle] = secret;
|
||||
}
|
||||
}
|
||||
|
||||
const fetchMock = vi.fn();
|
||||
@ -108,7 +130,8 @@ const exampleHunks = [hunk1, hunk2];
|
||||
|
||||
function buildDefaultAIService() {
|
||||
const gitConfig = new DummyGitConfigService(structuredClone(defaultGitConfig));
|
||||
return new AIService(gitConfig, cloud);
|
||||
const secretsService = new DummySecretsService(structuredClone(defaultSecretsConfig));
|
||||
return new AIService(gitConfig, secretsService, cloud);
|
||||
}
|
||||
|
||||
describe.concurrent('AIService', () => {
|
||||
@ -130,10 +153,10 @@ describe.concurrent('AIService', () => {
|
||||
test('When token is bring your own, When a openAI token is present. It returns OpenAIClient', async () => {
|
||||
const gitConfig = new DummyGitConfigService({
|
||||
...defaultGitConfig,
|
||||
[GitAIConfigKey.OpenAIKeyOption]: KeyOption.BringYourOwn,
|
||||
[GitAIConfigKey.OpenAIKey]: 'sk-asdfasdf'
|
||||
[GitAIConfigKey.OpenAIKeyOption]: KeyOption.BringYourOwn
|
||||
});
|
||||
const aiService = new AIService(gitConfig, cloud);
|
||||
const secretsService = new DummySecretsService({ [AISecretHandle.OpenAIKey]: 'sk-asdfasdf' });
|
||||
const aiService = new AIService(gitConfig, secretsService, cloud);
|
||||
|
||||
expect(unwrap(await aiService.buildClient())).toBeInstanceOf(OpenAIClient);
|
||||
});
|
||||
@ -141,10 +164,10 @@ describe.concurrent('AIService', () => {
|
||||
test('When token is bring your own, When a openAI token is blank. It returns undefined', async () => {
|
||||
const gitConfig = new DummyGitConfigService({
|
||||
...defaultGitConfig,
|
||||
[GitAIConfigKey.OpenAIKeyOption]: KeyOption.BringYourOwn,
|
||||
[GitAIConfigKey.OpenAIKey]: undefined
|
||||
[GitAIConfigKey.OpenAIKeyOption]: KeyOption.BringYourOwn
|
||||
});
|
||||
const aiService = new AIService(gitConfig, cloud);
|
||||
const secretsService = new DummySecretsService();
|
||||
const aiService = new AIService(gitConfig, secretsService, cloud);
|
||||
|
||||
expect(await aiService.buildClient()).toStrictEqual(
|
||||
buildFailureFromAny(
|
||||
@ -157,10 +180,12 @@ describe.concurrent('AIService', () => {
|
||||
const gitConfig = new DummyGitConfigService({
|
||||
...defaultGitConfig,
|
||||
[GitAIConfigKey.ModelProvider]: ModelKind.Anthropic,
|
||||
[GitAIConfigKey.AnthropicKeyOption]: KeyOption.BringYourOwn,
|
||||
[GitAIConfigKey.AnthropicKey]: 'sk-ant-api03-asdfasdf'
|
||||
[GitAIConfigKey.AnthropicKeyOption]: KeyOption.BringYourOwn
|
||||
});
|
||||
const aiService = new AIService(gitConfig, cloud);
|
||||
const secretsService = new DummySecretsService({
|
||||
[AISecretHandle.AnthropicKey]: 'test-key'
|
||||
});
|
||||
const aiService = new AIService(gitConfig, secretsService, cloud);
|
||||
|
||||
expect(unwrap(await aiService.buildClient())).toBeInstanceOf(AnthropicAIClient);
|
||||
});
|
||||
@ -169,10 +194,10 @@ describe.concurrent('AIService', () => {
|
||||
const gitConfig = new DummyGitConfigService({
|
||||
...defaultGitConfig,
|
||||
[GitAIConfigKey.ModelProvider]: ModelKind.Anthropic,
|
||||
[GitAIConfigKey.AnthropicKeyOption]: KeyOption.BringYourOwn,
|
||||
[GitAIConfigKey.AnthropicKey]: undefined
|
||||
[GitAIConfigKey.AnthropicKeyOption]: KeyOption.BringYourOwn
|
||||
});
|
||||
const aiService = new AIService(gitConfig, cloud);
|
||||
const secretsService = new DummySecretsService();
|
||||
const aiService = new AIService(gitConfig, secretsService, cloud);
|
||||
|
||||
expect(await aiService.buildClient()).toStrictEqual(
|
||||
buildFailureFromAny(
|
||||
|
@ -19,6 +19,7 @@ import { splitMessage } from '$lib/utils/commitMessage';
|
||||
import OpenAI from 'openai';
|
||||
import type { GitConfigService } from '$lib/backend/gitConfigService';
|
||||
import type { HttpClient } from '$lib/backend/httpClient';
|
||||
import type { SecretsService } from '$lib/secrets/secretsService';
|
||||
import type { Hunk } from '$lib/vbranches/types';
|
||||
|
||||
const maxDiffLengthLimitForAPI = 5000;
|
||||
@ -28,14 +29,17 @@ export enum KeyOption {
|
||||
ButlerAPI = 'butlerAPI'
|
||||
}
|
||||
|
||||
export enum AISecretHandle {
|
||||
OpenAIKey = 'aiOpenAIKey',
|
||||
AnthropicKey = 'aiAnthropicKey'
|
||||
}
|
||||
|
||||
export enum GitAIConfigKey {
|
||||
ModelProvider = 'gitbutler.aiModelProvider',
|
||||
OpenAIKeyOption = 'gitbutler.aiOpenAIKeyOption',
|
||||
OpenAIModelName = 'gitbutler.aiOpenAIModelName',
|
||||
OpenAIKey = 'gitbutler.aiOpenAIKey',
|
||||
AnthropicKeyOption = 'gitbutler.aiAnthropicKeyOption',
|
||||
AnthropicModelName = 'gitbutler.aiAnthropicModelName',
|
||||
AnthropicKey = 'gitbutler.aiAnthropicKey',
|
||||
DiffLengthLimit = 'gitbutler.diffLengthLimit',
|
||||
OllamaEndpoint = 'gitbutler.aiOllamaEndpoint',
|
||||
OllamaModelName = 'gitbutler.aiOllamaModelName'
|
||||
@ -72,6 +76,7 @@ function shuffle<T>(items: T[]): T[] {
|
||||
export class AIService {
|
||||
constructor(
|
||||
private gitConfig: GitConfigService,
|
||||
private secretsService: SecretsService,
|
||||
private cloud: HttpClient
|
||||
) {}
|
||||
|
||||
@ -90,7 +95,7 @@ export class AIService {
|
||||
}
|
||||
|
||||
async getOpenAIKey() {
|
||||
return await this.gitConfig.get(GitAIConfigKey.OpenAIKey);
|
||||
return await this.secretsService.get(AISecretHandle.OpenAIKey);
|
||||
}
|
||||
|
||||
async getOpenAIModleName() {
|
||||
@ -108,7 +113,7 @@ export class AIService {
|
||||
}
|
||||
|
||||
async getAnthropicKey() {
|
||||
return await this.gitConfig.get(GitAIConfigKey.AnthropicKey);
|
||||
return await this.secretsService.get(AISecretHandle.AnthropicKey);
|
||||
}
|
||||
|
||||
async getAnthropicModelName() {
|
||||
|
@ -5,6 +5,10 @@ export class GitConfigService {
|
||||
return (await invoke<T | undefined>('git_get_global_config', { key })) || undefined;
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<undefined> {
|
||||
return await invoke('git_remove_global_config', { key });
|
||||
}
|
||||
|
||||
async getWithDefault<T extends string>(key: string, defaultValue: T): Promise<T> {
|
||||
const value = await invoke<T | undefined>('git_get_global_config', { key });
|
||||
return value || defaultValue;
|
||||
|
55
app/src/lib/secrets/secretsService.ts
Normal file
55
app/src/lib/secrets/secretsService.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { AISecretHandle } from '$lib/ai/service';
|
||||
import { invoke } from '$lib/backend/ipc';
|
||||
import { buildContext } from '$lib/utils/context';
|
||||
import type { GitConfigService } from '$lib/backend/gitConfigService';
|
||||
|
||||
const MIGRATION_HANDLES = [
|
||||
AISecretHandle.AnthropicKey.toString(),
|
||||
AISecretHandle.OpenAIKey.toString()
|
||||
];
|
||||
|
||||
export type SecretsService = {
|
||||
get(handle: string): Promise<string | undefined>;
|
||||
set(handle: string, secret: string): Promise<void>;
|
||||
};
|
||||
|
||||
export const [getSecretsService, setSecretsService] =
|
||||
buildContext<SecretsService>('secretsService');
|
||||
|
||||
export class RustSecretService implements SecretsService {
|
||||
constructor(private gitConfigService: GitConfigService) {}
|
||||
|
||||
async get(handle: string) {
|
||||
const secret = await invoke<string>('secret_get_global', { handle });
|
||||
if (secret) return secret;
|
||||
|
||||
if (MIGRATION_HANDLES.includes(handle)) {
|
||||
const key = 'gitbutler.' + handle;
|
||||
const migratedSecret = await this.migrate(key, handle);
|
||||
if (migratedSecret !== undefined) return migratedSecret;
|
||||
}
|
||||
}
|
||||
|
||||
async set(handle: string, secret: string) {
|
||||
await invoke('secret_set_global', {
|
||||
handle,
|
||||
secret
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a specific key from git config to secrets.
|
||||
*
|
||||
* TODO: Remove this code and the dependency on GitConfigService in the future.
|
||||
*/
|
||||
private async migrate(key: string, handle: string): Promise<string | undefined> {
|
||||
const secretInConfig = await this.gitConfigService.get(key);
|
||||
if (secretInConfig === undefined) return;
|
||||
|
||||
await this.set(handle, secretInConfig);
|
||||
await this.gitConfigService.remove(key);
|
||||
|
||||
console.warn(`Migrated Git config "${key}" to secret store.`);
|
||||
return secretInConfig;
|
||||
}
|
||||
}
|
@ -100,3 +100,15 @@ export function getContextStoreBySymbol<T, S extends Readable<T> = Readable<T>>(
|
||||
if (!instance) throw new Error(`no instance of \`Readable<${key.toString()}[]>\` in context`);
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function buildContext<T>(name: string): [() => T, (value: T | undefined) => void] {
|
||||
const identifier = Symbol(name);
|
||||
return [
|
||||
() => {
|
||||
return svelteGetContext<T>(identifier);
|
||||
},
|
||||
(value: T | undefined) => {
|
||||
setContext(identifier, value);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import ToastController from '$lib/notifications/ToastController.svelte';
|
||||
import { RemotesService } from '$lib/remotes/service';
|
||||
import { setSecretsService } from '$lib/secrets/secretsService';
|
||||
import { SETTINGS, loadUserSettings } from '$lib/settings/userSettings';
|
||||
import { User, UserService } from '$lib/stores/user';
|
||||
import * as events from '$lib/utils/events';
|
||||
@ -36,6 +37,7 @@
|
||||
setContext(SETTINGS, userSettings);
|
||||
|
||||
// Setters do not need to be reactive since `data` never updates
|
||||
setSecretsService(data.secretsService);
|
||||
setContext(UserService, data.userService);
|
||||
setContext(ProjectService, data.projectService);
|
||||
setContext(UpdaterService, data.updaterService);
|
||||
|
@ -10,6 +10,7 @@ import { UpdaterService } from '$lib/backend/updater';
|
||||
import { LineManagerFactory } from '$lib/commitLines/lineManager';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import { RemotesService } from '$lib/remotes/service';
|
||||
import { RustSecretService } from '$lib/secrets/secretsService';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import { mockTauri } from '$lib/testing/index';
|
||||
import lscache from 'lscache';
|
||||
@ -54,7 +55,8 @@ export async function load() {
|
||||
const githubService = new GitHubService(userService.accessToken$, remoteUrl$);
|
||||
|
||||
const gitConfig = new GitConfigService();
|
||||
const aiService = new AIService(gitConfig, httpClient);
|
||||
const secretsService = new RustSecretService(gitConfig);
|
||||
const aiService = new AIService(gitConfig, secretsService, httpClient);
|
||||
const remotesService = new RemotesService();
|
||||
const aiPromptService = new AIPromptService();
|
||||
const lineManagerFactory = new LineManagerFactory();
|
||||
@ -73,6 +75,7 @@ export async function load() {
|
||||
aiService,
|
||||
remotesService,
|
||||
aiPromptService,
|
||||
lineManagerFactory
|
||||
lineManagerFactory,
|
||||
secretsService
|
||||
};
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { AIService, GitAIConfigKey, KeyOption } from '$lib/ai/service';
|
||||
import { AISecretHandle, AIService, GitAIConfigKey, KeyOption } from '$lib/ai/service';
|
||||
import { OpenAIModelName, AnthropicModelName, ModelKind } from '$lib/ai/types';
|
||||
import { GitConfigService } from '$lib/backend/gitConfigService';
|
||||
import AiPromptEdit from '$lib/components/AIPromptEdit/AIPromptEdit.svelte';
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import WelcomeSigninAction from '$lib/components/WelcomeSigninAction.svelte';
|
||||
import { getSecretsService } from '$lib/secrets/secretsService';
|
||||
import ContentWrapper from '$lib/settings/ContentWrapper.svelte';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import InfoMessage from '$lib/shared/InfoMessage.svelte';
|
||||
@ -18,6 +19,7 @@
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
const gitConfigService = getContext(GitConfigService);
|
||||
const secretsService = getSecretsService();
|
||||
const aiService = getContext(AIService);
|
||||
const userService = getContext(UserService);
|
||||
const user = userService.user;
|
||||
@ -34,22 +36,26 @@
|
||||
let ollamaEndpoint: string | undefined;
|
||||
let ollamaModel: string | undefined;
|
||||
|
||||
function setConfiguration(key: GitAIConfigKey, value: string | undefined) {
|
||||
async function setConfiguration(key: GitAIConfigKey, value: string | undefined) {
|
||||
if (!initialized) return;
|
||||
|
||||
gitConfigService.set(key, value || '');
|
||||
}
|
||||
|
||||
async function setSecret(handle: AISecretHandle, secret: string | undefined) {
|
||||
if (!initialized) return;
|
||||
await secretsService.set(handle, secret || '');
|
||||
}
|
||||
|
||||
$: setConfiguration(GitAIConfigKey.ModelProvider, modelKind);
|
||||
|
||||
$: setConfiguration(GitAIConfigKey.OpenAIKeyOption, openAIKeyOption);
|
||||
$: setConfiguration(GitAIConfigKey.OpenAIModelName, openAIModelName);
|
||||
$: setConfiguration(GitAIConfigKey.OpenAIKey, openAIKey);
|
||||
$: setSecret(AISecretHandle.OpenAIKey, openAIKey);
|
||||
|
||||
$: setConfiguration(GitAIConfigKey.AnthropicKeyOption, anthropicKeyOption);
|
||||
$: setConfiguration(GitAIConfigKey.AnthropicModelName, anthropicModelName);
|
||||
$: setConfiguration(GitAIConfigKey.AnthropicKey, anthropicKey);
|
||||
$: setConfiguration(GitAIConfigKey.DiffLengthLimit, diffLengthLimit?.toString());
|
||||
$: setSecret(AISecretHandle.AnthropicKey, anthropicKey);
|
||||
|
||||
$: setConfiguration(GitAIConfigKey.OllamaEndpoint, ollamaEndpoint);
|
||||
$: setConfiguration(GitAIConfigKey.OllamaModelName, ollamaModel);
|
||||
|
@ -5,6 +5,9 @@ edition = "2021"
|
||||
authors = ["GitButler <gitbutler@gitbutler.com>"]
|
||||
publish = false
|
||||
|
||||
[[test]]
|
||||
name = "secret"
|
||||
path = "tests/secret/mod.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
once_cell = "1.19"
|
||||
@ -12,6 +15,7 @@ pretty_assertions = "1.4"
|
||||
gitbutler-testsupport.workspace = true
|
||||
gitbutler-git = { workspace = true, features = ["test-askpass-path"] }
|
||||
glob = "0.3.1"
|
||||
serial_test = "3.1.1"
|
||||
|
||||
[dependencies]
|
||||
toml = "0.8.13"
|
||||
@ -26,8 +30,9 @@ fslock = "0.2.1"
|
||||
futures = "0.3"
|
||||
git2.workspace = true
|
||||
git2-hooks = "0.3"
|
||||
gix = { workspace = true, features = ["dirwalk"] }
|
||||
gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] }
|
||||
itertools = "0.13"
|
||||
keyring.workspace = true
|
||||
lazy_static = "1.4.0"
|
||||
md5 = "0.7.0"
|
||||
hex = "0.4.3"
|
||||
@ -46,7 +51,7 @@ tempfile = "3.10"
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = [ "rt-multi-thread", "rt", "macros" ] }
|
||||
tracing = "0.1.40"
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
urlencoding = "2.1.3"
|
||||
uuid.workspace = true
|
||||
walkdir = "2.5.0"
|
||||
|
@ -27,20 +27,17 @@ impl Proxy {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn proxy_user(&self, user: users::User) -> users::User {
|
||||
match Url::parse(&user.picture) {
|
||||
Ok(picture) => users::User {
|
||||
picture: self.proxy(&picture).await.map_or_else(
|
||||
|error| {
|
||||
tracing::error!(?error, "failed to proxy user picture");
|
||||
user.picture.clone()
|
||||
},
|
||||
|url| url.to_string(),
|
||||
),
|
||||
..user
|
||||
},
|
||||
Err(_) => user,
|
||||
pub async fn proxy_user(&self, mut user: users::User) -> users::User {
|
||||
if let Ok(picture) = Url::parse(&user.picture) {
|
||||
user.picture = self.proxy(&picture).await.map_or_else(
|
||||
|error| {
|
||||
tracing::error!(?error, "failed to proxy user picture");
|
||||
user.picture.clone()
|
||||
},
|
||||
|url| url.to_string(),
|
||||
);
|
||||
}
|
||||
user
|
||||
}
|
||||
|
||||
async fn proxy_virtual_branch_commit(
|
||||
|
@ -29,6 +29,8 @@ pub mod project_repository;
|
||||
pub mod projects;
|
||||
pub mod rebase;
|
||||
pub mod remotes;
|
||||
pub mod secret;
|
||||
pub mod serde;
|
||||
pub mod ssh;
|
||||
pub mod storage;
|
||||
pub mod synchronize;
|
||||
@ -40,99 +42,3 @@ pub mod virtual_branches;
|
||||
pub mod windows;
|
||||
pub mod writer;
|
||||
pub mod zip;
|
||||
pub mod serde {
|
||||
use crate::virtual_branches::branch::HunkHash;
|
||||
use bstr::{BString, ByteSlice};
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn as_string_lossy<S>(v: &BString, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
v.to_str_lossy().serialize(s)
|
||||
}
|
||||
|
||||
pub fn hash_to_hex<S>(v: &HunkHash, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
format!("{v:x}").serialize(s)
|
||||
}
|
||||
|
||||
pub fn as_time_seconds_from_unix_epoch<S>(v: &git2::Time, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
v.seconds().serialize(s)
|
||||
}
|
||||
|
||||
pub mod oid_opt {
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
pub fn serialize<S>(v: &Option<git2::Oid>, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
v.as_ref().map(|v| v.to_string()).serialize(s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(d: D) -> Result<Option<git2::Oid>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let hex = <Option<String> as Deserialize>::deserialize(d)?;
|
||||
hex.map(|v| {
|
||||
v.parse()
|
||||
.map_err(|err: git2::Error| serde::de::Error::custom(err.to_string()))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod oid_vec {
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
pub fn serialize<S>(v: &[git2::Oid], s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let vec: Vec<String> = v.iter().map(|v| v.to_string()).collect();
|
||||
vec.serialize(s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(d: D) -> Result<Vec<git2::Oid>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let hex = <Vec<String> as Deserialize>::deserialize(d)?;
|
||||
let hex: Result<Vec<git2::Oid>, D::Error> = hex
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
git2::Oid::from_str(v.as_str())
|
||||
.map_err(|err: git2::Error| serde::de::Error::custom(err.to_string()))
|
||||
})
|
||||
.collect();
|
||||
hex
|
||||
}
|
||||
}
|
||||
|
||||
pub mod oid {
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
pub fn serialize<S>(v: &git2::Oid, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
v.to_string().serialize(s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(d: D) -> Result<git2::Oid, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let hex = String::deserialize(d)?;
|
||||
hex.parse()
|
||||
.map_err(|err: git2::Error| serde::de::Error::custom(err.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -378,10 +378,10 @@ impl Repository {
|
||||
"pushing code to gb repo",
|
||||
);
|
||||
|
||||
let access_token = user
|
||||
.map(|user| user.access_token.clone())
|
||||
.context("access token is missing")
|
||||
let user = user
|
||||
.context("need user to push to gitbutler")
|
||||
.context(Code::ProjectGitAuth)?;
|
||||
let access_token = user.access_token()?;
|
||||
|
||||
let mut callbacks = git2::RemoteCallbacks::new();
|
||||
if self.project.omit_certificate_check.unwrap_or(false) {
|
||||
|
237
crates/gitbutler-core/src/secret.rs
Normal file
237
crates/gitbutler-core/src/secret.rs
Normal file
@ -0,0 +1,237 @@
|
||||
//! This module contains facilities to handle the persistence of secrets.
|
||||
//!
|
||||
//! These are stateless and global, while discouraging storing secrets
|
||||
//! in memory beyond their use.
|
||||
|
||||
use crate::types::Sensitive;
|
||||
use anyhow::Result;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Determines how a secret's name should be modified to produce a namespace.
|
||||
///
|
||||
/// Namespaces can be used to partition secrets, depending on some criteria.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Namespace {
|
||||
/// Each application build, like `dev`, `production` and `nightly` have their
|
||||
/// own set of secrets. They do not overlap, which reflects how data-files
|
||||
/// are stored as well.
|
||||
BuildKind,
|
||||
/// All secrets are in a single namespace. There is no partitioning.
|
||||
/// This can be useful for secrets to be shared across all build kinds.
|
||||
Global,
|
||||
}
|
||||
|
||||
/// Persist `secret` in `namespace` so that it can be retrieved by the given `handle`.
|
||||
pub fn persist(handle: &str, secret: &Sensitive<String>, namespace: Namespace) -> Result<()> {
|
||||
let entry = entry_for(handle, namespace)?;
|
||||
if secret.0.is_empty() {
|
||||
entry.delete_password()?;
|
||||
} else {
|
||||
entry.set_password(&secret.0)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Obtain the previously [stored](persist()) secret known as `handle` from `namespace`.
|
||||
pub fn retrieve(handle: &str, namespace: Namespace) -> Result<Option<Sensitive<String>>> {
|
||||
match entry_for(handle, namespace)?.get_password() {
|
||||
Ok(secret) => Ok(Some(Sensitive(secret))),
|
||||
Err(keyring::Error::NoEntry) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the secret at `handle` permanently from `namespace`.
|
||||
pub fn delete(handle: &str, namespace: Namespace) -> Result<()> {
|
||||
Ok(entry_for(handle, namespace)?.delete_password()?)
|
||||
}
|
||||
|
||||
/// Use this `identifier` as 'namespace' for identifying secrets.
|
||||
/// Each namespace has its own set of secrets, useful for different application versions.
|
||||
///
|
||||
/// Note that the namespace will be `development` if `identifier` is empty (or wasn't set).
|
||||
pub fn set_application_namespace(identifier: impl Into<String>) {
|
||||
*NAMESPACE.lock().unwrap() = identifier.into()
|
||||
}
|
||||
|
||||
fn entry_for(handle: &str, namespace: Namespace) -> Result<keyring::Entry> {
|
||||
let ns = match namespace {
|
||||
Namespace::BuildKind => NAMESPACE.lock().unwrap().clone(),
|
||||
Namespace::Global => "gitbutler".into(),
|
||||
};
|
||||
Ok(keyring::Entry::new(
|
||||
&format!(
|
||||
"{prefix}-{handle}",
|
||||
prefix = if ns.is_empty() { "development" } else { &ns }
|
||||
),
|
||||
"GitButler",
|
||||
)?)
|
||||
}
|
||||
|
||||
/// How to further specialize secrets to avoid name clashes in the globally shared keystore.
|
||||
static NAMESPACE: Mutex<String> = Mutex::new(String::new());
|
||||
|
||||
/// A keystore that uses git-credentials under to hood. It's useful on Systems that nag the user
|
||||
/// with popups if the underlying binary changes, and is available if `git` can be found and executed.
|
||||
pub mod git_credentials {
|
||||
use anyhow::Result;
|
||||
use keyring::credential::{CredentialApi, CredentialBuilderApi, CredentialPersistence};
|
||||
use keyring::Credential;
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
|
||||
pub(super) struct Store(gix::config::File<'static>);
|
||||
|
||||
impl Store {
|
||||
/// Create an instance by resolving the global environment just well enough.
|
||||
///
|
||||
/// # Limitation
|
||||
///
|
||||
/// This does not fully resolve includes, so it's not truly production ready but should be
|
||||
/// fine for developer setups.
|
||||
fn from_globals() -> Result<Self> {
|
||||
Ok(Store(gix::config::File::from_globals()?))
|
||||
}
|
||||
|
||||
/// Provide credentials preconfigured for the given secrets `handle`.
|
||||
/// They can then be queried.
|
||||
fn credentials(
|
||||
&self,
|
||||
handle: &str,
|
||||
password: Option<&str>,
|
||||
) -> Result<(
|
||||
gix::credentials::helper::Cascade,
|
||||
gix::credentials::helper::Action,
|
||||
gix::prompt::Options<'static>,
|
||||
)> {
|
||||
let url = gix::Url::from_parts(
|
||||
gix::url::Scheme::Https,
|
||||
Some("gitbutler-secrets".into()),
|
||||
password.map(ToOwned::to_owned),
|
||||
Some("gitbutler.com".into()),
|
||||
None,
|
||||
format!("/{handle}").into(),
|
||||
false,
|
||||
)?;
|
||||
gix::config::credential_helpers(
|
||||
url,
|
||||
&self.0,
|
||||
true,
|
||||
&mut gix::config::section::is_trusted,
|
||||
gix::open::permissions::Environment::isolated(),
|
||||
true, /* use http path by default */
|
||||
)
|
||||
.map(|mut t| {
|
||||
let ctx = t.1.context_mut().expect("get always has context");
|
||||
// Assure the context has fields for all parts in the URL, even
|
||||
// if later we choose to use store or erase actions.
|
||||
// Usually these are naturally populated,
|
||||
// but not if we do everything by hand.
|
||||
// This is not a shortcoming in `gitoxide` - it simply doesn't touch
|
||||
// the output of previous invocations to not unintentionally affect them.
|
||||
ctx.destructure_url_in_place(true /* use http path */)
|
||||
.expect("input URL is valid");
|
||||
t.2.mode = gix::prompt::Mode::Disable;
|
||||
t
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type SharedStore = Arc<Store>;
|
||||
|
||||
struct Entry {
|
||||
handle: String,
|
||||
store: SharedStore,
|
||||
}
|
||||
|
||||
impl CredentialApi for Entry {
|
||||
#[instrument(skip(self, password), err(Debug))]
|
||||
fn set_password(&self, password: &str) -> keyring::Result<()> {
|
||||
// credential helper on macos can't overwrite existing values apparently, workaround that.
|
||||
#[cfg(target_os = "macos")]
|
||||
self.delete_password().ok();
|
||||
let (mut cascade, action, prompt) = self
|
||||
.store
|
||||
.credentials(&self.handle, Some(password))
|
||||
.map_err(|err| keyring::Error::PlatformFailure(err.into()))?;
|
||||
let ctx = action.context().expect("available for get").to_owned();
|
||||
let action = gix::credentials::helper::NextAction::from(ctx).store();
|
||||
cascade
|
||||
.invoke(action, prompt)
|
||||
.map_err(|err| keyring::Error::PlatformFailure(err.into()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err(Debug))]
|
||||
fn get_password(&self) -> keyring::Result<String> {
|
||||
let (mut cascade, get_action, prompt) = self
|
||||
.store
|
||||
.credentials(&self.handle, None)
|
||||
.map_err(|err| keyring::Error::PlatformFailure(err.into()))?;
|
||||
match cascade.invoke(get_action, prompt) {
|
||||
Ok(Some(out)) => Ok(out.identity.password),
|
||||
Ok(None) => Err(keyring::Error::NoEntry),
|
||||
Err(err) => {
|
||||
tracing::debug!(err = ?err, "credential-helper invoke failed - usually this means it wanted to prompt which is disabled");
|
||||
Err(keyring::Error::NoEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err(Debug))]
|
||||
fn delete_password(&self) -> keyring::Result<()> {
|
||||
let (mut cascade, action, prompt) = self
|
||||
.store
|
||||
.credentials(&self.handle, None)
|
||||
.map_err(|err| keyring::Error::PlatformFailure(err.into()))?;
|
||||
let ctx = action.context().expect("available for get").to_owned();
|
||||
let action = gix::credentials::helper::NextAction::from(ctx).erase();
|
||||
cascade
|
||||
.invoke(action, prompt)
|
||||
.map_err(|err| keyring::Error::PlatformFailure(err.into()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Builder {
|
||||
pub(super) store: SharedStore,
|
||||
}
|
||||
|
||||
impl CredentialBuilderApi for Builder {
|
||||
fn build(
|
||||
&self,
|
||||
_target: Option<&str>,
|
||||
service: &str,
|
||||
_user: &str,
|
||||
) -> keyring::Result<Box<Credential>> {
|
||||
let credential = Entry {
|
||||
handle: service.to_string(),
|
||||
store: self.store.clone(),
|
||||
};
|
||||
Ok(Box::new(credential))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
/// We keep information in memory
|
||||
fn persistence(&self) -> CredentialPersistence {
|
||||
CredentialPersistence::UntilReboot
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the credentials store so that secrets are using `git credential`.
|
||||
#[instrument(err(Debug))]
|
||||
pub fn setup() -> Result<()> {
|
||||
let store = Arc::new(Store::from_globals()?);
|
||||
keyring::set_default_credential_builder(Box::new(Builder { store }));
|
||||
Ok(())
|
||||
}
|
||||
}
|
94
crates/gitbutler-core/src/serde.rs
Normal file
94
crates/gitbutler-core/src/serde.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use crate::virtual_branches::branch::HunkHash;
|
||||
use bstr::{BString, ByteSlice};
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn as_string_lossy<S>(v: &BString, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
v.to_str_lossy().serialize(s)
|
||||
}
|
||||
|
||||
pub fn hash_to_hex<S>(v: &HunkHash, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
format!("{v:x}").serialize(s)
|
||||
}
|
||||
|
||||
pub fn as_time_seconds_from_unix_epoch<S>(v: &git2::Time, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
v.seconds().serialize(s)
|
||||
}
|
||||
|
||||
pub mod oid_opt {
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
pub fn serialize<S>(v: &Option<git2::Oid>, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
v.as_ref().map(|v| v.to_string()).serialize(s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(d: D) -> Result<Option<git2::Oid>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let hex = <Option<String> as Deserialize>::deserialize(d)?;
|
||||
hex.map(|v| {
|
||||
v.parse()
|
||||
.map_err(|err: git2::Error| serde::de::Error::custom(err.to_string()))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod oid_vec {
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
pub fn serialize<S>(v: &[git2::Oid], s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let vec: Vec<String> = v.iter().map(|v| v.to_string()).collect();
|
||||
vec.serialize(s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(d: D) -> Result<Vec<git2::Oid>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let hex = <Vec<String> as Deserialize>::deserialize(d)?;
|
||||
let hex: Result<Vec<git2::Oid>, D::Error> = hex
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
git2::Oid::from_str(v.as_str())
|
||||
.map_err(|err: git2::Error| serde::de::Error::custom(err.to_string()))
|
||||
})
|
||||
.collect();
|
||||
hex
|
||||
}
|
||||
}
|
||||
|
||||
pub mod oid {
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
pub fn serialize<S>(v: &git2::Oid, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
v.to_string().serialize(s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(d: D) -> Result<git2::Oid, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let hex = String::deserialize(d)?;
|
||||
hex.parse()
|
||||
.map_err(|err: git2::Error| serde::de::Error::custom(err.to_string()))
|
||||
}
|
||||
}
|
@ -6,11 +6,11 @@ impl<T> Serialize for Sensitive<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
unreachable!("BUG: Sensitive data cannot be serialized - it needs to be extracted and put into a struct for serialization explicitly")
|
||||
}
|
||||
}
|
||||
impl<'de, T> Deserialize<'de> for Sensitive<T>
|
||||
|
@ -1,9 +1,16 @@
|
||||
use super::{storage::Storage, User};
|
||||
use crate::secret;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{storage::Storage, User};
|
||||
|
||||
/// TODO(ST): useless intermediary - remove
|
||||
/// TODO(ST): rename to `Login` - seems more akin to what it does
|
||||
/// This type deals with user-related data which is only known if the user is logged in to GitButler.
|
||||
///
|
||||
/// ### Migrations: V1 -> V2
|
||||
///
|
||||
/// V2 is implied by not storing the `access_token` in plain text anymore, nor the GitHub token even if present.
|
||||
/// It happens automatically on [Self::get_user()] and [Self::set_user()]
|
||||
#[derive(Clone)]
|
||||
pub struct Controller {
|
||||
storage: Storage,
|
||||
@ -18,21 +25,46 @@ impl Controller {
|
||||
Controller::new(Storage::from_path(path))
|
||||
}
|
||||
|
||||
pub fn get_user(&self) -> anyhow::Result<Option<User>> {
|
||||
match self.storage.get().context("failed to get user") {
|
||||
Ok(user) => Ok(user),
|
||||
Err(err) => {
|
||||
self.storage.delete().ok();
|
||||
Err(err)
|
||||
}
|
||||
/// Return the current login, or `None` if there is none yet.
|
||||
pub fn get_user(&self) -> Result<Option<User>> {
|
||||
let user = self.storage.get().context("failed to get user")?;
|
||||
if let Some(user) = &user {
|
||||
write_without_secrets_if_secrets_present(&self.storage, user.clone())?;
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Note that secrets are never written in plain text, but we assure they are stored.
|
||||
pub fn set_user(&self, user: &User) -> Result<()> {
|
||||
if !write_without_secrets_if_secrets_present(&self.storage, user.clone())? {
|
||||
self.storage.set(user).context("failed to set user")
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_user(&self, user: &User) -> anyhow::Result<()> {
|
||||
self.storage.set(user).context("failed to set user")
|
||||
}
|
||||
|
||||
pub fn delete_user(&self) -> anyhow::Result<()> {
|
||||
self.storage.delete().context("failed to delete user")
|
||||
pub fn delete_user(&self) -> Result<()> {
|
||||
self.storage.delete().context("failed to delete user")?;
|
||||
let namespace = secret::Namespace::BuildKind;
|
||||
secret::delete(User::ACCESS_TOKEN_HANDLE, namespace).ok();
|
||||
secret::delete(User::GITHUB_ACCESS_TOKEN_HANDLE, namespace).ok();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// As `user` sports interior mutability right now, let's play it safe and work with fully owned items only.
|
||||
fn write_without_secrets_if_secrets_present(storage: &Storage, user: User) -> Result<bool> {
|
||||
let mut needs_write = false;
|
||||
let namespace = secret::Namespace::BuildKind;
|
||||
if let Some(gb_token) = user.access_token.borrow_mut().take() {
|
||||
needs_write |= secret::persist(User::ACCESS_TOKEN_HANDLE, &gb_token, namespace).is_ok();
|
||||
}
|
||||
if let Some(gh_token) = user.github_access_token.borrow_mut().take() {
|
||||
needs_write |=
|
||||
secret::persist(User::GITHUB_ACCESS_TOKEN_HANDLE, &gh_token, namespace).is_ok();
|
||||
}
|
||||
if needs_write {
|
||||
storage.set(&user)?;
|
||||
}
|
||||
Ok(needs_write)
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
use crate::secret;
|
||||
use crate::types::Sensitive;
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
pub struct User {
|
||||
@ -12,9 +15,49 @@ pub struct User {
|
||||
pub locale: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub access_token: Sensitive<String>,
|
||||
/// The presence of a GitButler access token is required for a valid user, but it's optional
|
||||
/// as it's not actually stored anymore, but fetch on demand in a separate step as its
|
||||
/// storage location is the [secrets store](crate::secret).
|
||||
#[serde(skip_serializing)]
|
||||
pub(super) access_token: RefCell<Option<Sensitive<String>>>,
|
||||
pub role: Option<String>,
|
||||
pub github_access_token: Option<Sensitive<String>>,
|
||||
/// The semantics here are the same as for `access_token`, but this token is truly optional.
|
||||
#[serde(skip_serializing)]
|
||||
pub(super) github_access_token: RefCell<Option<Sensitive<String>>>,
|
||||
#[serde(default)]
|
||||
pub github_username: Option<String>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub(super) const ACCESS_TOKEN_HANDLE: &'static str = "gitbutler_access_token";
|
||||
pub(super) const GITHUB_ACCESS_TOKEN_HANDLE: &'static str = "github_access_token";
|
||||
|
||||
/// Return the access token of the user after fetching it from the secrets store.
|
||||
///
|
||||
/// It's cached after the first retrieval.
|
||||
pub fn access_token(&self) -> Result<Sensitive<String>> {
|
||||
if let Some(token) = self.access_token.borrow().as_ref() {
|
||||
return Ok(token.clone());
|
||||
}
|
||||
let err_msg = "access token for user was deleted from keychain - login is now invalid";
|
||||
let secret = secret::retrieve(Self::ACCESS_TOKEN_HANDLE, secret::Namespace::BuildKind)?
|
||||
.context(err_msg)?;
|
||||
*self.access_token.borrow_mut() = Some(secret.clone());
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
/// Obtain the GitHub access token, if it is stored either on this instance or in the secrets store.
|
||||
///
|
||||
/// Note that if retrieved from the secrets store, it will be cached on instance.
|
||||
pub fn github_access_token(&self) -> Result<Option<Sensitive<String>>> {
|
||||
if let Some(token) = self.github_access_token.borrow().as_ref() {
|
||||
return Ok(Some(token.clone()));
|
||||
}
|
||||
let secret = secret::retrieve(
|
||||
Self::GITHUB_ACCESS_TOKEN_HANDLE,
|
||||
secret::Namespace::BuildKind,
|
||||
)?;
|
||||
self.github_access_token.borrow_mut().clone_from(&secret);
|
||||
Ok(secret)
|
||||
}
|
||||
}
|
||||
|
15
crates/gitbutler-core/tests/fixtures/users/login-only.v1
vendored
Normal file
15
crates/gitbutler-core/tests/fixtures/users/login-only.v1
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": 13612,
|
||||
"name": "Sebastian Thiel",
|
||||
"given_name": null,
|
||||
"family_name": null,
|
||||
"email": "sebastian.thiel@icloud.com",
|
||||
"picture": "https://avatars.githubusercontent.com/u/63622?v=4",
|
||||
"locale": null,
|
||||
"created_at": "2024-03-26T13:17:05Z",
|
||||
"updated_at": "2024-06-24T15:21:45Z",
|
||||
"access_token": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"role": null,
|
||||
"github_access_token": null,
|
||||
"github_username": null
|
||||
}
|
15
crates/gitbutler-core/tests/fixtures/users/with-github.v1
vendored
Normal file
15
crates/gitbutler-core/tests/fixtures/users/with-github.v1
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": 13612,
|
||||
"name": "Sebastian Thiel",
|
||||
"given_name": null,
|
||||
"family_name": null,
|
||||
"email": "sebastian.thiel@icloud.com",
|
||||
"picture": "https://avatars.githubusercontent.com/u/63622?v=4",
|
||||
"locale": null,
|
||||
"created_at": "2024-03-26T13:17:05Z",
|
||||
"updated_at": "2024-06-24T15:21:45Z",
|
||||
"access_token": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"role": null,
|
||||
"github_access_token": "gho_AAAAAAAAAAAAABBBBBBBBBBBBBBBCCCCCCCC",
|
||||
"github_username": null
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use std::str;
|
||||
|
||||
use gitbutler_core::types::Sensitive;
|
||||
use gitbutler_core::{
|
||||
git::credentials::{Credential, Helper, SshCredential},
|
||||
keys, project_repository, projects, users,
|
||||
@ -12,7 +11,7 @@ use gitbutler_testsupport::{temp_dir, test_repository};
|
||||
#[derive(Default)]
|
||||
struct TestCase<'a> {
|
||||
remote_url: &'a str,
|
||||
github_access_token: Option<Sensitive<&'a str>>,
|
||||
with_github_login: bool,
|
||||
preferred_key: projects::AuthKey,
|
||||
}
|
||||
|
||||
@ -20,11 +19,14 @@ impl TestCase<'_> {
|
||||
fn run(&self) -> Vec<(String, Vec<Credential>)> {
|
||||
let local_app_data = temp_dir();
|
||||
|
||||
gitbutler_testsupport::secrets::setup_blackhole_store();
|
||||
let users = users::Controller::from_path(local_app_data.path());
|
||||
let user = users::User {
|
||||
github_access_token: self.github_access_token.map(|s| Sensitive(s.0.to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
let user: users::User = serde_json::from_str(if self.with_github_login {
|
||||
include_str!("../../tests/fixtures/users/with-github.v1")
|
||||
} else {
|
||||
include_str!("../../tests/fixtures/users/login-only.v1")
|
||||
})
|
||||
.expect("valid v1 sample user");
|
||||
users.set_user(&user).unwrap();
|
||||
|
||||
let keys = keys::Controller::from_path(local_app_data.path());
|
||||
@ -56,7 +58,7 @@ mod not_github {
|
||||
fn https() {
|
||||
let test_case = TestCase {
|
||||
remote_url: "https://gitlab.com/test-gitbutler/test.git",
|
||||
github_access_token: Some(Sensitive("token")),
|
||||
with_github_login: true,
|
||||
preferred_key: projects::AuthKey::Local {
|
||||
private_key_path: PathBuf::from("/tmp/id_rsa"),
|
||||
},
|
||||
@ -80,7 +82,7 @@ mod not_github {
|
||||
fn ssh() {
|
||||
let test_case = TestCase {
|
||||
remote_url: "git@gitlab.com:test-gitbutler/test.git",
|
||||
github_access_token: Some(Sensitive("token")),
|
||||
with_github_login: true,
|
||||
preferred_key: projects::AuthKey::Local {
|
||||
private_key_path: PathBuf::from("/tmp/id_rsa"),
|
||||
},
|
||||
@ -115,7 +117,7 @@ mod github {
|
||||
fn https() {
|
||||
let test_case = TestCase {
|
||||
remote_url: "https://github.com/gitbutlerapp/gitbutler.git",
|
||||
github_access_token: Some(Sensitive("token")),
|
||||
with_github_login: true,
|
||||
preferred_key: projects::AuthKey::Local {
|
||||
private_key_path: PathBuf::from("/tmp/id_rsa"),
|
||||
},
|
||||
@ -139,7 +141,7 @@ mod github {
|
||||
fn ssh() {
|
||||
let test_case = TestCase {
|
||||
remote_url: "git@github.com:gitbutlerapp/gitbutler.git",
|
||||
github_access_token: Some(Sensitive("token")),
|
||||
with_github_login: true,
|
||||
preferred_key: projects::AuthKey::Local {
|
||||
private_key_path: PathBuf::from("/tmp/id_rsa"),
|
||||
},
|
||||
|
96
crates/gitbutler-core/tests/secret/credentials.rs
Normal file
96
crates/gitbutler-core/tests/secret/credentials.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use keyring::credential::{CredentialApi, CredentialBuilderApi, CredentialPersistence};
|
||||
use keyring::Credential;
|
||||
use std::any::Any;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Store(BTreeMap<String, String>);
|
||||
|
||||
pub(super) type SharedStore = Arc<Mutex<Store>>;
|
||||
|
||||
struct Entry {
|
||||
handle: String,
|
||||
store: SharedStore,
|
||||
}
|
||||
|
||||
impl CredentialApi for Entry {
|
||||
fn set_password(&self, password: &str) -> keyring::Result<()> {
|
||||
self.store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.0
|
||||
.insert(self.handle.clone(), password.into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_password(&self) -> keyring::Result<String> {
|
||||
match self.store.lock().unwrap().0.get(&self.handle) {
|
||||
Some(secret) => Ok(secret.clone()),
|
||||
None => Err(keyring::Error::NoEntry),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_password(&self) -> keyring::Result<()> {
|
||||
self.store.lock().unwrap().0.remove(&self.handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Builder {
|
||||
pub(super) store: SharedStore,
|
||||
}
|
||||
|
||||
impl CredentialBuilderApi for Builder {
|
||||
fn build(
|
||||
&self,
|
||||
_target: Option<&str>,
|
||||
service: &str,
|
||||
_user: &str,
|
||||
) -> keyring::Result<Box<Credential>> {
|
||||
let credential = Entry {
|
||||
handle: service.to_string(),
|
||||
store: self.store.clone(),
|
||||
};
|
||||
Ok(Box::new(credential))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
/// We keep information in memory
|
||||
fn persistence(&self) -> CredentialPersistence {
|
||||
CredentialPersistence::ProcessOnly
|
||||
}
|
||||
}
|
||||
|
||||
static CURRENT_STORE: Mutex<Option<SharedStore>> = Mutex::new(None);
|
||||
|
||||
/// Initialize the credentials store to be isolated and usable for testing.
|
||||
///
|
||||
/// Note that this is a resource shared in the process, and deterministic tests must
|
||||
/// use the `[serial]` annotation.
|
||||
pub fn setup() {
|
||||
let store = SharedStore::default();
|
||||
*CURRENT_STORE.lock().unwrap() = Some(store.clone());
|
||||
|
||||
keyring::set_default_credential_builder(Box::new(Builder { store }));
|
||||
}
|
||||
|
||||
/// Return the amount of stored secrets
|
||||
pub fn count() -> usize {
|
||||
CURRENT_STORE
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.expect("BUG: call setup")
|
||||
.lock()
|
||||
.unwrap()
|
||||
.0
|
||||
.len()
|
||||
}
|
59
crates/gitbutler-core/tests/secret/mod.rs
Normal file
59
crates/gitbutler-core/tests/secret/mod.rs
Normal file
@ -0,0 +1,59 @@
|
||||
//! Note that these tests *must* be run in their own process, as they rely on having a deterministic
|
||||
//! credential store. Due to its global nature, tests cannot run in parallel
|
||||
//! (or mixed with parallel tests that set their own credential store)
|
||||
use gitbutler_core::secret;
|
||||
use gitbutler_core::types::Sensitive;
|
||||
use serial_test::serial;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn retrieve_unknown_is_none() {
|
||||
credentials::setup();
|
||||
for ns in all_namespaces() {
|
||||
assert!(secret::retrieve("does not exist for sure", *ns)
|
||||
.expect("no error to ask for non-existing")
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn store_and_retrieve() -> anyhow::Result<()> {
|
||||
credentials::setup();
|
||||
for ns in all_namespaces() {
|
||||
secret::persist("new", &Sensitive("secret".into()), *ns)?;
|
||||
let secret = secret::retrieve("new", *ns)?.expect("it was just stored");
|
||||
assert_eq!(
|
||||
secret.0, "secret",
|
||||
"note that this works only if the engine supports actual persistence, \
|
||||
which should be the default outside of tests"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn store_empty_equals_deletion() -> anyhow::Result<()> {
|
||||
credentials::setup();
|
||||
for ns in all_namespaces() {
|
||||
secret::persist("new", &Sensitive("secret".into()), *ns)?;
|
||||
assert_eq!(credentials::count(), 1);
|
||||
|
||||
secret::persist("new", &Sensitive("".into()), *ns)?;
|
||||
assert_eq!(
|
||||
secret::retrieve("new", *ns)?.map(|s| s.0),
|
||||
None,
|
||||
"empty passwords are automatically deleted"
|
||||
);
|
||||
assert_eq!(credentials::count(), 0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn all_namespaces() -> &'static [secret::Namespace] {
|
||||
&[secret::Namespace::Global, secret::Namespace::BuildKind]
|
||||
}
|
||||
|
||||
pub(crate) mod credentials;
|
||||
mod users;
|
119
crates/gitbutler-core/tests/secret/users.rs
Normal file
119
crates/gitbutler-core/tests/secret/users.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use crate::{credentials, credentials::count as count_secrets};
|
||||
use gitbutler_core::users::User;
|
||||
use serial_test::serial;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::tempdir;
|
||||
|
||||
/// Validate that secrets previously stored in plain-text are auto-migrated into the secrets store.
|
||||
/// From there, data-structures for use by the frontend need to be 'enriched' with secrets before sending them,
|
||||
/// or before using them.
|
||||
#[test]
|
||||
#[serial]
|
||||
fn auto_migration_of_secrets_on_when_getting_and_setting_user() -> anyhow::Result<()> {
|
||||
for (name, has_github_token) in [("login-only.v1", false), ("with-github.v1", true)] {
|
||||
credentials::setup();
|
||||
let app_data = tempdir()?;
|
||||
|
||||
let users = gitbutler_core::users::Controller::from_path(app_data.path());
|
||||
assert!(
|
||||
users.get_user()?.is_none(),
|
||||
"Users are bound to logins, so there is none by default"
|
||||
);
|
||||
assert_eq!(count_secrets(), 0, "no secret is associated with anything");
|
||||
|
||||
let buf = std::fs::read(user_fixture(name))?;
|
||||
let user_json_path = app_data.path().join("user.json");
|
||||
std::fs::write(&user_json_path, &buf)?;
|
||||
|
||||
let user = users.get_user()?.expect("previous v1 user was read");
|
||||
let expected_secrets = if has_github_token { 2 } else { 1 };
|
||||
assert_eq!(
|
||||
count_secrets(),
|
||||
expected_secrets,
|
||||
"it automatically entered the secrets to the secrets store after getting the existing user"
|
||||
);
|
||||
|
||||
let assert_access_token_values = |user: &User| -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
user.access_token()?.0,
|
||||
"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"it can make the access token available"
|
||||
);
|
||||
if has_github_token {
|
||||
assert_eq!(
|
||||
user.github_access_token()?.map(|s| s.0),
|
||||
Some("gho_AAAAAAAAAAAAABBBBBBBBBBBBBBBCCCCCCCC".into()),
|
||||
"it can make the access token available"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
assert_access_token_values(&user)?;
|
||||
|
||||
let assert_no_secret_in_plain_text = || -> anyhow::Result<()> {
|
||||
let buf = std::fs::read(&user_json_path)?;
|
||||
let value: serde_json::Value = serde_json::from_slice(&buf)?;
|
||||
assert_eq!(
|
||||
value.get("access_token"),
|
||||
None,
|
||||
"access token wasn't written back (right after getting it)"
|
||||
);
|
||||
assert_eq!(
|
||||
value.get("github_access_token"),
|
||||
None,
|
||||
"access token wasn't written back"
|
||||
);
|
||||
Ok(())
|
||||
};
|
||||
assert_no_secret_in_plain_text()?;
|
||||
|
||||
let user = users.get_user()?.expect("stored user can be read");
|
||||
assert_access_token_values(&user)?;
|
||||
|
||||
users.delete_user()?;
|
||||
assert_eq!(
|
||||
count_secrets(),
|
||||
0,
|
||||
"deletion of a user automatically deletes its secretes"
|
||||
);
|
||||
assert!(
|
||||
!user_json_path.exists(),
|
||||
"it deletes the whole file, i.e. all associated user data"
|
||||
);
|
||||
|
||||
users.set_user(&user)?;
|
||||
assert_eq!(
|
||||
count_secrets(),
|
||||
expected_secrets,
|
||||
"the in-memory users had its secrets cached, so they are picked up and stored officially. \
|
||||
This is important, as the frontend sends these initially"
|
||||
);
|
||||
assert_no_secret_in_plain_text()?;
|
||||
|
||||
// forget all passwords
|
||||
credentials::setup();
|
||||
let user = users
|
||||
.get_user()?
|
||||
.expect("user still on disk and passwords are accessed lazily");
|
||||
assert!(
|
||||
user.access_token().is_err(),
|
||||
"this is critical - we have a user without access token, this fails early"
|
||||
);
|
||||
assert!(
|
||||
users.get_user()?.is_some(),
|
||||
"Client code needs to handle this case and delete the user, \
|
||||
otherwise it's there and errors forever"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn user_fixture(name: &str) -> PathBuf {
|
||||
let fixture = Path::new("tests/fixtures/users").join(name);
|
||||
assert!(
|
||||
fixture.exists(),
|
||||
"BUG: fixture at {fixture:?} ought to exist"
|
||||
);
|
||||
fixture
|
||||
}
|
@ -79,6 +79,11 @@ impl App {
|
||||
Ok(value.to_string())
|
||||
}
|
||||
|
||||
pub fn git_remove_global_config(key: &str) -> Result<()> {
|
||||
let mut config = git2::Config::open_default()?;
|
||||
Ok(config.remove(key)?)
|
||||
}
|
||||
|
||||
pub fn git_get_global_config(key: &str) -> Result<Option<String>> {
|
||||
let config = git2::Config::open_default()?;
|
||||
let value = config.get_string(key);
|
||||
|
@ -103,6 +103,12 @@ pub async fn git_set_global_config(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(err(Debug))]
|
||||
pub async fn git_remove_global_config(key: &str) -> Result<(), Error> {
|
||||
Ok(app::App::git_remove_global_config(key)?)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(_handle), err(Debug))]
|
||||
pub async fn git_get_global_config(
|
||||
|
@ -27,7 +27,9 @@ pub mod github;
|
||||
pub mod keys;
|
||||
pub mod projects;
|
||||
pub mod remotes;
|
||||
pub mod secret;
|
||||
pub mod undo;
|
||||
pub mod users;
|
||||
pub mod virtual_branches;
|
||||
|
||||
pub mod zip;
|
||||
|
@ -15,14 +15,17 @@
|
||||
|
||||
use gitbutler_core::{assets, git, storage};
|
||||
use gitbutler_tauri::{
|
||||
app, askpass, commands, config, github, keys, logs, menu, projects, remotes, undo, users,
|
||||
virtual_branches, watcher, zip,
|
||||
app, askpass, commands, config, github, keys, logs, menu, projects, remotes, secret, undo,
|
||||
users, virtual_branches, watcher, zip,
|
||||
};
|
||||
use tauri::{generate_context, Manager};
|
||||
use tauri_plugin_log::LogTarget;
|
||||
|
||||
fn main() {
|
||||
let tauri_context = generate_context!();
|
||||
gitbutler_core::secret::set_application_namespace(
|
||||
&tauri_context.config().tauri.bundle.identifier,
|
||||
);
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
@ -66,6 +69,14 @@ fn main() {
|
||||
|
||||
logs::init(&app_handle);
|
||||
|
||||
// On MacOS, in dev mode with debug assertions, we encounter popups each time
|
||||
// the binary is rebuilt. To counter that, use a git-credential based implementation.
|
||||
// This isn't an issue for actual release build (i.e. nightly, production),
|
||||
// hence the specific condition.
|
||||
if cfg!(debug_assertions) && cfg!(target_os = "macos") {
|
||||
gitbutler_core::secret::git_credentials::setup().ok();
|
||||
}
|
||||
|
||||
// SAFETY(qix-): This is safe because we're initializing the askpass broker here,
|
||||
// SAFETY(qix-): before any other threads would ever access it.
|
||||
unsafe {
|
||||
@ -157,6 +168,7 @@ fn main() {
|
||||
commands::delete_all_data,
|
||||
commands::mark_resolved,
|
||||
commands::git_set_global_config,
|
||||
commands::git_remove_global_config,
|
||||
commands::git_get_global_config,
|
||||
commands::git_test_push,
|
||||
commands::git_test_fetch,
|
||||
@ -204,6 +216,8 @@ fn main() {
|
||||
virtual_branches::commands::squash_branch_commit,
|
||||
virtual_branches::commands::fetch_from_remotes,
|
||||
virtual_branches::commands::move_commit,
|
||||
secret::secret_get_global,
|
||||
secret::secret_set_global,
|
||||
undo::list_snapshots,
|
||||
undo::restore_snapshot,
|
||||
undo::snapshot_diff,
|
||||
|
23
crates/gitbutler-tauri/src/secret.rs
Normal file
23
crates/gitbutler-tauri/src/secret.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use crate::error::Error;
|
||||
use gitbutler_core::secret;
|
||||
use gitbutler_core::types::Sensitive;
|
||||
use std::sync::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(err(Debug))]
|
||||
pub async fn secret_get_global(handle: &str) -> Result<Option<String>, Error> {
|
||||
Ok(secret::retrieve(handle, secret::Namespace::Global)?.map(|s| s.0))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(secret), err(Debug), fields(secret = "<redacted>"))]
|
||||
pub async fn secret_set_global(handle: &str, secret: String) -> Result<(), Error> {
|
||||
static FAIR_QUEUE: Mutex<()> = Mutex::new(());
|
||||
let _one_at_a_time_to_prevent_races = FAIR_QUEUE.lock().unwrap();
|
||||
Ok(secret::persist(
|
||||
handle,
|
||||
&Sensitive(secret),
|
||||
secret::Namespace::Global,
|
||||
)?)
|
||||
}
|
@ -3,6 +3,7 @@ pub mod commands {
|
||||
assets,
|
||||
users::{controller::Controller, User},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tracing::instrument;
|
||||
|
||||
@ -10,12 +11,18 @@ pub mod commands {
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn get_user(handle: AppHandle) -> Result<Option<User>, Error> {
|
||||
pub async fn get_user(handle: AppHandle) -> Result<Option<UserWithSecrets>, Error> {
|
||||
let app = handle.state::<Controller>();
|
||||
let proxy = handle.state::<assets::Proxy>();
|
||||
|
||||
match app.get_user()? {
|
||||
Some(user) => Ok(Some(proxy.proxy_user(user).await)),
|
||||
Some(user) => {
|
||||
if let Err(err) = user.access_token() {
|
||||
app.delete_user()?;
|
||||
return Err(err.context("Please login to GitButler again").into());
|
||||
}
|
||||
Ok(Some(proxy.proxy_user(user).await.try_into()?))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
@ -40,4 +47,59 @@ pub mod commands {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UserWithSecrets {
|
||||
id: u64,
|
||||
name: Option<String>,
|
||||
given_name: Option<String>,
|
||||
family_name: Option<String>,
|
||||
email: String,
|
||||
picture: String,
|
||||
locale: Option<String>,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
access_token: String,
|
||||
role: Option<String>,
|
||||
github_access_token: Option<String>,
|
||||
github_username: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<User> for UserWithSecrets {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: User) -> Result<Self, Self::Error> {
|
||||
let access_token = value.access_token()?;
|
||||
let github_access_token = value.github_access_token()?;
|
||||
let User {
|
||||
id,
|
||||
name,
|
||||
given_name,
|
||||
family_name,
|
||||
email,
|
||||
picture,
|
||||
locale,
|
||||
created_at,
|
||||
updated_at,
|
||||
role,
|
||||
github_username,
|
||||
..
|
||||
} = value;
|
||||
Ok(UserWithSecrets {
|
||||
id,
|
||||
name,
|
||||
given_name,
|
||||
family_name,
|
||||
email,
|
||||
picture,
|
||||
locale,
|
||||
created_at,
|
||||
updated_at,
|
||||
access_token: access_token.0,
|
||||
role,
|
||||
github_access_token: github_access_token.map(|s| s.0),
|
||||
github_username,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,4 +15,6 @@ once_cell = "1.19"
|
||||
git2.workspace = true
|
||||
pretty_assertions = "1.4"
|
||||
tempfile = "3.10.1"
|
||||
keyring.workspace = true
|
||||
serde_json = "1.0"
|
||||
gitbutler-core = { path = "../gitbutler-core" }
|
||||
|
15
crates/gitbutler-testsupport/src/fixtures/user/minimal.v1
Normal file
15
crates/gitbutler-testsupport/src/fixtures/user/minimal.v1
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": 0,
|
||||
"name": "test",
|
||||
"given_name": null,
|
||||
"family_name": null,
|
||||
"email": "test@email.com",
|
||||
"picture": "",
|
||||
"locale": null,
|
||||
"created_at": "",
|
||||
"updated_at": "",
|
||||
"access_token": "token",
|
||||
"role": null,
|
||||
"github_access_token": null,
|
||||
"github_username": null
|
||||
}
|
@ -60,3 +60,9 @@ pub fn init_opts_bare() -> git2::RepositoryInitOptions {
|
||||
opts.bare(true);
|
||||
opts
|
||||
}
|
||||
|
||||
/// A secrets store to prevent secrets to be written into the systems own store.
|
||||
///
|
||||
/// Note that this can't be used if secrets themselves are under test as it' doesn't act
|
||||
/// like a real store, i.e. stored secrets can't be retrieved anymore.
|
||||
pub mod secrets;
|
||||
|
45
crates/gitbutler-testsupport/src/secrets.rs
Normal file
45
crates/gitbutler-testsupport/src/secrets.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use std::any::Any;
|
||||
|
||||
/// Assure we have a mock secrets store so tests don't start writing secrets into the user's actual store,
|
||||
/// as this will affect their GitButler instance.
|
||||
pub fn setup_blackhole_store() {
|
||||
keyring::set_default_credential_builder(Box::new(BlackholeBuilder))
|
||||
}
|
||||
|
||||
/// A builder that is completely mocked, able to read no secret, but allowing to write any without error.
|
||||
struct BlackholeBuilder;
|
||||
|
||||
struct BlackholeCredential;
|
||||
|
||||
impl keyring::credential::CredentialApi for BlackholeCredential {
|
||||
fn set_password(&self, _password: &str) -> keyring::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_password(&self) -> keyring::Result<String> {
|
||||
Err(keyring::Error::NoEntry)
|
||||
}
|
||||
|
||||
fn delete_password(&self) -> keyring::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl keyring::credential::CredentialBuilderApi for BlackholeBuilder {
|
||||
fn build(
|
||||
&self,
|
||||
_target: Option<&str>,
|
||||
_service: &str,
|
||||
_user: &str,
|
||||
) -> keyring::Result<Box<keyring::Credential>> {
|
||||
Ok(Box::new(BlackholeCredential))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use gitbutler_core::types::Sensitive;
|
||||
use gitbutler_core::{git::RepositoryExt, project_repository};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
@ -48,12 +47,10 @@ impl Suite {
|
||||
self.local_app_data.as_ref().unwrap().path()
|
||||
}
|
||||
pub fn sign_in(&self) -> gitbutler_core::users::User {
|
||||
let user = gitbutler_core::users::User {
|
||||
name: Some("test".to_string()),
|
||||
email: "test@email.com".to_string(),
|
||||
access_token: Sensitive("token".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
crate::secrets::setup_blackhole_store();
|
||||
let user: gitbutler_core::users::User =
|
||||
serde_json::from_str(include_str!("fixtures/user/minimal.v1"))
|
||||
.expect("valid v1 user file");
|
||||
self.users.set_user(&user).expect("failed to add user");
|
||||
user
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user