diff --git a/crates/lineark-sdk/tests/online.rs b/crates/lineark-sdk/tests/online.rs index e5c4270..30ced71 100644 --- a/crates/lineark-sdk/tests/online.rs +++ b/crates/lineark-sdk/tests/online.rs @@ -1061,6 +1061,63 @@ mod online { client.team_delete(team_id).await.unwrap(); } + // ── Issue VCS Branch Search ───────────────────────────────────────────── + + #[test_with::runtime_ignore_if(no_online_test_token)] + async fn issue_vcs_branch_search_found() { + use lineark_sdk::generated::inputs::IssueCreateInput; + + let client = test_client(); + + // Create an issue so we can look up its branchName. + let teams = client.teams::().first(1).send().await.unwrap(); + let team_id = teams.nodes[0].id.clone().unwrap(); + + let uid = &uuid::Uuid::new_v4().to_string()[..8]; + let input = IssueCreateInput { + title: Some(format!("[test] SDK branch search {uid}")), + team_id: Some(team_id), + priority: Some(4), + ..Default::default() + }; + let entity = client.issue_create::(input).await.unwrap(); + let issue_id = entity.id.clone().unwrap(); + let _issue_guard = IssueGuard { + token: test_token(), + id: issue_id.clone(), + }; + + // Read the issue to get its branchName field. + let branch_name = entity + .branch_name + .clone() + .expect("newly created issue should have a branchName"); + + let result = client + .issue_vcs_branch_search::(branch_name) + .await + .unwrap(); + + assert!(result.is_some(), "should find issue by branch name"); + let found = result.unwrap(); + assert_eq!(found.id, Some(issue_id)); + } + + #[test_with::runtime_ignore_if(no_online_test_token)] + async fn issue_vcs_branch_search_not_found() { + let client = test_client(); + + let result = client + .issue_vcs_branch_search::("nonexistent-branch-xyz-999".to_string()) + .await + .unwrap(); + + assert!( + result.is_none(), + "should return None for nonexistent branch" + ); + } + // ── Error handling ────────────────────────────────────────────────────── #[test_with::runtime_ignore_if(no_online_test_token)] diff --git a/crates/lineark/src/commands/issues.rs b/crates/lineark/src/commands/issues.rs index a3895f5..fa488f0 100644 --- a/crates/lineark/src/commands/issues.rs +++ b/crates/lineark/src/commands/issues.rs @@ -125,6 +125,11 @@ pub enum IssuesAction { /// Issue identifier (e.g., ENG-123) or UUID. identifier: String, }, + /// Find the issue associated with a Git branch name. + FindBranch { + /// Git branch name to search for. + branch_name: String, + }, /// Delete (trash) an issue. Use --permanently to delete permanently. /// /// Examples: @@ -557,6 +562,21 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res let items = filter_done_search(&conn.nodes, show_done); print_search_list(&items, format); } + IssuesAction::FindBranch { branch_name } => { + let result: Option = client + .issue_vcs_branch_search(branch_name.clone()) + .await + .map_err(|e| anyhow::anyhow!("{}", e))?; + match result { + Some(issue) => output::print_one(&issue, format), + None => { + return Err(anyhow::anyhow!( + "No issue found for branch '{}'", + branch_name + )) + } + } + } IssuesAction::Create { title, team, diff --git a/crates/lineark/src/commands/usage.rs b/crates/lineark/src/commands/usage.rs index 047b4e8..20ff355 100644 --- a/crates/lineark/src/commands/usage.rs +++ b/crates/lineark/src/commands/usage.rs @@ -58,6 +58,7 @@ COMMANDS: [--mine] Only issues assigned to me [--show-done] Include done/canceled issues lineark issues read Full issue detail incl. sub-issues, comments, relations + lineark issues find-branch Find issue by Git branch name lineark issues search [-l N] Full-text search [--team KEY] [--assignee NAME-OR-ID|me] Filter by team, assignee, status [--status NAME,...] [--show-done] Comma-separated status names diff --git a/crates/lineark/tests/offline.rs b/crates/lineark/tests/offline.rs index 4fe0253..79c1b64 100644 --- a/crates/lineark/tests/offline.rs +++ b/crates/lineark/tests/offline.rs @@ -810,6 +810,35 @@ fn usage_includes_teams_create() { .stdout(predicate::str::contains("teams members remove")); } +// ── Issues find-branch ─────────────────────────────────────────────────────── + +#[test] +fn issues_find_branch_help_shows_description() { + lineark() + .args(["issues", "find-branch", "--help"]) + .assert() + .success() + .stdout(predicate::str::contains("Git branch name")); +} + +#[test] +fn issues_help_shows_find_branch_subcommand() { + lineark() + .args(["issues", "--help"]) + .assert() + .success() + .stdout(predicate::str::contains("find-branch")); +} + +#[test] +fn usage_includes_find_branch() { + lineark() + .arg("usage") + .assert() + .success() + .stdout(predicate::str::contains("issues find-branch")); +} + // ── Self command ───────────────────────────────────────────────────────────── #[test] diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index de91d90..de526df 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -5,7 +5,7 @@ use assert_cmd::Command; use lineark_sdk::generated::inputs::ProjectCreateInput; -use lineark_sdk::generated::types::{Issue, IssueRelation, Project}; +use lineark_sdk::generated::types::{Issue, IssueRelation, Project, Team}; use lineark_sdk::Client; use predicates::prelude::*; @@ -3455,6 +3455,86 @@ mod online { assert_eq!(result["success"].as_bool(), Some(true)); } + // ── Issues find-branch ────────────────────────────────────────────────── + + #[test_with::runtime_ignore_if(no_online_test_token)] + fn issues_find_branch_returns_issue() { + use lineark_sdk::generated::inputs::IssueCreateInput; + + let token = api_token(); + let client = Client::from_token(&token).unwrap(); + + // Create an issue via SDK to get the branch name. + let rt = tokio::runtime::Runtime::new().unwrap(); + let (issue_id, branch_name) = rt.block_on(async { + let teams = client.teams::().first(1).send().await.unwrap(); + let team_id = teams.nodes[0].id.clone().unwrap(); + + let input = IssueCreateInput { + title: Some(format!( + "[test] CLI find-branch {}", + &uuid::Uuid::new_v4().to_string()[..8] + )), + team_id: Some(team_id), + priority: Some(4), + ..Default::default() + }; + let entity = client.issue_create::(input).await.unwrap(); + let issue_id = entity.id.clone().unwrap(); + let branch_name = entity + .branch_name + .clone() + .expect("created issue should have a branchName"); + (issue_id, branch_name) + }); + + let _issue_guard = IssueGuard { + token: token.clone(), + id: issue_id.clone(), + }; + + // Run the CLI find-branch command. + let output = lineark() + .args([ + "--api-token", + &token, + "--format", + "json", + "issues", + "find-branch", + &branch_name, + ]) + .output() + .expect("failed to execute lineark"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "issues find-branch should succeed.\nstdout: {stdout}\nstderr: {stderr}" + ); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert!( + json.get("identifier").is_some(), + "find-branch JSON should contain identifier" + ); + } + + #[test_with::runtime_ignore_if(no_online_test_token)] + fn issues_find_branch_no_match_exits_nonzero() { + let token = api_token(); + lineark() + .args([ + "--api-token", + &token, + "issues", + "find-branch", + "nonexistent-branch-abc-xyz-987654321", + ]) + .assert() + .failure() + .stderr(predicate::str::contains("No issue found")); + } + // ── Issues list with --project filter ─────────────────────────────────── #[test_with::runtime_ignore_if(no_online_test_token)]