Skip to main content

git_sprout/
lib.rs

1// ABOUTME: Entry point shared by the git-sprout and git-worktree-fast binaries.
2// ABOUTME: Decides whether a `worktree add` can be accelerated and runs it either way.
3
4use std::ffi::OsString;
5use std::process::ExitCode;
6
7pub mod argv;
8pub mod attributes;
9pub mod clone;
10pub mod delegate;
11pub mod git;
12pub mod interrupt;
13pub mod plan;
14pub mod scratch_index;
15pub mod source;
16pub mod sprout;
17pub mod stats;
18pub mod tree;
19pub mod verify;
20
21use argv::{AddCommand, Invocation};
22use stats::Stats;
23
24/// Runs the tool with the given argv tail (everything after the program name).
25pub fn run(args: Vec<OsString>) -> ExitCode {
26    let mut stats = Stats::default();
27
28    let (git_args, reason) = match argv::parse(&args) {
29        Invocation::Version => {
30            println!("git-sprout {}", env!("CARGO_PKG_VERSION"));
31            return ExitCode::SUCCESS;
32        }
33        Invocation::Delegate { git_args, reason } => (git_args, reason.to_string()),
34        Invocation::Add(add) => match decline(&add) {
35            Some(reason) => (add.git_args(), reason),
36            None => return sprout::add(&add, &mut stats),
37        },
38    };
39
40    stats.fall_back(reason);
41    stats.emit();
42    delegate::exec_git(&git_args)
43}
44
45/// Why this request cannot be accelerated, if it cannot be.
46fn decline(add: &AddCommand) -> Option<String> {
47    if add.no_cow {
48        return Some("--no-cow".to_string());
49    }
50    if std::env::var_os("SPROUT_DISABLE").is_some_and(|value| value == "1") {
51        return Some("SPROUT_DISABLE=1".to_string());
52    }
53    if !add.checkout {
54        return Some("--no-checkout".to_string());
55    }
56    if add.orphan {
57        return Some("--orphan".to_string());
58    }
59    if head_is_unborn(add) {
60        return Some("no commits yet".to_string());
61    }
62    None
63}
64
65/// True when the repository has no commit for HEAD to name.
66///
67/// Git infers `--orphan` for itself in that situation, without the flag ever
68/// appearing in argv, and then refuses to combine its own inference with the
69/// `--no-checkout` this tool relies on. Declining here keeps the request on
70/// git's own path, where it succeeds.
71fn head_is_unborn(add: &AddCommand) -> bool {
72    add.commit_ish.is_none()
73        && git::Git::new(add.globals.clone())
74            .capture(None, ["rev-parse", "--verify", "--quiet", "HEAD"])
75            .is_ok_and(|output| output.is_empty())
76}