Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.
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
82 changes: 82 additions & 0 deletions src-tauri/src/git/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ pub struct PullRequest {
pub updated_at: String,
}

/// A GitHub issue (for display in picker)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Issue {
pub number: u64,
pub title: String,
pub author: String,
pub updated_at: String,
pub labels: Vec<String>,
}

// =============================================================================
// Cache
// =============================================================================
Expand Down Expand Up @@ -285,6 +295,78 @@ pub fn search_pull_requests(repo: &Path, query: &str) -> Result<Vec<PullRequest>
Ok(items.into_iter().map(Into::into).collect())
}

// =============================================================================
// Issues
// =============================================================================

/// Response from `gh issue list --json`
#[derive(Debug, Deserialize)]
struct GhIssueListItem {
number: u64,
title: String,
author: GhAuthor,
#[serde(rename = "updatedAt")]
updated_at: String,
labels: Vec<GhLabel>,
}

#[derive(Debug, Deserialize)]
struct GhLabel {
name: String,
}

impl From<GhIssueListItem> for Issue {
fn from(item: GhIssueListItem) -> Self {
Issue {
number: item.number,
title: item.title,
author: item.author.login,
updated_at: item.updated_at,
labels: item.labels.into_iter().map(|l| l.name).collect(),
}
}
}

/// List open issues for the repo
pub fn list_issues(repo: &Path) -> Result<Vec<Issue>, GitError> {
let output = run_gh(
repo,
&[
"issue",
"list",
"--state=open",
"--limit=50",
"--json=number,title,author,updatedAt,labels",
],
)?;

let items: Vec<GhIssueListItem> =
serde_json::from_str(&output).map_err(|e| GitError::CommandFailed(e.to_string()))?;

Ok(items.into_iter().map(Into::into).collect())
}

/// Search for issues on GitHub using a query string.
/// Uses GitHub's search syntax via `gh issue list --search`.
pub fn search_issues(repo: &Path, query: &str) -> Result<Vec<Issue>, GitError> {
let output = run_gh(
repo,
&[
"issue",
"list",
"--state=open",
"--limit=50",
&format!("--search={query}"),
"--json=number,title,author,updatedAt,labels",
],
)?;

let items: Vec<GhIssueListItem> =
serde_json::from_str(&output).map_err(|e| GitError::CommandFailed(e.to_string()))?;

Ok(items.into_iter().map(Into::into).collect())
}

/// Fetch PR refs and compute merge-base
///
/// - Fetches refs/pull/{number}/head
Expand Down
6 changes: 3 additions & 3 deletions src-tauri/src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ pub use diff::{get_file_diff, get_unified_diff, list_diff_files};
pub use files::{get_file_at_ref, search_files};
pub use github::{
check_github_auth, create_pull_request, fetch_pr, get_pr_for_branch,
invalidate_cache as invalidate_pr_cache, list_pull_requests, push_branch, search_pull_requests,
sync_review_to_github, update_pull_request, CreatePrResult, GitHubAuthStatus, GitHubSyncResult,
PullRequest, PullRequestInfo,
invalidate_cache as invalidate_pr_cache, list_issues, list_pull_requests, push_branch,
search_issues, search_pull_requests, sync_review_to_github, update_pull_request,
CreatePrResult, GitHubAuthStatus, GitHubSyncResult, Issue, PullRequest, PullRequestInfo,
};
pub use refs::{
detect_default_branch, get_repo_root, list_branches, list_refs, merge_base, resolve_ref,
Expand Down
31 changes: 31 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,35 @@ async fn search_pull_requests(
.map_err(|e| e.to_string())?
}

/// List open issues for the repo.
#[tauri::command(rename_all = "camelCase")]
async fn list_issues(repo_path: Option<String>) -> Result<Vec<git::Issue>, String> {
let path = repo_path
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
// Run on blocking thread pool to avoid blocking the UI
tokio::task::spawn_blocking(move || git::list_issues(&path).map_err(|e| e.to_string()))
.await
.map_err(|e| e.to_string())?
}

/// Search for issues on GitHub using a query string.
#[tauri::command(rename_all = "camelCase")]
async fn search_issues(
repo_path: Option<String>,
query: String,
) -> Result<Vec<git::Issue>, String> {
let path = repo_path
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
// Run on blocking thread pool to avoid blocking the UI
tokio::task::spawn_blocking(move || {
git::search_issues(&path, &query).map_err(|e| e.to_string())
})
.await
.map_err(|e| e.to_string())?
}

/// Fetch PR refs and compute merge-base.
/// Returns DiffSpec with concrete SHAs.
#[tauri::command(rename_all = "camelCase")]
Expand Down Expand Up @@ -3573,6 +3602,8 @@ pub fn run() {
check_github_auth,
list_pull_requests,
search_pull_requests,
list_issues,
search_issues,
fetch_pr,
sync_review_to_github,
invalidate_pr_cache,
Expand Down
Loading