]>
git.proxmox.com Git - rustc.git/blob - src/tools/cargo/crates/xtask-bump-check/src/xtask.rs
6 //! xtask-bump-check --base-rev <REV> --head-rev <REV>
9 //! Checks if there is any member got changed since a base commit
10 //! but forgot to bump its version.
13 use std
::collections
::HashMap
;
18 use cargo
::core
::dependency
::Dependency
;
19 use cargo
::core
::registry
::PackageRegistry
;
20 use cargo
::core
::Package
;
21 use cargo
::core
::Registry
;
22 use cargo
::core
::SourceId
;
23 use cargo
::core
::Workspace
;
24 use cargo
::sources
::source
::QueryKind
;
25 use cargo
::util
::command_prelude
::*;
26 use cargo
::util
::ToSemver
;
27 use cargo
::CargoResult
;
28 use cargo_util
::ProcessBuilder
;
30 const UPSTREAM_BRANCH
: &str = "master";
31 const STATUS
: &str = "BumpCheck";
33 pub fn cli() -> clap
::Command
{
34 clap
::Command
::new("xtask-bump-check")
38 "Use verbose output (-vv very verbose/build.rs output)",
41 .action(ArgAction
::Count
)
46 opt("color", "Coloring: auto, always, never")
50 .arg(opt("base-rev", "Git revision to lookup for a baseline"))
51 .arg(opt("head-rev", "Git revision with changes"))
52 .arg(flag("frozen", "Require Cargo.lock and cache are up to date").global(true))
53 .arg(flag("locked", "Require Cargo.lock is up to date").global(true))
54 .arg(flag("offline", "Run without accessing the network").global(true))
55 .arg(multi_opt("config", "KEY=VALUE", "Override a configuration value").global(true))
57 Arg
::new("unstable-features")
58 .help("Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details")
61 .action(ArgAction
::Append
)
66 pub fn exec(args
: &clap
::ArgMatches
, config
: &mut cargo
::util
::Config
) -> cargo
::CliResult
{
67 config_configure(config
, args
)?
;
69 bump_check(args
, config
)?
;
74 fn config_configure(config
: &mut Config
, args
: &ArgMatches
) -> CliResult
{
75 let verbose
= args
.verbose();
76 // quiet is unusual because it is redefined in some subcommands in order
77 // to provide custom help text.
78 let quiet
= args
.flag("quiet");
79 let color
= args
.get_one
::<String
>("color").map(String
::as_str
);
80 let frozen
= args
.flag("frozen");
81 let locked
= args
.flag("locked");
82 let offline
= args
.flag("offline");
83 let mut unstable_flags
= vec
![];
84 if let Some(values
) = args
.get_many
::<String
>("unstable-features") {
85 unstable_flags
.extend(values
.cloned());
87 let mut config_args
= vec
![];
88 if let Some(values
) = args
.get_many
::<String
>("config") {
89 config_args
.extend(values
.cloned());
105 /// Main entry of `xtask-bump-check`.
107 /// Assumption: version number are incremental. We never have point release for old versions.
108 fn bump_check(args
: &clap
::ArgMatches
, config
: &cargo
::util
::Config
) -> CargoResult
<()> {
109 let ws
= args
.workspace(config
)?
;
110 let repo
= git2
::Repository
::open(ws
.root())?
;
111 let base_commit
= get_base_commit(config
, args
, &repo
)?
;
112 let head_commit
= get_head_commit(args
, &repo
)?
;
113 let referenced_commit
= get_referenced_commit(&repo
, &base_commit
)?
;
114 let changed_members
= changed(&ws
, &repo
, &base_commit
, &head_commit
)?
;
115 let status
= |msg
: &str| config
.shell().status(STATUS
, msg
);
117 status(&format
!("base commit `{}`", base_commit
.id()))?
;
118 status(&format
!("head commit `{}`", head_commit
.id()))?
;
120 let mut needs_bump
= Vec
::new();
122 check_crates_io(config
, &changed_members
, &mut needs_bump
)?
;
124 if let Some(referenced_commit
) = referenced_commit
.as_ref() {
125 status(&format
!("compare against `{}`", referenced_commit
.id()))?
;
126 for referenced_member
in checkout_ws(&ws
, &repo
, referenced_commit
)?
.members() {
127 let pkg_name
= referenced_member
.name().as_str();
128 let Some(changed_member
) = changed_members
.get(pkg_name
) else {
129 tracing
::trace
!("skipping {pkg_name}, may be removed or not published");
133 if changed_member
.version() <= referenced_member
.version() {
134 needs_bump
.push(*changed_member
);
139 if !needs_bump
.is_empty() {
142 let mut msg
= String
::new();
143 msg
.push_str("Detected changes in these crates but no version bump found:\n");
144 for pkg
in needs_bump
{
145 writeln
!(&mut msg
, " {}@{}", pkg
.name(), pkg
.version())?
;
147 msg
.push_str("\nPlease bump at least one patch version in each corresponding Cargo.toml.");
151 // Tracked by https://github.com/obi1kenobi/cargo-semver-checks/issues/511
154 "cargo-credential-1password",
156 "cargo-credential-libsecret",
158 "cargo-credential-macos-keychain",
160 "cargo-credential-wincred",
163 // Even when we test against baseline-rev, we still need to make sure a
164 // change doesn't violate SemVer rules against crates.io releases. The
165 // possibility of this happening is nearly zero but no harm to check twice.
166 let mut cmd
= ProcessBuilder
::new("cargo");
167 cmd
.arg("semver-checks")
168 .arg("check-release")
170 .args(&exclude_args
);
171 config
.shell().status("Running", &cmd
)?
;
174 if let Some(referenced_commit
) = referenced_commit
.as_ref() {
175 let mut cmd
= ProcessBuilder
::new("cargo");
176 cmd
.arg("semver-checks")
178 .arg("--baseline-rev")
179 .arg(referenced_commit
.id().to_string())
180 .args(&exclude_args
);
181 config
.shell().status("Running", &cmd
)?
;
185 status("no version bump needed for member crates.")?
;
190 /// Returns the commit of upstream `master` branch if `base-rev` is missing.
191 fn get_base_commit
<'a
>(
193 args
: &clap
::ArgMatches
,
194 repo
: &'a git2
::Repository
,
195 ) -> CargoResult
<git2
::Commit
<'a
>> {
196 let base_commit
= match args
.get_one
::<String
>("base-rev") {
198 let obj
= repo
.revparse_single(sha
)?
;
199 obj
.peel_to_commit()?
202 let upstream_branches
= repo
203 .branches(Some(git2
::BranchType
::Remote
))?
204 .filter_map(|r
| r
.ok())
210 .ends_with(&format
!("/{UPSTREAM_BRANCH}"))
213 .collect
::<Vec
<_
>>();
214 if upstream_branches
.is_empty() {
216 "could not find `base-sha` for `{UPSTREAM_BRANCH}`, pass it in directly"
219 let upstream_ref
= upstream_branches
[0].get();
220 if upstream_branches
.len() > 1 {
221 let name
= upstream_ref
.name().expect("name is valid UTF-8");
222 let _
= config
.shell().warn(format
!(
223 "multiple `{UPSTREAM_BRANCH}` found, picking {name}"
226 upstream_ref
.peel_to_commit()?
232 /// Returns `HEAD` of the Git repository if `head-rev` is missing.
233 fn get_head_commit
<'a
>(
234 args
: &clap
::ArgMatches
,
235 repo
: &'a git2
::Repository
,
236 ) -> CargoResult
<git2
::Commit
<'a
>> {
237 let head_commit
= match args
.get_one
::<String
>("head-rev") {
239 let head_obj
= repo
.revparse_single(sha
)?
;
240 head_obj
.peel_to_commit()?
243 let head_ref
= repo
.head()?
;
244 head_ref
.peel_to_commit()?
250 /// Gets the referenced commit to compare if version bump needed.
252 /// * When merging into nightly, check the version with beta branch
253 /// * When merging into beta, check the version with stable branch
254 /// * When merging into stable, check against crates.io registry directly
255 fn get_referenced_commit
<'a
>(
256 repo
: &'a git2
::Repository
,
257 base
: &git2
::Commit
<'a
>,
258 ) -> CargoResult
<Option
<git2
::Commit
<'a
>>> {
259 let [beta
, stable
] = beta_and_stable_branch(repo
)?
;
260 let rev_id
= base
.id();
261 let stable_commit
= stable
.get().peel_to_commit()?
;
262 let beta_commit
= beta
.get().peel_to_commit()?
;
264 let referenced_commit
= if rev_id
== stable_commit
.id() {
266 } else if rev_id
== beta_commit
.id() {
267 tracing
::trace
!("stable branch from `{}`", stable
.name().unwrap().unwrap());
270 tracing
::trace
!("beta branch from `{}`", beta
.name().unwrap().unwrap());
274 Ok(referenced_commit
)
277 /// Get the current beta and stable branch in cargo repository.
281 /// * The repository contains the full history of `<remote>/rust-1.*.0` branches.
282 /// * The version part of `<remote>/rust-1.*.0` always ends with a zero.
283 /// * The maximum version is for beta channel, and the second one is for stable.
284 fn beta_and_stable_branch(repo
: &git2
::Repository
) -> CargoResult
<[git2
::Branch
<'_
>; 2]> {
285 let mut release_branches
= Vec
::new();
286 for branch
in repo
.branches(Some(git2
::BranchType
::Remote
))?
{
287 let (branch
, _
) = branch?
;
288 let name
= branch
.name()?
.unwrap();
289 let Some((_
, version
)) = name
.split_once("/rust-") else {
290 tracing
::trace
!("branch `{name}` is not in the format of `<remote>/rust-<semver>`");
293 let Ok(version
) = version
.to_semver() else {
294 tracing
::trace
!("branch `{name}` is not a valid semver: `{version}`");
297 release_branches
.push((version
, branch
));
299 release_branches
.sort_unstable_by(|a
, b
| a
.0.cmp(&b
.0));
300 release_branches
.dedup_by(|a
, b
| a
.0 == b
.0);
302 let beta
= release_branches
.pop().unwrap();
303 let stable
= release_branches
.pop().unwrap();
305 assert_eq
!(beta
.0.major
, 1);
306 assert_eq
!(beta
.0.patch
, 0);
307 assert_eq
!(stable
.0.major
, 1);
308 assert_eq
!(stable
.0.patch
, 0);
309 assert_ne
!(beta
.0.minor
, stable
.0.minor
);
311 Ok([beta
.1, stable
.1])
314 /// Lists all changed workspace members between two commits.
316 ws
: &'ws Workspace
<'_
>,
317 repo
: &'r git2
::Repository
,
318 base_commit
: &git2
::Commit
<'r
>,
319 head
: &git2
::Commit
<'r
>,
320 ) -> CargoResult
<HashMap
<&'ws
str, &'ws Package
>> {
321 let root_pkg_name
= ws
.current()?
.name(); // `cargo` crate.
324 .filter(|pkg
| pkg
.name() != root_pkg_name
) // Only take care of sub crates here.
325 .filter(|pkg
| pkg
.publish() != &Some(vec
![])) // filter out `publish = false`
327 // Having relative package root path so that we can compare with
328 // paths of changed files to determine which package has changed.
329 let relative_pkg_root
= pkg
.root().strip_prefix(ws
.root()).unwrap();
330 (relative_pkg_root
, pkg
)
332 .collect
::<Vec
<_
>>();
333 let base_tree
= base_commit
.as_object().peel_to_tree()?
;
334 let head_tree
= head
.as_object().peel_to_tree()?
;
335 let diff
= repo
.diff_tree_to_tree(Some(&base_tree
), Some(&head_tree
), Default
::default())?
;
337 let mut changed_members
= HashMap
::new();
339 for delta
in diff
.deltas() {
340 let old
= delta
.old_file().path().unwrap();
341 let new
= delta
.new_file().path().unwrap();
342 for (ref pkg_root
, pkg
) in ws_members
.iter() {
343 if old
.starts_with(pkg_root
) || new
.starts_with(pkg_root
) {
344 changed_members
.insert(pkg
.name().as_str(), *pkg
);
350 tracing
::trace
!("changed_members: {:?}", changed_members
.keys());
354 /// Compares version against published crates on crates.io.
356 /// Assumption: We always release a version larger than all existing versions.
357 fn check_crates_io
<'a
>(
359 changed_members
: &HashMap
<&'a
str, &'a Package
>,
360 needs_bump
: &mut Vec
<&'a Package
>,
361 ) -> CargoResult
<()> {
362 let source_id
= SourceId
::crates_io(config
)?
;
363 let mut registry
= PackageRegistry
::new(config
)?
;
364 let _lock
= config
.acquire_package_cache_lock()?
;
365 registry
.lock_patches();
366 config
.shell().status(
368 format_args
!("compare against `{}`", source_id
.display_registry_name()),
370 for (name
, member
) in changed_members
{
371 let current
= member
.version();
372 let version_req
= format
!(">={current}");
373 let query
= Dependency
::parse(*name
, Some(&version_req
), source_id
)?
;
374 let possibilities
= loop {
375 // Exact to avoid returning all for path/git
376 match registry
.query_vec(&query
, QueryKind
::Exact
) {
377 task
::Poll
::Ready(res
) => {
380 task
::Poll
::Pending
=> registry
.block_until_ready()?
,
383 if possibilities
.is_empty() {
384 tracing
::trace
!("dep `{name}` has no version greater than or equal to `{current}`");
387 "`{name}@{current}` needs a bump because its should have a version newer than crates.io: {:?}`",
390 .map(|s
| format
!("{}@{}", s
.name(), s
.version()))
391 .collect
::<Vec
<_
>>(),
393 needs_bump
.push(member
);
400 /// Checkouts a temporary workspace to do further version comparisons.
401 fn checkout_ws
<'cfg
, 'a
>(
402 ws
: &Workspace
<'cfg
>,
403 repo
: &'a git2
::Repository
,
404 referenced_commit
: &git2
::Commit
<'a
>,
405 ) -> CargoResult
<Workspace
<'cfg
>> {
406 let repo_path
= repo
.path().as_os_str().to_str().unwrap();
407 // Put it under `target/cargo-<short-id>`
408 let short_id
= &referenced_commit
.id().to_string()[..7];
409 let checkout_path
= ws
.target_dir().join(format
!("cargo-{short_id}"));
410 let checkout_path
= checkout_path
.as_path_unlocked();
411 let _
= fs
::remove_dir_all(checkout_path
);
412 let new_repo
= git2
::build
::RepoBuilder
::new()
413 .clone_local(git2
::build
::CloneLocal
::Local
)
414 .clone(repo_path
, checkout_path
)?
;
415 let obj
= new_repo
.find_object(referenced_commit
.id(), None
)?
;
416 new_repo
.reset(&obj
, git2
::ResetType
::Hard
, None
)?
;
417 Workspace
::new(&checkout_path
.join("Cargo.toml"), ws
.config())
422 cli().debug_assert();