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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ overview. Click on a command to see its detailed documentation.
| Command | Description |
|---|---|
| [**`clone`**](./docs/commands/clone.md) | Clones repositories from your config file. |
| [**`ls`**](./docs/commands/ls.md) | Lists repositories with optional filtering. |
| [**`run`**](./docs/commands/run.md) | Runs a shell command or a pre-defined recipe in each repository. |
| [**`pr`**](./docs/commands/pr.md) | Creates pull requests for repositories with changes. |
| [**`rm`**](./docs/commands/rm.md) | Removes cloned repositories from your local disk. |
Expand Down
134 changes: 134 additions & 0 deletions docs/commands/ls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# repos ls

The `ls` command lists repositories specified in your `config.yaml` file with
optional filtering capabilities.

## Usage

```bash
repos ls [OPTIONS] [REPOS]...
```

## Description

This command is used to display information about repositories defined in your
configuration. It's particularly useful for reviewing which repositories will be
included when using specific tag filters, helping you preview the scope of
operations before running commands like `clone`, `run`, or `pr`.

The output includes repository names, URLs, tags, configured paths, and branches
for each repository.

## Arguments

- `[REPOS]...`: A space-separated list of specific repository names to list. If
not provided, `repos` will fall back to filtering by tags or listing all
repositories defined in the config.

## Options

- `-c, --config <CONFIG>`: Specifies the path to the configuration file.
Defaults to `config.yaml`.
- `-t, --tag <TAG>`: Filters repositories to list only those that have the
specified tag. This option can be used multiple times to include repositories
with *any* of the specified tags (OR logic).
- `-e, --exclude-tag <EXCLUDE_TAG>`: Excludes repositories that have the
specified tag. This can be used to filter out repositories from the listing.
This option can be used multiple times.
- `-h, --help`: Prints help information.

## Output Format

For each repository, the command displays:

- **Name**: The repository identifier
- **URL**: The Git remote URL
- **Tags**: Associated tags (if any)
- **Path**: Configured local path (if specified)
- **Branch**: Configured branch (if specified)

The output also includes a summary showing the total count of repositories found.

## Examples

### List all repositories

```bash
repos ls
```

### List specific repositories by name

```bash
repos ls repo-one repo-two
```

### List repositories with a specific tag

This is particularly useful to see which repositories will be affected when
running commands with the same tag filter.

```bash
repos ls --tag backend
```

### List repositories with multiple tags

This will list repositories that have *either* the `frontend` or the `rust`
tag.

```bash
repos ls -t frontend -t rust
```

### Exclude repositories with a specific tag

This will list all repositories *except* those with the `deprecated` tag.

```bash
repos ls --exclude-tag deprecated
```

### Combine inclusion and exclusion

This will list all repositories with the `backend` tag but exclude those that
also have the `deprecated` tag.

```bash
repos ls -t backend -e deprecated
```

### Preview before cloning

Before cloning repositories with a specific tag, you can preview which ones will
be affected:

```bash
# Preview which repositories have the 'flow' tag
repos ls --tag flow

# Then clone them
repos clone --tag flow
```

### Use with custom config

```bash
repos ls --config path/to/custom-config.yaml
```

## Use Cases

1. **Preview Tag Filters**: Check which repositories will be included in
operations that use the same tag filters.

2. **Explore Configuration**: Quickly view all repositories defined in your
config without needing to open the file.

3. **Verify Tags**: Ensure repositories are properly tagged before running bulk
operations.

4. **Review Paths**: Check configured paths and branches for repositories.

5. **Filter Testing**: Experiment with different tag combinations to understand
how filters work before applying them to operations like `clone` or `run`.
251 changes: 251 additions & 0 deletions src/commands/ls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
//! List command implementation

use super::{Command, CommandContext};
use anyhow::Result;
use async_trait::async_trait;
use colored::*;

/// List command for displaying repositories with optional filtering
pub struct ListCommand;

#[async_trait]
impl Command for ListCommand {
async fn execute(&self, context: &CommandContext) -> Result<()> {
let repositories = context.config.filter_repositories(
&context.tag,
&context.exclude_tag,
context.repos.as_deref(),
);

if repositories.is_empty() {
let mut filter_parts = Vec::new();

if !context.tag.is_empty() {
filter_parts.push(format!("tags {:?}", context.tag));
}
if !context.exclude_tag.is_empty() {
filter_parts.push(format!("excluding tags {:?}", context.exclude_tag));
}
if let Some(repos) = &context.repos {
filter_parts.push(format!("repositories {:?}", repos));
}

let filter_desc = if filter_parts.is_empty() {
"no repositories found".to_string()
} else {
filter_parts.join(" and ")
};

println!(
"{}",
format!("No repositories found with {filter_desc}").yellow()
);
return Ok(());
}

// Print summary header
println!(
"{}",
format!("Found {} repositories", repositories.len()).green()
);
println!();

// Print each repository
for repo in &repositories {
println!("{} {}", "•".blue(), repo.name.bold());
println!(" URL: {}", repo.url);

if !repo.tags.is_empty() {
println!(" Tags: {}", repo.tags.join(", ").cyan());
}

if let Some(path) = &repo.path {
println!(" Path: {}", path);
}

if let Some(branch) = &repo.branch {
println!(" Branch: {}", branch);
}

println!();
}

// Print summary footer
println!(
"{}",
format!("Total: {} repositories", repositories.len()).green()
);

Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, Repository};

/// Helper function to create a test config with repositories
fn create_test_config() -> Config {
let mut repo1 = Repository::new(
"test-repo-1".to_string(),
"https://github.com/test/repo1.git".to_string(),
);
repo1.tags = vec!["frontend".to_string(), "javascript".to_string()];

let mut repo2 = Repository::new(
"test-repo-2".to_string(),
"https://github.com/test/repo2.git".to_string(),
);
repo2.tags = vec!["backend".to_string(), "rust".to_string()];

let mut repo3 = Repository::new(
"test-repo-3".to_string(),
"https://github.com/test/repo3.git".to_string(),
);
repo3.tags = vec!["frontend".to_string(), "typescript".to_string()];

Config {
repositories: vec![repo1, repo2, repo3],
recipes: vec![],
}
}

/// Helper to create CommandContext for testing
fn create_context(
config: Config,
tag: Vec<String>,
exclude_tag: Vec<String>,
repos: Option<Vec<String>>,
) -> CommandContext {
CommandContext {
config,
tag,
exclude_tag,
repos,
parallel: false,
}
}

#[tokio::test]
async fn test_list_command_all_repositories() {
let config = create_test_config();
let command = ListCommand;

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_with_tag_filter() {
let config = create_test_config();
let command = ListCommand;

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_with_exclude_tag() {
let config = create_test_config();
let command = ListCommand;

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

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

#[tokio::test]
async fn test_list_command_with_both_filters() {
let config = create_test_config();
let command = ListCommand;

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

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

#[tokio::test]
async fn test_list_command_no_matches() {
let config = create_test_config();
let command = ListCommand;

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

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

#[tokio::test]
async fn test_list_command_with_repo_filter() {
let config = create_test_config();
let command = ListCommand;

let context = create_context(
config,
vec![],
vec![],
Some(vec!["test-repo-1".to_string(), "test-repo-2".to_string()]),
);

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

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

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_multiple_tags() {
let config = create_test_config();
let command = ListCommand;

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

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

#[tokio::test]
async fn test_list_command_combined_filters() {
let config = create_test_config();
let command = ListCommand;

let context = create_context(
config,
vec!["frontend".to_string()],
vec![],
Some(vec!["test-repo-1".to_string()]),
);

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