Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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.
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ fn run() -> Result<()> {
generic::run_lensfun_update_data(&ctx)
})?;
runner.execute(Step::Poetry, "Poetry", || generic::run_poetry(&ctx))?;
runner.execute(Step::Uv, "uv", || generic::run_uv(&ctx))?;
runner.execute_with_updated(Step::Uv, "uv", || generic::run_uv(&ctx))?;
runner.execute(Step::Zvm, "ZVM", || generic::run_zvm(&ctx))?;
runner.execute(Step::Aqua, "aqua", || generic::run_aqua(&ctx))?;
runner.execute(Step::Bun, "bun", || generic::run_bun(&ctx))?;
Expand Down
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::terminal::print_error;
use crate::{config::Step, terminal::should_retry};
use color_eyre::eyre::Result;
Expand All @@ -26,6 +26,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 @@ -44,8 +60,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
55 changes: 48 additions & 7 deletions src/steps/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use color_eyre::eyre::Result;
use jetbrains_toolbox_updater::{find_jetbrains_toolbox, update_jetbrains_toolbox, FindError};
use lazy_static::lazy_static;
use regex::bytes::Regex;
use regex::Regex;
Comment thread
GideonBear marked this conversation as resolved.
use rust_i18n::t;
use semver::Version;
use tempfile::tempfile_in;
Expand All @@ -18,6 +18,7 @@
use crate::command::{CommandExt, Utf8Output};
use crate::execution_context::ExecutionContext;
use crate::executor::ExecutorOutput;
use crate::report::UpdatedComponent;
use crate::terminal::{print_separator, shell};
use crate::utils::{check_is_python_2_or_shim, get_require_sudo_string, require, require_option, which, PathExt};
use crate::HOME_DIR;
Expand Down Expand Up @@ -1234,7 +1235,8 @@
use std::os::unix::ffi::OsStrExt;

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

let script = fs::read(poetry)?;
Expand Down Expand Up @@ -1335,10 +1337,12 @@
.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 @@ -1380,6 +1384,8 @@
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
Expand All @@ -1392,10 +1398,16 @@
.is_ok();

if self_update_feature_enabled {
ctx.run_type()
let output = ctx
.run_type()
.execute(&uv_exec)
.args(["self", "update"])
.status_checked()?;
.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 All @@ -1418,7 +1430,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 stderr.contains(ERROR_MSG) {
// Feature `self-update` is disabled, nothing to do.
Expand All @@ -1432,14 +1444,43 @@
if !output.status.success() {
return Err(eyre!("uv self update failed"));
}

self_output = Some(output);
}
};

// Extract if the self-update happened

lazy_static! {
static ref UV_SELF_REGEX: Regex = 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.run_type()
.execute(&uv_exec)
.args(["tool", "upgrade", "--all"])
.status_checked()
.status_checked()?;

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 @@ -173,7 +173,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