├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cargo-workspaces ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── changed.rs │ ├── create.rs │ ├── exec.rs │ ├── init.rs │ ├── list.rs │ ├── main.rs │ ├── plan.rs │ ├── publish.rs │ ├── rename.rs │ ├── utils │ │ ├── basic_checks.rs │ │ ├── cargo.rs │ │ ├── changable.rs │ │ ├── config.rs │ │ ├── dag.rs │ │ ├── dev_dep_remover.rs │ │ ├── error.rs │ │ ├── git.rs │ │ ├── list.rs │ │ ├── mod.rs │ │ ├── pkg.rs │ │ ├── publish.rs │ │ └── version.rs │ └── version.rs └── tests │ ├── create.rs │ ├── exec.rs │ ├── init.rs │ ├── list.rs │ ├── snapshots │ ├── create__already_exists.snap │ ├── create__create_bin_2015-2.snap │ ├── create__create_bin_2015.snap │ ├── create__create_bin_2018.snap │ ├── create__create_lib_2015.snap │ ├── create__create_lib_2018.snap │ ├── create__create_lib_and_bin_fails.snap │ ├── create__exclude_fails.snap │ ├── create__member_glob-2.snap │ ├── create__member_glob.snap │ ├── exec__normal-2.snap │ ├── exec__normal.snap │ ├── exec__normal_ignore-2.snap │ ├── exec__normal_ignore.snap │ ├── init__no_path.snap │ ├── init__normal-2.snap │ ├── init__normal.snap │ ├── init__normal_with_manifest-2.snap │ ├── init__normal_with_manifest.snap │ ├── init__root-2.snap │ ├── init__root.snap │ ├── init__root_with_manifest-2.snap │ ├── init__root_with_manifest.snap │ ├── init__root_with_manifest_no_workspace-2.snap │ ├── init__root_with_manifest_no_workspace.snap │ ├── list__all.snap │ ├── list__json_conflicts_with_long.snap │ ├── list__long.snap │ ├── list__long_all.snap │ ├── list__long_root.snap │ └── list__single.snap │ └── utils.rs └── fixtures ├── create ├── Cargo.toml ├── dep1 │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── dep2 │ ├── Cargo.toml │ └── src │ └── lib.rs ├── normal ├── Cargo.toml ├── dep1 │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── dep2 │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── top │ ├── Cargo.toml │ └── src │ └── main.rs ├── normal_git ├── HEAD └── config ├── private ├── Cargo.toml ├── private │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── simple │ ├── Cargo.toml │ └── src │ └── main.rs ├── root ├── Cargo.toml └── src │ └── main.rs └── single ├── Cargo.toml └── simple ├── Cargo.toml └── src └── main.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | jobs: 8 | ci: 9 | name: CI 10 | needs: [test, lint, lockfile] 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Done 14 | run: exit 0 15 | test: 16 | name: Tests 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - os: macos-13 22 | target: x86_64-apple-darwin 23 | - os: macos-latest 24 | target: aarch64-apple-darwin 25 | - os: ubuntu-latest 26 | target: x86_64-unknown-linux-gnu 27 | - os: ubuntu-latest 28 | target: i686-unknown-linux-gnu 29 | - os: windows-latest 30 | target: x86_64-pc-windows-msvc 31 | - os: windows-latest 32 | target: i686-pc-windows-msvc 33 | runs-on: ${{ matrix.os }} 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | - name: Install rust 38 | uses: dtolnay/rust-toolchain@1.78.0 39 | with: 40 | targets: ${{ matrix.target }} 41 | - name: Install linker 42 | if: matrix.target == 'i686-unknown-linux-gnu' 43 | run: | 44 | sudo apt-get update 45 | sudo apt-get install gcc-multilib 46 | - name: Test 47 | run: cargo test --target ${{ matrix.target }} --manifest-path cargo-workspaces/Cargo.toml 48 | lint: 49 | name: Lint 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | - name: Install rust 55 | uses: dtolnay/rust-toolchain@1.78.0 56 | with: 57 | components: rustfmt, clippy 58 | - name: Lint check 59 | run: cargo clippy --all-targets --all-features --manifest-path cargo-workspaces/Cargo.toml -- -D warnings 60 | - name: Format check 61 | run: cargo fmt --manifest-path cargo-workspaces/Cargo.toml -- --check 62 | lockfile: 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | - name: Install rust 68 | uses: dtolnay/rust-toolchain@1.78.0 69 | - name: Lockfile check 70 | run: cargo update -w --locked --manifest-path cargo-workspaces/Cargo.toml 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are backup files generated by rustfmt 2 | **/*.rs.bk 3 | 4 | cargo-workspaces/target 5 | fixtures/**/Cargo.lock 6 | fixtures/**/target 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.0 4 | 5 | ### BREAKING 6 | * Update MSRV to 1.78.0 7 | * Remove `include-merged-tags` option from `changed`, `version` & `publish` subcommands 8 | 9 | ### Enhancements 10 | * Allow `init` subcommand to use existing manifest 11 | * Improve error message when no public packages found with `list` 12 | * Added support for `2024` edition when creating package 13 | * Added `locked` flag to `publish` subcommand 14 | * Added `since` flag to `version` & `publish` subcommands 15 | * Added `publish-interval` flag to `publish` subcommand 16 | 17 | ### Bug Fixes 18 | * Fixes issue with first publish to custom registries 19 | * Fixes issue with recognising the last tag with `changed` 20 | * Fixes issue with removing dev deps and cargo's git ops 21 | 22 | ## 0.3.6 23 | 24 | ### Bug Fixes 25 | * Fix issue with Ctrl+C termination 26 | * Fix issue with ignoring dev-dependencies for targets when publishing 27 | 28 | ## 0.3.5 29 | 30 | ### Enhancements 31 | * Allow error on no changes detected 32 | * Added `plan` subcommand to list crates to be published 33 | * Add dry-run mode for publishing 34 | 35 | ## 0.3.4 36 | 37 | ### Bug Fixes 38 | * Fixes issue with listing public members when private members exist 39 | 40 | ## 0.3.3 41 | 42 | ### Enhancements 43 | * Allow ignore private crates for `exec` 44 | * Listing now follows DAG order of the dependencies 45 | 46 | ## 0.3.2 47 | 48 | ### Bug Fixes 49 | * Fix issue with creating a package with newer versions of Rust 50 | 51 | ## 0.3.1 52 | 53 | ### Enhancements 54 | * Infer package name when creating a workspace member crate 55 | * Improve time taken for versioning and releasing 56 | * Support TLS and authentication for sparse registries 57 | 58 | ### Bug Fixes 59 | * Fix issue with publishing to sparse registries 60 | 61 | ## 0.3.0 62 | 63 | ### BREAKING 64 | * Update MSRV to 1.70.0 65 | 66 | ### Enhancements 67 | * Renamed `--from-git` flag to `--publish-as-is` 68 | * Add new workspace member entry automatically when creating it 69 | 70 | ### Bug Fixes 71 | * Respect protocols in registry URLs 72 | 73 | ## 0.2.44 74 | 75 | ### Enhancements 76 | * Better recognition on when to ignore dev-dependencies, avoids some publishing issues with `--from-git` flag 77 | 78 | ## 0.2.43 79 | 80 | ### Enhancements 81 | * Ignore dev-dependencies when publishing to Cargo, avoids some versioning issues 82 | 83 | ### Bug Fixes 84 | * Respect `registry` option when checking index during publishing 85 | 86 | ## 0.2.42 87 | 88 | ### Enhancements 89 | * Added `ignore` flag to `exec` subcommand 90 | 91 | ## 0.2.41 92 | 93 | ### Bug Fixes 94 | * Fix bug with some dependency entries not being updated 95 | 96 | ## 0.2.39 97 | 98 | ### Bug Fixes 99 | * Fix bug with not updating version in `workspace.dependencies` 100 | 101 | ## 0.2.38 102 | 103 | ### Enhancements 104 | * Supports cargo workspace inheritance 105 | 106 | ## 0.2.37 107 | 108 | ### Enhancements 109 | * Added `skip` option during versioning 110 | 111 | ### Bug Fixes 112 | * Restores cursor if versioning is cancelled 113 | 114 | ## 0.2.36 115 | 116 | ### Enhancements 117 | * Improve the glob pattern support allowed in arguments of subcommands 118 | 119 | ## 0.2.35 120 | 121 | ### Enhancements 122 | * Allow renaming single crates 123 | 124 | ## 0.2.34 125 | 126 | ### Enhancements 127 | * Added `registry` flag to `publish` subcommand 128 | 129 | ## 0.2.33 130 | 131 | ### Bug Fixes 132 | * Support target dependencies when changing version and renaming packages 133 | 134 | ## 0.2.30 135 | 136 | ### Bug Fixes 137 | * Remove some flakiness in detecting git command success 138 | 139 | ## 0.2.29 140 | 141 | ### Enhancements 142 | * Added `lib`, `bin` flags to `create` subcommand 143 | * Added `edition`, `name` options to `create` subcommand 144 | 145 | ## 0.2.28 146 | 147 | ### Enhancements 148 | * Support reading some options from manifests 149 | 150 | ## 0.2.26 151 | 152 | ### Enhancements 153 | * Support private registries 154 | * Skipping published crates is now the default behaviour 155 | 156 | ## 0.2.24 157 | 158 | ### Bug fixes 159 | * Don't add untracked files when publishing/versioning 160 | 161 | ## 0.2.23 162 | 163 | ### Enhancements 164 | * Added `--no-global-tag` flag 165 | 166 | ## 0.2.17 167 | 168 | ### Enhancements 169 | * Treat `main` branch similarily to `master` 170 | 171 | ## 0.2.16 172 | 173 | ### Enhancements 174 | * Forward verbose to cargo where possible 175 | 176 | ## 0.2.15 177 | 178 | ### Enhancements 179 | * Added init subcommand 180 | 181 | ## 0.2.14 182 | 183 | ### Bug Fixes 184 | * Allow tag prefix to be actually empty. 185 | 186 | ### Enhancements 187 | * Executing a command now follows DAG order of the dependencies. 188 | 189 | ## 0.2.12 190 | 191 | ### Enhancements 192 | * Allow dirty working directories to be published 193 | 194 | ## 0.2.11 195 | 196 | ### Bug Fixes 197 | * Support cases where dependencies are renamed 198 | 199 | ### Enhancements 200 | * Added rename subcommand 201 | 202 | ## 0.2.10 203 | 204 | ### Bug Fixes 205 | * Improve tag pushing to work with followTags config 206 | 207 | ## 0.2.9 208 | 209 | ### Enhancements 210 | * Added custom option to skipping prompt 211 | 212 | ## 0.2.8 213 | 214 | ### Bug Fixes 215 | * Fix issue with crates-index not being up to date even after refreshing 216 | 217 | ## 0.2.4 218 | 219 | ### Bug Fixes 220 | * Verify each crate during publishing only and not before 221 | * Wait for crates-index to be up to date before publishing the next package 222 | 223 | ### Enhancements 224 | * Added option to skip verification 225 | 226 | ## 0.2.3 227 | 228 | ### Bug Fixes 229 | * Improve detection of LF 230 | 231 | ## 0.2.2 232 | 233 | ### Bug Fixes 234 | * Improve change detection on windows 235 | 236 | ## 0.2.1 237 | 238 | ### Enhancements 239 | * Don't complain about no changes when force option is specified during versioning 240 | 241 | ## 0.2.0 242 | 243 | #### Breaking 244 | * Improved the next version determination for prereleases 245 | 246 | #### Enhancements 247 | * Added prerelease identifier selection option for versioning 248 | * Added prerelease option to skipping prompt 249 | 250 | ## 0.1.9 251 | 252 | #### Enhancements 253 | * Update Cargo.lock for the versioned packages 254 | 255 | ## 0.1.8 256 | 257 | #### Enhancements 258 | * Improved CI usage by implementing prompt skipping 259 | 260 | ## 0.1.7 261 | 262 | #### Enhancements 263 | * Allow versioning for private packages 264 | 265 | ## 0.1.5 266 | 267 | #### Bug Fixes 268 | * Verify all the crates first before publishing 269 | * Fixed windows LF issues with git 270 | 271 | ## 0.1.4 272 | 273 | #### Enhancements 274 | * Annotate generated tags 275 | * Allow individual tag prefixes 276 | 277 | ## 0.1.3 278 | 279 | #### Enhancements 280 | * Add readme to crates.io 281 | 282 | ## 0.1.2 283 | 284 | #### Bug Fixes 285 | * Fixed path issues with long listing crates on windows 286 | 287 | ## 0.1.1 288 | 289 | * Initial release 290 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pavan Kumar Sunkara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # cargo-workspaces 3 | 4 | Inspired by [Lerna](https://lerna.js.org/) 5 | 6 | A tool that optimizes the workflow around cargo workspaces with `git` and `cargo` by providing utilities to 7 | version, publish, execute commands and more. 8 | 9 | I made this to work on [clap](https://github.com/clap-rs/clap) and other projects that rely on workspaces. 10 | But this will also work on single crates because by default every individual crate is a workspace. 11 | 12 | 1. [Installation](#installation) 13 | 2. [Usage](#usage) 14 | 1. [Init](#init) 15 | 2. [Create](#create) 16 | 3. [List](#list) 17 | 4. [Changed](#changed) 18 | 5. [Exec](#exec) 19 | 6. [Version](#version) 20 | 1. [Fixed or Independent](#fixed-or-independent) 21 | 7. [Publish](#publish) 22 | 8. [Rename](#rename) 23 | 9. [Plan](#plan) 24 | 3. [Config](#config) 25 | 4. [Changelog](#changelog) 26 | 27 | ## Installation 28 | 29 | ``` 30 | cargo install cargo-workspaces 31 | ``` 32 | 33 | ## Usage 34 | 35 | The installed tool can be called by `cargo workspaces` or `cargo ws`. Both of them point to the same. 36 | 37 | You can use `cargo ws help` or `cargo ws help ` anytime to understand allowed options. 38 | 39 | The basic commands available for this tool are given below. Assuming you run them inside a cargo workspace. 40 | 41 | ### Init 42 | 43 | Initializes a new cargo workspace in the given directory. Creates `Cargo.toml` if it does not exist and 44 | fills the `members` with the all the crates that can be found in that directory. 45 | 46 | ``` 47 | USAGE: 48 | cargo workspaces init [OPTIONS] [PATH] 49 | 50 | ARGS: 51 | Path to the workspace root [default: .] 52 | 53 | OPTIONS: 54 | -h, --help Print help information 55 | --resolver Workspace feature resolver version [possible values: 1, 2] 56 | ``` 57 | 58 | ### Create 59 | 60 | Interactively creates a new crate in the workspace. *We recommend using this instead of `cargo new`*. All 61 | the crates start with `0.0.0` version because the [version](#version) is responsible for determining the 62 | version. 63 | 64 | ``` 65 | USAGE: 66 | cargo workspaces create [OPTIONS] 67 | 68 | ARGS: 69 | Path for the crate relative to the workspace manifest 70 | 71 | OPTIONS: 72 | --bin Whether this is a binary crate 73 | --edition The crate edition [possible values: 2015, 2018, 2021, 2024] 74 | -h, --help Print help information 75 | --lib Whether this is a library crate 76 | --name The name of the crate 77 | ``` 78 | 79 | ### List 80 | 81 | Lists crates in the workspace. 82 | 83 | ``` 84 | USAGE: 85 | cargo workspaces list [OPTIONS] 86 | 87 | OPTIONS: 88 | -h, --help Print help information 89 | 90 | LIST OPTIONS: 91 | -a, --all Show private crates that are normally hidden 92 | --json Show information as a JSON array 93 | -l, --long Show extended information 94 | ``` 95 | 96 | Several aliases are available. 97 | 98 | * `cargo ws ls` implies `cargo ws list` 99 | * `cargo ws ll` implies `cargo ws list --long` 100 | * `cargo ws la` implies `cargo ws list --all` 101 | 102 | ### Changed 103 | 104 | List crates that have changed since the last git tag. This is useful to see the list of crates that 105 | would be the subjects of the next [version](#version) or [publish](#publish) command. 106 | 107 | ``` 108 | USAGE: 109 | cargo workspaces changed [OPTIONS] 110 | 111 | OPTIONS: 112 | --error-on-empty Return non-zero exit code if no changes detected 113 | --force Always include targeted crates matched by glob even when there are no changes 114 | -h, --help Print help information 115 | --ignore-changes Ignore changes in files matched by glob 116 | --since Use this git reference instead of the last tag 117 | 118 | LIST OPTIONS: 119 | -a, --all Show private crates that are normally hidden 120 | --json Show information as a JSON array 121 | -l, --long Show extended information 122 | ``` 123 | 124 | ### Exec 125 | 126 | Executes an arbitrary command in each crate of the workspace. 127 | 128 | ``` 129 | USAGE: 130 | cargo workspaces exec [OPTIONS] ... 131 | 132 | ARGS: 133 | ... 134 | 135 | OPTIONS: 136 | -h, --help Print help information 137 | --ignore Ignore the crates matched by glob 138 | --ignore-private Ignore private crates 139 | --no-bail Continue executing command despite non-zero exit in a given crate 140 | ``` 141 | 142 | For example, if you want to run `ls -l` in each crate, you can simply do `cargo ws exec ls -l`. 143 | 144 | ### Version 145 | 146 | Bump versions of the crates in the workspace. This command does the following: 147 | 148 | 1. Identifies crates that have been updated since the previous tagged release 149 | 2. Prompts for a new version according to the crate 150 | 3. Modifies crate manifest to reflect new release 151 | 4. Update intra-workspace dependency version constraints if needed 152 | 5. Commits those changes 153 | 6. Tags the commit 154 | 7. Pushes to the git remote 155 | 156 | You can influence the above steps with the flags and options for this command. 157 | 158 | ``` 159 | USAGE: 160 | cargo workspaces version [OPTIONS] [ARGS] 161 | 162 | OPTIONS: 163 | -h, --help Print help information 164 | 165 | VERSION ARGS: 166 | Increment all versions by the given explicit semver keyword while skipping the prompts for them 167 | [possible values: major, minor, patch, premajor, preminor, prepatch, skip, prerelease, custom] 168 | Specify custom version value when 'bump' is set to 'custom' 169 | 170 | VERSION OPTIONS: 171 | -a, --all Also do versioning for private crates (will not be published) 172 | --exact Specify inter dependency version numbers exactly with `=` 173 | --force Always include targeted crates matched by glob even when there are no changes 174 | --ignore-changes Ignore changes in files matched by glob 175 | --pre-id Specify prerelease identifier 176 | -y, --yes Skip confirmation prompt 177 | 178 | GIT OPTIONS: 179 | --allow-branch Specify which branches to allow from [default: master] 180 | --amend Amend the existing commit, instead of generating a new one 181 | --git-remote Push git changes to the specified remote [default: origin] 182 | --individual-tag-prefix Customize prefix for individual tags (should contain `%n`) [default: %n@] 183 | -m, --message Use a custom commit message when creating the version commit [default: Release %v] 184 | --no-git-commit Do not commit version changes 185 | --no-git-push Do not push generated commit and tags to git remote 186 | --no-git-tag Do not tag generated commit 187 | --no-global-tag Do not create a global tag for a workspace 188 | --no-individual-tags Do not tag individual versions for crates 189 | --tag-prefix Customize tag prefix (can be empty) [default: v] 190 | ``` 191 | 192 | #### Fixed or Independent 193 | 194 | By default, all the crates in the workspace will share a single version. But if you want the crate to have 195 | it's version be independent of the other crates, you can add the following to that crate: 196 | 197 | ```toml 198 | [package.metadata.workspaces] 199 | independent = true 200 | ``` 201 | 202 | For more details, check [Config](#config) section below. 203 | 204 | ### Publish 205 | 206 | Publish all the crates from the workspace in the correct order according to the dependencies. By default, 207 | this command runs [version](#version) first. If you do not want that to happen, you can supply the 208 | `--from-git` option. 209 | 210 | To avoid potential rate-limiting by the registry when publishing many crates, you can use the `--publish-interval ` option. For example, `cargo workspaces publish --publish-interval 10` will wait 10 seconds between each crate publication. 211 | 212 | > Note: dev-dependencies are not taken into account when building the dependency 213 | > graph used to determine the proper publishing order. This is because 214 | > dev-dependencies are ignored by `cargo publish` - as such, a dev-dependency on a 215 | > local crate (with a `path` attribute), should _not_ have a `version` field. 216 | 217 | ``` 218 | USAGE: 219 | cargo workspaces publish [OPTIONS] [ARGS] 220 | 221 | OPTIONS: 222 | -h, --help Print help information 223 | 224 | VERSION ARGS: 225 | Increment all versions by the given explicit semver keyword while skipping the prompts for them 226 | [possible values: major, minor, patch, premajor, preminor, prepatch, skip, prerelease, custom] 227 | Specify custom version value when 'bump' is set to 'custom' 228 | 229 | VERSION OPTIONS: 230 | -a, --all Also do versioning for private crates (will not be published) 231 | --exact Specify inter dependency version numbers exactly with `=` 232 | --force Always include targeted crates matched by glob even when there are no changes 233 | --ignore-changes Ignore changes in files matched by glob 234 | --pre-id Specify prerelease identifier 235 | --since Use this git reference instead of the last tag 236 | -y, --yes Skip confirmation prompt 237 | 238 | GIT OPTIONS: 239 | --allow-branch Specify which branches to allow from [default: master] 240 | --amend Amend the existing commit, instead of generating a new one 241 | --git-remote Push git changes to the specified remote [default: origin] 242 | --individual-tag-prefix Customize prefix for individual tags (should contain `%n`) [default: %n@] 243 | -m, --message Use a custom commit message when creating the version commit [default: Release %v] 244 | --no-git-commit Do not commit version changes 245 | --no-git-push Do not push generated commit and tags to git remote 246 | --no-git-tag Do not tag generated commit 247 | --no-global-tag Do not create a global tag for a workspace 248 | --no-individual-tags Do not tag individual versions for crates 249 | --tag-prefix Customize tag prefix (can be empty) [default: v] 250 | 251 | PUBLISH OPTIONS: 252 | --allow-dirty Allow dirty working directories to be published 253 | --dry-run Runs in dry-run mode 254 | --locked Assert that `Cargo.lock` will remain unchanged 255 | --no-remove-dev-deps Don't remove dev-dependencies while publishing 256 | --no-verify Skip crate verification (not recommended) 257 | --publish-as-is Publish crates from the current commit without versioning 258 | --publish-interval Number of seconds to wait between publish attempt 259 | 260 | REGISTRY OPTIONS: 261 | --registry The Cargo registry to use 262 | --token The token to use for accessing the registry 263 | ``` 264 | 265 | ### Rename 266 | 267 | Rename crates in the project. You can run this command when you might want to publish the crates with a standard prefix. 268 | 269 | ``` 270 | USAGE: 271 | cargo workspaces rename [OPTIONS] 272 | 273 | ARGS: 274 | The value that should be used as new name (should contain `%n`) 275 | 276 | OPTIONS: 277 | -a, --all Rename private crates too 278 | -f, --from Rename only a specific crate 279 | -h, --help Print help information 280 | --ignore Ignore the crates matched by glob 281 | ``` 282 | 283 | ### Plan 284 | 285 | List the crates in publishing order. This does not check for changes or try to version. It takes the crates as-is. 286 | 287 | ``` 288 | USAGE: 289 | cargo workspaces plan [OPTIONS] 290 | 291 | OPTIONS: 292 | -h, --help Print help information 293 | --skip-published Skip already published crate versions 294 | 295 | REGISTRY OPTIONS: 296 | --registry The Cargo registry to use 297 | --token The token to use for accessing the registry 298 | 299 | LIST OPTIONS: 300 | --json Show information as a JSON array 301 | -l, --long Show extended information 302 | ``` 303 | 304 | ## Config 305 | 306 | There are two kind of options. 307 | 308 | * **Workspace**: Options that are specified in the workspace with `[workspace.metadata.workspaces]` 309 | * **Package**: Options that are specified in the package with `[package.metadata.workspaces]` 310 | 311 | If an option is allowed to exist in both places, it means that the value specified in the **Package** 312 | overrides the value specified in **Workspace**. 313 | 314 | | Name | Type | Workspace | Package | Used in Commands | 315 | | --- | --- | :---: | :---: | --- | 316 | | `allow_branch` | `String` | Yes | No | `version`, `publish` | 317 | | `independent` | `bool` | No | Yes | `version`, `publish` | 318 | | `no_individual_tags` | `bool` | Yes | No | `version`, `publish` | 319 | 320 | 321 | ## Contributors 322 | Here is a list of [Contributors](http://github.com/pksunkara/cargo-workspaces/contributors) 323 | 324 | 325 | ### TODO 326 | 327 | ## Changelog 328 | Please see [CHANGELOG.md](CHANGELOG.md). 329 | 330 | 331 | ## License 332 | MIT/X11 333 | 334 | 335 | ## Bug Reports 336 | Report [here](http://github.com/pksunkara/cargo-workspaces/issues). 337 | 338 | 339 | ## Creator 340 | Pavan Kumar Sunkara (pavan.sss1991@gmail.com) 341 | 342 | Follow me on [github](https://github.com/users/follow?target=pksunkara), [twitter](http://twitter.com/pksunkara) 343 | -------------------------------------------------------------------------------- /cargo-workspaces/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-workspaces" 3 | version = "0.4.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | description = "Cargo workspace manager" 7 | repository = "https://github.com/pksunkara/cargo-workspaces" 8 | license = "MIT" 9 | readme = "README.md" 10 | exclude = ["tests"] 11 | rust-version = "1.78" 12 | default-run = "cargo-workspaces" 13 | 14 | [[bin]] 15 | name = "cargo-workspaces" 16 | path = "src/main.rs" 17 | 18 | [[bin]] 19 | name = "cargo-ws" 20 | path = "src/../src/main.rs" 21 | test = false 22 | bench = false 23 | 24 | [profile.release] 25 | lto = "thin" 26 | 27 | [dependencies] 28 | camino = "1.0.1" 29 | cargo_metadata = "0.13.1" 30 | clap = { version = "~3.1.12", features = ["derive", "wrap_help", "unstable-replace"] } 31 | oclif = "0.4.0" 32 | openssl = { version = "0.10", optional = true, features = ["vendored"] } 33 | semver = "0.11" 34 | serde = { version = "1.0.188", features = ["derive"] } 35 | serde_json = "1.0.107" 36 | thiserror = "1.0.48" 37 | regex = "1.3.7" 38 | glob = "0.3.1" 39 | globset = "0.4.13" 40 | dialoguer = "0.9.0" 41 | lazy_static = "1.4.0" 42 | indexmap = "1.6.0" 43 | tame-index = { version = "0.9.0", features = ["git", "sparse"] } 44 | dunce = "1.0.4" 45 | ctrlc = "3.4.1" 46 | toml_edit = "0.19.10" 47 | url = "2.5.2" 48 | 49 | [dev-dependencies] 50 | assert_cmd = "1.0" 51 | insta = { version = "1.32.0", features = ["redactions"] } 52 | indoc = "1.0.9" 53 | serial_test = "2.0.0" 54 | tempfile = "3.6.0" 55 | 56 | [workspace.metadata.workspaces] 57 | no_individual_tags = true 58 | -------------------------------------------------------------------------------- /cargo-workspaces/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /cargo-workspaces/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /cargo-workspaces/src/changed.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{list, ChangeData, ChangeOpt, Error, ListOpt, Result}; 2 | 3 | use cargo_metadata::Metadata; 4 | use clap::Parser; 5 | use oclif::term::TERM_OUT; 6 | 7 | /// List crates that have changed since the last tagged release 8 | #[derive(Debug, Parser)] 9 | pub struct Changed { 10 | #[clap(flatten)] 11 | list: ListOpt, 12 | 13 | #[clap(flatten)] 14 | change: ChangeOpt, 15 | 16 | /// Return non-zero exit code if no changes detected 17 | #[clap(long)] 18 | error_on_empty: bool, 19 | } 20 | 21 | impl Changed { 22 | pub fn run(self, metadata: Metadata) -> Result { 23 | let mut since = self.change.since.clone(); 24 | 25 | if self.change.since.is_none() { 26 | let change_data = ChangeData::new(&metadata, &self.change)?; 27 | 28 | if change_data.count == "0" { 29 | TERM_OUT 30 | .write_line("Current HEAD is already released, skipping change detection")?; 31 | return self.finish(); 32 | } 33 | 34 | since = change_data.since; 35 | } 36 | 37 | let pkgs = self 38 | .change 39 | .get_changed_pkgs(&metadata, &since, self.list.all)?; 40 | 41 | if pkgs.0.is_empty() && self.error_on_empty { 42 | return self.finish(); 43 | } 44 | 45 | list(&pkgs.0, self.list) 46 | } 47 | 48 | fn finish(self) -> Result { 49 | if self.error_on_empty { 50 | return Err(Error::NoChanges); 51 | } 52 | 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cargo-workspaces/src/create.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{cargo, change_versions, info, Error, Result, INTERNAL_ERR}; 2 | 3 | use camino::Utf8PathBuf; 4 | use cargo_metadata::Metadata; 5 | use clap::{ArgEnum, Parser}; 6 | use dialoguer::{theme::ColorfulTheme, Input, Select}; 7 | use dunce::canonicalize; 8 | use glob::Pattern; 9 | use oclif::term::TERM_ERR; 10 | use semver::Version; 11 | use toml_edit::{Array, Document, Formatted, Item, Table, Value}; 12 | 13 | use std::{ 14 | collections::BTreeMap as Map, 15 | env::current_dir, 16 | fs::{create_dir_all, read_to_string, remove_dir_all, remove_file, write}, 17 | path::Path, 18 | }; 19 | 20 | #[derive(Debug, Clone, ArgEnum)] 21 | enum Edition { 22 | #[clap(name = "2015")] 23 | Fifteen, 24 | #[clap(name = "2018")] 25 | Eighteen, 26 | #[clap(name = "2021")] 27 | TwentyOne, 28 | #[clap(name = "2024")] 29 | TwentyFour, 30 | } 31 | 32 | /// Create a new workspace crate 33 | #[derive(Debug, Parser)] 34 | pub struct Create { 35 | /// Path for the crate relative to the workspace manifest 36 | path: String, 37 | 38 | /// The crate edition 39 | #[clap(long, arg_enum)] 40 | edition: Option, 41 | 42 | /// Whether this is a binary crate 43 | #[clap(long, conflicts_with = "lib")] 44 | bin: bool, 45 | 46 | /// Whether this is a library crate 47 | #[clap(long)] 48 | lib: bool, 49 | 50 | /// The name of the crate 51 | #[clap(long)] 52 | name: Option, 53 | } 54 | 55 | impl Create { 56 | pub fn run(&self, metadata: Metadata) -> Result { 57 | if canonicalize(&metadata.workspace_root)? != canonicalize(current_dir()?)? { 58 | return Err(Error::MustBeRunFromWorkspaceRoot); 59 | } 60 | 61 | let path = metadata.workspace_root.join(&self.path); 62 | 63 | if Path::new(&path).exists() { 64 | return Err(Error::PathAlreadyExists); 65 | } 66 | 67 | create_dir_all(&path)?; 68 | 69 | if !canonicalize(&path)?.starts_with(canonicalize(&metadata.workspace_root)?) { 70 | return Err(Error::InvalidMemberPath); 71 | } 72 | 73 | remove_dir_all(&path)?; 74 | 75 | let workspace_root = metadata.workspace_root.join("Cargo.toml"); 76 | let backup = read_to_string(&workspace_root)?; 77 | 78 | self.try_run(metadata).or_else(|e| { 79 | // cleanup itself may fail and we want to notify the user in that case 80 | // otherwise just propagate the error that caused the cleanup 81 | cleanup(&workspace_root, backup, &self.path).and(Err(e)) 82 | })?; 83 | 84 | info!("success", "ok"); 85 | 86 | Ok(()) 87 | } 88 | 89 | fn try_run(&self, metadata: Metadata) -> Result { 90 | self.add_workspace_toml_entry(&metadata)?; 91 | self.create_new_workspace_member(&metadata)?; 92 | 93 | Ok(()) 94 | } 95 | 96 | // adds info about new member to workspace's Cargo.toml file 97 | // 98 | // # Fails if 99 | // 100 | // - toml files are generally corrupted 101 | // - exclude list contains new member's name 102 | // - members list contains new member's name 103 | fn add_workspace_toml_entry(&self, metadata: &Metadata) -> Result { 104 | let workspace_root = metadata.workspace_root.join("Cargo.toml"); 105 | let mut workspace_manifest = read_to_string(&workspace_root)?.parse::()?; 106 | 107 | add_workspace_member(metadata, &mut workspace_manifest, &self.path)?; 108 | 109 | write(workspace_root, workspace_manifest.to_string())?; 110 | 111 | Ok(()) 112 | } 113 | 114 | // creates new member crate 115 | // 116 | // # Fails if 117 | // 118 | // - conflicting options were chosen 119 | // - `cargo new` fails 120 | // - another package with the same name was already created somewhere 121 | fn create_new_workspace_member(&self, metadata: &Metadata) -> Result { 122 | let theme = ColorfulTheme::default(); 123 | let path = metadata.workspace_root.join(&self.path); 124 | 125 | let name = match self.name.as_ref() { 126 | Some(n) => n.to_owned(), 127 | None => Input::with_theme(&theme) 128 | .default(path.file_name().map(|s| s.to_owned()).unwrap_or_default()) 129 | .with_prompt("Name of the crate") 130 | .interact_text_on(&TERM_ERR)?, 131 | }; 132 | 133 | let template = if self.lib { 134 | 0 135 | } else if self.bin { 136 | 1 137 | } else { 138 | Select::with_theme(&theme) 139 | .items(&["library", "binary"]) 140 | .default(1) 141 | .with_prompt("Type of the crate") 142 | .interact_on(&TERM_ERR)? 143 | }; 144 | 145 | let editions = Edition::value_variants() 146 | .iter() 147 | .map(|x| x.to_possible_value().unwrap().get_name()) 148 | .collect::>(); 149 | 150 | let edition = match &self.edition { 151 | Some(edition) => match *edition { 152 | Edition::Fifteen => 0, 153 | Edition::Eighteen => 1, 154 | Edition::TwentyOne => 2, 155 | Edition::TwentyFour => 3, 156 | }, 157 | None => Select::with_theme(&theme) 158 | .items(&editions) 159 | .default(2) 160 | .with_prompt("Rust edition") 161 | .interact_on(&TERM_ERR)?, 162 | }; 163 | 164 | let mut args = vec!["new", "--name", &name, "--edition", editions[edition]]; 165 | 166 | if template == 0 { 167 | args.push("--lib"); 168 | } else { 169 | args.push("--bin"); 170 | } 171 | 172 | args.push(path.as_str()); 173 | 174 | let (stdout, stderr) = cargo(&metadata.workspace_root, &args, &[])?; 175 | 176 | if [&stdout, &stderr] 177 | .iter() 178 | .any(|out| out.contains("two packages")) 179 | { 180 | return Err(Error::DuplicatePackageName); 181 | } 182 | 183 | if !stderr.contains("Created") && !stderr.contains("Creating") { 184 | return Err(Error::Create); 185 | } 186 | 187 | let manifest = path.join("Cargo.toml"); 188 | let mut versions = Map::new(); 189 | 190 | versions.insert( 191 | name.to_owned(), 192 | Version::parse("0.0.0").expect(INTERNAL_ERR), 193 | ); 194 | 195 | write( 196 | &manifest, 197 | change_versions(read_to_string(&manifest)?, &name, &versions, false)?, 198 | )?; 199 | 200 | Ok(()) 201 | } 202 | } 203 | 204 | fn cleanup(workspace_root: &Utf8PathBuf, backup: String, path: &str) -> Result { 205 | // reset manifest doc 206 | remove_file(workspace_root)?; 207 | write(workspace_root, backup)?; 208 | 209 | // remove created crate, might not be there so ignore errors 210 | _ = remove_dir_all(path); 211 | 212 | // cleanup successful 213 | Ok(()) 214 | } 215 | 216 | fn add_workspace_member( 217 | metadata: &Metadata, 218 | manifest: &mut Document, 219 | new_member_path: &str, 220 | ) -> Result { 221 | let path = metadata.workspace_root.join(new_member_path).to_string(); 222 | 223 | let workspace_table = manifest 224 | .entry("workspace") 225 | .or_insert(Item::Table(Table::new())) 226 | .as_table_mut() 227 | .ok_or_else(|| { 228 | Error::WorkspaceBadFormat("workspace manifest item must be a table".into()) 229 | })?; 230 | 231 | if let Some(exclude_item) = workspace_table.get("exclude") { 232 | if let Some(pattern) = 233 | exists_in_glob_list(metadata, exclude_item, &path, "workspace.exclude")? 234 | { 235 | return Err(Error::InWorkspaceExclude(pattern.into())); 236 | } 237 | } 238 | 239 | let members_item = workspace_table 240 | .entry("members") 241 | .or_insert(Item::Value(Value::Array(Array::new()))); 242 | 243 | // If the member is already in the members list, we don't need to do anything 244 | if exists_in_glob_list(metadata, members_item, &path, "workspace.members")?.is_some() { 245 | return Ok(()); 246 | } 247 | 248 | let members_array = members_item.as_array_mut().expect(INTERNAL_ERR); 249 | 250 | let (prefix, suffix) = members_array 251 | .iter() 252 | .last() 253 | .map(|item| item.decor()) 254 | .and_then(|decor| Some((decor.prefix()?.as_str()?, decor.suffix()?.as_str()?))) 255 | .unwrap_or(("\n ", ",\n")); 256 | 257 | let new_elem = 258 | Value::String(Formatted::new(new_member_path.to_owned())).decorated(prefix, suffix); 259 | 260 | members_array.push_formatted(new_elem); 261 | 262 | Ok(()) 263 | } 264 | 265 | fn exists_in_glob_list<'a>( 266 | metadata: &'a Metadata, 267 | array_item: &'a Item, 268 | path: &'a str, 269 | error_name: &'a str, 270 | ) -> Result> { 271 | let paths = array_item 272 | .as_array() 273 | .ok_or_else(|| { 274 | Error::WorkspaceBadFormat(format!("{error_name} manifest item must be an array")) 275 | })? 276 | .iter() 277 | .map(|elem| { 278 | elem.as_str().ok_or_else(|| { 279 | Error::WorkspaceBadFormat(format!("{error_name} manifest items must be strings")) 280 | }) 281 | }) 282 | .collect::>>()?; 283 | 284 | for pattern in paths { 285 | if Pattern::new(&format!("{}/{pattern}", metadata.workspace_root))?.matches(path) { 286 | return Ok(Some(pattern)); 287 | } 288 | } 289 | 290 | Ok(None) 291 | } 292 | -------------------------------------------------------------------------------- /cargo-workspaces/src/exec.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{dag, filter_private, info, Error, Result, INTERNAL_ERR}; 2 | 3 | use cargo_metadata::Metadata; 4 | use clap::Parser; 5 | use globset::{Error as GlobsetError, Glob}; 6 | 7 | use std::{process::Command, result::Result as StdResult}; 8 | 9 | /// Execute an arbitrary command in each crate 10 | #[derive(Debug, Parser)] 11 | #[clap(trailing_var_arg(true))] 12 | pub struct Exec { 13 | /// Continue executing command despite non-zero exit in a given crate 14 | #[clap(long)] 15 | no_bail: bool, 16 | 17 | /// Ignore the crates matched by glob 18 | #[clap(long, value_name = "PATTERN")] 19 | ignore: Option, 20 | 21 | /// Ignore private crates 22 | #[clap(long)] 23 | ignore_private: bool, 24 | 25 | #[clap(required = true)] 26 | args: Vec, 27 | } 28 | 29 | impl Exec { 30 | pub fn run(&self, metadata: Metadata) -> Result { 31 | let pkgs = metadata 32 | .packages 33 | .iter() 34 | .map(|x| (x.clone(), x.version.to_string())) 35 | .collect::>(); 36 | 37 | let (names, mut visited) = dag(&pkgs); 38 | 39 | if self.ignore_private { 40 | visited = filter_private(visited, &pkgs); 41 | } 42 | 43 | let ignore = self 44 | .ignore 45 | .clone() 46 | .map(|x| Glob::new(&x)) 47 | .map_or::, _>(Ok(None), |x| Ok(x.ok()))?; 48 | 49 | for p in &visited { 50 | let (pkg, _) = names.get(p).expect(INTERNAL_ERR); 51 | 52 | if let Some(pattern) = &ignore { 53 | if pattern.compile_matcher().is_match(&pkg.name) { 54 | continue; 55 | } 56 | } 57 | 58 | let dir = pkg 59 | .manifest_path 60 | .parent() 61 | .ok_or_else(|| Error::ManifestHasNoParent(pkg.name.clone()))?; 62 | 63 | let status = Command::new(self.args.first().expect(INTERNAL_ERR)) 64 | .args(&self.args[1..]) 65 | .current_dir(dir) 66 | .status()?; 67 | 68 | if !self.no_bail && !status.success() { 69 | return Err(Error::Bail); 70 | } 71 | } 72 | 73 | info!("success", "ok"); 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cargo-workspaces/src/init.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{info, Error, Result}; 2 | 3 | use cargo_metadata::MetadataCommand; 4 | use clap::{ArgEnum, Parser}; 5 | use dunce::canonicalize; 6 | use glob::glob; 7 | use toml_edit::{Array, Document, Formatted, Item, Table, Value}; 8 | 9 | use std::{ 10 | collections::HashSet, 11 | fs::{read_to_string, write}, 12 | io::ErrorKind, 13 | path::PathBuf, 14 | }; 15 | 16 | #[derive(Debug, Clone, Copy, ArgEnum)] 17 | enum Resolver { 18 | #[clap(name = "1")] 19 | V1, 20 | #[clap(name = "2")] 21 | V2, 22 | } 23 | 24 | impl Resolver { 25 | fn name(&self) -> &str { 26 | match self { 27 | Resolver::V1 => "1", 28 | Resolver::V2 => "2", 29 | } 30 | } 31 | } 32 | 33 | /// Initializes a new cargo workspace 34 | #[derive(Debug, Parser)] 35 | pub struct Init { 36 | /// Path to the workspace root 37 | #[clap(parse(from_os_str), default_value = ".")] 38 | path: PathBuf, 39 | 40 | /// Workspace feature resolver version 41 | #[clap(long, arg_enum)] 42 | resolver: Option, 43 | } 44 | 45 | impl Init { 46 | pub fn run(&self) -> Result { 47 | if !self.path.is_dir() { 48 | return Err(Error::WorkspaceRootNotDir( 49 | self.path.to_string_lossy().to_string(), 50 | )); 51 | } 52 | 53 | let cargo_toml = self.path.join("Cargo.toml"); 54 | 55 | // NOTE: Globset is not used here because it does not support file iterator 56 | let pkgs = glob(&format!("{}/**/Cargo.toml", self.path.display()))?.filter_map(|e| e.ok()); 57 | 58 | let mut workspace_roots = HashSet::new(); 59 | 60 | for path in pkgs { 61 | let metadata = MetadataCommand::default() 62 | .manifest_path(path) 63 | .exec() 64 | .map_err(|e| Error::Init(e.to_string()))?; 65 | 66 | workspace_roots.insert(metadata.workspace_root); 67 | } 68 | 69 | let ws = canonicalize(&self.path)?; 70 | 71 | let mut document = match read_to_string(cargo_toml.as_path()) { 72 | Ok(manifest) => manifest.parse()?, 73 | Err(err) if err.kind() == ErrorKind::NotFound => Document::default(), 74 | Err(err) => return Err(err.into()), 75 | }; 76 | 77 | let is_root_package = document.get("package").is_some(); 78 | 79 | let workspace = document 80 | .entry("workspace") 81 | .or_insert_with(|| Item::Table(Table::default())) 82 | .as_table_mut() 83 | .ok_or_else(|| { 84 | Error::WorkspaceBadFormat( 85 | "no workspace table found in workspace Cargo.toml".to_string(), 86 | ) 87 | })?; 88 | 89 | // workspace members 90 | { 91 | let workspace_members = workspace 92 | .entry("members") 93 | .or_insert_with(|| Item::Value(Value::Array(Array::new()))) 94 | .as_array_mut() 95 | .ok_or_else(|| { 96 | Error::WorkspaceBadFormat( 97 | "members was not an array in workspace Cargo.toml".to_string(), 98 | ) 99 | })?; 100 | 101 | let mut members: Vec<_> = workspace_roots 102 | .iter() 103 | .filter_map(|m| m.strip_prefix(&ws).ok()) 104 | .map(|path| path.to_string()) 105 | .collect(); 106 | 107 | // Remove the root Cargo.toml if not package 108 | if !is_root_package { 109 | if let Some(index) = members.iter().position(|x| x.is_empty()) { 110 | members.remove(index); 111 | } 112 | } 113 | 114 | members.sort(); 115 | 116 | info!("crates", members.join(", ")); 117 | 118 | let max_member = members.len().saturating_sub(1); 119 | 120 | workspace_members.extend(members.into_iter().enumerate().map(|(i, val)| { 121 | let prefix = "\n "; 122 | let suffix = if i == max_member { ",\n" } else { "" }; 123 | Value::String(Formatted::new(val)).decorated(prefix, suffix) 124 | })); 125 | } 126 | 127 | // workspace resolver 128 | if let Some(resolver) = self.resolver { 129 | workspace.entry("resolver").or_insert_with(|| { 130 | Item::Value(Value::String(Formatted::new(resolver.name().to_owned()))) 131 | }); 132 | } 133 | 134 | write(cargo_toml, document.to_string())?; 135 | 136 | info!("initialized", self.path.display()); 137 | Ok(()) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /cargo-workspaces/src/list.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{dag, get_pkgs, list, ListOpt, Result, INTERNAL_ERR}; 2 | use cargo_metadata::Metadata; 3 | use clap::Parser; 4 | 5 | /// List crates in the project 6 | #[derive(Debug, Parser)] 7 | #[clap(alias = "ls")] 8 | pub struct List { 9 | #[clap(flatten)] 10 | list: ListOpt, 11 | } 12 | 13 | impl List { 14 | pub fn run(self, metadata: Metadata) -> Result { 15 | let pkgs = metadata 16 | .packages 17 | .iter() 18 | .map(|x| (x.clone(), x.version.to_string())) 19 | .collect::>(); 20 | 21 | let (names, visited) = dag(&pkgs); 22 | 23 | let pkg_ids = visited 24 | .into_iter() 25 | .map(|p| names.get(&p).expect(INTERNAL_ERR).0.id.clone()); 26 | 27 | let pkgs = get_pkgs(&metadata, self.list.all)?; 28 | 29 | let ordered_pkgs = pkg_ids 30 | .into_iter() 31 | .filter_map(|id| pkgs.iter().find(|p| p.id == id)) 32 | .cloned() 33 | .collect::>(); 34 | 35 | list(&ordered_pkgs, self.list) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cargo-workspaces/src/main.rs: -------------------------------------------------------------------------------- 1 | mod changed; 2 | mod create; 3 | mod exec; 4 | mod init; 5 | mod list; 6 | mod plan; 7 | mod publish; 8 | mod rename; 9 | mod version; 10 | 11 | mod utils; 12 | 13 | use cargo_metadata::{CargoOpt, MetadataCommand}; 14 | use clap::Parser; 15 | use oclif::finish; 16 | 17 | #[derive(Debug, Parser)] 18 | enum Subcommand { 19 | // TODO: add 20 | List(list::List), 21 | Changed(changed::Changed), 22 | Version(version::Version), 23 | Publish(publish::Publish), 24 | Exec(exec::Exec), 25 | Create(create::Create), 26 | Rename(rename::Rename), 27 | Init(init::Init), 28 | Plan(plan::Plan), 29 | } 30 | 31 | #[derive(Debug, Parser)] 32 | #[clap( 33 | version, 34 | replace("la", &["list", "-a"]), 35 | replace("ll", &["list", "-l"]) 36 | )] 37 | struct Opt { 38 | /// Path to workspace Cargo.toml 39 | #[clap(long, value_name = "path")] 40 | manifest_path: Option, 41 | 42 | /// Verbose mode 43 | #[clap(short)] 44 | verbose: bool, 45 | 46 | #[clap(subcommand)] 47 | subcommand: Subcommand, 48 | } 49 | 50 | #[derive(Debug, Parser)] 51 | #[clap(name = "cargo-workspaces", bin_name = "cargo", version)] 52 | enum Cargo { 53 | #[clap(alias = "ws")] 54 | Workspaces(Opt), 55 | } 56 | 57 | fn main() { 58 | set_handlers(); 59 | 60 | let Cargo::Workspaces(opt) = Cargo::parse(); 61 | 62 | if opt.verbose { 63 | utils::set_debug(); 64 | } 65 | 66 | let result = if let Subcommand::Init(ref init) = opt.subcommand { 67 | init.run() 68 | } else { 69 | let mut cmd = MetadataCommand::new(); 70 | 71 | cmd.features(CargoOpt::AllFeatures); 72 | cmd.no_deps(); 73 | 74 | if let Some(path) = opt.manifest_path { 75 | cmd.manifest_path(path); 76 | } 77 | 78 | let metadata = cmd.exec().unwrap(); 79 | 80 | match opt.subcommand { 81 | Subcommand::List(x) => x.run(metadata), 82 | Subcommand::Changed(x) => x.run(metadata), 83 | Subcommand::Version(x) => x.run(metadata), 84 | Subcommand::Publish(x) => x.run(metadata), 85 | Subcommand::Exec(x) => x.run(metadata), 86 | Subcommand::Create(x) => x.run(metadata), 87 | Subcommand::Rename(x) => x.run(metadata), 88 | Subcommand::Plan(x) => x.run(metadata), 89 | _ => unreachable!(), 90 | } 91 | }; 92 | 93 | finish(result) 94 | } 95 | 96 | fn set_handlers() { 97 | // https://github.com/console-rs/dialoguer/issues/77 98 | ctrlc::set_handler(move || { 99 | let term = dialoguer::console::Term::stdout(); 100 | let _ = term.show_cursor(); 101 | // Mimic normal `Ctrl-C` exit code. 102 | std::process::exit(130); 103 | }) 104 | .expect("Error setting Ctrl-C handler"); 105 | } 106 | -------------------------------------------------------------------------------- /cargo-workspaces/src/plan.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{ 2 | create_http_client, dag, filter_private, get_pkgs, is_published, list, package_registry, 3 | ListOpt, ListPublicOpt, RegistryOpt, Result, INTERNAL_ERR, 4 | }; 5 | 6 | use cargo_metadata::Metadata; 7 | use clap::Parser; 8 | 9 | /// List the crates in publishing order 10 | #[derive(Debug, Parser)] 11 | pub struct Plan { 12 | /// Skip already published crate versions 13 | #[clap(long)] 14 | skip_published: bool, 15 | 16 | #[clap(flatten)] 17 | registry: RegistryOpt, 18 | 19 | #[clap(flatten)] 20 | list: ListPublicOpt, 21 | } 22 | 23 | impl Plan { 24 | pub fn run(self, metadata: Metadata) -> Result { 25 | let pkgs = metadata 26 | .packages 27 | .iter() 28 | .map(|x| (x.clone(), x.version.to_string())) 29 | .collect::>(); 30 | 31 | let (names, visited) = dag(&pkgs); 32 | 33 | let http_client = create_http_client(&metadata.workspace_root, &self.registry.token)?; 34 | 35 | let pkg_ids = filter_private(visited, &pkgs) 36 | .into_iter() 37 | .map(|p| { 38 | let (pkg, version) = names.get(&p).expect(INTERNAL_ERR); 39 | 40 | let published = if self.skip_published { 41 | let index_url = 42 | package_registry(&metadata, self.registry.registry.as_ref(), pkg)?; 43 | is_published(&http_client, index_url, &pkg.name, version)? 44 | } else { 45 | false 46 | }; 47 | 48 | Ok((pkg.id.clone(), published)) 49 | }) 50 | .collect::>>()? 51 | .into_iter() 52 | .filter_map(|(id, published)| (!published).then_some(id)); 53 | 54 | let pkgs = get_pkgs(&metadata, false)?; 55 | 56 | let ordered_pkgs = pkg_ids 57 | .into_iter() 58 | .filter_map(|id| pkgs.iter().find(|p| p.id == id)) 59 | .cloned() 60 | .collect::>(); 61 | 62 | list( 63 | &ordered_pkgs, 64 | ListOpt { 65 | all: false, 66 | list: self.list, 67 | }, 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cargo-workspaces/src/publish.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use crate::utils::{ 4 | basic_checks, cargo, create_http_client, dag, filter_private, info, is_published, 5 | package_registry, should_remove_dev_deps, warn, DevDependencyRemover, Error, RegistryOpt, 6 | Result, VersionOpt, INTERNAL_ERR, 7 | }; 8 | 9 | use camino::Utf8PathBuf; 10 | use cargo_metadata::Metadata; 11 | use clap::Parser; 12 | 13 | /// Publish crates in the project 14 | #[derive(Debug, Parser)] 15 | #[clap(next_help_heading = "PUBLISH OPTIONS")] 16 | pub struct Publish { 17 | #[clap(flatten)] 18 | version: VersionOpt, 19 | 20 | /// Publish crates from the current commit without versioning 21 | // TODO: conflicts_with = "version" (group) 22 | #[clap(long, alias = "from-git")] 23 | publish_as_is: bool, 24 | 25 | /// Skip already published crate versions 26 | #[clap(long, hide = true)] 27 | skip_published: bool, 28 | 29 | /// Skip crate verification (not recommended) 30 | #[clap(long)] 31 | no_verify: bool, 32 | 33 | /// Allow dirty working directories to be published 34 | #[clap(long)] 35 | allow_dirty: bool, 36 | 37 | /// Don't remove dev-dependencies while publishing 38 | #[clap(long)] 39 | no_remove_dev_deps: bool, 40 | 41 | /// Runs in dry-run mode 42 | #[clap(long)] 43 | dry_run: bool, 44 | 45 | #[clap(flatten)] 46 | registry: RegistryOpt, 47 | 48 | /// Assert that `Cargo.lock` will remain unchanged 49 | #[clap(long)] 50 | locked: bool, 51 | 52 | /// Number of seconds to wait between publish attempts 53 | #[clap(long, value_name = "SECONDS")] 54 | publish_interval: Option, 55 | } 56 | 57 | impl Publish { 58 | pub fn run(mut self, metadata: Metadata) -> Result { 59 | if self.dry_run { 60 | warn!( 61 | "Dry run doesn't check that all dependencies have been published.", 62 | "" 63 | ); 64 | 65 | if !self.publish_as_is { 66 | warn!("Dry run doesn't perform versioning.", ""); 67 | self.publish_as_is = true; 68 | } 69 | } 70 | 71 | let pkgs = if !self.publish_as_is { 72 | self.version 73 | .do_versioning(&metadata)? 74 | .iter() 75 | .map(|x| { 76 | ( 77 | metadata 78 | .packages 79 | .iter() 80 | .find(|y| x.0 == &y.name) 81 | .expect(INTERNAL_ERR) 82 | .clone(), 83 | x.1.to_string(), 84 | ) 85 | }) 86 | .collect::>() 87 | } else { 88 | metadata 89 | .packages 90 | .iter() 91 | .map(|x| (x.clone(), x.version.to_string())) 92 | .collect() 93 | }; 94 | 95 | let (names, visited) = dag(&pkgs); 96 | 97 | // Filter out private packages 98 | let visited = filter_private(visited, &pkgs); 99 | 100 | let http_client = create_http_client(&metadata.workspace_root, &self.registry.token)?; 101 | 102 | for p in &visited { 103 | let (pkg, version) = names.get(p).expect(INTERNAL_ERR); 104 | let name = pkg.name.clone(); 105 | 106 | if self.dry_run { 107 | info!("checking", name); 108 | 109 | if !self.no_verify && !self.build(&metadata.workspace_root, p)? { 110 | warn!("build failed", ""); 111 | } 112 | 113 | basic_checks(pkg)?; 114 | } 115 | 116 | let mut args = vec!["publish"]; 117 | 118 | let name_ver = format!("{} v{}", name, version); 119 | let index_url = package_registry(&metadata, self.registry.registry.as_ref(), pkg)?; 120 | 121 | if is_published(&http_client, index_url, &name, version)? { 122 | info!("already published", name_ver); 123 | continue; 124 | } 125 | 126 | if self.dry_run { 127 | args.push("--dry-run"); 128 | } 129 | 130 | if self.no_verify || self.dry_run { 131 | args.push("--no-verify"); 132 | } 133 | 134 | if self.locked { 135 | args.push("--locked"); 136 | } 137 | 138 | if let Some(ref registry) = self.registry.registry { 139 | args.push("--registry"); 140 | args.push(registry); 141 | } 142 | 143 | if let Some(ref token) = self.registry.token { 144 | args.push("--token"); 145 | args.push(token); 146 | } 147 | 148 | if let Some(interval) = self.publish_interval { 149 | if interval > 0 && !self.dry_run { 150 | info!( 151 | "waiting", 152 | format!("{} seconds before publishing {}", interval, name_ver) 153 | ); 154 | thread::sleep(Duration::from_secs(interval)); 155 | } 156 | } 157 | 158 | let dev_deps_remover = 159 | if self.no_remove_dev_deps || !should_remove_dev_deps(&pkg.dependencies, &pkgs) { 160 | None 161 | } else { 162 | warn!( 163 | "removing dev-deps since some refer to workspace members with versions", 164 | name_ver 165 | ); 166 | Some(DevDependencyRemover::remove_dev_deps(p.as_std_path())?) 167 | }; 168 | 169 | if dev_deps_remover.is_some() || self.allow_dirty { 170 | args.push("--allow-dirty"); 171 | } 172 | 173 | args.push("--manifest-path"); 174 | args.push(p.as_str()); 175 | 176 | let (_, stderr) = cargo(&metadata.workspace_root, &args, &[])?; 177 | 178 | drop(dev_deps_remover); 179 | 180 | if !stderr.contains("Uploading") || stderr.contains("error:") { 181 | if self.dry_run { 182 | warn!("publish failed", name_ver); 183 | } else { 184 | return Err(Error::Publish(name)); 185 | } 186 | } 187 | 188 | if !self.dry_run { 189 | info!("published", name_ver); 190 | } 191 | } 192 | 193 | info!("success", "ok"); 194 | Ok(()) 195 | } 196 | 197 | fn build(&self, workspace_root: &Utf8PathBuf, manifest_path: &Utf8PathBuf) -> Result { 198 | let mut args = vec!["build"]; 199 | 200 | args.push("--manifest-path"); 201 | args.push(manifest_path.as_str()); 202 | 203 | let (_stdout, stderr) = cargo(workspace_root, &args, &[])?; 204 | 205 | if stderr.contains("could not compile") { 206 | return Ok(false); 207 | } 208 | 209 | Ok(true) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /cargo-workspaces/src/rename.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{get_pkgs, rename_packages, validate_value_containing_name, Error}; 2 | 3 | use cargo_metadata::Metadata; 4 | use clap::Parser; 5 | use globset::{Error as GlobsetError, Glob}; 6 | 7 | use std::{collections::BTreeMap as Map, fs}; 8 | 9 | /// Rename crates in the project 10 | #[derive(Debug, Parser)] 11 | pub struct Rename { 12 | /// Rename private crates too 13 | #[clap(short, long)] 14 | pub all: bool, 15 | 16 | /// Ignore the crates matched by glob 17 | #[clap(long, value_name = "pattern")] 18 | pub ignore: Option, 19 | 20 | /// Rename only a specific crate 21 | #[clap(short, long, value_name = "crate", conflicts_with_all = &["all", "ignore"])] 22 | pub from: Option, 23 | 24 | /// The value that should be used as new name (should contain `%n`) 25 | #[clap(forbid_empty_values(true))] 26 | pub to: String, 27 | } 28 | 29 | impl Rename { 30 | pub fn run(self, metadata: Metadata) -> Result<(), Error> { 31 | let pkgs = get_pkgs(&metadata, self.all || self.from.is_some())?; 32 | 33 | let ignore = self 34 | .ignore 35 | .clone() 36 | .map(|x| Glob::new(&x)) 37 | .map_or::, _>(Ok(None), |x| Ok(x.ok()))?; 38 | 39 | let mut rename_map = Map::new(); 40 | 41 | if let Some(from) = self.from { 42 | if pkgs 43 | .iter() 44 | .map(|p| &p.name) 45 | .collect::>() 46 | .contains(&&from) 47 | { 48 | rename_map.insert(from, self.to.clone()); 49 | } else { 50 | return Err(Error::PackageNotFound { id: from }); 51 | } 52 | } else { 53 | // Validate the `to` value 54 | validate_value_containing_name(&self.to) 55 | .map_err(|_| Error::MustContainPercentN("".into()))?; 56 | 57 | for pkg in pkgs { 58 | if let Some(pattern) = &ignore { 59 | if pattern.compile_matcher().is_match(&pkg.name) { 60 | continue; 61 | } 62 | } 63 | 64 | let new_name = self.to.replace("%n", &pkg.name); 65 | 66 | rename_map.insert(pkg.name, new_name); 67 | } 68 | } 69 | 70 | for pkg in &metadata.packages { 71 | if rename_map.contains_key(&pkg.name) 72 | || pkg 73 | .dependencies 74 | .iter() 75 | .map(|p| &p.name) 76 | .any(|p| rename_map.contains_key(p)) 77 | { 78 | fs::write( 79 | &pkg.manifest_path, 80 | format!( 81 | "{}\n", 82 | rename_packages( 83 | fs::read_to_string(&pkg.manifest_path)?, 84 | &pkg.name, 85 | &rename_map, 86 | )? 87 | ), 88 | )?; 89 | } 90 | } 91 | 92 | let workspace_root = metadata.workspace_root.join("Cargo.toml"); 93 | fs::write( 94 | &workspace_root, 95 | format!( 96 | "{}\n", 97 | rename_packages(fs::read_to_string(&workspace_root)?, "", &rename_map)? 98 | ), 99 | )?; 100 | 101 | Ok(()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/basic_checks.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::Package; 2 | use url::Url; 3 | 4 | use crate::utils::{warn, Result}; 5 | 6 | /// Performs basic checks to make sure that crate can be published. 7 | /// Returns `Ok(())` if no problems were found, otherwise returns a list of 8 | /// strings, each describing a problem. 9 | /// 10 | /// This method is a simple heuristic, and if it returns `Ok(())`, it does not 11 | /// guarantee that the crate can be published successfully. 12 | /// 13 | /// Current list of checks is based on the [cargo reference recommendations][1]. 14 | /// 15 | /// [1]: https://doc.rust-lang.org/cargo/reference/publishing.html#before-publishing-a-new-crate 16 | pub fn basic_checks(pkg: &Package) -> Result { 17 | let mut problems = Vec::new(); 18 | 19 | // Mandatory fields. 20 | if pkg.description.is_none() { 21 | problems.push("'description' field should be set".to_string()); 22 | } 23 | if pkg.license.is_none() && pkg.license_file.is_none() { 24 | problems.push("either 'license' or 'license-file' field should be set".to_string()); 25 | } 26 | 27 | // Too long description. 28 | const MAX_DESCRIPTION_LEN: usize = 1000; 29 | if pkg 30 | .description 31 | .as_ref() 32 | .map(|d| d.len() > MAX_DESCRIPTION_LEN) 33 | .unwrap_or(false) 34 | { 35 | problems.push(format!( 36 | "Description is too long (max {} characters)", 37 | MAX_DESCRIPTION_LEN 38 | )); 39 | } 40 | 41 | // URLs must be valid. 42 | validate_url(&pkg.homepage.as_deref(), "homepage", &mut problems); 43 | validate_url( 44 | &pkg.documentation.as_deref(), 45 | "documentation", 46 | &mut problems, 47 | ); 48 | validate_url(&pkg.repository.as_deref(), "repository", &mut problems); 49 | 50 | // Keywords limit and size 51 | const MAX_KEYWORDS: usize = 5; 52 | if pkg.keywords.len() > MAX_KEYWORDS { 53 | problems.push(format!("Too many keywords (max {} keywords)", MAX_KEYWORDS)); 54 | } 55 | 56 | const MAX_KEYWORD_LEN: usize = 20; 57 | for kw in pkg.keywords.iter() { 58 | if kw.len() > MAX_KEYWORD_LEN { 59 | problems.push(format!( 60 | "Keyword is too long (max {} characters): {}", 61 | MAX_KEYWORD_LEN, kw 62 | )); 63 | } else if !valid_keyword(kw) { 64 | problems.push(format!("Keyword contains invalid characters: {}", kw)); 65 | } 66 | } 67 | 68 | for problem in problems { 69 | warn!("check failed", problem); 70 | } 71 | 72 | Ok(()) 73 | } 74 | 75 | // Adapted from: 76 | // https://github.com/rust-lang/crates.io/blob/d507a12560ab923c2a1a061e5365fe6b1f1293a8/src/models/keyword.rs#L56 77 | fn valid_keyword(keyword: &str) -> bool { 78 | let mut chars = keyword.chars(); 79 | let first = match chars.next() { 80 | None => return false, 81 | Some(c) => c, 82 | }; 83 | first.is_ascii_alphanumeric() 84 | && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '+') 85 | } 86 | 87 | // Adapted from: 88 | // https://github.com/rust-lang/crates.io/blob/d507a12560ab923c2a1a061e5365fe6b1f1293a8/src/controllers/krate/publish.rs#L233 89 | fn validate_url(url: &Option<&str>, field: &str, problems: &mut Vec) { 90 | let Some(url) = url else { 91 | return; 92 | }; 93 | 94 | // Manually check the string, as `Url::parse` may normalize relative URLs 95 | // making it difficult to ensure that both slashes are present. 96 | if !url.starts_with("http://") && !url.starts_with("https://") { 97 | problems.push(format!( 98 | "URL for field `{field}` must begin with http:// or https:// (url: {url})" 99 | )); 100 | } 101 | 102 | // Ensure the entire URL parses as well 103 | if Url::parse(url).is_err() { 104 | problems.push(format!("`{field}` is not a valid url: `{url}`")); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/cargo.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{debug, get_debug, Error, Result, INTERNAL_ERR}; 2 | 3 | use camino::Utf8Path; 4 | use lazy_static::lazy_static; 5 | use oclif::term::TERM_ERR; 6 | use regex::{Captures, Regex}; 7 | use semver::{Version, VersionReq}; 8 | 9 | use std::{ 10 | collections::BTreeMap as Map, 11 | io::{BufRead, BufReader}, 12 | process::{Command, Stdio}, 13 | }; 14 | 15 | const CRLF: &str = "\r\n"; 16 | const LF: &str = "\n"; 17 | 18 | lazy_static! { 19 | static ref NAME: Regex = 20 | Regex::new(r#"^(\s*['"]?name['"]?\s*=\s*['"])([0-9A-Za-z-_]+)(['"].*)$"#).expect(INTERNAL_ERR); 21 | static ref VERSION: Regex = 22 | Regex::new(r#"^(\s*['"]?version['"]?\s*=\s*['"])([^'"]+)(['"].*)$"#) 23 | .expect(INTERNAL_ERR); 24 | static ref PACKAGE: Regex = 25 | Regex::new(r#"^(\s*['"]?package['"]?\s*=\s*['"])([0-9A-Za-z-_]+)(['"].*)$"#).expect(INTERNAL_ERR); 26 | static ref DEP_TABLE: Regex = 27 | Regex::new(r#"^\[(target\.'?([^']+)'?\.|workspace\.)?dependencies]"#).expect(INTERNAL_ERR); 28 | static ref DEP_ENTRY: Regex = 29 | Regex::new(r#"^\[(target\.'?([^']+)'?\.|workspace\.)?dependencies\.([0-9A-Za-z-_]+)]"#).expect(INTERNAL_ERR); 30 | static ref BUILD_DEP_TABLE: Regex = 31 | Regex::new(r#"^\[(target\.'?([^']+)'?\.|workspace\.)?build-dependencies]"#).expect(INTERNAL_ERR); 32 | static ref BUILD_DEP_ENTRY: Regex = 33 | Regex::new(r#"^\[(target\.'?([^']+)'?\.|workspace\.)?build-dependencies\.([0-9A-Za-z-_]+)]"#).expect(INTERNAL_ERR); 34 | static ref DEV_DEP_TABLE: Regex = 35 | Regex::new(r#"^\[(target\.'?([^']+)'?\.|workspace\.)?dev-dependencies]"#).expect(INTERNAL_ERR); 36 | static ref DEV_DEP_ENTRY: Regex = 37 | Regex::new(r#"^\[(target\.'?([^']+)'?\.|workspace\.)?dev-dependencies\.([0-9A-Za-z-_]+)]"#).expect(INTERNAL_ERR); 38 | static ref DEP_DIRECT_VERSION: Regex = 39 | Regex::new(r#"^(\s*['"]?([0-9A-Za-z-_]+)['"]?\s*=\s*['"])([^'"]+)(['"].*)$"#) 40 | .expect(INTERNAL_ERR); 41 | static ref DEP_OBJ_VERSION: Regex = 42 | Regex::new(r#"^(\s*['"]?([0-9A-Za-z-_]+)['"]?\s*=\s*\{.*['"]?version['"]?\s*=\s*['"])([^'"]+)(['"].*}.*)$"#) 43 | .expect(INTERNAL_ERR); 44 | static ref DEP_OBJ_RENAME_VERSION: Regex = 45 | Regex::new(r#"^(\s*['"]?([0-9A-Za-z-_]+)['"]?\s*=\s*\{.*['"]?version['"]?\s*=\s*['"])([^'"]+)(['"].*['"]?package['"]?\s*=\s*['"]([0-9A-Za-z-_]+)['"].*}.*)$"#) 46 | .expect(INTERNAL_ERR); 47 | static ref DEP_OBJ_RENAME_BEFORE_VERSION: Regex = 48 | Regex::new(r#"^(\s*['"]?[0-9A-Za-z-_]+['"]?\s*=\s*\{.*['"]?package['"]?\s*=\s*['"]([0-9A-Za-z-_]+)['"].*['"]?version['"]?\s*=\s*['"])([^'"]+)(['"].*}.*)$"#) 49 | .expect(INTERNAL_ERR); 50 | static ref DEP_DIRECT_NAME: Regex = 51 | Regex::new(r#"^(\s*['"]?([0-9A-Za-z-_]+)['"]?\s*=\s*)(['"][^'"]+['"])(.*)$"#) 52 | .expect(INTERNAL_ERR); 53 | static ref DEP_OBJ_NAME: Regex = 54 | Regex::new(r#"^(\s*['"]?([0-9A-Za-z-_]+)['"]?\s*=\s*\{(.*[^\s])?)(\s*}.*)$"#) 55 | .expect(INTERNAL_ERR); 56 | static ref DEP_OBJ_RENAME_NAME: Regex = 57 | Regex::new(r#"^(\s*['"]?[0-9A-Za-z-_]+['"]?\s*=\s*\{.*['"]?package['"]?\s*=\s*['"])([0-9A-Za-z-_]+)(['"].*}.*)$"#) 58 | .expect(INTERNAL_ERR); 59 | static ref WORKSPACE_KEY: Regex = 60 | Regex::new(r#"['"]?workspace['"]?\s*=\s*true"#).expect(INTERNAL_ERR); 61 | } 62 | 63 | pub fn cargo<'a>( 64 | root: &Utf8Path, 65 | args: &[&'a str], 66 | env: &[(&'a str, &'a str)], 67 | ) -> Result<(String, String)> { 68 | debug!("cargo", args.join(" ")); 69 | 70 | let mut args = args.to_vec(); 71 | 72 | if TERM_ERR.features().colors_supported() { 73 | args.push("--color"); 74 | args.push("always"); 75 | } 76 | 77 | if get_debug() { 78 | args.push("-v"); 79 | } 80 | 81 | let args_text = args.iter().map(|x| x.to_string()).collect::>(); 82 | 83 | let mut stderr_lines = vec![]; 84 | 85 | let mut child = Command::new("cargo") 86 | .current_dir(root) 87 | .args(&args) 88 | .envs(env.iter().copied()) 89 | .stdout(Stdio::piped()) 90 | .stderr(Stdio::piped()) 91 | .spawn() 92 | .map_err(|err| Error::Cargo { 93 | err, 94 | args: args_text.clone(), 95 | })?; 96 | 97 | { 98 | let stderr = child.stderr.as_mut().expect(INTERNAL_ERR); 99 | 100 | for line in BufReader::new(stderr).lines() { 101 | let line = line?; 102 | 103 | eprintln!("{}", line); 104 | stderr_lines.push(line); 105 | } 106 | } 107 | 108 | let output = child.wait_with_output().map_err(|err| Error::Cargo { 109 | err, 110 | args: args_text, 111 | })?; 112 | 113 | let output_stdout = String::from_utf8(output.stdout)?; 114 | let output_stderr = stderr_lines.join("\n"); 115 | 116 | debug!("cargo stderr", output_stderr); 117 | debug!("cargo stdout", output_stdout); 118 | 119 | Ok(( 120 | output_stdout.trim().to_owned(), 121 | output_stderr.trim().to_owned(), 122 | )) 123 | } 124 | 125 | pub fn cargo_config_get(root: &Utf8Path, name: &str) -> Result { 126 | // You know how we sometimes have to make the best of an unfortunate 127 | // situation? This is one of those situations. 128 | // 129 | // In order to support private registries, we need to know the URL of their 130 | // index. That's stored in a `.cargo/config.toml` file, which could be in 131 | // the `root` directory, or someplace else, like `~/.cargo/config.toml`. 132 | // 133 | // In order to match cargo's lookup strategy, the best option is to use 134 | // `cargo config get`. However, that's unstable. Since we don't want 135 | // cargo-workspaces to require nightly, we can use two combined escape 136 | // hatches: 137 | // 138 | // 1. Set the `RUSTC_BOOTSTRAP` environment variable to `1` 139 | // 2. Pass `-Z unstable-options` to cargo 140 | // 141 | // This works because stable rustc versions contain exactly the same code as 142 | // nightly versions, but all the nightly features are gated. This allows 143 | // stable rustc versions to recognize nightly features and tell you: "no, you 144 | // need a nightly for this". But rustc should be able to compile rustc, and 145 | // the rustc codebase uses nightly features, so `RUSTC_BOOTSTRAP` removes that 146 | // gating. 147 | // 148 | // This is generally frowned upon (it's only supposed to be used to 149 | // bootstrap rustc), but here it's _just_ to get access to `cargo config`, 150 | // we're not actually building crates with 151 | // rustc-stable-masquerading-as-nightly. 152 | 153 | debug!("cargo config get", name); 154 | 155 | let args = vec!["-Z", "unstable-options", "config", "get", name]; 156 | let env = &[("RUSTC_BOOTSTRAP", "1")]; 157 | 158 | let (stdout, _) = cargo(root, &args, env)?; 159 | 160 | // `cargo config get` returns TOML output, like so: 161 | // 162 | // $ RUSTC_BOOTSTRAP=1 cargo -Z unstable-options config get registries.foobar.index 163 | // registries.foobar.index = "https://dl.cloudsmith.io/basic/some-org/foobar/cargo/index.git" 164 | // 165 | // The right thing to do is probably to pull in a TOML crate, but since the 166 | // output is so predictable, and in the interest of keeping dependencies low, 167 | // we just do some text wrangling instead: 168 | 169 | // tokens is ["registries.foobar.index", "\"some-url\""] 170 | let tokens = stdout 171 | .split(" = ") 172 | .map(|x| x.to_string()) 173 | .collect::>(); 174 | 175 | // value is "\"some-url\"" 176 | let value = tokens.get(1).ok_or(Error::BadConfigGetOutput(stdout))?; 177 | 178 | // we return "some-url" 179 | Ok(value 180 | .trim() 181 | .trim_start_matches('"') 182 | .trim_end_matches('"') 183 | .into()) 184 | } 185 | 186 | #[derive(Debug)] 187 | enum Context { 188 | Beginning, 189 | Package, 190 | Dependencies, 191 | DependencyEntry(String), 192 | DontCare, 193 | } 194 | 195 | fn edit_version( 196 | caps: Captures, 197 | new_lines: &mut Vec, 198 | versions: &Map, 199 | exact: bool, 200 | version_index: usize, 201 | ) -> Result { 202 | if let Some(new_version) = versions.get(&caps[version_index]) { 203 | if exact { 204 | new_lines.push(format!("{}={}{}", &caps[1], new_version, &caps[4])); 205 | } else if !VersionReq::parse(&caps[3])?.matches(new_version) { 206 | new_lines.push(format!("{}{}{}", &caps[1], new_version, &caps[4])); 207 | } 208 | } 209 | 210 | Ok(()) 211 | } 212 | 213 | fn rename_dep( 214 | caps: Captures, 215 | new_lines: &mut Vec, 216 | renames: &Map, 217 | name_index: usize, 218 | ) -> Result { 219 | if let Some(new_name) = renames.get(&caps[name_index]) { 220 | new_lines.push(format!("{}{}{}", &caps[1], new_name, &caps[3])); 221 | } 222 | 223 | Ok(()) 224 | } 225 | 226 | fn parse( 227 | manifest: String, 228 | dev_deps: bool, 229 | package_f: P, 230 | dependencies_f: D, 231 | dependency_entries_f: DE, 232 | dependency_pkg_f: DP, 233 | ) -> Result 234 | where 235 | P: Fn(&str, &mut Vec) -> Result, 236 | D: Fn(&str, &mut Vec) -> Result, 237 | DE: Fn(&str, &str, &mut Vec) -> Result>, 238 | DP: Fn(&str, &mut Vec) -> Result, 239 | { 240 | let mut context = Context::Beginning; 241 | let mut new_lines = vec![]; 242 | 243 | for line in manifest.lines() { 244 | let trimmed = line.trim(); 245 | let count = new_lines.len(); 246 | 247 | #[allow(clippy::if_same_then_else)] 248 | if trimmed.starts_with("[package]") || trimmed.starts_with("[workspace.package]") { 249 | context = Context::Package; 250 | } else if DEP_TABLE.captures(trimmed).is_some() { 251 | context = Context::Dependencies; 252 | } else if BUILD_DEP_TABLE.captures(trimmed).is_some() { 253 | context = Context::Dependencies; 254 | } else if DEV_DEP_TABLE.captures(trimmed).is_some() && dev_deps { 255 | context = Context::Dependencies; 256 | } else if let Some(caps) = DEP_ENTRY.captures(trimmed) { 257 | context = Context::DependencyEntry(caps[3].to_string()); 258 | } else if let Some(caps) = BUILD_DEP_ENTRY.captures(trimmed) { 259 | context = Context::DependencyEntry(caps[3].to_string()); 260 | } else if let Some(caps) = DEV_DEP_ENTRY.captures(trimmed) { 261 | // TODO: let-chain 262 | if dev_deps { 263 | context = Context::DependencyEntry(caps[3].to_string()); 264 | } 265 | } else if trimmed.starts_with('[') { 266 | if let Context::DependencyEntry(ref dep) = context { 267 | dependency_pkg_f(dep, &mut new_lines)?; 268 | } 269 | 270 | context = Context::DontCare; 271 | } else { 272 | // TODO: Support `package.version` like stuff (with quotes) at beginning 273 | match context { 274 | Context::Package => package_f(line, &mut new_lines)?, 275 | Context::Dependencies => dependencies_f(line, &mut new_lines)?, 276 | Context::DependencyEntry(ref dep) => { 277 | if let Some(new_context) = dependency_entries_f(dep, line, &mut new_lines)? { 278 | context = new_context; 279 | } 280 | } 281 | _ => {} 282 | } 283 | } 284 | 285 | if new_lines.len() == count { 286 | new_lines.push(line.to_string()); 287 | } 288 | } 289 | 290 | if let Context::DependencyEntry(ref dep) = context { 291 | dependency_pkg_f(dep, &mut new_lines)?; 292 | } 293 | 294 | Ok(new_lines.join(if manifest.contains(CRLF) { CRLF } else { LF })) 295 | } 296 | 297 | pub fn rename_packages( 298 | manifest: String, 299 | pkg_name: &str, 300 | renames: &Map, 301 | ) -> Result { 302 | parse( 303 | manifest, 304 | true, 305 | |line, new_lines| { 306 | if let Some(to) = renames.get(pkg_name) { 307 | if let Some(caps) = NAME.captures(line) { 308 | new_lines.push(format!("{}{}{}", &caps[1], to, &caps[3])); 309 | } 310 | } 311 | 312 | Ok(()) 313 | }, 314 | |line, new_lines| { 315 | if let Some(caps) = DEP_DIRECT_NAME.captures(line) { 316 | if let Some(new_name) = renames.get(&caps[2]) { 317 | new_lines.push(format!( 318 | "{}{{ version = {}, package = \"{}\" }}{}", 319 | &caps[1], &caps[3], new_name, &caps[4] 320 | )); 321 | } 322 | } else if let Some(caps) = DEP_OBJ_RENAME_NAME.captures(line) { 323 | rename_dep(caps, new_lines, renames, 2)?; 324 | } else if let Some(caps) = DEP_OBJ_NAME.captures(line) { 325 | if let Some(new_name) = renames.get(&caps[2]) { 326 | if WORKSPACE_KEY.captures(&caps[3]).is_none() { 327 | new_lines.push(format!( 328 | "{}, package = \"{}\"{}", 329 | &caps[1], new_name, &caps[4] 330 | )); 331 | } 332 | } 333 | } 334 | 335 | Ok(()) 336 | }, 337 | |_, line, new_lines| { 338 | if let Some(caps) = PACKAGE.captures(line) { 339 | rename_dep(caps, new_lines, renames, 2)?; 340 | Ok(Some(Context::DontCare)) 341 | } else { 342 | Ok(None) 343 | } 344 | }, 345 | |dep, new_lines| { 346 | if let Some(new_name) = renames.get(dep) { 347 | new_lines.push(format!("package = \"{}\"", new_name)); 348 | } 349 | 350 | Ok(()) 351 | }, 352 | ) 353 | } 354 | 355 | pub fn change_versions( 356 | manifest: String, 357 | pkg_name: &str, 358 | versions: &Map, 359 | exact: bool, 360 | ) -> Result { 361 | parse( 362 | manifest, 363 | false, 364 | |line, new_lines| { 365 | if let Some(new_version) = versions.get(pkg_name) { 366 | if let Some(caps) = VERSION.captures(line) { 367 | new_lines.push(format!("{}{}{}", &caps[1], new_version, &caps[3])); 368 | } 369 | } 370 | 371 | Ok(()) 372 | }, 373 | |line, new_lines| { 374 | if let Some(caps) = DEP_DIRECT_VERSION.captures(line) { 375 | edit_version(caps, new_lines, versions, exact, 2)?; 376 | } else if let Some(caps) = DEP_OBJ_RENAME_VERSION.captures(line) { 377 | edit_version(caps, new_lines, versions, exact, 5)?; 378 | } else if let Some(caps) = DEP_OBJ_RENAME_BEFORE_VERSION.captures(line) { 379 | edit_version(caps, new_lines, versions, exact, 2)?; 380 | } else if let Some(caps) = DEP_OBJ_VERSION.captures(line) { 381 | edit_version(caps, new_lines, versions, exact, 2)?; 382 | } 383 | 384 | Ok(()) 385 | }, 386 | |dep, line, new_lines| { 387 | if let Some(caps) = PACKAGE.captures(line) { 388 | return Ok(Some(Context::DependencyEntry(caps[2].to_string()))); 389 | } else if let Some(caps) = VERSION.captures(line) { 390 | if let Some(new_version) = versions.get(dep) { 391 | if exact { 392 | new_lines.push(format!("{}={}{}", &caps[1], new_version, &caps[3])); 393 | } else if !VersionReq::parse(&caps[2])?.matches(new_version) { 394 | new_lines.push(format!("{}{}{}", &caps[1], new_version, &caps[3])); 395 | } 396 | } 397 | } 398 | 399 | Ok(None) 400 | }, 401 | |_, _| Ok(()), 402 | ) 403 | } 404 | 405 | #[cfg(test)] 406 | mod test { 407 | use super::*; 408 | use indoc::indoc; 409 | 410 | #[test] 411 | fn test_version() { 412 | let m = indoc! {r#" 413 | [package] 414 | version = "0.1.0" 415 | "#}; 416 | 417 | let mut v = Map::new(); 418 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 419 | 420 | assert_eq!( 421 | change_versions(m.into(), "this", &v, false).unwrap(), 422 | indoc! {r#" 423 | [package] 424 | version = "0.3.0""# 425 | } 426 | ); 427 | } 428 | 429 | #[test] 430 | fn test_version_comments() { 431 | let m = indoc! {r#" 432 | [package] 433 | version="0.1.0" # hello 434 | "#}; 435 | 436 | let mut v = Map::new(); 437 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 438 | 439 | assert_eq!( 440 | change_versions(m.into(), "this", &v, false).unwrap(), 441 | indoc! {r#" 442 | [package] 443 | version="0.3.0" # hello"# 444 | } 445 | ); 446 | } 447 | 448 | #[test] 449 | fn test_version_quotes() { 450 | let m = indoc! {r#" 451 | [package] 452 | "version" = "0.1.0" 453 | "#}; 454 | 455 | let mut v = Map::new(); 456 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 457 | 458 | assert_eq!( 459 | change_versions(m.into(), "this", &v, false).unwrap(), 460 | indoc! {r#" 461 | [package] 462 | "version" = "0.3.0""# 463 | } 464 | ); 465 | } 466 | 467 | #[test] 468 | fn test_version_single_quotes() { 469 | let m = indoc! {r#" 470 | [package] 471 | 'version'='0.1.0'# hello 472 | "#}; 473 | 474 | let mut v = Map::new(); 475 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 476 | 477 | assert_eq!( 478 | change_versions(m.into(), "this", &v, false).unwrap(), 479 | indoc! {r#" 480 | [package] 481 | 'version'='0.3.0'# hello"# 482 | } 483 | ); 484 | } 485 | 486 | #[test] 487 | fn test_version_workspace() { 488 | let m = indoc! {r#" 489 | [workspace.package] 490 | version = "0.1.0" 491 | "#}; 492 | 493 | let mut v = Map::new(); 494 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 495 | 496 | assert_eq!( 497 | change_versions(m.into(), "this", &v, false).unwrap(), 498 | indoc! {r#" 499 | [workspace.package] 500 | version = "0.3.0""# 501 | } 502 | ); 503 | } 504 | 505 | #[test] 506 | fn test_version_dependencies() { 507 | let m = indoc! {r#" 508 | [dependencies] 509 | this = "0.0.1" # hello 510 | "#}; 511 | 512 | let mut v = Map::new(); 513 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 514 | 515 | assert_eq!( 516 | change_versions(m.into(), "another", &v, false).unwrap(), 517 | indoc! {r#" 518 | [dependencies] 519 | this = "0.3.0" # hello"# 520 | } 521 | ); 522 | } 523 | 524 | #[test] 525 | fn test_version_dependencies_object() { 526 | let m = indoc! {r#" 527 | [dependencies] 528 | this = { path = "../", version = "0.0.1" } # hello 529 | "#}; 530 | 531 | let mut v = Map::new(); 532 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 533 | 534 | assert_eq!( 535 | change_versions(m.into(), "another", &v, false).unwrap(), 536 | indoc! {r#" 537 | [dependencies] 538 | this = { path = "../", version = "0.3.0" } # hello"# 539 | } 540 | ); 541 | } 542 | 543 | #[test] 544 | fn test_version_dependencies_object_renamed() { 545 | let m = indoc! {r#" 546 | [dependencies] 547 | this2 = { path = "../", version = "0.0.1", package = "this" } # hello 548 | "#}; 549 | 550 | let mut v = Map::new(); 551 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 552 | 553 | assert_eq!( 554 | change_versions(m.into(), "another", &v, false).unwrap(), 555 | indoc! {r#" 556 | [dependencies] 557 | this2 = { path = "../", version = "0.3.0", package = "this" } # hello"# 558 | } 559 | ); 560 | } 561 | 562 | #[test] 563 | fn test_version_dependencies_object_renamed_before_version() { 564 | let m = indoc! {r#" 565 | [dependencies] 566 | this2 = { path = "../", package = "this", version = "0.0.1" } # hello 567 | "#}; 568 | 569 | let mut v = Map::new(); 570 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 571 | 572 | assert_eq!( 573 | change_versions(m.into(), "another", &v, false).unwrap(), 574 | indoc! {r#" 575 | [dependencies] 576 | this2 = { path = "../", package = "this", version = "0.3.0" } # hello"# 577 | } 578 | ); 579 | } 580 | 581 | #[test] 582 | fn test_version_dependency_table() { 583 | let m = indoc! {r#" 584 | [dependencies.this] 585 | path = "../" 586 | version = "0.0.1" # hello 587 | "#}; 588 | 589 | let mut v = Map::new(); 590 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 591 | 592 | assert_eq!( 593 | change_versions(m.into(), "another", &v, false).unwrap(), 594 | indoc! {r#" 595 | [dependencies.this] 596 | path = "../" 597 | version = "0.3.0" # hello"# 598 | } 599 | ); 600 | } 601 | 602 | // #[test] 603 | // fn test_dependency_table_renamed() { 604 | // // TODO: Not correct when `package` key exists 605 | // let m = indoc! {r#" 606 | // [dependencies.this2] 607 | // path = "../" 608 | // version = "0.0.1" # hello" 609 | // package = "this" 610 | // "#}; 611 | 612 | // let mut v = Map::new(); 613 | // v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 614 | 615 | // assert_eq!( 616 | // change_versions(m, "another", &v, false).unwrap(), 617 | // indoc! {r#" 618 | // [dependencies.this2] 619 | // path = "../" 620 | // version = "0.3.0" # hello" 621 | // package = "this""# 622 | // } 623 | // ); 624 | // } 625 | 626 | #[test] 627 | fn test_version_dependency_table_renamed_before_version() { 628 | let m = indoc! {r#" 629 | [dependencies.this2] 630 | path = "../" 631 | package = "this" 632 | version = "0.0.1" # hello 633 | "#}; 634 | 635 | let mut v = Map::new(); 636 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 637 | 638 | assert_eq!( 639 | change_versions(m.into(), "another", &v, false).unwrap(), 640 | indoc! {r#" 641 | [dependencies.this2] 642 | path = "../" 643 | package = "this" 644 | version = "0.3.0" # hello"# 645 | } 646 | ); 647 | } 648 | 649 | #[test] 650 | fn test_version_target_dependencies() { 651 | let m = indoc! {r#" 652 | [target.x86_64-pc-windows-gnu.dependencies] 653 | this = "0.0.1" # hello 654 | "#}; 655 | 656 | let mut v = Map::new(); 657 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 658 | 659 | assert_eq!( 660 | change_versions(m.into(), "another", &v, false).unwrap(), 661 | indoc! {r#" 662 | [target.x86_64-pc-windows-gnu.dependencies] 663 | this = "0.3.0" # hello"# 664 | } 665 | ); 666 | } 667 | 668 | #[test] 669 | fn test_version_target_cfg_dependencies() { 670 | let m = indoc! {r#" 671 | [target.'cfg(not(any(target_arch = "wasm32", target_os = "emscripten")))'.dependencies] 672 | this = "0.0.1" # hello 673 | "#}; 674 | 675 | let mut v = Map::new(); 676 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 677 | 678 | assert_eq!( 679 | change_versions(m.into(), "another", &v, false).unwrap(), 680 | indoc! {r#" 681 | [target.'cfg(not(any(target_arch = "wasm32", target_os = "emscripten")))'.dependencies] 682 | this = "0.3.0" # hello"# 683 | } 684 | ); 685 | } 686 | 687 | #[test] 688 | fn test_version_workspace_dependencies() { 689 | let m = indoc! {r#" 690 | [workspace.dependencies] 691 | this = "0.0.1" # hello 692 | "#}; 693 | 694 | let mut v = Map::new(); 695 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 696 | 697 | assert_eq!( 698 | change_versions(m.into(), "another", &v, false).unwrap(), 699 | indoc! {r#" 700 | [workspace.dependencies] 701 | this = "0.3.0" # hello"# 702 | } 703 | ); 704 | } 705 | 706 | #[test] 707 | fn test_version_ignore_workspace() { 708 | let m = indoc! {r#" 709 | [dependencies] 710 | this = { workspace = true } # hello 711 | "#}; 712 | 713 | let mut v = Map::new(); 714 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 715 | 716 | assert_eq!( 717 | change_versions(m.into(), "another", &v, false).unwrap(), 718 | indoc! {r#" 719 | [dependencies] 720 | this = { workspace = true } # hello"# 721 | } 722 | ); 723 | } 724 | 725 | #[test] 726 | fn test_version_ignore_dotted_workspace() { 727 | let m = indoc! {r#" 728 | [dependencies] 729 | this.workspace = true # hello 730 | "#}; 731 | 732 | let mut v = Map::new(); 733 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 734 | 735 | assert_eq!( 736 | change_versions(m.into(), "another", &v, false).unwrap(), 737 | indoc! {r#" 738 | [dependencies] 739 | this.workspace = true # hello"# 740 | } 741 | ); 742 | } 743 | 744 | #[test] 745 | fn test_exact() { 746 | let m = indoc! {r#" 747 | [dependencies] 748 | this = { path = "../", version = "0.0.1" } # hello 749 | "#}; 750 | 751 | let mut v = Map::new(); 752 | v.insert("this".to_string(), Version::parse("0.3.0").unwrap()); 753 | 754 | assert_eq!( 755 | change_versions(m.into(), "another", &v, true).unwrap(), 756 | indoc! {r#" 757 | [dependencies] 758 | this = { path = "../", version = "=0.3.0" } # hello"# 759 | } 760 | ); 761 | } 762 | 763 | #[test] 764 | fn test_name() { 765 | let m = indoc! {r#" 766 | [package] 767 | name = "this" 768 | "#}; 769 | 770 | let mut v = Map::new(); 771 | v.insert("this".to_string(), "ra_this".to_string()); 772 | 773 | assert_eq!( 774 | rename_packages(m.into(), "this", &v).unwrap(), 775 | indoc! {r#" 776 | [package] 777 | name = "ra_this""# 778 | } 779 | ); 780 | } 781 | 782 | #[test] 783 | fn test_name_dependencies() { 784 | let m = indoc! {r#" 785 | [dependencies] 786 | this = "0.0.1" # hello 787 | "#}; 788 | 789 | let mut v = Map::new(); 790 | v.insert("this".to_string(), "ra_this".to_string()); 791 | 792 | assert_eq!( 793 | rename_packages(m.into(), "another", &v).unwrap(), 794 | indoc! {r#" 795 | [dependencies] 796 | this = { version = "0.0.1", package = "ra_this" } # hello"# 797 | } 798 | ); 799 | } 800 | 801 | #[test] 802 | fn test_name_dependencies_object() { 803 | let m = indoc! {r#" 804 | [dependencies] 805 | this = { path = "../", version = "0.0.1" } # hello 806 | "#}; 807 | 808 | let mut v = Map::new(); 809 | v.insert("this".to_string(), "ra_this".to_string()); 810 | 811 | assert_eq!( 812 | rename_packages(m.into(), "another", &v).unwrap(), 813 | indoc! {r#" 814 | [dependencies] 815 | this = { path = "../", version = "0.0.1", package = "ra_this" } # hello"# 816 | } 817 | ); 818 | } 819 | 820 | #[test] 821 | fn test_name_dependencies_object_renamed() { 822 | let m = indoc! {r#" 823 | [dependencies] 824 | this2 = { path = "../", version = "0.0.1", package = "this" } # hello 825 | "#}; 826 | 827 | let mut v = Map::new(); 828 | v.insert("this".to_string(), "ra_this".to_string()); 829 | 830 | assert_eq!( 831 | rename_packages(m.into(), "another", &v).unwrap(), 832 | indoc! {r#" 833 | [dependencies] 834 | this2 = { path = "../", version = "0.0.1", package = "ra_this" } # hello"# 835 | } 836 | ); 837 | } 838 | 839 | #[test] 840 | fn test_name_dependencies_object_renamed_before_version() { 841 | let m = indoc! {r#" 842 | [dependencies] 843 | this2 = { path = "../", package = "this", version = "0.0.1" } # hello 844 | "#}; 845 | 846 | let mut v = Map::new(); 847 | v.insert("this".to_string(), "ra_this".to_string()); 848 | 849 | assert_eq!( 850 | rename_packages(m.into(), "another", &v).unwrap(), 851 | indoc! {r#" 852 | [dependencies] 853 | this2 = { path = "../", package = "ra_this", version = "0.0.1" } # hello"# 854 | } 855 | ); 856 | } 857 | 858 | #[test] 859 | fn test_name_dependency_table() { 860 | let m = indoc! {r#" 861 | [dependencies.this] 862 | path = "../" 863 | version = "0.0.1" # hello 864 | "#}; 865 | 866 | let mut v = Map::new(); 867 | v.insert("this".to_string(), "ra_this".to_string()); 868 | 869 | assert_eq!( 870 | rename_packages(m.into(), "another", &v).unwrap(), 871 | indoc! {r#" 872 | [dependencies.this] 873 | path = "../" 874 | version = "0.0.1" # hello 875 | package = "ra_this""# 876 | } 877 | ); 878 | } 879 | 880 | #[test] 881 | fn test_name_dependency_table_renamed() { 882 | let m = indoc! {r#" 883 | [dependencies.this2] 884 | path = "../" 885 | version = "0.0.1" # hello" 886 | package = "this" 887 | "#}; 888 | 889 | let mut v = Map::new(); 890 | v.insert("this".to_string(), "ra_this".to_string()); 891 | 892 | assert_eq!( 893 | rename_packages(m.into(), "another", &v).unwrap(), 894 | indoc! {r#" 895 | [dependencies.this2] 896 | path = "../" 897 | version = "0.0.1" # hello" 898 | package = "ra_this""# 899 | } 900 | ); 901 | } 902 | 903 | #[test] 904 | fn test_name_dependency_table_renamed_before_version() { 905 | let m = indoc! {r#" 906 | [dependencies.this2] 907 | path = "../" 908 | package = "this" 909 | version = "0.0.1" # hello 910 | "#}; 911 | 912 | let mut v = Map::new(); 913 | v.insert("this".to_string(), "ra_this".to_string()); 914 | 915 | assert_eq!( 916 | rename_packages(m.into(), "another", &v).unwrap(), 917 | indoc! {r#" 918 | [dependencies.this2] 919 | path = "../" 920 | package = "ra_this" 921 | version = "0.0.1" # hello"# 922 | } 923 | ); 924 | } 925 | 926 | #[test] 927 | fn test_name_target_dependencies() { 928 | let m = indoc! {r#" 929 | [target.x86_64-pc-windows-gnu.dependencies] 930 | this = "0.0.1" # hello 931 | "#}; 932 | 933 | let mut v = Map::new(); 934 | v.insert("this".to_string(), "ra_this".to_string()); 935 | 936 | assert_eq!( 937 | rename_packages(m.into(), "another", &v).unwrap(), 938 | indoc! {r#" 939 | [target.x86_64-pc-windows-gnu.dependencies] 940 | this = { version = "0.0.1", package = "ra_this" } # hello"# 941 | } 942 | ); 943 | } 944 | 945 | #[test] 946 | fn test_name_target_cfg_dependencies() { 947 | let m = indoc! {r#" 948 | [target.'cfg(not(any(target_arch = "wasm32", target_os = "emscripten")))'.dependencies] 949 | this = "0.0.1" # hello 950 | "#}; 951 | 952 | let mut v = Map::new(); 953 | v.insert("this".to_string(), "ra_this".to_string()); 954 | 955 | assert_eq!( 956 | rename_packages(m.into(), "another", &v).unwrap(), 957 | indoc! {r#" 958 | [target.'cfg(not(any(target_arch = "wasm32", target_os = "emscripten")))'.dependencies] 959 | this = { version = "0.0.1", package = "ra_this" } # hello"# 960 | } 961 | ); 962 | } 963 | 964 | #[test] 965 | fn test_name_workspace_dependencies() { 966 | let m = indoc! {r#" 967 | [workspace.dependencies] 968 | this = "0.0.1" # hello 969 | "#}; 970 | 971 | let mut v = Map::new(); 972 | v.insert("this".to_string(), "ra_this".to_string()); 973 | 974 | assert_eq!( 975 | rename_packages(m.into(), "another", &v).unwrap(), 976 | indoc! {r#" 977 | [workspace.dependencies] 978 | this = { version = "0.0.1", package = "ra_this" } # hello"# 979 | } 980 | ); 981 | } 982 | 983 | #[test] 984 | fn test_name_ignore_workspace() { 985 | let m = indoc! {r#" 986 | [dependencies] 987 | this = { workspace = true } # hello 988 | "#}; 989 | 990 | let mut v = Map::new(); 991 | v.insert("this".to_string(), "ra_this".to_string()); 992 | 993 | assert_eq!( 994 | rename_packages(m.into(), "another", &v).unwrap(), 995 | indoc! {r#" 996 | [dependencies] 997 | this = { workspace = true } # hello"# 998 | } 999 | ); 1000 | } 1001 | 1002 | #[test] 1003 | fn test_name_ignore_workspace_with_keys() { 1004 | let m = indoc! {r#" 1005 | [dependencies] 1006 | this = { workspace = true, optional = true } # hello 1007 | "#}; 1008 | 1009 | let mut v = Map::new(); 1010 | v.insert("this".to_string(), "ra_this".to_string()); 1011 | 1012 | assert_eq!( 1013 | rename_packages(m.into(), "another", &v).unwrap(), 1014 | indoc! {r#" 1015 | [dependencies] 1016 | this = { workspace = true, optional = true } # hello"# 1017 | } 1018 | ); 1019 | } 1020 | 1021 | #[test] 1022 | fn test_name_ignore_dotted_workspace() { 1023 | let m = indoc! {r#" 1024 | [dependencies] 1025 | this.workspace = true # hello 1026 | "#}; 1027 | 1028 | let mut v = Map::new(); 1029 | v.insert("this".to_string(), "ra_this".to_string()); 1030 | 1031 | assert_eq!( 1032 | rename_packages(m.into(), "another", &v).unwrap(), 1033 | indoc! {r#" 1034 | [dependencies] 1035 | this.workspace = true # hello"# 1036 | } 1037 | ); 1038 | } 1039 | } 1040 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/changable.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{get_pkgs, git, info, Error, Pkg, INTERNAL_ERR}; 2 | use cargo_metadata::Metadata; 3 | use clap::Parser; 4 | use globset::{Error as GlobsetError, Glob}; 5 | use std::path::Path; 6 | 7 | #[derive(Debug, Parser)] 8 | pub struct ChangeOpt { 9 | // TODO: include_dirty 10 | /// Always include targeted crates matched by glob even when there are no changes 11 | #[clap(long, value_name = "PATTERN")] 12 | pub force: Option, 13 | 14 | /// Ignore changes in files matched by glob 15 | #[clap(long, value_name = "PATTERN")] 16 | pub ignore_changes: Option, 17 | 18 | /// Use this git reference instead of the last tag 19 | #[clap(long, forbid_empty_values(true))] 20 | pub since: Option, 21 | } 22 | 23 | #[derive(Debug, Default)] 24 | pub struct ChangeData { 25 | pub since: Option, 26 | pub count: String, 27 | pub dirty: bool, 28 | } 29 | 30 | impl ChangeData { 31 | pub fn new(metadata: &Metadata, _change: &ChangeOpt) -> Result { 32 | let (_, sha, _) = git( 33 | &metadata.workspace_root, 34 | &["rev-list", "--tags", "--max-count=1"], 35 | )?; 36 | 37 | if sha.is_empty() { 38 | return Ok(Self { 39 | count: "1".to_string(), 40 | since: None, 41 | ..Default::default() 42 | }); 43 | } 44 | 45 | let (_, count, _) = git( 46 | &metadata.workspace_root, 47 | &["rev-list", "--count", &format!("HEAD...{sha}")], 48 | )?; 49 | 50 | let since = git( 51 | &metadata.workspace_root, 52 | &["describe", "--exact-match", "--tags", &sha], 53 | ) 54 | .ok() 55 | .map(|x| x.1); 56 | 57 | Ok(Self { 58 | count, 59 | since, 60 | ..Default::default() 61 | }) 62 | } 63 | } 64 | 65 | impl ChangeOpt { 66 | pub fn get_changed_pkgs( 67 | &self, 68 | metadata: &Metadata, 69 | // Optional because there can be no tags 70 | since: &Option, 71 | private: bool, 72 | ) -> Result<(Vec, Vec), Error> { 73 | let pkgs = get_pkgs(metadata, private)?; 74 | 75 | let pkgs = if let Some(since) = since { 76 | info!("looking for changes since", since); 77 | 78 | let (_, changed_files, _) = git( 79 | &metadata.workspace_root, 80 | &["diff", "--name-only", "--relative", since], 81 | )?; 82 | 83 | let changed_files = changed_files 84 | .split('\n') 85 | .filter(|f| !f.is_empty()) 86 | .map(Path::new) 87 | .collect::>(); 88 | 89 | let force = self 90 | .force 91 | .clone() 92 | .map(|x| Glob::new(&x)) 93 | .map_or::, _>(Ok(None), |x| Ok(x.ok()))?; 94 | let ignore_changes = self 95 | .ignore_changes 96 | .clone() 97 | .map(|x| Glob::new(&x)) 98 | .map_or::, _>(Ok(None), |x| Ok(x.ok()))?; 99 | 100 | pkgs.into_iter().partition(|p: &Pkg| { 101 | if let Some(pattern) = &force { 102 | if pattern.compile_matcher().is_match(&p.name) { 103 | return true; 104 | } 105 | } 106 | 107 | changed_files.iter().any(|f| { 108 | if let Some(pattern) = &ignore_changes { 109 | if pattern 110 | .compile_matcher() 111 | .is_match(f.to_str().expect(INTERNAL_ERR)) 112 | { 113 | return false; 114 | } 115 | } 116 | 117 | f.starts_with(&p.path) 118 | }) 119 | }) 120 | } else { 121 | (pkgs, vec![]) 122 | }; 123 | 124 | Ok(pkgs) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/config.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{Error, Result}; 2 | 3 | use serde::Deserialize; 4 | use serde_json::{from_value, Value}; 5 | 6 | #[derive(Deserialize, Default)] 7 | struct MetadataWorkspaces { 8 | pub workspaces: Option, 9 | } 10 | 11 | // TODO: Validation of conflicting options (hard to tell conflicts if between cli and option) 12 | pub fn read_config(value: &Value) -> Result 13 | where 14 | T: for<'de> Deserialize<'de> + Default, 15 | { 16 | from_value::>>(value.clone()) 17 | .map_err(Error::BadMetadata) 18 | .map(|v| v.unwrap_or_default().workspaces.unwrap_or_default()) 19 | } 20 | 21 | #[derive(Deserialize, Default, Debug, Clone, Ord, Eq, PartialOrd, PartialEq)] 22 | pub struct PackageConfig { 23 | pub independent: Option, 24 | } 25 | 26 | #[derive(Deserialize, Default, Debug, Clone, Ord, Eq, PartialOrd, PartialEq)] 27 | pub struct WorkspaceConfig { 28 | pub allow_branch: Option, 29 | pub no_individual_tags: Option, 30 | } 31 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/dag.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use cargo_metadata::{DependencyKind, Package}; 3 | use indexmap::IndexSet as Set; 4 | 5 | use std::collections::BTreeMap as Map; 6 | 7 | pub fn dag( 8 | pkgs: &[(Package, String)], 9 | ) -> (Map<&Utf8PathBuf, (&Package, &String)>, Set) { 10 | let mut names = Map::new(); 11 | let mut visited = Set::new(); 12 | 13 | for (pkg, version) in pkgs { 14 | names.insert(&pkg.manifest_path, (pkg, version)); 15 | dag_insert(pkgs, pkg, &mut visited); 16 | } 17 | 18 | (names, visited) 19 | } 20 | 21 | fn dag_insert(pkgs: &[(Package, String)], pkg: &Package, visited: &mut Set) { 22 | if visited.contains(&pkg.manifest_path) { 23 | return; 24 | } 25 | 26 | for d in &pkg.dependencies { 27 | if let Some((dep, _)) = pkgs.iter().find(|(p, _)| d.name == p.name) { 28 | match d.kind { 29 | DependencyKind::Normal | DependencyKind::Build => { 30 | dag_insert(pkgs, dep, visited); 31 | } 32 | _ => {} 33 | } 34 | } 35 | } 36 | 37 | visited.insert(pkg.manifest_path.clone()); 38 | } 39 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/dev_dep_remover.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{read_to_string, write}, 3 | path::Path, 4 | }; 5 | 6 | use cargo_metadata::{Dependency, DependencyKind, Package}; 7 | use semver::VersionReq; 8 | use toml_edit::Document; 9 | 10 | use crate::utils::Result; 11 | 12 | /// Removes all dev-dependencies from a Cargo.toml then restores the file when dropped. 13 | pub struct DevDependencyRemover { 14 | manifest_path: std::path::PathBuf, 15 | original_toml: String, 16 | } 17 | 18 | impl DevDependencyRemover { 19 | pub fn remove_dev_deps(manifest_path: &Path) -> Result { 20 | let original_toml = read_to_string(manifest_path)?; 21 | let mut document = original_toml.parse::()?; 22 | 23 | document.as_table_mut().remove("dev-dependencies"); 24 | 25 | if let Some(table) = document.as_table_mut().get_mut("target") { 26 | if let Some(table) = table.as_table_mut() { 27 | table.iter_mut().for_each(|(_, value)| { 28 | if let Some(table) = value.as_table_mut() { 29 | table.remove("dev-dependencies"); 30 | } 31 | }); 32 | } 33 | } 34 | 35 | write(manifest_path, document.to_string())?; 36 | 37 | Ok(Self { 38 | manifest_path: manifest_path.to_owned(), 39 | original_toml, 40 | }) 41 | } 42 | } 43 | 44 | impl Drop for DevDependencyRemover { 45 | fn drop(&mut self) { 46 | let _ = write(&self.manifest_path, &self.original_toml); 47 | } 48 | } 49 | 50 | pub fn should_remove_dev_deps(deps: &[Dependency], pkgs: &[(Package, String)]) -> bool { 51 | let mut names = vec![]; 52 | let no_version = VersionReq::parse("*").unwrap(); 53 | 54 | for (pkg, _) in pkgs { 55 | names.push(&pkg.name); 56 | } 57 | 58 | for dep in deps { 59 | if dep.kind == DependencyKind::Development 60 | && names.contains(&&dep.name) 61 | && dep.source.is_none() 62 | && dep.req != no_version 63 | { 64 | return true; 65 | } 66 | } 67 | 68 | false 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use std::{ 74 | fs::{create_dir, read_to_string}, 75 | path::PathBuf, 76 | }; 77 | 78 | use cargo_metadata::MetadataCommand; 79 | 80 | use super::*; 81 | 82 | #[test] 83 | fn test_remove_dev_deps() { 84 | let tempdir = tempfile::tempdir().unwrap(); 85 | let manifest_path = tempdir.path().join("Cargo.toml"); 86 | 87 | let original_toml = r#" 88 | [package] 89 | name = "foo" # A comment 90 | version = "0.1.0" 91 | 92 | [dependencies] 93 | dep1 = "1.0.0" 94 | 95 | [dev-dependencies] 96 | dep2 = "2.0.1" 97 | 98 | [workspace.metadata.workspaces] 99 | "#; 100 | 101 | write(&manifest_path, original_toml).unwrap(); 102 | 103 | let remover = DevDependencyRemover::remove_dev_deps(&manifest_path).unwrap(); 104 | 105 | assert_eq!( 106 | read_to_string(&manifest_path).unwrap(), 107 | r#" 108 | [package] 109 | name = "foo" # A comment 110 | version = "0.1.0" 111 | 112 | [dependencies] 113 | dep1 = "1.0.0" 114 | 115 | [workspace.metadata.workspaces] 116 | "# 117 | ); 118 | 119 | drop(remover); 120 | 121 | assert_eq!(read_to_string(&manifest_path).unwrap(), original_toml); 122 | } 123 | 124 | #[test] 125 | fn test_remove_dev_deps_target() { 126 | let tempdir = tempfile::tempdir().unwrap(); 127 | let manifest_path = tempdir.path().join("Cargo.toml"); 128 | 129 | let original_toml = r#" 130 | [package] 131 | name = "foo" # A comment 132 | version = "0.1.0" 133 | 134 | [dependencies] 135 | dep1 = "1.0.0" 136 | 137 | [target.'cfg(unix)'.dev-dependencies] 138 | dep2 = "2.0.1" 139 | 140 | [workspace.metadata.workspaces] 141 | "#; 142 | 143 | write(&manifest_path, original_toml).unwrap(); 144 | 145 | let remover = DevDependencyRemover::remove_dev_deps(&manifest_path).unwrap(); 146 | 147 | assert_eq!( 148 | read_to_string(&manifest_path).unwrap(), 149 | r#" 150 | [package] 151 | name = "foo" # A comment 152 | version = "0.1.0" 153 | 154 | [dependencies] 155 | dep1 = "1.0.0" 156 | 157 | [workspace.metadata.workspaces] 158 | "# 159 | ); 160 | 161 | drop(remover); 162 | 163 | assert_eq!(read_to_string(&manifest_path).unwrap(), original_toml); 164 | } 165 | 166 | #[test] 167 | fn test_should_remove_dev_deps_normal() { 168 | let tempdir = tempfile::tempdir().unwrap(); 169 | let manifest_path = tempdir.path().join("Cargo.toml"); 170 | 171 | let original_toml = r#" 172 | [package] 173 | name = "foo" # A comment 174 | version = "0.1.0" 175 | 176 | [lib] 177 | path = "lib.rs" 178 | 179 | [dev-dependencies] 180 | syn = "2" 181 | "#; 182 | 183 | write(&manifest_path, original_toml).unwrap(); 184 | 185 | let (deps, pkgs) = args(&manifest_path, "foo"); 186 | 187 | assert!(!should_remove_dev_deps(&deps, &pkgs)) 188 | } 189 | 190 | #[test] 191 | fn test_should_remove_dev_deps_member_path() { 192 | let tempdir = tempfile::tempdir().unwrap(); 193 | let manifest_path = tempdir.path().join("Cargo.toml"); 194 | 195 | let original_toml = r#" 196 | [package] 197 | name = "foo" # A comment 198 | version = "0.1.0" 199 | 200 | [lib] 201 | path = "lib.rs" 202 | 203 | [dev-dependencies] 204 | bar = { path = "./bar" } 205 | 206 | [workspace] 207 | members = [".", "bar"] 208 | "#; 209 | 210 | write(&manifest_path, original_toml).unwrap(); 211 | 212 | let member_toml = r#" 213 | [package] 214 | name = "bar" # A comment 215 | version = "0.1.0" 216 | 217 | [lib] 218 | path = "lib.rs" 219 | "#; 220 | 221 | create_dir(tempdir.path().join("bar")).unwrap(); 222 | write(tempdir.path().join("bar").join("Cargo.toml"), member_toml).unwrap(); 223 | 224 | let (deps, pkgs) = args(&manifest_path, "foo"); 225 | 226 | assert!(!should_remove_dev_deps(&deps, &pkgs)) 227 | } 228 | 229 | #[test] 230 | fn test_should_remove_dev_deps_member_version() { 231 | let tempdir = tempfile::tempdir().unwrap(); 232 | let manifest_path = tempdir.path().join("Cargo.toml"); 233 | 234 | let original_toml = r#" 235 | [package] 236 | name = "foo" # A comment 237 | version = "0.1.0" 238 | 239 | [lib] 240 | path = "lib.rs" 241 | 242 | [dev-dependencies] 243 | bar = "0.1.0" 244 | 245 | [workspace] 246 | members = [".", "bar"] 247 | "#; 248 | 249 | write(&manifest_path, original_toml).unwrap(); 250 | 251 | let member_toml = r#" 252 | [package] 253 | name = "bar" # A comment 254 | version = "0.1.0" 255 | 256 | [lib] 257 | path = "lib.rs" 258 | "#; 259 | 260 | create_dir(tempdir.path().join("bar")).unwrap(); 261 | write(tempdir.path().join("bar").join("Cargo.toml"), member_toml).unwrap(); 262 | 263 | let (deps, pkgs) = args(&manifest_path, "foo"); 264 | 265 | // This won't remove it because it's reading from crates.io 266 | assert!(!should_remove_dev_deps(&deps, &pkgs)) 267 | } 268 | 269 | #[test] 270 | fn test_should_remove_dev_deps_member_workspace_dependency() { 271 | let tempdir = tempfile::tempdir().unwrap(); 272 | let manifest_path = tempdir.path().join("Cargo.toml"); 273 | 274 | let original_toml = r#" 275 | [package] 276 | name = "foo" # A comment 277 | version = { workspace = true } 278 | 279 | [lib] 280 | path = "lib.rs" 281 | 282 | [dev-dependencies] 283 | bar = { workspace = true } 284 | 285 | [workspace] 286 | members = [".", "bar"] 287 | 288 | [workspace.package] 289 | version = "0.1.0" 290 | 291 | [workspace.dependencies] 292 | bar = { version = "0.1.0", path = "./bar" } 293 | "#; 294 | 295 | write(&manifest_path, original_toml).unwrap(); 296 | 297 | let member_toml = r#" 298 | [package] 299 | name = "bar" # A comment 300 | version = { workspace = true } 301 | 302 | [lib] 303 | path = "lib.rs" 304 | "#; 305 | 306 | create_dir(tempdir.path().join("bar")).unwrap(); 307 | write(tempdir.path().join("bar").join("Cargo.toml"), member_toml).unwrap(); 308 | 309 | let (deps, pkgs) = args(&manifest_path, "foo"); 310 | 311 | assert!(should_remove_dev_deps(&deps, &pkgs)) 312 | } 313 | 314 | #[test] 315 | fn test_should_remove_dev_deps_member_workspace_dependency_target() { 316 | let tempdir = tempfile::tempdir().unwrap(); 317 | let manifest_path = tempdir.path().join("Cargo.toml"); 318 | 319 | let original_toml = r#" 320 | [package] 321 | name = "foo" # A comment 322 | version = { workspace = true } 323 | 324 | [lib] 325 | path = "lib.rs" 326 | 327 | [target.'cfg(unix)'.dev-dependencies] 328 | bar = { workspace = true } 329 | 330 | [workspace] 331 | members = [".", "bar"] 332 | 333 | [workspace.package] 334 | version = "0.1.0" 335 | 336 | [workspace.dependencies] 337 | bar = { version = "0.1.0", path = "./bar" } 338 | "#; 339 | 340 | write(&manifest_path, original_toml).unwrap(); 341 | 342 | let member_toml = r#" 343 | [package] 344 | name = "bar" # A comment 345 | version = { workspace = true } 346 | 347 | [lib] 348 | path = "lib.rs" 349 | "#; 350 | 351 | create_dir(tempdir.path().join("bar")).unwrap(); 352 | write(tempdir.path().join("bar").join("Cargo.toml"), member_toml).unwrap(); 353 | 354 | let (deps, pkgs) = args(&manifest_path, "foo"); 355 | 356 | assert!(should_remove_dev_deps(&deps, &pkgs)) 357 | } 358 | 359 | fn args(manifest_path: &PathBuf, dep: &str) -> (Vec, Vec<(Package, String)>) { 360 | let mut cmd = MetadataCommand::new(); 361 | 362 | cmd.manifest_path(manifest_path); 363 | cmd.no_deps(); 364 | 365 | let metadata = cmd.exec().unwrap(); 366 | 367 | let pkgs = metadata 368 | .packages 369 | .iter() 370 | .map(|x| (x.clone(), x.version.to_string())) 371 | .collect(); 372 | 373 | let pkg = metadata.packages.iter().find(|x| x.name == dep).unwrap(); 374 | 375 | (pkg.dependencies.clone(), pkgs) 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/error.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use oclif::{term::ERR_YELLOW, CliError}; 3 | use thiserror::Error; 4 | 5 | use std::{ 6 | io, 7 | sync::atomic::{AtomicBool, Ordering}, 8 | }; 9 | 10 | lazy_static! { 11 | static ref DEBUG: AtomicBool = AtomicBool::new(false); 12 | } 13 | 14 | pub fn get_debug() -> bool { 15 | DEBUG.load(Ordering::Relaxed) 16 | } 17 | 18 | pub fn set_debug() { 19 | DEBUG.store(true, Ordering::Relaxed); 20 | } 21 | 22 | macro_rules! _debug { 23 | ($desc:literal, $val:expr) => {{ 24 | if $crate::utils::get_debug() { 25 | oclif::term::TERM_ERR.write_line(&format!( 26 | "{} {} {}", 27 | oclif::term::ERR_GREEN.apply_to("debug"), 28 | oclif::term::ERR_MAGENTA.apply_to($desc), 29 | $val 30 | ))?; 31 | oclif::term::TERM_ERR.flush()?; 32 | } 33 | }}; 34 | } 35 | 36 | macro_rules! _info { 37 | ($desc:literal, $val:expr) => {{ 38 | oclif::term::TERM_ERR.write_line(&format!( 39 | "{} {} {}", 40 | oclif::term::ERR_GREEN.apply_to("info"), 41 | oclif::term::ERR_MAGENTA.apply_to($desc), 42 | $val 43 | ))?; 44 | oclif::term::TERM_ERR.flush()?; 45 | }}; 46 | } 47 | 48 | macro_rules! _warn { 49 | ($desc:literal, $val:expr) => {{ 50 | oclif::term::TERM_ERR.write_line(&format!( 51 | "{} {} {}", 52 | oclif::term::ERR_YELLOW.apply_to("warn"), 53 | oclif::term::ERR_MAGENTA.apply_to($desc), 54 | $val 55 | ))?; 56 | oclif::term::TERM_ERR.flush()?; 57 | }}; 58 | } 59 | 60 | pub(crate) use _debug as debug; 61 | pub(crate) use _info as info; 62 | pub(crate) use _warn as warn; 63 | 64 | #[derive(Error, Debug)] 65 | pub enum Error { 66 | #[error("package {id} is not inside workspace {ws}")] 67 | PackageNotInWorkspace { id: String, ws: String }, 68 | #[error("unable to find package {id}")] 69 | PackageNotFound { id: String }, 70 | #[error("did not find any public packages (use -a to include private packages)")] 71 | NoPublicPackages, 72 | #[error("did not find any package")] 73 | EmptyWorkspace, 74 | #[error("package {0}'s manifest has no parent directory")] 75 | ManifestHasNoParent(String), 76 | #[error("unable to read metadata specified in Cargo.toml: {0}")] 77 | BadMetadata(serde_json::Error), 78 | #[error("command needs to be run from the workspace root")] 79 | MustBeRunFromWorkspaceRoot, 80 | 81 | #[error("unable to verify package {0}")] 82 | Verify(String), 83 | #[error("unable to publish package {0}")] 84 | Publish(String), 85 | #[error("unable to update Cargo.lock")] 86 | Update, 87 | 88 | #[error("{0} value must contain '%n'")] 89 | MustContainPercentN(String), 90 | 91 | #[error("unable to create crate")] 92 | Create, 93 | #[error("path already exists")] 94 | PathAlreadyExists, 95 | #[error("member path is not inside workspace root")] 96 | InvalidMemberPath, 97 | #[error("the workspace already contains a package with this name")] 98 | DuplicatePackageName, 99 | #[error("path for crate is in workspace.exclude list ({0})")] 100 | InWorkspaceExclude(String), 101 | 102 | #[error("given path {0} is not a folder")] 103 | WorkspaceRootNotDir(String), 104 | #[error("unable to initialize workspace: {0}")] 105 | Init(String), 106 | 107 | #[error("unable to run cargo command with args {args:?}, got {err}")] 108 | Cargo { err: io::Error, args: Vec }, 109 | #[error("unable to run git command with args {args:?}, got {err}")] 110 | Git { err: io::Error, args: Vec }, 111 | 112 | #[error("child command failed to exit successfully")] 113 | Bail, 114 | 115 | #[error("not a git repository")] 116 | NotGit, 117 | #[error("no commits in this repository")] 118 | NoCommits, 119 | #[error("not on a git branch")] 120 | NotBranch, 121 | #[error("remote {remote} not found or branch {branch} not in {remote}")] 122 | NoRemote { remote: String, branch: String }, 123 | #[error("local branch {branch} is behind upstream {upstream}")] 124 | BehindRemote { upstream: String, branch: String }, 125 | #[error("not allowed to run on branch {branch} because it doesn't match pattern {pattern}")] 126 | BranchNotAllowed { branch: String, pattern: String }, 127 | #[error("unable to add files to git index, out = {0}, err = {1}")] 128 | NotAdded(String, String), 129 | #[error("unable to commit to git, out = {0}, err = {1}")] 130 | NotCommitted(String, String), 131 | #[error("unable to tag {0}, out = {1}, err = {2}")] 132 | NotTagged(String, String, String), 133 | #[error("unable to push to remote, out = {0}, err = {1}")] 134 | NotPushed(String, String), 135 | 136 | #[error("no changes detected")] 137 | NoChanges, 138 | 139 | #[error("could not understand 'cargo config get' output: {0}")] 140 | BadConfigGetOutput(String), 141 | #[error("crates index error: {0}")] 142 | CratesRegistry(#[from] tame_index::Error), 143 | #[error("unsupported crates index type")] 144 | UnsupportedCratesIndexType, 145 | #[error("crates index error: {0}")] 146 | CratesReqwest(#[from] tame_index::external::reqwest::Error), 147 | 148 | #[error("the workspace manifest has bad format: {0}")] 149 | WorkspaceBadFormat(String), 150 | 151 | #[error("{0}")] 152 | Semver(#[from] semver::ReqParseError), 153 | #[error("{0}")] 154 | Glob(#[from] glob::GlobError), 155 | #[error("{0}")] 156 | GlobPattern(#[from] glob::PatternError), 157 | #[error("{0}")] 158 | Globset(#[from] globset::Error), 159 | #[error("{0}")] 160 | Serde(#[from] serde_json::Error), 161 | #[error("{0}")] 162 | Io(#[from] io::Error), 163 | #[error("cannot convert command output to string, {0}")] 164 | FromUtf8(#[from] std::string::FromUtf8Error), 165 | #[error("{0}")] 166 | Toml(#[from] toml_edit::TomlError), 167 | } 168 | 169 | impl CliError for Error { 170 | fn color(self) -> Self { 171 | match self { 172 | Self::PackageNotInWorkspace { id, ws } => Self::PackageNotInWorkspace { 173 | id: format!("{}", ERR_YELLOW.apply_to(id)), 174 | ws: format!("{}", ERR_YELLOW.apply_to(ws)), 175 | }, 176 | Self::PackageNotFound { id } => Self::PackageNotFound { 177 | id: format!("{}", ERR_YELLOW.apply_to(id)), 178 | }, 179 | Self::Verify(pkg) => Self::Verify(format!("{}", ERR_YELLOW.apply_to(pkg))), 180 | Self::Publish(pkg) => Self::Publish(format!("{}", ERR_YELLOW.apply_to(pkg))), 181 | Self::MustContainPercentN(val) => { 182 | Self::MustContainPercentN(format!("{}", ERR_YELLOW.apply_to(val))) 183 | } 184 | Self::WorkspaceRootNotDir(path) => { 185 | Self::WorkspaceRootNotDir(format!("{}", ERR_YELLOW.apply_to(path))) 186 | } 187 | Self::NoRemote { remote, branch } => Self::NoRemote { 188 | remote: format!("{}", ERR_YELLOW.apply_to(remote)), 189 | branch: format!("{}", ERR_YELLOW.apply_to(branch)), 190 | }, 191 | Self::BehindRemote { upstream, branch } => Self::BehindRemote { 192 | upstream: format!("{}", ERR_YELLOW.apply_to(upstream)), 193 | branch: format!("{}", ERR_YELLOW.apply_to(branch)), 194 | }, 195 | Self::BranchNotAllowed { branch, pattern } => Self::BranchNotAllowed { 196 | branch: format!("{}", ERR_YELLOW.apply_to(branch)), 197 | pattern: format!("{}", ERR_YELLOW.apply_to(pattern)), 198 | }, 199 | Self::NotTagged(tag, out, err) => { 200 | Self::NotTagged(format!("{}", ERR_YELLOW.apply_to(tag)), out, err) 201 | } 202 | _ => self, 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/git.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{ 2 | debug, info, validate_value_containing_name, Error, WorkspaceConfig, INTERNAL_ERR, 3 | }; 4 | 5 | use camino::Utf8PathBuf; 6 | use clap::Parser; 7 | use globset::Glob; 8 | use semver::Version; 9 | 10 | use std::{ 11 | collections::BTreeMap as Map, 12 | process::{Command, ExitStatus}, 13 | }; 14 | 15 | pub fn git(root: &Utf8PathBuf, args: &[&str]) -> Result<(ExitStatus, String, String), Error> { 16 | debug!("git", args.to_vec().join(" ")); 17 | 18 | let output = Command::new("git") 19 | .current_dir(root) 20 | .args(args) 21 | .output() 22 | .map_err(|err| Error::Git { 23 | err, 24 | args: args.iter().map(|x| x.to_string()).collect(), 25 | })?; 26 | 27 | Ok(( 28 | output.status, 29 | String::from_utf8(output.stdout)?.trim().to_owned(), 30 | String::from_utf8(output.stderr)?.trim().to_owned(), 31 | )) 32 | } 33 | 34 | #[derive(Debug, Parser)] 35 | #[clap(next_help_heading = "GIT OPTIONS")] 36 | pub struct GitOpt { 37 | /// Do not commit version changes 38 | #[clap(long, conflicts_with_all = &[ 39 | "allow-branch", "amend", "message", "no-git-tag", 40 | "tag-prefix", "individual-tag-prefix", "no-individual-tags", 41 | "no-git-push", "git-remote", "no-global-tag" 42 | ])] 43 | pub no_git_commit: bool, 44 | 45 | /// Specify which branches to allow from [default: master] 46 | #[clap(long, value_name = "PATTERN", forbid_empty_values(true))] 47 | pub allow_branch: Option, 48 | 49 | /// Amend the existing commit, instead of generating a new one 50 | #[clap(long)] 51 | pub amend: bool, 52 | 53 | /// Use a custom commit message when creating the version commit [default: Release %v] 54 | #[clap( 55 | short, 56 | long, 57 | conflicts_with_all = &["amend"], 58 | forbid_empty_values(true) 59 | )] 60 | pub message: Option, 61 | 62 | /// Do not tag generated commit 63 | #[clap(long, conflicts_with_all = &["tag-prefix", "individual-tag-prefix", "no-individual-tags"])] 64 | pub no_git_tag: bool, 65 | 66 | /// Do not tag individual versions for crates 67 | #[clap(long, conflicts_with_all = &["individual-tag-prefix"])] 68 | pub no_individual_tags: bool, 69 | 70 | /// Do not create a global tag for a workspace 71 | #[clap(long)] 72 | pub no_global_tag: bool, 73 | 74 | /// Customize tag prefix (can be empty) 75 | #[clap(long, default_value = "v", value_name = "PREFIX")] 76 | pub tag_prefix: String, 77 | 78 | /// Customize prefix for individual tags (should contain `%n`) 79 | #[clap( 80 | long, 81 | default_value = "%n@", 82 | value_name = "PREFIX", 83 | validator = validate_value_containing_name, 84 | forbid_empty_values(true) 85 | )] 86 | pub individual_tag_prefix: String, 87 | 88 | /// Do not push generated commit and tags to git remote 89 | #[clap(long, conflicts_with_all = &["git-remote"])] 90 | pub no_git_push: bool, 91 | 92 | /// Push git changes to the specified remote 93 | #[clap( 94 | long, 95 | default_value = "origin", 96 | value_name = "REMOTE", 97 | forbid_empty_values(true) 98 | )] 99 | pub git_remote: String, 100 | } 101 | 102 | impl GitOpt { 103 | pub fn validate( 104 | &self, 105 | root: &Utf8PathBuf, 106 | config: &WorkspaceConfig, 107 | ) -> Result, Error> { 108 | let mut ret = None; 109 | 110 | if !self.no_git_commit { 111 | let (_, out, err) = git(root, &["rev-list", "--count", "--all", "--max-count=1"])?; 112 | 113 | if err.contains("not a git repository") { 114 | return Err(Error::NotGit); 115 | } 116 | 117 | if out == "0" { 118 | return Err(Error::NoCommits); 119 | } 120 | 121 | let (_, branch, _) = git(root, &["rev-parse", "--abbrev-ref", "HEAD"])?; 122 | 123 | if branch == "HEAD" { 124 | return Err(Error::NotBranch); 125 | } 126 | 127 | ret = Some(branch.clone()); 128 | 129 | // Get the final `allow_branch` value 130 | let allow_branch_default_value = String::from("master"); 131 | let allow_branch = self.allow_branch.as_ref().unwrap_or_else(|| { 132 | config 133 | .allow_branch 134 | .as_ref() 135 | .unwrap_or(&allow_branch_default_value) 136 | }); 137 | 138 | // Treat `main` as `master` 139 | let test_branch = if branch == "main" && allow_branch.as_str() == "master" { 140 | "master".into() 141 | } else { 142 | branch.clone() 143 | }; 144 | 145 | let pattern = Glob::new(allow_branch)?; 146 | 147 | if !pattern.compile_matcher().is_match(test_branch) { 148 | return Err(Error::BranchNotAllowed { 149 | branch, 150 | pattern: pattern.glob().to_string(), 151 | }); 152 | } 153 | 154 | if !self.no_git_push { 155 | let remote_branch = format!("{}/{}", self.git_remote, branch); 156 | 157 | let (_, out, _) = git( 158 | root, 159 | &[ 160 | "show-ref", 161 | "--verify", 162 | &format!("refs/remotes/{}", remote_branch), 163 | ], 164 | )?; 165 | 166 | if out.is_empty() { 167 | return Err(Error::NoRemote { 168 | remote: self.git_remote.clone(), 169 | branch, 170 | }); 171 | } 172 | 173 | git(root, &["remote", "update"])?; 174 | 175 | let (_, out, _) = git( 176 | root, 177 | &[ 178 | "rev-list", 179 | "--left-only", 180 | "--count", 181 | &format!("{}...{}", remote_branch, branch), 182 | ], 183 | )?; 184 | 185 | if out != "0" { 186 | return Err(Error::BehindRemote { 187 | branch, 188 | upstream: remote_branch, 189 | }); 190 | } 191 | } 192 | } 193 | 194 | Ok(ret) 195 | } 196 | 197 | pub fn commit( 198 | &self, 199 | root: &Utf8PathBuf, 200 | new_version: &Option, 201 | new_versions: &Map, 202 | branch: Option, 203 | config: &WorkspaceConfig, 204 | ) -> Result<(), Error> { 205 | if !self.no_git_commit { 206 | info!("version", "committing changes"); 207 | 208 | let branch = branch.expect(INTERNAL_ERR); 209 | let added = git(root, &["add", "-u"])?; 210 | 211 | if !added.0.success() { 212 | return Err(Error::NotAdded(added.1, added.2)); 213 | } 214 | 215 | let mut args = vec!["commit".to_string()]; 216 | 217 | if self.amend { 218 | args.push("--amend".to_string()); 219 | args.push("--no-edit".to_string()); 220 | } else { 221 | args.push("-m".to_string()); 222 | 223 | let mut msg = "Release %v"; 224 | 225 | if let Some(supplied) = &self.message { 226 | msg = supplied; 227 | } 228 | 229 | let mut msg = self.commit_msg(msg, new_versions); 230 | 231 | msg = msg.replace( 232 | "%v", 233 | &new_version 234 | .as_ref() 235 | .map_or("independent packages".to_string(), |x| format!("{}", x)), 236 | ); 237 | 238 | args.push(msg); 239 | } 240 | 241 | let committed = git(root, &args.iter().map(|x| x.as_str()).collect::>())?; 242 | 243 | if !committed.0.success() { 244 | return Err(Error::NotCommitted(committed.1, committed.2)); 245 | } 246 | 247 | if !self.no_git_tag { 248 | info!("version", "tagging"); 249 | 250 | if !self.no_global_tag { 251 | if let Some(version) = new_version { 252 | let tag = format!("{}{}", &self.tag_prefix, version); 253 | self.tag(root, &tag, &tag)?; 254 | } 255 | } 256 | 257 | if !(self.no_individual_tags || config.no_individual_tags.unwrap_or_default()) { 258 | for (p, v) in new_versions { 259 | let tag = format!("{}{}", self.individual_tag_prefix.replace("%n", p), v); 260 | self.tag(root, &tag, &tag)?; 261 | } 262 | } 263 | } 264 | 265 | if !self.no_git_push { 266 | info!("git", "pushing"); 267 | 268 | let pushed = git(root, &["push", "--follow-tags", &self.git_remote, &branch])?; 269 | 270 | if !pushed.0.success() { 271 | return Err(Error::NotPushed(pushed.1, pushed.2)); 272 | } 273 | } 274 | } 275 | 276 | Ok(()) 277 | } 278 | 279 | fn tag(&self, root: &Utf8PathBuf, tag: &str, msg: &str) -> Result<(), Error> { 280 | let tagged = git(root, &["tag", tag, "-m", msg])?; 281 | 282 | if !tagged.0.success() { 283 | return Err(Error::NotTagged(tag.to_string(), tagged.1, tagged.2)); 284 | } 285 | 286 | Ok(()) 287 | } 288 | 289 | fn commit_msg(&self, msg: &str, new_versions: &Map) -> String { 290 | format!( 291 | "{}\n\n{}\n\nGenerated by cargo-workspaces", 292 | msg, 293 | new_versions 294 | .iter() 295 | .map(|x| format!("{}@{}", x.0, x.1)) 296 | .collect::>() 297 | .join("\n") 298 | ) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/list.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{Pkg, Result, INTERNAL_ERR}; 2 | 3 | use clap::Parser; 4 | use oclif::{console::style, term::TERM_OUT}; 5 | use serde_json::to_string_pretty; 6 | 7 | use std::{cmp::max, path::Path}; 8 | 9 | #[derive(Debug, Parser)] 10 | #[clap(next_help_heading = "LIST OPTIONS")] 11 | pub struct ListPublicOpt { 12 | /// Show extended information 13 | #[clap(short, long)] 14 | pub long: bool, 15 | 16 | /// Show information as a JSON array 17 | #[clap(long, conflicts_with = "long")] 18 | pub json: bool, 19 | } 20 | 21 | #[derive(Debug, Parser)] 22 | #[clap(next_help_heading = "LIST OPTIONS")] 23 | pub struct ListOpt { 24 | #[clap(flatten)] 25 | pub list: ListPublicOpt, 26 | 27 | /// Show private crates that are normally hidden 28 | #[clap(short, long)] 29 | pub all: bool, 30 | } 31 | 32 | pub fn list(pkgs: &[Pkg], list: ListOpt) -> Result { 33 | if list.list.json { 34 | return Ok(TERM_OUT.write_line(&to_string_pretty(pkgs)?)?); 35 | } 36 | 37 | if pkgs.is_empty() { 38 | return Ok(()); 39 | } 40 | 41 | let first = pkgs.iter().map(|x| x.name.len()).max().expect(INTERNAL_ERR); 42 | let second = pkgs 43 | .iter() 44 | .map(|x| x.version.to_string().len() + 1) 45 | .max() 46 | .expect(INTERNAL_ERR); 47 | let third = pkgs 48 | .iter() 49 | .map(|x| max(1, x.path.as_os_str().len())) 50 | .max() 51 | .expect(INTERNAL_ERR); 52 | 53 | for pkg in pkgs { 54 | if !list.all && pkg.private { 55 | continue; 56 | } 57 | 58 | TERM_OUT.write_str(&pkg.name)?; 59 | let mut width = first - pkg.name.len(); 60 | 61 | if list.list.long { 62 | let path = if pkg.path.as_os_str().is_empty() { 63 | Path::new(".") 64 | } else { 65 | pkg.path.as_path() 66 | }; 67 | 68 | TERM_OUT.write_str(&format!( 69 | "{:f$} {}{:s$} {}", 70 | "", 71 | style(format!("v{}", pkg.version)).green(), 72 | "", 73 | style(path.display()).black().bright(), 74 | f = width, 75 | s = second - pkg.version.to_string().len() - 1, 76 | ))?; 77 | 78 | width = third - pkg.path.as_os_str().len(); 79 | } 80 | 81 | if list.all && pkg.private { 82 | TERM_OUT.write_str(&format!( 83 | "{:w$} ({})", 84 | "", 85 | style("PRIVATE").red(), 86 | w = width 87 | ))?; 88 | } 89 | 90 | TERM_OUT.write_line("")?; 91 | } 92 | 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod basic_checks; 2 | mod cargo; 3 | mod changable; 4 | mod config; 5 | mod dag; 6 | mod dev_dep_remover; 7 | mod error; 8 | mod git; 9 | mod list; 10 | mod pkg; 11 | mod publish; 12 | mod version; 13 | 14 | pub use basic_checks::basic_checks; 15 | pub use cargo::{cargo, cargo_config_get, change_versions, rename_packages}; 16 | pub use changable::{ChangeData, ChangeOpt}; 17 | pub use config::{read_config, PackageConfig, WorkspaceConfig}; 18 | pub use dag::dag; 19 | pub use dev_dep_remover::{should_remove_dev_deps, DevDependencyRemover}; 20 | pub(crate) use error::{debug, info, warn}; 21 | pub use error::{get_debug, set_debug, Error}; 22 | pub use git::{git, GitOpt}; 23 | pub use list::{list, ListOpt, ListPublicOpt}; 24 | pub use pkg::{get_pkgs, is_private, Pkg}; 25 | pub use publish::{ 26 | create_http_client, filter_private, is_published, package_registry, RegistryOpt, 27 | }; 28 | pub use version::VersionOpt; 29 | 30 | pub type Result = std::result::Result; 31 | 32 | pub const INTERNAL_ERR: &str = "Internal error message. Please create an issue on https://github.com/pksunkara/cargo-workspaces"; 33 | 34 | pub fn validate_value_containing_name(value: &str) -> std::result::Result<(), String> { 35 | if !value.contains("%n") { 36 | return Err("must contain '%n'\n".to_string()); 37 | } 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/pkg.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{read_config, Error, PackageConfig, Result, INTERNAL_ERR}; 2 | 3 | use cargo_metadata::{Metadata, Package, PackageId}; 4 | use oclif::CliError; 5 | use semver::Version; 6 | use serde::Serialize; 7 | 8 | use std::path::PathBuf; 9 | 10 | #[derive(Serialize, Debug, Clone, Ord, Eq, PartialOrd, PartialEq)] 11 | pub struct Pkg { 12 | #[serde(skip)] 13 | pub id: PackageId, 14 | pub name: String, 15 | pub version: Version, 16 | pub location: PathBuf, 17 | #[serde(skip)] 18 | pub path: PathBuf, 19 | pub private: bool, 20 | #[serde(skip)] 21 | pub config: PackageConfig, 22 | } 23 | 24 | pub fn is_private(pkg: &Package) -> bool { 25 | pkg.publish.is_some() && pkg.publish.as_ref().expect(INTERNAL_ERR).is_empty() 26 | } 27 | 28 | pub fn get_pkgs(metadata: &Metadata, all: bool) -> Result> { 29 | let mut pkgs = vec![]; 30 | 31 | for id in &metadata.workspace_members { 32 | if let Some(pkg) = metadata.packages.iter().find(|x| x.id == *id) { 33 | let private = is_private(pkg); 34 | 35 | if !all && private { 36 | continue; 37 | } 38 | 39 | let loc = pkg.manifest_path.strip_prefix(&metadata.workspace_root); 40 | 41 | if loc.is_err() { 42 | return Err(Error::PackageNotInWorkspace { 43 | id: pkg.id.repr.clone(), 44 | ws: metadata.workspace_root.to_string(), 45 | }); 46 | } 47 | 48 | let loc = loc.expect(INTERNAL_ERR); 49 | let loc = if loc.is_file() { 50 | loc.parent().expect(INTERNAL_ERR) 51 | } else { 52 | loc 53 | }; 54 | 55 | pkgs.push(Pkg { 56 | id: pkg.id.clone(), 57 | name: pkg.name.clone(), 58 | version: pkg.version.clone(), 59 | location: metadata.workspace_root.join(loc).into(), 60 | path: loc.into(), 61 | private, 62 | config: read_config(&pkg.metadata)?, 63 | }); 64 | } else { 65 | Error::PackageNotFound { 66 | id: id.repr.clone(), 67 | } 68 | .print()?; 69 | } 70 | } 71 | 72 | if pkgs.is_empty() { 73 | let error = if all { 74 | Error::EmptyWorkspace 75 | } else { 76 | Error::NoPublicPackages 77 | }; 78 | 79 | return Err(error); 80 | } 81 | 82 | pkgs.sort(); 83 | Ok(pkgs) 84 | } 85 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/publish.rs: -------------------------------------------------------------------------------- 1 | //! Helper functions useful when publishing (or preparing for publishing) crates. 2 | 3 | use std::convert::TryFrom; 4 | 5 | use crate::utils::{cargo_config_get, is_private, Error, Result}; 6 | 7 | use camino::Utf8PathBuf; 8 | use cargo_metadata::{Metadata, Package}; 9 | use clap::Parser; 10 | use indexmap::IndexSet as Set; 11 | use tame_index::{ 12 | external::{ 13 | http::{HeaderMap, HeaderValue}, 14 | reqwest::{blocking::Client, header::AUTHORIZATION, Certificate}, 15 | }, 16 | index::{ComboIndex, ComboIndexCache, RemoteGitIndex, RemoteSparseIndex}, 17 | utils::flock::LockOptions, 18 | IndexLocation, IndexUrl, KrateName, 19 | }; 20 | 21 | #[derive(Debug, Parser)] 22 | #[clap(next_help_heading = "REGISTRY OPTIONS")] 23 | pub struct RegistryOpt { 24 | /// The token to use for accessing the registry 25 | #[clap(long, forbid_empty_values(true))] 26 | pub token: Option, 27 | 28 | /// The Cargo registry to use 29 | #[clap(long, forbid_empty_values(true))] 30 | pub registry: Option, 31 | } 32 | 33 | pub fn filter_private(visited: Set, pkgs: &[(Package, String)]) -> Set { 34 | visited 35 | .into_iter() 36 | .filter(|x| { 37 | pkgs.iter() 38 | .find(|(p, _)| p.manifest_path == *x) 39 | .map(|(pkg, _)| !is_private(pkg)) 40 | .unwrap_or(false) 41 | }) 42 | .collect() 43 | } 44 | 45 | pub fn package_registry<'a>( 46 | metadata: &Metadata, 47 | registry: Option<&'a String>, 48 | pkg: &Package, 49 | ) -> Result> { 50 | let url = if let Some(registry) = 51 | registry.or_else(|| pkg.publish.as_deref().and_then(|x| x.first())) 52 | { 53 | let registry_url = cargo_config_get( 54 | &metadata.workspace_root, 55 | &format!("registries.{}.index", registry), 56 | )?; 57 | IndexUrl::NonCratesIo(registry_url.into()) 58 | } else { 59 | IndexUrl::crates_io(None, None, None)? 60 | }; 61 | 62 | Ok(url) 63 | } 64 | 65 | pub fn create_http_client(workspace_root: &Utf8PathBuf, token: &Option) -> Result { 66 | let client_builder = Client::builder().use_rustls_tls(); 67 | let client_builder = if let Some(ref token) = token { 68 | let mut headers = HeaderMap::new(); 69 | headers.insert(AUTHORIZATION, HeaderValue::from_str(token).unwrap()); 70 | client_builder.default_headers(headers) 71 | } else { 72 | client_builder 73 | }; 74 | let http_cainfo = cargo_config_get(workspace_root, "http.cainfo").ok(); 75 | let client_builder = if let Some(http_cainfo) = http_cainfo { 76 | client_builder 77 | .tls_built_in_root_certs(false) 78 | .add_root_certificate(Certificate::from_pem(&std::fs::read(http_cainfo)?)?) 79 | } else { 80 | client_builder 81 | }; 82 | Ok(client_builder.build()?) 83 | } 84 | 85 | pub fn is_published( 86 | client: &Client, 87 | index_url: IndexUrl, 88 | name: &str, 89 | version: &str, 90 | ) -> Result { 91 | let index_cache = ComboIndexCache::new(IndexLocation::new(index_url))?; 92 | let lock = LockOptions::cargo_package_lock(None)?.try_lock()?; 93 | 94 | let index: ComboIndex = match index_cache { 95 | ComboIndexCache::Git(git) => { 96 | let mut rgi = RemoteGitIndex::new(git, &lock)?; 97 | 98 | rgi.fetch(&lock)?; 99 | rgi.into() 100 | } 101 | ComboIndexCache::Sparse(sparse) => RemoteSparseIndex::new(sparse, client.clone()).into(), 102 | _ => return Err(Error::UnsupportedCratesIndexType), 103 | }; 104 | 105 | let index_crate = index.krate(KrateName::try_from(name)?, false, &lock); 106 | match index_crate { 107 | Ok(Some(crate_data)) => Ok(crate_data.versions.iter().any(|v| v.version == version)), 108 | Ok(None) | Err(tame_index::Error::NoCrateVersions) => Ok(false), 109 | Err(e) => Err(e.into()), 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /cargo-workspaces/src/utils/version.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{ 2 | cargo, change_versions, info, read_config, ChangeData, ChangeOpt, Error, GitOpt, Pkg, Result, 3 | WorkspaceConfig, INTERNAL_ERR, 4 | }; 5 | 6 | use cargo_metadata::Metadata; 7 | use clap::{ArgEnum, Parser}; 8 | use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; 9 | use oclif::{ 10 | console::Style, 11 | term::{TERM_ERR, TERM_OUT}, 12 | }; 13 | use semver::{Identifier, Version}; 14 | 15 | use std::{collections::BTreeMap as Map, fs, process::exit}; 16 | 17 | #[derive(Debug, Clone, ArgEnum)] 18 | pub enum Bump { 19 | Major, 20 | Minor, 21 | Patch, 22 | Premajor, 23 | Preminor, 24 | Prepatch, 25 | Skip, 26 | Prerelease, 27 | Custom, 28 | } 29 | 30 | impl Bump { 31 | pub fn selected(&self) -> usize { 32 | match self { 33 | Bump::Major => 2, 34 | Bump::Minor => 1, 35 | Bump::Patch => 0, 36 | Bump::Premajor => 5, 37 | Bump::Preminor => 4, 38 | Bump::Prepatch => 3, 39 | Bump::Skip => 6, 40 | Bump::Prerelease => 7, 41 | Bump::Custom => 8, 42 | } 43 | } 44 | } 45 | 46 | #[derive(Debug, Parser)] 47 | #[clap(next_help_heading = "VERSION OPTIONS")] 48 | pub struct VersionOpt { 49 | /// Increment all versions by the given explicit 50 | /// semver keyword while skipping the prompts for them 51 | #[clap(arg_enum, help_heading = "VERSION ARGS")] 52 | pub bump: Option, 53 | 54 | /// Specify custom version value when 'bump' is set to 'custom' 55 | #[clap(required_if_eq("bump", "custom"), help_heading = "VERSION ARGS")] 56 | pub custom: Option, 57 | 58 | /// Specify prerelease identifier 59 | #[clap(long, value_name = "IDENTIFIER", forbid_empty_values(true))] 60 | pub pre_id: Option, 61 | 62 | #[clap(flatten)] 63 | pub change: ChangeOpt, 64 | 65 | #[clap(flatten)] 66 | pub git: GitOpt, 67 | 68 | /// Also do versioning for private crates (will not be published) 69 | #[clap(short, long)] 70 | pub all: bool, 71 | 72 | /// Specify inter dependency version numbers exactly with `=` 73 | #[clap(long)] 74 | pub exact: bool, 75 | 76 | /// Skip confirmation prompt 77 | #[clap(short, long)] 78 | pub yes: bool, 79 | } 80 | 81 | impl VersionOpt { 82 | pub fn do_versioning(&self, metadata: &Metadata) -> Result> { 83 | let config: WorkspaceConfig = read_config(&metadata.workspace_metadata)?; 84 | let branch = self.git.validate(&metadata.workspace_root, &config)?; 85 | let mut since = self.change.since.clone(); 86 | 87 | if self.change.since.is_none() { 88 | let change_data = ChangeData::new(metadata, &self.change)?; 89 | 90 | if self.change.force.is_none() && change_data.count == "0" && !change_data.dirty { 91 | TERM_OUT.write_line("Current HEAD is already released, skipping versioning")?; 92 | return Ok(Map::new()); 93 | } 94 | 95 | since = change_data.since; 96 | } 97 | 98 | let (mut changed_p, mut unchanged_p) = 99 | self.change.get_changed_pkgs(metadata, &since, self.all)?; 100 | 101 | if changed_p.is_empty() { 102 | TERM_OUT.write_line("No changes detected, skipping versioning")?; 103 | return Ok(Map::new()); 104 | } 105 | 106 | let mut new_version = None; 107 | let mut new_versions = vec![]; 108 | 109 | while !changed_p.is_empty() { 110 | self.get_new_versions(metadata, changed_p, &mut new_version, &mut new_versions)?; 111 | 112 | let pkgs = unchanged_p.into_iter().partition::, _>(|p| { 113 | let pkg = metadata 114 | .packages 115 | .iter() 116 | .find(|x| x.name == p.name) 117 | .expect(INTERNAL_ERR); 118 | 119 | pkg.dependencies.iter().any(|x| { 120 | if let Some(version) = new_versions.iter().find(|y| x.name == y.0).map(|y| &y.1) 121 | { 122 | !x.req.matches(version) 123 | } else { 124 | false 125 | } 126 | }) 127 | }); 128 | 129 | changed_p = pkgs.0; 130 | unchanged_p = pkgs.1; 131 | } 132 | 133 | let new_versions = self.confirm_versions(new_versions)?; 134 | 135 | for p in &metadata.packages { 136 | if new_versions.contains_key(&p.name) 137 | && p.dependencies 138 | .iter() 139 | .all(|x| new_versions.contains_key(&x.name)) 140 | { 141 | continue; 142 | } 143 | 144 | fs::write( 145 | &p.manifest_path, 146 | format!( 147 | "{}\n", 148 | change_versions( 149 | fs::read_to_string(&p.manifest_path)?, 150 | &p.name, 151 | &new_versions, 152 | self.exact, 153 | )? 154 | ), 155 | )?; 156 | } 157 | 158 | if let Some(new_version) = &new_version { 159 | let workspace_root = metadata.workspace_root.join("Cargo.toml"); 160 | let mut new_versions = new_versions.clone(); 161 | 162 | new_versions.insert("".to_string(), new_version.clone()); 163 | 164 | fs::write( 165 | &workspace_root, 166 | format!( 167 | "{}\n", 168 | change_versions( 169 | fs::read_to_string(&workspace_root)?, 170 | "", 171 | &new_versions, 172 | self.exact 173 | )? 174 | ), 175 | )?; 176 | } 177 | 178 | let output = cargo(&metadata.workspace_root, &["update", "-w"], &[])?; 179 | 180 | if output.1.contains("error:") { 181 | return Err(Error::Update); 182 | } 183 | 184 | self.git.commit( 185 | &metadata.workspace_root, 186 | &new_version, 187 | &new_versions, 188 | branch, 189 | &config, 190 | )?; 191 | 192 | Ok(new_versions) 193 | } 194 | 195 | fn get_new_versions( 196 | &self, 197 | metadata: &Metadata, 198 | pkgs: Vec, 199 | new_version: &mut Option, 200 | new_versions: &mut Vec<(String, Version, Version)>, 201 | ) -> Result { 202 | let (independent_pkgs, same_pkgs) = pkgs 203 | .into_iter() 204 | .partition::, _>(|p| p.config.independent.unwrap_or(false)); 205 | 206 | if !same_pkgs.is_empty() { 207 | let cur_version = same_pkgs 208 | .iter() 209 | .map(|p| { 210 | &metadata 211 | .packages 212 | .iter() 213 | .find(|x| x.id == p.id) 214 | .expect(INTERNAL_ERR) 215 | .version 216 | }) 217 | .max() 218 | .expect(INTERNAL_ERR); 219 | 220 | if new_version.is_none() { 221 | info!("current common version", cur_version); 222 | 223 | *new_version = self.ask_version(cur_version, None)?; 224 | } 225 | 226 | if let Some(ref new_version) = new_version { 227 | for p in &same_pkgs { 228 | new_versions.push((p.name.to_string(), new_version.clone(), p.version.clone())); 229 | } 230 | } 231 | } 232 | 233 | for p in &independent_pkgs { 234 | let new_version = self.ask_version(&p.version, Some(&p.name))?; 235 | 236 | if let Some(new_version) = new_version { 237 | new_versions.push((p.name.to_string(), new_version, p.version.clone())); 238 | } 239 | } 240 | 241 | Ok(()) 242 | } 243 | 244 | fn confirm_versions( 245 | &self, 246 | versions: Vec<(String, Version, Version)>, 247 | ) -> Result> { 248 | let mut new_versions = Map::new(); 249 | let style = Style::new().for_stderr(); 250 | 251 | TERM_ERR.write_line("\nChanges:")?; 252 | 253 | for v in versions { 254 | TERM_ERR.write_line(&format!( 255 | " - {}: {} => {}", 256 | style.clone().yellow().apply_to(&v.0), 257 | v.2, 258 | style.clone().cyan().apply_to(&v.1), 259 | ))?; 260 | new_versions.insert(v.0, v.1); 261 | } 262 | 263 | TERM_ERR.write_line("")?; 264 | TERM_ERR.flush()?; 265 | 266 | let create = self.yes 267 | || Confirm::with_theme(&ColorfulTheme::default()) 268 | .with_prompt("Are you sure you want to create these versions?") 269 | .default(false) 270 | .interact_on(&TERM_ERR)?; 271 | 272 | if !create { 273 | exit(0); 274 | } 275 | 276 | Ok(new_versions) 277 | } 278 | 279 | /// Returns Ok(None) for skip option 280 | fn ask_version( 281 | &self, 282 | cur_version: &Version, 283 | pkg_name: Option<&str>, 284 | ) -> Result> { 285 | let mut items = version_items(cur_version, &self.pre_id); 286 | 287 | items.push((format!("Skip (stays {cur_version})"), None)); 288 | items.push(("Custom Prerelease".to_string(), None)); 289 | items.push(("Custom Version".to_string(), None)); 290 | 291 | let prompt = if let Some(name) = pkg_name { 292 | format!("for {} ", name) 293 | } else { 294 | "".to_string() 295 | }; 296 | 297 | let theme = ColorfulTheme::default(); 298 | 299 | let selected = if let Some(bump) = &self.bump { 300 | bump.selected() 301 | } else { 302 | Select::with_theme(&theme) 303 | .with_prompt(&format!( 304 | "Select a new version {}(currently {})", 305 | prompt, cur_version 306 | )) 307 | .items(&items.iter().map(|x| &x.0).collect::>()) 308 | .default(0) 309 | .interact_on(&TERM_ERR)? 310 | }; 311 | 312 | let new_version = if selected == 6 { 313 | return Ok(None); 314 | } else if selected == 7 { 315 | let custom = custom_pre(cur_version); 316 | 317 | let preid = if let Some(preid) = &self.pre_id { 318 | preid.clone() 319 | } else { 320 | Input::with_theme(&theme) 321 | .with_prompt(&format!( 322 | "Enter a prerelease identifier (default: '{}', yielding {})", 323 | custom.0, custom.1 324 | )) 325 | .default(custom.0.to_string()) 326 | .interact_on(&TERM_ERR)? 327 | }; 328 | 329 | inc_preid(cur_version, Identifier::AlphaNumeric(preid)) 330 | } else if selected == 8 { 331 | if let Some(version) = &self.custom { 332 | version.clone() 333 | } else { 334 | Input::with_theme(&theme) 335 | .with_prompt("Enter a custom version") 336 | .interact_on(&TERM_ERR)? 337 | } 338 | } else { 339 | items 340 | .get(selected) 341 | .expect(INTERNAL_ERR) 342 | .clone() 343 | .1 344 | .expect(INTERNAL_ERR) 345 | }; 346 | 347 | Ok(Some(new_version)) 348 | } 349 | } 350 | 351 | fn inc_pre(pre: &[Identifier], preid: &Option) -> Vec { 352 | match pre.first() { 353 | Some(Identifier::AlphaNumeric(id)) => { 354 | vec![Identifier::AlphaNumeric(id.clone()), Identifier::Numeric(0)] 355 | } 356 | Some(Identifier::Numeric(_)) => vec![Identifier::Numeric(0)], 357 | None => vec![ 358 | Identifier::AlphaNumeric( 359 | preid 360 | .as_ref() 361 | .map_or_else(|| "alpha".to_string(), |x| x.clone()), 362 | ), 363 | Identifier::Numeric(0), 364 | ], 365 | } 366 | } 367 | 368 | fn inc_preid(cur_version: &Version, preid: Identifier) -> Version { 369 | let mut version = cur_version.clone(); 370 | 371 | if cur_version.pre.is_empty() { 372 | version.increment_patch(); 373 | version.pre = vec![preid, Identifier::Numeric(0)]; 374 | } else { 375 | match cur_version.pre.first().expect(INTERNAL_ERR) { 376 | Identifier::AlphaNumeric(id) => { 377 | version.pre = vec![preid.clone()]; 378 | 379 | if preid.to_string() == *id { 380 | match cur_version.pre.get(1) { 381 | Some(Identifier::Numeric(n)) => { 382 | version.pre.push(Identifier::Numeric(n + 1)) 383 | } 384 | _ => version.pre.push(Identifier::Numeric(0)), 385 | }; 386 | } else { 387 | version.pre.push(Identifier::Numeric(0)); 388 | } 389 | } 390 | Identifier::Numeric(n) => { 391 | if preid.to_string() == n.to_string() { 392 | version.pre.clone_from(&cur_version.pre); 393 | 394 | if let Some(Identifier::Numeric(n)) = version 395 | .pre 396 | .iter_mut() 397 | .rfind(|x| matches!(x, Identifier::Numeric(_))) 398 | { 399 | *n += 1; 400 | } 401 | } else { 402 | version.pre = vec![preid, Identifier::Numeric(0)]; 403 | } 404 | } 405 | } 406 | } 407 | 408 | version 409 | } 410 | 411 | fn custom_pre(cur_version: &Version) -> (Identifier, Version) { 412 | let id = if let Some(id) = cur_version.pre.first() { 413 | id.clone() 414 | } else { 415 | Identifier::AlphaNumeric("alpha".to_string()) 416 | }; 417 | 418 | (id.clone(), inc_preid(cur_version, id)) 419 | } 420 | 421 | fn inc_patch(mut cur_version: Version) -> Version { 422 | if !cur_version.pre.is_empty() { 423 | cur_version.pre.clear(); 424 | } else { 425 | cur_version.increment_patch(); 426 | } 427 | 428 | cur_version 429 | } 430 | 431 | fn inc_minor(mut cur_version: Version) -> Version { 432 | if !cur_version.pre.is_empty() && cur_version.patch == 0 { 433 | cur_version.pre.clear(); 434 | } else { 435 | cur_version.increment_minor(); 436 | } 437 | 438 | cur_version 439 | } 440 | 441 | fn inc_major(mut cur_version: Version) -> Version { 442 | if !cur_version.pre.is_empty() && cur_version.patch == 0 && cur_version.minor == 0 { 443 | cur_version.pre.clear(); 444 | } else { 445 | cur_version.increment_major(); 446 | } 447 | 448 | cur_version 449 | } 450 | 451 | fn version_items(cur_version: &Version, preid: &Option) -> Vec<(String, Option)> { 452 | let mut items = vec![]; 453 | 454 | let v = inc_patch(cur_version.clone()); 455 | items.push((format!("Patch ({})", &v), Some(v))); 456 | 457 | let v = inc_minor(cur_version.clone()); 458 | items.push((format!("Minor ({})", &v), Some(v))); 459 | 460 | let v = inc_major(cur_version.clone()); 461 | items.push((format!("Major ({})", &v), Some(v))); 462 | 463 | let mut v = cur_version.clone(); 464 | v.increment_patch(); 465 | v.pre = inc_pre(&cur_version.pre, preid); 466 | items.push((format!("Prepatch ({})", &v), Some(v))); 467 | 468 | let mut v = cur_version.clone(); 469 | v.increment_minor(); 470 | v.pre = inc_pre(&cur_version.pre, preid); 471 | items.push((format!("Preminor ({})", &v), Some(v))); 472 | 473 | let mut v = cur_version.clone(); 474 | v.increment_major(); 475 | v.pre = inc_pre(&cur_version.pre, preid); 476 | items.push((format!("Premajor ({})", &v), Some(v))); 477 | 478 | items 479 | } 480 | 481 | #[cfg(test)] 482 | mod test_super { 483 | use super::*; 484 | 485 | #[test] 486 | fn test_inc_patch() { 487 | let v = inc_patch(Version::parse("0.7.2").unwrap()); 488 | assert_eq!(v.to_string(), "0.7.3"); 489 | } 490 | 491 | #[test] 492 | fn test_inc_patch_on_prepatch() { 493 | let v = inc_patch(Version::parse("0.7.2-rc.0").unwrap()); 494 | assert_eq!(v.to_string(), "0.7.2"); 495 | } 496 | 497 | #[test] 498 | fn test_inc_patch_on_preminor() { 499 | let v = inc_patch(Version::parse("0.7.0-rc.0").unwrap()); 500 | assert_eq!(v.to_string(), "0.7.0"); 501 | } 502 | 503 | #[test] 504 | fn test_inc_patch_on_premajor() { 505 | let v = inc_patch(Version::parse("1.0.0-rc.0").unwrap()); 506 | assert_eq!(v.to_string(), "1.0.0"); 507 | } 508 | 509 | #[test] 510 | fn test_inc_minor() { 511 | let v = inc_minor(Version::parse("0.7.2").unwrap()); 512 | assert_eq!(v.to_string(), "0.8.0"); 513 | } 514 | 515 | #[test] 516 | fn test_inc_minor_on_prepatch() { 517 | let v = inc_minor(Version::parse("0.7.2-rc.0").unwrap()); 518 | assert_eq!(v.to_string(), "0.8.0"); 519 | } 520 | 521 | #[test] 522 | fn test_inc_minor_on_preminor() { 523 | let v = inc_minor(Version::parse("0.7.0-rc.0").unwrap()); 524 | assert_eq!(v.to_string(), "0.7.0"); 525 | } 526 | 527 | #[test] 528 | fn test_inc_minor_on_premajor() { 529 | let v = inc_minor(Version::parse("1.0.0-rc.0").unwrap()); 530 | assert_eq!(v.to_string(), "1.0.0"); 531 | } 532 | 533 | #[test] 534 | fn test_inc_major() { 535 | let v = inc_major(Version::parse("0.7.2").unwrap()); 536 | assert_eq!(v.to_string(), "1.0.0"); 537 | } 538 | 539 | #[test] 540 | fn test_inc_major_on_prepatch() { 541 | let v = inc_major(Version::parse("0.7.2-rc.0").unwrap()); 542 | assert_eq!(v.to_string(), "1.0.0"); 543 | } 544 | 545 | #[test] 546 | fn test_inc_major_on_preminor() { 547 | let v = inc_major(Version::parse("0.7.0-rc.0").unwrap()); 548 | assert_eq!(v.to_string(), "1.0.0"); 549 | } 550 | 551 | #[test] 552 | fn test_inc_major_on_premajor_with_patch() { 553 | let v = inc_major(Version::parse("1.0.1-rc.0").unwrap()); 554 | assert_eq!(v.to_string(), "2.0.0"); 555 | } 556 | 557 | #[test] 558 | fn test_inc_major_on_premajor() { 559 | let v = inc_major(Version::parse("1.0.0-rc.0").unwrap()); 560 | assert_eq!(v.to_string(), "1.0.0"); 561 | } 562 | 563 | #[test] 564 | fn test_inc_preid() { 565 | let v = inc_preid( 566 | &Version::parse("3.0.0").unwrap(), 567 | Identifier::AlphaNumeric("beta".to_string()), 568 | ); 569 | assert_eq!(v.to_string(), "3.0.1-beta.0"); 570 | } 571 | 572 | #[test] 573 | fn test_inc_preid_on_alpha() { 574 | let v = inc_preid( 575 | &Version::parse("3.0.0-alpha.19").unwrap(), 576 | Identifier::AlphaNumeric("beta".to_string()), 577 | ); 578 | assert_eq!(v.to_string(), "3.0.0-beta.0"); 579 | } 580 | 581 | #[test] 582 | fn test_inc_preid_on_num() { 583 | let v = inc_preid( 584 | &Version::parse("3.0.0-11.19").unwrap(), 585 | Identifier::AlphaNumeric("beta".to_string()), 586 | ); 587 | assert_eq!(v.to_string(), "3.0.0-beta.0"); 588 | } 589 | 590 | #[test] 591 | fn test_custom_pre() { 592 | let v = custom_pre(&Version::parse("3.0.0").unwrap()); 593 | assert_eq!(v.0, Identifier::AlphaNumeric("alpha".to_string())); 594 | assert_eq!(v.1.to_string(), "3.0.1-alpha.0"); 595 | } 596 | 597 | #[test] 598 | fn test_custom_pre_on_single_alpha() { 599 | let v = custom_pre(&Version::parse("3.0.0-a").unwrap()); 600 | assert_eq!(v.0, Identifier::AlphaNumeric("a".to_string())); 601 | assert_eq!(v.1.to_string(), "3.0.0-a.0"); 602 | } 603 | 604 | #[test] 605 | fn test_custom_pre_on_single_alpha_with_second_num() { 606 | let v = custom_pre(&Version::parse("3.0.0-a.11").unwrap()); 607 | assert_eq!(v.0, Identifier::AlphaNumeric("a".to_string())); 608 | assert_eq!(v.1.to_string(), "3.0.0-a.12"); 609 | } 610 | 611 | #[test] 612 | fn test_custom_pre_on_second_alpha() { 613 | let v = custom_pre(&Version::parse("3.0.0-a.b").unwrap()); 614 | assert_eq!(v.0, Identifier::AlphaNumeric("a".to_string())); 615 | assert_eq!(v.1.to_string(), "3.0.0-a.0"); 616 | } 617 | 618 | #[test] 619 | fn test_custom_pre_on_second_alpha_with_num() { 620 | let v = custom_pre(&Version::parse("3.0.0-a.b.1").unwrap()); 621 | assert_eq!(v.0, Identifier::AlphaNumeric("a".to_string())); 622 | assert_eq!(v.1.to_string(), "3.0.0-a.0"); 623 | } 624 | 625 | #[test] 626 | fn test_custom_pre_on_single_num() { 627 | let v = custom_pre(&Version::parse("3.0.0-11").unwrap()); 628 | assert_eq!(v.0, Identifier::Numeric(11)); 629 | assert_eq!(v.1.to_string(), "3.0.0-12"); 630 | } 631 | 632 | #[test] 633 | fn test_custom_pre_on_single_num_with_second_alpha() { 634 | let v = custom_pre(&Version::parse("3.0.0-11.a").unwrap()); 635 | assert_eq!(v.0, Identifier::Numeric(11)); 636 | assert_eq!(v.1.to_string(), "3.0.0-12.a"); 637 | } 638 | 639 | #[test] 640 | fn test_custom_pre_on_second_num() { 641 | let v = custom_pre(&Version::parse("3.0.0-11.20").unwrap()); 642 | assert_eq!(v.0, Identifier::Numeric(11)); 643 | assert_eq!(v.1.to_string(), "3.0.0-11.21"); 644 | } 645 | 646 | #[test] 647 | fn test_custom_pre_on_multiple_num() { 648 | let v = custom_pre(&Version::parse("3.0.0-11.20.a.55.c").unwrap()); 649 | assert_eq!(v.0, Identifier::Numeric(11)); 650 | assert_eq!(v.1.to_string(), "3.0.0-11.20.a.56.c"); 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /cargo-workspaces/src/version.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{info, Result, VersionOpt}; 2 | use cargo_metadata::Metadata; 3 | use clap::Parser; 4 | 5 | /// Bump version of crates 6 | #[derive(Debug, Parser)] 7 | pub struct Version { 8 | #[clap(flatten)] 9 | version: VersionOpt, 10 | } 11 | 12 | impl Version { 13 | pub fn run(self, metadata: Metadata) -> Result { 14 | self.version.do_versioning(&metadata)?; 15 | 16 | info!("success", "ok"); 17 | Ok(()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/create.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | use insta::assert_snapshot; 3 | use serial_test::serial; 4 | use std::{ 5 | fs::{read_to_string, remove_dir, remove_dir_all, remove_file, write}, 6 | path::Path, 7 | }; 8 | 9 | /// Clean up a cargo or cargo-workspace created package directory 10 | /// This doesn't use remove_dir_all, so it's safer, but more liable to break 11 | /// if cargo new is updated in the future 12 | /// package_dir is the directory containing the package 13 | /// package_type is bin or lib 14 | fn clean_package_dir(package_path: &Path, package_type: &str) { 15 | assert_ne!(package_path.as_os_str(), ""); 16 | assert_ne!(package_path.as_os_str(), "/"); 17 | 18 | let exists = package_path.exists(); 19 | if !exists { 20 | return; 21 | } 22 | 23 | let cargo_path = package_path.join("Cargo.toml"); 24 | let exists = cargo_path.exists(); 25 | if exists { 26 | remove_file(cargo_path).unwrap(); 27 | } 28 | 29 | let src_path = match package_type { 30 | "bin" => package_path.join("src").join("main.rs"), 31 | "lib" => package_path.join("src").join("lib.rs"), 32 | _ => { 33 | return; 34 | } 35 | }; 36 | 37 | let exists = src_path.exists(); 38 | if exists { 39 | remove_file(src_path).unwrap(); 40 | } 41 | 42 | let src_path = package_path.join("src"); 43 | let exists = src_path.exists(); 44 | if exists { 45 | remove_dir(src_path).unwrap(); 46 | } 47 | 48 | let git_path = package_path.join(".git"); 49 | let gitignore_path = package_path.join(".gitignore"); 50 | let exists = git_path.exists(); 51 | if exists { 52 | remove_dir_all(git_path).unwrap(); 53 | remove_file(gitignore_path).unwrap(); 54 | } 55 | 56 | let exists = package_path.exists(); 57 | if exists { 58 | remove_dir(package_path).unwrap(); 59 | } 60 | } 61 | 62 | /// Test creating a 2015 bin package 63 | #[test] 64 | #[serial] 65 | fn test_create_bin_2015() { 66 | let package_name = "mynewcrate-bin-2015"; 67 | let dir = "../fixtures/create"; 68 | let package_path = Path::new(dir).join(package_name); 69 | let workspace_manifest_path = Path::new(dir).join("Cargo.toml"); 70 | let manifest_path = package_path.join("Cargo.toml"); 71 | 72 | let backup = read_to_string(&workspace_manifest_path).unwrap(); 73 | clean_package_dir(&package_path, "bin"); 74 | 75 | let _err = utils::run_err( 76 | dir, 77 | &[ 78 | "ws", 79 | "create", 80 | package_name, 81 | "--edition", 82 | "2015", 83 | "--bin", 84 | "--name", 85 | package_name, 86 | ], 87 | ); 88 | 89 | let manifest = read_to_string(manifest_path).unwrap(); 90 | let workspace_manifest = read_to_string(&workspace_manifest_path).unwrap(); 91 | 92 | assert_snapshot!(&manifest); 93 | assert_snapshot!(&workspace_manifest); 94 | 95 | clean_package_dir(&package_path, "bin"); 96 | write(workspace_manifest_path, backup).unwrap(); 97 | } 98 | 99 | /// Test creating a 2015 lib package 100 | #[test] 101 | #[serial] 102 | fn test_create_lib_2015() { 103 | let package_name = "mynewcrate-lib-2015"; 104 | let dir = "../fixtures/create"; 105 | let package_path = Path::new(dir).join(package_name); 106 | let manifest_path = package_path.join("Cargo.toml"); 107 | 108 | let backup = read_to_string(Path::new(dir).join("Cargo.toml")).unwrap(); 109 | clean_package_dir(&package_path, "lib"); 110 | 111 | let _err = utils::run_err( 112 | dir, 113 | &[ 114 | "ws", 115 | "create", 116 | package_name, 117 | "--edition", 118 | "2015", 119 | "--lib", 120 | "--name", 121 | package_name, 122 | ], 123 | ); 124 | 125 | let manifest = read_to_string(manifest_path).unwrap(); 126 | 127 | assert_snapshot!(&manifest); 128 | 129 | clean_package_dir(&package_path, "lib"); 130 | write(Path::new(dir).join("Cargo.toml"), backup).unwrap(); 131 | } 132 | 133 | /// Test creating a 2018 bin package 134 | #[test] 135 | #[serial] 136 | fn test_create_bin_2018() { 137 | let package_name = "mynewcrate-bin-2018"; 138 | let dir = "../fixtures/create"; 139 | let package_path = Path::new(dir).join(package_name); 140 | let manifest_path = package_path.join("Cargo.toml"); 141 | 142 | let backup = read_to_string(Path::new(dir).join("Cargo.toml")).unwrap(); 143 | clean_package_dir(&package_path, "bin"); 144 | 145 | let _err = utils::run_err( 146 | dir, 147 | &[ 148 | "ws", 149 | "create", 150 | package_name, 151 | "--edition", 152 | "2018", 153 | "--bin", 154 | "--name", 155 | package_name, 156 | ], 157 | ); 158 | 159 | let manifest = read_to_string(manifest_path).unwrap(); 160 | 161 | assert_snapshot!(&manifest); 162 | 163 | clean_package_dir(&package_path, "bin"); 164 | write(Path::new(dir).join("Cargo.toml"), backup).unwrap(); 165 | } 166 | 167 | /// Test creating a 2018 lib package 168 | #[test] 169 | #[serial] 170 | fn test_create_lib_2018() { 171 | let package_name = "mynewcrate-lib-2018"; 172 | let dir = "../fixtures/create"; 173 | let package_path = Path::new(dir).join(package_name); 174 | let manifest_path = package_path.join("Cargo.toml"); 175 | 176 | let backup = read_to_string(Path::new(dir).join("Cargo.toml")).unwrap(); 177 | clean_package_dir(&package_path, "lib"); 178 | 179 | let _err = utils::run_err( 180 | dir, 181 | &[ 182 | "ws", 183 | "create", 184 | package_name, 185 | "--edition", 186 | "2018", 187 | "--lib", 188 | "--name", 189 | package_name, 190 | ], 191 | ); 192 | 193 | let manifest = read_to_string(manifest_path).unwrap(); 194 | 195 | assert_snapshot!(&manifest); 196 | 197 | clean_package_dir(&package_path, "lib"); 198 | write(Path::new(dir).join("Cargo.toml"), backup).unwrap(); 199 | } 200 | 201 | /// Test that you can't create a library and binary package at the same time 202 | #[test] 203 | #[serial] 204 | fn test_create_lib_and_bin_fails() { 205 | let package_name = "mynewcrate-lib-and-bin-2018"; 206 | let dir = "../fixtures/create"; 207 | let package_path = Path::new(dir).join(package_name); 208 | 209 | clean_package_dir(&package_path, "lib"); 210 | clean_package_dir(&package_path, "bin"); 211 | 212 | let err = utils::run_err( 213 | dir, 214 | &[ 215 | "ws", 216 | "create", 217 | package_name, 218 | "--edition", 219 | "2018", 220 | "--lib", 221 | "--bin", 222 | "--name", 223 | package_name, 224 | ], 225 | ); 226 | 227 | assert_snapshot!(err); 228 | 229 | let exists = package_path.exists(); 230 | assert!(!exists); 231 | 232 | clean_package_dir(&package_path, "lib"); 233 | clean_package_dir(&package_path, "bin"); 234 | } 235 | 236 | #[test] 237 | #[serial] 238 | fn test_member_glob() { 239 | let package_name = "dep3"; 240 | let dir = "../fixtures/create"; 241 | let package_path = Path::new(dir).join(package_name); 242 | let workspace_manifest_path = Path::new(dir).join("Cargo.toml"); 243 | let manifest_path = package_path.join("Cargo.toml"); 244 | 245 | let backup = read_to_string(&workspace_manifest_path).unwrap(); 246 | clean_package_dir(&package_path, "lib"); 247 | 248 | let _err = utils::run_err( 249 | dir, 250 | &[ 251 | "ws", 252 | "create", 253 | package_name, 254 | "--edition", 255 | "2018", 256 | "--lib", 257 | "--name", 258 | package_name, 259 | ], 260 | ); 261 | 262 | let manifest = read_to_string(manifest_path).unwrap(); 263 | let workspace_manifest = read_to_string(&workspace_manifest_path).unwrap(); 264 | 265 | assert_snapshot!(&manifest); 266 | assert_snapshot!(&workspace_manifest); 267 | 268 | clean_package_dir(&package_path, "lib"); 269 | write(workspace_manifest_path, backup).unwrap(); 270 | } 271 | 272 | #[test] 273 | #[serial] 274 | fn test_exclude_fails() { 275 | let package_name = "tmp2"; 276 | let dir = "../fixtures/create"; 277 | let package_path = Path::new(dir).join(package_name); 278 | 279 | clean_package_dir(&package_path, "lib"); 280 | 281 | let err = utils::run_err( 282 | dir, 283 | &[ 284 | "ws", 285 | "create", 286 | package_name, 287 | "--edition", 288 | "2018", 289 | "--lib", 290 | "--name", 291 | package_name, 292 | ], 293 | ); 294 | 295 | assert_snapshot!(err); 296 | 297 | let exists = package_path.exists(); 298 | assert!(!exists); 299 | 300 | clean_package_dir(&package_path, "lib"); 301 | } 302 | 303 | #[test] 304 | #[serial] 305 | fn test_already_exists() { 306 | let package_name = "dep1"; 307 | let dir = "../fixtures/create"; 308 | 309 | let err = utils::run_err( 310 | dir, 311 | &[ 312 | "ws", 313 | "create", 314 | package_name, 315 | "--edition", 316 | "2018", 317 | "--lib", 318 | "--name", 319 | package_name, 320 | ], 321 | ); 322 | 323 | assert_snapshot!(err); 324 | } 325 | 326 | #[test] 327 | #[serial] 328 | fn test_duplicate_package_name() { 329 | let package_name = "dep1"; 330 | let dir = "../fixtures/create"; 331 | 332 | let err = utils::run_err( 333 | dir, 334 | &[ 335 | "ws", 336 | "create", 337 | "dep3", 338 | "--edition", 339 | "2018", 340 | "--lib", 341 | "--name", 342 | package_name, 343 | ], 344 | ); 345 | 346 | assert!(err.contains("error: the workspace already contains a package with this name")); 347 | 348 | let exists = Path::new(dir).join("dep3").exists(); 349 | assert!(!exists); 350 | } 351 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/exec.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | #[cfg(not(windows))] 3 | use insta::assert_snapshot; 4 | 5 | // #[cfg(windows)] 6 | // static PRINT: &str = "cd"; 7 | 8 | #[cfg(not(windows))] 9 | static PRINT: &str = "cat"; 10 | 11 | // TODO: Get exec test working on windows 12 | #[cfg(not(windows))] 13 | #[test] 14 | fn test_normal() { 15 | let (out, err) = utils::run("../fixtures/normal", &["ws", "exec", PRINT, "Cargo.toml"]); 16 | assert_snapshot!(err); 17 | assert_snapshot!(out); 18 | } 19 | 20 | // TODO: Get exec test working on windows 21 | #[cfg(not(windows))] 22 | #[test] 23 | fn test_normal_ignore() { 24 | let (out, err) = utils::run( 25 | "../fixtures/normal", 26 | &["ws", "exec", "--ignore={dep2,top}", PRINT, "Cargo.toml"], 27 | ); 28 | assert_snapshot!(err); 29 | assert_snapshot!(out); 30 | } 31 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/init.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | use insta::assert_snapshot; 3 | use serial_test::serial; 4 | use std::fs::{read_to_string, rename, write}; 5 | 6 | #[test] 7 | fn test_no_path() { 8 | let err = utils::run_err("../fixtures/single", &["ws", "init", "new"]); 9 | assert_snapshot!(err); 10 | } 11 | 12 | #[test] 13 | #[serial] 14 | fn test_normal() { 15 | let manifest = "../fixtures/normal/Cargo.toml"; 16 | let backup = "../fixtures/normal/Cargo.toml.bak"; 17 | 18 | // Rename Cargo.toml 19 | rename(manifest, backup).unwrap(); 20 | 21 | let err = utils::run_err("../fixtures/normal", &["ws", "init"]); 22 | assert_snapshot!(err); 23 | 24 | let data = read_to_string(manifest).unwrap(); 25 | assert_snapshot!(data); 26 | 27 | // Rename Cargo.toml 28 | rename(backup, manifest).unwrap(); 29 | } 30 | 31 | #[test] 32 | #[serial] 33 | fn test_normal_with_manifest() { 34 | let manifest = "../fixtures/normal/Cargo.toml"; 35 | let content = read_to_string(manifest).unwrap(); 36 | 37 | let err = utils::run_err("../fixtures/normal", &["ws", "init"]); 38 | assert_snapshot!(err); 39 | 40 | let data = read_to_string(manifest).unwrap(); 41 | assert_snapshot!(data); 42 | 43 | // Restore Cargo.toml 44 | write(manifest, content).unwrap(); 45 | } 46 | 47 | #[test] 48 | #[serial] 49 | fn test_root() { 50 | let manifest = "../fixtures/root/Cargo.toml"; 51 | let backup = "../fixtures/root/Cargo.toml.bak"; 52 | 53 | // Rename Cargo.toml 54 | rename(manifest, backup).unwrap(); 55 | 56 | let err = utils::run_err("../fixtures/root", &["ws", "init"]); 57 | assert_snapshot!(err); 58 | 59 | let data = read_to_string(manifest).unwrap(); 60 | assert_snapshot!(data); 61 | 62 | // Rename Cargo.toml 63 | rename(backup, manifest).unwrap(); 64 | } 65 | 66 | #[test] 67 | #[serial] 68 | fn test_root_with_manifest() { 69 | let manifest = "../fixtures/root/Cargo.toml"; 70 | let content = read_to_string(manifest).unwrap(); 71 | 72 | let err = utils::run_err("../fixtures/root", &["ws", "init"]); 73 | assert_snapshot!(err); 74 | 75 | let data = read_to_string(manifest).unwrap(); 76 | assert_snapshot!(data); 77 | 78 | // Restore Cargo.toml 79 | write(manifest, content).unwrap(); 80 | } 81 | 82 | #[test] 83 | #[serial] 84 | fn test_root_with_manifest_no_workspace() { 85 | let manifest = "../fixtures/normal/Cargo.toml"; 86 | let backup = "../fixtures/normal/Cargo.toml.bak"; 87 | let root_manifest = "../fixtures/root/Cargo.toml"; 88 | 89 | // Rename Cargo.toml 90 | rename(manifest, backup).unwrap(); 91 | write(manifest, read_to_string(root_manifest).unwrap()).unwrap(); 92 | 93 | let err = utils::run_err("../fixtures/normal", &["ws", "init"]); 94 | assert_snapshot!(err); 95 | 96 | let data = read_to_string(manifest).unwrap(); 97 | assert_snapshot!(data); 98 | 99 | // Rename Cargo.toml 100 | rename(backup, manifest).unwrap(); 101 | } 102 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/list.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | use insta::assert_snapshot; 3 | 4 | #[test] 5 | fn test_single() { 6 | let out = utils::run_out("../fixtures/single", &["ws", "ls"]); 7 | assert_snapshot!(out); 8 | } 9 | 10 | #[test] 11 | fn test_long() { 12 | let out = utils::run_out("../fixtures/single", &["ws", "ll"]); 13 | assert_snapshot!(out); 14 | } 15 | 16 | #[test] 17 | fn test_long_root() { 18 | let out = utils::run_out("../fixtures/root", &["ws", "ll"]); 19 | assert_snapshot!(out); 20 | } 21 | 22 | #[test] 23 | fn test_all() { 24 | let out = utils::run_out("../fixtures/private", &["ws", "la"]); 25 | assert_snapshot!(out); 26 | } 27 | 28 | #[test] 29 | fn test_long_all() { 30 | let out = utils::run_out("../fixtures/private", &["ws", "list", "--long", "--all"]); 31 | assert_snapshot!(out); 32 | } 33 | 34 | #[test] 35 | fn test_json() { 36 | let out = utils::run_out("../fixtures/private", &["ws", "list", "--json"]); 37 | 38 | assert!(out.contains(r#""name": "simple""#)); 39 | assert!(out.contains(r#""version": "0.1.0-rc.0""#)); 40 | assert!(out.contains(r#""private": false"#)); 41 | 42 | assert!(!out.contains(r#""name": "private""#)); 43 | assert!(!out.contains(r#""version": "0.2.0""#)); 44 | assert!(!out.contains(r#""private": true"#)); 45 | } 46 | 47 | #[test] 48 | fn test_json_all() { 49 | let out = utils::run_out("../fixtures/private", &["ws", "list", "--json", "--all"]); 50 | 51 | assert!(out.contains(r#""name": "simple""#)); 52 | assert!(out.contains(r#""version": "0.1.0-rc.0""#)); 53 | assert!(out.contains(r#""private": false"#)); 54 | 55 | assert!(out.contains(r#""name": "private""#)); 56 | assert!(out.contains(r#""version": "0.2.0""#)); 57 | assert!(out.contains(r#""private": true"#)); 58 | } 59 | 60 | #[test] 61 | fn test_json_conflicts_with_long() { 62 | let err = utils::run_err("../fixtures/private", &["ws", "list", "--long", "--json"]); 63 | assert_snapshot!(err); 64 | } 65 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__already_exists.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | assertion_line: 269 4 | expression: err 5 | --- 6 | error: path already exists 7 | 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__create_bin_2015-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | assertion_line: 93 4 | expression: "&workspace_manifest" 5 | --- 6 | [workspace] 7 | members = [ 8 | "dep*", 9 | "mynewcrate-bin-2015", 10 | ] 11 | exclude = [ 12 | "tmp*", 13 | ] 14 | 15 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__create_bin_2015.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | assertion_line: 87 4 | expression: "&manifest" 5 | --- 6 | [package] 7 | name = "mynewcrate-bin-2015" 8 | version = "0.0.0" 9 | edition = "2015" 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__create_bin_2018.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | assertion_line: 149 4 | expression: "&manifest" 5 | --- 6 | [package] 7 | name = "mynewcrate-bin-2018" 8 | version = "0.0.0" 9 | edition = "2018" 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__create_lib_2015.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | expression: "&manifest" 4 | 5 | --- 6 | [package] 7 | name = "mynewcrate-lib-2015" 8 | version = "0.0.0" 9 | edition = "2015" 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__create_lib_2018.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | expression: "&manifest" 4 | 5 | --- 6 | [package] 7 | name = "mynewcrate-lib-2018" 8 | version = "0.0.0" 9 | edition = "2018" 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__create_lib_and_bin_fails.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | assertion_line: 210 4 | expression: err 5 | --- 6 | error: The argument '--lib' cannot be used with '--bin' 7 | 8 | USAGE: 9 | cargo workspaces create --edition --lib --name 10 | 11 | For more information try --help 12 | 13 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__exclude_fails.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | assertion_line: 241 4 | expression: err 5 | --- 6 | error: path for crate is in workspace.exclude list (tmp*) 7 | 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__member_glob-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | assertion_line: 266 4 | expression: "&workspace_manifest" 5 | --- 6 | [workspace] 7 | members = [ 8 | "dep*", 9 | ] 10 | exclude = [ 11 | "tmp*", 12 | ] 13 | 14 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/create__member_glob.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/create.rs 3 | assertion_line: 265 4 | expression: "&manifest" 5 | --- 6 | [package] 7 | name = "dep3" 8 | version = "0.0.0" 9 | edition = "2018" 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/exec__normal-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/exec.rs 3 | expression: out 4 | --- 5 | [package] 6 | name = "dep1" 7 | version = "0.1.0" 8 | authors = ["Pavan Kumar Sunkara "] 9 | edition = "2018" 10 | 11 | [dependencies] 12 | [package] 13 | name = "dep2" 14 | version = "0.1.0" 15 | authors = ["Pavan Kumar Sunkara "] 16 | edition = "2018" 17 | 18 | [dependencies] 19 | pre_dep1 = { version = "0.1.0", path = "../dep1", package = "dep1" } 20 | [package] 21 | name = "top" 22 | version = "0.1.0" 23 | authors = ["Pavan Kumar Sunkara "] 24 | edition = "2018" 25 | 26 | [dependencies] 27 | dep = { version = "0.1.0", path = "../dep1", package = "dep1" } 28 | dep2 = { version = "0.1.0", path = "../dep2" } 29 | 30 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/exec__normal.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/exec.rs 3 | expression: err 4 | --- 5 | info success ok 6 | 7 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/exec__normal_ignore-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/exec.rs 3 | assertion_line: 29 4 | expression: out 5 | 6 | --- 7 | [package] 8 | name = "dep1" 9 | version = "0.1.0" 10 | authors = ["Pavan Kumar Sunkara "] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | 15 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/exec__normal_ignore.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/exec.rs 3 | assertion_line: 28 4 | expression: err 5 | 6 | --- 7 | info success ok 8 | 9 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__no_path.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | expression: err 4 | 5 | --- 6 | error: given path new is not a folder 7 | 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__normal-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | expression: manifest 4 | --- 5 | [workspace] 6 | members = [ 7 | "dep1", 8 | "dep2", 9 | "top", 10 | ] 11 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__normal.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 26 4 | expression: err 5 | --- 6 | info crates dep1, dep2, top 7 | info initialized . 8 | 9 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__normal_with_manifest-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 41 4 | expression: data 5 | --- 6 | [workspace] 7 | members = [ 8 | "top", 9 | "dep1", 10 | "dep2", 11 | ] 12 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__normal_with_manifest.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 35 4 | expression: err 5 | --- 6 | info crates 7 | info initialized . 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__root-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 56 4 | expression: data 5 | --- 6 | [workspace] 7 | members = [] 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__root.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 53 4 | expression: err 5 | --- 6 | info crates 7 | info initialized . 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__root_with_manifest-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 76 4 | expression: data 5 | --- 6 | [package] 7 | name = "root" 8 | version = "0.1.0" 9 | authors = ["Pavan Kumar Sunkara "] 10 | edition = "2018" 11 | 12 | [[bin]] 13 | name = "root" 14 | path = "src/main.rs" 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | 20 | [workspace] 21 | members = [ 22 | "", 23 | ] 24 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__root_with_manifest.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 68 4 | expression: err 5 | --- 6 | info crates 7 | info initialized . 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__root_with_manifest_no_workspace-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 97 4 | expression: data 5 | --- 6 | [package] 7 | name = "root" 8 | version = "0.1.0" 9 | authors = ["Pavan Kumar Sunkara "] 10 | edition = "2018" 11 | 12 | [[bin]] 13 | name = "root" 14 | path = "src/main.rs" 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | 20 | [workspace] 21 | members = [ 22 | "", 23 | "dep1", 24 | "dep2", 25 | "top", 26 | ] 27 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/init__root_with_manifest_no_workspace.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/init.rs 3 | assertion_line: 94 4 | expression: err 5 | --- 6 | info crates , dep1, dep2, top 7 | info initialized . 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/list__all.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/list.rs 3 | expression: out 4 | --- 5 | private (PRIVATE) 6 | simple 7 | 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/list__json_conflicts_with_long.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/list.rs 3 | assertion_line: 63 4 | expression: err 5 | 6 | --- 7 | error: The argument '--long' cannot be used with '--json' 8 | 9 | USAGE: 10 | cargo workspaces list --long 11 | 12 | For more information try --help 13 | 14 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/list__long.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/list.rs 3 | expression: out 4 | --- 5 | simple v0.1.0 simple 6 | 7 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/list__long_all.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/list.rs 3 | expression: out 4 | --- 5 | private v0.2.0 private (PRIVATE) 6 | simple v0.1.0-rc.0 simple 7 | 8 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/list__long_root.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/list.rs 3 | expression: out 4 | --- 5 | root v0.1.0 . 6 | 7 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/snapshots/list__single.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/list.rs 3 | expression: out 4 | --- 5 | simple 6 | 7 | -------------------------------------------------------------------------------- /cargo-workspaces/tests/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use assert_cmd::Command; 3 | use std::str::from_utf8; 4 | 5 | pub fn run(dir: &str, args: &[&str]) -> (String, String) { 6 | let output = Command::cargo_bin("cargo-ws") 7 | .unwrap() 8 | .current_dir(dir) 9 | .args(args) 10 | .output() 11 | .unwrap(); 12 | 13 | let out = from_utf8(&output.stdout).unwrap(); 14 | let err = from_utf8(&output.stderr).unwrap(); 15 | 16 | (out.to_string(), err.to_string()) 17 | } 18 | 19 | pub fn run_out(dir: &str, args: &[&str]) -> String { 20 | let (out, err) = run(dir, args); 21 | 22 | assert!(err.is_empty()); 23 | out 24 | } 25 | 26 | pub fn run_err(dir: &str, args: &[&str]) -> String { 27 | let (out, err) = run(dir, args); 28 | 29 | assert!(out.is_empty()); 30 | err 31 | } 32 | -------------------------------------------------------------------------------- /fixtures/create/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "dep*", 4 | ] 5 | exclude = [ 6 | "tmp*", 7 | ] 8 | -------------------------------------------------------------------------------- /fixtures/create/dep1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dep1" 3 | version = "0.1.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /fixtures/create/dep1/src/lib.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pksunkara/cargo-workspaces/7e9d493cb991b8e7a714df4e33afd0a56d09b87c/fixtures/create/dep1/src/lib.rs -------------------------------------------------------------------------------- /fixtures/create/dep2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dep2" 3 | version = "0.1.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /fixtures/create/dep2/src/lib.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pksunkara/cargo-workspaces/7e9d493cb991b8e7a714df4e33afd0a56d09b87c/fixtures/create/dep2/src/lib.rs -------------------------------------------------------------------------------- /fixtures/normal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "top", 4 | "dep1", 5 | "dep2", 6 | ] 7 | -------------------------------------------------------------------------------- /fixtures/normal/dep1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dep1" 3 | version = "0.1.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /fixtures/normal/dep1/src/lib.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pksunkara/cargo-workspaces/7e9d493cb991b8e7a714df4e33afd0a56d09b87c/fixtures/normal/dep1/src/lib.rs -------------------------------------------------------------------------------- /fixtures/normal/dep2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dep2" 3 | version = "0.1.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | pre_dep1 = { version = "0.1.0", path = "../dep1", package = "dep1" } 9 | -------------------------------------------------------------------------------- /fixtures/normal/dep2/src/lib.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pksunkara/cargo-workspaces/7e9d493cb991b8e7a714df4e33afd0a56d09b87c/fixtures/normal/dep2/src/lib.rs -------------------------------------------------------------------------------- /fixtures/normal/top/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "top" 3 | version = "0.1.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | dep = { version = "0.1.0", path = "../dep1", package = "dep1" } 9 | dep2 = { version = "0.1.0", path = "../dep2" } 10 | -------------------------------------------------------------------------------- /fixtures/normal/top/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/normal_git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /fixtures/normal_git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /fixtures/private/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "private", 4 | "simple", 5 | ] 6 | -------------------------------------------------------------------------------- /fixtures/private/private/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "private" 3 | version = "0.2.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | publish = false 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /fixtures/private/private/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fixtures/private/simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple" 3 | version = "0.1.0-rc.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /fixtures/private/simple/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/root/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "root" 3 | version = "0.1.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | 7 | [[bin]] 8 | name = "root" 9 | path = "src/main.rs" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | -------------------------------------------------------------------------------- /fixtures/root/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/single/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "simple" 4 | ] 5 | -------------------------------------------------------------------------------- /fixtures/single/simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple" 3 | version = "0.1.0" 4 | authors = ["Pavan Kumar Sunkara "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /fixtures/single/simple/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | --------------------------------------------------------------------------------