Skip to content

Commit

Permalink
Merge pull request #217 from MordechaiHadad/feat/list-remote
Browse files Browse the repository at this point in the history
Add list-remote command
  • Loading branch information
MordechaiHadad authored Jun 13, 2024
2 parents d91bbaa + 933ef18 commit eb0fd7a
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 39 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ Update existing version, can specify either a version or the flag `--all`

---

- `bob list-remote`

List all remote neovim versions available for download.

---

## ⚙ Configuration

This section is a bit more advanced and thus the user will have to do the work himself since bob doesn't do that.
Expand Down
8 changes: 6 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::{
config::Config,
handlers::{
self, erase_handler, list_handler, rollback_handler, sync_handler, uninstall_handler,
update_handler, InstallResult,
self, erase_handler, list_handler, list_remote_handler, rollback_handler, sync_handler,
uninstall_handler, update_handler, InstallResult,
},
};
use anyhow::Result;
Expand Down Expand Up @@ -94,6 +94,9 @@ enum Cli {
#[clap(visible_alias = "ls")]
List,

#[clap(visible_alias = "ls-remote")]
ListRemote,

/// Generate shell completion
Complete {
/// Shell to generate completion for
Expand Down Expand Up @@ -199,6 +202,7 @@ pub async fn start(config: Config) -> Result<()> {
Cli::Update(data) => {
update_handler::start(data, &client, config).await?;
}
Cli::ListRemote => list_remote_handler::start(config, client).await?,
}

Ok(())
Expand Down
48 changes: 27 additions & 21 deletions src/github_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,38 +139,51 @@ pub struct ErrorResponse {
pub documentation_url: String,
}

/// Fetches the upstream nightly version from the GitHub API.
/// Asynchronously makes a GitHub API request.
///
/// This function sends a GET request to the GitHub API to fetch the upstream nightly version of the software. The request is sent to the URL "https://api.github.com/repos/neovim/neovim/releases/tags/nightly".
/// This function takes a reference to a `Client` and a URL as arguments. It sets the "user-agent" header to "bob" and the "Accept" header to "application/vnd.github.v3+json".
/// It then sends the request and awaits the response. It reads the response body as text and returns it as a `String`.
///
/// # Parameters
/// # Arguments
///
/// * `client: &Client` - The HTTP client used to send the request.
/// * `client` - A reference to a `Client` used to make the request.
/// * `url` - A URL that implements `AsRef<str>` and `reqwest::IntoUrl`.
///
/// # Returns
///
/// * `Result<UpstreamVersion>` - The upstream nightly version as an `UpstreamVersion` object, or an error if the request failed.
/// This function returns a `Result` that contains a `String` representing the response body if the operation was successful.
/// If the operation failed, the function returns `Err` with a description of the error.
///
/// # Example
///
/// ```rust
/// let client = Client::new();
/// let result = get_upstream_nightly(&client).await;
/// match result {
/// Ok(version) => println!("Received version: {:?}", version),
/// Err(e) => println!("An error occurred: {:?}", e),
/// }
/// let url = "https://api.github.com/repos/neovim/neovim/tags";
/// let response = make_github_request(&client, url).await?;
/// ```
pub async fn get_upstream_nightly(client: &Client) -> Result<UpstreamVersion> {
pub async fn make_github_request<T: AsRef<str> + reqwest::IntoUrl>(
client: &Client,
url: T,
) -> Result<String> {
let response = client
.get("https://api.github.com/repos/neovim/neovim/releases/tags/nightly")
.get(url)
.header("user-agent", "bob")
.header("Accept", "application/vnd.github.v3+json")
.send()
.await?
.text()
.await?;

Ok(response)
}

pub async fn get_upstream_nightly(client: &Client) -> Result<UpstreamVersion> {
let response = make_github_request(
client,
"https://api.github.com/repos/neovim/neovim/releases/tags/nightly",
)
.await?;

deserialize_response(response)
}

Expand Down Expand Up @@ -205,15 +218,8 @@ pub async fn get_commits_for_nightly(
since: &DateTime<Utc>,
until: &DateTime<Utc>,
) -> Result<Vec<RepoCommit>> {
let response = client
.get(format!(
"https://api.github.com/repos/neovim/neovim/commits?since={since}&until={until}&per_page=100"))
.header("user-agent", "bob")
.header("Accept", "application/vnd.github.v3+json")
.send()
.await?
.text()
.await?;
let response = make_github_request(client, format!(
"https://api.github.com/repos/neovim/neovim/commits?since={since}&until={until}&per_page=100")).await?;

deserialize_response(response)
}
Expand Down
131 changes: 131 additions & 0 deletions src/handlers/list_remote_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use std::{fs, path::PathBuf};

use anyhow::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use yansi::Paint;

use crate::{
config::Config,
github_requests::{deserialize_response, make_github_request},
helpers::{self, directories, version::search_stable_version},
};

/// Asynchronously starts the process of listing remote versions of Neovim.
///
/// This function takes a `Config` and a `Client` as arguments. It first gets the downloads directory path by calling the `get_downloads_directory` function.
/// It then makes a GitHub API request to get the tags of the Neovim repository, which represent the versions of Neovim.
/// The function then reads the downloads directory and filters the entries that contain 'v' in their names, which represent the local versions of Neovim.
/// It deserializes the response from the GitHub API request into a vector of `RemoteVersion`.
/// It filters the versions that start with 'v' and then iterates over the filtered versions.
/// For each version, it checks if it is installed locally and if it is the stable version.
/// It then prints the version name in green if it is being used, in yellow if it is installed but not being used, and in default color if it is not installed.
/// It also appends ' (stable)' to the version name if it is the stable version.
///
/// # Arguments
///
/// * `config` - A `Config` containing the application configuration.
/// * `client` - A `Client` used to make the GitHub API request.
///
/// # Returns
///
/// This function returns a `Result` that contains `()` if the operation was successful.
/// If the operation failed, the function returns `Err` with a description of the error.
///
/// # Example
///
/// ```rust
/// let config = Config::default();
/// let client = Client::new();
/// start(config, client).await?;
/// ```
pub async fn start(config: Config, client: Client) -> Result<()> {
let downloads_dir = directories::get_downloads_directory(&config).await?;
let response = make_github_request(
&client,
"https://api.github.com/repos/neovim/neovim/tags?per_page=50",
)
.await?;

let mut local_versions: Vec<PathBuf> = fs::read_dir(downloads_dir)?
.filter_map(Result::ok)
.filter(|entry| {
entry
.path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.contains('v')
})
.map(|entry| entry.path())
.collect();

let versions: Vec<RemoteVersion> = deserialize_response(response)?;
let filtered_versions: Vec<RemoteVersion> = versions
.into_iter()
.filter(|v| v.name.starts_with('v'))
.collect();

let stable_version = search_stable_version(&client).await?;
let padding = " ".repeat(12);

for version in filtered_versions {
let version_installed = local_versions.iter().any(|v| {
v.file_name()
.and_then(|str| str.to_str())
.map_or(false, |str| str.contains(&version.name))
});

let stable_version_string = if stable_version == version.name {
" (stable)"
} else {
""
};

if helpers::version::is_version_used(&version.name, &config).await {
println!(
"{padding}{}{}",
Paint::green(version.name),
stable_version_string
);
} else if version_installed {
println!(
"{padding}{}{}",
Paint::yellow(&version.name),
stable_version_string
);

local_versions.retain(|v| {
v.file_name()
.and_then(|str| str.to_str())
.map_or(true, |str| !str.contains(&version.name))
});
} else {
println!("{padding}{}{}", version.name, stable_version_string);
}
}

Ok(())
}

/// Represents a remote version of Neovim.
///
/// This struct is used to deserialize the response from the GitHub API request that gets the tags of the Neovim repository.
/// Each tag represents a version of Neovim, and the `name` field of the `RemoteVersion` struct represents the name of the version.
///
/// # Fields
///
/// * `name` - A `String` that represents the name of the version.
///
/// # Example
///
/// ```rust
/// let remote_version = RemoteVersion {
/// name: "v0.5.0".to_string(),
/// };
/// ```
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
struct RemoteVersion {
pub name: String,
}
1 change: 1 addition & 0 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod erase_handler;
pub mod install_handler;
pub mod list_handler;
pub mod list_remote_handler;
pub mod rollback_handler;
pub mod sync_handler;
pub mod uninstall_handler;
Expand Down
27 changes: 11 additions & 16 deletions src/helpers/version/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,34 +259,29 @@ pub async fn is_version_used(version: &str, config: &Config) -> bool {
}
}

/// Searches for the stable version of Neovim from the GitHub releases.
/// Asynchronously searches for the stable version of Neovim.
///
/// This function sends a GET request to the GitHub API to fetch the latest 10 releases of the Neovim repository. It then deserializes the response into a vector of `UpstreamVersion` objects and finds the stable release and the version of the stable release.
/// This function takes a reference to a `Client` as an argument and makes a GitHub API request to get the releases of the Neovim repository.
/// It then deserializes the response into a vector of `UpstreamVersion`.
/// It finds the release that has the tag name "stable" and the release that has the same `target_commitish` as the stable release but does not have the tag name "stable".
/// The function returns the tag name of the found release.
///
/// # Arguments
///
/// * `client` - The HTTP client to use for the request.
/// * `client` - A reference to a `Client` used to make the GitHub API request.
///
/// # Returns
///
/// * `Result<String>` - Returns a `Result` that contains the tag name of the stable version, or an error if the operation failed.
///
/// # Errors
///
/// This function will return an error if:
///
/// * The GET request to the GitHub API fails.
/// * The response from the GitHub API cannot be deserialized into a vector of `UpstreamVersion` objects.
/// * The stable release or the version of the stable release cannot be found.
/// This function returns a `Result` that contains a `String` representing the tag name of the stable version if the operation was successful.
/// If the operation failed, the function returns `Err` with a description of the error.
///
/// # Example
///
/// ```rust
/// let client = Client::new();
/// let stable_version = search_stable_version(&client).await.unwrap();
/// println!("The stable version is {}", stable_version);
/// ``
async fn search_stable_version(client: &Client) -> Result<String> {
/// let stable_version = search_stable_version(&client).await?;
/// ```
pub async fn search_stable_version(client: &Client) -> Result<String> {
let response = client
.get("https://api.github.com/repos/neovim/neovim/releases?per_page=10")
.header("user-agent", "bob")
Expand Down

0 comments on commit eb0fd7a

Please sign in to comment.