use super::*;
pub(super) fn tui_new_args(branch: &str, base: Option<String>) -> NewArgs {
NewArgs {
branch: branch.to_string(),
from: base,
track: None,
no_track: false,
no_switch: true,
no_hooks: false,
copy_from: None,
init_submodules: false,
no_init_submodules: true,
}
}
pub(super) fn run_create_command(
cx: &mut Cx,
branch: &str,
base: Option<String>,
decision: Option<CreateDecision>,
) -> CreateOutcome {
let args = tui_new_args(branch, base);
match decision {
None => match commands::new::detect_stale_base(cx, &args) {
Ok(Some(stale)) => CreateOutcome::NeedsStaleConfirm {
behind: stale.behind,
upstream_display: stale.upstream_display,
can_fast_forward: stale.can_fast_forward,
},
_ => create_core(cx, &args),
},
Some(CreateDecision::Update) => match commands::new::update_stale_base(cx, &args) {
Ok(()) => create_core(cx, &args),
Err(e) => CreateOutcome::Failed(e.to_string()),
},
Some(CreateDecision::Proceed) => create_core(cx, &args),
}
}
pub(super) fn create_core(cx: &mut Cx, args: &NewArgs) -> CreateOutcome {
match commands::new::run_core(cx, &CapturingHookRunner, args, false, false) {
Ok(_) => match detect_pending_submodules(cx, &args.branch) {
Some((dir, count, auto)) => CreateOutcome::CreatedNeedsSubmodules { dir, count, auto },
None => CreateOutcome::Created,
},
Err(e) => CreateOutcome::Failed(e.to_string()),
}
}
pub(super) fn detect_pending_submodules(cx: &Cx, branch: &str) -> Option<(PathBuf, usize, bool)> {
let git = cx.git.clone();
let session = open_session(cx, git.as_ref()).ok()?;
let auto = match session.config.submodules_init {
SubmoduleInit::Never => return None,
SubmoduleInit::Always => true,
SubmoduleInit::Prompt => false,
};
let worktrees = enumerate_worktrees(&session.repo, git.as_ref()).ok()?;
let dir = worktrees
.iter()
.find(|w| w.branch.as_deref() == Some(branch))
.map(|w| w.path.clone())?;
let pending = crate::git::submodule::uninitialized(git.as_ref(), &dir).ok()?;
if pending.is_empty() {
None
} else {
Some((dir, pending.len(), auto))
}
}
pub(super) fn run_materialize_command(
cx: &mut Cx,
branch: &str,
) -> std::result::Result<(), String> {
let args = tui_new_args(branch, None);
commands::new::run_core(cx, &CapturingHookRunner, &args, false, false)
.map(|_| ())
.map_err(|e| e.to_string())
}
pub(super) fn run_remove_command(cx: &mut Cx, query: &str) -> std::result::Result<(), String> {
let opts = commands::remove::RemoveOptions {
force_remove: true,
force_branch: false,
keep_branch: false,
no_hooks: false,
};
commands::remove::remove_query(cx, &CapturingHookRunner, query, &opts, false)
.map(|_| ())
.map_err(|e| e.to_string())
}
pub(super) fn run_delete_branch_command(
cx: &mut Cx,
branch: &str,
force: bool,
) -> std::result::Result<(), String> {
commands::remove::delete_branch_query(cx, branch, force, false)
.map(|_| ())
.map_err(|e| e.to_string())
}
pub(super) fn run_checkout_pr_command(
cx: &mut Cx,
number: u64,
) -> std::result::Result<(PathBuf, bool), String> {
let git = cx.git.clone();
let gh = cx.gh.clone();
let session = open_session(cx, git.as_ref()).map_err(|e| e.to_string())?;
let dir = session
.repo
.current_workdir()
.unwrap_or_else(|| session.primary_root.clone());
commands::pr::checkout_pr_worktree(
cx,
git.as_ref(),
gh.as_ref(),
&CapturingHookRunner,
&session,
&dir,
&number.to_string(),
false,
false,
)
.map_err(|e| e.to_string())
}
pub(super) fn run_checkout_branch_command(
cx: &mut Cx,
worktree_dir: &Path,
branch: &str,
) -> std::result::Result<commands::checkout::SyncOutcome, String> {
let git = cx.git.clone();
let session = open_session(cx, git.as_ref()).map_err(|e| e.to_string())?;
commands::checkout::checkout_branch_in_worktree(
cx,
git.as_ref(),
&session,
worktree_dir,
branch,
false,
None,
false,
)
.map_err(|e| e.to_string())
}
pub(super) fn run_sync_command(
cx: &mut Cx,
worktree_dir: &Path,
) -> std::result::Result<commands::sync::SyncOutcome, String> {
let git = cx.git.clone();
let session = open_session(cx, git.as_ref()).map_err(|e| e.to_string())?;
commands::sync::sync_worktree(cx, git.as_ref(), &session, worktree_dir, None, false, false)
.map_err(|e| e.to_string())
}
pub(super) fn run_sync_branch_command(
cx: &mut Cx,
branch: &str,
) -> std::result::Result<commands::sync::SyncOutcome, String> {
let git = cx.git.clone();
let session = open_session(cx, git.as_ref()).map_err(|e| e.to_string())?;
commands::sync::sync_branch(cx, git.as_ref(), &session, branch, false)
.map_err(|e| e.to_string())
}
pub(super) fn run_init_submodules_command(
cx: &mut Cx,
worktree_dir: &Path,
) -> std::result::Result<(), String> {
let git = cx.git.clone();
crate::git::submodule::update_init(git.as_ref(), worktree_dir).map_err(|e| e.to_string())
}
pub(super) fn spawn_enrichment(
root: PathBuf,
git: Arc<dyn GitCli + Send + Sync>,
tx: mpsc::Sender<Vec<Worktree>>,
) {
tokio::task::spawn_blocking(move || {
if let Ok(repo) = Repo::discover(&root)
&& let Ok(worktrees) = build_rows(&repo, git.as_ref())
{
let _ = tx.blocking_send(worktrees);
}
});
}
pub(super) fn mark_all_loaded(app: &mut App, worktrees: Vec<Worktree>) {
let paths: Vec<PathBuf> = worktrees.iter().map(|w| w.path.clone()).collect();
app.set_worktrees(worktrees);
for path in paths {
app.mark_loaded(path);
}
}
pub(crate) fn do_refresh(cx: &Cx, app: &mut App, root: &Path) {
let git = cx.git.clone();
if let Ok(repo) = Repo::discover(root) {
if let Ok(branches) = crate::git::all_branches(repo.gix()) {
app.branches = branches;
}
if let Ok(worktrees) = build_rows(&repo, git.as_ref()) {
mark_all_loaded(app, worktrees);
}
}
}
#[cfg(test)]
pub(crate) fn do_fetch_prs(cx: &Cx, session: &Session, app: &mut App) {
let dir = session
.repo
.current_workdir()
.unwrap_or_else(|| session.primary_root.clone());
apply_prs(app, fetch_prs_result(cx.gh.as_ref(), &dir));
}
pub(super) fn fetch_prs_result(
gh: &dyn crate::gh::GhClient,
dir: &Path,
) -> std::result::Result<Vec<PrItem>, String> {
gh.list_open_prs(dir)
.map(|prs| {
prs.into_iter()
.map(|p| {
let pr_state = p.pr_state().as_str().to_string();
PrItem {
number: p.number,
title: p.title,
author: p.author.login,
state: pr_state,
created_at: p.created_at,
}
})
.collect()
})
.map_err(|e| e.to_string())
}
pub(super) fn apply_prs(app: &mut App, result: std::result::Result<Vec<PrItem>, String>) {
if let Mode::PrPicker(state) = &mut app.mode {
state.loading = false;
match result {
Ok(prs) => state.prs = prs,
Err(e) => state.error = Some(e),
}
}
}
#[cfg(test)]
pub(crate) fn do_create(
cx: &mut Cx,
session: &Session,
app: &mut App,
branch: String,
base: Option<String>,
) {
let outcome = run_job(
JobCx::capture(cx),
Job::Create {
branch,
base,
decision: None,
},
);
apply_outcome(cx, session, app, outcome);
}
#[cfg(test)]
pub(crate) fn do_remove(cx: &mut Cx, session: &Session, app: &mut App, index: usize) {
let Some(query) = remove_query_of(app, index) else {
return;
};
let outcome = run_job(JobCx::capture(cx), Job::Remove { query });
apply_outcome(cx, session, app, outcome);
}
#[cfg(test)]
pub(crate) fn do_delete_branch(
cx: &mut Cx,
session: &Session,
app: &mut App,
branch: String,
force: bool,
) {
let outcome = run_job(JobCx::capture(cx), Job::DeleteBranch { branch, force });
apply_outcome(cx, session, app, outcome);
}
#[cfg(test)]
pub(crate) fn do_materialize_branch(cx: &mut Cx, session: &Session, app: &mut App, branch: String) {
let outcome = run_job(JobCx::capture(cx), Job::Materialize { branch });
apply_outcome(cx, session, app, outcome);
}
#[cfg(test)]
pub(crate) fn do_checkout_pr(cx: &mut Cx, session: &Session, app: &mut App, number: u64) {
let outcome = run_job(JobCx::capture(cx), Job::CheckoutPr { number });
apply_outcome(cx, session, app, outcome);
}
#[cfg(test)]
pub(crate) fn do_checkout_branch(
cx: &mut Cx,
session: &Session,
app: &mut App,
index: usize,
branch: String,
) {
let Some(worktree_dir) = app.worktrees.get(index).map(|w| w.path.clone()) else {
return;
};
let outcome = run_job(
JobCx::capture(cx),
Job::CheckoutBranch {
worktree_dir,
branch,
},
);
apply_outcome(cx, session, app, outcome);
}
#[cfg(test)]
pub(crate) fn do_sync(cx: &mut Cx, session: &Session, app: &mut App, index: usize) {
let Some(worktree) = app.worktrees.get(index) else {
return;
};
let label = worktree
.branch
.clone()
.unwrap_or_else(|| "worktree".to_string());
let job = if worktree.has_worktree {
Job::Sync {
worktree_dir: worktree.path.clone(),
label,
}
} else {
let Some(branch) = worktree.branch.clone() else {
return;
};
Job::SyncBranch { branch, label }
};
let outcome = run_job(JobCx::capture(cx), job);
apply_outcome(cx, session, app, outcome);
}
pub(super) fn apply_create(
cx: &Cx,
app: &mut App,
branch: &str,
base: Option<String>,
outcome: CreateOutcome,
root: &Path,
) {
match outcome {
CreateOutcome::Created => {
app.set_status(format!("created {branch}"), StatusKind::Success);
do_refresh(cx, app, root);
let _ = app.select_branch(branch);
close_create_modal(app);
}
CreateOutcome::CreatedNeedsSubmodules { dir, count, auto } => {
app.set_status(format!("created {branch}"), StatusKind::Success);
do_refresh(cx, app, root);
let _ = app.select_branch(branch);
if !auto && app.may_apply_mode(JobHome::Create) {
app.mode = Mode::ConfirmInitSubmodules(InitSubmodulesState {
dir,
branch: branch.to_string(),
count,
});
} else {
app.queue_job(Effect::InitSubmodules { dir, count });
close_create_modal(app);
}
}
CreateOutcome::NeedsStaleConfirm {
behind,
upstream_display,
can_fast_forward,
} => {
if app.may_apply_mode(JobHome::Create) {
app.mode = Mode::ConfirmStaleBase(StaleBaseState {
branch: branch.to_string(),
base,
behind,
upstream_display,
can_fast_forward,
});
} else {
app.set_status(
format!("{branch}: base is behind {upstream_display}; not created"),
StatusKind::Error,
);
}
}
CreateOutcome::Failed(e) => match &mut app.mode {
Mode::Create(state) => state.error = Some(e),
_ => app.set_status(e, StatusKind::Error),
},
}
}
fn close_create_modal(app: &mut App) {
if app.may_apply_mode(JobHome::Create) {
app.mode = Mode::List;
}
}
pub(super) fn apply_remove(
cx: &Cx,
app: &mut App,
query: &str,
result: std::result::Result<(), String>,
root: &Path,
) {
match result {
Ok(()) => app.set_status(format!("removed {query}"), StatusKind::Success),
Err(e) => app.set_status(e, StatusKind::Error),
}
if app.may_apply_mode(JobHome::List) {
app.mode = Mode::List;
}
do_refresh(cx, app, root);
}
pub(super) fn apply_delete_branch(
cx: &Cx,
app: &mut App,
branch: &str,
force: bool,
result: std::result::Result<(), String>,
root: &Path,
) {
match result {
Ok(()) => {
app.set_status(format!("deleted branch {branch}"), StatusKind::Success);
do_refresh(cx, app, root);
if app.may_apply_mode(JobHome::List) {
app.mode = Mode::List;
}
}
Err(e) if !force && e.contains("not fully merged") => {
let index = app
.worktrees
.iter()
.position(|w| !w.has_worktree && w.branch.as_deref() == Some(branch));
match index {
Some(index) if app.may_apply_mode(JobHome::List) => {
app.mode = Mode::ConfirmDeleteBranch { index, force: true };
}
_ => app.set_status(e, StatusKind::Error),
}
}
Err(e) => {
app.set_status(e, StatusKind::Error);
if app.may_apply_mode(JobHome::List) {
app.mode = Mode::List;
}
}
}
}
pub(super) fn apply_materialize(
cx: &Cx,
app: &mut App,
branch: &str,
result: std::result::Result<(), String>,
root: &Path,
) {
match result {
Ok(()) => {
app.set_status(format!("created {branch}"), StatusKind::Success);
do_refresh(cx, app, root);
let _ = app.select_branch(branch);
if let Some((dir, count, _auto)) = detect_pending_submodules(cx, branch) {
app.queue_job(Effect::InitSubmodules { dir, count });
}
}
Err(e) => app.set_status(e, StatusKind::Error),
}
if app.may_apply_mode(JobHome::List) {
app.mode = Mode::List;
}
}
pub(super) fn apply_checkout_pr(
cx: &Cx,
app: &mut App,
number: u64,
result: std::result::Result<(PathBuf, bool), String>,
root: &Path,
) {
match result {
Ok((path, _existed)) => {
app.set_status(format!("checked out PR #{number}"), StatusKind::Success);
do_refresh(cx, app, root);
app.select_path(&path);
if app.may_apply_mode(JobHome::PrPicker) {
app.mode = Mode::List;
}
}
Err(e) => match &mut app.mode {
Mode::PrPicker(state) => state.error = Some(e),
_ => app.set_status(e, StatusKind::Error),
},
}
}
pub(super) fn apply_checkout_branch(
cx: &Cx,
app: &mut App,
branch: &str,
result: std::result::Result<commands::checkout::SyncOutcome, String>,
root: &Path,
) {
match result {
Ok(outcome) => {
app.set_status(
format!(
"checked out {branch}{}",
commands::checkout::sync_suffix(outcome)
),
StatusKind::Success,
);
do_refresh(cx, app, root);
if app.may_apply_mode(JobHome::Checkout) {
app.mode = Mode::List;
}
}
Err(e) => match &mut app.mode {
Mode::Checkout(state) => {
state.error = Some(e);
state.submitting = false;
}
_ => app.set_status(e, StatusKind::Error),
},
}
}
pub(super) fn apply_sync(
cx: &Cx,
app: &mut App,
label: &str,
result: std::result::Result<commands::sync::SyncOutcome, String>,
root: &Path,
) {
use commands::sync::SyncOutcome;
match result {
Ok(outcome) => {
let kind = match outcome {
SyncOutcome::Diverged
| SyncOutcome::DivergedNoWorktree
| SyncOutcome::Dirty
| SyncOutcome::PushRejected => StatusKind::Error,
_ => StatusKind::Success,
};
app.set_status(
format!("synced {label}{}", commands::sync::sync_suffix(outcome)),
kind,
);
do_refresh(cx, app, root);
}
Err(e) => app.set_status(e, StatusKind::Error),
}
if app.may_apply_mode(JobHome::List) {
app.mode = Mode::List;
}
}
pub(super) fn apply_init_submodules(
cx: &Cx,
app: &mut App,
count: usize,
result: std::result::Result<(), String>,
root: &Path,
) {
match result {
Ok(()) => {
app.set_status(
format!("initialized {count} submodule(s)"),
StatusKind::Success,
);
do_refresh(cx, app, root);
}
Err(e) => app.set_status(
format!("failed to initialize submodules: {e}"),
StatusKind::Error,
),
}
if app.may_apply_mode(JobHome::List) {
app.mode = Mode::List;
}
}
pub(crate) fn do_draft_pr_ai(
cx: &mut Cx,
session: &Session,
app: &mut App,
ctx: &sendit::PrContext,
) {
let dir = session
.repo
.current_workdir()
.unwrap_or_else(|| session.primary_root.clone());
let opts = match &app.mode {
Mode::PrCompose(state) => crate::agent::AgentOptions {
model: state.model,
effort: state.effort,
},
_ => crate::agent::AgentOptions::default(),
};
let _ = cx.err.line(&format!(
"Drafting PR with {} (effort {})…",
opts.model.label(),
opts.effort.id()
));
let result = crate::commands::pr_open::draft_with_ai(cx.agent.as_ref(), ctx, &dir, &opts);
if let Mode::PrCompose(state) = &mut app.mode {
match result {
Ok((title, body)) => {
state.title = title;
state.body = body;
state.error = None;
}
Err(e) => state.error = Some(e.to_string()),
}
state.submitting = false;
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn do_submit_pr(
cx: &mut Cx,
session: &Session,
app: &mut App,
ctx: &sendit::PrContext,
action: sendit::PrAction,
title: String,
body: String,
draft: bool,
outcome: &mut Option<(sendit::PrOutcome, sendit::PrSpec)>,
) -> bool {
let git = cx.git.clone();
let gh = cx.gh.clone();
let dir = session
.repo
.current_workdir()
.unwrap_or_else(|| session.primary_root.clone());
let spec = sendit::PrSpec { title, body, draft };
let result = crate::commands::pr_open::submit_pr(
git.as_ref(),
gh.as_ref(),
&session.primary_root,
&dir,
&session.config.pr_default_remote,
ctx,
&spec,
action,
);
match result {
Ok(out) => {
let _ = crate::commands::pr_open::record_pr_metadata(
git.as_ref(),
&session.primary_root,
&ctx.branch,
&ctx.trunk,
&out,
&spec.title,
);
*outcome = Some((out, spec));
true
}
Err(e) => {
if let Mode::PrCompose(state) = &mut app.mode {
state.error = Some(e.to_string());
state.submitting = false;
}
false
}
}
}