├── .github └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── release.toml ├── src ├── config.rs ├── lib.rs ├── main.rs ├── patcher.rs ├── patcher │ └── diff_ui.rs ├── rebaser.rs └── selecter.rs ├── static ├── 00-initial-state.png ├── 01-selector.gif ├── 20-initial-full-repo.png ├── 21-with-upstream.gif ├── asciicast.png └── full-workflow-simple.gif ├── tests └── basic.rs └── wix └── main.wxs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2024, axodotdev 2 | # SPDX-License-Identifier: MIT or Apache-2.0 3 | # 4 | # CI that: 5 | # 6 | # * checks for a Git Tag that looks like a release 7 | # * builds artifacts with cargo-dist (archives, installers, hashes) 8 | # * uploads those artifacts to temporary workflow zip 9 | # * on success, uploads the artifacts to a GitHub Release 10 | # 11 | # Note that the GitHub Release will be created with a generated 12 | # title/body based on your changelogs. 13 | 14 | name: Release 15 | 16 | permissions: 17 | contents: write 18 | id-token: write 19 | attestations: write 20 | 21 | # This task will run whenever you push a git tag that looks like a version 22 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 23 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 24 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 25 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 26 | # 27 | # If PACKAGE_NAME is specified, then the announcement will be for that 28 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 29 | # 30 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 31 | # (cargo-dist-able) packages in the workspace with that version (this mode is 32 | # intended for workspaces with only one dist-able package, or with all dist-able 33 | # packages versioned/released in lockstep). 34 | # 35 | # If you push multiple tags at once, separate instances of this workflow will 36 | # spin up, creating an independent announcement for each one. However, GitHub 37 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 38 | # mistake. 39 | # 40 | # If there's a prerelease-style suffix to the version, then the release(s) 41 | # will be marked as a prerelease. 42 | on: 43 | pull_request: 44 | push: 45 | tags: 46 | - '**[0-9]+.[0-9]+.[0-9]+*' 47 | 48 | jobs: 49 | # Run 'cargo dist plan' (or host) to determine what tasks we need to do 50 | plan: 51 | runs-on: "ubuntu-20.04" 52 | outputs: 53 | val: ${{ steps.plan.outputs.manifest }} 54 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 55 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 56 | publishing: ${{ !github.event.pull_request }} 57 | env: 58 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | steps: 60 | - uses: actions/checkout@v4 61 | with: 62 | submodules: recursive 63 | - name: Install cargo-dist 64 | # we specify bash to get pipefail; it guards against the `curl` command 65 | # failing. otherwise `sh` won't catch that `curl` returned non-0 66 | shell: bash 67 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.16.0/cargo-dist-installer.sh | sh" 68 | # sure would be cool if github gave us proper conditionals... 69 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 70 | # functionality based on whether this is a pull_request, and whether it's from a fork. 71 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 72 | # but also really annoying to build CI around when it needs secrets to work right.) 73 | - id: plan 74 | run: | 75 | cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 76 | echo "cargo dist ran successfully" 77 | cat plan-dist-manifest.json 78 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 79 | - name: "Upload dist-manifest.json" 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: artifacts-plan-dist-manifest 83 | path: plan-dist-manifest.json 84 | 85 | # Build and packages all the platform-specific things 86 | build-local-artifacts: 87 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 88 | # Let the initial task tell us to not run (currently very blunt) 89 | needs: 90 | - plan 91 | 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') }} 92 | strategy: 93 | fail-fast: false 94 | # Target platforms/runners are computed by cargo-dist in create-release. 95 | # Each member of the matrix has the following arguments: 96 | # 97 | # - runner: the github runner 98 | # - dist-args: cli flags to pass to cargo dist 99 | # - install-dist: expression to run to install cargo-dist on the runner 100 | # 101 | # Typically there will be: 102 | # - 1 "global" task that builds universal installers 103 | # - N "local" tasks that build each platform's binaries and platform-specific installers 104 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 105 | runs-on: ${{ matrix.runner }} 106 | env: 107 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 109 | steps: 110 | - name: enable windows longpaths 111 | run: | 112 | git config --global core.longpaths true 113 | - uses: actions/checkout@v4 114 | with: 115 | submodules: recursive 116 | - uses: swatinem/rust-cache@v2 117 | with: 118 | key: ${{ join(matrix.targets, '-') }} 119 | cache-provider: ${{ matrix.cache_provider }} 120 | - name: Install cargo-dist 121 | run: ${{ matrix.install_dist }} 122 | # Get the dist-manifest 123 | - name: Fetch local artifacts 124 | uses: actions/download-artifact@v4 125 | with: 126 | pattern: artifacts-* 127 | path: target/distrib/ 128 | merge-multiple: true 129 | - name: Install dependencies 130 | run: | 131 | ${{ matrix.packages_install }} 132 | - name: Build artifacts 133 | run: | 134 | # Actually do builds and make zips and whatnot 135 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 136 | echo "cargo dist ran successfully" 137 | - name: Attest 138 | uses: actions/attest-build-provenance@v1 139 | with: 140 | subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*" 141 | - id: cargo-dist 142 | name: Post-build 143 | # We force bash here just because github makes it really hard to get values up 144 | # to "real" actions without writing to env-vars, and writing to env-vars has 145 | # inconsistent syntax between shell and powershell. 146 | shell: bash 147 | run: | 148 | # Parse out what we just built and upload it to scratch storage 149 | echo "paths<> "$GITHUB_OUTPUT" 150 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 151 | echo "EOF" >> "$GITHUB_OUTPUT" 152 | 153 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 154 | - name: "Upload artifacts" 155 | uses: actions/upload-artifact@v4 156 | with: 157 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 158 | path: | 159 | ${{ steps.cargo-dist.outputs.paths }} 160 | ${{ env.BUILD_MANIFEST_NAME }} 161 | 162 | # Build and package all the platform-agnostic(ish) things 163 | build-global-artifacts: 164 | needs: 165 | - plan 166 | - build-local-artifacts 167 | runs-on: "ubuntu-20.04" 168 | env: 169 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 170 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 171 | steps: 172 | - uses: actions/checkout@v4 173 | with: 174 | submodules: recursive 175 | - name: Install cargo-dist 176 | shell: bash 177 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.16.0/cargo-dist-installer.sh | sh" 178 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 179 | - name: Fetch local artifacts 180 | uses: actions/download-artifact@v4 181 | with: 182 | pattern: artifacts-* 183 | path: target/distrib/ 184 | merge-multiple: true 185 | - id: cargo-dist 186 | shell: bash 187 | run: | 188 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 189 | echo "cargo dist ran successfully" 190 | 191 | # Parse out what we just built and upload it to scratch storage 192 | echo "paths<> "$GITHUB_OUTPUT" 193 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 194 | echo "EOF" >> "$GITHUB_OUTPUT" 195 | 196 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 197 | - name: "Upload artifacts" 198 | uses: actions/upload-artifact@v4 199 | with: 200 | name: artifacts-build-global 201 | path: | 202 | ${{ steps.cargo-dist.outputs.paths }} 203 | ${{ env.BUILD_MANIFEST_NAME }} 204 | # Determines if we should publish/announce 205 | host: 206 | needs: 207 | - plan 208 | - build-local-artifacts 209 | - build-global-artifacts 210 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 211 | 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') }} 212 | env: 213 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 214 | runs-on: "ubuntu-20.04" 215 | outputs: 216 | val: ${{ steps.host.outputs.manifest }} 217 | steps: 218 | - uses: actions/checkout@v4 219 | with: 220 | submodules: recursive 221 | - name: Install cargo-dist 222 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.16.0/cargo-dist-installer.sh | sh" 223 | # Fetch artifacts from scratch-storage 224 | - name: Fetch artifacts 225 | uses: actions/download-artifact@v4 226 | with: 227 | pattern: artifacts-* 228 | path: target/distrib/ 229 | merge-multiple: true 230 | # This is a harmless no-op for GitHub Releases, hosting for that happens in "announce" 231 | - id: host 232 | shell: bash 233 | run: | 234 | cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 235 | echo "artifacts uploaded and released successfully" 236 | cat dist-manifest.json 237 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 238 | - name: "Upload dist-manifest.json" 239 | uses: actions/upload-artifact@v4 240 | with: 241 | # Overwrite the previous copy 242 | name: artifacts-dist-manifest 243 | path: dist-manifest.json 244 | 245 | publish-homebrew-formula: 246 | needs: 247 | - plan 248 | - host 249 | runs-on: "ubuntu-20.04" 250 | env: 251 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 252 | PLAN: ${{ needs.plan.outputs.val }} 253 | GITHUB_USER: "axo bot" 254 | GITHUB_EMAIL: "admin+bot@axo.dev" 255 | if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} 256 | steps: 257 | - uses: actions/checkout@v4 258 | with: 259 | repository: "quodlibetor/homebrew-git-tools" 260 | token: ${{ secrets.HOMEBREW_TAP_TOKEN }} 261 | # So we have access to the formula 262 | - name: Fetch homebrew formulae 263 | uses: actions/download-artifact@v4 264 | with: 265 | pattern: artifacts-* 266 | path: Formula/ 267 | merge-multiple: true 268 | # This is extra complex because you can make your Formula name not match your app name 269 | # so we need to find releases with a *.rb file, and publish with that filename. 270 | - name: Commit formula files 271 | run: | 272 | git config --global user.name "${GITHUB_USER}" 273 | git config --global user.email "${GITHUB_EMAIL}" 274 | 275 | for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do 276 | filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) 277 | name=$(echo "$filename" | sed "s/\.rb$//") 278 | version=$(echo "$release" | jq .app_version --raw-output) 279 | 280 | git add "Formula/${filename}" 281 | git commit -m "${name} ${version}" 282 | done 283 | git push 284 | 285 | # Create a GitHub Release while uploading all files to it 286 | announce: 287 | needs: 288 | - plan 289 | - host 290 | - publish-homebrew-formula 291 | # use "always() && ..." to allow us to wait for all publish jobs while 292 | # still allowing individual publish jobs to skip themselves (for prereleases). 293 | # "host" however must run to completion, no skipping allowed! 294 | if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} 295 | runs-on: "ubuntu-20.04" 296 | env: 297 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 298 | steps: 299 | - uses: actions/checkout@v4 300 | with: 301 | submodules: recursive 302 | - name: "Download GitHub Artifacts" 303 | uses: actions/download-artifact@v4 304 | with: 305 | pattern: artifacts-* 306 | path: artifacts 307 | merge-multiple: true 308 | - name: Cleanup 309 | run: | 310 | # Remove the granular manifests 311 | rm -f artifacts/*-dist-manifest.json 312 | - name: Create GitHub Release 313 | env: 314 | PRERELEASE_FLAG: "${{ fromJson(needs.host.outputs.val).announcement_is_prerelease && '--prerelease' || '' }}" 315 | ANNOUNCEMENT_TITLE: "${{ fromJson(needs.host.outputs.val).announcement_title }}" 316 | ANNOUNCEMENT_BODY: "${{ fromJson(needs.host.outputs.val).announcement_github_body }}" 317 | run: | 318 | # Write and read notes from a file to avoid quoting breaking things 319 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 320 | 321 | gh release create "${{ needs.plan.outputs.tag }}" --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" $PRERELEASE_FLAG 322 | gh release upload "${{ needs.plan.outputs.tag }}" artifacts/* 323 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | env: 9 | RUST_BACKTRACE: 1 10 | CARGO_TERM_COLOR: always 11 | CLICOLOR: 1 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest] 18 | rustv: [stable, beta] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Rust 23 | uses: moonrepo/setup-rust@v1 24 | with: 25 | channel: ${{ matrix.rustv }} 26 | bins: cargo-nextest 27 | components: clippy,rustfmt 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Test 31 | run: cargo nextest run 32 | - name: Clippy 33 | run: cargo clippy -- -Dwarnings 34 | - name: fmt 35 | run: cargo fmt --check 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /deployment 2 | /target 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | # Version 0.2.7 4 | 5 | - Support arbitrary refs (i.e. tags like `v0.1.0` and full refspecs 6 | like `ref/pull/ID`) as the merge-base selector. 7 | 8 | # Version 0.2.6 9 | 10 | - Fix and improve the experience of working with a main-only workflow. 11 | - Provide a tailored error message if your current branch is the selected upstream branch 12 | - Work correctly with explicitly-defined remote upstream branches. 13 | 14 | # Version 0.2.5 15 | 16 | - Correctly find git repos in parent dirs of CWD 17 | - Enable experimental github release attestations in cargo-dist 18 | 19 | # Version 0.2.4 20 | 21 | - Retarget multiple branches pointing at the same commit, eg: 22 | > updated branch my-cool-branch: deadbeef -> c0ffee 23 | 24 | # Version 0.2.3 25 | 26 | - Allow setting the diff theme 27 | - Read configuration from git config as well as arguments and env vars 28 | - Choose whether to display full diff or just a diffstat based on terminal 29 | height instead of a constant 30 | - Add -u alias for --default-upstream-branch 31 | 32 | # Version 0.2.2 33 | 34 | - Correctly retarget branches if the target of the edit is also a branch (#24) 35 | - Check if main, master, develop, or trunk exist as reasonable default upstream branches 36 | - Leave the repo in a less confusing state if the edit target is a conflict 37 | 38 | # Version 0.2.1 39 | 40 | - Remove last dependency on external git binary, using libgit2 for all git interactions 41 | - Show backtraces on error if RUST_BACKTRACE=1 is in the environment 42 | - Correctly stash and unstash changes before the rebase 43 | 44 | # Version 0.2.0 45 | 46 | - Rename to git-instafix because there are a bunch of existing projects named git-fixup 47 | 48 | # Version 0.1.9 49 | 50 | - CI and doc improvements 51 | - Use libgit2 instead of shelling out for more things. 52 | - Create binaries and install scripts with cargo-dist 53 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 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.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 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.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.4" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.0.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" 64 | dependencies = [ 65 | "windows-sys 0.52.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.52.0", 76 | ] 77 | 78 | [[package]] 79 | name = "anyhow" 80 | version = "1.0.83" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" 83 | dependencies = [ 84 | "backtrace", 85 | ] 86 | 87 | [[package]] 88 | name = "assert_cmd" 89 | version = "2.0.14" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" 92 | dependencies = [ 93 | "anstyle", 94 | "bstr", 95 | "doc-comment", 96 | "predicates", 97 | "predicates-core", 98 | "predicates-tree", 99 | "wait-timeout", 100 | ] 101 | 102 | [[package]] 103 | name = "assert_fs" 104 | version = "1.1.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "2cd762e110c8ed629b11b6cde59458cc1c71de78ebbcc30099fc8e0403a2a2ec" 107 | dependencies = [ 108 | "anstyle", 109 | "doc-comment", 110 | "globwalk", 111 | "predicates", 112 | "predicates-core", 113 | "predicates-tree", 114 | "tempfile", 115 | ] 116 | 117 | [[package]] 118 | name = "backtrace" 119 | version = "0.3.71" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 122 | dependencies = [ 123 | "addr2line", 124 | "cc", 125 | "cfg-if", 126 | "libc", 127 | "miniz_oxide", 128 | "object", 129 | "rustc-demangle", 130 | ] 131 | 132 | [[package]] 133 | name = "base64" 134 | version = "0.21.7" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 137 | 138 | [[package]] 139 | name = "bincode" 140 | version = "1.3.3" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 143 | dependencies = [ 144 | "serde", 145 | ] 146 | 147 | [[package]] 148 | name = "bitflags" 149 | version = "1.3.2" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 152 | 153 | [[package]] 154 | name = "bitflags" 155 | version = "2.5.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 158 | 159 | [[package]] 160 | name = "bstr" 161 | version = "1.9.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" 164 | dependencies = [ 165 | "memchr", 166 | "regex-automata", 167 | "serde", 168 | ] 169 | 170 | [[package]] 171 | name = "cc" 172 | version = "1.0.97" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" 175 | dependencies = [ 176 | "jobserver", 177 | "libc", 178 | "once_cell", 179 | ] 180 | 181 | [[package]] 182 | name = "cfg-if" 183 | version = "1.0.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 186 | 187 | [[package]] 188 | name = "clap" 189 | version = "4.5.4" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 192 | dependencies = [ 193 | "clap_builder", 194 | "clap_derive", 195 | ] 196 | 197 | [[package]] 198 | name = "clap_builder" 199 | version = "4.5.2" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 202 | dependencies = [ 203 | "anstream", 204 | "anstyle", 205 | "clap_lex", 206 | "strsim", 207 | "terminal_size", 208 | ] 209 | 210 | [[package]] 211 | name = "clap_derive" 212 | version = "4.5.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 215 | dependencies = [ 216 | "heck", 217 | "proc-macro2", 218 | "quote", 219 | "syn", 220 | ] 221 | 222 | [[package]] 223 | name = "clap_lex" 224 | version = "0.7.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 227 | 228 | [[package]] 229 | name = "colorchoice" 230 | version = "1.0.1" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 233 | 234 | [[package]] 235 | name = "console" 236 | version = "0.15.8" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 239 | dependencies = [ 240 | "encode_unicode", 241 | "lazy_static", 242 | "libc", 243 | "unicode-width", 244 | "windows-sys 0.52.0", 245 | ] 246 | 247 | [[package]] 248 | name = "crc32fast" 249 | version = "1.4.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" 252 | dependencies = [ 253 | "cfg-if", 254 | ] 255 | 256 | [[package]] 257 | name = "crossbeam-deque" 258 | version = "0.8.5" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 261 | dependencies = [ 262 | "crossbeam-epoch", 263 | "crossbeam-utils", 264 | ] 265 | 266 | [[package]] 267 | name = "crossbeam-epoch" 268 | version = "0.9.18" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 271 | dependencies = [ 272 | "crossbeam-utils", 273 | ] 274 | 275 | [[package]] 276 | name = "crossbeam-utils" 277 | version = "0.8.19" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 280 | 281 | [[package]] 282 | name = "deranged" 283 | version = "0.3.11" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 286 | dependencies = [ 287 | "powerfmt", 288 | ] 289 | 290 | [[package]] 291 | name = "dialoguer" 292 | version = "0.11.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" 295 | dependencies = [ 296 | "console", 297 | "shell-words", 298 | "tempfile", 299 | "thiserror", 300 | "zeroize", 301 | ] 302 | 303 | [[package]] 304 | name = "difflib" 305 | version = "0.4.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 308 | 309 | [[package]] 310 | name = "doc-comment" 311 | version = "0.3.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 314 | 315 | [[package]] 316 | name = "either" 317 | version = "1.11.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" 320 | 321 | [[package]] 322 | name = "encode_unicode" 323 | version = "0.3.6" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 326 | 327 | [[package]] 328 | name = "equivalent" 329 | version = "1.0.1" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 332 | 333 | [[package]] 334 | name = "errno" 335 | version = "0.3.9" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 338 | dependencies = [ 339 | "libc", 340 | "windows-sys 0.52.0", 341 | ] 342 | 343 | [[package]] 344 | name = "fastrand" 345 | version = "2.1.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 348 | 349 | [[package]] 350 | name = "flate2" 351 | version = "1.0.30" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" 354 | dependencies = [ 355 | "crc32fast", 356 | "miniz_oxide", 357 | ] 358 | 359 | [[package]] 360 | name = "fnv" 361 | version = "1.0.7" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 364 | 365 | [[package]] 366 | name = "form_urlencoded" 367 | version = "1.2.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 370 | dependencies = [ 371 | "percent-encoding", 372 | ] 373 | 374 | [[package]] 375 | name = "gimli" 376 | version = "0.28.1" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 379 | 380 | [[package]] 381 | name = "git-instafix" 382 | version = "0.2.7" 383 | dependencies = [ 384 | "anyhow", 385 | "assert_cmd", 386 | "assert_fs", 387 | "clap", 388 | "console", 389 | "dialoguer", 390 | "git2", 391 | "itertools", 392 | "syntect", 393 | "termcolor", 394 | "terminal_size", 395 | ] 396 | 397 | [[package]] 398 | name = "git2" 399 | version = "0.18.3" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" 402 | dependencies = [ 403 | "bitflags 2.5.0", 404 | "libc", 405 | "libgit2-sys", 406 | "log", 407 | "url", 408 | ] 409 | 410 | [[package]] 411 | name = "globset" 412 | version = "0.4.14" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" 415 | dependencies = [ 416 | "aho-corasick", 417 | "bstr", 418 | "log", 419 | "regex-automata", 420 | "regex-syntax", 421 | ] 422 | 423 | [[package]] 424 | name = "globwalk" 425 | version = "0.9.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" 428 | dependencies = [ 429 | "bitflags 2.5.0", 430 | "ignore", 431 | "walkdir", 432 | ] 433 | 434 | [[package]] 435 | name = "hashbrown" 436 | version = "0.14.5" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 439 | 440 | [[package]] 441 | name = "heck" 442 | version = "0.5.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 445 | 446 | [[package]] 447 | name = "idna" 448 | version = "0.5.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 451 | dependencies = [ 452 | "unicode-bidi", 453 | "unicode-normalization", 454 | ] 455 | 456 | [[package]] 457 | name = "ignore" 458 | version = "0.4.22" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" 461 | dependencies = [ 462 | "crossbeam-deque", 463 | "globset", 464 | "log", 465 | "memchr", 466 | "regex-automata", 467 | "same-file", 468 | "walkdir", 469 | "winapi-util", 470 | ] 471 | 472 | [[package]] 473 | name = "indexmap" 474 | version = "2.2.6" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 477 | dependencies = [ 478 | "equivalent", 479 | "hashbrown", 480 | ] 481 | 482 | [[package]] 483 | name = "is_terminal_polyfill" 484 | version = "1.70.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 487 | 488 | [[package]] 489 | name = "itertools" 490 | version = "0.12.1" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 493 | dependencies = [ 494 | "either", 495 | ] 496 | 497 | [[package]] 498 | name = "itoa" 499 | version = "1.0.11" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 502 | 503 | [[package]] 504 | name = "jobserver" 505 | version = "0.1.31" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" 508 | dependencies = [ 509 | "libc", 510 | ] 511 | 512 | [[package]] 513 | name = "lazy_static" 514 | version = "1.4.0" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 517 | 518 | [[package]] 519 | name = "libc" 520 | version = "0.2.154" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 523 | 524 | [[package]] 525 | name = "libgit2-sys" 526 | version = "0.16.2+1.7.2" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" 529 | dependencies = [ 530 | "cc", 531 | "libc", 532 | "libz-sys", 533 | "pkg-config", 534 | ] 535 | 536 | [[package]] 537 | name = "libz-sys" 538 | version = "1.1.16" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" 541 | dependencies = [ 542 | "cc", 543 | "libc", 544 | "pkg-config", 545 | "vcpkg", 546 | ] 547 | 548 | [[package]] 549 | name = "line-wrap" 550 | version = "0.2.0" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" 553 | 554 | [[package]] 555 | name = "linked-hash-map" 556 | version = "0.5.6" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 559 | 560 | [[package]] 561 | name = "linux-raw-sys" 562 | version = "0.4.13" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 565 | 566 | [[package]] 567 | name = "log" 568 | version = "0.4.21" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 571 | 572 | [[package]] 573 | name = "memchr" 574 | version = "2.7.2" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 577 | 578 | [[package]] 579 | name = "miniz_oxide" 580 | version = "0.7.2" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 583 | dependencies = [ 584 | "adler", 585 | ] 586 | 587 | [[package]] 588 | name = "num-conv" 589 | version = "0.1.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 592 | 593 | [[package]] 594 | name = "object" 595 | version = "0.32.2" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 598 | dependencies = [ 599 | "memchr", 600 | ] 601 | 602 | [[package]] 603 | name = "once_cell" 604 | version = "1.19.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 607 | 608 | [[package]] 609 | name = "onig" 610 | version = "6.4.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" 613 | dependencies = [ 614 | "bitflags 1.3.2", 615 | "libc", 616 | "once_cell", 617 | "onig_sys", 618 | ] 619 | 620 | [[package]] 621 | name = "onig_sys" 622 | version = "69.8.1" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" 625 | dependencies = [ 626 | "cc", 627 | "pkg-config", 628 | ] 629 | 630 | [[package]] 631 | name = "percent-encoding" 632 | version = "2.3.1" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 635 | 636 | [[package]] 637 | name = "pkg-config" 638 | version = "0.3.30" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 641 | 642 | [[package]] 643 | name = "plist" 644 | version = "1.6.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" 647 | dependencies = [ 648 | "base64", 649 | "indexmap", 650 | "line-wrap", 651 | "quick-xml", 652 | "serde", 653 | "time", 654 | ] 655 | 656 | [[package]] 657 | name = "powerfmt" 658 | version = "0.2.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 661 | 662 | [[package]] 663 | name = "predicates" 664 | version = "3.1.0" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" 667 | dependencies = [ 668 | "anstyle", 669 | "difflib", 670 | "predicates-core", 671 | ] 672 | 673 | [[package]] 674 | name = "predicates-core" 675 | version = "1.0.6" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 678 | 679 | [[package]] 680 | name = "predicates-tree" 681 | version = "1.0.9" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 684 | dependencies = [ 685 | "predicates-core", 686 | "termtree", 687 | ] 688 | 689 | [[package]] 690 | name = "proc-macro2" 691 | version = "1.0.82" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" 694 | dependencies = [ 695 | "unicode-ident", 696 | ] 697 | 698 | [[package]] 699 | name = "quick-xml" 700 | version = "0.31.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 703 | dependencies = [ 704 | "memchr", 705 | ] 706 | 707 | [[package]] 708 | name = "quote" 709 | version = "1.0.36" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 712 | dependencies = [ 713 | "proc-macro2", 714 | ] 715 | 716 | [[package]] 717 | name = "regex-automata" 718 | version = "0.4.6" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 721 | dependencies = [ 722 | "aho-corasick", 723 | "memchr", 724 | "regex-syntax", 725 | ] 726 | 727 | [[package]] 728 | name = "regex-syntax" 729 | version = "0.8.3" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 732 | 733 | [[package]] 734 | name = "rustc-demangle" 735 | version = "0.1.24" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 738 | 739 | [[package]] 740 | name = "rustix" 741 | version = "0.38.34" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 744 | dependencies = [ 745 | "bitflags 2.5.0", 746 | "errno", 747 | "libc", 748 | "linux-raw-sys", 749 | "windows-sys 0.52.0", 750 | ] 751 | 752 | [[package]] 753 | name = "ryu" 754 | version = "1.0.18" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 757 | 758 | [[package]] 759 | name = "same-file" 760 | version = "1.0.6" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 763 | dependencies = [ 764 | "winapi-util", 765 | ] 766 | 767 | [[package]] 768 | name = "serde" 769 | version = "1.0.201" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" 772 | dependencies = [ 773 | "serde_derive", 774 | ] 775 | 776 | [[package]] 777 | name = "serde_derive" 778 | version = "1.0.201" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" 781 | dependencies = [ 782 | "proc-macro2", 783 | "quote", 784 | "syn", 785 | ] 786 | 787 | [[package]] 788 | name = "serde_json" 789 | version = "1.0.117" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" 792 | dependencies = [ 793 | "itoa", 794 | "ryu", 795 | "serde", 796 | ] 797 | 798 | [[package]] 799 | name = "shell-words" 800 | version = "1.1.0" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 803 | 804 | [[package]] 805 | name = "strsim" 806 | version = "0.11.1" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 809 | 810 | [[package]] 811 | name = "syn" 812 | version = "2.0.62" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "9f660c3bfcefb88c538776b6685a0c472e3128b51e74d48793dc2a488196e8eb" 815 | dependencies = [ 816 | "proc-macro2", 817 | "quote", 818 | "unicode-ident", 819 | ] 820 | 821 | [[package]] 822 | name = "syntect" 823 | version = "5.2.0" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" 826 | dependencies = [ 827 | "bincode", 828 | "bitflags 1.3.2", 829 | "flate2", 830 | "fnv", 831 | "once_cell", 832 | "onig", 833 | "plist", 834 | "regex-syntax", 835 | "serde", 836 | "serde_derive", 837 | "serde_json", 838 | "thiserror", 839 | "walkdir", 840 | "yaml-rust", 841 | ] 842 | 843 | [[package]] 844 | name = "tempfile" 845 | version = "3.10.1" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 848 | dependencies = [ 849 | "cfg-if", 850 | "fastrand", 851 | "rustix", 852 | "windows-sys 0.52.0", 853 | ] 854 | 855 | [[package]] 856 | name = "termcolor" 857 | version = "1.4.1" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 860 | dependencies = [ 861 | "winapi-util", 862 | ] 863 | 864 | [[package]] 865 | name = "terminal_size" 866 | version = "0.3.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 869 | dependencies = [ 870 | "rustix", 871 | "windows-sys 0.48.0", 872 | ] 873 | 874 | [[package]] 875 | name = "termtree" 876 | version = "0.4.1" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 879 | 880 | [[package]] 881 | name = "thiserror" 882 | version = "1.0.60" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" 885 | dependencies = [ 886 | "thiserror-impl", 887 | ] 888 | 889 | [[package]] 890 | name = "thiserror-impl" 891 | version = "1.0.60" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" 894 | dependencies = [ 895 | "proc-macro2", 896 | "quote", 897 | "syn", 898 | ] 899 | 900 | [[package]] 901 | name = "time" 902 | version = "0.3.36" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 905 | dependencies = [ 906 | "deranged", 907 | "itoa", 908 | "num-conv", 909 | "powerfmt", 910 | "serde", 911 | "time-core", 912 | "time-macros", 913 | ] 914 | 915 | [[package]] 916 | name = "time-core" 917 | version = "0.1.2" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 920 | 921 | [[package]] 922 | name = "time-macros" 923 | version = "0.2.18" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 926 | dependencies = [ 927 | "num-conv", 928 | "time-core", 929 | ] 930 | 931 | [[package]] 932 | name = "tinyvec" 933 | version = "1.6.0" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 936 | dependencies = [ 937 | "tinyvec_macros", 938 | ] 939 | 940 | [[package]] 941 | name = "tinyvec_macros" 942 | version = "0.1.1" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 945 | 946 | [[package]] 947 | name = "unicode-bidi" 948 | version = "0.3.15" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 951 | 952 | [[package]] 953 | name = "unicode-ident" 954 | version = "1.0.12" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 957 | 958 | [[package]] 959 | name = "unicode-normalization" 960 | version = "0.1.23" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 963 | dependencies = [ 964 | "tinyvec", 965 | ] 966 | 967 | [[package]] 968 | name = "unicode-width" 969 | version = "0.1.12" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 972 | 973 | [[package]] 974 | name = "url" 975 | version = "2.5.0" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 978 | dependencies = [ 979 | "form_urlencoded", 980 | "idna", 981 | "percent-encoding", 982 | ] 983 | 984 | [[package]] 985 | name = "utf8parse" 986 | version = "0.2.1" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 989 | 990 | [[package]] 991 | name = "vcpkg" 992 | version = "0.2.15" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 995 | 996 | [[package]] 997 | name = "wait-timeout" 998 | version = "0.2.0" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1001 | dependencies = [ 1002 | "libc", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "walkdir" 1007 | version = "2.5.0" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1010 | dependencies = [ 1011 | "same-file", 1012 | "winapi-util", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "winapi-util" 1017 | version = "0.1.8" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 1020 | dependencies = [ 1021 | "windows-sys 0.52.0", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "windows-sys" 1026 | version = "0.48.0" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1029 | dependencies = [ 1030 | "windows-targets 0.48.5", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "windows-sys" 1035 | version = "0.52.0" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1038 | dependencies = [ 1039 | "windows-targets 0.52.5", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "windows-targets" 1044 | version = "0.48.5" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1047 | dependencies = [ 1048 | "windows_aarch64_gnullvm 0.48.5", 1049 | "windows_aarch64_msvc 0.48.5", 1050 | "windows_i686_gnu 0.48.5", 1051 | "windows_i686_msvc 0.48.5", 1052 | "windows_x86_64_gnu 0.48.5", 1053 | "windows_x86_64_gnullvm 0.48.5", 1054 | "windows_x86_64_msvc 0.48.5", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "windows-targets" 1059 | version = "0.52.5" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1062 | dependencies = [ 1063 | "windows_aarch64_gnullvm 0.52.5", 1064 | "windows_aarch64_msvc 0.52.5", 1065 | "windows_i686_gnu 0.52.5", 1066 | "windows_i686_gnullvm", 1067 | "windows_i686_msvc 0.52.5", 1068 | "windows_x86_64_gnu 0.52.5", 1069 | "windows_x86_64_gnullvm 0.52.5", 1070 | "windows_x86_64_msvc 0.52.5", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "windows_aarch64_gnullvm" 1075 | version = "0.48.5" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1078 | 1079 | [[package]] 1080 | name = "windows_aarch64_gnullvm" 1081 | version = "0.52.5" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1084 | 1085 | [[package]] 1086 | name = "windows_aarch64_msvc" 1087 | version = "0.48.5" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1090 | 1091 | [[package]] 1092 | name = "windows_aarch64_msvc" 1093 | version = "0.52.5" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1096 | 1097 | [[package]] 1098 | name = "windows_i686_gnu" 1099 | version = "0.48.5" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1102 | 1103 | [[package]] 1104 | name = "windows_i686_gnu" 1105 | version = "0.52.5" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1108 | 1109 | [[package]] 1110 | name = "windows_i686_gnullvm" 1111 | version = "0.52.5" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1114 | 1115 | [[package]] 1116 | name = "windows_i686_msvc" 1117 | version = "0.48.5" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1120 | 1121 | [[package]] 1122 | name = "windows_i686_msvc" 1123 | version = "0.52.5" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1126 | 1127 | [[package]] 1128 | name = "windows_x86_64_gnu" 1129 | version = "0.48.5" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1132 | 1133 | [[package]] 1134 | name = "windows_x86_64_gnu" 1135 | version = "0.52.5" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1138 | 1139 | [[package]] 1140 | name = "windows_x86_64_gnullvm" 1141 | version = "0.48.5" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1144 | 1145 | [[package]] 1146 | name = "windows_x86_64_gnullvm" 1147 | version = "0.52.5" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1150 | 1151 | [[package]] 1152 | name = "windows_x86_64_msvc" 1153 | version = "0.48.5" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1156 | 1157 | [[package]] 1158 | name = "windows_x86_64_msvc" 1159 | version = "0.52.5" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1162 | 1163 | [[package]] 1164 | name = "yaml-rust" 1165 | version = "0.4.5" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1168 | dependencies = [ 1169 | "linked-hash-map", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "zeroize" 1174 | version = "1.7.0" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" 1177 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "git-instafix" 3 | version = "0.2.7" 4 | authors = ["Brandon W Maister "] 5 | edition = "2021" 6 | default-run = "git-instafix" 7 | publish = false 8 | homepage = "https://github.com/quodlibetor/git-instafix" 9 | repository = "https://github.com/quodlibetor/git-instafix" 10 | description = """Apply staged git changes to an ancestor git commit. 11 | """ 12 | 13 | [package.metadata.wix] 14 | upgrade-guid = "F5B771EA-3725-4523-9EE3-06FA112A5573" 15 | path-guid = "A8CD8E47-6A93-4B11-B617-865BFCA4C29F" 16 | license = false 17 | eula = false 18 | 19 | [dependencies] 20 | anyhow = { version = "1.0.79", features = ["backtrace"] } 21 | clap = { version = "4.5.1", features = ["derive", "env", "wrap_help"] } 22 | console = "0.15.8" 23 | dialoguer = "0.11.0" 24 | git2 = { version = "0.18.2", default-features = false } 25 | termcolor = "1.4.1" 26 | terminal_size = "0.3.0" 27 | syntect = "5.2.0" 28 | 29 | [dev-dependencies] 30 | assert_cmd = "2.0.13" 31 | assert_fs = "1.1.1" 32 | itertools = "0.12.1" 33 | 34 | # The profile that 'cargo dist' will build with 35 | [profile.dist] 36 | inherits = "release" 37 | lto = "thin" 38 | 39 | [package.metadata.dist] 40 | dist = true 41 | 42 | # Config for 'cargo dist' 43 | [workspace.metadata.dist] 44 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 45 | cargo-dist-version = "0.16.0" 46 | # CI backends to support 47 | ci = "github" 48 | # The installers to generate for each app 49 | installers = ["shell", "powershell", "homebrew", "msi"] 50 | # A GitHub repo to push Homebrew formulas to 51 | tap = "quodlibetor/homebrew-git-tools" 52 | # Target platforms to build apps for (Rust target-triple syntax) 53 | targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] 54 | # The archive format to use for windows builds (defaults .zip) 55 | windows-archive = ".tar.gz" 56 | # The archive format to use for non-windows builds (defaults .tar.xz) 57 | unix-archive = ".tar.xz" 58 | # Publish jobs to run in CI 59 | publish-jobs = ["homebrew"] 60 | publish-prerelease = true 61 | # Publish jobs to run in CI 62 | pr-run-mode = "plan" 63 | # Whether to install an updater program 64 | install-updater = false 65 | # Whether to enable GitHub Attestations 66 | github-attestations = true 67 | -------------------------------------------------------------------------------- /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 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Brandon W Maister 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git instafix 2 | 3 | Quickly fix up an old commit using your currently-staged changes. 4 | 5 | ![usage](./static/full-workflow-simple.gif) 6 | 7 | ## Usage 8 | 9 | After installation, just run `git instafix` to commit your currently-staged 10 | changes to an older commit in your branch. 11 | 12 | By default, `git instafix` checks for staged changes and offers to amend an old 13 | commit. 14 | 15 | Given a repo that looks like: 16 | 17 | ![linear-repo](./static/00-initial-state.png) 18 | 19 | Running `git instafix` will allow you to edit an old commit: 20 | 21 | ![linear-repo-fixup](./static/01-selector.gif) 22 | 23 | The default behavior will check if your current HEAD commit has an `upstream` 24 | branch and show you only the commits between where you currently are and that 25 | commit. If there is no upstream for HEAD you will see the behavior above. 26 | 27 | If you're using a pull-request workflow (e.g. github) you will often have repos that look more like this: 28 | 29 | ![full-repo](./static/20-initial-full-repo.png) 30 | 31 | You can set `GIT_INSTAFIX_UPSTREAM` to a branch name and `git instafix` will only 32 | show changes between HEAD and the merge-base: 33 | 34 | ![full-repo-fixup](./static/21-with-upstream.gif) 35 | 36 | In general this is just what you want, since you probably shouldn't be editing 37 | commits that other people are working off of. 38 | 39 | After you select the commit to edit, `git instafix` will apply your staged changes 40 | to that commit without any further prompting or work from you. 41 | 42 | Adding the `--squash` flag will behave the same, but after you have selected the commit amend to 43 | git will give you a chance to edit the commit message before changing the tree at that point. 44 | 45 | ## Installation 46 | 47 | You can install the latest version with curl: 48 | 49 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/quodlibetor/git-instafix/releases/latest/download/git-instafix-installer.sh | sh 50 | 51 | If you have Homebrew (including linuxbrew) you can install it with: 52 | 53 | brew install quodlibetor/git-tools/git-instafix 54 | 55 | You can also install from this repo with `cargo`: 56 | 57 | cargo install --git https://github.com/quodlibetor/git-instafix 58 | 59 | Otherwise, you will need to compile with Rust. Install rust, clone this repo, 60 | build, and then copy the binary into your bin dir: 61 | 62 | curl https://sh.rustup.rs -sSf | sh 63 | git clone https://github.com/quodlibetor/git-instafix && cd git-instafix 64 | cargo build --release 65 | cp target/release/git-instafix /usr/local/bin/git-instafix 66 | 67 | ## Similar or related projects 68 | 69 | * [`git-absorb`](https://github.com/tummychow/git-absorb) is a fantastic tool that will 70 | automatically determine which commits to amend. It is reliable, the main downside to it is that 71 | it relies on a diff intersection between your changes and ancestor commits, and so sometimes 72 | cannot determine which commits to amend. 73 | * [`git-fixup`](https://github.com/keis/git-fixup) is more-or less a pure-shell version of this 74 | same tool. We have some different features. The big differences between `git-fixup` and 75 | `git-instafix` are pretty much all surface level, and `git-instafix` is written in Rust which 76 | allows for some slightly fancier interactions. `git-instafix` does not depend 77 | on the system it's being run on having a git binary, instead using libgit2 for 78 | all git interactions. 79 | 80 | ## License 81 | 82 | git-instafix is licensed under either of 83 | 84 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 85 | http://www.apache.org/licenses/LICENSE-2.0) 86 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 87 | http://opensource.org/licenses/MIT) 88 | 89 | at your option. 90 | 91 | Patches and bug reports welcome! 92 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | publish = false 2 | 3 | pre-release-replacements = [ 4 | {file="CHANGELOG.md", search="# Unreleased", replace="# Unreleased\n\n# Version {{version}}"}, 5 | ] 6 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use clap::Parser; 4 | 5 | // Env vars that provide defaults for args 6 | const MAX_COMMITS_VAR: &str = "GIT_INSTAFIX_MAX_COMMITS"; 7 | const MAX_COMMITS_SETTING: &str = "instafix.max-commits"; 8 | const UPSTREAM_VAR: &str = "GIT_INSTAFIX_UPSTREAM"; 9 | pub const UPSTREAM_SETTING: &str = "instafix.default-upstream-branch"; 10 | const REQUIRE_NEWLINE_VAR: &str = "GIT_INSTAFIX_REQUIRE_NEWLINE"; 11 | const REQUIRE_NEWLINE_SETTING: &str = "instafix.require-newline"; 12 | const THEME_VAR: &str = "GIT_INSTAFIX_THEME"; 13 | const THEME_SETTING: &str = "instafix.theme"; 14 | 15 | // Other defaults 16 | pub(crate) const DEFAULT_UPSTREAM_BRANCHES: &[&str] = &["main", "master", "develop", "trunk"]; 17 | pub const DEFAULT_THEME: &str = "base16-ocean.dark"; 18 | 19 | #[derive(Parser, Debug)] 20 | #[clap( 21 | version, 22 | about = "Fix a commit in your history with your currently-staged changes", 23 | long_about = "Fix a commit in your history with your currently-staged changes 24 | 25 | When run with no arguments this will: 26 | 27 | * If you have no staged changes, ask if you'd like to stage all changes 28 | * Print a `diff --stat` of your currently staged changes 29 | * Provide a list of commits to fixup or amend going back to: 30 | * The merge-base of HEAD and the environment var GIT_INSTAFIX_UPSTREAM 31 | (if it is set) 32 | * HEAD's upstream 33 | * Fixup your selected commit with the staged changes 34 | ", 35 | max_term_width = 100 36 | )] 37 | struct Args { 38 | /// Change the commit message that you amend, instead of using the original commit message 39 | #[clap(short = 's', long, hide = true)] 40 | squash: Option, 41 | /// The maximum number of commits to show when looking for your merge point 42 | /// 43 | /// [gitconfig: instafix.max-commits] 44 | #[clap(short = 'm', long = "max-commits", env = MAX_COMMITS_VAR)] 45 | max_commits: Option, 46 | 47 | /// Specify a commit to ammend by the subject line of the commit 48 | #[clap(short = 'P', long)] 49 | commit_message_pattern: Option, 50 | 51 | /// The branch to not go past when looking for your merge point 52 | /// 53 | /// [gitconfig: instafix.default-upstream-branch] 54 | #[clap(short = 'u', long, env = UPSTREAM_VAR)] 55 | default_upstream_branch: Option, 56 | 57 | /// Require a newline when confirming y/n questions 58 | /// 59 | /// [gitconfig: instafix.require-newline] 60 | #[clap(long, env = REQUIRE_NEWLINE_VAR)] 61 | require_newline: Option, 62 | 63 | /// Show the possible color themes for output 64 | #[clap(long)] 65 | help_themes: bool, 66 | 67 | /// Use this theme 68 | #[clap(long, env = THEME_VAR)] 69 | theme: Option, 70 | } 71 | 72 | /// Fully configured arguments after loading from env and gitconfig 73 | pub struct Config { 74 | /// Change the commit message that you amend, instead of using the original commit message 75 | pub squash: bool, 76 | /// The maximum number of commits to show when looking for your merge point 77 | pub max_commits: usize, 78 | /// Specify a commit to ammend by the subject line of the commit 79 | pub commit_message_pattern: Option, 80 | pub default_upstream_branch: Option, 81 | /// Require a newline when confirming y/n questions 82 | pub require_newline: bool, 83 | /// User requested info about themes 84 | pub help_themes: bool, 85 | /// Which theme to use 86 | pub theme: String, 87 | } 88 | 89 | /// Create a Config based on arguments and env vars 90 | pub fn load_config_from_args_env_git() -> Config { 91 | let mut args = Args::parse(); 92 | if env::args().next().unwrap().ends_with("squash") { 93 | args.squash = Some(true) 94 | } 95 | args_to_config_using_git_config(args).unwrap() 96 | } 97 | 98 | fn args_to_config_using_git_config(args: Args) -> Result { 99 | let mut cfg = git2::Config::open_default()?; 100 | let repo = git2::Repository::discover(".")?; 101 | cfg.add_file(&repo.path().join("config"), git2::ConfigLevel::Local, false)?; 102 | Ok(Config { 103 | squash: args 104 | .squash 105 | .unwrap_or_else(|| cfg.get_bool("instafix.squash").unwrap_or(false)), 106 | max_commits: args 107 | .max_commits 108 | .unwrap_or_else(|| cfg.get_i32(MAX_COMMITS_SETTING).unwrap_or(15) as usize), 109 | commit_message_pattern: args.commit_message_pattern, 110 | default_upstream_branch: args 111 | .default_upstream_branch 112 | .or_else(|| cfg.get_string(UPSTREAM_SETTING).ok()), 113 | require_newline: args 114 | .require_newline 115 | .unwrap_or_else(|| cfg.get_bool(REQUIRE_NEWLINE_SETTING).unwrap_or(false)), 116 | help_themes: args.help_themes, 117 | theme: args.theme.unwrap_or_else(|| { 118 | cfg.get_string(THEME_SETTING) 119 | .unwrap_or_else(|_| DEFAULT_THEME.to_string()) 120 | }), 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod patcher; 3 | mod rebaser; 4 | mod selecter; 5 | 6 | use anyhow::Context; 7 | use git2::{Branch, Commit, Repository}; 8 | use syntect::highlighting::ThemeSet; 9 | 10 | pub use config::load_config_from_args_env_git; 11 | 12 | pub fn instafix(c: config::Config) -> Result<(), anyhow::Error> { 13 | let repo = Repository::open_from_env().context("opening repo")?; 14 | let diff = patcher::create_diff(&repo, &c.theme, c.require_newline).context("creating diff")?; 15 | let head = repo.head().context("finding head commit")?; 16 | let head_branch = Branch::wrap(head); 17 | let upstream = 18 | selecter::get_merge_base(&repo, &head_branch, c.default_upstream_branch.as_deref()) 19 | .context("creating merge base")?; 20 | let commit_to_amend = selecter::select_commit_to_amend( 21 | &repo, 22 | upstream, 23 | c.max_commits, 24 | c.commit_message_pattern.as_deref(), 25 | ) 26 | .context("selecting commit to amend")?; 27 | eprintln!("Selected {}", commit_display(&commit_to_amend)); 28 | patcher::do_fixup_commit(&repo, &head_branch, &commit_to_amend, c.squash) 29 | .context("doing fixup commit")?; 30 | let needs_stash = patcher::worktree_is_dirty(&repo)?; 31 | if needs_stash { 32 | // TODO: is it reasonable to create a new repo to work around lifetime issues? 33 | let mut repo = Repository::open_from_env()?; 34 | let sig = repo.signature()?.clone(); 35 | repo.stash_save(&sig, "git-instafix stashing changes", None)?; 36 | } 37 | let current_branch = Branch::wrap(repo.head()?); 38 | rebaser::do_rebase(&repo, ¤t_branch, &commit_to_amend, &diff)?; 39 | if needs_stash { 40 | let mut repo = Repository::open(".")?; 41 | repo.stash_pop(0, None)?; 42 | } 43 | 44 | Ok(()) 45 | } 46 | 47 | /// Display a commit as "short_hash summary" 48 | fn commit_display(commit: &Commit) -> String { 49 | format!( 50 | "{} {}", 51 | &commit.id().to_string()[0..10], 52 | commit.summary().unwrap_or(""), 53 | ) 54 | } 55 | 56 | fn format_ref(rf: &git2::Reference<'_>) -> Result { 57 | let shorthand = rf.shorthand().unwrap_or(""); 58 | let sha = rf.peel_to_commit()?.id().to_string(); 59 | Ok(format!("{} ({})", shorthand, &sha[..10])) 60 | } 61 | 62 | /// A vec of all built-in theme names 63 | pub fn print_themes() { 64 | println!("Available themes:"); 65 | for theme in ThemeSet::load_defaults().themes.keys() { 66 | println!(" {}", theme); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Brandon W Maister 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::env; 16 | 17 | use git_instafix::load_config_from_args_env_git; 18 | 19 | fn main() { 20 | let config = load_config_from_args_env_git(); 21 | 22 | if config.help_themes { 23 | git_instafix::print_themes(); 24 | return; 25 | } 26 | 27 | if let Err(e) = git_instafix::instafix(config) { 28 | // An empty message means don't display any error message 29 | let msg = e.to_string(); 30 | if !msg.is_empty() { 31 | if env::var("RUST_BACKTRACE").as_deref() == Ok("1") { 32 | println!("Error: {:?}", e); 33 | } else { 34 | println!("Error: {:#}", e); 35 | } 36 | } 37 | std::process::exit(1); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/patcher.rs: -------------------------------------------------------------------------------- 1 | //! mod patcher creates a patch/commit that represents the change to apply in the later rebase 2 | 3 | mod diff_ui; 4 | 5 | use anyhow::bail; 6 | use dialoguer::Confirm; 7 | use git2::Branch; 8 | use git2::Commit; 9 | use git2::Diff; 10 | use git2::Repository; 11 | use terminal_size::{terminal_size, Height}; 12 | 13 | use diff_ui::native_diff; 14 | use diff_ui::print_diff_lines; 15 | use diff_ui::print_diffstat; 16 | 17 | /// Get a diff either from the index or the diff from the index to the working tree 18 | pub(crate) fn create_diff<'a>( 19 | repo: &'a Repository, 20 | theme: &str, 21 | require_newline: bool, 22 | ) -> Result, anyhow::Error> { 23 | let head = repo.head()?; 24 | let head_tree = head.peel_to_tree()?; 25 | let staged_diff = repo.diff_tree_to_index(Some(&head_tree), None, None)?; 26 | let dirty_diff = repo.diff_index_to_workdir(None, None)?; 27 | let diffstat = staged_diff.stats()?; 28 | let diff = if diffstat.files_changed() == 0 { 29 | let dirty_workdir_stats = dirty_diff.stats()?; 30 | if dirty_workdir_stats.files_changed() > 0 { 31 | let Height(h) = terminal_size().map(|(_w, h)| h).unwrap_or(Height(24)); 32 | let cutoff_height = (h - 5) as usize; // give some room for the prompt 33 | let total_change = dirty_workdir_stats.insertions() + dirty_workdir_stats.deletions(); 34 | if total_change >= cutoff_height { 35 | print_diffstat("Unstaged", &dirty_diff)?; 36 | } else { 37 | let diff_lines = native_diff(&dirty_diff, theme)?; 38 | if diff_lines.len() >= cutoff_height { 39 | print_diffstat("Unstaged", &dirty_diff)?; 40 | } else { 41 | print_diff_lines(&diff_lines)?; 42 | } 43 | } 44 | if !Confirm::new() 45 | .with_prompt("Nothing staged, stage and commit everything?") 46 | .wait_for_newline(require_newline) 47 | .interact()? 48 | { 49 | bail!(""); 50 | } 51 | } else { 52 | bail!("Nothing staged and no tracked files have any changes"); 53 | } 54 | repo.apply(&dirty_diff, git2::ApplyLocation::Index, None)?; 55 | // the diff that we return knows whether it's from the index to the 56 | // workdir or the HEAD to the index, so now that we've created a new 57 | // commit we need a new diff. 58 | repo.diff_tree_to_index(Some(&head_tree), None, None)? 59 | } else { 60 | diff_ui::print_diffstat("Staged", &staged_diff)?; 61 | staged_diff 62 | }; 63 | 64 | Ok(diff) 65 | } 66 | 67 | pub(crate) fn worktree_is_dirty(repo: &Repository) -> Result { 68 | let head = repo.head()?; 69 | let head_tree = head.peel_to_tree()?; 70 | let staged_diff = repo.diff_tree_to_index(Some(&head_tree), None, None)?; 71 | let dirty_diff = repo.diff_index_to_workdir(None, None)?; 72 | let diffstat = staged_diff.stats()?; 73 | let dirty_workdir_stats = dirty_diff.stats()?; 74 | Ok(diffstat.files_changed() > 0 || dirty_workdir_stats.files_changed() > 0) 75 | } 76 | 77 | /// Commit the current index as a fixup or squash commit 78 | pub(crate) fn do_fixup_commit<'a>( 79 | repo: &'a Repository, 80 | head_branch: &'a Branch, 81 | commit_to_amend: &'a Commit, 82 | squash: bool, 83 | ) -> Result<(), anyhow::Error> { 84 | let msg = if squash { 85 | format!("squash! {}", commit_to_amend.id()) 86 | } else { 87 | format!("fixup! {}", commit_to_amend.id()) 88 | }; 89 | 90 | let sig = repo.signature()?; 91 | let mut idx = repo.index()?; 92 | let tree = repo.find_tree(idx.write_tree()?)?; 93 | let head_commit = head_branch.get().peel_to_commit()?; 94 | repo.commit(Some("HEAD"), &sig, &sig, &msg, &tree, &[&head_commit])?; 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /src/patcher/diff_ui.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write as _; 2 | 3 | use anyhow::Context as _; 4 | use git2::Diff; 5 | use git2::DiffFormat; 6 | use git2::DiffStatsFormat; 7 | use syntect::easy::HighlightLines; 8 | use syntect::highlighting::ThemeSet; 9 | use syntect::parsing::SyntaxSet; 10 | use syntect::util::as_24_bit_terminal_escaped; 11 | use termcolor::StandardStream; 12 | use termcolor::{ColorChoice, WriteColor as _}; 13 | 14 | pub(crate) fn native_diff(diff: &Diff<'_>, theme: &str) -> Result, anyhow::Error> { 15 | let ss = SyntaxSet::load_defaults_newlines(); 16 | let ts = ThemeSet::load_defaults(); 17 | let syntax = ss.find_syntax_by_extension("patch").unwrap(); 18 | let mut h = HighlightLines::new( 19 | syntax, 20 | ts.themes 21 | .get(theme) 22 | .unwrap_or_else(|| &ts.themes[crate::config::DEFAULT_THEME]), 23 | ); 24 | 25 | let mut inner_err = None; 26 | let mut diff_lines = Vec::new(); 27 | 28 | diff.print(DiffFormat::Patch, |_delta, _hunk, line| { 29 | let content = std::str::from_utf8(line.content()).unwrap(); 30 | let origin = line.origin(); 31 | match origin { 32 | '+' | '-' | ' ' => { 33 | let diff_line = format!("{origin}{content}"); 34 | let ranges = match h.highlight_line(&diff_line, &ss) { 35 | Ok(ranges) => ranges, 36 | Err(err) => { 37 | inner_err = Some(err); 38 | return false; 39 | } 40 | }; 41 | let escaped = as_24_bit_terminal_escaped(&ranges[..], true); 42 | diff_lines.push(escaped); 43 | } 44 | _ => { 45 | let ranges = match h.highlight_line(content, &ss) { 46 | Ok(ranges) => ranges, 47 | Err(err) => { 48 | inner_err = Some(err); 49 | return false; 50 | } 51 | }; 52 | let escaped = as_24_bit_terminal_escaped(&ranges[..], true); 53 | diff_lines.push(escaped); 54 | } 55 | } 56 | true 57 | })?; 58 | 59 | if let Some(err) = inner_err { 60 | Err(err.into()) 61 | } else { 62 | Ok(diff_lines) 63 | } 64 | } 65 | 66 | pub(crate) fn print_diff_lines(diff_lines: &[String]) -> Result<(), anyhow::Error> { 67 | let mut stdout = StandardStream::stdout(ColorChoice::Auto); 68 | for line in diff_lines { 69 | write!(&mut stdout, "{}", line)?; 70 | } 71 | stdout.reset()?; 72 | writeln!(&mut stdout)?; 73 | Ok(()) 74 | } 75 | 76 | pub(crate) fn print_diffstat(prefix: &str, diff: &Diff<'_>) -> Result<(), anyhow::Error> { 77 | let buf = diff.stats()?.to_buf(DiffStatsFormat::FULL, 80)?; 78 | let stat = std::str::from_utf8(&buf).context("converting diffstat to utf-8")?; 79 | println!("{prefix} changes:\n{stat}"); 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /src/rebaser.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::Context as _; 4 | use anyhow::{anyhow, bail}; 5 | use git2::AnnotatedCommit; 6 | use git2::Branch; 7 | use git2::Commit; 8 | use git2::Diff; 9 | use git2::Oid; 10 | use git2::{Rebase, Repository}; 11 | 12 | use crate::commit_display; 13 | 14 | pub(crate) fn do_rebase( 15 | repo: &Repository, 16 | branch: &Branch, 17 | commit_to_amend: &Commit, 18 | diff: &Diff, 19 | ) -> Result<(), anyhow::Error> { 20 | let first_parent = repo.find_annotated_commit(commit_parent(commit_to_amend)?.id())?; 21 | let branch_commit = repo.reference_to_annotated_commit(branch.get())?; 22 | let fixup_commit = branch.get().peel_to_commit()?; 23 | let fixup_message = fixup_commit.message(); 24 | 25 | let rebase = &mut repo 26 | .rebase(Some(&branch_commit), Some(&first_parent), None, None) 27 | .context("starting rebase")?; 28 | 29 | let mut branches = RepoBranches::for_repo(repo)?; 30 | 31 | if let Err(e) = apply_diff_in_rebase(repo, rebase, diff, &mut branches) { 32 | print_help_and_abort_rebase(rebase, &first_parent).context("aborting rebase")?; 33 | return Err(e); 34 | } 35 | 36 | match do_rebase_inner(repo, rebase, fixup_message, branches) { 37 | Ok(_) => { 38 | rebase.finish(None)?; 39 | Ok(()) 40 | } 41 | Err(e) => { 42 | print_help_and_abort_rebase(rebase, &first_parent).context("aborting rebase")?; 43 | Err(e) 44 | } 45 | } 46 | } 47 | 48 | pub(crate) fn print_help_and_abort_rebase( 49 | rebase: &mut Rebase, 50 | first_parent: &AnnotatedCommit, 51 | ) -> Result<(), git2::Error> { 52 | eprintln!("Aborting rebase, your changes are in the head commit."); 53 | eprintln!("You can apply it manually via:"); 54 | eprintln!( 55 | " git rebase --interactive --autosquash {}~", 56 | first_parent.id() 57 | ); 58 | rebase.abort()?; 59 | Ok(()) 60 | } 61 | 62 | pub(crate) fn apply_diff_in_rebase( 63 | repo: &Repository, 64 | rebase: &mut Rebase, 65 | diff: &Diff, 66 | branches: &mut RepoBranches, 67 | ) -> Result<(), anyhow::Error> { 68 | match rebase.next() { 69 | Some(ref res) => { 70 | let op = res.as_ref().map_err(|e| anyhow!("No commit: {}", e))?; 71 | let target_commit = repo.find_commit(op.id())?; 72 | repo.apply(diff, git2::ApplyLocation::Both, None)?; 73 | let mut idx = repo.index()?; 74 | let oid = idx.write_tree()?; 75 | let tree = repo.find_tree(oid)?; 76 | 77 | // TODO: Support squash amends 78 | 79 | let rewrit_id = target_commit.amend(None, None, None, None, None, Some(&tree))?; 80 | let rewrit_object = repo.find_object(rewrit_id, None)?; 81 | let rewrit_commit_id = repo.find_commit(rewrit_object.id())?.id(); 82 | let retargeted = 83 | branches.retarget_branches(target_commit.id(), rewrit_commit_id, rebase)?; 84 | for b in retargeted { 85 | println!("{}", b); 86 | } 87 | 88 | repo.reset(&rewrit_object, git2::ResetType::Soft, None)?; 89 | } 90 | None => bail!("Unable to start rebase: no first step in rebase"), 91 | }; 92 | Ok(()) 93 | } 94 | 95 | /// Do a rebase, pulling all intermediate branches along the way 96 | pub(crate) fn do_rebase_inner( 97 | repo: &Repository, 98 | rebase: &mut Rebase, 99 | fixup_message: Option<&str>, 100 | mut branches: RepoBranches, 101 | ) -> Result<(), anyhow::Error> { 102 | let sig = repo.signature()?; 103 | 104 | while let Some(ref res) = rebase.next() { 105 | use git2::RebaseOperationType::*; 106 | 107 | let op = res.as_ref().map_err(|e| anyhow!("Err: {}", e))?; 108 | match op.kind() { 109 | Some(Pick) => { 110 | let commit = repo.find_commit(op.id())?; 111 | let message = commit.message(); 112 | if message.is_some() && message != fixup_message { 113 | let new_id = rebase.commit(None, &sig, None)?; 114 | let retargeted = branches.retarget_branches(commit.id(), new_id, rebase)?; 115 | for b in retargeted { 116 | println!("{}", b); 117 | } 118 | } 119 | } 120 | Some(Fixup) | Some(Squash) | Some(Exec) | Some(Edit) | Some(Reword) => { 121 | // None of this should happen, we'd need to manually create the commits 122 | bail!("Unable to handle {:?} rebase operation", op.kind().unwrap()) 123 | } 124 | None => {} 125 | } 126 | } 127 | 128 | Ok(()) 129 | } 130 | 131 | pub(crate) struct RepoBranches<'a>(HashMap>>); 132 | 133 | pub(crate) struct RetargetedBranch { 134 | pub(crate) name: String, 135 | pub(crate) from: Oid, 136 | pub(crate) to: Oid, 137 | } 138 | 139 | impl std::fmt::Display for RetargetedBranch { 140 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 141 | let from = &self.from.to_string()[..15]; 142 | let to = &self.to.to_string()[..15]; 143 | let name = &self.name; 144 | f.write_fmt(format_args!("updated branch {name}: {from} -> {to}")) 145 | } 146 | } 147 | 148 | impl<'a> RepoBranches<'a> { 149 | pub(crate) fn for_repo(repo: &'a Repository) -> Result, anyhow::Error> { 150 | let mut branches: HashMap> = HashMap::new(); 151 | for (branch, _type) in repo.branches(Some(git2::BranchType::Local))?.flatten() { 152 | let oid = branch.get().peel_to_commit()?.id(); 153 | branches.entry(oid).or_default().push(branch); 154 | } 155 | Ok(RepoBranches(branches)) 156 | } 157 | 158 | /// Move branches whos commits have moved 159 | pub(crate) fn retarget_branches( 160 | &mut self, 161 | original_commit: Oid, 162 | target_commit: Oid, 163 | rebase: &mut Rebase<'_>, 164 | ) -> Result, anyhow::Error> { 165 | let mut retargeted = vec![]; 166 | if let Some(branches) = self.0.get_mut(&original_commit) { 167 | // Don't retarget the last branch, rebase.finish does that for us 168 | if rebase.operation_current() != Some(rebase.len() - 1) { 169 | for branch in branches.iter_mut() { 170 | retargeted.push(RetargetedBranch { 171 | name: branch 172 | .name() 173 | .context("getting a branch name")? 174 | .ok_or(anyhow!("branch should have a name"))? 175 | .to_owned(), 176 | from: original_commit, 177 | to: target_commit, 178 | }); 179 | branch 180 | .get_mut() 181 | .set_target(target_commit, "git-instafix retarget historical branch")?; 182 | } 183 | } 184 | } 185 | Ok(retargeted) 186 | } 187 | } 188 | 189 | pub(crate) fn commit_parent<'a>(commit: &'a Commit) -> Result, anyhow::Error> { 190 | match commit.parents().next() { 191 | Some(c) => Ok(c), 192 | None => bail!("Commit '{}' has no parents", commit_display(commit)), 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/selecter.rs: -------------------------------------------------------------------------------- 1 | //! mod selector is responsible for tooling around selecting which commit to ammend 2 | 3 | use std::collections::HashMap; 4 | 5 | use anyhow::{anyhow, bail}; 6 | use console::style; 7 | use dialoguer::Select; 8 | use git2::{Branch, BranchType, Commit, Oid, Reference, Repository}; 9 | 10 | use crate::config; 11 | use crate::format_ref; 12 | 13 | pub(crate) struct CommitSelection<'a> { 14 | pub commit: Commit<'a>, 15 | pub reference: Reference<'a>, 16 | } 17 | 18 | pub(crate) fn select_commit_to_amend<'a>( 19 | repo: &'a Repository, 20 | upstream: Option, 21 | max_commits: usize, 22 | message_pattern: Option<&str>, 23 | ) -> Result, anyhow::Error> { 24 | let mut walker = repo.revwalk()?; 25 | walker.push_head()?; 26 | let commits = if let Some(upstream) = upstream.as_ref() { 27 | let upstream_oid = upstream.commit.id(); 28 | let commits = walker 29 | .flatten() 30 | .take_while(|rev| *rev != upstream_oid) 31 | .take(max_commits) 32 | .map(|rev| repo.find_commit(rev)) 33 | .collect::, _>>()?; 34 | 35 | let head = repo.head()?; 36 | let current_branch_name = head 37 | .shorthand() 38 | .ok_or_else(|| anyhow!("HEAD's name is invalid utf-8"))?; 39 | if repo.head()?.peel_to_commit()?.id() == upstream.commit.id() 40 | && current_branch_name == upstream.reference.name().unwrap() 41 | { 42 | let upstream_setting = config::UPSTREAM_SETTING; 43 | bail!( 44 | "HEAD is already pointing at a common upstream branch\n\ 45 | If you don't create branches for your work consider setting upstream to a remote ref:\n\ 46 | \n \ 47 | git config {upstream_setting} origin/{current_branch_name}" 48 | ) 49 | } 50 | commits 51 | } else { 52 | walker 53 | .flatten() 54 | .take(max_commits) 55 | .map(|rev| repo.find_commit(rev)) 56 | .collect::, _>>()? 57 | }; 58 | if commits.is_empty() { 59 | bail!( 60 | "No commits between {} and {:?}", 61 | format_ref(&repo.head()?)?, 62 | upstream 63 | .map(|u| u.commit.id().to_string()) 64 | .unwrap_or_else(|| "".to_string()) 65 | ); 66 | } 67 | let branches: HashMap = repo 68 | .branches(None)? 69 | .filter_map(|b| { 70 | b.ok().and_then(|(b, _type)| { 71 | let name: Option = b.name().ok().and_then(|n| n.map(|n| n.to_owned())); 72 | let oid = b.into_reference().resolve().ok().and_then(|r| r.target()); 73 | name.and_then(|name| oid.map(|oid| (oid, name))) 74 | }) 75 | }) 76 | .collect(); 77 | if let Some(message_pattern) = message_pattern.as_ref() { 78 | let first = commit_id_and_summary(&commits, commits.len() - 1); 79 | let last = commit_id_and_summary(&commits, 0); 80 | commits 81 | .into_iter() 82 | .find(|commit| { 83 | commit 84 | .summary() 85 | .map(|s| s.contains(message_pattern)) 86 | .unwrap_or(false) 87 | }) 88 | .ok_or_else(|| { 89 | anyhow::anyhow!( 90 | "No commit contains the pattern in its summary between {}..{}", 91 | first, 92 | last 93 | ) 94 | }) 95 | } else { 96 | let rev_aliases = commits 97 | .iter() 98 | .enumerate() 99 | .map(|(i, commit)| { 100 | let bname = if i > 0 { 101 | branches 102 | .get(&commit.id()) 103 | .map(|n| format!("({}) ", n)) 104 | .unwrap_or_default() 105 | } else { 106 | String::new() 107 | }; 108 | format!( 109 | "{} {}{}", 110 | &style(&commit.id().to_string()[0..10]).blue(), 111 | style(bname).green(), 112 | commit.summary().unwrap_or("no commit summary") 113 | ) 114 | }) 115 | .collect::>(); 116 | if upstream.is_none() { 117 | println!("Select a commit to amend (no upstream for HEAD):"); 118 | } else { 119 | println!("Select a commit to amend:"); 120 | } 121 | let selected = Select::new().items(&rev_aliases).default(0).interact(); 122 | Ok(repo.find_commit(commits[selected?].id())?) 123 | } 124 | } 125 | 126 | pub(crate) fn get_merge_base<'a>( 127 | repo: &'a Repository, 128 | head_branch: &'a Branch, 129 | upstream_name: Option<&str>, 130 | ) -> Result>, anyhow::Error> { 131 | let (upstream, branch) = if let Some(explicit_upstream_name) = upstream_name { 132 | let reference = repo.resolve_reference_from_short_name(explicit_upstream_name)?; 133 | let r2 = repo.resolve_reference_from_short_name(explicit_upstream_name)?; 134 | (reference.peel_to_commit()?, r2) 135 | } else if let Some(branch) = find_default_upstream_branch(repo) { 136 | ( 137 | branch.into_reference().peel_to_commit()?, 138 | find_default_upstream_branch(repo).unwrap().into_reference(), 139 | ) 140 | } else if let Ok(upstream) = head_branch.upstream() { 141 | ( 142 | upstream.into_reference().peel_to_commit()?, 143 | head_branch.upstream().unwrap().into_reference(), 144 | ) 145 | } else { 146 | return Ok(None); 147 | }; 148 | 149 | let mb = repo.merge_base( 150 | head_branch 151 | .get() 152 | .target() 153 | .expect("all branches should have a target"), 154 | upstream.id(), 155 | )?; 156 | let commit = repo.find_object(mb, None).unwrap(); 157 | 158 | Ok(Some(CommitSelection { 159 | commit: commit.peel_to_commit()?, 160 | reference: branch, 161 | })) 162 | } 163 | 164 | pub(crate) fn commit_id_and_summary(commits: &[Commit<'_>], idx: usize) -> String { 165 | let first = commits 166 | .get(idx) 167 | .map(|c| { 168 | format!( 169 | "{} ({})", 170 | &c.id().to_string()[..10], 171 | c.summary().unwrap_or("") 172 | ) 173 | }) 174 | .unwrap_or_else(|| "".into()); 175 | first 176 | } 177 | 178 | /// Check if any of the `config::DEFAULT_UPSTREAM_BRANCHES` exist in the repository 179 | fn find_default_upstream_branch(repo: &Repository) -> Option { 180 | crate::config::DEFAULT_UPSTREAM_BRANCHES 181 | .iter() 182 | .find_map(|b| repo.find_branch(b, BranchType::Local).ok()) 183 | } 184 | -------------------------------------------------------------------------------- /static/00-initial-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibetor/git-instafix/b9b27e0b5f0eed13f444a39f4aed406b627fb4eb/static/00-initial-state.png -------------------------------------------------------------------------------- /static/01-selector.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibetor/git-instafix/b9b27e0b5f0eed13f444a39f4aed406b627fb4eb/static/01-selector.gif -------------------------------------------------------------------------------- /static/20-initial-full-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibetor/git-instafix/b9b27e0b5f0eed13f444a39f4aed406b627fb4eb/static/20-initial-full-repo.png -------------------------------------------------------------------------------- /static/21-with-upstream.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibetor/git-instafix/b9b27e0b5f0eed13f444a39f4aed406b627fb4eb/static/21-with-upstream.gif -------------------------------------------------------------------------------- /static/asciicast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibetor/git-instafix/b9b27e0b5f0eed13f444a39f4aed406b627fb4eb/static/asciicast.png -------------------------------------------------------------------------------- /static/full-workflow-simple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibetor/git-instafix/b9b27e0b5f0eed13f444a39f4aed406b627fb4eb/static/full-workflow-simple.gif -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use std::process::Output; 2 | 3 | use assert_cmd::Command; 4 | use assert_fs::prelude::*; 5 | use itertools::Itertools; 6 | 7 | #[test] 8 | fn test_can_compile() { 9 | let td = assert_fs::TempDir::new().unwrap(); 10 | let mut cmd = fixup(&td); 11 | let ex = cmd.arg("--help").output().unwrap(); 12 | let out = String::from_utf8(ex.stdout).unwrap(); 13 | let err = String::from_utf8(ex.stderr).unwrap(); 14 | assert!( 15 | out.contains("Fix a commit in your history with your currently-staged changes"), 16 | "out={} err='{}'", 17 | out, 18 | err 19 | ); 20 | } 21 | 22 | #[test] 23 | fn straightforward() { 24 | let td = assert_fs::TempDir::new().unwrap(); 25 | git_init(&td); 26 | 27 | git_file_commit("a", &td); 28 | git_file_commit("b", &td); 29 | git(&["checkout", "-b", "changes", "HEAD~"], &td); 30 | for n in &["c", "d", "e"] { 31 | git_file_commit(n, &td); 32 | } 33 | 34 | let out = git_log(&td); 35 | assert_eq!( 36 | out, 37 | "\ 38 | * e HEAD -> changes 39 | * d 40 | * c 41 | | * b main 42 | |/ 43 | * a 44 | ", 45 | "log:\n{}", 46 | out 47 | ); 48 | 49 | td.child("new").touch().unwrap(); 50 | git(&["add", "new"], &td); 51 | 52 | fixup(&td).args(["-P", "d"]).output().unwrap(); 53 | 54 | let shown = git_out( 55 | &["diff-tree", "--no-commit-id", "--name-only", "-r", ":/d"], 56 | &td, 57 | ); 58 | let files = string(shown.stdout); 59 | let err = string(shown.stderr); 60 | 61 | assert_eq!( 62 | files, 63 | "\ 64 | file_d 65 | new 66 | ", 67 | "out: {} err: {}", 68 | files, 69 | err 70 | ); 71 | } 72 | 73 | #[test] 74 | fn uses_merge_base_for_all_defaults() { 75 | for branch in ["main", "develop", "trunk", "master"] { 76 | eprintln!("testing branch {branch}"); 77 | let td = assert_fs::TempDir::new().unwrap(); 78 | git_init_default_branch_name(branch, &td); 79 | 80 | git_commits(&["a", "b", "c", "d"], &td); 81 | git(&["checkout", "-b", "changes", ":/c"], &td); 82 | git_commits(&["f", "g"], &td); 83 | 84 | let expected = format!( 85 | "\ 86 | * g HEAD -> changes 87 | * f 88 | | * d {branch} 89 | |/ 90 | * c 91 | * b 92 | * a 93 | " 94 | ); 95 | let actual = git_log(&td); 96 | assert_eq!( 97 | expected, actual, 98 | "expected:\n{}\nactual:\n{}", 99 | expected, actual 100 | ); 101 | 102 | // commits *before* the merge base of a default branch don't get found by 103 | // default 104 | td.child("new").touch().unwrap(); 105 | git(&["add", "new"], &td); 106 | fixup(&td).args(["-P", "b"]).assert().failure(); 107 | // commits *after* the merge base of a default branch *do* get found by default 108 | git(&["reset", "HEAD~"], &td); 109 | git(&["add", "new"], &td); 110 | fixup(&td).args(["-P", "f"]).assert().success(); 111 | } 112 | } 113 | 114 | #[test] 115 | fn simple_straightline_commits() { 116 | let td = assert_fs::TempDir::new().unwrap(); 117 | git_init(&td); 118 | 119 | git_commits(&["a", "b"], &td); 120 | git(&["checkout", "-b", "changes"], &td); 121 | git(&["branch", "-u", "main"], &td); 122 | git_commits(&["target", "d"], &td); 123 | 124 | let log = git_log(&td); 125 | assert_eq!( 126 | log, 127 | "\ 128 | * d HEAD -> changes 129 | * target 130 | * b main 131 | * a 132 | ", 133 | "log:\n{}", 134 | log 135 | ); 136 | 137 | td.child("new").touch().unwrap(); 138 | git(&["add", "new"], &td); 139 | 140 | fixup(&td).args(["-P", "target"]).assert().success(); 141 | 142 | let (files, err) = git_changed_files("target", &td); 143 | 144 | assert_eq!( 145 | files, 146 | "\ 147 | file_target 148 | new 149 | ", 150 | "out: {} err: {}", 151 | files, 152 | err 153 | ); 154 | } 155 | 156 | #[test] 157 | fn simple_straightline_tag_and_reference() { 158 | let td = assert_fs::TempDir::new().unwrap(); 159 | git_init(&td); 160 | 161 | let base = "v0.1.0"; 162 | 163 | git_commits(&["a", "b"], &td); 164 | git(&["tag", base], &td); 165 | git(&["checkout", "-b", "changes"], &td); 166 | git_commits(&["target", "d"], &td); 167 | 168 | let log = git_log(&td); 169 | assert_eq!( 170 | log, 171 | "\ 172 | * d HEAD -> changes 173 | * target 174 | * b tag: v0.1.0, main 175 | * a 176 | ", 177 | "log:\n{}", 178 | log 179 | ); 180 | 181 | td.child("new").touch().unwrap(); 182 | git(&["add", "new"], &td); 183 | 184 | fixup(&td) 185 | .args(["-P", "target", "-u", base]) 186 | .assert() 187 | .success(); 188 | 189 | let (files, err) = git_changed_files("target", &td); 190 | 191 | assert_eq!( 192 | files, 193 | "\ 194 | file_target 195 | new 196 | ", 197 | "out: {} err: {}", 198 | files, 199 | err 200 | ); 201 | 202 | // also check that we can use the full refspec definition 203 | 204 | td.child("new-full-ref").touch().unwrap(); 205 | git(&["add", "new-full-ref"], &td); 206 | fixup(&td) 207 | .args(["-P", "target", "-u", &format!("refs/tags/{base}")]) 208 | .unwrap(); 209 | 210 | let (files, err) = git_changed_files("target", &td); 211 | assert_eq!( 212 | files, 213 | "\ 214 | file_target 215 | new 216 | new-full-ref 217 | ", 218 | "out: {} err: {}", 219 | files, 220 | err 221 | ); 222 | } 223 | 224 | #[test] 225 | fn simple_straightline_remote_branch() { 226 | let remote_td = assert_fs::TempDir::new().unwrap(); 227 | git_init(&remote_td); 228 | git_commits(&["a", "b"], &remote_td); 229 | 230 | let td = assert_fs::TempDir::new().unwrap(); 231 | git_init(&td); 232 | let remote_path = &remote_td 233 | .path() 234 | .as_os_str() 235 | .to_owned() 236 | .into_string() 237 | .unwrap(); 238 | git(&["remote", "add", "origin", remote_path], &td); 239 | git(&["pull", "origin", "main:main"], &td); 240 | git_commits(&["target", "d"], &td); 241 | 242 | let log = git_log(&td); 243 | assert_eq!( 244 | log, 245 | "\ 246 | * d HEAD -> main 247 | * target 248 | * b origin/main 249 | * a 250 | ", 251 | "log:\n{}", 252 | log 253 | ); 254 | 255 | td.child("new").touch().unwrap(); 256 | git(&["add", "new"], &td); 257 | 258 | fixup(&td) 259 | .args(["-P", "target", "-u", "origin/main"]) 260 | .assert() 261 | .success(); 262 | 263 | let (files, err) = git_changed_files("target", &td); 264 | 265 | assert_eq!( 266 | files, 267 | "\ 268 | file_target 269 | new 270 | ", 271 | "out: {} err: {}", 272 | files, 273 | err 274 | ); 275 | } 276 | 277 | #[test] 278 | fn stashes_before_rebase() { 279 | let td = assert_fs::TempDir::new().unwrap(); 280 | git_init(&td); 281 | 282 | git_commits(&["a", "b"], &td); 283 | git(&["checkout", "-b", "changes"], &td); 284 | git(&["branch", "-u", "main"], &td); 285 | git_commits(&["target", "d"], &td); 286 | 287 | let log = git_log(&td); 288 | assert_eq!( 289 | log, 290 | "\ 291 | * d HEAD -> changes 292 | * target 293 | * b main 294 | * a 295 | ", 296 | "log:\n{}", 297 | log 298 | ); 299 | 300 | td.child("new").touch().unwrap(); 301 | 302 | let edited_file = "file_d"; 303 | td.child(edited_file).write_str("somthing").unwrap(); 304 | 305 | git(&["add", "new"], &td); 306 | let tracked_changed_files = git_worktree_changed_files(&td); 307 | assert_eq!(tracked_changed_files.trim(), edited_file); 308 | 309 | fixup(&td).args(["-P", "target"]).assert().success(); 310 | 311 | let (files, err) = git_changed_files("target", &td); 312 | 313 | assert_eq!( 314 | files, 315 | "\ 316 | file_target 317 | new 318 | ", 319 | "out: {} err: {}", 320 | files, 321 | err 322 | ); 323 | 324 | let popped_stashed_files = git_worktree_changed_files(&td); 325 | assert_eq!(popped_stashed_files.trim(), edited_file); 326 | } 327 | 328 | #[test] 329 | fn test_no_commit_in_range() { 330 | let td = assert_fs::TempDir::new().unwrap(); 331 | eprintln!("tempdir: {:?}", td.path()); 332 | git_init(&td); 333 | 334 | git_commits(&["a", "b", "c", "d"], &td); 335 | git(&["checkout", "-b", "changes", ":/c"], &td); 336 | git(&["branch", "-u", "main"], &td); 337 | git_commits(&["target", "f", "g"], &td); 338 | 339 | let out = git_log(&td); 340 | assert_eq!( 341 | out, 342 | "\ 343 | * g HEAD -> changes 344 | * f 345 | * target 346 | | * d main 347 | |/ 348 | * c 349 | * b 350 | * a 351 | ", 352 | "log:\n{}", 353 | out 354 | ); 355 | 356 | td.child("new").touch().unwrap(); 357 | git(&["add", "new"], &td); 358 | 359 | let assertion = fixup(&td).args(["-P", "b"]).assert().failure(); 360 | let out = string(assertion.get_output().stdout.clone()); 361 | let expected = "No commit contains the pattern"; 362 | assert!( 363 | out.contains(expected), 364 | "expected: {}\nactual: {}", 365 | expected, 366 | out 367 | ); 368 | 369 | fixup(&td).args(["-P", "target"]).assert().success(); 370 | 371 | let (files, err) = git_changed_files("target", &td); 372 | 373 | assert_eq!( 374 | files, 375 | "\ 376 | file_target 377 | new 378 | ", 379 | "out: {} err: {}", 380 | files, 381 | err 382 | ); 383 | } 384 | 385 | #[test] 386 | fn retarget_branches_in_range() { 387 | let td = assert_fs::TempDir::new().unwrap(); 388 | git_init(&td); 389 | 390 | git_commits(&["a", "b"], &td); 391 | git(&["checkout", "-b", "intermediate"], &td); 392 | git_commits(&["target", "c", "d"], &td); 393 | git(&["checkout", "-b", "points-at-intermediate"], &td); 394 | 395 | git(&["checkout", "-b", "changes"], &td); 396 | git_commits(&["e", "f"], &td); 397 | 398 | let expected = "\ 399 | * f HEAD -> changes 400 | * e 401 | * d points-at-intermediate, intermediate 402 | * c 403 | * target 404 | * b main 405 | * a 406 | "; 407 | let out = git_log(&td); 408 | assert_eq!(out, expected, "log:\n{}\nexpected:\n{}", out, expected); 409 | 410 | td.child("new").touch().unwrap(); 411 | git(&["add", "new"], &td); 412 | 413 | fixup(&td).args(["-P", "target"]).assert().success(); 414 | 415 | let (files, err) = git_changed_files("target", &td); 416 | 417 | assert_eq!( 418 | files, 419 | "\ 420 | file_target 421 | new 422 | ", 423 | "out: {} err: {}", 424 | files, 425 | err 426 | ); 427 | 428 | // should be identical to before 429 | let out = git_log(&td); 430 | assert_eq!(out, expected, "\nactual:\n{}\nexpected:\n{}", out, expected); 431 | } 432 | 433 | #[test] 434 | fn retarget_branch_target_of_edit() { 435 | let td = assert_fs::TempDir::new().unwrap(); 436 | git_init(&td); 437 | 438 | git_commits(&["a", "b"], &td); 439 | git(&["checkout", "-b", "intermediate"], &td); 440 | git_commits(&["c", "d", "target"], &td); 441 | 442 | git(&["checkout", "-b", "changes"], &td); 443 | git_commits(&["e", "f"], &td); 444 | 445 | let expected = "\ 446 | * f HEAD -> changes 447 | * e 448 | * target intermediate 449 | * d 450 | * c 451 | * b main 452 | * a 453 | "; 454 | let out = git_log(&td); 455 | assert_eq!( 456 | out, expected, 457 | "before rebase:\nactual:\n{}\nexpected:\n{}", 458 | out, expected 459 | ); 460 | 461 | td.child("new").touch().unwrap(); 462 | git(&["add", "new"], &td); 463 | 464 | fixup(&td).args(["-P", "target"]).assert().success(); 465 | 466 | let out = git_log(&td); 467 | assert_eq!( 468 | out, expected, 469 | "after rebase\nactual:\n{}\nexpected:\n{}", 470 | out, expected 471 | ); 472 | 473 | let (files, err) = git_changed_files("target", &td); 474 | assert_eq!( 475 | files, 476 | "\ 477 | file_target 478 | new 479 | ", 480 | "out: {} err: {}", 481 | files, 482 | err 483 | ); 484 | 485 | // should be identical to before 486 | let out = git_log(&td); 487 | assert_eq!(out, expected, "\nactual:\n{}\nexpected:\n{}", out, expected); 488 | } 489 | 490 | /////////////////////////////////////////////////////////////////////////////// 491 | // Helpers 492 | 493 | fn git_commits(ids: &[&str], tempdir: &assert_fs::TempDir) { 494 | for n in ids { 495 | git_file_commit(n, tempdir); 496 | } 497 | } 498 | 499 | fn git_init(tempdir: &assert_fs::TempDir) { 500 | git_init_default_branch_name("main", tempdir) 501 | } 502 | 503 | fn git_init_default_branch_name(name: &str, tempdir: &assert_fs::TempDir) { 504 | git(&["init", "--initial-branch", name], tempdir); 505 | git(&["config", "user.email", "nobody@nowhere.com"], tempdir); 506 | git(&["config", "user.name", "nobody"], tempdir); 507 | } 508 | 509 | /// Create a file and commit it with a mesage that is just the name of the file 510 | fn git_file_commit(name: &str, tempdir: &assert_fs::TempDir) { 511 | tempdir.child(format!("file_{}", name)).touch().unwrap(); 512 | git(&["add", "-A"], tempdir); 513 | git(&["commit", "-m", name], tempdir); 514 | } 515 | 516 | /// Get the git shown output for the target commit 517 | fn git_changed_files(name: &str, tempdir: &assert_fs::TempDir) -> (String, String) { 518 | let out = git_out( 519 | &[ 520 | "diff-tree", 521 | "--no-commit-id", 522 | "--name-only", 523 | "-r", 524 | &format!(":/{}", name), 525 | ], 526 | tempdir, 527 | ); 528 | (string(out.stdout), string(out.stderr)) 529 | } 530 | 531 | fn git_worktree_changed_files(td: &assert_fs::TempDir) -> String { 532 | string(git_out(&["diff", "--name-only"], td).stdout) 533 | } 534 | 535 | /// Run git in tempdir with args and panic if theres an error 536 | fn git(args: &[&str], tempdir: &assert_fs::TempDir) { 537 | git_inner(args, tempdir).ok().unwrap(); 538 | } 539 | 540 | fn git_out(args: &[&str], tempdir: &assert_fs::TempDir) -> Output { 541 | git_inner(args, tempdir).output().unwrap() 542 | } 543 | 544 | fn git_log(tempdir: &assert_fs::TempDir) -> String { 545 | let mut s = String::from_utf8( 546 | git_inner(&["log", "--all", "--format=%s %D", "--graph"], tempdir) 547 | .output() 548 | .unwrap() 549 | .stdout, 550 | ) 551 | .unwrap() 552 | .lines() 553 | .map(|l| l.trim_end()) 554 | .join("\n"); 555 | s.push('\n'); 556 | s 557 | } 558 | 559 | fn string(from: Vec) -> String { 560 | String::from_utf8(from).unwrap() 561 | } 562 | 563 | fn git_inner(args: &[&str], tempdir: &assert_fs::TempDir) -> Command { 564 | let mut cmd = Command::new("git"); 565 | cmd.args(args).current_dir(tempdir.path()); 566 | cmd 567 | } 568 | 569 | /// Get something that can get args added to it 570 | fn fixup(dir: &assert_fs::TempDir) -> Command { 571 | let mut c = Command::cargo_bin("git-instafix").unwrap(); 572 | c.current_dir(dir.path()) 573 | .env_remove("GIT_INSTAFIX_UPSTREAM"); 574 | c 575 | } 576 | -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 81 | 82 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 107 | 112 | 113 | 114 | 115 | 123 | 124 | 125 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 146 | 147 | 151 | 152 | 153 | 154 | 155 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 192 | 1 193 | 1 194 | 195 | 196 | 197 | 198 | 203 | 204 | 205 | 206 | 214 | 215 | 216 | 217 | 225 | 226 | 227 | 228 | 229 | 230 | --------------------------------------------------------------------------------