diff --git a/oxen-rust/src/cli/src/cmd/workspace/list.rs b/oxen-rust/src/cli/src/cmd/workspace/list.rs index fb86fbd39..26f39e1a4 100644 --- a/oxen-rust/src/cli/src/cmd/workspace/list.rs +++ b/oxen-rust/src/cli/src/cmd/workspace/list.rs @@ -1,7 +1,11 @@ use async_trait::async_trait; use clap::{ArgMatches, Command}; +use colored::Colorize; +use liboxen::view::RemoteStagedStatus; +use std::path::Path; use liboxen::api; +use liboxen::constants; use liboxen::{error::OxenError, model::LocalRepository}; use crate::cmd::RunCmd; @@ -45,16 +49,100 @@ impl RunCmd for WorkspaceListCmd { return Ok(()); } - println!("id\tname\tcommit_id\tcommit_message"); + println!("id\tname\tcommit_id\tcommit_message\tstatus"); for workspace in workspaces { + let ws_changes: WorkspaceChanges = api::client::workspaces::changes::list( + &remote_repo, + &workspace.id, + Path::new(""), + constants::DEFAULT_PAGE_NUM, + constants::DEFAULT_PAGE_SIZE, + ) + .await + .into(); + println!( - "{}\t{}\t{}\t{}", + "{}\t{}\t{}\t{}\t{}", workspace.id, workspace.name.unwrap_or("".to_string()), workspace.commit.id, - workspace.commit.message + workspace.commit.message, + ws_changes, ); } Ok(()) } } + +/// The status of file changes in a workspace. +#[derive(Debug, Clone, PartialEq, Eq)] +enum WorkspaceChanges { + /// There are no changes in the workspace as compared to its commit. + Clean, + /// There are some file changes: tracks added, modified, and deleted files. + Changes { + added: usize, + modified: usize, + deleted: usize, + }, + /// Unable to determine the status of the workspace. + Unknown, +} + +/// Converts a Result into either a Clean or Changes if its Ok otherwise its Unknown. +impl From> for WorkspaceChanges { + fn from(result: Result) -> Self { + match result { + Ok(status) => (&status).into(), + Err(_) => WorkspaceChanges::Unknown, + } + } +} + +/// Converts a RemoteStagedStatus into either a Clean or Changes variant of a WorkspaceChanges. +impl From<&RemoteStagedStatus> for WorkspaceChanges { + fn from(status: &RemoteStagedStatus) -> Self { + let added = status.added_files.total_entries; + let modified = status.modified_files.total_entries; + let deleted = status.removed_files.total_entries; + + if added == 0 && modified == 0 && deleted == 0 { + WorkspaceChanges::Clean + } else { + WorkspaceChanges::Changes { + added, + modified, + deleted, + } + } + } +} + +impl std::fmt::Display for WorkspaceChanges { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WorkspaceChanges::Clean => write!(f, "clean"), + WorkspaceChanges::Changes { + added, + modified, + deleted, + } => { + let status_entry = { + let mut parts = Vec::new(); + if *added > 0 { + parts.push(format!("{}", format!("+{}", added).green())); + } + if *modified > 0 { + parts.push(format!("{}", format!("~{}", modified).yellow())); + } + if *deleted > 0 { + parts.push(format!("{}", format!("-{}", deleted).red())); + } + parts.join(" ") + }; + write!(f, "{}", status_entry) + } + WorkspaceChanges::Unknown => write!(f, "unknown"), + } + } +} diff --git a/oxen-rust/src/lib/src/api/client/workspaces.rs b/oxen-rust/src/lib/src/api/client/workspaces.rs index af4ad92d0..24246781b 100644 --- a/oxen-rust/src/lib/src/api/client/workspaces.rs +++ b/oxen-rust/src/lib/src/api/client/workspaces.rs @@ -190,6 +190,8 @@ mod tests { use super::*; + use std::path::Path; + use crate::api; use crate::command; use crate::constants; @@ -622,4 +624,61 @@ mod tests { }) .await } + + #[tokio::test] + async fn test_list_workspaces_with_file_changes() -> Result<(), OxenError> { + test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move { + let branch_name = DEFAULT_BRANCH_NAME; + + // Create two workspaces - one clean, one with changes + let workspace_clean_id = "workspace_clean"; + let workspace_with_changes_id = "workspace_with_changes"; + + create(&remote_repo, branch_name, workspace_clean_id).await?; + create(&remote_repo, branch_name, workspace_with_changes_id).await?; + + // Add a file to workspace_with_changes + let test_file = test::test_img_file(); + api::client::workspaces::files::upload_single_file( + &remote_repo, + workspace_with_changes_id, + "", + &test_file, + ) + .await?; + + // List all workspaces + let workspaces = list(&remote_repo).await?; + assert_eq!(workspaces.len(), 2); + + // Verify the clean workspace has no changes + let clean_status = api::client::workspaces::changes::list( + &remote_repo, + workspace_clean_id, + Path::new(""), + constants::DEFAULT_PAGE_NUM, + constants::DEFAULT_PAGE_SIZE, + ) + .await?; + assert_eq!(clean_status.added_files.total_entries, 0); + assert_eq!(clean_status.modified_files.total_entries, 0); + assert_eq!(clean_status.removed_files.total_entries, 0); + + // Verify the workspace with changes has 1 added file + let changes_status = api::client::workspaces::changes::list( + &remote_repo, + workspace_with_changes_id, + Path::new(""), + constants::DEFAULT_PAGE_NUM, + constants::DEFAULT_PAGE_SIZE, + ) + .await?; + assert_eq!(changes_status.added_files.total_entries, 1); + assert_eq!(changes_status.modified_files.total_entries, 0); + assert_eq!(changes_status.removed_files.total_entries, 0); + + Ok(remote_repo) + }) + .await + } }