From c932636742d38d4376daca504fb41524e0ecb45e Mon Sep 17 00:00:00 2001 From: Jordan Morano Date: Mon, 20 Oct 2025 10:05:17 -0400 Subject: [PATCH 1/2] Add CLI command to list Git-style references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a new `icechunk ref list` command that displays all branches and tags in an Icechunk repository, similar to `git show-ref`. This addresses the need for users to easily view all references in a repository from the command line without needing to use the Python API. Changes: - Add `ref` subcommand with `list` operation to CLI - Implement `ref_list()` handler that calls Repository::list_branches() and Repository::list_tags() - Format output as `branch: ` and `tag: ` for clarity - Add comprehensive test that creates repository with multiple branches and tags and verifies correct output Test coverage: - Added `test_ref_list()` that creates a repo with main branch, feature branch, and v1.0 tag, then verifies all refs appear in output - Test passes with `cargo test --lib --features cli` Fixes #827 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- icechunk/src/cli/interface.rs | 97 +++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/icechunk/src/cli/interface.rs b/icechunk/src/cli/interface.rs index d77a26396..4ae4d69e9 100644 --- a/icechunk/src/cli/interface.rs +++ b/icechunk/src/cli/interface.rs @@ -38,6 +38,8 @@ enum Command { Snapshot(SnapshotCommand), #[command(subcommand, about = "Manage configuration")] Config(ConfigCommand), + #[command(subcommand, about = "Manage references (branches and tags)")] + Ref(RefCommand), } #[derive(Debug, Subcommand)] @@ -68,6 +70,12 @@ enum ConfigCommand { List, } +#[derive(Debug, Subcommand)] +enum RefCommand { + #[clap(name = "list", about = "List all references (branches and tags) in a repository")] + List(RefListCommand), +} + #[derive(Debug, Args)] struct CreateCommand { #[arg(name = "alias", help = "Alias of the repository in the config")] @@ -105,6 +113,12 @@ struct ListCommand { branch: String, } +#[derive(Debug, Args)] +struct RefListCommand { + #[arg(name = "alias", help = "Alias of the repository in the config")] + repo: RepositoryAlias, +} + const CONFIG_DIR: &str = "icechunk"; const CONFIG_NAME: &str = "cli-config.yaml"; @@ -246,6 +260,34 @@ async fn snapshot_list( Ok(()) } +async fn ref_list( + list_cmd: &RefListCommand, + config: &CliConfig, + mut writer: impl std::io::Write, +) -> Result<()> { + let repo = + config.repos.get(&list_cmd.repo).context("Repository not found in config")?; + let storage = get_storage(repo).await?; + let config = Some(repo.get_config().clone()); + + let repository = Repository::open(config, Arc::clone(&storage), HashMap::new()) + .await + .context(format!("Failed to open repository {:?}", list_cmd.repo))?; + + let branches = repository.list_branches().await?; + let tags = repository.list_tags().await?; + + for branch in branches { + writeln!(writer, "branch: {}", branch)?; + } + + for tag in tags { + writeln!(writer, "tag: {}", tag)?; + } + + Ok(()) +} + async fn config_add(add_cmd: &AddCommand, config: &CliConfig) -> Result { if config.repos.contains_key(&add_cmd.repo) { return Err(anyhow::anyhow!("Repository {:?} already exists", add_cmd.repo)); @@ -422,6 +464,9 @@ pub async fn run_cli(args: IcechunkCLI) -> Result<()> { Command::Snapshot(SnapshotCommand::List(list_cmd)) => { snapshot_list(&list_cmd, &config, stdout()).await } + Command::Ref(RefCommand::List(list_cmd)) => { + ref_list(&list_cmd, &config, stdout()).await + } Command::Config(ConfigCommand::Init(init_cmd)) => { let new_config = config_init(&init_cmd, &config).await?; write_config(&new_config)?; @@ -535,4 +580,56 @@ mod tests { assert!(output.contains("LocalFileSystem")); } + + #[tokio_test] + async fn test_ref_list() { + let temp = assert_fs::TempDir::new().unwrap(); + let path = temp.path().to_path_buf(); + + let repo_alias = RepositoryAlias("test-repo".to_string()); + let repo_def = RepositoryDefinition::LocalFileSystem { + path: path.clone(), + config: RepositoryConfig::default(), + }; + + let mut repos = HashMap::new(); + repos.insert(repo_alias.clone(), repo_def); + + let config = CliConfig { repos }; + + let init_cmd = CreateCommand { repo: repo_alias.clone() }; + + repo_create(&init_cmd, &config).await.unwrap(); + + // Open the repository and create additional branches and tags + let storage = get_storage(config.repos.get(&repo_alias).unwrap()).await.unwrap(); + let repo_config = Some(config.repos.get(&repo_alias).unwrap().get_config().clone()); + let repository = + Repository::open(repo_config, Arc::clone(&storage), HashMap::new()).await.unwrap(); + + // Get the current snapshot ID from main branch + let main_snapshot = repository.lookup_branch("main").await.unwrap(); + + // Create a new branch + repository.create_branch("feature", &main_snapshot).await.unwrap(); + + // Create a tag + repository.create_tag("v1.0", &main_snapshot).await.unwrap(); + + // Now test the ref list command + let list_cmd = RefListCommand { repo: repo_alias.clone() }; + + let mut writer = Vec::new(); + ref_list(&list_cmd, &config, &mut writer).await.unwrap(); + + let output = String::from_utf8(writer).unwrap(); + + // Verify output contains all expected references + assert!(output.contains("branch: main")); + assert!(output.contains("branch: feature")); + assert!(output.contains("tag: v1.0")); + + // Count the lines to ensure we have exactly 3 references + assert_eq!(output.lines().count(), 3); + } } From aa98ae4dcd6fcaecca5b88ae167b7124bad243f9 Mon Sep 17 00:00:00 2001 From: Jordan Morano Date: Mon, 20 Oct 2025 21:14:00 -0400 Subject: [PATCH 2/2] Fix formatting errors --- icechunk/src/cli/interface.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/icechunk/src/cli/interface.rs b/icechunk/src/cli/interface.rs index 4ae4d69e9..cc2967836 100644 --- a/icechunk/src/cli/interface.rs +++ b/icechunk/src/cli/interface.rs @@ -72,7 +72,10 @@ enum ConfigCommand { #[derive(Debug, Subcommand)] enum RefCommand { - #[clap(name = "list", about = "List all references (branches and tags) in a repository")] + #[clap( + name = "list", + about = "List all references (branches and tags) in a repository" + )] List(RefListCommand), } @@ -603,9 +606,12 @@ mod tests { // Open the repository and create additional branches and tags let storage = get_storage(config.repos.get(&repo_alias).unwrap()).await.unwrap(); - let repo_config = Some(config.repos.get(&repo_alias).unwrap().get_config().clone()); + let repo_config = + Some(config.repos.get(&repo_alias).unwrap().get_config().clone()); let repository = - Repository::open(repo_config, Arc::clone(&storage), HashMap::new()).await.unwrap(); + Repository::open(repo_config, Arc::clone(&storage), HashMap::new()) + .await + .unwrap(); // Get the current snapshot ID from main branch let main_snapshot = repository.lookup_branch("main").await.unwrap();