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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bb-cli"
version = "0.3.5"
version = "0.3.6"
edition = "2024"

[dependencies]
Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# bbcli v0.3.5
# bbcli v0.3.6

Bitbucket CLI is a command line interface for interacting with Bitbucket.

Expand Down Expand Up @@ -64,6 +64,41 @@ To use this CLI, you need an API Token from your Atlassian account:
bb config init
```

## Global Flags

- `--json`: Output results in JSON format (available for `list` commands).

## Usage

### Repositories

List repositories in your workspace:

```bash
bb repo list

# List with a custom limit (default is 100)
bb repo list --limit 20
```

### Configuration

View your current configuration (active profile and local overrides):

```bash
bb config list
```

Set a configuration value:

```bash
# Set the active profile (user)
bb config set user <PROFILE_NAME>

# Set workspace for the default profile
bb config set profile.default.workspace <WORKSPACE_NAME>
```

## Development

For contributing to this repository, you can set up the pre-push hooks (recommended):
Expand Down
46 changes: 44 additions & 2 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::de::DeserializeOwned;
///
/// Handles communication with the Bitbucket Cloud API v2.0.
/// Supports authentication via Basic Auth (App Password).
#[derive(Clone)]
pub struct BitbucketClient {
client: Client,
base_url: String,
Expand Down Expand Up @@ -107,9 +108,11 @@ impl BitbucketClient {
limit: Option<u32>,
) -> Result<Vec<crate::api::models::PullRequest>> {
let mut all_prs = Vec::new();
// Use pagelen=100 (max) or limit if smaller to optimize API calls
let page_len = limit.map(|l| std::cmp::min(l, 100)).unwrap_or(100);
let mut path = format!(
"/repositories/{}/{}/pullrequests?state={}",
workspace, repo, state
"/repositories/{}/{}/pullrequests?state={}&pagelen={}",
workspace, repo, state, page_len
);

loop {
Expand All @@ -135,6 +138,45 @@ impl BitbucketClient {
Ok(all_prs)
}

/// List repositories in a workspace
///
/// # Arguments
///
/// * `workspace` - The workspace ID or slug
/// * `limit` - Optional maximum number of repositories to return
pub async fn list_repositories(
&self,
workspace: &str,
limit: Option<u32>,
) -> Result<Vec<crate::api::models::Repository>> {
let mut all_repos = Vec::new();
// Use pagelen=100 (max) or limit if smaller to optimize API calls
let page_len = limit.map(|l| std::cmp::min(l, 100)).unwrap_or(100);
let mut path = format!("/repositories/{}?pagelen={}", workspace, page_len);

loop {
let response: crate::api::models::PaginatedResponse<crate::api::models::Repository> =
self.get(&path).await?;

all_repos.extend(response.values);

// Check if we've reached the limit
let limit_reached = limit.is_some_and(|max| all_repos.len() >= max as usize);

if limit_reached {
all_repos.truncate(limit.unwrap() as usize);
break;
}

match response.next {
Some(next_url) => path = next_url,
None => break,
}
}

Ok(all_repos)
}

/// Get a single pull request by ID
///
/// # Arguments
Expand Down
5 changes: 5 additions & 0 deletions src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ pub struct Repository {
pub name: String,
pub full_name: String,
pub uuid: String,
pub description: Option<String>,
pub language: Option<String>,
pub updated_on: Option<String>,
pub website: Option<String>,
pub is_private: Option<bool>,
}

#[derive(Debug, Deserialize, Serialize)]
Expand Down
2 changes: 2 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ pub enum Commands {
Auth(commands::auth::AuthArgs),
/// Configuration
Config(commands::config::ConfigArgs),
/// Repository operations
Repo(commands::repo::RepoArgs),
}
15 changes: 4 additions & 11 deletions src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ async fn get_authenticated_user(profile: Option<&Profile>) -> Result<User> {
// Verify password exists in keyring
let api_token = crate::utils::auth::get_credentials(username)?;

let base_url = profile
.and_then(|p| p.api_url.clone())
.unwrap_or_else(|| crate::constants::DEFAULT_API_URL.to_string());
let base_url = crate::constants::DEFAULT_API_URL.to_string();

// Verify credentials against API
let client =
Expand All @@ -45,10 +43,8 @@ async fn get_authenticated_user(profile: Option<&Profile>) -> Result<User> {
}

/// Attempt to log in with provided credentials
async fn check_login(profile: Option<&Profile>, username: &str, api_token: &str) -> Result<User> {
let base_url = profile
.and_then(|p| p.api_url.clone())
.unwrap_or_else(|| crate::constants::DEFAULT_API_URL.to_string());
async fn check_login(username: &str, api_token: &str) -> Result<User> {
let base_url = crate::constants::DEFAULT_API_URL.to_string();

// Verify credentials work with API first
let client = crate::api::client::BitbucketClient::new(
Expand Down Expand Up @@ -106,10 +102,7 @@ pub async fn handle(_ctx: &AppContext, args: AuthArgs) -> Result<()> {

ui::info(msg::VERIFYING_CREDENTIALS);

let config = crate::config::manager::ProfileConfig::load().ok();
let profile = config.as_ref().and_then(|c| c.get_active_profile());

match check_login(profile, username, api_token).await {
match check_login(username, api_token).await {
Ok(user) => {
ui::success(msg::AUTH_SUCCESS);
ui::info(&msg::CREDENTIALS_SAVED.replace("{}", username));
Expand Down
Loading
Loading