Skip to content

Commit a1f92a7

Browse files
committed
fix remote fork issue
1 parent b2692a6 commit a1f92a7

1 file changed

Lines changed: 104 additions & 21 deletions

File tree

src/github.rs

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ use crate::git2_ops::GitRepo;
2222
pub struct GitHubConfig {
2323
pub token: String,
2424
pub api_base: String,
25+
/// GitHub usernames whose PRs should be synced.
26+
/// When non-empty, only PRs from these authors will be synced.
27+
/// When empty, PRs from forks will be excluded.
28+
pub sync_authors: Vec<String>,
2529
}
2630

2731
/// Repository identification (owner/repo extracted from remote URL)
@@ -50,6 +54,8 @@ pub struct PullRequest {
5054
pub html_url: String,
5155
pub base: PrBranchRef,
5256
pub head: PrBranchRef,
57+
/// The user who created this PR
58+
pub user: PrUser,
5359
#[serde(default)]
5460
pub draft: bool,
5561
#[serde(default)]
@@ -59,11 +65,26 @@ pub struct PullRequest {
5965
pub merged_at: Option<String>,
6066
}
6167

68+
/// Minimal user info for PR author
69+
#[derive(Debug, Clone, Deserialize)]
70+
pub struct PrUser {
71+
pub login: String,
72+
}
73+
6274
#[derive(Debug, Clone, Deserialize)]
6375
pub struct PrBranchRef {
6476
#[serde(rename = "ref")]
6577
pub ref_name: String,
6678
pub sha: String,
79+
/// Repository info (may be null if the fork was deleted)
80+
pub repo: Option<PrRepoRef>,
81+
}
82+
83+
/// Minimal repo info for PR head/base references
84+
#[derive(Debug, Clone, Deserialize)]
85+
pub struct PrRepoRef {
86+
/// Full name of the repo (e.g., "owner/repo")
87+
pub full_name: String,
6788
}
6889

6990
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
@@ -99,6 +120,22 @@ impl PullRequest {
99120
self.merged || self.merged_at.is_some()
100121
}
101122

123+
/// Check if this PR is from a fork (head repo differs from base repo)
124+
///
125+
/// Returns true if:
126+
/// - The head repo is missing (fork was deleted)
127+
/// - The head repo full_name differs from the base repo full_name
128+
pub fn is_from_fork(&self) -> bool {
129+
match (&self.head.repo, &self.base.repo) {
130+
// If head repo is missing, the fork was probably deleted - treat as fork PR
131+
(None, _) => true,
132+
// If base repo is missing, something is weird but assume not a fork
133+
(_, None) => false,
134+
// Compare the full names
135+
(Some(head_repo), Some(base_repo)) => head_repo.full_name != base_repo.full_name,
136+
}
137+
}
138+
102139
/// Get the display state for this PR
103140
pub fn display_state(&self) -> PrDisplayState {
104141
if self.is_merged() {
@@ -191,13 +228,17 @@ impl GitHubClient {
191228

192229
/// Load config from environment/git config/config file
193230
pub fn from_env(repo_id: &RepoIdentifier) -> Result<Self, GitHubError> {
194-
let token = find_github_token(&repo_id.host)?;
231+
let (token, sync_authors) = find_github_config(&repo_id.host)?;
195232
let api_base = if repo_id.host == "github.com" {
196233
"https://api.github.com".to_string()
197234
} else {
198235
format!("https://{}/api/v3", repo_id.host)
199236
};
200-
Ok(Self::new(GitHubConfig { token, api_base }))
237+
Ok(Self::new(GitHubConfig {
238+
token,
239+
api_base,
240+
sync_authors,
241+
}))
201242
}
202243

203244
/// Get PR by number
@@ -313,9 +354,37 @@ impl GitHubClient {
313354
page += 1;
314355
}
315356

316-
// Build map of head branch name -> PR
357+
// Build map of head branch name -> PR, filtering out irrelevant PRs
317358
let pr_map: std::collections::HashMap<String, PullRequest> = all_prs
318359
.into_iter()
360+
.filter(|pr| {
361+
// If sync_authors is configured, only include PRs from those authors
362+
if !self.config.sync_authors.is_empty() {
363+
let included = self.config.sync_authors.contains(&pr.user.login);
364+
if !included {
365+
tracing::debug!(
366+
"Skipping PR #{} '{}' - author '{}' not in sync_authors",
367+
pr.number,
368+
pr.title,
369+
pr.user.login
370+
);
371+
}
372+
return included;
373+
}
374+
375+
// Otherwise, filter out PRs from forks
376+
if pr.is_from_fork() {
377+
tracing::debug!(
378+
"Skipping PR #{} '{}' - from fork (head: {:?})",
379+
pr.number,
380+
pr.title,
381+
pr.head.repo.as_ref().map(|r| &r.full_name)
382+
);
383+
return false;
384+
}
385+
386+
true
387+
})
319388
.map(|pr| (pr.head.ref_name.clone(), pr))
320389
.collect();
321390

@@ -450,22 +519,35 @@ pub fn get_repo_identifier(git_repo: &GitRepo) -> Result<RepoIdentifier> {
450519
parse_remote_url(&remote_url)
451520
}
452521

453-
/// Find GitHub token from various sources
454-
fn find_github_token(host: &str) -> Result<String, GitHubError> {
522+
/// Load GitHub configuration from XDG config file
523+
fn load_github_config_file() -> Option<GitHubConfigFile> {
524+
let config_path = get_github_config_path().ok()?;
525+
let contents = fs::read_to_string(&config_path).ok()?;
526+
serde_yaml::from_str(&contents).ok()
527+
}
528+
529+
/// Find GitHub token and config from various sources
530+
fn find_github_config(host: &str) -> Result<(String, Vec<String>), GitHubError> {
531+
let config_file = load_github_config_file();
532+
let sync_authors = config_file
533+
.as_ref()
534+
.map(|c| c.sync_authors.clone())
535+
.unwrap_or_default();
536+
455537
// 1. Check GITHUB_TOKEN env var
456538
if let Ok(token) = std::env::var("GITHUB_TOKEN")
457539
&& !token.is_empty()
458540
{
459541
tracing::debug!("Using GitHub token from GITHUB_TOKEN env var");
460-
return Ok(token);
542+
return Ok((token, sync_authors));
461543
}
462544

463545
// 2. Check GH_TOKEN env var (used by gh CLI)
464546
if let Ok(token) = std::env::var("GH_TOKEN")
465547
&& !token.is_empty()
466548
{
467549
tracing::debug!("Using GitHub token from GH_TOKEN env var");
468-
return Ok(token);
550+
return Ok((token, sync_authors));
469551
}
470552

471553
// 3. Check git config github.token
@@ -477,37 +559,39 @@ fn find_github_token(host: &str) -> Result<String, GitHubError> {
477559
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
478560
if !token.is_empty() {
479561
tracing::debug!("Using GitHub token from git config");
480-
return Ok(token);
562+
return Ok((token, sync_authors));
481563
}
482564
}
483565

484-
// 4. Check XDG config file
485-
if let Ok(config_path) = get_github_config_path()
486-
&& let Ok(contents) = fs::read_to_string(&config_path)
487-
&& let Ok(config) = serde_yaml::from_str::<GitHubConfigFile>(&contents)
488-
{
566+
// 4. Check XDG config file for token
567+
if let Some(config) = config_file {
489568
// Check for host-specific token first
490569
if let Some(hosts) = &config.hosts
491570
&& let Some(token) = hosts.get(host)
492571
{
493572
tracing::debug!("Using GitHub token from config file (host-specific)");
494-
return Ok(token.clone());
573+
return Ok((token.clone(), sync_authors));
495574
}
496575
// Fall back to default token
497576
if let Some(token) = config.default_token {
498577
tracing::debug!("Using GitHub token from config file (default)");
499-
return Ok(token);
578+
return Ok((token, sync_authors));
500579
}
501580
}
502581

503582
Err(GitHubError::NoToken)
504583
}
505584

506585
/// GitHub config file structure
507-
#[derive(Debug, Deserialize, Serialize)]
586+
#[derive(Debug, Default, Deserialize, Serialize)]
508587
struct GitHubConfigFile {
509588
default_token: Option<String>,
510589
hosts: Option<std::collections::HashMap<String, String>>,
590+
/// GitHub usernames whose PRs should be synced.
591+
/// When set, only PRs from these authors will be synced.
592+
/// When empty/unset, PRs from forks will be excluded.
593+
#[serde(default)]
594+
sync_authors: Vec<String>,
511595
}
512596

513597
/// Get path to GitHub config file
@@ -525,10 +609,9 @@ pub fn save_github_token(token: &str) -> Result<()> {
525609
.place_config_file("github.yaml")
526610
.context("Failed to create config directory")?;
527611

528-
let config = GitHubConfigFile {
529-
default_token: Some(token.to_string()),
530-
hosts: None,
531-
};
612+
// Load existing config to preserve other settings (like sync_authors)
613+
let mut config = load_github_config_file().unwrap_or_default();
614+
config.default_token = Some(token.to_string());
532615

533616
let contents = serde_yaml::to_string(&config)?;
534617
fs::write(&config_path, contents)?;
@@ -548,7 +631,7 @@ pub fn save_github_token(token: &str) -> Result<()> {
548631

549632
/// Check if GitHub token is configured
550633
pub fn has_github_token(host: &str) -> bool {
551-
find_github_token(host).is_ok()
634+
find_github_config(host).is_ok()
552635
}
553636

554637
/// Interactive token setup

0 commit comments

Comments
 (0)