├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yaml └── workflows │ ├── build.yml │ ├── build_and_publish.yml │ └── security_audit.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── root ├── app │ └── sync.sh └── etc │ ├── cont-init.d │ ├── 10-adduser.sh │ └── 20-cron.sh │ └── services.d │ └── cron │ └── run └── src ├── args.rs ├── config.rs ├── github.rs ├── github_repos.graphql ├── github_schema.graphql └── main.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | # Cargo 2 | /target 3 | 4 | # IntelliJ IDEA 5 | /.idea 6 | *.iml 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "cargo" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | 14 | - package-ecosystem: "docker" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - '**' 8 | - '!trunk' 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - run: docker build . 17 | 18 | cargo: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - run: docker build . 24 | - uses: actions-rs/toolchain@v1.0.7 25 | with: 26 | toolchain: stable 27 | - uses: actions-rs/cargo@v1.0.3 28 | with: 29 | command: publish 30 | args: --dry-run 31 | -------------------------------------------------------------------------------- /.github/workflows/build_and_publish.yml: -------------------------------------------------------------------------------- 1 | name: build and publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - trunk 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: crazy-max/ghaction-docker-meta@v1 18 | id: docker_meta 19 | with: 20 | images: | 21 | jakewharton/gitout 22 | ghcr.io/jakewharton/gitout 23 | tag-semver: | 24 | {{version}} 25 | {{major}} 26 | {{major}}.{{minor}} 27 | 28 | - uses: docker/login-action@v2.1.0 29 | with: 30 | username: jakewharton 31 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 32 | 33 | - run: echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin 34 | 35 | - uses: docker/build-push-action@v4.0.0 36 | with: 37 | push: true 38 | tags: ${{ steps.docker_meta.outputs.tags }} 39 | labels: ${{ steps.docker_meta.outputs.labels }} 40 | 41 | - uses: actions-rs/toolchain@v1.0.7 42 | with: 43 | toolchain: stable 44 | 45 | - uses: actions-rs/cargo@v1.0.3 46 | if: "!startsWith(github.event.ref, 'refs/tags')" 47 | with: 48 | command: publish 49 | args: --dry-run 50 | 51 | - uses: actions-rs/cargo@v1.0.3 52 | if: "startsWith(github.event.ref, 'refs/tags')" 53 | env: 54 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 55 | with: 56 | command: publish 57 | -------------------------------------------------------------------------------- /.github/workflows/security_audit.yml: -------------------------------------------------------------------------------- 1 | name: security audit 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | audit: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions-rs/audit-check@v1.2.0 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo 2 | /target 3 | 4 | # IntelliJ IDEA 5 | /.idea 6 | *.iml 7 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | tab_spaces = 2 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 0.2.0 5 | ------------- 6 | 7 | *2020-05-23* 8 | 9 | * New: Output will now only print repositories which have updates. A counter is included to display overall progress. 10 | * Fix: Do not ping healthcheck if command fails in Docker container. 11 | 12 | 13 | Version 0.1.0 14 | ------------- 15 | 16 | *2020-05-19* 17 | 18 | Initial release. 19 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.11.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "ascii" 16 | version = "0.9.3" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.0.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 36 | 37 | [[package]] 38 | name = "base64" 39 | version = "0.13.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 42 | 43 | [[package]] 44 | name = "bitflags" 45 | version = "1.2.1" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 48 | 49 | [[package]] 50 | name = "bumpalo" 51 | version = "3.2.1" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187" 54 | 55 | [[package]] 56 | name = "byteorder" 57 | version = "1.3.4" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 60 | 61 | [[package]] 62 | name = "bytes" 63 | version = "0.5.4" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" 66 | 67 | [[package]] 68 | name = "bytes" 69 | version = "1.0.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" 72 | 73 | [[package]] 74 | name = "cc" 75 | version = "1.0.50" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 78 | dependencies = [ 79 | "jobserver", 80 | ] 81 | 82 | [[package]] 83 | name = "cfg-if" 84 | version = "0.1.10" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 87 | 88 | [[package]] 89 | name = "cfg-if" 90 | version = "1.0.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 93 | 94 | [[package]] 95 | name = "clap" 96 | version = "2.33.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 99 | dependencies = [ 100 | "ansi_term", 101 | "atty", 102 | "bitflags", 103 | "strsim", 104 | "textwrap", 105 | "unicode-width", 106 | "vec_map", 107 | ] 108 | 109 | [[package]] 110 | name = "combine" 111 | version = "3.8.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" 114 | dependencies = [ 115 | "ascii", 116 | "byteorder", 117 | "either", 118 | "memchr", 119 | "unreachable", 120 | ] 121 | 122 | [[package]] 123 | name = "core-foundation" 124 | version = "0.9.1" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" 127 | dependencies = [ 128 | "core-foundation-sys", 129 | "libc", 130 | ] 131 | 132 | [[package]] 133 | name = "core-foundation-sys" 134 | version = "0.8.2" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" 137 | 138 | [[package]] 139 | name = "either" 140 | version = "1.5.3" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 143 | 144 | [[package]] 145 | name = "encoding_rs" 146 | version = "0.8.22" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28" 149 | dependencies = [ 150 | "cfg-if 0.1.10", 151 | ] 152 | 153 | [[package]] 154 | name = "fnv" 155 | version = "1.0.6" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 158 | 159 | [[package]] 160 | name = "foreign-types" 161 | version = "0.3.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 164 | dependencies = [ 165 | "foreign-types-shared", 166 | ] 167 | 168 | [[package]] 169 | name = "foreign-types-shared" 170 | version = "0.1.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 173 | 174 | [[package]] 175 | name = "form_urlencoded" 176 | version = "1.0.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" 179 | dependencies = [ 180 | "matches", 181 | "percent-encoding", 182 | ] 183 | 184 | [[package]] 185 | name = "futures-channel" 186 | version = "0.3.4" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" 189 | dependencies = [ 190 | "futures-core", 191 | ] 192 | 193 | [[package]] 194 | name = "futures-core" 195 | version = "0.3.4" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" 198 | 199 | [[package]] 200 | name = "futures-io" 201 | version = "0.3.4" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" 204 | 205 | [[package]] 206 | name = "futures-sink" 207 | version = "0.3.4" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" 210 | 211 | [[package]] 212 | name = "futures-task" 213 | version = "0.3.4" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" 216 | 217 | [[package]] 218 | name = "futures-util" 219 | version = "0.3.4" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" 222 | dependencies = [ 223 | "futures-core", 224 | "futures-io", 225 | "futures-task", 226 | "memchr", 227 | "pin-utils", 228 | "slab", 229 | ] 230 | 231 | [[package]] 232 | name = "getrandom" 233 | version = "0.1.14" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 236 | dependencies = [ 237 | "cfg-if 0.1.10", 238 | "libc", 239 | "wasi", 240 | ] 241 | 242 | [[package]] 243 | name = "git2" 244 | version = "0.15.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" 247 | dependencies = [ 248 | "bitflags", 249 | "libc", 250 | "libgit2-sys", 251 | "log", 252 | "openssl-probe", 253 | "openssl-sys", 254 | "url", 255 | ] 256 | 257 | [[package]] 258 | name = "gitout" 259 | version = "0.2.0" 260 | dependencies = [ 261 | "git2", 262 | "graphql_client", 263 | "reqwest", 264 | "serde", 265 | "serde_json", 266 | "structopt", 267 | "toml", 268 | ] 269 | 270 | [[package]] 271 | name = "graphql-introspection-query" 272 | version = "0.2.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "7f2a4732cf5140bd6c082434494f785a19cfb566ab07d1382c3671f5812fed6d" 275 | dependencies = [ 276 | "serde", 277 | ] 278 | 279 | [[package]] 280 | name = "graphql-parser" 281 | version = "0.4.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" 284 | dependencies = [ 285 | "combine", 286 | "thiserror", 287 | ] 288 | 289 | [[package]] 290 | name = "graphql_client" 291 | version = "0.11.0" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "7fc16d75d169fddb720d8f1c7aed6413e329e1584079b9734ff07266a193f5bc" 294 | dependencies = [ 295 | "graphql_query_derive", 296 | "serde", 297 | "serde_json", 298 | ] 299 | 300 | [[package]] 301 | name = "graphql_client_codegen" 302 | version = "0.11.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "f290ecfa3bea3e8a157899dc8a1d96ee7dd6405c18c8ddd213fc58939d18a0e9" 305 | dependencies = [ 306 | "graphql-introspection-query", 307 | "graphql-parser", 308 | "heck 0.4.0", 309 | "lazy_static", 310 | "proc-macro2", 311 | "quote", 312 | "serde", 313 | "serde_json", 314 | "syn", 315 | ] 316 | 317 | [[package]] 318 | name = "graphql_query_derive" 319 | version = "0.11.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "a755cc59cda2641ea3037b4f9f7ef40471c329f55c1fa2db6fa0bb7ae6c1f7ce" 322 | dependencies = [ 323 | "graphql_client_codegen", 324 | "proc-macro2", 325 | "syn", 326 | ] 327 | 328 | [[package]] 329 | name = "h2" 330 | version = "0.3.10" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" 333 | dependencies = [ 334 | "bytes 1.0.1", 335 | "fnv", 336 | "futures-core", 337 | "futures-sink", 338 | "futures-util", 339 | "http", 340 | "indexmap", 341 | "slab", 342 | "tokio", 343 | "tokio-util", 344 | "tracing", 345 | ] 346 | 347 | [[package]] 348 | name = "hashbrown" 349 | version = "0.9.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" 352 | 353 | [[package]] 354 | name = "heck" 355 | version = "0.3.1" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 358 | dependencies = [ 359 | "unicode-segmentation", 360 | ] 361 | 362 | [[package]] 363 | name = "heck" 364 | version = "0.4.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 367 | 368 | [[package]] 369 | name = "hermit-abi" 370 | version = "0.1.8" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" 373 | dependencies = [ 374 | "libc", 375 | ] 376 | 377 | [[package]] 378 | name = "http" 379 | version = "0.2.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" 382 | dependencies = [ 383 | "bytes 0.5.4", 384 | "fnv", 385 | "itoa 0.4.5", 386 | ] 387 | 388 | [[package]] 389 | name = "http-body" 390 | version = "0.4.0" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" 393 | dependencies = [ 394 | "bytes 1.0.1", 395 | "http", 396 | ] 397 | 398 | [[package]] 399 | name = "httparse" 400 | version = "1.7.1" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" 403 | 404 | [[package]] 405 | name = "httpdate" 406 | version = "1.0.2" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 409 | 410 | [[package]] 411 | name = "hyper" 412 | version = "0.14.19" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" 415 | dependencies = [ 416 | "bytes 1.0.1", 417 | "futures-channel", 418 | "futures-core", 419 | "futures-util", 420 | "h2", 421 | "http", 422 | "http-body", 423 | "httparse", 424 | "httpdate", 425 | "itoa 1.0.1", 426 | "pin-project-lite", 427 | "socket2 0.4.2", 428 | "tokio", 429 | "tower-service", 430 | "tracing", 431 | "want", 432 | ] 433 | 434 | [[package]] 435 | name = "hyper-tls" 436 | version = "0.5.0" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 439 | dependencies = [ 440 | "bytes 1.0.1", 441 | "hyper", 442 | "native-tls", 443 | "tokio", 444 | "tokio-native-tls", 445 | ] 446 | 447 | [[package]] 448 | name = "idna" 449 | version = "0.2.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" 452 | dependencies = [ 453 | "matches", 454 | "unicode-bidi", 455 | "unicode-normalization", 456 | ] 457 | 458 | [[package]] 459 | name = "indexmap" 460 | version = "1.6.2" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" 463 | dependencies = [ 464 | "autocfg", 465 | "hashbrown", 466 | ] 467 | 468 | [[package]] 469 | name = "ipnet" 470 | version = "2.3.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" 473 | 474 | [[package]] 475 | name = "itoa" 476 | version = "0.4.5" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 479 | 480 | [[package]] 481 | name = "itoa" 482 | version = "1.0.1" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 485 | 486 | [[package]] 487 | name = "jobserver" 488 | version = "0.1.21" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" 491 | dependencies = [ 492 | "libc", 493 | ] 494 | 495 | [[package]] 496 | name = "js-sys" 497 | version = "0.3.45" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" 500 | dependencies = [ 501 | "wasm-bindgen", 502 | ] 503 | 504 | [[package]] 505 | name = "lazy_static" 506 | version = "1.4.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 509 | 510 | [[package]] 511 | name = "libc" 512 | version = "0.2.112" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" 515 | 516 | [[package]] 517 | name = "libgit2-sys" 518 | version = "0.14.0+1.5.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" 521 | dependencies = [ 522 | "cc", 523 | "libc", 524 | "libssh2-sys", 525 | "libz-sys", 526 | "openssl-sys", 527 | "pkg-config", 528 | ] 529 | 530 | [[package]] 531 | name = "libssh2-sys" 532 | version = "0.2.19" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "ca46220853ba1c512fc82826d0834d87b06bcd3c2a42241b7de72f3d2fe17056" 535 | dependencies = [ 536 | "cc", 537 | "libc", 538 | "libz-sys", 539 | "openssl-sys", 540 | "pkg-config", 541 | "vcpkg", 542 | ] 543 | 544 | [[package]] 545 | name = "libz-sys" 546 | version = "1.1.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "af67924b8dd885cccea261866c8ce5b74d239d272e154053ff927dae839f5ae9" 549 | dependencies = [ 550 | "cc", 551 | "libc", 552 | "pkg-config", 553 | "vcpkg", 554 | ] 555 | 556 | [[package]] 557 | name = "log" 558 | version = "0.4.8" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 561 | dependencies = [ 562 | "cfg-if 0.1.10", 563 | ] 564 | 565 | [[package]] 566 | name = "matches" 567 | version = "0.1.8" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 570 | 571 | [[package]] 572 | name = "memchr" 573 | version = "2.3.3" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 576 | 577 | [[package]] 578 | name = "mime" 579 | version = "0.3.16" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 582 | 583 | [[package]] 584 | name = "mio" 585 | version = "0.7.9" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "a5dede4e2065b3842b8b0af444119f3aa331cc7cc2dd20388bfb0f5d5a38823a" 588 | dependencies = [ 589 | "libc", 590 | "log", 591 | "miow", 592 | "ntapi", 593 | "winapi", 594 | ] 595 | 596 | [[package]] 597 | name = "miow" 598 | version = "0.3.6" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" 601 | dependencies = [ 602 | "socket2 0.3.19", 603 | "winapi", 604 | ] 605 | 606 | [[package]] 607 | name = "native-tls" 608 | version = "0.2.11" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 611 | dependencies = [ 612 | "lazy_static", 613 | "libc", 614 | "log", 615 | "openssl", 616 | "openssl-probe", 617 | "openssl-sys", 618 | "schannel", 619 | "security-framework", 620 | "security-framework-sys", 621 | "tempfile", 622 | ] 623 | 624 | [[package]] 625 | name = "ntapi" 626 | version = "0.3.4" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "7a31937dea023539c72ddae0e3571deadc1414b300483fa7aaec176168cfa9d2" 629 | dependencies = [ 630 | "winapi", 631 | ] 632 | 633 | [[package]] 634 | name = "num_cpus" 635 | version = "1.12.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" 638 | dependencies = [ 639 | "hermit-abi", 640 | "libc", 641 | ] 642 | 643 | [[package]] 644 | name = "once_cell" 645 | version = "1.7.2" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 648 | 649 | [[package]] 650 | name = "openssl" 651 | version = "0.10.33" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577" 654 | dependencies = [ 655 | "bitflags", 656 | "cfg-if 1.0.0", 657 | "foreign-types", 658 | "libc", 659 | "once_cell", 660 | "openssl-sys", 661 | ] 662 | 663 | [[package]] 664 | name = "openssl-probe" 665 | version = "0.1.2" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 668 | 669 | [[package]] 670 | name = "openssl-sys" 671 | version = "0.9.61" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f" 674 | dependencies = [ 675 | "autocfg", 676 | "cc", 677 | "libc", 678 | "pkg-config", 679 | "vcpkg", 680 | ] 681 | 682 | [[package]] 683 | name = "percent-encoding" 684 | version = "2.1.0" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 687 | 688 | [[package]] 689 | name = "pin-project-lite" 690 | version = "0.2.9" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 693 | 694 | [[package]] 695 | name = "pin-utils" 696 | version = "0.1.0-alpha.4" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" 699 | 700 | [[package]] 701 | name = "pkg-config" 702 | version = "0.3.17" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" 705 | 706 | [[package]] 707 | name = "ppv-lite86" 708 | version = "0.2.6" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 711 | 712 | [[package]] 713 | name = "proc-macro-error" 714 | version = "1.0.2" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" 717 | dependencies = [ 718 | "proc-macro-error-attr", 719 | "proc-macro2", 720 | "quote", 721 | "syn", 722 | "version_check", 723 | ] 724 | 725 | [[package]] 726 | name = "proc-macro-error-attr" 727 | version = "1.0.2" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" 730 | dependencies = [ 731 | "proc-macro2", 732 | "quote", 733 | "syn", 734 | "syn-mid", 735 | "version_check", 736 | ] 737 | 738 | [[package]] 739 | name = "proc-macro2" 740 | version = "1.0.36" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 743 | dependencies = [ 744 | "unicode-xid", 745 | ] 746 | 747 | [[package]] 748 | name = "quote" 749 | version = "1.0.3" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" 752 | dependencies = [ 753 | "proc-macro2", 754 | ] 755 | 756 | [[package]] 757 | name = "rand" 758 | version = "0.7.3" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 761 | dependencies = [ 762 | "getrandom", 763 | "libc", 764 | "rand_chacha", 765 | "rand_core", 766 | "rand_hc", 767 | ] 768 | 769 | [[package]] 770 | name = "rand_chacha" 771 | version = "0.2.2" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 774 | dependencies = [ 775 | "ppv-lite86", 776 | "rand_core", 777 | ] 778 | 779 | [[package]] 780 | name = "rand_core" 781 | version = "0.5.1" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 784 | dependencies = [ 785 | "getrandom", 786 | ] 787 | 788 | [[package]] 789 | name = "rand_hc" 790 | version = "0.2.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 793 | dependencies = [ 794 | "rand_core", 795 | ] 796 | 797 | [[package]] 798 | name = "redox_syscall" 799 | version = "0.1.56" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 802 | 803 | [[package]] 804 | name = "remove_dir_all" 805 | version = "0.5.2" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" 808 | dependencies = [ 809 | "winapi", 810 | ] 811 | 812 | [[package]] 813 | name = "reqwest" 814 | version = "0.11.13" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" 817 | dependencies = [ 818 | "base64", 819 | "bytes 1.0.1", 820 | "encoding_rs", 821 | "futures-core", 822 | "futures-util", 823 | "h2", 824 | "http", 825 | "http-body", 826 | "hyper", 827 | "hyper-tls", 828 | "ipnet", 829 | "js-sys", 830 | "log", 831 | "mime", 832 | "native-tls", 833 | "once_cell", 834 | "percent-encoding", 835 | "pin-project-lite", 836 | "serde", 837 | "serde_json", 838 | "serde_urlencoded", 839 | "tokio", 840 | "tokio-native-tls", 841 | "tower-service", 842 | "url", 843 | "wasm-bindgen", 844 | "wasm-bindgen-futures", 845 | "web-sys", 846 | "winreg", 847 | ] 848 | 849 | [[package]] 850 | name = "ryu" 851 | version = "1.0.3" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" 854 | 855 | [[package]] 856 | name = "schannel" 857 | version = "0.1.18" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "039c25b130bd8c1321ee2d7de7fde2659fa9c2744e4bb29711cfc852ea53cd19" 860 | dependencies = [ 861 | "lazy_static", 862 | "winapi", 863 | ] 864 | 865 | [[package]] 866 | name = "security-framework" 867 | version = "2.1.2" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" 870 | dependencies = [ 871 | "bitflags", 872 | "core-foundation", 873 | "core-foundation-sys", 874 | "libc", 875 | "security-framework-sys", 876 | ] 877 | 878 | [[package]] 879 | name = "security-framework-sys" 880 | version = "2.1.1" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d" 883 | dependencies = [ 884 | "core-foundation-sys", 885 | "libc", 886 | ] 887 | 888 | [[package]] 889 | name = "serde" 890 | version = "1.0.147" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" 893 | dependencies = [ 894 | "serde_derive", 895 | ] 896 | 897 | [[package]] 898 | name = "serde_derive" 899 | version = "1.0.147" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" 902 | dependencies = [ 903 | "proc-macro2", 904 | "quote", 905 | "syn", 906 | ] 907 | 908 | [[package]] 909 | name = "serde_json" 910 | version = "1.0.87" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" 913 | dependencies = [ 914 | "itoa 1.0.1", 915 | "ryu", 916 | "serde", 917 | ] 918 | 919 | [[package]] 920 | name = "serde_urlencoded" 921 | version = "0.7.1" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 924 | dependencies = [ 925 | "form_urlencoded", 926 | "itoa 1.0.1", 927 | "ryu", 928 | "serde", 929 | ] 930 | 931 | [[package]] 932 | name = "slab" 933 | version = "0.4.2" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 936 | 937 | [[package]] 938 | name = "smallvec" 939 | version = "1.2.0" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" 942 | 943 | [[package]] 944 | name = "socket2" 945 | version = "0.3.19" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" 948 | dependencies = [ 949 | "cfg-if 1.0.0", 950 | "libc", 951 | "winapi", 952 | ] 953 | 954 | [[package]] 955 | name = "socket2" 956 | version = "0.4.2" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" 959 | dependencies = [ 960 | "libc", 961 | "winapi", 962 | ] 963 | 964 | [[package]] 965 | name = "strsim" 966 | version = "0.8.0" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 969 | 970 | [[package]] 971 | name = "structopt" 972 | version = "0.3.26" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 975 | dependencies = [ 976 | "clap", 977 | "lazy_static", 978 | "structopt-derive", 979 | ] 980 | 981 | [[package]] 982 | name = "structopt-derive" 983 | version = "0.4.18" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 986 | dependencies = [ 987 | "heck 0.3.1", 988 | "proc-macro-error", 989 | "proc-macro2", 990 | "quote", 991 | "syn", 992 | ] 993 | 994 | [[package]] 995 | name = "syn" 996 | version = "1.0.92" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" 999 | dependencies = [ 1000 | "proc-macro2", 1001 | "quote", 1002 | "unicode-xid", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "syn-mid" 1007 | version = "0.5.0" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" 1010 | dependencies = [ 1011 | "proc-macro2", 1012 | "quote", 1013 | "syn", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "tempfile" 1018 | version = "3.1.0" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 1021 | dependencies = [ 1022 | "cfg-if 0.1.10", 1023 | "libc", 1024 | "rand", 1025 | "redox_syscall", 1026 | "remove_dir_all", 1027 | "winapi", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "textwrap" 1032 | version = "0.11.0" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1035 | dependencies = [ 1036 | "unicode-width", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "thiserror" 1041 | version = "1.0.31" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 1044 | dependencies = [ 1045 | "thiserror-impl", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "thiserror-impl" 1050 | version = "1.0.31" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 1053 | dependencies = [ 1054 | "proc-macro2", 1055 | "quote", 1056 | "syn", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "tokio" 1061 | version = "1.3.0" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "8d56477f6ed99e10225f38f9f75f872f29b8b8bd8c0b946f63345bb144e9eeda" 1064 | dependencies = [ 1065 | "autocfg", 1066 | "bytes 1.0.1", 1067 | "libc", 1068 | "memchr", 1069 | "mio", 1070 | "num_cpus", 1071 | "pin-project-lite", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "tokio-native-tls" 1076 | version = "0.3.0" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 1079 | dependencies = [ 1080 | "native-tls", 1081 | "tokio", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "tokio-util" 1086 | version = "0.6.4" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "ec31e5cc6b46e653cf57762f36f71d5e6386391d88a72fd6db4508f8f676fb29" 1089 | dependencies = [ 1090 | "bytes 1.0.1", 1091 | "futures-core", 1092 | "futures-sink", 1093 | "log", 1094 | "pin-project-lite", 1095 | "tokio", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "toml" 1100 | version = "0.5.9" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 1103 | dependencies = [ 1104 | "serde", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "tower-service" 1109 | version = "0.3.0" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" 1112 | 1113 | [[package]] 1114 | name = "tracing" 1115 | version = "0.1.25" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" 1118 | dependencies = [ 1119 | "cfg-if 1.0.0", 1120 | "pin-project-lite", 1121 | "tracing-core", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "tracing-core" 1126 | version = "0.1.17" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" 1129 | dependencies = [ 1130 | "lazy_static", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "try-lock" 1135 | version = "0.2.2" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" 1138 | 1139 | [[package]] 1140 | name = "unicode-bidi" 1141 | version = "0.3.4" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 1144 | dependencies = [ 1145 | "matches", 1146 | ] 1147 | 1148 | [[package]] 1149 | name = "unicode-normalization" 1150 | version = "0.1.12" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" 1153 | dependencies = [ 1154 | "smallvec", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "unicode-segmentation" 1159 | version = "1.6.0" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 1162 | 1163 | [[package]] 1164 | name = "unicode-width" 1165 | version = "0.1.7" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 1168 | 1169 | [[package]] 1170 | name = "unicode-xid" 1171 | version = "0.2.0" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 1174 | 1175 | [[package]] 1176 | name = "unreachable" 1177 | version = "1.0.0" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" 1180 | dependencies = [ 1181 | "void", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "url" 1186 | version = "2.2.0" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" 1189 | dependencies = [ 1190 | "form_urlencoded", 1191 | "idna", 1192 | "matches", 1193 | "percent-encoding", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "vcpkg" 1198 | version = "0.2.8" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" 1201 | 1202 | [[package]] 1203 | name = "vec_map" 1204 | version = "0.8.1" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 1207 | 1208 | [[package]] 1209 | name = "version_check" 1210 | version = "0.9.1" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" 1213 | 1214 | [[package]] 1215 | name = "void" 1216 | version = "1.0.2" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 1219 | 1220 | [[package]] 1221 | name = "want" 1222 | version = "0.3.0" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1225 | dependencies = [ 1226 | "log", 1227 | "try-lock", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "wasi" 1232 | version = "0.9.0+wasi-snapshot-preview1" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 1235 | 1236 | [[package]] 1237 | name = "wasm-bindgen" 1238 | version = "0.2.68" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" 1241 | dependencies = [ 1242 | "cfg-if 0.1.10", 1243 | "wasm-bindgen-macro", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "wasm-bindgen-backend" 1248 | version = "0.2.68" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" 1251 | dependencies = [ 1252 | "bumpalo", 1253 | "lazy_static", 1254 | "log", 1255 | "proc-macro2", 1256 | "quote", 1257 | "syn", 1258 | "wasm-bindgen-shared", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "wasm-bindgen-futures" 1263 | version = "0.4.18" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da" 1266 | dependencies = [ 1267 | "cfg-if 0.1.10", 1268 | "js-sys", 1269 | "wasm-bindgen", 1270 | "web-sys", 1271 | ] 1272 | 1273 | [[package]] 1274 | name = "wasm-bindgen-macro" 1275 | version = "0.2.68" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" 1278 | dependencies = [ 1279 | "quote", 1280 | "wasm-bindgen-macro-support", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "wasm-bindgen-macro-support" 1285 | version = "0.2.68" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" 1288 | dependencies = [ 1289 | "proc-macro2", 1290 | "quote", 1291 | "syn", 1292 | "wasm-bindgen-backend", 1293 | "wasm-bindgen-shared", 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "wasm-bindgen-shared" 1298 | version = "0.2.68" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" 1301 | 1302 | [[package]] 1303 | name = "web-sys" 1304 | version = "0.3.37" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb" 1307 | dependencies = [ 1308 | "js-sys", 1309 | "wasm-bindgen", 1310 | ] 1311 | 1312 | [[package]] 1313 | name = "winapi" 1314 | version = "0.3.9" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1317 | dependencies = [ 1318 | "winapi-i686-pc-windows-gnu", 1319 | "winapi-x86_64-pc-windows-gnu", 1320 | ] 1321 | 1322 | [[package]] 1323 | name = "winapi-i686-pc-windows-gnu" 1324 | version = "0.4.0" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1327 | 1328 | [[package]] 1329 | name = "winapi-x86_64-pc-windows-gnu" 1330 | version = "0.4.0" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1333 | 1334 | [[package]] 1335 | name = "winreg" 1336 | version = "0.10.1" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 1339 | dependencies = [ 1340 | "winapi", 1341 | ] 1342 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitout" 3 | version = "0.2.0" 4 | authors = ["Jake Wharton "] 5 | edition = "2018" 6 | description = "A command-line tool to automatically backup Git repositories from GitHub or anywhere" 7 | readme = "README.md" 8 | repository = "https://github.com/JakeWharton/gitout/" 9 | license = "MIT" 10 | keywords = ["git", "github", "backup"] 11 | categories = ["command-line-utilities"] 12 | exclude = [".github/**"] 13 | 14 | [dependencies] 15 | git2 = "0.15" 16 | reqwest = { version = "0.11", features = ["blocking", "json"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | structopt = "0.3" 20 | toml = "0.5" 21 | graphql_client = "0.11" 22 | 23 | [profile.release] 24 | codegen-units = 1 25 | lto = true 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Cross-compile the app for musl to create a statically-linked binary for alpine. 2 | FROM rust:1.65.0 AS rust 3 | RUN rustup component add clippy rustfmt 4 | WORKDIR /app 5 | COPY Cargo.toml Cargo.lock .rustfmt.toml ./ 6 | COPY src ./src 7 | RUN cargo build --release 8 | RUN cargo clippy 9 | RUN cargo test 10 | RUN cargo fmt -- --check 11 | 12 | 13 | FROM golang:1.18-alpine AS shell 14 | RUN apk add --no-cache shellcheck 15 | ENV GO111MODULE=on 16 | RUN go install mvdan.cc/sh/v3/cmd/shfmt@latest 17 | WORKDIR /overlay 18 | COPY root/ ./ 19 | COPY .editorconfig / 20 | RUN find . -type f | xargs shellcheck -e SC1008 21 | RUN shfmt -d . 22 | 23 | 24 | FROM debian:buster-slim 25 | ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.1/s6-overlay-amd64-installer /tmp/ 26 | RUN chmod +x /tmp/s6-overlay-amd64-installer && /tmp/s6-overlay-amd64-installer / 27 | ENV \ 28 | # Fail if cont-init scripts exit with non-zero code. 29 | S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \ 30 | # Show full backtraces for crashes. 31 | RUST_BACKTRACE=full \ 32 | CRON="" \ 33 | HEALTHCHECK_ID="" \ 34 | HEALTHCHECK_HOST="https://hc-ping.com" \ 35 | PUID="" \ 36 | PGID="" \ 37 | GITOUT_ARGS="" 38 | RUN apt-get update && \ 39 | apt-get install -y --no-install-recommends \ 40 | curl \ 41 | && \ 42 | rm -rf /var/lib/apt/lists/* 43 | COPY root/ / 44 | WORKDIR /app 45 | COPY --from=rust /app/target/release/gitout ./ 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Jake Wharton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Git Out 2 | ======= 3 | 4 | A command-line tool to automatically backup Git repositories from GitHub or anywhere. 5 | 6 | The `gitout` tool will clone git repos from GitHub or any other git hosting service. 7 | If the repository was already cloned, it will fetch any updates to keep your local copy in sync. 8 | 9 | When you add your GitHub username and a token, `gitout` will discover all of your owned repositories and synchronize them automatically. 10 | You can opt-in to having repositories that you've starred or watched synchronized as well. 11 | 12 | The cloned repositories are [bare](https://www.saintsjd.com/2011/01/what-is-a-bare-git-repository/). 13 | In other words, there is no working copy of the files for you to interact with. 14 | If you need access to the files, you can `git clone /path/to/bare/repo`. 15 | 16 | 17 | Installation 18 | ------------ 19 | 20 | ### Rust 21 | 22 | If you have Rust installed you can install the binary by running `cargo install gitout`. 23 | 24 | [![Latest version](https://img.shields.io/crates/v/gitout.svg)](https://crates.io/crates/gitout) 25 | 26 | ### Docker 27 | 28 | The binary is available inside the `jakewharton/gitout` Docker container which can run it as a cron job. 29 | 30 | [![Docker Image Version](https://img.shields.io/docker/v/jakewharton/gitout?sort=semver)][hub] 31 | [![Docker Image Size](https://img.shields.io/docker/image-size/jakewharton/gitout)][layers] 32 | 33 | [hub]: https://hub.docker.com/r/jakewharton/gitout/ 34 | [layers]: https://microbadger.com/images/jakewharton/gitout 35 | 36 | Mount a `/data` volume which is where the repositories will be stored. 37 | Mount the `/config` folder which contains a `config.toml` or mount a `/config/config.toml` file directly. 38 | Specify a `CRON` environment variable with a cron specifier dictating the schedule for when the tool should run. 39 | 40 | ``` 41 | $ docker run -d \ 42 | -v /path/to/data:/data \ 43 | -v /path/to/config.toml:/config/config.toml \ 44 | -e "CRON=0 * * * *" \ 45 | jakewharton/gitout 46 | ``` 47 | 48 | For help creating a valid cron specifier, visit [cron.help](https://cron.help/#0_*_*_*_*). 49 | 50 | To be notified when sync is failing visit https://healthchecks.io, create a check, and specify the ID to the container using the `HEALTHCHECK_ID` environment variable (for example, `-e "HEALTHCHECK_ID=..."`). 51 | 52 | To write data as a particular user, the `PUID` and `PGID` environment variables can be set to your user ID and group ID, respectively. 53 | 54 | If you're using Docker Compose, an example setup looks like; 55 | ```yaml 56 | services: 57 | gitout: 58 | image: jakewharton/gitout:latest 59 | restart: unless-stopped 60 | volumes: 61 | - /path/to/data:/data 62 | - /path/to/config:/config 63 | environment: 64 | - "CRON=0 * * * *" 65 | #Optional: 66 | - "HEALTHCHECK_ID=..." 67 | - "PUID=..." 68 | - "PGID=..." 69 | ``` 70 | 71 | Note: You may want to specify an explicit version rather than `latest`. 72 | See https://hub.docker.com/r/jakewharton/gitout/tags or `CHANGELOG.md` for the available versions. 73 | 74 | ### Binaries 75 | 76 | TODO GitHub releases download binaries https://github.com/JakeWharton/gitout/issues/8 77 | 78 | 79 | Usage 80 | ----- 81 | 82 | ``` 83 | $ gitout --help 84 | gitout 0.1.0 85 | 86 | USAGE: 87 | gitout [FLAGS] 88 | 89 | FLAGS: 90 | --dry-run Print actions instead of performing them 91 | -h, --help Prints help information 92 | -V, --version Prints version information 93 | -v, --verbose Enable verbose logging 94 | 95 | ARGS: 96 | Configuration file 97 | Backup directory 98 | ``` 99 | 100 | 101 | Configuration specification 102 | --------------------------- 103 | 104 | Until version 1.0 of the tool, the TOML version is set to 0 and may change incompatibly between 0.x releases. 105 | You can find migration information in the `CHANGELOG.md` file. 106 | 107 | ```toml 108 | version = 0 109 | 110 | [github] 111 | user = "example" 112 | token = "abcd1234efgh5678ij90" 113 | 114 | [github.clone] 115 | starred = true # Optional, default false 116 | watched = true # Optional, default false 117 | # Extra repos to synchronize that are not owned, starred, or watched by you. 118 | repos = [ 119 | "JakeWharton/gitout", 120 | ] 121 | # Repos temporary or otherwise that you do not want to be synchronized. 122 | ignored = [ 123 | "JakeWharton/TestParameterInjector", 124 | ] 125 | 126 | # Repos not on GitHub to synchronize. 127 | [git.repos] 128 | asm = "https://gitlab.ow2.org/asm/asm.git" 129 | ``` 130 | 131 | ### Creating a GitHub token 132 | 133 | 1. Visit https://github.com/settings/tokens 134 | 2. Click "Generate new token" 135 | 3. Type "gitout" in the name field 136 | 4. Select the "repo", "gist", and "read:user" scopes 137 | - `repo`: Needed to discover and clone private repositories (if you only have public repositories then just `public_repo` will also work) 138 | - `gist`: Needed to discover and clone private gists (if you only have public gists then this is not required) 139 | - `read:user`: Needed to traverse your owned, starred, and watched repo lists 140 | 5. Select "Generate token" 141 | 6. Copy the value into your `config.toml` as it will not be shown again 142 | 143 | 144 | Development 145 | ----------- 146 | 147 | If you have Rust installed, a debug binary can be built with `cargo build` and a release binary with `cargo build --release`. 148 | The binary will be in `target/debug/gitout` or `target/release/gitout`, respectively. 149 | Run all the tests with `cargo test`. 150 | Format the code with `cargo fmt`. 151 | Run the Clippy tool with `cargo clippy`. 152 | 153 | If you have Docker but not Rust, run `docker build .` which will do everything. This is what runs on CI. 154 | 155 | 156 | LICENSE 157 | ====== 158 | 159 | MIT. See `LICENSE.txt`. 160 | 161 | Copyright 2020 Jake Wharton 162 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Release Process 2 | =============== 3 | 4 | 1. Update `Cargo.toml` with new version number. 5 | 6 | 2. Build and test 7 | 8 | ``` 9 | $ docker build . 10 | ``` 11 | 12 | 3. Update `CHANGELOG.md` with new version information. 13 | 14 | 4. Update `README.md` with any new information. 15 | 16 | 5. Commit version finalization. 17 | 18 | ``` 19 | $ git commit -am "Version X.Y.Z" 20 | ``` 21 | 22 | (Replacing X.Y.Z with the actual release version.) 23 | 24 | 6. Tag Git SHA. 25 | 26 | ``` 27 | $ git tag -a X.Y.Z -m 'Version X.Y.Z' 28 | ``` 29 | 30 | (Replacing X.Y.Z with the actual release version.) 31 | 32 | 7. Push commits and tag to GitHub. 33 | 34 | ``` 35 | $ git push 36 | $ git push --tags 37 | ``` 38 | 39 | This will release to Docker Hub and crates.io. 40 | -------------------------------------------------------------------------------- /root/app/sync.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | if [ -n "$HEALTHCHECK_ID" ]; then 4 | curl -sS -X POST -o /dev/null "$HEALTHCHECK_HOST/$HEALTHCHECK_ID/start" 5 | fi 6 | 7 | # If gitout fails we want to avoid triggering the health check. 8 | set -e 9 | 10 | # shellcheck disable=SC2086 11 | /app/gitout --no-archive $GITOUT_ARGS /config/config.toml /data 12 | 13 | if [ -n "$HEALTHCHECK_ID" ]; then 14 | curl -sS -X POST -o /dev/null --fail "$HEALTHCHECK_HOST/$HEALTHCHECK_ID" 15 | fi 16 | -------------------------------------------------------------------------------- /root/etc/cont-init.d/10-adduser.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | # 3 | # Copyright (c) 2017 Joshua Avalon 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | PUID=${PUID:-1001} 24 | PGID=${PGID:-1001} 25 | 26 | groupmod -o -g "$PGID" abc 27 | usermod -o -u "$PUID" abc 28 | 29 | echo " 30 | Initializing container 31 | 32 | User uid: $(id -u abc) 33 | User gid: $(id -g abc) 34 | " 35 | 36 | chown abc:abc /app 37 | -------------------------------------------------------------------------------- /root/etc/cont-init.d/20-cron.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | if [ -z "$CRON" ]; then 4 | echo " 5 | Not running in cron mode 6 | " 7 | exit 0 8 | fi 9 | 10 | if [ ! -d /data ]; then 11 | echo " 12 | ERROR: '/data' directory must be mounted 13 | " 14 | exit 1 15 | fi 16 | if [ ! -d /config ]; then 17 | echo " 18 | ERROR: '/config' directory must be mounted 19 | " 20 | exit 1 21 | fi 22 | if [ ! -f /config/config.toml ]; then 23 | echo " 24 | ERROR: '/config/config.toml' file must exist 25 | " 26 | exit 1 27 | fi 28 | 29 | if [ -z "$HEALTHCHECK_ID" ]; then 30 | echo " 31 | NOTE: Define HEALTHCHECK_ID with https://healthchecks.io to monitor sync job" 32 | fi 33 | 34 | # Set up the cron schedule. 35 | echo " 36 | Initializing cron 37 | 38 | $CRON 39 | " 40 | crontab -u abc -d # Delete any existing crontab. 41 | echo "$CRON /usr/bin/flock -n /app/sync.lock /app/sync.sh" >/tmp/crontab.tmp 42 | crontab -u abc /tmp/crontab.tmp 43 | rm /tmp/crontab.tmp 44 | -------------------------------------------------------------------------------- /root/etc/services.d/cron/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | # Log level 5 (and below) is noisy during periodic wakeup where nothing happens. 4 | /usr/sbin/crond -f -l 6 5 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use structopt::StructOpt; 4 | 5 | #[derive(Debug, PartialEq, StructOpt)] 6 | pub struct Args { 7 | /// Configuration file 8 | #[structopt(parse(from_os_str))] 9 | pub config: PathBuf, 10 | 11 | /// Backup directory 12 | #[structopt(parse(from_os_str))] 13 | pub destination: PathBuf, 14 | 15 | /// Enable verbose logging 16 | #[structopt(short, long)] 17 | pub verbose: bool, 18 | 19 | /// Enable experimental repository archiving 20 | #[structopt(long)] 21 | pub experimental_archive: bool, 22 | 23 | /// Print actions instead of performing them 24 | #[structopt(long)] 25 | pub dry_run: bool, 26 | } 27 | 28 | pub fn parse_args() -> Args { 29 | Args::from_args() 30 | } 31 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use toml::de::Error; 3 | use toml::value::Table; 4 | 5 | #[derive(Debug, Deserialize, PartialEq)] 6 | pub struct Config { 7 | pub version: u32, 8 | pub github: Option, 9 | pub git: Option, 10 | } 11 | 12 | #[derive(Debug, Deserialize, PartialEq)] 13 | pub struct GitHub { 14 | pub user: String, 15 | pub token: String, 16 | #[serde(default)] 17 | pub archive: GitHubArchive, 18 | #[serde(default)] 19 | pub clone: GitHubClone, 20 | } 21 | 22 | #[derive(Debug, Deserialize, PartialEq)] 23 | pub struct GitHubArchive { 24 | #[serde(default)] 25 | pub owned: bool, 26 | // #[serde(default)] 27 | // pub repos: Vec, 28 | } 29 | 30 | impl Default for GitHubArchive { 31 | fn default() -> Self { 32 | GitHubArchive { 33 | owned: true, 34 | // repos: vec![], 35 | } 36 | } 37 | } 38 | 39 | #[derive(Debug, Deserialize, PartialEq)] 40 | pub struct GitHubClone { 41 | #[serde(default)] 42 | pub starred: bool, 43 | #[serde(default)] 44 | pub watched: bool, 45 | #[serde(default)] 46 | pub gists: bool, 47 | #[serde(default)] 48 | pub repos: Vec, 49 | #[serde(default)] 50 | pub ignored: Vec, 51 | } 52 | 53 | impl Default for GitHubClone { 54 | fn default() -> Self { 55 | GitHubClone { 56 | starred: false, 57 | watched: false, 58 | gists: true, 59 | repos: vec![], 60 | ignored: vec![], 61 | } 62 | } 63 | } 64 | 65 | #[derive(Debug, Deserialize, PartialEq)] 66 | pub struct Git { 67 | #[serde(default)] 68 | pub repos: Table, 69 | } 70 | 71 | pub fn parse_config(s: &str) -> Result { 72 | toml::from_str(s) 73 | } 74 | 75 | #[cfg(test)] 76 | mod test { 77 | use super::*; 78 | use toml::value::Value; 79 | 80 | #[test] 81 | fn empty() { 82 | let actual = parse_config( 83 | r#" 84 | version = 0 85 | "#, 86 | ) 87 | .unwrap(); 88 | let expected = Config { 89 | version: 0, 90 | github: None, 91 | git: None, 92 | }; 93 | assert_eq!(actual, expected) 94 | } 95 | 96 | #[test] 97 | fn full() { 98 | let actual = parse_config( 99 | r#" 100 | version = 0 101 | 102 | [github] 103 | user = "user" 104 | token = "token" 105 | 106 | [github.archive] 107 | owned = false 108 | #repos = [ 109 | # "example/one" 110 | #] 111 | 112 | [github.clone] 113 | starred = true 114 | watched = true 115 | gists = false 116 | repos = [ 117 | "example/two", 118 | ] 119 | 120 | [git.repos] 121 | example = "https://example.com/example.git" 122 | "#, 123 | ) 124 | .unwrap(); 125 | let mut repos = Table::new(); 126 | repos.insert( 127 | "example".to_string(), 128 | Value::from("https://example.com/example.git"), 129 | ); 130 | let expected = Config { 131 | version: 0, 132 | github: Some(GitHub { 133 | user: "user".to_string(), 134 | token: "token".to_string(), 135 | archive: GitHubArchive { 136 | owned: false, 137 | // repos: vec!["example/one".to_string()], 138 | }, 139 | clone: GitHubClone { 140 | starred: true, 141 | watched: true, 142 | gists: false, 143 | repos: vec!["example/two".to_string()], 144 | ignored: vec![], 145 | }, 146 | }), 147 | git: Some(Git { repos }), 148 | }; 149 | assert_eq!(actual, expected) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/github.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::copy; 3 | use std::path::PathBuf; 4 | use std::time::Duration; 5 | use std::{fs, thread}; 6 | 7 | use graphql_client::{GraphQLQuery, Response}; 8 | use reqwest::blocking::Client; 9 | use reqwest::header::ACCEPT; 10 | use serde::Deserialize; 11 | use serde::Serialize; 12 | 13 | #[derive(GraphQLQuery)] 14 | #[graphql( 15 | schema_path = "src/github_schema.graphql", 16 | query_path = "src/github_repos.graphql", 17 | response_derives = "Debug" 18 | )] 19 | struct UserRepos; 20 | 21 | pub fn user_repos(client: &Client, user: &str, token: &str) -> Repositories { 22 | let mut owned_after: Option = None; 23 | let mut owned_repos: Vec = vec![]; 24 | let mut starred_after: Option = None; 25 | let mut starred_repos: Vec = vec![]; 26 | let mut watched_after: Option = None; 27 | let mut watched_repos: Vec = vec![]; 28 | let mut gists_after: Option = None; 29 | let mut gists_repos: Vec = vec![]; 30 | loop { 31 | let query = UserRepos::build_query(user_repos::Variables { 32 | login: user.to_string(), 33 | owner_after: owned_after.clone(), 34 | starred_after: starred_after.clone(), 35 | watched_after: watched_after.clone(), 36 | gists_after: gists_after.clone(), 37 | }); 38 | let response = client 39 | .post("https://api.github.com/graphql") 40 | .bearer_auth(token) 41 | .json(&query) 42 | .send() 43 | .unwrap(); 44 | 45 | let body: Response = response.json().unwrap(); 46 | let user = body.data.unwrap().user.unwrap(); 47 | 48 | let owned_response = user.repositories.edges.unwrap(); 49 | let starred_response = user.starred_repositories.edges.unwrap(); 50 | let watched_response = user.watching.edges.unwrap(); 51 | let gists_response = user.gists.edges.unwrap(); 52 | if owned_response.is_empty() 53 | && starred_response.is_empty() 54 | && watched_response.is_empty() 55 | && gists_response.is_empty() 56 | { 57 | break; 58 | } 59 | for repository in owned_response { 60 | let repository = repository.unwrap(); 61 | 62 | owned_after = Some(repository.cursor); 63 | owned_repos.push(repository.node.unwrap().name_with_owner); 64 | } 65 | for repository in starred_response { 66 | let repository = repository.unwrap(); 67 | 68 | starred_after = Some(repository.cursor); 69 | starred_repos.push(repository.node.name_with_owner); 70 | } 71 | for repository in watched_response { 72 | let repository = repository.unwrap(); 73 | 74 | watched_after = Some(repository.cursor); 75 | watched_repos.push(repository.node.unwrap().name_with_owner); 76 | } 77 | for gist in gists_response { 78 | let gist = gist.unwrap(); 79 | 80 | gists_after = Some(gist.cursor); 81 | gists_repos.push(gist.node.unwrap().name); 82 | } 83 | } 84 | 85 | Repositories { 86 | owned: owned_repos, 87 | starred: starred_repos, 88 | watched: watched_repos, 89 | gists: gists_repos, 90 | } 91 | } 92 | 93 | #[derive(Debug, PartialEq)] 94 | pub struct Repositories { 95 | pub owned: Vec, 96 | pub starred: Vec, 97 | pub watched: Vec, 98 | pub gists: Vec, 99 | } 100 | 101 | pub fn archive_repo(client: &Client, dir: &PathBuf, repository: &str, token: &str) { 102 | let migration_request = MigrationRequest { 103 | repositories: vec![repository.to_owned()], 104 | }; 105 | let create_response: MigrationResponse = client 106 | .post("https://api.github.com/user/migrations") 107 | .bearer_auth(token) 108 | .header(ACCEPT, "application/vnd.github.wyandotte-preview+json") 109 | .json(&migration_request) 110 | .send() 111 | .unwrap() 112 | .error_for_status() 113 | .unwrap() 114 | .json() 115 | .unwrap(); 116 | let migration_id = create_response.id; 117 | let mut migration_state = create_response.state; 118 | 119 | let mut wait = Duration::from_secs(2); 120 | loop { 121 | if migration_state == "exported" { 122 | break; 123 | } 124 | if migration_state == "failed" { 125 | panic!("Creating migration for {} failed", &repository); 126 | } 127 | 128 | thread::sleep(wait); 129 | if wait < Duration::from_secs(64) { 130 | wait *= 2 131 | } 132 | 133 | let status_url = format!("https://api.github.com/user/migrations/{0}", migration_id); 134 | let status_response: MigrationResponse = client 135 | .get(&status_url) 136 | .bearer_auth(token) 137 | .header(ACCEPT, "application/vnd.github.wyandotte-preview+json") 138 | .send() 139 | .unwrap() 140 | .error_for_status() 141 | .unwrap() 142 | .json() 143 | .unwrap(); 144 | migration_state = status_response.state; 145 | } 146 | 147 | // In order to never lose data if we crash we must perform a dance to update archives: 148 | // 1. Download the new archive to repo.zip.new. 149 | // 2. Delete the old archive repo.zip. 150 | // 3. Rename the new archive from repo.zip.new to repo.zip. 151 | 152 | let mut archive_old = dir.clone(); 153 | archive_old.push(format!("{0}.zip", &repository)); 154 | let mut archive_new = dir.clone(); 155 | archive_new.push(format!("{0}.zip.new", &repository)); 156 | 157 | let mut archive_dir = archive_old.clone(); 158 | archive_dir.pop(); 159 | if !fs::metadata(&archive_dir).map_or_else(|_| false, |m| m.is_dir()) { 160 | fs::create_dir_all(&archive_dir).unwrap(); 161 | } 162 | 163 | let archive_old_exists = fs::metadata(&archive_old).map_or_else(|_| false, |m| m.is_file()); 164 | let archive_new_exists = fs::metadata(&archive_new).map_or_else(|_| false, |m| m.is_file()); 165 | 166 | if archive_new_exists { 167 | fs::remove_file(&archive_new).unwrap(); 168 | } 169 | 170 | // Step 1: 171 | let download_url = format!( 172 | "https://api.github.com/user/migrations/{0}/archive", 173 | migration_id 174 | ); 175 | let mut download_request = client 176 | .get(&download_url) 177 | .bearer_auth(token) 178 | .header(ACCEPT, "application/vnd.github.wyandotte-preview+json") 179 | .send() 180 | .unwrap() 181 | .error_for_status() 182 | .unwrap(); 183 | 184 | let mut archive_file = File::create(&archive_new).unwrap(); 185 | copy(&mut download_request, &mut archive_file).unwrap(); 186 | 187 | // Step 2: 188 | if archive_old_exists { 189 | fs::rename(&archive_old, &archive_old).unwrap(); 190 | } 191 | 192 | // Step 3: 193 | fs::rename(&archive_new, &archive_old).unwrap(); 194 | } 195 | 196 | #[derive(Serialize)] 197 | struct MigrationRequest { 198 | repositories: Vec, 199 | } 200 | 201 | #[derive(Deserialize)] 202 | struct MigrationResponse { 203 | id: u64, 204 | state: String, 205 | } 206 | -------------------------------------------------------------------------------- /src/github_repos.graphql: -------------------------------------------------------------------------------- 1 | query UserRepos( 2 | $login: String!, 3 | $owner_after: String, 4 | $starred_after: String, 5 | $watched_after: String, 6 | $gists_after: String, 7 | ) { 8 | user(login: $login) { 9 | repositories(first: 100, ownerAffiliations: OWNER, after: $owner_after) { 10 | edges { 11 | cursor 12 | node { 13 | nameWithOwner 14 | } 15 | } 16 | } 17 | starredRepositories(first: 100, after: $starred_after) { 18 | edges { 19 | cursor 20 | node { 21 | nameWithOwner 22 | } 23 | } 24 | } 25 | watching(first: 100, after: $watched_after) { 26 | edges { 27 | cursor 28 | node { 29 | nameWithOwner 30 | } 31 | } 32 | } 33 | gists(first: 100, privacy: ALL, after: $gists_after) { 34 | edges { 35 | cursor 36 | node { 37 | name 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::io::Write; 3 | use std::path::PathBuf; 4 | use std::{fs, io}; 5 | 6 | use git2::{Cred, FetchOptions, RemoteCallbacks, Repository}; 7 | use reqwest::blocking::Client; 8 | 9 | mod args; 10 | mod config; 11 | mod github; 12 | 13 | fn main() { 14 | let args = args::parse_args(); 15 | if args.verbose { 16 | dbg!(&args); 17 | } 18 | let args::Args { 19 | config, 20 | destination, 21 | verbose, 22 | experimental_archive, 23 | dry_run, 24 | } = args; 25 | 26 | if !dry_run { 27 | let destination_metadata = fs::metadata(&destination).unwrap(); 28 | if !destination_metadata.is_dir() { 29 | panic!("Destination must exist and must be a directory") 30 | } 31 | } 32 | 33 | let config = fs::read_to_string(config).unwrap(); 34 | let config = config::parse_config(&config).unwrap(); 35 | if verbose { 36 | dbg!(&config); 37 | } 38 | 39 | if let Some(github) = config.github { 40 | static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 41 | 42 | // TODO make this owned by a GitHub struct so it can be entirely encapsulated in github.rs. 43 | let client = Client::builder() 44 | .user_agent(APP_USER_AGENT) 45 | .build() 46 | .unwrap(); 47 | 48 | println!("Querying GitHub information for {0}…", &github.user); 49 | io::stdout().flush().unwrap(); 50 | let user_repos = github::user_repos(&client, &github.user, &github.token); 51 | if verbose { 52 | dbg!(&user_repos); 53 | } 54 | println!(); 55 | 56 | let mut github_dir = destination.clone(); 57 | github_dir.push("github"); 58 | 59 | let mut archive_dir = github_dir.clone(); 60 | archive_dir.push("archive"); 61 | 62 | let mut archive_repos = vec![]; 63 | // archive_repos.extend(github.archive.repos.clone()); 64 | if github.archive.owned { 65 | archive_repos.extend(user_repos.owned.clone()); 66 | } 67 | let archive_repos: HashSet = archive_repos.into_iter().collect(); 68 | 69 | if experimental_archive { 70 | println!("Archiving {0} GitHub repositories…", archive_repos.len()); 71 | for (i, repo) in archive_repos.iter().enumerate() { 72 | print!( 73 | "{0}/{1} Archiving {2}… ", 74 | i + 1, 75 | &archive_repos.len(), 76 | &repo 77 | ); 78 | io::stdout().flush().unwrap(); 79 | 80 | github::archive_repo(&client, &archive_dir, &repo, &github.token); 81 | 82 | println!("Done"); 83 | } 84 | println!(); 85 | } 86 | 87 | if github.clone.gists { 88 | let mut gists_dir = github_dir.clone(); 89 | gists_dir.push("gists"); 90 | 91 | let gist_names = user_repos.gists; 92 | println!("Checking {0} GitHub gists for updates…", gist_names.len()); 93 | for (i, name) in gist_names.iter().enumerate() { 94 | print!("\r{0}/{1} ", i + 1, &gist_names.len()); 95 | io::stdout().flush().unwrap(); 96 | 97 | let url = format!("https://gist.github.com/{0}.git", &name); 98 | let username = &github.user; 99 | let password = &github.token; 100 | clone_or_fetch_bare(&gists_dir, &name, &url, dry_run, Some((username, password))); 101 | } 102 | println!("\n"); 103 | } 104 | 105 | let mut clone_dir = github_dir; 106 | clone_dir.push("clone"); 107 | 108 | let mut clone_repos = vec![]; 109 | clone_repos.extend(user_repos.owned.clone()); 110 | clone_repos.extend(archive_repos); 111 | clone_repos.extend(github.clone.repos.clone()); 112 | if github.clone.starred { 113 | clone_repos.extend(user_repos.starred); 114 | } 115 | if github.clone.watched { 116 | clone_repos.extend(user_repos.watched); 117 | } 118 | if !github.clone.ignored.is_empty() { 119 | clone_repos.retain(|r| !github.clone.ignored.contains(r)) 120 | } 121 | let clone_repos: HashSet = clone_repos.into_iter().collect(); 122 | 123 | println!( 124 | "Checking {0} GitHub repositories for updates…", 125 | clone_repos.len() 126 | ); 127 | for (i, repo) in clone_repos.iter().enumerate() { 128 | print!("\r{0}/{1} ", i + 1, &clone_repos.len()); 129 | io::stdout().flush().unwrap(); 130 | 131 | let url = format!("https://github.com/{0}.git", &repo); 132 | let username = &github.user; 133 | let password = &github.token; 134 | clone_or_fetch_bare(&clone_dir, &repo, &url, dry_run, Some((username, password))); 135 | } 136 | println!("\n"); 137 | } 138 | 139 | if let Some(git) = config.git { 140 | let mut git_dir = destination; 141 | git_dir.push("git"); 142 | 143 | println!( 144 | "Checking {0} git repository clones for updates…", 145 | git.repos.len() 146 | ); 147 | for (i, (path, url)) in git.repos.iter().enumerate() { 148 | print!("\r{0}/{1} ", i + 1, git.repos.len()); 149 | io::stdout().flush().unwrap(); 150 | 151 | let url = url.as_str().unwrap(); 152 | clone_or_fetch_bare(&git_dir, &path, url, dry_run, None) 153 | } 154 | println!("\n"); 155 | } 156 | 157 | println!("Done!"); 158 | } 159 | 160 | fn clone_or_fetch_bare( 161 | dir: &PathBuf, 162 | repository: &str, 163 | url: &str, 164 | dry_run: bool, 165 | credentials: Option<(&str, &str)>, 166 | ) { 167 | let mut updated = false; 168 | 169 | { 170 | let mut callbacks = RemoteCallbacks::new(); 171 | 172 | if let Some((username, password)) = credentials { 173 | callbacks.credentials(move |_url, _username_from_url, _allowed_types| { 174 | Cred::userpass_plaintext(username, password) 175 | }); 176 | } 177 | 178 | callbacks.transfer_progress(|_progress| { 179 | if !updated { 180 | print!("Synchronizing {0} from {1}… ", &repository, &url); 181 | io::stdout().flush().unwrap(); 182 | updated = true; 183 | } 184 | true 185 | }); 186 | 187 | let mut fo = FetchOptions::new(); 188 | fo.remote_callbacks(callbacks); 189 | 190 | if !dry_run { 191 | let mut repo_dir = dir.clone(); 192 | repo_dir.push(repository); 193 | 194 | let repo_exists = fs::metadata(&repo_dir).map_or_else(|_| false, |m| m.is_dir()); 195 | 196 | let repository: Repository; 197 | let mut origin = if repo_exists { 198 | repository = Repository::open_bare(&repo_dir).unwrap(); 199 | repository.find_remote("origin").unwrap() 200 | } else { 201 | fs::create_dir_all(&repo_dir).unwrap(); 202 | repository = Repository::init_bare(&repo_dir).unwrap(); 203 | repository.remote("origin", url).unwrap() 204 | }; 205 | 206 | origin.fetch(&[] as &[String], Some(&mut fo), None).unwrap(); 207 | } 208 | } 209 | 210 | if updated { 211 | println!("Done") 212 | } 213 | } 214 | --------------------------------------------------------------------------------