Skip to content

Commit 94599f4

Browse files
committed
make git stack interactive
1 parent e229777 commit 94599f4

File tree

6 files changed

+174
-125
lines changed

6 files changed

+174
-125
lines changed

src/main.rs

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ mod stats;
2424
mod sync;
2525
mod tui;
2626
#[derive(Parser)]
27-
#[command(author, version, about)]
27+
#[command(author, version, about, infer_subcommands = true)]
2828
struct Args {
2929
#[arg(long, short, global = true, help = "Enable verbose output")]
3030
verbose: bool,
@@ -52,10 +52,9 @@ enum Command {
5252
/// Whether to fetch the latest changes from the remote before showing the status.
5353
#[arg(long, short, default_value_t = false)]
5454
fetch: bool,
55-
/// Launch interactive TUI mode for branch navigation and checkout.
56-
#[arg(long, short = 'i', default_value_t = false)]
57-
interactive: bool,
5855
},
56+
/// Launch interactive TUI mode for branch navigation and checkout.
57+
Interactive,
5958
/// Open the git-stack state file in an editor for manual editing.
6059
Edit,
6160
/// Restack your active branch onto its parent branch.
@@ -352,7 +351,7 @@ fn inner_main() -> Result<()> {
352351
Some(Command::Mount { parent_branch }) => {
353352
state.mount(&git_repo, &repo, &current_branch, parent_branch)
354353
}
355-
Some(Command::Status { fetch, interactive }) => {
354+
Some(Command::Status { fetch }) => {
356355
state.try_auto_mount(&git_repo, &repo, &current_branch)?;
357356
status(
358357
&git_repo,
@@ -361,9 +360,12 @@ fn inner_main() -> Result<()> {
361360
&current_branch,
362361
fetch,
363362
args.verbose,
364-
interactive,
365363
)
366364
}
365+
Some(Command::Interactive) => {
366+
state.try_auto_mount(&git_repo, &repo, &current_branch)?;
367+
interactive(&git_repo, state, &repo, &current_branch, args.verbose)
368+
}
367369
Some(Command::Delete { branch_name }) => state.delete_branch(&repo, &branch_name),
368370
Some(Command::Cleanup { dry_run, all }) => {
369371
state.cleanup_missing_branches(&git_repo, &repo, dry_run, all)
@@ -419,7 +421,6 @@ fn inner_main() -> Result<()> {
419421
&current_branch,
420422
false,
421423
args.verbose,
422-
false, // interactive
423424
)
424425
}
425426
}
@@ -501,7 +502,6 @@ fn status(
501502
orig_branch: &str,
502503
fetch: bool,
503504
verbose: bool,
504-
interactive: bool,
505505
) -> Result<()> {
506506
if fetch {
507507
git_fetch()?;
@@ -533,22 +533,58 @@ fn status(
533533
&display_authors,
534534
);
535535

536-
if interactive {
537-
// Run TUI and handle checkout if user selected a branch
538-
if let Some(branch_to_checkout) = tui::run_tui(renderable, verbose)? {
539-
run_git(&["checkout", &branch_to_checkout])?;
540-
}
541-
} else {
542-
// Render to CLI
543-
render::render_cli(&renderable, verbose);
536+
// Render to CLI
537+
render::render_cli(&renderable, verbose);
544538

545-
if !state.branch_exists_in_tree(repo, orig_branch) {
546-
eprintln!(
547-
"The current branch {} is not in the stack tree.",
548-
orig_branch.red()
549-
);
550-
eprintln!("Run `git stack mount <parent_branch>` to add it.");
551-
}
539+
if !state.branch_exists_in_tree(repo, orig_branch) {
540+
eprintln!(
541+
"The current branch {} is not in the stack tree.",
542+
orig_branch.red()
543+
);
544+
eprintln!("Run `git stack mount <parent_branch>` to add it.");
545+
}
546+
547+
state.save_state()?;
548+
Ok(())
549+
}
550+
551+
fn interactive(
552+
git_repo: &GitRepo,
553+
mut state: State,
554+
repo: &str,
555+
orig_branch: &str,
556+
verbose: bool,
557+
) -> Result<()> {
558+
// ensure_trunk creates the tree if it doesn't exist (no-op if no remote)
559+
let _trunk = state.ensure_trunk(git_repo, repo);
560+
561+
// Auto-cleanup any missing branches before displaying the tree
562+
state.auto_cleanup_missing_branches(git_repo, repo)?;
563+
564+
// Try to fetch PR info from GitHub (graceful degradation on failure)
565+
let pr_cache = fetch_pr_cache(git_repo);
566+
567+
// Load display_authors for filtering (show other authors dimmed)
568+
let display_authors = github::load_display_authors();
569+
570+
let Some(tree) = state.get_tree(repo) else {
571+
println!("No stack configured for this repository.");
572+
return Ok(());
573+
};
574+
575+
// Compute renderable tree
576+
let renderable = render::compute_renderable_tree(
577+
git_repo,
578+
tree,
579+
orig_branch,
580+
verbose,
581+
pr_cache.as_ref(),
582+
&display_authors,
583+
);
584+
585+
// Run TUI and handle checkout if user selected a branch
586+
if let Some(branch_to_checkout) = tui::run_tui(renderable, verbose)? {
587+
run_git(&["checkout", &branch_to_checkout])?;
552588
}
553589

554590
state.save_state()?;
@@ -676,11 +712,7 @@ fn squash_branch(
676712
}
677713

678714
/// Handle the --continue flag for resuming a squash operation.
679-
fn handle_squash_continue(
680-
git_repo: &GitRepo,
681-
state: &mut State,
682-
repo: &str,
683-
) -> Result<()> {
715+
fn handle_squash_continue(git_repo: &GitRepo, state: &mut State, repo: &str) -> Result<()> {
684716
let pending = state
685717
.get_pending_squash(repo)
686718
.ok_or_else(|| anyhow!("No pending squash operation to continue."))?
@@ -690,7 +722,10 @@ fn handle_squash_continue(
690722
let status_output = run_git(&["status", "--porcelain"])?;
691723
let has_conflicts = status_output
692724
.output()
693-
.map(|s| s.lines().any(|line| line.starts_with("UU") || line.starts_with("AA")))
725+
.map(|s| {
726+
s.lines()
727+
.any(|line| line.starts_with("UU") || line.starts_with("AA"))
728+
})
694729
.unwrap_or(false);
695730

696731
if has_conflicts {
@@ -719,11 +754,7 @@ fn handle_squash_continue(
719754
}
720755

721756
/// Handle the --abort flag for aborting a squash operation.
722-
fn handle_squash_abort(
723-
git_repo: &GitRepo,
724-
state: &mut State,
725-
repo: &str,
726-
) -> Result<()> {
757+
fn handle_squash_abort(git_repo: &GitRepo, state: &mut State, repo: &str) -> Result<()> {
727758
let pending = state
728759
.get_pending_squash(repo)
729760
.ok_or_else(|| anyhow!("No pending squash operation to abort."))?
@@ -1001,8 +1032,7 @@ fn sync_pr_bases_after_restack(git_repo: &GitRepo, state: &State, repo: &str) ->
10011032
.get_tree(repo)
10021033
.ok_or_else(|| anyhow!("No stack tree found"))?;
10031034

1004-
let trunk =
1005-
crate::git::git_trunk(git_repo).ok_or_else(|| anyhow!("No remote configured"))?;
1035+
let trunk = crate::git::git_trunk(git_repo).ok_or_else(|| anyhow!("No remote configured"))?;
10061036

10071037
// Collect branches with depth for bottom-up processing
10081038
let branches_with_depth = collect_branches_with_depth(tree, &tree.name, 0);
@@ -1228,8 +1258,7 @@ fn handle_import_command(
12281258
}
12291259

12301260
let client = GitHubClient::from_env(&repo_id)?;
1231-
let trunk =
1232-
crate::git::git_trunk(git_repo).ok_or_else(|| anyhow!("No remote configured"))?;
1261+
let trunk = crate::git::git_trunk(git_repo).ok_or_else(|| anyhow!("No remote configured"))?;
12331262

12341263
// Ensure trunk exists in tree
12351264
state.ensure_trunk(git_repo, repo);
@@ -1533,8 +1562,8 @@ fn handle_pr_command(
15331562
state.try_auto_mount(git_repo, repo, &branch_name)?;
15341563

15351564
// Check if this is the trunk branch (can't create PR for main)
1536-
let trunk = crate::git::git_trunk(git_repo)
1537-
.ok_or_else(|| anyhow!("No remote configured"))?;
1565+
let trunk =
1566+
crate::git::git_trunk(git_repo).ok_or_else(|| anyhow!("No remote configured"))?;
15381567
if branch_name == trunk.main_branch {
15391568
bail!(
15401569
"Cannot create a PR for the trunk branch '{}'.",
@@ -1706,8 +1735,8 @@ fn handle_pr_command(
17061735
PrAction::Sync { all, dry_run } => {
17071736
use github::UpdatePrRequest;
17081737

1709-
let trunk = crate::git::git_trunk(git_repo)
1710-
.ok_or_else(|| anyhow!("No remote configured"))?;
1738+
let trunk =
1739+
crate::git::git_trunk(git_repo).ok_or_else(|| anyhow!("No remote configured"))?;
17111740

17121741
// Get branches to sync with depth for bottom-up processing
17131742
let branches_to_sync: Vec<(String, String, usize)> = if all {

src/render/cli.rs

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use colored::Colorize;
44

55
use super::{
6-
colors::{string_to_color, theme, ThemeColor},
6+
colors::{ThemeColor, string_to_color, theme},
77
tree_data::{RenderableBranch, RenderableTree},
88
};
99
use crate::github::PrDisplayState;
@@ -72,35 +72,43 @@ fn render_branch(branch: &RenderableBranch, verbose: bool) {
7272
};
7373

7474
// Diff stats
75-
let diff_stats = branch.diff_stats.as_ref().map(|ds| {
76-
let green = theme::GREEN.apply_dim(dim);
77-
let red = theme::RED.apply_dim(dim);
78-
let prefix = if ds.reliable { "" } else { "~ " };
79-
format!(
80-
" [{}{}{}]",
81-
prefix,
82-
apply_color(&format!("+{}", ds.additions), green),
83-
apply_color(&format!(" -{}", ds.deletions), red)
84-
)
85-
}).unwrap_or_default();
75+
let diff_stats = branch
76+
.diff_stats
77+
.as_ref()
78+
.map(|ds| {
79+
let green = theme::GREEN.apply_dim(dim);
80+
let red = theme::RED.apply_dim(dim);
81+
let prefix = if ds.reliable { "" } else { "~ " };
82+
format!(
83+
" [{}{}{}]",
84+
prefix,
85+
apply_color(&format!("+{}", ds.additions), green),
86+
apply_color(&format!(" -{}", ds.deletions), red)
87+
)
88+
})
89+
.unwrap_or_default();
8690

8791
// Local status
88-
let local_status = branch.local_status.as_ref().map(|ls| {
89-
let mut parts = Vec::new();
90-
let green = theme::GREEN.apply_dim(dim);
91-
let yellow = theme::YELLOW.apply_dim(dim);
92-
let gray = theme::GRAY.apply_dim(dim);
93-
if ls.staged > 0 {
94-
parts.push(apply_color(&format!("+{}", ls.staged), green).to_string());
95-
}
96-
if ls.unstaged > 0 {
97-
parts.push(apply_color(&format!("~{}", ls.unstaged), yellow).to_string());
98-
}
99-
if ls.untracked > 0 {
100-
parts.push(apply_color(&format!("?{}", ls.untracked), gray).to_string());
101-
}
102-
format!(" [{}]", parts.join(" "))
103-
}).unwrap_or_default();
92+
let local_status = branch
93+
.local_status
94+
.as_ref()
95+
.map(|ls| {
96+
let mut parts = Vec::new();
97+
let green = theme::GREEN.apply_dim(dim);
98+
let yellow = theme::YELLOW.apply_dim(dim);
99+
let gray = theme::GRAY.apply_dim(dim);
100+
if ls.staged > 0 {
101+
parts.push(apply_color(&format!("+{}", ls.staged), green).to_string());
102+
}
103+
if ls.unstaged > 0 {
104+
parts.push(apply_color(&format!("~{}", ls.unstaged), yellow).to_string());
105+
}
106+
if ls.untracked > 0 {
107+
parts.push(apply_color(&format!("?{}", ls.untracked), gray).to_string());
108+
}
109+
format!(" [{}]", parts.join(" "))
110+
})
111+
.unwrap_or_default();
104112

105113
if verbose {
106114
render_verbose_line(branch, &branch_name, &diff_stats, &local_status, dim);
@@ -117,34 +125,38 @@ fn render_simple_line(
117125
dim: f32,
118126
) {
119127
// PR info
120-
let pr_info = branch.pr_info.as_ref().map(|pr| {
121-
let gray = theme::GRAY.apply_dim(dim);
122-
let green = theme::GREEN.apply_dim(dim);
123-
let purple = theme::PURPLE.apply_dim(dim);
124-
let red = theme::RED.apply_dim(dim);
128+
let pr_info = branch
129+
.pr_info
130+
.as_ref()
131+
.map(|pr| {
132+
let gray = theme::GRAY.apply_dim(dim);
133+
let green = theme::GREEN.apply_dim(dim);
134+
let purple = theme::PURPLE.apply_dim(dim);
135+
let red = theme::RED.apply_dim(dim);
125136

126-
let state_colored = match pr.state {
127-
PrDisplayState::Draft => apply_color(&format!("[{}]", pr.state), gray),
128-
PrDisplayState::Open => apply_color(&format!("[{}]", pr.state), green),
129-
PrDisplayState::Merged => apply_color(&format!("[{}]", pr.state), purple),
130-
PrDisplayState::Closed => apply_color(&format!("[{}]", pr.state), red),
131-
};
137+
let state_colored = match pr.state {
138+
PrDisplayState::Draft => apply_color(&format!("[{}]", pr.state), gray),
139+
PrDisplayState::Open => apply_color(&format!("[{}]", pr.state), green),
140+
PrDisplayState::Merged => apply_color(&format!("[{}]", pr.state), purple),
141+
PrDisplayState::Closed => apply_color(&format!("[{}]", pr.state), red),
142+
};
132143

133-
let author_color = string_to_color(&pr.author).apply_dim(dim);
134-
let author_colored = apply_color(&format!("@{}", pr.author), author_color);
144+
let author_color = string_to_color(&pr.author).apply_dim(dim);
145+
let author_colored = apply_color(&format!("@{}", pr.author), author_color);
135146

136-
let pr_num = theme::PR_NUMBER.apply_dim(dim);
137-
let number_colored = apply_color(&format!("#{}", pr.number), pr_num);
147+
let pr_num = theme::PR_NUMBER.apply_dim(dim);
148+
let number_colored = apply_color(&format!("#{}", pr.number), pr_num);
138149

139-
let arrow = theme::PR_ARROW.apply_dim(dim);
140-
format!(
141-
" {} {} {} {}",
142-
apply_color("", arrow),
143-
author_colored,
144-
number_colored,
145-
state_colored
146-
)
147-
}).unwrap_or_default();
150+
let arrow = theme::PR_ARROW.apply_dim(dim);
151+
format!(
152+
" {} {} {} {}",
153+
apply_color("", arrow),
154+
author_colored,
155+
number_colored,
156+
state_colored
157+
)
158+
})
159+
.unwrap_or_default();
148160

149161
println!("{}{}{}{}", branch_name, diff_stats, local_status, pr_info);
150162
}
@@ -215,13 +227,17 @@ fn render_verbose_line(
215227
};
216228

217229
// LKG parent
218-
let lkg_info = branch.verbose.as_ref()
230+
let lkg_info = branch
231+
.verbose
232+
.as_ref()
219233
.and_then(|v| v.lkg_parent.as_ref())
220234
.map(|lkg| format!(" (lkg parent {})", apply_color(lkg, gold)))
221235
.unwrap_or_default();
222236

223237
// Stack method
224-
let method_info = branch.verbose.as_ref()
238+
let method_info = branch
239+
.verbose
240+
.as_ref()
225241
.map(|v| {
226242
let method_color = theme::GREEN.apply_dim(dim);
227243
format!(" ({})", apply_color(&v.stack_method, method_color))
@@ -251,10 +267,6 @@ fn render_verbose_line(
251267
} else {
252268
note.blue()
253269
};
254-
println!(
255-
" {} {}",
256-
apply_color("›", theme::TREE),
257-
note_display
258-
);
270+
println!(" {} {}", apply_color("›", theme::TREE), note_display);
259271
}
260272
}

0 commit comments

Comments
 (0)