Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ assignees: ''
* Does it have a `--dry-run` option? i.e., print what should be done and exit
* Does it need the user to confirm the execution? And does it provide a `--yes`
option to skip this step?
* Can Topgrade extract the components that it updated to use in the summary?

## I want to suggest some general feature

Topgrade should...

## More information

<!-- Assuming that someone else implements the feature,
please state if you know how to test it from a side branch of Topgrade. -->
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
## What does this PR do


## Standards checklist

- [ ] The PR title is descriptive
- [ ] I have read `CONTRIBUTING.md`
- [ ] *Optional:* I have tested the code myself
- [ ] If this PR introduces new user-facing messages they are translated

## For new steps

- [ ] *Optional:* Topgrade skips this step where needed
- [ ] *Optional:* The `--dry-run` option works with this step
- [ ] *Optional:* The `--yes` option works with this step if it is supported by
- [ ] *Optional:* The `--yes` option works with this step if it is supported by
the underlying command
- [ ] *Optional:* This step extracts and returns its updated components

If you developed a feature or a bug fix for someone else and you do not have the
means to test it, please tag this person here.
60 changes: 58 additions & 2 deletions src/report.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::borrow::Cow;
use std::fmt::Display;

pub enum StepResult {
Success,
Success(Option<UpdatedComponents>),
Failure,
Ignored,
Skipped(String),
Expand All @@ -10,12 +11,67 @@ pub enum StepResult {
impl StepResult {
pub fn failed(&self) -> bool {
match self {
StepResult::Success | StepResult::Ignored | StepResult::Skipped(_) => false,
StepResult::Success(_) | StepResult::Ignored | StepResult::Skipped(_) => false,
StepResult::Failure => true,
}
}
}

pub struct UpdatedComponents(Vec<UpdatedComponent>);

impl UpdatedComponents {
pub fn new(updated: Vec<UpdatedComponent>) -> Self {
Self(updated)
}
}

impl Display for UpdatedComponents {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0.as_slice() {
[] => write!(f, "No updates found"),
components => {
writeln!(f, "Updated:")?;
let updates = components
.iter()
.map(|c| format!("- {c}"))
.collect::<Vec<_>>()
.join("\n");
write!(f, "{updates}")?;
Ok(())
}
}
}
}

pub struct UpdatedComponent {
name: String,
from_version: Option<String>,
to_version: Option<String>,
}

impl UpdatedComponent {
pub fn new(name: String, from_version: Option<String>, to_version: Option<String>) -> Self {
Self {
name,
from_version,
to_version,
}
}
}

impl Display for UpdatedComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.from_version, &self.to_version) {
(None, None) => write!(f, "{}", self.name),
(None, Some(to_version)) => write!(f, "{} to {}", self.name, to_version),
(Some(from_version), None) => write!(f, "{} from {}", self.name, from_version),
(Some(from_version), Some(to_version)) => {
write!(f, "{} from {} to {}", self.name, from_version, to_version)
}
}
}
}

type CowString<'a> = Cow<'a, str>;
type ReportData<'a> = Vec<(CowString<'a>, StepResult)>;
pub struct Report<'a> {
Expand Down
23 changes: 20 additions & 3 deletions src/runner.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::ctrlc;
use crate::error::{DryRun, SkipStep};
use crate::execution_context::ExecutionContext;
use crate::report::{Report, StepResult};
use crate::report::{Report, StepResult, UpdatedComponent, UpdatedComponents};
use crate::step::Step;
use crate::terminal::print_error;
use crate::terminal::should_retry;
Expand All @@ -27,6 +27,22 @@ impl<'a> Runner<'a> {
where
F: Fn() -> Result<()>,
M: Into<Cow<'a, str>> + Debug,
{
self._execute(step, key, || func().map(|()| None))
}

pub fn execute_with_updated<F, M>(&mut self, step: Step, key: M, func: F) -> Result<()>
where
F: Fn() -> Result<Vec<UpdatedComponent>>,
M: Into<Cow<'a, str>> + Debug,
{
self._execute(step, key, || func().map(Some))
}

fn _execute<F, M>(&mut self, step: Step, key: M, func: F) -> Result<()>
where
F: Fn() -> Result<Option<Vec<UpdatedComponent>>>,
M: Into<Cow<'a, str>> + Debug,
{
if !self.ctx.config().should_run(step) {
return Ok(());
Expand All @@ -45,8 +61,9 @@ impl<'a> Runner<'a> {

loop {
match func() {
Ok(()) => {
self.report.push_result(Some((key, StepResult::Success)));
Ok(updated) => {
self.report
.push_result(Some((key, StepResult::Success(updated.map(UpdatedComponents::new)))));
break;
}
Err(e) if e.downcast_ref::<DryRun>().is_some() => break,
Expand Down
2 changes: 1 addition & 1 deletion src/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ impl Step {
#[cfg(target_os = "linux")]
runner.execute(*self, "toolbx", || toolbx::run_toolbx(ctx))?
}
Uv => runner.execute(*self, "uv", || generic::run_uv(ctx))?,
Uv => runner.execute_with_updated(*self, "uv", || generic::run_uv(ctx))?,
Vagrant => {
if ctx.config().should_run(Vagrant) {
if let Ok(boxes) = vagrant::collect_boxes(ctx) {
Expand Down
55 changes: 46 additions & 9 deletions src/steps/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use color_eyre::eyre::Result;
use color_eyre::eyre::{eyre, OptionExt};
use jetbrains_toolbox_updater::{find_jetbrains_toolbox, update_jetbrains_toolbox, FindError};
use regex::bytes::Regex;
use regex::Regex;
Comment thread
GideonBear marked this conversation as resolved.
use rust_i18n::t;
use semver::Version;
use std::ffi::OsString;
Expand All @@ -19,6 +19,7 @@
use crate::execution_context::ExecutionContext;
use crate::executor::ExecutorOutput;
use crate::output_changed_message;
use crate::report::UpdatedComponent;
use crate::step::Step;
use crate::sudo::SudoExecuteOpts;
use crate::terminal::{print_separator, shell};
Expand Down Expand Up @@ -1233,8 +1234,8 @@
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;

static SHEBANG_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap());
static SHEBANG_REGEX: LazyLock<regex::bytes::Regex> =
LazyLock::new(|| regex::bytes::Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap());

let script = fs::read(poetry)?;
if let Some(c) = SHEBANG_REGEX.captures(&script) {
Expand All @@ -1253,8 +1254,9 @@

use std::str;

static SHEBANG_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap());
static SHEBANG_REGEX: LazyLock<regex::bytes::Regex> = LazyLock::new(|| {
regex::bytes::Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap()
});

let data = fs::read(poetry)?;

Expand Down Expand Up @@ -1329,10 +1331,12 @@
ctx.execute(&poetry).args(["self", "update"]).status_checked()
}

pub fn run_uv(ctx: &ExecutionContext) -> Result<()> {
pub fn run_uv(ctx: &ExecutionContext) -> Result<Vec<UpdatedComponent>> {
Comment thread
GideonBear marked this conversation as resolved.
let uv_exec = require("uv")?;
print_separator("uv");

let mut updated = vec![];

// 1. Run `uv self update` if the `uv` binary is built with the `self-update`
// cargo feature enabled.
//
Expand Down Expand Up @@ -1370,14 +1374,21 @@
let version =
Version::parse(version_str).wrap_err_with(|| output_changed_message!("uv --version", "Invalid version"))?;

let mut self_output = None;

if version < Version::new(0, 4, 25) {
// For uv before version 0.4.25 (exclusive), the `self` sub-command only
// exists under the `self-update` feature, we run `uv self --help` to check
// the feature gate.
let self_update_feature_enabled = ctx.execute(&uv_exec).args(["self", "--help"]).output_checked().is_ok();

if self_update_feature_enabled {
ctx.execute(&uv_exec).args(["self", "update"]).status_checked()?;
let output = ctx.execute(&uv_exec).args(["self", "update"]).output_checked()?;

std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;

self_output = Some(output);
}
} else {
// After 0.4.25 (inclusive), running `uv self` succeeds regardless of the
Expand Down Expand Up @@ -1412,7 +1423,7 @@
ExecutorOutput::Wet(wet) => wet,
ExecutorOutput::Dry => unreachable!("the whole function returns when we run `uv --version` under dry-run"),
};
let stderr = std::str::from_utf8(&output.stderr).expect("output should be UTF-8 encoded");
let stderr = std::str::from_utf8(&output.stderr).wrap_err("Output should be valid UTF-8")?;

if ERROR_MSGS.iter().any(|&n| stderr.contains(n)) {
// Feature `self-update` is disabled, nothing to do.
Expand All @@ -1426,20 +1437,46 @@
if !output.status.success() {
return Err(eyre!("uv self update failed"));
}

self_output = Some(output);
}
};

// Extract if the self-update happened

static UV_SELF_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"success: (?:(?:You're on the latest version of uv \((?:v[\.0-9]+)\))|(?:Upgraded uv from (v[\.0-9]+) to (v[\.0-9]+)!))"
).expect("Uv self-update output regex always compiles")
});

if let Some(output) = self_output {
let captures = UV_SELF_REGEX
.captures(std::str::from_utf8(&output.stderr).wrap_err("Output should be valid UTF-8")?)
.ok_or_else(|| eyre!(output_changed_message!("uv self update", "regex did not match")))?;
match (captures.get(1), captures.get(2)) {
(None, None) => (),
(Some(from_version), Some(to_version)) => updated.push(UpdatedComponent::new(
"(self-update) uv".to_string(),
Some(from_version.as_str().to_string()),
Some(to_version.as_str().to_string()),
)),
_ => unreachable!("Regex should match none or both groups"),
}
}

// 2. Update the installed tools
// TODO: include this in `updated`
Comment thread Fixed

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
ctx.execute(&uv_exec)
.args(["tool", "upgrade", "--all"])
.status_checked()?;

if ctx.config().cleanup() {
// 3. Prune cache
ctx.execute(&uv_exec).args(["cache", "prune"]).status_checked()?;
}

Ok(())
Ok(updated)
}

/// Involve `zvm upgrade` to update ZVM
Expand Down
10 changes: 9 additions & 1 deletion src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,15 @@ impl Terminal {
"{}: {}\n",
key,
match result {
StepResult::Success => format!("{}", style(t!("OK")).bold().green()),
StepResult::Success(updated) => {
let mut s = format!("{}", style(t!("OK")).bold().green());
// Only add the ": No updates found" or ": Updated:" when this step
// supports extracting updated components
if let Some(updated) = updated {
s.push_str(&format!(": {updated}"));
}
s
}
StepResult::Failure => format!("{}", style(t!("FAILED")).bold().red()),
StepResult::Ignored => format!("{}", style(t!("IGNORED")).bold().yellow()),
StepResult::Skipped(reason) => format!("{}: {}", style(t!("SKIPPED")).bold().blue(), reason),
Expand Down
Loading