Skip to content

Commit

Permalink
add ability to get workspace by name and clear workspaces. as well as…
Browse files Browse the repository at this point in the history
… cli param for --remote on list workspaces
  • Loading branch information
gschoeni committed Dec 21, 2024
1 parent 96c0e60 commit a0c4ccb
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 37 deletions.
4 changes: 4 additions & 0 deletions src/cli/src/cmd/workspace.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
pub mod add;
pub use add::WorkspaceAddCmd;

pub mod clear;
pub use clear::WorkspaceClearCmd;

pub mod create;
pub use create::WorkspaceCreateCmd;

Expand Down Expand Up @@ -80,6 +83,7 @@ impl WorkspaceCmd {
fn get_subcommands(&self) -> HashMap<String, Box<dyn RunCmd>> {
let commands: Vec<Box<dyn RunCmd>> = vec![
Box::new(WorkspaceAddCmd),
Box::new(WorkspaceClearCmd),
Box::new(WorkspaceCommitCmd),
Box::new(WorkspaceCreateCmd),
Box::new(WorkspaceDfCmd),
Expand Down
66 changes: 66 additions & 0 deletions src/cli/src/cmd/workspace/clear.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use async_trait::async_trait;
use clap::{ArgMatches, Command};

use dialoguer::Confirm;
use liboxen::api;
use liboxen::{error::OxenError, model::LocalRepository};

use crate::cmd::RunCmd;
pub const NAME: &str = "clear";
pub struct WorkspaceClearCmd;

#[async_trait]
impl RunCmd for WorkspaceClearCmd {
fn name(&self) -> &str {
NAME
}

fn args(&self) -> Command {
Command::new(NAME).about("Clears all workspaces").arg(
clap::Arg::new("remote")
.short('r')
.long("remote")
.help("Remote repository name")
.required(false),
)
}

async fn run(&self, args: &ArgMatches) -> Result<(), OxenError> {
let repository = LocalRepository::from_current_dir()?;
let remote_name = args.get_one::<String>("remote");
let remote_repo = match remote_name {
Some(name) => {
let remote = repository
.get_remote(name)
.ok_or(OxenError::remote_not_set(name))?;
api::client::repositories::get_by_remote(&remote)
.await?
.ok_or(OxenError::remote_not_found(remote))?
}
None => api::client::repositories::get_default_remote(&repository).await?,
};

match Confirm::new()
.with_prompt(format!(
"Are you sure you want to clear all workspaces for remote: {}?",
remote_repo.name
))
.interact()
{
Ok(true) => {
api::client::workspaces::clear(&remote_repo).await?;
println!("All workspaces cleared");
}
Ok(false) => {
return Ok(());
}
Err(e) => {
return Err(OxenError::basic_str(format!(
"Error confirming deletion: {e}"
)));
}
}

Ok(())
}
}
24 changes: 21 additions & 3 deletions src/cli/src/cmd/workspace/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,30 @@ impl RunCmd for WorkspaceListCmd {
}

fn args(&self) -> Command {
Command::new(NAME).about("Lists all workspaces")
Command::new(NAME).about("Lists all workspaces").arg(
clap::Arg::new("remote")
.short('r')
.long("remote")
.help("Remote repository name")
.required(false),
)
}

async fn run(&self, _args: &ArgMatches) -> Result<(), OxenError> {
async fn run(&self, args: &ArgMatches) -> Result<(), OxenError> {
let repository = LocalRepository::from_current_dir()?;
let remote_repo = api::client::repositories::get_default_remote(&repository).await?;
let remote_name = args.get_one::<String>("remote");
let remote_repo = match remote_name {
Some(name) => {
let remote = repository
.get_remote(name)
.ok_or(OxenError::remote_not_set(name))?;
api::client::repositories::get_by_remote(&remote)
.await?
.ok_or(OxenError::remote_not_found(remote))?
}
None => api::client::repositories::get_default_remote(&repository).await?,
};

let workspaces = api::client::workspaces::list(&remote_repo).await?;
for workspace in workspaces {
println!("id\tname\tcommit_id\tcommit_message");
Expand Down
117 changes: 116 additions & 1 deletion src/lib/src/api/client/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::error::OxenError;
use crate::model::RemoteRepository;
use crate::view::workspaces::ListWorkspaceResponseView;
use crate::view::workspaces::{NewWorkspace, WorkspaceResponse};
use crate::view::WorkspaceResponseView;
use crate::view::{StatusMessage, WorkspaceResponseView};

pub async fn list(remote_repo: &RemoteRepository) -> Result<Vec<WorkspaceResponse>, OxenError> {
let url = api::endpoint::url_from_repo(remote_repo, "/workspaces")?;
Expand Down Expand Up @@ -48,6 +48,36 @@ pub async fn get(
}
}

pub async fn get_by_name(
remote_repo: &RemoteRepository,
name: impl AsRef<str>,
) -> Result<Option<WorkspaceResponse>, OxenError> {
let name = name.as_ref();
let url = api::endpoint::url_from_repo(remote_repo, &format!("/workspaces?name={name}"))?;
let client = client::new_for_url(&url)?;
let res = client.get(&url).send().await?;
let body = client::parse_json_body(&url, res).await?;
let response: Result<ListWorkspaceResponseView, serde_json::Error> =
serde_json::from_str(&body);
match response {
Ok(val) => {
if val.workspaces.len() == 1 {
Ok(Some(val.workspaces[0].clone()))
} else if val.workspaces.is_empty() {
Ok(None)
} else {
Err(OxenError::basic_str(format!(
"expected 1 workspace, got {}",
val.workspaces.len()
)))
}
}
Err(err) => Err(OxenError::basic_str(format!(
"error parsing response from {url}\n\nErr {err:?} \n\n{body}"
))),
}
}

pub async fn create(
remote_repo: &RemoteRepository,
branch_name: impl AsRef<str>,
Expand Down Expand Up @@ -132,6 +162,24 @@ pub async fn delete(
}
}

pub async fn clear(remote_repo: &RemoteRepository) -> Result<(), OxenError> {
let url = api::endpoint::url_from_repo(remote_repo, "/workspaces")?;
log::debug!("clear workspaces {}\n", url);

let client = client::new_for_url(&url)?;
let res = client.delete(&url).send().await?;

let body = client::parse_json_body(&url, res).await?;
log::debug!("delete workspace got body: {}", body);
let response: Result<StatusMessage, serde_json::Error> = serde_json::from_str(&body);
match response {
Ok(_) => Ok(()),
Err(err) => Err(OxenError::basic_str(format!(
"error parsing response from {url}\n\nErr {err:?} \n\n{body}"
))),
}
}

#[cfg(test)]
mod tests {

Expand Down Expand Up @@ -177,6 +225,73 @@ mod tests {
assert_eq!(workspace.name, Some(workspace_name.to_string()));
assert_eq!(workspace.id, workspace_id);

let workspace = get_by_name(&remote_repo, &workspace_name).await?;
assert!(workspace.is_some());
assert_eq!(
workspace.as_ref().unwrap().name,
Some(workspace_name.to_string())
);
assert_eq!(workspace.as_ref().unwrap().id, workspace_id);

Ok(remote_repo)
})
.await
}

#[tokio::test]
async fn test_get_workspace_by_name() -> Result<(), OxenError> {
test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
let branch_name = "main";
let workspace_id = "test_workspace_id";
let workspace_name = "test_workspace_name";
create_with_name(&remote_repo, branch_name, workspace_id, workspace_name).await?;

// Create a second workspace with a different name
let workspace_id2 = "test_workspace_id2";
let workspace_name2 = "test_workspace_name2";
create_with_name(&remote_repo, branch_name, workspace_id2, workspace_name2).await?;

let workspace = get_by_name(&remote_repo, &workspace_name).await?;
assert!(workspace.is_some());
assert_eq!(
workspace.as_ref().unwrap().name,
Some(workspace_name.to_string())
);
assert_eq!(workspace.as_ref().unwrap().id, workspace_id);

let workspace2 = get_by_name(&remote_repo, &workspace_name2).await?;
assert!(workspace2.is_some());
assert_eq!(
workspace2.as_ref().unwrap().name,
Some(workspace_name2.to_string())
);
assert_eq!(workspace2.as_ref().unwrap().id, workspace_id2);

Ok(remote_repo)
})
.await
}

#[tokio::test]
async fn test_clear_workspaces() -> Result<(), OxenError> {
test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
// Create 10 workspaces
for i in 0..10 {
create(
&remote_repo,
DEFAULT_BRANCH_NAME,
&format!("test_workspace_{i}"),
)
.await?;
}

// Clear them
clear(&remote_repo).await?;

// Check they are gone
let workspaces = list(&remote_repo).await?;
assert_eq!(workspaces.len(), 0);

Ok(remote_repo)
})
.await
Expand Down
1 change: 1 addition & 0 deletions src/lib/src/model/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct WorkspaceConfig {
pub workspace_commit_id: String,
pub is_editable: bool,
pub workspace_name: String,
pub workspace_id: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
Expand Down
52 changes: 24 additions & 28 deletions src/lib/src/repositories/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,19 @@ pub fn get(repo: &LocalRepository, workspace_id: impl AsRef<str>) -> Result<Work
log::debug!("workspace::get workspace_id: {workspace_id:?} hash: {workspace_id_hash:?}");

let workspace_dir = Workspace::workspace_dir(repo, &workspace_id_hash);
get_by_dir(repo, workspace_dir)
}

pub fn get_by_dir(
repo: &LocalRepository,
workspace_dir: impl AsRef<Path>,
) -> Result<Workspace, OxenError> {
let workspace_dir = workspace_dir.as_ref();
let config_path = workspace_dir.join(OXEN_HIDDEN_DIR).join(WORKSPACE_CONFIG);
let workspace_id = workspace_dir.file_name().unwrap().to_str().unwrap();

if !config_path.exists() {
log::debug!("workspace::get workspace not found: {:?}", workspace_dir);
return Err(OxenError::workspace_not_found(workspace_id.into()));
}

Expand All @@ -47,10 +57,10 @@ pub fn get(repo: &LocalRepository, workspace_id: impl AsRef<str>) -> Result<Work
};

Ok(Workspace {
id: workspace_id.to_owned(),
id: config.workspace_id.unwrap_or(workspace_id.to_owned()),
name: Some(config.workspace_name),
base_repo: repo.clone(),
workspace_repo: LocalRepository::new(&workspace_dir)?,
workspace_repo: LocalRepository::new(workspace_dir)?,
commit,
is_editable: config.is_editable,
})
Expand Down Expand Up @@ -115,6 +125,7 @@ pub fn create_with_name(
workspace_commit_id: commit.id.clone(),
is_editable,
workspace_name: workspace_name.to_string(),
workspace_id: Some(workspace_id.to_string()),
};

let toml_string = match toml::to_string(&workspace_config) {
Expand Down Expand Up @@ -165,33 +176,8 @@ pub fn list(repo: &LocalRepository) -> Result<Vec<Workspace>, OxenError> {

let mut workspaces = Vec::new();
for workspace_hash in workspaces_hashes {
let workspace_config_path = workspace_hash.join(OXEN_HIDDEN_DIR).join(WORKSPACE_CONFIG);

if !workspace_config_path.exists() {
log::warn!("Workspace config not found at: {:?}", workspace_config_path);
continue;
}

// Read the workspace config file
let config_toml = match util::fs::read_from_path(&workspace_config_path) {
Ok(content) => content,
Err(e) => {
log::error!("Failed to read workspace config: {}", e);
continue;
}
};

// Deserialize the TOML content
let workspace_config: WorkspaceConfig = match toml::from_str(&config_toml) {
Ok(config) => config,
Err(e) => {
log::error!("Failed to deserialize workspace config: {}", e);
continue;
}
};

// Construct the Workspace and add it to the list
match get(repo, workspace_config.workspace_name) {
match get_by_dir(repo, workspace_hash) {
Ok(workspace) => workspaces.push(workspace),
Err(e) => {
log::error!("Failed to create workspace: {}", e);
Expand Down Expand Up @@ -240,6 +226,16 @@ pub fn delete(workspace: &Workspace) -> Result<(), OxenError> {
Ok(())
}

pub fn clear(repo: &LocalRepository) -> Result<(), OxenError> {
let workspaces_dir = Workspace::workspaces_dir(repo);
if !workspaces_dir.exists() {
return Ok(());
}

util::fs::remove_dir_all(&workspaces_dir)?;
Ok(())
}

pub fn commit(
workspace: &Workspace,
new_commit: &NewCommitBody,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/src/view/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct NewWorkspace {

// HACK to get this to work with our hub where we don't keep parent_ids 🤦‍♂️
// TODO: it should just be a Commit object
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct WorkspaceCommit {
pub id: String,
pub message: String,
Expand All @@ -41,7 +41,7 @@ impl From<WorkspaceCommit> for Commit {
}
}

#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct WorkspaceResponse {
pub id: String,
pub name: Option<String>,
Expand Down
Loading

0 comments on commit a0c4ccb

Please sign in to comment.