├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .patchy ├── config.toml └── remove-tab.patch ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── default.nix ├── dist-workspace.toml ├── example-config.toml ├── flake.lock ├── flake.nix ├── rust-toolchain.toml ├── shell.nix ├── src ├── backup.rs ├── cli.rs ├── commands │ ├── branch_fetch.rs │ ├── gen_patch.rs │ ├── init.rs │ ├── mod.rs │ ├── pr_fetch.rs │ └── run.rs ├── commit.rs ├── git.rs ├── interact.rs ├── lib.rs ├── main.rs ├── types.rs └── utils.rs ├── tests ├── fixtures │ └── patches │ │ ├── helix-readme-all-every.patch │ │ ├── helix-readme-all-most.patch │ │ ├── helix-readme-all-some.patch │ │ ├── helix-readme-most-every.patch │ │ ├── helix-readme-some-most.patch │ │ └── helix-remove-tab.patch └── run.rs └── wix └── main.wxs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | check: 13 | name: Check 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions-rust-lang/setup-rust-toolchain@v1 18 | - run: cargo check 19 | 20 | format: 21 | name: Format 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions-rust-lang/setup-rust-toolchain@v1 26 | - run: cargo fmt --check 27 | 28 | test: 29 | name: Test Suite 30 | runs-on: ${{ matrix.os }} 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest, macos-latest, windows-latest] 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions-rust-lang/setup-rust-toolchain@v1 37 | - run: cargo test --workspace 38 | 39 | lint: 40 | name: Clippy 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: actions-rust-lang/setup-rust-toolchain@v1 45 | - run: cargo clippy --workspace --all-targets -- -D warnings 46 | - run: cargo doc --no-deps --workspace --document-private-items 47 | env: 48 | RUSTDOCFLAGS: -D warnings 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-20.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | - name: Install dist 63 | # we specify bash to get pipefail; it guards against the `curl` command 64 | # failing. otherwise `sh` won't catch that `curl` returned non-0 65 | shell: bash 66 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh" 67 | - name: Cache dist 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: cargo-dist-cache 71 | path: ~/.cargo/bin/dist 72 | # sure would be cool if github gave us proper conditionals... 73 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 74 | # functionality based on whether this is a pull_request, and whether it's from a fork. 75 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 76 | # but also really annoying to build CI around when it needs secrets to work right.) 77 | - id: plan 78 | run: | 79 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 80 | echo "dist ran successfully" 81 | cat plan-dist-manifest.json 82 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 83 | - name: "Upload dist-manifest.json" 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: artifacts-plan-dist-manifest 87 | path: plan-dist-manifest.json 88 | 89 | # Build and packages all the platform-specific things 90 | build-local-artifacts: 91 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 92 | # Let the initial task tell us to not run (currently very blunt) 93 | needs: 94 | - plan 95 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 96 | strategy: 97 | fail-fast: false 98 | # Target platforms/runners are computed by dist in create-release. 99 | # Each member of the matrix has the following arguments: 100 | # 101 | # - runner: the github runner 102 | # - dist-args: cli flags to pass to dist 103 | # - install-dist: expression to run to install dist on the runner 104 | # 105 | # Typically there will be: 106 | # - 1 "global" task that builds universal installers 107 | # - N "local" tasks that build each platform's binaries and platform-specific installers 108 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 109 | runs-on: ${{ matrix.runner }} 110 | container: ${{ matrix.container && matrix.container.image || null }} 111 | env: 112 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 114 | steps: 115 | - name: enable windows longpaths 116 | run: | 117 | git config --global core.longpaths true 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: recursive 121 | - name: Install Rust non-interactively if not already installed 122 | if: ${{ matrix.container }} 123 | run: | 124 | if ! command -v cargo > /dev/null 2>&1; then 125 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 126 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 127 | fi 128 | - name: Install dist 129 | run: ${{ matrix.install_dist.run }} 130 | # Get the dist-manifest 131 | - name: Fetch local artifacts 132 | uses: actions/download-artifact@v4 133 | with: 134 | pattern: artifacts-* 135 | path: target/distrib/ 136 | merge-multiple: true 137 | - name: Install dependencies 138 | run: | 139 | ${{ matrix.packages_install }} 140 | - name: Build artifacts 141 | run: | 142 | # Actually do builds and make zips and whatnot 143 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 144 | echo "dist ran successfully" 145 | - id: cargo-dist 146 | name: Post-build 147 | # We force bash here just because github makes it really hard to get values up 148 | # to "real" actions without writing to env-vars, and writing to env-vars has 149 | # inconsistent syntax between shell and powershell. 150 | shell: bash 151 | run: | 152 | # Parse out what we just built and upload it to scratch storage 153 | echo "paths<> "$GITHUB_OUTPUT" 154 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 155 | echo "EOF" >> "$GITHUB_OUTPUT" 156 | 157 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 158 | - name: "Upload artifacts" 159 | uses: actions/upload-artifact@v4 160 | with: 161 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 162 | path: | 163 | ${{ steps.cargo-dist.outputs.paths }} 164 | ${{ env.BUILD_MANIFEST_NAME }} 165 | 166 | # Build and package all the platform-agnostic(ish) things 167 | build-global-artifacts: 168 | needs: 169 | - plan 170 | - build-local-artifacts 171 | runs-on: "ubuntu-20.04" 172 | env: 173 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 175 | steps: 176 | - uses: actions/checkout@v4 177 | with: 178 | submodules: recursive 179 | - name: Install cached dist 180 | uses: actions/download-artifact@v4 181 | with: 182 | name: cargo-dist-cache 183 | path: ~/.cargo/bin/ 184 | - run: chmod +x ~/.cargo/bin/dist 185 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 186 | - name: Fetch local artifacts 187 | uses: actions/download-artifact@v4 188 | with: 189 | pattern: artifacts-* 190 | path: target/distrib/ 191 | merge-multiple: true 192 | - id: cargo-dist 193 | shell: bash 194 | run: | 195 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 196 | echo "dist ran successfully" 197 | 198 | # Parse out what we just built and upload it to scratch storage 199 | echo "paths<> "$GITHUB_OUTPUT" 200 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 201 | echo "EOF" >> "$GITHUB_OUTPUT" 202 | 203 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 204 | - name: "Upload artifacts" 205 | uses: actions/upload-artifact@v4 206 | with: 207 | name: artifacts-build-global 208 | path: | 209 | ${{ steps.cargo-dist.outputs.paths }} 210 | ${{ env.BUILD_MANIFEST_NAME }} 211 | # Determines if we should publish/announce 212 | host: 213 | needs: 214 | - plan 215 | - build-local-artifacts 216 | - build-global-artifacts 217 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 218 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 219 | env: 220 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 221 | runs-on: "ubuntu-20.04" 222 | outputs: 223 | val: ${{ steps.host.outputs.manifest }} 224 | steps: 225 | - uses: actions/checkout@v4 226 | with: 227 | submodules: recursive 228 | - name: Install cached dist 229 | uses: actions/download-artifact@v4 230 | with: 231 | name: cargo-dist-cache 232 | path: ~/.cargo/bin/ 233 | - run: chmod +x ~/.cargo/bin/dist 234 | # Fetch artifacts from scratch-storage 235 | - name: Fetch artifacts 236 | uses: actions/download-artifact@v4 237 | with: 238 | pattern: artifacts-* 239 | path: target/distrib/ 240 | merge-multiple: true 241 | - id: host 242 | shell: bash 243 | run: | 244 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 245 | echo "artifacts uploaded and released successfully" 246 | cat dist-manifest.json 247 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 248 | - name: "Upload dist-manifest.json" 249 | uses: actions/upload-artifact@v4 250 | with: 251 | # Overwrite the previous copy 252 | name: artifacts-dist-manifest 253 | path: dist-manifest.json 254 | # Create a GitHub Release while uploading all files to it 255 | - name: "Download GitHub Artifacts" 256 | uses: actions/download-artifact@v4 257 | with: 258 | pattern: artifacts-* 259 | path: artifacts 260 | merge-multiple: true 261 | - name: Cleanup 262 | run: | 263 | # Remove the granular manifests 264 | rm -f artifacts/*-dist-manifest.json 265 | - name: Create GitHub Release 266 | env: 267 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 268 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 269 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 270 | RELEASE_COMMIT: "${{ github.sha }}" 271 | run: | 272 | # Write and read notes from a file to avoid quoting breaking things 273 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 274 | 275 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 276 | 277 | publish-homebrew-formula: 278 | needs: 279 | - plan 280 | - host 281 | runs-on: "ubuntu-20.04" 282 | env: 283 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 284 | PLAN: ${{ needs.plan.outputs.val }} 285 | GITHUB_USER: "axo bot" 286 | GITHUB_EMAIL: "admin+bot@axo.dev" 287 | if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} 288 | steps: 289 | - uses: actions/checkout@v4 290 | with: 291 | repository: "nik-rev/homebrew-tap" 292 | token: ${{ secrets.HOMEBREW_TAP_TOKEN }} 293 | # So we have access to the formula 294 | - name: Fetch homebrew formulae 295 | uses: actions/download-artifact@v4 296 | with: 297 | pattern: artifacts-* 298 | path: Formula/ 299 | merge-multiple: true 300 | # This is extra complex because you can make your Formula name not match your app name 301 | # so we need to find releases with a *.rb file, and publish with that filename. 302 | - name: Commit formula files 303 | run: | 304 | git config --global user.name "${GITHUB_USER}" 305 | git config --global user.email "${GITHUB_EMAIL}" 306 | 307 | for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do 308 | filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) 309 | name=$(echo "$filename" | sed "s/\.rb$//") 310 | version=$(echo "$release" | jq .app_version --raw-output) 311 | 312 | export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" 313 | brew update 314 | # We avoid reformatting user-provided data such as the app description and homepage. 315 | brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true 316 | 317 | git add "Formula/${filename}" 318 | git commit -m "${name} ${version}" 319 | done 320 | git push 321 | 322 | announce: 323 | needs: 324 | - plan 325 | - host 326 | - publish-homebrew-formula 327 | # use "always() && ..." to allow us to wait for all publish jobs while 328 | # still allowing individual publish jobs to skip themselves (for prereleases). 329 | # "host" however must run to completion, no skipping allowed! 330 | if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} 331 | runs-on: "ubuntu-20.04" 332 | env: 333 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 334 | steps: 335 | - uses: actions/checkout@v4 336 | with: 337 | submodules: recursive 338 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.patchy/config.toml: -------------------------------------------------------------------------------- 1 | # Main github repository to fetch from. 2 | # This is going to be our base, into which we merge patches and pull requests. 3 | # 4 | # Examples 5 | # 6 | # repo = "helix-editor/helix" 7 | # repo = "microsoft/vscode" 8 | 9 | repo = "helix-editor/helix" 10 | 11 | # The main repository's branch 12 | # 13 | # Examples 14 | # 15 | # remote-branch = "master" 16 | # remote-branch = "main" 17 | 18 | remote-branch = "master" 19 | 20 | # Branch which patchy will use to do all of its work on 21 | # 22 | # Examples 23 | # 24 | # local-branch = "some-branch-1234" 25 | 26 | local-branch = "patchy" 27 | 28 | # list of pull requests numbers which you would like to merge into the repository and branch you have specified previously 29 | # 30 | # Examples 31 | # 32 | # pull-requests = [ "12254", "10000", "8145"] 33 | 34 | pull-requests = [] 35 | 36 | # Optional: A list of patches to apply 37 | # 38 | # A patch allows you to do specify custom commits and not have to rely on there being a pull request for that change 39 | # 40 | # You can generate patches from a commit with: `patchy gen-patch `. See `patchy --help` for more info. 41 | # 42 | # Examples 43 | # 44 | # With the below config, patchcy will look for the following files: 45 | # - `.patchy/my-patch123.patch` 46 | # - `.patchy/another-patch.patch` 47 | # - `.patchy/1234.patch` 48 | # 49 | # patches = [ "my-patch123", "another-patch", "1234" ] 50 | 51 | patches = ['remove-tab'] 52 | -------------------------------------------------------------------------------- /.patchy/remove-tab.patch: -------------------------------------------------------------------------------- 1 | From 9f13ffd1036c302e1bdaf31dd4c8fcd1202ba981 Mon Sep 17 00:00:00 2001 2 | From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> 3 | Date: Wed, 18 Dec 2024 15:05:00 +0000 4 | Subject: [PATCH] feat: remove tab keybindings 5 | 6 | --- 7 | helix-term/src/keymap/default.rs | 2 +- 8 | helix-term/src/ui/menu.rs | 4 ++-- 9 | 2 files changed, 3 insertions(+), 3 deletions(-) 10 | 11 | diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs 12 | index c6cefd92..105b8f99 100644 13 | --- a/helix-term/src/keymap/default.rs 14 | +++ b/helix-term/src/keymap/default.rs 15 | @@ -215,7 +215,7 @@ pub fn default() -> HashMap { 16 | 17 | // z family for save/restore/combine from/to sels from register 18 | 19 | - "C-i" | "tab" => jump_forward, // tab == 20 | + "C-i" => jump_forward, // tab == 21 | "C-o" => jump_backward, 22 | "C-s" => save_selection, 23 | 24 | diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs 25 | index 612832ce..aaba784a 100644 26 | --- a/helix-term/src/ui/menu.rs 27 | +++ b/helix-term/src/ui/menu.rs 28 | @@ -274,12 +274,12 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { 29 | return EventResult::Consumed(close_fn); 30 | } 31 | // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) 32 | - shift!(Tab) | key!(Up) | ctrl!('p') => { 33 | + key!(Up) | ctrl!('p') => { 34 | self.move_up(); 35 | (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); 36 | return EventResult::Consumed(None); 37 | } 38 | - key!(Tab) | key!(Down) | ctrl!('n') => { 39 | + key!(Down) | ctrl!('n') => { 40 | // arrow down/ctrl-n/tab advances completion choice (including updating the doc) 41 | self.move_down(); 42 | (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); 43 | -- 44 | 2.47.0 45 | 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.0.0 2 | 3 | Breaking 4 | 5 | I've rewritten the CLI in [Clap](https://github.com/clap-rs/clap) instead of a hand-written parser. 6 | 7 | This has 2 advantages: 8 | 9 | - Easier to maintain. The CLI is now declarative. Help is automatically generated. In total we went from about 3,000 lines of code (mostly tests) to just under 200 for the command-line parsing logic. 10 | - More intuitive. The previous CLI interface was nothing like you'd find in any other command-line app. 11 | 12 | And it implies that you can't do stuff like fetch more than 1 PR or Branch using patchy. I don't think people used this much. However, if you'd like to do it you can just invoke `patchy` more than once. 13 | 14 | # v1.3.0 - 2024-01-29 15 | 16 | - Added new `patchy branch-fetch` subcommand, allows fetching GitHub branches locally. Usage: 17 | 18 | ``` 19 | Usage: 20 | 21 | patchy branch-fetch [] [] 22 | » Fetch remote branches into a local branch 23 | 24 | Examples: 25 | 26 | patchy branch-fetch helix-editor/helix/master 27 | » Fetch a single branch 28 | 29 | patchy branch-fetch 'helix-editor/helix/master@6049f20' 30 | » Fetch a single branch at a certain commit 31 | ``` 32 | 33 | - Using a `pr-fetch` with no arguments will bring up the help menu now. 34 | 35 | # Patchy v1.2.7 36 | 37 | - Improved error message 38 | - Create sub-directories if they do not exist 39 | - Fix issue where we tried to create directory when it already existed, so it would abort the entire program 40 | 41 | # Patchy v1.2.6 42 | 43 | - Improved error messages 44 | 45 | # Patchy v1.2.2 - v1.2.5 46 | 47 | - Fixes download links as github username of author changed 48 | 49 | # Patchy v1.2.1 50 | 51 | - Fixes `patchy pr-fetch` not working on pull requests merged by patchy 52 | 53 | # Patchy v1.2.0 54 | 55 | - Use `redirect.github.com` instead of `github.com` not to spam PRs 56 | - Use more readable branch names 57 | - Support pull requests in command-line and config starting with `#` 58 | - Handle when no arguments were passed to gen-patch 59 | - Improved error handling 60 | - Add `--yes` and `-y` flags to `patchy run` 61 | - Fixed `gen-patch` subcommand 62 | 63 | # Patchy v1.1.5 64 | 65 | - Performance improvements 66 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell", 76 | "windows-sys 0.59.0", 77 | ] 78 | 79 | [[package]] 80 | name = "anyhow" 81 | version = "1.0.98" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 84 | 85 | [[package]] 86 | name = "assert_cmd" 87 | version = "2.0.16" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 90 | dependencies = [ 91 | "anstyle", 92 | "bstr", 93 | "doc-comment", 94 | "libc", 95 | "predicates", 96 | "predicates-core", 97 | "predicates-tree", 98 | "wait-timeout", 99 | ] 100 | 101 | [[package]] 102 | name = "autocfg" 103 | version = "1.4.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 106 | 107 | [[package]] 108 | name = "backtrace" 109 | version = "0.3.74" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 112 | dependencies = [ 113 | "addr2line", 114 | "cfg-if", 115 | "libc", 116 | "miniz_oxide", 117 | "object", 118 | "rustc-demangle", 119 | "windows-targets 0.52.6", 120 | ] 121 | 122 | [[package]] 123 | name = "base64" 124 | version = "0.22.1" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 127 | 128 | [[package]] 129 | name = "bitflags" 130 | version = "2.9.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 133 | 134 | [[package]] 135 | name = "bstr" 136 | version = "1.12.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" 139 | dependencies = [ 140 | "memchr", 141 | "regex-automata", 142 | "serde", 143 | ] 144 | 145 | [[package]] 146 | name = "bumpalo" 147 | version = "3.17.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 150 | 151 | [[package]] 152 | name = "bytes" 153 | version = "1.10.1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 156 | 157 | [[package]] 158 | name = "cc" 159 | version = "1.2.16" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" 162 | dependencies = [ 163 | "shlex", 164 | ] 165 | 166 | [[package]] 167 | name = "cfg-if" 168 | version = "1.0.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 171 | 172 | [[package]] 173 | name = "cfg_aliases" 174 | version = "0.2.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 177 | 178 | [[package]] 179 | name = "clap" 180 | version = "4.5.39" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 183 | dependencies = [ 184 | "clap_builder", 185 | "clap_derive", 186 | ] 187 | 188 | [[package]] 189 | name = "clap_builder" 190 | version = "4.5.39" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 193 | dependencies = [ 194 | "anstream", 195 | "anstyle", 196 | "clap_lex", 197 | "strsim", 198 | "terminal_size", 199 | ] 200 | 201 | [[package]] 202 | name = "clap_derive" 203 | version = "4.5.32" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 206 | dependencies = [ 207 | "anstyle", 208 | "heck", 209 | "proc-macro2", 210 | "pulldown-cmark", 211 | "quote", 212 | "syn", 213 | ] 214 | 215 | [[package]] 216 | name = "clap_lex" 217 | version = "0.7.4" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 220 | 221 | [[package]] 222 | name = "colorchoice" 223 | version = "1.0.3" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 226 | 227 | [[package]] 228 | name = "colored" 229 | version = "2.2.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 232 | dependencies = [ 233 | "lazy_static", 234 | "windows-sys 0.59.0", 235 | ] 236 | 237 | [[package]] 238 | name = "console" 239 | version = "0.15.11" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 242 | dependencies = [ 243 | "encode_unicode", 244 | "libc", 245 | "once_cell", 246 | "unicode-width", 247 | "windows-sys 0.59.0", 248 | ] 249 | 250 | [[package]] 251 | name = "convert_case" 252 | version = "0.6.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 255 | dependencies = [ 256 | "unicode-segmentation", 257 | ] 258 | 259 | [[package]] 260 | name = "copy_dir" 261 | version = "0.1.3" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" 264 | dependencies = [ 265 | "walkdir", 266 | ] 267 | 268 | [[package]] 269 | name = "dialoguer" 270 | version = "0.11.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" 273 | dependencies = [ 274 | "console", 275 | "shell-words", 276 | "tempfile", 277 | "thiserror 1.0.69", 278 | "zeroize", 279 | ] 280 | 281 | [[package]] 282 | name = "diff" 283 | version = "0.1.13" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 286 | 287 | [[package]] 288 | name = "difflib" 289 | version = "0.4.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 292 | 293 | [[package]] 294 | name = "displaydoc" 295 | version = "0.2.5" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 298 | dependencies = [ 299 | "proc-macro2", 300 | "quote", 301 | "syn", 302 | ] 303 | 304 | [[package]] 305 | name = "doc-comment" 306 | version = "0.3.3" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 309 | 310 | [[package]] 311 | name = "documented" 312 | version = "0.9.1" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "bc6db32f0995bc4553d2de888999075acd0dbeef75ba923503f6a724263dc6f3" 315 | dependencies = [ 316 | "documented-macros", 317 | "phf", 318 | "thiserror 1.0.69", 319 | ] 320 | 321 | [[package]] 322 | name = "documented-macros" 323 | version = "0.9.1" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "a394bb35929b58f9a5fd418f7c6b17a4b616efcc1e53e6995ca123948f87e5fa" 326 | dependencies = [ 327 | "convert_case", 328 | "itertools", 329 | "optfield", 330 | "proc-macro2", 331 | "quote", 332 | "strum", 333 | "syn", 334 | ] 335 | 336 | [[package]] 337 | name = "either" 338 | version = "1.15.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 341 | 342 | [[package]] 343 | name = "encode_unicode" 344 | version = "1.0.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 347 | 348 | [[package]] 349 | name = "env_filter" 350 | version = "0.1.3" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 353 | dependencies = [ 354 | "log", 355 | "regex", 356 | ] 357 | 358 | [[package]] 359 | name = "env_logger" 360 | version = "0.11.8" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 363 | dependencies = [ 364 | "anstream", 365 | "anstyle", 366 | "env_filter", 367 | "jiff", 368 | "log", 369 | ] 370 | 371 | [[package]] 372 | name = "equivalent" 373 | version = "1.0.2" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 376 | 377 | [[package]] 378 | name = "errno" 379 | version = "0.3.10" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 382 | dependencies = [ 383 | "libc", 384 | "windows-sys 0.59.0", 385 | ] 386 | 387 | [[package]] 388 | name = "fastrand" 389 | version = "2.3.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 392 | 393 | [[package]] 394 | name = "float-cmp" 395 | version = "0.10.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" 398 | dependencies = [ 399 | "num-traits", 400 | ] 401 | 402 | [[package]] 403 | name = "fnv" 404 | version = "1.0.7" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 407 | 408 | [[package]] 409 | name = "form_urlencoded" 410 | version = "1.2.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 413 | dependencies = [ 414 | "percent-encoding", 415 | ] 416 | 417 | [[package]] 418 | name = "futures-channel" 419 | version = "0.3.31" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 422 | dependencies = [ 423 | "futures-core", 424 | "futures-sink", 425 | ] 426 | 427 | [[package]] 428 | name = "futures-core" 429 | version = "0.3.31" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 432 | 433 | [[package]] 434 | name = "futures-io" 435 | version = "0.3.31" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 438 | 439 | [[package]] 440 | name = "futures-sink" 441 | version = "0.3.31" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 444 | 445 | [[package]] 446 | name = "futures-task" 447 | version = "0.3.31" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 450 | 451 | [[package]] 452 | name = "futures-util" 453 | version = "0.3.31" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 456 | dependencies = [ 457 | "futures-core", 458 | "futures-io", 459 | "futures-sink", 460 | "futures-task", 461 | "memchr", 462 | "pin-project-lite", 463 | "pin-utils", 464 | "slab", 465 | ] 466 | 467 | [[package]] 468 | name = "getrandom" 469 | version = "0.2.15" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 472 | dependencies = [ 473 | "cfg-if", 474 | "js-sys", 475 | "libc", 476 | "wasi 0.11.0+wasi-snapshot-preview1", 477 | "wasm-bindgen", 478 | ] 479 | 480 | [[package]] 481 | name = "getrandom" 482 | version = "0.3.1" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 485 | dependencies = [ 486 | "cfg-if", 487 | "libc", 488 | "wasi 0.13.3+wasi-0.2.2", 489 | "windows-targets 0.52.6", 490 | ] 491 | 492 | [[package]] 493 | name = "gimli" 494 | version = "0.31.1" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 497 | 498 | [[package]] 499 | name = "hashbrown" 500 | version = "0.15.2" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 503 | 504 | [[package]] 505 | name = "heck" 506 | version = "0.5.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 509 | 510 | [[package]] 511 | name = "http" 512 | version = "1.3.1" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 515 | dependencies = [ 516 | "bytes", 517 | "fnv", 518 | "itoa", 519 | ] 520 | 521 | [[package]] 522 | name = "http-body" 523 | version = "1.0.1" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 526 | dependencies = [ 527 | "bytes", 528 | "http", 529 | ] 530 | 531 | [[package]] 532 | name = "http-body-util" 533 | version = "0.1.3" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 536 | dependencies = [ 537 | "bytes", 538 | "futures-core", 539 | "http", 540 | "http-body", 541 | "pin-project-lite", 542 | ] 543 | 544 | [[package]] 545 | name = "httparse" 546 | version = "1.10.1" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 549 | 550 | [[package]] 551 | name = "hyper" 552 | version = "1.6.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 555 | dependencies = [ 556 | "bytes", 557 | "futures-channel", 558 | "futures-util", 559 | "http", 560 | "http-body", 561 | "httparse", 562 | "itoa", 563 | "pin-project-lite", 564 | "smallvec", 565 | "tokio", 566 | "want", 567 | ] 568 | 569 | [[package]] 570 | name = "hyper-rustls" 571 | version = "0.27.5" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 574 | dependencies = [ 575 | "futures-util", 576 | "http", 577 | "hyper", 578 | "hyper-util", 579 | "rustls", 580 | "rustls-pki-types", 581 | "tokio", 582 | "tokio-rustls", 583 | "tower-service", 584 | "webpki-roots", 585 | ] 586 | 587 | [[package]] 588 | name = "hyper-util" 589 | version = "0.1.10" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 592 | dependencies = [ 593 | "bytes", 594 | "futures-channel", 595 | "futures-util", 596 | "http", 597 | "http-body", 598 | "hyper", 599 | "pin-project-lite", 600 | "socket2", 601 | "tokio", 602 | "tower-service", 603 | "tracing", 604 | ] 605 | 606 | [[package]] 607 | name = "icu_collections" 608 | version = "1.5.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 611 | dependencies = [ 612 | "displaydoc", 613 | "yoke", 614 | "zerofrom", 615 | "zerovec", 616 | ] 617 | 618 | [[package]] 619 | name = "icu_locid" 620 | version = "1.5.0" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 623 | dependencies = [ 624 | "displaydoc", 625 | "litemap", 626 | "tinystr", 627 | "writeable", 628 | "zerovec", 629 | ] 630 | 631 | [[package]] 632 | name = "icu_locid_transform" 633 | version = "1.5.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 636 | dependencies = [ 637 | "displaydoc", 638 | "icu_locid", 639 | "icu_locid_transform_data", 640 | "icu_provider", 641 | "tinystr", 642 | "zerovec", 643 | ] 644 | 645 | [[package]] 646 | name = "icu_locid_transform_data" 647 | version = "1.5.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 650 | 651 | [[package]] 652 | name = "icu_normalizer" 653 | version = "1.5.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 656 | dependencies = [ 657 | "displaydoc", 658 | "icu_collections", 659 | "icu_normalizer_data", 660 | "icu_properties", 661 | "icu_provider", 662 | "smallvec", 663 | "utf16_iter", 664 | "utf8_iter", 665 | "write16", 666 | "zerovec", 667 | ] 668 | 669 | [[package]] 670 | name = "icu_normalizer_data" 671 | version = "1.5.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 674 | 675 | [[package]] 676 | name = "icu_properties" 677 | version = "1.5.1" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 680 | dependencies = [ 681 | "displaydoc", 682 | "icu_collections", 683 | "icu_locid_transform", 684 | "icu_properties_data", 685 | "icu_provider", 686 | "tinystr", 687 | "zerovec", 688 | ] 689 | 690 | [[package]] 691 | name = "icu_properties_data" 692 | version = "1.5.0" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 695 | 696 | [[package]] 697 | name = "icu_provider" 698 | version = "1.5.0" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 701 | dependencies = [ 702 | "displaydoc", 703 | "icu_locid", 704 | "icu_provider_macros", 705 | "stable_deref_trait", 706 | "tinystr", 707 | "writeable", 708 | "yoke", 709 | "zerofrom", 710 | "zerovec", 711 | ] 712 | 713 | [[package]] 714 | name = "icu_provider_macros" 715 | version = "1.5.0" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 718 | dependencies = [ 719 | "proc-macro2", 720 | "quote", 721 | "syn", 722 | ] 723 | 724 | [[package]] 725 | name = "idna" 726 | version = "1.0.3" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 729 | dependencies = [ 730 | "idna_adapter", 731 | "smallvec", 732 | "utf8_iter", 733 | ] 734 | 735 | [[package]] 736 | name = "idna_adapter" 737 | version = "1.2.0" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 740 | dependencies = [ 741 | "icu_normalizer", 742 | "icu_properties", 743 | ] 744 | 745 | [[package]] 746 | name = "indexmap" 747 | version = "2.8.0" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" 750 | dependencies = [ 751 | "equivalent", 752 | "hashbrown", 753 | "serde", 754 | ] 755 | 756 | [[package]] 757 | name = "ipnet" 758 | version = "2.11.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 761 | 762 | [[package]] 763 | name = "is_terminal_polyfill" 764 | version = "1.70.1" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 767 | 768 | [[package]] 769 | name = "itertools" 770 | version = "0.13.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 773 | dependencies = [ 774 | "either", 775 | ] 776 | 777 | [[package]] 778 | name = "itoa" 779 | version = "1.0.15" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 782 | 783 | [[package]] 784 | name = "jiff" 785 | version = "0.2.14" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" 788 | dependencies = [ 789 | "jiff-static", 790 | "log", 791 | "portable-atomic", 792 | "portable-atomic-util", 793 | "serde", 794 | ] 795 | 796 | [[package]] 797 | name = "jiff-static" 798 | version = "0.2.14" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 801 | dependencies = [ 802 | "proc-macro2", 803 | "quote", 804 | "syn", 805 | ] 806 | 807 | [[package]] 808 | name = "js-sys" 809 | version = "0.3.77" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 812 | dependencies = [ 813 | "once_cell", 814 | "wasm-bindgen", 815 | ] 816 | 817 | [[package]] 818 | name = "kinded" 819 | version = "0.3.0" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c" 822 | dependencies = [ 823 | "kinded_macros", 824 | ] 825 | 826 | [[package]] 827 | name = "kinded_macros" 828 | version = "0.3.0" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d" 831 | dependencies = [ 832 | "convert_case", 833 | "proc-macro2", 834 | "quote", 835 | "syn", 836 | ] 837 | 838 | [[package]] 839 | name = "lazy_static" 840 | version = "1.5.0" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 843 | 844 | [[package]] 845 | name = "libc" 846 | version = "0.2.172" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 849 | 850 | [[package]] 851 | name = "linux-raw-sys" 852 | version = "0.9.3" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 855 | 856 | [[package]] 857 | name = "litemap" 858 | version = "0.7.5" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 861 | 862 | [[package]] 863 | name = "lock_api" 864 | version = "0.4.12" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 867 | dependencies = [ 868 | "autocfg", 869 | "scopeguard", 870 | ] 871 | 872 | [[package]] 873 | name = "log" 874 | version = "0.4.26" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 877 | 878 | [[package]] 879 | name = "memchr" 880 | version = "2.7.4" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 883 | 884 | [[package]] 885 | name = "mime" 886 | version = "0.3.17" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 889 | 890 | [[package]] 891 | name = "miniz_oxide" 892 | version = "0.8.5" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 895 | dependencies = [ 896 | "adler2", 897 | ] 898 | 899 | [[package]] 900 | name = "mio" 901 | version = "1.0.3" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 904 | dependencies = [ 905 | "libc", 906 | "wasi 0.11.0+wasi-snapshot-preview1", 907 | "windows-sys 0.52.0", 908 | ] 909 | 910 | [[package]] 911 | name = "normalize-line-endings" 912 | version = "0.3.0" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 915 | 916 | [[package]] 917 | name = "num-traits" 918 | version = "0.2.19" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 921 | dependencies = [ 922 | "autocfg", 923 | ] 924 | 925 | [[package]] 926 | name = "nutype" 927 | version = "0.6.1" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "3340cb6773b0794ecb3f62ff66631d580f57151d9415c10ee8a27a357aeb998b" 930 | dependencies = [ 931 | "nutype_macros", 932 | ] 933 | 934 | [[package]] 935 | name = "nutype_macros" 936 | version = "0.6.1" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "35c955e27d02868fe90b9c2dc901661fd7ed67ec382782bdc67c6aa8d2e957a9" 939 | dependencies = [ 940 | "cfg-if", 941 | "kinded", 942 | "proc-macro2", 943 | "quote", 944 | "rustc_version", 945 | "syn", 946 | "urlencoding", 947 | ] 948 | 949 | [[package]] 950 | name = "object" 951 | version = "0.36.7" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 954 | dependencies = [ 955 | "memchr", 956 | ] 957 | 958 | [[package]] 959 | name = "once_cell" 960 | version = "1.21.3" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 963 | 964 | [[package]] 965 | name = "optfield" 966 | version = "0.3.0" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "fa59f025cde9c698fcb4fcb3533db4621795374065bee908215263488f2d2a1d" 969 | dependencies = [ 970 | "proc-macro2", 971 | "quote", 972 | "syn", 973 | ] 974 | 975 | [[package]] 976 | name = "parking_lot" 977 | version = "0.12.3" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 980 | dependencies = [ 981 | "lock_api", 982 | "parking_lot_core", 983 | ] 984 | 985 | [[package]] 986 | name = "parking_lot_core" 987 | version = "0.9.10" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 990 | dependencies = [ 991 | "cfg-if", 992 | "libc", 993 | "redox_syscall", 994 | "smallvec", 995 | "windows-targets 0.52.6", 996 | ] 997 | 998 | [[package]] 999 | name = "patchy-bin" 1000 | version = "1.3.0" 1001 | dependencies = [ 1002 | "anyhow", 1003 | "assert_cmd", 1004 | "clap", 1005 | "colored", 1006 | "copy_dir", 1007 | "dialoguer", 1008 | "documented", 1009 | "env_logger", 1010 | "indexmap", 1011 | "log", 1012 | "nutype", 1013 | "once_cell", 1014 | "predicates", 1015 | "pretty_assertions", 1016 | "rand", 1017 | "reqwest", 1018 | "serde", 1019 | "serde_json", 1020 | "tap", 1021 | "tempfile", 1022 | "tokio", 1023 | "toml", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "percent-encoding" 1028 | version = "2.3.1" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1031 | 1032 | [[package]] 1033 | name = "phf" 1034 | version = "0.11.3" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 1037 | dependencies = [ 1038 | "phf_macros", 1039 | "phf_shared", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "phf_generator" 1044 | version = "0.11.3" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 1047 | dependencies = [ 1048 | "phf_shared", 1049 | "rand", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "phf_macros" 1054 | version = "0.11.3" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" 1057 | dependencies = [ 1058 | "phf_generator", 1059 | "phf_shared", 1060 | "proc-macro2", 1061 | "quote", 1062 | "syn", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "phf_shared" 1067 | version = "0.11.3" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 1070 | dependencies = [ 1071 | "siphasher", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "pin-project-lite" 1076 | version = "0.2.16" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1079 | 1080 | [[package]] 1081 | name = "pin-utils" 1082 | version = "0.1.0" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1085 | 1086 | [[package]] 1087 | name = "portable-atomic" 1088 | version = "1.11.0" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 1091 | 1092 | [[package]] 1093 | name = "portable-atomic-util" 1094 | version = "0.2.4" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 1097 | dependencies = [ 1098 | "portable-atomic", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "ppv-lite86" 1103 | version = "0.2.21" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1106 | dependencies = [ 1107 | "zerocopy", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "predicates" 1112 | version = "3.1.3" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 1115 | dependencies = [ 1116 | "anstyle", 1117 | "difflib", 1118 | "float-cmp", 1119 | "normalize-line-endings", 1120 | "predicates-core", 1121 | "regex", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "predicates-core" 1126 | version = "1.0.9" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" 1129 | 1130 | [[package]] 1131 | name = "predicates-tree" 1132 | version = "1.0.12" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" 1135 | dependencies = [ 1136 | "predicates-core", 1137 | "termtree", 1138 | ] 1139 | 1140 | [[package]] 1141 | name = "pretty_assertions" 1142 | version = "1.4.1" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 1145 | dependencies = [ 1146 | "diff", 1147 | "yansi", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "proc-macro2" 1152 | version = "1.0.94" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 1155 | dependencies = [ 1156 | "unicode-ident", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "pulldown-cmark" 1161 | version = "0.13.0" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" 1164 | dependencies = [ 1165 | "bitflags", 1166 | "memchr", 1167 | "unicase", 1168 | ] 1169 | 1170 | [[package]] 1171 | name = "quinn" 1172 | version = "0.11.6" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" 1175 | dependencies = [ 1176 | "bytes", 1177 | "pin-project-lite", 1178 | "quinn-proto", 1179 | "quinn-udp", 1180 | "rustc-hash", 1181 | "rustls", 1182 | "socket2", 1183 | "thiserror 2.0.12", 1184 | "tokio", 1185 | "tracing", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "quinn-proto" 1190 | version = "0.11.9" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" 1193 | dependencies = [ 1194 | "bytes", 1195 | "getrandom 0.2.15", 1196 | "rand", 1197 | "ring", 1198 | "rustc-hash", 1199 | "rustls", 1200 | "rustls-pki-types", 1201 | "slab", 1202 | "thiserror 2.0.12", 1203 | "tinyvec", 1204 | "tracing", 1205 | "web-time", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "quinn-udp" 1210 | version = "0.5.10" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" 1213 | dependencies = [ 1214 | "cfg_aliases", 1215 | "libc", 1216 | "once_cell", 1217 | "socket2", 1218 | "tracing", 1219 | "windows-sys 0.59.0", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "quote" 1224 | version = "1.0.40" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1227 | dependencies = [ 1228 | "proc-macro2", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "rand" 1233 | version = "0.8.5" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1236 | dependencies = [ 1237 | "libc", 1238 | "rand_chacha", 1239 | "rand_core", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "rand_chacha" 1244 | version = "0.3.1" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1247 | dependencies = [ 1248 | "ppv-lite86", 1249 | "rand_core", 1250 | ] 1251 | 1252 | [[package]] 1253 | name = "rand_core" 1254 | version = "0.6.4" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1257 | dependencies = [ 1258 | "getrandom 0.2.15", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "redox_syscall" 1263 | version = "0.5.10" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" 1266 | dependencies = [ 1267 | "bitflags", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "regex" 1272 | version = "1.11.1" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1275 | dependencies = [ 1276 | "aho-corasick", 1277 | "memchr", 1278 | "regex-automata", 1279 | "regex-syntax", 1280 | ] 1281 | 1282 | [[package]] 1283 | name = "regex-automata" 1284 | version = "0.4.9" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1287 | dependencies = [ 1288 | "aho-corasick", 1289 | "memchr", 1290 | "regex-syntax", 1291 | ] 1292 | 1293 | [[package]] 1294 | name = "regex-syntax" 1295 | version = "0.8.5" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1298 | 1299 | [[package]] 1300 | name = "reqwest" 1301 | version = "0.12.14" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254" 1304 | dependencies = [ 1305 | "base64", 1306 | "bytes", 1307 | "futures-channel", 1308 | "futures-core", 1309 | "futures-util", 1310 | "http", 1311 | "http-body", 1312 | "http-body-util", 1313 | "hyper", 1314 | "hyper-rustls", 1315 | "hyper-util", 1316 | "ipnet", 1317 | "js-sys", 1318 | "log", 1319 | "mime", 1320 | "once_cell", 1321 | "percent-encoding", 1322 | "pin-project-lite", 1323 | "quinn", 1324 | "rustls", 1325 | "rustls-pemfile", 1326 | "rustls-pki-types", 1327 | "serde", 1328 | "serde_json", 1329 | "serde_urlencoded", 1330 | "sync_wrapper", 1331 | "tokio", 1332 | "tokio-rustls", 1333 | "tower", 1334 | "tower-service", 1335 | "url", 1336 | "wasm-bindgen", 1337 | "wasm-bindgen-futures", 1338 | "web-sys", 1339 | "webpki-roots", 1340 | "windows-registry", 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "ring" 1345 | version = "0.17.14" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1348 | dependencies = [ 1349 | "cc", 1350 | "cfg-if", 1351 | "getrandom 0.2.15", 1352 | "libc", 1353 | "untrusted", 1354 | "windows-sys 0.52.0", 1355 | ] 1356 | 1357 | [[package]] 1358 | name = "rustc-demangle" 1359 | version = "0.1.24" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1362 | 1363 | [[package]] 1364 | name = "rustc-hash" 1365 | version = "2.1.1" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1368 | 1369 | [[package]] 1370 | name = "rustc_version" 1371 | version = "0.4.1" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 1374 | dependencies = [ 1375 | "semver", 1376 | ] 1377 | 1378 | [[package]] 1379 | name = "rustix" 1380 | version = "1.0.7" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 1383 | dependencies = [ 1384 | "bitflags", 1385 | "errno", 1386 | "libc", 1387 | "linux-raw-sys", 1388 | "windows-sys 0.59.0", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "rustls" 1393 | version = "0.23.23" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" 1396 | dependencies = [ 1397 | "once_cell", 1398 | "ring", 1399 | "rustls-pki-types", 1400 | "rustls-webpki", 1401 | "subtle", 1402 | "zeroize", 1403 | ] 1404 | 1405 | [[package]] 1406 | name = "rustls-pemfile" 1407 | version = "2.2.0" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1410 | dependencies = [ 1411 | "rustls-pki-types", 1412 | ] 1413 | 1414 | [[package]] 1415 | name = "rustls-pki-types" 1416 | version = "1.11.0" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 1419 | dependencies = [ 1420 | "web-time", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "rustls-webpki" 1425 | version = "0.102.8" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1428 | dependencies = [ 1429 | "ring", 1430 | "rustls-pki-types", 1431 | "untrusted", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "rustversion" 1436 | version = "1.0.20" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1439 | 1440 | [[package]] 1441 | name = "ryu" 1442 | version = "1.0.20" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1445 | 1446 | [[package]] 1447 | name = "same-file" 1448 | version = "1.0.6" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1451 | dependencies = [ 1452 | "winapi-util", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "scopeguard" 1457 | version = "1.2.0" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1460 | 1461 | [[package]] 1462 | name = "semver" 1463 | version = "1.0.26" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 1466 | 1467 | [[package]] 1468 | name = "serde" 1469 | version = "1.0.219" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1472 | dependencies = [ 1473 | "serde_derive", 1474 | ] 1475 | 1476 | [[package]] 1477 | name = "serde_derive" 1478 | version = "1.0.219" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1481 | dependencies = [ 1482 | "proc-macro2", 1483 | "quote", 1484 | "syn", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "serde_json" 1489 | version = "1.0.140" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1492 | dependencies = [ 1493 | "itoa", 1494 | "memchr", 1495 | "ryu", 1496 | "serde", 1497 | ] 1498 | 1499 | [[package]] 1500 | name = "serde_spanned" 1501 | version = "0.6.8" 1502 | source = "registry+https://github.com/rust-lang/crates.io-index" 1503 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1504 | dependencies = [ 1505 | "serde", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "serde_urlencoded" 1510 | version = "0.7.1" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1513 | dependencies = [ 1514 | "form_urlencoded", 1515 | "itoa", 1516 | "ryu", 1517 | "serde", 1518 | ] 1519 | 1520 | [[package]] 1521 | name = "shell-words" 1522 | version = "1.1.0" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 1525 | 1526 | [[package]] 1527 | name = "shlex" 1528 | version = "1.3.0" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1531 | 1532 | [[package]] 1533 | name = "signal-hook-registry" 1534 | version = "1.4.5" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 1537 | dependencies = [ 1538 | "libc", 1539 | ] 1540 | 1541 | [[package]] 1542 | name = "siphasher" 1543 | version = "1.0.1" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 1546 | 1547 | [[package]] 1548 | name = "slab" 1549 | version = "0.4.9" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1552 | dependencies = [ 1553 | "autocfg", 1554 | ] 1555 | 1556 | [[package]] 1557 | name = "smallvec" 1558 | version = "1.15.0" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1561 | 1562 | [[package]] 1563 | name = "socket2" 1564 | version = "0.5.8" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1567 | dependencies = [ 1568 | "libc", 1569 | "windows-sys 0.52.0", 1570 | ] 1571 | 1572 | [[package]] 1573 | name = "stable_deref_trait" 1574 | version = "1.2.0" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1577 | 1578 | [[package]] 1579 | name = "strsim" 1580 | version = "0.11.1" 1581 | source = "registry+https://github.com/rust-lang/crates.io-index" 1582 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1583 | 1584 | [[package]] 1585 | name = "strum" 1586 | version = "0.26.3" 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" 1588 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1589 | dependencies = [ 1590 | "strum_macros", 1591 | ] 1592 | 1593 | [[package]] 1594 | name = "strum_macros" 1595 | version = "0.26.4" 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" 1597 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1598 | dependencies = [ 1599 | "heck", 1600 | "proc-macro2", 1601 | "quote", 1602 | "rustversion", 1603 | "syn", 1604 | ] 1605 | 1606 | [[package]] 1607 | name = "subtle" 1608 | version = "2.6.1" 1609 | source = "registry+https://github.com/rust-lang/crates.io-index" 1610 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1611 | 1612 | [[package]] 1613 | name = "syn" 1614 | version = "2.0.100" 1615 | source = "registry+https://github.com/rust-lang/crates.io-index" 1616 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1617 | dependencies = [ 1618 | "proc-macro2", 1619 | "quote", 1620 | "unicode-ident", 1621 | ] 1622 | 1623 | [[package]] 1624 | name = "sync_wrapper" 1625 | version = "1.0.2" 1626 | source = "registry+https://github.com/rust-lang/crates.io-index" 1627 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1628 | dependencies = [ 1629 | "futures-core", 1630 | ] 1631 | 1632 | [[package]] 1633 | name = "synstructure" 1634 | version = "0.13.1" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1637 | dependencies = [ 1638 | "proc-macro2", 1639 | "quote", 1640 | "syn", 1641 | ] 1642 | 1643 | [[package]] 1644 | name = "tap" 1645 | version = "1.0.1" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 1648 | 1649 | [[package]] 1650 | name = "tempfile" 1651 | version = "3.20.0" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 1654 | dependencies = [ 1655 | "fastrand", 1656 | "getrandom 0.3.1", 1657 | "once_cell", 1658 | "rustix", 1659 | "windows-sys 0.59.0", 1660 | ] 1661 | 1662 | [[package]] 1663 | name = "terminal_size" 1664 | version = "0.4.2" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" 1667 | dependencies = [ 1668 | "rustix", 1669 | "windows-sys 0.59.0", 1670 | ] 1671 | 1672 | [[package]] 1673 | name = "termtree" 1674 | version = "0.5.1" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 1677 | 1678 | [[package]] 1679 | name = "thiserror" 1680 | version = "1.0.69" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1683 | dependencies = [ 1684 | "thiserror-impl 1.0.69", 1685 | ] 1686 | 1687 | [[package]] 1688 | name = "thiserror" 1689 | version = "2.0.12" 1690 | source = "registry+https://github.com/rust-lang/crates.io-index" 1691 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1692 | dependencies = [ 1693 | "thiserror-impl 2.0.12", 1694 | ] 1695 | 1696 | [[package]] 1697 | name = "thiserror-impl" 1698 | version = "1.0.69" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1701 | dependencies = [ 1702 | "proc-macro2", 1703 | "quote", 1704 | "syn", 1705 | ] 1706 | 1707 | [[package]] 1708 | name = "thiserror-impl" 1709 | version = "2.0.12" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1712 | dependencies = [ 1713 | "proc-macro2", 1714 | "quote", 1715 | "syn", 1716 | ] 1717 | 1718 | [[package]] 1719 | name = "tinystr" 1720 | version = "0.7.6" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1723 | dependencies = [ 1724 | "displaydoc", 1725 | "zerovec", 1726 | ] 1727 | 1728 | [[package]] 1729 | name = "tinyvec" 1730 | version = "1.9.0" 1731 | source = "registry+https://github.com/rust-lang/crates.io-index" 1732 | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 1733 | dependencies = [ 1734 | "tinyvec_macros", 1735 | ] 1736 | 1737 | [[package]] 1738 | name = "tinyvec_macros" 1739 | version = "0.1.1" 1740 | source = "registry+https://github.com/rust-lang/crates.io-index" 1741 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1742 | 1743 | [[package]] 1744 | name = "tokio" 1745 | version = "1.44.1" 1746 | source = "registry+https://github.com/rust-lang/crates.io-index" 1747 | checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" 1748 | dependencies = [ 1749 | "backtrace", 1750 | "bytes", 1751 | "libc", 1752 | "mio", 1753 | "parking_lot", 1754 | "pin-project-lite", 1755 | "signal-hook-registry", 1756 | "socket2", 1757 | "tokio-macros", 1758 | "windows-sys 0.52.0", 1759 | ] 1760 | 1761 | [[package]] 1762 | name = "tokio-macros" 1763 | version = "2.5.0" 1764 | source = "registry+https://github.com/rust-lang/crates.io-index" 1765 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1766 | dependencies = [ 1767 | "proc-macro2", 1768 | "quote", 1769 | "syn", 1770 | ] 1771 | 1772 | [[package]] 1773 | name = "tokio-rustls" 1774 | version = "0.26.2" 1775 | source = "registry+https://github.com/rust-lang/crates.io-index" 1776 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1777 | dependencies = [ 1778 | "rustls", 1779 | "tokio", 1780 | ] 1781 | 1782 | [[package]] 1783 | name = "toml" 1784 | version = "0.8.20" 1785 | source = "registry+https://github.com/rust-lang/crates.io-index" 1786 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 1787 | dependencies = [ 1788 | "serde", 1789 | "serde_spanned", 1790 | "toml_datetime", 1791 | "toml_edit", 1792 | ] 1793 | 1794 | [[package]] 1795 | name = "toml_datetime" 1796 | version = "0.6.8" 1797 | source = "registry+https://github.com/rust-lang/crates.io-index" 1798 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1799 | dependencies = [ 1800 | "serde", 1801 | ] 1802 | 1803 | [[package]] 1804 | name = "toml_edit" 1805 | version = "0.22.24" 1806 | source = "registry+https://github.com/rust-lang/crates.io-index" 1807 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 1808 | dependencies = [ 1809 | "indexmap", 1810 | "serde", 1811 | "serde_spanned", 1812 | "toml_datetime", 1813 | "winnow", 1814 | ] 1815 | 1816 | [[package]] 1817 | name = "tower" 1818 | version = "0.5.2" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1821 | dependencies = [ 1822 | "futures-core", 1823 | "futures-util", 1824 | "pin-project-lite", 1825 | "sync_wrapper", 1826 | "tokio", 1827 | "tower-layer", 1828 | "tower-service", 1829 | ] 1830 | 1831 | [[package]] 1832 | name = "tower-layer" 1833 | version = "0.3.3" 1834 | source = "registry+https://github.com/rust-lang/crates.io-index" 1835 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1836 | 1837 | [[package]] 1838 | name = "tower-service" 1839 | version = "0.3.3" 1840 | source = "registry+https://github.com/rust-lang/crates.io-index" 1841 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1842 | 1843 | [[package]] 1844 | name = "tracing" 1845 | version = "0.1.41" 1846 | source = "registry+https://github.com/rust-lang/crates.io-index" 1847 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1848 | dependencies = [ 1849 | "pin-project-lite", 1850 | "tracing-core", 1851 | ] 1852 | 1853 | [[package]] 1854 | name = "tracing-core" 1855 | version = "0.1.33" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1858 | dependencies = [ 1859 | "once_cell", 1860 | ] 1861 | 1862 | [[package]] 1863 | name = "try-lock" 1864 | version = "0.2.5" 1865 | source = "registry+https://github.com/rust-lang/crates.io-index" 1866 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1867 | 1868 | [[package]] 1869 | name = "unicase" 1870 | version = "2.8.1" 1871 | source = "registry+https://github.com/rust-lang/crates.io-index" 1872 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 1873 | 1874 | [[package]] 1875 | name = "unicode-ident" 1876 | version = "1.0.18" 1877 | source = "registry+https://github.com/rust-lang/crates.io-index" 1878 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1879 | 1880 | [[package]] 1881 | name = "unicode-segmentation" 1882 | version = "1.12.0" 1883 | source = "registry+https://github.com/rust-lang/crates.io-index" 1884 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1885 | 1886 | [[package]] 1887 | name = "unicode-width" 1888 | version = "0.2.0" 1889 | source = "registry+https://github.com/rust-lang/crates.io-index" 1890 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1891 | 1892 | [[package]] 1893 | name = "untrusted" 1894 | version = "0.9.0" 1895 | source = "registry+https://github.com/rust-lang/crates.io-index" 1896 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1897 | 1898 | [[package]] 1899 | name = "url" 1900 | version = "2.5.4" 1901 | source = "registry+https://github.com/rust-lang/crates.io-index" 1902 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1903 | dependencies = [ 1904 | "form_urlencoded", 1905 | "idna", 1906 | "percent-encoding", 1907 | ] 1908 | 1909 | [[package]] 1910 | name = "urlencoding" 1911 | version = "2.1.3" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 1914 | 1915 | [[package]] 1916 | name = "utf16_iter" 1917 | version = "1.0.5" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1920 | 1921 | [[package]] 1922 | name = "utf8_iter" 1923 | version = "1.0.4" 1924 | source = "registry+https://github.com/rust-lang/crates.io-index" 1925 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1926 | 1927 | [[package]] 1928 | name = "utf8parse" 1929 | version = "0.2.2" 1930 | source = "registry+https://github.com/rust-lang/crates.io-index" 1931 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1932 | 1933 | [[package]] 1934 | name = "wait-timeout" 1935 | version = "0.2.1" 1936 | source = "registry+https://github.com/rust-lang/crates.io-index" 1937 | checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 1938 | dependencies = [ 1939 | "libc", 1940 | ] 1941 | 1942 | [[package]] 1943 | name = "walkdir" 1944 | version = "2.5.0" 1945 | source = "registry+https://github.com/rust-lang/crates.io-index" 1946 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1947 | dependencies = [ 1948 | "same-file", 1949 | "winapi-util", 1950 | ] 1951 | 1952 | [[package]] 1953 | name = "want" 1954 | version = "0.3.1" 1955 | source = "registry+https://github.com/rust-lang/crates.io-index" 1956 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1957 | dependencies = [ 1958 | "try-lock", 1959 | ] 1960 | 1961 | [[package]] 1962 | name = "wasi" 1963 | version = "0.11.0+wasi-snapshot-preview1" 1964 | source = "registry+https://github.com/rust-lang/crates.io-index" 1965 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1966 | 1967 | [[package]] 1968 | name = "wasi" 1969 | version = "0.13.3+wasi-0.2.2" 1970 | source = "registry+https://github.com/rust-lang/crates.io-index" 1971 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 1972 | dependencies = [ 1973 | "wit-bindgen-rt", 1974 | ] 1975 | 1976 | [[package]] 1977 | name = "wasm-bindgen" 1978 | version = "0.2.100" 1979 | source = "registry+https://github.com/rust-lang/crates.io-index" 1980 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1981 | dependencies = [ 1982 | "cfg-if", 1983 | "once_cell", 1984 | "rustversion", 1985 | "wasm-bindgen-macro", 1986 | ] 1987 | 1988 | [[package]] 1989 | name = "wasm-bindgen-backend" 1990 | version = "0.2.100" 1991 | source = "registry+https://github.com/rust-lang/crates.io-index" 1992 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1993 | dependencies = [ 1994 | "bumpalo", 1995 | "log", 1996 | "proc-macro2", 1997 | "quote", 1998 | "syn", 1999 | "wasm-bindgen-shared", 2000 | ] 2001 | 2002 | [[package]] 2003 | name = "wasm-bindgen-futures" 2004 | version = "0.4.50" 2005 | source = "registry+https://github.com/rust-lang/crates.io-index" 2006 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 2007 | dependencies = [ 2008 | "cfg-if", 2009 | "js-sys", 2010 | "once_cell", 2011 | "wasm-bindgen", 2012 | "web-sys", 2013 | ] 2014 | 2015 | [[package]] 2016 | name = "wasm-bindgen-macro" 2017 | version = "0.2.100" 2018 | source = "registry+https://github.com/rust-lang/crates.io-index" 2019 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 2020 | dependencies = [ 2021 | "quote", 2022 | "wasm-bindgen-macro-support", 2023 | ] 2024 | 2025 | [[package]] 2026 | name = "wasm-bindgen-macro-support" 2027 | version = "0.2.100" 2028 | source = "registry+https://github.com/rust-lang/crates.io-index" 2029 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 2030 | dependencies = [ 2031 | "proc-macro2", 2032 | "quote", 2033 | "syn", 2034 | "wasm-bindgen-backend", 2035 | "wasm-bindgen-shared", 2036 | ] 2037 | 2038 | [[package]] 2039 | name = "wasm-bindgen-shared" 2040 | version = "0.2.100" 2041 | source = "registry+https://github.com/rust-lang/crates.io-index" 2042 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 2043 | dependencies = [ 2044 | "unicode-ident", 2045 | ] 2046 | 2047 | [[package]] 2048 | name = "web-sys" 2049 | version = "0.3.77" 2050 | source = "registry+https://github.com/rust-lang/crates.io-index" 2051 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2052 | dependencies = [ 2053 | "js-sys", 2054 | "wasm-bindgen", 2055 | ] 2056 | 2057 | [[package]] 2058 | name = "web-time" 2059 | version = "1.1.0" 2060 | source = "registry+https://github.com/rust-lang/crates.io-index" 2061 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2062 | dependencies = [ 2063 | "js-sys", 2064 | "wasm-bindgen", 2065 | ] 2066 | 2067 | [[package]] 2068 | name = "webpki-roots" 2069 | version = "0.26.8" 2070 | source = "registry+https://github.com/rust-lang/crates.io-index" 2071 | checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" 2072 | dependencies = [ 2073 | "rustls-pki-types", 2074 | ] 2075 | 2076 | [[package]] 2077 | name = "winapi-util" 2078 | version = "0.1.9" 2079 | source = "registry+https://github.com/rust-lang/crates.io-index" 2080 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 2081 | dependencies = [ 2082 | "windows-sys 0.59.0", 2083 | ] 2084 | 2085 | [[package]] 2086 | name = "windows-link" 2087 | version = "0.1.0" 2088 | source = "registry+https://github.com/rust-lang/crates.io-index" 2089 | checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" 2090 | 2091 | [[package]] 2092 | name = "windows-registry" 2093 | version = "0.4.0" 2094 | source = "registry+https://github.com/rust-lang/crates.io-index" 2095 | checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" 2096 | dependencies = [ 2097 | "windows-result", 2098 | "windows-strings", 2099 | "windows-targets 0.53.0", 2100 | ] 2101 | 2102 | [[package]] 2103 | name = "windows-result" 2104 | version = "0.3.1" 2105 | source = "registry+https://github.com/rust-lang/crates.io-index" 2106 | checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" 2107 | dependencies = [ 2108 | "windows-link", 2109 | ] 2110 | 2111 | [[package]] 2112 | name = "windows-strings" 2113 | version = "0.3.1" 2114 | source = "registry+https://github.com/rust-lang/crates.io-index" 2115 | checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 2116 | dependencies = [ 2117 | "windows-link", 2118 | ] 2119 | 2120 | [[package]] 2121 | name = "windows-sys" 2122 | version = "0.52.0" 2123 | source = "registry+https://github.com/rust-lang/crates.io-index" 2124 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2125 | dependencies = [ 2126 | "windows-targets 0.52.6", 2127 | ] 2128 | 2129 | [[package]] 2130 | name = "windows-sys" 2131 | version = "0.59.0" 2132 | source = "registry+https://github.com/rust-lang/crates.io-index" 2133 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2134 | dependencies = [ 2135 | "windows-targets 0.52.6", 2136 | ] 2137 | 2138 | [[package]] 2139 | name = "windows-targets" 2140 | version = "0.52.6" 2141 | source = "registry+https://github.com/rust-lang/crates.io-index" 2142 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2143 | dependencies = [ 2144 | "windows_aarch64_gnullvm 0.52.6", 2145 | "windows_aarch64_msvc 0.52.6", 2146 | "windows_i686_gnu 0.52.6", 2147 | "windows_i686_gnullvm 0.52.6", 2148 | "windows_i686_msvc 0.52.6", 2149 | "windows_x86_64_gnu 0.52.6", 2150 | "windows_x86_64_gnullvm 0.52.6", 2151 | "windows_x86_64_msvc 0.52.6", 2152 | ] 2153 | 2154 | [[package]] 2155 | name = "windows-targets" 2156 | version = "0.53.0" 2157 | source = "registry+https://github.com/rust-lang/crates.io-index" 2158 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 2159 | dependencies = [ 2160 | "windows_aarch64_gnullvm 0.53.0", 2161 | "windows_aarch64_msvc 0.53.0", 2162 | "windows_i686_gnu 0.53.0", 2163 | "windows_i686_gnullvm 0.53.0", 2164 | "windows_i686_msvc 0.53.0", 2165 | "windows_x86_64_gnu 0.53.0", 2166 | "windows_x86_64_gnullvm 0.53.0", 2167 | "windows_x86_64_msvc 0.53.0", 2168 | ] 2169 | 2170 | [[package]] 2171 | name = "windows_aarch64_gnullvm" 2172 | version = "0.52.6" 2173 | source = "registry+https://github.com/rust-lang/crates.io-index" 2174 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2175 | 2176 | [[package]] 2177 | name = "windows_aarch64_gnullvm" 2178 | version = "0.53.0" 2179 | source = "registry+https://github.com/rust-lang/crates.io-index" 2180 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 2181 | 2182 | [[package]] 2183 | name = "windows_aarch64_msvc" 2184 | version = "0.52.6" 2185 | source = "registry+https://github.com/rust-lang/crates.io-index" 2186 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2187 | 2188 | [[package]] 2189 | name = "windows_aarch64_msvc" 2190 | version = "0.53.0" 2191 | source = "registry+https://github.com/rust-lang/crates.io-index" 2192 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 2193 | 2194 | [[package]] 2195 | name = "windows_i686_gnu" 2196 | version = "0.52.6" 2197 | source = "registry+https://github.com/rust-lang/crates.io-index" 2198 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2199 | 2200 | [[package]] 2201 | name = "windows_i686_gnu" 2202 | version = "0.53.0" 2203 | source = "registry+https://github.com/rust-lang/crates.io-index" 2204 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 2205 | 2206 | [[package]] 2207 | name = "windows_i686_gnullvm" 2208 | version = "0.52.6" 2209 | source = "registry+https://github.com/rust-lang/crates.io-index" 2210 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2211 | 2212 | [[package]] 2213 | name = "windows_i686_gnullvm" 2214 | version = "0.53.0" 2215 | source = "registry+https://github.com/rust-lang/crates.io-index" 2216 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 2217 | 2218 | [[package]] 2219 | name = "windows_i686_msvc" 2220 | version = "0.52.6" 2221 | source = "registry+https://github.com/rust-lang/crates.io-index" 2222 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2223 | 2224 | [[package]] 2225 | name = "windows_i686_msvc" 2226 | version = "0.53.0" 2227 | source = "registry+https://github.com/rust-lang/crates.io-index" 2228 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 2229 | 2230 | [[package]] 2231 | name = "windows_x86_64_gnu" 2232 | version = "0.52.6" 2233 | source = "registry+https://github.com/rust-lang/crates.io-index" 2234 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2235 | 2236 | [[package]] 2237 | name = "windows_x86_64_gnu" 2238 | version = "0.53.0" 2239 | source = "registry+https://github.com/rust-lang/crates.io-index" 2240 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 2241 | 2242 | [[package]] 2243 | name = "windows_x86_64_gnullvm" 2244 | version = "0.52.6" 2245 | source = "registry+https://github.com/rust-lang/crates.io-index" 2246 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2247 | 2248 | [[package]] 2249 | name = "windows_x86_64_gnullvm" 2250 | version = "0.53.0" 2251 | source = "registry+https://github.com/rust-lang/crates.io-index" 2252 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 2253 | 2254 | [[package]] 2255 | name = "windows_x86_64_msvc" 2256 | version = "0.52.6" 2257 | source = "registry+https://github.com/rust-lang/crates.io-index" 2258 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2259 | 2260 | [[package]] 2261 | name = "windows_x86_64_msvc" 2262 | version = "0.53.0" 2263 | source = "registry+https://github.com/rust-lang/crates.io-index" 2264 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 2265 | 2266 | [[package]] 2267 | name = "winnow" 2268 | version = "0.7.10" 2269 | source = "registry+https://github.com/rust-lang/crates.io-index" 2270 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 2271 | dependencies = [ 2272 | "memchr", 2273 | ] 2274 | 2275 | [[package]] 2276 | name = "wit-bindgen-rt" 2277 | version = "0.33.0" 2278 | source = "registry+https://github.com/rust-lang/crates.io-index" 2279 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 2280 | dependencies = [ 2281 | "bitflags", 2282 | ] 2283 | 2284 | [[package]] 2285 | name = "write16" 2286 | version = "1.0.0" 2287 | source = "registry+https://github.com/rust-lang/crates.io-index" 2288 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2289 | 2290 | [[package]] 2291 | name = "writeable" 2292 | version = "0.5.5" 2293 | source = "registry+https://github.com/rust-lang/crates.io-index" 2294 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2295 | 2296 | [[package]] 2297 | name = "yansi" 2298 | version = "1.0.1" 2299 | source = "registry+https://github.com/rust-lang/crates.io-index" 2300 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 2301 | 2302 | [[package]] 2303 | name = "yoke" 2304 | version = "0.7.5" 2305 | source = "registry+https://github.com/rust-lang/crates.io-index" 2306 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 2307 | dependencies = [ 2308 | "serde", 2309 | "stable_deref_trait", 2310 | "yoke-derive", 2311 | "zerofrom", 2312 | ] 2313 | 2314 | [[package]] 2315 | name = "yoke-derive" 2316 | version = "0.7.5" 2317 | source = "registry+https://github.com/rust-lang/crates.io-index" 2318 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 2319 | dependencies = [ 2320 | "proc-macro2", 2321 | "quote", 2322 | "syn", 2323 | "synstructure", 2324 | ] 2325 | 2326 | [[package]] 2327 | name = "zerocopy" 2328 | version = "0.8.25" 2329 | source = "registry+https://github.com/rust-lang/crates.io-index" 2330 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 2331 | dependencies = [ 2332 | "zerocopy-derive", 2333 | ] 2334 | 2335 | [[package]] 2336 | name = "zerocopy-derive" 2337 | version = "0.8.25" 2338 | source = "registry+https://github.com/rust-lang/crates.io-index" 2339 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 2340 | dependencies = [ 2341 | "proc-macro2", 2342 | "quote", 2343 | "syn", 2344 | ] 2345 | 2346 | [[package]] 2347 | name = "zerofrom" 2348 | version = "0.1.6" 2349 | source = "registry+https://github.com/rust-lang/crates.io-index" 2350 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2351 | dependencies = [ 2352 | "zerofrom-derive", 2353 | ] 2354 | 2355 | [[package]] 2356 | name = "zerofrom-derive" 2357 | version = "0.1.6" 2358 | source = "registry+https://github.com/rust-lang/crates.io-index" 2359 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2360 | dependencies = [ 2361 | "proc-macro2", 2362 | "quote", 2363 | "syn", 2364 | "synstructure", 2365 | ] 2366 | 2367 | [[package]] 2368 | name = "zeroize" 2369 | version = "1.8.1" 2370 | source = "registry+https://github.com/rust-lang/crates.io-index" 2371 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2372 | 2373 | [[package]] 2374 | name = "zerovec" 2375 | version = "0.10.4" 2376 | source = "registry+https://github.com/rust-lang/crates.io-index" 2377 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2378 | dependencies = [ 2379 | "yoke", 2380 | "zerofrom", 2381 | "zerovec-derive", 2382 | ] 2383 | 2384 | [[package]] 2385 | name = "zerovec-derive" 2386 | version = "0.10.3" 2387 | source = "registry+https://github.com/rust-lang/crates.io-index" 2388 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2389 | dependencies = [ 2390 | "proc-macro2", 2391 | "quote", 2392 | "syn", 2393 | ] 2394 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "patchy-bin" 3 | version = "1.3.0" 4 | edition = "2024" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["git", "github", "fork", "patchy"] 8 | categories = ["development-tools", "command-line-utilities"] 9 | authors = ["Nikita Revenco"] 10 | description = "A tool which makes it easy to declaratively manage personal forks by automatically merging pull requests" 11 | repository = "https://github.com/nik-rev/patchy" 12 | homepage = "https://github.com/nik-rev/patchy" 13 | 14 | [[bin]] 15 | name = "patchy" 16 | path = "src/main.rs" 17 | 18 | [lib] 19 | name = "patchy" 20 | 21 | [package.metadata.wix] 22 | upgrade-guid = "5868B076-2779-431F-9B51-0B12B052711C" 23 | path-guid = "17921C69-1BA1-422E-BCFC-0F5C960BEDF0" 24 | license = false 25 | eula = false 26 | 27 | [dependencies] 28 | anyhow = "1.0" 29 | serde = { version = "1.0", features = ["derive"] } 30 | toml = "0.8" 31 | tokio = { version = "1.42", features = ["full"] } 32 | reqwest = { version = "0.12", default-features = false, features = [ 33 | "blocking", 34 | "json", 35 | "rustls-tls", 36 | ] } 37 | serde_json = "1.0" 38 | tempfile = "3.14" 39 | rand = "0.8" 40 | colored = "2.2" 41 | indexmap = { version = "2.7", features = ["serde"] } 42 | once_cell = "1.17" 43 | documented = "0.9" 44 | log = "0.4" 45 | env_logger = "0.11" 46 | nutype = "0.6" 47 | clap = { version = "4.5.39", features = [ 48 | "derive", 49 | "wrap_help", 50 | "unstable-markdown", 51 | ] } 52 | tap = "1.0.1" 53 | dialoguer = "0.11.0" 54 | 55 | [dev-dependencies] 56 | pretty_assertions = "1.4" 57 | assert_cmd = "2.0.16" 58 | copy_dir = "0.1.3" 59 | predicates = "3.1.3" 60 | 61 | # The profile that 'dist' will build with 62 | [profile.dist] 63 | inherits = "release" 64 | codegen-units = 1 65 | lto = "fat" 66 | 67 | [lints.clippy] 68 | pedantic = { priority = -1, level = "warn" } 69 | nursery = { priority = -1, level = "warn" } 70 | missing_errors_doc = "allow" 71 | too_many_lines = "allow" 72 | must_use_candidate = "allow" 73 | missing_const_for_fn = "allow" 74 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # patchy 2 | 3 | ![image](https://github.com/user-attachments/assets/95546bae-5f61-4a14-b849-c4805bbe27af) 4 | 5 | `patchy` makes it easy to maintain personal forks in which you merge some pull requests of your liking to have more features than other people. 6 | 7 | - [Why should I use it?](#why-should-i-use-it) 8 | - [Usage](#usage) 9 | - [Config](#config) 10 | - [Patches](#patches) 11 | - [Versioning](#versioning) 12 | - [Installation](#installation) 13 | - [Binary](#binary) 14 | - [Homebrew](#homebrew) 15 | - [Cargo](#cargo) 16 | - [PowerShell](#powershell) 17 | - [Nix](#nix) 18 | - [Merge conflicts](#merge-conflicts) 19 | 20 | ## Why should I use it? 21 | 22 | - Merge multiple pull requests and commits into a single repo effortlessly 23 | - Sync those pull requests and the main branch with a single command 24 | - Add new pull requests and update existing ones easily 25 | 26 | ## Usage 27 | 28 | Go to a git repo, and initialize the config file: 29 | 30 | ```bash 31 | patchy init 32 | ``` 33 | 34 | Invoke `patchy` by running the following command: 35 | 36 | ```bash 37 | patchy run 38 | ``` 39 | 40 | ### Config 41 | 42 | I'm using the [Helix Editor](https://github.com/helix-editor/helix) but there are some pull requests which add awesome features. 43 | 44 | I found myself very frequently doing the same tasks in order to sync the 4 pull requests I like to use and keep them up to date. With patchy, I just run one command and it handles the rest. 45 | 46 | Here's my config: 47 | 48 | ```toml 49 | # main repository to fetch from 50 | repo = "helix-editor/helix" 51 | 52 | # the repository's branch 53 | remote-branch = "master" 54 | 55 | # This is the branch where you will see all result from patchy's work. Set it to any branch you want. 56 | # WARNING: Make sure you do not store any important work on this branch. It will be erased. 57 | local-branch = "patchy" 58 | 59 | # List of pull requests which you would like to merge 60 | # TIP: Add comments above pull requests to help yourself understand which PRs do what 61 | pull-requests = [ 62 | # syntax highlighting for nginx files 63 | "12309", 64 | # adds file explorer 65 | "11285", 66 | # global status line 67 | "8908", 68 | # command expansions 69 | "11164", 70 | ] 71 | 72 | # An optional list of patches to apply, more on them later 73 | patches = ["remove-tab"] 74 | ``` 75 | 76 | Running `patchy run` outputs: 77 | 78 | ![patchy output](https://github.com/user-attachments/assets/c0076588-6e57-4a80-9d05-955a4dff2580) 79 | 80 | With this, all I will need to do is run `patchy run` and it will automatically update all of the pull requests and sync the master branch to the latest changes. 81 | 82 | ### Patches 83 | 84 | You might want to apply some changes to your repo, but it's not a pull request. No worries! `patchy` is built for this. 85 | 86 | Create a patch from a commit: 87 | 88 | ```bash 89 | # obtain commit hashes e.g. from `git log` 90 | patchy gen-patch 91 | ``` 92 | 93 | For example, I'm running: 94 | 95 | ```bash 96 | patchy gen-patch 7bb8ec5a77769d88855d41dd5fecfaece54cf471 97 | ``` 98 | 99 | It generated a file, `.patchy/feat-swap-light-and-dark-colors.patch`: 100 | 101 | ```patch 102 | diff --git a/README.md m/README.md 103 | index 11a909b2..4eae6a8d 100644 104 | --- a/README.md 105 | +++ m/README.md 106 | @@ -2,8 +2,8 @@ 107 | 108 |

109 | 110 | - 111 | 112 | + 113 | Helix 114 | 115 |

116 | ``` 117 | 118 | To use your new `.patch`, edit your `.patchy/config.toml` like so: 119 | 120 | ```diff 121 | --- patches = [] 122 | +++ patches = [ "feat-swap-light-and-dark-colors" ] 123 | ``` 124 | 125 | ### Versioning 126 | 127 | Each pull request's branch contains commits. By default, we will always use the latest commit. However you can pin a commit to a specific version with the following syntax: 128 | 129 | ```toml 130 | remote-branch = "main @ cfd225baedbb5fb9cbc9742f91244fa50882b580" 131 | 132 | pull-requests = [ 133 | "145 @ fccc58957eece10d0818dfa000bf5123e26ee32f", 134 | "88 @ a556aeef3736a3b6b79bb9507d26224f5c0c3449" 135 | ] 136 | ``` 137 | 138 | Where the hashes represent each `sha1` hash of every commit. 139 | 140 | This is handy if you don't want things to randomly break when some of the pull requests push a new change. 141 | 142 | ## Installation 143 | 144 | Patchy can be installed on Linux, Windows and macOS. 145 | 146 | ### Binary 147 | 148 | Install the binary directly into your system, available for macOS and Linux. 149 | 150 | Recommended for Linux users. 151 | 152 | ```bash 153 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/nik-rev/patchy/releases/latest/download/patchy-bin-installer.sh | sh 154 | ``` 155 | 156 | ### Homebrew 157 | 158 | Recommended for macOS users. 159 | 160 | ```bash 161 | brew install nik-rev/tap/patchy-bin 162 | ``` 163 | 164 | ### Cargo 165 | 166 | ```bash 167 | cargo install patchy-bin 168 | ``` 169 | 170 | ### PowerShell 171 | 172 | Recommended for Windows users. 173 | 174 | ```powershell 175 | powershell -ExecutionPolicy ByPass -c "irm https://github.com/nik-rev/patchy/releases/latest/download/patchy-installer.ps1 | iex" 176 | ``` 177 | 178 | ### Nix 179 | 180 | ```bash 181 | nix profile install github:nik-rev/patchy/main 182 | ``` 183 | 184 |
185 | 186 | 187 | Example usage with flakes 188 | 189 | 190 | If the software you are using has a `flake.nix` which automatically builds this software, then using patchy with it is straightforward. 191 | 192 | 1. Use patchy to create your own remote fork. 193 | 194 | Let's say you fork the [`helix-editor/helix`](https://github.com/helix-editor/helix) and your fork is located at `your-username/helix`, the patchy branch is called `patchy` 195 | 196 | 1. Add your fork's input in your `flake.nix` as follows: 197 | 198 | ```nix 199 | inputs.helix.url = "github:your-username/helix/patchy"; 200 | ``` 201 | 202 | 1. Use the input in your home-manager: 203 | 204 | ```nix 205 | programs.helix.package = inputs.helix.packages.${pkgs.system}.helix; 206 | ``` 207 | 208 | This is easier when the target repository has a `flake.nix` which fully builds the software. Which, the [`helix-editor/helix`](https://github.com/helix-editor/helix) does have for example. 209 | 210 |
211 | 212 | ## Merge conflicts 213 | 214 | If you merge a lot of PRs, it's likely some of them will clash with eachother and there will be conflicts. 215 | 216 | Say you merge 10 pull requests, 3 of which couldn't be merged due to merge conflicts. The other 7 will be merged, while branches for those 3 will exist and you will be able to merge them yourself. 217 | 218 | ```text 219 | ✗ Could not merge branch 11164/command-expansion into the current branch for pull request #11164 Command expansion v2 since the merge is non-trivial. 220 | You will need to merge it yourself: 221 | git merge --squash 11164/command-expansion 222 | Note: To learn how to merge only once and re-use for subsequent invocations of patchy, see Merge conflicts (github) 223 | ``` 224 | 225 |
226 | 227 | 228 | 229 | Fixing merge conflicts and retaining the fixes, declaratively 230 | 231 | 232 | 233 | Okay, now merge the branch: 234 | 235 | ```sh 236 | git merge --squash 11164/command-expansion 237 | ``` 238 | 239 | We need `--squash` since `patchy gen-patch` does not work on Merge commits. But using `--squash` makes it into a non-merge commit. 240 | 241 | Now fix the merge conflicts. 242 | 243 | Then, commit your changes: 244 | 245 | ```sh 246 | git commit -m "merge branch 11164/command-expansion" 247 | ``` 248 | 249 | This creates the following commit: 250 | 251 | ```text 252 | 2fb6c3c7 (HEAD -> patchy) Merge branch '11164/command-expansion' into patchy 253 | ``` 254 | 255 | Now that you have this commit, let's generate a `.patch` file for it: 256 | 257 | ```text 258 | > patchy gen-patch 2fb6c3c7 --patch-filename=merge-11164 259 | ✓ Created patch file at .patchy/merge-11164.patch 260 | ``` 261 | 262 | Now, you can go ahead and add the patch to your config: 263 | 264 | ```diff 265 | - pull-requests = [ "1234", "1111", "11164" ] 266 | + pull-requests = [ "1234", "1111" ] 267 | 268 | - patches = [ ] 269 | + patches = [ "merge-11164" ] 270 | ``` 271 | 272 | When you do this, you won't need to solve this merge conflict anymore as the `merge-11164` patch includes both the entire pull request as well as how it was merged. 273 | 274 | Note that if you change the _order_ of your pull requests in `pull-requests` you may see merge conflicts again. It is recommended to keep the order the same once you fix the conflicts. 275 | 276 |
277 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | }: 4 | let 5 | manifest = pkgs.lib.importTOML ./Cargo.toml; 6 | in 7 | pkgs.rustPlatform.buildRustPackage { 8 | pname = manifest.lib.name; 9 | version = manifest.package.version; 10 | 11 | cargoLock.lockFile = ./Cargo.lock; 12 | 13 | src = pkgs.lib.cleanSource ./.; 14 | } 15 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.28.0" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = ["shell", "powershell", "npm", "homebrew", "msi"] 12 | # A GitHub repo to push Homebrew formulas to 13 | tap = "nik-rev/homebrew-tap" 14 | # Target platforms to build apps for (Rust target-triple syntax) 15 | targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] 16 | # A namespace to use when publishing this package to the npm registry 17 | npm-scope = "@nikitarevenco" 18 | # Path that installers should place binaries in 19 | install-path = "CARGO_HOME" 20 | # Publish jobs to run in CI 21 | publish-jobs = ["homebrew"] 22 | # Whether to install an updater program 23 | install-updater = true 24 | -------------------------------------------------------------------------------- /example-config.toml: -------------------------------------------------------------------------------- 1 | # Main github repository to fetch from. 2 | # This is going to be our base, into which we merge patches and pull requests. 3 | # 4 | # Examples 5 | # 6 | # repo = "helix-editor/helix" 7 | # repo = "microsoft/vscode" 8 | 9 | repo = "" 10 | 11 | # The main repository's branch 12 | # 13 | # Examples 14 | # 15 | # remote-branch = "master" 16 | # remote-branch = "main" 17 | # 18 | # The above always fetch the latest commit. 19 | # -> To use a specific commit, use the following syntax: 20 | # remote-branch = " @ " 21 | # 22 | # so for example: 23 | # 24 | # remote-branch = "master @ fccc58957eece10d0818dfa000bf5123e26ee32f" 25 | 26 | remote-branch = "main" 27 | 28 | # Branch which patchy will use to do all of its work on 29 | # 30 | # Examples 31 | # 32 | # local-branch = "some-branch-1234" 33 | 34 | local-branch = "patchy" 35 | 36 | # list of pull requests numbers which you would like to merge into the repository and branch you have specified previously 37 | # 38 | # Examples 39 | # 40 | # pull-requests = [ "12254", "10000", "8145" ] 41 | # 42 | # The above always fetch the latest commit for each pull request. 43 | # 44 | # To use a specific commit, use the following syntax: 45 | # " @ " 46 | # 47 | # so for example: 48 | # 49 | # pull-requests = [ 50 | # "12254", 51 | # "10000 @ a556aeef3736a3b6b79bb9507d26224f5c0c3449", 52 | # "8145 @ 840cb7e9982699ff107d0577691b5db9806b3b66" 53 | # ] 54 | 55 | pull-requests = [] 56 | 57 | # Optional: A list of patches to apply 58 | # 59 | # A patch allows you to do specify custom commits and not have to rely on there being a pull request for that change 60 | # 61 | # You can generate patches from a commit with: `patchy gen-patch `. 62 | # See `patchy gen-patch --help` for more info. 63 | # 64 | # Examples 65 | # 66 | # With the below config, patchy will look for the following files: 67 | # - `.patchy/my-patch123.patch` 68 | # - `.patchy/another-patch.patch` 69 | # - `.patchy/1234.patch` 70 | # 71 | # patches = [ "my-patch123", "another-patch", "1234" ] 72 | 73 | # patches = [] 74 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1748460289, 24 | "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1748658947, 52 | "narHash": "sha256-F+nGITu6D7RswJlm8qCuU1PCuOSgDeAqaDKWW1n1jmQ=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "fc82ce758cc5df6a6d5d24e75710321cdbdc787a", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | rust-overlay = { 6 | url = "github:oxalica/rust-overlay"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | }; 10 | 11 | outputs = 12 | { 13 | self, 14 | nixpkgs, 15 | flake-utils, 16 | rust-overlay, 17 | }: 18 | flake-utils.lib.eachDefaultSystem ( 19 | system: 20 | let 21 | overlays = [ (import rust-overlay) ]; 22 | pkgs = import nixpkgs { 23 | inherit system overlays; 24 | }; 25 | nativeBuildInputs = [ 26 | (pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) 27 | ]; 28 | in 29 | with pkgs; 30 | { 31 | devShells.default = mkShell { 32 | inherit nativeBuildInputs; 33 | # This is needed or rust-analyzer will not work correctly. 34 | # Source: https://discourse.nixos.org/t/rust-src-not-found-and-other-misadventures-of-developing-rust-on-nixos/11570 35 | RUST_SRC_PATH = "${ 36 | pkgs.rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; } 37 | }/lib/rustlib/src/rust/library"; 38 | }; 39 | packages.default = callPackage ./. { }; 40 | } 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | toolchain.channel = "1.86" 2 | toolchain.components = ["rust-analyzer", "clippy", "rustfmt"] 3 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | }: 4 | pkgs.mkShell { 5 | # Get dependencies from the main package 6 | inputsFrom = [ (pkgs.callPackage ./default.nix { }) ]; 7 | # Additional tooling 8 | buildInputs = with pkgs; [ 9 | git 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /src/backup.rs: -------------------------------------------------------------------------------- 1 | //! Backup files that we are about to override, to make sure the user does not 2 | //! lose any work 3 | use std::ffi::OsString; 4 | use std::fs::{self, File, ReadDir}; 5 | use std::io::Write as _; 6 | use std::path::PathBuf; 7 | 8 | use tempfile::tempfile; 9 | 10 | use crate::CONFIG_ROOT; 11 | use crate::git::GIT_ROOT; 12 | 13 | pub fn files(config_files: ReadDir) -> anyhow::Result> { 14 | let mut backups = Vec::new(); 15 | 16 | for entry in config_files { 17 | let config_file = entry?; 18 | 19 | let path = config_file.path(); 20 | let contents = fs::read_to_string(&path)?; 21 | 22 | let filename = config_file.file_name(); 23 | let mut destination_backed_up = tempfile()?; 24 | 25 | write!(destination_backed_up, "{contents}")?; 26 | 27 | backups.push((filename, destination_backed_up, contents)); 28 | } 29 | 30 | Ok(backups) 31 | } 32 | pub fn restore(file_name: &OsString, contents: &str) -> anyhow::Result<()> { 33 | let path = GIT_ROOT.join(PathBuf::from(CONFIG_ROOT.as_str()).join(file_name)); 34 | let mut file = File::create(&path)?; 35 | 36 | write!(file, "{contents}")?; 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! Parse the command-line arguments 2 | 3 | use std::{path::PathBuf, str::FromStr}; 4 | 5 | use clap::{ 6 | Parser, Subcommand, 7 | builder::styling::{AnsiColor, Effects}, 8 | }; 9 | use tap::Pipe as _; 10 | 11 | use crate::{commands, commit::Commit}; 12 | 13 | /// A tool which makes it easy to declaratively manage personal forks by automatically merging pull requests 14 | #[derive(Parser)] 15 | #[command(version, styles = STYLES, long_about = None)] 16 | pub struct Cli { 17 | #[command(subcommand)] 18 | pub command: Command, 19 | } 20 | 21 | #[derive(Subcommand)] 22 | pub enum Command { 23 | /// Create example config file 24 | Init, 25 | /// Invoke patchy 26 | Run { 27 | /// Do not prompt when overwriting local-branch specified in the config 28 | #[arg(short, long)] 29 | yes: bool, 30 | }, 31 | /// Generate a .patch file from a commit hash 32 | GenPatch { 33 | /// Transform this commit into a `.patch` file 34 | commit: Commit, 35 | /// Choose a custom file name for the `.patch` file 36 | #[arg(short, long)] 37 | filename: Option, 38 | }, 39 | /// Fetch pull request for a GitHub repository as a local branch 40 | PrFetch { 41 | /// Fetch PR of this number 42 | pr: u32, 43 | /// The remote branch in the format `repo-owner/repo/branch` 44 | /// 45 | /// The final part (`/branch`) is optional and defaults to `main` 46 | /// 47 | /// If omitted, uses the `origin` of the current repository 48 | remote: Option, 49 | /// Choose a custom branch name for the fetched repo 50 | #[arg(short, long)] 51 | branch: Option, 52 | /// When fetching this PR, reset to this commit 53 | #[arg(short = 'C', long)] 54 | commit: Option, 55 | /// Check out the first fetched pull request 56 | #[arg(short, long)] 57 | checkout: bool, 58 | }, 59 | /// Fetch branch for a GitHub repository as a local branch 60 | BranchFetch { 61 | /// The remote branch in the format `repo-owner/repo/branch` 62 | /// 63 | /// The final part (`/branch`) is optional and defaults to `main` 64 | remote: Remote, 65 | /// When fetching this branch, reset to this commit 66 | #[arg(short = 'C', long)] 67 | commit: Option, 68 | /// Check out the fetched branch 69 | #[arg(short, long)] 70 | checkout: bool, 71 | }, 72 | } 73 | 74 | impl Command { 75 | pub async fn execute(self) -> anyhow::Result<()> { 76 | match self { 77 | Self::Init => commands::init()?, 78 | Self::Run { yes } => commands::run(yes).await?, 79 | Self::GenPatch { commit, filename } => { 80 | commands::gen_patch(commit, filename)?; 81 | } 82 | Self::PrFetch { 83 | pr, 84 | remote, 85 | branch, 86 | commit, 87 | checkout, 88 | } => commands::pr_fetch(pr, remote, branch, commit, checkout).await?, 89 | Self::BranchFetch { 90 | remote, 91 | commit, 92 | checkout, 93 | } => commands::branch_fetch(remote, commit, checkout).await?, 94 | } 95 | 96 | Ok(()) 97 | } 98 | } 99 | 100 | /// Styles for the CLI 101 | const STYLES: clap::builder::Styles = clap::builder::Styles::styled() 102 | .header(AnsiColor::BrightGreen.on_default().effects(Effects::BOLD)) 103 | .usage(AnsiColor::BrightGreen.on_default().effects(Effects::BOLD)) 104 | .literal(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD)) 105 | .placeholder(AnsiColor::BrightCyan.on_default()) 106 | .error(AnsiColor::BrightRed.on_default().effects(Effects::BOLD)) 107 | .valid(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD)) 108 | .invalid(AnsiColor::BrightYellow.on_default().effects(Effects::BOLD)); 109 | 110 | /// Represents a single branch 111 | #[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord)] 112 | pub struct Branch { 113 | /// Name of the GitHub owner of the repository 114 | pub repo_owner: String, 115 | /// Name of the repository this branch belongs to 116 | pub repo_name: String, 117 | /// Name of this branch in the remote 118 | pub name: String, 119 | /// When fetching this PR, reset to this commit 120 | pub commit: Option, 121 | } 122 | 123 | /// Example: `helix-editor/helix/master` 124 | #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Default)] 125 | pub struct Remote { 126 | /// Example: `helix-editor` 127 | pub owner: String, 128 | /// Example: `helix` 129 | pub repo: String, 130 | /// Example: `master` 131 | pub branch: String, 132 | } 133 | 134 | impl Remote { 135 | const DEFAULT_BRANCH: &str = "main"; 136 | } 137 | 138 | impl FromStr for Remote { 139 | type Err = String; 140 | 141 | fn from_str(s: &str) -> Result { 142 | s.split_once('/') 143 | .ok_or_else(|| "Expected format: `owner/repo`".to_string())? 144 | .pipe(|(owner, rest)| { 145 | rest.split_once('/') 146 | .unwrap_or((rest, Self::DEFAULT_BRANCH)) 147 | .pipe(|(repo, branch)| Self { 148 | owner: owner.to_string(), 149 | repo: repo.to_string(), 150 | branch: branch.to_string(), 151 | }) 152 | }) 153 | .pipe(Ok) 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod test { 159 | use super::*; 160 | 161 | #[test] 162 | fn parse_remote() { 163 | assert_eq!( 164 | "helix-editor/helix".parse::().unwrap(), 165 | Remote { 166 | owner: "helix-editor".to_string(), 167 | repo: "helix".to_string(), 168 | branch: "main".to_string() 169 | } 170 | ); 171 | assert_eq!( 172 | "helix-editor/helix/master".parse::().unwrap(), 173 | Remote { 174 | owner: "helix-editor".to_string(), 175 | repo: "helix".to_string(), 176 | branch: "master".to_string() 177 | } 178 | ); 179 | "helix-editor".parse::().unwrap_err(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/commands/branch_fetch.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize as _; 2 | 3 | use crate::cli::Remote; 4 | use crate::commit::Commit; 5 | use crate::git::{fetch_branch, git}; 6 | use crate::{fail, success}; 7 | 8 | pub async fn branch_fetch( 9 | remote: Remote, 10 | commit: Option, 11 | checkout: bool, 12 | ) -> anyhow::Result<()> { 13 | match fetch_branch(&remote, commit.as_ref()).await { 14 | Ok((_, info)) => { 15 | success!( 16 | "Fetched branch {}/{}/{} available at branch {}{}", 17 | remote.owner, 18 | remote.repo, 19 | info.branch.upstream_branch_name, 20 | info.branch.local_branch_name.bright_cyan(), 21 | commit 22 | .map(|commit_hash| { 23 | format!(", at commit {}", commit_hash.as_ref().bright_yellow()) 24 | }) 25 | .unwrap_or_default() 26 | ); 27 | 28 | // Attempt to cleanup after ourselves 29 | let _ = git(["remote", "remove", &info.remote.local_remote_alias]); 30 | 31 | if checkout { 32 | if let Err(cant_checkout) = git(["checkout", &info.branch.local_branch_name]) { 33 | fail!( 34 | "Could not check out branch 35 | {}:\n{cant_checkout}", 36 | info.branch.local_branch_name 37 | ); 38 | } else { 39 | success!("checked out: {}", info.branch.local_branch_name); 40 | } 41 | } 42 | } 43 | Err(err) => { 44 | fail!("{err}"); 45 | } 46 | } 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/gen_patch.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::PathBuf; 3 | 4 | use anyhow::bail; 5 | 6 | use crate::commit::Commit; 7 | use crate::git::{GIT_ROOT, git}; 8 | use crate::utils::normalize_commit_msg; 9 | use crate::{CONFIG_ROOT, note, success}; 10 | 11 | pub fn gen_patch(commit: Commit, filename: Option) -> anyhow::Result<()> { 12 | let config_path = GIT_ROOT.join(CONFIG_ROOT.as_str()); 13 | 14 | if !config_path.exists() { 15 | note!( 16 | "Config directory {} does not exist, creating it...", 17 | config_path.to_string_lossy() 18 | ); 19 | fs::create_dir_all(&config_path)?; 20 | } 21 | 22 | // 1. if the user provides a custom filename for the patch file, use that 23 | // 2. otherwise use the commit message 24 | // 3. if all fails use the commit hash 25 | let patch_filename = filename.map_or_else( 26 | || { 27 | git(["log", "--format=%B", "--max-count=1", commit.as_ref()]).map_or_else( 28 | |_| commit.clone().into_inner(), 29 | |commit_msg| normalize_commit_msg(&commit_msg), 30 | ) 31 | }, 32 | |filename| filename.to_str().unwrap_or_default().to_string(), 33 | ); 34 | 35 | let patch_filename = format!("{patch_filename}.patch"); 36 | 37 | let patch_file_path = config_path.join(&patch_filename); 38 | 39 | // Paths are UTF-8 encoded. If we cannot convert to UTF-8 that means it is not a 40 | // valid path 41 | let Some(patch_file_path_str) = patch_file_path.as_os_str().to_str() else { 42 | bail!("Not a valid path: {patch_file_path:?}"); 43 | }; 44 | 45 | if let Err(err) = git([ 46 | "format-patch", 47 | "-1", 48 | &commit.clone().into_inner(), 49 | "--output", 50 | patch_file_path_str, 51 | ]) { 52 | bail!( 53 | "Could not get patch output for patch {}\n{err}", 54 | commit.into_inner() 55 | ); 56 | } 57 | 58 | success!( 59 | "Created patch file at {}", 60 | patch_file_path.to_string_lossy() 61 | ); 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /src/commands/init.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | use std::io::Write as _; 3 | 4 | use anyhow::bail; 5 | use colored::Colorize as _; 6 | 7 | use crate::git::GIT_ROOT; 8 | use crate::{CONFIG_FILE, CONFIG_ROOT, confirm_prompt, success}; 9 | 10 | pub fn init() -> anyhow::Result<()> { 11 | let example_config = include_bytes!("../../example-config.toml"); 12 | 13 | let config_path = GIT_ROOT.join(CONFIG_ROOT.as_str()); 14 | 15 | let config_file_path = config_path.join(CONFIG_FILE); 16 | 17 | if config_file_path.exists() 18 | && !confirm_prompt!( 19 | "File {} already exists. Overwrite it?", 20 | config_file_path.to_string_lossy().bright_blue(), 21 | ) 22 | { 23 | bail!("Did not overwrite {}", config_file_path.display()); 24 | } 25 | 26 | fs::create_dir_all(config_path)?; 27 | 28 | let mut file = File::create(&config_file_path)?; 29 | 30 | file.write_all(example_config)?; 31 | 32 | success!("Created config file {}", config_file_path.display()); 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod branch_fetch; 2 | pub mod gen_patch; 3 | pub mod init; 4 | pub mod pr_fetch; 5 | pub mod run; 6 | 7 | pub use branch_fetch::branch_fetch; 8 | pub use gen_patch::gen_patch; 9 | pub use init::init; 10 | pub use pr_fetch::pr_fetch; 11 | pub use run::run; 12 | -------------------------------------------------------------------------------- /src/commands/pr_fetch.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context as _, anyhow}; 2 | use colored::Colorize as _; 3 | 4 | use crate::cli::Remote; 5 | use crate::commit::Commit; 6 | use crate::git::{fetch_pull_request, git}; 7 | use crate::utils::display_link; 8 | use crate::{fail, success}; 9 | 10 | /// Allow users to prefix their PRs with octothorpe, e.g. #12345 instead of 11 | /// 12345. This is just a QOL addition since some people may use it due to habit 12 | pub fn ignore_octothorpe(arg: &str) -> String { 13 | if arg.starts_with('#') { 14 | arg.get(1..).unwrap_or_default() 15 | } else { 16 | arg 17 | } 18 | .into() 19 | } 20 | 21 | pub async fn pr_fetch( 22 | pr: u32, 23 | remote: Option, 24 | branch: Option, 25 | commit: Option, 26 | checkout: bool, 27 | ) -> anyhow::Result<()> { 28 | pub const GITHUB_REMOTE_PREFIX: &str = "git@github.com:"; 29 | pub const GITHUB_REMOTE_SUFFIX: &str = ".git"; 30 | 31 | // The user hasn't provided a custom remote, so we're going to try `origin` 32 | let remote = remote.map_or_else( 33 | || -> anyhow::Result { 34 | let remote = git(["remote", "get-url", "origin"])?; 35 | let err = || anyhow!("git command returned invalid remote. Output {remote}"); 36 | if remote.starts_with(GITHUB_REMOTE_PREFIX) && remote.ends_with(GITHUB_REMOTE_SUFFIX) { 37 | let start = GITHUB_REMOTE_PREFIX.len(); 38 | let end = remote.len() - GITHUB_REMOTE_SUFFIX.len(); 39 | let (owner, repo) = remote 40 | .get(start..end) 41 | .and_then(|x| x.split_once('/')) 42 | .with_context(err)?; 43 | Ok(Remote { 44 | owner: owner.to_string(), 45 | repo: repo.to_string(), 46 | branch: "main".to_string(), 47 | }) 48 | } else { 49 | Err(err()) 50 | } 51 | }, 52 | Ok, 53 | )?; 54 | 55 | match fetch_pull_request( 56 | &format!("{}/{}", remote.owner, remote.repo), 57 | // TODO: make fetch_pull_request accept a u32 instead 58 | &pr.to_string(), 59 | branch.as_deref(), 60 | commit.as_ref(), 61 | ) 62 | .await 63 | { 64 | Ok((response, info)) => { 65 | success!( 66 | "Fetched pull request {} available at branch {}{}", 67 | display_link( 68 | &format!( 69 | "{}{}{}{}", 70 | "#".bright_blue(), 71 | pr.to_string().bright_blue(), 72 | " ".bright_blue(), 73 | response.title.bright_blue().italic() 74 | ), 75 | &response.html_url 76 | ), 77 | info.branch.local_branch_name.bright_cyan(), 78 | commit 79 | .clone() 80 | .map(|commit_hash| { 81 | format!(", at commit {}", commit_hash.as_ref().bright_yellow()) 82 | }) 83 | .unwrap_or_default() 84 | ); 85 | 86 | // Attempt to cleanup after ourselves 87 | let _ = git(["remote", "remove", &info.remote.local_remote_alias]); 88 | 89 | if checkout { 90 | if let Err(cant_checkout) = git(["checkout", &info.branch.local_branch_name]) { 91 | fail!( 92 | "Could not check out branch {}:\n{cant_checkout}", 93 | info.branch.local_branch_name 94 | ); 95 | } else { 96 | success!( 97 | "Automatically checked out the first branch: {}", 98 | info.branch.local_branch_name 99 | ); 100 | } 101 | } 102 | } 103 | Err(err) => { 104 | fail!("{err}"); 105 | } 106 | } 107 | 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /src/commands/run.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | use std::io::Write as _; 3 | use std::path::PathBuf; 4 | 5 | use anyhow::{anyhow, bail}; 6 | use colored::Colorize as _; 7 | 8 | use crate::commands::pr_fetch::ignore_octothorpe; 9 | use crate::commit::Commit; 10 | use crate::git::{self, GIT_ROOT, git}; 11 | use crate::note; 12 | use crate::types::{Branch, BranchAndRemote, Configuration, Remote}; 13 | use crate::utils::{display_link, with_uuid}; 14 | use crate::{APP_NAME, CONFIG_FILE, CONFIG_ROOT, commands, confirm_prompt, fail, success}; 15 | 16 | /// Parses user inputs of the form `` 17 | /// 18 | /// Returns the user's input but also the commit hash if it exists 19 | pub fn parse_if_maybe_hash(input: &str, syntax: &str) -> (String, Option) { 20 | let parts: Vec<_> = input.split(syntax).collect(); 21 | 22 | let len = parts.len(); 23 | 24 | if len == 1 { 25 | // The string does not contain the , so the user chose to use the latest 26 | // commit rather than a specific one 27 | (input.into(), None) 28 | } else { 29 | // They want to use a specific commit 30 | let output: String = parts[0..len - 1].iter().map(|s| String::from(*s)).collect(); 31 | let commit_hash = (parts[len - 1].to_owned()).parse::().ok(); 32 | (output, commit_hash) 33 | } 34 | } 35 | 36 | pub async fn run(yes: bool) -> anyhow::Result<()> { 37 | println!(); 38 | let root = CONFIG_ROOT.as_str(); 39 | let config_path = GIT_ROOT.join(root); 40 | 41 | let config_file_path = config_path.join(CONFIG_FILE); 42 | 43 | let Ok(config_raw) = fs::read_to_string(config_file_path.clone()) else { 44 | fail!("Could not find configuration file at {root}/{CONFIG_FILE}",); 45 | 46 | // We don't want to have *any* sort of prompt when using the -y flag since that 47 | // would be problematic in scripts 48 | if !yes && confirm_prompt!("Would you like us to run `patchy init` to initialize it?",) { 49 | commands::init()?; 50 | } else if yes { 51 | note!("You can create it with `patchy init`",); 52 | } else { 53 | // user said "no" in the prompt, so we don't do any initializing 54 | } 55 | 56 | // We don't want to read the default configuration file as config_raw. Since 57 | // it's empty there's no reason why the user would want to run it. 58 | 59 | return Ok(()); 60 | }; 61 | 62 | log::trace!("Using configuration file {config_file_path:?}"); 63 | 64 | let config = toml::from_str::(&config_raw).map_err(|err| { 65 | anyhow!("Could not parse `{root}/{CONFIG_FILE}` configuration file:\n{err}",) 66 | })?; 67 | 68 | let (remote_branch, commit_hash) = parse_if_maybe_hash(&config.remote_branch, " @ "); 69 | 70 | if config.repo.is_empty() { 71 | bail!( 72 | "You haven't specified a `repo` in your config, which can be for example: 73 | - `helix-editor/helix` 74 | - `microsoft/vscode` 75 | 76 | For more information see this guide: https://github.com/nik-rev/patchy/blob/main/README.md" 77 | ); 78 | } 79 | 80 | let config_files = fs::read_dir(&config_path).map_err(|err| { 81 | anyhow!( 82 | "Failed to read files in directory `{}`:\n{err}", 83 | &config_path.display() 84 | ) 85 | })?; 86 | 87 | let backed_up_files = { 88 | let mut backups = Vec::new(); 89 | 90 | for config_file in config_files { 91 | let config_file = config_file?; 92 | 93 | let path = config_file.path(); 94 | let backup = fs::read_to_string(&path) 95 | .map_err(|err| anyhow!("{err}")) 96 | .and_then(|contents| { 97 | let filename = config_file.file_name(); 98 | let mut destination_backed_up = 99 | tempfile::tempfile().map_err(|err| anyhow!("{err}"))?; 100 | 101 | write!(destination_backed_up, "{contents}")?; 102 | 103 | Ok((filename, destination_backed_up, contents)) 104 | }) 105 | .map_err(|err| { 106 | anyhow!("Failed to create backups for configuration files:\n{err}") 107 | })?; 108 | 109 | backups.push(backup); 110 | } 111 | 112 | backups 113 | }; 114 | 115 | let info = BranchAndRemote { 116 | branch: Branch { 117 | upstream_branch_name: remote_branch.clone(), 118 | local_branch_name: with_uuid(&remote_branch), 119 | }, 120 | remote: Remote { 121 | repository_url: format!("https://github.com/{}.git", config.repo), 122 | local_remote_alias: with_uuid(&config.repo), 123 | }, 124 | }; 125 | 126 | git::add_remote_branch(&info, commit_hash.as_ref())?; 127 | 128 | let previous_branch = git::checkout_from_remote( 129 | &info.branch.local_branch_name, 130 | &info.remote.local_remote_alias, 131 | )?; 132 | 133 | if config.pull_requests.is_empty() { 134 | log::info!( 135 | "You haven't specified any pull requests to fetch in your config, {}", 136 | display_link( 137 | "see the instructions on how to configure patchy.", 138 | "https://github.com/nik-rev/patchy?tab=readme-ov-file#config" 139 | ) 140 | ); 141 | } else { 142 | // TODO: make this concurrent, see https://users.rust-lang.org/t/processing-subprocesses-concurrently/79638/3 143 | // Git cannot handle multiple threads executing commands in the same repository, 144 | // so we can't use threads, but we can run processes in the background 145 | for pull_request in &config.pull_requests { 146 | let pull_request = ignore_octothorpe(pull_request); 147 | let (pull_request, commit_hash) = parse_if_maybe_hash(&pull_request, " @ "); 148 | // TODO: refactor this to not use such deep nesting 149 | match git::fetch_pull_request(&config.repo, &pull_request, None, commit_hash.as_ref()) 150 | .await 151 | { 152 | Ok((response, info)) => { 153 | match git::merge_pull_request( 154 | &info, 155 | &pull_request, 156 | &response.title, 157 | &response.html_url, 158 | ) { 159 | Ok(()) => { 160 | success!( 161 | "Merged pull request {}", 162 | display_link( 163 | &format!( 164 | "{}{}{}{}", 165 | "#".bright_blue(), 166 | pull_request.bright_blue(), 167 | " ".bright_blue(), 168 | &response.title.bright_blue().italic() 169 | ), 170 | &response.html_url 171 | ), 172 | ); 173 | } 174 | Err(err) => { 175 | fail!("{err}"); 176 | } 177 | } 178 | } 179 | Err(err) => { 180 | fail!("Could not fetch branch from remote\n{err}"); 181 | } 182 | } 183 | } 184 | } 185 | 186 | if let Err(err) = fs::create_dir_all(GIT_ROOT.join(CONFIG_ROOT.as_str())) { 187 | git(["checkout", &previous_branch])?; 188 | 189 | git::delete_remote_and_branch( 190 | &info.remote.local_remote_alias, 191 | &info.branch.local_branch_name, 192 | )?; 193 | 194 | bail!("Could not create directory {}\n{err}", CONFIG_ROOT.as_str()); 195 | } 196 | 197 | for (file_name, _file, contents) in &backed_up_files { 198 | let path = GIT_ROOT.join(PathBuf::from(CONFIG_ROOT.as_str()).join(file_name)); 199 | let mut file = 200 | File::create(&path).map_err(|err| anyhow!("failed to restore backup: {err}"))?; 201 | 202 | write!(file, "{contents}")?; 203 | } 204 | 205 | // apply patches if they exist 206 | for patch in config.patches { 207 | let file_name = GIT_ROOT 208 | .join(CONFIG_ROOT.as_str()) 209 | .join(format!("{patch}.patch")); 210 | if !file_name.exists() { 211 | fail!("Could not find patch {patch}, skipping"); 212 | continue; 213 | } 214 | 215 | if let Err(err) = git(["am", "--keep-cr", "--signoff", &file_name.to_string_lossy()]) { 216 | git(["am", "--abort"])?; 217 | return Err(anyhow!("Could not apply patch {patch}, skipping\n{err}")); 218 | } 219 | 220 | let last_commit_message = git(["log", "-1", "--format=%B"])?; 221 | success!( 222 | "Applied patch {patch} {}", 223 | last_commit_message 224 | .lines() 225 | .next() 226 | .unwrap_or_default() 227 | .bright_blue() 228 | .italic() 229 | ); 230 | } 231 | 232 | git(["add", CONFIG_ROOT.as_str()])?; 233 | git([ 234 | "commit", 235 | "--message", 236 | &format!("{APP_NAME}: Restore configuration files"), 237 | ])?; 238 | 239 | let temporary_branch = with_uuid("temp-branch"); 240 | 241 | git(["switch", "--create", &temporary_branch])?; 242 | 243 | git::delete_remote_and_branch( 244 | &info.remote.local_remote_alias, 245 | &info.branch.local_branch_name, 246 | )?; 247 | 248 | if yes 249 | || confirm_prompt!( 250 | "Overwrite branch {}? This is irreversible.", 251 | config.local_branch.cyan() 252 | ) 253 | { 254 | // forcefully renames the branch we are currently on into the branch specified 255 | // by the user. WARNING: this is a destructive action which erases the 256 | // original branch 257 | git([ 258 | "branch", 259 | "--move", 260 | "--force", 261 | &temporary_branch, 262 | &config.local_branch, 263 | ])?; 264 | if yes { 265 | note!( 266 | "Automatically overwrote branch {} since you supplied the {} flag", 267 | config.local_branch.cyan(), 268 | "--yes".bright_magenta() 269 | ); 270 | } 271 | success!("Success!"); 272 | } else { 273 | let overwrite_command = format!( 274 | "git branch --move --force {temporary_branch} {}", 275 | config.local_branch 276 | ); 277 | note!( 278 | "You can still manually overwrite {} with:\n {overwrite_command}\n", 279 | config.local_branch.cyan(), 280 | ); 281 | return Ok(()); 282 | } 283 | 284 | Ok(()) 285 | } 286 | -------------------------------------------------------------------------------- /src/commit.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use nutype::nutype; 4 | 5 | /// Represents a git commit 6 | #[nutype( 7 | validate(not_empty, predicate = is_valid_commit_hash), 8 | derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, AsRef) 9 | )] 10 | pub struct Commit(String); 11 | 12 | /// Does not check if the commit hash exists, just checks if it is potentially 13 | /// valid A commit hash can consist of `a-f` and `0-9` characters 14 | pub fn is_valid_commit_hash(hash: &str) -> bool { 15 | hash.chars().all(|ch| ch.is_ascii_hexdigit()) 16 | } 17 | 18 | impl FromStr for Commit { 19 | type Err = String; 20 | 21 | fn from_str(s: &str) -> std::result::Result { 22 | Self::try_new(s).map_err(|err| match err { 23 | CommitError::NotEmptyViolated => "commit cannot be empty".to_string(), 24 | CommitError::PredicateViolated => format!("invalid commit hash: {s}"), 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for interacting with Git via spawning processes 2 | //! 3 | //! TODO: 4 | //! - Extract into a separate module, put it behind some more nice API 5 | //! - Use `gix`? Or anyways, we could go without spawning an entire process each 6 | //! time we want to interact with Git 7 | use std::path::{Path, PathBuf}; 8 | use std::process::{self, Output}; 9 | use std::sync::LazyLock; 10 | use std::{env, io}; 11 | 12 | use anyhow::{Result, anyhow, bail}; 13 | use colored::Colorize as _; 14 | use reqwest::Client; 15 | 16 | use crate::commit::Commit; 17 | use crate::fail; 18 | use crate::types::{BranchAndRemote, GitHubResponse, Remote, Repo}; 19 | use crate::utils::{display_link, make_request, normalize_commit_msg, with_uuid}; 20 | 21 | pub fn spawn_git(args: &[&str], git_dir: &Path) -> Result { 22 | process::Command::new("git") 23 | .args(args) 24 | .current_dir(git_dir) 25 | .output() 26 | } 27 | 28 | pub fn get_git_output(output: &Output, args: &[&str]) -> anyhow::Result { 29 | if output.status.success() { 30 | Ok(String::from_utf8_lossy(&output.stdout) 31 | .trim_end() 32 | .to_owned()) 33 | } else { 34 | Err(anyhow::anyhow!( 35 | "Git command failed.\nCommand: git {}\nStdout: {}\nStderr: {}", 36 | args.join(" "), 37 | String::from_utf8_lossy(&output.stdout), 38 | String::from_utf8_lossy(&output.stderr), 39 | )) 40 | } 41 | } 42 | 43 | pub fn get_git_root() -> anyhow::Result { 44 | let current_dir = env::current_dir()?; 45 | 46 | let args = ["rev-parse", "--show-toplevel"]; 47 | 48 | let root = spawn_git(&args, ¤t_dir)?; 49 | 50 | get_git_output(&root, &args).map(Into::into) 51 | } 52 | 53 | pub static GIT_ROOT: std::sync::LazyLock = 54 | std::sync::LazyLock::new(|| match get_git_root() { 55 | Ok(root) => root, 56 | Err(err) => { 57 | fail!("Failed to determine Git root directory.\n{err}"); 58 | process::exit(1) 59 | } 60 | }); 61 | 62 | pub fn git(args: [&str; N]) -> Result { 63 | log::trace!("$ git {}", args.join(" ")); 64 | get_git_output(&spawn_git(&args, &GIT_ROOT)?, &args) 65 | } 66 | 67 | pub static CLIENT: LazyLock = LazyLock::new(|| *Box::new(reqwest::Client::new())); 68 | 69 | /// Fetches a branch of a remote into local. Optionally accepts a commit hash 70 | /// for versioning. 71 | pub fn add_remote_branch( 72 | info: &BranchAndRemote, 73 | commit_hash: Option<&Commit>, 74 | ) -> anyhow::Result<()> { 75 | if let Err(err) = git([ 76 | "remote", 77 | "add", 78 | &info.remote.local_remote_alias, 79 | &info.remote.repository_url, 80 | ]) { 81 | git(["remote", "remove", &info.remote.local_remote_alias])?; 82 | bail!("Failed to fetch remote: {err}"); 83 | } 84 | 85 | log::trace!( 86 | "Added remote {} for repository {}", 87 | &info.remote.repository_url, 88 | &info.remote.local_remote_alias 89 | ); 90 | 91 | if let Err(err) = git([ 92 | "fetch", 93 | &info.remote.repository_url, 94 | &format!( 95 | "{}:{}", 96 | info.branch.upstream_branch_name, info.branch.local_branch_name 97 | ), 98 | ]) { 99 | bail!( 100 | "Failed to find branch {} of GitHub repository {}. Are you sure it exists?\n{err}", 101 | info.branch.upstream_branch_name, 102 | info.remote.repository_url 103 | ); 104 | } 105 | 106 | log::trace!( 107 | "Fetched branch {} as {} from repository {}", 108 | info.branch.upstream_branch_name, 109 | info.branch.local_branch_name, 110 | &info.remote.repository_url 111 | ); 112 | 113 | if let Some(commit_hash) = commit_hash { 114 | git([ 115 | "branch", 116 | "--force", 117 | &info.branch.local_branch_name, 118 | commit_hash.as_ref(), 119 | ]) 120 | .map_err(|err| { 121 | anyhow!( 122 | "Failed to find commit {} of branch {}. Are you sure it exists?\n{err}", 123 | commit_hash.as_ref(), 124 | info.branch.local_branch_name 125 | ) 126 | })?; 127 | 128 | log::trace!("...and did a hard reset to commit {}", commit_hash.as_ref()); 129 | } 130 | 131 | Ok(()) 132 | } 133 | 134 | /// Removes a remote and its branch 135 | /// 136 | /// Only call this function only runs if the script created 137 | /// the branch or if the user gave explicit permission 138 | pub fn delete_remote_and_branch(remote: &str, branch: &str) -> anyhow::Result<()> { 139 | git(["branch", "--delete", "--force", branch])?; 140 | git(["remote", "remove", remote])?; 141 | Ok(()) 142 | } 143 | 144 | pub fn checkout_from_remote(branch: &str, remote: &str) -> anyhow::Result { 145 | let current_branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).map_err(|err| { 146 | if let Err(err) = delete_remote_and_branch(remote, branch) { 147 | err 148 | } else { 149 | anyhow!( 150 | "Couldn't get the current branch. This usually happens \ 151 | when the current branch does \ 152 | not have any commits.\n{err}" 153 | ) 154 | } 155 | })?; 156 | 157 | if let Err(err) = git(["checkout", branch]) { 158 | delete_remote_and_branch(remote, branch)?; 159 | bail!("Failed to checkout branch: {branch}, which belongs to remote {remote}\n{err}"); 160 | } 161 | 162 | Ok(current_branch) 163 | } 164 | 165 | pub fn merge_into_main( 166 | local_branch: &str, 167 | remote_branch: &str, 168 | ) -> anyhow::Result { 169 | log::trace!("Merging branch {local_branch}"); 170 | 171 | if let Err(err) = git(["merge", "--squash", local_branch]) { 172 | // nukes the worktree 173 | git(["reset", "--hard"])?; 174 | bail!("Could not merge {remote_branch}\n{err}"); 175 | } 176 | 177 | // --squash will NOT commit anything. So we need to make it manually 178 | git([ 179 | "commit", 180 | "--message", 181 | &format!("patchy: Merge {local_branch}",), 182 | ])?; 183 | 184 | Ok(format!("Merged {remote_branch} successfully")) 185 | } 186 | 187 | pub fn merge_pull_request( 188 | info: &BranchAndRemote, 189 | pull_request: &str, 190 | pr_title: &str, 191 | pr_url: &str, 192 | ) -> anyhow::Result<()> { 193 | merge_into_main( 194 | &info.branch.local_branch_name, 195 | &info.branch.upstream_branch_name, 196 | ) 197 | .map_err(|err| { 198 | let pr = display_link( 199 | &format!( 200 | "{}{}{}{}", 201 | "#".bright_blue(), 202 | pull_request.bright_blue(), 203 | " ".bright_blue(), 204 | pr_title.bright_blue().italic() 205 | ), 206 | pr_url, 207 | ); 208 | 209 | let support_url = display_link( 210 | "Merge conflicts (github)", 211 | "https://github.com/nik-rev/patchy?tab=readme-ov-file#merge-conflicts", 212 | ) 213 | .bright_blue(); 214 | 215 | anyhow!( 216 | "Could not merge branch {} into the current branch for pull request {pr} since the \ 217 | merge is non-trivial.\nYou will need to merge it yourself:\n {} {0}\nNote: To learn \ 218 | how to merge only once and re-use for subsequent invocations of patchy, see \ 219 | {support_url}\nSkipping this PR. Error message from git:\n{err}", 220 | &info.branch.local_branch_name.bright_cyan(), 221 | "git merge --squash".bright_blue() 222 | ) 223 | })?; 224 | 225 | let has_unstaged_changes = git(["diff", "--cached", "--quiet"]).is_err(); 226 | 227 | if has_unstaged_changes { 228 | git([ 229 | "commit", 230 | "--message", 231 | &format!( 232 | "patchy: auto-merge pull request {}", 233 | &pr_url.replace("github.com", "redirect.github.com") 234 | ), 235 | ])?; 236 | } 237 | 238 | delete_remote_and_branch( 239 | &info.remote.local_remote_alias, 240 | &info.branch.local_branch_name, 241 | )?; 242 | 243 | Ok(()) 244 | } 245 | 246 | enum AvailableBranch { 247 | /// In this case, we can just use the original `branch` that we passed in 248 | First, 249 | /// The first branch was available, so we slapped on some arbitrary 250 | /// identifier at the end Represents a branch like some-branch-2, 251 | /// some-branch-3 252 | Other(String), 253 | } 254 | 255 | /// Given a branch, either return this branch or the first available branch with 256 | /// an identifier at the end (a `-#`) where `#` represents a number 257 | /// So we can keep on "trying" for a branch that isn't used. We might try 258 | /// `some-branch`, and if it already exists we will then try: 259 | /// 260 | /// - some-branch-2 261 | /// - some-branch-3 262 | /// - some-branch-4 263 | /// - ... 264 | /// 265 | /// Stopping when we find the first available 266 | /// 267 | /// We do not want to return a branch if it already exists, since we don't want 268 | /// to overwrite any branch potentially losing the user their work 269 | /// 270 | /// We also don't want to ask for a prompt for a custom name, as it would be 271 | /// pretty annoying to specify a name for each branch if you have like 30 pull 272 | /// requests you want to merge 273 | fn first_available_branch(branch: &str) -> AvailableBranch { 274 | let branch_exists = git(["rev-parse", "--verify", branch]).is_err(); 275 | 276 | if branch_exists { 277 | return AvailableBranch::First; 278 | } 279 | 280 | // the first number for which the branch does not exist 281 | #[expect( 282 | clippy::maybe_infinite_iter, 283 | reason = "there is definitely not an infinite number of branches" 284 | )] 285 | let number = (2..) 286 | .find(|current| { 287 | let branch_with_num = format!("{current}-{branch}"); 288 | git(["rev-parse", "--verify", &branch_with_num]).is_err() 289 | }) 290 | .expect("There will eventually be a #-branch which is available."); 291 | 292 | let branch_name = format!("{number}-{branch}"); 293 | 294 | AvailableBranch::Other(branch_name) 295 | } 296 | 297 | pub async fn fetch_branch( 298 | remote: &crate::cli::Remote, 299 | commit: Option<&Commit>, 300 | ) -> anyhow::Result<(Repo, BranchAndRemote)> { 301 | let url = format!( 302 | "https://api.github.com/repos/{}/{}", 303 | remote.owner, remote.repo 304 | ); 305 | 306 | let response = make_request(&url).await.map_err(|err| { 307 | anyhow!( 308 | "Could not fetch branch: {}/{}\n{err}\n", 309 | remote.owner, 310 | remote.repo 311 | ) 312 | })?; 313 | 314 | let response: Repo = serde_json::from_str(&response).map_err(|err| { 315 | anyhow!("Could not parse response.\n{response}. Could not parse because: \n{err}") 316 | })?; 317 | 318 | let info = BranchAndRemote { 319 | branch: crate::types::Branch { 320 | local_branch_name: remote.branch.clone(), 321 | upstream_branch_name: remote.branch.clone(), 322 | }, 323 | remote: Remote { 324 | repository_url: response.clone_url.clone(), 325 | local_remote_alias: with_uuid(&format!("{}/{}", &remote.owner, remote.repo)), 326 | }, 327 | }; 328 | 329 | add_remote_branch(&info, commit).map_err(|err| { 330 | anyhow!( 331 | "Could not add remote branch {}/{}, skipping.\n{err}", 332 | remote.owner, 333 | remote.repo 334 | ) 335 | })?; 336 | 337 | Ok((response, info)) 338 | } 339 | 340 | pub async fn fetch_pull_request( 341 | repo: &str, 342 | pull_request: &str, 343 | custom_branch_name: Option<&str>, 344 | commit_hash: Option<&Commit>, 345 | ) -> anyhow::Result<(GitHubResponse, BranchAndRemote)> { 346 | let url = format!("https://api.github.com/repos/{repo}/pulls/{pull_request}"); 347 | 348 | let response = make_request(&url) 349 | .await 350 | .map_err(|err| anyhow!("Could not fetch pull request #{pull_request}\n{err}\n"))?; 351 | 352 | let response: GitHubResponse = serde_json::from_str(&response).map_err(|err| { 353 | anyhow!("Could not parse response.\n{response}. Could not parse because: \n{err}") 354 | })?; 355 | 356 | let info = BranchAndRemote { 357 | branch: crate::types::Branch { 358 | upstream_branch_name: response.head.r#ref.clone(), 359 | local_branch_name: custom_branch_name.map_or_else( 360 | || { 361 | let branch_name = &format!("{pull_request}/{}", &response.head.r#ref); 362 | 363 | match first_available_branch(branch_name) { 364 | AvailableBranch::First => branch_name.to_string(), 365 | AvailableBranch::Other(branch) => branch, 366 | } 367 | }, 368 | Into::into, 369 | ), 370 | }, 371 | remote: Remote { 372 | repository_url: response.head.repo.clone_url.clone(), 373 | local_remote_alias: with_uuid(&format!( 374 | "{title}-{}", 375 | pull_request, 376 | title = normalize_commit_msg(&response.html_url) 377 | )), 378 | }, 379 | }; 380 | 381 | add_remote_branch(&info, commit_hash).map_err(|err| { 382 | anyhow!("Could not add remote branch for pull request #{pull_request}, skipping.\n{err}") 383 | })?; 384 | 385 | Ok((response, info)) 386 | } 387 | -------------------------------------------------------------------------------- /src/interact.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for user interaction 2 | 3 | /// Print a success message 4 | #[macro_export] 5 | macro_rules! note { 6 | ($($arg:tt)*) => {{ 7 | println!(" {}{}", 8 | colored::Colorize::bold(colored::Colorize::bright_blue("note: ")), 9 | format!($($arg)*)) 10 | }}; 11 | } 12 | 13 | /// Print a success message 14 | #[macro_export] 15 | macro_rules! success { 16 | ($($arg:tt)*) => {{ 17 | println!(" {}{}", 18 | colored::Colorize::bold(colored::Colorize::bright_green("✓ ")), 19 | format!($($arg)*)) 20 | }}; 21 | } 22 | 23 | /// Print a fail message 24 | #[macro_export] 25 | macro_rules! fail { 26 | ($($arg:tt)*) => {{ 27 | eprintln!(" {}{}", 28 | colored::Colorize::bold(colored::Colorize::bright_red("✗ ")), 29 | format!($($arg)*)) 30 | }}; 31 | } 32 | 33 | /// Get a yes or no answer from the user 34 | #[macro_export] 35 | macro_rules! confirm_prompt { 36 | ($($arg:tt)*) => {{ 37 | dialoguer::Confirm::new() 38 | .with_prompt(format!( 39 | "\n {} {}", 40 | "»".bright_black(), 41 | format!($($arg)*) 42 | )) 43 | .interact() 44 | .unwrap() 45 | }}; 46 | } 47 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(doc, doc = include_str!("../README.md"))] 2 | use std::env; 3 | use std::sync::LazyLock; 4 | 5 | mod cli; 6 | mod commands; 7 | mod commit; 8 | mod git; 9 | pub mod interact; 10 | mod types; 11 | mod utils; 12 | 13 | static CONFIG_ROOT: LazyLock = 14 | LazyLock::new(|| env::var("PATCHY_CONFIG_ROOT").unwrap_or_else(|_| ".patchy".into())); 15 | const CONFIG_FILE: &str = "config.toml"; 16 | const APP_NAME: &str = "patchy"; 17 | 18 | pub use cli::Cli; 19 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::process::ExitCode; 2 | 3 | use clap::Parser as _; 4 | 5 | #[tokio::main] 6 | async fn main() -> ExitCode { 7 | if let Err(err) = patchy::Cli::parse().command.execute().await { 8 | patchy::fail!("{err}"); 9 | ExitCode::FAILURE 10 | } else { 11 | ExitCode::SUCCESS 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexSet; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Represents the TOML config 5 | #[derive(Deserialize, Debug)] 6 | #[serde(rename_all = "kebab-case")] 7 | pub struct Configuration { 8 | pub local_branch: String, 9 | pub patches: IndexSet, 10 | pub pull_requests: Vec, 11 | pub remote_branch: String, 12 | pub repo: String, 13 | } 14 | 15 | /// Data returned by GitHub's API 16 | #[derive(Serialize, Deserialize, Debug)] 17 | pub struct GitHubResponse { 18 | pub head: Head, 19 | pub title: String, 20 | pub html_url: String, 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Debug)] 24 | pub struct Head { 25 | pub repo: Repo, 26 | pub r#ref: String, 27 | } 28 | 29 | #[derive(Serialize, Deserialize, Debug)] 30 | pub struct Repo { 31 | pub clone_url: String, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct Branch { 36 | pub upstream_branch_name: String, 37 | pub local_branch_name: String, 38 | } 39 | 40 | #[derive(Debug)] 41 | pub struct Remote { 42 | pub repository_url: String, 43 | pub local_remote_alias: String, 44 | } 45 | 46 | #[derive(Debug)] 47 | pub struct BranchAndRemote { 48 | pub branch: Branch, 49 | pub remote: Remote, 50 | } 51 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use rand::{Rng as _, distributions}; 3 | use reqwest::header::USER_AGENT; 4 | 5 | use crate::git::CLIENT; 6 | 7 | /// Add a uuid identifier to the string to make it unique 8 | pub fn with_uuid(s: &str) -> String { 9 | format!( 10 | "{uuid}-{s}", 11 | uuid = rand::thread_rng() 12 | .sample_iter(&distributions::Alphanumeric) 13 | .take(4) 14 | .map(char::from) 15 | .collect::() 16 | ) 17 | } 18 | 19 | /// Converts a commit message to only contain lowercase characters, underscores 20 | /// and dashes 21 | pub fn normalize_commit_msg(commit_msg: &str) -> String { 22 | commit_msg 23 | .chars() 24 | .map(|c| { 25 | if c.is_alphanumeric() { 26 | c.to_ascii_lowercase() 27 | } else if c.is_whitespace() { 28 | '_' 29 | } else { 30 | '-' 31 | } 32 | }) 33 | .collect() 34 | } 35 | 36 | /// Style a snippet of text as a link 37 | pub fn display_link(text: &str, url: &str) -> String { 38 | format!("\u{1b}]8;;{url}\u{1b}\\{text}\u{1b}]8;;\u{1b}\\") 39 | } 40 | 41 | /// Send a GET request to the specified URL 42 | pub async fn make_request(url: &str) -> anyhow::Result { 43 | let request = CLIENT 44 | .get(url) 45 | .header(USER_AGENT, "{APP_NAME}") 46 | .send() 47 | .await; 48 | 49 | match request { 50 | Ok(res) if res.status().is_success() => { 51 | let out = res.text().await?; 52 | 53 | Ok(out) 54 | } 55 | Ok(res) => { 56 | let status = res.status(); 57 | let text = res.text().await?; 58 | 59 | Err(anyhow!( 60 | "Request failed with status: {status}\nRequested URL: {url}\nResponse: {text}", 61 | )) 62 | } 63 | Err(err) => Err(anyhow!("Error sending request: {err}")), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/fixtures/patches/helix-readme-all-every.patch: -------------------------------------------------------------------------------- 1 | From 8ba2459c9e01a5200ca7af1c6c369191a9c1bcec Mon Sep 17 00:00:00 2001 2 | From: Jonathan Morley 3 | Date: Tue, 11 Mar 2025 17:19:38 -0400 4 | Subject: [PATCH] patch-every 5 | 6 | --- 7 | README.md | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/README.md b/README.md 11 | index 11a909b2..103df743 100644 12 | --- a/README.md 13 | +++ b/README.md 14 | @@ -26,7 +26,7 @@ myself agreeing with most of Kakoune's design decisions. 15 | For more information, see the [website](https://helix-editor.com) or 16 | [documentation](https://docs.helix-editor.com/). 17 | 18 | -All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 19 | +Every shortcut/keymap can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 20 | 21 | [Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting) 22 | 23 | -- 24 | 2.47.1 25 | 26 | -------------------------------------------------------------------------------- /tests/fixtures/patches/helix-readme-all-most.patch: -------------------------------------------------------------------------------- 1 | From 7f73cfc35c56c2e9b6f4556f272f166919c84f64 Mon Sep 17 00:00:00 2001 2 | From: Jonathan Morley 3 | Date: Tue, 11 Mar 2025 17:19:10 -0400 4 | Subject: [PATCH] patch-most 5 | 6 | --- 7 | README.md | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/README.md b/README.md 11 | index 11a909b2..be7304cc 100644 12 | --- a/README.md 13 | +++ b/README.md 14 | @@ -26,7 +26,7 @@ myself agreeing with most of Kakoune's design decisions. 15 | For more information, see the [website](https://helix-editor.com) or 16 | [documentation](https://docs.helix-editor.com/). 17 | 18 | -All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 19 | +Most shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 20 | 21 | [Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting) 22 | 23 | -- 24 | 2.47.1 25 | 26 | -------------------------------------------------------------------------------- /tests/fixtures/patches/helix-readme-all-some.patch: -------------------------------------------------------------------------------- 1 | From e3d71c1d1f72b760785f48225d0121871b97b514 Mon Sep 17 00:00:00 2001 2 | From: Jonathan Morley 3 | Date: Tue, 11 Mar 2025 17:18:33 -0400 4 | Subject: [PATCH] patch-some 5 | 6 | --- 7 | README.md | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/README.md b/README.md 11 | index 11a909b2..9aa84126 100644 12 | --- a/README.md 13 | +++ b/README.md 14 | @@ -26,7 +26,7 @@ myself agreeing with most of Kakoune's design decisions. 15 | For more information, see the [website](https://helix-editor.com) or 16 | [documentation](https://docs.helix-editor.com/). 17 | 18 | -All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 19 | +Some shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 20 | 21 | [Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting) 22 | 23 | -- 24 | 2.47.1 25 | 26 | -------------------------------------------------------------------------------- /tests/fixtures/patches/helix-readme-most-every.patch: -------------------------------------------------------------------------------- 1 | From 4f82761df065d4f0d49dd7778049e0b04a45e0f8 Mon Sep 17 00:00:00 2001 2 | From: Jonathan Morley 3 | Date: Wed, 12 Mar 2025 11:18:41 -0400 4 | Subject: [PATCH] patch-every 5 | 6 | --- 7 | README.md | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/README.md b/README.md 11 | index be7304cc..103df743 100644 12 | --- a/README.md 13 | +++ b/README.md 14 | @@ -26,7 +26,7 @@ myself agreeing with most of Kakoune's design decisions. 15 | For more information, see the [website](https://helix-editor.com) or 16 | [documentation](https://docs.helix-editor.com/). 17 | 18 | -Most shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 19 | +Every shortcut/keymap can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 20 | 21 | [Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting) 22 | 23 | -- 24 | 2.47.1 25 | 26 | -------------------------------------------------------------------------------- /tests/fixtures/patches/helix-readme-some-most.patch: -------------------------------------------------------------------------------- 1 | From 222f97202f50a6dabaae5d7beeda2dd2989403dd Mon Sep 17 00:00:00 2001 2 | From: Jonathan Morley 3 | Date: Wed, 12 Mar 2025 11:18:22 -0400 4 | Subject: [PATCH] patch-most 5 | 6 | --- 7 | README.md | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/README.md b/README.md 11 | index 9aa84126..be7304cc 100644 12 | --- a/README.md 13 | +++ b/README.md 14 | @@ -26,7 +26,7 @@ myself agreeing with most of Kakoune's design decisions. 15 | For more information, see the [website](https://helix-editor.com) or 16 | [documentation](https://docs.helix-editor.com/). 17 | 18 | -Some shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 19 | +Most shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html). 20 | 21 | [Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting) 22 | 23 | -- 24 | 2.47.1 25 | 26 | -------------------------------------------------------------------------------- /tests/fixtures/patches/helix-remove-tab.patch: -------------------------------------------------------------------------------- 1 | From 9f13ffd1036c302e1bdaf31dd4c8fcd1202ba981 Mon Sep 17 00:00:00 2001 2 | From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> 3 | Date: Wed, 18 Dec 2024 15:05:00 +0000 4 | Subject: [PATCH] feat: remove tab keybindings 5 | 6 | --- 7 | helix-term/src/keymap/default.rs | 2 +- 8 | helix-term/src/ui/menu.rs | 4 ++-- 9 | 2 files changed, 3 insertions(+), 3 deletions(-) 10 | 11 | diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs 12 | index c6cefd92..105b8f99 100644 13 | --- a/helix-term/src/keymap/default.rs 14 | +++ b/helix-term/src/keymap/default.rs 15 | @@ -215,7 +215,7 @@ pub fn default() -> HashMap { 16 | 17 | // z family for save/restore/combine from/to sels from register 18 | 19 | - "C-i" | "tab" => jump_forward, // tab == 20 | + "C-i" => jump_forward, // tab == 21 | "C-o" => jump_backward, 22 | "C-s" => save_selection, 23 | 24 | diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs 25 | index 612832ce..aaba784a 100644 26 | --- a/helix-term/src/ui/menu.rs 27 | +++ b/helix-term/src/ui/menu.rs 28 | @@ -274,12 +274,12 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { 29 | return EventResult::Consumed(close_fn); 30 | } 31 | // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) 32 | - shift!(Tab) | key!(Up) | ctrl!('p') => { 33 | + key!(Up) | ctrl!('p') => { 34 | self.move_up(); 35 | (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); 36 | return EventResult::Consumed(None); 37 | } 38 | - key!(Tab) | key!(Down) | ctrl!('n') => { 39 | + key!(Down) | ctrl!('n') => { 40 | // arrow down/ctrl-n/tab advances completion choice (including updating the doc) 41 | self.move_down(); 42 | (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); 43 | -- 44 | 2.47.0 45 | 46 | -------------------------------------------------------------------------------- /tests/run.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any())] 2 | #![cfg(test)] 3 | use std::process::Command; 4 | 5 | use assert_cmd::prelude::*; 6 | use copy_dir::copy_dir; 7 | use predicates::prelude::*; 8 | use tempfile::{TempDir, tempdir}; 9 | 10 | fn initialize(repository: &str, branch: &str, pull_requests: &[&str], patches: &[&str]) -> TempDir { 11 | let temp_dir = tempdir().expect("tempdir failed"); 12 | 13 | Command::new("git") 14 | .args(["init"]) 15 | .current_dir(temp_dir.path()) 16 | .output() 17 | .expect("git init failed"); 18 | 19 | std::fs::write(temp_dir.path().join("file.txt"), "content").expect("writing file.txt failed"); 20 | 21 | Command::new("git") 22 | .args(["config", "user.name", "GitHub Actions"]) 23 | .current_dir(temp_dir.path()) 24 | .output() 25 | .expect("git config user.name failed"); 26 | 27 | Command::new("git") 28 | .args(["config", "user.email", "user@example.com"]) 29 | .current_dir(temp_dir.path()) 30 | .output() 31 | .expect("git config user.email failed"); 32 | 33 | Command::new("git") 34 | .args(["add", "README.md"]) 35 | .current_dir(temp_dir.path()) 36 | .output() 37 | .expect("git add README.md failed"); 38 | 39 | Command::new("git") 40 | .args(["commit", "-m", "Initial commit with README"]) 41 | .current_dir(temp_dir.path()) 42 | .output() 43 | .expect("git commit failed"); 44 | copy_dir("tests/fixtures/patches", temp_dir.path().join(".patchy")).expect("copy_dir failed"); 45 | 46 | std::fs::write( 47 | temp_dir.path().join(".patchy").join("config.toml"), 48 | format!( 49 | " 50 | repo = \"{repository}\" 51 | remote-branch = \"{branch}\" 52 | local-branch = \"patchy\" 53 | pull-requests = {pull_requests:?} 54 | patches = {patches:?} 55 | " 56 | ), 57 | ) 58 | .expect("writing config.toml failed"); 59 | 60 | Command::new("git") 61 | .args(["add", ".patchy"]) 62 | .current_dir(temp_dir.path()) 63 | .output() 64 | .expect("git add failed"); 65 | 66 | Command::new("git") 67 | .args(["commit", "--allow-empty", "-n", "-m=initial commit"]) 68 | .current_dir(temp_dir.path()) 69 | .output() 70 | .expect("git commit failed"); 71 | 72 | temp_dir 73 | } 74 | 75 | #[test] 76 | fn test_helix_remove_tab() { 77 | let tmp = initialize( 78 | "helix-editor/helix", 79 | "master @ 1bd7a3901cf73a9219470dafc65f3c7119e96cc0", 80 | &[], 81 | &["helix-remove-tab"], 82 | ); 83 | 84 | Command::cargo_bin("patchy") 85 | .unwrap() 86 | .args(["run", "--yes"]) 87 | .current_dir(tmp.as_ref()) 88 | .assert() 89 | .success() 90 | .stdout(predicate::str::contains( 91 | "✓ Applied patch helix-remove-tab feat: remove tab keybindings", 92 | )) 93 | .stdout(predicate::str::contains("Success!")); 94 | } 95 | 96 | #[test] 97 | #[ignore] 98 | fn test_conflicting_patches() { 99 | let tmp = initialize( 100 | "helix-editor/helix", 101 | "master @ 1bd7a3901cf73a9219470dafc65f3c7119e96cc0", 102 | &[], 103 | &[ 104 | "helix-readme-all-every", 105 | "helix-readme-all-most", 106 | // "helix-readme-all-some", 107 | ], 108 | ); 109 | 110 | std::process::Command::cargo_bin("patchy") 111 | .unwrap() 112 | .args(["run", "--yes"]) 113 | .current_dir(tmp.path()) 114 | .assert() 115 | .failure() 116 | .stdout(predicate::str::contains( 117 | "✓ Applied patch helix-readme-all-every patch-every", 118 | )) 119 | .stderr(predicate::str::contains( 120 | "✗ Could not apply patch helix-readme-all-most, skipping", 121 | )) 122 | .stdout(predicate::str::contains("Success!").not()); 123 | } 124 | 125 | #[test] 126 | fn test_sequential_patches() { 127 | let tmp = initialize( 128 | "helix-editor/helix", 129 | "master @ 1bd7a3901cf73a9219470dafc65f3c7119e96cc0", 130 | &[], 131 | &[ 132 | "helix-readme-all-some", 133 | "helix-readme-some-most", 134 | "helix-readme-most-every", 135 | ], 136 | ); 137 | 138 | std::process::Command::cargo_bin("patchy") 139 | .unwrap() 140 | .args(["run", "--yes"]) 141 | .current_dir(tmp.path()) 142 | .assert() 143 | // This should pass, as the patches are applied in order 144 | .success() 145 | .stdout(predicate::str::contains( 146 | "✓ Applied patch helix-readme-all-some patch-some", 147 | )) 148 | .stdout(predicate::str::contains( 149 | "✓ Applied patch helix-readme-some-most patch-most", 150 | )) 151 | .stdout(predicate::str::contains( 152 | "✓ Applied patch helix-readme-most-every patch-every", 153 | )) 154 | .stdout(predicate::str::contains("Success!")); 155 | } 156 | 157 | #[test] 158 | fn test_nonexistent_patch() { 159 | let tmp = initialize( 160 | "helix-editor/helix", 161 | "master @ 1bd7a3901cf73a9219470dafc65f3c7119e96cc0", 162 | &[], 163 | &["foo"], 164 | ); 165 | 166 | std::process::Command::cargo_bin("patchy") 167 | .unwrap() 168 | .args(["run", "--yes"]) 169 | .current_dir(tmp.path()) 170 | .assert() 171 | .success() 172 | .stderr(predicate::str::contains( 173 | "✗ Could not find patch foo, skipping", 174 | )) 175 | .stdout(predicate::str::contains("Success!")); 176 | } 177 | -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 106 | 111 | 112 | 113 | 114 | 122 | 123 | 124 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 145 | 146 | 150 | 151 | 152 | 153 | 154 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 191 | 1 192 | 1 193 | 194 | 195 | 196 | 197 | 202 | 203 | 204 | 205 | 213 | 214 | 215 | 216 | 224 | 225 | 226 | 227 | 228 | 229 | --------------------------------------------------------------------------------