Skip to content

Commit a849e5d

Browse files
committed
remove is_local bit from branches
1 parent de4b4e4 commit a849e5d

File tree

4 files changed

+152
-175
lines changed

4 files changed

+152
-175
lines changed

src/github.rs

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ use crate::{git2_ops::GitRepo, state::write_file_secure};
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>,
25+
/// GitHub usernames whose PRs should be displayed prominently in status.
26+
/// When non-empty, PRs from other authors will be shown dimmed/collapsed.
27+
/// No longer used for filtering during sync.
28+
pub display_authors: Vec<String>,
2929
}
3030

3131
/// Repository identification (owner/repo extracted from remote URL)
@@ -241,7 +241,7 @@ pub struct CachedPrUser {
241241
/// Result from list_prs operations, containing both filtered PRs and all author mappings
242242
#[derive(Debug)]
243243
pub struct PrListResult {
244-
/// Filtered PRs (by sync_authors or fork filter)
244+
/// PRs filtered to exclude forks
245245
pub prs: std::collections::HashMap<String, PullRequest>,
246246
/// All branch -> author mappings (before filtering)
247247
pub all_authors: std::collections::HashMap<String, String>,
@@ -303,7 +303,7 @@ impl GitHubClient {
303303

304304
/// Load config from environment/git config/config file
305305
pub fn from_env(repo_id: &RepoIdentifier) -> Result<Self, GitHubError> {
306-
let (token, sync_authors) = find_github_config(&repo_id.host)?;
306+
let (token, display_authors) = find_github_config(&repo_id.host)?;
307307
let api_base = if repo_id.host == "github.com" {
308308
"https://api.github.com".to_string()
309309
} else {
@@ -312,7 +312,7 @@ impl GitHubClient {
312312
Ok(Self::new(GitHubConfig {
313313
token,
314314
api_base,
315-
sync_authors,
315+
display_authors,
316316
}))
317317
}
318318

@@ -449,25 +449,11 @@ impl GitHubClient {
449449
.map(|pr| (pr.head.ref_name.clone(), pr.user.login.clone()))
450450
.collect();
451451

452-
// Build map of head branch name -> PR, filtering out irrelevant PRs
452+
// Build map of head branch name -> PR, filtering out PRs from forks
453453
let prs: std::collections::HashMap<String, PullRequest> = all_prs
454454
.into_iter()
455455
.filter(|pr| {
456-
// If sync_authors is configured, only include PRs from those authors
457-
if !self.config.sync_authors.is_empty() {
458-
let included = self.config.sync_authors.contains(&pr.user.login);
459-
if !included {
460-
tracing::debug!(
461-
"Skipping PR #{} '{}' - author '{}' not in sync_authors",
462-
pr.number,
463-
pr.title,
464-
pr.user.login
465-
);
466-
}
467-
return included;
468-
}
469-
470-
// Otherwise, filter out PRs from forks
456+
// Filter out PRs from forks (we can't track remote branches for forks)
471457
if pr.is_from_fork() {
472458
tracing::debug!(
473459
"Skipping PR #{} '{}' - from fork (head: {:?})",
@@ -477,7 +463,6 @@ impl GitHubClient {
477463
);
478464
return false;
479465
}
480-
481466
true
482467
})
483468
.map(|pr| (pr.head.ref_name.clone(), pr))
@@ -568,14 +553,9 @@ impl GitHubClient {
568553
Ok(PrListResult { prs, all_authors })
569554
}
570555

571-
/// Check if a PR should be included based on sync_authors and fork filtering
556+
/// Check if a PR should be included based on fork filtering
572557
fn should_include_pr(&self, pr: &PullRequest) -> bool {
573-
// If sync_authors is configured, only include PRs from those authors
574-
if !self.config.sync_authors.is_empty() {
575-
return self.config.sync_authors.contains(&pr.user.login);
576-
}
577-
578-
// Otherwise, filter out PRs from forks
558+
// Filter out PRs from forks (we can't track remote branches for forks)
579559
!pr.is_from_fork()
580560
}
581561

@@ -774,28 +754,36 @@ fn load_github_config_file() -> Option<GitHubConfigFile> {
774754
serde_yaml::from_str(&contents).ok()
775755
}
776756

757+
/// Load display_authors from the GitHub config file.
758+
/// Returns an empty vec if the config file doesn't exist or has no display_authors.
759+
pub fn load_display_authors() -> Vec<String> {
760+
load_github_config_file()
761+
.map(|c| c.display_authors)
762+
.unwrap_or_default()
763+
}
764+
777765
/// Find GitHub token and config from various sources
778766
fn find_github_config(host: &str) -> Result<(String, Vec<String>), GitHubError> {
779767
let config_file = load_github_config_file();
780-
let sync_authors = config_file
768+
let display_authors = config_file
781769
.as_ref()
782-
.map(|c| c.sync_authors.clone())
770+
.map(|c| c.display_authors.clone())
783771
.unwrap_or_default();
784772

785773
// 1. Check GITHUB_TOKEN env var
786774
if let Ok(token) = std::env::var("GITHUB_TOKEN")
787775
&& !token.is_empty()
788776
{
789777
tracing::debug!("Using GitHub token from GITHUB_TOKEN env var");
790-
return Ok((token, sync_authors));
778+
return Ok((token, display_authors));
791779
}
792780

793781
// 2. Check GH_TOKEN env var (used by gh CLI)
794782
if let Ok(token) = std::env::var("GH_TOKEN")
795783
&& !token.is_empty()
796784
{
797785
tracing::debug!("Using GitHub token from GH_TOKEN env var");
798-
return Ok((token, sync_authors));
786+
return Ok((token, display_authors));
799787
}
800788

801789
// 3. Check git config github.token
@@ -807,7 +795,7 @@ fn find_github_config(host: &str) -> Result<(String, Vec<String>), GitHubError>
807795
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
808796
if !token.is_empty() {
809797
tracing::debug!("Using GitHub token from git config");
810-
return Ok((token, sync_authors));
798+
return Ok((token, display_authors));
811799
}
812800
}
813801

@@ -818,12 +806,12 @@ fn find_github_config(host: &str) -> Result<(String, Vec<String>), GitHubError>
818806
&& let Some(token) = hosts.get(host)
819807
{
820808
tracing::debug!("Using GitHub token from config file (host-specific)");
821-
return Ok((token.clone(), sync_authors));
809+
return Ok((token.clone(), display_authors));
822810
}
823811
// Fall back to default token
824812
if let Some(token) = config.default_token {
825813
tracing::debug!("Using GitHub token from config file (default)");
826-
return Ok((token, sync_authors));
814+
return Ok((token, display_authors));
827815
}
828816
}
829817

@@ -835,11 +823,10 @@ fn find_github_config(host: &str) -> Result<(String, Vec<String>), GitHubError>
835823
struct GitHubConfigFile {
836824
default_token: Option<String>,
837825
hosts: Option<std::collections::HashMap<String, String>>,
838-
/// GitHub usernames whose PRs should be synced.
839-
/// When set, only PRs from these authors will be synced.
840-
/// When empty/unset, PRs from forks will be excluded.
826+
/// GitHub usernames whose PRs should be displayed prominently in status.
827+
/// When set, PRs from other authors will be shown dimmed/collapsed.
841828
#[serde(default)]
842-
sync_authors: Vec<String>,
829+
display_authors: Vec<String>,
843830
}
844831

845832
/// Get path to GitHub config file
@@ -857,7 +844,7 @@ pub fn save_github_token(token: &str) -> Result<()> {
857844
.place_config_file("github.yaml")
858845
.context("Failed to create config directory")?;
859846

860-
// Load existing config to preserve other settings (like sync_authors)
847+
// Load existing config to preserve other settings (like display_authors)
861848
let mut config = load_github_config_file().unwrap_or_default();
862849
config.default_token = Some(token.to_string());
863850

src/main.rs

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,23 @@ fn recur_tree(
478478
parent_branch: Option<&str>,
479479
verbose: bool,
480480
pr_cache: Option<&std::collections::HashMap<String, github::PullRequest>>,
481+
display_authors: &[String],
481482
) -> Result<()> {
483+
// Check if this branch should be dimmed (filtered by display_authors)
484+
let pr_author = pr_cache
485+
.and_then(|cache| cache.get(&branch.name))
486+
.map(|pr| pr.user.login.as_str());
487+
let is_dimmed = if display_authors.is_empty() {
488+
false
489+
} else {
490+
pr_author.map_or(false, |author| {
491+
!display_authors.contains(&author.to_string())
492+
})
493+
};
494+
495+
// Check if branch is remote-only (not local)
496+
let is_remote_only = !git_repo.branch_exists(&branch.name);
497+
482498
let Ok(branch_status) = git_repo
483499
.branch_status(parent_branch, &branch.name)
484500
.with_context(|| {
@@ -488,6 +504,44 @@ fn recur_tree(
488504
)
489505
})
490506
else {
507+
// For remote-only branches, the local branch may not exist
508+
if is_remote_only {
509+
// Show remote-only branch even without status
510+
let is_current_branch = branch.name == orig_branch;
511+
if is_current_branch {
512+
print!("{} ", selection_marker().bright_purple().bold());
513+
} else {
514+
print!(" ");
515+
}
516+
for _ in 0..depth {
517+
print!("{}", "┃ ".truecolor(55, 55, 50));
518+
}
519+
let branch_name_colored = if is_dimmed {
520+
branch.name.truecolor(90, 90, 90)
521+
} else {
522+
branch.name.truecolor(128, 128, 128)
523+
};
524+
println!(
525+
"{} {}",
526+
branch_name_colored,
527+
"(remote)".truecolor(90, 90, 90)
528+
);
529+
530+
// Recurse into children
531+
for child in &branch.branches {
532+
recur_tree(
533+
git_repo,
534+
child,
535+
depth + 1,
536+
orig_branch,
537+
Some(branch.name.as_ref()),
538+
verbose,
539+
pr_cache,
540+
display_authors,
541+
)?;
542+
}
543+
return Ok(());
544+
}
491545
tracing::warn!("Branch {} does not exist", branch.name);
492546
return Ok(());
493547
};
@@ -504,11 +558,23 @@ fn recur_tree(
504558
}
505559

506560
// Branch name coloring: green for synced, red for diverged, bold for current branch
507-
let branch_name_colored = match (is_current_branch, branch_status.is_descendent) {
508-
(true, true) => branch.name.truecolor(142, 192, 124).bold(),
509-
(true, false) => branch.name.red().bold(),
510-
(false, true) => branch.name.truecolor(142, 192, 124),
511-
(false, false) => branch.name.red(),
561+
// Dimmed branches (filtered by display_authors) are shown in gray
562+
let branch_name_colored = if is_dimmed {
563+
branch.name.truecolor(90, 90, 90)
564+
} else {
565+
match (is_current_branch, branch_status.is_descendent) {
566+
(true, true) => branch.name.truecolor(142, 192, 124).bold(),
567+
(true, false) => branch.name.red().bold(),
568+
(false, true) => branch.name.truecolor(142, 192, 124),
569+
(false, false) => branch.name.red(),
570+
}
571+
};
572+
573+
// Remote-only indicator
574+
let remote_indicator = if is_remote_only {
575+
" (remote)".truecolor(90, 90, 90).to_string()
576+
} else {
577+
String::new()
512578
};
513579

514580
// Get diff stats from LKG ancestor to current branch
@@ -553,8 +619,9 @@ fn recur_tree(
553619

554620
if verbose {
555621
println!(
556-
"{}{}{} ({}) {}{}{}{}",
622+
"{}{}{}{} ({}) {}{}{}{}",
557623
branch_name_colored,
624+
remote_indicator,
558625
diff_stats,
559626
local_status,
560627
branch_status.sha[..8].truecolor(215, 153, 33),
@@ -654,8 +721,8 @@ fn recur_tree(
654721
String::new()
655722
};
656723
println!(
657-
"{}{}{}{}",
658-
branch_name_colored, diff_stats, local_status, pr_info
724+
"{}{}{}{}{}",
725+
branch_name_colored, remote_indicator, diff_stats, local_status, pr_info
659726
);
660727
}
661728

@@ -695,6 +762,7 @@ fn recur_tree(
695762
Some(branch.name.as_ref()),
696763
verbose,
697764
pr_cache,
765+
display_authors,
698766
)?;
699767
}
700768
Ok(())
@@ -740,6 +808,9 @@ fn status(
740808
// Try to fetch PR info from GitHub (graceful degradation on failure)
741809
let pr_cache = fetch_pr_cache(git_repo);
742810

811+
// Load display_authors for filtering (show other authors dimmed)
812+
let display_authors = github::load_display_authors();
813+
743814
let tree = state
744815
.get_tree_mut(repo)
745816
.expect("tree exists after ensure_trunk");
@@ -751,6 +822,7 @@ fn status(
751822
None,
752823
verbose,
753824
pr_cache.as_ref(),
825+
&display_authors,
754826
)?;
755827
if !state.branch_exists_in_tree(repo, orig_branch) {
756828
eprintln!(
@@ -1186,7 +1258,17 @@ fn handle_import_command(
11861258
println!();
11871259
let tree = state.get_tree(repo).expect("tree exists after import");
11881260
let pr_cache = fetch_pr_cache(git_repo);
1189-
recur_tree(git_repo, tree, 0, branch, None, false, pr_cache.as_ref())?;
1261+
let display_authors = github::load_display_authors();
1262+
recur_tree(
1263+
git_repo,
1264+
tree,
1265+
0,
1266+
branch,
1267+
None,
1268+
false,
1269+
pr_cache.as_ref(),
1270+
&display_authors,
1271+
)?;
11901272

11911273
Ok(())
11921274
}

src/state.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ impl State {
178178
/// then check whether the branch exists in the git repo. If it does, then let the user know
179179
/// that they need to use `git checkout` to check it out. If it doesn't, then create a new
180180
/// branch.
181+
///
182+
/// For branches tracked in git-stack but not existing locally, this will create the local
183+
/// branch from the remote ref (origin/branch_name) on-demand.
181184
pub fn checkout(
182185
&mut self,
183186
git_repo: &GitRepo,
@@ -195,8 +198,10 @@ impl State {
195198
self.save_state()?;
196199

197200
let branch_exists_in_tree = self.branch_exists_in_tree(repo, &branch_name);
201+
let branch_exists_locally = git_branch_exists(git_repo, &branch_name);
198202

199-
if git_branch_exists(git_repo, &branch_name) {
203+
// Case 1: Branch exists locally - just check it out
204+
if branch_exists_locally {
200205
if !branch_exists_in_tree {
201206
tracing::warn!(
202207
"Branch {branch_name} exists in the git repo but is not tracked by git-stack. \
@@ -208,6 +213,26 @@ impl State {
208213
return Ok(());
209214
}
210215

216+
// Case 2: Branch is in tree but doesn't exist locally - create from remote
217+
if branch_exists_in_tree {
218+
let remote_ref = format!("origin/{}", branch_name);
219+
if git_repo.ref_exists(&remote_ref) {
220+
// Create local branch from remote ref
221+
run_git(&["checkout", "-b", &branch_name, &remote_ref])?;
222+
println!(
223+
"Branch {branch_name} created from remote and checked out.",
224+
branch_name = branch_name.yellow()
225+
);
226+
return Ok(());
227+
} else {
228+
bail!(
229+
"Branch {branch_name} is tracked by git-stack but doesn't exist locally or on remote.",
230+
branch_name = branch_name.red()
231+
);
232+
}
233+
}
234+
235+
// Case 3: Branch doesn't exist anywhere - create a new branch from current
211236
let branch = self
212237
.get_tree_branch_mut(repo, &current_branch)
213238
.ok_or_else(|| {

0 commit comments

Comments
 (0)