24 Commits

Author SHA1 Message Date
fc4266bf67 oops 2022-01-23 22:17:43 +01:00
936e2bdba8 WIP: Add github integration 2022-01-23 22:16:22 +01:00
7e673200c8 Fix error on empty cloned repository 2022-01-23 22:11:54 +01:00
44a716248e Release v0.6.0 2022-01-23 12:28:45 +01:00
d20006a325 Merge branch 'develop' 2022-01-23 12:28:15 +01:00
f8adec1413 e2e: Ignore pip and setuptools for autoupdate 2022-01-23 12:27:59 +01:00
868269359c dependencies: Update serde to 1.0.135 2022-01-23 12:25:20 +01:00
61d4a4a0d8 e2e: Add PyYAML for YAML parsing 2022-01-22 11:23:53 +01:00
4e4de95a07 depcheck: Fix command to update crates.io index 2022-01-22 11:23:53 +01:00
9b64de7991 Add YAML as a config format option
@mustafa89 ;)
2022-01-22 11:23:53 +01:00
e45de3b498 depcheck: Fix crates.io cache update 2022-01-22 11:23:53 +01:00
6e4c388195 Add --stash options to pull and rebase 2022-01-22 11:23:53 +01:00
6436a8194e Disable "raw" SSH key usage
There is no sane way to get that fallback working with libgit2. Plus,
it's not a good practice anyway to have a non-password protected SSH
key.
2022-01-22 11:23:53 +01:00
f10ae25b2a Justfile: Add target to clean up 2022-01-22 11:23:53 +01:00
fd6b3b7438 Release v0.5.1 2022-01-22 11:19:37 +01:00
d68ff012f2 e2e_tests/pip: Update pyparsing to 3.0.7 2022-01-22 10:58:59 +01:00
9aad65edac dependencies: Update clap to 3.0.10 2022-01-22 10:58:59 +01:00
c370ef5815 dependencies: Update serde to 1.0.134 2022-01-22 10:58:59 +01:00
8f5b743ea4 Cargo.lock: Updating openssl-probe v0.1.4 -> v0.1.5 2022-01-22 10:58:47 +01:00
c0e981dbd4 Cargo.lock: Updating getrandom v0.2.3 -> v0.2.4 2022-01-22 10:58:47 +01:00
4303621b30 Cargo.lock: Updating smallvec v1.7.0 -> v1.8.0 2022-01-22 10:58:47 +01:00
63e04a9dcf dependencies: Update clap to 3.0.7 2022-01-22 10:58:47 +01:00
08ee946f2e dependencies: Update clap to 3.0.6 2022-01-22 10:58:47 +01:00
81de5a2d70 e2e_tests/pip: Update GitPython to 3.1.26 2022-01-22 10:58:47 +01:00
21 changed files with 1586 additions and 463 deletions

443
Cargo.lock generated
View File

@@ -11,6 +11,17 @@ dependencies = [
"memchr",
]
[[package]]
name = "async-channel"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
dependencies = [
"concurrent-queue",
"event-listener",
"futures-core",
]
[[package]]
name = "atty"
version = "0.2.14"
@@ -34,6 +45,24 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cache-padded"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
[[package]]
name = "castaway"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
[[package]]
name = "cc"
version = "1.0.72"
@@ -51,9 +80,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.0.5"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f34b09b9ee8c7c7b400fe2f8df39cafc9538b03d6ba7f4ae13e4cb90bfbb7d"
checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375"
dependencies = [
"atty",
"bitflags",
@@ -68,9 +97,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "3.0.5"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a0645a430ec9136d2d701e54a95d557de12649a9dd7109ced3187e648ac824"
checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153"
dependencies = [
"heck 0.4.0",
"proc-macro-error",
@@ -91,6 +120,15 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "concurrent-queue"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
dependencies = [
"cache-padded",
]
[[package]]
name = "console"
version = "0.15.0"
@@ -106,6 +144,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
dependencies = [
"cfg-if",
"lazy_static",
]
[[package]]
name = "crossterm"
version = "0.22.1"
@@ -131,6 +179,37 @@ dependencies = [
"winapi",
]
[[package]]
name = "curl"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de97b894edd5b5bcceef8b78d7da9b75b1d2f2f9a910569d0bde3dd31d84939"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2",
"winapi",
]
[[package]]
name = "curl-sys"
version = "0.4.52+curl-7.81.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b8c2d1023ea5fded5b7b892e4b8e95f70038a421126a056761a84246a28971"
dependencies = [
"cc",
"libc",
"libnghttp2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"winapi",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
@@ -158,6 +237,36 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [
"cfg-if",
]
[[package]]
name = "event-listener"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
[[package]]
name = "fastrand"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
dependencies = [
"instant",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
@@ -175,10 +284,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "getrandom"
version = "0.2.3"
name = "futures-core"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
[[package]]
name = "futures-io"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
[[package]]
name = "futures-lite"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "getrandom"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
dependencies = [
"cfg-if",
"libc",
@@ -187,14 +323,18 @@ dependencies = [
[[package]]
name = "git-repo-manager"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"clap",
"comfy-table",
"console",
"git2",
"isahc",
"parse_link_header",
"regex",
"serde",
"serde_json",
"serde_yaml",
"shellexpand",
"tempdir",
"toml",
@@ -245,6 +385,17 @@ dependencies = [
"libc",
]
[[package]]
name = "http"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "idna"
version = "0.2.3"
@@ -275,6 +426,41 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "isahc"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d140e84730d325378912ede32d7cd53ef1542725503b3353e5ec8113c7c6f588"
dependencies = [
"async-channel",
"castaway",
"crossbeam-utils",
"curl",
"curl-sys",
"encoding_rs",
"event-listener",
"futures-lite",
"http",
"log",
"mime",
"once_cell",
"polling",
"serde",
"serde_json",
"slab",
"sluice",
"tracing",
"tracing-futures",
"url",
"waker-fn",
]
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "jobserver"
version = "0.1.24"
@@ -292,9 +478,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.112"
version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9"
[[package]]
name = "libgit2-sys"
@@ -310,6 +496,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libnghttp2-sys"
version = "0.1.7+1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "libssh2-sys"
version = "0.2.23"
@@ -336,6 +532,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "lock_api"
version = "0.4.5"
@@ -366,6 +568,12 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mio"
version = "0.7.14"
@@ -405,9 +613,9 @@ checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "openssl-probe"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
@@ -431,6 +639,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "parking"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
[[package]]
name = "parking_lot"
version = "0.11.2"
@@ -456,18 +670,68 @@ dependencies = [
"winapi",
]
[[package]]
name = "parse_link_header"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40728c9c01de984c45f49385ab054fdc31cd3322658a6934347887e72cb48df9"
dependencies = [
"http",
"lazy_static",
"regex",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]]
name = "pkg-config"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[package]]
name = "polling"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259"
dependencies = [
"cfg-if",
"libc",
"log",
"wepoll-ffi",
"winapi",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -503,9 +767,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
@@ -592,6 +856,22 @@ dependencies = [
"winapi",
]
[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -600,24 +880,47 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0"
dependencies = [
"indexmap",
"ryu",
"serde",
"yaml-rust",
]
[[package]]
name = "shellexpand"
version = "2.1.0"
@@ -658,10 +961,37 @@ dependencies = [
]
[[package]]
name = "smallvec"
version = "1.7.0"
name = "slab"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]]
name = "sluice"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
dependencies = [
"async-channel",
"futures-core",
"futures-io",
]
[[package]]
name = "smallvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]]
name = "socket2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "strsim"
@@ -689,9 +1019,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.85"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
@@ -757,6 +1087,49 @@ dependencies = [
"serde",
]
[[package]]
name = "tracing"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
dependencies = [
"lazy_static",
]
[[package]]
name = "tracing-futures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [
"pin-project",
"tracing",
]
[[package]]
name = "unicode-bidi"
version = "0.3.7"
@@ -814,12 +1187,27 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "waker-fn"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wepoll-ffi"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
dependencies = [
"cc",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -850,3 +1238,12 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "git-repo-manager"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
authors = [
"Hannes Körber <hannes@hkoerber.de>",
@@ -40,7 +40,7 @@ path = "src/grm/main.rs"
version = "=0.5.8"
[dependencies.serde]
version = "=1.0.133"
version = "=1.0.135"
features = ["derive"]
[dependencies.git2]
@@ -50,7 +50,7 @@ version = "=0.13.25"
version = "=2.1.0"
[dependencies.clap]
version = "=3.0.5"
version = "=3.0.10"
features = ["derive", "cargo"]
[dependencies.console]
@@ -62,5 +62,18 @@ version = "=1.5.4"
[dependencies.comfy-table]
version = "=5.0.0"
[dependencies.serde_yaml]
version = "=0.8.23"
[dependencies.serde_json]
version = "=1.0.78"
[dependencies.isahc]
version = "=1.6.0"
features = ["json"]
[dependencies.parse_link_header]
version = "=0.3.2"
[dev-dependencies.tempdir]
version = "=0.3.7"

View File

@@ -52,3 +52,6 @@ check-pip-requirements: e2e-venv
@cd ./e2e_tests \
&& . ./venv/bin/activate \
&& pip list --outdated | grep -q '.' && exit 1 || exit 0
clean:
cargo clean

View File

@@ -14,7 +14,13 @@ AUTOUPDATE_DISABLED = []
if os.path.exists(INDEX_DIR):
subprocess.run(
["git", "pull", "--depth=1", "origin"],
["git", "fetch", "--depth=1", "origin"],
cwd=INDEX_DIR,
check=True,
capture_output=True,
)
subprocess.run(
["git", "reset", "--hard", "origin/master"],
cwd=INDEX_DIR,
check=True,
capture_output=True,
@@ -33,7 +39,7 @@ update_necessary = False
# This updates the crates.io index, see https://github.com/rust-lang/cargo/issues/3377
subprocess.run(
["cargo", "search", "--limit", "0"],
["cargo", "update", "--dry-run"],
check=True,
capture_output=False, # to get some git output
)

View File

@@ -5,7 +5,8 @@ Manager](https://github.com/hakoerber/git-repo-manager/) (GRM for short), a
tool that helps you manage git repositories.
GRM helps you manage git repositories in a declarative way. Configure your
repositories in a TOML file, GRM does the rest. Take a look at [the example
repositories in a TOML or YAML file, GRM does the rest. Take a look at [the
example
configuration](https://github.com/hakoerber/git-repo-manager/blob/master/example.config.toml)
to get a feel for the way you configure your repositories. See the [repository
tree chapter](./repos.md) for details.

View File

@@ -74,3 +74,9 @@ $ grm repos status
╰──────────┴──────────┴────────┴──────────┴───────┴─────────╯
```
## YAML
By default, the repo configuration uses TOML. If you prefer YAML, just give it
a YAML file instead (file ending does not matter, `grm` will figure out the format
itself). For generating a configuration, pass `--format yaml` to `grm repo find`
to generate YAML instead of TOML.

View File

@@ -309,6 +309,10 @@ grm wt pull --rebase
[✔] my-cool-branch: Done
```
As noted, this will fail if there are any local changes in your worktree. If you
want to stash these changes automatically before the pull (and unstash them
afterwards), use the `--stash` option.
This will rebase your changes onto the upstream branch. This is mainly helpful
for persistent branches that change on the remote side.
@@ -346,6 +350,10 @@ run two commands.
I understand that the UX is not the most intuitive. If you can think of an
improvement, please let me know (e.g. via an GitHub issue)!
As with `pull`, `rebase` will also refuse to run when there are changes in your
worktree. And you can also use the `--stash` option to stash/unstash changes
automatically.
### Manual access
GRM isn't doing any magic, it's just git under the hood. If you need to have access

View File

@@ -1,12 +1,13 @@
attrs==21.4.0
gitdb==4.0.9
GitPython==3.1.25
GitPython==3.1.26
iniconfig==1.1.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.6
pyparsing==3.0.7
pytest==6.2.5
PyYAML==6.0
smmap==5.0.0
toml==0.10.2
typing_extensions==4.0.1

View File

@@ -3,6 +3,8 @@
import tempfile
import toml
import pytest
import yaml
from helpers import *
@@ -30,6 +32,16 @@ def test_repos_find_empty():
assert len(cmd.stderr) != 0
def test_repos_find_invalid_format():
with tempfile.TemporaryDirectory() as tmpdir:
cmd = grm(
["repos", "find", tmpdir, "--format", "invalidformat"], is_invalid=True
)
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert "isn't a valid value" in cmd.stderr
def test_repos_find_non_git_repos():
with tempfile.TemporaryDirectory() as tmpdir:
shell(
@@ -50,7 +62,9 @@ def test_repos_find_non_git_repos():
assert len(cmd.stderr) != 0
def test_repos_find():
@pytest.mark.parametrize("default", [True, False])
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_find(configtype, default):
with tempfile.TemporaryDirectory() as tmpdir:
shell(
f"""
@@ -83,11 +97,19 @@ def test_repos_find():
"""
)
cmd = grm(["repos", "find", tmpdir])
args = ["repos", "find", tmpdir]
if not default:
args += ["--format", configtype]
cmd = grm(args)
assert cmd.returncode == 0
assert len(cmd.stderr) == 0
output = toml.loads(cmd.stdout)
if default or configtype == "toml":
output = toml.loads(cmd.stdout)
elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout)
else:
raise NotImplementedError()
assert isinstance(output, dict)
assert set(output.keys()) == {"trees"}
@@ -125,14 +147,24 @@ def test_repos_find():
assert origin["url"] == "https://example.com/repo2.git"
def test_repos_find_in_root():
@pytest.mark.parametrize("default", [True, False])
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_find_in_root(configtype, default):
with TempGitRepository() as repo_dir:
cmd = grm(["repos", "find", repo_dir])
args = ["repos", "find", repo_dir]
if not default:
args += ["--format", configtype]
cmd = grm(args)
assert cmd.returncode == 0
assert len(cmd.stderr) == 0
output = toml.loads(cmd.stdout)
if default or configtype == "toml":
output = toml.loads(cmd.stdout)
elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout)
else:
raise NotImplementedError()
assert isinstance(output, dict)
assert set(output.keys()) == {"trees"}
@@ -160,7 +192,9 @@ def test_repos_find_in_root():
assert someremote["type"] == "file"
def test_repos_find_with_invalid_repo():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
@pytest.mark.parametrize("default", [True, False])
def test_repos_find_with_invalid_repo(configtype, default):
with tempfile.TemporaryDirectory() as tmpdir:
shell(
f"""
@@ -193,11 +227,19 @@ def test_repos_find_with_invalid_repo():
"""
)
cmd = grm(["repos", "find", tmpdir])
args = ["repos", "find", tmpdir]
if not default:
args += ["--format", configtype]
cmd = grm(args)
assert cmd.returncode == 0
assert "broken" in cmd.stderr
output = toml.loads(cmd.stdout)
if default or configtype == "toml":
output = toml.loads(cmd.stdout)
elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout)
else:
raise NotImplementedError()
assert isinstance(output, dict)
assert set(output.keys()) == {"trees"}

View File

@@ -2,6 +2,7 @@
import tempfile
import re
import textwrap
import pytest
import toml
@@ -9,8 +10,134 @@ import git
from helpers import *
templates = {
"repo_simple": {
"toml": """
[[trees]]
root = "{root}"
def test_repos_sync_config_is_valid_symlink():
[[trees.repos]]
name = "test"
""",
"yaml": """
trees:
- root: "{root}"
repos:
- name: "test"
""",
},
"repo_with_remote": {
"toml": """
[[trees]]
root = "{root}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "{remotename}"
url = "file://{remote}"
type = "file"
""",
"yaml": textwrap.dedent(
"""
trees:
- root: "{root}"
repos:
- name: test
remotes:
- name: "{remotename}"
url: "file://{remote}"
type: "file"
"""
),
},
"repo_with_two_remotes": {
"toml": """
[[trees]]
root = "{root}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
""",
"yaml": textwrap.dedent(
"""
trees:
- root: "{root}"
repos:
- name: "test"
remotes:
- name: "origin"
url: "file://{remote1}"
type: "file"
- name: "origin2"
url: "file://{remote2}"
type: "file"
"""
),
},
"worktree_repo_simple": {
"toml": """
[[trees]]
root = "{root}"
[[trees.repos]]
name = "test"
worktree_setup = true
""",
"yaml": textwrap.dedent(
"""
trees:
- root: "{root}"
repos:
- name: test
worktree_setup: true
"""
),
},
"worktree_repo_with_remote": {
"toml": """
[[trees]]
root = "{root}"
[[trees.repos]]
name = "test"
worktree_setup = true
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote}"
type = "file"
""",
"yaml": textwrap.dedent(
"""
trees:
- root: "{root}"
repos:
- name: test
worktree_setup: true
remotes:
- name: origin
url: "file://{remote}"
type: "file"
"""
),
},
}
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_config_is_valid_symlink(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote, head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
@@ -20,20 +147,13 @@ def test_repos_sync_config_is_valid_symlink():
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote, remotename="origin"
)
)
subprocess.run(["cat", config.name])
cmd = grm(["repos", "sync", "--config", config_symlink])
assert cmd.returncode == 0
@@ -85,20 +205,13 @@ def test_repos_sync_config_is_unreadable():
assert "permission denied" in cmd.stderr.lower()
def test_repos_sync_unmanaged_repos():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_unmanaged_repos(configtype):
with tempfile.TemporaryDirectory() as root:
with TempGitRepository(dir=root) as unmanaged_repo:
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{root}"
[[trees.repos]]
name = "test"
"""
)
f.write(templates["repo_simple"][configtype].format(root=root))
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
@@ -112,19 +225,12 @@ def test_repos_sync_unmanaged_repos():
assert any([re.match(regex, l) for l in cmd.stderr.lower().split("\n")])
def test_repos_sync_root_is_file():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_root_is_file(configtype):
with tempfile.NamedTemporaryFile() as target:
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target.name}"
[[trees.repos]]
name = "test"
"""
)
f.write(templates["repo_simple"][configtype].format(root=target.name))
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode != 0
@@ -132,30 +238,17 @@ def test_repos_sync_root_is_file():
assert "not a directory" in cmd.stderr.lower()
def test_repos_sync_normal_clone():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_normal_clone(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
templates["repo_with_two_remotes"][configtype].format(
root=target, remote1=remote1, remote2=remote2
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -183,19 +276,12 @@ def test_repos_sync_normal_clone():
assert urls[0] == f"file://{remote2}"
def test_repos_sync_normal_init():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_normal_init(configtype):
with tempfile.TemporaryDirectory() as target:
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
"""
)
f.write(templates["repo_simple"][configtype].format(root=target))
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
@@ -210,25 +296,17 @@ def test_repos_sync_normal_init():
assert not repo.head.is_valid()
def test_repos_sync_normal_add_remote():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_normal_add_remote(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote1, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -246,23 +324,9 @@ def test_repos_sync_normal_add_remote():
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
templates["repo_with_two_remotes"][configtype].format(
root=target, remote1=remote1, remote2=remote2
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -282,30 +346,17 @@ def test_repos_sync_normal_add_remote():
assert urls[0] == f"file://{remote2}"
def test_repos_sync_normal_remove_remote():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_normal_remove_remote(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
templates["repo_with_two_remotes"][configtype].format(
root=target, remote1=remote1, remote2=remote2
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -326,18 +377,9 @@ def test_repos_sync_normal_remove_remote():
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote2, remotename="origin2"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -369,25 +411,17 @@ def test_repos_sync_normal_remove_remote():
assert urls[0] == f"file://{remote2}"
def test_repos_sync_normal_change_remote_url():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_normal_change_remote_url(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote1, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -405,18 +439,9 @@ def test_repos_sync_normal_change_remote_url():
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote2}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote2, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -429,25 +454,17 @@ def test_repos_sync_normal_change_remote_url():
assert urls[0] == f"file://{remote2}"
def test_repos_sync_normal_change_remote_name():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_normal_change_remote_name(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote1, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -465,18 +482,9 @@ def test_repos_sync_normal_change_remote_name():
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote1}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote1, remotename="origin2"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -492,25 +500,16 @@ def test_repos_sync_normal_change_remote_name():
assert urls[0] == f"file://{remote1}"
def test_repos_sync_worktree_clone():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_worktree_clone(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote, head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote}"
type = "file"
"""
templates["worktree_repo_with_remote"][configtype].format(
root=target, remote=remote, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -530,19 +529,13 @@ def test_repos_sync_worktree_clone():
assert str(repo.head.commit) == head_commit_sha
def test_repos_sync_worktree_init():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_worktree_init(configtype):
with tempfile.TemporaryDirectory() as target:
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
"""
templates["worktree_repo_simple"][configtype].format(root=target)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -559,43 +552,42 @@ def test_repos_sync_worktree_init():
assert not repo.head.is_valid()
def test_repos_sync_invalid_toml():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_invalid_syntax(configtype):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = invalid as there are no quotes ;)
"""
)
if configtype == "toml":
f.write(
f"""
[[trees]]
root = invalid as there are no quotes ;)
"""
)
elif configtype == "yaml":
f.write(
f"""
trees:
wrong:
indentation:
"""
)
else:
raise NotImplementedError()
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode != 0
def test_repos_sync_unchanged():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_unchanged(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
templates["repo_with_two_remotes"][configtype].format(
root=target, remote1=remote1, remote2=remote2
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -609,25 +601,17 @@ def test_repos_sync_unchanged():
assert before == after
def test_repos_sync_normal_change_to_worktree():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_normal_change_to_worktree(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote1, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -637,19 +621,9 @@ def test_repos_sync_normal_change_to_worktree():
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
templates["worktree_repo_with_remote"][configtype].format(
root=target, remote=remote1, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -658,26 +632,17 @@ def test_repos_sync_normal_change_to_worktree():
assert "not using a worktree setup" in cmd.stderr
def test_repos_sync_worktree_change_to_normal():
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
def test_repos_sync_worktree_change_to_normal(configtype):
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
templates["worktree_repo_with_remote"][configtype].format(
root=target, remote=remote1, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])
@@ -687,18 +652,9 @@ def test_repos_sync_worktree_change_to_normal():
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
templates["repo_with_remote"][configtype].format(
root=target, remote=remote1, remotename="origin"
)
)
cmd = grm(["repos", "sync", "--config", config.name])

View File

@@ -2,6 +2,8 @@
from helpers import *
import re
import pytest
import git
@@ -51,7 +53,9 @@ def test_worktree_fetch():
@pytest.mark.parametrize("rebase", [True, False])
@pytest.mark.parametrize("ffable", [True, False])
def test_worktree_pull(rebase, ffable):
@pytest.mark.parametrize("has_changes", [True, False])
@pytest.mark.parametrize("stash", [True, False])
def test_worktree_pull(rebase, ffable, has_changes, stash):
with TempGitRepositoryWorktree() as (base_dir, root_commit):
with TempGitFileRemote() as (remote_path, _remote_sha):
shell(
@@ -94,51 +98,79 @@ def test_worktree_pull(rebase, ffable):
"""
)
if has_changes:
shell(
f"""
cd {base_dir}/master
echo change >> root-commit-in-worktree-1
echo uncommitedchange > uncommitedchange
"""
)
args = ["wt", "pull"]
if rebase:
args += ["--rebase"]
if stash:
args += ["--stash"]
cmd = grm(args, cwd=base_dir)
assert cmd.returncode == 0
assert repo.commit("upstream/master").hexsha == remote_commit
assert repo.commit("origin/master").hexsha == root_commit
assert (
repo.commit("master").hexsha != repo.commit("origin/master").hexsha
)
if not rebase:
if ffable:
assert (
repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha
)
assert (
repo.commit("master").hexsha
== repo.commit("upstream/master").hexsha
)
assert repo.commit("upstream/master").hexsha == remote_commit
else:
assert "cannot be fast forwarded" in cmd.stderr
assert (
repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha
)
assert repo.commit("master").hexsha != remote_commit
assert repo.commit("upstream/master").hexsha == remote_commit
if has_changes and not stash:
assert cmd.returncode != 0
assert re.match(r".*master.*contains changes.*", cmd.stderr)
else:
if ffable:
assert (
repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha
)
assert (
repo.commit("master").hexsha
== repo.commit("upstream/master").hexsha
)
assert repo.commit("upstream/master").hexsha == remote_commit
assert repo.commit("upstream/master").hexsha == remote_commit
assert repo.commit("origin/master").hexsha == root_commit
assert (
repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha
)
if has_changes:
assert ["uncommitedchange"] == repo.untracked_files
assert repo.is_dirty()
else:
assert (
repo.commit("master").message.strip()
== "local-commit-in-master"
)
assert repo.commit("master~1").hexsha == remote_commit
assert not repo.is_dirty()
if not rebase:
if ffable:
assert cmd.returncode == 0
assert (
repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha
)
assert (
repo.commit("master").hexsha
== repo.commit("upstream/master").hexsha
)
assert (
repo.commit("upstream/master").hexsha == remote_commit
)
else:
assert cmd.returncode != 0
assert "cannot be fast forwarded" in cmd.stderr
assert (
repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha
)
assert repo.commit("master").hexsha != remote_commit
assert (
repo.commit("upstream/master").hexsha == remote_commit
)
else:
assert cmd.returncode == 0
if ffable:
assert (
repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha
)
assert (
repo.commit("master").hexsha
== repo.commit("upstream/master").hexsha
)
assert (
repo.commit("upstream/master").hexsha == remote_commit
)
else:
assert (
repo.commit("master").message.strip()
== "local-commit-in-master"
)
assert repo.commit("master~1").hexsha == remote_commit

View File

@@ -2,15 +2,18 @@
from helpers import *
import pytest
import re
import pytest
import git
@pytest.mark.parametrize("pull", [True, False])
@pytest.mark.parametrize("rebase", [True, False])
@pytest.mark.parametrize("ffable", [True, False])
def test_worktree_rebase(pull, rebase, ffable):
@pytest.mark.parametrize("has_changes", [True, False])
@pytest.mark.parametrize("stash", [True, False])
def test_worktree_rebase(pull, rebase, ffable, has_changes, stash):
with TempGitRepositoryWorktree() as (base_dir, _root_commit):
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write('persistent_branches = ["mybasebranch"]')
@@ -83,6 +86,14 @@ def test_worktree_rebase(pull, rebase, ffable):
"""
)
if has_changes:
shell(
f"""
cd {base_dir}/myfeatbranch
echo uncommitedchange > uncommitedchange
"""
)
grm(["wt", "delete", "--force", "tmp"], cwd=base_dir)
repo = git.Repo(f"{base_dir}/.git-main-working-tree")
@@ -133,17 +144,23 @@ def test_worktree_rebase(pull, rebase, ffable):
args += ["--pull"]
if rebase:
args += ["--rebase"]
if stash:
args += ["--stash"]
cmd = grm(args, cwd=base_dir)
print(args)
if rebase and not pull:
assert cmd.returncode != 0
assert len(cmd.stderr) != 0
elif has_changes and not stash:
assert cmd.returncode != 0
assert re.match(r".*myfeatbranch.*contains changes.*", cmd.stderr)
else:
assert cmd.returncode == 0
repo = git.Repo(f"{base_dir}/myfeatbranch")
if has_changes:
assert ["uncommitedchange"] == repo.untracked_files
if pull:
if rebase:
assert cmd.returncode == 0
if ffable:
assert (
repo.commit("HEAD").message.strip()
@@ -190,6 +207,7 @@ def test_worktree_rebase(pull, rebase, ffable):
assert repo.commit("HEAD~6").message.strip() == "commit-root"
else:
if ffable:
assert cmd.returncode == 0
assert (
repo.commit("HEAD").message.strip()
== "commit-in-feat-remote"
@@ -208,6 +226,7 @@ def test_worktree_rebase(pull, rebase, ffable):
)
assert repo.commit("HEAD~4").message.strip() == "commit-root"
else:
assert cmd.returncode != 0
assert (
repo.commit("HEAD").message.strip()
== "commit-in-feat-local-no-ff"
@@ -226,6 +245,7 @@ def test_worktree_rebase(pull, rebase, ffable):
)
assert repo.commit("HEAD~4").message.strip() == "commit-root"
else:
assert cmd.returncode == 0
if ffable:
assert repo.commit("HEAD").message.strip() == "commit-in-feat-local"
assert (

View File

@@ -9,6 +9,8 @@ source ./venv/bin/activate
pip --disable-pip-version-check install -r ./requirements.txt
pip3 list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | while read -r package ; do
[[ "$package" == "pip" ]] && continue
[[ "$package" == "setuptools" ]] && continue
pip install --upgrade "${package}"
version="$(pip show "${package}" | grep '^Version' | cut -d ' ' -f 2)"
message="e2e_tests/pip: Update ${package} to ${version}"

16
example.config.yaml Normal file
View File

@@ -0,0 +1,16 @@
trees:
- root: "~/example-projects/"
repos:
- name: "git-repo-manager"
remotes:
- name: "origin"
url: "https://code.hkoerber.de/hannes/git-repo-manager.git"
type: "https"
- name: "github"
url: "https://github.com/hakoerber/git-repo-manager.git"
type: "https"
- name: "dotfiles"
remotes:
- name: "origin"
url: "https://github.com/hakoerber/dotfiles.git"
type: "https"

View File

@@ -36,6 +36,10 @@ impl Config {
Err(error) => Err(error.to_string()),
}
}
pub fn as_yaml(&self) -> Result<String, String> {
serde_yaml::to_string(self).map_err(|e| e.to_string())
}
}
#[derive(Debug, Serialize, Deserialize)]
@@ -62,12 +66,15 @@ pub fn read_config(path: &str) -> Result<Config, String> {
let config: Config = match toml::from_str(&content) {
Ok(c) => c,
Err(e) => {
return Err(format!(
"Error parsing configuration file \"{}\": {}",
path, e
))
}
Err(_) => match serde_yaml::from_str(&content) {
Ok(c) => c,
Err(e) => {
return Err(format!(
"Error parsing configuration file \"{}\": {}",
path, e
))
}
},
};
Ok(config)

View File

@@ -31,20 +31,94 @@ pub struct Repos {
#[derive(Parser)]
pub enum ReposAction {
#[clap(
visible_alias = "run",
about = "Synchronize the repositories to the configured values"
)]
Sync(Sync),
#[clap(about = "Generate a repository configuration from an existing file tree")]
Find(Find),
#[clap(subcommand)]
Sync(SyncAction),
#[clap(subcommand)]
Find(FindAction),
#[clap(about = "Show status of configured repositories")]
Status(OptionalConfig),
}
#[derive(Parser)]
#[clap(about = "Sync local repositories with a configured list")]
pub enum SyncAction {
#[clap(
visible_alias = "run",
about = "Synchronize the repositories to the configured values"
)]
Config(Config),
#[clap(about = "Synchronize the repositories from a remote provider")]
Remote(Remote),
}
#[derive(Parser)]
#[clap(about = "Generate a repository configuration from existing repositories")]
pub enum FindAction {
#[clap(about = "Find local repositories")]
Local(FindLocalArgs),
#[clap(about = "Find repositories on remote provider")]
Remote(FindRemoteArgs),
}
#[derive(Parser)]
pub struct FindLocalArgs {
#[clap(help = "The path to search through")]
pub path: String,
#[clap(
arg_enum,
short,
long,
help = "Format to produce",
default_value_t = ConfigFormat::Toml,
)]
pub format: ConfigFormat,
}
#[derive(Parser)]
#[clap()]
pub struct Sync {
pub struct FindRemoteArgs {
#[clap(arg_enum, short, long, help = "Remote provider to use")]
pub provider: RemoteProvider,
#[clap(
multiple_occurrences = true,
name = "user",
long,
help = "Users to get repositories from"
)]
pub users: Vec<String>,
#[clap(
multiple_occurrences = true,
name = "group",
long,
help = "Groups to get repositories from"
)]
pub groups: Vec<String>,
#[clap(long, help = "Get repositories that belong to the requesting user")]
pub owner: bool,
#[clap(long, help = "Command to get API token")]
pub token_command: String,
#[clap(
arg_enum,
short,
long,
help = "Format to produce",
default_value_t = ConfigFormat::Toml,
)]
pub format: ConfigFormat,
#[clap(long, help = "Root of the repo tree to produce")]
pub root: String,
}
#[derive(Parser)]
#[clap()]
pub struct Config {
#[clap(
short,
long,
@@ -54,6 +128,40 @@ pub struct Sync {
pub config: String,
}
#[derive(clap::ArgEnum, Clone)]
pub enum RemoteProvider {
Github,
}
#[derive(Parser)]
#[clap()]
pub struct Remote {
#[clap(arg_enum, short, long, help = "Remote provider to use")]
pub provider: RemoteProvider,
#[clap(
multiple_occurrences = true,
name = "user",
long,
help = "Users to get repositories from"
)]
pub users: Vec<String>,
#[clap(
multiple_occurrences = true,
name = "group",
long,
help = "Groups to get repositories from"
)]
pub groups: Vec<String>,
#[clap(long, help = "Get repositories that belong to the requesting user")]
pub owner: bool,
#[clap(long, help = "Command to get API token")]
pub token_command: String,
}
#[derive(Parser)]
#[clap()]
pub struct OptionalConfig {
@@ -61,10 +169,10 @@ pub struct OptionalConfig {
pub config: Option<String>,
}
#[derive(Parser)]
pub struct Find {
#[clap(help = "The path to search through")]
pub path: String,
#[derive(clap::ArgEnum, Clone)]
pub enum ConfigFormat {
Yaml,
Toml,
}
#[derive(Parser)]
@@ -132,6 +240,8 @@ pub struct WorktreeFetchArgs {}
pub struct WorktreePullArgs {
#[clap(long = "--rebase", help = "Perform a rebase instead of a fast-forward")]
pub rebase: bool,
#[clap(long = "--stash", help = "Stash & unstash changes before & after pull")]
pub stash: bool,
}
#[derive(Parser)]
@@ -140,6 +250,11 @@ pub struct WorktreeRebaseArgs {
pub pull: bool,
#[clap(long = "--rebase", help = "Perform a rebase when doing a pull")]
pub rebase: bool,
#[clap(
long = "--stash",
help = "Stash & unstash changes before & after rebase"
)]
pub stash: bool,
}
pub fn parse() -> Opts {

View File

@@ -5,6 +5,7 @@ mod cmd;
use grm::config;
use grm::output::*;
use grm::provider::Provider;
use grm::repo;
fn main() {
@@ -12,26 +13,88 @@ fn main() {
match opts.subcmd {
cmd::SubCommand::Repos(repos) => match repos.action {
cmd::ReposAction::Sync(sync) => {
let config = match config::read_config(&sync.config) {
Ok(config) => config,
Err(error) => {
print_error(&error);
process::exit(1);
}
};
match grm::sync_trees(config) {
Ok(success) => {
if !success {
process::exit(1)
cmd::ReposAction::Sync(sync) => match sync {
cmd::SyncAction::Config(args) => {
let config = match config::read_config(&args.config) {
Ok(config) => config,
Err(error) => {
print_error(&error);
process::exit(1);
}
};
match grm::sync_trees(config) {
Ok(success) => {
if !success {
process::exit(1)
}
}
Err(error) => {
print_error(&format!("Error syncing trees: {}", error));
process::exit(1);
}
}
Err(error) => {
print_error(&format!("Error syncing trees: {}", error));
process::exit(1);
}
cmd::SyncAction::Remote(args) => {
let users = if args.users.is_empty() {
None
} else {
Some(args.users)
};
let groups = if args.groups.is_empty() {
None
} else {
Some(args.groups)
};
let token_process = std::process::Command::new("/usr/bin/env")
.arg("sh")
.arg("-c")
.arg(args.token_command)
.output();
let token: String = match token_process {
Err(error) => {
print_error(&format!("Failed to run token-command: {}", error));
process::exit(1);
}
Ok(output) => {
let stderr = String::from_utf8(output.stderr).unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
if !output.status.success() {
if !stderr.is_empty() {
print_error(&format!("Token command failed: {}", stderr));
} else {
print_error("Token command failed.");
}
}
if !stderr.is_empty() {
print_error(&format!("Token command produced stderr: {}", stderr));
}
if stdout.is_empty() {
print_error("Token command did not produce output");
}
let token = stdout.split('\n').next().unwrap();
token.to_string()
}
};
let filter = grm::provider::Filter::new(users, groups, args.owner);
let github = grm::provider::Github::new(filter, token);
match github.get_repos() {
Ok(repos) => println!("{:?}", repos),
Err(error) => {
print_error(&format!("Error: {}", error));
process::exit(1);
}
}
}
}
},
cmd::ReposAction::Status(args) => match &args.config {
Some(config_path) => {
let config = match config::read_config(config_path) {
@@ -79,60 +142,187 @@ fn main() {
}
}
},
cmd::ReposAction::Find(find) => {
let path = Path::new(&find.path);
if !path.exists() {
print_error(&format!("Path \"{}\" does not exist", path.display()));
process::exit(1);
}
if !path.is_dir() {
print_error(&format!("Path \"{}\" is not a directory", path.display()));
process::exit(1);
}
let path = match path.canonicalize() {
Ok(path) => path,
Err(error) => {
print_error(&format!(
"Failed to canonicalize path \"{}\". This is a bug. Error message: {}",
&path.display(),
error
));
cmd::ReposAction::Find(find) => match find {
cmd::FindAction::Local(args) => {
let path = Path::new(&args.path);
if !path.exists() {
print_error(&format!("Path \"{}\" does not exist", path.display()));
process::exit(1);
}
};
let (found_repos, warnings) = match grm::find_in_tree(&path) {
Ok((repos, warnings)) => (repos, warnings),
Err(error) => {
print_error(&error);
if !path.is_dir() {
print_error(&format!("Path \"{}\" is not a directory", path.display()));
process::exit(1);
}
};
let trees = grm::config::Trees::from_vec(vec![found_repos]);
if trees.as_vec_ref().iter().all(|t| match &t.repos {
None => false,
Some(r) => r.is_empty(),
}) {
print_warning("No repositories found");
} else {
let config = trees.to_config();
let toml = match config.as_toml() {
Ok(toml) => toml,
let path = match path.canonicalize() {
Ok(path) => path,
Err(error) => {
print_error(&format!("Failed converting config to TOML: {}", &error));
print_error(&format!(
"Failed to canonicalize path \"{}\". This is a bug. Error message: {}",
&path.display(),
error
));
process::exit(1);
}
};
print!("{}", toml);
let (found_repos, warnings) = match grm::find_in_tree(&path) {
Ok((repos, warnings)) => (repos, warnings),
Err(error) => {
print_error(&error);
process::exit(1);
}
};
let trees = grm::config::Trees::from_vec(vec![found_repos]);
if trees.as_vec_ref().iter().all(|t| match &t.repos {
None => false,
Some(r) => r.is_empty(),
}) {
print_warning("No repositories found");
} else {
let config = trees.to_config();
match args.format {
cmd::ConfigFormat::Toml => {
let toml = match config.as_toml() {
Ok(toml) => toml,
Err(error) => {
print_error(&format!(
"Failed converting config to TOML: {}",
&error
));
process::exit(1);
}
};
print!("{}", toml);
}
cmd::ConfigFormat::Yaml => {
let yaml = match config.as_yaml() {
Ok(yaml) => yaml,
Err(error) => {
print_error(&format!(
"Failed converting config to YAML: {}",
&error
));
process::exit(1);
}
};
print!("{}", yaml);
}
}
}
for warning in warnings {
print_warning(&warning);
}
}
for warning in warnings {
print_warning(&warning);
cmd::FindAction::Remote(args) => {
let users = if args.users.is_empty() {
None
} else {
Some(args.users)
};
let groups = if args.groups.is_empty() {
None
} else {
Some(args.groups)
};
let token_process = std::process::Command::new("/usr/bin/env")
.arg("sh")
.arg("-c")
.arg(args.token_command)
.output();
let token: String = match token_process {
Err(error) => {
print_error(&format!("Failed to run token-command: {}", error));
process::exit(1);
}
Ok(output) => {
let stderr = String::from_utf8(output.stderr).unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
if !output.status.success() {
if !stderr.is_empty() {
print_error(&format!("Token command failed: {}", stderr));
} else {
print_error("Token command failed.");
}
}
if !stderr.is_empty() {
print_error(&format!("Token command produced stderr: {}", stderr));
}
if stdout.is_empty() {
print_error("Token command did not produce output");
}
let token = stdout.split('\n').next().unwrap();
token.to_string()
}
};
let filter = grm::provider::Filter::new(users, groups, args.owner);
let github = grm::provider::Github::new(filter, token);
match github.get_repos() {
Ok(repos) => {
let mut trees: Vec<config::Tree> = vec![];
for (namespace, repolist) in repos {
let tree = config::Tree {
root: Path::new(&args.root)
.join(namespace)
.display()
.to_string(),
repos: Some(repolist),
};
trees.push(tree);
}
let config = config::Config {
trees: config::Trees::from_vec(trees),
};
match args.format {
cmd::ConfigFormat::Toml => {
let toml = match config.as_toml() {
Ok(toml) => toml,
Err(error) => {
print_error(&format!(
"Failed converting config to TOML: {}",
&error
));
process::exit(1);
}
};
print!("{}", toml);
}
cmd::ConfigFormat::Yaml => {
let yaml = match config.as_yaml() {
Ok(yaml) => yaml,
Err(error) => {
print_error(&format!(
"Failed converting config to YAML: {}",
&error
));
process::exit(1);
}
};
print!("{}", yaml);
}
}
}
Err(error) => {
print_error(&format!("Error: {}", error));
process::exit(1);
}
}
}
}
},
},
cmd::SubCommand::Worktree(args) => {
let cwd = std::env::current_dir().unwrap_or_else(|error| {
@@ -350,26 +540,27 @@ fn main() {
process::exit(1);
});
let mut failures = false;
for worktree in repo.get_worktrees().unwrap_or_else(|error| {
print_error(&format!("Error getting worktrees: {}", error));
process::exit(1);
}) {
if let Some(warning) =
worktree
.forward_branch(args.rebase)
.unwrap_or_else(|error| {
print_error(&format!(
"Error updating worktree branch: {}",
error
));
process::exit(1);
})
if let Some(warning) = worktree
.forward_branch(args.rebase, args.stash)
.unwrap_or_else(|error| {
print_error(&format!("Error updating worktree branch: {}", error));
process::exit(1);
})
{
print_warning(&format!("{}: {}", worktree.name(), warning));
failures = true;
} else {
print_success(&format!("{}: Done", worktree.name()));
}
}
if failures {
process::exit(1);
}
}
cmd::WorktreeAction::Rebase(args) => {
if args.rebase && !args.pull {
@@ -406,10 +597,12 @@ fn main() {
process::exit(1);
});
let mut failures = false;
for worktree in &worktrees {
if args.pull {
if let Some(warning) = worktree
.forward_branch(args.rebase)
.forward_branch(args.rebase, args.stash)
.unwrap_or_else(|error| {
print_error(&format!(
"Error updating worktree branch: {}",
@@ -418,28 +611,29 @@ fn main() {
process::exit(1);
})
{
failures = true;
print_warning(&format!("{}: {}", worktree.name(), warning));
}
}
}
for worktree in &worktrees {
if let Some(warning) =
worktree
.rebase_onto_default(&config)
.unwrap_or_else(|error| {
print_error(&format!(
"Error rebasing worktree branch: {}",
error
));
process::exit(1);
})
if let Some(warning) = worktree
.rebase_onto_default(&config, args.stash)
.unwrap_or_else(|error| {
print_error(&format!("Error rebasing worktree branch: {}", error));
process::exit(1);
})
{
failures = true;
print_warning(&format!("{}: {}", worktree.name(), warning));
} else {
print_success(&format!("{}: Done", worktree.name()));
}
}
if failures {
process::exit(1);
}
}
}
}

View File

@@ -6,13 +6,14 @@ use std::process;
pub mod config;
pub mod output;
pub mod provider;
pub mod repo;
pub mod table;
use config::{Config, Tree};
use output::*;
use repo::{clone_repo, detect_remote_type, Remote, RepoConfig};
use repo::{clone_repo, detect_remote_type, Remote, RemoteType, RepoConfig};
pub use repo::{RemoteTrackingStatus, Repo, RepoErrorKind, WorktreeRemoveFailureReason};

199
src/provider/github.rs Normal file
View File

@@ -0,0 +1,199 @@
use std::collections::HashMap;
use isahc::prelude::*;
use serde::Deserialize;
use crate::{Remote, RemoteType, RepoConfig};
use super::Filter;
use super::Provider;
use super::SecretToken;
#[derive(Deserialize)]
#[serde(untagged)]
enum GithubUserProjectResponse {
Success(Vec<GithubProject>),
Failure(GithubFailureResponse),
}
#[derive(Deserialize)]
struct GithubProject {
pub name: String,
pub full_name: String,
pub clone_url: String,
pub ssh_url: String,
pub private: bool,
}
impl GithubProject {
fn into_repo_config(self) -> RepoConfig {
RepoConfig {
name: self.name,
worktree_setup: false,
remotes: Some(vec![Remote {
name: String::from("github"),
url: match self.private {
true => self.ssh_url,
false => self.clone_url,
},
remote_type: match self.private {
true => RemoteType::Ssh,
false => RemoteType::Https,
},
}]),
}
}
}
#[derive(Deserialize)]
struct GithubFailureResponse {
pub message: String,
}
pub struct Github {
filter: Filter,
secret_token: SecretToken,
}
impl Github {
fn get_repo_list_from_uri(
uri: &str,
secret_token: &SecretToken,
) -> Result<Vec<(String, GithubProject)>, String> {
let mut repos: Vec<(String, GithubProject)> = vec![];
let client = isahc::HttpClient::new().map_err(|error| error.to_string())?;
let request = isahc::Request::builder()
.uri(uri)
.header("accept", " application/vnd.github.v3+json")
.header("authorization", format!("token {}", secret_token))
.body(())
.map_err(|error| error.to_string())?;
let mut response = client.send(request).map_err(|error| error.to_string())?;
let success = response.status().is_success();
{
let response: GithubUserProjectResponse = response
.json()
.map_err(|error| format!("Failed deserializing response: {}", error))?;
if !success {
match response {
GithubUserProjectResponse::Failure(error) => return Err(error.message),
_ => return Err(String::from("Unknown response error")),
}
}
match response {
GithubUserProjectResponse::Failure(error) => {
return Err(format!(
"Received error response but no error code: {}",
error.message
))
}
GithubUserProjectResponse::Success(repo_list) => {
for repo in repo_list {
let (namespace, _name) = repo
.full_name
.rsplit_once('/')
.unwrap_or(("", &repo.full_name));
repos.push((namespace.to_string(), repo));
}
}
}
}
let headers = response.headers();
if let Some(link_header) = headers.get("link") {
let link_header = link_header.to_str().map_err(|error| error.to_string())?;
let link_header =
parse_link_header::parse(link_header).map_err(|error| error.to_string())?;
let next_page = link_header.get(&Some(String::from("next")));
if let Some(page) = next_page {
let following_repos = Github::get_repo_list_from_uri(&page.raw_uri, secret_token)?;
repos.extend(following_repos);
}
}
Ok(repos)
}
}
impl Provider for Github {
fn new(filter: Filter, secret_token: SecretToken) -> Self {
Github {
filter,
secret_token,
}
}
fn get_repos(&self) -> Result<HashMap<String, Vec<RepoConfig>>, String> {
let mut namespaces: HashMap<String, HashMap<String, RepoConfig>> = HashMap::new();
let mut register = |namespace: String, repo: GithubProject| {
let name = repo.name.clone();
let repo_config = repo.into_repo_config();
match namespaces.get_mut(&namespace) {
Some(ns) => match ns.get_mut(&name) {
Some(_entry) => {}
None => {
ns.insert(name, repo_config);
}
},
None => {
let mut ns = HashMap::new();
ns.insert(name, repo_config);
namespaces.insert(namespace, ns);
}
}
};
if let Some(users) = &self.filter.users {
for user in users {
let repos = Github::get_repo_list_from_uri(
&format!("https://api.github.com/users/{}/repos", user),
&self.secret_token,
)?;
for (namespace, repo) in repos {
register(namespace, repo);
}
}
}
if let Some(groups) = &self.filter.groups {
for group in groups {
let repos = Github::get_repo_list_from_uri(
&format!("https://api.github.com/orgs/{}/repos", group),
&self.secret_token,
)?;
for (namespace, repo) in repos {
register(namespace, repo);
}
}
}
if self.filter.owner {
let repos = Github::get_repo_list_from_uri(
"https://api.github.com/user/repos?affiliation=owner",
&self.secret_token,
)?;
for (namespace, repo) in repos {
register(namespace, repo);
}
}
let mut ret: HashMap<String, Vec<RepoConfig>> = HashMap::new();
for (namespace, repos) in namespaces {
ret.insert(namespace, repos.into_values().collect());
}
Ok(ret)
}
}

30
src/provider/mod.rs Normal file
View File

@@ -0,0 +1,30 @@
pub mod github;
pub use github::Github;
use super::RepoConfig;
use std::collections::HashMap;
pub struct Filter {
users: Option<Vec<String>>,
groups: Option<Vec<String>>,
owner: bool,
}
type SecretToken = String;
impl Filter {
pub fn new(users: Option<Vec<String>>, groups: Option<Vec<String>>, owner: bool) -> Self {
Filter {
users,
groups,
owner,
}
}
}
pub trait Provider {
fn new(filter: Filter, secret_token: SecretToken) -> Self;
fn get_repos(&self) -> Result<HashMap<String, Vec<RepoConfig>>, String>;
}

View File

@@ -181,17 +181,30 @@ impl Worktree {
&self.name
}
pub fn forward_branch(&self, rebase: bool) -> Result<Option<String>, String> {
pub fn forward_branch(&self, rebase: bool, stash: bool) -> Result<Option<String>, String> {
let repo = Repo::open(Path::new(&self.name), false)
.map_err(|error| format!("Error opening worktree: {}", error))?;
if let Ok(remote_branch) = repo.find_local_branch(&self.name)?.upstream() {
let status = repo.status(false)?;
let mut stashed_changes = false;
if !status.clean() {
return Ok(Some(String::from("Worktree contains changes")));
if stash {
repo.stash()?;
stashed_changes = true;
} else {
return Ok(Some(String::from("Worktree contains changes")));
}
}
let unstash = || -> Result<(), String> {
if stashed_changes {
repo.stash_pop()?;
}
Ok(())
};
let remote_annotated_commit = repo
.0
.find_annotated_commit(remote_branch.commit()?.id().0)
@@ -231,6 +244,7 @@ impl Worktree {
continue;
}
rebase.abort().map_err(convert_libgit2_error)?;
unstash()?;
return Err(convert_libgit2_error(error));
}
}
@@ -243,9 +257,11 @@ impl Worktree {
.map_err(convert_libgit2_error)?;
if analysis.is_up_to_date() {
unstash()?;
return Ok(None);
}
if !analysis.is_fast_forward() {
unstash()?;
return Ok(Some(String::from("Worktree cannot be fast forwarded")));
}
@@ -257,15 +273,18 @@ impl Worktree {
)
.map_err(convert_libgit2_error)?;
}
unstash()?;
} else {
return Ok(Some(String::from("No remote branch to rebase onto")));
};
Ok(None)
}
pub fn rebase_onto_default(
&self,
config: &Option<WorktreeRootConfig>,
stash: bool,
) -> Result<Option<String>, String> {
let repo = Repo::open(Path::new(&self.name), false)
.map_err(|error| format!("Error opening worktree: {}", error))?;
@@ -291,6 +310,25 @@ impl Worktree {
},
};
let status = repo.status(false)?;
let mut stashed_changes = false;
if !status.clean() {
if stash {
repo.stash()?;
stashed_changes = true;
} else {
return Ok(Some(String::from("Worktree contains changes")));
}
}
let unstash = || -> Result<(), String> {
if stashed_changes {
repo.stash_pop()?;
}
Ok(())
};
let base_branch = repo.find_local_branch(&default_branch_name)?;
let base_annotated_commit = repo
.0
@@ -330,11 +368,13 @@ impl Worktree {
continue;
}
rebase.abort().map_err(convert_libgit2_error)?;
unstash()?;
return Err(convert_libgit2_error(error));
}
}
rebase.finish(None).map_err(convert_libgit2_error)?;
unstash()?;
Ok(None)
}
}
@@ -456,6 +496,35 @@ impl Repo {
}
}
pub fn stash(&self) -> Result<(), String> {
let head_branch = self.head_branch()?;
let head = head_branch.commit()?;
let author = head.author();
// This is honestly quite horrible. The problem is that all stash operations expect a
// mutable reference (as they, well, mutate the repo after all). But we are heavily using
// immutable references a lot with this struct. I'm really not sure how to best solve this.
// Right now, we just open the repo AGAIN. It is safe, as we are only accessing the stash
// with the second reference, so there are no cross effects. But it just smells. Also,
// using `unwrap()` here as we are already sure that the repo is openable(?).
let mut repo = Repo::open(self.0.path(), false).unwrap();
repo.0
.stash_save2(&author, None, Some(git2::StashFlags::INCLUDE_UNTRACKED))
.map_err(convert_libgit2_error)?;
Ok(())
}
pub fn stash_pop(&self) -> Result<(), String> {
let mut repo = Repo::open(self.0.path(), false).unwrap();
repo.0
.stash_pop(
0,
Some(git2::StashApplyOptions::new().reinstantiate_index()),
)
.map_err(convert_libgit2_error)?;
Ok(())
}
pub fn rename_remote(&self, remote: &RemoteHandle, new_name: &str) -> Result<(), String> {
let failed_refspecs = self
.0
@@ -1253,6 +1322,10 @@ impl Commit<'_> {
pub fn id(&self) -> Oid {
Oid(self.0.id())
}
pub(self) fn author(&self) -> git2::Signature {
self.0.author()
}
}
impl<'a> Branch<'a> {
@@ -1323,9 +1396,7 @@ fn get_remote_callbacks() -> git2::RemoteCallbacks<'static> {
Some(username) => username,
None => panic!("Could not get username. This is a bug"),
};
git2::Cred::ssh_key_from_agent(username).or_else(|_| {
git2::Cred::ssh_key(username, None, &crate::env_home().join(".ssh/id_rsa"), None)
})
git2::Cred::ssh_key_from_agent(username)
});
callbacks
@@ -1436,8 +1507,11 @@ pub fn clone_repo(
repo.rename_remote(&origin, &remote.name)?;
}
let mut active_branch = repo.head_branch()?;
active_branch.set_upstream(&remote.name, &active_branch.name()?)?;
// If there is no head_branch, we most likely cloned an empty repository and
// there is no point in setting any upstreams.
if let Ok(mut active_branch) = repo.head_branch() {
active_branch.set_upstream(&remote.name, &active_branch.name()?)?;
};
Ok(())
}