Feedback on `cargo-upgrade` to prepare it for merging

I'd call it --only-incompatible. However, I struggle to see a strong cases for keeping a mix of latest and maybe-not-latest dependencies, so maybe this switch could just not exist?

If I want to have a dependency at not-absolutely-latest it's because of MSRV or for keeping patch version at .0 or one less than latest, in case the very latest version gets yanked. But when a previously-incompatible dependency is upgraded to max, that's not following these rules, and it's just arbitrary.

1 Like

I do use the current --skip-compatible, and would use --only-incompatible, for one specific reason: it helps reduce the amount of change that happens at once. I like to do cargo upgrade --skip-compatible, make any fixes necessary for that to compile, test, commit, then cargo upgrade, test, commit.

2 Likes

If I understood, you would prefer that --compatible=true --incompatible=true be hard coded?

We have several use cases (including the one Josh mentioned)

  • Some library authors want to keep up on breaking changes but otherwise want to keep version requirements low to allow dependents to choose the version right for them (lower audit churn with fewer upgrades, workaround bugs, MSRV, etc)
  • Frequently, users will want to defer certain breaking changes because of the level of churn or missing functionality
  • Some maintainers would like to have isolated PRs for semver-compatible and semver-breaking upgrades as semver-breaking upgrades deserve a bit more scrutiny (is the crate exposed in the API making the upgrade a breaking release? were there behavior changes that might not have been caught by tests?)

With these in mind, our options to go down this route are

  • Decide the tool should intentionally exclude these workflows
  • Enumerate workarounds and weigh out the cost of those workarounds (number of users and level of work) against designing in the features

What I will note here is that this use case will still require manual intervention for the newly upgraded dependencies. Namely, a dependency will be upgraded e.g. from 1.0.0 to 2.1.8, whereas such a developer would prefer to upgrade to 2.1.0 or even 2.0.0.

Upgrading to the latest dependency version but also not using the latest version are competing desires. The goal of cargo update is to use the most recent compatible version; I think it completely fair that cargo update --allow-breaking (or whatever it ends up called) should take the same default stance, and always update (all dependencies) to the latest version.

This imho provides a very clear modality:

command result
update update lockfile versions to latest compatible
update --save update, additionally updating versions in the manifest
update --allow-breaking update without the restriction to compatible releases; errors if incompatible updates are available, as that requires changing the manifest
update --allow-breaking --save update, including incompatible versions, saving to the manifest

A update --only-breaking is then a reasonable addition, and that flag could reasonably be interpreted to update purely only the breaking release, resulting in the selection of 2.0.0, as any further updating would not be a breaking update.

(Talking about opt-in to "breaking" releases might also be preferable to "compatible" and "incompatible" anyway, since the concept of "breaking changes" is verbage used in Rust messaging already whereas "incompatible" is used less. It also avoids lessening the bias towards compatible releases being compatible and thus considered desirable by default. Not being eager about pushing people to use the latest compatible version weakens the claim that the versions are compatible as well as the pressure to actually keep them compatible, if updating is made more opt-in.)

4 Likes

Something I was just think is that the name implies nothing else will be upgraded. So a user seeing cargo upgrade --only-incompatible might think it is doing a cargo upgrade --compatible false --incompatible true --pinned false

Thanks for calling this out as I forgot to document it. I realized as I was writing the --help output that breaking / non-breaking would imply different semantics than we have today. --compatible / --incompatible focuses on whether it is compatible with the current version requirement or not while "breaking" focuses purely on semver regardless of the version requirement.

If we shift the focus to "breaking", we'll still need to deal with compatibility for the default case, so we'll be mixing concepts (which can be fine). This also makes me realize that "pinned" dependencies have several potential modes (compatible, incompatible but non-breaking, incompatible and breaking) that aren't quite covered. Granted, I'm not too worried about the "pinned" workflow since my main care abouts are (1) not touching them, (2) reporting them to the user to care about. --pinned existing is more of an escape hatch for the any users concerned that cargo-upgrade "isn't doing upgrades". Depending on the modes we support, I can see having this start life as unstable.

Is there a reason your examples are focusing on the "merge into cargo-update" route instead of "cargo-upgrade today" or "deprecating cargo-update, merging cargo-upgrade" (same thingf from two different perspectives)? Calling this out mostly to check if there is any unsaid or lost-track-of feedback to consider.

If we go this route, what if we tweak this so that it is --breaking allow and --breaking only with the possibility of --breaking (no value) implying --breaking allow? This consolidates the modes into one flag to look at with the potential for a short-hand.

Also, unsure if its just me but there is something about allow, skip, etc flag prefixes that feels out of place and overly long.

Something else to consider if we want to natively support going that low is if we should also reflect this in cargo-add somehow and how to make the intent of it clear and consistent in both.

I though that's what it was supposed to do? (if you upgrade and exclude deps that are "compatible" that leaves only upgrades of deps that aren't "compatible")

Having different meanings of "incompatible" and "not compatible" is really confusing, way more than update vs upgrade terminology.

1 Like

Another way to frame it is

  • --compatible [true|false] is --compatible [allow|ignore]
  • --incompatible [true|false] is --incompatible [allow|ignore]

EDIT: Changing the values away from true/false seem like they'd mitigate the confusion, enough for it to work?

But in English "incompatible" is the opposite of "compatible", so I don't understand what is the third behavior implied living somewhere in between that is neither compatible nor not-incompatible. Why "compatible = negation" means something else than the dictionary negation of the word "compatible"?

It's like:

cargo --wet=not # doesn't mean dry?
cargo --dry # it's not not-wet?
2 Likes

If we change it to cargo upgrade --compatible ignore --incompatible allow, it shifts the language away from direct negation so it is no longer interpretable as opposites.

@epage I think using options that take arguments is more complex than arguments that just turn on/off a boolean. --incompatible feels simpler than --incompatible allow, and --only-incompatible feels simpler than --incompatible --no-compatible. Within reason, I'd rather feel like I have three different modes (no options, --incompatible, and --only-incompatible) than feel like I have two sets of two modes where one combination makes no sense (compatible yes/no, incompatible yes/no, where no/no doesn't make sense).

I realize that from a "function parameters" perspective, passing a set of boolean parameters feels the most orthogonal. But in a command-line tool, brevity feels like an important virtue, and the modes people operate in the most often should be easy to invoke.

2 Likes

I'm going to sit on this for now because

  • I would rather not break things so soon if we decide to experiment in this direction
  • I would like to give myself time to mull this over to try to remove any knee jerk reaction bias.

Something we should remember is we are talking about a subset of functionality and in dramatically changing the workflows for --compatible and --incompatible, we also need to keep in mind --pinned.

With all that said, in the short term I have released a new version that changes true to allow and false to ignore, using aliases to maintain compatibility to address the immediate confusion over --compatible false sounding like --incompatible (there is a reddit thread running in parallel top this conversation).

I tried cargo upgrade from cargo-edit 0.11 for the first time today, and it apparently can't do the thing that I want to do (upgrade incompatible versions only). It additionally spews way too much output (although there's a summary at the end which is more useful, if still overly verbose for my taste).

upgrade incompatible versions only

How come cargo upgrade --compatible ignore -i didn't work?

It additionally spews way too much output (although there's a summary at the end which is more useful, if still overly verbose for my taste).

Could you open an issue with more details for us to discuss this?

I didn't find it, because cargo upgrade --help does not mention it:

VERSION:
        --compatible [<true|false>...]      Upgrade to latest compatible version [default: true]
    -i, --incompatible [<true|false>...]    Upgrade to latest incompatible version [default: false]
        --pinned [<true|false>...]          Upgrade pinned to latest incompatible version [default: false]

(It only mentions true and false.)

--save lacks semantics for me, at least I don't have an intuitive understanding of what it's supposed to do. This mentions that --pinned updates unpinned version requirements, which is unclear.

I feel like a lot of the proposed interface is unclear. I think one reason is that there's a clear distinction between updating requirements (which are implicitly the only way to upgrade across incompatible versions) and updating the actual dependencies used (what cargo update means today).

1 Like

Ah, right. In the latest release, I've renamed true to allow and false to ignore (with alias fro compatibility) due to the amount of confusion over the meaning of true and false.

That was a copy/paste error that has been fixed.

I think that confusion will continue to exist, regardless of the values passed to the key-value options.

I finally used the new cargo-upgrade today so have feedback on the mismatch with how it fits my workflow for libraries compared to the old design.

In general for libraries I try to minimize the amount of compatible dependency updates I do. Since it won't affect my downstream users anyway continuously updating them just causes extra maintenance burden. The two main reasons I will do an update are when I need a new feature, or when there is a new incompatible version and I get round to updating. When I do do an upgrade I always update to the latest version, (except rare cases where I need to keep a compatible version because it's a public dependency or even more rarely when I need a new feature but am not ready for an incompatible upgrade), I also want to minimize churn in the Cargo.lock when doing the update.

With the old design (0.8.0 IIRC, can't test it because it segfaults inside libgit2) this was a simple cargo upgrade -p foo. That would update only that dependency and rely on cargo's standard "satisfy incompatible versions" to update transitive dependencies in Cargo.lock. It would also default to doing an incompatible update.

With the new design (0.11.5) the equivalent seems to be cargo upgrade --incompatible=allow --recursive=false -p foo. Sometimes I won't need the --incompatible=allow, but I'd estimate incompatible upgrades are likely the majority of the upgrades I do.

6 Likes