Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions oxen-rust/src/cli/src/cmd/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub use download::WorkspaceDownloadCmd;
pub mod list;
pub use list::WorkspaceListCmd;

pub mod ls;
pub use ls::WorkspaceLsCmd;

pub mod restore;
pub use restore::WorkspaceRestoreCmd;

Expand Down Expand Up @@ -96,6 +99,7 @@ impl WorkspaceCmd {
Box::new(WorkspaceDiffCmd),
Box::new(WorkspaceDeleteCmd),
Box::new(WorkspaceListCmd),
Box::new(WorkspaceLsCmd),
Box::new(WorkspaceRmCmd),
Box::new(WorkspaceRestoreCmd),
Box::new(WorkspaceStatusCmd),
Expand Down
137 changes: 137 additions & 0 deletions oxen-rust/src/cli/src/cmd/workspace/ls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use async_trait::async_trait;
use clap::{Arg, ArgMatches, Command};

use liboxen::api;
use liboxen::error;
use liboxen::error::OxenError;
use liboxen::model::LocalRepository;
use liboxen::util;

use std::path::PathBuf;

use crate::cmd::RunCmd;
pub const NAME: &str = "ls";
pub struct WorkspaceLsCmd;

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

fn args(&self) -> Command {
Command::new(NAME)
.about("List files in a workspace directory")
.arg(
Arg::new("workspace-id")
.long("workspace-id")
.short('w')
.required_unless_present("workspace-name")
.conflicts_with("workspace-name")
.help("The workspace_id of the workspace"),
)
.arg(
Arg::new("workspace-name")
.long("workspace-name")
.short('n')
.required_unless_present("workspace-id")
.conflicts_with("workspace-id")
.help("The name of the workspace"),
)
.arg(Arg::new("path").help("Directory path to list (defaults to root)"))
.arg(
Arg::new("page")
.long("page")
.short('p')
.help("Page number")
.default_value("1")
.action(clap::ArgAction::Set),
)
.arg(
Arg::new("page-size")
.long("page-size")
.help("Number of entries per page")
.default_value("100")
.action(clap::ArgAction::Set),
)
}

async fn run(&self, args: &ArgMatches) -> Result<(), OxenError> {
let workspace_id = args.get_one::<String>("workspace-id");
let workspace_name = args.get_one::<String>("workspace-name");
let workspace_identifier = match workspace_id {
Some(id) => id,
None => {
if let Some(name) = workspace_name {
name
} else {
return Err(OxenError::basic_str(
"Either workspace-id or workspace-name must be provided.",
));
}
}
};

let path = args
.get_one::<String>("path")
.map(PathBuf::from)
.unwrap_or_default();

let page: usize = args
.get_one::<String>("page")
.expect("Must supply page")
.parse()
.expect("page must be a valid integer");
let page_size: usize = args
.get_one::<String>("page-size")
.expect("Must supply page-size")
.parse()
.expect("page-size must be a valid integer");

let repo_dir = util::fs::get_repo_root_from_current_dir()
.ok_or(OxenError::basic_str(error::NO_REPO_FOUND))?;
let repository = LocalRepository::from_dir(&repo_dir)?;
let remote_repo = api::client::repositories::get_default_remote(&repository).await?;

let result = api::client::workspaces::ls::list(
&remote_repo,
workspace_identifier,
&path,
page,
page_size,
)
.await?;

// Print entries
for entry in &result.entries {
let prefix = if entry.is_dir() { "d " } else { " " };
let status = match &entry {
liboxen::view::entries::EMetadataEntry::WorkspaceMetadataEntry(ws) => {
ws.changes.as_ref().map_or("", |c| match c.status {
liboxen::model::StagedEntryStatus::Added => " [added]",
liboxen::model::StagedEntryStatus::Modified => " [modified]",
liboxen::model::StagedEntryStatus::Removed => " [removed]",
_ => "",
})
}
_ => "",
};
println!(
"{}{}{} ({} bytes)",
prefix,
entry.filename(),
status,
entry.size()
);
}

if result.total_pages > 1 {
println!(
"\nPage {}/{} ({} total entries)",
result.page_number, result.total_pages, result.total_entries
);
}

Ok(())
}
}
1 change: 1 addition & 0 deletions oxen-rust/src/lib/src/api/client/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod changes;
pub mod commits;
pub mod data_frames;
pub mod files;
pub mod ls;

use std::path::Path;

Expand Down
152 changes: 152 additions & 0 deletions oxen-rust/src/lib/src/api/client/workspaces/ls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use std::path::Path;

use crate::api;
use crate::api::client;
use crate::error::OxenError;
use crate::model::RemoteRepository;
use crate::view::entries::PaginatedDirEntriesResponse;
use crate::view::PaginatedDirEntries;

pub async fn list(
remote_repo: &RemoteRepository,
workspace_id: impl AsRef<str>,
directory: impl AsRef<Path>,
page: usize,
page_size: usize,
) -> Result<PaginatedDirEntries, OxenError> {
let workspace_id = workspace_id.as_ref();
let path_str = directory.as_ref().to_str().unwrap();
let uri = if path_str.is_empty() || path_str == "." {
format!("/workspaces/{workspace_id}/ls?page={page}&page_size={page_size}")
} else {
format!("/workspaces/{workspace_id}/ls/{path_str}?page={page}&page_size={page_size}")
};
let url = api::endpoint::url_from_repo(remote_repo, &uri)?;
log::debug!("workspaces::ls url: {url}");

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: PaginatedDirEntriesResponse = serde_json::from_str(&body).map_err(|err| {
OxenError::basic_str(format!(
"api::workspaces::ls error parsing response from {url}\n\nErr {err:?}\n\n{body}"
))
})?;
Ok(response.entries)
}

#[cfg(test)]
mod tests {
use crate::config::UserConfig;
use crate::constants::DEFAULT_BRANCH_NAME;
use crate::error::OxenError;
use crate::test;
use crate::view::entries::EMetadataEntry;
use crate::{api, constants};

#[tokio::test]
async fn test_workspace_ls_root() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
let workspace_id = UserConfig::identifier()?;
let _workspace =
api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, &workspace_id)
.await?;

let result = api::client::workspaces::ls::list(
&remote_repo,
&workspace_id,
"",
constants::DEFAULT_PAGE_NUM,
constants::DEFAULT_PAGE_SIZE,
)
.await?;

assert!(!result.entries.is_empty(), "Should return entries at root");

Ok(remote_repo)
})
.await
}

#[tokio::test]
async fn test_workspace_ls_with_additions() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|local_repo, remote_repo| async move {
let workspace_id = UserConfig::identifier()?;
let _workspace =
api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, &workspace_id)
.await?;

// Upload a file to the workspace
let test_file = local_repo.path.join("new_file.txt");
crate::util::fs::write_to_path(&test_file, "new content")?;
api::client::workspaces::files::upload_single_file(
&remote_repo,
&workspace_id,
"",
test_file,
)
.await?;

let result = api::client::workspaces::ls::list(
&remote_repo,
&workspace_id,
"",
constants::DEFAULT_PAGE_NUM,
constants::DEFAULT_PAGE_SIZE,
)
.await?;

// Find the added file
let added = result
.entries
.iter()
.find(|e| e.filename() == "new_file.txt");
assert!(added.is_some(), "Should find newly added file");

if let Some(EMetadataEntry::WorkspaceMetadataEntry(ws_entry)) = added {
assert!(ws_entry.changes.is_some(), "Added file should have changes");
}

Ok(remote_repo)
})
.await
}

#[tokio::test]
async fn test_workspace_ls_with_removals() -> Result<(), OxenError> {
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
let workspace_id = UserConfig::identifier()?;
let _workspace =
api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, &workspace_id)
.await?;

// Remove a file from the workspace
let rm_path = std::path::Path::new("annotations")
.join("train")
.join("bounding_box.csv");
api::client::workspaces::changes::rm(&remote_repo, &workspace_id, &rm_path).await?;

let result = api::client::workspaces::ls::list(
&remote_repo,
&workspace_id,
"annotations/train",
constants::DEFAULT_PAGE_NUM,
constants::DEFAULT_PAGE_SIZE,
)
.await?;

// The removed file should NOT appear in the listing
let removed = result
.entries
.iter()
.find(|e| e.filename() == "bounding_box.csv");
assert!(
removed.is_none(),
"Removed file should not appear in workspace ls"
);

Ok(remote_repo)
})
.await
}
}
31 changes: 18 additions & 13 deletions oxen-rust/src/lib/src/core/v_latest/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ pub fn dir_entries_with_depth(
Ok(entries)
}

fn dir_node_to_metadata_entry(
/// Getting a directory's own metadata entry.
pub(crate) fn dir_node_to_metadata_entry(
repo: &LocalRepository,
node: &MerkleTreeNode,
parsed_resource: &ParsedResource,
Expand All @@ -300,29 +301,33 @@ fn dir_node_to_metadata_entry(
return Ok(None);
};

if let std::collections::hash_map::Entry::Vacant(e) =
found_commits.entry(*dir_node.last_commit_id())
{
let commit = match found_commits.entry(*dir_node.last_commit_id()) {
std::collections::hash_map::Entry::Vacant(e) => {
let _perf_commit = crate::perf_guard!("core::entries::get_commit_by_hash");
let commit = repositories::commits::get_by_hash(repo, dir_node.last_commit_id())?.ok_or(
OxenError::commit_id_does_not_exist(dir_node.last_commit_id().to_string()),
)?;
e.insert(commit);
}
let v = e.insert(commit);
v.as_ref()
},
std::collections::hash_map::Entry::Occupied(e) => e.get(),
};

let commit = found_commits.get(dir_node.last_commit_id()).unwrap();
let mut parsed_resource = parsed_resource.clone();
if should_append_resource {
parsed_resource.resource = parsed_resource.resource.join(dir_node.name());
parsed_resource.path = parsed_resource.path.join(dir_node.name());
}
let parsed_resource = {
let mut parsed_resource = parsed_resource.clone();
if should_append_resource {
parsed_resource.resource = parsed_resource.resource.join(dir_node.name());
parsed_resource.path = parsed_resource.path.join(dir_node.name());
}
parsed_resource
};

Ok(Some(MetadataEntry {
filename: dir_node.name().to_string(),
hash: dir_node.hash().to_string(),
is_dir: true,
latest_commit: Some(commit.clone()),
resource: Some(parsed_resource.clone()),
resource: Some(parsed_resource),
size: dir_node.num_bytes(),
data_type: EntryDataType::Dir,
mime_type: "inode/directory".to_string(),
Expand Down
Loading