Skip to content

Commit 8f17e5b

Browse files
author
Malcolm Greaves
committed
wip
1 parent 1c622bd commit 8f17e5b

File tree

10 files changed

+864
-4
lines changed

10 files changed

+864
-4
lines changed

oxen-rust/src/cli/src/cmd/workspace.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ pub use download::WorkspaceDownloadCmd;
2525
pub mod list;
2626
pub use list::WorkspaceListCmd;
2727

28+
pub mod ls;
29+
pub use ls::WorkspaceLsCmd;
30+
2831
pub mod restore;
2932
pub use restore::WorkspaceRestoreCmd;
3033

@@ -96,6 +99,7 @@ impl WorkspaceCmd {
9699
Box::new(WorkspaceDiffCmd),
97100
Box::new(WorkspaceDeleteCmd),
98101
Box::new(WorkspaceListCmd),
102+
Box::new(WorkspaceLsCmd),
99103
Box::new(WorkspaceRmCmd),
100104
Box::new(WorkspaceRestoreCmd),
101105
Box::new(WorkspaceStatusCmd),
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use async_trait::async_trait;
2+
use clap::{Arg, ArgMatches, Command};
3+
4+
use liboxen::api;
5+
use liboxen::error;
6+
use liboxen::error::OxenError;
7+
use liboxen::model::LocalRepository;
8+
use liboxen::util;
9+
10+
use std::path::PathBuf;
11+
12+
use crate::cmd::RunCmd;
13+
pub const NAME: &str = "ls";
14+
pub struct WorkspaceLsCmd;
15+
16+
#[async_trait]
17+
impl RunCmd for WorkspaceLsCmd {
18+
fn name(&self) -> &str {
19+
NAME
20+
}
21+
22+
fn args(&self) -> Command {
23+
Command::new(NAME)
24+
.about("List files in a workspace directory")
25+
.arg(
26+
Arg::new("workspace-id")
27+
.long("workspace-id")
28+
.short('w')
29+
.required_unless_present("workspace-name")
30+
.conflicts_with("workspace-name")
31+
.help("The workspace_id of the workspace"),
32+
)
33+
.arg(
34+
Arg::new("workspace-name")
35+
.long("workspace-name")
36+
.short('n')
37+
.required_unless_present("workspace-id")
38+
.conflicts_with("workspace-id")
39+
.help("The name of the workspace"),
40+
)
41+
.arg(Arg::new("path").help("Directory path to list (defaults to root)"))
42+
.arg(
43+
Arg::new("page")
44+
.long("page")
45+
.short('p')
46+
.help("Page number")
47+
.default_value("1")
48+
.action(clap::ArgAction::Set),
49+
)
50+
.arg(
51+
Arg::new("page-size")
52+
.long("page-size")
53+
.help("Number of entries per page")
54+
.default_value("100")
55+
.action(clap::ArgAction::Set),
56+
)
57+
}
58+
59+
async fn run(&self, args: &ArgMatches) -> Result<(), OxenError> {
60+
let workspace_id = args.get_one::<String>("workspace-id");
61+
let workspace_name = args.get_one::<String>("workspace-name");
62+
let workspace_identifier = match workspace_id {
63+
Some(id) => id,
64+
None => {
65+
if let Some(name) = workspace_name {
66+
name
67+
} else {
68+
return Err(OxenError::basic_str(
69+
"Either workspace-id or workspace-name must be provided.",
70+
));
71+
}
72+
}
73+
};
74+
75+
let path = args
76+
.get_one::<String>("path")
77+
.map(PathBuf::from)
78+
.unwrap_or_default();
79+
80+
let page: usize = args
81+
.get_one::<String>("page")
82+
.expect("Must supply page")
83+
.parse()
84+
.expect("page must be a valid integer");
85+
let page_size: usize = args
86+
.get_one::<String>("page-size")
87+
.expect("Must supply page-size")
88+
.parse()
89+
.expect("page-size must be a valid integer");
90+
91+
let repo_dir = util::fs::get_repo_root_from_current_dir()
92+
.ok_or(OxenError::basic_str(error::NO_REPO_FOUND))?;
93+
let repository = LocalRepository::from_dir(&repo_dir)?;
94+
let remote_repo = api::client::repositories::get_default_remote(&repository).await?;
95+
96+
let result = api::client::workspaces::ls::list(
97+
&remote_repo,
98+
workspace_identifier,
99+
&path,
100+
page,
101+
page_size,
102+
)
103+
.await?;
104+
105+
// Print entries
106+
for entry in &result.entries {
107+
let prefix = if entry.is_dir() { "d " } else { " " };
108+
let status = match &entry {
109+
liboxen::view::entries::EMetadataEntry::WorkspaceMetadataEntry(ws) => {
110+
ws.changes.as_ref().map_or("", |c| match c.status {
111+
liboxen::model::StagedEntryStatus::Added => " [added]",
112+
liboxen::model::StagedEntryStatus::Modified => " [modified]",
113+
liboxen::model::StagedEntryStatus::Removed => " [removed]",
114+
_ => "",
115+
})
116+
}
117+
_ => "",
118+
};
119+
println!(
120+
"{}{}{} ({} bytes)",
121+
prefix,
122+
entry.filename(),
123+
status,
124+
entry.size()
125+
);
126+
}
127+
128+
if result.total_pages > 1 {
129+
println!(
130+
"\nPage {}/{} ({} total entries)",
131+
result.page_number, result.total_pages, result.total_entries
132+
);
133+
}
134+
135+
Ok(())
136+
}
137+
}

oxen-rust/src/lib/src/api/client/workspaces.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod changes;
22
pub mod commits;
33
pub mod data_frames;
44
pub mod files;
5+
pub mod ls;
56

67
use std::path::Path;
78

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use std::path::Path;
2+
3+
use crate::api;
4+
use crate::api::client;
5+
use crate::error::OxenError;
6+
use crate::model::RemoteRepository;
7+
use crate::view::entries::PaginatedDirEntriesResponse;
8+
use crate::view::PaginatedDirEntries;
9+
10+
pub async fn list(
11+
remote_repo: &RemoteRepository,
12+
workspace_id: impl AsRef<str>,
13+
directory: impl AsRef<Path>,
14+
page: usize,
15+
page_size: usize,
16+
) -> Result<PaginatedDirEntries, OxenError> {
17+
let workspace_id = workspace_id.as_ref();
18+
let path_str = directory.as_ref().to_str().unwrap();
19+
let uri = if path_str.is_empty() || path_str == "." {
20+
format!("/workspaces/{workspace_id}/ls?page={page}&page_size={page_size}")
21+
} else {
22+
format!("/workspaces/{workspace_id}/ls/{path_str}?page={page}&page_size={page_size}")
23+
};
24+
let url = api::endpoint::url_from_repo(remote_repo, &uri)?;
25+
log::debug!("workspaces::ls url: {url}");
26+
27+
let client = client::new_for_url(&url)?;
28+
let res = client.get(&url).send().await?;
29+
let body = client::parse_json_body(&url, res).await?;
30+
let response: PaginatedDirEntriesResponse = serde_json::from_str(&body).map_err(|err| {
31+
OxenError::basic_str(format!(
32+
"api::workspaces::ls error parsing response from {url}\n\nErr {err:?}\n\n{body}"
33+
))
34+
})?;
35+
Ok(response.entries)
36+
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use crate::config::UserConfig;
41+
use crate::constants::DEFAULT_BRANCH_NAME;
42+
use crate::error::OxenError;
43+
use crate::test;
44+
use crate::view::entries::EMetadataEntry;
45+
use crate::{api, constants};
46+
47+
#[tokio::test]
48+
async fn test_workspace_ls_root() -> Result<(), OxenError> {
49+
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
50+
let workspace_id = UserConfig::identifier()?;
51+
let _workspace =
52+
api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, &workspace_id)
53+
.await?;
54+
55+
let result = api::client::workspaces::ls::list(
56+
&remote_repo,
57+
&workspace_id,
58+
"",
59+
constants::DEFAULT_PAGE_NUM,
60+
constants::DEFAULT_PAGE_SIZE,
61+
)
62+
.await?;
63+
64+
assert!(!result.entries.is_empty(), "Should return entries at root");
65+
66+
Ok(remote_repo)
67+
})
68+
.await
69+
}
70+
71+
#[tokio::test]
72+
async fn test_workspace_ls_with_additions() -> Result<(), OxenError> {
73+
test::run_remote_repo_test_bounding_box_csv_pushed(|local_repo, remote_repo| async move {
74+
let workspace_id = UserConfig::identifier()?;
75+
let _workspace =
76+
api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, &workspace_id)
77+
.await?;
78+
79+
// Upload a file to the workspace
80+
let test_file = local_repo.path.join("new_file.txt");
81+
crate::util::fs::write_to_path(&test_file, "new content")?;
82+
api::client::workspaces::files::upload_single_file(
83+
&remote_repo,
84+
&workspace_id,
85+
"",
86+
test_file,
87+
)
88+
.await?;
89+
90+
let result = api::client::workspaces::ls::list(
91+
&remote_repo,
92+
&workspace_id,
93+
"",
94+
constants::DEFAULT_PAGE_NUM,
95+
constants::DEFAULT_PAGE_SIZE,
96+
)
97+
.await?;
98+
99+
// Find the added file
100+
let added = result
101+
.entries
102+
.iter()
103+
.find(|e| e.filename() == "new_file.txt");
104+
assert!(added.is_some(), "Should find newly added file");
105+
106+
if let Some(EMetadataEntry::WorkspaceMetadataEntry(ws_entry)) = added {
107+
assert!(ws_entry.changes.is_some(), "Added file should have changes");
108+
}
109+
110+
Ok(remote_repo)
111+
})
112+
.await
113+
}
114+
115+
#[tokio::test]
116+
async fn test_workspace_ls_with_removals() -> Result<(), OxenError> {
117+
test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
118+
let workspace_id = UserConfig::identifier()?;
119+
let _workspace =
120+
api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, &workspace_id)
121+
.await?;
122+
123+
// Remove a file from the workspace
124+
let rm_path = std::path::Path::new("annotations")
125+
.join("train")
126+
.join("bounding_box.csv");
127+
api::client::workspaces::changes::rm(&remote_repo, &workspace_id, &rm_path).await?;
128+
129+
let result = api::client::workspaces::ls::list(
130+
&remote_repo,
131+
&workspace_id,
132+
"annotations/train",
133+
constants::DEFAULT_PAGE_NUM,
134+
constants::DEFAULT_PAGE_SIZE,
135+
)
136+
.await?;
137+
138+
// The removed file should NOT appear in the listing
139+
let removed = result
140+
.entries
141+
.iter()
142+
.find(|e| e.filename() == "bounding_box.csv");
143+
assert!(
144+
removed.is_none(),
145+
"Removed file should not appear in workspace ls"
146+
);
147+
148+
Ok(remote_repo)
149+
})
150+
.await
151+
}
152+
}

oxen-rust/src/lib/src/core/v_latest/entries.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ pub fn dir_entries_with_depth(
285285
Ok(entries)
286286
}
287287

288+
/// Public wrapper for getting a directory's own metadata entry (without appending resource).
289+
pub fn dir_node_to_metadata_entry_public(
290+
repo: &LocalRepository,
291+
node: &MerkleTreeNode,
292+
parsed_resource: &ParsedResource,
293+
found_commits: &mut HashMap<MerkleHash, Commit>,
294+
) -> Result<Option<MetadataEntry>, OxenError> {
295+
dir_node_to_metadata_entry(repo, node, parsed_resource, found_commits, false)
296+
}
297+
288298
fn dir_node_to_metadata_entry(
289299
repo: &LocalRepository,
290300
node: &MerkleTreeNode,

0 commit comments

Comments
 (0)