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
91 changes: 81 additions & 10 deletions src/commands/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,26 @@ use super::{Command, CommandContext};
use anyhow::Result;
use async_trait::async_trait;
use colored::*;
use serde::Serialize;

/// Output format for a repository in JSON mode
#[derive(Serialize)]
struct RepositoryOutput {
name: String,
url: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
branch: Option<String>,
}

/// List command for displaying repositories with optional filtering
pub struct ListCommand;
pub struct ListCommand {
/// Output in JSON format
pub json: bool,
}

#[async_trait]
impl Command for ListCommand {
Expand All @@ -17,6 +34,24 @@ impl Command for ListCommand {
context.repos.as_deref(),
);

if self.json {
// JSON output mode
let output: Vec<RepositoryOutput> = repositories
.iter()
.map(|repo| RepositoryOutput {
name: repo.name.clone(),
url: repo.url.clone(),
tags: repo.tags.clone(),
path: repo.path.clone(),
branch: repo.branch.clone(),
})
.collect();

println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}

// Human-readable output mode
if repositories.is_empty() {
let mut filter_parts = Vec::new();

Expand Down Expand Up @@ -130,7 +165,7 @@ mod tests {
#[tokio::test]
async fn test_list_command_all_repositories() {
let config = create_test_config();
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(config, vec![], vec![], None);

Expand All @@ -141,7 +176,7 @@ mod tests {
#[tokio::test]
async fn test_list_command_with_tag_filter() {
let config = create_test_config();
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(config, vec!["frontend".to_string()], vec![], None);

Expand All @@ -152,7 +187,7 @@ mod tests {
#[tokio::test]
async fn test_list_command_with_exclude_tag() {
let config = create_test_config();
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(config, vec![], vec!["backend".to_string()], None);

Expand All @@ -163,7 +198,7 @@ mod tests {
#[tokio::test]
async fn test_list_command_with_both_filters() {
let config = create_test_config();
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(
config,
Expand All @@ -179,7 +214,7 @@ mod tests {
#[tokio::test]
async fn test_list_command_no_matches() {
let config = create_test_config();
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(config, vec!["nonexistent".to_string()], vec![], None);

Expand All @@ -190,7 +225,7 @@ mod tests {
#[tokio::test]
async fn test_list_command_with_repo_filter() {
let config = create_test_config();
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(
config,
Expand All @@ -209,7 +244,7 @@ mod tests {
repositories: vec![],
recipes: vec![],
};
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(config, vec![], vec![], None);

Expand All @@ -220,7 +255,7 @@ mod tests {
#[tokio::test]
async fn test_list_command_multiple_tags() {
let config = create_test_config();
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(
config,
Expand All @@ -236,7 +271,7 @@ mod tests {
#[tokio::test]
async fn test_list_command_combined_filters() {
let config = create_test_config();
let command = ListCommand;
let command = ListCommand { json: false };

let context = create_context(
config,
Expand All @@ -248,4 +283,40 @@ mod tests {
let result = command.execute(&context).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_list_command_json_output() {
let config = create_test_config();
let command = ListCommand { json: true };

let context = create_context(config, vec![], vec![], None);

let result = command.execute(&context).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_list_command_json_with_filters() {
let config = create_test_config();
let command = ListCommand { json: true };

let context = create_context(config, vec!["frontend".to_string()], vec![], None);

let result = command.execute(&context).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_list_command_json_empty() {
let config = Config {
repositories: vec![],
recipes: vec![],
};
let command = ListCommand { json: true };

let context = create_context(config, vec![], vec![], None);

let result = command.execute(&context).await;
assert!(result.is_ok());
}
}
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ enum Commands {
/// Exclude repositories with these tags (can be specified multiple times)
#[arg(short = 'e', long)]
exclude_tag: Vec<String>,

/// Output in JSON format for machine consumption
#[arg(long)]
json: bool,
},

/// Create a config.yaml file from discovered Git repositories
Expand Down Expand Up @@ -449,6 +453,7 @@ async fn execute_builtin_command(command: Commands) -> Result<()> {
config,
tag,
exclude_tag,
json,
} => {
let config = Config::load_config(&config)?;

Expand All @@ -464,7 +469,7 @@ async fn execute_builtin_command(command: Commands) -> Result<()> {
parallel: false, // List command doesn't need parallel execution
repos: if repos.is_empty() { None } else { Some(repos) },
};
ListCommand.execute(&context).await?;
ListCommand { json }.execute(&context).await?;
}
Commands::Init {
output,
Expand Down