Repository: denoland/deno
Pull request: feat(upgrade): support `deno upgrade 1.46.0`

This commit changes `deno upgrade` subcommand to accept
a positional argument that can be either a version, release channel
name or a git hash, making invocations of `deno upgrade` much
more concise:

```
# before
$ deno upgrade --version 1.46.0
# after 
$ deno upgrade 1.46.0
```

```
# before
$ deno upgrade --canary
# after
$ deno upgrade canary
```

```
# specific canary version before
$ deno upgrade --canary --version f042c39180c1b345de8e7b5f0dfae5d0a49b161f
# after
$ deno upgrade f042c39180c1b345de8e7b5f0dfae5d0a49b161f
```

Old flags are still supported, but hidden from the help output.

Changed files:
filename: cli/args/flags.rs
patch: @@ -412,6 +412,7 @@ pub struct UpgradeFlags {
   pub canary: bool,
   pub version: Option<String>,
   pub output: Option<String>,
+  pub version_or_hash_or_channel: Option<String>,
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -3018,16 +3019,31 @@ The declaration file could be saved and used for typing information.",
 fn upgrade_subcommand() -> Command {
   command(
     "upgrade",
-    "Upgrade deno executable to the given version.
-Defaults to latest.
+    color_print::cstr!("<g>Upgrade</> deno executable to the given version.
+
+<g>Latest</>
+
+  deno upgrade
+
+<g>Specific version</>
+
+  deno upgrade <p(245)>1.45.0</>
+  deno upgrade <p(245)>1.46.0-rc.1</>
+  deno upgrade <p(245)>9bc2dd29ad6ba334fd57a20114e367d3c04763d4</>
+
+<g>Channel</>
+
+  deno upgrade <p(245)>stable</>
+  deno upgrade <p(245)>rc</>
+  deno upgrade <p(245)>canary</>
 
 The version is downloaded from
 https://github.com/denoland/deno/releases
 and is used to replace the current executable.
 
 If you want to not replace the current Deno executable but instead download an
 update to a different location, use the --output flag:
-  deno upgrade --output $HOME/my_deno",
+  deno upgrade --output $HOME/my_deno"),
     UnstableArgsConfig::None,
   )
   .hide(cfg!(not(feature = "upgrade")))
@@ -3037,7 +3053,8 @@ update to a different location, use the --output flag:
         Arg::new("version")
           .long("version")
           .help("The version to upgrade to")
-          .help_heading(UPGRADE_HEADING),
+          .help_heading(UPGRADE_HEADING)// NOTE(bartlomieju): pre-v1.46 compat
+          .hide(true),
       )
       .arg(
         Arg::new("output")
@@ -3067,15 +3084,25 @@ update to a different location, use the --output flag:
           .long("canary")
           .help("Upgrade to canary builds")
           .action(ArgAction::SetTrue)
-          .help_heading(UPGRADE_HEADING),
+          .help_heading(UPGRADE_HEADING)// NOTE(bartlomieju): pre-v1.46 compat
+          .hide(true),
       )
       .arg(
         Arg::new("release-candidate")
           .long("rc")
           .help("Upgrade to a release candidate")
           .conflicts_with_all(["canary", "version"])
           .action(ArgAction::SetTrue)
-          .help_heading(UPGRADE_HEADING),
+          .help_heading(UPGRADE_HEADING)
+          // NOTE(bartlomieju): pre-v1.46 compat
+          .hide(true),
+      )
+      .arg(
+        Arg::new("version-or-hash-or-channel")
+          .help(color_print::cstr!("Version <p(245)>(v1.46.0)</>, channel <p(245)>(rc, canary)</> or commit hash <p(245)>(9bc2dd29ad6ba334fd57a20114e367d3c04763d4)</>"))
+          .value_name("VERSION")
+          .action(ArgAction::Append)
+          .trailing_var_arg(true),
       )
       .arg(ca_file_arg())
       .arg(unsafely_ignore_certificate_errors_arg())
@@ -4911,13 +4938,16 @@ fn upgrade_parse(flags: &mut Flags, matches: &mut ArgMatches) {
   let release_candidate = matches.get_flag("release-candidate");
   let version = matches.remove_one::<String>("version");
   let output = matches.remove_one::<String>("output");
+  let version_or_hash_or_channel =
+    matches.remove_one::<String>("version-or-hash-or-channel");
   flags.subcommand = DenoSubcommand::Upgrade(UpgradeFlags {
     dry_run,
     force,
     release_candidate,
     canary,
     version,
     output,
+    version_or_hash_or_channel,
   });
 }
 
@@ -5426,6 +5456,7 @@ mod tests {
           release_candidate: false,
           version: None,
           output: None,
+          version_or_hash_or_channel: None,
         }),
         ..Flags::default()
       }
@@ -5445,6 +5476,7 @@ mod tests {
           release_candidate: false,
           version: None,
           output: Some(String::from("example.txt")),
+          version_or_hash_or_channel: None,
         }),
         ..Flags::default()
       }
@@ -9435,6 +9467,7 @@ mod tests {
           release_candidate: false,
           version: None,
           output: None,
+          version_or_hash_or_channel: None,
         }),
         ca_data: Some(CaData::File("example.crt".to_owned())),
         ..Flags::default()
@@ -9455,6 +9488,7 @@ mod tests {
           release_candidate: true,
           version: None,
           output: None,
+          version_or_hash_or_channel: None,
         }),
         ..Flags::default()
       }

filename: cli/tools/upgrade.rs
patch: @@ -547,7 +547,7 @@ pub async fn upgrade(
   Ok(())
 }
 
-#[derive(Debug)]
+#[derive(Debug, PartialEq)]
 enum RequestedVersion {
   Latest(ReleaseChannel),
   SpecificVersion(ReleaseChannel, String),
@@ -556,20 +556,34 @@ enum RequestedVersion {
 impl RequestedVersion {
   fn from_upgrade_flags(upgrade_flags: UpgradeFlags) -> Result<Self, AnyError> {
     let is_canary = upgrade_flags.canary;
-    let is_release_candidate = upgrade_flags.release_candidate;
-
-    let Some(passed_version) = upgrade_flags.version else {
-      let channel = if is_canary {
-        ReleaseChannel::Canary
-      } else if is_release_candidate {
-        ReleaseChannel::Rc
+    let re_hash = lazy_regex::regex!("^[0-9a-f]{40}$");
+    let channel = if is_canary {
+      ReleaseChannel::Canary
+    } else if upgrade_flags.release_candidate {
+      ReleaseChannel::Rc
+    } else {
+      ReleaseChannel::Stable
+    };
+    let mut maybe_passed_version = upgrade_flags.version.clone();
+
+    if let Some(val) = &upgrade_flags.version_or_hash_or_channel {
+      if let Ok(channel) = ReleaseChannel::deserialize(&val.to_lowercase()) {
+        // TODO(bartlomieju): print error if any other flags passed?
+        return Ok(Self::Latest(channel));
+      } else if re_hash.is_match(val) {
+        return Ok(Self::SpecificVersion(
+          ReleaseChannel::Canary,
+          val.to_string(),
+        ));
       } else {
-        ReleaseChannel::Stable
-      };
+        maybe_passed_version = Some(val.to_string());
+      }
+    }
+
+    let Some(passed_version) = maybe_passed_version else {
       return Ok(Self::Latest(channel));
     };
 
-    let re_hash = lazy_regex::regex!("^[0-9a-f]{40}$");
     let passed_version = passed_version
       .strip_prefix('v')
       .unwrap_or(&passed_version)
@@ -1039,6 +1053,104 @@ mod test {
 
   use super::*;
 
+  #[test]
+  fn test_requested_version() {
+    let mut upgrade_flags = UpgradeFlags {
+      dry_run: false,
+      force: false,
+      release_candidate: false,
+      canary: false,
+      version: None,
+      output: None,
+      version_or_hash_or_channel: None,
+    };
+
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Stable));
+
+    upgrade_flags.version = Some("1.46.0".to_string());
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(
+      req_ver,
+      RequestedVersion::SpecificVersion(
+        ReleaseChannel::Stable,
+        "1.46.0".to_string()
+      )
+    );
+
+    upgrade_flags.version = None;
+    upgrade_flags.canary = true;
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Canary));
+
+    upgrade_flags.version =
+      Some("5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string());
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(
+      req_ver,
+      RequestedVersion::SpecificVersion(
+        ReleaseChannel::Canary,
+        "5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string()
+      )
+    );
+
+    upgrade_flags.version = None;
+    upgrade_flags.canary = false;
+    upgrade_flags.release_candidate = true;
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Rc));
+
+    upgrade_flags.release_candidate = false;
+    upgrade_flags.version_or_hash_or_channel = Some("v1.46.5".to_string());
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(
+      req_ver,
+      RequestedVersion::SpecificVersion(
+        ReleaseChannel::Stable,
+        "1.46.5".to_string()
+      )
+    );
+
+    upgrade_flags.version_or_hash_or_channel = Some("2.0.0-rc.0".to_string());
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(
+      req_ver,
+      RequestedVersion::SpecificVersion(
+        ReleaseChannel::Rc,
+        "2.0.0-rc.0".to_string()
+      )
+    );
+
+    upgrade_flags.version_or_hash_or_channel = Some("canary".to_string());
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Canary,));
+
+    upgrade_flags.version_or_hash_or_channel = Some("rc".to_string());
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Rc,));
+
+    upgrade_flags.version_or_hash_or_channel =
+      Some("5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string());
+    let req_ver =
+      RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
+    assert_eq!(
+      req_ver,
+      RequestedVersion::SpecificVersion(
+        ReleaseChannel::Canary,
+        "5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string()
+      )
+    );
+  }
+
   #[test]
   fn test_parse_upgrade_check_file() {
     // NOTE(bartlomieju): pre-1.46 format