Skip to content
Merged
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: 3 additions & 1 deletion crates/liboxen/src/model/staged_dir_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ use std::hash::{Hash, Hasher};
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use super::StagedEntryStatus;

// Used for a quick summary of directory
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
pub struct StagedDirStats {
#[schema(value_type = String)]
pub path: PathBuf,
pub num_files_staged: usize,
pub total_files: usize,
Expand Down
4 changes: 3 additions & 1 deletion crates/liboxen/src/model/summarized_staged_dir_stats.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::model::StagedDirStats;

Expand All @@ -22,10 +23,11 @@ use std::path::{Path, PathBuf};
/// Rolled up to:
/// annotations/ -> num_staged: 3, total: 4

#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
pub struct SummarizedStagedDirStats {
pub num_files_staged: usize,
pub total_files: usize,
#[schema(value_type = HashMap<String, Vec<StagedDirStats>>)]
pub paths: HashMap<PathBuf, Vec<StagedDirStats>>,
}

Expand Down
5 changes: 3 additions & 2 deletions crates/liboxen/src/view/remote_staged_status.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{collections::HashMap, path::PathBuf};

use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::{
model::{
Expand All @@ -13,15 +14,15 @@ use crate::{
use super::{PaginatedDirEntries, StatusMessage, entries::EMetadataEntry};

// TODO: Removed dirs
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, ToSchema)]
pub struct RemoteStagedStatus {
pub added_dirs: SummarizedStagedDirStats,
pub added_files: PaginatedDirEntries,
pub modified_files: PaginatedDirEntries,
pub removed_files: PaginatedDirEntries,
}

#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, ToSchema)]
pub struct RemoteStagedStatusResponse {
#[serde(flatten)]
pub status: StatusMessage,
Expand Down
8 changes: 8 additions & 0 deletions crates/oxen-py/src/py_workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ impl PyWorkspace {
Ok(())
}

fn unstage(&self, path: PathBuf) -> Result<(), PyOxenError> {
pyo3_async_runtimes::tokio::get_runtime().block_on(async {
api::client::workspaces::changes::rm(self.repo.repo()?, &self.get_identifier(), path)
.await
})?;
Ok(())
}

fn delete(&self) -> Result<(), PyOxenError> {
pyo3_async_runtimes::tokio::get_runtime().block_on(async {
api::client::workspaces::delete(self.repo.repo()?, &self.id).await
Expand Down
3 changes: 2 additions & 1 deletion crates/oxen-server/src/controllers/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,8 @@ pub async fn mergeability(req: HttpRequest) -> Result<HttpResponse, OxenHttpErro
(status = 200, description = "Workspace committed successfully", body = CommitResponse),
(status = 400, description = "Invalid request body"),
(status = 404, description = "Workspace or branch not found"),
(status = 422, description = "Unprocessable Entity, e.g., workspace is behind main branch")
(status = 409, description = "Conflict — a staged file also changed on the target branch since the workspace's base commit"),
(status = 422, description = "Unprocessable Entity — the commit failed for another reason")
)
)]
pub async fn commit(req: HttpRequest, body: String) -> Result<HttpResponse, OxenHttpError> {
Expand Down
49 changes: 49 additions & 0 deletions crates/oxen-server/src/controllers/workspaces/changes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ use actix_web::{HttpRequest, HttpResponse, web};

use std::path::PathBuf;

/// List staged changes in a workspace
#[utoipa::path(
get,
path = "/api/repos/{namespace}/{repo_name}/workspaces/{workspace_id}/changes",
description = "List the staged changes (added, modified, and removed files) in a workspace. The added, modified, and removed lists are each paginated independently, with the same page and page_size applied to each list.",
tag = "Workspace Files",
params(
("namespace" = String, Path, description = "The namespace of the repository", example = "ox"),
("repo_name" = String, Path, description = "The name of the repository", example = "ImageNet-1k"),
("workspace_id" = String, Path, description = "The UUID of the workspace", example = "580c0587-c157-417b-9118-8686d63d2745"),
("page" = Option<usize>, Query, description = "Page number for pagination (default 1)"),
("page_size" = Option<usize>, Query, description = "Number of entries per page (default 100, must be at least 1)")
),
responses(
(status = 200, description = "Staged changes in the workspace", body = RemoteStagedStatusResponse),
(status = 400, description = "Invalid page_size"),
(status = 404, description = "Workspace not found")
)
)]
pub async fn list_root(
req: HttpRequest,
query: web::Query<PageNumQuery>,
Expand All @@ -31,6 +50,11 @@ pub async fn list_root(
let repo = get_repo(app_data, namespace, repo_name)?;
let page_num = query.page.unwrap_or(constants::DEFAULT_PAGE_NUM);
let page_size = query.page_size.unwrap_or(constants::DEFAULT_PAGE_SIZE);
if page_size == 0 {
return Err(OxenHttpError::BadRequest(
"page_size must be at least 1".into(),
));
}

log::debug!("/changes looking up workspace_id: {workspace_id}");
let Some(workspace) = repositories::workspaces::get(&repo, &workspace_id)? else {
Expand All @@ -55,6 +79,26 @@ pub async fn list_root(
Ok(HttpResponse::Ok().json(response))
}

/// List staged changes under a directory in a workspace
#[utoipa::path(
get,
path = "/api/repos/{namespace}/{repo_name}/workspaces/{workspace_id}/changes/{path}",
description = "List the staged changes (added, modified, and removed files) under a directory in a workspace. The added, modified, and removed lists are each paginated independently, with the same page and page_size applied to each list.",
tag = "Workspace Files",
params(
("namespace" = String, Path, description = "The namespace of the repository", example = "ox"),
("repo_name" = String, Path, description = "The name of the repository", example = "ImageNet-1k"),
("workspace_id" = String, Path, description = "The UUID of the workspace", example = "580c0587-c157-417b-9118-8686d63d2745"),
("path" = String, Path, description = "The directory to list staged changes under", example = "images/train"),
("page" = Option<usize>, Query, description = "Page number for pagination (default 1)"),
("page_size" = Option<usize>, Query, description = "Number of entries per page (default 100, must be at least 1)")
),
responses(
(status = 200, description = "Staged changes under the directory", body = RemoteStagedStatusResponse),
(status = 400, description = "Invalid page_size"),
(status = 404, description = "Workspace not found")
)
)]
pub async fn list(
req: HttpRequest,
query: web::Query<PageNumQuery>,
Expand All @@ -69,6 +113,11 @@ pub async fn list(
let path = PathBuf::from(path_param(&req, "path")?);
let page_num = query.page.unwrap_or(constants::DEFAULT_PAGE_NUM);
let page_size = query.page_size.unwrap_or(constants::DEFAULT_PAGE_SIZE);
if page_size == 0 {
return Err(OxenHttpError::BadRequest(
"page_size must be at least 1".into(),
));
}

log::debug!("/changes looking up workspace_id: {workspace_id}");
let Some(workspace) = repositories::workspaces::get(&repo, &workspace_id)? else {
Expand Down
2 changes: 1 addition & 1 deletion crates/oxen-server/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ impl error::ResponseError for OxenHttpError {
"status_message": MSG_CONFLICT,
});

HttpResponse::NotFound().json(error_json)
HttpResponse::Conflict().json(error_json)
}
OxenHttpError::DatasetAlreadyIndexed(path) => {
let error_json = json!({
Expand Down
2 changes: 2 additions & 0 deletions crates/oxen-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ const START_SERVER_USAGE: &str = "Usage: `oxen-server start -i 0.0.0.0 -p 3000`"
crate::controllers::workspaces::mergeability,
crate::controllers::workspaces::commit,
// Workspaces - changes
crate::controllers::workspaces::changes::list_root,
crate::controllers::workspaces::changes::list,
crate::controllers::workspaces::changes::unstage,
crate::controllers::workspaces::changes::unstage_many,
// Workspaces - files
Expand Down
18 changes: 16 additions & 2 deletions oxen-python/python/oxen/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,28 @@ def add_bytes(self, src: str, buf: bytes, dst: str = "") -> None:

def rm(self, path: str) -> None:
"""
Remove a file from the workspace
Unstage a file that was previously added to the workspace.
Despite the name, this does not stage a deletion of a file in
the base repo. Prefer `unstage`, which does the same thing
through the non-deprecated endpoint.

Args:
path: `str`
The path to the file on workspace to be removed
The path to the staged file to unstage
"""
self._workspace.rm(path)

def unstage(self, path: str) -> None:
"""
Unstage a file that was previously added to the workspace,
without touching the base repo.

Args:
path: `str`
The path to the staged file to unstage
"""
self._workspace.unstage(path)

def commit(
self,
message: str,
Expand Down
Loading