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
57 changes: 57 additions & 0 deletions crates/lineark-sdk/tests/online.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Team>().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::<Issue>(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::<Issue>(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::<Issue>("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)]
Expand Down
20 changes: 20 additions & 0 deletions crates/lineark/src/commands/issues.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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<IssueDetail> = 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,
Expand Down
1 change: 1 addition & 0 deletions crates/lineark/src/commands/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ COMMANDS:
[--mine] Only issues assigned to me
[--show-done] Include done/canceled issues
lineark issues read <IDENTIFIER> Full issue detail incl. sub-issues, comments, relations
lineark issues find-branch <BRANCH> Find issue by Git branch name
lineark issues search <QUERY> [-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
Expand Down
29 changes: 29 additions & 0 deletions crates/lineark/tests/offline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
82 changes: 81 additions & 1 deletion crates/lineark/tests/online.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down Expand Up @@ -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::<Team>().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::<Issue>(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)]
Expand Down