pub mod checkout;
pub mod complete;
pub mod completions;
pub mod config_cmd;
pub mod drop;
pub mod init;
pub mod issue;
pub mod list;
pub mod new;
pub mod path;
pub mod pr;
#[cfg(feature = "pr")]
pub mod pr_open;
pub mod prune;
pub mod remove;
pub mod root;
pub mod shell_init;
pub mod staleness;
pub mod status_cmd;
pub mod switch;
pub mod sync;
use std::path::{Path, PathBuf};
use crate::config::{Config, SubmoduleInit};
use crate::cx::Cx;
use crate::error::{Error, Result};
use crate::git::cli::GitCli;
use crate::git::discover::Repo;
use crate::hooks::{HookContext, HookRunner};
use crate::model::Worktree;
use crate::query::{self, Resolved};
use crate::worktree::{CreatedWorktree, HookOutcome, Workspace, WorkspaceParts, build_worktrees};
pub(crate) use crate::worktree::{resolve_target, rollback_worktree, same_path};
pub(crate) struct Session {
pub(crate) repo: Repo,
pub(crate) primary_root: PathBuf,
pub(crate) config: Config,
}
impl Session {
pub(crate) fn parts<'a>(&'a self, env: &'a crate::cx::Env) -> WorkspaceParts<'a> {
WorkspaceParts {
repo: &self.repo,
config: &self.config,
root: &self.primary_root,
env,
}
}
}
pub(crate) fn open_session(cx: &Cx, git: &dyn GitCli) -> Result<Session> {
let ws = Workspace::discover(&cx.cwd, &cx.env, git)?;
let (repo, primary_root, config) = ws.into_session_parts();
Ok(Session {
repo,
primary_root,
config,
})
}
pub(crate) enum Resolution {
Found(usize),
Ambiguous,
NotFound,
}
pub(crate) fn resolve_query(cx: &mut Cx, worktrees: &[Worktree], query: &str) -> Resolution {
#[cfg(feature = "tui")]
return resolve_query_with(cx, worktrees, query, |cx, q| {
crate::tui::run_tui(cx, Some(q))
});
#[cfg(not(feature = "tui"))]
resolve_query_with(cx, worktrees, query, |_, _| {
Err(Error::operation("no interactive picker in this build"))
})
}
fn resolve_query_with(
cx: &mut Cx,
worktrees: &[Worktree],
query: &str,
picker: impl FnOnce(&mut Cx, &str) -> Result<Option<PathBuf>>,
) -> Resolution {
match query::resolve(worktrees, query) {
Resolved::One(index) => Resolution::Found(index),
Resolved::Ambiguous(indices) => {
if cx.err.is_tty() {
match picker(cx, query) {
Ok(Some(path)) => worktrees
.iter()
.position(|w| same_path(&w.path, &path))
.map_or(Resolution::Ambiguous, Resolution::Found),
Ok(None) => Resolution::Ambiguous,
Err(_) => list_candidates(cx, worktrees, query, &indices),
}
} else {
list_candidates(cx, worktrees, query, &indices)
}
}
Resolved::NotFound => Resolution::NotFound,
}
}
fn list_candidates(
cx: &mut Cx,
worktrees: &[Worktree],
query: &str,
indices: &[usize],
) -> Resolution {
let _ = cx
.err
.line(&format!("query {query:?} is ambiguous; candidates:"));
for &index in indices {
let _ = cx
.err
.line(&format!(" {}", candidate_label(&worktrees[index])));
}
Resolution::Ambiguous
}
pub(crate) fn candidate_label(worktree: &Worktree) -> String {
match &worktree.branch {
Some(branch) => branch.clone(),
None => worktree.path.display().to_string(),
}
}
fn auto_yes(cx: &mut Cx, prompt: &str) -> Result<bool> {
if !cx.assume_yes {
return Ok(false);
}
cx.err.line(&format!("{prompt}y (--yes)"))?;
Ok(true)
}
pub(crate) fn confirm(cx: &mut Cx, prompt: &str) -> Result<bool> {
if auto_yes(cx, prompt)? {
return Ok(true);
}
cx.err.text(prompt)?;
cx.err.flush()?;
let line = cx.input.read_line()?;
let answer = line.trim().to_ascii_lowercase();
Ok(answer == "y" || answer == "yes")
}
pub(crate) fn confirm_default_yes(cx: &mut Cx, prompt: &str) -> Result<bool> {
if auto_yes(cx, prompt)? {
return Ok(true);
}
cx.err.text(prompt)?;
cx.err.flush()?;
let line = cx.input.read_line()?;
let answer = line.trim().to_ascii_lowercase();
Ok(answer != "n" && answer != "no")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Choice {
Update,
Proceed,
Cancel,
}
pub(crate) fn choose(cx: &mut Cx, prompt: &str) -> Result<Choice> {
if cx.assume_yes {
cx.err.line(&format!("{prompt}u (--yes)"))?;
return Ok(Choice::Update);
}
cx.err.text(prompt)?;
cx.err.flush()?;
let line = cx.input.read_line()?;
Ok(match line.trim().to_ascii_lowercase().as_str() {
"u" | "update" => Choice::Update,
"p" | "proceed" => Choice::Proceed,
_ => Choice::Cancel,
})
}
pub(crate) fn maybe_init_submodules(
cx: &mut Cx,
git: &dyn GitCli,
dir: &Path,
policy: SubmoduleInit,
flag_override: Option<bool>,
seed: bool,
) -> Result<()> {
let enabled = flag_override.unwrap_or(matches!(policy, SubmoduleInit::Always));
if !enabled {
return Ok(());
}
let pending = crate::git::submodule::uninitialized(git, dir)?;
if pending.is_empty() {
return Ok(());
}
init_submodules(cx, git, dir, pending.len(), seed);
Ok(())
}
pub(crate) fn maybe_init_submodules_interactive(
cx: &mut Cx,
git: &dyn GitCli,
dir: &Path,
policy: SubmoduleInit,
flag_override: Option<bool>,
prompt: bool,
seed: bool,
) -> Result<()> {
if flag_override.is_some() || !matches!(policy, SubmoduleInit::Prompt) {
return maybe_init_submodules(cx, git, dir, policy, flag_override, seed);
}
if !(cx.assume_yes || (prompt && cx.err.is_tty())) {
return Ok(());
}
let pending = crate::git::submodule::uninitialized(git, dir)?;
if pending.is_empty() {
return Ok(());
}
let ask = format!(
"repository has {} uninitialized submodule(s); initialize recursively (`git submodule update --init --recursive`)? [Y/n] ",
pending.len()
);
if confirm_default_yes(cx, &ask)? {
init_submodules(cx, git, dir, pending.len(), seed);
}
Ok(())
}
fn init_submodules(cx: &mut Cx, git: &dyn GitCli, dir: &Path, count: usize, seed: bool) {
let _ = cx.err.line(&format!("initializing {count} submodule(s)โฆ"));
let (report, result) = crate::git::submodule::populate(git, dir, seed);
if !report.seeded.is_empty() {
let _ = cx.err.line(&format!(
"seeded {} submodule(s) from local object stores",
report.seeded.len()
));
}
if let Err(e) = result {
let _ = cx
.err
.line(&format!("warning: failed to initialize submodules: {e}"));
}
}
pub(crate) fn build_target_row(cx: &Cx, target: &Path) -> Result<Worktree> {
let git = cx.git.clone();
let repo = Repo::discover(&cx.cwd)?;
let worktrees = build_worktrees(&repo, git.as_ref())?;
worktrees
.into_iter()
.find(|w| same_path(&w.path, target))
.ok_or_else(|| Error::operation("created worktree not found"))
}
pub(crate) fn log_copy_outcome(cx: &mut Cx, outcome: &crate::copy::CopyOutcome) {
if cx.verbose == 0 {
return;
}
for path in &outcome.copied {
let _ = cx.err.line(&format!("copied {}", path.display()));
}
for path in &outcome.skipped_existing {
let _ = cx
.err
.line(&format!("skipped (target exists) {}", path.display()));
}
}
pub(crate) struct Nav<'a> {
pub(crate) json: bool,
pub(crate) no_switch: bool,
pub(crate) note: &'a str,
pub(crate) start: Option<&'a str>,
}
pub(crate) fn finish_worktree(
cx: &mut Cx,
runner: &dyn HookRunner,
target: &Path,
hook_ctx: &HookContext,
nav: Nav<'_>,
) -> Result<u8> {
let Some(command) = nav.start else {
return emit_worktree(cx, target, nav.json, nav.no_switch, nav.note);
};
if !nav.no_switch {
hand_path_to_shell(cx, target)?;
}
cx.err.line(&format!("{} {}", nav.note, target.display()))?;
crate::hooks::run_start(runner, command, hook_ctx)
}
pub(crate) fn report_created(cx: &mut Cx, created: &CreatedWorktree) -> Result<()> {
log_copy_outcome(cx, &created.copy);
match &created.post_create {
HookOutcome::ExitedNonZero(code) => {
cx.err.line(&format!(
"warning: post_create hook exited with status {code}"
))?;
}
HookOutcome::Failed(error) => {
cx.err
.line(&format!("warning: post_create hook failed: {error}"))?;
}
HookOutcome::Skipped | HookOutcome::Succeeded => {}
}
Ok(())
}
fn hand_path_to_shell(cx: &mut Cx, target: &Path) -> Result<()> {
match cx.env.get("WT_CD_FILE").filter(|p| !p.is_empty()) {
Some(cd_file) => Ok(std::fs::write(
cd_file,
target.to_string_lossy().as_bytes(),
)?),
None => {
if cx.err.is_tty() && !cx.out.is_tty() {
cx.err.line(
"warning: --start cannot return you to the worktree; your shell \
integration predates it. Re-run `wt shell-init <shell>`.",
)?;
}
Ok(())
}
}
}
fn emit_worktree(
cx: &mut Cx,
target: &Path,
json: bool,
no_switch: bool,
note: &str,
) -> Result<u8> {
if json {
let row = build_target_row(cx, target)?;
cx.out.line(&row.to_json_line()?)?;
} else if no_switch {
cx.err.line(&format!("{note} {}", target.display()))?;
} else {
cx.out.line(&target.to_string_lossy())?;
}
Ok(0)
}
#[cfg(test)]
mod tests {
use super::{Nav, Resolution, finish_worktree, resolve_query_with};
use crate::cx::Stream;
use crate::error::Result;
use crate::hooks::{HookContext, HookRunner};
use crate::model::Worktree;
use crate::testutil::{SharedBuf, test_cx};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
struct FakeRunner {
code: i32,
last: Mutex<Option<String>>,
}
impl FakeRunner {
fn new(code: i32) -> Self {
Self {
code,
last: Mutex::new(None),
}
}
}
impl HookRunner for FakeRunner {
fn run(&self, command: &str, _ctx: &HookContext) -> Result<i32> {
*self.last.lock().unwrap() = Some(command.to_string());
Ok(self.code)
}
}
fn hook_ctx(dir: &Path) -> HookContext {
HookContext {
worktree_path: dir.to_path_buf(),
branch: "feat/x".into(),
repo_root: dir.to_path_buf(),
base_ref: None,
pr_number: None,
}
}
#[test]
fn finish_worktree_without_start_prints_the_path() {
let mut t = test_cx(&[], "/work");
let target = PathBuf::from("/work/wt");
let code = finish_worktree(
&mut t.cx,
&FakeRunner::new(0),
&target,
&hook_ctx(&target),
Nav {
json: false,
no_switch: false,
note: "created worktree at",
start: None,
},
)
.unwrap();
assert_eq!(code, 0);
assert_eq!(t.out.contents(), "/work/wt\n");
assert!(t.err.contents().is_empty());
}
#[test]
fn finish_worktree_without_start_honors_no_switch() {
let mut t = test_cx(&[], "/work");
let target = PathBuf::from("/work/wt");
finish_worktree(
&mut t.cx,
&FakeRunner::new(0),
&target,
&hook_ctx(&target),
Nav {
json: false,
no_switch: true,
note: "created worktree at",
start: None,
},
)
.unwrap();
assert!(t.out.contents().is_empty());
assert_eq!(t.err.contents(), "created worktree at /work/wt\n");
}
#[test]
fn finish_worktree_with_start_writes_the_cd_file_and_propagates_the_code() {
let dir = tempfile::tempdir().unwrap();
let cd_file = dir.path().join("cd");
let mut t = test_cx(&[("WT_CD_FILE", cd_file.to_str().unwrap())], "/work");
let target = dir.path().to_path_buf();
let runner = FakeRunner::new(3);
let code = finish_worktree(
&mut t.cx,
&runner,
&target,
&hook_ctx(&target),
Nav {
json: false,
no_switch: false,
note: "created worktree at",
start: Some("claude"),
},
)
.unwrap();
assert_eq!(code, 3);
assert_eq!(runner.last.lock().unwrap().as_deref(), Some("claude"));
assert_eq!(
std::fs::read_to_string(&cd_file).unwrap(),
target.to_string_lossy()
);
assert!(t.out.contents().is_empty(), "stdout is the command's");
assert!(t.err.contents().contains("created worktree at"));
}
#[test]
fn finish_worktree_hands_over_the_path_even_when_the_command_fails() {
let dir = tempfile::tempdir().unwrap();
let cd_file = dir.path().join("cd");
let mut t = test_cx(&[("WT_CD_FILE", cd_file.to_str().unwrap())], "/work");
let target = dir.path().to_path_buf();
let code = finish_worktree(
&mut t.cx,
&FakeRunner::new(1),
&target,
&hook_ctx(&target),
Nav {
json: false,
no_switch: false,
note: "created worktree at",
start: Some("false"),
},
)
.unwrap();
assert_eq!(code, 1);
assert!(cd_file.exists());
}
#[test]
fn finish_worktree_with_start_and_no_switch_skips_the_cd_file() {
let dir = tempfile::tempdir().unwrap();
let cd_file = dir.path().join("cd");
let mut t = test_cx(&[("WT_CD_FILE", cd_file.to_str().unwrap())], "/work");
let target = dir.path().to_path_buf();
let runner = FakeRunner::new(0);
finish_worktree(
&mut t.cx,
&runner,
&target,
&hook_ctx(&target),
Nav {
json: false,
no_switch: true,
note: "created worktree at",
start: Some("echo hi"),
},
)
.unwrap();
assert!(!cd_file.exists());
assert_eq!(runner.last.lock().unwrap().as_deref(), Some("echo hi"));
}
#[test]
fn finish_worktree_warns_when_the_shell_wrapper_is_stale() {
let err = SharedBuf::new();
let mut t = test_cx(&[], "/work");
t.cx.err = Stream::new(Box::new(err.clone()), true); let target = PathBuf::from("/work/wt");
finish_worktree(
&mut t.cx,
&FakeRunner::new(0),
&target,
&hook_ctx(&target),
Nav {
json: false,
no_switch: false,
note: "created worktree at",
start: Some("claude"),
},
)
.unwrap();
assert!(
err.contents().contains("wt shell-init"),
"{}",
err.contents()
);
}
#[test]
fn finish_worktree_is_quiet_without_shell_integration_on_a_tty() {
let out = SharedBuf::new();
let err = SharedBuf::new();
let mut t = test_cx(&[], "/work");
t.cx.out = Stream::new(Box::new(out.clone()), true);
t.cx.err = Stream::new(Box::new(err.clone()), true);
let target = PathBuf::from("/work/wt");
finish_worktree(
&mut t.cx,
&FakeRunner::new(0),
&target,
&hook_ctx(&target),
Nav {
json: false,
no_switch: false,
note: "created worktree at",
start: Some("claude"),
},
)
.unwrap();
assert!(!err.contents().contains("shell-init"), "{}", err.contents());
}
fn wt(branch: &str) -> Worktree {
let slug = branch.replace('/', "-");
let mut w = Worktree::new(PathBuf::from(format!("/r/{slug}")));
w.branch = Some(branch.to_string());
w.slug = Some(slug);
w
}
fn worktrees() -> Vec<Worktree> {
vec![wt("main"), wt("feature/login"), wt("feature/logout")]
}
fn cx_with_err_tty(is_tty: bool) -> (crate::testutil::TestCx, SharedBuf) {
let mut t = test_cx(&[], "/work");
let err = SharedBuf::new();
t.cx.err = Stream::new(Box::new(err.clone()), is_tty);
(t, err)
}
#[test]
fn unique_match_is_found() {
let (mut t, _err) = cx_with_err_tty(false);
let r = resolve_query_with(&mut t.cx, &worktrees(), "main", |_, _| {
panic!("picker must not run for a unique match")
});
assert!(matches!(r, Resolution::Found(0)));
}
#[test]
fn no_match_is_not_found() {
let (mut t, _err) = cx_with_err_tty(true);
let r = resolve_query_with(&mut t.cx, &worktrees(), "zzz", |_, _| {
panic!("picker must not run when nothing matches")
});
assert!(matches!(r, Resolution::NotFound));
}
#[test]
fn non_tty_ambiguous_lists_candidates() {
let (mut t, err) = cx_with_err_tty(false);
let r = resolve_query_with(&mut t.cx, &worktrees(), "feature/log", |_, _| {
panic!("picker must not run when stderr is not a TTY")
});
assert!(matches!(r, Resolution::Ambiguous));
let out = err.contents();
assert!(out.contains("ambiguous"));
assert!(out.contains("feature/login"));
assert!(out.contains("feature/logout"));
}
#[test]
fn tty_picker_selection_maps_to_index() {
let (mut t, err) = cx_with_err_tty(true);
let wts = worktrees();
let chosen = wts[2].path.clone();
let r = resolve_query_with(&mut t.cx, &wts, "feature/log", move |_, q| {
assert_eq!(q, "feature/log");
Ok(Some(chosen))
});
assert!(matches!(r, Resolution::Found(2)));
assert!(err.contents().is_empty());
}
#[test]
fn tty_picker_cancel_is_ambiguous_without_output() {
let (mut t, err) = cx_with_err_tty(true);
let r = resolve_query_with(&mut t.cx, &worktrees(), "feature/log", |_, _| Ok(None));
assert!(matches!(r, Resolution::Ambiguous));
assert!(err.contents().is_empty());
}
#[test]
fn tty_picker_error_falls_back_to_listing() {
let (mut t, err) = cx_with_err_tty(true);
let r = resolve_query_with(&mut t.cx, &worktrees(), "feature/log", |_, _| {
Err(crate::error::Error::operation("boom"))
});
assert!(matches!(r, Resolution::Ambiguous));
assert!(err.contents().contains("ambiguous"));
assert!(err.contents().contains("feature/login"));
}
#[test]
fn tty_picker_unknown_path_is_ambiguous() {
let (mut t, err) = cx_with_err_tty(true);
let r = resolve_query_with(&mut t.cx, &worktrees(), "feature/log", |_, _| {
Ok(Some(PathBuf::from("/somewhere/else")))
});
assert!(matches!(r, Resolution::Ambiguous));
assert!(err.contents().is_empty());
}
#[test]
fn confirm_default_yes_treats_empty_as_yes() {
use crate::commands::confirm_default_yes;
use crate::testutil::CannedInput;
let cases = [
("", true),
("y", true),
("yes", true),
("anything", true),
("n", false),
("N", false),
("no", false),
];
for (answer, expected) in cases {
let mut t = test_cx(&[], "/work");
t.cx.input = Box::new(CannedInput::new(&[answer]));
assert_eq!(
confirm_default_yes(&mut t.cx, "? ").unwrap(),
expected,
"answer {answer:?}"
);
}
}
#[test]
fn assume_yes_answers_prompts_without_reading_stdin() {
use crate::commands::{Choice, choose, confirm, confirm_default_yes};
use crate::testutil::CannedInput;
let mut t = test_cx(&[], "/work");
t.cx.assume_yes = true;
t.cx.input = Box::new(CannedInput::new(&["n", "n", "c"]));
assert!(confirm(&mut t.cx, "remove? ").unwrap());
assert!(confirm_default_yes(&mut t.cx, "init? ").unwrap());
assert_eq!(choose(&mut t.cx, "stale? ").unwrap(), Choice::Update);
assert_eq!(t.cx.input.read_line().unwrap(), "n\n");
let err = t.err.contents();
assert!(err.contains("remove? y (--yes)"), "{err}");
assert!(err.contains("init? y (--yes)"), "{err}");
assert!(err.contains("stale? u (--yes)"), "{err}");
}
#[test]
fn choose_maps_answers_and_defaults_to_cancel() {
use crate::commands::{Choice, choose};
use crate::testutil::CannedInput;
let cases = [
("update", Choice::Update),
("u", Choice::Update),
("proceed", Choice::Proceed),
("p", Choice::Proceed),
("", Choice::Cancel),
("nonsense", Choice::Cancel),
];
for (answer, expected) in cases {
let mut t = test_cx(&[], "/work");
t.cx.input = Box::new(CannedInput::new(&[answer]));
assert_eq!(choose(&mut t.cx, "? ").unwrap(), expected);
}
}
mod submodules {
use super::cx_with_err_tty;
use crate::commands::{maybe_init_submodules, maybe_init_submodules_interactive};
use crate::config::SubmoduleInit;
use crate::git::cli::{GitCli, GitOutput, RealGit};
use crate::git::submodule::uninitialized;
use crate::testutil::{CannedInput, TestRepo};
use std::path::Path;
fn repo_with_uninitialized_submodule() -> TestRepo {
let repo = TestRepo::init();
repo.add_submodule("libs/sub");
repo.deinit_submodule("libs/sub");
repo
}
fn is_initialized(repo: &TestRepo) -> bool {
repo.root().join("libs/sub/sub.txt").exists()
}
#[test]
fn disabled_policy_is_a_noop() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Never,
None,
true,
)
.unwrap();
assert!(!is_initialized(&repo));
assert!(err.contents().is_empty());
}
#[test]
fn always_policy_initializes() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Always,
None,
true,
)
.unwrap();
assert!(is_initialized(&repo));
assert!(uninitialized(&RealGit, repo.root()).unwrap().is_empty());
assert!(err.contents().contains("initializing 1 submodule"));
}
#[test]
fn flag_override_forces_init_over_never() {
let repo = repo_with_uninitialized_submodule();
let (mut t, _err) = cx_with_err_tty(true);
maybe_init_submodules(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Never,
Some(true),
true,
)
.unwrap();
assert!(is_initialized(&repo));
}
#[test]
fn flag_override_forces_skip_over_always() {
let repo = repo_with_uninitialized_submodule();
let (mut t, _err) = cx_with_err_tty(true);
maybe_init_submodules(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Always,
Some(false),
true,
)
.unwrap();
assert!(!is_initialized(&repo));
}
#[test]
fn enabled_with_no_submodules_is_a_noop() {
let repo = TestRepo::init();
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Always,
None,
true,
)
.unwrap();
assert!(err.contents().is_empty());
}
struct StatusOkUpdateFails;
impl GitCli for StatusOkUpdateFails {
fn run_raw(&self, _repo: &Path, _args: &[&str]) -> crate::error::Result<GitOutput> {
Ok(GitOutput {
success: true,
stdout: "-deadbeef libs/sub\n".into(),
stderr: String::new(),
})
}
fn run(&self, _repo: &Path, _args: &[&str]) -> crate::error::Result<String> {
Err(crate::error::Error::operation("boom"))
}
}
#[test]
fn prompt_default_yes_empty_initializes() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(true);
t.cx.input = Box::new(CannedInput::new(&[""]));
maybe_init_submodules_interactive(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Prompt,
None,
true,
true,
)
.unwrap();
assert!(is_initialized(&repo));
assert!(err.contents().contains("uninitialized submodule"));
}
#[test]
fn prompt_no_leaves_uninitialized() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(true);
t.cx.input = Box::new(CannedInput::new(&["n"]));
maybe_init_submodules_interactive(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Prompt,
None,
true,
true,
)
.unwrap();
assert!(!is_initialized(&repo));
assert!(err.contents().contains("uninitialized submodule"));
assert!(!err.contents().contains("initializing"));
}
#[test]
fn prompt_with_no_submodules_does_not_ask() {
let repo = TestRepo::init();
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules_interactive(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Prompt,
None,
true,
true,
)
.unwrap();
assert!(err.contents().is_empty());
}
#[test]
fn prompt_non_tty_is_a_silent_skip() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(false);
maybe_init_submodules_interactive(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Prompt,
None,
true,
true,
)
.unwrap();
assert!(!is_initialized(&repo));
assert!(err.contents().is_empty());
}
#[test]
fn prompt_false_gate_skips_even_at_a_tty() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules_interactive(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Prompt,
None,
false,
true,
)
.unwrap();
assert!(!is_initialized(&repo));
assert!(err.contents().is_empty());
}
#[test]
fn flag_skip_overrides_prompt_without_asking() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules_interactive(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Prompt,
Some(false),
true,
true,
)
.unwrap();
assert!(!is_initialized(&repo));
assert!(err.contents().is_empty());
}
#[test]
fn flag_init_overrides_prompt_without_asking() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules_interactive(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Prompt,
Some(true),
true,
true,
)
.unwrap();
assert!(is_initialized(&repo));
assert!(err.contents().contains("initializing"));
assert!(!err.contents().contains("uninitialized submodule"));
}
#[test]
fn never_policy_does_not_ask_at_a_tty() {
let repo = repo_with_uninitialized_submodule();
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules_interactive(
&mut t.cx,
&RealGit,
repo.root(),
SubmoduleInit::Never,
None,
true,
true,
)
.unwrap();
assert!(!is_initialized(&repo));
assert!(err.contents().is_empty());
}
#[test]
fn update_failure_is_non_fatal_and_warns() {
let (mut t, err) = cx_with_err_tty(true);
maybe_init_submodules(
&mut t.cx,
&StatusOkUpdateFails,
Path::new("/work"),
SubmoduleInit::Always,
None,
true,
)
.unwrap();
let out = err.contents();
assert!(out.contains("initializing 1 submodule"));
assert!(out.contains("warning: failed to initialize submodules"));
assert!(out.contains("boom"));
}
}
}