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
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
push:
branches: [main]
pull_request:

env:
CARGO_TERM_COLOR: always

jobs:
fmt:
name: Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --check

clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy -- -D warnings

check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo check

test:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test

audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
16 changes: 7 additions & 9 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,7 @@ impl ApiClient {
Ok(api_err) => {
format!(
"{} ({}): {}",
api_err.error_type,
api_err.status_code,
api_err.message
api_err.error_type, api_err.status_code, api_err.message
)
}
Err(_) => {
Expand Down Expand Up @@ -153,7 +151,8 @@ impl ApiClient {
}
}
let path_and_query = format!("{}?{}", url.path(), url.query().unwrap_or(""));
self.request(reqwest::Method::GET, &path_and_query, None).await
self.request(reqwest::Method::GET, &path_and_query, None)
.await
}

pub async fn get_bug(&self, bug_id: &BugId) -> Result<Bug> {
Expand All @@ -175,8 +174,7 @@ impl ApiClient {
notes: notes.map(String::from),
};
let body = serde_json::to_value(request)?;
self.request(reqwest::Method::POST, &path, Some(body))
.await
self.request(reqwest::Method::POST, &path, Some(body)).await
}

pub async fn list_repos(&self, limit: u32, offset: u32) -> Result<ReposResponse> {
Expand All @@ -187,16 +185,16 @@ impl ApiClient {
query.append_pair("offset", &offset.to_string());
}
let path_and_query = format!("{}?{}", url.path(), url.query().unwrap_or(""));
self.request(reqwest::Method::GET, &path_and_query, None).await
self.request(reqwest::Method::GET, &path_and_query, None)
.await
}
}

fn check_version_compatibility(api_version: &str) -> Result<()> {
// CLI v0.1.x supports API v1.x
const SUPPORTED_API_VERSIONS: &str = "^1.0";

let api_version =
Version::parse(api_version).context("Failed to parse API version")?;
let api_version = Version::parse(api_version).context("Failed to parse API version")?;

let requirement = VersionReq::parse(SUPPORTED_API_VERSIONS)?;

Expand Down
20 changes: 4 additions & 16 deletions src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,7 @@ impl crate::output::Formattable for Bug {

// Wrap long fields for better table display
let wrapped_title = crate::utils::wrap_text(&self.title, 50);
let wrapped_file = crate::utils::wrap_path(
self.file_path.as_deref().unwrap_or("-"),
40
);
let wrapped_file = crate::utils::wrap_path(self.file_path.as_deref().unwrap_or("-"), 40);

vec![
Cell::new(self.id.as_str()),
Expand All @@ -239,23 +236,14 @@ impl crate::output::Formattable for Repo {
}

fn to_csv_row(&self) -> Vec<String> {
vec![
self.full_name.clone(),
self.org_name.clone(),
]
vec![self.full_name.clone(), self.org_name.clone()]
}

fn table_headers() -> Vec<Cell> {
vec![
Cell::new("Repository"),
Cell::new("Organization"),
]
vec![Cell::new("Repository"), Cell::new("Organization")]
}

fn to_table_row(&self) -> Vec<Cell> {
vec![
Cell::new(&self.full_name),
Cell::new(&self.org_name),
]
vec![Cell::new(&self.full_name), Cell::new(&self.org_name)]
}
}
6 changes: 2 additions & 4 deletions src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,8 @@ pub async fn handle(command: &AuthCommands, cli: &crate::Cli) -> Result<()> {
}

// Test the token by making an API call
let client = crate::api::client::ApiClient::new(
cli.api_url.clone(),
Some(token.clone()),
)?;
let client =
crate::api::client::ApiClient::new(cli.api_url.clone(), Some(token.clone()))?;

let user_info = client
.get_current_user()
Expand Down
43 changes: 31 additions & 12 deletions src/commands/bugs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ async fn resolve_repo_id(
let mut offset = 0;

loop {
let repos = client.list_repos(limit, offset).await
let repos = client
.list_repos(limit, offset)
.await
.context("Failed to fetch repositories while resolving identifier")?;

if let Some(repo) = repos.repos.iter().find(|r| r.full_name == repo_identifier) {
Expand All @@ -155,7 +157,9 @@ async fn resolve_repo_id(
let mut matching_repos = Vec::new();

loop {
let repos = client.list_repos(limit, offset).await
let repos = client
.list_repos(limit, offset)
.await
.context("Failed to fetch repositories while resolving identifier")?;

let page_size = repos.repos.len();
Expand Down Expand Up @@ -209,7 +213,8 @@ pub async fn handle(command: &BugCommands, cli: &crate::Cli) -> Result<()> {
format,
} => {
// Resolve owner/repo or repo to internal repo ID
let resolved_repo_id = resolve_repo_id(&client, repo).await
let resolved_repo_id = resolve_repo_id(&client, repo)
.await
.context("Failed to resolve repository identifier")?;

let offset = crate::utils::page_to_offset(*page, *limit);
Expand All @@ -225,18 +230,22 @@ pub async fn handle(command: &BugCommands, cli: &crate::Cli) -> Result<()> {
BugCommands::Show { bug_id } => {
use crate::api::types::BugId;

let bug_id = BugId::new(bug_id)
.map_err(|e| anyhow::anyhow!(e))?;
let bug_id = BugId::new(bug_id).map_err(|e| anyhow::anyhow!(e))?;

let bug = client.get_bug(&bug_id).await
let bug = client
.get_bug(&bug_id)
.await
.context("Failed to fetch bug details")?;

println!("{}", "Bug Details".bold());
println!("ID: {}", bug.id);
println!("Title: {}", bug.title);
println!("Report: {}", bug.summary);
println!("File: {}", bug.file_path.as_deref().unwrap_or("-"));
println!("Created: {}", crate::utils::format_datetime(bug.created_at));
println!(
"Created: {}",
crate::utils::format_datetime(bug.created_at)
);
println!(
"Security: {}",
bug.is_security_vulnerability
Expand All @@ -246,7 +255,10 @@ pub async fn handle(command: &BugCommands, cli: &crate::Cli) -> Result<()> {
if let Some(review) = bug.review {
println!("\nReview:");
println!(" State: {}", review.state);
println!(" Date: {}", crate::utils::format_datetime(review.created_at));
println!(
" Date: {}",
crate::utils::format_datetime(review.created_at)
);
if let Some(reason) = review.dismissal_reason {
println!(" Reason: {}", reason);
}
Expand All @@ -271,17 +283,24 @@ pub async fn handle(command: &BugCommands, cli: &crate::Cli) -> Result<()> {
bail!("--dismissal-reason is required when state is 'dismissed'");
}

let bug_id = BugId::new(bug_id)
.map_err(|e| anyhow::anyhow!(e))?;
let bug_id = BugId::new(bug_id).map_err(|e| anyhow::anyhow!(e))?;

let dismissal_reason_str = dismissal_reason.as_ref().map(|r| r.as_str());

client
.update_bug_review(&bug_id, state.as_str(), dismissal_reason_str, notes.as_deref())
.update_bug_review(
&bug_id,
state.as_str(),
dismissal_reason_str,
notes.as_deref(),
)
.await
.context("Failed to update bug review")?;

println!("{}", format!("✓ Updated bug review to: {}", state.as_str()).green());
println!(
"{}",
format!("✓ Updated bug review to: {}", state.as_str()).green()
);
Ok(())
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/commands/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@ pub async fn handle(command: &RepoCommands, cli: &crate::Cli) -> Result<()> {
let client = cli.create_client()?;

match command {
RepoCommands::List { limit, page, format } => {
RepoCommands::List {
limit,
page,
format,
} => {
let offset = crate::utils::page_to_offset(*page, *limit);

let repos = client.list_repos(*limit, offset).await
let repos = client
.list_repos(*limit, offset)
.await
.context("Failed to fetch repositories")?;

crate::output::output_list(&repos.repos, repos.total, format)
Expand Down
4 changes: 2 additions & 2 deletions src/config/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pub fn config_path() -> Result<PathBuf> {
PathBuf::from(xdg_config).join("detail-cli")
} else if cfg!(windows) {
// Windows: use LOCALAPPDATA
let local_app_data = std::env::var("LOCALAPPDATA")
.context("LOCALAPPDATA environment variable not set")?;
let local_app_data =
std::env::var("LOCALAPPDATA").context("LOCALAPPDATA environment variable not set")?;
PathBuf::from(local_app_data).join("detail-cli")
} else {
// Others: use ~/.config
Expand Down
21 changes: 8 additions & 13 deletions src/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,16 @@ fn print_update_success(result: &axoupdater::UpdateResult) {
let new_version = result.new_version.to_string();

eprintln!();
eprintln!("{}", "─".repeat(60).dimmed());
eprintln!(
"{}",
"─".repeat(60).dimmed()
);
eprintln!(
"{}",
format!("✓ Updated Detail CLI from v{} to v{}", old_version, new_version).green()
);
eprintln!(
"{}",
" Changes will apply on next run".dimmed()
);
eprintln!(
"{}",
"─".repeat(60).dimmed()
format!(
"✓ Updated Detail CLI from v{} to v{}",
old_version, new_version
)
.green()
);
eprintln!("{}", " Changes will apply on next run".dimmed());
eprintln!("{}", "─".repeat(60).dimmed());
eprintln!();
}